Skip to content

Commit eeff9c4

Browse files
committed
fix: release session lock on user abort
1 parent 9518d1f commit eeff9c4

2 files changed

Lines changed: 79 additions & 5 deletions

File tree

src/agents/embedded-agent-runner/run/attempt.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2976,6 +2976,9 @@ export async function runEmbeddedAttempt(
29762976
}
29772977
abortCompaction();
29782978
void abortActiveSession();
2979+
void sessionLockController.releaseHeldLockForAbort().catch((err) => {
2980+
log.warn(`failed to release session lock on abort: runId=${params.runId} ${String(err)}`);
2981+
});
29792982
if (isTimeout && queueHandleForAbandonment) {
29802983
markActiveEmbeddedRunAbandoned({
29812984
sessionId: params.sessionId,
@@ -2984,11 +2987,6 @@ export async function runEmbeddedAttempt(
29842987
sessionFile: params.sessionFile,
29852988
reason: "timeout",
29862989
});
2987-
void sessionLockController.releaseHeldLockForAbort().catch((err) => {
2988-
log.warn(
2989-
`failed to release session lock on timeout abort: runId=${params.runId} ${String(err)}`,
2990-
);
2991-
});
29922990
}
29932991
};
29942992
abortRunForExternalSignal = abortRun;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
import {
3+
cleanupTempPaths,
4+
createContextEngineAttemptRunner,
5+
createContextEngineBootstrapAndAssemble,
6+
createDefaultEmbeddedSession,
7+
getHoisted,
8+
resetEmbeddedAttemptHarness,
9+
} from "./attempt.spawn-workspace.test-support.js";
10+
11+
const tempPaths: string[] = [];
12+
13+
beforeEach(() => {
14+
resetEmbeddedAttemptHarness();
15+
});
16+
17+
afterEach(async () => {
18+
await cleanupTempPaths(tempPaths);
19+
});
20+
21+
describe("embedded attempt user abort lock release", () => {
22+
it("releases the retained session lock before cleanup after user abort", async () => {
23+
const hoisted = getHoisted();
24+
const lockEvents: string[] = [];
25+
let lockCount = 0;
26+
hoisted.acquireSessionWriteLockMock.mockReset().mockImplementation(async () => {
27+
lockCount += 1;
28+
const lockId = lockCount;
29+
lockEvents.push(`acquire-${lockId}`);
30+
return {
31+
release: async () => {
32+
lockEvents.push(`release-${lockId}`);
33+
},
34+
};
35+
});
36+
37+
let markPromptStarted!: () => void;
38+
const promptStarted = new Promise<void>((resolve) => {
39+
markPromptStarted = resolve;
40+
});
41+
let settlePrompt!: () => void;
42+
const promptCanSettle = new Promise<void>((resolve) => {
43+
settlePrompt = resolve;
44+
});
45+
const abortController = new AbortController();
46+
const contextEngine = createContextEngineBootstrapAndAssemble();
47+
48+
const run = createContextEngineAttemptRunner({
49+
contextEngine,
50+
sessionKey: "agent:main:user-abort-lock",
51+
tempPaths,
52+
createSession: () =>
53+
createDefaultEmbeddedSession({
54+
prompt: async () => {
55+
markPromptStarted();
56+
await promptCanSettle;
57+
},
58+
}),
59+
attemptOverrides: {
60+
abortSignal: abortController.signal,
61+
},
62+
});
63+
64+
await promptStarted;
65+
abortController.abort(new Error("user abort"));
66+
settlePrompt();
67+
68+
await run;
69+
70+
const retainedReleaseIndex = lockEvents.indexOf("release-1");
71+
const cleanupAcquireIndex = lockEvents.indexOf("acquire-2");
72+
expect(retainedReleaseIndex).toBeGreaterThan(-1);
73+
expect(cleanupAcquireIndex).toBeGreaterThan(-1);
74+
expect(retainedReleaseIndex).toBeLessThan(cleanupAcquireIndex);
75+
});
76+
});

0 commit comments

Comments
 (0)