fix: Stale lock recovery and MCP init retry

Fixes #47 — "database is locked" after crash and MCP "not initialized"
when project IS initialized.

- FileLock: treat locks older than 10 minutes as stale regardless of PID
  status, covering cases where PID was reused or kill signal check fails
- MCP server: log errors from tryInitializeDefault() to stderr instead of
  silently swallowing, so transient open failures are diagnosable
- MCP server: retryInitIfNeeded() properly cleans up failed instances
  before retrying, preventing resource leaks
- CLI: add 'codegraph unlock' command for manual lock file removal
This commit is contained in:
Colby McHenry
2026-03-18 14:50:25 -05:00
parent bdbe59b457
commit b964d5909a
3 changed files with 51 additions and 4 deletions
+10 -4
View File
@@ -184,6 +184,9 @@ export class FileLock {
private lockPath: string;
private held = false;
/** Locks older than this are considered stale regardless of PID status */
private static readonly STALE_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
constructor(lockPath: string) {
this.lockPath = lockPath;
}
@@ -197,15 +200,18 @@ export class FileLock {
try {
const content = fs.readFileSync(this.lockPath, 'utf-8').trim();
const pid = parseInt(content, 10);
const stat = fs.statSync(this.lockPath);
const lockAge = Date.now() - stat.mtimeMs;
if (!isNaN(pid) && this.isProcessAlive(pid)) {
// Treat locks older than the timeout as stale, regardless of PID
if (lockAge < FileLock.STALE_TIMEOUT_MS && !isNaN(pid) && this.isProcessAlive(pid)) {
throw new Error(
`CodeGraph database is locked by another process (PID ${pid}). ` +
`If this is stale, delete ${this.lockPath}`
`If this is stale, run 'codegraph unlock' or delete ${this.lockPath}`
);
}
// Stale lock - remove it
// Stale lock (dead process or timed out) - remove it
fs.unlinkSync(this.lockPath);
} catch (err) {
if (err instanceof Error && err.message.includes('locked by another')) {
@@ -225,7 +231,7 @@ export class FileLock {
// Race condition: another process grabbed the lock between our check and write
throw new Error(
'CodeGraph database is locked by another process. ' +
`If this is stale, delete ${this.lockPath}`
`If this is stale, run 'codegraph unlock' or delete ${this.lockPath}`
);
}
throw err;