From 4c6dd7ed6e560aa8149aaa00c39f45f3f36253d4 Mon Sep 17 00:00:00 2001 From: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 11:42:53 +0000 Subject: [PATCH] fix(clawsweeper): address review for automerge-openclaw-openclaw-82891 (2) --- .../run/attempt.session-lock.test.ts | 40 +++++++++++++++++++ .../run/attempt.session-lock.ts | 17 ++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/agents/pi-embedded-runner/run/attempt.session-lock.test.ts b/src/agents/pi-embedded-runner/run/attempt.session-lock.test.ts index 615c7983f8a..12d550dc822 100644 --- a/src/agents/pi-embedded-runner/run/attempt.session-lock.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.session-lock.test.ts @@ -78,6 +78,46 @@ describe("embedded attempt session lock lifecycle", () => { expect(events).toEqual(["prep-release", "post-write", "post-release"]); }); + it("reuses its active post-prompt lock for nested session writes", async () => { + const events: string[] = []; + const sessionFile = await createTempSessionFile(); + const acquireSessionWriteLock = vi + .fn() + .mockResolvedValueOnce({ release: vi.fn(async () => events.push("prep-release")) }) + .mockResolvedValueOnce({ release: vi.fn(async () => events.push("post-release")) }) + .mockRejectedValueOnce( + new SessionWriteLockTimeoutError({ + timeoutMs: lockOptions.timeoutMs, + owner: "pid=789", + lockPath: `${sessionFile}.lock`, + }), + ); + + const controller = await createEmbeddedAttemptSessionLockController({ + acquireSessionWriteLock, + lockOptions: { ...lockOptions, sessionFile }, + }); + + await controller.releaseForPrompt(); + await controller.withSessionWriteLock(async () => { + events.push("outer-start"); + await fs.appendFile(sessionFile, '{"type":"message","id":"local"}\n', "utf8"); + await controller.withSessionWriteLock(async () => { + events.push("inner-write"); + }); + events.push("outer-end"); + }); + + expect(acquireSessionWriteLock).toHaveBeenCalledTimes(2); + expect(events).toEqual([ + "prep-release", + "outer-start", + "inner-write", + "outer-end", + "post-release", + ]); + }); + it("drains queued Pi session events before reacquiring for cleanup", async () => { const events: string[] = []; let resolveQueue!: () => void; diff --git a/src/agents/pi-embedded-runner/run/attempt.session-lock.ts b/src/agents/pi-embedded-runner/run/attempt.session-lock.ts index ab09334c9f8..dc981a2eed1 100644 --- a/src/agents/pi-embedded-runner/run/attempt.session-lock.ts +++ b/src/agents/pi-embedded-runner/run/attempt.session-lock.ts @@ -1,3 +1,4 @@ +import { AsyncLocalStorage } from "node:async_hooks"; import fs from "node:fs/promises"; import { isSessionWriteLockTimeoutError } from "../../session-write-lock-error.js"; import type { acquireSessionWriteLock } from "../../session-write-lock.js"; @@ -257,6 +258,7 @@ export async function createEmbeddedAttemptSessionLockController(params: { }); let heldLock: SessionLock | undefined = await acquireLock(); + const activeWriteLock = new AsyncLocalStorage(); let fenceFingerprint: SessionFileFingerprint | undefined; let fenceActive = false; let takeoverDetected = false; @@ -310,12 +312,21 @@ export async function createEmbeddedAttemptSessionLockController(params: { if (takeoverDetected) { throw new EmbeddedAttemptSessionTakeoverError(params.lockOptions.sessionFile); } + if (activeWriteLock.getStore()) { + return await run(); + } const { lock, owned } = await acquireWriteLock(); try { await assertSessionFileFence(); - const result = await run(); - await refreshSessionFileFence(); - return result; + const runWithLock = async () => { + const result = await run(); + await refreshSessionFileFence(); + return result; + }; + if (owned) { + return await activeWriteLock.run(lock, runWithLock); + } + return await runWithLock(); } finally { if (owned) { await lock.release();