@@ -35,6 +35,9 @@ const hoisted = vi.hoisted(() => {
3535 const initializeSessionMock = vi . fn ( ) ;
3636 const startAcpSpawnParentStreamRelayMock = vi . fn ( ) ;
3737 const resolveAcpSpawnStreamLogPathMock = vi . fn ( ) ;
38+ const loadSessionStoreMock = vi . fn ( ) ;
39+ const resolveStorePathMock = vi . fn ( ) ;
40+ const resolveSessionTranscriptFileMock = vi . fn ( ) ;
3841 const state = {
3942 cfg : createDefaultSpawnConfig ( ) ,
4043 } ;
@@ -49,6 +52,9 @@ const hoisted = vi.hoisted(() => {
4952 initializeSessionMock,
5053 startAcpSpawnParentStreamRelayMock,
5154 resolveAcpSpawnStreamLogPathMock,
55+ loadSessionStoreMock,
56+ resolveStorePathMock,
57+ resolveSessionTranscriptFileMock,
5258 state,
5359 } ;
5460} ) ;
@@ -86,6 +92,24 @@ vi.mock("../gateway/call.js", () => ({
8692 callGateway : ( opts : unknown ) => hoisted . callGatewayMock ( opts ) ,
8793} ) ) ;
8894
95+ vi . mock ( "../config/sessions.js" , async ( importOriginal ) => {
96+ const actual = await importOriginal < typeof import ( "../config/sessions.js" ) > ( ) ;
97+ return {
98+ ...actual ,
99+ loadSessionStore : ( storePath : string ) => hoisted . loadSessionStoreMock ( storePath ) ,
100+ resolveStorePath : ( store : unknown , opts : unknown ) => hoisted . resolveStorePathMock ( store , opts ) ,
101+ } ;
102+ } ) ;
103+
104+ vi . mock ( "../config/sessions/transcript.js" , async ( importOriginal ) => {
105+ const actual = await importOriginal < typeof import ( "../config/sessions/transcript.js" ) > ( ) ;
106+ return {
107+ ...actual ,
108+ resolveSessionTranscriptFile : ( params : unknown ) =>
109+ hoisted . resolveSessionTranscriptFileMock ( params ) ,
110+ } ;
111+ } ) ;
112+
89113vi . mock ( "../acp/control-plane/manager.js" , ( ) => {
90114 return {
91115 getAcpSessionManager : ( ) => ( {
@@ -263,6 +287,34 @@ describe("spawnAcpDirect", () => {
263287 hoisted . resolveAcpSpawnStreamLogPathMock
264288 . mockReset ( )
265289 . mockReturnValue ( "/tmp/sess-main.acp-stream.jsonl" ) ;
290+ hoisted . resolveStorePathMock . mockReset ( ) . mockReturnValue ( "/tmp/codex-sessions.json" ) ;
291+ hoisted . loadSessionStoreMock . mockReset ( ) . mockImplementation ( ( ) => {
292+ const store : Record < string , { sessionId : string ; updatedAt : number } > = { } ;
293+ return new Proxy ( store , {
294+ get ( _target , prop ) {
295+ if ( typeof prop === "string" && prop . startsWith ( "agent:codex:acp:" ) ) {
296+ return { sessionId : "sess-123" , updatedAt : Date . now ( ) } ;
297+ }
298+ return undefined ;
299+ } ,
300+ } ) ;
301+ } ) ;
302+ hoisted . resolveSessionTranscriptFileMock
303+ . mockReset ( )
304+ . mockImplementation ( async ( params : unknown ) => {
305+ const typed = params as { threadId ?: string } ;
306+ const sessionFile = typed . threadId
307+ ? `/tmp/agents/codex/sessions/sess-123-topic-${ typed . threadId } .jsonl`
308+ : "/tmp/agents/codex/sessions/sess-123.jsonl" ;
309+ return {
310+ sessionFile,
311+ sessionEntry : {
312+ sessionId : "sess-123" ,
313+ updatedAt : Date . now ( ) ,
314+ sessionFile,
315+ } ,
316+ } ;
317+ } ) ;
266318 } ) ;
267319
268320 it ( "spawns ACP session, binds a new thread, and dispatches initial task" , async ( ) => {
@@ -286,6 +338,13 @@ describe("spawnAcpDirect", () => {
286338 expect ( result . childSessionKey ) . toMatch ( / ^ a g e n t : c o d e x : a c p : / ) ;
287339 expect ( result . runId ) . toBe ( "run-1" ) ;
288340 expect ( result . mode ) . toBe ( "session" ) ;
341+ const patchCalls = hoisted . callGatewayMock . mock . calls
342+ . map ( ( call : unknown [ ] ) => call [ 0 ] as { method ?: string ; params ?: Record < string , unknown > } )
343+ . filter ( ( request ) => request . method === "sessions.patch" ) ;
344+ expect ( patchCalls [ 0 ] ?. params ) . toMatchObject ( {
345+ key : result . childSessionKey ,
346+ spawnedBy : "agent:main:main" ,
347+ } ) ;
289348 expect ( hoisted . sessionBindingBindMock ) . toHaveBeenCalledWith (
290349 expect . objectContaining ( {
291350 targetKind : "session" ,
@@ -308,6 +367,12 @@ describe("spawnAcpDirect", () => {
308367 mode : "persistent" ,
309368 } ) ,
310369 ) ;
370+ const transcriptCalls = hoisted . resolveSessionTranscriptFileMock . mock . calls . map (
371+ ( call : unknown [ ] ) => call [ 0 ] as { threadId ?: string } ,
372+ ) ;
373+ expect ( transcriptCalls ) . toHaveLength ( 2 ) ;
374+ expect ( transcriptCalls [ 0 ] ?. threadId ) . toBeUndefined ( ) ;
375+ expect ( transcriptCalls [ 1 ] ?. threadId ) . toBe ( "child-thread" ) ;
311376 } ) ;
312377
313378 it ( "does not inline delivery for fresh oneshot ACP runs" , async ( ) => {
@@ -328,6 +393,13 @@ describe("spawnAcpDirect", () => {
328393
329394 expect ( result . status ) . toBe ( "accepted" ) ;
330395 expect ( result . mode ) . toBe ( "run" ) ;
396+ expect ( hoisted . resolveSessionTranscriptFileMock ) . toHaveBeenCalledWith (
397+ expect . objectContaining ( {
398+ sessionId : "sess-123" ,
399+ storePath : "/tmp/codex-sessions.json" ,
400+ agentId : "codex" ,
401+ } ) ,
402+ ) ;
331403 const agentCall = hoisted . callGatewayMock . mock . calls
332404 . map ( ( call : unknown [ ] ) => call [ 0 ] as { method ?: string ; params ?: Record < string , unknown > } )
333405 . find ( ( request ) => request . method === "agent" ) ;
@@ -337,6 +409,32 @@ describe("spawnAcpDirect", () => {
337409 expect ( agentCall ?. params ?. threadId ) . toBeUndefined ( ) ;
338410 } ) ;
339411
412+ it ( "keeps ACP spawn running when session-file persistence fails" , async ( ) => {
413+ hoisted . resolveSessionTranscriptFileMock . mockRejectedValueOnce ( new Error ( "disk full" ) ) ;
414+
415+ const result = await spawnAcpDirect (
416+ {
417+ task : "Investigate flaky tests" ,
418+ agentId : "codex" ,
419+ mode : "run" ,
420+ } ,
421+ {
422+ agentSessionKey : "agent:main:main" ,
423+ agentChannel : "telegram" ,
424+ agentAccountId : "default" ,
425+ agentTo : "telegram:6098642967" ,
426+ agentThreadId : "1" ,
427+ } ,
428+ ) ;
429+
430+ expect ( result . status ) . toBe ( "accepted" ) ;
431+ expect ( result . childSessionKey ) . toMatch ( / ^ a g e n t : c o d e x : a c p : / ) ;
432+ const agentCall = hoisted . callGatewayMock . mock . calls
433+ . map ( ( call : unknown [ ] ) => call [ 0 ] as { method ?: string ; params ?: Record < string , unknown > } )
434+ . find ( ( request ) => request . method === "agent" ) ;
435+ expect ( agentCall ?. params ?. sessionKey ) . toBe ( result . childSessionKey ) ;
436+ } ) ;
437+
340438 it ( "includes cwd in ACP thread intro banner when provided at spawn time" , async ( ) => {
341439 const result = await spawnAcpDirect (
342440 {
0 commit comments