Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/resources/extensions/gsd/auto-start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { migrateToExternalState, recoverFailedMigration } from "./migrate-extern
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
import { gsdRoot, resolveMilestoneFile } from "./paths.js";
import { invalidateAllCaches } from "./cache.js";
import { writeLock, clearLock } from "./crash-recovery.js";
import { writeLock, clearLock, readCrashLock, isLockProcessAlive } from "./crash-recovery.js";
import {
acquireSessionLock,
releaseSessionLock,
Expand Down Expand Up @@ -620,6 +620,12 @@ export async function bootstrapAutoSession(
return false;
}

const startupLock = readCrashLock(base);
if (startupLock && !isLockProcessAlive(startupLock)) {
clearLock(base);
ctx.ui.notify("Cleared stale auto-mode worker state.", "info");
}

const lockResult = acquireSessionLock(base);
if (!lockResult.acquired) {
ctx.ui.notify(lockResult.reason, "error");
Expand Down
3 changes: 1 addition & 2 deletions src/resources/extensions/gsd/auto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,6 @@ import { createWorkspace, scopeMilestone } from "./workspace.js";
import {
registerAutoWorker,
markWorkerStopping,
markWorkerStoppingByPid,
} from "./db/auto-workers.js";
import { releaseMilestoneLease } from "./db/milestone-leases.js";
import { normalizeRealPath } from "./paths.js";
Expand Down Expand Up @@ -923,7 +922,7 @@ export function checkRemoteAutoSession(projectRoot: string): {

if (!isLockProcessAlive(lock)) {
// Stale lock from a dead process — not a live remote session
markWorkerStoppingByPid(normalizeRealPath(projectRoot), lock.pid);
clearLock(projectRoot);
return { running: false };
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import test from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";

import { checkRemoteAutoSession } from "../auto.ts";
import { openDatabase, closeDatabase, _getAdapter } from "../gsd-db.ts";
import { registerAutoWorker } from "../db/auto-workers.ts";
import { normalizeRealPath } from "../paths.ts";
import { readCrashLock } from "../crash-recovery.ts";

function makeBase(): string {
const base = mkdtempSync(join(tmpdir(), "gsd-remote-lock-cleanup-"));
mkdirSync(join(base, ".gsd"), { recursive: true });
return base;
}

function cleanup(base: string): void {
try { closeDatabase(); } catch {}
try { rmSync(base, { recursive: true, force: true }); } catch {}
}

function expireWorker(workerId: string): void {
const db = _getAdapter()!;
db.prepare(
`UPDATE workers SET last_heartbeat_at = '1970-01-01T00:00:00.000Z' WHERE worker_id = :worker_id`,
).run({ ":worker_id": workerId });
}

function setWorkerPid(workerId: string, pid: number): void {
const db = _getAdapter()!;
db.prepare(
`UPDATE workers SET pid = :pid WHERE worker_id = :worker_id`,
).run({ ":pid": pid, ":worker_id": workerId });
}

test("checkRemoteAutoSession clears stale lock state when lock PID is dead", (t) => {
const base = makeBase();
t.after(() => cleanup(base));

openDatabase(join(base, ".gsd", "gsd.db"));
const workerId = registerAutoWorker({ projectRootRealpath: normalizeRealPath(base) });
setWorkerPid(workerId, 99999);
expireWorker(workerId);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

assert.ok(readCrashLock(base), "precondition: stale lock exists before remote session check");

const remote = checkRemoteAutoSession(base);
assert.deepEqual(remote, { running: false });
assert.equal(readCrashLock(base), null, "stale lock should be cleared by remote session check");
});
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,25 @@ test("auto bootstrap validates blocked directories before touching .gsd migratio
const bootstrapIdx = autoStartSrc.indexOf("export async function bootstrapAutoSession(");
const bootstrapBody = autoStartSrc.slice(bootstrapIdx);
const bootstrapValidationIdx = bootstrapBody.indexOf("validateDirectory(base)");
const staleCrashReadIdx = bootstrapBody.indexOf("const startupLock = readCrashLock(base)");
const staleCrashClearIdx = bootstrapBody.indexOf("clearLock(base);");
const lockIdx = bootstrapBody.indexOf("acquireSessionLock(base)");
const bootstrapMigrationIdx = bootstrapBody.indexOf("migrateToExternalState(base)");

assert.ok(bootstrapIdx > -1, "bootstrapAutoSession should exist");
assert.ok(bootstrapValidationIdx > -1, "bootstrapAutoSession should validate the base directory");
assert.ok(lockIdx > -1, "bootstrapAutoSession should acquire a session lock for safe projects");
assert.ok(bootstrapMigrationIdx > -1, "bootstrapAutoSession should still migrate safe projects");
assert.ok(staleCrashReadIdx > -1, "bootstrapAutoSession should probe stale crash lock state before lock acquisition");
assert.ok(staleCrashClearIdx > -1, "bootstrapAutoSession should clear stale crash lock state when detected");
assert.ok(
bootstrapValidationIdx < lockIdx && bootstrapValidationIdx < bootstrapMigrationIdx,
"fresh bootstrap must reject blocked directories before locking or migrating .gsd state",
);
assert.ok(
staleCrashReadIdx < lockIdx && staleCrashClearIdx < lockIdx,
"fresh bootstrap must auto-clear stale crash lock state before session lock acquisition",
);
});

test("fresh start registers the auto worker before bootstrap enters worktree flow (#5405)", () => {
Expand Down
Loading