Skip to content

fix(server): storage cleanup keeps a retained worktree folder without .git - #31155

Open
juliusmarminge wants to merge 4 commits into
mainfrom
fix/issue-31136
Open

juliusmarminge wants to merge 4 commits into
mainfrom
fix/issue-31136

Conversation

@juliusmarminge

Copy link
Copy Markdown
Member

Problem

A thread can still point at a worktree folder that Git no longer has registered, but that still holds leftover files (for example yarn.lock and an empty node_modules after git worktree remove). With any worktree cleanup rule on, every cleanup run recorded that folder as failed with NotFound: FileSystem.stat (<worktree>/.git) and logged storage cleanup failed for worktree. The expected outcome is that the folder is kept as "Not a linked worktree" and its files are left alone.

Reproduced on main (7f6f47b) with a dev server on a fresh database:

  1. Start a thread in a new worktree.
  2. Settle the thread.
  3. Run git worktree remove on the worktree.
  4. Recreate the folder with only yarn.lock and an empty node_modules.
  5. Turn on "delete settled worktrees after 30 days".

The sweep then logs the NotFound failure. Two other combinations fail the same way: inactive-days plus merged rules with the same folder, and deleted-thread plus unchanged rules with an empty folder.

Change

StorageCleanup.cleanWorktrees called fs.stat on the worktree's .git without first checking that it exists. It now checks fs.exists first, the same way the orphan-worktree scan in this file already does, and keeps the folder as "Not a linked worktree" when .git is missing. fs.exists returns false only for NotFound, so permission errors and other filesystem errors are still reported as failures.

Scope and approval

Fixes #31136

Triaged via the automated issue flow.

Verification

  • New test storageCleanup.test.ts › "keeps a thread's recreated worktree folder that has no .git". It uses a real linked worktree, removes it with git worktree remove, then recreates the folder with yarn.lock and an empty node_modules.

    • On main it fails with outcome: "failed" and reason: "NotFound: FileSystem.stat (…/worktrees/feature/.git)".
    • With the fix it passes, and yarn.lock is still there.
  • vp test run apps/server/src/storageCleanup.test.ts: 176/176 pass.

  • vp run check:changed: format, lint, knip, graphql, and typecheck pass; tests 178/178.

  • Runtime check with a dev server on a fresh database and a synthetic scratch repo, same steps as above, before and after:

    Before (main 7f6f47b27b)
    WARN storage cleanup failed for worktree
      reason: { _tag: 'NotFound', module: 'FileSystem', method: 'stat',
                pathOrDescriptor: '<home>/worktrees/repo/t3code-repro-31136/.git' }
      cause: ENOENT: no such file or directory, stat '<home>/worktrees/repo/t3code-repro-31136/.git'
    
    After (this branch)
    No 'storage cleanup failed for worktree' warnings.
    Settings > Maintenance > Cleanup > Last run: "0 removed, 1 kept"
      t3code-repro-31136 — Not a linked worktree
    Folder contents afterwards: node_modules/ yarn.lock (unchanged)
    
  • Server-only change with no visible change in the UI, so there are no before/after screenshots. Not checked on macOS (the reporter's platform). The changed code is platform-independent filesystem handling.

AI-assisted; verified with the tests and tools listed above.

…e without .git (#31136)

A thread's retained worktree path that still exists but has no .git
(a git-removed worktree recreated with only yarn.lock and node_modules)
should be kept as "Not a linked worktree". On main, cleanup stats the
missing .git and reports a failed NotFound entry instead.

AI-assisted; verified with the tests and tools listed above.
… .git

A thread's worktree folder that exists but has no .git entry (for example
one removed with `git worktree remove` and recreated by a package manager)
made the unguarded stat fail with NotFound, so every cleanup run recorded a
failure for it. Check for .git with fs.exists first, matching the orphan
scan, so the folder is kept as "Not a linked worktree". fs.exists only maps
NotFound to false, so other filesystem errors still fail the check.

Fixes #31136

AI-assisted; verified with the tests and tools listed above.
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XS 0-9 changed lines (additions + deletions). labels Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 21.5 KiB 21.5 KiB 0 B (0.0%) 24.4 KiB ✅
Codex Live turn messages 1 1 0 (0.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.9 KiB 21.9 KiB 0 B (0.0%) 24.4 KiB ✅
Claude Live turn messages 1 1 0 (0.0%) 8 ✅

Baseline: 17a6a01 · PR result: f745328 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@juliusmarminge juliusmarminge added the bugfix Fix PR opened by an issue lead; enters the PR loop label Nov 4, 2026
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Nov 4, 2026
@macroscopeapp

macroscopeapp Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at f745328

Macroscope's review found this PR approvable — Cleanup now explicitly keeps retained worktree folders that lack .git instead of reporting an inspection failure. Existing deletion eligibility and filesystem safeguards remain unchanged.

You can add or adjust custom eligibility rules. Learn more.

@juliusmarminge juliusmarminge added the pr:gate-failed A land gate failed label Nov 4, 2026
@coderabbitai

coderabbitai Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

Storage cleanup now checks whether .git exists before calling stat. If it is absent, cleanup reports Not a linked worktree. A regression test verifies that cleanup keeps the directory and preserves its lockfile.

Changes: Worktree cleanup

Layer / File(s) Summary
Worktree metadata check and regression coverage
apps/server/src/storageCleanup.ts, apps/server/src/storageCleanup.test.ts
worktreePathReason returns “not a linked worktree” when .git is missing. The regression test verifies that cleanup keeps the directory and leaves its yarn.lock unchanged.

Estimated code review effort: 2 (Simple) | ~8 minutes

Change: Bug fix · Severity of issue fixed: Medium

Possibly related issues

  • Issue 24390 — This change addresses cleanup failure when .git is absent. It does not address the separate checkout-recovery gap described in that issue.

Suggested reviewers: maria-rcks, stienswout

Merge Risk: Minimal · up to f7453

Missing worktree metadata is handled without suppressing other filesystem failures. No actionable merge risk remains.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title clearly summarizes the main change: storage cleanup keeps a retained worktree folder when it has no .git directory.
Description check Passed The description covers the problem, change, scope, and verification with specific reproduction steps and test results. It notes AI assistance but does not identify the model and harness as the templat…
Linked Issues check Passed Issue #31136 requires cleanup to keep a retained directory when .git is missing, report it as not a linked worktree, and preserve residual files. The whole-PR diff checks fs.exists(gitPath) befo…
Out of Scope Changes check Passed The whole-PR diff changes only storageCleanup.ts and its test. Both changes directly implement issue #31136. The test provides coverage for the requested residual-directory case. No unrelated change…
Full details: Description check

Explanation

The description covers the problem, change, scope, and verification with specific reproduction steps and test results. It notes AI assistance but does not identify the model and harness as the template requests when an agent was used; this is a minor omission.

Full details: Linked Issues check

Explanation

Issue #31136 requires cleanup to keep a retained directory when .git is missing, report it as not a linked worktree, and preserve residual files. The whole-PR diff checks fs.exists(gitPath) before fs.stat in worktreePathReason. The new test removes a real linked worktree, recreates the directory with node_modules and yarn.lock, then verifies the kept reason and preserved lockfile. The change also retains the stat path for an existing .git entry.

Full details: Out of Scope Changes check

Explanation

The whole-PR diff changes only storageCleanup.ts and its test. Both changes directly implement issue #31136. The test provides coverage for the requested residual-directory case. No unrelated changes appear in the whole-PR diff.

Finishing Touches

Generate docstrings

  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Usage-based review receipt

Note

This review was completed with usage-based billing: files reviewed beyond your plan's included limits are billed at $0.25/file. View usage-based billing.

Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 872d221 and f745328.

Files selected for processing: 2

  • apps/server/src/storageCleanup.test.ts
  • apps/server/src/storageCleanup.ts

Limit details: You’ve used all 10 included reviews currently available.

# Conflicts:
#	apps/server/src/storageCleanup.ts
@macroscopeapp
macroscopeapp Bot dismissed their stale review November 10, 2026 23:01

Dismissing prior approval to re-evaluate f745328

Copy link
Copy Markdown
Member Author

Evidence

Both sides: real Linux dev server and web client, separate checkouts and fresh databases, light theme, 1280×800 CSS viewport. Synthetic data only; displayed local paths and environment names are privacy-redacted.

Steps: create and settle a thread in a managed linked worktree through server RPC; remove the worktree with Git while retaining the thread; recreate its folder with yarn.lock and empty node_modules, without .git; enable immediate settled-worktree cleanup, allow the settlement grace to pass, then click Settings → Maintenance → Delete now and expand Last run. Repeat cleanup twice. Immediate retention was used identically on both sides instead of waiting 30 days.

Before After
17a6a01d84 f745328769
Before: missing .git causes cleanup failure After: folder kept as Not a linked worktree
Before interaction recording After interaction recording
0 removed, 0 kept, 1 failed; NotFound: FileSystem.stat (.../.git) 0 removed, 1 kept, 0 failed; Not a linked worktree

Both runs preserved identical yarn.lock contents/hash and empty node_modules: filesystem transcript. Repeated sweeps reproduced the same outcomes: cleanup reports, server warnings.

Drivers: seed/RPC, interaction/redaction, folder preparation, filesystem verification, reproduction instructions.

AI-assisted; verified with the tests and tools listed above.

Not shown:

  • Thirty-day aging was not exercised: both runs used the supported immediate settled-worktree policy without modifying persisted timestamps.
  • Desktop and mobile clients were not exercised; evidence covers the shared cleanup server through the real web client.
  • Freshness of origin/main could not be verified because both fetch attempts returned HTTP 503; the base is the computed merge-base using the available origin/main ref.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Fix PR opened by an issue lead; enters the PR loop pr:gate-failed A land gate failed size:XS 0-9 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Storage cleanup fails when a retained worktree directory has no .git file

1 participant