fix: recover a qwen session whose backing chat does not exist - #314
Merged
Conversation
A forked qwen session died on its first message with "CLI process exited with code 1", every time. The fork is the trigger. primeFromFork copies the parent's transcript into the fork's own transcript file and deliberately does NOT mark the backend as started — its comment says "a fork's backend is brand new and must run its first turn as a create, not a resume". But on the next daemon restart restoreScrollback loads those copied rows, sees a non-empty scrollback, and calls setHasQueried(true). The fork's first real turn then issues `resume: <id>` for a chat qwen-code has never created. Reproduced on the affected box against @qwen-code/sdk 0.1.8: identical env, cwd, model and authType, flipping ONLY `sessionId:` to `resume:` for an unknown id, turns a clean `result success` into exactly the reported error. The project's chats/ directory held no file for that id. This is qwen-specific because Claude Code tolerates resuming an unknown id and starts fresh, while qwen-code exits during initialization. ClaudeProvider additionally recovers from it — matching "No conversation found with session ID" and firing onRecoveryNeeded, which resets to a fresh backing id and replays the turn. QwenProvider declared that field but never fired it. Fire it, mirroring the claude path. qwen-code gives no distinguishing message — just the generic exit — so the predicate is necessarily broader than Claude's and will also catch a CLI that died at startup for an unrelated reason. The guards bound the cost: recovery runs only when resuming (hasQueried), only once per backing session (backingRecoveryAttempted), and re-runs the same turn as a create. A misfire costs one retry, after which the real error surfaces normally instead of being swallowed. Note this does not change restoreScrollback's hasQueried heuristic, which is the underlying inaccuracy. Gating that on a persisted backing id is not currently possible: claude_code_session_id is written only on provider switch, rotation and recovery — never on a normal first turn — so every existing session would read as "never started" and lose its backend context once. That deserves its own change with a migration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
akashjavelin
approved these changes
Sep 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
A forked qwen session (
sandbox-exelixis (fork)) failed on every message with:Credentials and config were fine — a fresh qwen session in the same worktree, with the same env, model and authType, runs to
result success.Root cause
Session resume, not config.
primeFromForkcopies the parent's transcript into the fork's own transcript file, and deliberately does not mark the backend as started —session.ts:2516says why: "a fork's backend is brand new and must run its first turn as a create, not a resume."restoreScrollback(session.ts:2579) loads those copied rows, seesmessages.length > 0, and callssetHasQueried(true).resume: <id>for a chat qwen-code has never created.Reproduced on the affected box against
@qwen-code/sdk0.1.8 — identical env, cwd, model and authType, flipping only the session option:sessionId: <fresh uuid>MSG: result successresume: <unknown id>CLI process exited with code 1(byte-identical to the daemon log)The project's
chats/directory held no file for that id.Why qwen-only: Claude Code tolerates resuming an unknown id and starts fresh; qwen-code exits during initialization.
ClaudeProvideralso actively recovers — it matches"No conversation found with session ID"and firesonRecoveryNeeded, which resets to a fresh backing id and replays the turn.QwenProviderdeclared that field but never fired it.Change
Fire it, mirroring the claude path.
qwen-code gives no distinguishing message — just the generic exit — so the predicate is necessarily broader than Claude's, and will also catch a CLI that died at startup for an unrelated reason (a rejected key, say). The guards bound that cost: recovery runs only when resuming (
hasQueried), only once per backing session (backingRecoveryAttempted), and re-runs the same turn as a create. A misfire costs one retry, after which the real error surfaces normally instead of being swallowed. The trade-off is documented atisBackingSessionMissing.What this deliberately does NOT change
restoreScrollback'shasQueriedheuristic is the underlying inaccuracy — "has scrollback" is not the same as "has a backend session". Gating it on a persisted backing id is not currently possible:claude_code_session_idis written only on provider switch, rotation and recovery — never on a normal first turn — so every existing session would read as "never started" and lose its backend context once. That deserves its own change with a migration, so this PR fixes the crash without taking that risk.Operators hitting this today can unblock a stuck session with
/rotate, which mints a fresh backing id and clears the flag.Verification
bun test— 2414 pass, 19 skip, 0 fail (161 files)bun run typecheck— cleanbun run lint— cleanNew coverage on
isBackingSessionMissing: the generic qwen exit, the wrapped initialization-error form, the claude-style message, and non-matches (code 2, connection errors,invalid_api_key, empty).🤖 Generated with Claude Code