diff --git a/src/agents/embedded-agent-runner/run/attempt-abort.test.ts b/src/agents/embedded-agent-runner/run/attempt-abort.test.ts new file mode 100644 index 00000000000..b30ed1aca0b --- /dev/null +++ b/src/agents/embedded-agent-runner/run/attempt-abort.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it, vi } from "vitest"; +import { releaseEmbeddedAttemptSessionLockForAbort } from "./attempt-abort.js"; + +describe("releaseEmbeddedAttemptSessionLockForAbort", () => { + it("releases the retained session lock for manual aborts", async () => { + const releaseHeldLockForAbort = vi.fn(async () => {}); + const warn = vi.fn(); + + releaseEmbeddedAttemptSessionLockForAbort({ + sessionLockController: { releaseHeldLockForAbort }, + log: { warn }, + runId: "run-manual", + abortKind: "abort", + }); + + await Promise.resolve(); + + expect(releaseHeldLockForAbort).toHaveBeenCalledTimes(1); + expect(warn).not.toHaveBeenCalled(); + }); + + it("logs release failures without throwing from the abort path", async () => { + const releaseError = new Error("locked"); + const releaseHeldLockForAbort = vi.fn(async () => { + throw releaseError; + }); + const warn = vi.fn(); + + releaseEmbeddedAttemptSessionLockForAbort({ + sessionLockController: { releaseHeldLockForAbort }, + log: { warn }, + runId: "run-timeout", + abortKind: "timeout abort", + }); + + await Promise.resolve(); + await Promise.resolve(); + + expect(releaseHeldLockForAbort).toHaveBeenCalledTimes(1); + expect(warn).toHaveBeenCalledWith( + "failed to release session lock on timeout abort: runId=run-timeout Error: locked", + ); + }); +}); diff --git a/src/agents/embedded-agent-runner/run/attempt-abort.ts b/src/agents/embedded-agent-runner/run/attempt-abort.ts new file mode 100644 index 00000000000..db44dfc0e61 --- /dev/null +++ b/src/agents/embedded-agent-runner/run/attempt-abort.ts @@ -0,0 +1,18 @@ +import type { EmbeddedAttemptSessionLockController } from "./attempt.session-lock.js"; + +type AbortLockReleaseLog = { + warn(message: string): void; +}; + +export function releaseEmbeddedAttemptSessionLockForAbort(params: { + sessionLockController: Pick; + log: AbortLockReleaseLog; + runId: string; + abortKind: "abort" | "timeout abort"; +}): void { + void params.sessionLockController.releaseHeldLockForAbort().catch((err) => { + params.log.warn( + `failed to release session lock on ${params.abortKind}: runId=${params.runId} ${String(err)}`, + ); + }); +} diff --git a/src/agents/embedded-agent-runner/run/attempt.ts b/src/agents/embedded-agent-runner/run/attempt.ts index 4899efdb26c..c7a765e7d2d 100644 --- a/src/agents/embedded-agent-runner/run/attempt.ts +++ b/src/agents/embedded-agent-runner/run/attempt.ts @@ -308,6 +308,7 @@ import { rotateTranscriptAfterCompaction, shouldRotateCompactionTranscript, } from "../compaction-successor-transcript.js"; +import { releaseEmbeddedAttemptSessionLockForAbort } from "./attempt-abort.js"; import { resolveAttemptWorkspaceBootstrapRouting } from "./attempt-bootstrap-routing.js"; import { configureEmbeddedAttemptHttpRuntime } from "./attempt-http-runtime.js"; import { @@ -2984,12 +2985,13 @@ export async function runEmbeddedAttempt( sessionFile: params.sessionFile, reason: "timeout", }); - void sessionLockController.releaseHeldLockForAbort().catch((err) => { - log.warn( - `failed to release session lock on timeout abort: runId=${params.runId} ${String(err)}`, - ); - }); } + releaseEmbeddedAttemptSessionLockForAbort({ + sessionLockController, + log, + runId: params.runId, + abortKind: isTimeout ? "timeout abort" : "abort", + }); }; abortRunForExternalSignal = abortRun; const idleTimeoutTrigger: ((error: Error) => void) | undefined = (error) => {