fix: ignore stale rows in fast abort

This commit is contained in:
Tak Hoffman
2026-03-24 17:52:28 -05:00
parent a03bbca4df
commit 1fd684329d
2 changed files with 81 additions and 1 deletions

View File

@@ -692,4 +692,72 @@ describe("abort detection", () => {
expect.objectContaining({ runId: "run-2", childSessionKey: depth2Key }),
);
});
it("cascade stop still traverses an ended current parent when a stale older active row exists", async () => {
subagentRegistryMocks.listSubagentRunsForRequester.mockClear();
subagentRegistryMocks.markSubagentRunTerminated.mockClear();
const sessionKey = "telegram:parent";
const depth1Key = "agent:main:subagent:child-ended-stale";
const depth2Key = "agent:main:subagent:child-ended-stale:subagent:grandchild-active";
const now = Date.now();
const { cfg } = await createAbortConfig({
nowMs: now,
sessionIdsByKey: {
[sessionKey]: "session-parent",
[depth1Key]: "session-child-ended-stale",
[depth2Key]: "session-grandchild-active",
},
});
subagentRegistryMocks.listSubagentRunsForRequester
.mockReturnValueOnce([
{
runId: "run-stale-parent",
childSessionKey: depth1Key,
requesterSessionKey: sessionKey,
requesterDisplayKey: "telegram:parent",
task: "stale orchestrator",
cleanup: "keep",
createdAt: now - 2_000,
startedAt: now - 1_900,
},
{
runId: "run-current-parent",
childSessionKey: depth1Key,
requesterSessionKey: sessionKey,
requesterDisplayKey: "telegram:parent",
task: "current orchestrator",
cleanup: "keep",
createdAt: now - 1_000,
startedAt: now - 900,
endedAt: now - 500,
outcome: { status: "ok" },
},
])
.mockReturnValueOnce([
{
runId: "run-active-child",
childSessionKey: depth2Key,
requesterSessionKey: depth1Key,
requesterDisplayKey: depth1Key,
task: "leaf worker",
cleanup: "keep",
createdAt: now - 400,
},
])
.mockReturnValueOnce([]);
const result = await runStopCommand({
cfg,
sessionKey,
from: "telegram:parent",
to: "telegram:parent",
});
expect(result.stoppedSubagents).toBe(1);
expectSessionLaneCleared(depth2Key);
expect(subagentRegistryMocks.markSubagentRunTerminated).toHaveBeenCalledWith(
expect.objectContaining({ runId: "run-active-child", childSessionKey: depth2Key }),
);
});
});

View File

@@ -5,6 +5,7 @@ import {
listSubagentRunsForController,
markSubagentRunTerminated,
} from "../../agents/subagent-registry.js";
import type { SubagentRunRecord } from "../../agents/subagent-registry.js";
import {
resolveInternalSessionKey,
resolveMainSessionAlias,
@@ -136,7 +137,18 @@ export function stopSubagentsForRequester(params: {
if (!requesterKey) {
return { stopped: 0 };
}
const runs = abortDeps.listSubagentRunsForController(requesterKey);
const dedupedRunsByChildKey = new Map<string, SubagentRunRecord>();
for (const run of abortDeps.listSubagentRunsForController(requesterKey)) {
const childKey = run.childSessionKey?.trim();
if (!childKey) {
continue;
}
const existing = dedupedRunsByChildKey.get(childKey);
if (!existing || run.createdAt >= existing.createdAt) {
dedupedRunsByChildKey.set(childKey, run);
}
}
const runs = Array.from(dedupedRunsByChildKey.values());
if (runs.length === 0) {
return { stopped: 0 };
}