mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-20 07:22:06 +08:00
45d3293c6a
Three fixes for a repo that commits a large JS/TS theme/SDK (Metronic under static/, ~1,600 tracked files): 1. A SECOND "Resolving refs" quadratic that #915 didn't cover. #915 capped import-name collisions; this caps method-name collisions (init/update/render re-declared on every widget), which flow through matchMethodCall Strategy 3 and findBestMatch instead. New AMBIGUOUS_NAME_CEILING (default 500, env CODEGRAPH_AMBIGUOUS_NAME_CEILING): above it the fuzzy strategies decline rather than score K candidates — no proximity score can pick the one true target among thousands anyway. Resolving drops from O(K^2) to linear in refs (e.g. 900-file synthetic: 28.7s -> 3.4s), edge counts unchanged, and the cap never fires on normal repos (max real method-collision ~40). 2. A new `exclude` array in codegraph.json keeps git-TRACKED paths out of the index, which .gitignore can't do (enumeration is `git ls-files`). Mirrors the existing includeIgnored plumbing across the git, sync, and non-git-walk paths. 3. `index`/`init` now install the #850 liveness + #277 ppid watchdogs (which were serve-only), so a wedged or orphaned indexer self-terminates instead of pinning a core. The --liftoff-only relaunch's spawnSync can't forward signals, so killing the parent shim used to orphan the worker. Tests: ubiquitous-name ceiling, exclude (incl. tracked-file exclusion on git + non-git), orphan self-termination (POSIX), and ppid-parser units. Shared the ppid parsers out of mcp/index.ts into mcp/ppid-watchdog.ts. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
78 lines
3.2 KiB
TypeScript
78 lines
3.2 KiB
TypeScript
/**
|
|
* Process supervision for long-running CLI commands (`index` / `init --index`).
|
|
*
|
|
* Indexing a large repo can run for a while on the main thread, and #999
|
|
* surfaced two ways that goes wrong when nothing is watching it:
|
|
*
|
|
* 1. **Orphaned worker.** `index` runs in a child re-exec'd with
|
|
* `--liftoff-only` (the WASM-flag relaunch). Its parent blocks in
|
|
* `spawnSync`, so when the parent shim is killed it cannot forward the
|
|
* signal — the child keeps running, now orphaned, pinning a core. The PPID
|
|
* watchdog (#277) notices the parent/host went away and exits the child.
|
|
* 2. **Wedged indexer.** The `#850` main-thread liveness watchdog — which
|
|
* SIGKILLs a process whose event loop stops turning — was wired only into
|
|
* the MCP `serve` path, so a wedged `index`/`init` was never auto-killed.
|
|
*
|
|
* Both reuse the exact mechanisms `serve` already uses; this just makes them
|
|
* available to a one-shot command. Best-effort and self-disabling: a missing
|
|
* watchdog never blocks the command from running. Both honour the same env
|
|
* switches as `serve` (`CODEGRAPH_NO_WATCHDOG`, `CODEGRAPH_PPID_POLL_MS=0`).
|
|
*/
|
|
import { installMainThreadWatchdog } from '../mcp/liveness-watchdog';
|
|
import { supervisionLostReason, parsePpidPollMs, parseHostPpid } from '../mcp/ppid-watchdog';
|
|
import { isProcessAlive } from '../mcp/daemon-registry';
|
|
import { HOST_PPID_ENV } from '../extraction/wasm-runtime-flags';
|
|
|
|
export interface CommandSupervision {
|
|
/** Tear down both watchdogs. Idempotent; call when the command finishes. */
|
|
stop(): void;
|
|
}
|
|
|
|
/**
|
|
* Install the liveness + PPID watchdogs for the duration of a CLI command.
|
|
* `label` is used in the shutdown notice (e.g. `"index"`). Returns a handle
|
|
* whose `stop()` must be called when the command completes so neither watchdog
|
|
* outlives it.
|
|
*/
|
|
export function installCommandSupervision(label: string): CommandSupervision {
|
|
// Liveness watchdog: a separate process that SIGKILLs us if our event loop
|
|
// stops turning for too long (a wedged synchronous loop). Self-disables on
|
|
// CODEGRAPH_NO_WATCHDOG.
|
|
const liveness = installMainThreadWatchdog();
|
|
|
|
// PPID watchdog: detect that the parent (or the host threaded past the
|
|
// relaunch shim) died and we've been orphaned, then exit instead of leaking.
|
|
const originalPpid = process.ppid;
|
|
const hostPpid = parseHostPpid(process.env[HOST_PPID_ENV]);
|
|
const pollMs = parsePpidPollMs(process.env.CODEGRAPH_PPID_POLL_MS);
|
|
let ppidTimer: ReturnType<typeof setInterval> | null = null;
|
|
if (pollMs > 0) {
|
|
ppidTimer = setInterval(() => {
|
|
const reason = supervisionLostReason({
|
|
originalPpid,
|
|
currentPpid: process.ppid,
|
|
hostPpid,
|
|
isAlive: isProcessAlive,
|
|
});
|
|
if (reason) {
|
|
try {
|
|
process.stderr.write(`[CodeGraph ${label}] Parent process exited (${reason}); aborting.\n`);
|
|
} catch { /* stderr gone with the parent — exit anyway */ }
|
|
process.exit(1);
|
|
}
|
|
}, pollMs);
|
|
// Never let the watchdog itself keep the process alive past its real work.
|
|
ppidTimer.unref();
|
|
}
|
|
|
|
let stopped = false;
|
|
return {
|
|
stop(): void {
|
|
if (stopped) return;
|
|
stopped = true;
|
|
if (ppidTimer) clearInterval(ppidTimer);
|
|
liveness?.stop();
|
|
},
|
|
};
|
|
}
|