Skip to content

Commit 051d8f4

Browse files
committed
fix: resolve agent_report even if parent history write fails
Change-Id: I4febc00a10f55517b9ec272b9fa834d606a98ef9 Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent 9ca648a commit 051d8f4

File tree

2 files changed

+111
-16
lines changed

2 files changed

+111
-16
lines changed

src/node/services/taskService.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,96 @@ describe("TaskService", () => {
258258
});
259259
});
260260

261+
describe("handleAgentReport", () => {
262+
it("should resolve awaiters even if parent history append fails", async () => {
263+
const parentWorkspaceId = "parent";
264+
const childWorkspaceId = "child";
265+
266+
const workspace = {
267+
id: childWorkspaceId,
268+
path: "/tmp/agent",
269+
name: "agent",
270+
projectName: "proj",
271+
projectPath: "/proj",
272+
createdAt: "2025-01-01T00:00:00.000Z",
273+
parentWorkspaceId,
274+
agentType: "research",
275+
taskStatus: "running",
276+
taskModel: "openai:gpt-5-codex",
277+
};
278+
279+
const projects = new Map([
280+
[
281+
"/proj",
282+
{
283+
workspaces: [workspace],
284+
},
285+
],
286+
]);
287+
288+
let idCounter = 0;
289+
const config = {
290+
generateStableId: () => `id-${idCounter++}`,
291+
getTaskSettings: () => ({
292+
maxParallelAgentTasks: 3,
293+
maxTaskNestingDepth: 3,
294+
}),
295+
listWorkspaceConfigs: () => [],
296+
getWorkspaceConfig: (id: string) => {
297+
if (id !== childWorkspaceId) {
298+
return null;
299+
}
300+
301+
return { projectPath: "/proj", workspace };
302+
},
303+
editConfig: (edit: (cfg: unknown) => unknown) => {
304+
edit({ projects });
305+
},
306+
};
307+
308+
const historyService = {
309+
getHistory: (_workspaceId: string) => Ok([]),
310+
appendToHistory: (workspaceId: string, _message: MuxMessage) => {
311+
if (workspaceId === parentWorkspaceId) {
312+
return Err("disk full");
313+
}
314+
315+
return Ok(undefined);
316+
},
317+
};
318+
319+
const partialService = {
320+
readPartial: () => null,
321+
writePartial: () => Ok(undefined),
322+
};
323+
324+
const workspaceService = {
325+
emitChatEvent: (_workspaceId: string, _event: unknown) => undefined,
326+
emitWorkspaceMetadata: (_workspaceId: string) => undefined,
327+
remove: (_workspaceId: string, _force?: boolean) => Ok(undefined),
328+
};
329+
330+
const aiService = {
331+
on: () => undefined,
332+
};
333+
334+
const service = new TaskService(
335+
config as never,
336+
historyService as never,
337+
partialService as never,
338+
workspaceService as never,
339+
aiService as never
340+
);
341+
342+
const reportPromise = service.awaitAgentReport(childWorkspaceId);
343+
344+
await service.handleAgentReport(childWorkspaceId, { reportMarkdown: "hello" });
345+
346+
expect(await reportPromise).toEqual({ reportMarkdown: "hello" });
347+
expect(workspace.taskStatus).toBe("reported");
348+
});
349+
});
350+
261351
describe("onStreamEnd", () => {
262352
it("should finalize tasks when report enforcement resume fails", async () => {
263353
const parentWorkspaceId = "parent";

src/node/services/taskService.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -401,10 +401,13 @@ export class TaskService {
401401
return;
402402
}
403403

404-
// Mark reported.
405-
await this.updateTaskWorkspace(workspaceId, {
406-
taskStatus: "reported",
407-
});
404+
// Resolve any in-flight tool call awaiters first so foreground `task` calls don't hang even if
405+
// persisting the report to parent history fails.
406+
const deferred = this.pendingReportByWorkspaceId.get(workspaceId);
407+
if (deferred) {
408+
deferred.resolve(report);
409+
this.pendingReportByWorkspaceId.delete(workspaceId);
410+
}
408411

409412
const preset = workspaceConfig.workspace.agentType
410413
? getAgentPreset(workspaceConfig.workspace.agentType)
@@ -425,20 +428,22 @@ export class TaskService {
425428
reportMessage
426429
);
427430
if (!appendResult.success) {
428-
throw new Error(appendResult.error);
431+
log.error("Failed to append subagent report to parent history", {
432+
workspaceId,
433+
parentWorkspaceId,
434+
error: appendResult.error,
435+
});
436+
} else {
437+
this.workspaceService.emitChatEvent(parentWorkspaceId, {
438+
...reportMessage,
439+
type: "message",
440+
} satisfies WorkspaceChatMessage);
429441
}
430442

431-
this.workspaceService.emitChatEvent(parentWorkspaceId, {
432-
...reportMessage,
433-
type: "message",
434-
} satisfies WorkspaceChatMessage);
435-
436-
// Resolve any in-flight tool call awaiters.
437-
const deferred = this.pendingReportByWorkspaceId.get(workspaceId);
438-
if (deferred) {
439-
deferred.resolve(report);
440-
this.pendingReportByWorkspaceId.delete(workspaceId);
441-
}
443+
// Mark reported after best-effort delivery to the parent.
444+
await this.updateTaskWorkspace(workspaceId, {
445+
taskStatus: "reported",
446+
});
442447

443448
// Durable tool output + auto-resume for interrupted parent streams.
444449
const parentToolCallId = workspaceConfig.workspace.taskParentToolCallId;

0 commit comments

Comments
 (0)