* feat(brainstorm): T1 cost guardrails + judge chunking + far-set cap Ports PR #1234 with a typed-error swap (Q2). Brings: - `--max-cost`, `--max-far-set`, `--strict-budget`, `--judge-model`, `--max-ideas-per-judge-call` CLI flags on `gbrain brainstorm` / `lsd` - Domain-bank prefix-cap + shuffle + final-trim to `m` by distance score - Judge auto-chunks idea sets > 100 across multiple LLM calls - UTF-16 surrogate sanitization on cross prompts - Phase-0.5 hard cost ceiling + mid-run cost guard Phase-1 diff from PR #1234: per-cross error-rethrow uses inline typed `BudgetExhausted` instead of string-match on the error message. Phase 2 of the wave will move the class to `src/core/budget/budget-tracker.ts` and the orchestrator will import it. Postmortem doc + 12-case regression test included verbatim from #1234. T1 of the brainstorm cost cathedral plan (~/.claude/plans/system-instruction-you-are-working-rippling-moth.md). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(budget): T2 BudgetTracker + BudgetExhausted + audit-week helper The keystone primitive for the v0.37.x budget cathedral. One class, one typed error, one schema-stable audit JSONL. Replaces three parallel copies (brainstorm orchestrator inline class, cycle/budget-meter, eval-contradictions cost-prompt/tracker) — those adapt to this one in T5/T6. Contracts pinned by 26 unit tests: - TX1: record() throws BudgetExhausted(reason:'cost') when cumulative spend > cap. A single underestimated call cannot leak past the cap. - TX2: reserve() hard-fails with BudgetExhausted(reason:'no_pricing') when cap is set + model is missing from pricing maps. When cap is unset, legacy warn-once behavior is preserved. - A3 amended: extractUsageFromError(err, fallback) returns err.usage when SDK provides it, else the pessimistic fallback (caller passes maxOutputTokens, not the optimistic pre-call estimate). - onExhausted callback fires once, synchronously, before the throw propagates. Callbacks do sync I/O (writeFileSync) for checkpoint persistence. - Audit JSONL is schema-stable: every line carries schema_version=1. Reorderings tolerated, field renames are breaking. Also ships src/core/audit-week-file.ts — the shared ISO-week filename helper consumed by every audit writer in T4. Year-boundary correctness pinned by 5 cases including 2020-W53 (the 53-week year), 2025-W01 rolling in from 2024-12-30 (Monday), and the GBRAIN_AUDIT_DIR override. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(gateway): T3 withBudgetTracker + AsyncLocalStorage composition TX5: every gateway.chat / embed / rerank call now auto-composes the active BudgetTracker via a module-internal AsyncLocalStorage. No per-call injection seam, no flag plumbing — callers wrap their entrypoint in `withBudgetTracker(tracker, async () => { ... })` and every downstream LLM call honors the cap. Outside any scope, the gateway is a budget no-op (back-compat with the pre-v0.37 contract). Wiring: - chat(): reserves on entry using prompt-char heuristic + opts.maxTokens. Records actual usage from result.usage on success; on failure, charges the pessimistic A3-amended fallback so the cap is real. - embed(): reserves total estimated input tokens (chars / chars-per-token). Records the same total in try/finally; SDK doesn't surface per-batch embed token counts. - rerank(): reserves and records query + docs char count. Reranker pricing isn't in the canonical map yet, so reserve() takes the warn-once path under no-cap and the TX2 hard-fail under cap. 6 unit cases pin the contract: chat auto-composes, outside-scope is no-op, nested scope restores outer, over-cap reserve throws BEFORE provider call (proves circuit breaker), TX1 mid-run cumulative cap fires via record(), parallel Promise.all scopes do not bleed trackers. All 255 existing gateway tests and 50 brainstorm tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(audit): T4 migrate 4 audit writers to shared isoWeekFilename helper Q1: extract the ISO-week filename math into one canonical helper (src/core/audit-week-file.ts, landed in T2) and migrate every audit JSONL writer in the codebase to consume it. Sites migrated: - src/core/minions/handlers/shell-audit.ts (shell-jobs-YYYY-Www.jsonl) - src/core/facts/phantom-audit.ts (phantoms-YYYY-Www.jsonl) - src/core/audit-slug-fallback.ts (slug-fallback-YYYY-Www.jsonl) - src/core/cycle/budget-meter.ts (dream-budget-YYYY-Www.jsonl) Each call site had its own copy of the ISO-week-from-Date algorithm. They mostly agreed but subtle drift was already accumulating (one used local time, one approximated the Thursday-anchor formula, etc.). One helper, one set of regression tests, no drift. Compute helpers (computeAuditFilename, computePhantomAuditFilename, computeSlugFallbackAuditFilename) are preserved as thin wrappers so existing import sites and tests don't break. All audit + slug-fallback + phantom + budget-meter tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cycle): T5 BudgetMeter schema_version=1 + golden fixture (A2 amended) Adapter pass: the existing BudgetMeter keeps its public shape (`BudgetMeter`, `SubmitEstimate`, `BudgetCheckResult`) verbatim so every dream-cycle call site keeps working without rewires. The audit JSONL grew one new field on every line: `schema_version: 1`. A2 amended: the codex outside-voice review relaxed the byte-stable contract to schema-stable. Field reorderings are tolerated; the documented set (schema_version, ts, phase, event, model, label, plus per-event cost or token fields) is what every consumer can rely on. Renames or removals are breaking. test/fixtures/dream-budget-schema-v1.jsonl carries one canonical row per event variant (submit / submit_denied / submit_unpriced) as documentation of the schema. The new in-suite case in test/budget-meter.test.ts walks every emitted line and asserts the fields are present + the right type. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(eval): T6 wrap eval-contradictions runner in withBudgetTracker The runner now installs a BudgetTracker scope around its body so every gateway-layer chat / embed / rerank call (the judge model + per-query embedding) auto-records via the AsyncLocalStorage from T3. Currently telemetry-only — the existing CostTracker remains the primary soft- ceiling enforcement, so the public --budget-usd surface and PreFlightBudgetError shape are byte-identical. The wiring is the seam: future waves can promote the cap to BudgetTracker semantics (TX1 + TX2 semantics on cumulative + no_pricing) by passing maxCostUsd through to BudgetTracker without touching the CLI. All 79 eval-contradictions tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(doctor): T7 --remediate budget tracker + checkpoint + --resume (A4) A4 amended: doctor --remediate gains a resumable cost ceiling. The runRemediate loop now runs inside `withBudgetTracker(tracker, ...)` so every gateway-routed LLM call inside a Minion handler (synthesize, patterns, consolidate, embed) honors the cap. When BudgetExhausted fires mid-run, the onExhausted callback persists a checkpoint of completed step ids + idempotency_keys to ~/.gbrain/remediation/<plan_hash>.json BEFORE the throw propagates, and the catch surfaces a paste-ready --resume hint. Wire-up: - New --resume <plan_hash> flag (with implicit "most recent matching" when no hash given) loads the checkpoint and skips already- completed steps. Mismatched plan_hash refuses with an explicit message. - --max-cost is now an alias for --max-usd. Both spellings honored and threaded through to BudgetTracker.maxCostUsd so the cap is a real ceiling, not just pre-flight advice. - On BudgetExhausted, exit 1 with the resume hint; on clean completion, clear the checkpoint. New file: src/core/remediation-checkpoint.ts with computePlanHash / save / load / list / clear helpers. Atomic write via .tmp + rename. Pinned by 13 unit cases including determinism + sort-order invariance + schema-mismatch return-null + atomic-rename. All 48 doctor.test.ts cases still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(subagent): T8 A1 ordering ASCII diagram before acquireLease Documents the load-bearing ordering invariant: the gateway's BudgetTracker reserve() runs (implicitly, via AsyncLocalStorage) BEFORE acquireLease() inside the subagent loop. A BudgetExhausted throw must NOT consume a rate-lease slot, because the lease is the rate-limit pacer for the entire fleet. The handler body intentionally does NOT explicitly thread BudgetTracker; TX5 (gateway-layer composition) handles that. The comment is the reader's signpost. No behavioral change. All 58 subagent tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(diarize): T9 payload-fitter (P6) with batch + summarize + gate Generic utility for fitting arbitrarily-large item lists into a downstream caller's per-call token budget. Two strategies: - 'batch': deterministic token-budgeted chunking. No LLM calls. The fitted list shape matches the input; the caller decides how to consume it (e.g. brainstorm judge concatenates per-chunk results). Surfaces a `dropped` count for items that exceed the per-call cap. - 'summarize': embed-cluster into ceil(items/4) groups via cheap deterministic nearest-neighbor on cosine; Haiku-summarize each cluster via Promise.allSettled at parallelism=4 (Perf1). Each Haiku call composes the active BudgetTracker via the gateway's AsyncLocalStorage scope (T3) — no per-call injection. Quality gate (codex outside-voice finding #4): when summarize's success_ratio < min_success_ratio (default 0.75), the result is flagged `degraded: true` so the caller (brainstorm) can decide to surface a partial result or abort. The fitter itself preserves the successful subset either way. Tested via 4 cases across two files (T3 contract): - happy path (all clusters succeed → degraded=false) - partial failure tolerated (1/5 fails, success_ratio=0.8 > 0.75 → degraded=false) - high-failure rate flips the gate (3/5 fails → degraded=true) - budget-respecting (BudgetExhausted thrown mid-cluster propagates via Promise.allSettled) 11 unit cases across batch + summarize. Brainstorm + cost-guardrails tests still green; judges.ts internal chunking deferred to a follow-up wave (TODOS) so the existing chunked-batch contract stays byte-stable during this drop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(brainstorm): T10 checkpoint + --resume with full idea bodies (P7) The brainstorm cathedral capstone. Crashed runs can resume cleanly via `gbrain brainstorm --resume <run_id>` (and `gbrain lsd --resume` etc). TX3 load-bearing contract: completed_crosses on disk carries FULL idea bodies (~50KB per run), not just counts. The resumed BrainstormResult contains the pre-crash ideas (loaded from disk) merged with the post- resume ideas — codex's outside-voice finding was that a resume that produces only "what we generated this run" is silent partial output. TX4 single rule: --resume continues any cross not in completed_crosses. The proposed --retry-failed was dropped per codex review; failed AND never-attempted crosses both go through --resume. A5 amended: run_id = sha256(question + profile + sort(close_slugs) + sort(far_slugs)).slice(0,16). NO embedding bits — stable across embedding-model swaps. 7-day mtime-based GC. Q2 fold: orchestrator.ts drops its inline BudgetExhausted class and re-exports the canonical one from src/core/budget/budget-tracker.ts (Phase 2). runBrainstorm now wraps the body in withBudgetTracker so every gateway-layer chat call auto-records cost. The cap remains opts.maxCostUsd (default $5). New CLI flags: --resume <run_id> Continue any cross not in completed_crosses. Refuses to start when run_id doesn't match the active inputs (paste-ready hint). --force-resume Bypass the 7-day staleness gate. --list-runs Print saved run_ids and exit. Cycle purge phase (the 9th cycle phase) now also GCs stale brainstorm checkpoints alongside op_checkpoints (~7d window). Tests: - 20 unit cases in test/brainstorm/checkpoint.test.ts: computeRunId is deterministic + slug-array-order invariant + stable across embedding-model swaps; round-trip preserves ideas verbatim; saveCheckpoint atomic via .tmp+rename; loadCheckpoint returns null on missing/schema-mismatch/corrupt-JSON; gcStaleCheckpoints unlinks >N days; listRuns mtime-ordered. - 3 E2E cases in test/e2e/brainstorm-resume.test.ts: crash on cross 4 → first run aborts with checkpoint of crosses 1..N with full idea bodies; second run with resumeRunId merges pre-crash + post-resume ideas (TX3 contract); mismatched run_id refuses with paste-ready hint. The PGLite schema-gap workaround in the E2E (CREATE VIEW page_links AS SELECT * FROM links) is filed as a follow-up in TODOS T12 — the real-engine brainstorm path needs that view to materialize as a canonical schema fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: T11 + T12 wave release docs + deferred follow-ups CHANGELOG entry for the brainstorm cost cathedral (Unreleased slot; /ship will assign the next version): - ELI10 lead per CLAUDE.md voice rules - "How to turn it on" with paste-ready commands - "Things to watch" calls out the A4 semantic shift for `doctor --remediate --max-usd` (pre-flight → mid-run abort with resumable checkpoint) - Itemized changes by file/area - "For contributors" section noting the 73 new tests + the PGLite schema-gap workaround for the E2E CLAUDE.md Key Files: 6 new entries for budget-tracker, audit-week-file, gateway withBudgetTracker, payload-fitter, brainstorm/checkpoint, remediation-checkpoint. Regenerated llms-full.txt + llms.txt (passes test/build-llms.test.ts). docs/incidents/2026-05-20-lsd-cost-explosion.md gains a closing "Shipped in v0.37.x (the budget cathedral wave)" section listing P1-P7 completion status + the deferred follow-ups so the incident's audit trail closes the loop. TODOS.md gets a new top section for the wave's deferred items: - PGLite `page_links` schema gap fix - Explicit --max-cost on extract / enrich / integrity auto - P5 config-schema budgets: block in ~/.gbrain/config.json - Multi-day brainstorm resume (>7d) - Async-batched audit writes (profiling trigger criterion) - BudgetLedger unification with BudgetTracker - judges.ts internal chunking → payload-fitter delegation Also: fixed a payload-fitter typecheck error (ChatFn import). Final typecheck is clean on every file the wave touched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(schema): F1 page_links view alias for both engines Brainstorm's domain-bank queries reference `page_links` (pglite-engine.ts:896, postgres-engine.ts:959) but the canonical table is `links`. Without the alias view, `gbrain brainstorm` against PGLite fails with `relation "page_links" does not exist`; the same was a latent bug on Postgres. This commit lands the fix at three sites: 1. `src/core/pglite-schema.ts` — embedded schema bundle gets the view at table-bundle time, so fresh PGLite installs are correct from boot. 2. `src/core/migrate.ts` v81 (`page_links_view_alias`) — existing brains on either engine pick up the view via `gbrain apply-migrations`. CREATE OR REPLACE VIEW is idempotent; re-running is safe. 3. `test/e2e/brainstorm-resume.test.ts` — removed the ad-hoc workaround view from the test setup. The E2E now exercises the same schema path real users will see. `TODOS.md` entry for the gap closed out. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(brainstorm): F2 pre-flight --max-cost refusal smoke E2E Pins the user-facing path that closed the original \$50 incident: when the pre-run estimate exceeds the configured cap, runBrainstorm throws BudgetExhausted with reason='cost' and a paste-ready hint pointing at --limit / --max-cost / --max-far-set before any chat call happens. The four assertions are the four things a real user can verify after the throw lands: 1. Typed BudgetExhausted (not a generic Error) 2. reason === 'cost' (not runtime or no_pricing) 3. Message names the remediation flags 4. No provider HTTP would have happened (chat.crossCalls === 0) Uses the same PGLite engine + tinyProfile + stub chatFn as the existing --resume tests. Hermetic; ~5s wallclock. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(reindex-code): F3 --max-cost flag via withBudgetTracker Wires gbrain reindex --code into the v0.38 budget cathedral. When the caller passes --max-cost N (or --max-cost-usd N), runReindexCode wraps its per-page import loop in withBudgetTracker so every gateway.embed() call inside importCodeFile auto-composes the cap. On BudgetExhausted, the partial-progress result reports what got reindexed before the cap fired plus a synthetic failure row naming the cap throw. reindex-code is idempotent (content_hash short-circuit in importCodeFile), so a re-run after a budget abort picks up where the cap fired — no manual checkpoint state needed. Both --max-cost and --max-cost-usd are accepted (symmetry with brainstorm which uses --max-cost, and a precedent for the spelling we want long-term). When --max-cost is unset, the body runs outside any tracker scope — byte- stable pre-F3 behavior for legacy callers. Files: src/commands/reindex-code.ts: - ReindexCodeOpts.maxCostUsd?: number - runReindexCode wraps body in withBudgetTracker when set - runReindexCodeCli parses --max-cost / --max-cost-usd - BudgetExhausted caught + returned as partial-progress result test/reindex-code-max-cost.serial.test.ts (NEW): - dry-run + maxCostUsd happy path - empty-brain + maxCostUsd hits early-return cleanly - no tracker installed when cap is unset (regression guard for the conditional wrap) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(schema): narrow page_links view projection to bootstrap-safe columns The v0.38 page_links view alias initially used SELECT * FROM links, which broke the pre-v0.13 bootstrap test: applyForwardReferenceBootstrap drops link_source + origin_page_id to simulate the pre-v0.13 schema shape, but the SELECT * view created a dependency that blocked the column DROP. Engine queries only reference pl.id (via COUNT(*)) and pl.to_page_id, so the view's projection is now SELECT id, from_page_id, to_page_id FROM links — what callers actually use, no more. This unblocks legacy-brain upgrade paths AND keeps the bootstrap forward-reference probes safe. Bootstrap suite: 15/15 pass after the change. Also files a P0 TODO for a pre-existing test failure (test/doctor-report-remote.test.ts "full report on healthy brain") that fails on master too — out of scope for this wave but noticed during /ship triage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: bump version to v0.39.0.0 Brainstorm cost cathedral wave (P1-P7). MINOR bump per user direction: new architectural seam (gateway-layer BudgetTracker via AsyncLocalStorage), 5 new modules, new CLI flags (--max-cost / --resume / --list-runs / --force-resume), new migration v81 (page_links view alias). No breaking changes — BudgetExhausted re-exported from orchestrator for back-compat; --max-usd preserved as alias for --max-cost; eval-contradictions --budget-usd surface byte-identical. CHANGELOG entry renamed from [Unreleased] to [0.39.0.0] and adds the mandatory "To take advantage of v0.39.0.0" block per CLAUDE.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(isolation): rename 3 env-mutating tests to .serial.test.ts (CI fix) CI's `check:test-isolation` flagged three tests added in the v0.39.0.0 cathedral that directly mutate `process.env` across test boundaries: - test/brainstorm/checkpoint.test.ts (mutates GBRAIN_HOME) - test/core/audit-week-file.test.ts (mutates GBRAIN_AUDIT_DIR) - test/core/remediation-checkpoint.test.ts (mutates GBRAIN_HOME) Per CLAUDE.md rule R1: env-mutating tests either use withEnv() OR rename to *.serial.test.ts (the quarantine escape hatch). The mutation lives in beforeEach/afterEach which spans the whole describe block, so .serial rename is the cleaner fix — withEnv() would require restructuring every test. The serial-test runner gives them their own bun process; no cross- file env races. Verified: check:test-isolation passes (527 non-serial unit files clean), `bun run verify` passes, all 41 tests in the three renamed files pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 KiB
Incident Report: LSD Brainstorm 53× Cost Overrun
Date: 2026-05-20
Severity: High (financial — $50.71 actual vs $0.96 estimated)
Component: gbrain lsd / gbrain brainstorm
Brain size: 13,690 pages, 16,314 links, ~2,000 unique directory prefixes
Version: v0.37.1.0 (first release of brainstorm/lsd)
What Happened
A user ran gbrain lsd "what story should Garry's List write next" --yes on a 13,690-page brain. The command:
- Estimated cost: $0.96 (2×12 = 24 crosses × 4 ideas + judge)
- Actual cost: $50.71 — 53× over estimate
- Token usage: 4,906,011 input + 2,399,239 output = 7.3M total tokens
- Far set pulled 1,985 pages instead of the configured 12
- Generated 15,868 raw ideas across the crosses (vs expected ~96)
- Judge phase failed: 2,989,338 tokens exceeded Claude Sonnet's 1M context limit
- Zero ideas surfaced to the user — complete failure
A retry with --limit 12 explicit:
- Far set correctly returned 12 pages, cost was $0.39
- But judge still failed:
parseJudgeJSON: no strategy produced valid JSON - Again, 0 ideas survived to output (96 generated, 0 scored)
Root Causes
RC1: Far Set Explosion (caused the $50 bill)
File: src/core/brainstorm/domain-bank.ts → fetchFar() → listPrefixSampledPages()
The domain bank samples pages by directory prefix to get diversity. listPrefixSampledPages returns one page per prefix passed in. On a 13K-page brain with ~2,000 unique prefixes (books/, civic/bundles/, civic/gl-article-*, people/, concepts/, etc.), passing all prefixes produces ~2,000 rows — not the configured m=12.
The cost estimator uses m (12) to predict crosses and cost. But the actual cross phase receives 1,985 far-set pages, producing 2 × 1985 = 3,970 crosses at 4 ideas each = 15,868 ideas.
The estimate formula is correct for the intended behavior; the far set selection is what diverged.
RC2: No Cost Circuit Breaker
There is no mechanism to:
- Abort if estimated cost exceeds a threshold
- Abort mid-run if actual spend diverges from estimate
- Cap the far set size regardless of prefix count
- Warn the user that a run will be expensive before proceeding
The --yes flag skips the 10-second cost preview wait, removing even the manual inspection opportunity.
RC3: Judge Context Overflow
The judge receives ALL ideas in a single prompt. With 15,868 ideas at ~350 tokens each, that's ~5.5M tokens — well beyond any model's context window.
Even on the retry with only 96 ideas, the judge failed with JSON parsing errors, suggesting the judge prompt/response format is fragile.
RC4: Unpaired UTF-16 Surrogates in Page Content
Two crosses failed with: The request body is not valid JSON: no low surrogate in string
Some pages (likely OCR imports or web scrapes) contain unpaired UTF-16 surrogates. When these get serialized into the JSON request body for the LLM API, the JSON encoder produces invalid JSON.
RC5: No Timeout on Individual Crosses
One cross timed out with no specific timeout configured. The default HTTP timeout allowed it to hang for an extended period before failing, consuming tokens on the API side.
Observed Token Flow
Configured: 2 close × 12 far = 24 crosses × 4 ideas = 96 ideas + 1 judge call
Actual: 2 close × 1985 far = 3970 crosses × 4 ideas = 15,868 ideas + 1 judge call (failed)
Per-cross tokens (estimated): ~1,200 in + 600 out
Actual total: 4,906,011 in + 2,399,239 out
The judge call alone would have been:
15,868 ideas × ~350 tokens = ~5.5M tokens (prompt)
Model limit: 1M tokens (Sonnet)
Overflow: 5.5× context limit
Proposed Fixes
P1: Far Set Cap (Critical — prevents cost explosion)
fetchFar() must cap the number of prefixes BEFORE calling listPrefixSampledPages. The cap should be max(m * 4, 50) to allow some diversity headroom while preventing runaway growth. Final selection trimmed to m by distance score.
Status: Implemented in dc080ac2.
P2: Cost Guardrails (Critical — defense in depth)
New flags for brainstorm and lsd commands:
--max-cost <usd>(default $5): hard-abort if pre-run estimate exceeds--strict-budget: abort mid-run if running cost exceeds 5× estimate--max-far-set <n>(default 50): explicit far set size cap
Status: Implemented in dc080ac2.
P3: Judge Chunking (Critical — prevents context overflow)
Split ideas into batches of ~100 before calling the judge LLM. Each batch is a separate API call; results concatenated. This bounds per-call token usage to ~35K regardless of total idea count.
Status: Implemented in dc080ac2.
P4: Unicode Sanitization (Medium — prevents cross failures)
Strip unpaired UTF-16 surrogates from page content before building cross prompts. This is a general problem for any gbrain function that serializes user-generated page content into JSON for API calls.
Status: Implemented in dc080ac2.
P5: Global Token & Time Budgets for All Analysis Functions (Proposed)
This is the bigger architectural ask. Every gbrain command that makes LLM calls should respect configurable budgets:
# Proposed config additions to ~/.gbrain/config.json
budgets:
# Global defaults
default:
max_input_tokens: 500_000 # per-command input token cap
max_output_tokens: 200_000 # per-command output token cap
max_cost_usd: 5.00 # per-command dollar cap
max_runtime_seconds: 300 # 5-minute wall-clock cap
# Per-command overrides
brainstorm:
max_cost_usd: 2.00
max_runtime_seconds: 120
lsd:
max_cost_usd: 5.00
max_runtime_seconds: 300
dream:
max_cost_usd: 10.00
max_runtime_seconds: 600
extract:
max_input_tokens: 1_000_000
max_runtime_seconds: 900
enrich:
max_cost_usd: 3.00
max_runtime_seconds: 180
Commands affected:
brainstorm/lsd— bisociation crosses + judge (this incident)dream— dream cycle phases (enrichment, emotional weight, etc.)extract all— link + timeline extraction across all pagesenrich— per-page deep enrichment with web researcheval— evaluation runs (suspected-contradictions, retrieval drift)integrity auto— automated content repairdoctor --remediate— autonomous self-healing via Minions
Implementation approach:
- Add a
BudgetTrackerclass that wraps LLM calls with token/cost/time accounting - Every analysis function receives a budget context
- On budget exhaustion: save partial results, emit a structured warning, exit cleanly
- CLI flags (
--max-cost,--max-tokens,--timeout) override config defaults --no-budgetescape hatch for power users who know what they're doing
P6: Diarization / Summarization for Oversized Payloads (Proposed)
When a judge or analysis phase receives more content than fits in context:
- Estimate tokens before calling the LLM
- If over budget, diarize: summarize/compress the content to fit
- For the judge specifically: rank ideas by a cheap heuristic first (keyword overlap, novelty score), then send only top-N to the LLM judge
- For other analysis: progressive summarization — chunk → summarize → merge summaries → final analysis
This is effectively a token budget allocator that decides how to spend a fixed token budget across variable-length inputs.
Example: 15,868 ideas need judging, context limit 900K tokens
Step 1: Cheap pre-filter (keyword dedup, obvious duplicates) → 8,000 unique ideas
Step 2: Batch into 80 chunks of 100 ideas each
Step 3: Judge each chunk → 80 calls × ~35K tokens = 2.8M total (spread across calls)
Step 4: Merge top ideas from each chunk → final ranking
Total cost: ~$2-3 instead of $50
P7: Structured Error Recovery (Proposed)
When a cross or judge call fails:
- Save the partial results immediately (don't wait for the full run)
- Emit a machine-readable error event (not just a log warning)
- Support
--retry-failedto re-run only the failed crosses without repeating successful ones - Checkpoint progress to disk so interrupted runs can resume
Impact
- Financial: $50.71 wasted on a single failed run
- User trust: Zero ideas delivered despite ~7M tokens processed
- Time: ~15 minutes of compute time, plus overnight delay in reporting results
Lessons
- First run of any new feature on a large brain should be dry-run or capped. The estimate was based on small-brain testing; 13K pages is a different universe.
- Cost estimators must account for actual data cardinality, not just configured parameters. The estimate used
m=12but the real far set was|prefixes|. - Every LLM-calling function needs a budget. This isn't just a brainstorm problem — it's an architectural gap in any system that makes variable numbers of LLM calls based on data size.
- JSON serialization of user content is a landmine. Any page could contain invalid Unicode. Sanitize at the serialization boundary, not per-feature.
Shipped in v0.37.x (the budget cathedral wave)
P1-P4 already shipped via PR #1234 (the first fix wave). P5-P7 plus a few architectural rounds shipped in the budget-cathedral wave that followed:
- P1 (far set cap):
fetchFar()insrc/core/brainstorm/domain-bank.tscaps prefix sampling tomax(m*4, 50)and trims final pages tomby distance. The 2K-prefix explosion class is closed. - P2 (cost guardrails):
--max-cost,--max-far-set,--strict-budget,--judge-model,--max-ideas-per-judge-callflags on brainstorm + lsd. Pre-flight estimate refusal, mid-run cost-ceiling abort. - P3 (judge chunking):
runJudgeinsrc/core/brainstorm/judges.tsauto-chunks at 100 ideas/call. Context-window overflow is structurally prevented. - P4 (unicode sanitization):
sanitizeUnicodeinsrc/core/brainstorm/orchestrator.tsstrips unpaired surrogates before serialization. - P5 (BudgetTracker at the gateway layer): new
src/core/budget/budget-tracker.tsis the canonical primitive. The gateway'swithBudgetTracker(tracker, fn)composes viaAsyncLocalStorage<BudgetTracker>so every gateway-routed LLM call inside the scope auto-records.BudgetExhaustedis a typed error withreason: 'cost' | 'runtime' | 'no_pricing'.record()throws when cumulative spend exceeds the cap (TX1).reserve()hard-fails onno_pricingwhen the cap is set + model missing from pricing maps (TX2). - P6 (payload-fitter):
src/core/diarize/payload-fitter.tswith'batch'and'summarize'strategies. Summarize embed-clusters (k=ceil(items/4)), Haiku-summarizes each cluster in parallel viaPromise.allSettledat parallelism=4. Surfacesdegraded: trueflag when success ratio < 0.75 so callers decide whether to surface a partial result or abort. - P7 (brainstorm checkpoint + --resume):
src/core/brainstorm/checkpoint.tspersists FULL idea bodies (not just counts — TX3 load-bearing). One--resume <run_id>flag covers both failed and never-attempted crosses (TX4).run_idformula uses NO embedding bits so the identity is stable across embedding-model swaps (A5 amended). 7-day mtime-based GC wired into the cycle purge phase.--list-runslists saved checkpoints.--force-resumebypasses the 7d staleness gate.
Also shipped alongside the wave (folded inline):
- doctor --remediate --resume: A4 amended. The mid-run cap is now a
real ceiling;
--max-costis an alias for--max-usd. On BudgetExhausted, the orchestrator persists a checkpoint at~/.gbrain/remediation/<plan_hash>.jsonand tells the user the exactgbrain doctor --remediate --resumecommand. The resumed run skips already-completed steps. - Audit-week-file consolidation (Q1): four call sites (shell-jobs / phantoms / slug-fallback / dream-budget) now share one ISO-week filename helper. Year-boundary correctness pinned by tests.
- eval-contradictions tracker telemetry: the existing CostTracker stays for the report shape; the runner additionally installs a withBudgetTracker scope for the gateway-layer telemetry path.
What did NOT make this wave (filed in TODOS for a follow-up):
- The schema fix for
page_linkson PGLite. The brainstorm domain-bank queries referencepage_linksbut the embedded schema only defineslinks; the E2E works around this with a view in test setup, but real PGLite users currently can't rungbrain brainstorm. Schema fix needed. --max-costflag onextract,enrich,integrity auto. The gateway-layer enforcement covers them when wrapped at the entrypoint, but the CLI flag wiring is deferred.- Async-batched audit writes. Sync
appendFileSyncis fine at typical volumes; revisit if profiling shows it dominates. - Multi-day brainstorm resume (>7d). The
--force-resumeflag is the operator escape hatch for now.