Skip to content

Commit 2f86ae7

Browse files
committed
fix(subagents): recover announce cleanup after kill/complete race
1 parent 604f22c commit 2f86ae7

File tree

3 files changed

+46
-0
lines changed

3 files changed

+46
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Docs: https://docs.openclaw.ai
3737
- Onboarding/API key input hardening: strip non-Latin1 Unicode artifacts from normalized secret input (while preserving Latin-1 content and internal spaces) so malformed copied API keys cannot trigger HTTP header `ByteString` construction crashes; adds regression coverage for shared normalization and MiniMax auth header usage. (#24496) Thanks @fa6maalassaf.
3838
- Kimi Coding/Anthropic tools compatibility: normalize `anthropic-messages` tool payloads to OpenAI-style `tools[].function` + compatible `tool_choice` when targeting Kimi Coding endpoints, restoring tool-call workflows that regressed after v2026.3.2. (#37038) Thanks @mochimochimochi-hub.
3939
- Heartbeat/workspace-path guardrails: append explicit workspace `HEARTBEAT.md` path guidance (and `docs/heartbeat.md` avoidance) to heartbeat prompts so heartbeat runs target workspace checklists reliably across packaged install layouts. (#37037) Thanks @stofancy.
40+
- Subagents/kill-complete announce race: when a late `subagent-complete` lifecycle event arrives after an earlier kill marker, clear stale kill suppression/cleanup flags and re-run announce cleanup so finished runs no longer get silently swallowed. (#37024) Thanks @cmfinlan.
4041
- Gateway/remote WS break-glass hostname support: honor `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1` for `ws://` hostname URLs (not only private IP literals) across onboarding validation and runtime gateway connection checks, while still rejecting public IP literals and non-unicast IPv6 endpoints. (#36930) Thanks @manju-rn.
4142
- Routing/binding lookup scalability: pre-index route bindings by channel/account and avoid full binding-list rescans on channel-account cache rollover, preventing multi-second `resolveAgentRoute` stalls in large binding configurations. (#36915) Thanks @songchenghao.
4243
- Browser/session cleanup: track browser tabs opened by session-scoped browser tool runs and close tracked tabs during `sessions.reset`/`sessions.delete` runtime cleanup, preventing orphaned tabs and unbounded browser memory growth after session teardown. (#36666) Thanks @Harnoor6693.

src/agents/subagent-registry.steer-restart.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,38 @@ describe("subagent registry steer restarts", () => {
447447
);
448448
});
449449

450+
it("recovers announce cleanup when completion arrives after a kill marker", async () => {
451+
const childSessionKey = "agent:main:subagent:kill-race";
452+
registerRun({
453+
runId: "run-kill-race",
454+
childSessionKey,
455+
task: "race test",
456+
});
457+
458+
expect(mod.markSubagentRunTerminated({ runId: "run-kill-race", reason: "manual kill" })).toBe(
459+
1,
460+
);
461+
expect(listMainRuns()[0]?.suppressAnnounceReason).toBe("killed");
462+
expect(listMainRuns()[0]?.cleanupHandled).toBe(true);
463+
expect(typeof listMainRuns()[0]?.cleanupCompletedAt).toBe("number");
464+
465+
emitLifecycleEnd("run-kill-race");
466+
await flushAnnounce();
467+
await flushAnnounce();
468+
469+
expect(announceSpy).toHaveBeenCalledTimes(1);
470+
const announce = (announceSpy.mock.calls[0]?.[0] ?? {}) as { childRunId?: string };
471+
expect(announce.childRunId).toBe("run-kill-race");
472+
473+
const run = listMainRuns()[0];
474+
expect(run?.endedReason).toBe("subagent-complete");
475+
expect(run?.outcome?.status).not.toBe("error");
476+
expect(run?.suppressAnnounceReason).toBeUndefined();
477+
expect(run?.cleanupHandled).toBe(true);
478+
expect(typeof run?.cleanupCompletedAt).toBe("number");
479+
expect(runSubagentEndedHookMock).toHaveBeenCalledTimes(1);
480+
});
481+
450482
it("retries deferred parent cleanup after a descendant announces", async () => {
451483
let parentAttempts = 0;
452484
announceSpy.mockImplementation(async (params: unknown) => {

src/agents/subagent-registry.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,19 @@ async function completeSubagentRun(params: {
338338
}
339339

340340
let mutated = false;
341+
// If a late lifecycle completion arrives after an earlier kill marker, allow
342+
// completion cleanup/announce to run instead of staying permanently suppressed.
343+
if (
344+
params.reason === SUBAGENT_ENDED_REASON_COMPLETE &&
345+
entry.suppressAnnounceReason === "killed" &&
346+
(entry.cleanupHandled || typeof entry.cleanupCompletedAt === "number")
347+
) {
348+
entry.suppressAnnounceReason = undefined;
349+
entry.cleanupHandled = false;
350+
entry.cleanupCompletedAt = undefined;
351+
mutated = true;
352+
}
353+
341354
const endedAt = typeof params.endedAt === "number" ? params.endedAt : Date.now();
342355
if (entry.endedAt !== endedAt) {
343356
entry.endedAt = endedAt;

0 commit comments

Comments
 (0)