Skip to content

fix: stop spawning sessions against state that is gone - #123

Merged
omartelo merged 2 commits into
mainfrom
fix/resume-prompt-checks-transcript
Jul 28, 2026
Merged

omartelo merged 2 commits into
mainfrom
fix/resume-prompt-checks-transcript

Conversation

@omartelo

@omartelo omartelo commented Jul 28, 2026 •

Copy link
Copy Markdown
Collaborator

Two failures with the same shape: a session row outlives what it points at, and
the spawn goes ahead anyway. Both end with the user looking at a terminal that
explains nothing.

The resume that cannot happen

Reopening a session that had run Claude Code always raised the resume prompt.
The decision read the row alone (resumableSession, sessions.ts):
kind === "claude" plus a non-empty providerSessionId was taken as proof the
conversation still existed. It is not — Claude Code prunes its own transcripts,
so an old enough id outlives what it points at. Accepting the prompt spawned
claude --resume <id> against nothing, and Claude's own error landed in the PTY
in place of a session.

It was a documented shortcut, taken back when a resume only ever followed a
restart (terminal.go, Start):

An id Claude no longer knows fails in the PTY like any other bad invocation —
the user sees Claude's own error.

Parked worktree sessions age, so it stopped being rare.

The check was already written: claudeTranscriptPath locates a conversation's
transcript by id for the context-window readout, honouring CLAUDE_CONFIG_DIR.
ResumeAvailable asks it for existence alone. The prompt is now raised only for
a conversation still on disk; when it is gone the session starts fresh and says
so. Offering a choice that cannot be honoured was the bug.

The checkout that is gone

A worktree removed outside lich leaves its session behind, pointing at a
directory that no longer exists. Terminal.Start rejected, and nothing caught
it
(TerminalView.tsx) — the terminal mounted empty, in silence.

Nothing recovers that session, so the gate settles the directory first and
closes it. Closes, not deletes. An absent path is not proof of a permanent
one: a mount that was not up yet answers exactly the same, and a deleted row
takes the provider conversation with it. Parking costs the same and loses
nothing — re-creating the worktree picks the session back up through
ReopenWorktreeSession, which already keys on the path.

WorkdirMissing is deliberately asymmetric: only a directory provably absent
(or a path that is not a directory) answers true. An empty cwd, a permission
error, a mount answering badly all fall through to the spawn, because closing a
session on a check that never answered is the worse trade.

The floor under both

Every other spawn failure — a provider binary that is not installed, a wrong
path in Settings › Providers, an exhausted fd table — reaches the frontend as
one opaque string from startPTY, indistinguishable from each other. They all
used to fail into the same blank terminal. They now surface as a toast, with the
session left in place: all of them are worth another try once the cause is
fixed, which is exactly why none of them may close a card.

Shape

The decision moved to lib/session/spawn-gate.ts, with its two backend checks
injected. The host's effect was already at the size limit, and the frontend gate
cannot render — injection is what puts the four outcomes, and both fail-safe
directions, under a test.

Test plan

  • gofmt -l ., go vet ./..., go test ./... (529 pass)
  • biome check (12 warnings, all the standing a11y backlog), vitest run (501 pass), tsc --noEmit + vite build
  • Cross-compile loop for linux/darwin/windows
  • TestResumeAvailable: transcript on disk, pruned transcript, empty id, shell session, another provider
  • TestWorkdirMissing: existing directory, empty cwd, deleted checkout, path that is a file
  • spawn-gate.test.ts: all four verdicts, plus each check failing (workdir spawns anyway, resume still asks)
  • By hand in task dev — the frontend gate is node-only, so the render path is unverified:
    • remove a session's transcript from ~/.claude/projects/ → no prompt, a toast, a working fresh session
    • transcript present → prompt still resumes
    • rm -rf a worktree behind an open session → toast, card closes, re-creating the worktree brings it back
    • point Settings › Providers at a binary that does not exist → toast naming the failure, card stays

Reopening a session that had run Claude Code always raised the resume
prompt, because the decision read the session row alone: a stored
provider session id was taken as proof the conversation still existed.
Claude Code prunes its own transcripts, so an old enough id outlived
what it pointed at, and accepting the prompt spawned `claude --resume`
against nothing — the user got Claude's own error inside the PTY in
place of a session.

The check the prompt was missing already existed. claudeTranscriptPath
locates a conversation's transcript by id for the context-window
readout; ResumeAvailable asks it for existence alone and exposes that
over the RPC, so the prompt is raised only for a conversation still on
disk. When it is gone the session spawns fresh and a toast says why.

A failed check still asks. Losing a live conversation to an answer
nobody gave costs more than the error this removes, and a transcript
pruned between the check and the spawn keeps the old behaviour as its
last resort.
@omartelo omartelo added the bug Something is broken label Jul 28, 2026
…at fail

A worktree removed outside lich leaves its session row behind, pointing
at a directory that no longer exists. Opening that session called Start,
which failed to chdir, and nothing caught the rejection: the terminal
mounted empty and the user was told nothing.

Nothing recovers a session whose checkout is gone, so the gate that
already decided whether to raise the resume prompt now settles the
directory first. A missing one closes the session and says why — closes,
not deletes: an absent path is not proof of a permanent one (a mount that
was not up yet answers the same way), and the row still carries the
provider conversation a later resume would need. Re-creating the worktree
picks both back up through ReopenWorktreeSession, which already keys on
the path.

Every other spawn failure is worth retrying — a provider binary that is
not installed, a wrong path in Settings — and startPTY reports them all
as one opaque string, so the frontend cannot tell them apart. They now
surface as a toast with the session left in place, rather than as the
same blank terminal.

The decision moved to lib/session/spawn-gate.ts with its two checks
injected. The host's effect was at the size limit, and the frontend gate
cannot render, so this is the only way the four outcomes are covered by
a test.
@omartelo omartelo changed the title fix: stop offering a resume the conversation cannot honour fix: stop spawning sessions against state that is gone Jul 28, 2026
@omartelo
omartelo merged commit 12c0285 into main Jul 28, 2026
3 checks passed
@omartelo
omartelo deleted the fix/resume-prompt-checks-transcript branch July 28, 2026 18:27
@omartelo omartelo mentioned this pull request Jul 30, 2026
3 of 4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something is broken

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant