From 179ab1009b142d6d4c8eb52caf004fab6172bd57 Mon Sep 17 00:00:00 2001 From: Adam Creeger Date: Thu, 9 Jul 2026 00:15:07 -0400 Subject: [PATCH 1/3] Phase 1: Fix session-in-use auth fallback in claude.ts --- src/utils/claude.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/utils/claude.ts b/src/utils/claude.ts index 2137ff19..d48966b7 100644 --- a/src/utils/claude.ts +++ b/src/utils/claude.ts @@ -449,6 +449,15 @@ export async function launchClaude( const retryExecaError = retryError as { stderr?: string; message?: string } // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional: empty string stderr should fall through to message const retryErrorMessage = retryExecaError.stderr || retryExecaError.message || 'Unknown Claude CLI error' + + if (attempt === 1 && bareModeAutoApplied) { + const isAuthError = /not logged in|unauthorized|authentication|invalid api key|Could not resolve credentials/i.test(retryErrorMessage) + if (isAuthError) { + logger.warn('Bare mode failed during --resume retry (likely expired OAuth token), retrying without --bare') + continue + } + } + throw new Error(`Claude CLI error: ${redactSettings(retryErrorMessage)}`) } } From a0f94cbfd1193ea2234d1e999fa31e0665f29ac0 Mon Sep 17 00:00:00 2001 From: Adam Creeger Date: Thu, 9 Jul 2026 00:17:08 -0400 Subject: [PATCH 2/3] Phase 2: Add noSessionPersistence to epic report launchClaude call --- src/lib/SessionSummaryService.test.ts | 1 + src/lib/SessionSummaryService.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/lib/SessionSummaryService.test.ts b/src/lib/SessionSummaryService.test.ts index 48ceed5a..a7a764a8 100644 --- a/src/lib/SessionSummaryService.test.ts +++ b/src/lib/SessionSummaryService.test.ts @@ -697,6 +697,7 @@ describe('SessionSummaryService', () => { expect(launchClaude).toHaveBeenCalledWith('Generated prompt content', { headless: true, model: 'sonnet', + noSessionPersistence: true, }) // Verify comment was posted to issue diff --git a/src/lib/SessionSummaryService.ts b/src/lib/SessionSummaryService.ts index a4b3afd8..b9e014ba 100644 --- a/src/lib/SessionSummaryService.ts +++ b/src/lib/SessionSummaryService.ts @@ -278,6 +278,7 @@ export class SessionSummaryService { const reportResult = await launchClaude(prompt, { headless: true, model: summaryModel, + noSessionPersistence: true, }) if (!reportResult || typeof reportResult !== 'string' || reportResult.trim() === '') { From 5d0223854beed9ce184bf1dbafe5769eef270e25 Mon Sep 17 00:00:00 2001 From: Adam Creeger Date: Thu, 9 Jul 2026 00:19:18 -0400 Subject: [PATCH 3/3] Phase 3: Add tests for session-in-use auth fallback --- src/utils/claude.test.ts | 82 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/src/utils/claude.test.ts b/src/utils/claude.test.ts index 302a4723..45f6a7de 100644 --- a/src/utils/claude.test.ts +++ b/src/utils/claude.test.ts @@ -2952,6 +2952,88 @@ describe('claude utils', () => { // Should only have been called once (no retry) expect(execa).toHaveBeenCalledTimes(1) }) + + it('should retry without --bare when session-in-use --resume retry fails with auth error', async () => { + // Set up OAuth token so bare mode will be auto-applied + process.env.CLAUDE_CODE_OAUTH_TOKEN = 'sk-ant-oat01-test-token' + + const sessionId = '01af28fe-8630-4778-ae85-39398ab84f54' + + // First call: fails with session-in-use error (bare mode auto-applied) + mockExeca().mockRejectedValueOnce({ + stderr: `Error: Session ID ${sessionId} is already in use.`, + exitCode: 1, + }) + + // Second call (--resume retry): fails with auth error + const authError = Object.assign(new Error('authentication_failed'), { + stderr: 'Invalid API key', + exitCode: 1, + }) + mockExeca().mockRejectedValueOnce(authError) + + // Third call: succeeds (retry without bare) + mockExeca().mockResolvedValueOnce({ + stdout: 'success without bare', + exitCode: 0, + }) + + const result = await launchClaude('test prompt', { + headless: true, + noSessionPersistence: true, + sessionId, + }) + + expect(result).toBe('success without bare') + expect(execa).toHaveBeenCalledTimes(3) + + // First call should have --bare and --session-id + const firstCallArgs = mockExeca().mock.calls[0][1] as string[] + expect(firstCallArgs).toContain('--bare') + expect(firstCallArgs).toContain('--session-id') + + // Second call (--resume) should have --bare and --resume + const secondCallArgs = mockExeca().mock.calls[1][1] as string[] + expect(secondCallArgs).toContain('--bare') + expect(secondCallArgs).toContain('--resume') + + // Third call should NOT have --bare (fallback) + const thirdCallArgs = mockExeca().mock.calls[2][1] as string[] + expect(thirdCallArgs).not.toContain('--bare') + + // Verify warning was logged + expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Bare mode failed during --resume retry')) + }) + + it('should throw when session-in-use --resume retry fails with non-auth error', async () => { + // Set up OAuth token so bare mode will be auto-applied + process.env.CLAUDE_CODE_OAUTH_TOKEN = 'sk-ant-oat01-test-token' + + const sessionId = '01af28fe-8630-4778-ae85-39398ab84f54' + + // First call: fails with session-in-use error + mockExeca().mockRejectedValueOnce({ + stderr: `Error: Session ID ${sessionId} is already in use.`, + exitCode: 1, + }) + + // Second call (--resume retry): fails with non-auth error + mockExeca().mockRejectedValueOnce({ + stderr: 'Some other error on retry', + exitCode: 1, + }) + + await expect( + launchClaude('test prompt', { + headless: true, + noSessionPersistence: true, + sessionId, + }) + ).rejects.toThrow('Claude CLI error: Some other error on retry') + + // Should have been called exactly twice (initial + resume retry, no bare fallback) + expect(execa).toHaveBeenCalledTimes(2) + }) }) describe.runIf(process.platform === 'darwin')('launchClaudeInNewTerminalWindow', () => {