Skip to content

feat: trusted recall and memory write surface (v1.35.0)#144

Merged
solaitken merged 16 commits into
mainfrom
feat/recall-trust-and-write-surface
Jul 19, 2026
Merged

feat: trusted recall and memory write surface (v1.35.0)#144
solaitken merged 16 commits into
mainfrom
feat/recall-trust-and-write-surface

Conversation

@solaitken

@solaitken solaitken commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

feat: trusted recall and memory write surface (v1.35.0)

Summary

Recall now reaches the agent at the moment it matters and can be trusted when it arrives: an opt-in bounded recall brief lands on every prompt, retrieval zero-ranks quarantined material and hands back receipts explaining every exclusion, a supersede relation on a successor fades the superseded note without editing it, and MCP agents get the full atomic note lifecycle - update, append, and all-or-nothing mixed batches - alongside a typed session-start timeline, a self-directing knowledge-gap loop, clip-proof pack identity, and fail-fast doctor readiness probes.

Why this matters

flowchart LR
    subgraph Recall["Recall at the right time"]
        P["User prompt"] -->|"opt-in, capped, audited"| B["Bounded recall brief<br/>(fenced untrusted)"]
        S["Session start"] --> T["Typed age-labeled timeline"]
        S --> G["Gap agenda"]
        E["Session end"] -->|"recurring gaps"| GT["Durable gap tasks<br/>auto-close on resolution"]
    end
    subgraph Trust["Trust in what returns"]
        Q["Search pool"] --> K1["Kernel 1: rank adjust"]
        K1 -->|"zero-rank + receipts"| TG["Trust gate"]
        K1 -->|"fade multiplier"| SF["Supersede fade"]
    end
    subgraph Write["Atomic write lifecycle"]
        U["brain_update_note"] --> K2["Kernel 2: write batch core"]
        A["brain_append_note"] --> K2
        W["brain_write_batch"] --> K2
        K2 -->|"all-or-nothing"| V["Vault"]
    end
    Trust --> Recall
Loading

Concrete benefits

  • A prompt-override or secret-exfiltration span can no longer rank into a context pack when the trust gate is on, and every exclusion is named and counted in retrieval_decision_trace instead of vanishing silently.
  • Deprecating a note now takes one edit on its successor: the supersedes relation fades the old note in both semantic and lexical search.
  • MCP agents maintain notes end to end without leaving the surface: update, append, and mixed atomic batches with typed errors and zero partial writes.
  • The recall inject hook turns stored knowledge into per-turn value under hard caps, with a per-decision audit trail, an untrusted-content fence, and nothing injected on any error.
  • Recurring recall gaps stop evaporating: they become durable tasks, show up as a session-start agenda, and close themselves once the gap is filled.
  • Budget clipping can no longer strip session identity, so continuation survives tiny output budgets.
  • o2b doctor --readiness answers the Day-0 question "is my wiring actually usable" with three functional probes, per-check timeouts, and a real exit code.

What ships

Unit Kanban Artifact
Bounded recall inject on UserPromptSubmit t_2ce46130 hooks/recall-inject.ts, src/core/brain/recall-inject.ts
Retrieval trust gate + per-pack receipts (kernel 1) t_5f61130a src/core/search/rank-adjust.ts, src/core/brain/trust/retrieval-gate.ts, retrieval-receipts.ts
Relation-only supersede fade t_c4a9cef8 src/core/search/result-filters.ts, ranker.ts
brain_update_note + brain_append_note (kernel 2) t_3ff3fe77 src/core/brain/write-batch.ts, src/mcp/brain/notes-tools.ts
General atomic batch write tool t_7718ab22 src/mcp/brain/write-batch-tools.ts
include_raw + extracted discriminator t_ac1d36ea src/core/brain/session-recall.ts, src/mcp/brain/recall-tools.ts
Typed age-labeled session-start timeline t_4adb0b8b src/core/brain/render/activity-line.ts, morning-brief.ts
Knowledge-gap loop t_67d38036 src/core/brain/gaps/gap-loop.ts, hooks/gap-promote.ts, hooks/gap-agenda.ts
Clip-protected session_id and agent_id t_5be0654d src/core/brain/continuity/, token-impact.ts
Fail-fast doctor readiness probes t_cc234ff5 src/core/doctor-readiness.ts
Rider: de-flake temporal-replace log-day test - tests/core/brain/lifecycle/temporal-replace.test.ts

MCP surface: 103 -> 106 tools. Every new surface is opt-in and byte-identical when its flag or param is omitted, each with a regression test.

Test plan

  • bun run fmt clean
  • bun run lint exactly 134 warnings / 0 errors (baseline)
  • bun run typecheck clean
  • bun test full foreground suite green (6496+ pass / 0 fail)
  • bun run scripts/sync-version.ts --check passes
  • Phase-4 QA smoke of all ten features through the real CLI/hooks in an isolated scratch vault (never the operator vault)
  • Self-review of the full branch diff versus main; one MEDIUM finding (unfenced recall brief) fixed in fix(hooks): neutralize and fence recall-inject brief as untrusted content

Summary by CodeRabbit

  • New Features
    • Added opt-in trusted recall injection, retrieval trust filtering with decision receipts, and superseded-memory ranking fade.
    • Added chronological activity timelines with typed markers and relative ages.
    • Added durable knowledge-gap agendas with automatic task promotion and closure.
    • Added note update, append, and atomic all-or-nothing batch writing tools (including MCP surface).
    • Added optional raw session-recall results with configurable budgets.
    • Added doctor --readiness readiness probes with detailed pass/fail reporting.
  • Documentation
    • Updated release notes and CLI/MCP reference documentation, plus supporting design/planning notes.
  • Chores
    • Bumped the release version to 1.35.0.

solaitken and others added 15 commits July 19, 2026 10:38
Phase-0 artifacts for the trusted recall and memory write surface wave:
design doc (consultant Variant 3, two shared kernels), implementation
plan (10 tasks plus one test de-flake rider), and the variant audit
trail with the raw consultant output.

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

The "temporalReplace logs a temporal-replace event" test read the log
via readLogDay(vault, "2026-07-18"), a hardcoded day. temporalReplace
logs under the UTC day of its wall-clock write instant (carried on the
result as loggedAt), not the belief instant `at`, so the test only
passed when run on 2026-07-18 and failed on every other date.

Derive the expected log day from res.loggedAt.slice(0, 10) so the test
is green on any wall-clock date. The sibling lifecycle tests
(chain-decay, tombstone) inject an explicit `now` and are already
deterministic, so they need no change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`o2b doctor --readiness` runs three probes past the static invariant
checks, each reporting exactly one of pass, fail-with-reason, or
skipped-not-configured (never a silent pass) under a per-check timeout,
and any fail yields a non-zero exit code:

- llm_key: the model-inference credential resolves. The deterministic
  core has no in-repo chat-LLM client (write-time model steps are handed
  to the host as needs-llm-step envelopes), so the only model credential
  the system resolves itself is the embedding provider's API key; this
  probe resolves that. Skips for providers that need no key (local).
- embedding_provider: the provider loads and its ping reports a model
  and a positive dimension. Skips when semantic search is disabled.
- runtime_adapter_wiring: the adapter registry is populated and the
  canonical MCP payload builds and plans for every adapter.

Without the flag, doctor output and exit behavior are byte-identical to
before (readiness stays null; the new JSON key and text section are only
added when the flag is present). A regression test proves the plain run
is an exact prefix of the --readiness run and that --json omits the
readiness key without the flag.

Deviations from plan.md: the plan named src/core/doctor.ts as the host
module, but that file's `doctor()` is a synchronous CheckResult[]
aggregator with several callers (openclaw, mcp, main); adding async
timeout-bounded probes there would ripple through all of them and risk
the byte-identical contract. The probes live in a new sibling module
src/core/doctor-readiness.ts and the `o2b doctor` CLI verb
(src/cli/main.ts) mounts them, matching the codebase layout. A new
src/core/install/adapters/all.ts barrel is the single source of the
adapter-registration list (DRY); install.ts now imports it instead of
repeating the nine side-effect imports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
agent_id now joins session_id as a first-class correlation field on the
token-impact (pack economics) and continuity identity, and both are
guaranteed to survive an output-budget clip.

- continuity/types.ts: named constants for the identity payload keys
  (session_id, agent_id, turn_id) and CLIP_PROTECTED_PAYLOAD_KEYS - the
  keys a clip must never drop.
- continuity/store.ts: clipPayloadToBudget(payload, budgetChars) - a
  pure primitive that trims a payload to a character budget, dropping
  non-protected keys in original order only as far as needed to fit, and
  ALWAYS retaining the protected identity keys (even when they alone
  exceed the budget). No budget pressure (undefined/non-finite budget, or
  a payload already within budget) returns the SAME reference unchanged,
  so it is byte-identical. This is the shared clip contract C2's
  include_raw will reuse.
- continuity/read-model.ts: lifts agent_id to a first-class agentId field
  beside sessionId/turnId, using the shared key constants.
- token-impact.ts: emitTokenImpact and recordTokenImpactOutcome accept an
  agentId and write agent_id beside session_id (only when provided, so
  callers that omit it produce byte-identical records and dedup ids).
  listTokenImpact gains an opt-in payloadBudgetChars filter that clips
  each retained sample's payload through the protected-fields contract; a
  record that fits, or an omitted budget, is returned by the same
  reference. summarizeTokenImpact never clips (aggregation needs every
  field), so the roll-up is unaffected.

A regression test drives a tiny budget and asserts session_id and
agent_id survive while a non-protected field is dropped, and that an
omitted budget leaves the payload byte-identical.

Deviation from plan.md: the plan listed continuity/types.ts and store.ts
plus token-impact.ts, which this matches; the "output-budget clip" it
references had no existing routine, so the clip is introduced as the
reusable clipPayloadToBudget primitive and consumed via listTokenImpact's
new payloadBudgetChars filter (the token-impact clip path). The read-model
lift is generic, so any continuity emitter that records agent_id gets the
first-class field for free.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The morning brief now renders its kept items as a single chronological,
typed, age-labeled timeline instead of three per-kind sections, so an
operator reads recent activity in the order it happened.

- New src/core/brain/render/activity-line.ts: a small pure rendering
  helper with a FIXED structural marker vocabulary (preference -> pref,
  openQuestion -> open, note -> note) and a relative-age label derived
  from each item's stored timestamp. renderActivityLine renders one
  bullet; renderActivityTimeline sorts items most-recent-first
  (undated last, deterministic tie-break) and joins them. No
  natural-language classification anywhere - the marker comes from the
  item kind, never its words.
- morning-brief.ts uses the helper: each kept preference, open question,
  and note becomes one timeline entry tagged by kind with its source
  timestamp; the rendered text is one "## Recent activity" feed. The
  public data (preferences, openQuestions, recentNotes arrays and their
  ageLabel fields) is unchanged, so brain_brief view=morning and the
  profile-doc/idea-discovery consumers that read those arrays are
  unaffected; only the rendered text becomes the timeline.

The helper is deliberately free of vault/I-O concerns so the later
knowledge-gap agenda (theme A) can reuse it without a shared
fail-open/fail-closed guarantee leaking between surfaces.

No deviations from plan.md; the new helper lives at the design's named
path src/core/brain/render/activity-line.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add kernel 1, the deterministic retrieval rank-adjustment sink
(src/core/search/rank-adjust.ts). Registered adjusters return a
per-candidate verdict (keep, multiply, exclude-with-reason); the sink
applies them, records exclusions with reasons, and re-sorts survivors.
With no adjuster registered, and when every adjuster only keeps or
multiplies by 1, it returns the exact input array, so the default
retrieval path is byte-identical. It is mounted before the final slice
in search.ts, so it covers BOTH the semantic and the pure-lexical paths
(both flow through the same pre-slice pool) and a gate exclusion lets a
deeper survivor backfill the window.

Add the first consumer, the retrieval trust gate
(src/core/brain/trust/retrieval-gate.ts). It contributes an exclude
verdict for quarantined material classified DETERMINISTICALLY from three
structural signals already in the codebase, reusing each module's
exported identity: the self-approval guardrail state
(BRAIN_PREFERENCE_STATUS.quarantine), the untrusted-source provenance
marker (UNTRUSTED_SOURCE_TAG), and the entity-contamination marker
(ENTITY_CONTAMINATION_FRONTMATTER_KEY, added to contamination.ts so that
module stays the single boundary). It reads only controlled-vocabulary
frontmatter keys, never note prose, so there is no natural-language word
list anywhere.

Excluded items are never silently dropped: every pack carries two
compact-reference receipts consistent with the context-receipt model
(retrieval_decision_trace counts and lists exclusions with reasons;
memory_trust_assessment summarizes the pack trust posture with a reason
histogram). Neither duplicates result payloads.

The gate is opt-in via recall.retrievalTrustGateEnabled (env
OPEN_SECOND_BRAIN_SEARCH_TRUST_GATE / config search_trust_gate_enabled),
default off. Flag off keeps ranking and the outcome shape byte-identical;
tests include the ablation (gate on vs off exact deltas) and a
byte-identical regression.

Deviations from plan.md paths (recorded per the codebase-wins rule):
- Receipt builders live at src/core/brain/trust/retrieval-receipts.ts
  (co-located with the gate); plan.md named a src/core/brain/receipts
  directory that does not exist.
- Receipts are surfaced as typed, opt-in compact-reference fields on
  SearchOutcome rather than emitted into the continuity context-receipt
  store, keeping the retrieval path self-contained and byte-identical
  when the gate is off.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the second consumer of kernel 1: a relation-only supersede fade
(t_c4a9cef8). The fade adjuster (src/core/search/result-filters.ts,
co-located with attachTrustMetadata) fades any candidate whose surfaced
typed relations mark it superseded, reusing hasSupersededRelation - the
exact source of truth deriveTrust / attachTrustMetadata use for the
inline trust.superseded display flag, refactored out of deriveTrust so
display and ranking agree on what "superseded" means. The fade factor is
the named constant SUPERSEDE_FADE_MULTIPLIER, placed beside the freshness
multipliers in ranker.ts as the same family of bounded score-scaling
constant.

Because the adjuster runs through kernel 1 at the shared pre-slice pool,
the fade covers BOTH the semantic and the pure-lexical paths. search.ts
fetches the pool's typed relations once and registers the adjuster only
when the fade is enabled.

The fade is opt-in via recall.supersedeFadeEnabled (env
OPEN_SECOND_BRAIN_SEARCH_SUPERSEDE_FADE / config
search_supersede_fade_enabled), default off. A pool with no supersede
relation ranks byte-identically whether the flag is on or off (both
covered by regression tests). The existing superseded-non-tip tombstone
drop is untouched: a tombstoned row is removed by the status filter
before the fade runs, so the fade never resurrects it.

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

Introduce kernel 2 (src/core/brain/write-batch.ts), the atomic
multi-operation write core that generalises the validate-project-commit
shape of runPinnedContextBatch: an ordered list of typed operations is
validated and projected in memory first, then committed to disk as a
unit; the first invalid operation aborts with a typed WriteBatchError
naming the operation index and no disk write happens.

Add the brain_update_note and brain_append_note MCP tools as
single-operation batches over kernel 2. update_note merges frontmatter
keys and/or replaces the body of an existing note; append_note appends
to the body. Both reuse the exact create-note safety envelope (path
traversal, the Brain machinery root, and vault-scope-excluded paths are
refused) and the atomic-write pipeline, so a mid-write failure leaves
the target byte-identical. A missing target is a typed error mapped to
INVALID_PARAMS.

The core takes typed operations only; the MCP layer maps request params
onto them (no MCP shapes inside the core). The create-note safety
envelope is extracted into resolveNoteTarget so the note write
operations reuse it exactly rather than re-deriving it.

Deviations from plan.md: kernel 2 lives at src/core/brain/write-batch.ts
(the plan's suggested home, confirmed against the layout). The registry
tool-count baseline moves 103 -> 105 and the frozen tool-name set gains
the two new names (the one legitimate baseline change for added tools).
A note target may appear at most once per batch: a second op on the same
file is refused with a typed duplicate_target error rather than silently
clobbering the first op's projection at commit time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the brain_write_batch MCP tool, the second consumer of the atomic
write-batch core (kernel 2). One ordered list of typed operations is
committed all-or-nothing: the whole batch is validated and projected in
memory first, and the first invalid operation aborts with a typed error
naming the operation index with no disk write - a later invalid op never
lets an earlier one land.

Extend kernel 2 with the full operation vocabulary: create note, update
note (body and/or frontmatter), append note, apply evidence, and append
log line. Each op maps onto an existing core writer (createNote,
appendApplyEvidence, appendBrainNote) rather than reimplementing it; the
apply_evidence and append_log_line ops are pre-validated (required
fields, result enum, preference existence, non-empty text) so a
malformed op aborts the batch before any commit.

Single-operation batches produce the same result as the dedicated
brain_create_note / brain_update_note / brain_append_note tools
(parity is covered by tests). The MCP layer maps request params onto the
core's typed operations and resolves the caller identity for the
log-writing ops; no MCP shapes reach the core. The pre-existing
brain_pinned_context batching behaviour is untouched.

Deviations from plan.md: the tool lives in a new module
src/mcp/brain/write-batch-tools.ts (registered through the brain-tools
aggregator) rather than being folded into notes-tools.ts. The registry
tool-count baseline moves 105 -> 106 and the frozen tool-name set gains
brain_write_batch (the one legitimate baseline change for an added tool).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task C2 (t_ac1d36ea). An optional include_raw flag on session recall
search inlines the original raw captures beside each derived record and
stamps an extracted boolean discriminator on every returned hit (true
for a distilled summary node, false for a raw session turn). The raw
captures reuse the existing collectRawRecords/expandedRawTurn machinery
already behind brain_session_expand rather than duplicating it: a summary
node carries the raw turns it was distilled from, a raw turn carries
itself.

Raw payloads respect the batch-1 clip contract: an optional
raw_budget_chars clips each inlined raw payload through
clipPayloadToBudget, so the protected identity keys (session_id,
agent_id) always survive while bulky fields such as the turn text drop
first. With the flag omitted the response is byte-identical to before
(neither extracted nor raw appears), covered by a regression test on both
the core and MCP surfaces.

The MCP surface is the existing brain_session_grep tool; no new tool is
added, so registry tool counts are unchanged. The two new schema
properties stay within the 300/160 description limits.

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

Task A2 (t_2ce46130). Adds an opt-in UserPromptSubmit hook that, on each
user prompt, relevance-recalls a small bounded brief of vault notes and
injects it as additionalContext.

Design:
- Opt-in via one flag (OPEN_SECOND_BRAIN_RECALL_INJECT_ENABLED /
  recall_inject_enabled), default OFF. Flag off is a byte-identical no-op
  (checked before any payload read or output), covered by a regression
  test.
- Pure decision core (src/core/brain/recall-inject.ts) with hard caps as
  named constants: max notes (4), max chars (900), a fixed time budget
  (2500ms), and a normalized confidence floor (0.35). It abstains on an
  empty prompt, no matches, or a top match below the floor. Any retriever
  error or a time-budget overrun is caught and surfaced as an explicit
  error decision, never a silent fallback.
- No new retriever: defaultRecallRetriever adapts the existing cross-vault
  search (which already consults every RecallSource); the brief's
  orientation line reuses deriveRecallHint over RecallHintInput.
- Every decision (inject, abstain, error) writes exactly one structured,
  payload-safe audit line (counts, scores, reason - never the prompt text
  or recalled content).
- The hook process is fail-open for the session: it arms the shared
  process ceiling and exits 0 on every path.

The hook is registered in hooks.json following the existing version-current
command shape and is distributed as a plain hooks/*.ts file resolved by the
o2b-hook wrapper (no manifest change needed). HookPayloadBase gains the
optional UserPromptSubmit `prompt` field.

Deviation: the decision core takes an injected retriever rather than calling
search directly, so the caps/floor/time-budget/brief logic is unit-testable
without a vault; the hook wires the default retriever.

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

Task A3 (t_67d38036). Closes the loop on recurring recall gaps, opt-in and
default OFF.

Core (src/core/brain/gaps/gap-loop.ts), each part independently testable:
- detectRecurringGaps aggregates the EXISTING recall-telemetry gap_counts
  aggregate, keeping gaps at or above a tunable threshold (named-constant
  default, env-overridable). Gap topics are telemetry keys verbatim, never
  words classified from prose.
- promoteGapsToTasks writes ONE durable gap-task note per recurring gap
  under Brain/gap-tasks/, deduped on a stable hash gap key via an exclusive
  create so re-promotion never collides or forks. Plain note files - they
  never touch the Hermes kanban board.
- renderGapAgenda renders open gap tasks as a compact session-start agenda
  through the shared batch-1 activity helper (each task is an openQuestion
  item carrying the fixed structural marker and a relative-age label).
- autoCloseRecalledGaps flips a task's frontmatter status to closed once its
  topic recalls at or above the confidence floor (mirroring the dream
  freshness auto-resolve precedent: a recorded status flip, never silent).
  It excludes the gap-task notes themselves from the recall, so a task can
  only close on genuine vault coverage elsewhere - never self-close on the
  placeholder that carries its own topic verbatim.

Wiring: two opt-in hooks gated by one flag (OPEN_SECOND_BRAIN_GAP_LOOP_
ENABLED / gap_loop_enabled), default OFF - flag off is a byte-identical
no-op with no writes (regression tests). gap-agenda (SessionStart) injects
the agenda; gap-promote (SessionEnd) promotes recurring gaps and auto-closes
recalled ones, writing a best-effort audit line. Both follow the existing
version-current hooks.json command shape and are distributed as plain
hooks/*.ts files.

Deviations:
- Auto-close runs in the gated session-end hook rather than inside the
  central dream planner. It faithfully mirrors the freshness auto-resolve
  precedent (recorded frontmatter status flip) while keeping all gap-loop
  writes on one opt-in surface, avoiding threading a retriever and a new
  gated branch through the deterministic dream pass. The dream module is
  untouched.
- doctor conceptGaps is left as a documented future secondary input: it
  requires a full (heavy) doctor report, and the telemetry gap_counts is the
  sufficient primary source; the plan marked it optional ("if it fits").
- grok-asset.ts is updated to mirror the new session hooks AND the A2
  recall-inject hook (which the A2 commit registered only in hooks.json)
  into the grok/Codex distribution asset, keeping the cross-runtime hook
  wiring consistent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace an em-dash with a hyphen in a session-recall test describe
string to match the branch prose convention (no em-dashes).

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

The UserPromptSubmit recall hook injected a brief built from vault note
titles verbatim (only length-trimmed) as additionalContext. Titles are
untrusted vault content, so a title carrying newlines, zero-width or bidi
characters, control characters, or a forged untrusted_source delimiter
could break the brief's line structure or smuggle encoded directives past
review.

Reuse the existing untrusted-source machinery rather than a parallel
neutralizer:
- Add fenceUntrustedContent, a path-less sibling of wrapUntrustedSource
  for aggregate content assembled from many notes. Same delimiter tag,
  same body neutralization; it carries an origin label instead of a
  per-file path/sha256.
- Neutralize every title before it reaches the recall-hint line or a note
  bullet: strip control/bidi/zero-width characters and escape delimiters
  via neutralizeUntrustedText, then collapse surviving newline/tab runs so
  a title stays on one line.
- Fence the finished brief as untrusted content and charge the fence
  delimiter overhead against the existing max-chars cap, so the whole
  fenced brief stays within the bound the audit relies on.

The audit line still records only counts, scores, and reason; behavior
with the flag off is unchanged.

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

CHANGELOG entry and link reference for 1.35.0, README What-is-new
rewrite, cli-reference section for the new doctor readiness flag,
timeline render, hook flags and search trust switches, and mcp.md
bullets for the three note-write tools and the include_raw params.
Version propagated with scripts/sync-version.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@solaitken
solaitken enabled auto-merge (squash) July 19, 2026 13:58
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d6399ef6-dd47-4ffe-8143-9264bdc72a63

📥 Commits

Reviewing files that changed from the base of the PR and between a2fafbf and 23f361b.

📒 Files selected for processing (11)
  • src/core/brain/morning-brief.ts
  • src/core/brain/recall-inject.ts
  • src/core/brain/trust/retrieval-gate.ts
  • src/core/brain/write-batch.ts
  • src/mcp/brain/notes-tools.ts
  • src/mcp/brain/write-batch-tools.ts
  • tests/cli/cli.test.ts
  • tests/core/brain/morning-brief.test.ts
  • tests/core/brain/recall-inject.test.ts
  • tests/mcp/brain-update-append-note.test.ts
  • tests/mcp/brain-write-batch.test.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • tests/cli/cli.test.ts
  • tests/core/brain/recall-inject.test.ts
  • src/core/brain/trust/retrieval-gate.ts
  • src/mcp/brain/write-batch-tools.ts
  • src/core/brain/morning-brief.ts
  • tests/mcp/brain-write-batch.test.ts
  • src/core/brain/recall-inject.ts
  • src/mcp/brain/notes-tools.ts
  • src/core/brain/write-batch.ts

📝 Walkthrough

Walkthrough

Changes

Open Second Brain 1.35.0 adds opt-in bounded recall injection, retrieval trust gating with receipts, supersede fading, durable recall-gap tasks, raw session recall, protected continuity payload clipping, atomic MCP write tools, and o2b doctor --readiness. Documentation, manifests, integrations, and tests were updated accordingly.

Trusted recall and memory write surface

Layer / File(s) Summary
Release contracts and rollout documentation
CHANGELOG.md, README.md, docs/brainstorm/..., docs/cli-reference.md, docs/mcp.md, manifests
Release metadata, design notes, implementation plans, CLI/MCP references, and package/plugin versions describe v1.35.0.
Recall injection, gap loop, and activity context
hooks/*, src/core/brain/recall-inject.ts, src/core/brain/gaps/*, src/core/brain/render/*, src/core/brain/morning-brief.ts
Prompt recall injection, durable gap-task promotion/closure, session agendas, and typed chronological activity rendering are implemented and hooked into lifecycle events.
Retrieval adjustment and trust receipts
src/core/search/*, src/core/brain/trust/*, tests/core/search/*, tests/core/brain/trust/*
Search resolves opt-in trust and supersede flags, applies deterministic rank adjustments, and returns compact trust receipts for exclusions.
Atomic note writes and MCP exposure
src/core/brain/write-batch.ts, src/mcp/brain/*, src/mcp/brain-tools.ts
Typed note, evidence, and log operations are projected before ordered atomic commits and exposed through single-operation and batch MCP tools.
Readiness probes, continuity clipping, and validation
src/core/doctor-readiness.ts, src/cli/main.ts, src/core/brain/continuity/*, tests/*
Doctor readiness probes, canonical adapter registration, protected payload clipping, raw session recall, and regression/integration coverage are added.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: itechmeat

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: the v1.35.0 trusted recall and memory write surface feature set.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/recall-trust-and-write-surface

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (4)
tests/cli/cli.test.ts (1)

215-253: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

No CLI-level test for the readiness-failure exit code path.

These tests cover pass/skip outcomes only; none forces a probe fail (e.g., semantic enabled with a missing key) to verify doctor --readiness actually exits 1 end-to-end. Since the readiness flag's stated purpose is CI gating, that wiring is worth a directed test, unless already covered at the unit level in tests/core/doctor-readiness.test.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/cli/cli.test.ts` around lines 215 - 253, Add a CLI-level test alongside
the existing readiness tests that configures the readiness environment to
produce a probe failure, such as enabling semantic checks without providing the
required key, then runs doctor with --readiness and asserts the command returns
exit code 1. Keep the existing pass/skip assertions and verify the output
identifies the failed readiness probe.
src/core/doctor-readiness.ts (1)

100-127: 🩺 Stability & Availability | 🔵 Trivial | ⚖️ Poor tradeoff

Timeout wrapper doesn't cancel the underlying call.

withReadinessTimeout reports a timeout/failure to the caller, but fn() (e.g., provider.ping() in probeEmbeddingProvider) keeps running in the background with no AbortSignal. A genuinely hung network call stays outstanding after the probe is marked fail, which is a soft rather than hard timeout. Given the CLI is short-lived, impact is limited, but this diverges slightly from the "never blocking the operator" framing in the module doc comment (the CLI output is unblocked, but the resource isn't released).

Wiring real cancellation would need provider.ping() to accept an AbortSignal, which is a larger, cross-file change; flagging for awareness rather than requiring it now.

Also applies to: 190-196

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/doctor-readiness.ts` around lines 100 - 127, No code change is
required for withReadinessTimeout; retain the current soft-timeout behavior and
do not introduce cross-file AbortSignal plumbing. Record the cancellation
limitation for future work around withReadinessTimeout and its
probeEmbeddingProvider callers.
tests/mcp/brain-update-append-note.test.ts (1)

70-87: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the specific error code, not just instanceof MCPError.

The file header documents that missing/invalid targets are "refused ... with a typed error mapped to INVALID_PARAMS," but none of these tests (Lines 70-75, 77-80, 82-87, 107-111, 113-117) actually check the error code/data — only that some MCPError was thrown. tests/mcp/brain-write-batch.test.ts already shows the pattern to mirror (asserting .data against { index, code }). Without this, a regression that maps these failures to the wrong error code would pass silently.

♻️ Example tightening for one case
-  test("a missing target is rejected with INVALID_PARAMS and writes nothing", async () => {
-    await expect(updateTool.handler(ctx, { path: "Notes/Ghost.md", content: "x" })).rejects.toThrow(
-      MCPError,
-    );
-    expect(existsSync(join(vault, "Notes/Ghost.md"))).toBe(false);
-  });
+  test("a missing target is rejected with INVALID_PARAMS and writes nothing", async () => {
+    let thrown: unknown;
+    try {
+      await updateTool.handler(ctx, { path: "Notes/Ghost.md", content: "x" });
+    } catch (err) {
+      thrown = err;
+    }
+    expect(thrown).toBeInstanceOf(MCPError);
+    expect((thrown as MCPError).code).toBe("INVALID_PARAMS");
+    expect(existsSync(join(vault, "Notes/Ghost.md"))).toBe(false);
+  });

Also applies to: 107-117

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/mcp/brain-update-append-note.test.ts` around lines 70 - 87, Strengthen
the rejection assertions in the affected tests for missing targets, missing
frontmatter/content, and path traversal, including the cases around the later
lines, to verify the thrown MCPError carries the expected INVALID_PARAMS
code/data rather than only checking its type. Mirror the assertion pattern used
in tests/mcp/brain-write-batch.test.ts while preserving the existing filesystem
side-effect checks.
src/core/brain/trust/retrieval-gate.ts (1)

80-82: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse hasEntityContaminationMarker instead of reimplementing the truthy check.

contamination.ts exports hasEntityContaminationMarker specifically so consumers like this gate share one definition of "truthy" for the marker. This file only imports the key constant and reimplements the equality check locally via truthy(), so the two definitions can silently drift.

♻️ Proposed fix
-import { ENTITY_CONTAMINATION_FRONTMATTER_KEY } from "../truth/contamination.ts";
+import { hasEntityContaminationMarker } from "../truth/contamination.ts";
-  if (truthy(meta[ENTITY_CONTAMINATION_FRONTMATTER_KEY])) {
+  if (hasEntityContaminationMarker(meta)) {

Based on the coding intent stated in contamination.ts: "so contamination stays a single boundary: the detector here and any read-time trust consumer (the retrieval trust gate) agree on the one structural key rather than each hardcoding it."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/brain/trust/retrieval-gate.ts` around lines 80 - 82, Update the
retrieval trust gate around the contamination check to call the exported
hasEntityContaminationMarker helper from contamination.ts instead of directly
reading ENTITY_CONTAMINATION_FRONTMATTER_KEY with truthy(). Remove the
now-unneeded local key import or truthy-based check, while preserving the
existing reason push behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude-plugin/plugin.json:
- Line 3: The mirrored version files were edited directly instead of being
synchronized from the canonical package.json version. Revert the manual edits in
.claude-plugin/plugin.json (line 3), .codex-plugin/plugin.json (line 3),
plugins/codex/.codex-plugin/plugin.json (line 3), plugins/hermes/plugin.yaml
(line 2), and pyproject.toml (line 13); update the version only in package.json,
then run scripts/sync-version.ts via bun run to regenerate all mirrored
manifests.

In `@src/core/brain/morning-brief.ts`:
- Around line 206-215: The open-question timeline entry in the OQ_PREFIX branch
should reuse the budget-trimmed kept.text instead of reconstructing the full
topic/domain string. Update the timeline push near keptQuestions.push while
preserving the existing timestamp and metadata behavior, so rendered text
matches totalChars.

In `@src/core/brain/recall-inject.ts`:
- Around line 164-179: Update the brief construction around the initial `lines`
header and `innerBudget` calculation so the returned fenced brief never exceeds
the supplied maxChars, including when the header alone is too large. Preserve
the existing hint and note inclusion behavior for budgets that can accommodate
the header, and ensure the result remains valid when the budget is smaller than
the header.
- Around line 193-203: Update renderNoteLine to pass note.path through the
existing single-line neutralization helper, neutralizeTitle, before
interpolating it into the pointer; preserve the current path and line-number
formatting while ensuring embedded newlines or control characters cannot break
the one-note-per-line output.

In `@src/core/brain/write-batch.ts`:
- Around line 165-189: Add a shared maximum batch-size limit and enforce it in
applyWriteBatch alongside the existing non-empty validation, rejecting
operations that exceed the limit before projection or commit. Update the
brain_write_batch inputSchema operations property with the same maxItems value
so oversized requests are rejected during schema validation.

In `@src/mcp/brain/notes-tools.ts`:
- Around line 59-74: Update writeBatchErrorToMcp so non-WriteBatchError values
are converted into a structured MCPError using INTERNAL_ERROR, matching the
fallback behavior of toolBrainCreateNote. Preserve the existing INVALID_PARAMS
mapping and details for WriteBatchError instances, while ensuring unexpected
commit-phase failures return an MCPError with a descriptive message and
underlying error information.

---

Nitpick comments:
In `@src/core/brain/trust/retrieval-gate.ts`:
- Around line 80-82: Update the retrieval trust gate around the contamination
check to call the exported hasEntityContaminationMarker helper from
contamination.ts instead of directly reading
ENTITY_CONTAMINATION_FRONTMATTER_KEY with truthy(). Remove the now-unneeded
local key import or truthy-based check, while preserving the existing reason
push behavior.

In `@src/core/doctor-readiness.ts`:
- Around line 100-127: No code change is required for withReadinessTimeout;
retain the current soft-timeout behavior and do not introduce cross-file
AbortSignal plumbing. Record the cancellation limitation for future work around
withReadinessTimeout and its probeEmbeddingProvider callers.

In `@tests/cli/cli.test.ts`:
- Around line 215-253: Add a CLI-level test alongside the existing readiness
tests that configures the readiness environment to produce a probe failure, such
as enabling semantic checks without providing the required key, then runs doctor
with --readiness and asserts the command returns exit code 1. Keep the existing
pass/skip assertions and verify the output identifies the failed readiness
probe.

In `@tests/mcp/brain-update-append-note.test.ts`:
- Around line 70-87: Strengthen the rejection assertions in the affected tests
for missing targets, missing frontmatter/content, and path traversal, including
the cases around the later lines, to verify the thrown MCPError carries the
expected INVALID_PARAMS code/data rather than only checking its type. Mirror the
assertion pattern used in tests/mcp/brain-write-batch.test.ts while preserving
the existing filesystem side-effect checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4c9c1b4c-8acf-4b55-95ae-dd0cd2c57cd0

📥 Commits

Reviewing files that changed from the base of the PR and between 426d06f and a2fafbf.

📒 Files selected for processing (84)
  • .claude-plugin/plugin.json
  • .codex-plugin/plugin.json
  • CHANGELOG.md
  • README.md
  • docs/brainstorm/recall-trust-and-write-surface/cli-output/claude.md
  • docs/brainstorm/recall-trust-and-write-surface/cli-output/prompt.md
  • docs/brainstorm/recall-trust-and-write-surface/design.md
  • docs/brainstorm/recall-trust-and-write-surface/plan.md
  • docs/brainstorm/recall-trust-and-write-surface/variants.md
  • docs/cli-reference.md
  • docs/mcp.md
  • hooks/gap-agenda.ts
  • hooks/gap-promote.ts
  • hooks/hooks.json
  • hooks/lib/stdin.ts
  • hooks/recall-inject.ts
  • openclaw.plugin.json
  • package.json
  • plugin.yaml
  • plugins/codex/.codex-plugin/plugin.json
  • plugins/hermes/plugin.yaml
  • pyproject.toml
  • src/cli/install/install.ts
  • src/cli/main.ts
  • src/core/brain/continuity/read-model.ts
  • src/core/brain/continuity/store.ts
  • src/core/brain/continuity/types.ts
  • src/core/brain/gaps/gap-loop.ts
  • src/core/brain/morning-brief.ts
  • src/core/brain/notes/create-note.ts
  • src/core/brain/paths.ts
  • src/core/brain/recall-inject.ts
  • src/core/brain/render/activity-line.ts
  • src/core/brain/session-recall.ts
  • src/core/brain/token-impact.ts
  • src/core/brain/trust/retrieval-gate.ts
  • src/core/brain/trust/retrieval-receipts.ts
  • src/core/brain/truth/contamination.ts
  • src/core/brain/untrusted-source.ts
  • src/core/brain/write-batch.ts
  • src/core/config.ts
  • src/core/doctor-readiness.ts
  • src/core/install/adapters/all.ts
  • src/core/install/grok-asset.ts
  • src/core/search/enrich.ts
  • src/core/search/index.ts
  • src/core/search/rank-adjust.ts
  • src/core/search/ranker.ts
  • src/core/search/result-filters.ts
  • src/core/search/search.ts
  • src/core/search/types.ts
  • src/mcp/brain-tools.ts
  • src/mcp/brain/notes-tools.ts
  • src/mcp/brain/recall-tools.ts
  • src/mcp/brain/write-batch-tools.ts
  • src/mcp/registry-guard.ts
  • tests/cli/cli.test.ts
  • tests/core/brain/continuity-read-model.test.ts
  • tests/core/brain/gap-loop.test.ts
  • tests/core/brain/lifecycle/temporal-replace.test.ts
  • tests/core/brain/recall-inject.test.ts
  • tests/core/brain/render/activity-line.test.ts
  • tests/core/brain/session-recall.test.ts
  • tests/core/brain/token-impact-clip.test.ts
  • tests/core/brain/trust/retrieval-gate.test.ts
  • tests/core/brain/trust/retrieval-receipts.test.ts
  • tests/core/brain/untrusted-source.test.ts
  • tests/core/brain/write-batch-vocab.test.ts
  • tests/core/brain/write-batch.test.ts
  • tests/core/doctor-readiness.test.ts
  • tests/core/search/rank-adjust.test.ts
  • tests/core/search/retrieval-trust-gate.test.ts
  • tests/core/search/store.test.ts
  • tests/core/search/store.vec.test.ts
  • tests/core/search/supersede-fade.test.ts
  • tests/helpers/search-fixtures.ts
  • tests/hooks/gap-loop.test.ts
  • tests/hooks/recall-inject.test.ts
  • tests/mcp/brain-tools-parity.test.ts
  • tests/mcp/brain-update-append-note.test.ts
  • tests/mcp/brain-write-batch.test.ts
  • tests/mcp/mcp.test.ts
  • tests/mcp/removed-tools.test.ts
  • tests/mcp/session-recall-tool.test.ts

Comment thread .claude-plugin/plugin.json
Comment thread src/core/brain/morning-brief.ts
Comment thread src/core/brain/recall-inject.ts Outdated
Comment thread src/core/brain/recall-inject.ts
Comment thread src/core/brain/write-batch.ts
Comment thread src/mcp/brain/notes-tools.ts
@solaitken

Copy link
Copy Markdown
Collaborator Author

CodeRabbit triage: 8 findings fixed in the follow-up commit (morning-brief open-question budget, recall-inject header cap and path neutralization, write-batch size bound with matching maxItems, MCPError wrapping for unexpected write errors, CLI-level readiness fail exit test, tightened update/append error-code assertions, shared contamination marker helper). Two deliberate skips: the mirrored-version Critical is answered inline (bump was done via sync-version.ts, CI sync check green), and the readiness soft-timeout note is acknowledged as a recorded limitation, matching the reviewer's own no-change-required guidance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@solaitken
solaitken merged commit 95dc857 into main Jul 19, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants