mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 08:53:22 +00:00
Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00279392b6 | ||
|
|
d68afbb534 | ||
|
|
99101aea34 | ||
|
|
19d0da22cb | ||
|
|
61da5e7732 | ||
|
|
a794c3b0d8 | ||
|
|
6d8d218a80 | ||
|
|
be48f9c599 | ||
|
|
1552d3ec80 | ||
|
|
f3b858cd01 | ||
|
|
da80f91f0b | ||
|
|
0176f11cd4 | ||
|
|
e9c92173f9 | ||
|
|
962bb0d35f | ||
|
|
ae27978053 | ||
|
|
ad36729a80 | ||
|
|
0ed0adeba9 | ||
|
|
6b7c6e49e1 | ||
|
|
66ad939109 | ||
|
|
6820dbf0ff | ||
|
|
49ee3a7e7c | ||
|
|
d01185baf7 | ||
|
|
3c974d9597 | ||
|
|
0b63783d95 | ||
|
|
1b24b0085d | ||
|
|
9c9de76d15 | ||
|
|
3f5ee1842a | ||
|
|
0bc02ea36b | ||
|
|
c3c046bac5 | ||
|
|
f60ba9c83d | ||
|
|
926579c051 | ||
|
|
cab7d81f54 | ||
|
|
395a2d080f |
@@ -187,6 +187,19 @@ jobs:
|
||||
key: bun-cache-${{ runner.os }}-${{ hashFiles('bun.lock') }}
|
||||
- run: bun install
|
||||
- run: bun test test/entity-resolve-perf.slow.test.ts --timeout=300000
|
||||
# MEMORY_VERBS v1 (Cathedral 1): the entity() p99 < 100ms contract gate
|
||||
# (20K-page corpus + ratio guard) shares this runner — same perf-job
|
||||
# shape, runs in parallel with the matrix.
|
||||
- run: bun test test/entity-card-perf.slow.test.ts --timeout=300000
|
||||
# Protocol self-certification: init a scratch brain and run the
|
||||
# conformance kit against gbrain's own stdio server. --synthesize is
|
||||
# safe here: no LLM key in CI, so it asserts the clean `unavailable`
|
||||
# protocol error instead of spending tokens.
|
||||
- name: MEMORY_VERBS conformance (self-certify, stdio)
|
||||
run: |
|
||||
export GBRAIN_HOME="$RUNNER_TEMP/gbrain-conformance"
|
||||
bun run src/cli.ts init --pglite --no-embedding --non-interactive
|
||||
bun run src/cli.ts protocol conformance --synthesize
|
||||
|
||||
test:
|
||||
# Pure matrix shard — no verify, no serial. Each shard runs its slice
|
||||
|
||||
@@ -2,6 +2,82 @@
|
||||
|
||||
All notable changes to GBrain will be documented in this file.
|
||||
|
||||
## [0.43.0.0] - 2026-08-08
|
||||
|
||||
**Your agent now has five memory verbs it can actually reach.** Cathedral 1 freezes
|
||||
a stable, versioned memory protocol — `recall`, `remember`, `entity`, `synthesize`,
|
||||
`forget` — over the brain's operation catalog, the way Postgres speaks one wire
|
||||
protocol to every client. Point any MCP harness at it (`claude mcp add gbrain --
|
||||
gbrain serve --surface verbs`, or the Codex/OpenClaw equivalents) and the agent sees
|
||||
exactly five self-describing tools instead of a wall of internal ops. Every response
|
||||
carries what it is, why it matched, where it came from, and what it spent (the token
|
||||
budget on `recall`, latency on `entity`, the full cost block on `synthesize`) — and the
|
||||
contract never breaks: v1 field names and meanings are frozen, changes are
|
||||
additive-forever.
|
||||
|
||||
What you can do now that you couldn't before:
|
||||
|
||||
- **Remember a fact once, recall it in a fresh session — in any harness.** `remember`
|
||||
takes mandatory provenance (where the fact came from) and an optional expiry, dedupes
|
||||
against what's already known, and supersedes the old fact when it changes
|
||||
("X joined acme-example" → "X left acme-example" — the outdated fact expires, the
|
||||
history stays). `recall` retrieves saved facts and,
|
||||
with a query, budget-packed page snippets — the server enforces the token budget and
|
||||
tells you what it dropped instead of trusting the client to trim.
|
||||
- **Look up one person/company/project as a compact card in well under 100ms, zero LLM
|
||||
calls.** `entity` resolves a name to a privacy-safe card (who/what, aliases,
|
||||
last-touched, open threads, top typed edges) — or, on a miss, near-miss suggestions
|
||||
instead of a dead end.
|
||||
- **Reason across pages when you actually need it.** `synthesize` is the explicitly
|
||||
expensive verb (it says so in its own description), with a best-effort cost block so
|
||||
agents choose it deliberately. No API key configured? It says `unavailable` with a
|
||||
fix, never a fake answer.
|
||||
- **Certify any memory server against the contract.** `gbrain protocol --json` publishes
|
||||
the machine-readable spec; `gbrain protocol conformance [--target <endpoint>]` runs the
|
||||
frozen-contract test suite against gbrain's own server or any MCP endpoint — and
|
||||
provably fails servers that don't comply. `gbrain protocol stats` shows per-verb
|
||||
adoption and your real time-to-first-use, all from a local file that never leaves
|
||||
your machine.
|
||||
|
||||
`gbrain serve` keeps every operation by default (`--surface full`) — existing installs
|
||||
are unchanged. The new `--surface verbs` is the quickstart surface for agents. No schema
|
||||
migration; the verbs ride the existing facts, pages, and typed-graph tables. The full
|
||||
contract, per-harness install blocks, and the additive-forever versioning policy live in
|
||||
[docs/protocol/MEMORY_VERBS_v1.md](docs/protocol/MEMORY_VERBS_v1.md).
|
||||
|
||||
**This release also fixes which search verb your agent reaches for (#2416).** The
|
||||
`search` and `query` tool descriptions, the mandatory lookup chain, and the search
|
||||
guides had drifted from reality: they described `search` as keyword-only (it has been
|
||||
cheap hybrid — vector + keyword, no LLM expansion — for many releases) and told agents
|
||||
to try `search` first for everything, falling back to `query` only when results looked
|
||||
"thin". For concept questions ("all the X that do Y", "the ecosystem around Z") that
|
||||
fallback never fired, and synonym-phrased matches dropped silently.
|
||||
|
||||
- **Concept questions now route to `query` by default.** The tool descriptions and the
|
||||
lookup-chain convention are intent-driven: exact known tokens → `search` (cheaper, no
|
||||
expansion call); concept / synonym / exhaustive-set questions → `query` first. Verified
|
||||
with a live LLM routing eval: all concept phrasings route to `query`, and personal
|
||||
questions still route to the salience ops.
|
||||
- **"Got results" is no longer treated as "got everything."** The descriptions, docs, and
|
||||
conventions now say it plainly: a populated `search` result set is not proof of
|
||||
coverage, and `query` is still top-K — literal exhaustive enumeration belongs to
|
||||
`list_pages` pagination.
|
||||
- **The CLI nudges you when it can help.** A concept-shaped `gbrain search "..."` prints a
|
||||
one-line hint on stderr suggesting the equivalent `gbrain query` call (results stay
|
||||
clean on stdout; `--quiet` silences it; never auto-reroutes).
|
||||
- **`think` cost accounting reads consistently.** When no LLM call ran (stubbed or no
|
||||
API key), `usage` is now uniformly `null` in `--json` output rather than sometimes
|
||||
missing.
|
||||
|
||||
To take advantage of v0.43.0.0: re-run `gbrain serve` with `--surface verbs` to give
|
||||
your coding agent the five-verb memory protocol (or keep `--surface full` for the
|
||||
complete operation catalog — both speak the verbs). Run `gbrain protocol conformance`
|
||||
to self-certify, and `gbrain protocol stats` to watch adoption. Memories your agent
|
||||
saves are readable by every agent connected to the brain by default; pass
|
||||
`visibility: "private"` for local-only facts. If your agent instructions or skill
|
||||
files copy the old "search first, query if thin" rule, refresh them from
|
||||
`skills/conventions/brain-first.md` — the shipped skillpack carries the corrected
|
||||
routing.
|
||||
## [0.42.76.0] - 2026-08-08
|
||||
|
||||
**Mistyped or unsupported flags now fail loudly instead of being silently ignored — including the ones that were supposed to make a command safe.**
|
||||
|
||||
@@ -38,7 +38,7 @@ mount, CEO-class with multiple team brains) and
|
||||
|
||||
## Architecture
|
||||
|
||||
Contract-first: `src/core/operations.ts` defines ~90 shared operations (v0.29 adds `get_recent_salience`, `find_anomalies`, `get_recent_transcripts`; v0.42.43.0 adds `volunteer_context` — push-based context, see `docs/guides/push-context.md`). CLI and MCP
|
||||
Contract-first: `src/core/operations.ts` defines ~110 shared operations (v0.29 adds `get_recent_salience`, `find_anomalies`, `get_recent_transcripts`; v0.42.43.0 adds `volunteer_context` — push-based context, see `docs/guides/push-context.md`; v0.43.0.0 adds the five frozen MEMORY_VERBS — `recall`, `remember`, `entity`, `synthesize`, `forget` — servable alone via `gbrain serve --surface verbs`, see `docs/protocol/MEMORY_VERBS_v1.md`). CLI and MCP
|
||||
server are both generated from this single source. Engine factory (`src/core/engine-factory.ts`)
|
||||
dynamically imports the configured engine (`'pglite'` or `'postgres'`). Skills are fat
|
||||
markdown files (tool-agnostic, work with both CLI and plugin contexts).
|
||||
@@ -119,6 +119,7 @@ detail on demand.)
|
||||
| push-based context (volunteer/watch/reflex window) | `docs/guides/push-context.md` |
|
||||
| schema packs / page types / extraction | `docs/architecture/schema-packs.md`, `type-taxonomy.md`, `lens-packs.md` |
|
||||
| thin-client / remote MCP / cross-modal | `docs/architecture/thin-client.md` |
|
||||
| memory verbs / MCP tool surface (`--surface`) / conformance | `docs/protocol/MEMORY_VERBS_v1.md` + the `verbs*`/`surface.ts`/`protocol.ts` entries in `KEY_FILES.md` |
|
||||
| the CLI surface (commands + flags) | `gbrain --help` / `gbrain --tools-json`, plus the relevant `KEY_FILES.md` entry |
|
||||
| running or writing tests | `docs/TESTING.md` |
|
||||
| bulk-command progress wiring | `docs/progress-events.md` |
|
||||
@@ -272,7 +273,7 @@ audit trail lives in the source repo's git history.
|
||||
|
||||
## Skills
|
||||
|
||||
Read the skill files in `skills/` before doing brain operations. GBrain ships 30 skills
|
||||
Read the skill files in `skills/` before doing brain operations. GBrain ships 52 skills
|
||||
organized by `skills/RESOLVER.md` (`AGENTS.md` is also accepted as of v0.19):
|
||||
|
||||
**Original 8 (conformance-migrated):** ingest (thin router), query, maintain, enrich,
|
||||
|
||||
@@ -91,7 +91,7 @@ Retrieve and follow the instructions at:
|
||||
https://raw.githubusercontent.com/garrytan/gbrain/master/INSTALL_FOR_AGENTS.md
|
||||
```
|
||||
|
||||
The agent installs GBrain, creates the brain, asks for your API keys, loads 43 skills, configures the dream cycle, and verifies the install end-to-end. ~30 minutes. You answer questions, it does the work.
|
||||
The agent installs GBrain, creates the brain, asks for your API keys, loads 52 skills, configures the dream cycle, and verifies the install end-to-end. ~30 minutes. You answer questions, it does the work.
|
||||
|
||||
> **Never set up an AI agent platform before?** The [personal-brain tutorial](docs/tutorials/personal-brain.md) walks the whole path end-to-end — picking OpenClaw vs Hermes, deploying it, pointing it at INSTALL_FOR_AGENTS.md, getting the API keys, and verifying the first query. Start there if any of the above is new.
|
||||
|
||||
@@ -99,13 +99,15 @@ The agent installs GBrain, creates the brain, asks for your API keys, loads 43 s
|
||||
|
||||
Already running Claude Code or Codex? There are two ways to wire GBrain in, depending on what you want.
|
||||
|
||||
**Just want a memory for your coding agent (recommended starting point).** Spin up a local brain and connect it in two commands — zero server, zero token, zero tunnel:
|
||||
**Just want a memory for your coding agent (recommended starting point).** Spin up a local brain and connect it in two commands — zero server, zero token, zero tunnel. `--surface verbs` gives your agent the five-verb memory protocol (`recall`, `remember`, `entity`, `synthesize`, `forget` — [MEMORY_VERBS v1](docs/protocol/MEMORY_VERBS_v1.md), frozen + additive-forever) instead of the full 110-tool wall; drop the flag for every operation:
|
||||
|
||||
```bash
|
||||
gbrain init --pglite # 2-second local brain (no Docker)
|
||||
claude mcp add gbrain -- gbrain serve # or: codex mcp add gbrain -- gbrain serve
|
||||
gbrain init --pglite # 2-second local brain (no Docker)
|
||||
claude mcp add gbrain -- gbrain serve --surface verbs # or: codex mcp add gbrain -- gbrain serve --surface verbs
|
||||
```
|
||||
|
||||
If `claude` is not found, install Claude Code first — or use the per-harness blocks in the [protocol doc](docs/protocol/MEMORY_VERBS_v1.md). Heads-up: memories agents save default to brain-wide visibility (every connected agent can recall them); pass `visibility: "private"` for local-only facts.
|
||||
|
||||
**Already have a brain on a remote host** (OpenClaw, Hermes, or any `gbrain serve --http`)? Point your laptop agents at it with one command each — `--install` wires it up and smoke-tests the token before handoff:
|
||||
|
||||
```bash
|
||||
@@ -117,7 +119,7 @@ gbrain connect https://your-host/mcp --token gbrain_xxx --agent codex --install
|
||||
|
||||
### Install the full autonomous setup into your existing agent
|
||||
|
||||
Want the whole thing — local brain, 43 skills, the overnight dream cycle that enriches while you sleep? Paste this into Codex, Claude Code, Cursor, or another coding agent:
|
||||
Want the whole thing — local brain, 52 skills, the overnight dream cycle that enriches while you sleep? Paste this into Codex, Claude Code, Cursor, or another coding agent:
|
||||
|
||||
```
|
||||
Retrieve and follow the instructions at:
|
||||
@@ -140,7 +142,7 @@ Postgres-at-scale, Supabase, and thin-client setup paths live in [`docs/INSTALL.
|
||||
|
||||
### Connect GBrain to your AI client (MCP)
|
||||
|
||||
GBrain exposes 30+ tools over MCP (stdio and HTTP). The specific snippet depends on which client you use:
|
||||
GBrain exposes 110 tools over MCP (stdio and HTTP) — or exactly the five memory verbs with `--surface verbs`. The specific snippet depends on which client you use:
|
||||
|
||||
- **[Claude Code](docs/mcp/CLAUDE_CODE.md)** — local: one command, `claude mcp add gbrain -- gbrain serve` (zero server, zero tunnel). Remote with just a bearer token: `gbrain connect https://your-host/mcp --token gbrain_xxx` prints a paste-ready block (or `--install` wires it up and smoke-tests the token).
|
||||
- **[Codex](docs/mcp/CODEX.md)** — `gbrain connect https://your-host/mcp --token gbrain_xxx --agent codex` (or `--install`). Codex reads the bearer from `$GBRAIN_REMOTE_TOKEN` at runtime, so the token never lands in Codex config.
|
||||
|
||||
@@ -1,5 +1,59 @@
|
||||
# TODOS
|
||||
|
||||
## #2416 follow-ups (query-steering wave)
|
||||
|
||||
- [ ] **P2 — MCP-envelope `hint` field for concept-shaped `search` calls.**
|
||||
**What:** surface the concept→query nudge to remote/MCP agent callers, not
|
||||
just the CLI. **Why:** MCP agents are the primary misrouting class the
|
||||
#2416 issue describes; the shipped CLI stderr nudge covers the caller class
|
||||
*least* at risk. **Context:** the `search` op returns a bare
|
||||
`SearchResult[]` (`src/core/operations.ts` — both return sites), so a hint
|
||||
needs an envelope change that ripples into `formatResult`, MCP
|
||||
serialization, and array-shape tests — deliberately kept out of the atomic
|
||||
#2416 commit. The pure classifier already exists
|
||||
(`looksConceptShaped`/`conceptNudge` in `src/core/search/query-intent.ts`);
|
||||
only the transport is missing. Consider a sibling metadata channel (like
|
||||
`_meta.metric_glossary`) rather than changing the array shape.
|
||||
**Depends on:** agreeing an envelope pattern that doesn't break existing
|
||||
MCP consumers.
|
||||
|
||||
## MEMORY_VERBS v1 follow-ups (filed v0.43.0.0 — Cathedral 1)
|
||||
|
||||
Deferred from the Cathedral 1 ship (CEO review, EXPANSION mode). Both are
|
||||
additive to the frozen v1 contract — neither breaks it. See plan + GSTACK
|
||||
REVIEW REPORT at `~/.claude/plans/system-instruction-you-are-working-agile-iverson.md`
|
||||
and the scope record at `~/.gstack/projects/garrytan-gbrain/ceo-plans/2026-06-12-memory-verbs-protocol.md`.
|
||||
|
||||
- [ ] **P3 — external-implementation certification PROGRAM.** The conformance
|
||||
TOOLING shipped (`gbrain protocol conformance --target <endpoint>`); the
|
||||
PROGRAM around it (badges, a registry of conformant implementations, listed
|
||||
third-party servers) waits for a second implementation to exist. **Why:** the
|
||||
protocol-not-product thesis only pays off once someone else implements
|
||||
MEMORY_VERBS; until then a certification program certifies an empty set.
|
||||
**Where:** new — would build on `src/commands/protocol.ts` conformance output.
|
||||
- [ ] **P3 — persistent open-threads model for the entity card.** v1 derives
|
||||
`entity.open_threads` from active commitment-kind facts + recent timeline
|
||||
entries (best-effort, possibly empty). A richer model (a real threads table:
|
||||
conversation id, opened/closed state, last activity) would make open-threads
|
||||
authoritative. **Why:** the card's open-threads field is the weakest signal
|
||||
in v1; a first-class threads store would make it load-bearing. **Where:**
|
||||
`src/core/verbs/entity-card.ts` open-threads assembly + a new schema table
|
||||
(additive — the card field already exists, so this is a quality upgrade, not
|
||||
a contract change).
|
||||
- [ ] **P2 — `recall` filter composition vs the spec (found by the v0.43.0.0
|
||||
cross-model doc review).** The handler dispatch is first-match
|
||||
(`supersessions` > `entity` > `session_id` > `since`), so `since` is
|
||||
silently ignored when `entity`/`session_id` is supplied, and `limit` has no
|
||||
server-side cap. Either compose the filters (additive — the spec's "filters
|
||||
the FACTS arm" wording already reads that way) or spell the precedence out
|
||||
in `docs/protocol/MEMORY_VERBS_v1.md`. **Where:** the `recall` handler in
|
||||
`src/core/operations.ts`.
|
||||
- [ ] **P3 — widen `synthesize`'s `unavailable` mapping.** Only the
|
||||
missing-key gateway warning maps to the `unavailable` error today; other
|
||||
no-usable-model failures can surface as `internal` (contract-legal but less
|
||||
actionable) or, worst case, a stubbed success. Audit the gateway failure
|
||||
modes and map every model-unusable path to `unavailable` with a fix
|
||||
suggestion. **Where:** the `synthesize` handler in `src/core/verbs.ts`.
|
||||
## Fix-wave 1 follow-ups (upgrade-wedge + trust-seam wave, 2026-08)
|
||||
|
||||
Deferred from the un-wedge-v121 hotfix wave (eng review + codex outside voice
|
||||
@@ -135,6 +189,7 @@ voice CLEARED). None block the wave.
|
||||
clause (`src/mcp/http-transport.ts` validateToken). Apply the same pattern —
|
||||
one fewer write per request on the `serve --http` hot path.
|
||||
Where: `src/core/oauth-provider.ts`.
|
||||
|
||||
## v0.42.67.0 follow-ups (Windows build tooling)
|
||||
|
||||
Filed as follow-ups from v0.42.67.0 (`.gitattributes` LF pin for `*.sh` +
|
||||
|
||||
+5
-4
@@ -9,7 +9,7 @@ Already running [OpenClaw](https://github.com/garrytan/openclaw) or [Hermes](htt
|
||||
```bash
|
||||
bun install -g github:garrytan/gbrain
|
||||
gbrain init --pglite # 2 seconds; no server
|
||||
gbrain skillpack scaffold --all # 43 skills scaffolded into your agent workspace
|
||||
gbrain skillpack scaffold --all # 52 skills scaffolded into your agent workspace
|
||||
gbrain doctor # green checks all the way down
|
||||
```
|
||||
|
||||
@@ -58,16 +58,17 @@ gbrain autopilot --install # background daemon for nightly enrichment
|
||||
**Wire this same local brain into your coding agent** — zero server, zero token:
|
||||
|
||||
```bash
|
||||
claude mcp add gbrain -- gbrain serve # Claude Code
|
||||
codex mcp add gbrain -- gbrain serve # Codex
|
||||
claude mcp add gbrain -- gbrain serve --surface verbs # Claude Code
|
||||
codex mcp add gbrain -- gbrain serve --surface verbs # Codex
|
||||
```
|
||||
|
||||
The agent spawns `gbrain serve` as a stdio subprocess against your local brain. Full walkthrough (both this local path and connecting to a remote brain), plus the brain-first protocol to paste into `CLAUDE.md` / `AGENTS.md`: **[Give your coding agent a memory](tutorials/connect-coding-agent.md)**.
|
||||
The agent spawns `gbrain serve` as a stdio subprocess against your local brain. `--surface verbs` gives the agent the five-verb memory protocol (`recall`, `remember`, `entity`, `synthesize`, `forget` — [MEMORY_VERBS v1](protocol/MEMORY_VERBS_v1.md)) instead of the full tool catalog; drop the flag (default `full`) for every operation. Full walkthrough (both this local path and connecting to a remote brain), plus the brain-first protocol to paste into `CLAUDE.md` / `AGENTS.md`: **[Give your coding agent a memory](tutorials/connect-coding-agent.md)**.
|
||||
|
||||
## 3. MCP server (any MCP client)
|
||||
|
||||
```bash
|
||||
gbrain serve # stdio MCP (Claude Desktop / Code / Cursor)
|
||||
gbrain serve --surface verbs # stdio MCP, just the 5 memory verbs (quickstart)
|
||||
gbrain serve --http # HTTP MCP with OAuth 2.1 + admin dashboard
|
||||
```
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ per-release `**vX.Y.Z:**` narration — CI enforces this
|
||||
- `src/commands/code-def.ts` + `src/commands/code-refs.ts` — symbol definition + references lookup. Query `content_chunks.symbol_name` or chunk_text ILIKE with `page_kind='code'` filter. Auto-JSON when stdout is not a TTY (gh-CLI convention). Bypass the standard `searchKeyword` `DISTINCT ON (slug)` collapse so multiple call-sites from the same file surface. The JSON envelope (CLI + the `code_def`/`code_refs` MCP ops) carries `status` + `ready` from `src/core/code-graph-readiness.ts` so a `count:0` result is distinguishable as `not_built` (no code indexed) vs `ready` (genuinely no match); human output prints a one-line hint when not ready.
|
||||
- `src/core/code-graph-readiness.ts` — typed readiness signal shared by the four code-* surfaces (`code-def`/`code-refs`/`code-callers`/`code-callees`). `resolveCodeReadiness(engine, {kind:'symbol'|'edge', count, sourceId?, allSources?})` returns `{status:'not_built'|'indexing'|'ready'|'unknown', ready, has_code, pending_edges}`. `count>0` short-circuits to `ready` with no query; on empty it runs `EXISTS` probes against `content_chunks` JOIN `pages` (`page_kind='code'`) — no `page_kind` index needed, and the pending probe rides the partial `idx_content_chunks_edges_backfill`. `kind:'symbol'` (code-def/refs) is 2-state + brain-wide because symbol metadata is set at chunk time; `kind:'edge'` (code-callers/callees) is 3-state + source-scoped, with the pending predicate mirroring the resolver (`edges_backfilled_at IS NULL OR < EDGE_EXTRACTOR_VERSION_TS` from `src/core/chunkers/symbol-resolver.ts`) so a resolver-version bump never falsely reports `ready`. Probe scope matches each command's result-query `deleted_at` posture (def/refs don't filter `deleted_at`, so neither do the probes). Any DB error returns `status:'unknown'` (fail-open; never breaks the command). `readinessHint(r)` renders the human one-liner. Wired into `code-def.ts`/`code-refs.ts` (brain-wide), `code-callers.ts`/`code-callees.ts` (resolved `sourceId`/`allSources`), and all four `code_*` MCP op handlers in `src/core/operations.ts`. Pinned by `test/code-graph-readiness.test.ts` + readiness-envelope cases in `test/e2e/code-intel-mcp-ops-pglite.test.ts`.
|
||||
- `src/core/search/` — Hybrid search: vector + keyword + RRF + multi-query expansion + dedup. `searchKeyword`/`searchKeywordChunks`/`searchVector` apply source-aware ranking at the SQL layer (curated content like `originals/`, `concepts/`, `writing/` outranks bulk content like `<fork>/chat/`, `daily/`, `media/x/`). `searchVector` uses a two-stage CTE so source-boost re-ranking doesn't kill the HNSW index. Hard-exclude prefixes (`test/`, `archive/`, `attachments/`, `.raw/` by default) filter at retrieval, not post-rank. Both gates honor `detail !== 'high'` so temporal queries surface chat pages normally.
|
||||
- `src/core/search/intent.ts` — Query intent classifier (entity/temporal/event/general → auto-selects detail level).
|
||||
- `src/core/search/query-intent.ts` — Deterministic query classifiers (pure, no LLM): `classifyQuery`/`classifyQueryIntent` (entity/temporal/event/general → auto-selects detail level, salience/recency/modality axes), `isAmbiguousModalityQuery` (LLM-escalation gate), and the #2416 concept-shape pair — `looksConceptShaped` (fuzzy-quantifier/landscape cues minus exact-identifier anti-signals, tuned to favor false-negatives; cues owned by other routers like "who are the"/find_experts and bare "anything"/salience are deliberately excluded) + `conceptNudge` (full one-line CLI hint string steering a concept-shaped `search` toward `query`; consumed by `maybePrintConceptNudge` in `src/cli.ts` on BOTH the local-engine and thin-client result paths, stderr-only, `--quiet`-gated). Pinned by `test/query-intent-concept.test.ts` + `test/cli-concept-nudge.test.ts`.
|
||||
- `src/core/search/eval.ts` — Retrieval eval harness: P@k, R@k, MRR, nDCG@k metrics + runEval() orchestrator.
|
||||
- `src/core/search/source-boost.ts` — Source-type boost map keyed by slug prefix. `DEFAULT_SOURCE_BOOSTS` (originals/ 1.5, concepts/ 1.3, writing/ 1.4, people/companies/deals/ 1.2, daily/ 0.8, media/x/ 0.7, <fork>/chat/ 0.5, archive/ 0.5, extracts/ 0.3) and `DEFAULT_HARD_EXCLUDES` (test/, attachments/, .raw/). `archive/` is DEMOTED (findable, ranked below curated), not hard-excluded — archive holds high-signal history users expect to retrieve; the demote is a prior at the SQL/fusion layer and the cross-encoder reranker can still promote a strongly-matching archive page. `parseSourceBoostEnv`/`parseHardExcludesEnv` parse comma-separated `prefix:factor` pairs from `GBRAIN_SOURCE_BOOST`/`GBRAIN_SEARCH_EXCLUDE`. `resolveBoostMap` and `resolveHardExcludes` merge defaults + env + caller `SearchOpts.exclude_slug_prefixes`/`include_slug_prefixes`. The surviving exclude policy is auditable via the `hidden_by_search_policy` doctor check (`src/commands/doctor.ts`, local + remote paths) which counts chunked pages withheld per active exclude prefix, reusing `resolveHardExcludes` + `buildVisibilityClause` + the exported `escapeLikePattern`.
|
||||
- `src/core/search/sql-ranking.ts` — Pure SQL string builders. `buildSourceFactorCase(slugColumn, boostMap, detail)` emits a CASE with longest-prefix-match wins (returns literal `'1.0'` when `detail === 'high'` for temporal-bypass parity with COMPILED_TRUTH_BOOST). `buildHardExcludeClause(slugColumn, prefixes)` emits `NOT (col LIKE 'p1%' OR col LIKE 'p2%')` — OR-chain wrapped in NOT, NOT `NOT LIKE ALL/ANY` (those quantifiers don't express set-exclusion). LIKE meta-character escape covers all three of `%`, `_`, AND `\` (backslash is Postgres LIKE's default escape char). Single-quote doubling on SQL string literals so injection-style inputs are inert text. `buildBestPerPagePoolCte(...)` is the shared per-page max-pool CTE both engines' `searchVector` inject — instead of returning the single best chunk per page from an inner `ORDER BY embedding <=> vec LIMIT N` (which let a page lose to a neighbor on ONE weak chunk while its strong chunk sat just below the inner cut), the CTE pools the BEST chunk score per `(source_id, slug)` composite key so a page surfaces on its strongest evidence; composite key (not bare slug) keeps multi-source brains correct; single source of truth so the two engines can't drift.
|
||||
@@ -276,6 +276,13 @@ per-release `**vX.Y.Z:**` narration — CI enforces this
|
||||
- `src/core/minions/plugin-loader.ts` — `GBRAIN_PLUGIN_PATH` discovery. Absolute paths only, left-wins collision, `gbrain.plugin.json` with `plugin_version: "gbrain-plugin-v1"`, plugins ship DEFS only (no new tools), `allowed_tools:` validated at load time against the derived registry.
|
||||
- `src/core/minions/tools/brain-allowlist.ts` — derives the subagent tool registry from `src/core/operations.ts` (15-name allow-list, size pinned by `test/brain-allowlist.serial.test.ts`). Includes `add_timeline_entry` (the canonical timeline write), fenced server-side by the same `enforceSubagentSlugFence` policy as `put_page`. By default `put_page` schema is namespace-wrapped per subagent (`^wiki/agents/<subagentId>/.+`). When `BuildBrainToolsOpts.allowedSlugPrefixes` is set, the put_page schema describes the prefix list to the model and the OperationContext is threaded with `allowedSlugPrefixes` — trust comes from `PROTECTED_JOB_NAMES` gating subagent submission (MCP cannot reach this field); only cycle.ts (synthesize/patterns) and direct CLI submitters set it. Allow-list includes `get_recent_salience` + `find_anomalies` but deliberately NOT `get_recent_transcripts` (all subagent calls run `ctx.remote === true` and the trust gate rejects remote callers, so it would always reject; the cycle synthesize phase calls `discoverTranscripts` directly instead). `paramsToInputSchema()` consumes `paramDefToSchema` from `src/mcp/tool-defs.ts`; required-aggregation at the tool-def level stays here (the shared helper is per-param).
|
||||
- `src/mcp/tool-defs.ts` — `buildToolDefs(ops)` helper; MCP server + subagent tool registry both call it, byte-for-byte equivalence pinned by `test/mcp-tool-defs.test.ts`. Exports the recursive `paramDefToSchema(p: ParamDef)` — single source of truth for ParamDef→JSON Schema mapping shared by three consumers: `buildToolDefs` (stdio MCP), `src/commands/serve-http.ts:837` (HTTP MCP `tools/list`), and `src/core/minions/tools/brain-allowlist.ts:84` (subagent registry). Recursive on `items` so nested array-of-arrays preserves inner shape on the wire. Key ordering (type, description, enum, default, items) is intentional so `JSON.stringify` output stays byte-stable. `test/mcp-tool-defs.test.ts` has a `findArrayWithoutItems` walker that fails on any `type: 'array'` lacking `items.type`.
|
||||
- `src/core/verbs.ts` — MEMORY_VERBS v1 (Cathedral 1): the four new frozen protocol verbs (`remember`, `entity`, `synthesize`, `forget`) as first-class Operations, plus `MEMORY_VERBS_VERSION` (single source of truth, =1), `VERB_NAMES`, the hand-authored `RESPONSE_SCHEMAS` registry (Operation carries input params only; response shapes live here and conformance validates LIVE responses against them), and `ERROR_SCHEMA`. The fifth verb is the extended `recall` op in operations.ts. RUNTIME LEAF invariant: operations.ts spreads `verbOperations` into its array at module-eval time, so this file must never statically import operations.ts VALUES (type-only imports fine; handlers use dynamic import) — violating it reintroduces a TDZ crash on whichever module evaluates second. Every verb error carries a populated `suggestion` + `protocol_version` (via `verbError` in operations.ts). The `forget` verb deliberately has NO cliHints (CLI_ONLY `forget` dispatches first and would shadow it). Frozen contract: docs/protocol/MEMORY_VERBS_v1.md; pinned by test/memory-verbs-conformance.test.ts.
|
||||
- `src/core/verbs/entity-card.ts` — `buildEntityCard(engine, sourceId, name, {remote})`: the zero-LLM sub-100ms card behind the `entity` verb. Resolution reuses the Retrieval Reflex arms (alias > exact title/slug > slug-suffix; exact-slug candidates include the RAW input because slugify flattens slashes in namespaced slugs); ties break on GREATEST(updated_at, last_retrieved_at). Per-arm degradation: a pre-page_aliases brain still resolves via arm 2 (aka returns empty, never throws). Card assembly is a parallel Promise.all of depth-1 indexed reads (page row, alias reverse lookup, getLinks+getBacklinks mentions-excluded cap 10, getBacklinkCounts, getTimeline(5), listFactsByEntity world-only-when-remote); deliberately NOT the recursive-CTE traversePaths — the card is a latency contract (CI gate: test/entity-card-perf.slow.test.ts, p99 < 100ms × GBRAIN_PERF_BUDGET_MULTIPLIER + 50× getPage-p50 ratio guard on a 20K corpus). `summary` runs through the exported `safeSynopsis` (the get_page fence boundary). Miss → keyword near-miss suggestions with create_safety hints.
|
||||
- `src/core/verbs/usage-log.ts` — E4 observability sidecar: one JSONL line per verb call at `~/.gbrain/integrations/memory-verbs/usage.jsonl` (gbrainPath — GBRAIN_HOME honored; `brainId()` = the resolved gbrain home). LOCAL ONLY, never uploaded, stats-only (lock-free 10MB rotation may drop lines; O_APPEND line-atomic, best-effort on Windows). `logVerbUsage` is fire-and-forget (never awaited, never throws); written from the DISPATCH layer so param-validation failures count. `readVerbUsage`/`earliestVerbUsageTs` feed `gbrain protocol stats` (incl. measured TTHW vs the init-stamped `protocol_installed_at`) and the doctor `memory_verbs_usage` check.
|
||||
- `src/core/verbs/conformance.ts` + `src/core/verbs/conformance-fixtures.ts` — the conformance runner core (transport-agnostic: minimal `ConformanceClient` = list_tools + call_tool) and the embedded fixture set. Deterministic by construction: shape/enum/behavior/round-trip checks only, never ranking quality. Validation is NON-STRICT on extra fields (additive-forever means unknown fields are always legal). Entity-card cases seed via put_page when the target exposes it and skip honestly on verbs-only targets; synthesize is cost-gated behind --synthesize. `validateAgainstSchema` is a minimal JSON-Schema-subset validator (type unions, required, properties, enum, const, items). Fixtures mirror to `test/fixtures/memory-verbs/cases.json` (BrainBench seeds; drift-guarded by the conformance test). The negative self-test (test/memory-verbs-conformance.test.ts) proves the runner FAILS a lying server.
|
||||
- `src/core/facts/write-single.ts` — `writeSingleFact(fact, ctx)`: the zero-LLM single-fact seam behind `remember` [E1]. `runFactsPipeline` is extraction-first (LLM-gated) and cannot back a pre-formed fact; this reuses the pipeline's post-extraction stages directly: resolve → embedding-cosine dedup (same 0.95 threshold) → fence-first write with the same legacy DB-only fallbacks (thin-client, unparented, stub-guard). Supersession [X1]: deterministic rule — same entity_slug + same kind + similarity ≥ threshold + DIFFERENT text ⇒ the new fact supersedes (fence path: append new + forgetFactInFence(old) + superseded_by link; DB path: engine insertFact supersedeId). Provenance lands on NewFact.source verbatim (no FactsBackstopCtx). No embedding provider ⇒ `degraded_dedup: true` (near-duplicates may insert — documented).
|
||||
- `src/mcp/surface.ts` — MCP tool-surface modes: `'verbs'` (exactly the ops marked `verb: true`) | `'full'` (default — identity; existing installs unchanged). `parseSurfaceFlag` (strict, loud reject), `resolveSurface` (flag > config `mcp_surface` > full), `filterOpsForSurface`, `allowedOpNames`. Enforcement is TWO-layer and fail-closed: the advertised list AND `dispatchToolCall`'s `allowedOps` set (a hidden op returns `unknown_tool` even when called by name) — applied on stdio (server.ts) and BOTH HTTP paths (serve-http.ts after `!localOnly`, and the second transport http-transport.ts). Pinned by test/mcp-surface.test.ts.
|
||||
- `src/commands/protocol.ts` — `gbrain protocol [--json] | conformance [--target <http-url|stdio-cmd>] [--token] [--synthesize] | stats [--days N]`. `--json` emits input schemas from the LIVE Operation defs + RESPONSE_SCHEMAS (doc/code can't drift). Conformance default target self-spawns gbrain's own stdio server (dev `.ts` entry vs compiled binary both handled); CI certifies stdio with --synthesize (no key ⇒ asserts the clean `unavailable` error). Stats aggregates the usage sidecar + the measured TTHW; output states "local JSONL only — never uploaded". CLI_ONLY + SELF_HELP wired in cli.ts; no pre-bound engine.
|
||||
- `src/core/minions/attachments.ts` — Attachment validation (path traversal, null byte, oversize, base64, duplicate detection).
|
||||
- `src/commands/agent.ts` — `gbrain agent run <prompt> [flags]` CLI. Submits `subagent` (or N children + 1 aggregator) under `{allowProtectedSubmit: true}`. Single-entry `--fanout-manifest` short-circuits. Children get `on_child_fail: 'continue'` + `max_stalled: 3`. `--follow` is the default on TTY; streams logs + polls `waitForCompletion` in parallel. Ctrl-C detaches, does not cancel.
|
||||
- `src/commands/agent-logs.ts` — `gbrain agent logs <job> [--follow] [--since]`. Merges JSONL heartbeat audit + `subagent_messages` into a chronological timeline. `parseSince` accepts ISO-8601 or relative (`5m`, `1h`, `2d`). Transcript tail renders only for terminal jobs.
|
||||
|
||||
@@ -97,7 +97,11 @@ Promote or reject them via `gbrain extraction-pending` / `gbrain
|
||||
extraction-review`.
|
||||
|
||||
The `search` MCP/CLI op is **cheap-hybrid** (vector + keyword + RRF + pool +
|
||||
title + alias, expansion off); `query` is the full-control variant. NamedThingBench
|
||||
title + alias, expansion off); `query` is the full-control variant. Route
|
||||
concept / landscape / "all-of-X" questions to `query` — expansion recovers
|
||||
synonym-phrased matches `search` can miss, and a populated `search` result set
|
||||
is not proof of coverage (both are top-K; exhaustive enumeration belongs to
|
||||
`list_pages`). NamedThingBench
|
||||
(`gbrain eval retrieval-quality`) gates these families on every PR. Diagnose a
|
||||
specific miss with `gbrain search diagnose "<q>" --target <slug>`.
|
||||
|
||||
@@ -116,7 +120,7 @@ The classifier is deterministic (no LLM call). Wrong classification degrades gra
|
||||
|
||||
For `detail: 'high'` searches, `src/core/search/expansion.ts` runs a Haiku-class LLM call to produce 2-3 query variants. Each variant runs through the full hybrid stack; results merge via RRF. Catches synonym misses without recall loss.
|
||||
|
||||
Expansion is opt-in per mode bundle (`tokenmax` on by default; `balanced` + `conservative` off). Default off in the cheap tiers because the LLM call adds ~$0.001/query and ~200ms — real money at scale.
|
||||
Expansion is opt-in per mode bundle (`tokenmax` on by default; `balanced` + `conservative` off). Default off in the cheap tiers because the LLM call adds ~$0.001/query and ~200ms — real money at scale. The `query` op is the exception: it defaults `expand: true` per call (pass `expand: false` to opt out) — expansion-by-default is what makes it the concept/landscape verb.
|
||||
|
||||
## Putting it together
|
||||
|
||||
|
||||
+28
-19
@@ -15,40 +15,48 @@ on user_asks_about(topic):
|
||||
if know_exact_slug(topic):
|
||||
# MODE 3: Direct get -- instant, no search overhead
|
||||
result = gbrain get <slug>
|
||||
# e.g., "Tell me about Pedro" -> gbrain get pedro-franceschi
|
||||
# e.g., "Tell me about Alice" -> gbrain get alice-example
|
||||
# Returns the FULL page -- compiled truth + timeline
|
||||
|
||||
elif topic.is_exact_name or topic.is_keyword:
|
||||
# MODE 1: Keyword search -- fast, no embeddings needed, day-one ready
|
||||
# MODE 1: Cheap-hybrid search -- vector + keyword + RRF, NO LLM
|
||||
# expansion. Embeds the query when embeddings are configured; the
|
||||
# keyword arm still works day-one without them (keyword-only is
|
||||
# also available via the search.mcp_keyword_only opt-out).
|
||||
results = gbrain search "{name_or_keyword}"
|
||||
# e.g., "Find anything about Series A" -> gbrain search "Series A"
|
||||
# Returns CHUNKS, not full pages
|
||||
|
||||
# IMPORTANT: keyword search returns chunks
|
||||
# IMPORTANT: search returns chunks
|
||||
# If the chunk confirms relevance, THEN load the full page:
|
||||
if chunk.confirms_relevance:
|
||||
full_page = gbrain get <slug_from_chunk>
|
||||
|
||||
elif topic.is_semantic_question:
|
||||
# MODE 2: Hybrid search -- semantic + keyword, needs embeddings
|
||||
elif topic.is_semantic_question or topic.is_concept_or_landscape:
|
||||
# MODE 2: Full hybrid -- adds multi-query LLM expansion on top of
|
||||
# vector + keyword + RRF. Owns concept / landscape / "all-of-X"
|
||||
# questions: expansion recovers synonym- and outcome-phrased
|
||||
# matches a single embedding misses. Costs one LLM expansion call
|
||||
# per query -- worth it for these question shapes.
|
||||
results = gbrain query "{natural language question}"
|
||||
# e.g., "Who do I know at fintech companies?" -> gbrain query "fintech contacts"
|
||||
# Returns ranked chunks via vector + keyword + RRF
|
||||
# e.g., "all the companies doing offshore wind" -> gbrain query "..."
|
||||
# Returns ranked chunks via vector + keyword + expansion + RRF
|
||||
|
||||
# Same rule: chunks first, then get full page if needed
|
||||
if chunk.confirms_relevance:
|
||||
full_page = gbrain get <slug_from_chunk>
|
||||
|
||||
# Quick reference:
|
||||
# | Mode | Command | Needs Embeddings | Speed | Best For |
|
||||
# |---------|----------------------|------------------|---------|---------------------------------|
|
||||
# | Keyword | gbrain search "term" | No | Fastest | Known names, exact matches |
|
||||
# | Hybrid | gbrain query "..." | Yes | Fast | Semantic questions, fuzzy match |
|
||||
# | Direct | gbrain get <slug> | No | Instant | When you know the slug |
|
||||
# | Mode | Command | Needs Embeddings | Speed | Best For |
|
||||
# |-------------|----------------------|------------------|---------|-------------------------------------------|
|
||||
# | Cheap-hybrid| gbrain search "term" | Uses if present | Fastest | Known names, exact tokens |
|
||||
# | Full hybrid | gbrain query "..." | Yes | Fast | Concept / landscape / "all-of-X", synonyms |
|
||||
# | Direct | gbrain get <slug> | No | Instant | When you know the slug |
|
||||
|
||||
# Progression over time:
|
||||
# Day 1: keyword search (works without embeddings)
|
||||
# After first embed: hybrid search unlocked
|
||||
# Day 1: search (keyword arm works without embeddings)
|
||||
# After first embed: vector arm + full hybrid (query) unlocked
|
||||
# Once you know slugs: direct get for speed
|
||||
|
||||
# Precedence for conflicting information within a page:
|
||||
@@ -61,16 +69,17 @@ on user_asks_about(topic):
|
||||
## Tricky Spots
|
||||
|
||||
1. **Search returns chunks, not full pages.** After `gbrain search` or `gbrain query`, you get excerpts. Always run `gbrain get <slug>` to load the full page when the chunk confirms relevance. Don't answer questions from chunks alone when the full context matters.
|
||||
2. **Keyword search works without embeddings.** On day one before any embedding run, `gbrain search` still works. Don't tell the user "search isn't available yet" -- keyword search is always available.
|
||||
3. **Don't use hybrid search for known names.** `gbrain query "Pedro Franceschi"` wastes embedding compute. Use `gbrain search "Pedro Franceschi"` or better yet `gbrain get pedro-franceschi` if you know the slug.
|
||||
4. **Token budget awareness.** A full page via `gbrain get` can be large. Read the search chunks first to confirm relevance before pulling the full page. "Did anyone mention the Series A?" -- search results (chunks) are probably enough. "Tell me everything about Pedro" -- get the full page.
|
||||
5. **Hybrid search needs embeddings to have been run.** If `gbrain query` returns nothing but `gbrain search` finds results, the embeddings haven't been generated yet. Run the embedding pipeline first.
|
||||
2. **Search works without embeddings.** On day one before any embedding run, `gbrain search` still works (the keyword arm carries it; the vector arm joins once embeddings exist). Don't tell the user "search isn't available yet" -- search is always available.
|
||||
3. **Don't use full hybrid for known names.** `gbrain query "Alice Example"` wastes an LLM expansion call. Use `gbrain search "Alice Example"` or better yet `gbrain get alice-example` if you know the slug.
|
||||
4. **Token budget awareness.** A full page via `gbrain get` can be large. Read the search chunks first to confirm relevance before pulling the full page. "Did anyone mention the Series A?" -- search results (chunks) are probably enough. "Tell me everything about Alice" -- get the full page.
|
||||
5. **Full hybrid needs embeddings to have been run.** If `gbrain query` returns nothing but `gbrain search` finds results, the embeddings haven't been generated yet. Run the embedding pipeline first.
|
||||
6. **A populated `gbrain search` result set is not proof you found everything.** Search runs without query expansion, so synonym- and outcome-phrased matches can be missed even when it returns plenty of hits. For "find every / all / the landscape of" questions, use `gbrain query`; for literal exhaustive enumeration ("list every page of type X"), use `list_pages` pagination. A nonzero count is not a completeness signal.
|
||||
|
||||
## How to Verify
|
||||
|
||||
1. Run `gbrain search "Pedro"` -- confirm it returns chunks with matching text and slug references.
|
||||
1. Run `gbrain search "Alice"` -- confirm it returns chunks with matching text and slug references.
|
||||
2. Run `gbrain query "who works at fintech companies"` -- confirm it returns semantically relevant results (not just keyword matches on "fintech").
|
||||
3. Run `gbrain get pedro-franceschi` -- confirm it returns the full page with compiled truth and timeline.
|
||||
3. Run `gbrain get alice-example` -- confirm it returns the full page with compiled truth and timeline.
|
||||
4. Compare: search for the same entity using all three modes. Keyword should be fastest, hybrid should surface conceptual matches, direct should return the complete page.
|
||||
5. After a search returns a chunk, run `gbrain get` on the slug from that chunk. Confirm the full page contains more context than the chunk alone.
|
||||
|
||||
|
||||
+11
-3
@@ -8,12 +8,18 @@
|
||||
## Option 1: Local (recommended, zero server needed)
|
||||
|
||||
```bash
|
||||
claude mcp add gbrain -- gbrain serve
|
||||
claude mcp add gbrain -- gbrain serve --surface verbs
|
||||
```
|
||||
|
||||
That's it. Claude Code spawns `gbrain serve` as a stdio subprocess. No server, no
|
||||
tunnel, no token needed. Works with both PGLite and Supabase engines.
|
||||
|
||||
`--surface verbs` exposes the five-verb memory protocol (`recall`, `remember`,
|
||||
`entity`, `synthesize`, `forget` — [MEMORY_VERBS v1](../protocol/MEMORY_VERBS_v1.md)),
|
||||
the surface built for agents and quickstarts. Drop the flag for the full
|
||||
operation catalog (`get_page`, `put_page`, `search`, graph ops, …) — `full` is
|
||||
the default and what existing installs already run.
|
||||
|
||||
## Option 2: Remote, one command (fastest from a bearer token)
|
||||
|
||||
If GBrain is running somewhere as an HTTP server (`gbrain serve --http`, see the
|
||||
@@ -79,8 +85,10 @@ You should see results from your GBrain knowledge base.
|
||||
> **`list_skills` returns nothing?** Skill discovery is gated by `mcp.publish_skills`
|
||||
> on the host. New brains from `gbrain init` default it ON; brains upgraded from an
|
||||
> older release stay OFF until you opt in. Enable it on the host with
|
||||
> `gbrain config set mcp.publish_skills true`. The core tools (search, query,
|
||||
> get_page, put_page, think, find_experts) work regardless. Note: `capture` is a
|
||||
> `gbrain config set mcp.publish_skills true`. Skill discovery and the core tools
|
||||
> named here (search, query, get_page, put_page, think, find_experts) are
|
||||
> full-surface — on `--surface verbs` the agent sees only the five memory verbs,
|
||||
> and `list_skills` isn't on the surface at all. Note: `capture` is a
|
||||
> CLI-only command, not an MCP tool — the agent writes over MCP with `put_page`.
|
||||
|
||||
## Remove
|
||||
|
||||
+3
-1
@@ -68,4 +68,6 @@ codex mcp remove gbrain
|
||||
- The token is a long-lived, full-access secret. Keep `GBRAIN_REMOTE_TOKEN` out of
|
||||
version control and prefer a scoped token if your host supports one.
|
||||
- Local stdio also works if you run the brain on the same machine:
|
||||
`codex mcp add gbrain -- gbrain serve`.
|
||||
`codex mcp add gbrain -- gbrain serve --surface verbs` — the five-verb memory
|
||||
protocol ([MEMORY_VERBS v1](../protocol/MEMORY_VERBS_v1.md)); drop the flag
|
||||
for the full operation catalog.
|
||||
|
||||
+8
-3
@@ -5,8 +5,8 @@
|
||||
> dashboard at `/admin`, scoped operations, and a live SSE activity feed.
|
||||
> Pre-v0.26 legacy bearer tokens still work — `verifyAccessToken` falls back
|
||||
> to the `access_tokens` table and grandfathers tokens to `read+write+admin`.
|
||||
> Postgres-only for the legacy fallback (the `access_tokens` table is Postgres-only);
|
||||
> OAuth tables work on both PGLite and Postgres. See [SECURITY.md](../../SECURITY.md)
|
||||
> Both the legacy fallback and the OAuth tables work on PGLite and Postgres
|
||||
> (both engine schemas carry `access_tokens`). See [SECURITY.md](../../SECURITY.md)
|
||||
> for env vars and tunable defaults.
|
||||
|
||||
Access your brain from any device, any AI client. GBrain ships two transports:
|
||||
@@ -18,11 +18,16 @@ for remote clients over OAuth 2.1.
|
||||
### Local stdio (zero setup)
|
||||
|
||||
```bash
|
||||
gbrain serve
|
||||
gbrain serve # full operation catalog (default)
|
||||
gbrain serve --surface verbs # just the 5 memory verbs (quickstart surface)
|
||||
```
|
||||
|
||||
Works with Claude Code, Cursor, Windsurf, and any MCP client that supports stdio.
|
||||
No server, no tunnel, no token needed. Works on both PGLite and Postgres engines.
|
||||
`--surface verbs` exposes exactly the five-verb memory protocol (`recall`,
|
||||
`remember`, `entity`, `synthesize`, `forget` —
|
||||
[MEMORY_VERBS v1](../protocol/MEMORY_VERBS_v1.md)) instead of the full catalog;
|
||||
omit the flag (default `full`) for every operation.
|
||||
|
||||
### Remote over OAuth 2.1 (recommended, v0.26.0+)
|
||||
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
# MEMORY_VERBS v1 — the memory wire protocol
|
||||
|
||||
GBrain's frozen five-verb memory interface over MCP: `recall`, `remember`,
|
||||
`entity`, `synthesize`, `forget`. The contract every harness can rely on the
|
||||
way every Postgres client relies on the wire protocol — and the contract any
|
||||
OTHER memory server can implement and certify against
|
||||
(`gbrain protocol conformance --target <endpoint>`).
|
||||
|
||||
```
|
||||
agent (any MCP harness)
|
||||
│ remember("picked Stripe over Adyen", provenance: "chat 2026-06-11")
|
||||
▼
|
||||
five verbs ── recall ── remember ── entity ── synthesize ── forget
|
||||
│ self-describing envelopes: protocol_version, evidence, provenance,
|
||||
│ budget meta, cost block, enumerated error codes + a populated fix
|
||||
▼
|
||||
your brain (reference implementation: gbrain; any conformant server)
|
||||
```
|
||||
|
||||
**Machine-readable spec:** `gbrain protocol --json` emits the input schemas
|
||||
from the live operation definitions plus the response-shape registry — doc and
|
||||
code structurally cannot drift; conformance validates live responses against
|
||||
the same registry.
|
||||
|
||||
## Versioning policy (the point of the freeze)
|
||||
|
||||
- Every field NAME and its SEMANTICS in v1 are frozen forever — never removed,
|
||||
renamed, or re-typed; meanings never change.
|
||||
- New OPTIONAL params and new OPTIONAL response fields may be added at any
|
||||
time (additive-forever). A conformant CLIENT must ignore unknown fields; a
|
||||
conformant SERVER must never reject unknown-to-v1 additions it itself ships.
|
||||
- `protocol_version` (integer, starts at `1`) rides every verb response and
|
||||
every verb error. It increments ONLY on a breaking change, which by policy
|
||||
requires a new `MEMORY_VERBS_v2` document — expected never.
|
||||
- Conformance pins a minimum version; certification asserts shape, enum
|
||||
validity, contract behavior, and round-trips — never ranking quality (that
|
||||
is BrainBench's job).
|
||||
- Enum values are part of the contract. Where an enum's DERIVATION is
|
||||
implementation-defined (noted per field), implementations may improve the
|
||||
derivation without a version bump; the values and their meanings stay fixed.
|
||||
|
||||
## Install (the 4-command quickstart)
|
||||
|
||||
```bash
|
||||
gbrain init --pglite # 2-second local brain
|
||||
claude mcp add gbrain -- gbrain serve --surface verbs # the five-verb surface
|
||||
gbrain remember "I prefer dark mode in every editor" --provenance demo --entity people/me
|
||||
gbrain recall --entity people/me # …now ask your agent in a NEW session
|
||||
```
|
||||
|
||||
> Memories agents save are readable by every agent connected to this brain;
|
||||
> pass `visibility: "private"` for local-CLI-only facts.
|
||||
|
||||
If `claude` is not found: install Claude Code first, or use a block below.
|
||||
|
||||
**Codex**
|
||||
```bash
|
||||
codex mcp add gbrain -- gbrain serve --surface verbs
|
||||
```
|
||||
|
||||
**OpenClaw / any stdio MCP host** — register the server command
|
||||
`gbrain serve --surface verbs`. Remote brains: `gbrain serve --http` on the
|
||||
host, then `gbrain connect https://host/mcp --token gbrain_xxx --install` on
|
||||
each client.
|
||||
|
||||
**Surface modes:** `--surface verbs` exposes EXACTLY the five verbs —
|
||||
advertised list AND dispatch are filtered fail-closed (a hidden op returns
|
||||
`unknown_tool` even when called by name). `--surface full` (the default)
|
||||
exposes every operation, verbs included. Why default full: verbs is for
|
||||
agents and quickstarts; full preserves existing advanced tooling. Persist a
|
||||
default with `gbrain config set mcp_surface verbs`.
|
||||
|
||||
## The verbs
|
||||
|
||||
### recall(query?, entity?, budget_tokens?, since?, session_id?, limit?, …) — read
|
||||
|
||||
Retrieve saved facts and (with `query`) budget-packed page snippets.
|
||||
|
||||
- `entity` scopes the FACTS arm; `query` runs the hybrid-search arm over
|
||||
pages; both present ⇒ both arms run.
|
||||
- `since`: ISO 8601 date/datetime — filters the FACTS arm only in v1. (The
|
||||
reference implementation also accepts relative phrases like `"8 hours ago"`
|
||||
as a convenience; only ISO 8601 is part of the frozen contract.)
|
||||
- `limit` is a PER-ARM cap (facts and search results each).
|
||||
- `budget_tokens`: SERVER-side packing — facts pack first (limit-capped
|
||||
one-liners, so search-arm starvation is bounded), search results take the
|
||||
remainder. The estimator is char/4 (±10–15%); `budget_used` reports packed
|
||||
tokens, `dropped_count` what didn't fit. Never advisory, never client-side.
|
||||
- No embedding provider configured? The search arm degrades to keyword-only
|
||||
and the response notes `search_degraded` — never an error.
|
||||
|
||||
Response — an additive SUPERSET of the pre-v1 facts envelope on EVERY call
|
||||
(all legacy fields unchanged; JSON consumers ignore additions):
|
||||
|
||||
| field | type | semantics |
|
||||
|---|---|---|
|
||||
| `protocol_version` | int | always present (every verb, every call) |
|
||||
| `facts[]` | array | legacy fact fields unchanged, PLUS per fact: `fact_id` (opaque STRING — the value `forget` accepts; the legacy numeric `id` stays for pre-v1 consumers) and `provenance` (the stored source attribution) |
|
||||
| `total` | int | count of facts returned |
|
||||
| `results[]` | array | search arm only: `slug`, `title`, `chunk`, `evidence`, `create_safety`, `provenance` (origin page slug) |
|
||||
| `search_degraded` | string? | present when keyword-only fallback fired |
|
||||
| `budget_tokens` / `budget_used` / `dropped_count` | int? | present when `budget_tokens` was passed |
|
||||
|
||||
**evidence** (enum, zero-LLM heuristic): `alias_hit` \| `exact_title_match` \|
|
||||
`high_vector_match` \| `keyword_exact` \| `weak_semantic` — why each result
|
||||
matched. **create_safety** (enum): `exists` (a page for this already exists)
|
||||
\| `probable` (likely exists; check before creating) \| `unknown` (no
|
||||
signal). The derivation of both is implementation-defined and may improve;
|
||||
the values are frozen.
|
||||
|
||||
### remember(fact, provenance, ttl?, entity?, kind?, visibility?) — write
|
||||
|
||||
Save ONE fact with mandatory attribution.
|
||||
|
||||
- `provenance` (REQUIRED, free text ≤500 chars, stored verbatim): e.g.
|
||||
`"conversation 2026-06-12"`, `"user said in chat"`, `"import: notes.md"`.
|
||||
Empty ⇒ `provenance_required` error with a fix.
|
||||
- `entity`: set whenever the fact is about a specific person/company/project —
|
||||
entity-scoped recall will not find unattributed facts.
|
||||
- `ttl`: duration shorthand (`"30d"`, `"12h"`, `"45m"`) or an absolute ISO 8601
|
||||
timestamp. ISO-8601 DURATIONS (`P30D`) are rejected with a self-correcting
|
||||
suggestion. Omitted ⇒ never expires.
|
||||
- `kind`: `event` \| `preference` \| `commitment` \| `belief` \| `fact`
|
||||
(default).
|
||||
- `visibility`: `world` (DEFAULT — readable by every agent connected to this
|
||||
brain; required for the remote remember→recall round-trip) \| `private`
|
||||
(local CLI reads only). The init quickstart carries the consent line.
|
||||
|
||||
Response: `{ id, status, status_text, entity_slug, valid_until,
|
||||
protocol_version }` (+ `degraded_dedup: true` when no embedding provider —
|
||||
near-duplicates may insert; dedup and supersession ride embedding similarity).
|
||||
|
||||
- `id` — opaque STRING (gbrain serializes integers; another implementation may
|
||||
use UUIDs). On `status: "duplicate"` it is the EXISTING fact's id.
|
||||
- `status` — `inserted` \| `duplicate` \| `superseded`. **Branch on `status`,
|
||||
never on `status_text`** (the human rendering). Supersession is
|
||||
implementation-defined; the reference rule: same entity + same kind +
|
||||
similarity above the dedup threshold + different text = the new fact
|
||||
supersedes the old ("X at acme-example" → "X left acme-example").
|
||||
- Omitted optional inputs echo as `null`, never absent.
|
||||
|
||||
### entity(name) — read, zero LLM, p99 < 100ms
|
||||
|
||||
One known person/company/project card. NEVER errors on a miss.
|
||||
|
||||
Resolution (frozen precedence): alias > exact title > slug/slug-suffix; ties
|
||||
break on most-recently-touched. Multi-hit ⇒ best match's card + runners-up in
|
||||
`suggestions`. Miss ⇒ `found: false` + keyword near-misses with
|
||||
`create_safety` hints.
|
||||
|
||||
Response: `{ protocol_version, found, latency_ms, card?, suggestions? }`.
|
||||
Card: `{ entity{slug,title,type}, aka[], summary, last_touched{updated_at,
|
||||
last_retrieved_at, last_timeline_date}, open_threads[], edges[],
|
||||
backlink_count, active_fact_count }`.
|
||||
|
||||
- `summary` passes the same privacy fences as `get_page` (takes + private
|
||||
facts stripped); remote callers never see private facts in the card.
|
||||
- `open_threads` (best-effort in v1): active commitment-kind facts + timeline
|
||||
entries from the last 90 days, capped at 3.
|
||||
- `edges`: top ~10 typed edges, mentions excluded, out-edges first.
|
||||
- The p99 < 100ms promise is op-layer latency (transport excluded), CI-gated
|
||||
on a 20K-page corpus. 200K validation recipe below.
|
||||
|
||||
### synthesize(question, since?, until?) — read, EXPENSIVE
|
||||
|
||||
`[EXPENSIVE / SLOW — makes LLM calls, seconds-to-minutes latency, costs
|
||||
money]` — the deliberately-priced slow verb. Prefer `recall`/`entity` for
|
||||
lookups; use synthesize only when the answer requires combining evidence
|
||||
across pages.
|
||||
|
||||
Response: `{ answer, sources[], gaps[], cost{model, input_tokens,
|
||||
output_tokens, usd_estimate}, protocol_version }`.
|
||||
|
||||
- The `cost` block is a BEST-EFFORT AGGREGATE (retries/multi-call flows sum;
|
||||
cache hits may undercount; token fields are `null` when a provider returns
|
||||
no accounting). Honest signal, not an invoice.
|
||||
- No LLM configured ⇒ the protocol error `unavailable` with a fix — never a
|
||||
fake answer.
|
||||
|
||||
### forget(id, reason?) — write
|
||||
|
||||
Expire a fact by its opaque string id (from `remember` or
|
||||
`recall.facts[].fact_id` — never a page slug). Idempotent: re-forgetting an
|
||||
already-expired fact returns `expired: false` (success); unknown id ⇒
|
||||
`not_found`. Facts are expired with an audit trail, never deleted.
|
||||
|
||||
Response: `{ id, expired, reason, protocol_version }`.
|
||||
|
||||
## Error contract (uniform across all five verbs)
|
||||
|
||||
```json
|
||||
{ "error": "<code>", "message": "...", "suggestion": "problem + cause + fix",
|
||||
"detail": "freeform specifics", "protocol_version": 1 }
|
||||
```
|
||||
|
||||
Codes (coarse on purpose — codes are for branching; `detail` carries the
|
||||
story): `invalid_params`, `provenance_required`, `not_found`, `scope_denied`,
|
||||
`unavailable` (a required dependency cannot serve: no API key, gateway down,
|
||||
model refusal — configure/retry, not a server bug), `budget_unsatisfiable`
|
||||
(RESERVED — schema-listed, never returned in v1), `internal`.
|
||||
|
||||
Every verb error carries a POPULATED `suggestion`. Specific cases: `recall` on
|
||||
an empty brain returns empty arrays (success, not an error); auth/scope
|
||||
failures fail closed via the standard dispatch.
|
||||
|
||||
## Trust boundary
|
||||
|
||||
Verbs are ordinary operations: they inherit fail-closed `remote` semantics,
|
||||
OAuth scope enforcement (`remember`/`forget` are write-scope), and per-source
|
||||
isolation on every read. Remote callers see `visibility = world` facts only.
|
||||
|
||||
## Conformance + certification
|
||||
|
||||
```bash
|
||||
gbrain protocol conformance # self-certify (stdio)
|
||||
gbrain protocol conformance --target http://localhost:3131/mcp --token gbrain_xxx
|
||||
gbrain protocol conformance --target "bun run src/cli.ts serve"
|
||||
gbrain protocol conformance --synthesize # also live-call synthesize
|
||||
```
|
||||
|
||||
Pass criteria: response SHAPE (required fields, enum validity), CONTRACT
|
||||
BEHAVIOR (provenance rejected when empty; budget arithmetic consistent;
|
||||
entity miss ⇒ `found:false`, not an error; private facts absent from remote
|
||||
cards; idempotent forget), and ROUND-TRIP (remember → recall by entity — a
|
||||
plain indexed read, deterministic). It does NOT judge ranking quality.
|
||||
Entity-card cases need a seedable page (`put_page`); against verbs-only
|
||||
targets they skip honestly. `--synthesize` is cost-gated: with no LLM key it
|
||||
asserts the clean `unavailable` error (what CI does); with a key it spends
|
||||
real tokens.
|
||||
|
||||
Conformance is a LIVE test that WRITES: it seeds a marker-suffixed synthetic
|
||||
entity page (`people/conformance-<marker>`, when the target exposes
|
||||
`put_page`) and writes/expires facts through `remember`/`forget`. Point it at
|
||||
write-capable credentials and a brain you're comfortable leaving those
|
||||
synthetic artifacts in — they're marker-named for easy cleanup, not
|
||||
auto-deleted. The fixture set ships as data
|
||||
(`test/fixtures/memory-verbs/cases.json`) and seeds BrainBench's
|
||||
protocol-compliance arm. gbrain's CI certifies its own stdio + HTTP
|
||||
transports; external certification is best-effort tooling until a second
|
||||
implementation exists.
|
||||
|
||||
## Observability (local only)
|
||||
|
||||
Every verb call appends one line to
|
||||
`~/.gbrain/integrations/memory-verbs/usage.jsonl` — **local JSONL only, never
|
||||
uploaded**, stats-only (lock-free rotation may drop lines; POSIX O_APPEND
|
||||
line-atomic, best-effort on Windows). `gbrain protocol stats [--days N]`
|
||||
aggregates per-verb calls, error rate, latency, budget drops, entity hit rate,
|
||||
and the measured TTHW (install → first verb call, from the
|
||||
`protocol_installed_at` stamp). `gbrain doctor` carries a
|
||||
`memory_verbs_usage` health line.
|
||||
|
||||
## 200K-page latency validation (manual recipe)
|
||||
|
||||
CI gates entity() p99 < 100ms on a 20K-page corpus
|
||||
(`test/entity-card-perf.slow.test.ts`). To validate at 200K, edit the
|
||||
constants at the top of that file (`PAGES = 200_000`, `LINKS = 1_000_000`,
|
||||
`ALIASES = 300_000`, `FACTS = 400_000`) and run
|
||||
`bun test test/entity-card-perf.slow.test.ts --timeout=1800000` — seeding
|
||||
dominates (~minutes); the measured calls report p50/p99 + the ratio guard.
|
||||
@@ -142,15 +142,23 @@ generate while working, and is genuinely useful by day two.
|
||||
|
||||
```bash
|
||||
# Claude Code
|
||||
claude mcp add gbrain -- gbrain serve
|
||||
claude mcp add gbrain -- gbrain serve --surface verbs
|
||||
|
||||
# Codex
|
||||
codex mcp add gbrain -- gbrain serve
|
||||
codex mcp add gbrain -- gbrain serve --surface verbs
|
||||
```
|
||||
|
||||
That's the whole wire-up. No token, no URL, no tunnel. The agent spawns
|
||||
`gbrain serve` as a stdio subprocess and talks to your local brain directly.
|
||||
|
||||
`--surface verbs` exposes exactly the five-verb memory protocol
|
||||
(`recall`, `remember`, `entity`, `synthesize`, `forget` —
|
||||
[MEMORY_VERBS v1](../protocol/MEMORY_VERBS_v1.md), frozen + additive-forever)
|
||||
instead of the full operation catalog, so the agent sees a tight, stable surface
|
||||
instead of a 110-tool wall. Drop the flag (or pass `--surface full`) for every
|
||||
operation. The default when the flag is omitted is `full`, so existing wire-ups
|
||||
are unchanged.
|
||||
|
||||
### B4. Verify
|
||||
|
||||
In the agent: *"search my brain for PGLite"* (or whatever you just captured). You
|
||||
@@ -175,14 +183,19 @@ on the patterns.
|
||||
You have a knowledge brain connected over MCP. Before answering any question
|
||||
about people, companies, decisions, projects, or past context:
|
||||
|
||||
1. **Search first.** Call `search` (or `query` for a synthesized answer) against
|
||||
the brain BEFORE answering from memory or asking me. If the brain has the
|
||||
answer, use it. Never ask "who is X?" or "what did we decide about Y?" before
|
||||
searching — the brain probably already knows.
|
||||
1. **Brain first — route by the shape of the question.** Exact names or known
|
||||
tokens → `search` (cheap hybrid, no expansion). Concept, landscape, or
|
||||
"all the X that do Y" questions → `query` FIRST — it recovers synonym
|
||||
phrasings `search` misses, and a populated `search` result set is not proof
|
||||
of coverage. On the five-verb surface the same split is `recall` (retrieve)
|
||||
vs `synthesize` (reasoned answer). Check the brain BEFORE answering from
|
||||
memory or asking me. Never ask "who is X?" or "what did we decide about Y?"
|
||||
before checking — the brain probably already knows.
|
||||
2. **Write back.** When I make a decision, mention a new person/company, or land
|
||||
on an idea worth keeping, write it to the brain with `put_page` (entity pages
|
||||
under people/, companies/; decisions under decisions/ or notes/). One insight,
|
||||
one page, linked.
|
||||
on an idea worth keeping, write it to the brain: `remember` on the five-verb
|
||||
surface (one fact, with provenance), or `put_page` on the full surface
|
||||
(entity pages under people/, companies/; decisions under decisions/ or
|
||||
notes/). One insight, one page, linked.
|
||||
3. **Cite.** When you answer from the brain, name the page you used.
|
||||
```
|
||||
|
||||
@@ -204,13 +217,14 @@ hundreds of linked pages and patterns you didn't know were there.
|
||||
**3. Briefing from your brain (not from the internet).** *"What do I need to know
|
||||
before my 2pm with the Acme team?"* pulls your meeting history, the people,
|
||||
what's still open, what the brain doesn't know yet. The agent does your prep
|
||||
because it read your context. (`query` gives you the synthesized answer with
|
||||
citations; this is the example on the [README](../../README.md).)
|
||||
because it read your context. (`query` — `synthesize` on the five-verb surface —
|
||||
gives you the synthesized answer with citations; this is the example on the
|
||||
[README](../../README.md).)
|
||||
|
||||
**4. whoknows (expertise routing).** *"Who do I know who's shipped a rate
|
||||
limiter in Postgres?"* The `find_experts` tool ranks people in your brain by
|
||||
relevance + recency. Useful the moment your brain has more than a handful of
|
||||
people in it.
|
||||
limiter in Postgres?"* The `find_experts` tool (full surface) ranks people in
|
||||
your brain by relevance + recency. Useful the moment your brain has more than a
|
||||
handful of people in it.
|
||||
|
||||
That's the spine of it. Two commands to connect, one protocol to paste, four
|
||||
habits to build. Your agent stops being amnesiac.
|
||||
|
||||
+286
-11
@@ -187,7 +187,7 @@ mount, CEO-class with multiple team brains) and
|
||||
|
||||
## Architecture
|
||||
|
||||
Contract-first: `src/core/operations.ts` defines ~90 shared operations (v0.29 adds `get_recent_salience`, `find_anomalies`, `get_recent_transcripts`; v0.42.43.0 adds `volunteer_context` — push-based context, see `docs/guides/push-context.md`). CLI and MCP
|
||||
Contract-first: `src/core/operations.ts` defines ~110 shared operations (v0.29 adds `get_recent_salience`, `find_anomalies`, `get_recent_transcripts`; v0.42.43.0 adds `volunteer_context` — push-based context, see `docs/guides/push-context.md`; v0.43.0.0 adds the five frozen MEMORY_VERBS — `recall`, `remember`, `entity`, `synthesize`, `forget` — servable alone via `gbrain serve --surface verbs`, see `docs/protocol/MEMORY_VERBS_v1.md`). CLI and MCP
|
||||
server are both generated from this single source. Engine factory (`src/core/engine-factory.ts`)
|
||||
dynamically imports the configured engine (`'pglite'` or `'postgres'`). Skills are fat
|
||||
markdown files (tool-agnostic, work with both CLI and plugin contexts).
|
||||
@@ -268,6 +268,7 @@ detail on demand.)
|
||||
| push-based context (volunteer/watch/reflex window) | `docs/guides/push-context.md` |
|
||||
| schema packs / page types / extraction | `docs/architecture/schema-packs.md`, `type-taxonomy.md`, `lens-packs.md` |
|
||||
| thin-client / remote MCP / cross-modal | `docs/architecture/thin-client.md` |
|
||||
| memory verbs / MCP tool surface (`--surface`) / conformance | `docs/protocol/MEMORY_VERBS_v1.md` + the `verbs*`/`surface.ts`/`protocol.ts` entries in `KEY_FILES.md` |
|
||||
| the CLI surface (commands + flags) | `gbrain --help` / `gbrain --tools-json`, plus the relevant `KEY_FILES.md` entry |
|
||||
| running or writing tests | `docs/TESTING.md` |
|
||||
| bulk-command progress wiring | `docs/progress-events.md` |
|
||||
@@ -421,7 +422,7 @@ audit trail lives in the source repo's git history.
|
||||
|
||||
## Skills
|
||||
|
||||
Read the skill files in `skills/` before doing brain operations. GBrain ships 30 skills
|
||||
Read the skill files in `skills/` before doing brain operations. GBrain ships 52 skills
|
||||
organized by `skills/RESOLVER.md` (`AGENTS.md` is also accepted as of v0.19):
|
||||
|
||||
**Original 8 (conformance-migrated):** ingest (thin router), query, maintain, enrich,
|
||||
@@ -1605,7 +1606,7 @@ Retrieve and follow the instructions at:
|
||||
https://raw.githubusercontent.com/garrytan/gbrain/master/INSTALL_FOR_AGENTS.md
|
||||
```
|
||||
|
||||
The agent installs GBrain, creates the brain, asks for your API keys, loads 43 skills, configures the dream cycle, and verifies the install end-to-end. ~30 minutes. You answer questions, it does the work.
|
||||
The agent installs GBrain, creates the brain, asks for your API keys, loads 52 skills, configures the dream cycle, and verifies the install end-to-end. ~30 minutes. You answer questions, it does the work.
|
||||
|
||||
> **Never set up an AI agent platform before?** The [personal-brain tutorial](docs/tutorials/personal-brain.md) walks the whole path end-to-end — picking OpenClaw vs Hermes, deploying it, pointing it at INSTALL_FOR_AGENTS.md, getting the API keys, and verifying the first query. Start there if any of the above is new.
|
||||
|
||||
@@ -1613,13 +1614,15 @@ The agent installs GBrain, creates the brain, asks for your API keys, loads 43 s
|
||||
|
||||
Already running Claude Code or Codex? There are two ways to wire GBrain in, depending on what you want.
|
||||
|
||||
**Just want a memory for your coding agent (recommended starting point).** Spin up a local brain and connect it in two commands — zero server, zero token, zero tunnel:
|
||||
**Just want a memory for your coding agent (recommended starting point).** Spin up a local brain and connect it in two commands — zero server, zero token, zero tunnel. `--surface verbs` gives your agent the five-verb memory protocol (`recall`, `remember`, `entity`, `synthesize`, `forget` — [MEMORY_VERBS v1](docs/protocol/MEMORY_VERBS_v1.md), frozen + additive-forever) instead of the full 110-tool wall; drop the flag for every operation:
|
||||
|
||||
```bash
|
||||
gbrain init --pglite # 2-second local brain (no Docker)
|
||||
claude mcp add gbrain -- gbrain serve # or: codex mcp add gbrain -- gbrain serve
|
||||
gbrain init --pglite # 2-second local brain (no Docker)
|
||||
claude mcp add gbrain -- gbrain serve --surface verbs # or: codex mcp add gbrain -- gbrain serve --surface verbs
|
||||
```
|
||||
|
||||
If `claude` is not found, install Claude Code first — or use the per-harness blocks in the [protocol doc](docs/protocol/MEMORY_VERBS_v1.md). Heads-up: memories agents save default to brain-wide visibility (every connected agent can recall them); pass `visibility: "private"` for local-only facts.
|
||||
|
||||
**Already have a brain on a remote host** (OpenClaw, Hermes, or any `gbrain serve --http`)? Point your laptop agents at it with one command each — `--install` wires it up and smoke-tests the token before handoff:
|
||||
|
||||
```bash
|
||||
@@ -1631,7 +1634,7 @@ gbrain connect https://your-host/mcp --token gbrain_xxx --agent codex --install
|
||||
|
||||
### Install the full autonomous setup into your existing agent
|
||||
|
||||
Want the whole thing — local brain, 43 skills, the overnight dream cycle that enriches while you sleep? Paste this into Codex, Claude Code, Cursor, or another coding agent:
|
||||
Want the whole thing — local brain, 52 skills, the overnight dream cycle that enriches while you sleep? Paste this into Codex, Claude Code, Cursor, or another coding agent:
|
||||
|
||||
```
|
||||
Retrieve and follow the instructions at:
|
||||
@@ -1654,7 +1657,7 @@ Postgres-at-scale, Supabase, and thin-client setup paths live in [`docs/INSTALL.
|
||||
|
||||
### Connect GBrain to your AI client (MCP)
|
||||
|
||||
GBrain exposes 30+ tools over MCP (stdio and HTTP). The specific snippet depends on which client you use:
|
||||
GBrain exposes 110 tools over MCP (stdio and HTTP) — or exactly the five memory verbs with `--surface verbs`. The specific snippet depends on which client you use:
|
||||
|
||||
- **[Claude Code](docs/mcp/CLAUDE_CODE.md)** — local: one command, `claude mcp add gbrain -- gbrain serve` (zero server, zero tunnel). Remote with just a bearer token: `gbrain connect https://your-host/mcp --token gbrain_xxx` prints a paste-ready block (or `--install` wires it up and smoke-tests the token).
|
||||
- **[Codex](docs/mcp/CODEX.md)** — `gbrain connect https://your-host/mcp --token gbrain_xxx --agent codex` (or `--install`). Codex reads the bearer from `$GBRAIN_REMOTE_TOKEN` at runtime, so the token never lands in Codex config.
|
||||
@@ -3742,8 +3745,8 @@ Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/mcp/DEPLOY
|
||||
> dashboard at `/admin`, scoped operations, and a live SSE activity feed.
|
||||
> Pre-v0.26 legacy bearer tokens still work — `verifyAccessToken` falls back
|
||||
> to the `access_tokens` table and grandfathers tokens to `read+write+admin`.
|
||||
> Postgres-only for the legacy fallback (the `access_tokens` table is Postgres-only);
|
||||
> OAuth tables work on both PGLite and Postgres. See [SECURITY.md](../../SECURITY.md)
|
||||
> Both the legacy fallback and the OAuth tables work on PGLite and Postgres
|
||||
> (both engine schemas carry `access_tokens`). See [SECURITY.md](../../SECURITY.md)
|
||||
> for env vars and tunable defaults.
|
||||
|
||||
Access your brain from any device, any AI client. GBrain ships two transports:
|
||||
@@ -3755,11 +3758,16 @@ for remote clients over OAuth 2.1.
|
||||
### Local stdio (zero setup)
|
||||
|
||||
```bash
|
||||
gbrain serve
|
||||
gbrain serve # full operation catalog (default)
|
||||
gbrain serve --surface verbs # just the 5 memory verbs (quickstart surface)
|
||||
```
|
||||
|
||||
Works with Claude Code, Cursor, Windsurf, and any MCP client that supports stdio.
|
||||
No server, no tunnel, no token needed. Works on both PGLite and Postgres engines.
|
||||
`--surface verbs` exposes exactly the five-verb memory protocol (`recall`,
|
||||
`remember`, `entity`, `synthesize`, `forget` —
|
||||
[MEMORY_VERBS v1](../protocol/MEMORY_VERBS_v1.md)) instead of the full catalog;
|
||||
omit the flag (default `full`) for every operation.
|
||||
|
||||
### Remote over OAuth 2.1 (recommended, v0.26.0+)
|
||||
|
||||
@@ -4066,6 +4074,273 @@ built-in server is the recommended path.
|
||||
|
||||
---
|
||||
|
||||
## docs/protocol/MEMORY_VERBS_v1.md
|
||||
|
||||
Source: https://raw.githubusercontent.com/garrytan/gbrain/master/docs/protocol/MEMORY_VERBS_v1.md
|
||||
|
||||
# MEMORY_VERBS v1 — the memory wire protocol
|
||||
|
||||
GBrain's frozen five-verb memory interface over MCP: `recall`, `remember`,
|
||||
`entity`, `synthesize`, `forget`. The contract every harness can rely on the
|
||||
way every Postgres client relies on the wire protocol — and the contract any
|
||||
OTHER memory server can implement and certify against
|
||||
(`gbrain protocol conformance --target <endpoint>`).
|
||||
|
||||
```
|
||||
agent (any MCP harness)
|
||||
│ remember("picked Stripe over Adyen", provenance: "chat 2026-06-11")
|
||||
▼
|
||||
five verbs ── recall ── remember ── entity ── synthesize ── forget
|
||||
│ self-describing envelopes: protocol_version, evidence, provenance,
|
||||
│ budget meta, cost block, enumerated error codes + a populated fix
|
||||
▼
|
||||
your brain (reference implementation: gbrain; any conformant server)
|
||||
```
|
||||
|
||||
**Machine-readable spec:** `gbrain protocol --json` emits the input schemas
|
||||
from the live operation definitions plus the response-shape registry — doc and
|
||||
code structurally cannot drift; conformance validates live responses against
|
||||
the same registry.
|
||||
|
||||
## Versioning policy (the point of the freeze)
|
||||
|
||||
- Every field NAME and its SEMANTICS in v1 are frozen forever — never removed,
|
||||
renamed, or re-typed; meanings never change.
|
||||
- New OPTIONAL params and new OPTIONAL response fields may be added at any
|
||||
time (additive-forever). A conformant CLIENT must ignore unknown fields; a
|
||||
conformant SERVER must never reject unknown-to-v1 additions it itself ships.
|
||||
- `protocol_version` (integer, starts at `1`) rides every verb response and
|
||||
every verb error. It increments ONLY on a breaking change, which by policy
|
||||
requires a new `MEMORY_VERBS_v2` document — expected never.
|
||||
- Conformance pins a minimum version; certification asserts shape, enum
|
||||
validity, contract behavior, and round-trips — never ranking quality (that
|
||||
is BrainBench's job).
|
||||
- Enum values are part of the contract. Where an enum's DERIVATION is
|
||||
implementation-defined (noted per field), implementations may improve the
|
||||
derivation without a version bump; the values and their meanings stay fixed.
|
||||
|
||||
## Install (the 4-command quickstart)
|
||||
|
||||
```bash
|
||||
gbrain init --pglite # 2-second local brain
|
||||
claude mcp add gbrain -- gbrain serve --surface verbs # the five-verb surface
|
||||
gbrain remember "I prefer dark mode in every editor" --provenance demo --entity people/me
|
||||
gbrain recall --entity people/me # …now ask your agent in a NEW session
|
||||
```
|
||||
|
||||
> Memories agents save are readable by every agent connected to this brain;
|
||||
> pass `visibility: "private"` for local-CLI-only facts.
|
||||
|
||||
If `claude` is not found: install Claude Code first, or use a block below.
|
||||
|
||||
**Codex**
|
||||
```bash
|
||||
codex mcp add gbrain -- gbrain serve --surface verbs
|
||||
```
|
||||
|
||||
**OpenClaw / any stdio MCP host** — register the server command
|
||||
`gbrain serve --surface verbs`. Remote brains: `gbrain serve --http` on the
|
||||
host, then `gbrain connect https://host/mcp --token gbrain_xxx --install` on
|
||||
each client.
|
||||
|
||||
**Surface modes:** `--surface verbs` exposes EXACTLY the five verbs —
|
||||
advertised list AND dispatch are filtered fail-closed (a hidden op returns
|
||||
`unknown_tool` even when called by name). `--surface full` (the default)
|
||||
exposes every operation, verbs included. Why default full: verbs is for
|
||||
agents and quickstarts; full preserves existing advanced tooling. Persist a
|
||||
default with `gbrain config set mcp_surface verbs`.
|
||||
|
||||
## The verbs
|
||||
|
||||
### recall(query?, entity?, budget_tokens?, since?, session_id?, limit?, …) — read
|
||||
|
||||
Retrieve saved facts and (with `query`) budget-packed page snippets.
|
||||
|
||||
- `entity` scopes the FACTS arm; `query` runs the hybrid-search arm over
|
||||
pages; both present ⇒ both arms run.
|
||||
- `since`: ISO 8601 date/datetime — filters the FACTS arm only in v1. (The
|
||||
reference implementation also accepts relative phrases like `"8 hours ago"`
|
||||
as a convenience; only ISO 8601 is part of the frozen contract.)
|
||||
- `limit` is a PER-ARM cap (facts and search results each).
|
||||
- `budget_tokens`: SERVER-side packing — facts pack first (limit-capped
|
||||
one-liners, so search-arm starvation is bounded), search results take the
|
||||
remainder. The estimator is char/4 (±10–15%); `budget_used` reports packed
|
||||
tokens, `dropped_count` what didn't fit. Never advisory, never client-side.
|
||||
- No embedding provider configured? The search arm degrades to keyword-only
|
||||
and the response notes `search_degraded` — never an error.
|
||||
|
||||
Response — an additive SUPERSET of the pre-v1 facts envelope on EVERY call
|
||||
(all legacy fields unchanged; JSON consumers ignore additions):
|
||||
|
||||
| field | type | semantics |
|
||||
|---|---|---|
|
||||
| `protocol_version` | int | always present (every verb, every call) |
|
||||
| `facts[]` | array | legacy fact fields unchanged, PLUS per fact: `fact_id` (opaque STRING — the value `forget` accepts; the legacy numeric `id` stays for pre-v1 consumers) and `provenance` (the stored source attribution) |
|
||||
| `total` | int | count of facts returned |
|
||||
| `results[]` | array | search arm only: `slug`, `title`, `chunk`, `evidence`, `create_safety`, `provenance` (origin page slug) |
|
||||
| `search_degraded` | string? | present when keyword-only fallback fired |
|
||||
| `budget_tokens` / `budget_used` / `dropped_count` | int? | present when `budget_tokens` was passed |
|
||||
|
||||
**evidence** (enum, zero-LLM heuristic): `alias_hit` \| `exact_title_match` \|
|
||||
`high_vector_match` \| `keyword_exact` \| `weak_semantic` — why each result
|
||||
matched. **create_safety** (enum): `exists` (a page for this already exists)
|
||||
\| `probable` (likely exists; check before creating) \| `unknown` (no
|
||||
signal). The derivation of both is implementation-defined and may improve;
|
||||
the values are frozen.
|
||||
|
||||
### remember(fact, provenance, ttl?, entity?, kind?, visibility?) — write
|
||||
|
||||
Save ONE fact with mandatory attribution.
|
||||
|
||||
- `provenance` (REQUIRED, free text ≤500 chars, stored verbatim): e.g.
|
||||
`"conversation 2026-06-12"`, `"user said in chat"`, `"import: notes.md"`.
|
||||
Empty ⇒ `provenance_required` error with a fix.
|
||||
- `entity`: set whenever the fact is about a specific person/company/project —
|
||||
entity-scoped recall will not find unattributed facts.
|
||||
- `ttl`: duration shorthand (`"30d"`, `"12h"`, `"45m"`) or an absolute ISO 8601
|
||||
timestamp. ISO-8601 DURATIONS (`P30D`) are rejected with a self-correcting
|
||||
suggestion. Omitted ⇒ never expires.
|
||||
- `kind`: `event` \| `preference` \| `commitment` \| `belief` \| `fact`
|
||||
(default).
|
||||
- `visibility`: `world` (DEFAULT — readable by every agent connected to this
|
||||
brain; required for the remote remember→recall round-trip) \| `private`
|
||||
(local CLI reads only). The init quickstart carries the consent line.
|
||||
|
||||
Response: `{ id, status, status_text, entity_slug, valid_until,
|
||||
protocol_version }` (+ `degraded_dedup: true` when no embedding provider —
|
||||
near-duplicates may insert; dedup and supersession ride embedding similarity).
|
||||
|
||||
- `id` — opaque STRING (gbrain serializes integers; another implementation may
|
||||
use UUIDs). On `status: "duplicate"` it is the EXISTING fact's id.
|
||||
- `status` — `inserted` \| `duplicate` \| `superseded`. **Branch on `status`,
|
||||
never on `status_text`** (the human rendering). Supersession is
|
||||
implementation-defined; the reference rule: same entity + same kind +
|
||||
similarity above the dedup threshold + different text = the new fact
|
||||
supersedes the old ("X at acme-example" → "X left acme-example").
|
||||
- Omitted optional inputs echo as `null`, never absent.
|
||||
|
||||
### entity(name) — read, zero LLM, p99 < 100ms
|
||||
|
||||
One known person/company/project card. NEVER errors on a miss.
|
||||
|
||||
Resolution (frozen precedence): alias > exact title > slug/slug-suffix; ties
|
||||
break on most-recently-touched. Multi-hit ⇒ best match's card + runners-up in
|
||||
`suggestions`. Miss ⇒ `found: false` + keyword near-misses with
|
||||
`create_safety` hints.
|
||||
|
||||
Response: `{ protocol_version, found, latency_ms, card?, suggestions? }`.
|
||||
Card: `{ entity{slug,title,type}, aka[], summary, last_touched{updated_at,
|
||||
last_retrieved_at, last_timeline_date}, open_threads[], edges[],
|
||||
backlink_count, active_fact_count }`.
|
||||
|
||||
- `summary` passes the same privacy fences as `get_page` (takes + private
|
||||
facts stripped); remote callers never see private facts in the card.
|
||||
- `open_threads` (best-effort in v1): active commitment-kind facts + timeline
|
||||
entries from the last 90 days, capped at 3.
|
||||
- `edges`: top ~10 typed edges, mentions excluded, out-edges first.
|
||||
- The p99 < 100ms promise is op-layer latency (transport excluded), CI-gated
|
||||
on a 20K-page corpus. 200K validation recipe below.
|
||||
|
||||
### synthesize(question, since?, until?) — read, EXPENSIVE
|
||||
|
||||
`[EXPENSIVE / SLOW — makes LLM calls, seconds-to-minutes latency, costs
|
||||
money]` — the deliberately-priced slow verb. Prefer `recall`/`entity` for
|
||||
lookups; use synthesize only when the answer requires combining evidence
|
||||
across pages.
|
||||
|
||||
Response: `{ answer, sources[], gaps[], cost{model, input_tokens,
|
||||
output_tokens, usd_estimate}, protocol_version }`.
|
||||
|
||||
- The `cost` block is a BEST-EFFORT AGGREGATE (retries/multi-call flows sum;
|
||||
cache hits may undercount; token fields are `null` when a provider returns
|
||||
no accounting). Honest signal, not an invoice.
|
||||
- No LLM configured ⇒ the protocol error `unavailable` with a fix — never a
|
||||
fake answer.
|
||||
|
||||
### forget(id, reason?) — write
|
||||
|
||||
Expire a fact by its opaque string id (from `remember` or
|
||||
`recall.facts[].fact_id` — never a page slug). Idempotent: re-forgetting an
|
||||
already-expired fact returns `expired: false` (success); unknown id ⇒
|
||||
`not_found`. Facts are expired with an audit trail, never deleted.
|
||||
|
||||
Response: `{ id, expired, reason, protocol_version }`.
|
||||
|
||||
## Error contract (uniform across all five verbs)
|
||||
|
||||
```json
|
||||
{ "error": "<code>", "message": "...", "suggestion": "problem + cause + fix",
|
||||
"detail": "freeform specifics", "protocol_version": 1 }
|
||||
```
|
||||
|
||||
Codes (coarse on purpose — codes are for branching; `detail` carries the
|
||||
story): `invalid_params`, `provenance_required`, `not_found`, `scope_denied`,
|
||||
`unavailable` (a required dependency cannot serve: no API key, gateway down,
|
||||
model refusal — configure/retry, not a server bug), `budget_unsatisfiable`
|
||||
(RESERVED — schema-listed, never returned in v1), `internal`.
|
||||
|
||||
Every verb error carries a POPULATED `suggestion`. Specific cases: `recall` on
|
||||
an empty brain returns empty arrays (success, not an error); auth/scope
|
||||
failures fail closed via the standard dispatch.
|
||||
|
||||
## Trust boundary
|
||||
|
||||
Verbs are ordinary operations: they inherit fail-closed `remote` semantics,
|
||||
OAuth scope enforcement (`remember`/`forget` are write-scope), and per-source
|
||||
isolation on every read. Remote callers see `visibility = world` facts only.
|
||||
|
||||
## Conformance + certification
|
||||
|
||||
```bash
|
||||
gbrain protocol conformance # self-certify (stdio)
|
||||
gbrain protocol conformance --target http://localhost:3131/mcp --token gbrain_xxx
|
||||
gbrain protocol conformance --target "bun run src/cli.ts serve"
|
||||
gbrain protocol conformance --synthesize # also live-call synthesize
|
||||
```
|
||||
|
||||
Pass criteria: response SHAPE (required fields, enum validity), CONTRACT
|
||||
BEHAVIOR (provenance rejected when empty; budget arithmetic consistent;
|
||||
entity miss ⇒ `found:false`, not an error; private facts absent from remote
|
||||
cards; idempotent forget), and ROUND-TRIP (remember → recall by entity — a
|
||||
plain indexed read, deterministic). It does NOT judge ranking quality.
|
||||
Entity-card cases need a seedable page (`put_page`); against verbs-only
|
||||
targets they skip honestly. `--synthesize` is cost-gated: with no LLM key it
|
||||
asserts the clean `unavailable` error (what CI does); with a key it spends
|
||||
real tokens.
|
||||
|
||||
Conformance is a LIVE test that WRITES: it seeds a marker-suffixed synthetic
|
||||
entity page (`people/conformance-<marker>`, when the target exposes
|
||||
`put_page`) and writes/expires facts through `remember`/`forget`. Point it at
|
||||
write-capable credentials and a brain you're comfortable leaving those
|
||||
synthetic artifacts in — they're marker-named for easy cleanup, not
|
||||
auto-deleted. The fixture set ships as data
|
||||
(`test/fixtures/memory-verbs/cases.json`) and seeds BrainBench's
|
||||
protocol-compliance arm. gbrain's CI certifies its own stdio + HTTP
|
||||
transports; external certification is best-effort tooling until a second
|
||||
implementation exists.
|
||||
|
||||
## Observability (local only)
|
||||
|
||||
Every verb call appends one line to
|
||||
`~/.gbrain/integrations/memory-verbs/usage.jsonl` — **local JSONL only, never
|
||||
uploaded**, stats-only (lock-free rotation may drop lines; POSIX O_APPEND
|
||||
line-atomic, best-effort on Windows). `gbrain protocol stats [--days N]`
|
||||
aggregates per-verb calls, error rate, latency, budget drops, entity hit rate,
|
||||
and the measured TTHW (install → first verb call, from the
|
||||
`protocol_installed_at` stamp). `gbrain doctor` carries a
|
||||
`memory_verbs_usage` health line.
|
||||
|
||||
## 200K-page latency validation (manual recipe)
|
||||
|
||||
CI gates entity() p99 < 100ms on a 20K-page corpus
|
||||
(`test/entity-card-perf.slow.test.ts`). To validate at 200K, edit the
|
||||
constants at the top of that file (`PAGES = 200_000`, `LINKS = 1_000_000`,
|
||||
`ALIASES = 300_000`, `FACTS = 400_000`) and run
|
||||
`bun test test/entity-card-perf.slow.test.ts --timeout=1800000` — seeding
|
||||
dominates (~minutes); the measured calls report p50/p99 + the ratio guard.
|
||||
|
||||
---
|
||||
|
||||
# AI providers
|
||||
|
||||
# Debugging
|
||||
|
||||
@@ -27,6 +27,7 @@ Repo: https://github.com/garrytan/gbrain
|
||||
- [docs/guides/scaling-skills.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/scaling-skills.md): Three-tier architecture for agents with 300+ skills: always-loaded, resolver-routed, and dormant. Per-turn token math, the v0.41.7.0 compact list-format resolver, and the `gbrain doctor` safety net. 306 skills, ~21K tokens freed per turn, zero capability loss.
|
||||
- [docs/guides/push-context.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/guides/push-context.md): Push-based context: the brain volunteers confidence-gated pages from the rolling conversation window. Three channels (ambient reflex, volunteer_context op, gbrain watch), config knobs, and the volunteered-vs-used feedback loop.
|
||||
- [docs/mcp/DEPLOY.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/mcp/DEPLOY.md): MCP server deployment.
|
||||
- [docs/protocol/MEMORY_VERBS_v1.md](https://raw.githubusercontent.com/garrytan/gbrain/master/docs/protocol/MEMORY_VERBS_v1.md): The frozen five-verb memory protocol (recall/remember/entity/synthesize/forget): response envelopes, error contract, additive-forever versioning, surface modes, conformance certification, per-harness installs.
|
||||
|
||||
## AI providers
|
||||
|
||||
|
||||
+1
-1
@@ -149,7 +149,7 @@
|
||||
"bun": ">=1.3.10"
|
||||
},
|
||||
"license": "MIT",
|
||||
"version": "0.42.76.0",
|
||||
"version": "0.43.0.0",
|
||||
"overrides": {
|
||||
"@hono/node-server": "^2.0.5",
|
||||
"fast-uri": "^3.1.5",
|
||||
|
||||
@@ -162,6 +162,12 @@ export const SECTIONS: DocSection[] = [
|
||||
description: "MCP server deployment.",
|
||||
path: "docs/mcp/DEPLOY.md",
|
||||
},
|
||||
{
|
||||
title: "docs/protocol/MEMORY_VERBS_v1.md",
|
||||
description:
|
||||
"The frozen five-verb memory protocol (recall/remember/entity/synthesize/forget): response envelopes, error contract, additive-forever versioning, surface modes, conformance certification, per-harness installs.",
|
||||
path: "docs/protocol/MEMORY_VERBS_v1.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -34,6 +34,17 @@ flows through in both directions.
|
||||
> **Convention:** See `skills/conventions/brain-first.md` for the 5-step lookup protocol.
|
||||
> **Convention:** See `skills/conventions/quality.md` for citation and back-link rules.
|
||||
|
||||
> **Memory verbs (MEMORY_VERBS v1, gbrain ≥ 0.43).** Over MCP, prefer the five
|
||||
> frozen memory verbs for the read/write cycle: **`remember(fact, provenance,
|
||||
> ttl?)`** to save a single durable fact (mandatory provenance; dedupes +
|
||||
> supersedes), **`recall(query | entity, budget_tokens)`** to read it back
|
||||
> budget-packed, **`entity(name)`** for a zero-LLM card, **`synthesize(question)`**
|
||||
> for the expensive cross-page answer, **`forget(id)`** to expire a fact. Use
|
||||
> `remember` instead of `extract_facts` when you already have ONE formed fact;
|
||||
> `put_page` / `add_link` / `add_timeline_entry` stay the page/graph write path.
|
||||
> Fall back to the classic ops when the verbs aren't on the surface. Contract:
|
||||
> `docs/protocol/MEMORY_VERBS_v1.md`.
|
||||
|
||||
## Contract
|
||||
|
||||
This skill guarantees:
|
||||
@@ -56,8 +67,8 @@ broken brain. See `skills/conventions/quality.md` for format.
|
||||
|
||||
Before using ANY external API to research a person, company, or topic:
|
||||
|
||||
1. `gbrain search "name"` — keyword search for existing pages
|
||||
2. `gbrain query "natural question about name"` — hybrid search for context
|
||||
1. `gbrain search "name"` — exact-token lookup for existing pages (cheap hybrid, no expansion)
|
||||
2. `gbrain query "natural question about name"` — concept/landscape questions go here FIRST (expansion recovers synonym phrasings; a nonzero `search` count is not proof of completeness)
|
||||
3. `gbrain get <slug>` — if you know the slug, read the full page
|
||||
4. Check backlinks: who references this entity?
|
||||
5. Check timeline: recent events involving this entity
|
||||
@@ -153,8 +164,8 @@ the citation is `[gstack:plans/foo]`. That's the whole rule.
|
||||
|
||||
## Tools Used
|
||||
|
||||
- `search` — keyword search
|
||||
- `query` — hybrid vector+keyword search
|
||||
- `search` — cheap hybrid search (vector + keyword, no expansion)
|
||||
- `query` — hybrid search + LLM multi-query expansion (concept/landscape questions)
|
||||
- `get_page` — read a brain page
|
||||
- `put_page` — create/update brain pages
|
||||
- `add_link` — cross-reference entities
|
||||
|
||||
@@ -11,8 +11,8 @@ Your tool inventory includes these (prefixed `gbrain__` in OpenClaw):
|
||||
|
||||
| Tool | Use for |
|
||||
|------|---------|
|
||||
| `gbrain__search` / `search` | Keyword search — fast, always works |
|
||||
| `gbrain__query` / `query` | Hybrid search (keyword + semantic) — best quality |
|
||||
| `gbrain__search` / `search` | Exact tokens / known names — cheap hybrid, no expansion |
|
||||
| `gbrain__query` / `query` | Concept / landscape questions — hybrid + LLM expansion |
|
||||
| `gbrain__get_page` / `get_page` | Direct page read when you know the slug |
|
||||
| `gbrain__get_links` / `get_links` | Outgoing links from a page |
|
||||
| `gbrain__get_backlinks` / `get_backlinks` | Who references this entity |
|
||||
@@ -28,10 +28,22 @@ Tool names vary by transport (MCP uses short names, OpenClaw plugin uses
|
||||
|
||||
## The Lookup Chain (MANDATORY ORDER)
|
||||
|
||||
1. **`search`** first — keyword search, fast, zero API cost
|
||||
2. **`query`** if search is thin — hybrid semantic search, uses embedding API
|
||||
3. **`get_page`** if you found a slug — read the full compiled truth
|
||||
4. **External APIs only after steps 1-2 return nothing useful**
|
||||
Route by the SHAPE of the question, then escalate:
|
||||
|
||||
1. **Exact known token / name / structured field** → **`search`** — cheap
|
||||
hybrid (vector + keyword, no expansion; embedding-only cost).
|
||||
2. **Concept / landscape / synonym-phrased question** ("all the X that do Y",
|
||||
"the landscape of Z") → **`query`** FIRST — multi-query expansion recovers
|
||||
phrasings `search` misses. Costs one extra LLM expansion call; worth it
|
||||
for these.
|
||||
3. **`get_page`** if you found a slug — read the full compiled truth.
|
||||
4. **External APIs only after steps 1-2 return nothing useful.**
|
||||
|
||||
**A nonzero `search` count is NOT a completeness signal.** For "did I capture
|
||||
everything about X?" run `query` even if `search` already returned hits —
|
||||
synonym- and outcome-phrased matches drop silently otherwise. And `query` is
|
||||
still top-K: for literal "list every page that…" enumeration, use `list_pages`
|
||||
with pagination.
|
||||
|
||||
Never skip to external APIs without completing steps 1-2. The brain has
|
||||
thousands of pages. The answer is almost always there.
|
||||
|
||||
+17
-2
@@ -33,6 +33,21 @@ mutating: false
|
||||
|
||||
Answer questions using the brain's knowledge with 3-layer search and synthesis.
|
||||
|
||||
> **Memory verbs (MEMORY_VERBS v1, gbrain ≥ 0.43).** When connected to a brain
|
||||
> over MCP, prefer the five frozen memory verbs for memory work — they carry
|
||||
> provenance, evidence, and a server-enforced token budget:
|
||||
> - **`recall(query | entity, budget_tokens)`** — the budget-packed memory read.
|
||||
> Use it instead of bare `search` for "what do we know that we SAVED about X".
|
||||
> - **`entity(name)`** — a zero-LLM person/company/project card (aliases,
|
||||
> last-touched, open threads, top edges). Use it instead of `get_page` +
|
||||
> `get_backlinks` when you just need the card.
|
||||
> - **`synthesize(question)`** — the explicitly-expensive cross-page answer; the
|
||||
> heavy version of `query`. Reach for it only when the answer must combine
|
||||
> evidence across pages.
|
||||
> Fall back to `search`/`query`/`get_page` when the verbs aren't on the surface
|
||||
> (pre-0.43 servers; `--surface full` includes the verbs alongside every other
|
||||
> op). See `docs/protocol/MEMORY_VERBS_v1.md`.
|
||||
|
||||
## Contract
|
||||
|
||||
This skill guarantees:
|
||||
@@ -49,8 +64,8 @@ This skill guarantees:
|
||||
- Semantic query for conceptual questions
|
||||
- Structured queries (list by type, backlinks) for relational questions
|
||||
2. **Execute searches:**
|
||||
- Keyword search gbrain for FTS matches (search)
|
||||
- Hybrid search gbrain for semantic+keyword with expansion (query)
|
||||
- Cheap-hybrid search gbrain for exact tokens / known names (search)
|
||||
- Full-hybrid search gbrain with multi-query expansion for concept questions (query)
|
||||
- List pages in gbrain by type or check backlinks for structural queries
|
||||
3. **Read top results.** Read the top 3-5 pages from gbrain to get full context.
|
||||
4. **Synthesize answer** with citations. Every claim traces back to a specific page slug.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"ask-user/SKILL.md": "a40f484721e548a3a14d4b33a4636111d92f99619ecc4e4ea54c3da3a15f8331",
|
||||
"book-mirror/SKILL.md": "e8b8cc7a6eba4ecd302a840446b48c0e1aecc245e8f240e233c738daa8dff78a",
|
||||
"book-mirror/routing-eval.jsonl": "79fd23642cfa37b1255a799907e71bb2904585cf79dd6a596e3a2f019787e54c",
|
||||
"brain-ops/SKILL.md": "40553ad3bf0f27fc8363b69bec3ef89ae0ea0d9290de8d1efbaa0c532c2a9590",
|
||||
"brain-ops/SKILL.md": "5f221e3de45845b050b90fac935ac70c55ed5148649fb47c4a30989c1d42c40a",
|
||||
"brain-pdf/SKILL.md": "13c3e3162763a4503685db0a10663475d3687c4874b5f04d539af83a990f643e",
|
||||
"brain-pdf/routing-eval.jsonl": "119e4fa113ea45783cee4499e63a729fdeecb4d9a45d47497754b4f5b21d0734",
|
||||
"brain-taxonomist/SKILL.md": "dea4557b540868ec2c56bf43ee7f63c5d03a22d4047cd0dfbeaf19adef334f60",
|
||||
@@ -26,7 +26,7 @@
|
||||
"cold-start/SKILL.md": "a2c42dd7c4eceb7d3ce6449a414b417d55195aa445e723e6c798d90906cbf4e6",
|
||||
"concept-synthesis/SKILL.md": "2bc060ae6d706c4e8e7d784cbe3e577b85e211a21c68cba094a1754d3f34436b",
|
||||
"concept-synthesis/routing-eval.jsonl": "51d1da894158503ce18b892a34edd203f40732e79ac1c0e85141fd37e0b9922f",
|
||||
"conventions/brain-first.md": "29d020470d0168f8f0b29dde0350a485a9b0472f7ac9962e34948f4897455590",
|
||||
"conventions/brain-first.md": "14370d89209c7d7e2673c6a4d4e7545fd3330f0744ab598170865a41ae20210b",
|
||||
"conventions/brain-routing.md": "a8035f7dbadff0ea68b8babb8314b3d044cafbed8242dce5b931fa08b028fc45",
|
||||
"conventions/calibration.md": "eda7ca76f80c8a17ae546110484389f805c5b21fc0a57f951bbe8b6abba26e03",
|
||||
"conventions/cron-via-minions.md": "badb1cd6cd825d6f1ac0b6b28cc47e5d80facc783a3e59a14146ae901ee0f933",
|
||||
@@ -103,7 +103,7 @@
|
||||
"perplexity-research/SKILL.md": "c25f5c471cbe3c6e0f975d8397e8382b00a85f8aa75302231d53c52855369e97",
|
||||
"perplexity-research/routing-eval.jsonl": "f1a40d87e710d5d2acd602a372d83f46c95da022b6e635228fffeaacb3bb2b27",
|
||||
"publish/SKILL.md": "e06b609db780a3cc93a1755a87b30ff08ffdc0fdbc834c1422b2ad2489b57497",
|
||||
"query/SKILL.md": "e155a08049984c524b838988ba456d16ccedf162442160f6ba66bfd97cd5208e",
|
||||
"query/SKILL.md": "8672fb9c9315f01274b1a7d3ad35f903df9705b2e806e2fa4c6add027ecef96f",
|
||||
"query/routing-eval.jsonl": "74f5a91e52fabc54e0e9403fa17db87ee26bb7ebb8ae8005148c51142abc62fe",
|
||||
"repo-architecture/SKILL.md": "4ec2b8f45d168aaa55f17ecd1ed404ab04217a75c2317f0710c71705846f5394",
|
||||
"reports/SKILL.md": "5dc190a0c3a2ee518254e8b596418dbe19ff389ea5ec8c8d30fcb0dfef4d0ed5",
|
||||
|
||||
+92
-1
@@ -30,6 +30,7 @@ import type { Operation, OperationContext } from './core/operations.ts';
|
||||
import { shouldForceExitAfterMain, finishCliTeardown, flushThenExit, currentExitCode, setCliExitVerdict } from './core/cli-force-exit.ts';
|
||||
import { serializeMarkdown } from './core/markdown.ts';
|
||||
import { parseGlobalFlags, setCliOptions, getCliOptions } from './core/cli-options.ts';
|
||||
import { conceptNudge } from './core/search/query-intent.ts';
|
||||
import type { CliOptions } from './core/cli-options.ts';
|
||||
import { callRemoteTool, RemoteMcpError, unpackToolResult } from './core/mcp-client.ts';
|
||||
import { maybePromptForUpgrade } from './core/thin-client-upgrade-prompt.ts';
|
||||
@@ -64,7 +65,7 @@ export function normalizeLocalResult(rawResult: unknown): unknown {
|
||||
}
|
||||
|
||||
// CLI-only commands that bypass the operation layer
|
||||
export const CLI_ONLY = new Set(['init', 'reinit-pglite', 'pglite-repair', 'upgrade', 'post-upgrade', 'check-update', 'integrations', 'publish', 'check-backlinks', 'lint', 'report', 'import', 'export', 'files', 'embed', 'serve', 'call', 'config', 'doctor', 'migrate', 'eval', 'sync', 'extract', 'extract-conversation-facts', 'enrich', 'features', 'autopilot', 'graph-query', 'jobs', 'agent', 'apply-migrations', 'skillpack-check', 'skillpack', 'resolvers', 'integrity', 'repair-jsonb', 'orphans', 'maintain', 'sources', 'mounts', 'dream', 'check-resolvable', 'routing-eval', 'skillify', 'smoke-test', 'providers', 'storage', 'repos', 'code-def', 'code-refs', 'reindex', 'reindex-code', 'reindex-frontmatter', 'code-callers', 'code-callees', 'reconcile-links', 'frontmatter', 'auth', 'friction', 'claw-test', 'book-mirror', 'takes', 'think', 'salience', 'anomalies', 'calibration', 'transcripts', 'models', 'remote', 'recall', 'forget', 'edges-backfill', 'cache', 'ze-switch', 'retrieval-upgrade', 'founder', 'brainstorm', 'lsd', 'schema', 'capture', 'onboard', 'conversation-parser', 'status', 'connect', 'skillopt', 'quarantine', 'self-upgrade', 'advisor', 'watch', 'reindex-search-vector', 'pages', 'bench', 'backfill',
|
||||
export const CLI_ONLY = new Set(['init', 'reinit-pglite', 'pglite-repair', 'upgrade', 'post-upgrade', 'check-update', 'integrations', 'publish', 'check-backlinks', 'lint', 'report', 'import', 'export', 'files', 'embed', 'serve', 'call', 'config', 'doctor', 'migrate', 'eval', 'sync', 'extract', 'extract-conversation-facts', 'enrich', 'features', 'autopilot', 'graph-query', 'jobs', 'agent', 'apply-migrations', 'skillpack-check', 'skillpack', 'resolvers', 'integrity', 'repair-jsonb', 'orphans', 'maintain', 'sources', 'mounts', 'dream', 'check-resolvable', 'routing-eval', 'skillify', 'smoke-test', 'providers', 'storage', 'repos', 'code-def', 'code-refs', 'reindex', 'reindex-code', 'reindex-frontmatter', 'code-callers', 'code-callees', 'reconcile-links', 'frontmatter', 'auth', 'friction', 'claw-test', 'book-mirror', 'takes', 'think', 'salience', 'anomalies', 'calibration', 'transcripts', 'models', 'remote', 'recall', 'forget', 'edges-backfill', 'cache', 'ze-switch', 'retrieval-upgrade', 'founder', 'brainstorm', 'lsd', 'schema', 'capture', 'onboard', 'conversation-parser', 'status', 'connect', 'skillopt', 'quarantine', 'self-upgrade', 'protocol', 'advisor', 'watch', 'reindex-search-vector', 'pages', 'bench', 'backfill',
|
||||
// v0.42.58 (#2035 class, caught by the handleCliOnly reachability sweep):
|
||||
// full handler at `case 'notability-eval'` but never dispatchable.
|
||||
'notability-eval']);
|
||||
@@ -130,6 +131,9 @@ const CLI_ONLY_SELF_HELP = new Set([
|
||||
// `gbrain connect --help` prints its own usage (flags + examples) from
|
||||
// runConnect; route around the generic one-line short-circuit.
|
||||
'connect',
|
||||
// MEMORY_VERBS v1 (Cathedral 1): protocol ships its own detailed HELP
|
||||
// (subcommands, conformance targets, the cost-gated --synthesize flag).
|
||||
'protocol',
|
||||
// `gbrain init --help` prints its own usage from runInit; route around the
|
||||
// generic one-line short-circuit (matches `connect`). Without this, `init`
|
||||
// is in CLI_ONLY but not CLI_ONLY_SELF_HELP, so the dispatcher's generic
|
||||
@@ -551,6 +555,7 @@ async function main() {
|
||||
const result = normalizeLocalResult(rawResult);
|
||||
const output = formatResult(op.name, result, params);
|
||||
if (output) process.stdout.write(output);
|
||||
maybePrintConceptNudge(op.name, params);
|
||||
} catch (e: unknown) {
|
||||
// v0.42.20.0 (codex D4): on error, set exitCode + return so the `finally`
|
||||
// STILL runs (drains every background-work sink + disconnects). A bare
|
||||
@@ -634,6 +639,7 @@ async function runThinClientRouted(
|
||||
const result = unpackToolResult(raw);
|
||||
const output = formatResult(op.name, result, params);
|
||||
if (output) process.stdout.write(output);
|
||||
maybePrintConceptNudge(op.name, params);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof RemoteMcpError) {
|
||||
const url = cfg.remote_mcp!.mcp_url;
|
||||
@@ -1214,6 +1220,20 @@ export async function makeContext(engine: BrainEngine, params: Record<string, un
|
||||
}
|
||||
|
||||
// Exported for tests (same import-safety contract as cliAliases/printOpHelp).
|
||||
/**
|
||||
* #2416: hint-only steering — a concept-shaped `search` gets a one-line
|
||||
* stderr nudge toward `query`. Never fires for other ops, never reroutes
|
||||
* (search stays the cheap hot path), and honors --quiet — the same silence
|
||||
* discipline as the identity banner. Called from BOTH result paths (local
|
||||
* engine + thin-client routed); formatResult can't host this because it
|
||||
* never sees the query text.
|
||||
*/
|
||||
export function maybePrintConceptNudge(opName: string, params: Record<string, unknown>): void {
|
||||
if (opName !== 'search' || getCliOptions().quiet) return;
|
||||
const nudge = conceptNudge(String(params.query ?? ''));
|
||||
if (nudge) process.stderr.write(nudge + '\n');
|
||||
}
|
||||
|
||||
export function formatResult(
|
||||
opName: string,
|
||||
result: unknown,
|
||||
@@ -1343,11 +1363,70 @@ export function formatResult(
|
||||
`#${v.id} ${v.snapshot_at?.toString().slice(0, 19) || '?'} ${v.compiled_truth?.slice(0, 60) || ''}...`,
|
||||
).join('\n') + '\n';
|
||||
}
|
||||
// MEMORY_VERBS v1 [F-E]: human-readable by default; trailing `--json`
|
||||
// escapes to the raw envelope (parseOpArgs ignores an unmatched trailing
|
||||
// flag, so the argv probe is safe).
|
||||
case 'remember': {
|
||||
if (process.argv.includes('--json')) break;
|
||||
const r = result as any;
|
||||
if (r.dry_run) return `[dry-run] would remember: ${r.fact}\n`;
|
||||
const lines = [r.status_text || `${r.status} (fact #${r.id})`];
|
||||
if (r.entity_slug) lines.push(` entity: ${r.entity_slug}`);
|
||||
if (r.valid_until) lines.push(` expires: ${r.valid_until}`);
|
||||
if (r.degraded_dedup) lines.push(' note: no embedding provider — duplicate detection degraded');
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
case 'entity': {
|
||||
if (process.argv.includes('--json')) break;
|
||||
const r = result as any;
|
||||
if (!r.found) {
|
||||
const lines = [`No entity found. (${r.latency_ms}ms)`];
|
||||
if (Array.isArray(r.suggestions) && r.suggestions.length) {
|
||||
lines.push('Did you mean:');
|
||||
for (const s of r.suggestions) lines.push(` ${s.slug} — ${s.title} [${s.create_safety}]`);
|
||||
}
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
const c = r.card;
|
||||
const lines = [`${c.entity.title} (${c.entity.slug})${c.entity.type ? ` [${c.entity.type}]` : ''} (${r.latency_ms}ms)`];
|
||||
if (c.summary) lines.push(` ${c.summary}`);
|
||||
if (c.aka?.length) lines.push(` aka: ${c.aka.join(', ')}`);
|
||||
const lt = c.last_touched || {};
|
||||
const touched = lt.updated_at || lt.last_retrieved_at || lt.last_timeline_date;
|
||||
if (touched) lines.push(` last touched: ${String(touched).slice(0, 10)}`);
|
||||
if (c.open_threads?.length) {
|
||||
lines.push(' open threads:');
|
||||
for (const t of c.open_threads) lines.push(` [${t.kind}] ${t.text}${t.date ? ` (${String(t.date).slice(0, 10)})` : ''}`);
|
||||
}
|
||||
if (c.edges?.length) {
|
||||
lines.push(' edges:');
|
||||
for (const e of c.edges) lines.push(` ${e.direction === 'out' ? '→' : '←'} ${e.type} ${e.slug}`);
|
||||
}
|
||||
lines.push(` backlinks: ${c.backlink_count} | active facts: ${c.active_fact_count}`);
|
||||
if (Array.isArray(r.suggestions) && r.suggestions.length) {
|
||||
lines.push(' other matches:');
|
||||
for (const s of r.suggestions) lines.push(` ${s.slug} — ${s.title}`);
|
||||
}
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
case 'synthesize': {
|
||||
if (process.argv.includes('--json')) break;
|
||||
const r = result as any;
|
||||
const lines = [r.answer || '(no answer)'];
|
||||
if (Array.isArray(r.sources) && r.sources.length) lines.push('', `sources: ${r.sources.join(', ')}`);
|
||||
if (Array.isArray(r.gaps) && r.gaps.length) lines.push(`gaps: ${r.gaps.join('; ')}`);
|
||||
const cost = r.cost || {};
|
||||
const tok = cost.input_tokens != null ? `${cost.input_tokens} in / ${cost.output_tokens} out` : 'tokens n/a';
|
||||
const usd = cost.usd_estimate != null ? ` (~$${Number(cost.usd_estimate).toFixed(4)})` : '';
|
||||
lines.push(`cost: ${cost.model} — ${tok}${usd}`);
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
default:
|
||||
// bigintToStringReplacer keeps this fallback renderer crash-proof even
|
||||
// if a future caller hands it a not-yet-normalized result. (#2450)
|
||||
return JSON.stringify(result, bigintToStringReplacer, 2) + '\n';
|
||||
}
|
||||
return JSON.stringify(result, null, 2) + '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1472,6 +1551,14 @@ async function handleCliOnly(command: string, args: string[]) {
|
||||
await runSchema(args);
|
||||
return;
|
||||
}
|
||||
// MEMORY_VERBS v1 (Cathedral 1): protocol introspection + conformance +
|
||||
// local usage stats. No pre-bound engine — conformance spawns its own
|
||||
// server; stats reads the local JSONL sidecar.
|
||||
if (command === 'protocol') {
|
||||
const { runProtocol } = await import('./commands/protocol.ts');
|
||||
await runProtocol(args);
|
||||
return;
|
||||
}
|
||||
if (command === 'init') {
|
||||
const { runInit } = await import('./commands/init.ts');
|
||||
await runInit(args);
|
||||
@@ -2930,9 +3017,13 @@ ADMIN
|
||||
features [--json] [--auto-fix] Scan usage + recommend unused features
|
||||
autopilot [--repo] [--interval N] Self-maintaining brain daemon
|
||||
config [show|get|set] <key> [val] Brain config
|
||||
protocol [conformance|stats] MEMORY_VERBS v1: schemas, conformance
|
||||
certification, local usage stats + TTHW
|
||||
storage status [--repo <path>] Storage tier status and health
|
||||
[--json] (git-tracked vs supabase-only)
|
||||
serve MCP server (stdio)
|
||||
--surface verbs|full Tool surface: the 5 memory verbs only, or
|
||||
every op (default full; verbs = quickstart)
|
||||
serve --http [--port N] HTTP MCP server with OAuth 2.1
|
||||
--token-ttl N Access token TTL in seconds (default: 3600)
|
||||
--enable-dcr Enable Dynamic Client Registration (DCR clients default to authorization_code)
|
||||
|
||||
@@ -4944,6 +4944,46 @@ export async function computePoolReapHealthCheck(
|
||||
* Policy-skill install state is reported in details (it ships into the HOST
|
||||
* repo, so absence in gbrain's own skills dir is expected, not a failure).
|
||||
*/
|
||||
/**
|
||||
* MEMORY_VERBS v1 (Cathedral 1, E4) — usage-sidecar health. Read-only,
|
||||
* fail-open. Stats only (local JSONL, never uploaded; never source of truth):
|
||||
* - no sidecar file → ok, "no verb calls recorded yet" (fresh install)
|
||||
* - recent events parse → ok, names the last verb + timestamp
|
||||
* - file exists, unreadable→ warn (observability degraded, verbs unaffected)
|
||||
*/
|
||||
export async function buildMemoryVerbsCheck(): Promise<Check> {
|
||||
const name = 'memory_verbs_usage';
|
||||
try {
|
||||
const { readVerbUsage, usageLogPath } = await import('../core/verbs/usage-log.ts');
|
||||
if (!existsSync(usageLogPath())) {
|
||||
return {
|
||||
name,
|
||||
status: 'ok',
|
||||
message: 'no verb calls recorded yet (sidecar appears on first remember/recall/entity/synthesize/forget)',
|
||||
};
|
||||
}
|
||||
const events = await readVerbUsage({ days: 30 });
|
||||
if (events.length === 0) {
|
||||
return { name, status: 'ok', message: 'sidecar present; no verb calls in the last 30 days' };
|
||||
}
|
||||
const last = events[events.length - 1];
|
||||
const byVerb = new Map<string, number>();
|
||||
for (const e of events) byVerb.set(e.verb, (byVerb.get(e.verb) ?? 0) + 1);
|
||||
const mix = [...byVerb.entries()].map(([v, n]) => `${v}:${n}`).join(' ');
|
||||
return {
|
||||
name,
|
||||
status: 'ok',
|
||||
message: `${events.length} verb calls in 30d (${mix}); last ${last.verb} at ${last.ts} — local JSONL only, never uploaded`,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
name,
|
||||
status: 'warn',
|
||||
message: `verb usage sidecar unreadable (${e instanceof Error ? e.message : String(e)}) — observability degraded; verbs unaffected`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function buildRetrievalReflexCheck(skillsDir: string | null): Check {
|
||||
const name = 'retrieval_reflex_health';
|
||||
try {
|
||||
@@ -5137,6 +5177,13 @@ export async function buildChecks(
|
||||
checks.push(buildRetrievalReflexCheck(skillsDir));
|
||||
}
|
||||
|
||||
// 1c. MEMORY_VERBS v1 usage sidecar health (Cathedral 1, E4). Read-only,
|
||||
// fail-open: reports whether the local JSONL sidecar is present + parseable
|
||||
// and when a verb last fired. Local file only — never uploaded.
|
||||
if (scope === 'all') {
|
||||
checks.push(await buildMemoryVerbsCheck());
|
||||
}
|
||||
|
||||
// 2. Skill conformance (SKILL group — gated)
|
||||
if (scope === 'all' && skillsDir) {
|
||||
const conformanceResult = skillConformanceCheck(skillsDir);
|
||||
|
||||
@@ -1049,6 +1049,9 @@ async function initPGLite(opts: {
|
||||
// gbrain invocation). mode_prompted=true so the upgrade-time banner doesn't
|
||||
// also fire on a fresh install. Hands-off: gbrain config set self_upgrade.mode auto
|
||||
config.self_upgrade = { mode: 'notify', mode_prompted: true, ...(config.self_upgrade ?? {}) };
|
||||
// MEMORY_VERBS v1 [D6C]: TTHW stamp — `gbrain protocol stats` derives
|
||||
// install→first-verb-call from this. Idempotent on re-init.
|
||||
config.protocol_installed_at = config.protocol_installed_at ?? new Date().toISOString();
|
||||
saveConfig(config);
|
||||
if (opts.schemaPack) {
|
||||
process.stderr.write(
|
||||
@@ -1086,6 +1089,7 @@ async function initPGLite(opts: {
|
||||
} else {
|
||||
console.log('Next: gbrain import <dir>');
|
||||
}
|
||||
printMemoryVerbsQuickstart();
|
||||
console.log('');
|
||||
console.log('When you outgrow local: gbrain migrate --to supabase');
|
||||
reportModStatus();
|
||||
@@ -1103,6 +1107,26 @@ async function initPGLite(opts: {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MEMORY_VERBS v1 quickstart funnel (E3 + D4B + T1 consent). Printed at the
|
||||
* end of both init epilogues. The copy-next block is EXACTLY three commands
|
||||
* (codex DX 9): wire the harness, write a memory, prove the resurrection.
|
||||
* The demo uses the facts arm only, so it works with NO embedding key [F-B].
|
||||
*/
|
||||
function printMemoryVerbsQuickstart(): void {
|
||||
console.log('');
|
||||
console.log('Give your agent memory (copy these three commands):');
|
||||
console.log(' claude mcp add gbrain -- gbrain serve --surface verbs');
|
||||
console.log(' gbrain remember "I prefer dark mode in every editor" --provenance demo --entity people/me');
|
||||
console.log(' gbrain recall --entity people/me');
|
||||
console.log('Now ask your agent in a NEW session — it remembers.');
|
||||
console.log('');
|
||||
console.log('Note: memories agents save are readable by every agent connected to');
|
||||
console.log('this brain; use visibility:"private" for local-only facts.');
|
||||
console.log('Other harnesses (Codex, OpenClaw): docs/protocol/MEMORY_VERBS_v1.md');
|
||||
console.log('If `claude` is not found: install Claude Code first, or use the per-harness blocks in that doc.');
|
||||
}
|
||||
|
||||
async function initPostgres(opts: {
|
||||
databaseUrl: string;
|
||||
jsonOutput: boolean;
|
||||
@@ -1297,6 +1321,8 @@ async function initPostgres(opts: {
|
||||
// gbrain invocation). mode_prompted=true so the upgrade-time banner doesn't
|
||||
// also fire on a fresh install. Hands-off: gbrain config set self_upgrade.mode auto
|
||||
config.self_upgrade = { mode: 'notify', mode_prompted: true, ...(config.self_upgrade ?? {}) };
|
||||
// MEMORY_VERBS v1 [D6C]: TTHW stamp (see the PGLite path).
|
||||
config.protocol_installed_at = config.protocol_installed_at ?? new Date().toISOString();
|
||||
saveConfig(config);
|
||||
console.log('Config saved to ~/.gbrain/config.json');
|
||||
if (opts.schemaPack) {
|
||||
@@ -1331,6 +1357,7 @@ async function initPostgres(opts: {
|
||||
} else {
|
||||
console.log('Next: gbrain import <dir>');
|
||||
}
|
||||
printMemoryVerbsQuickstart();
|
||||
reportModStatus();
|
||||
const { printAdvisoryIfRecommended } = await import('../core/skillpack/post-install-advisory.ts');
|
||||
const { VERSION } = await import('../version.ts');
|
||||
|
||||
@@ -26,6 +26,7 @@ import { v0_28_0 } from './v0_28_0.ts';
|
||||
import { v0_29_1 } from './v0_29_1.ts';
|
||||
import { v0_31_0 } from './v0_31_0.ts';
|
||||
import { v0_32_2 } from './v0_32_2.ts';
|
||||
import { v0_43_0 } from './v0_43_0.ts';
|
||||
|
||||
export const migrations: Migration[] = [
|
||||
v0_11_0,
|
||||
@@ -43,6 +44,7 @@ export const migrations: Migration[] = [
|
||||
v0_29_1,
|
||||
v0_31_0,
|
||||
v0_32_2,
|
||||
v0_43_0,
|
||||
];
|
||||
|
||||
/** Look up a migration by exact version string. */
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* v0.43.0.0 migration — MEMORY_VERBS v1 (Cathedral 1).
|
||||
*
|
||||
* PITCH-ONLY. There is NO schema or data migration: the five frozen memory
|
||||
* verbs (recall/remember/entity/synthesize/forget) ride the existing facts,
|
||||
* pages, and typed-graph tables. This entry exists solely so `gbrain
|
||||
* post-upgrade` / the self-upgrade NOTIFY channel actively tells an existing
|
||||
* install that the verbs landed and how to switch a harness onto them — the
|
||||
* propagation path the verbs otherwise lacked (the default surface stays
|
||||
* `full`, so an upgrade alone does NOT steer agents to the verbs).
|
||||
*
|
||||
* The orchestrator is a no-op that reports `complete` immediately —
|
||||
* idempotent by construction (it does nothing), so apply-migrations records
|
||||
* the ledger row and moves on.
|
||||
*/
|
||||
|
||||
import type { Migration, OrchestratorOpts, OrchestratorResult } from './types.ts';
|
||||
|
||||
async function orchestrator(_opts: OrchestratorOpts): Promise<OrchestratorResult> {
|
||||
// No schema/data work — MEMORY_VERBS v1 is a façade over existing tables.
|
||||
return { version: '0.43.0', status: 'complete', phases: [] };
|
||||
}
|
||||
|
||||
export const v0_43_0: Migration = {
|
||||
version: '0.43.0',
|
||||
featurePitch: {
|
||||
headline:
|
||||
'Five memory verbs — recall, remember, entity, synthesize, forget — are now the agent-facing memory protocol (MEMORY_VERBS v1).',
|
||||
description:
|
||||
'Point any MCP harness at `gbrain serve --surface verbs` to expose exactly these five self-describing tools instead of the full op wall: remember(fact, provenance) writes durable facts; recall(query|entity, budget_tokens) returns budget-packed memory; entity(name) is a zero-LLM card; synthesize(question) is the explicitly-expensive cross-page answer; forget(id) expires a fact. Existing `gbrain serve` (full surface) keeps working and now also lists the verbs, but `--surface verbs` is the clean agent surface. Set a default with `gbrain config set mcp_surface verbs`. Verify any endpoint with `gbrain protocol conformance`; see usage with `gbrain protocol stats`. Full contract: docs/protocol/MEMORY_VERBS_v1.md.',
|
||||
},
|
||||
orchestrator,
|
||||
};
|
||||
@@ -0,0 +1,304 @@
|
||||
/**
|
||||
* MEMORY_VERBS v1 — `gbrain protocol` (Cathedral 1, E2 + E4 + D6C).
|
||||
*
|
||||
* gbrain protocol [--json] machine-readable verb schemas + version
|
||||
* gbrain protocol conformance [...] certify an MCP endpoint against the contract
|
||||
* gbrain protocol stats [--days N] local verb usage + TTHW (never uploaded)
|
||||
*
|
||||
* Conformance targets:
|
||||
* (default) spawn gbrain's own stdio server (self-certify)
|
||||
* --target http://host/mcp Streamable HTTP endpoint [--token gbrain_xxx]
|
||||
* --target "cmd arg arg" spawn any stdio MCP server (space-split; for
|
||||
* commands with complex quoting, certify via a
|
||||
* small wrapper script)
|
||||
*
|
||||
* Input schemas emit from the LIVE Operation defs (doc/code can't drift);
|
||||
* response shapes come from the hand-authored RESPONSE_SCHEMAS registry,
|
||||
* which conformance validates LIVE responses against — registry-vs-code
|
||||
* drift is caught by the same fixtures that certify servers [c8].
|
||||
*/
|
||||
|
||||
import { operationsByName } from '../core/operations.ts';
|
||||
import {
|
||||
RESPONSE_SCHEMAS,
|
||||
ERROR_SCHEMA,
|
||||
MEMORY_VERBS_VERSION,
|
||||
VERB_NAMES,
|
||||
type VerbName,
|
||||
} from '../core/verbs.ts';
|
||||
import { buildToolDefs } from '../mcp/tool-defs.ts';
|
||||
import { runConformance, type ConformanceClient } from '../core/verbs/conformance.ts';
|
||||
import { readVerbUsage, earliestVerbUsageTs, usageLogPath } from '../core/verbs/usage-log.ts';
|
||||
import { loadConfig } from '../core/config.ts';
|
||||
import { setCliExitVerdict } from '../core/cli-force-exit.ts';
|
||||
|
||||
const HELP = `gbrain protocol — the MEMORY_VERBS v1 wire contract (frozen, additive-forever)
|
||||
|
||||
Usage:
|
||||
gbrain protocol [--json] Print the protocol: verb input schemas
|
||||
(from live defs), response schemas, error
|
||||
contract, version. --json for machines.
|
||||
gbrain protocol conformance Certify an MCP endpoint against the
|
||||
contract. Default target: gbrain's own
|
||||
stdio server (self-certification).
|
||||
--target http://host:3131/mcp HTTP MCP endpoint (add --token gbrain_xxx)
|
||||
--target "bun run src/cli.ts serve" Any stdio MCP server command
|
||||
--synthesize Also live-call synthesize (costs money
|
||||
when an LLM key is configured; without
|
||||
one it asserts the clean 'unavailable'
|
||||
error — what CI does)
|
||||
--json Machine-readable report
|
||||
gbrain protocol stats [--days N] Per-verb usage, error rate, budget drops,
|
||||
entity hit rate, TTHW (install -> first
|
||||
verb call). Local JSONL only — this data
|
||||
never leaves the machine. Default 30 days.
|
||||
|
||||
Docs: docs/protocol/MEMORY_VERBS_v1.md
|
||||
Why default surface is 'full': verbs is for agents and quickstarts
|
||||
(gbrain serve --surface verbs); full preserves existing advanced tooling.`;
|
||||
|
||||
export async function runProtocol(args: string[]): Promise<void> {
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
console.log(HELP);
|
||||
return;
|
||||
}
|
||||
const sub = args[0] && !args[0].startsWith('--') ? args[0] : null;
|
||||
|
||||
if (sub === 'conformance') {
|
||||
await runConformanceCommand(args.slice(1));
|
||||
return;
|
||||
}
|
||||
if (sub === 'stats') {
|
||||
await runStatsCommand(args.slice(1));
|
||||
return;
|
||||
}
|
||||
if (sub === null) {
|
||||
printProtocol(args.includes('--json'));
|
||||
return;
|
||||
}
|
||||
console.error(`Unknown protocol subcommand: ${sub}`);
|
||||
console.log(HELP);
|
||||
// [ship P1.3] PGLite/WASM clobbers process.exitCode; the CLI exit seam reads
|
||||
// the gbrain-owned verdict (setCliExitVerdict), not process.exitCode.
|
||||
setCliExitVerdict(1);
|
||||
}
|
||||
|
||||
// ─── protocol [--json] ───────────────────────────────────────────────────────
|
||||
|
||||
function buildProtocolDocument() {
|
||||
const verbOps = VERB_NAMES.map(n => operationsByName[n]).filter(Boolean);
|
||||
const toolDefs = buildToolDefs(verbOps);
|
||||
const verbs: Record<string, unknown> = {};
|
||||
for (const def of toolDefs) {
|
||||
verbs[def.name] = {
|
||||
description: def.description,
|
||||
input_schema: def.inputSchema,
|
||||
...(def.annotations ? { annotations: def.annotations } : {}),
|
||||
response_schema: RESPONSE_SCHEMAS[def.name as VerbName],
|
||||
};
|
||||
}
|
||||
return {
|
||||
protocol: 'MEMORY_VERBS',
|
||||
protocol_version: MEMORY_VERBS_VERSION,
|
||||
versioning_policy:
|
||||
'additive-forever: v1 field names and semantics never change; new optional fields/params may be added; breaking changes require MEMORY_VERBS_v2 (expected never)',
|
||||
verbs,
|
||||
error_schema: ERROR_SCHEMA,
|
||||
};
|
||||
}
|
||||
|
||||
function printProtocol(json: boolean): void {
|
||||
const doc = buildProtocolDocument();
|
||||
if (json) {
|
||||
console.log(JSON.stringify(doc, null, 2));
|
||||
return;
|
||||
}
|
||||
console.log(`MEMORY_VERBS v${MEMORY_VERBS_VERSION} — the frozen memory protocol (additive-forever)\n`);
|
||||
for (const name of VERB_NAMES) {
|
||||
const op = operationsByName[name];
|
||||
if (!op) continue;
|
||||
const params = Object.entries(op.params)
|
||||
.map(([k, v]) => `${k}${v.required ? '' : '?'}`)
|
||||
.join(', ');
|
||||
console.log(` ${name}(${params})`);
|
||||
console.log(` ${op.description.split('. ')[0]}.`);
|
||||
}
|
||||
console.log(`\nFull schemas: gbrain protocol --json`);
|
||||
console.log(`Doc: docs/protocol/MEMORY_VERBS_v1.md`);
|
||||
}
|
||||
|
||||
// ─── protocol conformance ────────────────────────────────────────────────────
|
||||
|
||||
async function runConformanceCommand(args: string[]): Promise<void> {
|
||||
const json = args.includes('--json');
|
||||
const synthesize = args.includes('--synthesize');
|
||||
const targetIdx = args.indexOf('--target');
|
||||
const target = targetIdx >= 0 ? args[targetIdx + 1] : null;
|
||||
const tokenIdx = args.indexOf('--token');
|
||||
const token = tokenIdx >= 0 ? args[tokenIdx + 1] : null;
|
||||
|
||||
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
|
||||
const client = new Client({ name: 'gbrain-conformance', version: '1.0.0' }, { capabilities: {} });
|
||||
|
||||
let transport: { close(): Promise<void> };
|
||||
if (target && /^https?:\/\//.test(target)) {
|
||||
const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
|
||||
const t = new StreamableHTTPClientTransport(new URL(target), {
|
||||
...(token ? { requestInit: { headers: { Authorization: `Bearer ${token}` } } } : {}),
|
||||
});
|
||||
await client.connect(t);
|
||||
transport = t;
|
||||
} else {
|
||||
const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');
|
||||
let command: string;
|
||||
let cmdArgs: string[];
|
||||
if (target) {
|
||||
const parts = target.split(/\s+/).filter(Boolean);
|
||||
command = parts[0];
|
||||
cmdArgs = parts.slice(1);
|
||||
} else {
|
||||
// Self-certification: spawn our own server. Dev (bun run src/cli.ts)
|
||||
// vs compiled binary both resolve to "this gbrain, serve".
|
||||
const entry = process.argv[1] ?? '';
|
||||
if (entry.endsWith('.ts')) {
|
||||
command = process.execPath;
|
||||
cmdArgs = ['run', entry, 'serve'];
|
||||
} else {
|
||||
command = process.execPath;
|
||||
cmdArgs = ['serve'];
|
||||
}
|
||||
}
|
||||
const env: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(process.env)) if (v !== undefined) env[k] = v;
|
||||
const t = new StdioClientTransport({ command, args: cmdArgs, env });
|
||||
await client.connect(t);
|
||||
transport = t;
|
||||
}
|
||||
|
||||
const adapter: ConformanceClient = {
|
||||
listTools: async () => {
|
||||
const { tools } = await client.listTools();
|
||||
return tools.map(t => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
annotations: (t as { annotations?: unknown }).annotations,
|
||||
}));
|
||||
},
|
||||
callTool: async (name, callArgs) => {
|
||||
const res = (await client.callTool({ name, arguments: callArgs })) as {
|
||||
isError?: boolean;
|
||||
content?: Array<{ type?: string; text?: string }>;
|
||||
};
|
||||
const text = (res.content ?? []).map(c => (typeof c.text === 'string' ? c.text : '')).join('\n');
|
||||
return { isError: res.isError, text };
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const report = await runConformance(adapter, { synthesize });
|
||||
if (json) {
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
} else {
|
||||
console.log(`MEMORY_VERBS v${report.protocol_version} conformance — target: ${target ?? 'self (stdio)'}\n`);
|
||||
for (const r of report.results) {
|
||||
const mark = r.status === 'pass' ? '✓' : r.status === 'skip' ? '−' : '✗';
|
||||
console.log(` ${mark} ${r.name}${r.detail ? ` — ${r.detail}` : ''}`);
|
||||
}
|
||||
console.log(`\n${report.passed} passed, ${report.failed} failed, ${report.skipped} skipped`);
|
||||
console.log(report.ok ? 'CONFORMANT' : 'NOT CONFORMANT');
|
||||
}
|
||||
// [ship P1.3] non-conformant target MUST exit non-zero so CI fails. The
|
||||
// CLI exit seam reads setCliExitVerdict (process.exitCode is unreliable on
|
||||
// PGLite/WASM and ignored by the force-exit path).
|
||||
if (!report.ok) setCliExitVerdict(1);
|
||||
} finally {
|
||||
try { await client.close(); } catch { /* best-effort */ }
|
||||
try { await transport.close(); } catch { /* best-effort */ }
|
||||
}
|
||||
}
|
||||
|
||||
// ─── protocol stats ──────────────────────────────────────────────────────────
|
||||
|
||||
async function runStatsCommand(args: string[]): Promise<void> {
|
||||
const json = args.includes('--json');
|
||||
const daysIdx = args.indexOf('--days');
|
||||
const days = daysIdx >= 0 ? parseInt(args[daysIdx + 1] ?? '30', 10) || 30 : 30;
|
||||
|
||||
const events = await readVerbUsage({ days });
|
||||
const byVerb = new Map<string, { calls: number; errors: number; latency: number; budgetDropped: number; entityFound: number; entityMiss: number }>();
|
||||
for (const e of events) {
|
||||
const v = byVerb.get(e.verb) ?? { calls: 0, errors: 0, latency: 0, budgetDropped: 0, entityFound: 0, entityMiss: 0 };
|
||||
v.calls += 1;
|
||||
if (!e.ok) v.errors += 1;
|
||||
v.latency += e.latency_ms;
|
||||
if (typeof e.budget_dropped === 'number') v.budgetDropped += e.budget_dropped;
|
||||
if (e.entity_found === true) v.entityFound += 1;
|
||||
if (e.entity_found === false) v.entityMiss += 1;
|
||||
byVerb.set(e.verb, v);
|
||||
}
|
||||
|
||||
// TTHW [D6C]: install stamp → first verb call, the real measured number the
|
||||
// post-ship boomerang review compares against the 2–5 min target.
|
||||
const cfg = loadConfig();
|
||||
const installedAt = cfg?.protocol_installed_at ?? null;
|
||||
const firstCall = await earliestVerbUsageTs();
|
||||
let tthw: string | null = null;
|
||||
if (installedAt && firstCall) {
|
||||
const deltaMs = Date.parse(firstCall) - Date.parse(installedAt);
|
||||
if (Number.isFinite(deltaMs) && deltaMs >= 0) tthw = formatDuration(deltaMs);
|
||||
}
|
||||
|
||||
if (json) {
|
||||
console.log(JSON.stringify({
|
||||
days,
|
||||
total_calls: events.length,
|
||||
by_verb: Object.fromEntries(
|
||||
[...byVerb.entries()].map(([k, v]) => [k, {
|
||||
calls: v.calls,
|
||||
errors: v.errors,
|
||||
avg_latency_ms: v.calls ? Math.round(v.latency / v.calls) : 0,
|
||||
budget_dropped_total: v.budgetDropped,
|
||||
...(k === 'entity' ? { found: v.entityFound, miss: v.entityMiss } : {}),
|
||||
}]),
|
||||
),
|
||||
tthw_install_to_first_verb: tthw,
|
||||
installed_at: installedAt,
|
||||
first_verb_call: firstCall,
|
||||
sidecar: usageLogPath(),
|
||||
privacy: 'local JSONL only — never uploaded',
|
||||
}, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`MEMORY_VERBS usage — last ${days} days (local JSONL only, never uploaded)\n`);
|
||||
if (events.length === 0) {
|
||||
console.log(' no verb calls recorded yet');
|
||||
} else {
|
||||
for (const [verb, v] of [...byVerb.entries()].sort((a, b) => b[1].calls - a[1].calls)) {
|
||||
const extras = [
|
||||
v.errors ? `${v.errors} errors` : null,
|
||||
v.budgetDropped ? `${v.budgetDropped} budget-dropped` : null,
|
||||
verb === 'entity' && (v.entityFound || v.entityMiss)
|
||||
? `hit ${v.entityFound}/${v.entityFound + v.entityMiss}`
|
||||
: null,
|
||||
].filter(Boolean).join(', ');
|
||||
console.log(` ${verb.padEnd(11)} ${String(v.calls).padStart(5)} calls avg ${Math.round(v.latency / v.calls)}ms${extras ? ` (${extras})` : ''}`);
|
||||
}
|
||||
}
|
||||
if (tthw) console.log(`\n TTHW: first verb call ${tthw} after install`);
|
||||
else if (installedAt) console.log(`\n TTHW: no verb calls since install (${installedAt})`);
|
||||
console.log(` sidecar: ${usageLogPath()}`);
|
||||
}
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
const s = Math.round(ms / 1000);
|
||||
if (s < 60) return `${s}s`;
|
||||
const m = Math.floor(s / 60);
|
||||
if (m < 60) return `${m}m${s % 60 ? `${s % 60}s` : ''}`;
|
||||
const h = Math.floor(m / 60);
|
||||
if (h < 48) return `${h}h${m % 60 ? `${m % 60}m` : ''}`;
|
||||
return `${Math.floor(h / 24)}d`;
|
||||
}
|
||||
|
||||
/** Test seam: the protocol document, without printing. */
|
||||
export { buildProtocolDocument };
|
||||
@@ -66,6 +66,10 @@ interface ParsedFlags {
|
||||
json: boolean;
|
||||
source: string;
|
||||
limit: number;
|
||||
// MEMORY_VERBS v1 [c4]: recall's verb params, routed through the recall OP
|
||||
// (this hand-rolled CLI otherwise ignores unknown flags silently).
|
||||
query: string | null;
|
||||
budgetTokens: number | null;
|
||||
// v0.32
|
||||
sinceLastRun: boolean;
|
||||
pending: boolean;
|
||||
@@ -94,6 +98,8 @@ function parseFlags(args: string[]): ParsedFlags {
|
||||
json: false,
|
||||
source: 'default',
|
||||
limit: 50,
|
||||
query: null,
|
||||
budgetTokens: null,
|
||||
sinceLastRun: false,
|
||||
pending: false,
|
||||
rollup: false,
|
||||
@@ -112,6 +118,8 @@ function parseFlags(args: string[]): ParsedFlags {
|
||||
if (a === '--json') { out.json = true; continue; }
|
||||
if (a === '--source') { out.source = args[++i] ?? 'default'; continue; }
|
||||
if (a === '--limit') { out.limit = parseInt(args[++i] ?? '50', 10) || 50; continue; }
|
||||
if (a === '--query') { out.query = args[++i] ?? null; continue; }
|
||||
if (a === '--budget-tokens') { out.budgetTokens = parseInt(args[++i] ?? '', 10) || null; continue; }
|
||||
if (a === '--since-last-run') { out.sinceLastRun = true; continue; }
|
||||
if (a === '--pending') { out.pending = true; continue; }
|
||||
if (a === '--rollup') { out.rollup = true; continue; }
|
||||
@@ -223,6 +231,13 @@ export async function runRecall(engine: BrainEngine, args: string[]): Promise<vo
|
||||
|
||||
const sourceId = await resolveSourceForRecall(engine, flags.source, thinClient);
|
||||
|
||||
// MEMORY_VERBS v1 [c4]: the verb params route through the recall OP so the
|
||||
// CLI and MCP exercise the same arm (query/budget packing/superset envelope).
|
||||
if (flags.query !== null || flags.budgetTokens !== null) {
|
||||
await runRecallVerb(engine, flags, sourceId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (flags.watchSeconds !== null) {
|
||||
await runWatchLoop(engine, flags, sourceId, thinClient, flags.watchSeconds);
|
||||
return;
|
||||
@@ -230,6 +245,69 @@ export async function runRecall(engine: BrainEngine, args: string[]): Promise<vo
|
||||
await runRecallOnce(engine, flags, sourceId, thinClient, 'briefing');
|
||||
}
|
||||
|
||||
/**
|
||||
* MEMORY_VERBS v1 — `gbrain recall --query ... [--budget-tokens N]` routes
|
||||
* through the recall OP (same code path MCP exercises) and renders facts +
|
||||
* search results with the budget footer. `--json` prints the raw envelope.
|
||||
*/
|
||||
async function runRecallVerb(engine: BrainEngine, flags: ParsedFlags, sourceId: string): Promise<void> {
|
||||
const { operationsByName } = await import('../core/operations.ts');
|
||||
const op = operationsByName['recall'];
|
||||
const ctx = {
|
||||
engine,
|
||||
config: loadConfig() || { engine: 'pglite' as const },
|
||||
logger: {
|
||||
info: (m: string) => process.stderr.write(`[info] ${m}\n`),
|
||||
warn: (m: string) => process.stderr.write(`[warn] ${m}\n`),
|
||||
error: (m: string) => process.stderr.write(`[error] ${m}\n`),
|
||||
},
|
||||
dryRun: false,
|
||||
remote: false as const,
|
||||
sourceId,
|
||||
};
|
||||
const result = (await op.handler(ctx, {
|
||||
...(flags.entity ? { entity: flags.entity } : {}),
|
||||
...(flags.query ? { query: flags.query } : {}),
|
||||
...(flags.budgetTokens ? { budget_tokens: flags.budgetTokens } : {}),
|
||||
...(flags.since ? { since: flags.since.toISOString() } : {}),
|
||||
...(flags.grep ? { grep: flags.grep } : {}),
|
||||
include_expired: flags.includeExpired,
|
||||
limit: flags.limit,
|
||||
})) as {
|
||||
facts: Array<{ fact_id: string; fact: string; kind: string; entity_slug: string | null; provenance: string }>;
|
||||
results?: Array<{ slug: string; title: string | null; evidence: string; chunk: string | null }>;
|
||||
search_degraded?: string;
|
||||
budget_tokens?: number;
|
||||
budget_used?: number;
|
||||
dropped_count?: number;
|
||||
};
|
||||
|
||||
if (flags.json) {
|
||||
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
||||
return;
|
||||
}
|
||||
const lines: string[] = [];
|
||||
if (result.facts.length) {
|
||||
lines.push('Facts:');
|
||||
for (const f of result.facts) {
|
||||
lines.push(` #${f.fact_id} [${f.kind}]${f.entity_slug ? ` (${f.entity_slug})` : ''} ${f.fact} — ${f.provenance}`);
|
||||
}
|
||||
}
|
||||
if (result.results?.length) {
|
||||
lines.push('Pages:');
|
||||
for (const r of result.results) {
|
||||
lines.push(` ${r.slug} [${r.evidence}] ${r.title ?? ''}`);
|
||||
if (r.chunk) lines.push(` ${r.chunk.replace(/\s+/g, ' ').slice(0, 160)}`);
|
||||
}
|
||||
}
|
||||
if (!lines.length) lines.push('Nothing recalled.');
|
||||
if (result.search_degraded) lines.push(`note: search degraded (${result.search_degraded})`);
|
||||
if (result.budget_tokens !== undefined) {
|
||||
lines.push(`budget: ${result.budget_used}/${result.budget_tokens} tokens used, ${result.dropped_count} dropped`);
|
||||
}
|
||||
process.stdout.write(lines.join('\n') + '\n');
|
||||
}
|
||||
|
||||
async function runRecallOnce(
|
||||
engine: BrainEngine,
|
||||
flags: ParsedFlags,
|
||||
|
||||
@@ -33,6 +33,7 @@ import { hasScope, ALLOWED_SCOPES_LIST, normalizeScopesInput } from '../core/sco
|
||||
import { normalizeSourceInput, normalizeFederatedReadInput } from '../core/source-id.ts';
|
||||
import { summarizeMcpParams, dispatchToolCall } from '../mcp/dispatch.ts';
|
||||
import { paramDefToSchema } from '../mcp/tool-defs.ts';
|
||||
import { filterOpsForSurface } from '../mcp/surface.ts';
|
||||
import { getBrainHotMemoryMeta } from '../core/facts/meta-hook.ts';
|
||||
import { loadConfig } from '../core/config.ts';
|
||||
import { buildError, serializeError } from '../core/errors.ts';
|
||||
@@ -475,6 +476,12 @@ interface ServeHttpOptions {
|
||||
* tracking the regenerated value through other means.
|
||||
*/
|
||||
suppressBootstrapToken?: boolean;
|
||||
/**
|
||||
* MEMORY_VERBS v1: tool-surface mode. 'verbs' = exactly the five protocol
|
||||
* verbs; 'full' (default) = every non-localOnly operation. Enforced on the
|
||||
* tool list AND in dispatch (fail-closed).
|
||||
*/
|
||||
surface?: 'verbs' | 'full';
|
||||
/**
|
||||
* #2624: force-print the generated admin bootstrap token even on a
|
||||
* non-TTY (containerized) start. By default the raw token is only printed
|
||||
@@ -1859,7 +1866,13 @@ export async function runServeHttp(engine: BrainEngine, options: ServeHttpOption
|
||||
// ---------------------------------------------------------------------------
|
||||
// MCP tool calls (bearer auth + scope enforcement)
|
||||
// ---------------------------------------------------------------------------
|
||||
const mcpOperations = operations.filter(op => !op.localOnly);
|
||||
// MEMORY_VERBS v1: surface filter applies AFTER the localOnly filter; the
|
||||
// same set feeds dispatch as allowedOps so hidden ops are uncallable, not
|
||||
// just unlisted [c2].
|
||||
const surface = options.surface ?? 'full';
|
||||
const mcpOperations = filterOpsForSurface(operations.filter(op => !op.localOnly), surface);
|
||||
const surfaceAllowedOps: ReadonlySet<string> | undefined =
|
||||
surface === 'full' ? undefined : new Set(mcpOperations.map(o => o.name));
|
||||
|
||||
// v0.36.x #1076: MCP Streamable HTTP spec — GET /mcp opens an optional SSE
|
||||
// backchannel for server-initiated messages. gbrain's transport is stateless
|
||||
@@ -1922,6 +1935,9 @@ export async function runServeHttp(engine: BrainEngine, options: ServeHttpOption
|
||||
),
|
||||
required: Object.entries(op.params).filter(([, v]) => v.required).map(([k]) => k),
|
||||
},
|
||||
// MEMORY_VERBS v1: ToolAnnotations emitted only when the op defines
|
||||
// them — existing tools stay byte-identical (mirrors buildToolDefs).
|
||||
...(op.annotations ? { annotations: op.annotations } : {}),
|
||||
})),
|
||||
};
|
||||
});
|
||||
@@ -2047,6 +2063,9 @@ export async function runServeHttp(engine: BrainEngine, options: ServeHttpOption
|
||||
takesHoldersAllowList: tokenAllowList,
|
||||
sourceId: tokenSourceId,
|
||||
metaHook: getBrainHotMemoryMeta,
|
||||
// MEMORY_VERBS v1: fail-closed surface enforcement + usage attribution.
|
||||
...(surfaceAllowedOps ? { allowedOps: surfaceAllowedOps } : {}),
|
||||
surface,
|
||||
// v0.31 follow-up fix: thread auth so the whoami op (and any
|
||||
// future scope-aware handlers) can introspect the caller. The
|
||||
// original D12/eE1 refactor moved dispatch into dispatchToolCall
|
||||
|
||||
+15
-4
@@ -44,7 +44,7 @@ export interface ServeOptions {
|
||||
// (which unconditionally attaches a 'data' listener to real
|
||||
// process.stdin and would pollute the test runner's stdin handle).
|
||||
// Defaults to the real implementation when omitted.
|
||||
startMcpServer?: (engine: BrainEngine) => Promise<void>;
|
||||
startMcpServer?: (engine: BrainEngine, opts?: { surface?: 'verbs' | 'full' }) => Promise<void>;
|
||||
// Test seam for the parent-process watchdog. The default
|
||||
// (`readLiveParentPid`) reads the live kernel PPID via `ps` on POSIX
|
||||
// because `process.ppid` is captured at process creation and does not
|
||||
@@ -150,6 +150,13 @@ export async function runServe(
|
||||
// that used `gbrain auth create` keep working unchanged).
|
||||
const isHttp = args.includes('--http');
|
||||
|
||||
// MEMORY_VERBS v1: tool-surface mode. Flag > config `mcp_surface` > 'full'.
|
||||
// 'verbs' exposes exactly the five protocol verbs (the quickstart surface);
|
||||
// 'full' (default) keeps every operation — existing installs see no change.
|
||||
const { parseSurfaceFlag, resolveSurface } = await import('../mcp/surface.ts');
|
||||
const { loadConfig } = await import('../core/config.ts');
|
||||
const surface = resolveSurface(parseSurfaceFlag(args), loadConfig());
|
||||
|
||||
if (isHttp) {
|
||||
const portIdx = args.indexOf('--port');
|
||||
const port = portIdx >= 0 ? parseInt(args[portIdx + 1]) || 3131 : 3131;
|
||||
@@ -196,7 +203,7 @@ export async function runServe(
|
||||
const printAdminToken = args.includes('--print-admin-token');
|
||||
|
||||
const { runServeHttp } = await import('./serve-http.ts');
|
||||
await runServeHttp(engine, { port, tokenTtl, enableDcr, enableDcrInsecure, publicUrl, logFullParams, bind, suppressBootstrapToken, printAdminToken });
|
||||
await runServeHttp(engine, { port, tokenTtl, enableDcr, enableDcrInsecure, publicUrl, logFullParams, bind, suppressBootstrapToken, printAdminToken, surface });
|
||||
|
||||
await finishHttpServe(engine, opts);
|
||||
return;
|
||||
@@ -207,7 +214,11 @@ export async function runServe(
|
||||
// trigger graceful release of the PGLite write lock held by `engine`.
|
||||
// The HTTP / OAuth path above has its own lifecycle in serve-http.ts
|
||||
// and is intentionally NOT wired into this stdio plumbing.
|
||||
console.error('Starting GBrain MCP server (stdio)...');
|
||||
console.error(
|
||||
surface === 'verbs'
|
||||
? 'Starting GBrain MCP server (stdio) — serving 5 memory verbs (MEMORY_VERBS v1)...'
|
||||
: 'Starting GBrain MCP server (stdio)...',
|
||||
);
|
||||
|
||||
installStdioLifecycle(engine, args, opts);
|
||||
|
||||
@@ -245,7 +256,7 @@ export async function runServe(
|
||||
}
|
||||
|
||||
try {
|
||||
await start(engine);
|
||||
await start(engine, { surface });
|
||||
} finally {
|
||||
if (bootDeadline) clearTimeout(bootDeadline);
|
||||
}
|
||||
|
||||
@@ -8,20 +8,20 @@
|
||||
// (help-text mentions count): accepting an ignored flag is the pre-#2185
|
||||
// status quo; missing a real one breaks working invocations.
|
||||
export const CLI_FLAG_REGISTRY: Record<string, readonly string[]> = {
|
||||
'advisor': ['--aliases', '--all', '--apply', '--brain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--skills-dir', '--source', '--stale', '--supersessions', '--thin', '--verbose', '--workspace', '--yes'],
|
||||
'advisor': ['--aliases', '--all', '--apply', '--brain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--skills-dir', '--source', '--stale', '--supersessions', '--surface', '--thin', '--verbose', '--workspace', '--yes'],
|
||||
'agent': ['--aliases', '--all', '--brain', '--detach', '--fanout-manifest', '--flag', '--flags', '--follow', '--help', '--include-null-signature', '--json', '--max-turns', '--model', '--no-extract', '--no-follow', '--note', '--pattern', '--pending', '--reset', '--resolve', '--since', '--source', '--stale', '--subagent-def', '--supersessions', '--thin', '--timeout-ms', '--tools', '--word'],
|
||||
'anomalies': ['--aliases', '--all', '--brain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--lookback-days', '--mcp-only', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--sigma', '--since', '--source', '--stale', '--supersessions', '--thin', '--timeout'],
|
||||
'apply-migrations': ['--ab', '--all', '--auto-update', '--brain', '--break-lock', '--build-index', '--by-mention', '--compile', '--days', '--dry-run', '--exclusive', '--fast', '--force', '--force-all', '--force-orchestrator', '--force-retry', '--force-schema', '--from-meetings', '--from-pages', '--help', '--history', '--host-dir', '--http', '--json', '--lang', '--list', '--locks', '--markdown', '--max-age', '--migrate-only', '--migration', '--mode', '--multimodal', '--no-autopilot-install', '--no-embedding', '--no-extract', '--non-interactive', '--phase', '--priority', '--refresh-unqualified', '--remediate', '--rollback', '--skip-verify', '--source', '--stale', '--undo-wave', '--use-captured-snapshot', '--with-calibration', '--yes'],
|
||||
'auth': ['--aliases', '--all', '--bound-brain', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-usd-per-day', '--enable-dcr', '--enable-dcr-insecure', '--fast', '--federated-read', '--force', '--from-pages', '--grant-types', '--help', '--http', '--include-null-signature', '--json', '--no-embedding', '--no-extract', '--pattern', '--pending', '--redirect-uri', '--reset', '--resolve', '--scopes', '--source', '--stale', '--supersessions', '--takes-holders', '--thin', '--token', '--token-endpoint-auth-method', '--yes'],
|
||||
'autopilot': ['--aliases', '--all', '--auto-fix', '--batch', '--brain', '--break-lock', '--by-type', '--check', '--dimensions', '--dir', '--drain', '--embedding-dimensions', '--embedding-model', '--explain', '--fast', '--ff-only', '--fix', '--force', '--force-break-lock', '--force-retry', '--from-pages', '--full', '--help', '--http', '--include-null-signature', '--inject-bootstrap', '--inline', '--input', '--install', '--interval', '--json', '--markdown', '--max-age', '--max-rss', '--max-usd', '--migrate-only', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--no-extract', '--no-inject', '--no-mutate', '--no-worker', '--non-interactive', '--now', '--once', '--output', '--path', '--pattern', '--pending', '--phase', '--refresh-cache', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--source', '--stale', '--status', '--supersessions', '--swap-only', '--target', '--target-score', '--thin', '--timeout', '--to', '--uninstall', '--unsafe-bypass-dream-guard', '--user', '--version', '--yes'],
|
||||
'anomalies': ['--aliases', '--all', '--brain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--lookback-days', '--mcp-only', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--sigma', '--since', '--source', '--stale', '--supersessions', '--surface', '--thin', '--timeout'],
|
||||
'apply-migrations': ['--ab', '--all', '--auto-update', '--brain', '--break-lock', '--build-index', '--by-mention', '--compile', '--days', '--dry-run', '--exclusive', '--fast', '--force', '--force-all', '--force-orchestrator', '--force-retry', '--force-schema', '--from-meetings', '--from-pages', '--help', '--history', '--host-dir', '--http', '--json', '--lang', '--list', '--locks', '--markdown', '--max-age', '--migrate-only', '--migration', '--mode', '--multimodal', '--no-autopilot-install', '--no-embedding', '--no-extract', '--non-interactive', '--phase', '--priority', '--refresh-unqualified', '--remediate', '--rollback', '--skip-verify', '--source', '--stale', '--surface', '--undo-wave', '--use-captured-snapshot', '--with-calibration', '--yes'],
|
||||
'auth': ['--aliases', '--all', '--bound-brain', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-usd-per-day', '--enable-dcr', '--enable-dcr-insecure', '--fast', '--federated-read', '--force', '--from-pages', '--grant-types', '--help', '--http', '--include-null-signature', '--json', '--no-embedding', '--no-extract', '--pattern', '--pending', '--redirect-uri', '--reset', '--resolve', '--scopes', '--source', '--stale', '--supersessions', '--surface', '--takes-holders', '--thin', '--token', '--token-endpoint-auth-method', '--yes'],
|
||||
'autopilot': ['--aliases', '--all', '--auto-fix', '--batch', '--brain', '--break-lock', '--by-type', '--check', '--dimensions', '--dir', '--drain', '--embedding-dimensions', '--embedding-model', '--explain', '--fast', '--ff-only', '--fix', '--force', '--force-break-lock', '--force-retry', '--from-pages', '--full', '--help', '--http', '--include-null-signature', '--inject-bootstrap', '--inline', '--input', '--install', '--interval', '--json', '--markdown', '--max-age', '--max-rss', '--max-usd', '--migrate-only', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--no-extract', '--no-inject', '--no-mutate', '--no-worker', '--non-interactive', '--now', '--once', '--output', '--path', '--pattern', '--pending', '--phase', '--refresh-cache', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--source', '--stale', '--status', '--supersessions', '--surface', '--swap-only', '--target', '--target-score', '--thin', '--timeout', '--to', '--uninstall', '--unsafe-bypass-dream-guard', '--user', '--version', '--yes'],
|
||||
'backfill': ['--aliases', '--all', '--batch-size', '--brain', '--concurrency', '--dry-run', '--fresh', '--help', '--include-null-signature', '--json', '--keep-index', '--list', '--max-errors', '--max-rows', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--resume', '--source', '--stale', '--supersessions', '--thin'],
|
||||
'bench': ['--baseline', '--brain', '--explain', '--force', '--from', '--help', '--json', '--label', '--lang', '--limit', '--markdown', '--multimodal', '--near-symbol', '--restore-only', '--source', '--stale', '--symbol-kind', '--thin', '--threshold-jaccard', '--threshold-latency-multiplier', '--threshold-top1', '--to', '--tool'],
|
||||
'book-mirror': ['--aliases', '--all', '--allow-empty', '--apply', '--asof', '--author', '--auto', '--background', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--brain-wide-max-cost-usd', '--budget-usd-per-day', '--by-mention', '--chapters-dir', '--content', '--context-file', '--date', '--days', '--dry-run', '--entities', '--explain', '--fast', '--federated', '--file', '--follow', '--force', '--from-pages', '--help', '--http', '--image', '--include-null-signature', '--json', '--kind', '--limit', '--max-turns', '--max-usd', '--mode', '--model', '--multimodal', '--no-confirm', '--no-embedding', '--no-extract', '--no-follow', '--offset', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--save', '--session', '--since', '--slug', '--slugs', '--source', '--stale', '--stats', '--supersessions', '--thin', '--timeout', '--timeout-ms', '--title', '--trusted-extraction', '--url', '--with-db', '--yes'],
|
||||
'brainstorm': ['--aliases', '--all', '--brain', '--chunker-debug', '--code', '--compile', '--fast', '--fix', '--force', '--force-rechunk', '--force-resume', '--from-pages', '--full', '--help', '--http', '--include-null-signature', '--json', '--judge-model', '--lang', '--limit', '--list-runs', '--markdown', '--max-cost', '--max-far-set', '--max-ideas-per-judge-call', '--model', '--no-embed', '--no-embedding', '--no-extract', '--no-save', '--pattern', '--pending', '--reset', '--resolve', '--resume', '--retry-failed', '--retry-judge', '--save', '--source', '--stale', '--strict-budget', '--supersessions', '--thin', '--timeout', '--yes'],
|
||||
'cache': ['--brain', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--no-embedding', '--source', '--yes'],
|
||||
'calibration': ['--ab', '--aliases', '--all', '--allow-empty', '--apply', '--asof', '--auto', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-usd-per-day', '--by-mention', '--content', '--date', '--days', '--dry-run', '--entities', '--explain', '--fast', '--federated', '--file', '--force', '--from-pages', '--help', '--holder', '--http', '--image', '--include-null-signature', '--json', '--key-prefix', '--kind', '--lang', '--limit', '--markdown', '--max-usd', '--mode', '--multimodal', '--near-symbol', '--no-embedding', '--no-extract', '--no-federated', '--offset', '--path', '--pattern', '--pending', '--phase', '--progress-interval', '--progress-json', '--quiet', '--regenerate', '--repo', '--reset', '--resolve', '--restore-only', '--save', '--scrub-gstack', '--session', '--since', '--slug', '--slugs', '--source', '--stale', '--stats', '--supersessions', '--symbol-kind', '--thin', '--trusted-extraction', '--undo-wave', '--url', '--with-calibration', '--with-db', '--yes'],
|
||||
'call': ['--aliases', '--all', '--all-sources', '--as-context', '--auto-fix', '--background', '--brain', '--by-mention', '--catch-up', '--concurrency', '--confirm-destructive', '--content', '--cost-estimate', '--count', '--days', '--depth', '--dim', '--dir', '--direction', '--enable-dcr', '--enable-dcr-insecure', '--explain', '--fast', '--federated', '--file', '--fix', '--follow', '--force', '--from', '--from-meetings', '--grant-types', '--grep', '--hard-deadline', '--help', '--http', '--image', '--include-expired', '--include-frontmatter', '--include-null-signature', '--infer-dates', '--install', '--interval', '--json', '--key', '--kind', '--lang', '--limit', '--link-source', '--link-type', '--migrate-only', '--missing-path', '--multimodal', '--ner', '--no-embed', '--no-expand', '--no-extract', '--no-federated', '--no-hard-deadline', '--no-retry-connect', '--no-save', '--older-than', '--page', '--param', '--params', '--password', '--path', '--pattern', '--pending', '--pglite', '--port', '--progress-interval', '--progress-json', '--public-url', '--queue', '--quiet', '--reenrich-after', '--refresh-cache', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--restore-only', '--save', '--scopes', '--session', '--sigma', '--since', '--slug', '--slug-prefix', '--source', '--source-id', '--stale', '--status', '--stdin', '--strategy', '--supabase', '--supersessions', '--symbol-kind', '--tag', '--thin', '--timeout', '--to', '--today', '--token', '--token-ttl', '--tools-json', '--type', '--url', '--version', '--watch', '--with-calibration', '--workers', '--yes'],
|
||||
'capture': ['--aliases', '--all', '--allow-empty', '--apply', '--asof', '--auto', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-usd-per-day', '--by-mention', '--content', '--date', '--days', '--depth', '--entities', '--fast', '--federated', '--file', '--force', '--from-pages', '--help', '--http', '--image', '--include-null-signature', '--json', '--kind', '--limit', '--max-usd', '--mcp-only', '--mode', '--multimodal', '--no-embedding', '--no-extract', '--no-federated', '--offset', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--repo', '--reset', '--resolve', '--restore-only', '--save', '--scopes', '--session', '--since', '--slug', '--slugs', '--source', '--stale', '--stats', '--stdin', '--supersessions', '--thin', '--timeout', '--trusted-extraction', '--type', '--url', '--what', '--where', '--who', '--with-db', '--yes'],
|
||||
'book-mirror': ['--aliases', '--all', '--allow-empty', '--apply', '--asof', '--author', '--auto', '--background', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--brain-wide-max-cost-usd', '--budget-usd-per-day', '--by-mention', '--chapters-dir', '--content', '--context-file', '--date', '--days', '--dry-run', '--entities', '--explain', '--fast', '--federated', '--file', '--follow', '--force', '--from-pages', '--help', '--http', '--image', '--include-null-signature', '--json', '--kind', '--limit', '--max-turns', '--max-usd', '--mode', '--model', '--multimodal', '--no-confirm', '--no-embedding', '--no-extract', '--no-follow', '--offset', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--save', '--session', '--since', '--slug', '--slugs', '--source', '--stale', '--stats', '--supersessions', '--surface', '--thin', '--timeout', '--timeout-ms', '--title', '--trusted-extraction', '--url', '--with-db', '--yes'],
|
||||
'brainstorm': ['--aliases', '--all', '--brain', '--chunker-debug', '--code', '--compile', '--fast', '--fix', '--force', '--force-rechunk', '--force-resume', '--from-pages', '--full', '--help', '--http', '--include-null-signature', '--json', '--judge-model', '--lang', '--limit', '--list-runs', '--markdown', '--max-cost', '--max-far-set', '--max-ideas-per-judge-call', '--model', '--no-embed', '--no-embedding', '--no-extract', '--no-save', '--pattern', '--pending', '--reset', '--resolve', '--resume', '--retry-failed', '--retry-judge', '--save', '--source', '--stale', '--strict-budget', '--supersessions', '--surface', '--thin', '--timeout', '--yes'],
|
||||
'cache': ['--brain', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--no-embedding', '--source', '--surface', '--yes'],
|
||||
'calibration': ['--ab', '--aliases', '--all', '--allow-empty', '--apply', '--asof', '--auto', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-usd-per-day', '--by-mention', '--content', '--date', '--days', '--dry-run', '--entities', '--explain', '--fast', '--federated', '--file', '--force', '--from-pages', '--help', '--holder', '--http', '--image', '--include-null-signature', '--json', '--key-prefix', '--kind', '--lang', '--limit', '--markdown', '--max-usd', '--mode', '--multimodal', '--near-symbol', '--no-embedding', '--no-extract', '--no-federated', '--offset', '--path', '--pattern', '--pending', '--phase', '--progress-interval', '--progress-json', '--quiet', '--regenerate', '--repo', '--reset', '--resolve', '--restore-only', '--save', '--scrub-gstack', '--session', '--since', '--slug', '--slugs', '--source', '--stale', '--stats', '--supersessions', '--surface', '--symbol-kind', '--thin', '--trusted-extraction', '--undo-wave', '--url', '--with-calibration', '--with-db', '--yes'],
|
||||
'call': ['--aliases', '--all', '--all-sources', '--as-context', '--auto-fix', '--background', '--brain', '--by-mention', '--catch-up', '--concurrency', '--confirm-destructive', '--content', '--cost-estimate', '--count', '--days', '--depth', '--dim', '--dir', '--direction', '--enable-dcr', '--enable-dcr-insecure', '--explain', '--fast', '--federated', '--file', '--fix', '--follow', '--force', '--from', '--from-meetings', '--grant-types', '--grep', '--hard-deadline', '--help', '--http', '--image', '--include-expired', '--include-frontmatter', '--include-null-signature', '--infer-dates', '--install', '--interval', '--json', '--key', '--kind', '--lang', '--limit', '--link-source', '--link-type', '--migrate-only', '--missing-path', '--multimodal', '--ner', '--no-embed', '--no-expand', '--no-extract', '--no-federated', '--no-hard-deadline', '--no-retry-connect', '--no-save', '--older-than', '--page', '--param', '--params', '--password', '--path', '--pattern', '--pending', '--pglite', '--port', '--progress-interval', '--progress-json', '--public-url', '--queue', '--quiet', '--reenrich-after', '--refresh-cache', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--restore-only', '--save', '--scopes', '--session', '--sigma', '--since', '--slug', '--slug-prefix', '--source', '--source-id', '--stale', '--status', '--stdin', '--strategy', '--supabase', '--supersessions', '--surface', '--symbol-kind', '--synthesize', '--tag', '--thin', '--timeout', '--to', '--today', '--token', '--token-ttl', '--tools-json', '--type', '--url', '--version', '--watch', '--with-calibration', '--workers', '--yes'],
|
||||
'capture': ['--aliases', '--all', '--allow-empty', '--apply', '--asof', '--auto', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-usd-per-day', '--by-mention', '--content', '--date', '--days', '--depth', '--entities', '--fast', '--federated', '--file', '--force', '--from-pages', '--help', '--http', '--image', '--include-null-signature', '--json', '--kind', '--limit', '--max-usd', '--mcp-only', '--mode', '--multimodal', '--no-embedding', '--no-extract', '--no-federated', '--offset', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--repo', '--reset', '--resolve', '--restore-only', '--save', '--scopes', '--session', '--since', '--slug', '--slugs', '--source', '--stale', '--stats', '--stdin', '--supersessions', '--surface', '--thin', '--timeout', '--trusted-extraction', '--type', '--url', '--what', '--where', '--who', '--with-db', '--yes'],
|
||||
'check-backlinks': ['--background', '--brain', '--brain-wide-max-cost-usd', '--dir', '--dry-run', '--explain', '--follow', '--help', '--include-frontmatter', '--json', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--source', '--stale', '--timeout', '--type'],
|
||||
'check-resolvable': ['--brain', '--dry-run', '--fix', '--help', '--json', '--skills-dir', '--source', '--strict', '--verbose'],
|
||||
'check-update': ['--all', '--brain', '--check', '--ff-only', '--help', '--json', '--markdown', '--migrate-only', '--non-interactive', '--refresh-cache', '--source', '--swap-only', '--to', '--version', '--yes'],
|
||||
@@ -30,76 +30,77 @@ export const CLI_FLAG_REGISTRY: Record<string, readonly string[]> = {
|
||||
'code-callers': ['--aliases', '--all', '--all-sources', '--brain', '--chunker-debug', '--clone-dir', '--confirm-destructive', '--federated', '--force', '--help', '--include-null-signature', '--json', '--limit', '--no-extract', '--no-federated', '--no-json', '--path', '--pattern', '--pending', '--repo', '--reset', '--resolve', '--restore-only', '--source', '--stale', '--supersessions', '--thin', '--url', '--url-managed', '--yes'],
|
||||
'code-def': ['--aliases', '--all', '--brain', '--chunker-debug', '--help', '--include-null-signature', '--json', '--lang', '--limit', '--no-extract', '--no-json', '--pattern', '--pending', '--pretty', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--thin', '--yes'],
|
||||
'code-refs': ['--aliases', '--all', '--brain', '--chunker-debug', '--help', '--include-null-signature', '--json', '--lang', '--limit', '--no-extract', '--no-json', '--pattern', '--pending', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--thin', '--yes'],
|
||||
'config': ['--aliases', '--all', '--brain', '--column', '--coverage-override', '--embedding-dimensions', '--embedding-model', '--fast', '--follow', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--markdown', '--model', '--multimodal', '--no-embedding', '--no-extract', '--pattern', '--pending', '--pglite', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--thin', '--yes'],
|
||||
'config': ['--aliases', '--all', '--brain', '--column', '--coverage-override', '--embedding-dimensions', '--embedding-model', '--fast', '--follow', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--markdown', '--model', '--multimodal', '--no-embedding', '--no-extract', '--pattern', '--pending', '--pglite', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--surface', '--thin', '--yes'],
|
||||
'connect': ['--agent', '--bearer-token-env-var', '--bind', '--brain', '--client-id', '--client-secret', '--force', '--grant-types', '--help', '--http', '--install', '--json', '--name', '--oauth', '--public-url', '--register', '--scopes', '--show-token', '--source', '--timeout-ms', '--token', '--token-endpoint-auth-method', '--url', '--version', '--yes'],
|
||||
'conversation-parser': ['--aliases', '--all', '--brain', '--help', '--include-null-signature', '--json', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--thin'],
|
||||
'doctor': ['--ab', '--abi', '--aliases', '--all', '--allow-shell-jobs', '--auto', '--auto-fix', '--auto-update', '--background', '--batch', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--build-index', '--by-mention', '--by-type', '--check', '--column', '--compile', '--concurrency', '--confidence', '--content-audit', '--count', '--days', '--detach', '--dim', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--exclusive', '--explain', '--fast', '--fix', '--follow', '--force', '--force-break-lock', '--force-retry', '--force-schema', '--format', '--fresh', '--from-meetings', '--from-pages', '--full', '--grant-types', '--health-interval', '--help', '--history', '--http', '--include-flagged', '--include-frontmatter', '--include-null-signature', '--include-pseudo', '--index-audit', '--input', '--json', '--lang', '--limit', '--locks', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-crashes', '--max-jobs', '--max-rss', '--max-usd', '--mcp-only', '--migrate-only', '--model', '--multimodal', '--name-only', '--name-status', '--near-symbol', '--nice', '--no', '--no-embed', '--no-embedding', '--no-extract', '--no-mutate', '--oauth-client-secret', '--older-than', '--once', '--overwrite', '--parallel', '--params', '--path', '--pattern', '--pending', '--pglite', '--phase', '--pid-file', '--porcelain', '--priority', '--progress-interval', '--progress-json', '--query', '--queue', '--quiet', '--rebuild-rollup', '--refresh', '--refresh-unqualified', '--regenerate', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--restore-only', '--resume', '--review-lower', '--rollback', '--scope', '--scopes', '--since', '--skills-dir', '--skip-bare-tweet', '--skip-failed', '--skip-urls', '--skip-verify', '--slugs', '--source', '--source-id', '--stale', '--status', '--strategy', '--strict', '--supabase', '--supersessions', '--symbol-kind', '--target', '--target-score', '--thin', '--timeout', '--to', '--top-k', '--type', '--undo-wave', '--unsafe-bypass-dream-guard', '--untracked-files', '--url', '--use-captured-snapshot', '--verbose', '--version', '--window', '--with-calibration', '--workers', '--yes'],
|
||||
'dream': ['--against', '--aliases', '--all', '--anchor', '--asof', '--background', '--batch', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--budget-usd', '--by-type', '--by-type-floor', '--code', '--compile', '--concurrent', '--ctx-size', '--cycles', '--date', '--dimensions', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--embeddings', '--expansion', '--explain', '--fast', '--federated', '--fix', '--fixtures', '--follow', '--force', '--force-break-lock', '--force-rechunk', '--force-retry', '--from', '--from-db', '--from-pages', '--help', '--http', '--include-null-signature', '--input', '--install', '--json', '--judge-model', '--keyword-only', '--lang', '--limit', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-runtime', '--max-tokens', '--max-usd', '--mcp-only', '--min-recall', '--mode', '--model', '--models', '--multimodal', '--name', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--no-llm', '--no-mutate', '--no-trajectory', '--once', '--output', '--path', '--pattern', '--pending', '--pglite', '--phase', '--priority', '--progress-interval', '--progress-json', '--pull', '--quiet', '--receipt-dir', '--remediate', '--repo', '--reranking', '--reset', '--resolve', '--restore-only', '--resume-from', '--retrieval-only', '--rounds', '--rubric-version', '--save', '--seed', '--skip-replay', '--slot-a-model', '--slot-b-model', '--slot-c-model', '--slug', '--slug-prefix', '--source', '--source-id', '--stale', '--supabase', '--supersessions', '--symbol-kind', '--take', '--task', '--thin', '--threshold', '--timeout', '--to', '--top-k', '--undo', '--unsafe-bypass-dream-guard', '--version', '--window', '--yes'],
|
||||
'doctor': ['--ab', '--abi', '--aliases', '--all', '--allow-shell-jobs', '--auto', '--auto-fix', '--auto-update', '--background', '--batch', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--build-index', '--by-mention', '--by-type', '--check', '--column', '--compile', '--concurrency', '--confidence', '--content-audit', '--count', '--days', '--detach', '--dim', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--exclusive', '--explain', '--fast', '--fix', '--follow', '--force', '--force-break-lock', '--force-retry', '--force-schema', '--format', '--fresh', '--from-meetings', '--from-pages', '--full', '--grant-types', '--health-interval', '--help', '--history', '--http', '--include-flagged', '--include-frontmatter', '--include-null-signature', '--include-pseudo', '--index-audit', '--input', '--json', '--lang', '--limit', '--locks', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-crashes', '--max-jobs', '--max-rss', '--max-usd', '--mcp-only', '--migrate-only', '--model', '--multimodal', '--name-only', '--name-status', '--near-symbol', '--nice', '--no', '--no-embed', '--no-embedding', '--no-extract', '--no-mutate', '--oauth-client-secret', '--older-than', '--once', '--overwrite', '--parallel', '--params', '--path', '--pattern', '--pending', '--pglite', '--phase', '--pid-file', '--porcelain', '--priority', '--progress-interval', '--progress-json', '--query', '--queue', '--quiet', '--rebuild-rollup', '--refresh', '--refresh-unqualified', '--regenerate', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--restore-only', '--resume', '--review-lower', '--rollback', '--scope', '--scopes', '--since', '--skills-dir', '--skip-bare-tweet', '--skip-failed', '--skip-urls', '--skip-verify', '--slugs', '--source', '--source-id', '--stale', '--status', '--strategy', '--strict', '--supabase', '--supersessions', '--surface', '--symbol-kind', '--target', '--target-score', '--thin', '--timeout', '--to', '--top-k', '--type', '--undo-wave', '--unsafe-bypass-dream-guard', '--untracked-files', '--url', '--use-captured-snapshot', '--verbose', '--version', '--window', '--with-calibration', '--workers', '--yes'],
|
||||
'dream': ['--against', '--aliases', '--all', '--anchor', '--asof', '--background', '--batch', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--budget-usd', '--by-type', '--by-type-floor', '--code', '--compile', '--concurrent', '--ctx-size', '--cycles', '--date', '--dimensions', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--embeddings', '--expansion', '--explain', '--fast', '--federated', '--fix', '--fixtures', '--follow', '--force', '--force-break-lock', '--force-rechunk', '--force-retry', '--from', '--from-db', '--from-pages', '--help', '--http', '--include-null-signature', '--input', '--install', '--json', '--judge-model', '--keyword-only', '--lang', '--limit', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-runtime', '--max-tokens', '--max-usd', '--mcp-only', '--min-recall', '--mode', '--model', '--models', '--multimodal', '--name', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--no-llm', '--no-mutate', '--no-trajectory', '--once', '--output', '--path', '--pattern', '--pending', '--pglite', '--phase', '--priority', '--progress-interval', '--progress-json', '--pull', '--quiet', '--receipt-dir', '--remediate', '--repo', '--reranking', '--reset', '--resolve', '--restore-only', '--resume-from', '--retrieval-only', '--rounds', '--rubric-version', '--save', '--seed', '--skip-replay', '--slot-a-model', '--slot-b-model', '--slot-c-model', '--slug', '--slug-prefix', '--source', '--source-id', '--stale', '--supabase', '--supersessions', '--surface', '--symbol-kind', '--take', '--task', '--thin', '--threshold', '--timeout', '--to', '--top-k', '--undo', '--unsafe-bypass-dream-guard', '--version', '--window', '--yes'],
|
||||
'edges-backfill': ['--aliases', '--all', '--all-sources', '--brain', '--concurrency', '--federated', '--help', '--include-null-signature', '--json', '--max-age', '--max-chunks', '--max-cost-usd', '--no-extract', '--no-federated', '--older-than', '--path', '--pattern', '--pending', '--repo', '--reset', '--resolve', '--restore-only', '--source', '--stale', '--supersessions', '--thin', '--timeout', '--workers'],
|
||||
'embed': ['--aliases', '--all', '--background', '--batch-size', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--catch-up', '--dry-run', '--embedding-dimensions', '--embedding-model', '--explain', '--fast', '--fix', '--follow', '--force', '--force-break-lock', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--lang', '--markdown', '--max-age', '--max-cost-usd', '--model', '--multimodal', '--name', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--pace', '--pace-max-concurrency', '--parallel', '--path', '--pattern', '--pending', '--pglite', '--prefix', '--priority', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--restore-only', '--serial', '--slugs', '--source', '--stale', '--supabase', '--supersessions', '--symbol-kind', '--thin', '--timeout', '--version'],
|
||||
'enrich': ['--aliases', '--all', '--all-sources', '--allow-empty', '--apply', '--asof', '--auto', '--background', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--budget-usd-per-day', '--by-mention', '--clone-dir', '--code', '--concurrency', '--confirm-destructive', '--content', '--date', '--days', '--dry-run', '--embedding-dimensions', '--embedding-model', '--entities', '--explain', '--fast', '--federated', '--file', '--fix', '--follow', '--force', '--force-break-lock', '--from-pages', '--help', '--http', '--image', '--include-null-signature', '--json', '--judge-model', '--kind', '--lang', '--limit', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-runtime', '--max-usd', '--min-context', '--mode', '--model', '--multimodal', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--offset', '--older-than', '--order', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--reenrich-after', '--remediate', '--reset', '--resolve', '--restore-only', '--resume', '--save', '--session', '--since', '--slug', '--slugs', '--source', '--source-id', '--stale', '--stats', '--supersessions', '--symbol-kind', '--thin', '--thin-threshold', '--timeout', '--trusted-extraction', '--types', '--url', '--url-managed', '--version', '--with-db', '--workers', '--yes'],
|
||||
'eval': ['--ab-relational', '--against', '--aliases', '--all', '--background', '--baseline', '--batch', '--brain', '--brain-wide-max-cost-usd', '--budget-usd', '--budget-usd-answer', '--budget-usd-retrieval', '--compare', '--compare-limit', '--concurrent', '--config-a', '--config-b', '--corpus', '--cycles', '--days', '--dedup-cosine', '--dedup-max-per-page', '--dedup-type-ratio', '--dimensions', '--distance-min', '--embedding-dimensions', '--embedding-model', '--expand', '--explain', '--fast', '--fixtures', '--follow', '--force', '--from-capture', '--from-db', '--from-pages', '--grounding-min', '--help', '--http', '--include-null-signature', '--input', '--json', '--judge', '--k', '--limit', '--max-pair-chars', '--max-tokens', '--max-usd', '--md', '--metric', '--min-recall', '--mode', '--model', '--models', '--modes', '--multimodal', '--name', '--no', '--no-cache', '--no-embed', '--no-embedding', '--no-expand', '--no-extract', '--no-llm', '--older-than', '--output', '--output-dir', '--parallel', '--pattern', '--pending', '--progress-interval', '--progress-json', '--qrels', '--queries-file', '--query', '--questions', '--quiet', '--receipt-dir', '--refresh-cache', '--remediate', '--reset', '--resolve', '--rrf-k', '--rubric-version', '--runs', '--sampling', '--save', '--seed', '--severity', '--short', '--show-toplevel', '--since', '--skip-replay', '--slot-a-model', '--slot-b-model', '--slot-c-model', '--slug', '--slug-prefix', '--source', '--stale', '--strategy', '--strict', '--suite', '--suites', '--supersessions', '--task', '--thin', '--threshold', '--threshold-expected-top1', '--threshold-first-relevant-hit', '--threshold-jaccard', '--threshold-latency-multiplier', '--threshold-recall-at-k', '--threshold-top1', '--timeout', '--tool', '--top-k', '--top-regressions', '--until', '--usefulness-min', '--verbose', '--version', '--with-code-intel', '--yes'],
|
||||
'embed': ['--aliases', '--all', '--background', '--batch-size', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--catch-up', '--dry-run', '--embedding-dimensions', '--embedding-model', '--explain', '--fast', '--fix', '--follow', '--force', '--force-break-lock', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--lang', '--markdown', '--max-age', '--max-cost-usd', '--model', '--multimodal', '--name', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--pace', '--pace-max-concurrency', '--parallel', '--path', '--pattern', '--pending', '--pglite', '--prefix', '--priority', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--restore-only', '--serial', '--slugs', '--source', '--stale', '--supabase', '--supersessions', '--surface', '--symbol-kind', '--thin', '--timeout', '--version'],
|
||||
'enrich': ['--aliases', '--all', '--all-sources', '--allow-empty', '--apply', '--asof', '--auto', '--background', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--budget-usd-per-day', '--by-mention', '--clone-dir', '--code', '--concurrency', '--confirm-destructive', '--content', '--date', '--days', '--dry-run', '--embedding-dimensions', '--embedding-model', '--entities', '--explain', '--fast', '--federated', '--file', '--fix', '--follow', '--force', '--force-break-lock', '--from-pages', '--help', '--http', '--image', '--include-null-signature', '--json', '--judge-model', '--kind', '--lang', '--limit', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-runtime', '--max-usd', '--min-context', '--mode', '--model', '--multimodal', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--offset', '--older-than', '--order', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--reenrich-after', '--remediate', '--reset', '--resolve', '--restore-only', '--resume', '--save', '--session', '--since', '--slug', '--slugs', '--source', '--source-id', '--stale', '--stats', '--supersessions', '--surface', '--symbol-kind', '--thin', '--thin-threshold', '--timeout', '--trusted-extraction', '--types', '--url', '--url-managed', '--version', '--with-db', '--workers', '--yes'],
|
||||
'eval': ['--ab-relational', '--against', '--aliases', '--all', '--background', '--baseline', '--batch', '--brain', '--brain-wide-max-cost-usd', '--budget-usd', '--budget-usd-answer', '--budget-usd-retrieval', '--compare', '--compare-limit', '--concurrent', '--config-a', '--config-b', '--corpus', '--cycles', '--days', '--dedup-cosine', '--dedup-max-per-page', '--dedup-type-ratio', '--dimensions', '--distance-min', '--embedding-dimensions', '--embedding-model', '--expand', '--explain', '--fast', '--fixtures', '--follow', '--force', '--from-capture', '--from-db', '--from-pages', '--grounding-min', '--help', '--http', '--include-null-signature', '--input', '--json', '--judge', '--k', '--limit', '--max-pair-chars', '--max-tokens', '--max-usd', '--md', '--metric', '--min-recall', '--mode', '--model', '--models', '--modes', '--multimodal', '--name', '--no', '--no-cache', '--no-embed', '--no-embedding', '--no-expand', '--no-extract', '--no-llm', '--older-than', '--output', '--output-dir', '--parallel', '--pattern', '--pending', '--progress-interval', '--progress-json', '--qrels', '--queries-file', '--query', '--questions', '--quiet', '--receipt-dir', '--refresh-cache', '--remediate', '--reset', '--resolve', '--rrf-k', '--rubric-version', '--runs', '--sampling', '--save', '--seed', '--severity', '--short', '--show-toplevel', '--since', '--skip-replay', '--slot-a-model', '--slot-b-model', '--slot-c-model', '--slug', '--slug-prefix', '--source', '--stale', '--strategy', '--strict', '--suite', '--suites', '--supersessions', '--surface', '--task', '--thin', '--threshold', '--threshold-expected-top1', '--threshold-first-relevant-hit', '--threshold-jaccard', '--threshold-latency-multiplier', '--threshold-recall-at-k', '--threshold-top1', '--timeout', '--tool', '--top-k', '--top-regressions', '--until', '--usefulness-min', '--verbose', '--version', '--with-code-intel', '--yes'],
|
||||
'export': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--dir', '--explain', '--federated', '--fix', '--follow', '--help', '--include-null-signature', '--json', '--lang', '--markdown', '--multimodal', '--near-symbol', '--no-extract', '--no-federated', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--repo', '--reset', '--resolve', '--restore-only', '--slug-prefix', '--source', '--stale', '--supersessions', '--symbol-kind', '--thin', '--timeout', '--type'],
|
||||
'extract': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--by-mention', '--catch-up', '--code', '--concurrency', '--dir', '--dry-run', '--explain', '--federated', '--follow', '--from-meetings', '--help', '--include-frontmatter', '--include-null-signature', '--infer-dates', '--json', '--kind', '--lang', '--markdown', '--max-age', '--max-cost-usd', '--multimodal', '--name-status', '--near-symbol', '--ner', '--no-extract', '--no-federated', '--older-than', '--pack', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--repo', '--reset', '--resolve', '--restore-only', '--run-id', '--since', '--slug', '--source', '--source-id', '--stale', '--strategy', '--supersessions', '--symbol-kind', '--thin', '--timeout', '--type', '--verbose', '--workers', '--yes'],
|
||||
'extract-conversation-facts': ['--aliases', '--all', '--all-sources', '--background', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--by-mention', '--clone-dir', '--code', '--concurrency', '--confirm-destructive', '--dry-run', '--embedding-dimensions', '--embedding-model', '--explain', '--fix', '--follow', '--force', '--force-break-lock', '--help', '--include-null-signature', '--json', '--judge-model', '--lang', '--limit', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-runtime', '--model', '--multimodal', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--older-than', '--override-disabled', '--path', '--pattern', '--pending', '--pglite', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--restore-only', '--segment-limit', '--session', '--since', '--sleep', '--slug', '--source', '--source-id', '--stale', '--supabase', '--supersessions', '--symbol-kind', '--thin', '--timeout', '--types', '--url', '--url-managed', '--version', '--workers', '--yes'],
|
||||
'features': ['--aliases', '--all', '--auto-fix', '--background', '--batch-size', '--brain', '--by-mention', '--catch-up', '--concurrency', '--dir', '--explain', '--from-meetings', '--help', '--include-frontmatter', '--include-null-signature', '--infer-dates', '--json', '--kind', '--ner', '--no-extract', '--pace', '--pace-max-concurrency', '--pack', '--path', '--pattern', '--pending', '--priority', '--progress-json', '--quiet', '--repo', '--reset', '--resolve', '--run-id', '--since', '--slugs', '--source', '--source-id', '--stale', '--supersessions', '--thin', '--type', '--verbose', '--workers'],
|
||||
'files': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--dry-run', '--explain', '--fast', '--follow', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--no-embedding', '--no-extract', '--no-pointer', '--page', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--retry-failed', '--source', '--stale', '--supersessions', '--thin', '--timeout', '--type', '--yes'],
|
||||
'forget': ['--aliases', '--all', '--as-context', '--brain', '--fast', '--federated', '--force', '--from-pages', '--grep', '--help', '--http', '--include-expired', '--include-null-signature', '--json', '--limit', '--mcp-only', '--no-embedding', '--no-extract', '--no-federated', '--path', '--pattern', '--pending', '--reason', '--repo', '--reset', '--resolve', '--restore-only', '--rollup', '--session', '--session-id', '--since', '--since-last-run', '--source', '--stale', '--supersessions', '--thin', '--timeout', '--today', '--watch'],
|
||||
'founder': ['--aliases', '--all', '--brain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--mcp-only', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--since', '--source', '--stale', '--supersessions', '--thin', '--timeout', '--until'],
|
||||
'files': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--dry-run', '--explain', '--fast', '--follow', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--no-embedding', '--no-extract', '--no-pointer', '--page', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--retry-failed', '--source', '--stale', '--supersessions', '--surface', '--thin', '--timeout', '--type', '--yes'],
|
||||
'forget': ['--aliases', '--all', '--allow-empty', '--apply', '--as-context', '--asof', '--auto', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-tokens', '--budget-usd-per-day', '--by-mention', '--content', '--date', '--days', '--entities', '--fast', '--federated', '--file', '--force', '--from-pages', '--grep', '--help', '--http', '--image', '--include-expired', '--include-null-signature', '--json', '--kind', '--limit', '--max-usd', '--mcp-only', '--mode', '--multimodal', '--no-embedding', '--no-extract', '--no-federated', '--offset', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--query', '--quiet', '--reason', '--repo', '--reset', '--resolve', '--restore-only', '--rollup', '--save', '--session', '--session-id', '--since', '--since-last-run', '--slug', '--slugs', '--source', '--stale', '--stats', '--supersessions', '--surface', '--thin', '--timeout', '--today', '--trusted-extraction', '--url', '--watch', '--with-db', '--yes'],
|
||||
'founder': ['--aliases', '--all', '--brain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--mcp-only', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--since', '--source', '--stale', '--supersessions', '--surface', '--thin', '--timeout', '--until'],
|
||||
'friction': ['--agent', '--brain', '--help', '--hint', '--json', '--kind', '--message', '--no-redact', '--phase', '--redact', '--run-id', '--severity', '--source', '--transcript-path', '--transcripts'],
|
||||
'frontmatter': ['--aliases', '--all', '--allow-catch-all', '--brain', '--cached', '--diff-filter', '--dry-run', '--exclude-standard', '--fast', '--fix', '--force', '--from-pages', '--get', '--help', '--http', '--include-catch-all', '--include-null-signature', '--json', '--name-only', '--name-status', '--no-embedding', '--no-extract', '--no-verify', '--others', '--pattern', '--pending', '--reset', '--resolve', '--source', '--stale', '--strategy', '--supersessions', '--thin', '--timeout', '--uninstall', '--write-back'],
|
||||
'graph-query': ['--aliases', '--all', '--brain', '--depth', '--direction', '--explain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-foreign', '--include-null-signature', '--json', '--lang', '--markdown', '--mcp-only', '--multimodal', '--near-symbol', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--restore-only', '--source', '--stale', '--supersessions', '--symbol-kind', '--thin', '--timeout', '--type'],
|
||||
'import': ['--aliases', '--all', '--asof', '--background', '--brain', '--brain-wide-max-cost-usd', '--by-mention', '--cached', '--code', '--compile', '--concurrency', '--embedding-dimensions', '--embedding-model', '--exclude', '--exclude-standard', '--explain', '--fast', '--federated', '--fix', '--follow', '--force', '--force-rechunk', '--fresh', '--from-pages', '--full', '--help', '--http', '--include-gitignored', '--include-null-signature', '--json', '--lang', '--markdown', '--max-age', '--multimodal', '--name-status', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--older-than', '--others', '--path', '--pattern', '--pending', '--pglite', '--priority', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--repo', '--reset', '--resolve', '--respect-gitignore', '--restore-only', '--since', '--skip-failed', '--source', '--source-id', '--stale', '--strategy', '--supabase', '--supersessions', '--thin', '--timeout', '--url', '--workers'],
|
||||
'init': ['--all', '--brain', '--chat-model', '--check', '--ctx-size', '--embedding-dimensions', '--embedding-model', '--expansion-model', '--fast', '--force', '--from-pages', '--grant-types', '--help', '--http', '--issuer-url', '--json', '--judge-model', '--key', '--mcp-only', '--mcp-url', '--migrate-only', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--non-interactive', '--oauth-client-id', '--oauth-client-secret', '--path', '--pglite', '--schema-pack', '--scopes', '--skip-embed-check', '--source', '--stale', '--supabase', '--target', '--to', '--touchpoint', '--url', '--version'],
|
||||
'integrations': ['--auto', '--brain', '--dry-run', '--embeddings', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--no-embedding', '--overwrite', '--refresh', '--reranking', '--source', '--target'],
|
||||
'integrity': ['--aliases', '--all', '--auto', '--backend', '--background', '--brain', '--brain-wide-max-cost-usd', '--check', '--confidence', '--cost', '--dry-run', '--explain', '--fast', '--follow', '--force', '--fresh', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--limit', '--no-embedding', '--no-extract', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--review-lower', '--skip-bare-tweet', '--skip-urls', '--source', '--stale', '--supabase', '--supersessions', '--thin', '--timeout', '--type', '--url'],
|
||||
'jobs': ['--abbrev-ref', '--aliases', '--all', '--allow-empty', '--allow-protected', '--allow-shell-jobs', '--apply', '--asof', '--auto', '--auto-with-prompt', '--background', '--backoff-delay', '--backoff-jitter', '--backoff-type', '--batch-size', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--break-lock', '--budget-usd', '--budget-usd-per-day', '--by-mention', '--cached', '--catch-up', '--check', '--cli-path', '--cluster', '--cluster-errors', '--code', '--concurrency', '--confidence', '--confirm-destructive', '--content', '--date', '--days', '--delay', '--detach', '--diff-filter', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--empty', '--entities', '--exclude', '--exclude-standard', '--explain', '--fast', '--federated', '--federated-read', '--file', '--fix', '--follow', '--force', '--force-break-lock', '--force-retry', '--format', '--fresh', '--from-meetings', '--from-pages', '--full', '--hard-deadline', '--health-interval', '--held-out', '--help', '--http', '--idempotency-key', '--image', '--include-frontmatter', '--include-gitignored', '--include-null-signature', '--infer-dates', '--inject-bootstrap', '--inline', '--input', '--install', '--interval', '--is-ancestor', '--json', '--kind', '--lang', '--limit', '--lock', '--markdown', '--max-age', '--max-attempts', '--max-cost-usd', '--max-crashes', '--max-rss', '--max-runtime-min', '--max-sources', '--max-stalled', '--max-usd', '--max-waiting', '--mcp-only', '--min-context', '--missing-path', '--mode', '--model', '--multimodal', '--name-only', '--name-status', '--near-symbol', '--ner', '--nice', '--no', '--no-auto-embed', '--no-embed', '--no-embedding', '--no-extract', '--no-federate', '--no-gpg-sign', '--no-hard-deadline', '--no-inject', '--no-mutate', '--no-pull', '--no-renames', '--no-schema-pack', '--no-verify', '--no-worker', '--now', '--offset', '--older-than', '--once', '--order', '--others', '--override-disabled', '--pace', '--pace-max-concurrency', '--pack', '--parallel', '--params', '--path', '--pattern', '--pending', '--phase', '--pid-file', '--priority', '--progress-interval', '--progress-json', '--queue', '--quiet', '--redact-secrets', '--reenrich-after', '--refresh-ms', '--remediate', '--repo', '--reset', '--resolve', '--respect-gitignore', '--restore-only', '--resume', '--retry-failed', '--review-lower', '--run-id', '--save', '--segment-limit', '--serial', '--session', '--short', '--show-toplevel', '--sigkill-rescue', '--since', '--skip-bare-tweet', '--skip-failed', '--skip-urls', '--sleep', '--slug', '--slugs', '--source', '--source-id', '--src-subpath', '--stale', '--stats', '--status', '--strategy', '--supersessions', '--swap-only', '--symbol-kind', '--target', '--thin', '--thin-threshold', '--timeout', '--timeout-ms', '--trusted-extraction', '--type', '--types', '--uninstall', '--unsafe-bypass-dream-guard', '--url', '--user', '--verbose', '--version', '--watch', '--wedge-rescue', '--with-db', '--workers', '--yes'],
|
||||
'lint': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--dry-run', '--exclude', '--explain', '--fast', '--fix', '--follow', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--no-embedding', '--no-extract', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--thin', '--timeout'],
|
||||
'frontmatter': ['--aliases', '--all', '--allow-catch-all', '--brain', '--cached', '--diff-filter', '--dry-run', '--exclude-standard', '--fast', '--fix', '--force', '--from-pages', '--get', '--help', '--http', '--include-catch-all', '--include-null-signature', '--json', '--name-only', '--name-status', '--no-embedding', '--no-extract', '--no-verify', '--others', '--pattern', '--pending', '--reset', '--resolve', '--source', '--stale', '--strategy', '--supersessions', '--surface', '--thin', '--timeout', '--uninstall', '--write-back'],
|
||||
'graph-query': ['--aliases', '--all', '--brain', '--depth', '--direction', '--explain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-foreign', '--include-null-signature', '--json', '--lang', '--markdown', '--mcp-only', '--multimodal', '--near-symbol', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--restore-only', '--source', '--stale', '--supersessions', '--surface', '--symbol-kind', '--thin', '--timeout', '--type'],
|
||||
'import': ['--aliases', '--all', '--asof', '--background', '--brain', '--brain-wide-max-cost-usd', '--by-mention', '--cached', '--code', '--compile', '--concurrency', '--embedding-dimensions', '--embedding-model', '--exclude', '--exclude-standard', '--explain', '--fast', '--federated', '--fix', '--follow', '--force', '--force-rechunk', '--fresh', '--from-pages', '--full', '--help', '--http', '--include-gitignored', '--include-null-signature', '--json', '--lang', '--markdown', '--max-age', '--multimodal', '--name-status', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--older-than', '--others', '--path', '--pattern', '--pending', '--pglite', '--priority', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--repo', '--reset', '--resolve', '--respect-gitignore', '--restore-only', '--since', '--skip-failed', '--source', '--source-id', '--stale', '--strategy', '--supabase', '--supersessions', '--surface', '--thin', '--timeout', '--url', '--workers'],
|
||||
'init': ['--all', '--brain', '--chat-model', '--check', '--ctx-size', '--embedding-dimensions', '--embedding-model', '--entity', '--expansion-model', '--fast', '--force', '--from-pages', '--grant-types', '--help', '--http', '--issuer-url', '--json', '--judge-model', '--key', '--mcp-only', '--mcp-url', '--migrate-only', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--non-interactive', '--oauth-client-id', '--oauth-client-secret', '--path', '--pglite', '--provenance', '--schema-pack', '--scopes', '--skip-embed-check', '--source', '--stale', '--supabase', '--surface', '--target', '--to', '--touchpoint', '--url', '--version'],
|
||||
'integrations': ['--auto', '--brain', '--dry-run', '--embeddings', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--no-embedding', '--overwrite', '--refresh', '--reranking', '--source', '--surface', '--target'],
|
||||
'integrity': ['--aliases', '--all', '--auto', '--backend', '--background', '--brain', '--brain-wide-max-cost-usd', '--check', '--confidence', '--cost', '--dry-run', '--explain', '--fast', '--follow', '--force', '--fresh', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--limit', '--no-embedding', '--no-extract', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--review-lower', '--skip-bare-tweet', '--skip-urls', '--source', '--stale', '--supabase', '--supersessions', '--surface', '--thin', '--timeout', '--type', '--url'],
|
||||
'jobs': ['--abbrev-ref', '--aliases', '--all', '--allow-empty', '--allow-protected', '--allow-shell-jobs', '--apply', '--asof', '--auto', '--auto-with-prompt', '--background', '--backoff-delay', '--backoff-jitter', '--backoff-type', '--batch-size', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--break-lock', '--budget-usd', '--budget-usd-per-day', '--by-mention', '--cached', '--catch-up', '--check', '--cli-path', '--cluster', '--cluster-errors', '--code', '--concurrency', '--confidence', '--confirm-destructive', '--content', '--date', '--days', '--delay', '--detach', '--diff-filter', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--empty', '--entities', '--exclude', '--exclude-standard', '--explain', '--fast', '--federated', '--federated-read', '--file', '--fix', '--follow', '--force', '--force-break-lock', '--force-retry', '--format', '--fresh', '--from-meetings', '--from-pages', '--full', '--hard-deadline', '--health-interval', '--held-out', '--help', '--http', '--idempotency-key', '--image', '--include-frontmatter', '--include-gitignored', '--include-null-signature', '--infer-dates', '--inject-bootstrap', '--inline', '--input', '--install', '--interval', '--is-ancestor', '--json', '--kind', '--lang', '--limit', '--lock', '--markdown', '--max-age', '--max-attempts', '--max-cost-usd', '--max-crashes', '--max-rss', '--max-runtime-min', '--max-sources', '--max-stalled', '--max-usd', '--max-waiting', '--mcp-only', '--min-context', '--missing-path', '--mode', '--model', '--multimodal', '--name-only', '--name-status', '--near-symbol', '--ner', '--nice', '--no', '--no-auto-embed', '--no-embed', '--no-embedding', '--no-extract', '--no-federate', '--no-gpg-sign', '--no-hard-deadline', '--no-inject', '--no-mutate', '--no-pull', '--no-renames', '--no-schema-pack', '--no-verify', '--no-worker', '--now', '--offset', '--older-than', '--once', '--order', '--others', '--override-disabled', '--pace', '--pace-max-concurrency', '--pack', '--parallel', '--params', '--path', '--pattern', '--pending', '--phase', '--pid-file', '--priority', '--progress-interval', '--progress-json', '--queue', '--quiet', '--redact-secrets', '--reenrich-after', '--refresh-ms', '--remediate', '--repo', '--reset', '--resolve', '--respect-gitignore', '--restore-only', '--resume', '--retry-failed', '--review-lower', '--run-id', '--save', '--segment-limit', '--serial', '--session', '--short', '--show-toplevel', '--sigkill-rescue', '--since', '--skip-bare-tweet', '--skip-failed', '--skip-urls', '--sleep', '--slug', '--slugs', '--source', '--source-id', '--src-subpath', '--stale', '--stats', '--status', '--strategy', '--supersessions', '--surface', '--swap-only', '--symbol-kind', '--target', '--thin', '--thin-threshold', '--timeout', '--timeout-ms', '--trusted-extraction', '--type', '--types', '--uninstall', '--unsafe-bypass-dream-guard', '--url', '--user', '--verbose', '--version', '--watch', '--wedge-rescue', '--with-db', '--workers', '--yes'],
|
||||
'lint': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--dry-run', '--exclude', '--explain', '--fast', '--fix', '--follow', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--no-embedding', '--no-extract', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--surface', '--thin', '--timeout'],
|
||||
'lsd': ['--brain', '--force-resume', '--help', '--json', '--judge-model', '--limit', '--list-runs', '--max-cost', '--max-far-set', '--max-ideas-per-judge-call', '--no-save', '--resume', '--retry-judge', '--save', '--source', '--strict-budget', '--yes'],
|
||||
'maintain': ['--aliases', '--all', '--background', '--brain', '--break-lock', '--by-mention', '--catch-up', '--column', '--concurrency', '--content-audit', '--count', '--detach', '--dim', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--explain', '--fast', '--fix', '--force', '--force-retry', '--force-schema', '--from-meetings', '--full', '--help', '--include-flagged', '--include-frontmatter', '--include-null-signature', '--index-audit', '--infer-dates', '--input', '--json', '--kind', '--lang', '--locks', '--markdown', '--max-cost', '--max-cost-usd', '--max-jobs', '--max-rss', '--max-usd', '--migrate-only', '--multimodal', '--near-symbol', '--ner', '--nice', '--no-extract', '--no-mutate', '--older-than', '--once', '--pack', '--parallel', '--params', '--path', '--pattern', '--pending', '--pglite', '--phase', '--pid-file', '--progress-json', '--query', '--queue', '--quiet', '--rebuild-rollup', '--regenerate', '--remediate', '--remediation-plan', '--reset', '--resolve', '--restore-only', '--resume', '--run-id', '--safe', '--scope', '--since', '--skills-dir', '--skip-failed', '--slugs', '--source', '--source-id', '--stale', '--status', '--supabase', '--supersessions', '--symbol-kind', '--target', '--target-score', '--thin', '--to', '--top-k', '--type', '--unsafe-bypass-dream-guard', '--url', '--verbose', '--window', '--workers', '--yes'],
|
||||
'migrate': ['--ab', '--aliases', '--all', '--auto-update', '--background', '--batch-size', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--build-index', '--by-mention', '--catch-up', '--compile', '--days', '--dim', '--dry-run', '--embedding-dimensions', '--embedding-model', '--embeddings', '--exclusive', '--explain', '--fast', '--follow', '--force', '--force-retry', '--force-schema', '--from-meetings', '--from-pages', '--help', '--history', '--http', '--ignore-env-override', '--ignore-missing-key', '--include-null-signature', '--json', '--lang', '--locks', '--markdown', '--max-age', '--model', '--multimodal', '--name', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--non-interactive', '--pace', '--pace-max-concurrency', '--parallel', '--path', '--pattern', '--pending', '--phase', '--prefix', '--priority', '--progress-interval', '--progress-json', '--quiet', '--refresh-unqualified', '--remediate', '--reranking', '--reset', '--resolve', '--restore-only', '--resume', '--rollback', '--skip-verify', '--slugs', '--source', '--stale', '--supersessions', '--symbol-kind', '--thin', '--timeout', '--to', '--undo', '--undo-wave', '--url', '--use-captured-snapshot', '--version', '--with-calibration', '--yes'],
|
||||
'models': ['--aliases', '--all', '--brain', '--embedding-dimensions', '--embedding-model', '--embeddings', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--judge-model', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reranking', '--reset', '--resolve', '--skip', '--source', '--stale', '--supersessions', '--thin', '--undo', '--version'],
|
||||
'migrate': ['--ab', '--aliases', '--all', '--auto-update', '--background', '--batch-size', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--build-index', '--by-mention', '--catch-up', '--compile', '--days', '--dim', '--dry-run', '--embedding-dimensions', '--embedding-model', '--embeddings', '--exclusive', '--explain', '--fast', '--follow', '--force', '--force-retry', '--force-schema', '--from-meetings', '--from-pages', '--help', '--history', '--http', '--ignore-env-override', '--ignore-missing-key', '--include-null-signature', '--json', '--lang', '--locks', '--markdown', '--max-age', '--model', '--multimodal', '--name', '--near-symbol', '--no', '--no-embed', '--no-embedding', '--no-extract', '--non-interactive', '--pace', '--pace-max-concurrency', '--parallel', '--path', '--pattern', '--pending', '--phase', '--prefix', '--priority', '--progress-interval', '--progress-json', '--quiet', '--refresh-unqualified', '--remediate', '--reranking', '--reset', '--resolve', '--restore-only', '--resume', '--rollback', '--skip-verify', '--slugs', '--source', '--stale', '--supersessions', '--surface', '--symbol-kind', '--thin', '--timeout', '--to', '--undo', '--undo-wave', '--url', '--use-captured-snapshot', '--version', '--with-calibration', '--yes'],
|
||||
'models': ['--aliases', '--all', '--brain', '--embedding-dimensions', '--embedding-model', '--embeddings', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--judge-model', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reranking', '--reset', '--resolve', '--skip', '--source', '--stale', '--supersessions', '--surface', '--thin', '--undo', '--version'],
|
||||
'mounts': ['--alias', '--brain', '--cache', '--database-path', '--database-url', '--db-path', '--db-url', '--engine', '--explain', '--help', '--id', '--json', '--lang', '--lock', '--markdown', '--mcp-url', '--multimodal', '--near-symbol', '--path', '--restore-only', '--skills-dir', '--source', '--stale', '--symbol-kind', '--thin', '--verbose'],
|
||||
'notability-eval': ['--aliases', '--all', '--brain', '--embedding-dimensions', '--embedding-model', '--help', '--in', '--include-null-signature', '--json', '--model', '--multimodal', '--no', '--no-embed', '--no-extract', '--out', '--pattern', '--pending', '--repo', '--reset', '--resolve', '--skip-llm', '--source', '--stale', '--supersessions', '--target-high', '--target-low', '--target-medium', '--thin', '--version'],
|
||||
'onboard': ['--aliases', '--all', '--allow-empty', '--allow-protected', '--apply', '--asof', '--auto', '--auto-with-prompt', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-usd-per-day', '--by-mention', '--check', '--content', '--date', '--days', '--entities', '--explain', '--federated', '--file', '--from-pages', '--help', '--history', '--http', '--image', '--include-null-signature', '--json', '--kind', '--limit', '--max-usd', '--mode', '--multimodal', '--no-extract', '--offset', '--params', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--remediation-plan', '--reset', '--resolve', '--resume', '--save', '--session', '--since', '--slug', '--slugs', '--source', '--stale', '--stats', '--supersessions', '--target-score', '--thin', '--trusted-extraction', '--url', '--with-db', '--yes'],
|
||||
'onboard': ['--aliases', '--all', '--allow-empty', '--allow-protected', '--apply', '--asof', '--auto', '--auto-with-prompt', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-usd-per-day', '--by-mention', '--check', '--content', '--date', '--days', '--entities', '--explain', '--federated', '--file', '--from-pages', '--help', '--history', '--http', '--image', '--include-null-signature', '--json', '--kind', '--limit', '--max-usd', '--mode', '--multimodal', '--no-extract', '--offset', '--params', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--remediation-plan', '--reset', '--resolve', '--resume', '--save', '--session', '--since', '--slug', '--slugs', '--source', '--stale', '--stats', '--supersessions', '--surface', '--target-score', '--thin', '--trusted-extraction', '--url', '--with-db', '--yes'],
|
||||
'orphans': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--count', '--explain', '--follow', '--help', '--include-null-signature', '--include-pseudo', '--json', '--no-extract', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--thin', '--timeout'],
|
||||
'pages': ['--aliases', '--all', '--brain', '--dry-run', '--help', '--include-null-signature', '--json', '--no-extract', '--older-than', '--pattern', '--pending', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--thin'],
|
||||
'pglite-repair': ['--brain', '--break-lock', '--dry-rnu', '--dry-run', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--no-embedding', '--path', '--quiet', '--source', '--yes'],
|
||||
'post-upgrade': ['--all', '--apply-clean-hunks', '--brain', '--check', '--code', '--compile', '--concurrency', '--embedding-dimensions', '--embedding-model', '--fast', '--ff-only', '--force', '--force-all', '--force-orchestrator', '--force-retry', '--force-schema', '--format', '--from-pages', '--help', '--host-dir', '--http', '--inject-bootstrap', '--inline', '--install', '--interval', '--json', '--limit', '--list', '--markdown', '--max-rss', '--migrate-only', '--migration', '--mode', '--model', '--multimodal', '--name-only', '--no', '--no-autopilot-install', '--no-embed', '--no-embedding', '--no-inject', '--no-worker', '--non-interactive', '--now', '--quiet', '--repo', '--since', '--skills-dir', '--skip-verify', '--source', '--stale', '--status', '--swap-only', '--target', '--to', '--uninstall', '--user', '--verbose', '--verify', '--version', '--workers', '--yes'],
|
||||
'providers': ['--brain', '--ctx-size', '--embedding-dimensions', '--embedding-model', '--embeddings', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--reranking', '--source', '--touchpoint', '--version'],
|
||||
'pglite-repair': ['--brain', '--break-lock', '--dry-rnu', '--dry-run', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--no-embedding', '--path', '--quiet', '--source', '--surface', '--yes'],
|
||||
'post-upgrade': ['--all', '--apply-clean-hunks', '--brain', '--check', '--code', '--compile', '--concurrency', '--embedding-dimensions', '--embedding-model', '--fast', '--ff-only', '--force', '--force-all', '--force-orchestrator', '--force-retry', '--force-schema', '--format', '--from-pages', '--help', '--host-dir', '--http', '--inject-bootstrap', '--inline', '--install', '--interval', '--json', '--limit', '--list', '--markdown', '--max-rss', '--migrate-only', '--migration', '--mode', '--model', '--multimodal', '--name-only', '--no', '--no-autopilot-install', '--no-embed', '--no-embedding', '--no-inject', '--no-worker', '--non-interactive', '--now', '--quiet', '--repo', '--since', '--skills-dir', '--skip-verify', '--source', '--stale', '--status', '--surface', '--swap-only', '--target', '--to', '--uninstall', '--user', '--verbose', '--verify', '--version', '--workers', '--yes'],
|
||||
'protocol': ['--all', '--allow-empty', '--apply', '--asof', '--auto', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-usd-per-day', '--by-mention', '--content', '--date', '--days', '--entities', '--fast', '--federated', '--file', '--force', '--from-pages', '--help', '--http', '--image', '--json', '--kind', '--limit', '--max-usd', '--mode', '--multimodal', '--no-embedding', '--offset', '--path', '--progress-interval', '--progress-json', '--quiet', '--save', '--session', '--since', '--slug', '--slugs', '--source', '--stats', '--surface', '--synthesize', '--target', '--timeout', '--token', '--trusted-extraction', '--url', '--with-db', '--yes'],
|
||||
'providers': ['--brain', '--ctx-size', '--embedding-dimensions', '--embedding-model', '--embeddings', '--fast', '--force', '--from-pages', '--help', '--http', '--json', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--reranking', '--source', '--surface', '--touchpoint', '--version'],
|
||||
'publish': ['--accent', '--bg', '--border', '--brain', '--card-bg', '--code-bg', '--error', '--fg', '--help', '--json', '--link', '--muted', '--out', '--password', '--source', '--title'],
|
||||
'quarantine': ['--aliases', '--all', '--apply', '--brain', '--code', '--compile', '--explain', '--fast', '--fix', '--force', '--force-rechunk', '--from-pages', '--help', '--http', '--include-flagged', '--include-null-signature', '--json', '--lang', '--limit', '--markdown', '--multimodal', '--near-symbol', '--no-embed', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--restore-only', '--source', '--stale', '--supersessions', '--symbol-kind', '--thin'],
|
||||
'recall': ['--aliases', '--all', '--as-context', '--brain', '--fast', '--federated', '--force', '--from-pages', '--grep', '--help', '--http', '--include-expired', '--include-null-signature', '--json', '--limit', '--mcp-only', '--no-embedding', '--no-extract', '--no-federated', '--path', '--pattern', '--pending', '--reason', '--repo', '--reset', '--resolve', '--restore-only', '--rollup', '--session', '--session-id', '--since', '--since-last-run', '--source', '--stale', '--supersessions', '--thin', '--timeout', '--today', '--watch'],
|
||||
'quarantine': ['--aliases', '--all', '--apply', '--brain', '--code', '--compile', '--explain', '--fast', '--fix', '--force', '--force-rechunk', '--from-pages', '--help', '--http', '--include-flagged', '--include-null-signature', '--json', '--lang', '--limit', '--markdown', '--multimodal', '--near-symbol', '--no-embed', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--restore-only', '--source', '--stale', '--supersessions', '--surface', '--symbol-kind', '--thin'],
|
||||
'recall': ['--aliases', '--all', '--allow-empty', '--apply', '--as-context', '--asof', '--auto', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--budget-tokens', '--budget-usd-per-day', '--by-mention', '--content', '--date', '--days', '--entities', '--fast', '--federated', '--file', '--force', '--from-pages', '--grep', '--help', '--http', '--image', '--include-expired', '--include-null-signature', '--json', '--kind', '--limit', '--max-usd', '--mcp-only', '--mode', '--multimodal', '--no-embedding', '--no-extract', '--no-federated', '--offset', '--path', '--pattern', '--pending', '--progress-interval', '--progress-json', '--query', '--quiet', '--reason', '--repo', '--reset', '--resolve', '--restore-only', '--rollup', '--save', '--session', '--session-id', '--since', '--since-last-run', '--slug', '--slugs', '--source', '--stale', '--stats', '--supersessions', '--surface', '--thin', '--timeout', '--today', '--trusted-extraction', '--url', '--watch', '--with-db', '--yes'],
|
||||
'reconcile-links': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--dry-run', '--explain', '--follow', '--help', '--include-frontmatter', '--include-null-signature', '--json', '--name-status', '--no-extract', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--source', '--stale', '--strategy', '--supersessions', '--thin', '--timeout', '--type'],
|
||||
'reindex': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--code', '--compile', '--concurrency', '--cost-estimate', '--dry-run', '--embedding-dimensions', '--embedding-model', '--explain', '--fast', '--fix', '--follow', '--force', '--force-break-lock', '--force-rechunk', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--lang', '--limit', '--markdown', '--max-age', '--max-cost-usd', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--no-extract', '--older-than', '--path', '--pattern', '--pending', '--pglite', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--repo', '--reset', '--resolve', '--source', '--stale', '--supabase', '--supersessions', '--thin', '--timeout', '--version', '--workers', '--yes'],
|
||||
'reindex': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--break-lock', '--code', '--compile', '--concurrency', '--cost-estimate', '--dry-run', '--embedding-dimensions', '--embedding-model', '--explain', '--fast', '--fix', '--follow', '--force', '--force-break-lock', '--force-rechunk', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--lang', '--limit', '--markdown', '--max-age', '--max-cost-usd', '--model', '--multimodal', '--no', '--no-embed', '--no-embedding', '--no-extract', '--older-than', '--path', '--pattern', '--pending', '--pglite', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--repo', '--reset', '--resolve', '--source', '--stale', '--supabase', '--supersessions', '--surface', '--thin', '--timeout', '--version', '--workers', '--yes'],
|
||||
'reindex-code': ['--abi', '--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--chunker-debug', '--code', '--compile', '--concurrency', '--dry-run', '--embedding-dimensions', '--embedding-model', '--explain', '--fix', '--follow', '--force', '--force-rechunk', '--help', '--include-null-signature', '--json', '--judge-model', '--lang', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-runtime', '--model', '--multimodal', '--no', '--no-embed', '--no-extract', '--older-than', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--serial', '--source', '--stale', '--supersessions', '--thin', '--timeout', '--version', '--workers', '--yes'],
|
||||
'reindex-frontmatter': ['--aliases', '--all', '--brain', '--concurrency', '--dry-run', '--force', '--help', '--include-null-signature', '--json', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--slug-prefix', '--source', '--stale', '--supersessions', '--thin', '--workers', '--yes'],
|
||||
'reindex-search-vector': ['--aliases', '--all', '--background', '--brain', '--brain-wide-max-cost-usd', '--dry-run', '--explain', '--follow', '--help', '--include-null-signature', '--json', '--no-extract', '--pattern', '--pending', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--thin', '--timeout', '--yes'],
|
||||
'reinit-pglite': ['--abbrev-ref', '--all', '--allow-empty', '--brain', '--break-lock', '--chat-model', '--concurrency', '--confirm-destructive', '--diff-filter', '--embedding-dimensions', '--embedding-model', '--empty', '--exclude', '--exclude-standard', '--expansion-model', '--fast', '--force', '--force-break-lock', '--format', '--from-pages', '--full', '--grant-types', '--hard-deadline', '--help', '--http', '--include-gitignored', '--interval', '--is-ancestor', '--issuer-url', '--json', '--key', '--lock', '--max-age', '--max-sources', '--mcp-only', '--mcp-url', '--migrate-only', '--missing-path', '--model', '--name-only', '--name-status', '--no-auto-embed', '--no-embed', '--no-embedding', '--no-extract', '--no-gpg-sign', '--no-hard-deadline', '--no-pull', '--no-renames', '--no-schema-pack', '--no-sync', '--no-verify', '--non-interactive', '--oauth-client-id', '--oauth-client-secret', '--parallel', '--path', '--pglite', '--priority', '--quiet', '--repo', '--retry-failed', '--schema-pack', '--scopes', '--serial', '--short', '--show-toplevel', '--skip-embed-check', '--skip-failed', '--slugs', '--source', '--source-id', '--src-subpath', '--stale', '--strategy', '--supabase', '--target', '--timeout', '--to', '--url', '--version', '--watch', '--workers', '--yes'],
|
||||
'remote': ['--all', '--background', '--brain', '--break-lock', '--by-mention', '--column', '--concurrency', '--content-audit', '--count', '--detach', '--dim', '--dir', '--drain', '--embedding-dimensions', '--embedding-model', '--fast', '--fix', '--force', '--force-retry', '--force-schema', '--from-pages', '--full', '--help', '--http', '--include-flagged', '--index-audit', '--json', '--locks', '--markdown', '--max-cost', '--max-cost-usd', '--max-jobs', '--max-rss', '--max-usd', '--mcp-only', '--migrate-only', '--multimodal', '--nice', '--no-embedding', '--older-than', '--parallel', '--params', '--pglite', '--phase', '--pid-file', '--progress-json', '--query', '--queue', '--rebuild-rollup', '--regenerate', '--remediate', '--remediation-plan', '--reset', '--resume', '--scope', '--scopes', '--since', '--skills-dir', '--skip-failed', '--slugs', '--source', '--source-id', '--stale', '--status', '--supabase', '--target', '--target-score', '--timeout', '--to', '--top-k', '--url', '--window', '--workers', '--yes'],
|
||||
'repair-jsonb': ['--background', '--brain', '--brain-wide-max-cost-usd', '--dry-run', '--explain', '--fast', '--follow', '--force', '--from-pages', '--help', '--http', '--json', '--lang', '--markdown', '--multimodal', '--near-symbol', '--no-embedding', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--restore-only', '--source', '--stale', '--supabase', '--symbol-kind', '--thin', '--timeout', '--url'],
|
||||
'reinit-pglite': ['--abbrev-ref', '--all', '--allow-empty', '--brain', '--break-lock', '--chat-model', '--concurrency', '--confirm-destructive', '--diff-filter', '--embedding-dimensions', '--embedding-model', '--empty', '--entity', '--exclude', '--exclude-standard', '--expansion-model', '--fast', '--force', '--force-break-lock', '--format', '--from-pages', '--full', '--grant-types', '--hard-deadline', '--help', '--http', '--include-gitignored', '--interval', '--is-ancestor', '--issuer-url', '--json', '--key', '--lock', '--max-age', '--max-sources', '--mcp-only', '--mcp-url', '--migrate-only', '--missing-path', '--model', '--name-only', '--name-status', '--no-auto-embed', '--no-embed', '--no-embedding', '--no-extract', '--no-gpg-sign', '--no-hard-deadline', '--no-pull', '--no-renames', '--no-schema-pack', '--no-sync', '--no-verify', '--non-interactive', '--oauth-client-id', '--oauth-client-secret', '--parallel', '--path', '--pglite', '--priority', '--provenance', '--quiet', '--repo', '--retry-failed', '--schema-pack', '--scopes', '--serial', '--short', '--show-toplevel', '--skip-embed-check', '--skip-failed', '--slugs', '--source', '--source-id', '--src-subpath', '--stale', '--strategy', '--supabase', '--surface', '--target', '--timeout', '--to', '--url', '--version', '--watch', '--workers', '--yes'],
|
||||
'remote': ['--all', '--background', '--brain', '--break-lock', '--by-mention', '--column', '--concurrency', '--content-audit', '--count', '--detach', '--dim', '--dir', '--drain', '--embedding-dimensions', '--embedding-model', '--fast', '--fix', '--force', '--force-retry', '--force-schema', '--from-pages', '--full', '--help', '--http', '--include-flagged', '--index-audit', '--json', '--locks', '--markdown', '--max-cost', '--max-cost-usd', '--max-jobs', '--max-rss', '--max-usd', '--mcp-only', '--migrate-only', '--multimodal', '--nice', '--no-embedding', '--older-than', '--parallel', '--params', '--pglite', '--phase', '--pid-file', '--progress-json', '--query', '--queue', '--rebuild-rollup', '--regenerate', '--remediate', '--remediation-plan', '--reset', '--resume', '--scope', '--scopes', '--since', '--skills-dir', '--skip-failed', '--slugs', '--source', '--source-id', '--stale', '--status', '--supabase', '--surface', '--target', '--target-score', '--timeout', '--to', '--top-k', '--url', '--window', '--workers', '--yes'],
|
||||
'repair-jsonb': ['--background', '--brain', '--brain-wide-max-cost-usd', '--dry-run', '--explain', '--fast', '--follow', '--force', '--from-pages', '--help', '--http', '--json', '--lang', '--markdown', '--multimodal', '--near-symbol', '--no-embedding', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--restore-only', '--source', '--stale', '--supabase', '--surface', '--symbol-kind', '--thin', '--timeout', '--url'],
|
||||
'report': ['--brain', '--content', '--dir', '--help', '--json', '--source', '--title', '--type'],
|
||||
'repos': ['--abbrev-ref', '--abort', '--aliases', '--all', '--all-sources', '--brain', '--branch', '--break-lock', '--cached', '--clone-dir', '--compile', '--confirm-destructive', '--detect', '--dry-run', '--explain', '--fast', '--federated', '--file', '--fix', '--force', '--force-break-lock', '--format', '--from-pages', '--full', '--get', '--git-dir', '--github-repo', '--help', '--http', '--include-null-signature', '--include-warns', '--json', '--keep-storage', '--lang', '--local', '--markdown', '--max-age', '--max-cost-usd', '--multimodal', '--name', '--name-only', '--name-status', '--near-symbol', '--no-cron', '--no-embedding', '--no-extract', '--no-federate', '--no-federated', '--no-harden', '--no-verify', '--params', '--pat-file', '--path', '--pattern', '--pending', '--porcelain', '--push-only', '--quiet', '--rebase', '--repo', '--reset', '--resolve', '--restore-only', '--secret', '--set', '--short', '--source', '--source-id', '--stale', '--status', '--strategy', '--supersessions', '--symbol-kind', '--thin', '--unset-all', '--url', '--url-managed', '--yes'],
|
||||
'repos': ['--abbrev-ref', '--abort', '--aliases', '--all', '--all-sources', '--brain', '--branch', '--break-lock', '--cached', '--clone-dir', '--compile', '--confirm-destructive', '--detect', '--dry-run', '--explain', '--fast', '--federated', '--file', '--fix', '--force', '--force-break-lock', '--format', '--from-pages', '--full', '--get', '--git-dir', '--github-repo', '--help', '--http', '--include-null-signature', '--include-warns', '--json', '--keep-storage', '--lang', '--local', '--markdown', '--max-age', '--max-cost-usd', '--multimodal', '--name', '--name-only', '--name-status', '--near-symbol', '--no-cron', '--no-embedding', '--no-extract', '--no-federate', '--no-federated', '--no-harden', '--no-verify', '--params', '--pat-file', '--path', '--pattern', '--pending', '--porcelain', '--push-only', '--quiet', '--rebase', '--repo', '--reset', '--resolve', '--restore-only', '--secret', '--set', '--short', '--source', '--source-id', '--stale', '--status', '--strategy', '--supersessions', '--surface', '--symbol-kind', '--thin', '--unset-all', '--url', '--url-managed', '--yes'],
|
||||
'resolvers': ['--auto', '--backend', '--brain', '--cost', '--help', '--json', '--source'],
|
||||
'retrieval-upgrade': ['--aliases', '--all', '--background', '--batch-size', '--brain', '--brain-wide-max-cost-usd', '--catch-up', '--dim', '--dry-run', '--embedding-dimensions', '--embedding-model', '--embeddings', '--explain', '--fast', '--follow', '--force', '--from-pages', '--help', '--http', '--ignore-env-override', '--ignore-missing-key', '--include-null-signature', '--json', '--markdown', '--model', '--multimodal', '--name', '--no', '--no-embed', '--no-embedding', '--no-extract', '--non-interactive', '--pace', '--pace-max-concurrency', '--parallel', '--pattern', '--pending', '--prefix', '--priority', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reranking', '--reset', '--resolve', '--resume', '--slugs', '--source', '--stale', '--supersessions', '--thin', '--timeout', '--to', '--undo', '--version', '--yes'],
|
||||
'retrieval-upgrade': ['--aliases', '--all', '--background', '--batch-size', '--brain', '--brain-wide-max-cost-usd', '--catch-up', '--dim', '--dry-run', '--embedding-dimensions', '--embedding-model', '--embeddings', '--explain', '--fast', '--follow', '--force', '--from-pages', '--help', '--http', '--ignore-env-override', '--ignore-missing-key', '--include-null-signature', '--json', '--markdown', '--model', '--multimodal', '--name', '--no', '--no-embed', '--no-embedding', '--no-extract', '--non-interactive', '--pace', '--pace-max-concurrency', '--parallel', '--pattern', '--pending', '--prefix', '--priority', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--reranking', '--reset', '--resolve', '--resume', '--slugs', '--source', '--stale', '--supersessions', '--surface', '--thin', '--timeout', '--to', '--undo', '--version', '--yes'],
|
||||
'routing-eval': ['--brain', '--fix', '--help', '--json', '--llm', '--skills-dir', '--source', '--strict', '--verbose'],
|
||||
'salience': ['--aliases', '--all', '--brain', '--days', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--kind', '--limit', '--mcp-only', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--slug-prefix', '--source', '--stale', '--supersessions', '--thin', '--timeout'],
|
||||
'schema': ['--alias', '--aliases', '--all', '--apply', '--as-filing-rules', '--brain', '--dims', '--expert', '--expert-routing', '--extractable', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--inverse', '--json', '--kind', '--no-embedding', '--no-extract', '--pack', '--page-type', '--pattern', '--pending', '--prefix', '--primitive', '--reset', '--resolve', '--schema-pack', '--since', '--source', '--source-id', '--stale', '--supersessions', '--target-type', '--thin', '--to', '--with-db'],
|
||||
'salience': ['--aliases', '--all', '--brain', '--days', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--kind', '--limit', '--mcp-only', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--slug-prefix', '--source', '--stale', '--supersessions', '--surface', '--thin', '--timeout'],
|
||||
'schema': ['--alias', '--aliases', '--all', '--apply', '--as-filing-rules', '--brain', '--dims', '--expert', '--expert-routing', '--extractable', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--inverse', '--json', '--kind', '--no-embedding', '--no-extract', '--pack', '--page-type', '--pattern', '--pending', '--prefix', '--primitive', '--reset', '--resolve', '--schema-pack', '--since', '--source', '--source-id', '--stale', '--supersessions', '--surface', '--target-type', '--thin', '--to', '--with-db'],
|
||||
'self-upgrade': ['--all', '--brain', '--check', '--check-only', '--ff-only', '--force', '--help', '--json', '--markdown', '--migrate-only', '--non-interactive', '--refresh-cache', '--source', '--swap-only', '--to', '--version', '--yes'],
|
||||
'serve': ['--aliases', '--all', '--bind', '--bound-slug-prefixes', '--brain', '--enable-dcr', '--enable-dcr-insecure', '--federated-read', '--help', '--http', '--include-null-signature', '--json', '--log-full-params', '--no-extract', '--pattern', '--pending', '--port', '--print-admin-token', '--public-url', '--reset', '--resolve', '--source', '--stale', '--stdio-idle-timeout', '--supersessions', '--suppress', '--suppress-bootstrap-token', '--thin', '--token-ttl', '--yes'],
|
||||
'serve': ['--aliases', '--all', '--bind', '--bound-slug-prefixes', '--brain', '--enable-dcr', '--enable-dcr-insecure', '--fast', '--federated-read', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--log-full-params', '--no-embedding', '--no-extract', '--pattern', '--pending', '--port', '--print-admin-token', '--public-url', '--reset', '--resolve', '--source', '--stale', '--stdio-idle-timeout', '--supersessions', '--suppress', '--suppress-bootstrap-token', '--surface', '--thin', '--token-ttl', '--yes'],
|
||||
'skillify': ['--brain', '--description', '--dry-run', '--force', '--help', '--json', '--mutating', '--recent', '--skills-dir', '--source', '--strict', '--triggers', '--verbose', '--writes-pages', '--writes-to'],
|
||||
'skillopt': ['--aliases', '--all', '--allow-mutate-bundled', '--background', '--batch-size', '--benchmark', '--bootstrap-from-routing', '--bootstrap-from-skill', '--bootstrap-reviewed', '--bootstrap-tasks', '--brain', '--brain-wide-max-cost-usd', '--chunker-debug', '--dry-run', '--epochs', '--follow', '--force', '--held-out', '--help', '--include-null-signature', '--json', '--judge-model', '--lr', '--lr-schedule', '--max-cost-usd', '--max-runtime-min', '--model', '--no-extract', '--no-mutate', '--optimizer-model', '--patch', '--pattern', '--pending', '--reset', '--resolve', '--resume', '--rewrite', '--skills-dir', '--source', '--split', '--stale', '--supersessions', '--target-model', '--target-models', '--thin', '--verbose', '--yes'],
|
||||
'skillpack': ['--all', '--apply-clean-hunks', '--author', '--brain', '--dry-run', '--exit-code', '--fast', '--fix', '--force', '--force-unlock', '--format', '--from', '--from-pages', '--frontmatter', '--full', '--help', '--homepage', '--http', '--json', '--license', '--list', '--minimal', '--name-only', '--no-cache', '--no-embedding', '--no-lint', '--note', '--out', '--overwrite-local', '--push', '--quick', '--quiet', '--refresh', '--repo', '--schema-pack', '--short', '--since', '--skills-dir', '--skip-doctor', '--source', '--strict', '--target', '--tier', '--trust', '--url', '--verbose', '--verify', '--workspace', '--yes'],
|
||||
'skillpack': ['--all', '--apply-clean-hunks', '--author', '--brain', '--dry-run', '--exit-code', '--fast', '--fix', '--force', '--force-unlock', '--format', '--from', '--from-pages', '--frontmatter', '--full', '--help', '--homepage', '--http', '--json', '--license', '--list', '--minimal', '--name-only', '--no-cache', '--no-embedding', '--no-lint', '--note', '--out', '--overwrite-local', '--push', '--quick', '--quiet', '--refresh', '--repo', '--schema-pack', '--short', '--since', '--skills-dir', '--skip-doctor', '--source', '--strict', '--surface', '--target', '--tier', '--trust', '--url', '--verbose', '--verify', '--workspace', '--yes'],
|
||||
'skillpack-check': ['--background', '--brain', '--brain-wide-max-cost-usd', '--explain', '--fast', '--follow', '--help', '--json', '--list', '--progress-interval', '--progress-json', '--quiet', '--remediate', '--source', '--stale', '--strict', '--timeout', '--yes'],
|
||||
'smoke-test': ['--brain', '--help', '--json', '--source'],
|
||||
'sources': ['--abbrev-ref', '--abort', '--aliases', '--all', '--all-sources', '--brain', '--branch', '--break-lock', '--cached', '--clone-dir', '--compile', '--confirm-destructive', '--detect', '--dry-run', '--explain', '--fast', '--federated', '--file', '--fix', '--force', '--force-break-lock', '--format', '--from-pages', '--full', '--get', '--git-dir', '--github-repo', '--help', '--http', '--include-null-signature', '--include-warns', '--json', '--keep-storage', '--lang', '--local', '--markdown', '--max-age', '--max-cost-usd', '--multimodal', '--name', '--name-only', '--name-status', '--near-symbol', '--no-cron', '--no-embedding', '--no-extract', '--no-federate', '--no-federated', '--no-harden', '--no-verify', '--params', '--pat-file', '--path', '--pattern', '--pending', '--porcelain', '--push-only', '--quiet', '--rebase', '--repo', '--reset', '--resolve', '--restore-only', '--secret', '--set', '--short', '--source', '--source-id', '--stale', '--status', '--strategy', '--supersessions', '--symbol-kind', '--thin', '--unset-all', '--url', '--url-managed', '--yes'],
|
||||
'status': ['--abbrev-ref', '--abi', '--abort', '--aliases', '--all', '--all-sources', '--allow-empty', '--apply', '--asof', '--auto', '--background', '--batch-size', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--brain-wide-max-cost-usd', '--branch', '--break-lock', '--budget-usd-per-day', '--by-mention', '--cached', '--catch-up', '--clone-dir', '--code', '--column', '--compile', '--concurrency', '--confirm-destructive', '--content', '--content-audit', '--count', '--date', '--days', '--deadline-ms', '--depth', '--detach', '--diff-filter', '--dim', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--empty', '--entities', '--exclude', '--exclude-standard', '--explain', '--fast', '--federated', '--ff-only', '--file', '--fix', '--follow', '--force', '--force-break-lock', '--force-rechunk', '--force-retry', '--force-schema', '--format', '--fresh', '--from-meetings', '--from-pages', '--full', '--git-path', '--hard-deadline', '--help', '--http', '--image', '--include-flagged', '--include-frontmatter', '--include-gitignored', '--include-null-signature', '--index-audit', '--infer-dates', '--install', '--interval', '--is-ancestor', '--json', '--judge-model', '--kind', '--lang', '--limit', '--lock', '--locks', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-jobs', '--max-rss', '--max-runtime', '--max-sources', '--max-usd', '--mcp-only', '--migrate-only', '--min-context', '--missing-path', '--mode', '--model', '--multimodal', '--name', '--name-only', '--name-status', '--near-symbol', '--ner', '--nice', '--no', '--no-auto-embed', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--no-gpg-sign', '--no-hard-deadline', '--no-pull', '--no-recurse-submodules', '--no-renames', '--no-schema-pack', '--no-verify', '--object-format', '--offset', '--older-than', '--order', '--others', '--overwrite', '--pace', '--pace-max-concurrency', '--pack', '--parallel', '--params', '--path', '--pattern', '--pending', '--pglite', '--phase', '--pid-file', '--porcelain', '--prefix', '--priority', '--progress-interval', '--progress-json', '--query', '--queue', '--quiet', '--rebase', '--rebuild-rollup', '--reenrich-after', '--refresh', '--regenerate', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--respect-gitignore', '--restore-only', '--resume', '--retry-failed', '--run-id', '--save', '--scope', '--scopes', '--section', '--serial', '--session', '--short', '--show-toplevel', '--since', '--skills-dir', '--skip-failed', '--slug', '--slugs', '--source', '--source-id', '--src-subpath', '--stale', '--stats', '--status', '--stdin', '--strategy', '--supabase', '--supersessions', '--symbol-kind', '--target', '--target-score', '--thin', '--thin-threshold', '--timeout', '--to', '--top-k', '--trusted-extraction', '--type', '--types', '--url', '--url-managed', '--verbose', '--verify', '--version', '--watch', '--what', '--where', '--who', '--window', '--with-db', '--workers', '--yes'],
|
||||
'sources': ['--abbrev-ref', '--abort', '--aliases', '--all', '--all-sources', '--brain', '--branch', '--break-lock', '--cached', '--clone-dir', '--compile', '--confirm-destructive', '--detect', '--dry-run', '--explain', '--fast', '--federated', '--file', '--fix', '--force', '--force-break-lock', '--format', '--from-pages', '--full', '--get', '--git-dir', '--github-repo', '--help', '--http', '--include-null-signature', '--include-warns', '--json', '--keep-storage', '--lang', '--local', '--markdown', '--max-age', '--max-cost-usd', '--multimodal', '--name', '--name-only', '--name-status', '--near-symbol', '--no-cron', '--no-embedding', '--no-extract', '--no-federate', '--no-federated', '--no-harden', '--no-verify', '--params', '--pat-file', '--path', '--pattern', '--pending', '--porcelain', '--push-only', '--quiet', '--rebase', '--repo', '--reset', '--resolve', '--restore-only', '--secret', '--set', '--short', '--source', '--source-id', '--stale', '--status', '--strategy', '--supersessions', '--surface', '--symbol-kind', '--thin', '--unset-all', '--url', '--url-managed', '--yes'],
|
||||
'status': ['--abbrev-ref', '--abi', '--abort', '--aliases', '--all', '--all-sources', '--allow-empty', '--apply', '--asof', '--auto', '--background', '--batch-size', '--bound-max-concurrent', '--bound-slug-prefixes', '--bound-source', '--bound-tools', '--brain', '--brain-wide-max-cost-usd', '--branch', '--break-lock', '--budget-usd-per-day', '--by-mention', '--cached', '--catch-up', '--clone-dir', '--code', '--column', '--compile', '--concurrency', '--confirm-destructive', '--content', '--content-audit', '--count', '--date', '--days', '--deadline-ms', '--depth', '--detach', '--diff-filter', '--dim', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--empty', '--entities', '--exclude', '--exclude-standard', '--explain', '--fast', '--federated', '--ff-only', '--file', '--fix', '--follow', '--force', '--force-break-lock', '--force-rechunk', '--force-retry', '--force-schema', '--format', '--fresh', '--from-meetings', '--from-pages', '--full', '--git-path', '--hard-deadline', '--help', '--http', '--image', '--include-flagged', '--include-frontmatter', '--include-gitignored', '--include-null-signature', '--index-audit', '--infer-dates', '--install', '--interval', '--is-ancestor', '--json', '--judge-model', '--kind', '--lang', '--limit', '--lock', '--locks', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-jobs', '--max-rss', '--max-runtime', '--max-sources', '--max-usd', '--mcp-only', '--migrate-only', '--min-context', '--missing-path', '--mode', '--model', '--multimodal', '--name', '--name-only', '--name-status', '--near-symbol', '--ner', '--nice', '--no', '--no-auto-embed', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--no-gpg-sign', '--no-hard-deadline', '--no-pull', '--no-recurse-submodules', '--no-renames', '--no-schema-pack', '--no-verify', '--object-format', '--offset', '--older-than', '--order', '--others', '--overwrite', '--pace', '--pace-max-concurrency', '--pack', '--parallel', '--params', '--path', '--pattern', '--pending', '--pglite', '--phase', '--pid-file', '--porcelain', '--prefix', '--priority', '--progress-interval', '--progress-json', '--query', '--queue', '--quiet', '--rebase', '--rebuild-rollup', '--reenrich-after', '--refresh', '--regenerate', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--respect-gitignore', '--restore-only', '--resume', '--retry-failed', '--run-id', '--save', '--scope', '--scopes', '--section', '--serial', '--session', '--short', '--show-toplevel', '--since', '--skills-dir', '--skip-failed', '--slug', '--slugs', '--source', '--source-id', '--src-subpath', '--stale', '--stats', '--status', '--stdin', '--strategy', '--supabase', '--supersessions', '--surface', '--symbol-kind', '--target', '--target-score', '--thin', '--thin-threshold', '--timeout', '--to', '--top-k', '--trusted-extraction', '--type', '--types', '--url', '--url-managed', '--verbose', '--verify', '--version', '--watch', '--what', '--where', '--who', '--window', '--with-db', '--workers', '--yes'],
|
||||
'storage': ['--aliases', '--all', '--brain', '--federated', '--fix', '--help', '--include-null-signature', '--json', '--no-extract', '--no-federated', '--path', '--pattern', '--pending', '--repo', '--reset', '--resolve', '--restore-only', '--source', '--stale', '--supersessions', '--thin', '--to'],
|
||||
'sync': ['--abbrev-ref', '--abi', '--abort', '--aliases', '--all', '--all-sources', '--allow-empty', '--asof', '--auto', '--background', '--batch-size', '--brain', '--brain-wide-max-cost-usd', '--branch', '--break-lock', '--by-mention', '--cached', '--catch-up', '--clone-dir', '--code', '--column', '--compile', '--concurrency', '--confirm-destructive', '--content-audit', '--count', '--depth', '--detach', '--diff-filter', '--dim', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--empty', '--exclude', '--exclude-standard', '--explain', '--fast', '--federated', '--ff-only', '--fix', '--follow', '--force', '--force-break-lock', '--force-rechunk', '--force-retry', '--force-schema', '--format', '--fresh', '--from-meetings', '--from-pages', '--full', '--git-path', '--hard-deadline', '--help', '--http', '--include-flagged', '--include-frontmatter', '--include-gitignored', '--include-null-signature', '--index-audit', '--infer-dates', '--interval', '--is-ancestor', '--json', '--kind', '--lang', '--lock', '--locks', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-jobs', '--max-rss', '--max-sources', '--max-usd', '--migrate-only', '--missing-path', '--model', '--multimodal', '--name', '--name-only', '--name-status', '--ner', '--nice', '--no-auto-embed', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--no-gpg-sign', '--no-hard-deadline', '--no-pull', '--no-recurse-submodules', '--no-renames', '--no-schema-pack', '--no-verify', '--object-format', '--older-than', '--others', '--overwrite', '--pace', '--pace-max-concurrency', '--pack', '--parallel', '--params', '--path', '--pattern', '--pending', '--pglite', '--phase', '--pid-file', '--porcelain', '--prefix', '--priority', '--progress-interval', '--progress-json', '--query', '--queue', '--quiet', '--rebase', '--rebuild-rollup', '--refresh', '--regenerate', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--respect-gitignore', '--restore-only', '--resume', '--retry-failed', '--run-id', '--save', '--scope', '--serial', '--short', '--show-toplevel', '--since', '--skills-dir', '--skip-failed', '--slug', '--slugs', '--source', '--source-id', '--src-subpath', '--stale', '--status', '--stdin', '--strategy', '--supabase', '--supersessions', '--target', '--target-score', '--thin', '--timeout', '--to', '--top-k', '--type', '--url', '--url-managed', '--verbose', '--verify', '--watch', '--window', '--workers', '--yes'],
|
||||
'takes': ['--aliases', '--all', '--brain', '--bucket-size', '--by', '--claim', '--dir', '--domain', '--dry-run', '--evidence', '--expired', '--fast', '--federated', '--force', '--from-pages', '--help', '--holder', '--http', '--include-covered', '--include-null-signature', '--json', '--kind', '--limit', '--max-pages', '--no-embedding', '--no-extract', '--no-federated', '--outcome', '--path', '--pattern', '--pending', '--quality', '--refresh', '--repo', '--reset', '--resolve', '--restore-only', '--row', '--since', '--slugs', '--sort', '--source', '--source-id', '--stale', '--supersessions', '--thin', '--unit', '--until', '--value', '--weight', '--who', '--yes'],
|
||||
'think': ['--aliases', '--all', '--anchor', '--brain', '--calibration-holder', '--explain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--max-usd', '--mcp-only', '--model', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--rounds', '--save', '--since', '--source', '--stale', '--supersessions', '--take', '--thin', '--timeout', '--until', '--with-calibration'],
|
||||
'sync': ['--abbrev-ref', '--abi', '--abort', '--aliases', '--all', '--all-sources', '--allow-empty', '--asof', '--auto', '--background', '--batch-size', '--brain', '--brain-wide-max-cost-usd', '--branch', '--break-lock', '--by-mention', '--cached', '--catch-up', '--clone-dir', '--code', '--column', '--compile', '--concurrency', '--confirm-destructive', '--content-audit', '--count', '--depth', '--detach', '--diff-filter', '--dim', '--dir', '--drain', '--dry-run', '--embedding-dimensions', '--embedding-model', '--empty', '--exclude', '--exclude-standard', '--explain', '--fast', '--federated', '--ff-only', '--fix', '--follow', '--force', '--force-break-lock', '--force-rechunk', '--force-retry', '--force-schema', '--format', '--fresh', '--from-meetings', '--from-pages', '--full', '--git-path', '--hard-deadline', '--help', '--http', '--include-flagged', '--include-frontmatter', '--include-gitignored', '--include-null-signature', '--index-audit', '--infer-dates', '--interval', '--is-ancestor', '--json', '--kind', '--lang', '--lock', '--locks', '--markdown', '--max-age', '--max-cost', '--max-cost-usd', '--max-jobs', '--max-rss', '--max-sources', '--max-usd', '--migrate-only', '--missing-path', '--model', '--multimodal', '--name', '--name-only', '--name-status', '--ner', '--nice', '--no-auto-embed', '--no-embed', '--no-embedding', '--no-extract', '--no-federated', '--no-gpg-sign', '--no-hard-deadline', '--no-pull', '--no-recurse-submodules', '--no-renames', '--no-schema-pack', '--no-verify', '--object-format', '--older-than', '--others', '--overwrite', '--pace', '--pace-max-concurrency', '--pack', '--parallel', '--params', '--path', '--pattern', '--pending', '--pglite', '--phase', '--pid-file', '--porcelain', '--prefix', '--priority', '--progress-interval', '--progress-json', '--query', '--queue', '--quiet', '--rebase', '--rebuild-rollup', '--refresh', '--regenerate', '--remediate', '--remediation-plan', '--repo', '--reset', '--resolve', '--respect-gitignore', '--restore-only', '--resume', '--retry-failed', '--run-id', '--save', '--scope', '--serial', '--short', '--show-toplevel', '--since', '--skills-dir', '--skip-failed', '--slug', '--slugs', '--source', '--source-id', '--src-subpath', '--stale', '--status', '--stdin', '--strategy', '--supabase', '--supersessions', '--surface', '--target', '--target-score', '--thin', '--timeout', '--to', '--top-k', '--type', '--url', '--url-managed', '--verbose', '--verify', '--watch', '--window', '--workers', '--yes'],
|
||||
'takes': ['--aliases', '--all', '--brain', '--bucket-size', '--by', '--claim', '--dir', '--domain', '--dry-run', '--evidence', '--expired', '--fast', '--federated', '--force', '--from-pages', '--help', '--holder', '--http', '--include-covered', '--include-null-signature', '--json', '--kind', '--limit', '--max-pages', '--no-embedding', '--no-extract', '--no-federated', '--outcome', '--path', '--pattern', '--pending', '--quality', '--refresh', '--repo', '--reset', '--resolve', '--restore-only', '--row', '--since', '--slugs', '--sort', '--source', '--source-id', '--stale', '--supersessions', '--surface', '--thin', '--unit', '--until', '--value', '--weight', '--who', '--yes'],
|
||||
'think': ['--aliases', '--all', '--anchor', '--brain', '--calibration-holder', '--explain', '--fast', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--max-usd', '--mcp-only', '--model', '--no-embedding', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--rounds', '--save', '--since', '--source', '--stale', '--supersessions', '--surface', '--take', '--thin', '--timeout', '--until', '--with-calibration'],
|
||||
'transcripts': ['--aliases', '--all', '--brain', '--days', '--full', '--help', '--include-null-signature', '--json', '--limit', '--no-extract', '--pattern', '--pending', '--reset', '--resolve', '--source', '--stale', '--supersessions', '--thin', '--timeout'],
|
||||
'upgrade': ['--all', '--apply-clean-hunks', '--brain', '--check', '--code', '--compile', '--concurrency', '--embedding-dimensions', '--embedding-model', '--fast', '--ff-only', '--force', '--force-all', '--force-orchestrator', '--force-retry', '--force-schema', '--format', '--from-pages', '--help', '--host-dir', '--http', '--inject-bootstrap', '--inline', '--install', '--interval', '--json', '--limit', '--list', '--markdown', '--max-rss', '--migrate-only', '--migration', '--mode', '--model', '--multimodal', '--name-only', '--no', '--no-autopilot-install', '--no-embed', '--no-embedding', '--no-inject', '--no-worker', '--non-interactive', '--now', '--quiet', '--repo', '--since', '--skills-dir', '--skip-verify', '--source', '--stale', '--status', '--swap-only', '--target', '--to', '--uninstall', '--user', '--verbose', '--verify', '--version', '--workers', '--yes'],
|
||||
'watch': ['--aliases', '--all', '--brain', '--fast', '--federated', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--max-pages', '--min-confidence', '--no-embedding', '--no-extract', '--no-federated', '--path', '--pattern', '--pending', '--repo', '--reset', '--resolve', '--restore-only', '--source', '--stale', '--supersessions', '--thin', '--window-turns'],
|
||||
'upgrade': ['--all', '--apply-clean-hunks', '--brain', '--check', '--code', '--compile', '--concurrency', '--embedding-dimensions', '--embedding-model', '--fast', '--ff-only', '--force', '--force-all', '--force-orchestrator', '--force-retry', '--force-schema', '--format', '--from-pages', '--help', '--host-dir', '--http', '--inject-bootstrap', '--inline', '--install', '--interval', '--json', '--limit', '--list', '--markdown', '--max-rss', '--migrate-only', '--migration', '--mode', '--model', '--multimodal', '--name-only', '--no', '--no-autopilot-install', '--no-embed', '--no-embedding', '--no-inject', '--no-worker', '--non-interactive', '--now', '--quiet', '--repo', '--since', '--skills-dir', '--skip-verify', '--source', '--stale', '--status', '--surface', '--swap-only', '--target', '--to', '--uninstall', '--user', '--verbose', '--verify', '--version', '--workers', '--yes'],
|
||||
'watch': ['--aliases', '--all', '--brain', '--fast', '--federated', '--force', '--from-pages', '--help', '--http', '--include-null-signature', '--json', '--max-pages', '--min-confidence', '--no-embedding', '--no-extract', '--no-federated', '--path', '--pattern', '--pending', '--repo', '--reset', '--resolve', '--restore-only', '--source', '--stale', '--supersessions', '--surface', '--thin', '--window-turns'],
|
||||
'ze-switch': ['--aliases', '--all', '--brain', '--confirm-reembed', '--dry-run', '--force', '--help', '--ignore-env-override', '--ignore-missing-key', '--include-null-signature', '--json', '--markdown', '--no-extract', '--non-interactive', '--pattern', '--pending', '--reset', '--resolve', '--resume', '--source', '--stale', '--supersessions', '--thin', '--undo', '--yes'],
|
||||
};
|
||||
|
||||
@@ -114,6 +114,17 @@ export interface GBrainConfig {
|
||||
provider_base_urls?: Record<string, string>;
|
||||
/** Optional chat request providerOptions overrides keyed by recipe id or "recipe:modelId". */
|
||||
provider_chat_options?: Record<string, Record<string, unknown>>;
|
||||
/**
|
||||
* MEMORY_VERBS v1 (Cathedral 1): default MCP tool surface for `gbrain serve`.
|
||||
* 'verbs' = exactly the 5 protocol verbs (the quickstart surface);
|
||||
* 'full' (default) = every operation. The `--surface` flag overrides per-run.
|
||||
*/
|
||||
mcp_surface?: 'verbs' | 'full';
|
||||
/**
|
||||
* MEMORY_VERBS v1 [D6C]: ISO timestamp stamped by `gbrain init` so
|
||||
* `gbrain protocol stats` can derive real TTHW (install → first verb call).
|
||||
*/
|
||||
protocol_installed_at?: string;
|
||||
/**
|
||||
* Optional storage backend config (S3/Supabase/local). Shape matches
|
||||
* `StorageConfig` in `./storage.ts`. Typed as `unknown` here to avoid
|
||||
@@ -948,6 +959,9 @@ export const KNOWN_CONFIG_KEYS: readonly string[] = [
|
||||
'chat_model',
|
||||
'chat_fallback_chain',
|
||||
'provider_base_urls',
|
||||
// MEMORY_VERBS v1 (Cathedral 1)
|
||||
'mcp_surface',
|
||||
'protocol_installed_at',
|
||||
'provider_chat_options',
|
||||
'storage',
|
||||
'eval',
|
||||
|
||||
@@ -106,7 +106,7 @@ export interface ResolvePointersOpts {
|
||||
sourceIds?: string[];
|
||||
}
|
||||
|
||||
interface PageRow {
|
||||
export interface PageRow {
|
||||
slug: string;
|
||||
source_id: string;
|
||||
title: string;
|
||||
@@ -290,8 +290,11 @@ function displayForRow(row: PageRow, displayByNorm: Map<string, string>): string
|
||||
* otherwise strip takes/private-fact fences from the body (the same boundary
|
||||
* get_page applies to untrusted readers) and take the first sentence. Never
|
||||
* returns raw compiled_truth.
|
||||
*
|
||||
* Exported for the MEMORY_VERBS v1 entity card (verbs/entity-card.ts) — the
|
||||
* card's `summary` field runs through THIS boundary, not a parallel one.
|
||||
*/
|
||||
function safeSynopsis(row: PageRow): string {
|
||||
export function safeSynopsis(row: PageRow): string {
|
||||
const fmSummary = row.frontmatter?.summary;
|
||||
if (typeof fmSummary === 'string' && fmSummary.trim()) {
|
||||
return clip(collapse(fmSummary), SYNOPSIS_MAX);
|
||||
|
||||
@@ -127,6 +127,7 @@ export const BRAIN_CHECK_NAMES: ReadonlySet<string> = new Set([
|
||||
* skill-flavored name) live under 'brain'.
|
||||
*/
|
||||
export const SKILL_CHECK_NAMES: ReadonlySet<string> = new Set([
|
||||
'memory_verbs_usage',
|
||||
'resolver_health',
|
||||
'retrieval_reflex_health',
|
||||
'skill_brain_first',
|
||||
|
||||
@@ -65,6 +65,12 @@ export interface FenceInputFact {
|
||||
/** Defaults to 1.0 when undefined (matches engine.insertFact behavior). */
|
||||
confidence?: number;
|
||||
validFrom?: Date;
|
||||
/**
|
||||
* MEMORY_VERBS v1 (c5): remember's ttl → valid_until. Date-only in the
|
||||
* fence cell; the DB column derives from it on the stamp step.
|
||||
* Undefined/null = never expires (pre-v1 behavior unchanged).
|
||||
*/
|
||||
validUntil?: Date | null;
|
||||
embedding: Float32Array | null;
|
||||
sessionId: string | null;
|
||||
}
|
||||
@@ -270,7 +276,10 @@ export async function writeFactsToFence(
|
||||
visibility: f.visibility,
|
||||
notability: f.notability ?? 'medium',
|
||||
validFrom: validFromStr,
|
||||
validUntil: undefined,
|
||||
// MEMORY_VERBS v1 (c5): remember's ttl threads through to the fence
|
||||
// cell — was hard-coded undefined, which silently dropped expiry on
|
||||
// this path. extractFactsFromFenceText derives the DB column from it.
|
||||
validUntil: f.validUntil ? f.validUntil.toISOString().slice(0, 10) : undefined,
|
||||
source: f.source,
|
||||
context: f.context ?? undefined,
|
||||
});
|
||||
|
||||
@@ -55,6 +55,7 @@ interface FactDbRow {
|
||||
row_num: number | null;
|
||||
source_markdown_slug: string | null;
|
||||
expired_at: Date | null;
|
||||
visibility: string;
|
||||
}
|
||||
|
||||
interface SourceRow {
|
||||
@@ -83,15 +84,43 @@ function todayUtc(): string {
|
||||
export async function forgetFactInFence(
|
||||
engine: BrainEngine,
|
||||
factId: number,
|
||||
opts: { reason?: string } = {},
|
||||
opts: {
|
||||
reason?: string;
|
||||
/**
|
||||
* MEMORY_VERBS v1 trust boundary [ship P1.1]: when set, the fact must
|
||||
* belong to this source or the call returns `not_found` (indistinguishable
|
||||
* from a truly-missing id — no cross-source existence leak). The `forget`
|
||||
* verb passes ctx.sourceId so a remote caller scoped to source A cannot
|
||||
* expire facts in source B by guessing global ids.
|
||||
*/
|
||||
sourceId?: string;
|
||||
/**
|
||||
* When true (remote callers), the fact must be visibility='world' or the
|
||||
* call returns `not_found` — a remote caller can't expire private facts it
|
||||
* could never read (mirrors recall's remote posture).
|
||||
*/
|
||||
worldOnly?: boolean;
|
||||
} = {},
|
||||
): Promise<ForgetFactResult> {
|
||||
const reason = opts.reason ?? 'forgotten';
|
||||
|
||||
const rows = await engine.executeRaw<FactDbRow>(
|
||||
`SELECT id, source_id, entity_slug, row_num, source_markdown_slug, expired_at
|
||||
`SELECT id, source_id, entity_slug, row_num, source_markdown_slug, expired_at, visibility
|
||||
FROM facts WHERE id = $1`,
|
||||
[factId],
|
||||
);
|
||||
// Trust-boundary scope check BEFORE any state inspection: a row outside the
|
||||
// caller's source (or private, for remote callers) is reported as not_found,
|
||||
// never distinguished from a missing id.
|
||||
if (rows.length === 1) {
|
||||
const r = rows[0];
|
||||
const outOfScope =
|
||||
(opts.sourceId !== undefined && r.source_id !== opts.sourceId) ||
|
||||
(opts.worldOnly === true && r.visibility !== 'world');
|
||||
if (outOfScope) {
|
||||
return { ok: false, path: 'not_found', reason };
|
||||
}
|
||||
}
|
||||
if (rows.length === 0) {
|
||||
return { ok: false, path: 'not_found', reason };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* MEMORY_VERBS v1 — `writeSingleFact`: the zero-LLM single-fact write seam
|
||||
* behind the `remember` verb [E1].
|
||||
*
|
||||
* `runFactsPipeline` is extraction-first (LLM-gated in extract.ts) and cannot
|
||||
* back a verb whose fact arrives pre-formed. This module reuses the pipeline's
|
||||
* post-extraction stages directly: resolve → dedup (embedding cosine, same
|
||||
* 0.95 threshold) → fence-first write (markdown durability) with the same
|
||||
* legacy DB-only fallbacks (thin-client, unparented, stub-guard).
|
||||
*
|
||||
* Supersession [X1, frozen as implementation-defined]: minimal deterministic
|
||||
* rule, zero LLM — when the top dedup candidate scores >= threshold with the
|
||||
* SAME kind but DIFFERENT text, the new fact supersedes it (a near-duplicate
|
||||
* with changed content is an update: "X at Acme" → "X left Acme"). Same text
|
||||
* → plain duplicate (existing id returned, nothing written).
|
||||
*
|
||||
* Degradation (documented in the protocol doc): with no embedding provider,
|
||||
* dedup/supersession are skipped on the fence path and near-duplicates may
|
||||
* insert — `degraded_dedup: true` tells the caller.
|
||||
*
|
||||
* Provenance (c6): callers pass free-text provenance which lands on
|
||||
* `NewFact.source` verbatim — this seam deliberately does NOT take a
|
||||
* FactsBackstopCtx (whose `source` union is pipeline-internal).
|
||||
*/
|
||||
|
||||
import type { BrainEngine, FactInsertStatus, NewFact } from '../engine.ts';
|
||||
|
||||
const DEDUP_THRESHOLD = 0.95;
|
||||
const DEDUP_CANDIDATE_LIMIT = 5;
|
||||
|
||||
export interface SingleFactInput {
|
||||
fact: string;
|
||||
/** Free-text attribution, stored verbatim as the fact's `source`. */
|
||||
provenance: string;
|
||||
kind?: NewFact['kind'];
|
||||
/** Free-form entity ref; canonicalized via resolveEntitySlug. */
|
||||
entity?: string | null;
|
||||
/** Facts-layer default 'private'; the remember VERB passes 'world' [F2]. */
|
||||
visibility?: 'private' | 'world';
|
||||
validUntil?: Date | null;
|
||||
sessionId?: string | null;
|
||||
confidence?: number;
|
||||
}
|
||||
|
||||
export interface SingleFactResult {
|
||||
id: number;
|
||||
status: FactInsertStatus;
|
||||
entity_slug: string | null;
|
||||
valid_until: Date | null;
|
||||
/** True when no embedding provider — dedup/supersession skipped. */
|
||||
degraded_dedup: boolean;
|
||||
}
|
||||
|
||||
export async function writeSingleFact(
|
||||
engine: BrainEngine,
|
||||
sourceId: string,
|
||||
input: SingleFactInput,
|
||||
): Promise<SingleFactResult> {
|
||||
const { resolveEntitySlug } = await import('../entities/resolve.ts');
|
||||
const { cosineSimilarity } = await import('./classify.ts');
|
||||
const { writeFactsToFence, lookupSourceLocalPath } = await import('./fence-write.ts');
|
||||
const { isAvailable, embedOne } = await import('../ai/gateway.ts');
|
||||
|
||||
const factText = input.fact.trim();
|
||||
const kind = input.kind ?? 'fact';
|
||||
const visibility = input.visibility ?? 'private';
|
||||
const validUntil = input.validUntil ?? null;
|
||||
|
||||
const resolvedSlug = input.entity
|
||||
? ((await resolveEntitySlug(engine, sourceId, input.entity)) ?? input.entity)
|
||||
: null;
|
||||
|
||||
// Embedding (NOT an LLM call): powers dedup + downstream recall. Fail-soft —
|
||||
// a missing/failing provider degrades dedup, never the write.
|
||||
let embedding: Float32Array | null = null;
|
||||
let degradedDedup = false;
|
||||
if (isAvailable('embedding')) {
|
||||
try {
|
||||
embedding = await embedOne(factText);
|
||||
} catch {
|
||||
degradedDedup = true;
|
||||
}
|
||||
} else {
|
||||
degradedDedup = true;
|
||||
}
|
||||
|
||||
// Dedup + supersession decision (same candidates + threshold as the pipeline).
|
||||
let supersedeId: number | null = null;
|
||||
if (resolvedSlug && embedding) {
|
||||
const candidates = await engine.findCandidateDuplicates(sourceId, resolvedSlug, factText, {
|
||||
embedding,
|
||||
k: DEDUP_CANDIDATE_LIMIT,
|
||||
});
|
||||
let top: (typeof candidates)[number] | null = null;
|
||||
let topScore = -1;
|
||||
for (const c of candidates) {
|
||||
if (!c.embedding) continue;
|
||||
const s = cosineSimilarity(embedding, c.embedding);
|
||||
if (s > topScore) {
|
||||
topScore = s;
|
||||
top = c;
|
||||
}
|
||||
}
|
||||
if (top && topScore >= DEDUP_THRESHOLD) {
|
||||
const textDiffers = collapse(top.fact) !== collapse(factText);
|
||||
if (top.kind === kind && textDiffers) {
|
||||
supersedeId = top.id; // X1: near-duplicate with changed content = update
|
||||
} else {
|
||||
return {
|
||||
id: top.id,
|
||||
status: 'duplicate',
|
||||
entity_slug: resolvedSlug,
|
||||
valid_until: top.valid_until ?? null,
|
||||
degraded_dedup: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newFact: NewFact = {
|
||||
fact: factText,
|
||||
kind,
|
||||
entity_slug: resolvedSlug,
|
||||
visibility,
|
||||
source: input.provenance,
|
||||
source_session: input.sessionId ?? null,
|
||||
confidence: input.confidence ?? 1.0,
|
||||
valid_until: validUntil,
|
||||
embedding,
|
||||
};
|
||||
|
||||
// Fence-first write (markdown durability — same policy as the pipeline):
|
||||
// requires a resolved, prefixed entity slug and a local_path. Everything
|
||||
// else takes the legacy DB-only insertFact path, which also handles the
|
||||
// supersedeId bookkeeping engine-side.
|
||||
const localPath = resolvedSlug ? await lookupSourceLocalPath(engine, sourceId) : null;
|
||||
const fenceable = resolvedSlug !== null && localPath !== null;
|
||||
|
||||
if (fenceable) {
|
||||
const result = await writeFactsToFence(
|
||||
engine,
|
||||
{ sourceId, localPath, slug: resolvedSlug },
|
||||
[
|
||||
{
|
||||
fact: factText,
|
||||
kind,
|
||||
notability: 'medium',
|
||||
source: input.provenance,
|
||||
context: null,
|
||||
visibility,
|
||||
confidence: input.confidence ?? 1.0,
|
||||
validFrom: new Date(),
|
||||
validUntil,
|
||||
embedding,
|
||||
sessionId: input.sessionId ?? null,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
if (result.fenceWriteFailed) {
|
||||
// Parse-validate rejected the .tmp (quarantined). Hard failure — do NOT
|
||||
// fall through to a DB row whose fence is broken (pipeline policy).
|
||||
throw new Error(
|
||||
`facts fence write failed for ${resolvedSlug} — .tmp quarantined; see the facts write-failure JSONL log`,
|
||||
);
|
||||
}
|
||||
if (!result.stubGuardBlocked && !result.legacyFallback) {
|
||||
const newId = result.ids[0];
|
||||
if (supersedeId !== null && newId !== undefined) {
|
||||
await expireSuperseded(engine, supersedeId, newId);
|
||||
return {
|
||||
id: newId,
|
||||
status: 'superseded',
|
||||
entity_slug: resolvedSlug,
|
||||
valid_until: validUntil,
|
||||
degraded_dedup: degradedDedup,
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: newId,
|
||||
status: 'inserted',
|
||||
entity_slug: resolvedSlug,
|
||||
valid_until: validUntil,
|
||||
degraded_dedup: degradedDedup,
|
||||
};
|
||||
}
|
||||
// stubGuardBlocked / defensive legacyFallback → DB-only path below.
|
||||
}
|
||||
|
||||
const inserted = await engine.insertFact(newFact, { // gbrain-allow-direct-insert: writeSingleFact legacy path for unparented / thin-client / stub-guarded facts (mirrors the pipeline's fallback buckets)
|
||||
source_id: sourceId,
|
||||
...(supersedeId !== null ? { supersedeId } : {}),
|
||||
});
|
||||
|
||||
return {
|
||||
id: inserted.id,
|
||||
status: supersedeId !== null ? 'superseded' : inserted.status,
|
||||
entity_slug: resolvedSlug,
|
||||
valid_until: validUntil,
|
||||
degraded_dedup: degradedDedup,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fence-path supersession bookkeeping: expire the old row through the fence
|
||||
* (strikethrough + valid_until, the same surface `forget` uses) and link
|
||||
* `superseded_by` for the audit trail. Both steps best-effort — the new fact
|
||||
* is already durably written; a partial supersede is an audit gap, not data
|
||||
* loss.
|
||||
*/
|
||||
async function expireSuperseded(engine: BrainEngine, oldId: number, newId: number): Promise<void> {
|
||||
try {
|
||||
const { forgetFactInFence } = await import('./forget.ts');
|
||||
await forgetFactInFence(engine, oldId, { reason: `superseded by fact #${newId}` });
|
||||
} catch {
|
||||
/* best-effort */
|
||||
}
|
||||
try {
|
||||
await engine.executeRaw(`UPDATE facts SET superseded_by = $1 WHERE id = $2`, [newId, oldId]);
|
||||
} catch {
|
||||
/* best-effort */
|
||||
}
|
||||
}
|
||||
|
||||
function collapse(s: string): string {
|
||||
return s.replace(/\s+/g, ' ').trim().toLowerCase();
|
||||
}
|
||||
@@ -66,6 +66,11 @@ export const LIST_PAGES_DESCRIPTION =
|
||||
|
||||
export const QUERY_DESCRIPTION =
|
||||
"Hybrid search with vector + keyword + multi-query expansion. " +
|
||||
"Prefer `query` for concept / synonym / landscape questions ('all the X that " +
|
||||
"do Y', 'the landscape of Z') — expansion recovers synonym- and " +
|
||||
"outcome-phrased matches a single embedding misses. Still top-K: for " +
|
||||
"exhaustive enumeration use list_pages; for exact known tokens `search` is " +
|
||||
"cheaper (no expansion LLM call). " +
|
||||
"For personal/emotional questions ('what's going on with me', 'anything notable', " +
|
||||
"'how am I feeling'), prefer get_recent_salience, find_anomalies, or " +
|
||||
"get_recent_transcripts. Semantic search returns polished pages and misses " +
|
||||
@@ -73,12 +78,19 @@ export const QUERY_DESCRIPTION =
|
||||
"mean impressive — they often mean difficult or emotionally charged.";
|
||||
|
||||
export const SEARCH_DESCRIPTION =
|
||||
"Keyword search using full-text search. For personal/emotional questions, " +
|
||||
"Cheap hybrid search (vector + keyword + RRF) with no LLM expansion. " +
|
||||
"Best for exact known tokens, names, and structured-field lookups. A populated " +
|
||||
"result set is NOT proof of coverage — for concept / synonym / landscape " +
|
||||
"questions use `query` (adds multi-query expansion); for exhaustive " +
|
||||
"enumeration use list_pages pagination. " +
|
||||
"For personal/emotional questions, " +
|
||||
"prefer get_recent_salience or find_anomalies — they surface activity bursts " +
|
||||
"without needing a search term. " +
|
||||
"For code-symbol questions (callers, callees, definitions, blast radius), use " +
|
||||
"code_callers / code_callees / code_def / code_refs instead — those return " +
|
||||
"structural graph data, not text chunks.";
|
||||
"structural graph data, not text chunks. " +
|
||||
"For agent memory reads (saved facts + budget-packed retrieval), prefer the " +
|
||||
"`recall` verb.";
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// v0.32.6 — contradiction probe MCP surface (M3)
|
||||
|
||||
+203
-6
@@ -26,6 +26,10 @@ import { buildVisibilityClause } from './search/sql-ranking.ts';
|
||||
import { bumpLastRetrievedAt } from './last-retrieved.ts';
|
||||
import { isSearchMode } from './search/mode.ts';
|
||||
import { stampEvidence } from './search/evidence.ts';
|
||||
import { packToBudget, estimateTokens, resultTokens } from './search/token-budget.ts';
|
||||
import { isAvailable } from './ai/gateway.ts';
|
||||
import { verbOperations, MEMORY_VERBS_VERSION } from './verbs.ts';
|
||||
export { MEMORY_VERBS_VERSION };
|
||||
import type { SearchResult } from './types.ts';
|
||||
import { CJK_SLUG_CHARS, PAGE_SLUG_SEG } from './cjk.ts';
|
||||
import { ALL_SOURCES } from './source-id.ts';
|
||||
@@ -72,10 +76,27 @@ export type ErrorCode =
|
||||
| 'rate_limited' // v0.31: gateway rate-limit upstream
|
||||
| 'extraction_failed' // v0.31: facts extractor failed (refusal, parse, abort)
|
||||
| 'fact_not_found' // v0.31: forget_fact / recall on unknown id
|
||||
// MEMORY_VERBS v1 protocol codes (frozen — docs/protocol/MEMORY_VERBS_v1.md).
|
||||
// Coarse on purpose: codes are for branching (configure/retry vs caller bug
|
||||
// vs server bug); the freeform `detail` field carries specifics.
|
||||
| 'not_found' // verb-level: unknown fact id (forget)
|
||||
| 'scope_denied' // verb-level: OAuth scope / trust-boundary refusal
|
||||
| 'provenance_required' // remember: provenance missing or empty
|
||||
| 'unavailable' // a required dependency cannot serve (no API key, gateway down, model refusal)
|
||||
| 'budget_unsatisfiable' // RESERVED in v1 — schema-listed, never returned
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
| (string & {}); // OPEN union for forward-compat (eE7 / D13)
|
||||
|
||||
export class OperationError extends Error {
|
||||
/**
|
||||
* MEMORY_VERBS v1: verb handlers set `protocolVersion` (=1) and may set
|
||||
* `detail` (freeform specifics, e.g. which dependency failed). Both are
|
||||
* additive — non-verb ops never set them and their envelopes are unchanged
|
||||
* (undefined keys drop out of JSON.stringify).
|
||||
*/
|
||||
public detail?: string;
|
||||
public protocolVersion?: number;
|
||||
|
||||
constructor(
|
||||
public code: ErrorCode,
|
||||
message: string,
|
||||
@@ -92,10 +113,29 @@ export class OperationError extends Error {
|
||||
message: this.message,
|
||||
suggestion: this.suggestion,
|
||||
docs: this.docs,
|
||||
detail: this.detail,
|
||||
protocol_version: this.protocolVersion,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MEMORY_VERBS v1 error constructor. Every verb error carries a populated
|
||||
* `suggestion` (problem + cause + fix — agents read it and self-correct;
|
||||
* conformance asserts non-empty) and `protocol_version: 1`.
|
||||
*/
|
||||
export function verbError(
|
||||
code: ErrorCode,
|
||||
message: string,
|
||||
suggestion: string,
|
||||
detail?: string,
|
||||
): OperationError {
|
||||
const e = new OperationError(code, message, suggestion);
|
||||
e.protocolVersion = MEMORY_VERBS_VERSION;
|
||||
if (detail !== undefined) e.detail = detail;
|
||||
return e;
|
||||
}
|
||||
|
||||
// --- Upload validators (Fix 1 / B5 / H5 / M4) ---
|
||||
|
||||
/**
|
||||
@@ -944,6 +984,22 @@ export interface Operation {
|
||||
*/
|
||||
scope?: 'read' | 'write' | 'admin' | 'sources_admin' | 'users_admin';
|
||||
localOnly?: boolean;
|
||||
/**
|
||||
* MEMORY_VERBS v1: marks the five frozen protocol verbs (recall, remember,
|
||||
* entity, synthesize, forget). `gbrain serve --surface verbs` exposes
|
||||
* EXACTLY the ops with `verb: true`; `full` (default) exposes everything.
|
||||
*/
|
||||
verb?: boolean;
|
||||
/**
|
||||
* MCP ToolAnnotations passthrough (SDK 1.29+). Emitted by buildToolDefs
|
||||
* ONLY when set — existing tools keep byte-identical definitions.
|
||||
*/
|
||||
annotations?: {
|
||||
title?: string;
|
||||
readOnlyHint?: boolean;
|
||||
destructiveHint?: boolean;
|
||||
idempotentHint?: boolean;
|
||||
};
|
||||
cliHints?: {
|
||||
name?: string;
|
||||
/**
|
||||
@@ -4455,7 +4511,7 @@ const sources_status: Operation = {
|
||||
const extract_facts: Operation = {
|
||||
name: 'extract_facts',
|
||||
description:
|
||||
'v0.31: extract personal-knowledge facts (events, preferences, commitments, beliefs) from a conversation turn into the per-source hot memory. Sanitizes turn_text via INJECTION_PATTERNS, calls Haiku to extract structured claims, runs the cosine fast-path + classifier dedup pipeline, INSERTs into facts. Returns counts by status. Skips extraction when the turn is dream-generated content (anti-loop).',
|
||||
'v0.31: extract personal-knowledge facts (events, preferences, commitments, beliefs) from a conversation turn into the per-source hot memory. Sanitizes turn_text via INJECTION_PATTERNS, calls Haiku to extract structured claims, runs the cosine fast-path + classifier dedup pipeline, INSERTs into facts. Returns counts by status. Skips extraction when the turn is dream-generated content (anti-loop). For agent memory writes of a SINGLE already-formed fact, prefer the `remember` verb (zero LLM, mandatory provenance).',
|
||||
params: {
|
||||
turn_text: { type: 'string', required: true, description: 'The user message or page body to extract facts from. Sanitized via INJECTION_PATTERNS before the LLM call.' },
|
||||
session_id: { type: 'string', description: 'Opaque session id (e.g. topic-id from MCP _meta.session_id, or CLI --session). Stored on each fact for the recall --session filter. Not an auth surface.' },
|
||||
@@ -4511,18 +4567,22 @@ const extract_facts: Operation = {
|
||||
const recall: Operation = {
|
||||
name: 'recall',
|
||||
description:
|
||||
'v0.31: query per-source hot memory (facts table). Filters by entity / since / session. Remote callers see only visibility=world facts. Returns most-recent first. v0.32 adds optional include_pending to return pending_consolidation_count alongside facts in one round trip.',
|
||||
'MEMORY VERB (v1): retrieve saved facts/snippets — the protocol read verb. Filters hot-memory facts by entity / since / session_id; pass `query` to ALSO run hybrid search over pages (results[] arm); pass `budget_tokens` for server-side packing (response reports budget_used + dropped_count — never trims client-side). Remote callers see visibility=world facts only. Routing: for ONE known person/company/project card use `entity` (zero LLM); for broad questions needing reasoning use `synthesize` (expensive). Branch on structured fields (status/kind/evidence), never on prose. Every response carries protocol_version.',
|
||||
params: {
|
||||
entity: { type: 'string', description: 'Entity slug (canonical). Returns facts about this entity newest first.' },
|
||||
since: { type: 'string', description: 'ISO datetime or duration shorthand (e.g. "8 hours ago"). Returns facts created since.' },
|
||||
query: { type: 'string', description: 'MEMORY_VERBS v1: free-text retrieval over pages (hybrid search arm). Response adds results[] (slug, title, chunk, evidence, create_safety, provenance). Combinable with entity (both arms run). Degrades to keyword-only search when no embedding provider is configured (search_degraded notes it; never an error).' },
|
||||
budget_tokens: { type: 'number', description: 'MEMORY_VERBS v1: server-side token budget (char/4 estimate). Facts pack first, then results. Response adds budget_tokens, budget_used, dropped_count.' },
|
||||
since: { type: 'string', description: 'ISO 8601 datetime or duration shorthand (e.g. "8 hours ago"). Filters the FACTS arm only.' },
|
||||
session_id: { type: 'string', description: 'Source session id (e.g. topic-A). Returns facts captured in that session.' },
|
||||
include_expired: { type: 'boolean', description: 'When true, include expired_at IS NOT NULL rows. Default false.' },
|
||||
supersessions: { type: 'boolean', description: 'When true, return only the supersession audit log (expired_at + superseded_by both set).' },
|
||||
limit: { type: 'number', description: 'Max rows to return. Default 50, cap 100.' },
|
||||
limit: { type: 'number', description: 'Per-arm cap: max fact rows AND max search results. Default 50, cap 100.' },
|
||||
grep: { type: 'string', description: 'Substring filter on fact text (case-insensitive). Applied client-side after recall.' },
|
||||
include_pending: { type: 'boolean', description: 'v0.32: when true, response includes pending_consolidation_count (facts not yet promoted to takes by the dream-cycle consolidate phase). One round trip; backward-compatible (field omitted when false).' },
|
||||
},
|
||||
scope: 'read',
|
||||
verb: true,
|
||||
annotations: { title: 'recall (memory read)', readOnlyHint: true },
|
||||
handler: async (ctx, p) => {
|
||||
const sourceId = ctx.sourceId ?? 'default';
|
||||
const limit = typeof p.limit === 'number' ? p.limit : 50;
|
||||
@@ -4593,8 +4653,58 @@ const recall: Operation = {
|
||||
}
|
||||
}
|
||||
|
||||
// ── MEMORY_VERBS v1 — query arm (G1B superset). Hybrid search over pages
|
||||
// when `query` is present; degrades to keyword-only with a note (never an
|
||||
// error) when no embedding provider is configured [F-B].
|
||||
const queryText = typeof p.query === 'string' && p.query.trim().length > 0 ? p.query.trim() : null;
|
||||
const budgetTokens =
|
||||
typeof p.budget_tokens === 'number' && Number.isFinite(p.budget_tokens) && p.budget_tokens > 0
|
||||
? Math.floor(p.budget_tokens)
|
||||
: null;
|
||||
|
||||
let searchResults: SearchResult[] = [];
|
||||
let searchDegraded: string | undefined;
|
||||
if (queryText) {
|
||||
const searchScope = sourceScopeOpts(ctx);
|
||||
if (!isAvailable('embedding')) {
|
||||
const raw = await ctx.engine.searchKeyword(queryText, { limit, ...searchScope });
|
||||
searchResults = dedupResults(raw);
|
||||
stampEvidenceSafe(searchResults);
|
||||
await stampContentFlags(ctx.engine, searchResults);
|
||||
searchDegraded = 'keyword_only_no_embedding_provider';
|
||||
} else {
|
||||
searchResults = await hybridSearchCached(ctx.engine, queryText, {
|
||||
limit,
|
||||
expansion: false,
|
||||
...searchScope,
|
||||
});
|
||||
}
|
||||
bumpLastRetrievedAt(ctx.engine, searchResults.map(r => r.page_id));
|
||||
}
|
||||
|
||||
// ── MEMORY_VERBS v1 — server-side budget packing. Facts pack first (cheap,
|
||||
// high-precision one-liners, per-arm limit-capped so starvation is bounded),
|
||||
// then search results take the remainder. packToBudget treats budget<=0 as
|
||||
// a no-op, so an exhausted remainder must drop explicitly.
|
||||
let packedFacts = rows;
|
||||
let packedResults = searchResults;
|
||||
let budgetUsed: number | undefined;
|
||||
let droppedCount: number | undefined;
|
||||
if (budgetTokens !== null) {
|
||||
const factsPack = packToBudget(rows, r => estimateTokens(r.fact), budgetTokens);
|
||||
packedFacts = factsPack.items;
|
||||
const remaining = budgetTokens - factsPack.meta.used;
|
||||
const resultsPack =
|
||||
remaining > 0
|
||||
? packToBudget(searchResults, resultTokens, remaining)
|
||||
: { items: [] as SearchResult[], meta: { budget: 0, used: 0, dropped: searchResults.length, kept: 0 } };
|
||||
packedResults = resultsPack.items;
|
||||
budgetUsed = factsPack.meta.used + resultsPack.meta.used;
|
||||
droppedCount = factsPack.meta.dropped + resultsPack.meta.dropped;
|
||||
}
|
||||
|
||||
return {
|
||||
facts: rows.map(r => ({
|
||||
facts: packedFacts.map(r => ({
|
||||
id: r.id,
|
||||
fact: r.fact,
|
||||
kind: r.kind,
|
||||
@@ -4614,9 +4724,33 @@ const recall: Operation = {
|
||||
source_session: r.source_session,
|
||||
confidence: r.confidence,
|
||||
created_at: r.created_at.toISOString(),
|
||||
// MEMORY_VERBS v1 additive fields (G1B). `fact_id` is the opaque
|
||||
// STRING id the `forget` verb accepts (legacy numeric `id` stays for
|
||||
// pre-v1 consumers — legacy fields are frozen byte-equal). `provenance`
|
||||
// is the protocol name for the stored source attribution.
|
||||
fact_id: String(r.id),
|
||||
provenance: r.source,
|
||||
})),
|
||||
total: rows.length,
|
||||
total: packedFacts.length,
|
||||
...(pending_consolidation_count !== undefined ? { pending_consolidation_count } : {}),
|
||||
// MEMORY_VERBS v1 envelope (G1B superset — additive on every response).
|
||||
protocol_version: MEMORY_VERBS_VERSION,
|
||||
...(queryText
|
||||
? {
|
||||
results: packedResults.map(r => ({
|
||||
slug: r.slug,
|
||||
title: r.title,
|
||||
chunk: r.chunk_text,
|
||||
evidence: r.evidence,
|
||||
create_safety: r.create_safety,
|
||||
provenance: r.slug,
|
||||
})),
|
||||
...(searchDegraded ? { search_degraded: searchDegraded } : {}),
|
||||
}
|
||||
: {}),
|
||||
...(budgetTokens !== null
|
||||
? { budget_tokens: budgetTokens, budget_used: budgetUsed, dropped_count: droppedCount }
|
||||
: {}),
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -4677,6 +4811,65 @@ function parseSinceParam(raw: unknown): Date | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* MEMORY_VERBS v1 — parse the `remember` verb's `ttl` param into a
|
||||
* `valid_until` Date. Sibling of parseSinceParam, pointed FORWARD.
|
||||
*
|
||||
* Accepted forms (frozen in docs/protocol/MEMORY_VERBS_v1.md):
|
||||
* - relative duration shorthand: '30d', '12h', '45m', '90s' (also
|
||||
* spelled-out: '30 days', '12 hours') → now + duration
|
||||
* - absolute ISO 8601 date or datetime: '2026-07-12', '2026-07-12T00:00:00Z'
|
||||
*
|
||||
* Explicitly REJECTED with a self-correcting suggestion: ISO-8601 duration
|
||||
* syntax ('P30D', 'PT12H') — agents that read "ISO 8601" as durations get a
|
||||
* fix, not a mystery. Returns null for null/undefined/empty (= never expires).
|
||||
* Throws verbError('invalid_params') on anything unparseable.
|
||||
*/
|
||||
export function parseTtlParam(raw: unknown): Date | null {
|
||||
if (raw == null) return null;
|
||||
if (typeof raw !== 'string') {
|
||||
throw verbError(
|
||||
'invalid_params',
|
||||
`ttl must be a string, got ${typeof raw}.`,
|
||||
'Pass a duration like "30d" or "12h", or an absolute ISO 8601 timestamp like "2026-07-12T00:00:00Z".',
|
||||
);
|
||||
}
|
||||
const s = raw.trim();
|
||||
if (!s) return null;
|
||||
|
||||
// ISO-8601 DURATION syntax is a documented trap — reject with the fix.
|
||||
if (/^P(T|\d)/i.test(s) && /^P(?:\d+[YMWD])*(?:T(?:\d+[HMS])+)?$/i.test(s)) {
|
||||
throw verbError(
|
||||
'invalid_params',
|
||||
`ttl "${s}" looks like an ISO-8601 duration, which is not accepted.`,
|
||||
`Use the shorthand form instead (e.g. "${s.replace(/^PT?/i, '').toLowerCase()}" style: "30d", "12h"), or an absolute ISO 8601 expiry timestamp.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Relative duration shorthand → now + duration.
|
||||
const dur = s.match(/^(\d+)\s*(s|sec|seconds?|m|min|minutes?|h|hr|hours?|d|days?)$/i);
|
||||
if (dur) {
|
||||
const n = parseInt(dur[1], 10);
|
||||
const unit = dur[2].toLowerCase();
|
||||
const ms =
|
||||
unit.startsWith('s') ? n * 1000 :
|
||||
unit.startsWith('m') ? n * 60 * 1000 :
|
||||
unit.startsWith('h') ? n * 60 * 60 * 1000 :
|
||||
n * 24 * 60 * 60 * 1000;
|
||||
return new Date(Date.now() + ms);
|
||||
}
|
||||
|
||||
// Absolute ISO 8601 date or datetime.
|
||||
const iso = Date.parse(s);
|
||||
if (Number.isFinite(iso)) return new Date(iso);
|
||||
|
||||
throw verbError(
|
||||
'invalid_params',
|
||||
`Cannot parse ttl "${s}".`,
|
||||
'Pass a duration like "30d" or "12h", or an absolute ISO 8601 timestamp like "2026-07-12T00:00:00Z". Omit ttl for a fact that never expires.',
|
||||
);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// v0.34 Cathedral III — code-intelligence ops (MCP-exposed).
|
||||
//
|
||||
@@ -6157,6 +6350,10 @@ const extraction_review: Operation = {
|
||||
};
|
||||
|
||||
export const operations: Operation[] = [
|
||||
// MEMORY_VERBS v1 (Cathedral 1) — remember/entity/synthesize/forget live in
|
||||
// verbs.ts; the fifth verb is the extended `recall` op below. Spread first
|
||||
// so `--surface verbs` agents see them at the top of the tool list.
|
||||
...verbOperations,
|
||||
// Page CRUD
|
||||
get_page, put_page, delete_page, list_pages,
|
||||
// v0.26.5 destructive-guard ops (page-level soft-delete + recovery + admin purge)
|
||||
|
||||
@@ -340,3 +340,64 @@ export function intentToDetail(intent: QueryIntent): 'low' | 'medium' | 'high' |
|
||||
export function autoDetectDetail(query: string): 'low' | 'medium' | 'high' | undefined {
|
||||
return classifyQuery(query).suggestedDetail;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────
|
||||
// #2416 — concept-shaped query detection (CLI nudge)
|
||||
// ─────────────────────────────────────────────────────────
|
||||
|
||||
// Fuzzy-quantifier / landscape cues. A concept-shaped question asks for a
|
||||
// SET defined by meaning ("all the X that do Y", "the landscape of Z") —
|
||||
// exactly where `query`'s multi-query expansion recovers synonym- and
|
||||
// outcome-phrased matches that the expansion-off `search` op can miss.
|
||||
//
|
||||
// Deliberately EXCLUDED cues (owned by other routers — a nudge toward
|
||||
// `query` on these would fight their descriptions):
|
||||
// - "who are the …" → find_experts
|
||||
// - bare "anything …" → get_recent_salience / find_anomalies
|
||||
const CONCEPT_CUE_PATTERNS: RegExp[] = [
|
||||
/\b(all|every)\b.+\b(that|who|which|doing|with|about|related to)\b/i,
|
||||
/\b(find|list|show)\s+(all|every|everything)\b/i,
|
||||
/\beverything\s+(about|on|matching|related)\b/i,
|
||||
/\bthe\s+(landscape|ecosystem|space|universe)\s+of\b/i,
|
||||
/\b(landscape|ecosystem)\s+(of|around)\b/i,
|
||||
/\bwhich\s+\w+[\w\s]*\b(do|does|are|have|use|work)\b/i,
|
||||
];
|
||||
|
||||
// Exact-identifier anti-signals: the query names a specific thing, so the
|
||||
// cheap `search` op is the right tool and a nudge would be noise.
|
||||
const CONCEPT_ANTI_PATTERNS: RegExp[] = [
|
||||
/["'“”][^"'“”]+["'“”]/, // quoted phrase — exact-match intent
|
||||
/\b[a-z0-9]+(?:-[a-z0-9]+){1,}\b/, // slug-like token (kebab-case)
|
||||
];
|
||||
|
||||
/**
|
||||
* True when a query is concept-shaped: it carries a fuzzy-quantifier or
|
||||
* landscape cue AND no exact-identifier anti-signal. Tuned to favor
|
||||
* false-negatives (silence) over false-positives (noise): short queries,
|
||||
* quoted phrases, slugs, and entity lookups (per classifyQueryIntent)
|
||||
* never trigger. Pure function; no LLM, no DB.
|
||||
*/
|
||||
export function looksConceptShaped(query: string): boolean {
|
||||
const q = query.trim();
|
||||
if (q.split(/\s+/).length < 3) return false; // bare token / proper-noun lookup
|
||||
if (!matches(CONCEPT_CUE_PATTERNS, q)) return false;
|
||||
if (matches(CONCEPT_ANTI_PATTERNS, q)) return false;
|
||||
if (classifyQueryIntent(q) === 'entity') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* One-line CLI hint steering a concept-shaped `search` toward `query`.
|
||||
* Returns null when the query is not concept-shaped. Message generation
|
||||
* lives here (not in cli.ts) so the full string is unit-testable; the CLI
|
||||
* wiring is a two-liner per dispatch path, stderr only, `--quiet`-gated.
|
||||
*/
|
||||
export function conceptNudge(query: string): string | null {
|
||||
if (!looksConceptShaped(query)) return null;
|
||||
const preview = query.length > 60 ? `${query.slice(0, 57)}...` : query;
|
||||
return (
|
||||
`hint: concept-shaped question — try \`gbrain query "${preview}"\` ` +
|
||||
`(adds multi-query expansion; recovers synonym-phrased matches search can miss). ` +
|
||||
`A nonzero search count is not proof of completeness.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,50 +63,66 @@ export interface TokenBudgetMeta {
|
||||
}
|
||||
|
||||
/**
|
||||
* Greedy top-down budget enforcement. Walks the input in order, accumulates
|
||||
* token costs, and stops as soon as adding the next result would exceed
|
||||
* the budget. Results are NOT re-ranked — caller's order is preserved.
|
||||
* Generic greedy top-down budget packer (v1 memory-verbs protocol). Walks
|
||||
* the input in order, accumulates per-item costs via the caller-supplied
|
||||
* cost function, and stops as soon as adding the next item would exceed
|
||||
* the budget. Items are NOT re-ranked — caller's order is preserved.
|
||||
*
|
||||
* Edge cases (all preserve the pre-v0.32 contract):
|
||||
* - budget undefined / <= 0: returns input unchanged; dropped=0, kept=N.
|
||||
* - First result alone exceeds budget: returns []; dropped=N, kept=0.
|
||||
* - First item alone exceeds budget: returns []; dropped=N, kept=0.
|
||||
* (Intentionally strict: the caller asked for a hard cap.)
|
||||
* - Input empty: returns []; budget unused.
|
||||
*/
|
||||
export function packToBudget<T>(
|
||||
items: T[],
|
||||
cost: (item: T) => number,
|
||||
budget: number | undefined,
|
||||
): { items: T[]; meta: TokenBudgetMeta } {
|
||||
const safeBudget = typeof budget === 'number' && budget > 0 ? budget : 0;
|
||||
|
||||
if (safeBudget === 0 || items.length === 0) {
|
||||
return {
|
||||
items,
|
||||
meta: {
|
||||
budget: safeBudget,
|
||||
used: items.reduce((acc, it) => acc + cost(it), 0),
|
||||
dropped: 0,
|
||||
kept: items.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const kept: T[] = [];
|
||||
let used = 0;
|
||||
for (const it of items) {
|
||||
const c = cost(it);
|
||||
if (used + c > safeBudget) break;
|
||||
kept.push(it);
|
||||
used += c;
|
||||
}
|
||||
|
||||
return {
|
||||
items: kept,
|
||||
meta: {
|
||||
budget: safeBudget,
|
||||
used,
|
||||
dropped: items.length - kept.length,
|
||||
kept: kept.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search-pipeline budget enforcement — a thin wrapper over packToBudget
|
||||
* with the SearchResult cost model (title + chunk_text). Behavior is
|
||||
* byte-identical to the pre-refactor implementation; pinned by
|
||||
* test/token-budget.test.ts.
|
||||
*/
|
||||
export function enforceTokenBudget(
|
||||
results: SearchResult[],
|
||||
budget: number | undefined,
|
||||
): { results: SearchResult[]; meta: TokenBudgetMeta } {
|
||||
const safeBudget = typeof budget === 'number' && budget > 0 ? budget : 0;
|
||||
|
||||
if (safeBudget === 0 || results.length === 0) {
|
||||
return {
|
||||
results,
|
||||
meta: {
|
||||
budget: safeBudget,
|
||||
used: results.reduce((acc, r) => acc + resultTokens(r), 0),
|
||||
dropped: 0,
|
||||
kept: results.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const kept: SearchResult[] = [];
|
||||
let used = 0;
|
||||
for (const r of results) {
|
||||
const cost = resultTokens(r);
|
||||
if (used + cost > safeBudget) break;
|
||||
kept.push(r);
|
||||
used += cost;
|
||||
}
|
||||
|
||||
return {
|
||||
results: kept,
|
||||
meta: {
|
||||
budget: safeBudget,
|
||||
used,
|
||||
dropped: results.length - kept.length,
|
||||
kept: kept.length,
|
||||
},
|
||||
};
|
||||
const { items, meta } = packToBudget(results, resultTokens, budget);
|
||||
return { results: items, meta };
|
||||
}
|
||||
|
||||
+23
-12
@@ -140,6 +140,13 @@ export interface ThinkResult {
|
||||
* pre-existing/test `ThinkResult` literals → treated as persistable (back-compat).
|
||||
*/
|
||||
synthesisOk?: boolean;
|
||||
/**
|
||||
* MEMORY_VERBS v1 [E2] — gateway token usage for the synthesis call(s),
|
||||
* summed across rounds. Best-effort: null when no LLM ran (graceful stub),
|
||||
* when a test client returns no usage, or when a provider omits accounting.
|
||||
* The synthesize verb maps this to its frozen `cost` block.
|
||||
*/
|
||||
usage?: { input_tokens: number; output_tokens: number } | null;
|
||||
/** Only set when --save was true and the caller persisted a synthesis page. */
|
||||
savedSlug?: string;
|
||||
/** Diagnostics for `--explain` callers (CLI surface for v0.29). */
|
||||
@@ -149,15 +156,6 @@ export interface ThinkResult {
|
||||
takesFromVector: number;
|
||||
graphHits: number;
|
||||
};
|
||||
/**
|
||||
* Token usage from the real LLM call, when one happened. Undefined on the
|
||||
* no-client/stub paths (no Anthropic key, model not usable) — same
|
||||
* distinction `synthesisOk` already makes. `think`'s cost was previously
|
||||
* unsurfaced anywhere: not in this CLI's own output, not in
|
||||
* `budget_ledger`, and invisible to a wrapping caller's own token
|
||||
* accounting (the LLM call `think` makes is its own separate API call).
|
||||
*/
|
||||
usage?: { input_tokens: number; output_tokens: number };
|
||||
/** USD cost computed from `usage` + `canonicalLookup(modelUsed)`, when both are available. */
|
||||
cost_usd?: number;
|
||||
}
|
||||
@@ -462,8 +460,10 @@ export async function runThink(
|
||||
// sentinel, which is non-JSON) and on the no-client early return below; the final
|
||||
// return ANDs it with a non-empty-answer check (catches valid-but-empty JSON).
|
||||
let synthesisOk = true;
|
||||
// [E2] best-effort usage aggregation across synthesis calls (single-pass in
|
||||
// v0.28+, but summed so the round loop inherits it when gap-fill lands).
|
||||
let usage: { input_tokens: number; output_tokens: number } | null = null;
|
||||
let response: ThinkResponse;
|
||||
let usage: { input_tokens: number; output_tokens: number } | undefined;
|
||||
if (opts.stubResponse) {
|
||||
response = opts.stubResponse;
|
||||
} else {
|
||||
@@ -513,6 +513,7 @@ export async function runThink(
|
||||
rounds: 0,
|
||||
warnings,
|
||||
synthesisOk: false, // #1698: no LLM ran — never persist this
|
||||
usage: null, // [E2] no LLM ran — no accounting
|
||||
diagnostics: {
|
||||
pagesFromHybrid: gather.diagnostics.pagesFromHybrid,
|
||||
takesFromKeyword: gather.diagnostics.takesFromKeyword,
|
||||
@@ -527,7 +528,17 @@ export async function runThink(
|
||||
system: systemPrompt,
|
||||
messages: [{ role: 'user', content: userMessage }],
|
||||
});
|
||||
usage = { input_tokens: result.usage.input_tokens, output_tokens: result.usage.output_tokens };
|
||||
// [E2] capture usage when the message carries it (test-injected clients
|
||||
// and providers without accounting leave it null).
|
||||
const u = (result as { usage?: { input_tokens?: number; output_tokens?: number } }).usage;
|
||||
if (u && typeof u.input_tokens === 'number' && typeof u.output_tokens === 'number') {
|
||||
// Single synthesis call in v0.28+; when gap-driven rounds land, sum here.
|
||||
const prev = usage as { input_tokens: number; output_tokens: number } | null;
|
||||
usage = {
|
||||
input_tokens: (prev?.input_tokens ?? 0) + u.input_tokens,
|
||||
output_tokens: (prev?.output_tokens ?? 0) + u.output_tokens,
|
||||
};
|
||||
}
|
||||
const block = result.content.find(b => b.type === 'text');
|
||||
const text = block && 'text' in block ? block.text : '';
|
||||
const parsed = tryParseJSON(text);
|
||||
@@ -572,13 +583,13 @@ export async function runThink(
|
||||
// #1698: persistable only when a real synthesis produced a non-empty answer.
|
||||
// ANDs the not-JSON/sentinel flag with a content check (catches valid-but-empty JSON).
|
||||
synthesisOk: synthesisOk && response.answer.trim().length > 0,
|
||||
usage,
|
||||
diagnostics: {
|
||||
pagesFromHybrid: gather.diagnostics.pagesFromHybrid,
|
||||
takesFromKeyword: gather.diagnostics.takesFromKeyword,
|
||||
takesFromVector: gather.diagnostics.takesFromVector,
|
||||
graphHits: gather.diagnostics.graphHits,
|
||||
},
|
||||
usage,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,559 @@
|
||||
/**
|
||||
* MEMORY_VERBS v1 — the frozen memory protocol verbs (Cathedral 1).
|
||||
*
|
||||
* Four first-class Operations (`remember`, `entity`, `synthesize`, `forget`)
|
||||
* that join the extended `recall` op (operations.ts) as the five-verb façade
|
||||
* over the operation catalog. Frozen contract: docs/protocol/MEMORY_VERBS_v1.md
|
||||
* — field names and semantics in v1 never change; additions are forever-
|
||||
* additive; `protocol_version` rides every response; errors carry enumerated
|
||||
* codes + populated `suggestion` (agents read it and self-correct).
|
||||
*
|
||||
* These are ordinary Operations: they inherit trust-boundary fail-closed
|
||||
* semantics (ctx.remote), scope enforcement, and source isolation like every
|
||||
* other op. `gbrain serve --surface verbs` exposes exactly the ops marked
|
||||
* `verb: true`.
|
||||
*
|
||||
* Import-cycle note: operations.ts spreads these into its `operations` array
|
||||
* at MODULE-EVAL time, so this file must be a RUNTIME LEAF — it may import
|
||||
* operations.ts types (erased) but never its values statically. Handlers load
|
||||
* verbError/parseTtlParam/sourceScopeOpts via dynamic import (the file's
|
||||
* existing style), which resolves after both modules finish evaluating.
|
||||
* MEMORY_VERBS_VERSION lives HERE (operations.ts imports it from us) for the
|
||||
* same reason. Violating this reintroduces the TDZ crash on whichever module
|
||||
* evaluates second.
|
||||
*/
|
||||
|
||||
import type { Operation } from './operations.ts';
|
||||
|
||||
/** Frozen protocol version for the MEMORY_VERBS v1 verb set. Single source of truth. */
|
||||
export const MEMORY_VERBS_VERSION = 1;
|
||||
|
||||
export const VERB_NAMES = ['recall', 'remember', 'entity', 'synthesize', 'forget'] as const;
|
||||
export type VerbName = (typeof VERB_NAMES)[number];
|
||||
|
||||
const FACT_KINDS = ['event', 'preference', 'commitment', 'belief', 'fact'] as const;
|
||||
const PROVENANCE_MAX = 500;
|
||||
|
||||
// ─── remember ────────────────────────────────────────────────────────────────
|
||||
|
||||
const remember: Operation = {
|
||||
name: 'remember',
|
||||
description:
|
||||
'MEMORY VERB (v1): save one fact to durable agent memory — the protocol write verb. ' +
|
||||
'provenance is REQUIRED (free text, e.g. "conversation 2026-06-12", "user said in chat", "import: notes.md"). ' +
|
||||
'Set `entity` whenever the fact is about a specific person/company/project — entity-scoped recall will not find it otherwise. ' +
|
||||
'ttl accepts duration shorthand ("30d", "12h") or an absolute ISO 8601 timestamp; ISO-8601 durations like "P30D" are rejected with a fix. ' +
|
||||
'visibility defaults to "world" (readable by every agent connected to this brain; pass "private" for local-CLI-only facts). ' +
|
||||
'Response: branch on `status` (inserted|duplicate|superseded), never on `status_text` (human rendering only). ' +
|
||||
'On duplicate, `id` is the EXISTING fact\'s id. For bulk extraction from a raw transcript use extract_facts instead.',
|
||||
params: {
|
||||
fact: { type: 'string', required: true, description: 'The fact to remember, one claim per call.' },
|
||||
provenance: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description:
|
||||
'Where this fact came from (REQUIRED, free text, max 500 chars). Examples: "conversation 2026-06-12", "user said in chat", "import: meeting-notes.md".',
|
||||
},
|
||||
ttl: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Optional expiry: duration shorthand ("30d", "12h", "45m") or absolute ISO 8601 timestamp ("2026-07-12T00:00:00Z"). NOT ISO-8601 durations ("P30D" is rejected). Omit = never expires.',
|
||||
},
|
||||
entity: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Person/company/project this fact is about (name or slug; canonicalized server-side). Set it whenever the fact has a subject — entity-scoped recall misses unattributed facts.',
|
||||
},
|
||||
kind: {
|
||||
type: 'string',
|
||||
enum: [...FACT_KINDS],
|
||||
description: 'Fact kind: event | preference | commitment | belief | fact (default).',
|
||||
},
|
||||
visibility: {
|
||||
type: 'string',
|
||||
enum: ['world', 'private'],
|
||||
description:
|
||||
'world (default): readable by every agent connected to this brain — required for the remote remember→recall round-trip. private: local CLI reads only.',
|
||||
},
|
||||
},
|
||||
mutating: true,
|
||||
scope: 'write',
|
||||
verb: true,
|
||||
annotations: { title: 'remember (memory write)', idempotentHint: true },
|
||||
handler: async (ctx, p) => {
|
||||
const { verbError, parseTtlParam } = await import('./operations.ts');
|
||||
const fact = typeof p.fact === 'string' ? p.fact.trim() : '';
|
||||
if (!fact) {
|
||||
throw verbError(
|
||||
'invalid_params',
|
||||
'fact must be a non-empty string.',
|
||||
'Pass the claim to remember, e.g. fact: "picked Stripe over Adyen — onboarding speed".',
|
||||
);
|
||||
}
|
||||
const provenance = typeof p.provenance === 'string' ? p.provenance.trim() : '';
|
||||
if (!provenance) {
|
||||
throw verbError(
|
||||
'provenance_required',
|
||||
'provenance is required and must be non-empty.',
|
||||
'Pass where the fact came from, e.g. provenance: "user told me, 2026-06-12" or "import: notes.md".',
|
||||
);
|
||||
}
|
||||
if (provenance.length > PROVENANCE_MAX) {
|
||||
throw verbError(
|
||||
'invalid_params',
|
||||
`provenance exceeds ${PROVENANCE_MAX} chars (got ${provenance.length}).`,
|
||||
'Shorten the attribution — provenance is a pointer, not a transcript.',
|
||||
);
|
||||
}
|
||||
const kind = typeof p.kind === 'string' ? p.kind : 'fact';
|
||||
if (!FACT_KINDS.includes(kind as (typeof FACT_KINDS)[number])) {
|
||||
throw verbError(
|
||||
'invalid_params',
|
||||
`kind "${kind}" is not a fact kind.`,
|
||||
`Use one of: ${FACT_KINDS.join(' | ')}.`,
|
||||
);
|
||||
}
|
||||
const visibility = typeof p.visibility === 'string' ? p.visibility : 'world';
|
||||
if (visibility !== 'world' && visibility !== 'private') {
|
||||
throw verbError(
|
||||
'invalid_params',
|
||||
`visibility "${visibility}" is not valid.`,
|
||||
'Use "world" (default — agents can recall it) or "private" (local CLI reads only).',
|
||||
);
|
||||
}
|
||||
const validUntil = parseTtlParam(p.ttl); // throws verbError(invalid_params) on bad input
|
||||
|
||||
if (ctx.dryRun) {
|
||||
return {
|
||||
dry_run: true,
|
||||
action: 'remember',
|
||||
fact,
|
||||
protocol_version: MEMORY_VERBS_VERSION,
|
||||
};
|
||||
}
|
||||
|
||||
const { writeSingleFact } = await import('./facts/write-single.ts');
|
||||
const result = await writeSingleFact(ctx.engine, ctx.sourceId ?? 'default', {
|
||||
fact,
|
||||
provenance,
|
||||
kind: kind as (typeof FACT_KINDS)[number],
|
||||
entity: typeof p.entity === 'string' && p.entity.trim() ? p.entity.trim() : null,
|
||||
visibility,
|
||||
validUntil,
|
||||
});
|
||||
|
||||
const statusText =
|
||||
result.status === 'inserted'
|
||||
? `remembered as fact #${result.id}`
|
||||
: result.status === 'duplicate'
|
||||
? `already knew this — kept fact #${result.id}`
|
||||
: `updated — fact #${result.id} supersedes the previous version`;
|
||||
|
||||
return {
|
||||
// Opaque STRING at the protocol level [T4]; gbrain serializes its ints.
|
||||
id: String(result.id),
|
||||
status: result.status,
|
||||
status_text: statusText,
|
||||
entity_slug: result.entity_slug ?? null,
|
||||
valid_until: result.valid_until ? result.valid_until.toISOString() : null,
|
||||
...(result.degraded_dedup ? { degraded_dedup: true } : {}),
|
||||
protocol_version: MEMORY_VERBS_VERSION,
|
||||
};
|
||||
},
|
||||
cliHints: { name: 'remember', positional: ['fact'] },
|
||||
};
|
||||
|
||||
// ─── entity ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const entity: Operation = {
|
||||
name: 'entity',
|
||||
description:
|
||||
'MEMORY VERB (v1): inspect ONE known person/company/project card — zero LLM calls, sub-100ms. ' +
|
||||
'Resolution: alias > exact title > slug-suffix; ties break on most-recently-touched. ' +
|
||||
'NEVER errors on a miss: returns found:false plus near-miss suggestions with create_safety hints ' +
|
||||
'(exists | probable | unknown — whether writing a new page would duplicate). ' +
|
||||
'Routing: for facts/snippets retrieval use recall; for broad questions needing reasoning use synthesize (expensive).',
|
||||
params: {
|
||||
name: { type: 'string', required: true, description: 'Free-text name, alias, or slug (e.g. "Alice Example", "people/alice-example").' },
|
||||
},
|
||||
scope: 'read',
|
||||
verb: true,
|
||||
annotations: { title: 'entity (card lookup, zero LLM)', readOnlyHint: true },
|
||||
handler: async (ctx, p) => {
|
||||
const { verbError } = await import('./operations.ts');
|
||||
const name = typeof p.name === 'string' ? p.name.trim() : '';
|
||||
if (!name) {
|
||||
throw verbError(
|
||||
'invalid_params',
|
||||
'name must be a non-empty string.',
|
||||
'Pass the entity to look up, e.g. name: "Alice Example" or name: "people/alice-example".',
|
||||
);
|
||||
}
|
||||
const t0 = Date.now();
|
||||
const { buildEntityCard } = await import('./verbs/entity-card.ts');
|
||||
const result = await buildEntityCard(ctx.engine, ctx.sourceId ?? 'default', name, {
|
||||
remote: ctx.remote !== false,
|
||||
});
|
||||
return {
|
||||
protocol_version: MEMORY_VERBS_VERSION,
|
||||
found: result.found,
|
||||
latency_ms: Date.now() - t0,
|
||||
...(result.card ? { card: result.card } : {}),
|
||||
...(result.suggestions !== undefined ? { suggestions: result.suggestions } : {}),
|
||||
};
|
||||
},
|
||||
cliHints: { name: 'entity', positional: ['name'] },
|
||||
};
|
||||
|
||||
// ─── synthesize ──────────────────────────────────────────────────────────────
|
||||
|
||||
const synthesize: Operation = {
|
||||
name: 'synthesize',
|
||||
description:
|
||||
'[EXPENSIVE / SLOW — makes LLM calls, seconds-to-minutes latency, costs money] ' +
|
||||
'MEMORY VERB (v1): answer a broad question using cross-page LLM reasoning with citations and gap analysis. ' +
|
||||
'Prefer recall (facts/snippets) or entity (one known card, zero LLM) for lookups — use synthesize only when the answer ' +
|
||||
'requires combining evidence across pages. Response carries a best-effort cost block (model, tokens, usd_estimate).',
|
||||
params: {
|
||||
question: { type: 'string', required: true, description: 'The question to answer.' },
|
||||
since: { type: 'string', description: 'Optional temporal window start (ISO 8601 date or datetime).' },
|
||||
until: { type: 'string', description: 'Optional temporal window end (ISO 8601 date or datetime).' },
|
||||
},
|
||||
scope: 'read',
|
||||
verb: true,
|
||||
annotations: { title: 'synthesize (slow, costly — LLM-backed)', readOnlyHint: true },
|
||||
handler: async (ctx, p) => {
|
||||
const { verbError, sourceScopeOpts } = await import('./operations.ts');
|
||||
const question = typeof p.question === 'string' ? p.question.trim() : '';
|
||||
if (!question) {
|
||||
throw verbError(
|
||||
'invalid_params',
|
||||
'question must be a non-empty string.',
|
||||
'Pass the question to synthesize an answer for, e.g. question: "what is our payments strategy?".',
|
||||
);
|
||||
}
|
||||
const scope = sourceScopeOpts(ctx);
|
||||
const { runThink } = await import('./think/index.ts');
|
||||
// Remote-safe delegation: save/take are NEVER offered through this verb,
|
||||
// for any caller — the verb is a pure read.
|
||||
const result = await runThink(ctx.engine, {
|
||||
question,
|
||||
since: p.since ? String(p.since) : undefined,
|
||||
until: p.until ? String(p.until) : undefined,
|
||||
takesHoldersAllowList: ctx.takesHoldersAllowList,
|
||||
...(scope.sourceId !== undefined ? { sourceId: scope.sourceId } : {}),
|
||||
...(scope.sourceIds !== undefined ? { allowedSources: scope.sourceIds } : {}),
|
||||
remote: ctx.remote === true,
|
||||
});
|
||||
|
||||
// [c10] runThink degrades gracefully to a no-LLM stub RESULT; the protocol
|
||||
// contract converts that state into an explicit `unavailable` error so
|
||||
// agents branch on configure/retry instead of relaying a fake answer.
|
||||
if (result.warnings.includes('NO_ANTHROPIC_API_KEY')) {
|
||||
throw verbError(
|
||||
'unavailable',
|
||||
'synthesize needs an LLM and none is configured.',
|
||||
'Set an API key (e.g. `gbrain config set anthropic_api_key sk-...` or ANTHROPIC_API_KEY) and retry. recall and entity work without one.',
|
||||
'chat gateway unconfigured (NO_ANTHROPIC_API_KEY)',
|
||||
);
|
||||
}
|
||||
|
||||
// Best-effort cost block [E5/m3]: actual tokens when the gateway reported
|
||||
// usage, priced via the canonical table; nulls when accounting is absent.
|
||||
const { canonicalLookup } = await import('./model-pricing.ts');
|
||||
const usage = result.usage ?? null;
|
||||
const pricing = canonicalLookup(result.modelUsed);
|
||||
const usdEstimate =
|
||||
usage && pricing
|
||||
? (usage.input_tokens * pricing.input + usage.output_tokens * pricing.output) / 1_000_000
|
||||
: null;
|
||||
|
||||
return {
|
||||
answer: result.answer,
|
||||
sources: result.citations.map(c => c.page_slug),
|
||||
gaps: result.gaps,
|
||||
cost: {
|
||||
model: result.modelUsed,
|
||||
input_tokens: usage?.input_tokens ?? null,
|
||||
output_tokens: usage?.output_tokens ?? null,
|
||||
usd_estimate: usdEstimate,
|
||||
},
|
||||
protocol_version: MEMORY_VERBS_VERSION,
|
||||
};
|
||||
},
|
||||
cliHints: { name: 'synthesize', positional: ['question'] },
|
||||
};
|
||||
|
||||
// ─── forget ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const forget: Operation = {
|
||||
name: 'forget',
|
||||
description:
|
||||
'MEMORY VERB (v1): expire a remembered fact by id — the protocol delete verb. ' +
|
||||
'`id` is the opaque string id returned by remember and recall (facts[].fact_id) — never a page slug. ' +
|
||||
'Idempotent: forgetting an already-expired fact returns expired:false (success), unknown id returns a not_found error. ' +
|
||||
'The fact is expired (audit trail kept), not deleted.',
|
||||
params: {
|
||||
id: { type: 'string', required: true, description: 'Opaque fact id from remember/recall (facts[].fact_id). Never a page slug.' },
|
||||
reason: { type: 'string', description: 'Optional reason, written to the fact\'s audit trail. Default: "forgotten".' },
|
||||
},
|
||||
mutating: true,
|
||||
scope: 'write',
|
||||
verb: true,
|
||||
annotations: { title: 'forget (expire a fact)', destructiveHint: true, idempotentHint: true },
|
||||
handler: async (ctx, p) => {
|
||||
const { verbError } = await import('./operations.ts');
|
||||
const rawId = typeof p.id === 'string' ? p.id.trim() : typeof p.id === 'number' ? String(p.id) : '';
|
||||
const numericId = Number(rawId);
|
||||
if (!rawId || !Number.isInteger(numericId) || numericId <= 0) {
|
||||
throw verbError(
|
||||
'not_found',
|
||||
`No fact with id "${String(p.id)}".`,
|
||||
'Pass the opaque string id returned by remember or recall (facts[].fact_id) — page slugs are not fact ids.',
|
||||
);
|
||||
}
|
||||
const reason = typeof p.reason === 'string' && p.reason.trim() ? p.reason.trim() : null;
|
||||
|
||||
if (ctx.dryRun) {
|
||||
return { dry_run: true, action: 'forget', id: rawId, protocol_version: MEMORY_VERBS_VERSION };
|
||||
}
|
||||
|
||||
const { forgetFactInFence } = await import('./facts/forget.ts');
|
||||
// [ship P1.1] trust boundary: scope the forget to the caller's source, and
|
||||
// for remote callers to world-visible facts only — a guessed global id
|
||||
// can't expire facts outside the caller's source or reach private facts.
|
||||
const result = await forgetFactInFence(ctx.engine, numericId, {
|
||||
...(reason ? { reason } : {}),
|
||||
sourceId: ctx.sourceId ?? 'default',
|
||||
worldOnly: ctx.remote !== false,
|
||||
});
|
||||
|
||||
if (!result.ok && result.path === 'not_found') {
|
||||
throw verbError(
|
||||
'not_found',
|
||||
`No fact with id "${rawId}".`,
|
||||
'Ids come from remember/recall (facts[].fact_id). recall the entity first to find the right fact.',
|
||||
);
|
||||
}
|
||||
if (!result.ok && result.path === 'already_expired') {
|
||||
// Idempotent re-forget: success, nothing changed.
|
||||
return {
|
||||
id: rawId,
|
||||
expired: false,
|
||||
reason,
|
||||
protocol_version: MEMORY_VERBS_VERSION,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: rawId,
|
||||
expired: true,
|
||||
reason,
|
||||
protocol_version: MEMORY_VERBS_VERSION,
|
||||
};
|
||||
},
|
||||
// NO cliHints: `gbrain forget` is a CLI_ONLY command (recall.ts runForget)
|
||||
// that dispatches BEFORE cliOps — a cliHint here would be silently
|
||||
// shadowed. The CLI surface for forgetting stays the existing command;
|
||||
// this verb is the MCP/protocol surface.
|
||||
};
|
||||
|
||||
export const verbOperations: Operation[] = [remember, entity, synthesize, forget];
|
||||
|
||||
// ─── RESPONSE_SCHEMAS — the protocol's response-shape registry [c8] ─────────
|
||||
//
|
||||
// `Operation` carries input params only; response envelopes live HERE, hand-
|
||||
// authored, and conformance validates LIVE responses against this registry so
|
||||
// registry-vs-code drift is caught by the same fixtures that certify servers.
|
||||
// Field names and semantics are FROZEN (additive-forever); enum values are
|
||||
// part of the contract.
|
||||
|
||||
const EVIDENCE_ENUM = ['alias_hit', 'exact_title_match', 'high_vector_match', 'keyword_exact', 'weak_semantic'];
|
||||
const CREATE_SAFETY_ENUM = ['exists', 'probable', 'unknown'];
|
||||
const STATUS_ENUM = ['inserted', 'duplicate', 'superseded'];
|
||||
|
||||
export const RESPONSE_SCHEMAS: Record<VerbName, Record<string, unknown>> = {
|
||||
recall: {
|
||||
type: 'object',
|
||||
required: ['facts', 'total', 'protocol_version'],
|
||||
properties: {
|
||||
protocol_version: { type: 'integer', const: MEMORY_VERBS_VERSION },
|
||||
total: { type: 'integer' },
|
||||
facts: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['id', 'fact', 'kind', 'fact_id', 'provenance'],
|
||||
properties: {
|
||||
id: { type: 'integer', description: 'LEGACY numeric id (pre-v1 consumers). Use fact_id.' },
|
||||
fact_id: { type: 'string', description: 'Opaque protocol id — the value forget accepts.' },
|
||||
fact: { type: 'string' },
|
||||
kind: { type: 'string', enum: FACT_KINDS as unknown as string[] },
|
||||
entity_slug: { type: ['string', 'null'] },
|
||||
provenance: { type: 'string' },
|
||||
valid_until: { type: ['string', 'null'] },
|
||||
visibility: { type: 'string', enum: ['private', 'world'] },
|
||||
},
|
||||
},
|
||||
},
|
||||
results: {
|
||||
type: 'array',
|
||||
description: 'Search arm — present only when `query` was passed.',
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['slug', 'title', 'evidence', 'create_safety', 'provenance'],
|
||||
properties: {
|
||||
slug: { type: 'string' },
|
||||
title: { type: ['string', 'null'] },
|
||||
chunk: { type: ['string', 'null'] },
|
||||
evidence: { type: 'string', enum: EVIDENCE_ENUM },
|
||||
create_safety: { type: 'string', enum: CREATE_SAFETY_ENUM },
|
||||
provenance: { type: 'string', description: 'Origin page slug.' },
|
||||
},
|
||||
},
|
||||
},
|
||||
search_degraded: { type: 'string', description: 'Present when the search arm fell back to keyword-only (no embedding provider).' },
|
||||
budget_tokens: { type: 'integer', description: 'Present when budget_tokens was passed.' },
|
||||
budget_used: { type: 'integer' },
|
||||
dropped_count: { type: 'integer' },
|
||||
},
|
||||
},
|
||||
remember: {
|
||||
type: 'object',
|
||||
required: ['id', 'status', 'status_text', 'entity_slug', 'valid_until', 'protocol_version'],
|
||||
properties: {
|
||||
protocol_version: { type: 'integer', const: MEMORY_VERBS_VERSION },
|
||||
id: { type: 'string', description: 'Opaque fact id. On status=duplicate this is the EXISTING fact\'s id.' },
|
||||
status: { type: 'string', enum: STATUS_ENUM, description: 'Branch on THIS, never on status_text.' },
|
||||
status_text: { type: 'string', description: 'Human rendering of status. Display only — never branch on it.' },
|
||||
entity_slug: { type: ['string', 'null'] },
|
||||
valid_until: { type: ['string', 'null'], description: 'ISO 8601 or null (never expires).' },
|
||||
degraded_dedup: { type: 'boolean', description: 'Present (true) when no embedding provider — near-duplicates may insert.' },
|
||||
},
|
||||
},
|
||||
entity: {
|
||||
type: 'object',
|
||||
required: ['protocol_version', 'found', 'latency_ms'],
|
||||
properties: {
|
||||
protocol_version: { type: 'integer', const: MEMORY_VERBS_VERSION },
|
||||
found: { type: 'boolean' },
|
||||
latency_ms: { type: 'integer' },
|
||||
card: {
|
||||
type: 'object',
|
||||
required: ['entity', 'aka', 'summary', 'last_touched', 'open_threads', 'edges', 'backlink_count', 'active_fact_count'],
|
||||
properties: {
|
||||
entity: {
|
||||
type: 'object',
|
||||
required: ['slug', 'title', 'type'],
|
||||
properties: { slug: { type: 'string' }, title: { type: 'string' }, type: { type: ['string', 'null'] } },
|
||||
},
|
||||
aka: { type: 'array', items: { type: 'string' } },
|
||||
summary: { type: 'string' },
|
||||
last_touched: {
|
||||
type: 'object',
|
||||
required: ['updated_at', 'last_retrieved_at', 'last_timeline_date'],
|
||||
properties: {
|
||||
updated_at: { type: ['string', 'null'] },
|
||||
last_retrieved_at: { type: ['string', 'null'] },
|
||||
last_timeline_date: { type: ['string', 'null'] },
|
||||
},
|
||||
},
|
||||
open_threads: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['kind', 'text', 'date'],
|
||||
properties: {
|
||||
kind: { type: 'string', enum: ['commitment', 'recent_event'] },
|
||||
text: { type: 'string' },
|
||||
date: { type: ['string', 'null'] },
|
||||
},
|
||||
},
|
||||
},
|
||||
edges: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['type', 'direction', 'slug'],
|
||||
properties: {
|
||||
type: { type: 'string' },
|
||||
direction: { type: 'string', enum: ['out', 'in'] },
|
||||
slug: { type: 'string' },
|
||||
context: { type: ['string', 'null'] },
|
||||
},
|
||||
},
|
||||
},
|
||||
backlink_count: { type: 'integer' },
|
||||
active_fact_count: { type: 'integer' },
|
||||
},
|
||||
},
|
||||
suggestions: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['slug', 'title', 'create_safety'],
|
||||
properties: {
|
||||
slug: { type: 'string' },
|
||||
title: { type: 'string' },
|
||||
create_safety: { type: 'string', enum: CREATE_SAFETY_ENUM },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
synthesize: {
|
||||
type: 'object',
|
||||
required: ['answer', 'sources', 'cost', 'protocol_version'],
|
||||
properties: {
|
||||
protocol_version: { type: 'integer', const: MEMORY_VERBS_VERSION },
|
||||
answer: { type: 'string' },
|
||||
sources: { type: 'array', items: { type: 'string' } },
|
||||
gaps: { type: 'array', items: { type: 'string' } },
|
||||
cost: {
|
||||
type: 'object',
|
||||
required: ['model', 'input_tokens', 'output_tokens', 'usd_estimate'],
|
||||
description: 'Best-effort aggregate (retries/multi-call flows sum; cache hits may undercount). Honest signal, not an invoice.',
|
||||
properties: {
|
||||
model: { type: 'string' },
|
||||
input_tokens: { type: ['integer', 'null'] },
|
||||
output_tokens: { type: ['integer', 'null'] },
|
||||
usd_estimate: { type: ['number', 'null'] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
forget: {
|
||||
type: 'object',
|
||||
required: ['id', 'expired', 'reason', 'protocol_version'],
|
||||
properties: {
|
||||
protocol_version: { type: 'integer', const: MEMORY_VERBS_VERSION },
|
||||
id: { type: 'string' },
|
||||
expired: { type: 'boolean', description: 'true = this call expired the fact; false = it was ALREADY expired (idempotent re-forget).' },
|
||||
reason: { type: ['string', 'null'] },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/** Error envelope schema (uniform across all five verbs). */
|
||||
export const ERROR_SCHEMA: Record<string, unknown> = {
|
||||
type: 'object',
|
||||
required: ['error', 'message'],
|
||||
properties: {
|
||||
error: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'invalid_params',
|
||||
'provenance_required',
|
||||
'not_found',
|
||||
'scope_denied',
|
||||
'unavailable',
|
||||
'budget_unsatisfiable', // RESERVED — schema-listed, never returned in v1
|
||||
'internal',
|
||||
],
|
||||
},
|
||||
message: { type: 'string' },
|
||||
suggestion: { type: 'string', description: 'Populated on every verb error: problem + cause + fix.' },
|
||||
detail: { type: 'string', description: 'Freeform specifics (e.g. which dependency failed).' },
|
||||
protocol_version: { type: 'integer', const: MEMORY_VERBS_VERSION },
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* MEMORY_VERBS v1 — conformance fixtures (E2).
|
||||
*
|
||||
* The canonical, embedded fixture set the conformance runner executes against
|
||||
* ANY MCP endpoint (default: gbrain's own server). Deterministic by
|
||||
* construction: every check is SHAPE (required fields, enum validity, const),
|
||||
* CONTRACT BEHAVIOR (provenance rejected, idempotent forget, miss→found:false),
|
||||
* or ROUND-TRIP (remember → recall by entity, a plain indexed read — no
|
||||
* embeddings, no ranking judgment). Ranking quality is BrainBench's job.
|
||||
*
|
||||
* These double as BrainBench seeds: `test/fixtures/memory-verbs/cases.json` is
|
||||
* the generated data-file mirror (drift-guarded by the conformance test) so
|
||||
* the eval suite imports the same cases without importing gbrain source.
|
||||
*
|
||||
* Stateful round-trips share a per-run marker (substituted for `{{marker}}`)
|
||||
* and thread ids via `saveAs`/`{{id:<key>}}` substitution.
|
||||
*/
|
||||
|
||||
export interface ConformanceCase {
|
||||
name: string;
|
||||
verb: 'recall' | 'remember' | 'entity' | 'synthesize' | 'forget';
|
||||
/** `{{marker}}` and `{{id:<key>}}` substitute at run time. */
|
||||
params: Record<string, unknown>;
|
||||
/** Validate the (parsed) response body against RESPONSE_SCHEMAS[verb]. */
|
||||
validateSchema?: boolean;
|
||||
/** Expect an isError response with this protocol code. */
|
||||
expectErrorCode?: string;
|
||||
/** On error responses: suggestion must be non-empty (F-D mandate). */
|
||||
expectSuggestion?: boolean;
|
||||
/** Field equality/predicate checks on the parsed response body. */
|
||||
expect?: Array<
|
||||
| { path: string; equals: unknown }
|
||||
| { path: string; oneOf: unknown[] }
|
||||
| { path: string; type: 'string' | 'number' | 'boolean' | 'array' | 'object' }
|
||||
| { path: string; gte: number }
|
||||
| { path: string; lte: number }
|
||||
| { path: string; nonEmptyString: true }
|
||||
| { path: string; absentOrNotContains: string }
|
||||
>;
|
||||
/** Save a response field for later cases ({{id:<key>}}). */
|
||||
saveAs?: { key: string; path: string };
|
||||
/** Only run when the runner was invoked with --synthesize (cost gate). */
|
||||
requiresSynthesizeFlag?: boolean;
|
||||
/**
|
||||
* Only run when the runner could seed the conformance entity PAGE (via
|
||||
* put_page when the target exposes it — full surface). entity() resolves
|
||||
* pages; on a verbs-only target with no seeding path these cases skip
|
||||
* honestly instead of failing on a structurally absent page.
|
||||
*/
|
||||
requiresSeededEntity?: boolean;
|
||||
}
|
||||
|
||||
export const CONFORMANCE_CASES: ConformanceCase[] = [
|
||||
// ── remember: contract behavior ─────────────────────────────────────────
|
||||
{
|
||||
name: 'remember rejects missing provenance (provenance_required + suggestion)',
|
||||
verb: 'remember',
|
||||
params: { fact: 'conformance {{marker}} fact without provenance' },
|
||||
expectErrorCode: 'invalid_params', // MCP-level required-param rejection
|
||||
expectSuggestion: false, // transport-level validation may not carry one
|
||||
},
|
||||
{
|
||||
name: 'remember rejects empty provenance (provenance_required + suggestion)',
|
||||
verb: 'remember',
|
||||
params: { fact: 'conformance {{marker}} fact empty provenance', provenance: ' ' },
|
||||
expectErrorCode: 'provenance_required',
|
||||
expectSuggestion: true,
|
||||
},
|
||||
{
|
||||
name: 'remember rejects ISO-8601 duration ttl with a fix (P30D trap)',
|
||||
verb: 'remember',
|
||||
params: { fact: 'conformance {{marker}} ttl trap', provenance: 'conformance run', ttl: 'P30D' },
|
||||
expectErrorCode: 'invalid_params',
|
||||
expectSuggestion: true,
|
||||
},
|
||||
{
|
||||
name: 'remember writes a fact (string id, enum status, echoed nulls)',
|
||||
verb: 'remember',
|
||||
params: {
|
||||
fact: 'conformance {{marker}}: the protocol round-trip fact',
|
||||
provenance: 'conformance run {{marker}}',
|
||||
entity: 'people/conformance-{{marker}}',
|
||||
kind: 'fact',
|
||||
},
|
||||
validateSchema: true,
|
||||
expect: [
|
||||
{ path: 'id', type: 'string' },
|
||||
{ path: 'status', oneOf: ['inserted', 'duplicate', 'superseded'] },
|
||||
{ path: 'protocol_version', equals: 1 },
|
||||
],
|
||||
saveAs: { key: 'fact1', path: 'id' },
|
||||
},
|
||||
{
|
||||
name: 'remember with ttl returns ISO valid_until',
|
||||
verb: 'remember',
|
||||
params: {
|
||||
fact: 'conformance {{marker}}: expiring fact',
|
||||
provenance: 'conformance run {{marker}}',
|
||||
entity: 'people/conformance-{{marker}}',
|
||||
ttl: '30d',
|
||||
},
|
||||
validateSchema: true,
|
||||
expect: [{ path: 'valid_until', type: 'string' }],
|
||||
saveAs: { key: 'fact2', path: 'id' },
|
||||
},
|
||||
{
|
||||
name: 'remember private fact (fence test setup)',
|
||||
verb: 'remember',
|
||||
params: {
|
||||
fact: 'conformance {{marker}} PRIVATE-SENTINEL commitment',
|
||||
provenance: 'conformance run {{marker}}',
|
||||
entity: 'people/conformance-{{marker}}',
|
||||
kind: 'commitment',
|
||||
visibility: 'private',
|
||||
},
|
||||
validateSchema: true,
|
||||
},
|
||||
|
||||
// ── recall: round-trip + superset + budget ──────────────────────────────
|
||||
{
|
||||
name: 'recall by entity round-trips the remembered fact (superset envelope)',
|
||||
verb: 'recall',
|
||||
params: { entity: 'people/conformance-{{marker}}' },
|
||||
validateSchema: true,
|
||||
expect: [
|
||||
{ path: 'protocol_version', equals: 1 },
|
||||
{ path: 'total', gte: 1 },
|
||||
{ path: 'facts.0.fact_id', type: 'string' },
|
||||
{ path: 'facts.0.provenance', type: 'string' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'recall with budget reports consistent budget meta',
|
||||
verb: 'recall',
|
||||
params: { entity: 'people/conformance-{{marker}}', budget_tokens: 10000 },
|
||||
validateSchema: true,
|
||||
expect: [
|
||||
{ path: 'budget_tokens', equals: 10000 },
|
||||
{ path: 'budget_used', gte: 0 },
|
||||
{ path: 'budget_used', lte: 10000 },
|
||||
{ path: 'dropped_count', gte: 0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'recall with budget smaller than the first item drops everything',
|
||||
verb: 'recall',
|
||||
params: { entity: 'people/conformance-{{marker}}', budget_tokens: 1 },
|
||||
validateSchema: true,
|
||||
expect: [
|
||||
{ path: 'total', equals: 0 },
|
||||
{ path: 'dropped_count', gte: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'recall with query returns the search arm (degradation allowed, never an error)',
|
||||
verb: 'recall',
|
||||
params: { query: 'conformance {{marker}} protocol round-trip', budget_tokens: 8000 },
|
||||
validateSchema: true,
|
||||
expect: [{ path: 'results', type: 'array' }],
|
||||
},
|
||||
|
||||
// ── entity: hit, miss, privacy fence ────────────────────────────────────
|
||||
{
|
||||
name: 'entity resolves the round-trip entity to a card',
|
||||
verb: 'entity',
|
||||
params: { name: 'people/conformance-{{marker}}' },
|
||||
validateSchema: true,
|
||||
requiresSeededEntity: true,
|
||||
expect: [
|
||||
{ path: 'found', equals: true },
|
||||
{ path: 'protocol_version', equals: 1 },
|
||||
{ path: 'card.entity.slug', type: 'string' },
|
||||
{ path: 'card.backlink_count', gte: 0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'entity miss returns found:false + suggestions (never an error)',
|
||||
verb: 'entity',
|
||||
params: { name: 'zzz-no-such-entity-{{marker}}' },
|
||||
validateSchema: true,
|
||||
expect: [
|
||||
{ path: 'found', equals: false },
|
||||
{ path: 'suggestions', type: 'array' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'entity card never leaks private facts to remote callers (fence test)',
|
||||
verb: 'entity',
|
||||
params: { name: 'people/conformance-{{marker}}' },
|
||||
requiresSeededEntity: true,
|
||||
expect: [{ path: 'card.open_threads', absentOrNotContains: 'PRIVATE-SENTINEL' }],
|
||||
},
|
||||
|
||||
// ── forget: idempotency + not_found ─────────────────────────────────────
|
||||
{
|
||||
name: 'forget expires the remembered fact',
|
||||
verb: 'forget',
|
||||
params: { id: '{{id:fact2}}', reason: 'conformance cleanup' },
|
||||
validateSchema: true,
|
||||
expect: [{ path: 'expired', equals: true }],
|
||||
},
|
||||
{
|
||||
name: 'forget again is idempotent (expired:false, success)',
|
||||
verb: 'forget',
|
||||
params: { id: '{{id:fact2}}' },
|
||||
validateSchema: true,
|
||||
expect: [{ path: 'expired', equals: false }],
|
||||
},
|
||||
{
|
||||
name: 'forget unknown id returns not_found with a suggestion',
|
||||
verb: 'forget',
|
||||
params: { id: '999999999' },
|
||||
expectErrorCode: 'not_found',
|
||||
expectSuggestion: true,
|
||||
},
|
||||
|
||||
// ── synthesize: cost-gated live call ────────────────────────────────────
|
||||
{
|
||||
name: 'synthesize answers (or reports unavailable) — cost-gated, pass --synthesize',
|
||||
verb: 'synthesize',
|
||||
params: { question: 'What do we know about conformance {{marker}}?' },
|
||||
requiresSynthesizeFlag: true,
|
||||
// Either a schema-valid answer (key configured) or a clean unavailable
|
||||
// error (no key). The runner accepts both; anything else fails.
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* MEMORY_VERBS v1 — conformance runner core (E2).
|
||||
*
|
||||
* Executes the embedded fixture set (conformance-fixtures.ts) against any MCP
|
||||
* endpoint via a minimal client surface (list_tools + call_tool only) and
|
||||
* returns a pass/fail table. Transport-agnostic: the CLI wraps it with stdio
|
||||
* spawn / HTTP transports; the negative self-test [F3] feeds it a mutated
|
||||
* in-process double.
|
||||
*
|
||||
* Validation is deliberately NON-STRICT on extra fields — the contract is
|
||||
* additive-forever, so unknown fields are always legal. A certifier that
|
||||
* rejected additions would break the versioning policy it certifies.
|
||||
*/
|
||||
|
||||
import { RESPONSE_SCHEMAS, ERROR_SCHEMA, MEMORY_VERBS_VERSION, type VerbName } from '../verbs.ts';
|
||||
import { CONFORMANCE_CASES, type ConformanceCase } from './conformance-fixtures.ts';
|
||||
|
||||
export interface ConformanceClient {
|
||||
listTools(): Promise<Array<{ name: string; description?: string; annotations?: unknown }>>;
|
||||
callTool(name: string, args: Record<string, unknown>): Promise<{ isError?: boolean; text: string }>;
|
||||
}
|
||||
|
||||
export interface CaseResult {
|
||||
name: string;
|
||||
verb: string;
|
||||
status: 'pass' | 'fail' | 'skip';
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export interface ConformanceReport {
|
||||
protocol_version: number;
|
||||
results: CaseResult[];
|
||||
passed: number;
|
||||
failed: number;
|
||||
skipped: number;
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal JSON-Schema-subset validator: type (incl. union arrays + integer),
|
||||
* required, properties (NON-strict), enum, const, items. Returns violations
|
||||
* as "path: problem" strings.
|
||||
*/
|
||||
export function validateAgainstSchema(
|
||||
value: unknown,
|
||||
schema: Record<string, unknown>,
|
||||
path = '$',
|
||||
): string[] {
|
||||
const out: string[] = [];
|
||||
const t = schema.type as string | string[] | undefined;
|
||||
if (t !== undefined) {
|
||||
const types = Array.isArray(t) ? t : [t];
|
||||
if (!types.some(ty => matchesType(value, ty))) {
|
||||
out.push(`${path}: expected ${types.join('|')}, got ${describe(value)}`);
|
||||
return out; // structural mismatch — deeper checks are noise
|
||||
}
|
||||
}
|
||||
if (schema.const !== undefined && value !== schema.const) {
|
||||
out.push(`${path}: expected const ${JSON.stringify(schema.const)}, got ${JSON.stringify(value)}`);
|
||||
}
|
||||
if (Array.isArray(schema.enum) && value !== null && !schema.enum.includes(value)) {
|
||||
out.push(`${path}: ${JSON.stringify(value)} not in enum [${(schema.enum as unknown[]).join(', ')}]`);
|
||||
}
|
||||
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
||||
const obj = value as Record<string, unknown>;
|
||||
for (const req of (schema.required as string[] | undefined) ?? []) {
|
||||
if (!(req in obj)) out.push(`${path}.${req}: required field missing`);
|
||||
}
|
||||
const props = (schema.properties as Record<string, Record<string, unknown>> | undefined) ?? {};
|
||||
for (const [k, sub] of Object.entries(props)) {
|
||||
if (k in obj && obj[k] !== undefined) out.push(...validateAgainstSchema(obj[k], sub, `${path}.${k}`));
|
||||
}
|
||||
}
|
||||
if (Array.isArray(value) && schema.items && typeof schema.items === 'object') {
|
||||
value.forEach((item, i) =>
|
||||
out.push(...validateAgainstSchema(item, schema.items as Record<string, unknown>, `${path}[${i}]`)),
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function matchesType(v: unknown, t: string): boolean {
|
||||
switch (t) {
|
||||
case 'string': return typeof v === 'string';
|
||||
case 'integer': return typeof v === 'number' && Number.isInteger(v);
|
||||
case 'number': return typeof v === 'number';
|
||||
case 'boolean': return typeof v === 'boolean';
|
||||
case 'array': return Array.isArray(v);
|
||||
case 'object': return v !== null && typeof v === 'object' && !Array.isArray(v);
|
||||
case 'null': return v === null;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
function describe(v: unknown): string {
|
||||
if (v === null) return 'null';
|
||||
if (Array.isArray(v)) return 'array';
|
||||
return typeof v;
|
||||
}
|
||||
|
||||
function getPath(obj: unknown, path: string): unknown {
|
||||
let cur: unknown = obj;
|
||||
for (const seg of path.split('.')) {
|
||||
if (cur === null || cur === undefined) return undefined;
|
||||
cur = (cur as Record<string, unknown>)[seg];
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
function substitute(value: unknown, marker: string, ids: Map<string, string>): unknown {
|
||||
if (typeof value === 'string') {
|
||||
let s = value.replaceAll('{{marker}}', marker);
|
||||
s = s.replace(/\{\{id:([a-z0-9_-]+)\}\}/gi, (_, key) => ids.get(key) ?? `MISSING-ID-${key}`);
|
||||
return s;
|
||||
}
|
||||
if (Array.isArray(value)) return value.map(v => substitute(v, marker, ids));
|
||||
if (value !== null && typeof value === 'object') {
|
||||
return Object.fromEntries(Object.entries(value as Record<string, unknown>).map(([k, v]) => [k, substitute(v, marker, ids)]));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function runChecks(body: unknown, checks: NonNullable<ConformanceCase['expect']>): string[] {
|
||||
const problems: string[] = [];
|
||||
for (const c of checks) {
|
||||
const v = getPath(body, c.path);
|
||||
if ('equals' in c) {
|
||||
if (v !== c.equals) problems.push(`${c.path}: expected ${JSON.stringify(c.equals)}, got ${JSON.stringify(v)}`);
|
||||
} else if ('oneOf' in c) {
|
||||
if (!c.oneOf.includes(v)) problems.push(`${c.path}: ${JSON.stringify(v)} not in ${JSON.stringify(c.oneOf)}`);
|
||||
} else if ('type' in c) {
|
||||
if (!matchesType(v, c.type)) problems.push(`${c.path}: expected ${c.type}, got ${describe(v)}`);
|
||||
} else if ('gte' in c) {
|
||||
if (typeof v !== 'number' || v < c.gte) problems.push(`${c.path}: expected >= ${c.gte}, got ${JSON.stringify(v)}`);
|
||||
} else if ('lte' in c) {
|
||||
if (typeof v !== 'number' || v > c.lte) problems.push(`${c.path}: expected <= ${c.lte}, got ${JSON.stringify(v)}`);
|
||||
} else if ('nonEmptyString' in c) {
|
||||
if (typeof v !== 'string' || !v.trim()) problems.push(`${c.path}: expected non-empty string`);
|
||||
} else if ('absentOrNotContains' in c) {
|
||||
const json = v === undefined ? '' : JSON.stringify(v);
|
||||
if (json.includes(c.absentOrNotContains)) problems.push(`${c.path}: must not contain "${c.absentOrNotContains}"`);
|
||||
}
|
||||
}
|
||||
return problems;
|
||||
}
|
||||
|
||||
export async function runConformance(
|
||||
client: ConformanceClient,
|
||||
opts: { marker?: string; synthesize?: boolean } = {},
|
||||
): Promise<ConformanceReport> {
|
||||
const marker = opts.marker ?? `run-${Date.now().toString(36)}`;
|
||||
const ids = new Map<string, string>();
|
||||
const results: CaseResult[] = [];
|
||||
|
||||
// Seed the conformance entity PAGE when the target exposes put_page (full
|
||||
// surface). entity() resolves pages; verbs-only targets with no seeding
|
||||
// path skip the entity-hit cases honestly (requiresSeededEntity).
|
||||
let seededEntity = false;
|
||||
try {
|
||||
const seed = await client.callTool('put_page', {
|
||||
slug: `people/conformance-${marker}`,
|
||||
content: `---\ntitle: Conformance ${marker}\ntype: person\n---\n\n# Conformance ${marker}\n\nSynthetic entity for a MEMORY_VERBS conformance run.\n`,
|
||||
});
|
||||
seededEntity = !seed.isError;
|
||||
} catch {
|
||||
seededEntity = false;
|
||||
}
|
||||
|
||||
// List-level checks first: the five verbs are advertised, synthesize is
|
||||
// marked expensive (description prefix is the load-bearing channel).
|
||||
try {
|
||||
const tools = await client.listTools();
|
||||
const byName = new Map(tools.map(t => [t.name, t]));
|
||||
for (const verb of Object.keys(RESPONSE_SCHEMAS) as VerbName[]) {
|
||||
results.push(
|
||||
byName.has(verb)
|
||||
? { name: `tools/list advertises ${verb}`, verb, status: 'pass', detail: '' }
|
||||
: { name: `tools/list advertises ${verb}`, verb, status: 'fail', detail: 'not advertised' },
|
||||
);
|
||||
}
|
||||
const synth = byName.get('synthesize');
|
||||
const marked = !!synth?.description?.startsWith('[EXPENSIVE');
|
||||
results.push({
|
||||
name: 'synthesize is marked expensive ([EXPENSIVE prefix)',
|
||||
verb: 'synthesize',
|
||||
status: marked ? 'pass' : 'fail',
|
||||
detail: marked ? '' : `description starts: "${synth?.description?.slice(0, 40) ?? '(missing)'}..."`,
|
||||
});
|
||||
} catch (e) {
|
||||
results.push({
|
||||
name: 'tools/list',
|
||||
verb: '-',
|
||||
status: 'fail',
|
||||
detail: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
}
|
||||
|
||||
for (const c of CONFORMANCE_CASES) {
|
||||
if (c.requiresSynthesizeFlag && !opts.synthesize) {
|
||||
results.push({ name: c.name, verb: c.verb, status: 'skip', detail: 'costs money — pass --synthesize' });
|
||||
continue;
|
||||
}
|
||||
if (c.requiresSeededEntity && !seededEntity) {
|
||||
results.push({ name: c.name, verb: c.verb, status: 'skip', detail: 'target has no put_page to seed the entity page (verbs-only surface)' });
|
||||
continue;
|
||||
}
|
||||
const params = substitute(c.params, marker, ids) as Record<string, unknown>;
|
||||
let res: { isError?: boolean; text: string };
|
||||
try {
|
||||
res = await client.callTool(c.verb, params);
|
||||
} catch (e) {
|
||||
results.push({ name: c.name, verb: c.verb, status: 'fail', detail: `transport: ${e instanceof Error ? e.message : String(e)}` });
|
||||
continue;
|
||||
}
|
||||
let body: unknown;
|
||||
try {
|
||||
body = JSON.parse(res.text);
|
||||
} catch {
|
||||
results.push({ name: c.name, verb: c.verb, status: 'fail', detail: `response not JSON: ${res.text.slice(0, 120)}` });
|
||||
continue;
|
||||
}
|
||||
|
||||
const problems: string[] = [];
|
||||
|
||||
if (c.verb === 'synthesize' && c.requiresSynthesizeFlag) {
|
||||
// Accept either a schema-valid answer (key configured) or a clean
|
||||
// `unavailable` protocol error (no key). Anything else fails.
|
||||
if (res.isError) {
|
||||
const err = body as { error?: string; suggestion?: string };
|
||||
if (err.error !== 'unavailable') problems.push(`expected unavailable, got error=${err.error}`);
|
||||
if (!err.suggestion?.trim()) problems.push('error.suggestion empty');
|
||||
problems.push(...validateAgainstSchema(body, ERROR_SCHEMA, '$err'));
|
||||
} else {
|
||||
problems.push(...validateAgainstSchema(body, RESPONSE_SCHEMAS.synthesize));
|
||||
}
|
||||
} else if (c.expectErrorCode) {
|
||||
if (!res.isError) {
|
||||
problems.push(`expected error ${c.expectErrorCode}, got success`);
|
||||
} else {
|
||||
const err = body as { error?: string; suggestion?: string; protocol_version?: number };
|
||||
if (err.error !== c.expectErrorCode) problems.push(`expected error=${c.expectErrorCode}, got ${err.error}`);
|
||||
if (c.expectSuggestion && !err.suggestion?.trim()) problems.push('error.suggestion empty (F-D mandate)');
|
||||
}
|
||||
} else {
|
||||
if (res.isError) {
|
||||
problems.push(`unexpected error: ${res.text.slice(0, 160)}`);
|
||||
} else {
|
||||
if (c.validateSchema) problems.push(...validateAgainstSchema(body, RESPONSE_SCHEMAS[c.verb]));
|
||||
if (c.expect) problems.push(...runChecks(body, c.expect));
|
||||
if (c.saveAs) {
|
||||
const v = getPath(body, c.saveAs.path);
|
||||
if (typeof v === 'string') ids.set(c.saveAs.key, v);
|
||||
else problems.push(`saveAs ${c.saveAs.path}: expected string id, got ${describe(v)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.push(
|
||||
problems.length === 0
|
||||
? { name: c.name, verb: c.verb, status: 'pass', detail: '' }
|
||||
: { name: c.name, verb: c.verb, status: 'fail', detail: problems.join('; ') },
|
||||
);
|
||||
}
|
||||
|
||||
const passed = results.filter(r => r.status === 'pass').length;
|
||||
const failed = results.filter(r => r.status === 'fail').length;
|
||||
const skipped = results.filter(r => r.status === 'skip').length;
|
||||
return { protocol_version: MEMORY_VERBS_VERSION, results, passed, failed, skipped, ok: failed === 0 };
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* MEMORY_VERBS v1 — `entity(name)` card builder (zero LLM, p99 < 100ms).
|
||||
*
|
||||
* Resolves a free-text name to ONE brain page via the Retrieval Reflex's
|
||||
* precision-biased arms (alias-first, then exact-title / exact-slug /
|
||||
* slug-suffix), then assembles a compact self-describing card from parallel
|
||||
* depth-1 indexed reads. Deliberately NOT the recursive-CTE traversal
|
||||
* (traversePaths) — the card is a latency contract, not a graph walk.
|
||||
*
|
||||
* Resolution precedence (frozen): alias > exact title > slug-suffix; ties
|
||||
* break on GREATEST(updated_at, last_retrieved_at) — "last_touched" is the
|
||||
* card's OUTPUT name, not a column. Multi-hit → best match wins, runners-up
|
||||
* land in `suggestions`. Miss → `found: false` + keyword near-misses with
|
||||
* create_safety hints. NEVER throws for data reasons; each arm is guarded so
|
||||
* a pre-page_aliases brain still resolves via arm 2 (same posture as the
|
||||
* shipped reflex).
|
||||
*
|
||||
* Privacy: `summary` runs through safeSynopsis (the get_page fence boundary);
|
||||
* facts respect visibility for remote callers (world-only).
|
||||
*/
|
||||
|
||||
import type { BrainEngine, FactRow } from '../engine.ts';
|
||||
import { normalizeAlias } from '../search/alias-normalize.ts';
|
||||
import { slugify } from '../entities/resolve.ts';
|
||||
import { safeSynopsis } from '../context/retrieval-reflex.ts';
|
||||
import { stampEvidence } from '../search/evidence.ts';
|
||||
import type { SearchResult } from '../types.ts';
|
||||
|
||||
const EDGE_CAP = 10;
|
||||
const OPEN_THREADS_CAP = 3;
|
||||
const OPEN_THREAD_TIMELINE_WINDOW_DAYS = 90;
|
||||
const SUGGESTION_CAP = 3;
|
||||
const FACT_FETCH_CAP = 100;
|
||||
|
||||
export interface EntityCardEdge {
|
||||
type: string;
|
||||
direction: 'out' | 'in';
|
||||
slug: string;
|
||||
context: string | null;
|
||||
}
|
||||
|
||||
export interface EntityOpenThread {
|
||||
kind: 'commitment' | 'recent_event';
|
||||
text: string;
|
||||
date: string | null;
|
||||
}
|
||||
|
||||
export interface EntityCard {
|
||||
entity: { slug: string; title: string; type: string | null };
|
||||
/** page_aliases reverse lookup (normalized forms). Empty on pre-migration brains. */
|
||||
aka: string[];
|
||||
/** Privacy-safe synopsis — same fence boundary as get_page. */
|
||||
summary: string;
|
||||
last_touched: {
|
||||
updated_at: string | null;
|
||||
last_retrieved_at: string | null;
|
||||
last_timeline_date: string | null;
|
||||
};
|
||||
/** Best-effort in v1: active commitment-kind facts + recent timeline entries. */
|
||||
open_threads: EntityOpenThread[];
|
||||
/** Top typed edges, mentions excluded, out-edges first. */
|
||||
edges: EntityCardEdge[];
|
||||
backlink_count: number;
|
||||
/** Active facts about this entity (capped count; visibility-filtered for remote). */
|
||||
active_fact_count: number;
|
||||
}
|
||||
|
||||
export interface EntitySuggestion {
|
||||
slug: string;
|
||||
title: string;
|
||||
create_safety: string;
|
||||
}
|
||||
|
||||
export interface EntityCardResult {
|
||||
found: boolean;
|
||||
card?: EntityCard;
|
||||
suggestions?: EntitySuggestion[];
|
||||
}
|
||||
|
||||
interface CardPageRow {
|
||||
slug: string;
|
||||
// v0.43 merge: retrieval-reflex's exported PageRow (safeSynopsis's param)
|
||||
// now requires source_id (federated push-context wave #2095). The card row
|
||||
// carries it too so it remains assignable.
|
||||
source_id: string;
|
||||
title: string;
|
||||
type: string | null;
|
||||
frontmatter: Record<string, unknown> | null;
|
||||
compiled_truth: string | null;
|
||||
updated_at: Date | string | null;
|
||||
last_retrieved_at: Date | string | null;
|
||||
}
|
||||
|
||||
/** Resolution arm rank: lower = higher confidence (frozen precedence ladder). */
|
||||
const ARM_ALIAS = 0;
|
||||
const ARM_EXACT = 1;
|
||||
const ARM_SUFFIX = 2;
|
||||
|
||||
export async function buildEntityCard(
|
||||
engine: BrainEngine,
|
||||
sourceId: string,
|
||||
name: string,
|
||||
opts: { remote: boolean },
|
||||
): Promise<EntityCardResult> {
|
||||
const trimmed = (name ?? '').trim();
|
||||
if (!trimmed) return { found: false, suggestions: [] };
|
||||
|
||||
const norm = normalizeAlias(trimmed);
|
||||
const titleLc = trimmed.toLowerCase();
|
||||
// Two exact-slug candidates: the slugified form for free-text names AND the
|
||||
// raw input — a caller passing an already-namespaced slug
|
||||
// ("people/alice-example") must hit exactly (slugify flattens the slash).
|
||||
const slug = slugify(trimmed);
|
||||
const exactSlugs = [...new Set([slug, trimmed].filter(Boolean))];
|
||||
|
||||
// Candidate slugs with their best arm rank.
|
||||
const rankBySlug = new Map<string, number>();
|
||||
const consider = (s: string, rank: number) => {
|
||||
if (!s) return;
|
||||
const prev = rankBySlug.get(s);
|
||||
if (prev === undefined || rank < prev) rankBySlug.set(s, rank);
|
||||
};
|
||||
|
||||
// Arm 1 — alias-first. Guarded: pre-migration brains lack page_aliases.
|
||||
if (norm) {
|
||||
try {
|
||||
const aliasMap = await engine.resolveAliases([norm], { sourceId });
|
||||
for (const hit of aliasMap.get(norm) ?? []) consider(hit.slug, ARM_ALIAS);
|
||||
} catch {
|
||||
/* no page_aliases table — degrade to arm 2 [E3] */
|
||||
}
|
||||
}
|
||||
|
||||
// Arm 2 — exact title / exact slug / slug-suffix, with the columns the
|
||||
// card's tie-break needs. Guarded like the reflex.
|
||||
let rows: CardPageRow[] = [];
|
||||
try {
|
||||
rows = await engine.executeRaw<CardPageRow>(
|
||||
`SELECT slug, source_id, title, type, frontmatter, compiled_truth, updated_at, last_retrieved_at
|
||||
FROM pages
|
||||
WHERE deleted_at IS NULL
|
||||
AND source_id = $1
|
||||
AND ( lower(title) = $2
|
||||
OR slug = ANY($3::text[])
|
||||
OR slug LIKE $4 )`,
|
||||
[sourceId, titleLc, exactSlugs, `%/${slug || trimmed}`],
|
||||
);
|
||||
} catch {
|
||||
rows = [];
|
||||
}
|
||||
const rowBySlug = new Map<string, CardPageRow>();
|
||||
for (const r of rows) {
|
||||
rowBySlug.set(r.slug, r);
|
||||
const isExact = (r.title ?? '').toLowerCase() === titleLc || exactSlugs.includes(r.slug);
|
||||
consider(r.slug, isExact ? ARM_EXACT : ARM_SUFFIX);
|
||||
}
|
||||
|
||||
// Hydrate alias-resolved slugs that arm 2 didn't fetch.
|
||||
const missing = [...rankBySlug.keys()].filter(s => !rowBySlug.has(s));
|
||||
if (missing.length) {
|
||||
try {
|
||||
const extra = await engine.executeRaw<CardPageRow>(
|
||||
`SELECT slug, source_id, title, type, frontmatter, compiled_truth, updated_at, last_retrieved_at
|
||||
FROM pages
|
||||
WHERE deleted_at IS NULL AND source_id = $1 AND slug = ANY($2::text[])`,
|
||||
[sourceId, missing],
|
||||
);
|
||||
for (const r of extra) rowBySlug.set(r.slug, r);
|
||||
} catch {
|
||||
/* stale alias rows — drop */
|
||||
}
|
||||
}
|
||||
|
||||
// Rank candidates: arm rank asc, then GREATEST(updated_at, last_retrieved_at) desc.
|
||||
const candidates = [...rankBySlug.entries()]
|
||||
.map(([s, rank]) => ({ slug: s, rank, row: rowBySlug.get(s) }))
|
||||
.filter((c): c is { slug: string; rank: number; row: CardPageRow } => c.row !== undefined)
|
||||
.sort((a, b) => a.rank - b.rank || lastTouchedMs(b.row) - lastTouchedMs(a.row));
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return { found: false, suggestions: await nearMissSuggestions(engine, sourceId, trimmed) };
|
||||
}
|
||||
|
||||
const best = candidates[0];
|
||||
const runnersUp: EntitySuggestion[] = candidates.slice(1, 1 + SUGGESTION_CAP).map(c => ({
|
||||
slug: c.slug,
|
||||
title: c.row.title ?? c.slug,
|
||||
// A page that resolved through the precision arms exists by definition.
|
||||
create_safety: 'exists',
|
||||
}));
|
||||
|
||||
const card = await assembleCard(engine, sourceId, best.row, opts.remote);
|
||||
return {
|
||||
found: true,
|
||||
card,
|
||||
...(runnersUp.length ? { suggestions: runnersUp } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function assembleCard(
|
||||
engine: BrainEngine,
|
||||
sourceId: string,
|
||||
row: CardPageRow,
|
||||
remote: boolean,
|
||||
): Promise<EntityCard> {
|
||||
const pageSlug = row.slug;
|
||||
const visibility = remote ? (['world'] as ('private' | 'world')[]) : undefined;
|
||||
|
||||
// Parallel depth-1 reads — every arm individually fail-soft so a partial
|
||||
// brain (no aliases, no timeline) still returns a card.
|
||||
//
|
||||
// [ship P1.2] Incoming edges + backlink_count are SOURCE-SAFE on BOTH sides.
|
||||
// engine.getBacklinks(slug,{sourceId}) only scopes the TARGET page's source,
|
||||
// so a foreign-source page linking to a same-named entity would leak its
|
||||
// slug; engine.getBacklinkCounts has no source param at all. We instead run
|
||||
// a both-sides-scoped query here (f.source_id = t.source_id = this source),
|
||||
// mentions excluded (matching the backlink-count convention). Outgoing edges
|
||||
// (getLinks) are the entity's OWN declared links — from-side scoped — so they
|
||||
// stay as-is.
|
||||
const [aka, outLinks, inEdges, backlinkCount, timeline, facts] = await Promise.all([
|
||||
engine
|
||||
.executeRaw<{ alias_norm: string }>(
|
||||
`SELECT alias_norm FROM page_aliases WHERE source_id = $1 AND slug = $2 ORDER BY alias_norm`,
|
||||
[sourceId, pageSlug],
|
||||
)
|
||||
.then(rs => rs.map(r => r.alias_norm))
|
||||
.catch(() => [] as string[]),
|
||||
engine.getLinks(pageSlug, { sourceId }).catch(() => []),
|
||||
engine
|
||||
.executeRaw<{ from_slug: string; link_type: string; context: string | null }>(
|
||||
`SELECT f.slug AS from_slug, l.link_type, l.context
|
||||
FROM links l
|
||||
JOIN pages f ON f.id = l.from_page_id
|
||||
JOIN pages t ON t.id = l.to_page_id
|
||||
WHERE t.slug = $1 AND t.source_id = $2 AND f.source_id = $2
|
||||
AND COALESCE(l.link_source, '') <> 'mentions'`,
|
||||
[pageSlug, sourceId],
|
||||
)
|
||||
.catch(() => [] as Array<{ from_slug: string; link_type: string; context: string | null }>),
|
||||
engine
|
||||
.executeRaw<{ n: string | number }>(
|
||||
`SELECT COUNT(*) AS n
|
||||
FROM links l
|
||||
JOIN pages f ON f.id = l.from_page_id
|
||||
JOIN pages t ON t.id = l.to_page_id
|
||||
WHERE t.slug = $1 AND t.source_id = $2 AND f.source_id = $2
|
||||
AND COALESCE(l.link_source, '') <> 'mentions'`,
|
||||
[pageSlug, sourceId],
|
||||
)
|
||||
.then(rs => Number(rs[0]?.n ?? 0))
|
||||
.catch(() => 0),
|
||||
engine.getTimeline(pageSlug, { limit: 5, sourceId }).catch(() => []),
|
||||
engine
|
||||
.listFactsByEntity(sourceId, pageSlug, {
|
||||
activeOnly: true,
|
||||
limit: FACT_FETCH_CAP,
|
||||
...(visibility ? { visibility } : {}),
|
||||
})
|
||||
.catch(() => [] as FactRow[]),
|
||||
]);
|
||||
|
||||
const edges: EntityCardEdge[] = [];
|
||||
for (const l of outLinks) {
|
||||
if (l.link_source === 'mentions') continue;
|
||||
edges.push({ type: l.link_type, direction: 'out', slug: l.to_slug, context: l.context || null });
|
||||
if (edges.length >= EDGE_CAP) break;
|
||||
}
|
||||
if (edges.length < EDGE_CAP) {
|
||||
for (const l of inEdges) {
|
||||
edges.push({ type: l.link_type, direction: 'in', slug: l.from_slug, context: l.context || null });
|
||||
if (edges.length >= EDGE_CAP) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Open threads (best-effort v1): active commitments first, then recent
|
||||
// timeline entries inside the window, capped together.
|
||||
const openThreads: EntityOpenThread[] = [];
|
||||
for (const f of facts) {
|
||||
if (f.kind !== 'commitment') continue;
|
||||
openThreads.push({ kind: 'commitment', text: f.fact, date: f.valid_from?.toISOString() ?? null });
|
||||
if (openThreads.length >= OPEN_THREADS_CAP) break;
|
||||
}
|
||||
if (openThreads.length < OPEN_THREADS_CAP) {
|
||||
const cutoff = Date.now() - OPEN_THREAD_TIMELINE_WINDOW_DAYS * 24 * 60 * 60 * 1000;
|
||||
for (const t of timeline) {
|
||||
const ts = Date.parse(t.date);
|
||||
if (!Number.isFinite(ts) || ts < cutoff) continue;
|
||||
openThreads.push({ kind: 'recent_event', text: t.summary, date: t.date });
|
||||
if (openThreads.length >= OPEN_THREADS_CAP) break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
entity: { slug: pageSlug, title: row.title ?? pageSlug, type: row.type ?? null },
|
||||
aka,
|
||||
summary: safeSynopsis(row),
|
||||
last_touched: {
|
||||
updated_at: toIso(row.updated_at),
|
||||
last_retrieved_at: toIso(row.last_retrieved_at),
|
||||
last_timeline_date: timeline.length ? timeline[0].date : null,
|
||||
},
|
||||
open_threads: openThreads,
|
||||
edges,
|
||||
backlink_count: backlinkCount,
|
||||
active_fact_count: facts.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Near-miss suggestions on a total miss (E5 delight): keyword search top-N
|
||||
* with evidence-derived create_safety so a typo'd name becomes a next move
|
||||
* instead of a dead end. Zero LLM; fail-soft to [].
|
||||
*/
|
||||
async function nearMissSuggestions(
|
||||
engine: BrainEngine,
|
||||
sourceId: string,
|
||||
name: string,
|
||||
): Promise<EntitySuggestion[]> {
|
||||
try {
|
||||
const raw = await engine.searchKeyword(name, { limit: SUGGESTION_CAP, sourceId });
|
||||
const results = raw as SearchResult[];
|
||||
stampEvidence(results);
|
||||
return results.map(r => ({
|
||||
slug: r.slug,
|
||||
title: r.title ?? r.slug,
|
||||
create_safety: r.create_safety ?? 'unknown',
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function lastTouchedMs(row: CardPageRow): number {
|
||||
const u = toMs(row.updated_at);
|
||||
const l = toMs(row.last_retrieved_at);
|
||||
return Math.max(u, l);
|
||||
}
|
||||
|
||||
function toMs(v: Date | string | null): number {
|
||||
if (v == null) return 0;
|
||||
const ms = v instanceof Date ? v.getTime() : Date.parse(v);
|
||||
return Number.isFinite(ms) ? ms : 0;
|
||||
}
|
||||
|
||||
function toIso(v: Date | string | null): string | null {
|
||||
const ms = toMs(v);
|
||||
return ms > 0 ? new Date(ms).toISOString() : null;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* MEMORY_VERBS v1 — per-verb usage sidecar (E4 observability).
|
||||
*
|
||||
* One JSONL line per verb call, written from the DISPATCH layer (so
|
||||
* param-validation failures are counted too — c11), fire-and-forget.
|
||||
* LOCAL ONLY: this file never leaves the machine and is never uploaded —
|
||||
* it is observability, never source of truth. Stats tolerate loss.
|
||||
*
|
||||
* Concurrency: append-only, one line-buffered write() per event (<4KB ⇒
|
||||
* atomic under POSIX O_APPEND; serve + jobs worker interleave safely at line
|
||||
* granularity; best-effort on Windows, documented). Rotation at 10MB is
|
||||
* lock-free best-effort — a concurrent double-rotate can drop lines, which
|
||||
* is acceptable for stats.
|
||||
*
|
||||
* Path: ~/.gbrain/integrations/memory-verbs/usage.jsonl via gbrainPath, so
|
||||
* GBRAIN_HOME is honored and brain_id (the resolved gbrain home) is a true
|
||||
* per-brain disambiguator [c11/m1].
|
||||
*/
|
||||
|
||||
import { appendFile, mkdir, rename, stat, readFile } from 'node:fs/promises';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { gbrainPath } from '../config.ts';
|
||||
|
||||
const ROTATE_BYTES = 10 * 1024 * 1024;
|
||||
|
||||
export interface VerbUsageEvent {
|
||||
ts: string;
|
||||
verb: string;
|
||||
surface: 'verbs' | 'full';
|
||||
remote: boolean;
|
||||
ok: boolean;
|
||||
latency_ms: number;
|
||||
brain_id: string;
|
||||
source_id: string;
|
||||
budget_dropped?: number;
|
||||
entity_found?: boolean;
|
||||
}
|
||||
|
||||
let _pathOverride: string | null = null;
|
||||
|
||||
/**
|
||||
* Test-only seam: redirect the sidecar to a temp file without mutating
|
||||
* process.env.GBRAIN_HOME (the test-isolation lint forbids global env
|
||||
* mutation). Pass null to restore. @internal exported for tests.
|
||||
*/
|
||||
export function __setUsageLogPathForTests(path: string | null): void {
|
||||
_pathOverride = path;
|
||||
}
|
||||
|
||||
export function usageLogPath(): string {
|
||||
return _pathOverride ?? gbrainPath('integrations', 'memory-verbs', 'usage.jsonl');
|
||||
}
|
||||
|
||||
/** The resolved gbrain home — the per-brain disambiguator for multi-brain stats. */
|
||||
export function brainId(): string {
|
||||
return gbrainPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire-and-forget append. NEVER throws, NEVER blocks the verb call — callers
|
||||
* do not await this (dispatch invokes it without await).
|
||||
*/
|
||||
export function logVerbUsage(event: Omit<VerbUsageEvent, 'ts' | 'brain_id'>): void {
|
||||
void (async () => {
|
||||
try {
|
||||
const path = usageLogPath();
|
||||
await mkdir(dirname(path), { recursive: true });
|
||||
// Best-effort lock-free rotation (stats-only; dropped lines tolerated).
|
||||
try {
|
||||
const s = await stat(path);
|
||||
if (s.size > ROTATE_BYTES) {
|
||||
await rename(path, join(dirname(path), 'usage.jsonl.1'));
|
||||
}
|
||||
} catch {
|
||||
/* no file yet, or a concurrent rotate won — either is fine */
|
||||
}
|
||||
const line =
|
||||
JSON.stringify({ ts: new Date().toISOString(), brain_id: brainId(), ...event }) + '\n';
|
||||
await appendFile(path, line, 'utf-8');
|
||||
} catch {
|
||||
/* observability never breaks the verb call */
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read events for `gbrain protocol stats` + the doctor check. Tolerates
|
||||
* malformed lines (torn writes on non-POSIX appends) by skipping them.
|
||||
*/
|
||||
export async function readVerbUsage(opts: { days?: number } = {}): Promise<VerbUsageEvent[]> {
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await readFile(usageLogPath(), 'utf-8');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const cutoff = opts.days ? Date.now() - opts.days * 24 * 60 * 60 * 1000 : null;
|
||||
const events: VerbUsageEvent[] = [];
|
||||
for (const line of raw.split('\n')) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const e = JSON.parse(line) as VerbUsageEvent;
|
||||
if (!e || typeof e.verb !== 'string' || typeof e.ts !== 'string') continue;
|
||||
if (cutoff !== null) {
|
||||
const ms = Date.parse(e.ts);
|
||||
if (!Number.isFinite(ms) || ms < cutoff) continue;
|
||||
}
|
||||
events.push(e);
|
||||
} catch {
|
||||
/* torn line — skip */
|
||||
}
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
/** Earliest event timestamp — the TTHW numerator for `protocol stats` [D6C]. */
|
||||
export async function earliestVerbUsageTs(): Promise<string | null> {
|
||||
const events = await readVerbUsage();
|
||||
let earliest: string | null = null;
|
||||
for (const e of events) {
|
||||
if (earliest === null || e.ts < earliest) earliest = e.ts;
|
||||
}
|
||||
return earliest;
|
||||
}
|
||||
+76
-2
@@ -10,6 +10,10 @@ import type { BrainEngine } from '../core/engine.ts';
|
||||
import { operations, OperationError, enforceBoundClientOpAllowList } from '../core/operations.ts';
|
||||
import type { Operation, OperationContext, AuthInfo } from '../core/operations.ts';
|
||||
import { loadConfig } from '../core/config.ts';
|
||||
import { VERB_NAMES, MEMORY_VERBS_VERSION } from '../core/verbs.ts';
|
||||
import { logVerbUsage } from '../core/verbs/usage-log.ts';
|
||||
|
||||
const VERB_NAME_SET: ReadonlySet<string> = new Set(VERB_NAMES);
|
||||
|
||||
export interface ToolResult {
|
||||
content: { type: 'text'; text: string }[];
|
||||
@@ -83,6 +87,20 @@ export interface DispatchOpts {
|
||||
* was replaced by dispatchToolCall.
|
||||
*/
|
||||
auth?: AuthInfo;
|
||||
/**
|
||||
* MEMORY_VERBS v1 surface enforcement [c2]. When set, a tool name outside
|
||||
* the set returns the unknown_tool envelope BEFORE resolution — fail-closed
|
||||
* at the SHARED layer, so a hidden op stays uncallable on every transport
|
||||
* even when only the tool LIST was filtered. Unset = full catalog
|
||||
* (pre-existing behavior, all current callers).
|
||||
*/
|
||||
allowedOps?: ReadonlySet<string>;
|
||||
/**
|
||||
* Which surface this transport is serving — recorded on the verb usage
|
||||
* sidecar so adoption stats can split quickstart installs from full
|
||||
* surfaces. Defaults to 'full'.
|
||||
*/
|
||||
surface?: 'verbs' | 'full';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -240,6 +258,33 @@ export async function dispatchToolCall(
|
||||
params: Record<string, unknown> | undefined,
|
||||
opts: DispatchOpts = {},
|
||||
): Promise<ToolResult> {
|
||||
const startedMs = Date.now();
|
||||
const isVerb = VERB_NAME_SET.has(name);
|
||||
// [c11] dispatch-layer usage sidecar for the five verbs — counts validation
|
||||
// failures too. Fire-and-forget; never awaited, never throws.
|
||||
const logVerb = (ok: boolean, extra?: { budget_dropped?: number; entity_found?: boolean }) => {
|
||||
if (!isVerb) return;
|
||||
logVerbUsage({
|
||||
verb: name,
|
||||
surface: opts.surface ?? 'full',
|
||||
remote: opts.remote ?? true,
|
||||
ok,
|
||||
latency_ms: Date.now() - startedMs,
|
||||
source_id: opts.sourceId ?? 'default',
|
||||
...(extra ?? {}),
|
||||
});
|
||||
};
|
||||
|
||||
// [c2] surface enforcement at the SHARED layer: a hidden op is uncallable
|
||||
// on every transport, not just unlisted. Same envelope as unknown ops so
|
||||
// the surface doesn't leak which names exist.
|
||||
if (opts.allowedOps && !opts.allowedOps.has(name)) {
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify({ error: 'unknown_tool', message: `Unknown tool: ${name}` }, null, 2) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const op = operations.find(o => o.name === name);
|
||||
if (!op) {
|
||||
// Always return JSON-shaped error content. v0.31 e2e tests
|
||||
@@ -256,8 +301,19 @@ export async function dispatchToolCall(
|
||||
const safeParams = params || {};
|
||||
const validationError = validateParams(op, safeParams);
|
||||
if (validationError) {
|
||||
logVerb(false);
|
||||
// [c7] verb validation errors speak the protocol envelope (suggestion +
|
||||
// protocol_version); non-verb ops keep the pre-existing shape untouched.
|
||||
const envelope = isVerb
|
||||
? {
|
||||
error: 'invalid_params',
|
||||
message: validationError,
|
||||
suggestion: 'Check the tool schema — required params and types are declared there.',
|
||||
protocol_version: MEMORY_VERBS_VERSION,
|
||||
}
|
||||
: { error: 'invalid_params', message: validationError };
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify({ error: 'invalid_params', message: validationError }, null, 2) }],
|
||||
content: [{ type: 'text', text: JSON.stringify(envelope, null, 2) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
@@ -286,6 +342,14 @@ export async function dispatchToolCall(
|
||||
// a silent hole. See CLIENT_FENCED_WRITE_OPS in operations.ts.
|
||||
enforceBoundClientOpAllowList(ctx.auth, op);
|
||||
const result = await op.handler(ctx, safeParams);
|
||||
// [E4] verb success metrics: budget drops + entity hit/miss when present.
|
||||
{
|
||||
const r = result as { dropped_count?: number; found?: boolean } | null;
|
||||
logVerb(true, {
|
||||
...(typeof r?.dropped_count === 'number' ? { budget_dropped: r.dropped_count } : {}),
|
||||
...(name === 'entity' && typeof r?.found === 'boolean' ? { entity_found: r.found } : {}),
|
||||
});
|
||||
}
|
||||
const out: ToolResult = { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
// v0.31 (eD3 + eE4): best-effort _meta.brain_hot_memory injection.
|
||||
// The hook is wrapped in its own try/catch — any DB blip / cache miss /
|
||||
@@ -302,6 +366,7 @@ export async function dispatchToolCall(
|
||||
}
|
||||
return out;
|
||||
} catch (e: unknown) {
|
||||
logVerb(false);
|
||||
if (e instanceof OperationError) {
|
||||
return { content: [{ type: 'text', text: JSON.stringify(e.toJSON(), null, 2) }], isError: true };
|
||||
}
|
||||
@@ -310,8 +375,17 @@ export async function dispatchToolCall(
|
||||
// plain `Error: ${msg}` strings here, which broke any caller that
|
||||
// tried JSON.parse(content).
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
// [c7] verbs speak the protocol envelope even for uncaught throws.
|
||||
const envelope = isVerb
|
||||
? {
|
||||
error: 'internal',
|
||||
message: msg,
|
||||
suggestion: 'This is a server-side failure, not a caller mistake. Retry once; if it persists, run `gbrain doctor`.',
|
||||
protocol_version: MEMORY_VERBS_VERSION,
|
||||
}
|
||||
: { error: 'internal_error', message: msg };
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify({ error: 'internal_error', message: msg }, null, 2) }],
|
||||
content: [{ type: 'text', text: JSON.stringify(envelope, null, 2) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import { operations } from '../core/operations.ts';
|
||||
import type { AuthInfo } from '../core/operations.ts';
|
||||
import { VERSION } from '../version.ts';
|
||||
import { dispatchToolCall } from './dispatch.ts';
|
||||
import { filterOpsForSurface } from './surface.ts';
|
||||
import { buildDefaultLimiters, type RateLimiter } from './rate-limit.ts';
|
||||
import { sqlQueryForEngine } from '../core/sql-query.ts';
|
||||
import { parseLegacyTokenScope, parseTakesHoldersAllowList, coerceLegacyPermissions } from '../core/legacy-token-scope.ts';
|
||||
@@ -61,6 +62,12 @@ interface HttpTransportOptions {
|
||||
engine: BrainEngine;
|
||||
/** Override limiters (for tests). Defaults to env-driven buildDefaultLimiters. */
|
||||
limiters?: { ip: RateLimiter; token: RateLimiter };
|
||||
/**
|
||||
* MEMORY_VERBS v1 [c1]: tool-surface mode for this transport (the SECOND
|
||||
* HTTP path — the OAuth path in serve-http.ts carries its own). 'verbs' =
|
||||
* exactly the five protocol verbs; 'full' (default) = everything.
|
||||
*/
|
||||
surface?: 'verbs' | 'full';
|
||||
}
|
||||
|
||||
interface AuthResult {
|
||||
@@ -156,7 +163,13 @@ export async function startHttpTransport(opts: HttpTransportOptions) {
|
||||
const limiters = opts.limiters || buildDefaultLimiters();
|
||||
const bodyCap = envInt('GBRAIN_HTTP_MAX_BODY_BYTES', DEFAULT_BODY_CAP);
|
||||
const corsAllowlist = parseCorsAllowlist();
|
||||
const tools = buildToolDefs(operations);
|
||||
// MEMORY_VERBS v1 [c1]: surface filter applies to THIS transport too —
|
||||
// the advertised list AND dispatch (allowedOps), fail-closed.
|
||||
const surface = opts.surface ?? 'full';
|
||||
const surfacedOps = filterOpsForSurface(operations, surface);
|
||||
const surfaceAllowedOps: ReadonlySet<string> | undefined =
|
||||
surface === 'full' ? undefined : new Set(surfacedOps.map(o => o.name));
|
||||
const tools = buildToolDefs(surfacedOps);
|
||||
|
||||
/**
|
||||
* v0.41.3 (T6): single consolidated CORS header builder. Pre-fix there were
|
||||
@@ -411,6 +424,9 @@ export async function startHttpTransport(opts: HttpTransportOptions) {
|
||||
// #1336: thread the token's federated_read grant so read ops scope
|
||||
// to the operator-granted sources via sourceScopeOpts.
|
||||
auth: auth.auth,
|
||||
// MEMORY_VERBS v1 [c1/c2]: fail-closed surface enforcement here too.
|
||||
...(surfaceAllowedOps ? { allowedOps: surfaceAllowedOps } : {}),
|
||||
surface,
|
||||
});
|
||||
const status = result.isError ? 'error' : 'success';
|
||||
logRequest(auth.tokenName!, `tools/call:${toolName}`, status, Date.now() - startedMs);
|
||||
|
||||
+13
-2
@@ -6,6 +6,7 @@ import { operations } from '../core/operations.ts';
|
||||
import { VERSION } from '../version.ts';
|
||||
import { buildToolDefs } from './tool-defs.ts';
|
||||
import { dispatchToolCall, validateParams, buildOperationContext } from './dispatch.ts';
|
||||
import { filterOpsForSurface, allowedOpNames, type McpSurface } from './surface.ts';
|
||||
import { getBrainHotMemoryMeta } from '../core/facts/meta-hook.ts';
|
||||
import { loadConfig } from '../core/config.ts';
|
||||
import {
|
||||
@@ -15,17 +16,24 @@ import {
|
||||
} from '../core/context/resolve-ipc.ts';
|
||||
import { resolveEntitiesToPointers, logDeliveredReflexPointers } from '../core/context/retrieval-reflex.ts';
|
||||
|
||||
export async function startMcpServer(engine: BrainEngine) {
|
||||
export async function startMcpServer(engine: BrainEngine, opts: { surface?: McpSurface } = {}) {
|
||||
const server = new Server(
|
||||
{ name: 'gbrain', version: VERSION },
|
||||
{ capabilities: { tools: {} } },
|
||||
);
|
||||
|
||||
// MEMORY_VERBS v1 surface mode: 'full' (default — every op, byte-identical
|
||||
// to pre-surface behavior) or 'verbs' (exactly the 5 protocol verbs).
|
||||
// Enforced BOTH on the advertised list and in dispatch (fail-closed [c2]).
|
||||
const surface: McpSurface = opts.surface ?? 'full';
|
||||
const surfacedOps = filterOpsForSurface(operations, surface);
|
||||
const allowedOps = surface === 'full' ? undefined : allowedOpNames(operations, surface);
|
||||
|
||||
// Generate tool definitions from operations. Extracted to buildToolDefs so
|
||||
// the subagent tool registry (v0.15+) can call the same mapper against a
|
||||
// filtered OPERATIONS subset instead of duplicating this shape.
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: buildToolDefs(operations),
|
||||
tools: buildToolDefs(surfacedOps),
|
||||
}));
|
||||
|
||||
// Dispatch tool calls via shared dispatch.ts (parity with HTTP transport).
|
||||
@@ -70,6 +78,9 @@ export async function startMcpServer(engine: BrainEngine) {
|
||||
// Code see the brain's relevant hot memory automatically alongside
|
||||
// every tool-call response. Best-effort; absorbs errors.
|
||||
metaHook: getBrainHotMemoryMeta,
|
||||
// MEMORY_VERBS v1: fail-closed surface enforcement + usage attribution.
|
||||
...(allowedOps ? { allowedOps } : {}),
|
||||
surface,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* MEMORY_VERBS v1 — MCP tool-surface modes (Cathedral 1).
|
||||
*
|
||||
* 'full' (default) — every operation, verbs included. Existing installs
|
||||
* see no change; e2e tool-count assertions hold.
|
||||
* 'verbs' — EXACTLY the five frozen protocol verbs (ops marked
|
||||
* `verb: true`). The quickstart surface: agents see
|
||||
* recall/remember/entity/synthesize/forget and nothing
|
||||
* else.
|
||||
*
|
||||
* Enforcement is two-layer and fail-closed: ListTools advertises the filtered
|
||||
* set, AND dispatchToolCall receives the same set as `allowedOps` so a hidden
|
||||
* op stays uncallable even if a client guesses its name (tool-list filtering
|
||||
* alone leaves dispatch resolving the global catalog — codex c2).
|
||||
*
|
||||
* Resolution: --surface flag > config `mcp_surface` > 'full'. Why default
|
||||
* full: verbs is for agents and quickstarts; full preserves existing advanced
|
||||
* tooling.
|
||||
*/
|
||||
|
||||
import type { Operation } from '../core/operations.ts';
|
||||
import type { GBrainConfig } from '../core/config.ts';
|
||||
|
||||
export type McpSurface = 'verbs' | 'full';
|
||||
|
||||
export function isMcpSurface(v: unknown): v is McpSurface {
|
||||
return v === 'verbs' || v === 'full';
|
||||
}
|
||||
|
||||
/** Strict flag parser — unknown values reject loudly (parseStdioIdleTimeout pattern). */
|
||||
export function parseSurfaceFlag(args: string[]): McpSurface | null {
|
||||
const idx = args.indexOf('--surface');
|
||||
if (idx < 0) return null;
|
||||
const raw = args[idx + 1];
|
||||
if (raw === undefined || raw.startsWith('--')) {
|
||||
throw new Error(`--surface requires a value: verbs | full`);
|
||||
}
|
||||
if (!isMcpSurface(raw)) {
|
||||
throw new Error(`Unknown --surface "${raw}". Use: verbs (the 5 memory verbs) | full (all operations, default)`);
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
/** Flag > config `mcp_surface` > 'full'. */
|
||||
export function resolveSurface(
|
||||
flag: McpSurface | null,
|
||||
config: Pick<GBrainConfig, 'mcp_surface'> | null | undefined,
|
||||
): McpSurface {
|
||||
if (flag) return flag;
|
||||
if (config && isMcpSurface(config.mcp_surface)) return config.mcp_surface;
|
||||
return 'full';
|
||||
}
|
||||
|
||||
export function filterOpsForSurface(ops: Operation[], surface: McpSurface): Operation[] {
|
||||
if (surface === 'full') return ops;
|
||||
return ops.filter(op => op.verb === true);
|
||||
}
|
||||
|
||||
/** The fail-closed allow-set handed to dispatchToolCall. */
|
||||
export function allowedOpNames(ops: Operation[], surface: McpSurface): ReadonlySet<string> {
|
||||
return new Set(filterOpsForSurface(ops, surface).map(o => o.name));
|
||||
}
|
||||
@@ -8,6 +8,17 @@ export interface McpToolDef {
|
||||
properties: Record<string, unknown>;
|
||||
required: string[];
|
||||
};
|
||||
/**
|
||||
* MCP ToolAnnotations (SDK 1.29+), emitted ONLY when the op defines them —
|
||||
* existing tools keep byte-identical definitions (the byte-equality
|
||||
* regression test depends on absent keys staying absent).
|
||||
*/
|
||||
annotations?: {
|
||||
title?: string;
|
||||
readOnlyHint?: boolean;
|
||||
destructiveHint?: boolean;
|
||||
idempotentHint?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,5 +61,6 @@ export function buildToolDefs(ops: Operation[]): McpToolDef[] {
|
||||
.filter(([, v]) => v.required)
|
||||
.map(([k]) => k),
|
||||
},
|
||||
...(op.annotations ? { annotations: op.annotations } : {}),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('buildPlan — diff against completed + installed VERSION', () => {
|
||||
// autopilot cooperative, v0.16.0 = subagent runtime, v0.18.0 = multi-
|
||||
// source brains, v0.18.1 = RLS hardening, v0.21.0 = Cathedral II
|
||||
// (renumbered from v0.20.0 after master shipped v0.20.x in parallel).
|
||||
expect(plan.skippedFuture.map(m => m.version)).toEqual(['0.12.0', '0.12.2', '0.13.0', '0.13.1', '0.14.0', '0.16.0', '0.18.0', '0.18.1', '0.21.0', '0.22.4', '0.28.0', '0.29.1', '0.31.0', '0.32.2']);
|
||||
expect(plan.skippedFuture.map(m => m.version)).toEqual(['0.12.0', '0.12.2', '0.13.0', '0.13.1', '0.14.0', '0.16.0', '0.18.0', '0.18.1', '0.21.0', '0.22.4', '0.28.0', '0.29.1', '0.31.0', '0.32.2', '0.43.0']);
|
||||
});
|
||||
|
||||
test('already applied → v0.11.0 lands in `applied` bucket, not pending', () => {
|
||||
@@ -148,7 +148,7 @@ describe('buildPlan — diff against completed + installed VERSION', () => {
|
||||
// v0.22.4, v0.28.0, v0.29.1, v0.31.0 were added later; installed=0.12.0
|
||||
// means they belong in skippedFuture, not pending. v0.11.0 and v0.12.0
|
||||
// stay pending despite being ≤ installed — that is the H9 invariant.
|
||||
expect(plan.skippedFuture.map(m => m.version)).toEqual(['0.12.2', '0.13.0', '0.13.1', '0.14.0', '0.16.0', '0.18.0', '0.18.1', '0.21.0', '0.22.4', '0.28.0', '0.29.1', '0.31.0', '0.32.2']);
|
||||
expect(plan.skippedFuture.map(m => m.version)).toEqual(['0.12.2', '0.13.0', '0.13.1', '0.14.0', '0.16.0', '0.18.0', '0.18.1', '0.21.0', '0.22.4', '0.28.0', '0.29.1', '0.31.0', '0.32.2', '0.43.0']);
|
||||
});
|
||||
|
||||
test('--migration filter narrows to one version', () => {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// #2416: CLI wiring for the concept-shaped search nudge. Same in-process
|
||||
// pattern as cli-query-image.test.ts (import the helper from src/cli.ts —
|
||||
// safe, the import.meta.main seam guards top-level execution). The helper is
|
||||
// called from BOTH result paths (local engine + thin-client routed), so
|
||||
// pinning its behavior here covers the shared wiring; the per-path call
|
||||
// sites are two-liners.
|
||||
|
||||
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
||||
import { maybePrintConceptNudge } from '../src/cli.ts';
|
||||
import {
|
||||
DEFAULT_CLI_OPTIONS,
|
||||
setCliOptions,
|
||||
_resetCliOptionsForTest,
|
||||
} from '../src/core/cli-options.ts';
|
||||
|
||||
let stderrChunks: string[];
|
||||
let originalWrite: typeof process.stderr.write;
|
||||
|
||||
beforeEach(() => {
|
||||
stderrChunks = [];
|
||||
originalWrite = process.stderr.write.bind(process.stderr);
|
||||
process.stderr.write = ((chunk: string | Uint8Array) => {
|
||||
stderrChunks.push(String(chunk));
|
||||
return true;
|
||||
}) as typeof process.stderr.write;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.stderr.write = originalWrite;
|
||||
_resetCliOptionsForTest();
|
||||
});
|
||||
|
||||
const CONCEPT_QUERY = 'all the companies that do offshore wind';
|
||||
|
||||
describe('#2416 — maybePrintConceptNudge wiring', () => {
|
||||
test('concept-shaped search emits the one-line query nudge to stderr', () => {
|
||||
setCliOptions({ ...DEFAULT_CLI_OPTIONS });
|
||||
maybePrintConceptNudge('search', { query: CONCEPT_QUERY });
|
||||
expect(stderrChunks.length).toBe(1);
|
||||
expect(stderrChunks[0]).toContain('gbrain query');
|
||||
expect(stderrChunks[0]).toContain('not proof of completeness');
|
||||
expect(stderrChunks[0].endsWith('\n')).toBe(true);
|
||||
});
|
||||
|
||||
test('exact-token search stays silent', () => {
|
||||
setCliOptions({ ...DEFAULT_CLI_OPTIONS });
|
||||
maybePrintConceptNudge('search', { query: 'stripe' });
|
||||
expect(stderrChunks.length).toBe(0);
|
||||
});
|
||||
|
||||
test('--quiet silences the nudge even for concept-shaped queries', () => {
|
||||
setCliOptions({ ...DEFAULT_CLI_OPTIONS, quiet: true });
|
||||
maybePrintConceptNudge('search', { query: CONCEPT_QUERY });
|
||||
expect(stderrChunks.length).toBe(0);
|
||||
});
|
||||
|
||||
test('the query op never nudges (only search does)', () => {
|
||||
setCliOptions({ ...DEFAULT_CLI_OPTIONS });
|
||||
maybePrintConceptNudge('query', { query: CONCEPT_QUERY });
|
||||
expect(stderrChunks.length).toBe(0);
|
||||
});
|
||||
|
||||
test('missing query param is a silent no-op, not a crash', () => {
|
||||
setCliOptions({ ...DEFAULT_CLI_OPTIONS });
|
||||
maybePrintConceptNudge('search', {});
|
||||
expect(stderrChunks.length).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -158,3 +158,29 @@ describeIfKey('v0.29 — LLM routes personal queries to v0.29 ops, not query() /
|
||||
}, 30_000);
|
||||
}
|
||||
});
|
||||
|
||||
// #2416 — concept/landscape questions must route to `query` (hybrid +
|
||||
// expansion), not `search` (cheap-hybrid, expansion off). This block is the
|
||||
// feature assertion for the #2416 description edits; the block above is the
|
||||
// regression guard (the new "prefer query for concept questions" copy must
|
||||
// NOT pull personal queries away from the salience ops).
|
||||
const CONCEPT_QUERY_PHRASINGS = [
|
||||
'find all the companies doing offshore wind',
|
||||
'the landscape of agent memory startups',
|
||||
'every investor that focuses on climate tech',
|
||||
'all the projects that use vector databases',
|
||||
'everything about my fundraising strategy discussions',
|
||||
'which portfolio companies have shipped AI features',
|
||||
'the ecosystem of MCP server implementations',
|
||||
'all the people that work on developer tools',
|
||||
];
|
||||
|
||||
describeIfKey('#2416 — LLM routes concept/landscape questions to query, not search', () => {
|
||||
for (const prompt of CONCEPT_QUERY_PHRASINGS) {
|
||||
test(`routes "${prompt}" to query`, async () => {
|
||||
const { tool } = await callClaudeWithTools(prompt);
|
||||
expect(tool).not.toBeNull();
|
||||
expect(tool).toBe('query');
|
||||
}, 30_000);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* MEMORY_VERBS v1 — entity() latency gate (Cathedral 1, frozen contract:
|
||||
* p99 < 100ms on a large corpus, zero LLM).
|
||||
*
|
||||
* Corpus: 20K pages / 100K links / 30K aliases / 40K facts seeded via
|
||||
* generate_series (pattern: entity-resolve-perf.slow.test.ts). 20 warmup +
|
||||
* 200 measured buildEntityCard calls over a mixed name set exercising all
|
||||
* three resolution arms (alias hit / exact title / slug-suffix) + misses.
|
||||
*
|
||||
* Two gates:
|
||||
* 1. HARD ABSOLUTE — p99 < 100ms × GBRAIN_PERF_BUDGET_MULTIPLIER (default 1;
|
||||
* loosen in CI only with evidence of runner noise). The protocol DOC
|
||||
* promises this number; the bound is op-layer latency (transport
|
||||
* excluded, as documented).
|
||||
* 2. RATIO GUARD (machine-independent) — entity p99 ≤ 50× max(getPage p50,
|
||||
* 1ms) on the same corpus. Calibration: the card is ~7 indexed reads +
|
||||
* a keyword search on the miss path, measured ~21× a 1ms-floored
|
||||
* getPage at 20K pages — an O(N) scan regression lands at 200ms+
|
||||
* (≥200×), far past the ceiling even on a slow runner, while the
|
||||
* 2.4× headroom absorbs planner noise.
|
||||
*
|
||||
* The 200K-page validation is a documented MANUAL recipe in
|
||||
* docs/protocol/MEMORY_VERBS_v1.md — not CI-gated (seed time would dominate).
|
||||
*
|
||||
* .slow.test.ts suffix keeps it out of the fast loop (`bun run test:slow`).
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
|
||||
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
||||
import { buildEntityCard } from '../src/core/verbs/entity-card.ts';
|
||||
|
||||
let engine: PGLiteEngine;
|
||||
|
||||
const PAGES = 20_000;
|
||||
const LINKS = 100_000;
|
||||
const ALIASES = 30_000;
|
||||
const FACTS = 40_000;
|
||||
const WARMUP = 20;
|
||||
const MEASURED = 200;
|
||||
const TARGET_ENTITIES = 50; // pages the measured calls rotate over
|
||||
|
||||
const P99_BUDGET_MS = 100 * (Number(process.env.GBRAIN_PERF_BUDGET_MULTIPLIER) || 1);
|
||||
// entity p99 ≤ 50× max(getPage p50, 1ms) — see the calibration note above.
|
||||
const RATIO_CEILING = 50;
|
||||
|
||||
function percentile(sorted: number[], p: number): number {
|
||||
const idx = Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1);
|
||||
return sorted[Math.max(0, idx)];
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
engine = new PGLiteEngine();
|
||||
await engine.connect({});
|
||||
await engine.initSchema();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const db = (engine as any).db;
|
||||
|
||||
// Target entities (real putPage so frontmatter/title behave like prod pages).
|
||||
for (let i = 0; i < TARGET_ENTITIES; i++) {
|
||||
const slug = `people/target-person-${i}`;
|
||||
await engine.putPage(slug, {
|
||||
type: 'person',
|
||||
title: `Target Person ${i}`,
|
||||
compiled_truth: `# Target Person ${i}\n\nRuns area ${i} at a-company. Synthetic perf-corpus entity.`,
|
||||
frontmatter: { type: 'person', title: `Target Person ${i}`, slug, summary: `Synthetic target ${i} for the entity-card latency gate.` },
|
||||
}, { sourceId: 'default' });
|
||||
}
|
||||
|
||||
// Filler pages in one generate_series insert.
|
||||
await db.query(
|
||||
`INSERT INTO pages (slug, type, title, compiled_truth, frontmatter, source_id, created_at, updated_at)
|
||||
SELECT 'filler/page-' || gs::text, 'note', 'Filler ' || gs::text, '# Filler', '{}', 'default', NOW(), NOW()
|
||||
FROM generate_series(1, ${PAGES}) gs`,
|
||||
);
|
||||
|
||||
// Links: filler→filler hub noise plus a fan-in/out around every target
|
||||
// (the card reads getLinks/getBacklinks — targets must have real edges).
|
||||
await db.query(
|
||||
`INSERT INTO links (from_page_id, to_page_id, link_type, link_source)
|
||||
SELECT p1.id, p2.id, 'mentions', 'mentions'
|
||||
FROM (SELECT id, row_number() OVER (ORDER BY id) rn FROM pages WHERE slug LIKE 'filler/%') p1
|
||||
JOIN (SELECT id, row_number() OVER (ORDER BY id) rn FROM pages WHERE slug LIKE 'filler/%') p2
|
||||
ON p2.rn = ((p1.rn * 7919) % ${PAGES}) + 1 AND p1.id <> p2.id
|
||||
CROSS JOIN generate_series(1, ${Math.ceil(LINKS / PAGES)}) g
|
||||
ON CONFLICT DO NOTHING`,
|
||||
);
|
||||
await db.query(
|
||||
`INSERT INTO links (from_page_id, to_page_id, link_type, link_source)
|
||||
SELECT t.id, f.id, 'works_at', 'markdown'
|
||||
FROM (SELECT id, row_number() OVER (ORDER BY id) rn FROM pages WHERE slug LIKE 'people/target-%') t
|
||||
JOIN (SELECT id, row_number() OVER (ORDER BY id) rn FROM pages WHERE slug LIKE 'filler/%' LIMIT 2000) f
|
||||
ON (f.rn % ${TARGET_ENTITIES}) + 1 = t.rn
|
||||
ON CONFLICT DO NOTHING`,
|
||||
);
|
||||
|
||||
// Aliases: bulk noise + 2 aliases per target.
|
||||
await db.query(
|
||||
`INSERT INTO page_aliases (source_id, alias_norm, slug)
|
||||
SELECT 'default', 'alias noise ' || gs::text, 'filler/page-' || ((gs % ${PAGES}) + 1)::text
|
||||
FROM generate_series(1, ${ALIASES}) gs
|
||||
ON CONFLICT DO NOTHING`,
|
||||
);
|
||||
for (let i = 0; i < TARGET_ENTITIES; i++) {
|
||||
await db.query(
|
||||
`INSERT INTO page_aliases (source_id, alias_norm, slug) VALUES
|
||||
('default', $1, $2), ('default', $3, $2)
|
||||
ON CONFLICT DO NOTHING`,
|
||||
[`tp${i}`, `people/target-person-${i}`, `target alias ${i}`],
|
||||
);
|
||||
}
|
||||
|
||||
// Facts: bulk noise across fillers + 20 active facts per target entity.
|
||||
await db.query(
|
||||
`INSERT INTO facts (source_id, entity_slug, fact, kind, visibility, notability, valid_from, source, confidence, created_at)
|
||||
SELECT 'default', 'filler/page-' || ((gs % ${PAGES}) + 1)::text,
|
||||
'noise fact ' || gs::text, 'fact', 'world', 'medium', NOW(), 'perf-seed', 1.0, NOW()
|
||||
FROM generate_series(1, ${FACTS - TARGET_ENTITIES * 20}) gs`,
|
||||
);
|
||||
await db.query(
|
||||
`INSERT INTO facts (source_id, entity_slug, fact, kind, visibility, notability, valid_from, source, confidence, created_at)
|
||||
SELECT 'default', 'people/target-person-' || t::text,
|
||||
'target fact ' || g::text || ' about person ' || t::text,
|
||||
CASE WHEN g % 5 = 0 THEN 'commitment' ELSE 'fact' END,
|
||||
'world', 'medium', NOW(), 'perf-seed', 1.0, NOW()
|
||||
FROM generate_series(0, ${TARGET_ENTITIES - 1}) t, generate_series(1, 20) g`,
|
||||
);
|
||||
}, 300_000);
|
||||
|
||||
afterAll(async () => {
|
||||
await engine.disconnect();
|
||||
});
|
||||
|
||||
describe('entity card p99 latency gate', () => {
|
||||
it(`p99 < ${P99_BUDGET_MS}ms on ${PAGES} pages AND ≤ ${RATIO_CEILING}× getPage p50`, async () => {
|
||||
// Mixed name set: alias hits, exact titles, namespaced slugs, suffixes, misses.
|
||||
const names: string[] = [];
|
||||
for (let i = 0; i < TARGET_ENTITIES; i++) {
|
||||
names.push(`tp${i}`); // alias arm
|
||||
names.push(`Target Person ${i}`); // exact-title arm
|
||||
names.push(`people/target-person-${i}`); // exact-slug arm
|
||||
names.push(`target-person-${i}`); // slug-suffix arm
|
||||
names.push(`zzz-absent-${i}`); // miss (suggestions path)
|
||||
}
|
||||
|
||||
for (let i = 0; i < WARMUP; i++) {
|
||||
await buildEntityCard(engine, 'default', names[i % names.length], { remote: true });
|
||||
}
|
||||
|
||||
const samples: number[] = [];
|
||||
for (let i = 0; i < MEASURED; i++) {
|
||||
const name = names[(i * 13) % names.length];
|
||||
const t0 = performance.now();
|
||||
await buildEntityCard(engine, 'default', name, { remote: true });
|
||||
samples.push(performance.now() - t0);
|
||||
}
|
||||
samples.sort((a, b) => a - b);
|
||||
const p50 = percentile(samples, 50);
|
||||
const p99 = percentile(samples, 99);
|
||||
|
||||
// Ratio baseline: getPage p50 on the same corpus.
|
||||
const pageSamples: number[] = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const t0 = performance.now();
|
||||
await engine.getPage(`people/target-person-${i % TARGET_ENTITIES}`, { sourceId: 'default' });
|
||||
pageSamples.push(performance.now() - t0);
|
||||
}
|
||||
pageSamples.sort((a, b) => a - b);
|
||||
const pageP50 = Math.max(percentile(pageSamples, 50), 1.0); // 1ms floor vs sub-ms division noise
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`[entity-card-perf] corpus=${PAGES}p+${LINKS}l+${ALIASES}a+${FACTS}f ` +
|
||||
`entity p50=${p50.toFixed(2)}ms p99=${p99.toFixed(2)}ms | getPage p50=${pageP50.toFixed(2)}ms ` +
|
||||
`| ratio=${(p99 / pageP50).toFixed(1)}x (ceiling ${RATIO_CEILING}x) | budget=${P99_BUDGET_MS}ms`,
|
||||
);
|
||||
|
||||
expect(p99).toBeLessThan(P99_BUDGET_MS);
|
||||
expect(p99 / pageP50).toBeLessThanOrEqual(RATIO_CEILING);
|
||||
}, 300_000);
|
||||
});
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
[
|
||||
{
|
||||
"name": "remember rejects missing provenance (provenance_required + suggestion)",
|
||||
"verb": "remember",
|
||||
"params": {
|
||||
"fact": "conformance {{marker}} fact without provenance"
|
||||
},
|
||||
"expectErrorCode": "invalid_params",
|
||||
"expectSuggestion": false
|
||||
},
|
||||
{
|
||||
"name": "remember rejects empty provenance (provenance_required + suggestion)",
|
||||
"verb": "remember",
|
||||
"params": {
|
||||
"fact": "conformance {{marker}} fact empty provenance",
|
||||
"provenance": " "
|
||||
},
|
||||
"expectErrorCode": "provenance_required",
|
||||
"expectSuggestion": true
|
||||
},
|
||||
{
|
||||
"name": "remember rejects ISO-8601 duration ttl with a fix (P30D trap)",
|
||||
"verb": "remember",
|
||||
"params": {
|
||||
"fact": "conformance {{marker}} ttl trap",
|
||||
"provenance": "conformance run",
|
||||
"ttl": "P30D"
|
||||
},
|
||||
"expectErrorCode": "invalid_params",
|
||||
"expectSuggestion": true
|
||||
},
|
||||
{
|
||||
"name": "remember writes a fact (string id, enum status, echoed nulls)",
|
||||
"verb": "remember",
|
||||
"params": {
|
||||
"fact": "conformance {{marker}}: the protocol round-trip fact",
|
||||
"provenance": "conformance run {{marker}}",
|
||||
"entity": "people/conformance-{{marker}}",
|
||||
"kind": "fact"
|
||||
},
|
||||
"validateSchema": true,
|
||||
"expect": [
|
||||
{
|
||||
"path": "id",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": "status",
|
||||
"oneOf": [
|
||||
"inserted",
|
||||
"duplicate",
|
||||
"superseded"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "protocol_version",
|
||||
"equals": 1
|
||||
}
|
||||
],
|
||||
"saveAs": {
|
||||
"key": "fact1",
|
||||
"path": "id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "remember with ttl returns ISO valid_until",
|
||||
"verb": "remember",
|
||||
"params": {
|
||||
"fact": "conformance {{marker}}: expiring fact",
|
||||
"provenance": "conformance run {{marker}}",
|
||||
"entity": "people/conformance-{{marker}}",
|
||||
"ttl": "30d"
|
||||
},
|
||||
"validateSchema": true,
|
||||
"expect": [
|
||||
{
|
||||
"path": "valid_until",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"saveAs": {
|
||||
"key": "fact2",
|
||||
"path": "id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "remember private fact (fence test setup)",
|
||||
"verb": "remember",
|
||||
"params": {
|
||||
"fact": "conformance {{marker}} PRIVATE-SENTINEL commitment",
|
||||
"provenance": "conformance run {{marker}}",
|
||||
"entity": "people/conformance-{{marker}}",
|
||||
"kind": "commitment",
|
||||
"visibility": "private"
|
||||
},
|
||||
"validateSchema": true
|
||||
},
|
||||
{
|
||||
"name": "recall by entity round-trips the remembered fact (superset envelope)",
|
||||
"verb": "recall",
|
||||
"params": {
|
||||
"entity": "people/conformance-{{marker}}"
|
||||
},
|
||||
"validateSchema": true,
|
||||
"expect": [
|
||||
{
|
||||
"path": "protocol_version",
|
||||
"equals": 1
|
||||
},
|
||||
{
|
||||
"path": "total",
|
||||
"gte": 1
|
||||
},
|
||||
{
|
||||
"path": "facts.0.fact_id",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": "facts.0.provenance",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "recall with budget reports consistent budget meta",
|
||||
"verb": "recall",
|
||||
"params": {
|
||||
"entity": "people/conformance-{{marker}}",
|
||||
"budget_tokens": 10000
|
||||
},
|
||||
"validateSchema": true,
|
||||
"expect": [
|
||||
{
|
||||
"path": "budget_tokens",
|
||||
"equals": 10000
|
||||
},
|
||||
{
|
||||
"path": "budget_used",
|
||||
"gte": 0
|
||||
},
|
||||
{
|
||||
"path": "budget_used",
|
||||
"lte": 10000
|
||||
},
|
||||
{
|
||||
"path": "dropped_count",
|
||||
"gte": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "recall with budget smaller than the first item drops everything",
|
||||
"verb": "recall",
|
||||
"params": {
|
||||
"entity": "people/conformance-{{marker}}",
|
||||
"budget_tokens": 1
|
||||
},
|
||||
"validateSchema": true,
|
||||
"expect": [
|
||||
{
|
||||
"path": "total",
|
||||
"equals": 0
|
||||
},
|
||||
{
|
||||
"path": "dropped_count",
|
||||
"gte": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "recall with query returns the search arm (degradation allowed, never an error)",
|
||||
"verb": "recall",
|
||||
"params": {
|
||||
"query": "conformance {{marker}} protocol round-trip",
|
||||
"budget_tokens": 8000
|
||||
},
|
||||
"validateSchema": true,
|
||||
"expect": [
|
||||
{
|
||||
"path": "results",
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "entity resolves the round-trip entity to a card",
|
||||
"verb": "entity",
|
||||
"params": {
|
||||
"name": "people/conformance-{{marker}}"
|
||||
},
|
||||
"validateSchema": true,
|
||||
"requiresSeededEntity": true,
|
||||
"expect": [
|
||||
{
|
||||
"path": "found",
|
||||
"equals": true
|
||||
},
|
||||
{
|
||||
"path": "protocol_version",
|
||||
"equals": 1
|
||||
},
|
||||
{
|
||||
"path": "card.entity.slug",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": "card.backlink_count",
|
||||
"gte": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "entity miss returns found:false + suggestions (never an error)",
|
||||
"verb": "entity",
|
||||
"params": {
|
||||
"name": "zzz-no-such-entity-{{marker}}"
|
||||
},
|
||||
"validateSchema": true,
|
||||
"expect": [
|
||||
{
|
||||
"path": "found",
|
||||
"equals": false
|
||||
},
|
||||
{
|
||||
"path": "suggestions",
|
||||
"type": "array"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "entity card never leaks private facts to remote callers (fence test)",
|
||||
"verb": "entity",
|
||||
"params": {
|
||||
"name": "people/conformance-{{marker}}"
|
||||
},
|
||||
"requiresSeededEntity": true,
|
||||
"expect": [
|
||||
{
|
||||
"path": "card.open_threads",
|
||||
"absentOrNotContains": "PRIVATE-SENTINEL"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "forget expires the remembered fact",
|
||||
"verb": "forget",
|
||||
"params": {
|
||||
"id": "{{id:fact2}}",
|
||||
"reason": "conformance cleanup"
|
||||
},
|
||||
"validateSchema": true,
|
||||
"expect": [
|
||||
{
|
||||
"path": "expired",
|
||||
"equals": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "forget again is idempotent (expired:false, success)",
|
||||
"verb": "forget",
|
||||
"params": {
|
||||
"id": "{{id:fact2}}"
|
||||
},
|
||||
"validateSchema": true,
|
||||
"expect": [
|
||||
{
|
||||
"path": "expired",
|
||||
"equals": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "forget unknown id returns not_found with a suggestion",
|
||||
"verb": "forget",
|
||||
"params": {
|
||||
"id": "999999999"
|
||||
},
|
||||
"expectErrorCode": "not_found",
|
||||
"expectSuggestion": true
|
||||
},
|
||||
{
|
||||
"name": "synthesize answers (or reports unavailable) — cost-gated, pass --synthesize",
|
||||
"verb": "synthesize",
|
||||
"params": {
|
||||
"question": "What do we know about conformance {{marker}}?"
|
||||
},
|
||||
"requiresSynthesizeFlag": true
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* MEMORY_VERBS v1 — surface-mode tests (Cathedral 1).
|
||||
*
|
||||
* - 'verbs' filters to EXACTLY the five protocol verbs
|
||||
* - 'full' is the identity (existing installs unchanged)
|
||||
* - dispatch-layer allowedOps is FAIL-CLOSED: a hidden op is uncallable
|
||||
* (unknown_tool), not merely unlisted [c2]
|
||||
* - flag parsing is strict (unknown value rejects loudly)
|
||||
* - resolution: flag > config mcp_surface > 'full'
|
||||
*/
|
||||
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'bun:test';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
||||
import { operations } from '../src/core/operations.ts';
|
||||
import { VERB_NAMES } from '../src/core/verbs.ts';
|
||||
import {
|
||||
filterOpsForSurface,
|
||||
allowedOpNames,
|
||||
parseSurfaceFlag,
|
||||
resolveSurface,
|
||||
} from '../src/mcp/surface.ts';
|
||||
import { dispatchToolCall } from '../src/mcp/dispatch.ts';
|
||||
import { __setUsageLogPathForTests } from '../src/core/verbs/usage-log.ts';
|
||||
|
||||
let engine: PGLiteEngine;
|
||||
let home: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Sidecar writes go to a temp file via the test seam — no global env mutation.
|
||||
home = mkdtempSync(join(tmpdir(), 'gbrain-surface-test-'));
|
||||
__setUsageLogPathForTests(join(home, 'usage.jsonl'));
|
||||
engine = new PGLiteEngine();
|
||||
await engine.connect({});
|
||||
await engine.initSchema();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await engine.disconnect();
|
||||
__setUsageLogPathForTests(null);
|
||||
try { rmSync(home, { recursive: true, force: true }); } catch { /* best-effort */ }
|
||||
});
|
||||
|
||||
describe('filterOpsForSurface', () => {
|
||||
it("'verbs' returns exactly the five protocol verbs", () => {
|
||||
const names = filterOpsForSurface(operations, 'verbs').map(o => o.name).sort();
|
||||
expect(names).toEqual([...VERB_NAMES].sort());
|
||||
});
|
||||
|
||||
it("'full' is the identity — existing installs see every op", () => {
|
||||
expect(filterOpsForSurface(operations, 'full')).toEqual(operations);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dispatch allowedOps — fail-closed [c2]', () => {
|
||||
it('a hidden op returns unknown_tool even when called by name', async () => {
|
||||
const allowed = allowedOpNames(operations, 'verbs');
|
||||
const res = await dispatchToolCall(engine, 'get_page', { slug: 'x' }, {
|
||||
remote: true,
|
||||
sourceId: 'default',
|
||||
allowedOps: allowed,
|
||||
surface: 'verbs',
|
||||
});
|
||||
expect(res.isError).toBe(true);
|
||||
const body = JSON.parse(res.content[0].text);
|
||||
expect(body.error).toBe('unknown_tool');
|
||||
});
|
||||
|
||||
it('a surfaced verb still dispatches under the same allowedOps set', async () => {
|
||||
const allowed = allowedOpNames(operations, 'verbs');
|
||||
const res = await dispatchToolCall(engine, 'entity', { name: 'zzz-nobody' }, {
|
||||
remote: true,
|
||||
sourceId: 'default',
|
||||
allowedOps: allowed,
|
||||
surface: 'verbs',
|
||||
});
|
||||
expect(res.isError ?? false).toBe(false);
|
||||
const body = JSON.parse(res.content[0].text);
|
||||
expect(body.found).toBe(false);
|
||||
});
|
||||
|
||||
it('without allowedOps (full surface) every op stays callable — pre-existing behavior', async () => {
|
||||
const res = await dispatchToolCall(engine, 'get_stats', {}, {
|
||||
remote: true,
|
||||
sourceId: 'default',
|
||||
});
|
||||
expect(res.isError ?? false).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseSurfaceFlag + resolveSurface', () => {
|
||||
it('parses verbs/full, rejects unknown values loudly, requires a value', () => {
|
||||
expect(parseSurfaceFlag(['--surface', 'verbs'])).toBe('verbs');
|
||||
expect(parseSurfaceFlag(['--surface', 'full'])).toBe('full');
|
||||
expect(parseSurfaceFlag(['serve'])).toBe(null);
|
||||
expect(() => parseSurfaceFlag(['--surface', 'all'])).toThrow(/Unknown --surface/);
|
||||
expect(() => parseSurfaceFlag(['--surface'])).toThrow(/requires a value/);
|
||||
expect(() => parseSurfaceFlag(['--surface', '--http'])).toThrow(/requires a value/);
|
||||
});
|
||||
|
||||
it('resolution: flag > config mcp_surface > full', () => {
|
||||
expect(resolveSurface('verbs', { mcp_surface: 'full' })).toBe('verbs');
|
||||
expect(resolveSurface(null, { mcp_surface: 'verbs' })).toBe('verbs');
|
||||
expect(resolveSurface(null, {})).toBe('full');
|
||||
expect(resolveSurface(null, null)).toBe('full');
|
||||
expect(resolveSurface(null, { mcp_surface: 'bogus' as never })).toBe('full');
|
||||
});
|
||||
});
|
||||
@@ -56,6 +56,10 @@ function legacyInlineMap(ops: typeof operations) {
|
||||
.filter(([, v]) => v.required)
|
||||
.map(([k]) => k),
|
||||
},
|
||||
// MEMORY_VERBS v1: ToolAnnotations passthrough, emitted ONLY when the op
|
||||
// defines them. The byte-stability contract is per-op: ops WITHOUT
|
||||
// annotations keep the exact pre-v1 shape (pinned explicitly below).
|
||||
...(op.annotations ? { annotations: op.annotations } : {}),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -66,6 +70,17 @@ describe('buildToolDefs', () => {
|
||||
expect(JSON.stringify(extracted)).toBe(JSON.stringify(inline));
|
||||
});
|
||||
|
||||
test('ops without annotations keep the pre-annotations shape exactly (no annotations key)', () => {
|
||||
const extracted = buildToolDefs(operations);
|
||||
for (const def of extracted) {
|
||||
const op = operations.find(o => o.name === def.name)!;
|
||||
if (!op.annotations) {
|
||||
expect('annotations' in def).toBe(false);
|
||||
expect(Object.keys(def)).toEqual(['name', 'description', 'inputSchema']);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('preserves operation count', () => {
|
||||
expect(buildToolDefs(operations).length).toBe(operations.length);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,533 @@
|
||||
/**
|
||||
* MEMORY_VERBS v1 conformance — the frozen-contract pin (Cathedral 1).
|
||||
*
|
||||
* In-process through dispatchToolCall (the exact layer both MCP transports
|
||||
* share), against in-memory PGLite. Covers:
|
||||
* - recall legacy SUPERSET regression (G1B: legacy fields byte-equal,
|
||||
* additions allowed — protocol_version everywhere, no carve-out)
|
||||
* - server-side budget packing math (incl. budget < first item)
|
||||
* - query-arm keyword degradation (never an error without embeddings)
|
||||
* - remember contract: provenance_required, ttl forms (P30D trap), enum
|
||||
* kinds, world default + the remote remember→recall round-trip [F2],
|
||||
* private facts hidden from remote readers
|
||||
* - entity: card shape vs RESPONSE_SCHEMAS, three resolution arms,
|
||||
* miss→suggestions, ZERO-LLM guard (chat transport rigged to throw)
|
||||
* - synthesize: [EXPENSIVE prefix, annotations, clean `unavailable` with
|
||||
* suggestion when no LLM is configured [c10]
|
||||
* - forget: idempotency (expired:false), not_found with suggestion
|
||||
* - writeSingleFact supersession rule [X1] + degraded dedup (embed seam)
|
||||
* - negative conformance self-test [F3]: the runner FAILS a lying server
|
||||
* - fixture mirror drift guard (cases.json === embedded module)
|
||||
*/
|
||||
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
||||
import { resetPgliteState } from './helpers/reset-pglite.ts';
|
||||
import { operationsByName } from '../src/core/operations.ts';
|
||||
import type { OperationContext } from '../src/core/operations.ts';
|
||||
import { dispatchToolCall } from '../src/mcp/dispatch.ts';
|
||||
import { buildToolDefs } from '../src/mcp/tool-defs.ts';
|
||||
import { RESPONSE_SCHEMAS, ERROR_SCHEMA, VERB_NAMES } from '../src/core/verbs.ts';
|
||||
import {
|
||||
runConformance,
|
||||
validateAgainstSchema,
|
||||
type ConformanceClient,
|
||||
} from '../src/core/verbs/conformance.ts';
|
||||
import { CONFORMANCE_CASES } from '../src/core/verbs/conformance-fixtures.ts';
|
||||
import { writeSingleFact } from '../src/core/facts/write-single.ts';
|
||||
import {
|
||||
configureGateway,
|
||||
__setChatTransportForTests,
|
||||
__setEmbedTransportForTests,
|
||||
} from '../src/core/ai/gateway.ts';
|
||||
import { __setUsageLogPathForTests } from '../src/core/verbs/usage-log.ts';
|
||||
import { withEnv } from './helpers/with-env.ts';
|
||||
|
||||
let engine: PGLiteEngine;
|
||||
let home: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Sidecar writes go to a temp file via the test seam — no global env mutation.
|
||||
home = mkdtempSync(join(tmpdir(), 'gbrain-verbs-test-'));
|
||||
__setUsageLogPathForTests(join(home, 'usage.jsonl'));
|
||||
engine = new PGLiteEngine();
|
||||
await engine.connect({});
|
||||
await engine.initSchema();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await engine.disconnect();
|
||||
__setUsageLogPathForTests(null);
|
||||
try { rmSync(home, { recursive: true, force: true }); } catch { /* best-effort */ }
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await resetPgliteState(engine);
|
||||
__setChatTransportForTests(null);
|
||||
__setEmbedTransportForTests(null);
|
||||
});
|
||||
|
||||
function localCtx(sourceId = 'default'): OperationContext {
|
||||
return {
|
||||
engine,
|
||||
config: {},
|
||||
logger: { info: () => {}, warn: () => {}, error: () => {} },
|
||||
dryRun: false,
|
||||
remote: false,
|
||||
sourceId,
|
||||
} as unknown as OperationContext;
|
||||
}
|
||||
|
||||
/** Remote-shaped call through the shared dispatcher (what both transports do). */
|
||||
async function callRemote(name: string, params: Record<string, unknown>) {
|
||||
const res = await dispatchToolCall(engine, name, params, {
|
||||
remote: true,
|
||||
takesHoldersAllowList: ['world'],
|
||||
sourceId: 'default',
|
||||
});
|
||||
return { isError: res.isError === true, body: JSON.parse(res.content[0].text) };
|
||||
}
|
||||
|
||||
async function seedEntityPage(slug: string, title: string, body = 'A synthetic test entity.') {
|
||||
const put = operationsByName['put_page'];
|
||||
await put.handler(localCtx(), {
|
||||
slug,
|
||||
content: `---\ntitle: ${title}\ntype: person\n---\n\n# ${title}\n\n${body}\n`,
|
||||
});
|
||||
}
|
||||
|
||||
describe('recall — G1B superset + budget packing', () => {
|
||||
it('legacy-param recall keeps every legacy field shape and adds only the v1 fields', async () => {
|
||||
const r1 = await callRemote('remember', {
|
||||
fact: 'superset regression fact',
|
||||
provenance: 'conformance test',
|
||||
entity: 'people/superset-test',
|
||||
});
|
||||
expect(r1.isError).toBe(false);
|
||||
|
||||
const { isError, body } = await callRemote('recall', { entity: 'people/superset-test' });
|
||||
expect(isError).toBe(false);
|
||||
// Legacy envelope fields, unchanged shapes.
|
||||
expect(typeof body.total).toBe('number');
|
||||
expect(Array.isArray(body.facts)).toBe(true);
|
||||
const f = body.facts[0];
|
||||
const LEGACY_FACT_KEYS = [
|
||||
'id', 'fact', 'kind', 'entity_slug', 'visibility', 'notability', 'valid_from',
|
||||
'valid_until', 'expired_at', 'superseded_by', 'consolidated_at',
|
||||
'consolidated_into', 'source', 'source_session', 'confidence', 'created_at',
|
||||
];
|
||||
for (const k of LEGACY_FACT_KEYS) expect(k in f).toBe(true);
|
||||
expect(typeof f.id).toBe('number'); // legacy numeric id is FROZEN
|
||||
// v1 additions (G1B superset — on EVERY response, no carve-out).
|
||||
expect(body.protocol_version).toBe(1);
|
||||
expect(f.fact_id).toBe(String(f.id));
|
||||
expect(f.provenance).toBe(f.source);
|
||||
// No query/budget params → no search/budget fields.
|
||||
expect('results' in body).toBe(false);
|
||||
expect('budget_tokens' in body).toBe(false);
|
||||
});
|
||||
|
||||
it('budget packing reports consistent meta and drops everything under a 1-token budget', async () => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await callRemote('remember', {
|
||||
fact: `budget fact number ${i} with some padding text to cost tokens`,
|
||||
provenance: 'conformance test',
|
||||
entity: 'people/budget-test',
|
||||
});
|
||||
}
|
||||
const big = await callRemote('recall', { entity: 'people/budget-test', budget_tokens: 10000 });
|
||||
expect(big.body.budget_tokens).toBe(10000);
|
||||
expect(big.body.budget_used).toBeGreaterThan(0);
|
||||
expect(big.body.budget_used).toBeLessThanOrEqual(10000);
|
||||
expect(big.body.dropped_count).toBe(0);
|
||||
expect(big.body.total).toBe(3);
|
||||
|
||||
const tiny = await callRemote('recall', { entity: 'people/budget-test', budget_tokens: 1 });
|
||||
expect(tiny.body.total).toBe(0);
|
||||
expect(tiny.body.dropped_count).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
|
||||
it('query arm degrades to keyword-only without an embedding provider — never an error', async () => {
|
||||
await seedEntityPage('people/query-arm-test', 'Query Arm Marker Qzx');
|
||||
const { isError, body } = await callRemote('recall', { query: 'Query Arm Marker Qzx' });
|
||||
expect(isError).toBe(false);
|
||||
expect(Array.isArray(body.results)).toBe(true);
|
||||
expect(body.search_degraded).toBe('keyword_only_no_embedding_provider');
|
||||
const violations = validateAgainstSchema(body, RESPONSE_SCHEMAS.recall);
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remember — contract behavior', () => {
|
||||
it('rejects empty provenance with provenance_required + a populated suggestion', async () => {
|
||||
const { isError, body } = await callRemote('remember', { fact: 'x', provenance: ' ' });
|
||||
expect(isError).toBe(true);
|
||||
expect(body.error).toBe('provenance_required');
|
||||
expect(typeof body.suggestion).toBe('string');
|
||||
expect(body.suggestion.length).toBeGreaterThan(0);
|
||||
expect(body.protocol_version).toBe(1);
|
||||
});
|
||||
|
||||
it('rejects ISO-8601 duration ttl (P30D) with a self-correcting suggestion', async () => {
|
||||
const { isError, body } = await callRemote('remember', {
|
||||
fact: 'ttl trap', provenance: 'test', ttl: 'P30D',
|
||||
});
|
||||
expect(isError).toBe(true);
|
||||
expect(body.error).toBe('invalid_params');
|
||||
expect(body.suggestion).toContain('30d');
|
||||
});
|
||||
|
||||
it('accepts duration ttl and returns a future ISO valid_until; echoes null entity_slug', async () => {
|
||||
const { isError, body } = await callRemote('remember', {
|
||||
fact: 'expiring fact with ttl', provenance: 'test', ttl: '30d',
|
||||
});
|
||||
expect(isError).toBe(false);
|
||||
expect(typeof body.id).toBe('string');
|
||||
expect(body.status).toBe('inserted');
|
||||
expect(body.entity_slug).toBe(null); // omitted optional inputs echo as null
|
||||
expect(Date.parse(body.valid_until)).toBeGreaterThan(Date.now());
|
||||
const violations = validateAgainstSchema(body, RESPONSE_SCHEMAS.remember);
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
|
||||
it('remote remember→recall round-trip holds (world default [F2]); private facts stay hidden', async () => {
|
||||
await callRemote('remember', {
|
||||
fact: 'world-visible round-trip fact', provenance: 'test', entity: 'people/roundtrip-test',
|
||||
});
|
||||
await callRemote('remember', {
|
||||
fact: 'PRIVATE-SENTINEL fact', provenance: 'test', entity: 'people/roundtrip-test',
|
||||
visibility: 'private',
|
||||
});
|
||||
const { body } = await callRemote('recall', { entity: 'people/roundtrip-test' });
|
||||
const texts = body.facts.map((f: { fact: string }) => f.fact).join('|');
|
||||
expect(texts).toContain('world-visible round-trip fact');
|
||||
expect(texts).not.toContain('PRIVATE-SENTINEL');
|
||||
});
|
||||
});
|
||||
|
||||
describe('entity — card, arms, zero LLM', () => {
|
||||
it('resolves an exact namespaced slug to a schema-valid card with the chat gateway rigged to throw', async () => {
|
||||
__setChatTransportForTests(() => {
|
||||
throw new Error('entity must NEVER call the chat LLM');
|
||||
});
|
||||
await seedEntityPage('people/card-test', 'Card Test Person', 'Runs engineering at a-company.');
|
||||
const { isError, body } = await callRemote('entity', { name: 'people/card-test' });
|
||||
expect(isError).toBe(false);
|
||||
expect(body.found).toBe(true);
|
||||
expect(typeof body.latency_ms).toBe('number');
|
||||
expect(body.card.entity.slug).toBe('people/card-test');
|
||||
expect(body.card.summary.length).toBeGreaterThan(0);
|
||||
const violations = validateAgainstSchema(body, RESPONSE_SCHEMAS.entity);
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
|
||||
it('resolves by exact title and by slug suffix (arms 2)', async () => {
|
||||
await seedEntityPage('people/arm-test-alice', 'Arm Test Alice');
|
||||
const byTitle = await callRemote('entity', { name: 'Arm Test Alice' });
|
||||
expect(byTitle.body.found).toBe(true);
|
||||
const bySuffix = await callRemote('entity', { name: 'arm-test-alice' });
|
||||
expect(bySuffix.body.found).toBe(true);
|
||||
});
|
||||
|
||||
it('miss returns found:false + suggestions, never an error', async () => {
|
||||
await seedEntityPage('people/suggestion-source', 'Suggestion Source Person');
|
||||
const { isError, body } = await callRemote('entity', { name: 'zzz-definitely-absent-entity' });
|
||||
expect(isError).toBe(false);
|
||||
expect(body.found).toBe(false);
|
||||
expect(Array.isArray(body.suggestions)).toBe(true);
|
||||
const violations = validateAgainstSchema(body, RESPONSE_SCHEMAS.entity);
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
|
||||
it('[ship P1.2] entity card backlinks are source-isolated on BOTH sides (no foreign from_slug leak)', async () => {
|
||||
// Same slug exists in two sources; a foreign-source page links to the
|
||||
// default-source entity. The card must NOT surface the foreign edge/count.
|
||||
await seedEntityPage('people/iso-target', 'Iso Target');
|
||||
// Register the foreign tenant source (FK target) + a foreign page that
|
||||
// links INTO the default-source entity.
|
||||
await engine.executeRaw(
|
||||
`INSERT INTO sources (id, name, config) VALUES ('other', 'other-tenant', '{}'::jsonb)
|
||||
ON CONFLICT (id) DO NOTHING`,
|
||||
[],
|
||||
);
|
||||
await engine.executeRaw(
|
||||
`INSERT INTO pages (slug, type, title, compiled_truth, frontmatter, source_id, created_at, updated_at)
|
||||
VALUES ('people/foreign-linker', 'person', 'Foreign Linker', '# Foreign', '{}', 'other', NOW(), NOW())`,
|
||||
[],
|
||||
);
|
||||
await engine.executeRaw(
|
||||
`INSERT INTO links (from_page_id, to_page_id, link_type, link_source)
|
||||
SELECT f.id, t.id, 'works_at', 'markdown'
|
||||
FROM pages f, pages t
|
||||
WHERE f.slug = 'people/foreign-linker' AND f.source_id = 'other'
|
||||
AND t.slug = 'people/iso-target' AND t.source_id = 'default'`,
|
||||
[],
|
||||
);
|
||||
const { body } = await callRemote('entity', { name: 'people/iso-target' });
|
||||
expect(body.found).toBe(true);
|
||||
// The foreign cross-source backlink must not appear in edges OR the count.
|
||||
const edgeSlugs = (body.card.edges as Array<{ slug: string }>).map(e => e.slug);
|
||||
expect(edgeSlugs).not.toContain('people/foreign-linker');
|
||||
expect(body.card.backlink_count).toBe(0);
|
||||
});
|
||||
|
||||
it('remote card never carries private commitment facts (fence test)', async () => {
|
||||
await seedEntityPage('people/fence-test', 'Fence Test Person');
|
||||
await callRemote('remember', {
|
||||
fact: 'PRIVATE-SENTINEL commitment text', provenance: 'test',
|
||||
entity: 'people/fence-test', kind: 'commitment', visibility: 'private',
|
||||
});
|
||||
const { body } = await callRemote('entity', { name: 'people/fence-test' });
|
||||
expect(body.found).toBe(true);
|
||||
expect(JSON.stringify(body.card.open_threads)).not.toContain('PRIVATE-SENTINEL');
|
||||
});
|
||||
});
|
||||
|
||||
describe('synthesize — marked expensive + unavailable conversion [c10]', () => {
|
||||
it('description starts with [EXPENSIVE and the tool def carries annotations', () => {
|
||||
const op = operationsByName['synthesize'];
|
||||
expect(op.description.startsWith('[EXPENSIVE')).toBe(true);
|
||||
const def = buildToolDefs([op])[0];
|
||||
expect(def.annotations?.readOnlyHint).toBe(true);
|
||||
expect(def.annotations?.title).toContain('costly');
|
||||
});
|
||||
|
||||
it('delegates to runThink and returns the frozen envelope with a priced cost block (chat seam — no real LLM)', async () => {
|
||||
// Fully hermetic regardless of the dev/CI machine's ambient credentials:
|
||||
// runThink builds its client via a real-key check (hasAnthropicKey reads
|
||||
// process.env), NOT the gateway chat seam — so we must BOTH provide a fake
|
||||
// key via withEnv (so the client builds) AND install the chat seam (so no
|
||||
// real API call fires). Without the env key, CI (credential-free) takes the
|
||||
// NO_ANTHROPIC_API_KEY path → the verb's `unavailable` conversion → isError.
|
||||
__setChatTransportForTests(async () => ({
|
||||
text: JSON.stringify({ answer: 'Synthesized test answer.', citations: [], gaps: ['none'] }),
|
||||
blocks: [],
|
||||
stopReason: 'end' as const,
|
||||
usage: { input_tokens: 1200, output_tokens: 80, cache_read_tokens: 0, cache_creation_tokens: 0 },
|
||||
model: 'anthropic:claude-haiku-4-5-20251001',
|
||||
providerId: 'anthropic',
|
||||
}));
|
||||
const { isError, body } = await withEnv({ ANTHROPIC_API_KEY: 'sk-test-hermetic' }, async () =>
|
||||
callRemote('synthesize', { question: 'what do we know?' }),
|
||||
);
|
||||
expect(isError).toBe(false);
|
||||
expect(body.answer).toBe('Synthesized test answer.');
|
||||
expect(body.protocol_version).toBe(1);
|
||||
expect(body.cost.input_tokens).toBe(1200);
|
||||
expect(body.cost.output_tokens).toBe(80);
|
||||
expect(Array.isArray(body.sources)).toBe(true);
|
||||
const violations = validateAgainstSchema(body, RESPONSE_SCHEMAS.synthesize);
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('forget — idempotency + not_found', () => {
|
||||
it('expires once, reports expired:false on re-forget, not_found on unknown id', async () => {
|
||||
const r = await callRemote('remember', {
|
||||
fact: 'fact to forget', provenance: 'test', entity: 'people/forget-test',
|
||||
});
|
||||
const id = r.body.id as string;
|
||||
|
||||
const first = await callRemote('forget', { id, reason: 'test cleanup' });
|
||||
expect(first.isError).toBe(false);
|
||||
expect(first.body.expired).toBe(true);
|
||||
expect(first.body.reason).toBe('test cleanup');
|
||||
expect(validateAgainstSchema(first.body, RESPONSE_SCHEMAS.forget)).toEqual([]);
|
||||
|
||||
const second = await callRemote('forget', { id });
|
||||
expect(second.isError).toBe(false);
|
||||
expect(second.body.expired).toBe(false);
|
||||
expect(second.body.reason).toBe(null); // omitted optional → null
|
||||
|
||||
const missing = await callRemote('forget', { id: '999999999' });
|
||||
expect(missing.isError).toBe(true);
|
||||
expect(missing.body.error).toBe('not_found');
|
||||
expect(missing.body.suggestion.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('[ship P1.1] a remote caller in source A cannot forget a fact in source B (returns not_found, not expired)', async () => {
|
||||
// Register the 'other' tenant source (FK target) then seed a fact in it.
|
||||
await engine.executeRaw(
|
||||
`INSERT INTO sources (id, name, config) VALUES ('other', 'other-tenant', '{}'::jsonb)
|
||||
ON CONFLICT (id) DO NOTHING`,
|
||||
[],
|
||||
);
|
||||
await engine.insertFact(
|
||||
{ fact: 'cross-source secret fact', kind: 'fact', entity_slug: null, visibility: 'world', source: 'seed' },
|
||||
{ source_id: 'other' },
|
||||
);
|
||||
const otherRows = await engine.executeRaw<{ id: number }>(
|
||||
`SELECT id FROM facts WHERE source_id = 'other' AND fact = 'cross-source secret fact' LIMIT 1`,
|
||||
[],
|
||||
);
|
||||
const foreignId = String(otherRows[0].id);
|
||||
|
||||
// Remote caller scoped to 'default' tries to forget the 'other'-source id.
|
||||
const res = await dispatchToolCall(engine, 'forget', { id: foreignId }, {
|
||||
remote: true, takesHoldersAllowList: ['world'], sourceId: 'default',
|
||||
});
|
||||
const body = JSON.parse(res.content[0].text);
|
||||
expect(res.isError).toBe(true);
|
||||
expect(body.error).toBe('not_found'); // no cross-source existence leak
|
||||
|
||||
// And the foreign fact is STILL active (not expired by the cross-source call).
|
||||
const stillActive = await engine.executeRaw<{ expired_at: Date | null }>(
|
||||
`SELECT expired_at FROM facts WHERE id = $1`,
|
||||
[otherRows[0].id],
|
||||
);
|
||||
expect(stillActive[0].expired_at).toBe(null);
|
||||
});
|
||||
|
||||
it('[ship P1.1] a remote caller cannot forget a private fact (world-only)', async () => {
|
||||
const r = await callRemote('remember', {
|
||||
fact: 'private fact remote cannot forget', provenance: 'test',
|
||||
entity: 'people/private-forget-test', visibility: 'private',
|
||||
});
|
||||
// remote remember defaults world; force a private one locally instead.
|
||||
const localRes = await dispatchToolCall(engine, 'remember', {
|
||||
fact: 'truly private fact', provenance: 'test',
|
||||
entity: 'people/private-forget-test', visibility: 'private',
|
||||
}, { remote: false, sourceId: 'default' });
|
||||
const localId = JSON.parse(localRes.content[0].text).id as string;
|
||||
|
||||
const res = await dispatchToolCall(engine, 'forget', { id: localId }, {
|
||||
remote: true, takesHoldersAllowList: ['world'], sourceId: 'default',
|
||||
});
|
||||
const body = JSON.parse(res.content[0].text);
|
||||
expect(res.isError).toBe(true);
|
||||
expect(body.error).toBe('not_found'); // remote can't reach a private fact
|
||||
void r;
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeSingleFact — supersession rule [X1] + degraded dedup', () => {
|
||||
function installDeterministicEmbedder() {
|
||||
// The schema's facts.embedding column is the init-time default dim (1536);
|
||||
// test vectors must match or pgvector's CheckExpectedDim rejects the row.
|
||||
const DIM = 1536;
|
||||
configureGateway({
|
||||
embedding_model: 'openai:text-embedding-3-small',
|
||||
embedding_dimensions: DIM,
|
||||
env: { OPENAI_API_KEY: 'sk-test-deterministic' },
|
||||
});
|
||||
// The seam replaces the AI SDK's embedMany({ model, values }) call.
|
||||
__setEmbedTransportForTests((async (opts: { values: string[] }) => ({
|
||||
embeddings: opts.values.map(t => {
|
||||
// Same vector for the SUPERSEDE-PAIR family (cosine 1.0 — above the
|
||||
// dedup threshold); a distinct deterministic vector otherwise.
|
||||
const v = new Array(DIM).fill(0);
|
||||
if (t.includes('SUPERSEDE-PAIR')) v[0] = 1;
|
||||
else for (let i = 0; i < 16; i++) v[i] = ((t.charCodeAt(i % t.length) % 13) + 1) / 13;
|
||||
return v;
|
||||
}),
|
||||
})) as never);
|
||||
}
|
||||
|
||||
it('near-duplicate with changed text and same kind SUPERSEDES; identical text is a duplicate', async () => {
|
||||
installDeterministicEmbedder();
|
||||
const a = await writeSingleFact(engine, 'default', {
|
||||
fact: 'SUPERSEDE-PAIR alice works at acme-example',
|
||||
provenance: 'test', entity: 'people/supersede-test', kind: 'fact',
|
||||
});
|
||||
expect(a.status).toBe('inserted');
|
||||
expect(a.degraded_dedup).toBe(false);
|
||||
|
||||
const dup = await writeSingleFact(engine, 'default', {
|
||||
fact: 'SUPERSEDE-PAIR alice works at acme-example',
|
||||
provenance: 'test', entity: 'people/supersede-test', kind: 'fact',
|
||||
});
|
||||
expect(dup.status).toBe('duplicate');
|
||||
expect(dup.id).toBe(a.id);
|
||||
|
||||
const updated = await writeSingleFact(engine, 'default', {
|
||||
fact: 'SUPERSEDE-PAIR alice LEFT acme-example, now at widget-co',
|
||||
provenance: 'test', entity: 'people/supersede-test', kind: 'fact',
|
||||
});
|
||||
expect(updated.status).toBe('superseded');
|
||||
expect(updated.id).not.toBe(a.id);
|
||||
|
||||
const rows = await engine.executeRaw<{ id: number; superseded_by: number | null }>(
|
||||
`SELECT id, superseded_by FROM facts WHERE id = $1`, [a.id],
|
||||
);
|
||||
expect(rows[0].superseded_by).toBe(updated.id);
|
||||
});
|
||||
|
||||
it('reports degraded_dedup when no embedding provider is configured', async () => {
|
||||
const r = await writeSingleFact(engine, 'default', {
|
||||
fact: 'a fact written with no embedding provider',
|
||||
provenance: 'test', entity: 'people/degraded-test',
|
||||
});
|
||||
expect(r.status).toBe('inserted');
|
||||
expect(r.degraded_dedup).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('conformance runner — negative self-test [F3]', () => {
|
||||
function lyingClient(corrupt: (verb: string, body: Record<string, unknown>) => Record<string, unknown>): ConformanceClient {
|
||||
return {
|
||||
listTools: async () =>
|
||||
VERB_NAMES.map(name => ({
|
||||
name,
|
||||
description: name === 'synthesize' ? '[EXPENSIVE / SLOW] x' : `MEMORY VERB (v1): ${name}`,
|
||||
})),
|
||||
callTool: async (name, params) => {
|
||||
const res = await dispatchToolCall(engine, name, params, {
|
||||
remote: true, takesHoldersAllowList: ['world'], sourceId: 'default',
|
||||
});
|
||||
const body = JSON.parse(res.content[0].text);
|
||||
const mutated = res.isError ? body : corrupt(name, body);
|
||||
return { isError: res.isError, text: JSON.stringify(mutated) };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
it('a certifier that cannot fail certifies nothing: missing fields, bad enums, wrong id types all flag', async () => {
|
||||
// Mutation 1: remember drops the required `status` field.
|
||||
const r1 = await runConformance(
|
||||
lyingClient((verb, body) => (verb === 'remember' ? (({ status: _s, ...rest }) => rest)(body as { status?: unknown } & Record<string, unknown>) : body)),
|
||||
{ marker: 'neg1' },
|
||||
);
|
||||
expect(r1.ok).toBe(false);
|
||||
|
||||
// Mutation 2: remember returns an out-of-enum status.
|
||||
const r2 = await runConformance(
|
||||
lyingClient((verb, body) => (verb === 'remember' ? { ...body, status: 'absorbed' } : body)),
|
||||
{ marker: 'neg2' },
|
||||
);
|
||||
expect(r2.ok).toBe(false);
|
||||
|
||||
// Mutation 3: recall re-types fact_id to a number (the opaque-string mandate [T4]).
|
||||
const r3 = await runConformance(
|
||||
lyingClient((verb, body) => {
|
||||
if (verb !== 'recall' || !Array.isArray((body as { facts?: unknown[] }).facts)) return body;
|
||||
return {
|
||||
...body,
|
||||
facts: (body.facts as Array<Record<string, unknown>>).map(f => ({ ...f, fact_id: Number(f.fact_id) })),
|
||||
};
|
||||
}),
|
||||
{ marker: 'neg3' },
|
||||
);
|
||||
expect(r3.ok).toBe(false);
|
||||
|
||||
// Honest server passes (sanity: the failures above are the mutations' doing).
|
||||
const honest = await runConformance(lyingClient((_v, b) => b), { marker: 'pos1' });
|
||||
const failures = honest.results.filter(r => r.status === 'fail');
|
||||
expect(failures).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fixture mirror + surface invariants', () => {
|
||||
it('test/fixtures/memory-verbs/cases.json matches the embedded fixture module (BrainBench seed drift guard)', () => {
|
||||
const onDisk = JSON.parse(readFileSync(join(import.meta.dir, 'fixtures/memory-verbs/cases.json'), 'utf-8'));
|
||||
expect(onDisk).toEqual(JSON.parse(JSON.stringify(CONFORMANCE_CASES)));
|
||||
});
|
||||
|
||||
it('exactly five ops carry verb: true and they match VERB_NAMES', async () => {
|
||||
const { operations } = await import('../src/core/operations.ts');
|
||||
const verbs = operations.filter(o => o.verb === true).map(o => o.name).sort();
|
||||
expect(verbs).toEqual([...VERB_NAMES].sort());
|
||||
});
|
||||
});
|
||||
@@ -109,6 +109,28 @@ describe('v0.29 — redirect hints on existing ops', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#2416 — concept/landscape routing between search and query', () => {
|
||||
test('search describes the cheap-hybrid default, not the keyword-era model', () => {
|
||||
expect(SEARCH_DESCRIPTION).toContain("Cheap hybrid search");
|
||||
expect(SEARCH_DESCRIPTION).toContain("no LLM expansion");
|
||||
expect(SEARCH_DESCRIPTION).not.toContain("Keyword search using full-text search");
|
||||
});
|
||||
|
||||
test('search declares the completeness boundary and both escape routes', () => {
|
||||
expect(SEARCH_DESCRIPTION).toContain("NOT proof of coverage");
|
||||
expect(SEARCH_DESCRIPTION).toContain("landscape");
|
||||
expect(SEARCH_DESCRIPTION).toContain("list_pages");
|
||||
});
|
||||
|
||||
test('query owns concept/landscape questions but does not oversell coverage', () => {
|
||||
expect(QUERY_DESCRIPTION).toContain("landscape");
|
||||
expect(QUERY_DESCRIPTION).toContain("expansion recovers synonym");
|
||||
expect(QUERY_DESCRIPTION).toContain("Still top-K");
|
||||
expect(QUERY_DESCRIPTION).toContain("list_pages");
|
||||
expect(QUERY_DESCRIPTION).toContain("cheaper");
|
||||
});
|
||||
});
|
||||
|
||||
describe('v0.29 — subagent allow-list', () => {
|
||||
test('includes get_recent_salience and find_anomalies', () => {
|
||||
expect(BRAIN_TOOL_ALLOWLIST.has('get_recent_salience')).toBe(true);
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* #2416 — concept-shaped query detection (CLI nudge toward `query`).
|
||||
*
|
||||
* The classifier is tuned to favor false-negatives (silence) over
|
||||
* false-positives (noise): the cost of a missed nudge is zero; the cost of
|
||||
* nagging on an exact-token lookup is trust erosion. The non-collision block
|
||||
* pins the deliberate cue exclusions — "who are the" (find_experts) and bare
|
||||
* "anything…" (salience ops) must NEVER trigger the query nudge.
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { looksConceptShaped, conceptNudge } from '../src/core/search/query-intent.ts';
|
||||
|
||||
describe('#2416 — looksConceptShaped: concept/landscape queries → true', () => {
|
||||
const CONCEPT_SHAPED = [
|
||||
'all the companies that do offshore wind',
|
||||
'every project that uses pgvector',
|
||||
'find all startups doing agent memory',
|
||||
'list everything about vector databases',
|
||||
'everything related to embedding pricing',
|
||||
'the landscape of agent memory startups',
|
||||
'the ecosystem of MCP servers',
|
||||
'which funds have invested in climate tech',
|
||||
'show all notes that mention fundraising strategy',
|
||||
];
|
||||
for (const q of CONCEPT_SHAPED) {
|
||||
test(`true: "${q}"`, () => {
|
||||
expect(looksConceptShaped(q)).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('#2416 — looksConceptShaped: exact-token / entity / quoted → false', () => {
|
||||
const NOT_CONCEPT_SHAPED = [
|
||||
// bare tokens / proper-noun lookups (short-query guard)
|
||||
'stripe',
|
||||
'Series A',
|
||||
'acme-example',
|
||||
// quoted phrase — exact-match intent
|
||||
'find all notes with "offshore wind"',
|
||||
// slug-like token
|
||||
'all the pages that link to widget-co-seed',
|
||||
// entity lookups (classifyQueryIntent === 'entity')
|
||||
'who is alice from acme',
|
||||
'tell me about widget co',
|
||||
// plain questions without a fuzzy-quantifier cue
|
||||
'how does the embed backfill work',
|
||||
'meeting notes from tuesday',
|
||||
];
|
||||
for (const q of NOT_CONCEPT_SHAPED) {
|
||||
test(`false: "${q}"`, () => {
|
||||
expect(looksConceptShaped(q)).toBe(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('#2416 — cue non-collision with other routers', () => {
|
||||
test('"who are the …" stays silent (owned by find_experts)', () => {
|
||||
expect(looksConceptShaped('who are the ML people in my network')).toBe(false);
|
||||
});
|
||||
test('bare "anything …" stays silent (owned by salience ops)', () => {
|
||||
expect(looksConceptShaped('anything notable lately')).toBe(false);
|
||||
expect(looksConceptShaped('anything crazy happening in my brain lately?')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#2416 — conceptNudge message', () => {
|
||||
test('returns null for non-concept queries', () => {
|
||||
expect(conceptNudge('stripe')).toBeNull();
|
||||
expect(conceptNudge('who is alice from acme')).toBeNull();
|
||||
});
|
||||
|
||||
test('returns a single-line hint naming `gbrain query` and the completeness caveat', () => {
|
||||
const msg = conceptNudge('all the companies that do offshore wind');
|
||||
expect(msg).not.toBeNull();
|
||||
expect(msg!).toContain('gbrain query');
|
||||
expect(msg!).toContain('all the companies that do offshore wind');
|
||||
expect(msg!).toContain('not proof of completeness');
|
||||
expect(msg!.includes('\n')).toBe(false);
|
||||
});
|
||||
|
||||
test('truncates long queries in the copy-paste suggestion', () => {
|
||||
const long = 'all the companies that are doing something with autonomous underwater drone inspection services';
|
||||
const msg = conceptNudge(long)!;
|
||||
expect(msg).toContain('...');
|
||||
expect(msg.length).toBeLessThan(320);
|
||||
});
|
||||
});
|
||||
@@ -423,15 +423,17 @@ describe('runThink + persistSynthesis — #1698 never persist empty', () => {
|
||||
expect(full.synthesisOk).toBe(true);
|
||||
});
|
||||
|
||||
test('opts.stubResponse path never made a real LLM call — usage stays undefined', async () => {
|
||||
test('opts.stubResponse path never made a real LLM call — usage stays null', async () => {
|
||||
// Same distinction synthesisOk already makes: opts.stubResponse bypasses
|
||||
// client.create() entirely, so there is no real usage to report. cost_usd
|
||||
// must not be computed (and should render as null in --json) when this
|
||||
// happens, since there is nothing to compute it from.
|
||||
// happens, since there is nothing to compute it from. Since the [E2]
|
||||
// MEMORY_VERBS usage-accounting change, "no LLM ran" is spelled `null`
|
||||
// (the frozen cost-block contract), not `undefined`.
|
||||
const result = await runThink(engine, {
|
||||
question: 'stub no usage', stubResponse: { answer: 'has content', citations: [], gaps: [] },
|
||||
});
|
||||
expect(result.usage).toBeUndefined();
|
||||
expect(result.usage).toBeNull();
|
||||
});
|
||||
|
||||
test('pre-existing ThinkResult literal without synthesisOk still persists (back-compat)', async () => {
|
||||
|
||||
Reference in New Issue
Block a user