Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions src/node/services/taskService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3690,6 +3690,62 @@ describe("TaskService", () => {
expect(report.reportMarkdown).toBe("ok");
});

test("waitForAgentReport reuses the standard completion reminder for awaiting_report tasks", async () => {
const config = await createTestConfig(rootDir);

const projectPath = path.join(rootDir, "repo");
const parentId = "parent-111";
const childId = "child-222";

await config.saveConfig({
projects: new Map([
[
projectPath,
{
trusted: true,
workspaces: [
{ path: path.join(projectPath, "parent"), id: parentId, name: "parent" },
{
path: path.join(projectPath, "child"),
id: childId,
name: "agent_explore_child",
parentWorkspaceId: parentId,
agentType: "explore",
taskStatus: "awaiting_report",
},
],
},
],
]),
taskSettings: { maxParallelAgentTasks: 1, maxTaskNestingDepth: 3 },
});

const { workspaceService, sendMessage } = createWorkspaceServiceMocks();
const { taskService } = createTaskServiceHarness(config, { workspaceService });

const waitError = await taskService
.waitForAgentReport(childId, { timeoutMs: 10 })
.catch((error: unknown) => error);

expect(waitError).toBeInstanceOf(Error);
if (waitError instanceof Error) {
expect(waitError.message).toBe("Timed out waiting for agent_report");
}
expect(sendMessage).toHaveBeenCalledTimes(1);
expect(sendMessage).toHaveBeenCalledWith(
childId,
expect.stringContaining("Your stream ended without calling agent_report"),
expect.any(Object),
expect.objectContaining({ synthetic: true, agentInitiated: true })
);
expect(sendMessage).not.toHaveBeenCalledWith(
childId,
expect.stringContaining("A caller is still waiting for agent_report"),
expect.any(Object),
expect.any(Object)
);
});

test("waitForAgentReport rejects interrupted tasks without waiting", async () => {
const config = await createTestConfig(rootDir);

Expand Down
10 changes: 5 additions & 5 deletions src/node/services/taskService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2053,9 +2053,11 @@ export class TaskService {
}

if (initialStatus === "awaiting_report") {
// Reuse the standard completion reminder when a waiter attaches instead of carrying a
// separate waiter-only recovery mode and prompt string.
void this.workspaceEventLocks
.withLock(taskId, async () => {
await this.promptTaskForRequiredCompletionTool(taskId, { reason: "waiter" });
await this.promptTaskForRequiredCompletionTool(taskId);
})
.catch((error: unknown) => {
log.error("Failed to resume awaiting_report task for waiter", {
Expand Down Expand Up @@ -3081,7 +3083,7 @@ export class TaskService {
private buildCompletionToolRecoveryMessage(
completionToolName: "agent_report" | "propose_plan",
options?: {
reason?: "startup" | "stream_end" | "error" | "waiter";
reason?: "startup" | "stream_end" | "error";
error?: Pick<ErrorEvent, "error" | "errorType">;
}
): string {
Expand All @@ -3105,8 +3107,6 @@ export class TaskService {
: "";
return `The previous ${completionToolLabel} attempt failed${errorType}. ${noExtraWorkInstruction} ${completionInstruction}`;
}
case "waiter":
return `A caller is still waiting for ${completionToolLabel} from this task. ${noExtraWorkInstruction} ${completionInstruction}`;
case "stream_end":
default:
return `Your stream ended without calling ${completionToolLabel}. ${noExtraWorkInstruction} ${completionInstruction}`;
Expand All @@ -3116,7 +3116,7 @@ export class TaskService {
private async promptTaskForRequiredCompletionTool(
workspaceId: string,
options?: {
reason?: "startup" | "stream_end" | "error" | "waiter";
reason?: "startup" | "stream_end" | "error";
error?: Pick<ErrorEvent, "error" | "errorType">;
}
): Promise<boolean> {
Expand Down
Loading