Skip to content

Commit 056c660

Browse files
committed
🤖 fix: resume awaiting_report tasks on restart
TaskService.initialize now resumes leaf tasks stuck in taskStatus=awaiting_report so they can emit agent_report after a crash/restart. Also harden the taskService tests by disabling commit signing in the temp repos to avoid hangs when developers have global commit.gpgsign enabled. Signed-off-by: Thomas Kosiewski <tk@coder.com> --- _Generated with `codex cli` • Model: `gpt-5.2` • Thinking: `xhigh`_ <!-- mux-attribution: model=gpt-5.2 thinking=xhigh --> Change-Id: I8e373a14a5d06b1998734dff50d1505924521754
1 parent 9fc4990 commit 056c660

File tree

2 files changed

+110
-0
lines changed

2 files changed

+110
-0
lines changed

src/node/services/taskService.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ function initGitRepo(projectPath: string): void {
2020
execSync("git init -b main", { cwd: projectPath, stdio: "ignore" });
2121
execSync('git config user.email "test@example.com"', { cwd: projectPath, stdio: "ignore" });
2222
execSync('git config user.name "test"', { cwd: projectPath, stdio: "ignore" });
23+
// Ensure tests don't hang when developers have global commit signing enabled.
24+
execSync("git config commit.gpgsign false", { cwd: projectPath, stdio: "ignore" });
2325
execSync("bash -lc 'echo \"hello\" > README.md'", { cwd: projectPath, stdio: "ignore" });
2426
execSync("git add README.md", { cwd: projectPath, stdio: "ignore" });
2527
execSync('git commit -m "init"', { cwd: projectPath, stdio: "ignore" });
@@ -301,6 +303,78 @@ describe("TaskService", () => {
301303
expect(started?.taskStatus).toBe("running");
302304
}, 20_000);
303305

306+
test("initialize resumes awaiting_report tasks after restart", async () => {
307+
const config = new Config(rootDir);
308+
309+
const projectPath = path.join(rootDir, "repo");
310+
const parentId = "parent-111";
311+
const childId = "child-222";
312+
313+
await config.saveConfig({
314+
projects: new Map([
315+
[
316+
projectPath,
317+
{
318+
workspaces: [
319+
{ path: path.join(projectPath, "parent"), id: parentId, name: "parent" },
320+
{
321+
path: path.join(projectPath, "child"),
322+
id: childId,
323+
name: "agent_explore_child",
324+
parentWorkspaceId: parentId,
325+
agentType: "explore",
326+
taskStatus: "awaiting_report",
327+
},
328+
],
329+
},
330+
],
331+
]),
332+
taskSettings: { maxParallelAgentTasks: 1, maxTaskNestingDepth: 3 },
333+
});
334+
335+
const historyService = new HistoryService(config);
336+
const partialService = new PartialService(config, historyService);
337+
338+
const aiService: AIService = {
339+
isStreaming: mock(() => false),
340+
on: mock(() => undefined),
341+
off: mock(() => undefined),
342+
} as unknown as AIService;
343+
344+
const resumeStream = mock(() => Promise.resolve(Ok(undefined)));
345+
346+
const workspaceService: WorkspaceService = {
347+
sendMessage: mock(() => Promise.resolve(Ok(undefined))),
348+
resumeStream,
349+
remove: mock(() => Promise.resolve(Ok(undefined))),
350+
emit: mock(() => true),
351+
} as unknown as WorkspaceService;
352+
353+
const initStateManager: InitStateManager = {
354+
startInit: mock(() => undefined),
355+
appendOutput: mock(() => undefined),
356+
endInit: mock(() => Promise.resolve()),
357+
} as unknown as InitStateManager;
358+
359+
const taskService = new TaskService(
360+
config,
361+
historyService,
362+
partialService,
363+
aiService,
364+
workspaceService,
365+
initStateManager
366+
);
367+
368+
await taskService.initialize();
369+
370+
expect(resumeStream).toHaveBeenCalledWith(
371+
childId,
372+
expect.objectContaining({
373+
toolPolicy: [{ regex_match: "^agent_report$", action: "require" }],
374+
})
375+
);
376+
});
377+
304378
test("waitForAgentReport does not time out while task is queued", async () => {
305379
const config = new Config(rootDir);
306380

src/node/services/taskService.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,46 @@ export class TaskService {
150150
await this.maybeStartQueuedTasks();
151151

152152
const config = this.config.loadConfigOrDefault();
153+
const awaitingReportTasks = this.listAgentTaskWorkspaces(config).filter(
154+
(t) => t.taskStatus === "awaiting_report"
155+
);
153156
const runningTasks = this.listAgentTaskWorkspaces(config).filter(
154157
(t) => t.taskStatus === "running"
155158
);
156159

160+
for (const task of awaitingReportTasks) {
161+
if (!task.id) continue;
162+
163+
// Avoid resuming a task while it still has active descendants (it shouldn't report yet).
164+
const hasActiveDescendants = this.hasActiveDescendantAgentTasks(config, task.id);
165+
if (hasActiveDescendants) {
166+
continue;
167+
}
168+
169+
// Restart-safety: if this task stream ends again without agent_report, fall back immediately.
170+
this.remindedAwaitingReport.add(task.id);
171+
172+
const model = task.taskModelString ?? defaultModel;
173+
const resumeResult = await this.workspaceService.resumeStream(task.id, {
174+
model,
175+
thinkingLevel: task.taskThinkingLevel,
176+
toolPolicy: [{ regex_match: "^agent_report$", action: "require" }],
177+
additionalSystemInstructions:
178+
"This task is awaiting its final agent_report. Call agent_report exactly once now.",
179+
});
180+
if (!resumeResult.success) {
181+
log.error("Failed to resume awaiting_report task on startup", {
182+
taskId: task.id,
183+
error: resumeResult.error,
184+
});
185+
186+
await this.fallbackReportMissingAgentReport({
187+
projectPath: task.projectPath,
188+
workspace: task,
189+
});
190+
}
191+
}
192+
157193
for (const task of runningTasks) {
158194
if (!task.id) continue;
159195
// Best-effort: if mux restarted mid-stream, nudge the agent to continue and report.

0 commit comments

Comments
 (0)