Skip to content

Commit b964d59

Browse files
committed
fix: Stale lock recovery and MCP init retry
Fixes colbymchenry#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
1 parent bdbe59b commit b964d59

3 files changed

Lines changed: 51 additions & 4 deletions

File tree

src/bin/codegraph.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,37 @@ program
10361036
process.exit(0);
10371037
});
10381038

1039+
/**
1040+
* codegraph unlock [path]
1041+
*/
1042+
program
1043+
.command('unlock [path]')
1044+
.description('Remove a stale lock file that is blocking indexing')
1045+
.action(async (pathArg: string | undefined) => {
1046+
const projectPath = resolveProjectPath(pathArg);
1047+
1048+
try {
1049+
if (!isInitialized(projectPath)) {
1050+
error(`CodeGraph not initialized in ${projectPath}`);
1051+
return;
1052+
}
1053+
1054+
const lockPath = path.join(getCodeGraphDir(projectPath), 'codegraph.lock');
1055+
1056+
if (!fs.existsSync(lockPath)) {
1057+
info('No lock file found — nothing to do');
1058+
return;
1059+
}
1060+
1061+
fs.unlinkSync(lockPath);
1062+
success('Removed lock file. You can now run indexing again.');
1063+
} catch (err) {
1064+
captureException(err);
1065+
error(`Failed to remove lock: ${err instanceof Error ? err.message : String(err)}`);
1066+
process.exit(1);
1067+
}
1068+
});
1069+
10391070
/**
10401071
* codegraph install
10411072
*/

src/mcp/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,18 @@ export class MCPServer {
120120
this.cg = await CodeGraph.open(resolvedRoot);
121121
this.toolHandler.setDefaultCodeGraph(this.cg);
122122
} catch (err) {
123+
// Log the error so transient failures are diagnosable (see issue #47)
123124
captureException(err);
125+
const msg = err instanceof Error ? err.message : String(err);
126+
process.stderr.write(`[CodeGraph MCP] Failed to open project at ${resolvedRoot}: ${msg}\n`);
124127
}
125128
}
126129

127130
/**
128131
* Retry initialization of the default project if it previously failed.
129132
* Called lazily on tool calls that need the default project.
133+
* Re-walks parent directories each time so it picks up projects
134+
* initialized after the MCP server started.
130135
*/
131136
private retryInitIfNeeded(): void {
132137
// Already initialized successfully
@@ -138,6 +143,11 @@ export class MCPServer {
138143
if (!resolvedRoot) return;
139144

140145
try {
146+
// Close any previously failed instance to avoid leaking resources
147+
if (this.cg) {
148+
try { this.cg.close(); } catch { /* ignore */ }
149+
this.cg = null;
150+
}
141151
this.cg = CodeGraph.openSync(resolvedRoot);
142152
this.projectPath = resolvedRoot;
143153
this.toolHandler.setDefaultCodeGraph(this.cg);

src/utils.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ export class FileLock {
184184
private lockPath: string;
185185
private held = false;
186186

187+
/** Locks older than this are considered stale regardless of PID status */
188+
private static readonly STALE_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
189+
187190
constructor(lockPath: string) {
188191
this.lockPath = lockPath;
189192
}
@@ -197,15 +200,18 @@ export class FileLock {
197200
try {
198201
const content = fs.readFileSync(this.lockPath, 'utf-8').trim();
199202
const pid = parseInt(content, 10);
203+
const stat = fs.statSync(this.lockPath);
204+
const lockAge = Date.now() - stat.mtimeMs;
200205

201-
if (!isNaN(pid) && this.isProcessAlive(pid)) {
206+
// Treat locks older than the timeout as stale, regardless of PID
207+
if (lockAge < FileLock.STALE_TIMEOUT_MS && !isNaN(pid) && this.isProcessAlive(pid)) {
202208
throw new Error(
203209
`CodeGraph database is locked by another process (PID ${pid}). ` +
204-
`If this is stale, delete ${this.lockPath}`
210+
`If this is stale, run 'codegraph unlock' or delete ${this.lockPath}`
205211
);
206212
}
207213

208-
// Stale lock - remove it
214+
// Stale lock (dead process or timed out) - remove it
209215
fs.unlinkSync(this.lockPath);
210216
} catch (err) {
211217
if (err instanceof Error && err.message.includes('locked by another')) {
@@ -225,7 +231,7 @@ export class FileLock {
225231
// Race condition: another process grabbed the lock between our check and write
226232
throw new Error(
227233
'CodeGraph database is locked by another process. ' +
228-
`If this is stale, delete ${this.lockPath}`
234+
`If this is stale, run 'codegraph unlock' or delete ${this.lockPath}`
229235
);
230236
}
231237
throw err;

0 commit comments

Comments
 (0)