Skip to content

extract_facts: empty slugs array triggers full-brain walk, dead-letters autopilot-cycle #1096

@navin-moorthy

Description

@navin-moorthy

Summary

runExtractFacts in src/core/cycle/extract-facts.ts treats opts.slugs = [] the same as opts.slugs being omitted because the guard checks falsiness on the array, not its presence. A sync no-op therefore turns into a full-brain facts walk via engine.getAllSlugs(). On multi-thousand-page brains this exceeds the autopilot-cycle timeout (~600s) and the job dead-letters with timeout exceeded.

Repro

  1. Brain with several thousand pages (reproduced on a 6,492-page Supabase brain; smaller brains should reproduce as long as the full walk exceeds the autopilot-cycle timeout).
  2. Run a sync with no changes — autopilot subsequently dispatches an autopilot-cycle job whose sync phase ends with slugs: [] (incremental no-op).
  3. The cycle reaches [cycle.extract_facts] start and runs for the full timeout window before dead-lettering.
$ gbrain jobs list --status active
ID     Name             Status  ...
1037   autopilot-cycle  active  ...   ← stays active until ~600s timeout

$ tail -f ~/.gbrain/autopilot.err
[cycle.extract_facts] start
... (no further output until timeout) ...
Autopilot stopping (cycle-failure-cap).

Root cause

src/core/cycle/extract-facts.ts (current 3933eb6/v0.35.1.0):

let slugs: string[];
if (opts.slugs && opts.slugs.length > 0) {
  slugs = opts.slugs;
} else {
  // Full walk: every page in the brain.
  const allSlugs = await engine.getAllSlugs();
  slugs = Array.from(allSlugs);
}

opts.slugs && opts.slugs.length > 0 is falsy for both `undefined` (intended full-walk) AND [] (incremental no-op from sync). Callers passing the empty array unintentionally trigger the full walk.

Suggested fix

Make presence — not length — distinguish the modes:

let slugs: string[];
if (opts.slugs !== undefined) {
  // Empty array is a real incremental no-op from sync. Do not fall back
  // to a full-brain walk or autopilot no-change cycles time out.
  slugs = opts.slugs;
} else {
  const allSlugs = await engine.getAllSlugs();
  slugs = Array.from(allSlugs);
}

Regression test

// test/extract-facts-phase.test.ts
test('runExtractFacts with empty slugs array scans 0 pages (incremental no-op)', async () => {
  const result = await runExtractFacts(engine, { slugs: [] });
  expect(result.pagesScanned).toBe(0);
});

Verification post-fix

On the same 6,492-page brain, post-patch runExtractFacts({ slugs: [] }) completes in ~0.4s; autopilot job that previously dead-lettered at ~600s now completes inside one cycle.

Workaround

Carry a local patch with the presence check above. Has been running on my install since 2026-05-13 (v0.33.1.0) with no regressions across the v0.34.x and v0.35.x wave.

Environment

  • gbrain v0.35.1.0 (3933eb6)
  • Bun 1.3.13
  • Supabase Postgres (ap-south-1 session-mode pooler, 5432)
  • macOS Sequoia 15.6

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions