Skip to content

Commit 153ee2a

Browse files
fix(subagents): killed subagent runs stay running in the task list (#99806)
* fix(subagents): reconcile killed task outcomes Co-authored-by: masatohoshino <[email protected]> * fix(subagents): retain reconciliation marker narrowing * fix(subagents): honor generations in latest-run views * test(subagents): align reconciliation fixtures * fix(subagents): keep steer recovery best effort * style(tasks): use type-only runtime contract import * refactor(subagents): keep generation ordering leaf-only * fix(subagents): sanitize persisted task owner ids * fix(subagents): preserve failed replay lifecycle reason * fix(agents): clear killed lifecycle timeout grace * fix(tasks): preserve canonical subagent outcomes * fix(agents): continue requester-wide cancellation * fix(agents): fence yield revival races * test(agents): mock detached task lookup * fix: fence subagent cleanup deletion --------- Co-authored-by: Peter Steinberger <[email protected]>
1 parent 9c5ea29 commit 153ee2a

46 files changed

Lines changed: 8523 additions & 572 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/agents/openclaw-tools.subagents.sessions-spawn.test-harness.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ vi.mock("../tasks/detached-task-runtime.js", () => ({
395395
completeTaskRunByRunId: vi.fn(),
396396
createRunningTaskRun: vi.fn(),
397397
failTaskRunByRunId: vi.fn(),
398+
findDetachedTaskRun: vi.fn(() => ({ lookup: "available" as const })),
398399
setDetachedTaskDeliveryStatusByRunId: vi.fn(),
399400
}));
400401

src/agents/subagent-announce-output.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
applySubagentWaitOutcome,
77
buildCompactAnnounceStatsLine,
88
buildChildCompletionFindings,
9+
dedupeLatestChildCompletionRows,
910
readSubagentOutput,
1011
} from "./subagent-announce-output.js";
1112

@@ -63,6 +64,22 @@ function sessionsYieldTurn(message = "Waiting for subagent completion.") {
6364
];
6465
}
6566

67+
describe("dedupeLatestChildCompletionRows", () => {
68+
it("prefers the newer generation when child runs share a creation timestamp", () => {
69+
const childSessionKey = "agent:main:subagent:reused";
70+
const older = {
71+
runId: "run-older",
72+
generation: 1,
73+
childSessionKey,
74+
task: "older",
75+
createdAt: 1_000,
76+
};
77+
const newer = { ...older, runId: "run-newer", generation: 2, task: "newer" };
78+
79+
expect(dedupeLatestChildCompletionRows([older, newer])).toStrictEqual([newer]);
80+
});
81+
});
82+
6683
describe("buildCompactAnnounceStatsLine", () => {
6784
afterEach(() => {
6885
testing.setDepsForTest();

src/agents/subagent-announce-output.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
resolveAgentIdFromSessionKey,
2020
resolveStorePath,
2121
} from "./subagent-announce.runtime.js";
22+
import { compareSubagentRunGeneration } from "./subagent-run-generation.js";
2223
import { assistantCallsSessionsYield, isSessionsYieldToolResult } from "./subagent-yield-output.js";
2324
import { extractAssistantText, sanitizeTextContent } from "./tools/chat-history-text.js";
2425
import { isAnnounceSkip } from "./tools/sessions-send-tokens.js";
@@ -416,9 +417,11 @@ export function buildChildCompletionFindings(
416417

417418
export function dedupeLatestChildCompletionRows(
418419
children: Array<{
420+
runId: string;
419421
childSessionKey: string;
420422
task: string;
421423
label?: string;
424+
generation?: number;
422425
createdAt: number;
423426
endedAt?: number;
424427
frozenResultText?: string | null;
@@ -438,7 +441,7 @@ export function dedupeLatestChildCompletionRows(
438441
const latestByChildSessionKey = new Map<string, (typeof children)[number]>();
439442
for (const child of children) {
440443
const existing = latestByChildSessionKey.get(child.childSessionKey);
441-
if (!existing || child.createdAt > existing.createdAt) {
444+
if (!existing || compareSubagentRunGeneration(child, existing) > 0) {
442445
latestByChildSessionKey.set(child.childSessionKey, child);
443446
}
444447
}

src/agents/subagent-announce.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,25 @@ describe("subagent announce seam flow", () => {
336336
});
337337
});
338338

339+
it("skips delete cleanup when the lifecycle owner invalidates the attempt", async () => {
340+
const didAnnounce = await runSubagentAnnounceFlow({
341+
childSessionKey: "agent:main:subagent:test",
342+
childRunId: "run-invalidated-delete",
343+
requesterSessionKey: "agent:main:main",
344+
requesterDisplayKey: "main",
345+
task: "do thing",
346+
timeoutMs: 10,
347+
cleanup: "delete",
348+
waitForCompletion: false,
349+
outcome: { status: "ok" },
350+
roundOneReply: "ANNOUNCE_SKIP",
351+
onBeforeDeleteChildSession: () => false,
352+
});
353+
354+
expect(didAnnounce).toBe(true);
355+
expect(sessionsDeleteSpy).not.toHaveBeenCalled();
356+
});
357+
339358
it("warns when ANNOUNCE_SKIP suppresses a cron job completion", async () => {
340359
const logSpy = vi.spyOn(defaultRuntime, "log").mockImplementation(() => {});
341360

src/agents/subagent-announce.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ export async function runSubagentAnnounceFlow(params: {
261261
signal?: AbortSignal;
262262
bestEffortDeliver?: boolean;
263263
onDeliveryResult?: (delivery: SubagentAnnounceDeliveryResult) => void;
264+
onBeforeDeleteChildSession?: () => boolean;
264265
}): Promise<boolean> {
265266
let didAnnounce = false;
266267
const expectsCompletionMessage = params.expectsCompletionMessage === true;
@@ -619,7 +620,7 @@ export async function runSubagentAnnounceFlow(params: {
619620
// Best-effort
620621
}
621622
}
622-
if (shouldDeleteChildSession) {
623+
if (shouldDeleteChildSession && (params.onBeforeDeleteChildSession?.() ?? true)) {
623624
await deleteSubagentSessionForCleanup({
624625
callGateway: subagentAnnounceDeps.callGateway,
625626
childSessionKey: params.childSessionKey,

0 commit comments

Comments
 (0)