From d2e6b4a695bc35eaadff7eaad18334e38d07095c Mon Sep 17 00:00:00 2001 From: dimakis Date: Fri, 22 May 2026 16:06:46 +0100 Subject: [PATCH 1/2] fix(server): skip --resume on first headless query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- server/__tests__/chat.test.ts | 37 +++++++++++++++++++++++++++++++++++ server/app.ts | 5 ++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/server/__tests__/chat.test.ts b/server/__tests__/chat.test.ts index b4ee95ec..cddf21aa 100644 --- a/server/__tests__/chat.test.ts +++ b/server/__tests__/chat.test.ts @@ -459,6 +459,43 @@ describe('discoverSessionWorktrees integration', () => { }); }); +describe('headless session does not pass resume on first query', () => { + let appSource: string; + let chatSource: string; + + beforeAll(async () => { + const { readFileSync } = await import('fs'); + const { join } = await import('path'); + appSource = readFileSync(join(import.meta.dirname, '..', 'app.ts'), 'utf-8'); + chatSource = readFileSync(join(import.meta.dirname, '..', 'chat.ts'), 'utf-8'); + }); + + it('headless startChat call omits resume option', () => { + // Find the headless startChat block (identified by NullTransport + headless clientId) + const headlessMarker = 'const clientId = `headless:${wtId}`'; + const headlessIdx = appSource.indexOf(headlessMarker); + expect(headlessIdx).toBeGreaterThan(-1); + + // Extract the startChat call after the headless marker + const startChatIdx = appSource.indexOf('await startChat(', headlessIdx); + expect(startChatIdx).toBeGreaterThan(-1); + + // Get the options object passed to startChat (up to the closing paren + semicolon) + const callEnd = appSource.indexOf(');', startChatIdx); + const callRegion = appSource.slice(startChatIdx, callEnd); + + // Must NOT contain resume as an option key (resume: ...) + expect(callRegion).not.toMatch(/resume\s*[,:]/); + expect(callRegion).not.toMatch(/resume\s*\?/); + }); + + it('interactive resume path still resolves SDK session ID', () => { + // The resume resolution logic in startChat must still exist for interactive sessions + expect(chatSource).toContain('getSessionSdkId(BASE_REPO, options.resume)'); + expect(chatSource).toContain('resolvedResume'); + }); +}); + describe('validateResumable', () => { it('returns valid for a CWD that passes git check', async () => { const { validateResumable } = await import('../chat.js'); diff --git a/server/app.ts b/server/app.ts index c6865d76..751568bd 100644 --- a/server/app.ts +++ b/server/app.ts @@ -480,7 +480,10 @@ async function handleSessionCreate( const clientId = `headless:${wtId}`; const transport = new NullTransport(); await startChat(transport, clientId, initialPrompt, { - resume: wtId, + // No resume — this is the first query, no SDK session exists yet. + // The SDK session UUID is captured in query-loop after the first + // assistant message and stored via updateSessionSdkId() for future + // queries (e.g. user replies from their phone). mode: mode ?? 'agent', model, isolation: true, From 33734648a63948b87217477a686613fa0dcc6c0d Mon Sep 17 00:00:00 2001 From: dimakis Date: Fri, 22 May 2026 18:06:27 +0100 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20review=20=E2=80=94=20remov?= =?UTF-8?q?e=20redundant=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The interactive-resume-path test duplicated existing coverage at line ~346. Removed per Centaur review. Co-Authored-By: Claude Opus 4.6 --- server/__tests__/chat.test.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/server/__tests__/chat.test.ts b/server/__tests__/chat.test.ts index cddf21aa..4a58d993 100644 --- a/server/__tests__/chat.test.ts +++ b/server/__tests__/chat.test.ts @@ -461,13 +461,11 @@ describe('discoverSessionWorktrees integration', () => { describe('headless session does not pass resume on first query', () => { let appSource: string; - let chatSource: string; beforeAll(async () => { const { readFileSync } = await import('fs'); const { join } = await import('path'); appSource = readFileSync(join(import.meta.dirname, '..', 'app.ts'), 'utf-8'); - chatSource = readFileSync(join(import.meta.dirname, '..', 'chat.ts'), 'utf-8'); }); it('headless startChat call omits resume option', () => { @@ -488,12 +486,6 @@ describe('headless session does not pass resume on first query', () => { expect(callRegion).not.toMatch(/resume\s*[,:]/); expect(callRegion).not.toMatch(/resume\s*\?/); }); - - it('interactive resume path still resolves SDK session ID', () => { - // The resume resolution logic in startChat must still exist for interactive sessions - expect(chatSource).toContain('getSessionSdkId(BASE_REPO, options.resume)'); - expect(chatSource).toContain('resolvedResume'); - }); }); describe('validateResumable', () => {