From 93ca82a2a75c01f4b45722159e22346867fddddc Mon Sep 17 00:00:00 2001 From: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com> Date: Fri, 30 Oct 2026 03:36:07 +0000 Subject: [PATCH] fix(agent-status): keep the conversation id across hookless claude turns (#36208) Claude reports slash commands like /context over OSC 7501 with no hooks, so idle panes lost their session and mobile chat could not find it. Fixes #36033 --- .../server-ingest-terminal-status.test.ts | 30 +++++++++++++++++-- .../server/server-ingest-terminal.ts | 5 ++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/main/agent-hooks/server-ingest-terminal-status.test.ts b/src/main/agent-hooks/server-ingest-terminal-status.test.ts index 1d9fb46736c..1d45ce84e90 100644 --- a/src/main/agent-hooks/server-ingest-terminal-status.test.ts +++ b/src/main/agent-hooks/server-ingest-terminal-status.test.ts @@ -215,7 +215,31 @@ describe('AgentHookServer ingestTerminalStatus', () => { } ) - it('drops the cached provider session when an OSC status starts a new turn', () => { + // Claude reports hookless slash commands (`/context`) as OSC 7501 working then done; the session lives on (#36033). + it('keeps the cached provider session when an OSC status starts a turn after done', () => { + const server = new AgentHookServer() + const providerSession = { key: 'session_id' as const, id: 'claude-session-1' } + + server.ingestRemote( + { + paneKey: PANE, + providerSession, + payload: { state: 'done', prompt: 'first', agentType: 'claude' } + }, + 'conn-1' + ) + for (const state of ['working', 'done'] as const) { + server.ingestTerminalStatus({ + paneKey: PANE, + connectionId: 'conn-1', + payload: { state, prompt: '', agentType: 'claude' } + }) + } + + expect(server.getStatusSnapshot()[0]).toMatchObject({ state: 'done', providerSession }) + }) + + it('drops the cached provider session when a relaunched agent starts a turn after the old one exited', () => { const server = new AgentHookServer() server.ingestRemote( @@ -226,10 +250,12 @@ describe('AgentHookServer ingestTerminalStatus', () => { }, 'conn-1' ) + server.reconcileEndedProcessForPaneKeys([PANE], { preserveResumeIdentity: true }) + expect(server.getStatusSnapshot()[0]).toMatchObject({ providerSessionOnly: true }) server.ingestTerminalStatus({ paneKey: PANE, connectionId: 'conn-1', - payload: { state: 'working', prompt: 'second', agentType: 'claude' } + payload: { state: 'working', prompt: '', agentType: 'claude' } }) expect(server.getStatusSnapshot()[0]?.providerSession).toBeUndefined() diff --git a/src/main/agent-hooks/server/server-ingest-terminal.ts b/src/main/agent-hooks/server/server-ingest-terminal.ts index 245bc92c6fa..6859fa3ee26 100644 --- a/src/main/agent-hooks/server/server-ingest-terminal.ts +++ b/src/main/agent-hooks/server/server-ingest-terminal.ts @@ -280,13 +280,14 @@ export abstract class AgentHookServerIngestTerminal extends AgentHookServerInges event.payload.agentType && event.payload.agentType !== 'unknown' ? event.payload.agentType : undefined - // Launch seeds are not completed turns; real completed sessions must not leak into reused panes. + // A live agent's turn after `done` keeps its session (OSC 7501 reports hookless slash commands); a new run or an exited agent's resume-only row does not. Launch seeds are not completed turns. const preservedProviderSession = previous?.providerSession && (claimedAgentType === undefined || claimedAgentType === previous.payload.agentType) && (previous.payload.state !== 'done' || event.payload.state === 'done' || - previous.observation?.origin === 'launch') + previous.observation?.origin === 'launch' || + (!newRun && previous.providerSessionOnly !== true)) ? previous.providerSession : undefined // Without a new main-agent fact, preserve it only while the row's state and agent still match.