A SIGKILL'd process (the #850 liveness watchdog, OOM, a crash) leaves its WAL on disk; the next session appends to the same file; and nothing ever truncated it — PASSIVE checkpoints fold frames but keep the file at its high-water mark, and the one shrinking path (a clean last-connection close) is exactly what a killed-daemon world never takes. Observed at 25.6 GB on a 5.46 GB DB, growing until the disk filled. - journal_size_limit on every connection: resetting checkpoints now clip the WAL back to the cap instead of leaving it at its high-water mark. - healOversizedWal() fired from every DatabaseConnection.open: off-thread PASSIVE fold + TRUNCATE when the leftover WAL exceeds the cap (64 MB, CODEGRAPH_WAL_HEAL_MB to override). Single-flight per connection with bounded retries — concurrent passes defeat each other (each checkpoint sees the other as a busy reader). - Daemon/direct MCP watchdogs now pass progressPaths (DB + WAL), extending the #1231 slow-disk deferral to the long-lived server so a healthy daemon mid slow statement isn't SIGKILL'd — fewer kills, fewer leaked WALs. - codegraph status shows WAL size (human + JSON) and warns when it dwarfs the DB; daemon.log lines and the watchdog kill notice now carry ISO timestamps so kills can be placed in time. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
0682137a42
commit
02c0e2c935
+46
-2
@@ -103,6 +103,47 @@ function daemonInternalSet(): boolean {
|
||||
return !!raw && raw !== '0' && raw.toLowerCase() !== 'false';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefix every `process.stderr.write` chunk with an ISO-8601 timestamp. Called
|
||||
* once, only when this process becomes the detached daemon — whose stderr is
|
||||
* appended to `.codegraph/daemon.log`. Before #1431 no log line carried a
|
||||
* timestamp, so watchdog kills and restarts could be counted but never placed
|
||||
* in time. (The watchdog child writes its kill notice through its own
|
||||
* inherited fd 2, bypassing this wrapper — it stamps that line itself.)
|
||||
*/
|
||||
export function timestampStderrLines(): void {
|
||||
const orig = process.stderr.write.bind(process.stderr);
|
||||
process.stderr.write = ((chunk: string | Uint8Array, ...rest: unknown[]) => {
|
||||
return (orig as (...args: unknown[]) => boolean)(stampLogChunk(chunk), ...rest);
|
||||
}) as typeof process.stderr.write;
|
||||
}
|
||||
|
||||
/** Prepend `[<ISO-8601>] ` to a log chunk; unknown chunk types pass through. */
|
||||
export function stampLogChunk(chunk: string | Uint8Array): string | Uint8Array {
|
||||
try {
|
||||
const stamp = `[${new Date().toISOString()}] `;
|
||||
if (typeof chunk === 'string') return stamp + chunk;
|
||||
if (Buffer.isBuffer(chunk)) return Buffer.concat([Buffer.from(stamp), chunk]);
|
||||
} catch { /* stamping is best-effort; never block the write */ }
|
||||
return chunk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Watchdog `progressPaths` for a server keyed on `root`'s index: the SQLite DB
|
||||
* + its WAL. With these, the #850 liveness watchdog only kills on heartbeat
|
||||
* silence when the DB files are NOT advancing — the same slow-disk deferral
|
||||
* the CLI `index`/`init` path got in #1231. Without it, one >timeout
|
||||
* synchronous statement on a big DB (multi-GB index behind Windows Defender)
|
||||
* SIGKILLs a perfectly healthy daemon — and a daemon SIGKILL'd at the end of
|
||||
* nearly every session is what ratcheted the WAL leak in #1431. A true wedge
|
||||
* still dies: a wedged loop writes nothing, so the files stay still.
|
||||
*/
|
||||
export function watchdogProgressPaths(root: string | null): { progressPaths?: string[] } {
|
||||
if (!root) return {};
|
||||
const dbPath = path.join(getCodeGraphDir(root), 'codegraph.db');
|
||||
return { progressPaths: [dbPath, `${dbPath}-wal`] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the project root the daemon machinery should key on. Returns
|
||||
* `null` when no `.codegraph/` is reachable from the candidate path — in
|
||||
@@ -346,7 +387,7 @@ export class MCPServer {
|
||||
this.mode = 'direct';
|
||||
this.installSignalHandlers();
|
||||
this.installPpidWatchdog();
|
||||
this.livenessWatchdog = installMainThreadWatchdog();
|
||||
this.livenessWatchdog = installMainThreadWatchdog(watchdogProgressPaths(resolveDaemonRoot(this.projectPath)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -359,6 +400,9 @@ export class MCPServer {
|
||||
* and reaps itself via client-refcount + idle timeout (see {@link Daemon}).
|
||||
*/
|
||||
private async startDaemonProcess(): Promise<void> {
|
||||
// In daemon mode stderr IS `.codegraph/daemon.log`; stamp every line so
|
||||
// kills/restarts can be placed in time (#1431 — the log was undatable).
|
||||
timestampStderrLines();
|
||||
const root = resolveDaemonRoot(this.projectPath) ?? this.projectPath ?? process.cwd();
|
||||
for (let attempt = 0; attempt < TAKEOVER_MAX_RETRIES; attempt++) {
|
||||
const lock = tryAcquireDaemonLock(root);
|
||||
@@ -371,7 +415,7 @@ export class MCPServer {
|
||||
// The detached daemon has no PPID watchdog or stdin lifeline, so a
|
||||
// wedged main thread would pin a core forever (#850). The liveness
|
||||
// watchdog is its only recovery path.
|
||||
this.livenessWatchdog = installMainThreadWatchdog();
|
||||
this.livenessWatchdog = installMainThreadWatchdog(watchdogProgressPaths(root));
|
||||
return; // the net.Server keeps the process alive
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user