feat(server): remove worktrees when their last thread settles - #3
Conversation
- removes a settled thread's worktree once no active threads share it and its session stops - preserves the project root, branches, and dirty worktrees - recreates removed worktrees from the saved branch on the next turn
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8f36cd8707
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const worktreePath = yield* canonicalize(thread.worktreePath); | ||
| if (worktreePath === (yield* canonicalize(project.workspaceRoot))) return; |
There was a problem hiding this comment.
Protect every configured project root
When a linked checkout is also registered as another project's workspaceRoot, and that project has no non-archived thread currently using it, this guard compares the target only with the settling thread's project root. The later loop derives roots only from other threads, so git worktree remove can delete the other configured project's root and leave that project unusable. Compare the target against every entry in snapshot.projects before removing it.
Useful? React with 👍 / 👎.
| // Git preserves the branch and refuses to remove a main or dirty worktree. | ||
| yield* gitWorkflow.removeWorktree({ cwd: project.workspaceRoot, path: worktreePath }); |
There was a problem hiding this comment.
Serialize cleanup with resumed turn startup
When a user submits a new turn immediately after settlement, cleanup can pass its snapshot checks before the turn's thread.unsettled event is projected. Because this removal is not serialized with ProviderCommandReactor.ensureThreadWorktree, turn startup can observe the directory before deletion and skip recreation, after which this call deletes the cwd underneath the newly starting provider. Recheck and remove under a per-worktree lock shared with turn startup, or otherwise serialize these operations.
Useful? React with 👍 / 👎.
| const events = yield* engine.subscribeDomainEvents; | ||
| yield* forkParked( | ||
| Stream.runForEach(events, (event) => | ||
| event.type === "thread.settled" || | ||
| (event.type === "thread.session-set" && event.payload.session.status === "stopped") | ||
| ? cleanupWorker.enqueue(event.payload.threadId) |
There was a problem hiding this comment.
Reconcile missed settlements when the reactor starts
If the server exits after a settlement is persisted but before cleanup completes, this hot domain-event subscription will not replay that event after restart. Startup never scans the snapshot for already-settled threads, and the periodic settlement sweep excludes them, so the worktree remains indefinitely. Enqueue eligible settled threads during startup or persist and retry a cleanup receipt.
AGENTS.md reference: AGENTS.md:L141-L143
Useful? React with 👍 / 👎.
What Changed
Settling a thread now removes its worktree once nothing active still uses it. The settlement reactor listens for
thread.settledand stoppedthread.session-setevents and enqueues cleanup, which:realPath) before comparing, so worktrees shared through symlink aliases are recognized and keptThe thread keeps its saved branch, so starting a new turn recreates the worktree. Covered by reactor tests for each guard rail, an integration check that the branch survives removal, and a note in
docs/user/thread-sidebar.md.Why
Settled threads left their worktrees on disk forever, so long-lived environments accumulated stale directories users had to remove by hand. Worktrees are cheap to recreate from the saved branch, so reclaiming them once the last thread settles loses nothing. Doing it inside the existing settlement reactor keeps cleanup event-driven and drain-ordered with the rest of settlement rather than adding another sweeper.
Checklist
Worked by GLM 5.3 Flash via opencode.