From 48178c9fdb44ae4aec65b6af82487a385bbdb84c Mon Sep 17 00:00:00 2001 From: root Date: Fri, 22 May 2026 01:47:30 +0000 Subject: [PATCH] fix: add timeout to doctor frontmatter_integrity check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On brains with 200K+ pages, the frontmatter scan walks every .md file on disk across all registered sources. This synchronous FS walk can take minutes (observed: >60s on a 216K-page brain with 3 sources), causing the doctor command to appear hung. scanBrainSources already supports an AbortSignal via opts.signal — the walkDir callback checks signal.aborted on every file, and the source loop breaks on abort. This commit passes AbortSignal.timeout (default 30s) from the doctor caller so the check degrades gracefully instead of blocking the entire health report. Configurable via GBRAIN_DOCTOR_FM_TIMEOUT_MS for brains that need more or less time. When the timeout fires, doctor reports a warn with instructions to run the full scan directly. --- src/commands/doctor.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 314dd6f50..f6d4c81b1 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -2972,9 +2972,13 @@ export async function runDoctor(engine: BrainEngine | null, args: string[], dbSo // backups so it works for both git and non-git brain repos. progress.heartbeat('frontmatter_integrity'); const fmHb = startHeartbeat(progress, 'scanning frontmatter…'); + // Guard against full-brain FS walks hanging on large brains (200K+ pages). + // AbortSignal.timeout fires after 30s; scanBrainSources respects opts.signal. + const fmTimeoutMs = parseInt(process.env.GBRAIN_DOCTOR_FM_TIMEOUT_MS || '30000', 10); try { const { scanBrainSources } = await import('../core/brain-writer.ts'); - const report = await scanBrainSources(engine); + const fmAbort = AbortSignal.timeout(fmTimeoutMs); + const report = await scanBrainSources(engine, { signal: fmAbort }); if (report.total === 0) { const sources = report.per_source.length; checks.push({ @@ -3002,10 +3006,14 @@ export async function runDoctor(engine: BrainEngine | null, args: string[], dbSo }); } } catch (e) { + const isTimeout = e instanceof DOMException && e.name === 'AbortError'; checks.push({ name: 'frontmatter_integrity', status: 'warn', - message: `Could not scan frontmatter: ${e instanceof Error ? e.message : String(e)}`, + message: isTimeout + ? `Frontmatter scan timed out after ${fmTimeoutMs / 1000}s (brain too large for full walk). ` + + `Run \`gbrain frontmatter validate \` directly, or raise GBRAIN_DOCTOR_FM_TIMEOUT_MS.` + : `Could not scan frontmatter: ${e instanceof Error ? e.message : String(e)}`, }); } finally { fmHb();