Files
gbrain/docs/guides/ambient-writeback.md
Garry Tan 5cfb84f1d3 v0.48.2.0 feat(reranker): voyage:rerank-2.5 default, no_key fail-open, readiness surfaces before the ZeroEntropy sunset
Reranker default moves to Voyage rerank-2.5 ahead of the ZeroEntropy hosted-API end date, with a fail-open no_key preflight (one audit row per process, no stderr), a reranker_skipped ranking-only degraded stage, and readiness surfaces in gbrain init, gbrain doctor reranker_health and gbrain search modes (remote callers get the verdict without the host key inventory). Merges master v0.48.1.0 (both degraded-stage vocabularies coexist). Docs: LongMemEval-S re-run at this release (strict recall_all@5 93.19% reranker off, 95.32% with the default reranker; receipts in gbrain-evals) and a present-tense rewrite of README and docs. Pins transitive fast-uri/qs via overrides.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 14:45:44 -07:00

14 KiB

Ambient memory writeback

Ambient recall (ambient-recall.md) is the READ side of memory: context arrives at session boundaries without a question being asked. This guide is the WRITE side: when the brain's operator opts in, agents save salient facts the user states directly — during ordinary conversation, without being told to — through the existing MEMORY_VERBS surface. No new protocol verb; remember and extract_facts do all the writing.

Say to your agent: "Turn on ambient memory writeback for my brain" — "remember things I tell you without being asked" — your agent runs gbrain config set memory.auto_writeback salient and gbrain bootstrap harness --yes.

Off by default. Nothing in this feature ever enables itself.

Who gets asked (and who is never asked)

gbrain distinguishes PERSONAL brains from company/team brains and only ever offers ambient writeback on personal ones — capturing what people say to agents on a shared brain is a privacy decision the whole team owns, not a default.

  • Declaration wins. brain.audience (personal | shared) is the declared axis: set it yourself, let company-brainify stamp shared at its Phase-5 handoff, or answer the bootstrap interview. A declaration always beats the heuristic.
  • The heuristic is conservative. Without a declaration, only ≥3 distinct non-automation MCP clients active in the last 30 days reads as shared evidence (client count measures surface breadth, not people — Claude Code + Codex + a phone client is one human). gbrain doctor's memory_writeback check shows the resolved audience and its reasons; correct a misclassification with gbrain config set brain.audience personal|shared.
  • The ask fires once. On personal brains, gbrain init and the post-upgrade banner print a one-time [AGENT]-relayed disclosure + ask (sentinel memory.auto_writeback_notice_shown); declining is permanent. gbrain advisor keeps a quiet reminder afterward — informational only, never --apply-able: consent is never automated.
  • Silence everywhere else: shared/unknown brains, mounted team brains, thin clients, remote MCP callers, and any classifier failure.
  • Explicitly enabling on a shared-classified brain works (operator sovereignty) but prints a caution: members' words get persisted into a store other authorized agents can read.

Modes

memory.auto_writeback What agents are told to save
off (default) Nothing — no instruction section, no backstop, no banking.
salient (recommended) Durable, notable claims only: preferences, corrections, decisions, commitments, relationships, project-state changes. The backstop keeps medium+ notability facts.
all Every direct factual user statement — still excluding operational chatter, assistant-generated content, secrets/credentials, and quoted third-party material. Precision rides the extractor's semantic skip rules (the second of the two filters); expect more low-value facts and more extraction spend.

Both planes carry the setting: gbrain config set memory.auto_writeback … dual-writes the DB plane (authoritative — the serve re-checks it before any extraction) and the ~/.gbrain/config.json mirror (what the engine-free Stop-hook child reads). The mirror never enables anything on its own: it is machine-global while DB rows are per-brain, so engine-backed resolution is DB-only — a mounted or selected brain whose operator never opted in cannot inherit another brain's setting. When the planes disagree (a failed dual-write, a reinitialized DB, or a config set run on another machine of a shared Postgres brain), that is plane drift: extraction gates hold banked turns without the terminal skip (nothing is destroyed), gbrain doctor warns with the one-line re-sync (gbrain config set memory.auto_writeback <mode>), and a DB-write failure during config set itself exits non-zero and says the runtime value is unchanged. Selecting a MOUNTED brain (--brain, GBRAIN_BRAIN_ID, .gbrain-mount) writes the mount's DB row only — the machine-local mirror gates the host's Stop hook, so enabling a team mount never opts the host's own conversations into banking. A wrong-brain hook bank remains harmless — the target serve's own DB gate decides.

The three activation surfaces

  1. MCP instructions (all transports). When enabled, the initialize handshake appends a ~15-line ambient-writeback contract to the base operating contract — one claim per remember call, entity whenever a person/company/project is the subject, concise provenance (harness + session id + date), durable facts without ttl, transient facts with the configured TTL, the skip-list, and the visibility rule. stdio resolves it at boot (restart to flip — same posture as mcp.strict_params); the HTTP transports resolve per request (restart-free, with a last-known-good bundle riding out config blips). The section only renders when the caller can actually invoke remember — OAuth tokens without write scope, slug-bound clients whose fence denies it, and clamped surfaces that drop it all get the base instructions instead (never orders to make calls dispatch will deny); extract_facts is only named when the transport's actual tool set can call it.
  2. Managed harness instruction blocks. gbrain bootstrap harness --yes installs the same contract (same builder — the surfaces cannot drift) as a managed block between <!-- gbrain:ambient-writeback:begin/end --> sentinels in user-scope CLAUDE.md (Claude Code) and $CODEX_HOME/AGENTS.md (Codex). Idempotent re-runs; converge-on-off (re-running with writeback off removes the block); --remove strips it. Codex caveat: when $CODEX_HOME/AGENTS.override.md exists, Codex ignores AGENTS.md entirely — bootstrap fails that target loudly and doctor warns, instead of reporting a dead integration as healthy. The block header names the serve endpoint; after changing mode/TTL/visibility config, re-run gbrain config set memory.auto_writeback <mode> then bootstrap harness (the config set refreshes the engine-free posture stamp the renderer reads; doctor's drift warning names the same combo). Blocks install only after the same run's MCP registration confirms, and a failed final smoke test strips the blocks that run installed — no block outlives a rolled-back registration. Registrar mode (--url to a non-loopback serve) never installs instruction blocks: the local setting speaks for the local brain, and the remote brain's own MCP instructions carry the contract when its operator enables writeback.
  3. The Claude Code Stop-hook backstop. After each assistant turn, the hook gates the user's message through a deterministic, zero-LLM filter (min length — CJK-aware, ack/greeting lexicon, slash commands, question-only turns, quoted/tool output, bulk pastes >8KB), secret-scans it, banks it as a content-addressed .wb- corpus file (same turn = same name = free dedup, even on keyless brains), and asks the serve to extract it asynchronously. The hook never blocks: its own 2s deadline inside Stop's 10s cap, exit 0 on every path, typed heartbeat reasons for every outcome. Serve down? The file waits for the maintenance sweep. The lane is engine-uniform: the IPC listener keys its socket off the brain's connection URL, so Postgres brains harvest the same way whenever a gbrain serve for that brain is running (heartbeat no_serve between serves — the banked file is the durable artifact either way).

Per-harness reality (honest limitations)

Harness Real-time contract Backstop
Claude Code MCP instructions + managed user CLAUDE.md block Stop-hook lane (above)
Codex MCP instructions + managed $CODEX_HOME/AGENTS.md block No per-turn hook exists (SessionEnd only, 3s hard-kill). The existing SessionEnd capture → corpus → maintenance-sweep extraction lane is the delayed backstop — whole-session, next-sweep latency, governed by facts.extraction_enabled (it predates this feature).
opencode / OpenClaw / others MCP instructions when connected None wired — follow-ups filed.

The workspace-bootstrap "same-turn write-back" contract (gbrain bootstrap contract, AGENTS.md) and the two surfaces above are three renderings of one posture; the MCP section and the harness blocks share one builder, and the workspace contract is the always-on convention documented in bootstrap.md. Keep them coherent when editing any of them.

TTL: transient facts expire at read time

Agents pass ttl: "3d" (configurable: memory.auto_writeback_transient_ttl, duration shorthand only, max 365d) on transient facts — current health, location, travel, mood, near-term schedule. Durable facts carry no TTL.

Expiry is exact-time and read-side: active reads (recall, entity cards, hot-memory injection, dedup candidates) exclude facts whose valid_until has passed — no sweeper needed, nothing mutated. The rows stay in the database as history (--asof and supersession views still see them), and a re-stated fact after expiry inserts fresh. Backstop-extracted facts currently get no TTL (the extractor doesn't classify transience yet — a filed follow-up), so a transient fact caught only by the backstop is durable until forgotten.

Note on lapsed rows: valid_until is temporal validity and active reads honor it, so a brain that already carries lapsed rows sees them leave the active set on the first read after the migration (facts-health counts step accordingly). Nothing is deleted or mutated; the doctor's validity_lapsed_facts count sizes the shift.

Privacy and visibility

  • What is never saved: greetings, acknowledgements, fact-free questions, the assistant's own inferences/diagnoses/speculation, tool output, quoted third-party material, pasted/imported text (unless the user explicitly asks), raw transcripts, and — in every mode — secrets/credentials (banked turns are secret-scanned before they touch disk; scanner unavailable = fail-closed skip).
  • world is not the internet. Fact visibility world means readable by agents authorized on this brain — the default that makes the remember→recall round-trip work across sessions. When facts.default_visibility is unset, the instruction template tells agents to write world; only an explicitly-private brain gets the private posture, stated with its trade-off: private facts are readable by the local CLI only, so remote agents cannot recall them later. An explicit private setting is never widened — not by the template, not by the backstop (which resolves facts.default_visibility exactly like extract_facts always has).
  • Backstop facts carry source: 'hook:writeback' and the session's GBRAIN_SOURCE on BOTH extraction paths: the prompt-time IPC ask carries it directly, and the banked filename embeds it (.src-<sourceId> segment) so the sweep fallback files the turn into the same source even when serve was down at Stop time — never the sweep's own source. A conversation spanning multiple brains still attributes to the configured one (same limitation as the existing capture lanes).

Cost posture

The instruction path costs nothing extra — the conversing model does the salience filtering in-line. The backstop runs one extraction call per banked turn, capped at 30 prompt-harvests per session (overflow degrades to the sweep's batch pass — freshness lost, nothing dropped within the corpus retention window; un-ingested files older than the corpus GC's 30-day retention are deleted, so a keyless brain — or one whose serve AND sweep stayed away for a month — does eventually shed unbanked turns; serve restarts reset the counter). Keyless brains skip extraction entirely (typed keyless skip) and still get agent-authored remember writes; note that keyless dedup is degraded (degraded_dedup) — near-duplicate phrasings may insert. See spend-controls for the brain-wide extraction switches.

Diagnostics

gbrain doctor → memory_writeback: resolved mode (+mode_valid), TTL (+ttl_valid), both visibility postures (instruction template vs backstop), brain audience + reasons, installed harness blocks (receipt vs live sentinel probe, override-file detection, config-drift warning — and a receipt target marked FAILED, e.g. a smoke-rollback strip that itself failed, stays a standing warn until a bootstrap harness re-run or --remove converges it), and validity-lapsed fact count. With writeback OFF the check still probes for lingering instruction blocks and warns — the off switch is incomplete until a bootstrap harness re-run converges them. It also reports 7-day counters — remember outcomes over MCP (all callers — the wire cannot distinguish ambient from explicit saves) and persisted backstop results from the serve-side harvest receipts. Counters are local, append-only, loss-tolerant observability — never a source of truth.

Enable / verify / disable

gbrain config set memory.auto_writeback salient
gbrain config set memory.auto_writeback_transient_ttl 3d   # optional; default
gbrain bootstrap harness --harness codex --yes
grep -n "gbrain:ambient-writeback" "${CODEX_HOME:-$HOME/.codex}/AGENTS.md"
gbrain doctor | grep -A6 memory_writeback
# In a NEW agent session, say: "I prefer dark mode in every editor." Then:
gbrain recall --grep "dark mode"
gbrain sweep --once   # drives the sweep backstop extraction immediately
# Off switch (anytime; converge harness blocks with another bootstrap run):
gbrain config set memory.auto_writeback off
gbrain bootstrap harness --yes