Skip to content

[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

Closed

Description

@kamil-koryciorz

Before submitting

Area

apps/server

Steps to reproduce

  1. Settings → Storage: enable Delete worktrees with deleted threads.
  2. 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.
  3. At about T+19 min delete the thread from the sidebar (no "remove worktree" prompt appeared in this case).
  4. Watch git worktree list in 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 → return

then 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 cwd

Workaround

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

  1. juliusmarminge commented on Oct 6, 2026

    @juliusmarminge
    Member

    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 worktreeOnDelete doesn't use) or #14966 (preview sessions left in memory).

    What I found

    • Deleting the thread detaches its provider session without writing stopped. planThreadDeletion emits provider-session.detached and queues a provider-session.detach effect; the detach event only drops the binding row, so the row in orchestration_v2_projection_provider_sessions keeps its previous status.
    • Claude sessions are exclusive (supportsMultipleProviderThreadsPerSession: false) and are stored as ready when they open. The only writer of stopped is writeReleasedSessionEvents, which emits provider-session.updated only for threads still in attachedThreadIds.
    • On detach, the thread is removed from attachedThreadIds first. For an exclusive session that was the last attachment, detach then calls releaseEntry right away, which sees the now-empty set, writes no event, cancels the idle fiber, and drops the in-memory entry. That leaves the row at ready with updatedAt from session open, matching what you saw.
    • Threads that aren't deleted stay attached, so the 30-minute idle release (DEFAULT_IDLE_TIMEOUT_MS) does write stopped, which is why waiting before deleting works.
    • Restarting can't repair it: shutdown only releases sessions still in the live map, and persisted sessions aren't resurrected.
    • cleanWorktrees then skips the worktree, since any session with status != 'stopped' whose cwd is inside it returns before git.removeWorktree. That query isn't its own trace span, which fits the trace you posted. provider_session_runtime isn't involved; Orchestrator V2 doesn't write that table.
    • A shared session (Codex) seems to have the same gap: detaching the last thread schedules an idle release, and that later release also sees an empty attachedThreadIds.

    Likely fix area

    The provider session detach/release path. One option is to persist stopped (or error) on the session row when the last thread detaches, using the thread id captured before it's removed from attachedThreadIds; since the session row is global, that update would still land after provider-session.detached. apps/server/src/storageCleanup.ts could 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.

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 6, 2026
  3. juliusmarminge commented on Oct 15, 2026

    @juliusmarminge
    Member

    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 cwd is inside the worktree only blocks removal when ProviderSessionManager.isLive says the session manager is still running it. A row stuck at ready after 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 ready row 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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions