Skip to content

Commit f5fa846

Browse files
committed
fix(agents): clear yielded subagent grace timers
1 parent 437b7c3 commit f5fa846

3 files changed

Lines changed: 67 additions & 1 deletion

File tree

src/agents/subagent-registry-run-manager.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ export function createSubagentRunManager(params: {
180180
stopSweeper(): void;
181181
resumeSubagentRun(runId: string): void;
182182
clearPendingLifecycleError(runId: string): void;
183+
clearPendingLifecycleTimeout(runId: string): void;
183184
resolveSubagentWaitTimeoutMs(cfg: OpenClawConfig, runTimeoutSeconds?: number): number;
184185
scheduleOrphanRecovery(args?: { delayMs?: number; maxRetries?: number }): void;
185186
resolveSubagentSessionCompletion(args: {
@@ -264,6 +265,8 @@ export function createSubagentRunManager(params: {
264265
waitTerminalOutcome?.reason === "aborted" || waitTerminalOutcome?.reason === "cancelled";
265266
const waitStatus = waitTerminalOutcome?.status ?? wait.status;
266267
if (wait.yielded === true && waitStatus !== "timeout" && !waitBlocked) {
268+
params.clearPendingLifecycleError(runId);
269+
params.clearPendingLifecycleTimeout(runId);
267270
if (
268271
markSubagentRunPausedAfterYield({
269272
entry,

src/agents/subagent-registry.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2117,6 +2117,68 @@ describe("subagent registry seam flow", () => {
21172117
expect(mocks.runSubagentAnnounceFlow).not.toHaveBeenCalled();
21182118
});
21192119

2120+
it("cancels a pending grace timer when agent.wait observes the yield after an aborted terminal (#92448)", async () => {
2121+
let resolveWait: (value: {
2122+
status: "ok";
2123+
startedAt: number;
2124+
endedAt: number;
2125+
yielded: true;
2126+
}) => void = () => {};
2127+
const waitResult = new Promise<{
2128+
status: "ok";
2129+
startedAt: number;
2130+
endedAt: number;
2131+
yielded: true;
2132+
}>((resolve) => {
2133+
resolveWait = resolve;
2134+
});
2135+
mocks.callGateway.mockImplementation(async (request: { method?: string }) => {
2136+
if (request.method === "agent.wait") {
2137+
return waitResult;
2138+
}
2139+
return {};
2140+
});
2141+
2142+
mod.registerSubagentRun({
2143+
runId: "run-wait-yield-after-pending-timeout",
2144+
childSessionKey: "agent:main:subagent:pending-wait-timeout",
2145+
requesterSessionKey: "agent:main:main",
2146+
requesterDisplayKey: "main",
2147+
task: "wait for child continuation through wait",
2148+
cleanup: "keep",
2149+
});
2150+
2151+
const lastOnAgentEventCall = mocks.onAgentEvent.mock.calls[
2152+
mocks.onAgentEvent.mock.calls.length - 1
2153+
] as unknown as
2154+
| [(evt: { runId: string; stream: string; data: Record<string, unknown> }) => void]
2155+
| undefined;
2156+
const lifecycleHandler = lastOnAgentEventCall?.[0];
2157+
expect(lifecycleHandler).toBeTypeOf("function");
2158+
2159+
lifecycleHandler?.({
2160+
runId: "run-wait-yield-after-pending-timeout",
2161+
stream: "lifecycle",
2162+
data: { phase: "end", startedAt: 111, endedAt: 222, aborted: true },
2163+
});
2164+
resolveWait({ status: "ok", startedAt: 111, endedAt: 333, yielded: true });
2165+
2166+
await waitForFast(() => {
2167+
const run = mod
2168+
.listSubagentRunsForRequester("agent:main:main")
2169+
.find((entry) => entry.runId === "run-wait-yield-after-pending-timeout");
2170+
expect(run?.pauseReason).toBe("sessions_yield");
2171+
});
2172+
2173+
await vi.advanceTimersByTimeAsync(60_000);
2174+
const run = mod
2175+
.listSubagentRunsForRequester("agent:main:main")
2176+
.find((entry) => entry.runId === "run-wait-yield-after-pending-timeout");
2177+
expect(run?.pauseReason).toBe("sessions_yield");
2178+
expect(run?.outcome?.status).not.toBe("timeout");
2179+
expect(mocks.runSubagentAnnounceFlow).not.toHaveBeenCalled();
2180+
});
2181+
21202182
it("announces blocked agent.wait snapshots as errors instead of success", async () => {
21212183
mocks.callGateway.mockImplementation(async (request: { method?: string }) => {
21222184
if (request.method === "agent.wait") {

src/agents/subagent-registry.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ function schedulePendingLifecycleTimeout(params: {
480480
if (!entry) {
481481
return;
482482
}
483-
if (entry.outcome?.status === "ok") {
483+
if (entry.outcome?.status === "ok" || entry.pauseReason === "sessions_yield") {
484484
return;
485485
}
486486
const completionParams = {
@@ -1210,6 +1210,7 @@ const subagentRunManager = createSubagentRunManager({
12101210
stopSweeper,
12111211
resumeSubagentRun,
12121212
clearPendingLifecycleError,
1213+
clearPendingLifecycleTimeout,
12131214
resolveSubagentWaitTimeoutMs,
12141215
scheduleOrphanRecovery: (args) => scheduleSubagentOrphanRecovery(args),
12151216
resolveSubagentSessionCompletion,

0 commit comments

Comments
 (0)