Files
Garry TanandClaude Fable 5 c2cc8b0207 v0.45.1.0 feat(context): hook-lane feedback loop + cross-turn dedupe for the harness hooks (#4025)
* refactor(context): extract pure gateVolunteeredPointers (no behavior change)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(context): volunteer-shaped resolve IPC — channel/cwd/volunteer fields, handler-result widening, server-side gate + validated source + delivery-point logging; listener on both engines

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(cli): gbrain volunteer-hook — one-shot harness hook entry (IPC-first; Postgres-only fallback; startup-hook-exempt)

T0 evidence: a UserPromptSubmit hook's additionalContext is recorded in the
session transcript as a structured hook_additional_context attachment and
transcript_path is stable across turns — cross-turn dedupe reads ONLY those
attachments (never raw tail substrings). Live smoke: seeded brain + running
serve → pointer injected via IPC at 0.43s wall, turn 2 suppressed, one event
banked channel=claude-code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(recipes): claude-code-reflex + codex-reflex (experimental) hook adapters

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(doctor): volunteer_channels per-adapter visibility

Engine-aware sibling of retrieval_reflex_health: groups
context_volunteer_events by channel (7d) so operators can see which push
adapters fire; message separates installed-but-unregistered from
registered-but-quiet; pre-v117 tolerant; info-only status.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(context): hook-lane feedback loop + cross-turn dedupe — converge cathedral-3 onto the #3975 harness hooks

The #3975 hook lane volunteered pages invisibly (nothing logged to
context_volunteer_events) and re-volunteered the same page every turn an
entity was mentioned (no priorContextText). This convergence ports the
cathedral-3 branch's two gap-fills into the blessed lane and retires its
parallel surface:

- transcripts: parseTranscript extracts injectedContextBlocks (structured
  hook_additional_context attachments — the T0-verified recorded shape of
  our own injections; never raw-turn substring matching)
- hook user-prompt: feeds those blocks as priorContextText (volunteer once
  per session, not once per mention) + sends a feedback-loop channel
  (--harness <claude-code|codex>, default claude-code); hook joins
  STARTUP_HOOK_SKIP_COMMANDS (per-prompt check-update spawn class, grep-pinned)
- turn-context: result exposes post-trim volunteered survivors
- resolve-ipc: additive TurnContextRequest.channel + onTurnContextDelivered
  seam (fires post-write only — abandoned blocks are never counted)
- serve: delivery callback logs volunteered pages + pointers per channel
- doctor: volunteer_channels check (per-channel 7d visibility; separates
  installed-but-unregistered from registered-but-quiet; pre-v117 tolerant)

Retired (never released): gbrain volunteer-hook, hook-adapters.ts,
claude-code-reflex + codex-reflex recipes, the volunteer-shaped resolve wire
extension. Kept: gateVolunteeredPointers extraction, VolunteerChannel
claude-code/codex values, the real hook-transcript fixture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: regenerate llms bundles for the hook-lane doc updates

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: coverage for the hook-lane delivery wiring

Extract logTurnContextDeliveryFireAndForget into volunteer-events.ts so the
shipped serve callback is unit-testable (channel fallback, sessionId clamp,
pointer+page rows in ONE synchronous registration — the double dynamic import
could register the pending write too late to survive process exit). Direct
units for isVolunteerChannel, explicit-channel logDeliveredReflexPointers,
and the pure gate (idempotency pin; stale parity claim in its doc corrected).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: pre-landing review fixes — 7-reviewer batch

Multi-confirmed: volunteer_channels now runs on the LOCAL doctor path too
(buildChecks; docs pointed there); transient DB errors no longer misreported
as pre-v117; priorContextText deduped + 32KB-capped hook-side. Red team:
delivery counts reconciled against the hook heartbeat (CAUTION when mostly
degraded — write-accept isn't injection); IPC clamp drops advisory
priorContextText BEFORE any window turn; foreign hook_additional_context
blocks (no gbrain marker) excluded from dedupe input; engine-aware quiet
guidance (Postgres brains aren't told to chase hook registration); 2MB dedupe
horizon documented. Security: wire channel claims restricted to harness
channels. DRY: VOLUNTEER_CHANNELS single source, shared reflexPointerRationale,
SESSION_ID_MAX_LEN shared with the op clamp; dead channel param dropped from
logDeliveredReflexPointers. Tests: budget-trim volunteered invariant under
real trim pressure, poll/ordering-based delivery-seam tests (no fixed sleeps),
doctor branch + engine-parity-row tests, wiring source pins, cap pins;
fixture pruned to load-bearing lines (environment inventory removed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: adversarial-review fixes — cross-model P1 + Claude adversarial batch

P1 (codex structured review + Claude adversarial, cross-model): the remote
run_doctor path now threads the caller's source scope into
volunteer_channels — a source-bound token can no longer see other sources'
activity counts/timestamps (must-never-violate source-isolation rule);
local trusted doctor stays brain-wide. Claude adversarial: IPC connection
handler gains a one-request re-entrancy guard (trailing bytes mid-await
double-processed the same line → duplicate handler work + duplicate
delivery logging); partial client-side trims now heartbeat as reason
'trimmed' (delivery-count drift visibility); heartbeat reconciliation uses
the same 7-day window as the counts + a 5-sample floor; one oversized
dedupe block no longer evicts all smaller ones (continue, not break);
quiet-channel guidance no longer claims old serves attribute hook traffic
to reflex (they log nothing); 'transient' claim softened to evidence;
marker-filter + unknown-channel-default comments state their honest limits.
Pins: scoped-query SQL/params, re-entrancy single-fire, updated wiring grep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: bump version and changelog (v0.45.1.0)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: restamp bootstrap runbook + regenerate vendored template repo for v0.45.1.0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: update project documentation for v0.45.1.0

Post-ship /document-release pass: fold the post-doc review fixes into the
reference docs (KEY_FILES: source-scoped remote volunteer_channels + heartbeat
reconciliation, 32KB-capped dedupe input, marker filter, IPC one-request
guard + clamp order), fix BRAINBENCH's now-stale memoryless-hook rationale,
and mark the doctor-check half of the push-context TODO shipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: cross-model doc-review fixes for v0.45.1.0

Codex doc review against the shipped diff: push-context channel table gains
the harness-hook row (three-channels claim was stale), the guide states the
PGLite-socket topology limit and the bounded dedupe horizon, delivery-point
claims are hedged to match the code (write-accept isn't proof of injection;
the doctor reconciles against the hook heartbeat, which can't inspect
registration), BRAINBENCH's future-tense hooks-PR sentence updated now the
hook has landed, and the adapter-flip + resolver-channel TODOS reflect the
current lane count. llms-full.txt regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(hook): document --harness in the user-prompt usage text

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 22:45:44 -07:00

163 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# BrainBench — cross-harness memory conformance methodology
BrainBench generalizes gbrain's internal eval surface into a reproducible,
cross-harness benchmark for agent memory. It operationalizes the four failure
modes of the agent-memory thesis: **know-to-ask** (nobody has a push path),
**push precision/recall** (the intrusion budget must be enforced),
**write-back fidelity** (memory write is even less solved than read), and
**cross-session continuity** (continuity that survives the harness hop).
Every subsequent memory PR must move — or hold, with a recorded justification —
a BrainBench number to merge.
Operator quickstart, corpus layout, and fixture-authoring rules live in
[`evals/brainbench/README.md`](../../evals/brainbench/README.md). This document
is the methodology: what the numbers mean, what they deliberately do not mean,
and how the gate governs change.
## Seam disclosure (read this before comparing rows)
Every scoreboard row carries a `seam` column:
| Harness | Seam | What the row actually measures |
|---|---|---|
| `openclaw` | **production** | The shipped OpenClaw context-engine pipeline, byte-for-byte (`extractCandidates``resolveEntitiesToPointers`, 3-pointer budget, prior-context suppression, markdown pointer block). |
| `claude-code` | **contract** | gbrain's memory primitives driven through the UserPromptSubmit hook wire contract (`{prompt, session_id, cwd}` in → `{hookSpecificOutput.additionalContext}` out, exported from `src/eval/brainbench/adapters/claude-code.ts`). 2-pointer budget; NO conversation memory — this row deliberately models the memoryless wire contract (suppression off), so the re-injection cost is visible as `false_fire_rate`; the shipped `gbrain hook user-prompt` layers transcript-based cross-turn dedupe on top of this same contract. |
| `codex` | **contract** | The fragments model: a static entity-index preamble (computed once, slugs not counted as injections) + at most ONE per-turn fragment. Measures how much push quality degrades when injection is mostly static. |
**Contract rows do NOT measure third-party harness behavior.** They measure
gbrain's primitives under each harness's injection-shape constraints. The rows
are comparable because fixtures, brain, and gold are identical — only the seam
contract varies. The real Claude Code integration has landed (`gbrain hook
user-prompt`, registered by `gbrain bootstrap`); flipping this adapter to exec
the real hook and report `production` numbers is a filed follow-up (TODOS.md —
"Flip contract adapters to production"). Same for codex fragments when that
integration lands. Also not graded, by design: the production orchestrator's
config gate, integration heartbeat, and 1500 ms timeout wrapper.
All three adapters drive ONE shared pipeline (`adapters/shared.ts`) with
declarative configs — comparability is structural, not disciplined.
## Metrics (formulas)
All micro-averaged per (harness × suite) cell; registered in
`src/core/eval/metric-glossary.ts` (plain-English in
[`METRIC_GLOSSARY.md`](METRIC_GLOSSARY.md)); JSON output carries one
`_meta.metric_glossary` block.
- `know_to_ask_failure_rate` = |should-retrieve turns where injected ∩ (gold acceptable) = ∅| / |should-retrieve turns|. Lower better.
- `false_fire_rate` = |stay-silent turns with any injection| / |stay-silent turns|. Lower better. Anti-gaming companion: "always inject" cannot win both.
- `push_precision` = Σ|injected ∩ (gold acceptable)| / Σ|injected| over turns with injection. `acceptable_slugs` count for precision, not recall.
- `push_recall` = Σ|injected ∩ gold| / Σ|gold| over should-retrieve turns. Pointer budgets cap this by design.
- `write_back_fidelity` = |gold facts that survive the PRODUCTION conversation→memory pipeline and are keyword-findable with correct entity attribution| / |gold facts|. The deterministic mode injects a gold extractor at the pipeline's extractor seam so segmentation, batching, dedup, and provenance stamping execute shipped code with zero LLM calls.
- `provenance_accuracy` = |surviving facts with correct {source, source_session, source_markdown_slug}| / |surviving facts|.
- `continuity_rate` = |decision probes recalled by the reader| / |probes|, per READER harness. The writer fixture's decisions persist through the production write-back pipeline — which is harness-INDEPENDENT in v1 — so each pair preps once and every harness replays the read-only reader against the same persisted state (an ordered writer×reader sweep would rebuild byte-identical brains for identical scores). A probe succeeds via pointer injection or stored-fact keyword lookup. The per-writer axis activates when harness-specific write paths land.
- `source_isolation_violations` = count of injected slugs from a non-active source. **Gates at zero**, every run, regardless of baseline — cross-source leakage is the data-leak invariant. Granularity disclosure: detection is slug-keyed, so it catches injection of slugs seeded ONLY in a foreign source; a same-slug cross-source CONTENT leak would require the engine's source-scoped SQL itself to fail, which the engine-layer source-isolation fuzz (gbrain-evals Cat 22) covers directly.
- `avg_injected_tokens` = mean estimated tokens (chars/4) of injected context per replayed turn. Intrusion-budget diagnostic; reported, NOT gated (gating awaits calibration data — filed TODO).
- `extraction_recall` / `extraction_precision``--llm` runs only: the real extractor's output vs gold keyword probes.
### What know-to-ask deliberately means in v1
It grades the **deterministic injection decision** — the Reflex pipeline that
ships at the seam. The agent never "knows to ask"; the reflex pushes. An
agent-LLM-in-the-loop replay (did the *model* issue a retrieval call when the
reflex stayed silent?) is **pre-registered as the `--live` extension**:
fixture-compatible, seeded, N-repeat methodology — and unimplemented. No LLM
grading is faked in v1.
### Difficulty is stratified on purpose
Several know-to-ask variants exercise documented v1 reflex limits (lowercase
mentions, surname-only references — `src/core/context/entity-salience.ts`).
Gold records what SHOULD happen; the committed baseline records what the
current system does (`know_to_ask_failure_rate` ≈ 0.15 at v1). The gap is the
measured roadmap, not a bug in the bench.
## Pre-registered expectations (v1, recorded before the first published run)
1. The production seam (openclaw) leads `push_recall` strictly: 3-pointer > 2-pointer > 1-fragment budgets. *(Observed at landing: 0.81 / 0.65 / 0.45.)*
2. The no-suppression contract (claude-code) is the only seam with `false_fire_rate` > 0. *(Observed: 0.020.03.)*
3. `write_back_fidelity` = 1.0 and `provenance_accuracy` = 1.0 in deterministic mode — the production pipeline must not lose or mis-attribute gold facts it was handed. Anything below 1.0 is a pipeline bug, not benchmark noise.
4. `source_isolation_violations` = 0 everywhere.
5. `push_precision` = 1.0 at v1 (exact-match resolution arms cannot inject an irrelevant page on this corpus); expected to dip below 1.0 when fuzzy/semantic resolution lands — that dip is the precision/recall trade made visible.
## Determinism & statistical posture
The harness is deterministic end-to-end: regex extraction + SQL resolution
(zero LLM, zero embeddings — facts seed with NULL embeddings; keyword/alias
arms carry retrieval), seeded PRNG corpus, one in-memory PGLite reset between
fixtures. Two runs produce identical metrics, so N-repeat error bars are
meaningless here (stddev = 0 by construction, the gbrain-evals "deterministic
adapters" convention) and the gate can be exact: **any flipped gold item is a
real behavior change.** Bootstrap/CI discipline applies to the future `--live`
and `--llm` published runs, which are model-stochastic.
## Gate governance (decision 4 — why a PR can't self-approve)
CI (`.github/workflows/test.yml` `brainbench` job, local parity
`scripts/ci-brainbench-gate.sh`) fetches the baseline **from main**
(`git show origin/master:evals/brainbench/baselines/main.json`) and compares
HEAD's fresh run against it:
- **Same `fixtures_hash`** → count-aware gate: any newly-failed gold item, any
adverse gated-metric move, or any isolation violation fails (exit 1).
- **Different hash** (the PR changed fixtures) → **corpus-bless mode**: the
PR's committed baseline must EXACTLY match HEAD's actual run (the file
cannot lie; exit 2 until `--update-baseline` is re-run), and any adverse
move vs main's baseline requires a `justification` string in the committed
baseline — visible in the PR diff, judged by the reviewer.
- `--allow-regression "reason"` is the local one-off escape hatch; the reason
is recorded in the run output. It is not available to CI.
The committed baseline is diff-stable by construction (metrics rounded to 4
decimals, keys sorted, receipts excluded; the run CONFIG — holdout/llm/
harness/suite sets — is bound into it, and comparisons across mismatched
configs are inconclusive). Same-hash hardening: any committed-baseline edit
without a fixture change must byte-match the actual run (receipts-backed), a
regressing receipts-backed update still needs a `justification`, gold_total
may not move at all under an unchanged corpus, and the CI script refuses a
working-tree baseline deletion. Holdout fixtures (~15%) are excluded from the
gate and scored only in published runs (`--include-holdout`).
Accepted residuals (review-enforced, by design): a `justification` string is
judged by the human reviewer, not parsed; count-preserving corpus dilution
(replacing hard fixtures with easy ones at equal gold_total) is visible only
in the fixture diff; and the ratchet does not auto-tighten — improvements
aren't banked into main's baseline until a PR updates it (a regression back
to the stale baseline level passes; periodic re-baselining is the operator's
job, filed as a TODO).
## Gold methodology
Gold derives from the corpus generator (the same PRNG step that authors a turn
authors its annotation, so gold-vs-text drift is structurally impossible for
generated fixtures), plus hand-authored spike fixtures that froze the schema.
A 10% double-label validation pass (independent agent review of fixture text vs
gold, blind to the generator's intent) is run at corpus-change time; its
receipt is recorded in the corpus `_ledger.json` and any disagreement is a
fixture bug to fix, not a tolerance to average over.
## Interop
- **Foreign runners (gbrain-evals):** the subprocess contract is
`gbrain eval brainbench --fixtures DIR --gold DIR --json --out FILE`;
schemas in `evals/brainbench/schema/`. The sibling gbrain-evals repo wires
this as `eval/runner/brainbench-memory.ts` with a published scorecard.
- **Memory-verbs conformance kit (Cathedral 1):** conformance scenarios
convert to BrainBench fixtures via the published fixture schema
(`schema_version` 1) once that wave lands — the conversion path is the
schema itself; no bespoke importer is required.
- **Naming note:** "BrainBench" historically also names the in-house
retrieval corpus in the sibling gbrain-evals repo (the 145-query relational
suite, Cat taxonomy) and `test/cathedral-ii-brainbench.test.ts` (v0.20.0
code-graph recall pins). This suite — the cross-harness memory conformance
bench — is the generalization the name now primarily refers to; the older
references stand unchanged.
## Extends docs/eval-bench.md
The capture → baseline → replay loop in [`eval-bench.md`](../eval-bench.md)
gates *retrieval result sets* at the query level. BrainBench gates the
*memory behaviors* above them. The two share the receipts discipline and the
.gbrain-evals run ledger (`EvalRunRecord` v3; brainbench records once per
sweep under `mode: 'n/a'`).