@@ -143,7 +143,7 @@ describe("CodexAppServerAgent", () => {
143143 } ) ;
144144 } ) ;
145145
146- it ( "isolates subagent output, usage, compaction, and completion " , async ( ) => {
146+ it ( "surfaces subagent activity while isolating its lifecycle state " , async ( ) => {
147147 const stub = makeStubRpc ( {
148148 initialize : { } ,
149149 "thread/start" : { thread : { id : "thr_1" } } ,
@@ -191,7 +191,6 @@ describe("CodexAppServerAgent", () => {
191191 prompt : "Review the implementation" ,
192192 } ,
193193 } ) ;
194- const sessionUpdateCount = sessionUpdates . length ;
195194 const extNotificationCount = extNotifications . length ;
196195
197196 stub . emit ( "item/agentMessage/delta" , {
@@ -215,6 +214,16 @@ describe("CodexAppServerAgent", () => {
215214 text : '{"source":"child"}' ,
216215 } ,
217216 } ) ;
217+ stub . emit ( "item/started" , {
218+ threadId : "subagent_1" ,
219+ turnId : "subagent_turn_1" ,
220+ item : {
221+ type : "commandExecution" ,
222+ id : "shared_command_id" ,
223+ command : "echo child" ,
224+ status : "inProgress" ,
225+ } ,
226+ } ) ;
218227 stub . emit ( "item/commandExecution/outputDelta" , {
219228 threadId : "subagent_1" ,
220229 turnId : "subagent_turn_1" ,
@@ -251,11 +260,9 @@ describe("CodexAppServerAgent", () => {
251260 expect ( {
252261 extNotifications : extNotifications . length ,
253262 promptSettled,
254- sessionUpdates : sessionUpdates . length ,
255263 } ) . toEqual ( {
256264 extNotifications : extNotificationCount ,
257265 promptSettled : false ,
258- sessionUpdates : sessionUpdateCount ,
259266 } ) ;
260267
261268 stub . emit ( "item/agentMessage/delta" , {
@@ -292,10 +299,14 @@ describe("CodexAppServerAgent", () => {
292299 await expect ( promptDone ) . resolves . toMatchObject ( { stopReason : "end_turn" } ) ;
293300 const serializedUpdates = JSON . stringify ( sessionUpdates ) ;
294301 expect ( serializedUpdates ) . toContain ( "spawn_agent" ) ;
302+ expect ( serializedUpdates ) . toContain ( "subagent prose" ) ;
303+ expect ( serializedUpdates ) . toContain ( "subagent reasoning" ) ;
304+ expect ( serializedUpdates ) . toContain ( "child command output" ) ;
305+ expect ( serializedUpdates ) . toContain (
306+ "subagent:subagent_1:shared_command_id" ,
307+ ) ;
308+ expect ( serializedUpdates ) . toContain ( '"parentToolCallId":"spawn_1"' ) ;
295309 expect ( serializedUpdates ) . toContain ( "parent response" ) ;
296- expect ( serializedUpdates ) . not . toContain ( "subagent prose" ) ;
297- expect ( serializedUpdates ) . not . toContain ( "subagent reasoning" ) ;
298- expect ( serializedUpdates ) . not . toContain ( "child command output" ) ;
299310 expect ( structuredOutputs ) . toEqual ( [ { source : "parent" } ] ) ;
300311 expect (
301312 extNotifications . filter (
@@ -304,6 +315,142 @@ describe("CodexAppServerAgent", () => {
304315 ) . toHaveLength ( 1 ) ;
305316 } ) ;
306317
318+ it . each ( [ "resumeAgent" , "sendInput" ] ) (
319+ "attaches child activity to the current %s call" ,
320+ async ( collaborationTool ) => {
321+ let turnNumber = 0 ;
322+ const stub = makeStubRpc ( {
323+ initialize : { } ,
324+ "thread/start" : { thread : { id : "thr_1" } } ,
325+ "turn/start" : ( ) => ( {
326+ turn : {
327+ id : `turn_${ ++ turnNumber } ` ,
328+ status : "inProgress" ,
329+ } ,
330+ } ) ,
331+ } ) ;
332+ const { client, sessionUpdates } = makeFakeClient ( ) ;
333+ const agent = new CodexAppServerAgent ( client , {
334+ processOptions : { binaryPath : "/bundle/codex" } ,
335+ model : "gpt-5.5" ,
336+ rpcFactory : stub . factory ,
337+ } ) ;
338+
339+ await agent . initialize ( init ) ;
340+ await agent . newSession ( { cwd : "/repo" } as unknown as NewSessionRequest ) ;
341+
342+ const firstPrompt = agent . prompt ( {
343+ sessionId : "thr_1" ,
344+ prompt : [ { type : "text" , text : "spawn" } ] ,
345+ } as unknown as PromptRequest ) ;
346+ stub . emit ( "item/started" , {
347+ threadId : "thr_1" ,
348+ turnId : "turn_1" ,
349+ item : {
350+ type : "collabAgentToolCall" ,
351+ id : "spawn_1" ,
352+ tool : "spawnAgent" ,
353+ receiverThreadIds : [ "subagent_1" ] ,
354+ status : "inProgress" ,
355+ } ,
356+ } ) ;
357+ stub . emit ( "turn/completed" , {
358+ threadId : "thr_1" ,
359+ turn : { id : "turn_1" , status : "completed" } ,
360+ } ) ;
361+ await firstPrompt ;
362+
363+ const secondPrompt = agent . prompt ( {
364+ sessionId : "thr_1" ,
365+ prompt : [ { type : "text" , text : "continue" } ] ,
366+ } as unknown as PromptRequest ) ;
367+ const currentCallId = `${ collaborationTool } _1` ;
368+ stub . emit ( "item/started" , {
369+ threadId : "thr_1" ,
370+ turnId : "turn_2" ,
371+ item : {
372+ type : "collabAgentToolCall" ,
373+ id : currentCallId ,
374+ tool : collaborationTool ,
375+ receiverThreadIds : [ "subagent_1" ] ,
376+ status : "inProgress" ,
377+ } ,
378+ } ) ;
379+ stub . emit ( "item/agentMessage/delta" , {
380+ threadId : "subagent_1" ,
381+ turnId : "subagent_turn_2" ,
382+ itemId : "message_2" ,
383+ delta : "continued work" ,
384+ } ) ;
385+
386+ expect ( JSON . stringify ( sessionUpdates ) ) . toContain (
387+ `"parentToolCallId":"${ currentCallId } "` ,
388+ ) ;
389+
390+ stub . emit ( "turn/completed" , {
391+ threadId : "thr_1" ,
392+ turn : { id : "turn_2" , status : "completed" } ,
393+ } ) ;
394+ await secondPrompt ;
395+ } ,
396+ ) ;
397+
398+ it ( "buffers child activity until its parent tool call arrives" , async ( ) => {
399+ const stub = makeStubRpc ( {
400+ initialize : { } ,
401+ "thread/start" : { thread : { id : "thr_1" } } ,
402+ "turn/start" : { turn : { id : "turn_1" , status : "inProgress" } } ,
403+ } ) ;
404+ const { client, sessionUpdates } = makeFakeClient ( ) ;
405+ const agent = new CodexAppServerAgent ( client , {
406+ processOptions : { binaryPath : "/bundle/codex" } ,
407+ model : "gpt-5.5" ,
408+ rpcFactory : stub . factory ,
409+ } ) ;
410+
411+ await agent . initialize ( init ) ;
412+ await agent . newSession ( { cwd : "/repo" } as unknown as NewSessionRequest ) ;
413+ const promptDone = agent . prompt ( {
414+ sessionId : "thr_1" ,
415+ prompt : [ { type : "text" , text : "delegate" } ] ,
416+ } as unknown as PromptRequest ) ;
417+
418+ stub . emit ( "item/agentMessage/delta" , {
419+ threadId : "subagent_1" ,
420+ turnId : "subagent_turn_1" ,
421+ itemId : "message_1" ,
422+ delta : "early child activity" ,
423+ } ) ;
424+ expect ( JSON . stringify ( sessionUpdates ) ) . not . toContain (
425+ "early child activity" ,
426+ ) ;
427+
428+ stub . emit ( "item/started" , {
429+ threadId : "thr_1" ,
430+ turnId : "turn_1" ,
431+ item : {
432+ type : "collabAgentToolCall" ,
433+ id : "spawn_1" ,
434+ tool : "spawnAgent" ,
435+ receiverThreadIds : [ "subagent_1" ] ,
436+ status : "inProgress" ,
437+ } ,
438+ } ) ;
439+
440+ const serializedUpdates = JSON . stringify ( sessionUpdates ) ;
441+ expect ( serializedUpdates ) . toContain ( "early child activity" ) ;
442+ expect ( serializedUpdates ) . toContain ( '"parentToolCallId":"spawn_1"' ) ;
443+ expect ( serializedUpdates . indexOf ( "spawn_agent" ) ) . toBeLessThan (
444+ serializedUpdates . indexOf ( "early child activity" ) ,
445+ ) ;
446+
447+ stub . emit ( "turn/completed" , {
448+ threadId : "thr_1" ,
449+ turn : { id : "turn_1" , status : "completed" } ,
450+ } ) ;
451+ await promptDone ;
452+ } ) ;
453+
307454 it . each ( [
308455 {
309456 label : "reads an empty goal" ,
@@ -2492,6 +2639,53 @@ describe("CodexAppServerAgent", () => {
24922639 } ) ;
24932640 } ) ;
24942641
2642+ it ( "restores subagent relationships from resumed thread history" , async ( ) => {
2643+ const stub = makeStubRpc ( {
2644+ initialize : { } ,
2645+ "thread/resume" : {
2646+ thread : {
2647+ id : "t1" ,
2648+ turns : [
2649+ {
2650+ items : [
2651+ {
2652+ type : "collabAgentToolCall" ,
2653+ id : "spawn_1" ,
2654+ tool : "spawnAgent" ,
2655+ receiverThreadIds : [ "subagent_1" ] ,
2656+ status : "completed" ,
2657+ } ,
2658+ ] ,
2659+ } ,
2660+ ] ,
2661+ } ,
2662+ } ,
2663+ } ) ;
2664+ const { client, sessionUpdates } = makeFakeClient ( ) ;
2665+ const agent = new CodexAppServerAgent ( client , {
2666+ processOptions : { binaryPath : "/x/codex" } ,
2667+ model : "gpt-5.5" ,
2668+ rpcFactory : stub . factory ,
2669+ } ) ;
2670+ await agent . initialize ( init ) ;
2671+ await agent . resumeSession ( {
2672+ sessionId : "t1" ,
2673+ cwd : "/r" ,
2674+ mcpServers : [ ] ,
2675+ } as unknown as Parameters < typeof agent . resumeSession > [ 0 ] ) ;
2676+
2677+ stub . emit ( "item/agentMessage/delta" , {
2678+ threadId : "subagent_1" ,
2679+ turnId : "subagent_turn_1" ,
2680+ itemId : "message_1" ,
2681+ delta : "still working" ,
2682+ } ) ;
2683+
2684+ expect ( JSON . stringify ( sessionUpdates ) ) . toContain (
2685+ '"parentToolCallId":"spawn_1"' ,
2686+ ) ;
2687+ } ) ;
2688+
24952689 it ( "forwards additionalDirectories to thread/start as writable_roots" , async ( ) => {
24962690 const stub = makeStubRpc ( { "thread/start" : { thread : { id : "t" } } } ) ;
24972691 const { client } = makeFakeClient ( ) ;
0 commit comments