fix(server): skip --resume on first headless query#355
Conversation
Headless sessions (POST /api/sessions with initialPrompt) passed the Mitzo worktree ID as --resume to the Claude Code SDK, which rejects non-UUID values. The first query has no SDK session to resume — omit resume and let the SDK create a new session. The UUID is captured via updateSessionSdkId() after the first assistant message for subsequent queries. Fixes 100% failure rate on PR Shepherd and all headless session dispatch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 2 issue(s) (1 warning).
server/__tests__/chat.test.ts
Correct one-line fix (removing resume: wtId for first headless query). The logic is sound and well-commented, but the tests rely on fragile source-code string scanning rather than behavioral assertions.
- 🟡 style (L473): Source-code-scanning tests (reading app.ts/chat.ts as strings and asserting on substrings) are fragile — any refactor that changes whitespace, variable names, or call structure will break them silently or produce false positives. The
indexOf(');', startChatIdx)heuristic fails if the options object contains a nested);sequence (e.g. in a comment or string). Consider testing the actual behavior (e.g. mockstartChatand assert the options it receives) rather than parsing source text. - 🔵 missing_tests (L492): The second test ('interactive resume path still resolves SDK session ID') duplicates the existing test at line ~346 which already asserts
chatSource.toContain('getSessionSdkId(BASE_REPO, options.resume)'). This is redundant coverage — both read chat.ts source and check the same substring.[fixable]
| chatSource = readFileSync(join(import.meta.dirname, '..', 'chat.ts'), 'utf-8'); | ||
| }); | ||
|
|
||
| it('headless startChat call omits resume option', () => { |
There was a problem hiding this comment.
🟡 style: Source-code-scanning tests (reading app.ts/chat.ts as strings and asserting on substrings) are fragile — any refactor that changes whitespace, variable names, or call structure will break them silently or produce false positives. The indexOf(');', startChatIdx) heuristic fails if the options object contains a nested ); sequence (e.g. in a comment or string). Consider testing the actual behavior (e.g. mock startChat and assert the options it receives) rather than parsing source text.
| expect(callRegion).not.toMatch(/resume\s*\?/); | ||
| }); | ||
|
|
||
| it('interactive resume path still resolves SDK session ID', () => { |
There was a problem hiding this comment.
🔵 missing_tests: The second test ('interactive resume path still resolves SDK session ID') duplicates the existing test at line ~346 which already asserts chatSource.toContain('getSessionSdkId(BASE_REPO, options.resume)'). This is redundant coverage — both read chat.ts source and check the same substring. [fixable]
The interactive-resume-path test duplicated existing coverage at line ~346. Removed per Centaur review. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 1 issue(s).
server/app.ts
Correct fix — the headless path was passing a worktree ID as a resume token before any SDK session existed, which would cause the SDK to reject the query. The test follows an established source-scanning pattern in this repo. LGTM with a minor comment verbosity nit.
- 🔵 style (L483): The 4-line comment explaining why
resumeis omitted is helpful for the initial commit but may become noise long-term. A single line like// First query — no SDK session to resume yetwould suffice given that the commit message and theupdateSessionSdkIdcall site already document the lifecycle.[fixable]
| const transport = new NullTransport(); | ||
| await startChat(transport, clientId, initialPrompt, { | ||
| resume: wtId, | ||
| // No resume — this is the first query, no SDK session exists yet. |
There was a problem hiding this comment.
🔵 style: The 4-line comment explaining why resume is omitted is helpful for the initial commit but may become noise long-term. A single line like // First query — no SDK session to resume yet would suffice given that the commit message and the updateSessionSdkId call site already document the lifecycle. [fixable]
Summary
POST /api/sessionswithinitialPrompt) passed the Mitzo worktree ID as--resumeto the Claude Code SDK, which rejects non-UUID values — causing 100% failure on all headless dispatch (PR Shepherd, etc.)resumeon the first headless query since no SDK session exists yet. The UUID is captured viaupdateSessionSdkId()after the first assistant message and used for subsequent queries.local_features/headless-session-resume/design.md(in mgmt)Test plan
startChat()call omitsresumeoptiongetSessionSdkId()POST /api/sessionswithinitialPrompt→ first query succeeds🤖 Generated with Claude Code