diff --git a/src/services/acp/__tests__/agent.test.ts b/src/services/acp/__tests__/agent.test.ts index 1635a7ea7b..2c6417f002 100644 --- a/src/services/acp/__tests__/agent.test.ts +++ b/src/services/acp/__tests__/agent.test.ts @@ -69,8 +69,11 @@ mockModulePreservingExports('../../../utils/config.ts', { enableConfigs: mock(() => {}), }) +const mockSwitchSession = mock(() => {}) + mockModulePreservingExports('../../../bootstrap/state.ts', { setOriginalCwd: mock(() => {}), + switchSession: mockSwitchSession, addSlowOperation: mock(() => {}), }) @@ -222,6 +225,7 @@ describe('AcpAgent', () => { delete process.env.ACP_PERMISSION_MODE delete process.env.CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS mockSetModel.mockClear() + mockSwitchSession.mockClear() mockSubmitMessage.mockReset() mockSubmitMessage.mockImplementation(async function* (_input: string) {}) mockGetMainLoopModel.mockClear() @@ -1157,4 +1161,66 @@ describe('AcpAgent', () => { expect(commit.input).toEqual({ hint: '[message]' }) }) }) + + describe('sessionId alignment with global state', () => { + test('newSession calls switchSession with the generated sessionId', async () => { + const agent = new AcpAgent(makeConn()) + const res = await agent.newSession({ cwd: '/tmp' } as any) + expect(mockSwitchSession).toHaveBeenCalledWith(res.sessionId) + }) + + test('resumeSession calls switchSession with the requested sessionId', async () => { + const agent = new AcpAgent(makeConn()) + const requestedId = 'resume-test-session-id' + await agent.unstable_resumeSession({ + sessionId: requestedId, + cwd: '/tmp', + mcpServers: [], + } as any) + + expect(mockSwitchSession).toHaveBeenCalledWith(requestedId) + }) + + test('loadSession calls switchSession with the requested sessionId', async () => { + const agent = new AcpAgent(makeConn()) + const requestedId = 'load-test-session-id' + await agent.loadSession({ + sessionId: requestedId, + cwd: '/tmp', + mcpServers: [], + } as any) + + expect(mockSwitchSession).toHaveBeenCalledWith(requestedId) + }) + + test('resumeSession with existing session still calls switchSession', async () => { + const agent = new AcpAgent(makeConn()) + const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any) + mockSwitchSession.mockClear() + + // Resume the same session — should still align global state + await agent.unstable_resumeSession({ + sessionId, + cwd: '/tmp', + mcpServers: [], + } as any) + + expect(mockSwitchSession).toHaveBeenCalledWith(sessionId) + }) + + test('prompt does not trigger additional switchSession for multi-session', async () => { + const agent = new AcpAgent(makeConn()) + await agent.newSession({ cwd: '/tmp' } as any) + await agent.newSession({ cwd: '/tmp' } as any) + mockSwitchSession.mockClear() + + // Prompts should not call switchSession — alignment happens at session creation + const s1 = agent.sessions.keys().next().value + await agent.prompt({ + sessionId: s1, + prompt: [{ type: 'text', text: 'hello' }], + } as any) + expect(mockSwitchSession).not.toHaveBeenCalled() + }) + }) }) diff --git a/src/services/acp/agent.ts b/src/services/acp/agent.ts index a824397c4b..4c747a6ac3 100644 --- a/src/services/acp/agent.ts +++ b/src/services/acp/agent.ts @@ -53,7 +53,8 @@ import { getEmptyToolPermissionContext } from '../../Tool.js' import type { PermissionMode } from '../../types/permissions.js' import type { Command } from '../../types/command.js' import { getCommands } from '../../commands.js' -import { setOriginalCwd } from '../../bootstrap/state.js' +import { setOriginalCwd, switchSession } from '../../bootstrap/state.js' +import type { SessionId } from '../../types/ids.js' import { enableConfigs } from '../../utils/config.js' import { FileStateCache } from '../../utils/fileStateCache.js' import { getDefaultAppState } from '../../state/AppStateStore.js' @@ -471,6 +472,10 @@ export class AcpAgent implements Agent { const sessionId = opts.sessionId ?? randomUUID() const cwd = params.cwd + // Align the global session state so that transcript persistence, + // analytics, and cost tracking use the ACP session ID. + switchSession(sessionId as SessionId) + // Set CWD for the session setOriginalCwd(cwd) const previousProcessCwd = process.cwd() @@ -675,6 +680,8 @@ export class AcpAgent implements Agent { | undefined, }) if (fingerprint === existingSession.sessionFingerprint) { + // Align global state so subsequent operations use the correct session + switchSession(params.sessionId as SessionId) return { sessionId: params.sessionId, modes: existingSession.modes, @@ -687,6 +694,10 @@ export class AcpAgent implements Agent { await this.teardownSession(params.sessionId) } + // Align global state BEFORE sessionIdExists() check — the lookup uses + // getSessionId() internally when resolving project-scoped paths. + switchSession(params.sessionId as SessionId) + // Set CWD early so session file lookup can find the right project directory setOriginalCwd(params.cwd)