From 9a0fd13ec2ee01e6c918c5c881c4a78ef60bffe8 Mon Sep 17 00:00:00 2001 From: luvs01 Date: Sun, 16 Aug 2026 13:03:44 +0900 Subject: [PATCH] fix(lab): retain grace period for stale ledger locks --- src/lab/ledger/store.ts | 11 ++++++++--- tests/lab-ledger-mutation-lock.test.ts | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/lab/ledger/store.ts b/src/lab/ledger/store.ts index cd1d37fc4e..bdc6b5e665 100644 --- a/src/lab/ledger/store.ts +++ b/src/lab/ledger/store.ts @@ -76,7 +76,7 @@ function isLockHolderAlive(pid: number): boolean { } } -/** Return true when a ledger lock file has dead metadata and can be recovered. */ +/** Return true when a ledger lock file has old, dead metadata and can be recovered. */ function isLedgerLockStale(lockPath: string): boolean { const meta = readLedgerLockMeta(lockPath); if (!meta) { @@ -86,7 +86,12 @@ function isLedgerLockStale(lockPath: string): boolean { return false; } } - return !isLockHolderAlive(meta.pid); + // A dead PID alone is not enough: between checking its liveness and + // unlinking, another process can replace the path with its own live lock. + // Keeping the same grace period as malformed locks prevents a freshly + // replaced lock from being selected for stale recovery. + return Date.now() - meta.createdAt > LEDGER_LOCK_STALE_MS + && !isLockHolderAlive(meta.pid); } /** Write lock ownership metadata to a newly created exclusive lock file. */ @@ -529,4 +534,4 @@ export function openLedgerStore(configDir?: string): LedgerStore { export function defaultLedgerPath(configDir?: string): string { return labLedgerPath(configDir); -} \ No newline at end of file +} diff --git a/tests/lab-ledger-mutation-lock.test.ts b/tests/lab-ledger-mutation-lock.test.ts index d659ca4d73..11b915fd16 100644 --- a/tests/lab-ledger-mutation-lock.test.ts +++ b/tests/lab-ledger-mutation-lock.test.ts @@ -147,7 +147,7 @@ test("appendLabEvent waits for the shared ledger mutation lock", async () => { } }); -test("appendLabEventIfAbsent immediately recovers a lock owned by an exited process", async () => { +test("appendLabEventIfAbsent recovers an aged lock owned by an exited process", async () => { const home = tempHome(); const ledgerPath = join(home, "lab", "compatibility.jsonl"); const lockPath = `${ledgerPath}.lock`; @@ -158,7 +158,7 @@ test("appendLabEventIfAbsent immediately recovers a lock owned by an exited proc import { writeFileSync } from "node:fs"; writeFileSync( ${JSON.stringify(lockPath)}, - JSON.stringify({ pid: process.pid, createdAt: Date.now(), token: "dead-holder" }), + JSON.stringify({ pid: process.pid, createdAt: Date.now() - 61_000, token: "dead-holder" }), { mode: 0o600 }, ); writeFileSync(${JSON.stringify(readyPath)}, "ready");