@@ -13,15 +13,17 @@ import { EventEmitter } from "events";
1313import type { Config } from "@/node/config" ;
1414import type { WorkspaceService } from "@/node/services/workspaceService" ;
1515import type { HistoryService } from "@/node/services/historyService" ;
16+ import type { PartialService } from "@/node/services/partialService" ;
1617import type { AIService } from "@/node/services/aiService" ;
17- import type { AgentType , TaskState } from "@/common/types/task" ;
18+ import type { AgentType , TaskState , TaskToolResult } from "@/common/types/task" ;
1819import { getAgentPreset } from "@/common/constants/agentPresets" ;
1920import { log } from "@/node/services/log" ;
2021import {
2122 createMuxMessage ,
2223 type MuxMessage ,
2324 type MuxFrontendMetadata ,
2425} from "@/common/types/message" ;
26+ import { isDynamicToolPart } from "@/common/types/toolParts" ;
2527import type { FrontendWorkspaceMetadata } from "@/common/types/workspace" ;
2628import { detectDefaultTrunkBranch , listLocalBranches } from "@/node/git" ;
2729import * as crypto from "crypto" ;
@@ -85,6 +87,7 @@ export class TaskService extends EventEmitter {
8587 private readonly config : Config ,
8688 private readonly workspaceService : WorkspaceService ,
8789 private readonly historyService : HistoryService ,
90+ private readonly partialService : PartialService ,
8891 private readonly aiService : AIService
8992 ) {
9093 super ( ) ;
@@ -324,8 +327,15 @@ export class TaskService extends EventEmitter {
324327 } ) ;
325328 }
326329
327- // Post report to parent workspace history
328- await this . postReportToParent ( workspaceId , taskState , args ) ;
330+ // If this was a foreground task, inject the result into the parent's tool call
331+ // This enables restart recovery - even if pendingCompletions was lost, the parent
332+ // will have the tool output when it resumes
333+ if ( taskState . parentToolCallId ) {
334+ await this . injectToolOutputToParent ( taskState , args ) ;
335+ } else {
336+ // Background task - post report as a message to parent history
337+ await this . postReportToParent ( workspaceId , taskState , args ) ;
338+ }
329339
330340 // Process queue (a slot freed up)
331341 await this . processQueue ( ) ;
@@ -373,6 +383,95 @@ export class TaskService extends EventEmitter {
373383 }
374384 }
375385
386+ /**
387+ * Inject task tool output into the parent workspace's pending tool call.
388+ * This updates the parent's partial.json or chat.jsonl so the tool result
389+ * persists across restarts.
390+ */
391+ private async injectToolOutputToParent (
392+ taskState : TaskState ,
393+ report : { reportMarkdown : string ; title ?: string }
394+ ) : Promise < void > {
395+ const { parentWorkspaceId, parentToolCallId } = taskState ;
396+ if ( ! parentToolCallId ) return ;
397+
398+ const toolOutput : TaskToolResult = {
399+ status : "completed" ,
400+ taskId : taskState . parentWorkspaceId , // The task workspace ID
401+ reportMarkdown : report . reportMarkdown ,
402+ reportTitle : report . title ,
403+ } ;
404+
405+ try {
406+ // Try to update partial.json first (most likely location for in-flight tool call)
407+ const partial = await this . partialService . readPartial ( parentWorkspaceId ) ;
408+ if ( partial ) {
409+ const updated = this . updateToolCallOutput ( partial , parentToolCallId , toolOutput ) ;
410+ if ( updated ) {
411+ await this . partialService . writePartial ( parentWorkspaceId , updated ) ;
412+ log . debug ( `Injected task result into parent partial` , {
413+ parentWorkspaceId,
414+ parentToolCallId,
415+ } ) ;
416+ return ;
417+ }
418+ }
419+
420+ // Fall back to chat history
421+ const historyResult = await this . historyService . getHistory ( parentWorkspaceId ) ;
422+ if ( historyResult . success ) {
423+ // Find the message with this tool call (search from newest to oldest)
424+ for ( let i = historyResult . data . length - 1 ; i >= 0 ; i -- ) {
425+ const msg = historyResult . data [ i ] ;
426+ const updated = this . updateToolCallOutput ( msg , parentToolCallId , toolOutput ) ;
427+ if ( updated ) {
428+ await this . historyService . updateHistory ( parentWorkspaceId , updated ) ;
429+ log . debug ( `Injected task result into parent history` , {
430+ parentWorkspaceId,
431+ parentToolCallId,
432+ } ) ;
433+ return ;
434+ }
435+ }
436+ }
437+
438+ // Couldn't find the tool call - fall back to posting as message
439+ log . warn ( `Could not find parent tool call ${ parentToolCallId } , posting as message instead` ) ;
440+ await this . postReportToParent ( parentWorkspaceId , taskState , report ) ;
441+ } catch ( error ) {
442+ log . error ( `Failed to inject tool output to parent ${ parentWorkspaceId } :` , error ) ;
443+ // Fall back to posting as message
444+ await this . postReportToParent ( parentWorkspaceId , taskState , report ) ;
445+ }
446+ }
447+
448+ /**
449+ * Update a tool call's output in a message. Returns the updated message if found, null otherwise.
450+ */
451+ private updateToolCallOutput (
452+ msg : MuxMessage ,
453+ toolCallId : string ,
454+ output : TaskToolResult
455+ ) : MuxMessage | null {
456+ let found = false ;
457+ const updatedParts = msg . parts . map ( ( part ) => {
458+ if ( ! isDynamicToolPart ( part ) || part . toolCallId !== toolCallId ) {
459+ return part ;
460+ }
461+ if ( part . toolName !== "task" ) {
462+ return part ;
463+ }
464+ found = true ;
465+ return {
466+ ...part ,
467+ state : "output-available" as const ,
468+ output,
469+ } ;
470+ } ) ;
471+
472+ return found ? { ...msg , parts : updatedParts } : null ;
473+ }
474+
376475 /**
377476 * Handle stream end - check if agent_report was called.
378477 */
0 commit comments