Skip to content

Commit 7cf5cc8

Browse files
ly-wang19claude
andcommitted
fix(agents): reconcile stale restart-aborted subagent runs instead of resurrecting them
restore reconciliation only treated a run as stale-unended when abortedLastRun was not true, so restart-aborted runs were exempted from the shared liveness policy and auto-recovered no matter how old. After long gateway downtime this resurrected hours-old aborted subagent runs. Drop the exemption so the existing isStaleUnendedSubagentRun window (2h or run timeout+grace) applies to aborted runs too; fresh restart-aborted runs are not yet stale and still recover. Fixes #90766 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
1 parent f05e987 commit 7cf5cc8

2 files changed

Lines changed: 53 additions & 9 deletions

File tree

src/agents/subagent-registry-helpers.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,11 @@ export function resolveSubagentRunOrphanReason(params: {
190190
}
191191
if (
192192
params.includeStaleUnended === true &&
193-
sessionEntry.abortedLastRun !== true &&
194193
isStaleUnendedSubagentRun(params.entry, params.now)
195194
) {
195+
// Stale-unended pruning also covers restart-aborted runs: the shared liveness window
196+
// keeps fresh restart recovery (recent aborts are not yet stale) while a long-downtime
197+
// aborted run is reconciled instead of auto-resurrected after the gateway comes back (#90766).
196198
return "stale-unended-run";
197199
}
198200
return null;

src/agents/subagent-registry.persistence.test.ts

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -724,21 +724,63 @@ describe("subagent registry persistence", () => {
724724
expect(listSubagentRunsForRequester("agent:main:main")).toHaveLength(0);
725725
});
726726

727-
it("keeps stale unended restored runs with abortedLastRun for restart recovery", async () => {
727+
it("reconciles stale unended restored runs even when restart-aborted (#90766)", async () => {
728+
const now = Date.now();
729+
const runId = "run-stale-aborted-restore";
730+
const childSessionKey = "agent:main:subagent:stale-aborted-restore";
731+
const registryPath = await writePersistedRegistry(
732+
{
733+
version: 2,
734+
runs: {
735+
[runId]: {
736+
runId,
737+
childSessionKey,
738+
requesterSessionKey: "agent:main:main",
739+
requesterDisplayKey: "main",
740+
task: "stale restart-aborted work",
741+
cleanup: "keep",
742+
createdAt: now - 3 * 60 * 60 * 1_000,
743+
startedAt: now - 3 * 60 * 60 * 1_000,
744+
},
745+
},
746+
},
747+
{ seedChildSessions: false },
748+
);
749+
await writeChildSessionEntry({
750+
sessionKey: childSessionKey,
751+
sessionId: "sess-stale-aborted-restore",
752+
updatedAt: now,
753+
abortedLastRun: true,
754+
});
755+
756+
restartRegistry();
757+
await waitForRegistryWork(async () => {
758+
const after = JSON.parse(await fs.readFile(registryPath, "utf8")) as {
759+
runs?: Record<string, unknown>;
760+
};
761+
return after.runs?.[runId] === undefined;
762+
});
763+
764+
// Long-downtime aborted run is reconciled, not auto-resumed.
765+
expect(callGateway).not.toHaveBeenCalled();
766+
expect(listSubagentRunsForRequester("agent:main:main")).toHaveLength(0);
767+
});
768+
769+
it("recovers fresh restart-aborted runs that are not yet stale (#90766)", async () => {
728770
vi.mocked(callGateway).mockImplementationOnce(async (request) => {
729771
expectFields(request, {
730772
method: "agent.wait",
731773
});
732774
expectFields((request as { params?: unknown }).params, {
733-
runId: "run-stale-aborted-restore",
775+
runId: "run-fresh-aborted-restore",
734776
});
735777
return {
736778
status: "pending",
737779
};
738780
});
739781
const now = Date.now();
740-
const runId = "run-stale-aborted-restore";
741-
const childSessionKey = "agent:main:subagent:stale-aborted-restore";
782+
const runId = "run-fresh-aborted-restore";
783+
const childSessionKey = "agent:main:subagent:fresh-aborted-restore";
742784
await writePersistedRegistry(
743785
{
744786
version: 2,
@@ -748,18 +790,18 @@ describe("subagent registry persistence", () => {
748790
childSessionKey,
749791
requesterSessionKey: "agent:main:main",
750792
requesterDisplayKey: "main",
751-
task: "stale restart-recoverable work",
793+
task: "fresh restart-recoverable work",
752794
cleanup: "keep",
753-
createdAt: now - 3 * 60 * 60 * 1_000,
754-
startedAt: now - 3 * 60 * 60 * 1_000,
795+
createdAt: now - 60_000,
796+
startedAt: now - 60_000,
755797
},
756798
},
757799
},
758800
{ seedChildSessions: false },
759801
);
760802
await writeChildSessionEntry({
761803
sessionKey: childSessionKey,
762-
sessionId: "sess-stale-aborted-restore",
804+
sessionId: "sess-fresh-aborted-restore",
763805
updatedAt: now,
764806
abortedLastRun: true,
765807
});

0 commit comments

Comments
 (0)