From 7790808a29b9173c6b719d69ae2144fa89b7d1d8 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 18:33:27 -0700 Subject: [PATCH 1/2] fix(gsd): recover from stale lockfile after crash or SIGKILL Add pre-flight stale lock cleanup before proper-lockfile acquisition: if the .lock/ directory exists but no auto.lock metadata is present (or the owning PID is dead), remove it proactively instead of waiting for the 30-min stale window. Also improve the error message when recovery fails to include the rm command for manual cleanup. Fixes #3218 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/session-lock.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/gsd/session-lock.ts b/src/resources/extensions/gsd/session-lock.ts index 1d5a4e7a3..e3bbe7c49 100644 --- a/src/resources/extensions/gsd/session-lock.ts +++ b/src/resources/extensions/gsd/session-lock.ts @@ -288,6 +288,20 @@ export function acquireSessionLock(basePath: string): SessionLockResult { const gsdDir = gsdRoot(basePath); const lockTarget = effectiveLockTarget(gsdDir); + // #3218: Pre-flight stale lock cleanup — if the .lock/ directory exists but + // no auto.lock metadata is present (or the PID is dead), remove the lock + // directory before attempting acquisition. This prevents the 30-min stale + // window from blocking /gsd after crashes, SIGKILL, or laptop sleep. + const lockDir = lockTarget + ".lock"; + if (existsSync(lockDir)) { + const existingData = readExistingLockData(lp); + const isOrphan = !existingData || (existingData.pid && !isPidAlive(existingData.pid)); + if (isOrphan) { + try { rmSync(lockDir, { recursive: true, force: true }); } catch { /* best-effort */ } + try { if (existsSync(lp)) unlinkSync(lp); } catch { /* best-effort */ } + } + } + try { // Try to acquire an exclusive OS-level lock on the lock target. // We lock a directory since proper-lockfile works best on directories, @@ -344,9 +358,11 @@ export function acquireSessionLock(basePath: string): SessionLockResult { } } + // #3218: Provide actionable workaround when lock recovery fails + const lockDirPath = lockTarget + ".lock"; const reason = existingPid ? `Another auto-mode session (PID ${existingPid}) appears to be running.\nStop it with \`kill ${existingPid}\` before starting a new session.` - : `Another auto-mode session is already running on this project.`; + : `Another auto-mode session lock is stuck on this project.\nRun: rm -rf "${lockDirPath}" && rm -f "${lp}"`; return { acquired: false, reason, existingPid }; } From ce3b31e3c4e97e0aae4bacd2a8518245bf573335 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 22:27:30 -0700 Subject: [PATCH 2/2] test: add regression test for stale lockfile auto-recovery Co-Authored-By: Claude Opus 4.6 (1M context) --- .../gsd/tests/stale-lockfile-recovery.test.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/resources/extensions/gsd/tests/stale-lockfile-recovery.test.ts diff --git a/src/resources/extensions/gsd/tests/stale-lockfile-recovery.test.ts b/src/resources/extensions/gsd/tests/stale-lockfile-recovery.test.ts new file mode 100644 index 000000000..c7a4ab2ab --- /dev/null +++ b/src/resources/extensions/gsd/tests/stale-lockfile-recovery.test.ts @@ -0,0 +1,36 @@ +/** + * stale-lockfile-recovery.test.ts — #3668 + * + * Verify that session-lock.ts contains pre-flight stale lock cleanup logic + * that removes orphaned lock directories when the owning PID is dead, + * preventing the 30-min stale window from blocking /gsd after crashes. + */ + +import { describe, test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const sourceFile = join(__dirname, "..", "session-lock.ts"); + +describe("stale lockfile auto-recovery (#3668)", () => { + const source = readFileSync(sourceFile, "utf-8"); + + test("checks for orphan lock with isPidAlive", () => { + assert.match(source, /isPidAlive\(existingData\.pid\)/); + }); + + test("removes stale lock directory with rmSync", () => { + assert.match(source, /rmSync\(lockDir,\s*\{\s*recursive:\s*true/); + }); + + test("references issue #3218 in pre-flight cleanup comment", () => { + assert.match(source, /#3218.*Pre-flight stale lock cleanup/); + }); + + test("provides actionable rm -rf workaround in error message", () => { + assert.match(source, /rm\s+-rf/); + }); +});