fix: harden compaction timeout follow-ups

This commit is contained in:
Ayaan Zaidi 2026-03-15 12:13:23 +05:30
parent f77a684131
commit 6a458ef29e
No known key found for this signature in database
5 changed files with 42 additions and 2 deletions

View File

@ -76,6 +76,20 @@ describe("compactWithSafetyTimeout", () => {
expect(onCancel).toHaveBeenCalledTimes(1); expect(onCancel).toHaveBeenCalledTimes(1);
expect(vi.getTimerCount()).toBe(0); expect(vi.getTimerCount()).toBe(0);
}); });
it("ignores onCancel errors and still rejects with the timeout", async () => {
vi.useFakeTimers();
const compactPromise = compactWithSafetyTimeout(() => new Promise<never>(() => {}), 30, {
onCancel: () => {
throw new Error("abortCompaction failed");
},
});
const timeoutAssertion = expect(compactPromise).rejects.toThrow("Compaction timed out");
await vi.advanceTimersByTimeAsync(30);
await timeoutAssertion;
expect(vi.getTimerCount()).toBe(0);
});
}); });
describe("resolveCompactionTimeoutMs", () => { describe("resolveCompactionTimeoutMs", () => {

View File

@ -37,7 +37,12 @@ export async function compactWithSafetyTimeout<T>(
return; return;
} }
canceled = true; canceled = true;
opts?.onCancel?.(); try {
opts?.onCancel?.();
} catch {
// Best-effort cancellation hook. Keep the timeout/abort path intact even
// if the underlying compaction cancel operation throws.
}
}; };
return await withTimeout( return await withTimeout(

View File

@ -132,6 +132,7 @@ import { flushPendingToolResultsAfterIdle } from "../wait-for-idle-before-flush.
import { waitForCompactionRetryWithAggregateTimeout } from "./compaction-retry-aggregate-timeout.js"; import { waitForCompactionRetryWithAggregateTimeout } from "./compaction-retry-aggregate-timeout.js";
import { import {
resolveRunTimeoutDuringCompaction, resolveRunTimeoutDuringCompaction,
resolveRunTimeoutWithCompactionGraceMs,
selectCompactionTimeoutSnapshot, selectCompactionTimeoutSnapshot,
shouldFlagCompactionTimeout, shouldFlagCompactionTimeout,
} from "./compaction-timeout.js"; } from "./compaction-timeout.js";
@ -1708,7 +1709,10 @@ export async function runEmbeddedAttempt(
const sessionLock = await acquireSessionWriteLock({ const sessionLock = await acquireSessionWriteLock({
sessionFile: params.sessionFile, sessionFile: params.sessionFile,
maxHoldMs: resolveSessionLockMaxHoldFromTimeout({ maxHoldMs: resolveSessionLockMaxHoldFromTimeout({
timeoutMs: params.timeoutMs, timeoutMs: resolveRunTimeoutWithCompactionGraceMs({
runTimeoutMs: params.timeoutMs,
compactionTimeoutMs: resolveCompactionTimeoutMs(params.config),
}),
}), }),
}); });

View File

@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import { castAgentMessage } from "../../test-helpers/agent-message-fixtures.js"; import { castAgentMessage } from "../../test-helpers/agent-message-fixtures.js";
import { import {
resolveRunTimeoutDuringCompaction, resolveRunTimeoutDuringCompaction,
resolveRunTimeoutWithCompactionGraceMs,
selectCompactionTimeoutSnapshot, selectCompactionTimeoutSnapshot,
shouldFlagCompactionTimeout, shouldFlagCompactionTimeout,
} from "./compaction-timeout.js"; } from "./compaction-timeout.js";
@ -62,6 +63,15 @@ describe("compaction-timeout helpers", () => {
).toBe("abort"); ).toBe("abort");
}); });
it("adds one compaction grace window to the run timeout budget", () => {
expect(
resolveRunTimeoutWithCompactionGraceMs({
runTimeoutMs: 600_000,
compactionTimeoutMs: 900_000,
}),
).toBe(1_500_000);
});
it("uses pre-compaction snapshot when compaction timeout occurs", () => { it("uses pre-compaction snapshot when compaction timeout occurs", () => {
const pre = [castAgentMessage({ role: "assistant", content: "pre" })] as const; const pre = [castAgentMessage({ role: "assistant", content: "pre" })] as const;
const current = [castAgentMessage({ role: "assistant", content: "current" })] as const; const current = [castAgentMessage({ role: "assistant", content: "current" })] as const;

View File

@ -24,6 +24,13 @@ export function resolveRunTimeoutDuringCompaction(params: {
return params.graceAlreadyUsed ? "abort" : "extend"; return params.graceAlreadyUsed ? "abort" : "extend";
} }
export function resolveRunTimeoutWithCompactionGraceMs(params: {
runTimeoutMs: number;
compactionTimeoutMs: number;
}): number {
return params.runTimeoutMs + params.compactionTimeoutMs;
}
export type SnapshotSelectionParams = { export type SnapshotSelectionParams = {
timedOutDuringCompaction: boolean; timedOutDuringCompaction: boolean;
preCompactionSnapshot: AgentMessage[] | null; preCompactionSnapshot: AgentMessage[] | null;