Skip to content

Commit 7ea32f0

Browse files
committed
fix(tasks): keep diagnostics json-only and wedged-aware
1 parent 70b501b commit 7ea32f0

4 files changed

Lines changed: 44 additions & 4 deletions

File tree

src/commands/tasks.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
resetTaskRegistryDeliveryRuntimeForTests,
1212
resetTaskRegistryForTests,
1313
} from "../tasks/task-registry.js";
14+
import * as taskRegistryMaintenance from "../tasks/task-registry.maintenance.js";
1415
import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js";
1516
import type { OpenClawTestState } from "../test-utils/openclaw-test-state.js";
1617
import { tasksAuditCommand, tasksMaintenanceCommand } from "./tasks.js";
@@ -269,6 +270,20 @@ describe("tasks commands", () => {
269270
});
270271
});
271272

273+
it("does not build JSON-only diagnostics for text maintenance output", async () => {
274+
await withTaskCommandStateDir(async () => {
275+
const diagnosticsSpy = vi.spyOn(
276+
taskRegistryMaintenance,
277+
"getTaskRegistryMaintenanceDiagnostics",
278+
);
279+
const runtime = createRuntime();
280+
281+
await tasksMaintenanceCommand({ json: false, apply: false }, runtime);
282+
283+
expect(diagnosticsSpy).not.toHaveBeenCalled();
284+
});
285+
});
286+
272287
it("keeps tasks maintenance JSON additive for TaskFlow state", async () => {
273288
await withTaskCommandStateDir(async () => {
274289
const now = Date.now();

src/commands/tasks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -665,9 +665,9 @@ export async function tasksMaintenanceCommand(
665665
const taskMaintenance = opts.apply
666666
? await runTaskRegistryMaintenance()
667667
: previewTaskRegistryMaintenance();
668-
// Diagnostics explain the task-maintenance decision above, before the
668+
// JSON diagnostics explain the task-maintenance decision above, before the
669669
// separate session-registry sweep can prune backing session rows.
670-
const diagnostics = getTaskRegistryMaintenanceDiagnostics();
670+
const diagnostics = opts.json ? getTaskRegistryMaintenanceDiagnostics() : undefined;
671671
const flowMaintenance = opts.apply
672672
? await runTaskFlowRegistryMaintenance()
673673
: previewTaskFlowRegistryMaintenance();

src/tasks/task-registry.maintenance.issue-60299.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,10 +316,14 @@ describe("task-registry maintenance issue #60299", () => {
316316

317317
it("marks subagent tasks lost when their child session recovery is tombstoned", async () => {
318318
const childSessionKey = "agent:main:subagent:wedged-child";
319+
const staleAt = Date.now() - 45 * 60_000;
319320
const task = makeStaleTask({
320321
runtime: "subagent",
321322
runId: "run-wedged-child",
322323
childSessionKey,
324+
createdAt: staleAt,
325+
startedAt: staleAt,
326+
lastEventAt: staleAt,
323327
});
324328

325329
const { currentTasks } = createTaskRegistryMaintenanceHarness({
@@ -341,6 +345,14 @@ describe("task-registry maintenance issue #60299", () => {
341345
});
342346

343347
expectMaintenanceCounts(previewTaskRegistryMaintenance(), { reconciled: 1 });
348+
expect(getTaskRegistryMaintenanceDiagnostics().staleRunningTasks).toContainEqual(
349+
expect.objectContaining({
350+
taskId: task.taskId,
351+
decision: "would_reconcile",
352+
reason: "subagent_recovery_wedged",
353+
detail: "subagent orphan recovery blocked after 2 rapid accepted resume attempts",
354+
}),
355+
);
344356
expectMaintenanceCounts(await runTaskRegistryMaintenance(), { reconciled: 1 });
345357
const storedTask = requireTaskRecord(currentTasks, task.taskId);
346358
expect(storedTask.status).toBe("lost");

src/tasks/task-registry.maintenance.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,9 @@ export type TaskRegistryMaintenanceTaskDiagnostic = {
173173
| "backing_session_missing"
174174
| "backing_session_present"
175175
| "cron_runtime_not_authoritative"
176-
| "lost_grace_pending";
176+
| "lost_grace_pending"
177+
| "subagent_recovery_wedged";
178+
detail?: string;
177179
ageMs: number;
178180
childSessionKey?: string;
179181
runId?: string;
@@ -981,10 +983,20 @@ function explainActiveTaskRetention(params: {
981983
task: TaskRecord;
982984
now: number;
983985
context: BackingSessionLookupContext;
984-
}): Pick<TaskRegistryMaintenanceTaskDiagnostic, "decision" | "reason"> {
986+
}): Pick<TaskRegistryMaintenanceTaskDiagnostic, "decision" | "reason" | "detail"> {
985987
if (!hasLostGraceExpired(params.task, params.now)) {
986988
return { decision: "retained", reason: "lost_grace_pending" };
987989
}
990+
if (params.task.runtime === "subagent") {
991+
const entry = findTaskSessionEntry(params.task, params.context);
992+
if (entry && isSubagentRecoveryWedgedEntry(entry)) {
993+
return {
994+
decision: "would_reconcile",
995+
reason: "subagent_recovery_wedged",
996+
detail: formatSubagentRecoveryWedgedReason(entry),
997+
};
998+
}
999+
}
9881000
if (!hasBackingSession(params.task, params.context)) {
9891001
return { decision: "would_reconcile", reason: "backing_session_missing" };
9901002
}
@@ -1025,6 +1037,7 @@ export function getTaskRegistryMaintenanceDiagnostics(): TaskRegistryMaintenance
10251037
decision: decision.decision,
10261038
reason: decision.reason,
10271039
ageMs,
1040+
...(decision.detail ? { detail: decision.detail } : {}),
10281041
...(task.childSessionKey ? { childSessionKey: task.childSessionKey } : {}),
10291042
...(task.runId ? { runId: task.runId } : {}),
10301043
});

0 commit comments

Comments
 (0)