@@ -469,6 +469,187 @@ describe("TaskService", () => {
469469 expect ( emit ) . toHaveBeenCalled ( ) ;
470470 } ) ;
471471
472+ test ( "agent_report finalizes pending task tool output from history when parent has no partial" , async ( ) => {
473+ const config = new Config ( rootDir ) ;
474+
475+ const projectPath = path . join ( rootDir , "repo" ) ;
476+ const parentId = "parent-111" ;
477+ const childId = "child-222" ;
478+
479+ await config . saveConfig ( {
480+ projects : new Map ( [
481+ [
482+ projectPath ,
483+ {
484+ workspaces : [
485+ { path : path . join ( projectPath , "parent" ) , id : parentId , name : "parent" } ,
486+ {
487+ path : path . join ( projectPath , "child" ) ,
488+ id : childId ,
489+ name : "agent_explore_child" ,
490+ parentWorkspaceId : parentId ,
491+ agentType : "explore" ,
492+ taskStatus : "running" ,
493+ } ,
494+ ] ,
495+ } ,
496+ ] ,
497+ ] ) ,
498+ taskSettings : { maxParallelAgentTasks : 3 , maxTaskNestingDepth : 3 } ,
499+ } ) ;
500+
501+ const historyService = new HistoryService ( config ) ;
502+ const partialService = new PartialService ( config , historyService ) ;
503+
504+ // Restart scenario: parent has no partial.json, only history containing the pending tool call.
505+ const parentHistoryMessage = createMuxMessage (
506+ "assistant-parent-history" ,
507+ "assistant" ,
508+ "Waiting on subagent…" ,
509+ { timestamp : Date . now ( ) } ,
510+ [
511+ {
512+ type : "dynamic-tool" ,
513+ toolCallId : "task-call-1" ,
514+ toolName : "task" ,
515+ input : { subagent_type : "explore" , prompt : "do the thing" } ,
516+ state : "input-available" ,
517+ } ,
518+ ]
519+ ) ;
520+ const appendParentHistory = await historyService . appendToHistory (
521+ parentId ,
522+ parentHistoryMessage
523+ ) ;
524+ expect ( appendParentHistory . success ) . toBe ( true ) ;
525+
526+ const childPartial = createMuxMessage (
527+ "assistant-child-partial" ,
528+ "assistant" ,
529+ "" ,
530+ { timestamp : Date . now ( ) } ,
531+ [
532+ {
533+ type : "dynamic-tool" ,
534+ toolCallId : "agent-report-call-1" ,
535+ toolName : "agent_report" ,
536+ input : { reportMarkdown : "Hello from child" , title : "Result" } ,
537+ state : "output-available" ,
538+ output : { success : true } ,
539+ } ,
540+ ]
541+ ) ;
542+ const writeChildPartial = await partialService . writePartial ( childId , childPartial ) ;
543+ expect ( writeChildPartial . success ) . toBe ( true ) ;
544+
545+ const aiService : AIService = {
546+ isStreaming : mock ( ( ) => false ) ,
547+ on : mock ( ( ) => undefined ) ,
548+ off : mock ( ( ) => undefined ) ,
549+ } as unknown as AIService ;
550+
551+ const sendMessage = mock ( ( ) => Promise . resolve ( Ok ( undefined ) ) ) ;
552+ const resumeStream = mock ( ( ) => Promise . resolve ( Ok ( undefined ) ) ) ;
553+ const remove = mock ( ( ) => Promise . resolve ( Ok ( undefined ) ) ) ;
554+ const emit = mock ( ( ) => true ) ;
555+
556+ const workspaceService : WorkspaceService = {
557+ sendMessage,
558+ resumeStream,
559+ remove,
560+ emit,
561+ } as unknown as WorkspaceService ;
562+
563+ const initStateManager : InitStateManager = {
564+ startInit : mock ( ( ) => undefined ) ,
565+ appendOutput : mock ( ( ) => undefined ) ,
566+ endInit : mock ( ( ) => Promise . resolve ( ) ) ,
567+ } as unknown as InitStateManager ;
568+
569+ const taskService = new TaskService (
570+ config ,
571+ historyService ,
572+ partialService ,
573+ aiService ,
574+ workspaceService ,
575+ initStateManager
576+ ) ;
577+
578+ const internal = taskService as unknown as {
579+ handleAgentReport : ( event : {
580+ type : "tool-call-end" ;
581+ workspaceId : string ;
582+ toolName : string ;
583+ } ) => Promise < void > ;
584+ } ;
585+ await internal . handleAgentReport ( {
586+ type : "tool-call-end" ,
587+ workspaceId : childId ,
588+ toolName : "agent_report" ,
589+ } ) ;
590+
591+ const parentPartial = await partialService . readPartial ( parentId ) ;
592+ expect ( parentPartial ) . toBeNull ( ) ;
593+
594+ const parentHistory = await historyService . getHistory ( parentId ) ;
595+ expect ( parentHistory . success ) . toBe ( true ) ;
596+ if ( parentHistory . success ) {
597+ const msg = parentHistory . data . find ( ( m ) => m . id === "assistant-parent-history" ) ?? null ;
598+ expect ( msg ) . not . toBeNull ( ) ;
599+ if ( msg ) {
600+ const toolPart = msg . parts . find (
601+ ( p ) =>
602+ p &&
603+ typeof p === "object" &&
604+ "type" in p &&
605+ ( p as { type ?: unknown } ) . type === "dynamic-tool"
606+ ) as unknown as
607+ | {
608+ toolName : string ;
609+ state : string ;
610+ output ?: unknown ;
611+ }
612+ | undefined ;
613+ expect ( toolPart ?. toolName ) . toBe ( "task" ) ;
614+ expect ( toolPart ?. state ) . toBe ( "output-available" ) ;
615+ expect ( toolPart ?. output && typeof toolPart . output === "object" ) . toBe ( true ) ;
616+ expect ( JSON . stringify ( toolPart ?. output ) ) . toContain ( "Hello from child" ) ;
617+ }
618+
619+ const combined = parentHistory . data
620+ . filter ( ( m ) => m . role === "assistant" )
621+ . flatMap ( ( m ) => m . parts )
622+ . map ( ( p ) =>
623+ p &&
624+ typeof p === "object" &&
625+ "type" in p &&
626+ ( p as { type ?: unknown } ) . type === "text" &&
627+ "text" in p
628+ ? ( p as { text ?: unknown } ) . text
629+ : ""
630+ )
631+ . join ( "" ) ;
632+ expect ( combined ) . toContain ( "Hello from child" ) ;
633+ }
634+
635+ const emitCalls = ( emit as unknown as { mock : { calls : Array < [ string , unknown ] > } } ) . mock . calls ;
636+ const hasTaskToolCallEnd = emitCalls . some ( ( call ) => {
637+ const [ eventName , payload ] = call ;
638+ if ( eventName !== "chat" ) return false ;
639+ if ( ! payload || typeof payload !== "object" ) return false ;
640+ const maybePayload = payload as { workspaceId ?: unknown ; message ?: unknown } ;
641+ if ( maybePayload . workspaceId !== parentId ) return false ;
642+ const message = maybePayload . message ;
643+ if ( ! message || typeof message !== "object" ) return false ;
644+ const maybeMessage = message as { type ?: unknown ; toolName ?: unknown } ;
645+ return maybeMessage . type === "tool-call-end" && maybeMessage . toolName === "task" ;
646+ } ) ;
647+ expect ( hasTaskToolCallEnd ) . toBe ( true ) ;
648+
649+ expect ( remove ) . toHaveBeenCalled ( ) ;
650+ expect ( resumeStream ) . toHaveBeenCalled ( ) ;
651+ } ) ;
652+
472653 test ( "missing agent_report triggers one reminder, then posts fallback output and cleans up" , async ( ) => {
473654 const config = new Config ( rootDir ) ;
474655
0 commit comments