Skip to content

Commit 310c48a

Browse files
authored
Merge 50f00ea into 5a64727
2 parents 5a64727 + 50f00ea commit 310c48a

6 files changed

Lines changed: 1193 additions & 28 deletions

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,23 @@ describe("subagent announce timeout config", () => {
475475
expect(internalEvents[0]?.result).not.toContain("data");
476476
});
477477

478+
it("keeps delete-mode timeout retryable while the embedded child request is still active", async () => {
479+
sessionStore["agent:main:subagent:worker"] = {
480+
sessionId: "child-session",
481+
};
482+
isEmbeddedAgentRunActiveMock.mockReturnValue(true);
483+
waitForEmbeddedAgentRunEndMock.mockResolvedValue(false);
484+
485+
const didAnnounce = await runAnnounceFlowForTest("run-timeout-delete-still-active", {
486+
cleanup: "delete",
487+
outcome: { status: "timeout" },
488+
roundOneReply: undefined,
489+
});
490+
491+
expect(didAnnounce).toBe(false);
492+
expect(findFinalDirectAgentCall()).toBeUndefined();
493+
});
494+
478495
it("does not announce cached reply text when the child run terminally failed", async () => {
479496
chatHistoryMessages = [
480497
{ role: "assistant", content: [{ type: "text", text: "stale history output" }] },

src/agents/subagent-announce.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,10 @@ export async function runSubagentAnnounceFlow(params: {
273273
const settled = await waitForEmbeddedAgentRunEnd(childSessionId, settleTimeoutMs);
274274
if (!settled && isEmbeddedAgentRunActive(childSessionId)) {
275275
shouldDeleteChildSession = false;
276-
return false;
276+
// Keep delete cleanup retryable until the active child can be removed.
277+
if (outcome?.status !== "timeout" || params.cleanup === "delete") {
278+
return false;
279+
}
277280
}
278281
}
279282

src/agents/subagent-registry-lifecycle.ts

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,53 @@ async function loadCleanupBrowserSessionsForLifecycleEnd(): Promise<
7575
return (await browserCleanupLoader.load()).cleanupBrowserSessionsForLifecycleEnd;
7676
}
7777

78+
function resolveSubagentRunDeadlineMs(
79+
entry: SubagentRunRecord,
80+
observedStartedAt?: number,
81+
): number | undefined {
82+
const timeoutSeconds = entry.runTimeoutSeconds;
83+
if (
84+
typeof timeoutSeconds !== "number" ||
85+
!Number.isFinite(timeoutSeconds) ||
86+
timeoutSeconds <= 0
87+
) {
88+
return undefined;
89+
}
90+
const startedAt =
91+
typeof observedStartedAt === "number" && Number.isFinite(observedStartedAt)
92+
? observedStartedAt
93+
: typeof entry.startedAt === "number" && Number.isFinite(entry.startedAt)
94+
? entry.startedAt
95+
: entry.createdAt;
96+
return Number.isFinite(startedAt) ? startedAt + Math.floor(timeoutSeconds * 1000) : undefined;
97+
}
98+
99+
function shouldPreserveExplicitRunTimeout(params: { entry: SubagentRunRecord }): boolean {
100+
if (params.entry.outcome?.status !== "timeout" || typeof params.entry.endedAt !== "number") {
101+
return false;
102+
}
103+
if (
104+
params.entry.cleanupHandled ||
105+
typeof params.entry.cleanupCompletedAt === "number" ||
106+
typeof params.entry.endedHookEmittedAt === "number" ||
107+
params.entry.delivery?.status === "delivered" ||
108+
typeof params.entry.delivery?.announcedAt === "number"
109+
) {
110+
return true;
111+
}
112+
return false;
113+
}
114+
115+
function resolveExpiredExplicitRunDeadlineMs(params: {
116+
entry: SubagentRunRecord;
117+
nextOutcome: SubagentRunOutcome;
118+
nextEndedAt: number;
119+
observedStartedAt?: number;
120+
}): number | undefined {
121+
const deadlineMs = resolveSubagentRunDeadlineMs(params.entry, params.observedStartedAt);
122+
return deadlineMs !== undefined && params.nextEndedAt > deadlineMs ? deadlineMs : undefined;
123+
}
124+
78125
export function createSubagentRegistryLifecycleController(params: {
79126
runs: Map<string, SubagentRunRecord>;
80127
resumedRuns: Set<string>;
@@ -1016,6 +1063,7 @@ export function createSubagentRegistryLifecycleController(params: {
10161063
sendFarewell?: boolean;
10171064
accountId?: string;
10181065
triggerCleanup: boolean;
1066+
startedAt?: number;
10191067
}) => {
10201068
params.clearPendingLifecycleError(completeParams.runId);
10211069
const entry = params.runs.get(completeParams.runId);
@@ -1036,8 +1084,40 @@ export function createSubagentRegistryLifecycleController(params: {
10361084
mutated = true;
10371085
}
10381086

1039-
const endedAt =
1040-
typeof completeParams.endedAt === "number" ? completeParams.endedAt : Date.now();
1087+
let endedAt = typeof completeParams.endedAt === "number" ? completeParams.endedAt : Date.now();
1088+
let completionOutcome = completeParams.outcome;
1089+
let completionReason = completeParams.reason;
1090+
if (
1091+
shouldPreserveExplicitRunTimeout({
1092+
entry,
1093+
})
1094+
) {
1095+
return;
1096+
}
1097+
1098+
const observedStartedAt =
1099+
typeof completeParams.startedAt === "number" && Number.isFinite(completeParams.startedAt)
1100+
? completeParams.startedAt
1101+
: undefined;
1102+
if (observedStartedAt !== undefined && entry.startedAt !== observedStartedAt) {
1103+
entry.startedAt = observedStartedAt;
1104+
if (typeof entry.sessionStartedAt !== "number") {
1105+
entry.sessionStartedAt = observedStartedAt;
1106+
}
1107+
mutated = true;
1108+
}
1109+
1110+
const expiredDeadlineMs = resolveExpiredExplicitRunDeadlineMs({
1111+
entry,
1112+
nextOutcome: completionOutcome,
1113+
nextEndedAt: endedAt,
1114+
observedStartedAt,
1115+
});
1116+
if (expiredDeadlineMs !== undefined) {
1117+
endedAt = expiredDeadlineMs;
1118+
completionOutcome = { status: "timeout" };
1119+
completionReason = SUBAGENT_ENDED_REASON_COMPLETE;
1120+
}
10411121
if (entry.endedAt !== endedAt) {
10421122
entry.endedAt = endedAt;
10431123
entry.execution = {
@@ -1048,7 +1128,7 @@ export function createSubagentRegistryLifecycleController(params: {
10481128
};
10491129
mutated = true;
10501130
}
1051-
const outcome = withSubagentOutcomeTiming(completeParams.outcome, {
1131+
const outcome = withSubagentOutcomeTiming(completionOutcome, {
10521132
startedAt: entry.startedAt,
10531133
endedAt,
10541134
});
@@ -1070,8 +1150,8 @@ export function createSubagentRegistryLifecycleController(params: {
10701150
};
10711151
mutated = true;
10721152
}
1073-
if (entry.endedReason !== completeParams.reason) {
1074-
entry.endedReason = completeParams.reason;
1153+
if (entry.endedReason !== completionReason) {
1154+
entry.endedReason = completionReason;
10751155
mutated = true;
10761156
}
10771157
if (entry.pauseReason !== undefined) {
@@ -1114,7 +1194,7 @@ export function createSubagentRegistryLifecycleController(params: {
11141194
!suppressedForSteerRestart &&
11151195
params.shouldEmitEndedHookForRun({
11161196
entry,
1117-
reason: completeParams.reason,
1197+
reason: completionReason,
11181198
});
11191199
const shouldDeferEndedHook =
11201200
shouldEmitEndedHook &&
@@ -1124,7 +1204,7 @@ export function createSubagentRegistryLifecycleController(params: {
11241204
if (!shouldDeferEndedHook && shouldEmitEndedHook) {
11251205
await params.emitSubagentEndedHookForRun({
11261206
entry,
1127-
reason: completeParams.reason,
1207+
reason: completionReason,
11281208
sendFarewell: completeParams.sendFarewell,
11291209
accountId: completeParams.accountId,
11301210
});

0 commit comments

Comments
 (0)