* feat(engine): add countUnconsolidatedFacts to BrainEngine + both engines
New `BrainEngine.countUnconsolidatedFacts(sourceId): Promise<number>` returns
the count of active + unconsolidated facts for a source. Single SQL:
COUNT(*) WHERE source_id = $1 AND consolidated_at IS NULL AND expired_at IS NULL.
Backs the v0.33 `gbrain recall --pending` flag and the `recall` MCP op's new
`include_pending` param. Source-scoped, no index needed (existing
facts(source_id) index covers the predicate).
* feat(recall): cursor state + recall rewrite + thin-client routing + watch loop
`gbrain recall` gains four new flags backed by a new cursor-state file:
- `--since-last-run` reads ~/.gbrain/recall-cursors/<source>.json. First run
defaults to 24h. Cursor is T_start (captured BEFORE the read SQL), not
T_finish, so facts inserted during render don't fall in a black hole
(Codex round 1 #2).
- `--pending` appends a "Pending consolidation: N" footer. Backed by the
new engine method; remote round-trips through one MCP call via the
recall op's new `include_pending` param.
- `--rollup` prepends a "Top mentions" header — top-5 entities by fact
count over the FULL result set, not a LIMIT slice (Codex round 1 #8).
JSON shape `top_entities: [{entity_slug, count}]` matches the existing
pinned key at test/facts-doctor-shape.test.ts:49.
- `--watch [SECONDS]` re-runs on interval. Default 60, range [1, 3600].
TTY: clear-and-redraw. Non-TTY: plain `--- <ts> ---` delimited blocks.
SIGINT-only clean exit. Per-tick try/catch + exponential backoff
`min(SECONDS × 2^(N-1), 5×SECONDS)`; exit after 5 consecutive failures
with briefing cursor NOT advanced. Watch uses a separate cursor file
(<source>.watch.json) so operator quitting watch doesn't clobber the
standalone briefing cursor (Codex round 2 #8).
Thin-client routing: runRecall + runForget mirror the salience.ts:80
pattern. On `gbrain init --mcp-only` installs the local engine call is
swapped for callRemoteTool('recall' | 'forget_fact', ...). The local
canonical source resolver's assertSourceExists check is skipped on
thin-client (empty local sources table); the kebab-case SOURCE_ID_RE
syntactic gate still runs locally. Fixes pre-existing silent-empty-results
on thin-client recall — the v0.31.1 wave missed it (Codex round 2 #6).
`recall` MCP op extended with optional `include_pending` param +
`pending_consolidation_count` output field. Backward-compatible.
No new MCP op. No schema migration.
State file uses atomic write via unique per-call tmp filename
(<source>.json.tmp.<pid>.<random>) + rename(2) (Codex round 1 #7).
Read returns null on missing/corrupt/future-shifted timestamps; caller
falls back to 24h.
* feat(thin-client): route jobs list/get + REFUSE 7 host-bound commands
Continues the v0.31.1 thin-client routing wave. v0.33 audit (Codex round 2
#4) source-grounded against operations.ts + each command file:
ROUTE additions (have MCP ops, mirror salience.ts:80 pattern):
- `gbrain jobs list` → callRemoteTool('list_jobs', ...)
- `gbrain jobs get <id>` → callRemoteTool('get_job', ...)
Other jobs subcommands (submit, cancel, retry, work, supervisor, prune,
stats, smoke) stay host-bound — they manage local queue state.
REFUSE additions to cli.ts THIN_CLIENT_REFUSED_COMMANDS + matching hints
in THIN_CLIENT_REFUSE_HINTS:
- `pages` — purge-deleted is admin+localOnly (operations.ts:856-864)
- `files` — file_list / file_url MCP ops are localOnly:true
- `eval` — export/prune/replay touch local engine; no MCP equivalent
- `code-def` / `code-refs` / `code-callers` / `code-callees` — NO MCP ops
exist for symbol lookup in operations.ts:2630-2671; deferred as a v0.34
candidate to add them
Each refuse hint names the host-side path the user should use instead.
Closes the silent-wrong-brain bug class for 9 commands total (recall +
forget routing landed in the prior commit).
* test: cover v0.33 recall extensions + thin-client routing audit (45 cases)
Three new test files pinning the v0.33 behavior + critical regression
guards from both Codex review rounds:
- test/recall-extensions.test.ts (17 cases, PGLite-backed). Covers
countUnconsolidatedFacts SQL semantics (ignores expired, ignores
consolidated, source-scoped, returns 0 on empty), cursor state file
round-trip + corrupt/future fallback + briefing vs watch separation
(Codex round 2 #8 regression guard) + atomic write tmp suffix
(Codex round 1 #7 regression guard) + non-fatal write failures.
Uses withEnv() for GBRAIN_HOME isolation per check-test-isolation.sh R1.
- test/recall-rollup.test.ts (8 pure-function cases). CRITICAL
regression guards for Codex round 1 #8:
1. Top-K computed over the FULL FactRow[], not a LIMIT-100 slice
(seeded with 150 facts to prove full-window math)
2. JSON shape pinned to `{entity_slug, count}` matching
test/facts-doctor-shape.test.ts:49 (the existing shape pin)
3. null entity_slug skipped, NOT bucketed as "(no entity)"
4. Ties broken alphabetically for stable output
- test/thin-client-routing-audit.test.ts (20 source-grounded cases).
Pins every v0.33 REFUSE addition in THIN_CLIENT_REFUSED_COMMANDS +
every matching hint in THIN_CLIENT_REFUSE_HINTS + every v0.31.1-era
original (no accidental removals). Pins every ROUTE addition's
callRemoteTool import + call site in recall.ts and jobs.ts. Catches
the audit-table regression mode that motivated the v0.31.1 wave
originally.
Net: 45 new test cases. All pass green against the v0.33 implementation.
* chore: bump version and changelog (v0.33.0)
v0.33.0 — agent integration: gbrain recall morning pulse + thin-client routing fix.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>