Repository navigation
[Bug]: Deleting a thread within 30 min of its last run leaves its provider session "ready" forever, so "Delete worktrees with deleted threads" never removes the worktree #16457
Description
Before submitting
- I searched existing issues and did not find a duplicate. (Closest: [Bug]: V2 storage cleanup never frees worktrees for completed/cancelled/interrupted/rolled_back threads #15146 is the idle-predicate gate for live threads; [Bug]: Deleting a thread leaves its preview sessions in PreviewManager for the life of the server #14966 is preview sessions surviving deletion. Neither covers provider sessions of deleted threads.)
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
- Settings → Storage: enable Delete worktrees with deleted threads.
- Start a thread in worktree mode (Claude provider), send one short prompt that changes nothing, let the run complete at T+0. Leave the thread alone.
- At about T+19 min delete the thread from the sidebar (no "remove worktree" prompt appeared in this case).
- Watch
git worktree listin the project and the server trace for the next 20+ minutes.
Expected behavior
The worktree is clean (no tracked changes, no ignored files), the thread is deleted and nothing is running in it, so the sweep triggered by thread.deleted (or a later one) removes the worktree. The docs say the server "waits for sessions and terminals to stop".
Actual behavior
Sweeps run at once and again on later events. Each reaches StorageCleanup.ignoredFiles for the worktree, then readThreads and containsProjectRoot, then stops without removing it. The worktree is still there 20 minutes after deletion (38 minutes after the run).
Cause as far as I can see: the thread's Claude session row in orchestration_v2_projection_provider_sessions still has status: "ready" with updatedAt equal to the run start. No agent process is alive in that cwd and provider_session_runtime has no row for it. Sessions of threads that were not deleted flipped to "stopped" exactly 30 minutes after their last run (DEFAULT_IDLE_TIMEOUT_MS = 18e5), so the idle release apparently never updates the projection once the thread is deleted. The sweep's guard
SELECT payload_json FROM orchestration_v2_projection_provider_sessions WHERE status != 'stopped'
// ... if any such session has cwd === worktreePath or inside it → returnthen skips the worktree on every pass.
Two control cases in the same session:
- A deleted thread whose worktree had an uncommitted tracked change: kept (correct, local changes).
- A thread deleted after its session had already reached
stopped: not covered by this run.
Impact
Minor bug or occasional failure (worktrees leak until a restart; disk, not data)
Version or commit
T3 Code Nightly 0.0.46-nightly.20261005.2689
Environment
macOS 26.7.1 (Apple Silicon), desktop app, Claude provider (claude-fable-5-1), thread launched through the MCP t3_thread_launch tool
Logs or stack traces
# server.trace.ndjson, UTC, thread deleted 09:42:54
09:42:54 StorageCleanup.sweep
09:42:54 StorageCleanup.cleanWorktrees
09:43:06 StorageCleanup.ignoredFiles git.cwd=~/.t3/worktrees/<project>/<worktree>
09:43:06 StorageCleanup.readThreads
09:43:07 StorageCleanup.containsProjectRoot
09:43:07 StorageCleanup.sweep (second pass, same outcome)
# projection row at 10:02 UTC
{"status":"ready","cwd":"~/.t3/worktrees/<project>/<worktree>","updatedAt":"2026-10-06T09:23:48.231Z"}
# runtime table: no row for this thread; no agent process with that cwdWorkaround
None found. Quitting and reopening the app does not help: the start-up sweep runs, the session row is still "ready" afterwards, and the worktree stays. Deleting the worktree by hand (the delete dialog's worktree option, or git worktree remove) is the only way. Avoidance: wait 30 minutes after a thread's last run before deleting it.
Written with Claude Fable 5.1 through T3's MCP tools; the human owner reviewed before posting.
Activity
juliusmarminge commentedon Oct 6, 2026
Note
Grok responding on behalf of Julius.
Triage
Thanks @kamil-koryciorz for the detailed trace and the session row snapshot, they matched the code closely. This doesn't look like a duplicate of #15146 (the idle-predicate gate for threads that still exist, which
worktreeOnDeletedoesn't use) or #14966 (preview sessions left in memory).What I found
stopped.planThreadDeletionemitsprovider-session.detachedand queues aprovider-session.detacheffect; the detach event only drops the binding row, so the row inorchestration_v2_projection_provider_sessionskeeps its previous status.supportsMultipleProviderThreadsPerSession: false) and are stored asreadywhen they open. The only writer ofstoppediswriteReleasedSessionEvents, which emitsprovider-session.updatedonly for threads still inattachedThreadIds.attachedThreadIdsfirst. For an exclusive session that was the last attachment,detachthen callsreleaseEntryright away, which sees the now-empty set, writes no event, cancels the idle fiber, and drops the in-memory entry. That leaves the row atreadywithupdatedAtfrom session open, matching what you saw.DEFAULT_IDLE_TIMEOUT_MS) does writestopped, which is why waiting before deleting works.cleanWorktreesthen skips the worktree, since any session withstatus != 'stopped'whosecwdis inside it returns beforegit.removeWorktree. That query isn't its own trace span, which fits the trace you posted.provider_session_runtimeisn't involved; Orchestrator V2 doesn't write that table.attachedThreadIds.Likely fix area
The provider session detach/release path. One option is to persist
stopped(orerror) on the session row when the last thread detaches, using the thread id captured before it's removed fromattachedThreadIds; since the session row is global, that update would still land afterprovider-session.detached.apps/server/src/storageCleanup.tscould keep treating a genuinely live session as a block.Until then, removing the worktree by hand (or waiting 30 minutes after the last run before deleting the thread) avoids it.
A maintainer will decide on the fix direction.
juliusmarminge commentedon Oct 15, 2026
Note
Grok responding on behalf of Julius.
Closing this as fixed by #15208, merged to main today. Storage cleanup no longer trusts the stored session row alone: a provider session whose
cwdis inside the worktree only blocks removal whenProviderSessionManager.isLivesays the session manager is still running it. A row stuck atreadyafter its last thread detached, which is what you traced here, no longer holds the worktree, so "Delete worktrees with deleted threads" removes it on the next sweep.The stale
readyrow itself is fixed separately in the open PR #17481. If a worktree is still kept after a nightly that includes #15208, comment here with the cleanup report entry and we'll reopen.