fix(gateway): dedupe exec followup continuations (#82717)

Co-authored-by: Miya <miya@Miyas-Mac-mini.local>
This commit is contained in:
Zennn
2026-05-16 17:39:26 -04:00
committed by GitHub
parent 842e6f1643
commit 91f45d9c8a
7 changed files with 1229 additions and 745 deletions

View File

@@ -306,6 +306,35 @@ describe("startGatewayMaintenanceTimers", () => {
stopMaintenanceTimers(timers);
});
it("keeps active exec approval dedupe aliases past the normal ttl", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-03-22T00:00:00Z"));
const { startGatewayMaintenanceTimers } = await import("./server-maintenance.js");
const deps = createMaintenanceTimerDeps();
const now = Date.now();
const runId = "exec-approval-followup:req-active:nonce:retry-1";
deps.chatAbortControllers.set(runId, createActiveRun("agent:main:main", "agent"));
deps.dedupe.set("agent:exec-approval-followup:req-active", {
ts: now - DEDUPE_TTL_MS - 1,
ok: true,
payload: { runId, status: "accepted" },
});
deps.dedupe.set("agent:exec-approval-followup:req-stale", {
ts: now - DEDUPE_TTL_MS - 1,
ok: true,
payload: { runId: "exec-approval-followup:req-stale:nonce:retry-1", status: "accepted" },
});
const timers = startGatewayMaintenanceTimers(deps);
await vi.advanceTimersByTimeAsync(60_000);
expect(deps.dedupe.has("agent:exec-approval-followup:req-active")).toBe(true);
expect(deps.dedupe.has("agent:exec-approval-followup:req-stale")).toBe(false);
stopMaintenanceTimers(timers);
});
it("evicts dedupe overflow by oldest timestamp even after reinsertion", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-03-22T00:00:00Z"));

View File

@@ -88,11 +88,29 @@ export function startGatewayMaintenanceTimers(params: {
const dedupeCleanup = setInterval(() => {
const AGENT_RUN_SEQ_MAX = 10_000;
const now = Date.now();
const isActiveRunDedupeKey = (key: string) => {
const resolveDedupeRunId = (key: string, entry: DedupeEntry) => {
if (!key.startsWith("agent:") && !key.startsWith("chat:")) {
return undefined;
}
const keyRunId = key.slice(key.indexOf(":") + 1);
if (keyRunId) {
const directEntry = params.chatAbortControllers.get(keyRunId);
if (directEntry) {
return keyRunId;
}
}
const payload = entry.payload;
return payload && typeof payload === "object" && !Array.isArray(payload)
? typeof (payload as { runId?: unknown }).runId === "string"
? (payload as { runId: string }).runId.trim() || undefined
: undefined
: undefined;
};
const isActiveRunDedupeKey = (key: string, dedupeEntry: DedupeEntry) => {
if (!key.startsWith("agent:") && !key.startsWith("chat:")) {
return false;
}
const runId = key.slice(key.indexOf(":") + 1);
const runId = resolveDedupeRunId(key, dedupeEntry);
const entry = runId ? params.chatAbortControllers.get(runId) : undefined;
if (!entry) {
return false;
@@ -100,7 +118,7 @@ export function startGatewayMaintenanceTimers(params: {
return key.startsWith("agent:") ? entry.kind === "agent" : entry.kind !== "agent";
};
for (const [k, v] of params.dedupe) {
if (isActiveRunDedupeKey(k)) {
if (isActiveRunDedupeKey(k, v)) {
continue;
}
if (now - v.ts > DEDUPE_TTL_MS) {
@@ -110,7 +128,7 @@ export function startGatewayMaintenanceTimers(params: {
if (params.dedupe.size > DEDUPE_MAX) {
const excess = params.dedupe.size - DEDUPE_MAX;
const oldestKeys = [...params.dedupe.entries()]
.filter(([key]) => !isActiveRunDedupeKey(key))
.filter(([key, entry]) => !isActiveRunDedupeKey(key, entry))
.toSorted(([, left], [, right]) => left.ts - right.ts)
.slice(0, excess)
.map(([key]) => key);

View File

@@ -1696,6 +1696,239 @@ describe("gateway agent handler", () => {
expect(callArgs.bashElevated).toEqual(bashElevated);
});
it("dedupes elevated exec approval followups across nonce idempotency keys", async () => {
const bashElevated = {
enabled: true,
allowed: true,
defaultLevel: "on" as const,
};
const firstRegistration = registerExecApprovalFollowupRuntimeHandoff({
approvalId: "req-elevated-duplicate",
sessionKey: "agent:main:telegram:direct:123",
bashElevated,
});
const secondRegistration = registerExecApprovalFollowupRuntimeHandoff({
approvalId: "req-elevated-duplicate",
sessionKey: "agent:main:telegram:direct:123",
bashElevated,
});
if (!firstRegistration || !secondRegistration) {
throw new Error("expected runtime handoff ids");
}
mockMainSessionEntry({
sessionId: "existing-session-id",
lastChannel: "telegram",
lastTo: "123",
});
mocks.agentCommand.mockImplementation(() => new Promise(() => {}));
const context = makeContext();
const agentCommandCallsBefore = mocks.agentCommand.mock.calls.length;
await invokeAgent(
{
message: "exec followup",
sessionKey: "agent:main:telegram:direct:123",
channel: "telegram",
idempotencyKey: firstRegistration.idempotencyKey,
internalRuntimeHandoffId: firstRegistration.handoffId,
},
{ reqId: "exec-followup-duplicate-1", client: backendGatewayClient(), context },
);
expect(mocks.agentCommand).toHaveBeenCalledTimes(agentCommandCallsBefore + 1);
const secondRespond = await invokeAgent(
{
message: "exec followup duplicate",
sessionKey: "agent:main:telegram:direct:123",
channel: "telegram",
idempotencyKey: secondRegistration.idempotencyKey,
internalRuntimeHandoffId: secondRegistration.handoffId,
},
{
reqId: "exec-followup-duplicate-2",
client: backendGatewayClient(),
context,
flushDispatch: false,
},
);
await flushScheduledDispatchStep();
await flushScheduledDispatchStep();
expect(mocks.agentCommand).toHaveBeenCalledTimes(agentCommandCallsBefore + 1);
expect(mockCallArg(secondRespond, 0, 3)).toEqual({ cached: true });
});
it("reserves exec approval followup dedupe before awaited session work", async () => {
const bashElevated = {
enabled: true,
allowed: true,
defaultLevel: "on" as const,
};
const firstRegistration = registerExecApprovalFollowupRuntimeHandoff({
approvalId: "req-elevated-overlap",
sessionKey: "agent:main:telegram:direct:123",
bashElevated,
});
const secondRegistration = registerExecApprovalFollowupRuntimeHandoff({
approvalId: "req-elevated-overlap",
sessionKey: "agent:main:telegram:direct:123",
bashElevated,
});
if (!firstRegistration || !secondRegistration) {
throw new Error("expected runtime handoff ids");
}
mockMainSessionEntry({
sessionId: "existing-session-id",
lastChannel: "telegram",
lastTo: "123",
});
let releaseFirstSessionWrite: (() => void) | undefined;
let sessionWriteCalls = 0;
mocks.updateSessionStore.mockImplementation(async (_path, updater) => {
sessionWriteCalls += 1;
if (sessionWriteCalls === 1) {
await new Promise<void>((resolve) => {
releaseFirstSessionWrite = resolve;
});
}
const store = {
"agent:main:main": buildExistingMainStoreEntry({
lastChannel: "telegram",
lastTo: "123",
}),
};
return await updater(store);
});
mocks.agentCommand.mockImplementation(() => new Promise(() => {}));
const context = makeContext();
const agentCommandCallsBefore = mocks.agentCommand.mock.calls.length;
const first = invokeAgent(
{
message: "exec followup",
sessionKey: "agent:main:telegram:direct:123",
channel: "telegram",
idempotencyKey: firstRegistration.idempotencyKey,
internalRuntimeHandoffId: firstRegistration.handoffId,
},
{
reqId: "exec-followup-overlap-1",
client: backendGatewayClient(),
context,
flushDispatch: false,
},
);
await waitForAssertion(() => expect(sessionWriteCalls).toBe(1));
const secondRespond = await invokeAgent(
{
message: "exec followup duplicate",
sessionKey: "agent:main:telegram:direct:123",
channel: "telegram",
idempotencyKey: secondRegistration.idempotencyKey,
internalRuntimeHandoffId: secondRegistration.handoffId,
},
{
reqId: "exec-followup-overlap-2",
client: backendGatewayClient(),
context,
flushDispatch: false,
},
);
expect(mocks.agentCommand).toHaveBeenCalledTimes(agentCommandCallsBefore);
expect(sessionWriteCalls).toBe(1);
expect(mockCallArg(secondRespond, 0, 1)).toMatchObject({
runId: firstRegistration.idempotencyKey,
status: "accepted",
});
expect(mockCallArg(secondRespond, 0, 3)).toEqual({ cached: true });
releaseFirstSessionWrite?.();
await first;
await flushScheduledDispatchStep();
await flushScheduledDispatchStep();
expect(mocks.agentCommand).toHaveBeenCalledTimes(agentCommandCallsBefore + 1);
});
it("clears reserved exec approval dedupe when pre-run session work fails", async () => {
const bashElevated = {
enabled: true,
allowed: true,
defaultLevel: "on" as const,
};
const firstRegistration = registerExecApprovalFollowupRuntimeHandoff({
approvalId: "req-elevated-pre-run-fail",
sessionKey: "agent:main:telegram:direct:123",
bashElevated,
});
const secondRegistration = registerExecApprovalFollowupRuntimeHandoff({
approvalId: "req-elevated-pre-run-fail",
sessionKey: "agent:main:telegram:direct:123",
bashElevated,
});
if (!firstRegistration || !secondRegistration) {
throw new Error("expected runtime handoff ids");
}
mockMainSessionEntry({
sessionId: "existing-session-id",
lastChannel: "telegram",
lastTo: "123",
});
const context = makeContext();
const agentCommandCallsBefore = mocks.agentCommand.mock.calls.length;
mocks.updateSessionStore.mockRejectedValueOnce(new Error("session write failed"));
await expect(
invokeAgent(
{
message: "exec followup",
sessionKey: "agent:main:telegram:direct:123",
channel: "telegram",
idempotencyKey: firstRegistration.idempotencyKey,
internalRuntimeHandoffId: firstRegistration.handoffId,
},
{
reqId: "exec-followup-pre-run-fail-1",
client: backendGatewayClient(),
context,
flushDispatch: false,
},
),
).rejects.toThrow("session write failed");
expect(context.dedupe.get(`agent:${firstRegistration.idempotencyKey}`)).toBeUndefined();
expect(
context.dedupe.get("agent:exec-approval-followup:req-elevated-pre-run-fail"),
).toBeUndefined();
expect(mocks.agentCommand).toHaveBeenCalledTimes(agentCommandCallsBefore);
const secondRespond = await invokeAgent(
{
message: "exec followup retry",
sessionKey: "agent:main:telegram:direct:123",
channel: "telegram",
idempotencyKey: secondRegistration.idempotencyKey,
internalRuntimeHandoffId: secondRegistration.handoffId,
},
{
reqId: "exec-followup-pre-run-fail-2",
client: backendGatewayClient(),
context,
flushDispatch: false,
},
);
expect(mockCallArg(secondRespond, 0, 1)).toMatchObject({
runId: secondRegistration.idempotencyKey,
status: "accepted",
});
await flushScheduledDispatchStep();
await flushScheduledDispatchStep();
expect(mocks.agentCommand).toHaveBeenCalledTimes(agentCommandCallsBefore + 1);
});
it("does not consume exec approval runtime handoffs from non-backend callers", async () => {
const bashElevated = {
enabled: true,

File diff suppressed because it is too large Load Diff