Open Second Brain
Memory server
First committed by hermes-vps-agent
Published in itechmeat/open-second-brain. Publisher page
550 GitHub stars
What it is
Second brain for AI agents using Obsidian-compatible Markdown vaults. Works with Hermes, Claude Code, Codex, and OpenClaw.
AutoGineer overview, from the brain’s own files
Open Second Brain acts as a memory provider for AI agents, enabling them to read and write persistent knowledge directly into an Obsidian-compatible Markdown vault. It employs a deterministic 'dream' pass to convert repeated signals into actionable rules, while maintaining a hash-chained log for auditability. The system supports various agent clients through MCP servers, lifecycle hooks, and a dedicated CLI, ensuring that agent-learned information remains accessible, versioned, and searchable as standard Markdown files.
How it works
Memory model: Hybrid
AutoGineer overview, from the brain’s own files
The system organizes data into a Brain folder containing Markdown files. Agents interact with this vault via MCP servers, lifecycle hooks, or the o2b CLI. Signals from agents are stored in an inbox, processed through a dream pass to generate preferences, and stored as rules in active.md. A search index is maintained in a hidden directory and can be rebuilt from the source files. The system also supports truth ledgers, decision models, and staged review gates for agent writes.
| Folder | Role | Files |
|---|---|---|
| .github | agent-config | 7 |
| bin | scripts | 1 |
| docs | docs | 774 |
| hooks | hooks | 35 |
| scripts | scripts | 22 |
| skills | agent-instructions | 6 |
| src | source-code | 1496 |
| templates | templates | 23 |
| tests | tests | 2101 |
| .agents | folder | 1 |
| .ai-notes | folder | 1 |
| .apb | folder | 87 |
| .claude-plugin | folder | 2 |
| .codex-plugin | folder | 1 |
| .githooks | folder | 2 |
| install | folder | 16 |
| openclaw | folder | 1 |
| openspec | folder | 33 |
| plugins | folder | 21 |
| schemas | folder | 5 |
The agent reads
- install/codex.md evidence: README.md
- search_iterative_recall_enabled evidence: README.md
- search_pool_max_per_session evidence: README.md
- docs/decision-models.md evidence: README.md
The agent writes
- package.json evidence: CLAUDE.md
- CHANGELOG.md evidence: CLAUDE.md
- docs/brain-store.md evidence: README.md
- docs/updating.md evidence: README.md
Requirements
Runtimes: Node.js, Python >=3.11, Bun
Databases: SQLite
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Google Antigravity, Kiro, OpenCode, Amp, Roo Code, Cline, JetBrains Junie, Devin (ex-Windsurf)
Set it up
This brain runs code automatically: 54 hooks (codex), 3 scripts. Review .githooks/pre-commit, .githooks/pre-push, hooks/hooks.json, package.json, plugins/codex/hooks/hooks.json before opening the folder in Claude Code. No step below turns a hook on for you.
.githooks/pre-commit:runs on git pre-commit.githooks/pre-push:runs on git pre-pushhooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" post-write-reminder; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook post-write-reminder; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" subagent-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook subagent-inject; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" reground-deliver; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook reground-deliver; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" active-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook active-inject; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" gap-agenda; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook gap-agenda; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" backlog-nudge; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook backlog-nudge; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" log-catchup; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook log-catchup; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" view-render; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook view-render; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" recall-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook recall-inject; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" nav-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook nav-inject; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" counterparty-brief; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook counterparty-brief; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" reground-deliver; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook reground-deliver; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" pretool-orient; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook pretool-orient; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" pretool-host-backup; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook pretool-host-backup; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" stop-log-guardrail; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook stop-log-guardrail; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" answer-quote-check; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook answer-quote-check; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" hygiene-digest; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook hygiene-digest; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" subagent-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook subagent-inject; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" gap-promote; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook gap-promote; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" active-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook active-inject; exit 0hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0package.json:bun scripts/prepare.ts || exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" post-write-reminder; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook post-write-reminder; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" subagent-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook subagent-inject; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" reground-deliver; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook reground-deliver; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" active-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook active-inject; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" gap-agenda; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook gap-agenda; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" backlog-nudge; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook backlog-nudge; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" log-catchup; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook log-catchup; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" view-render; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook view-render; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" recall-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook recall-inject; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" nav-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook nav-inject; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" counterparty-brief; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook counterparty-brief; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" reground-deliver; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook reground-deliver; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" pretool-orient; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook pretool-orient; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" pretool-host-backup; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook pretool-host-backup; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" stop-log-guardrail; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook stop-log-guardrail; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" answer-quote-check; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook answer-quote-check; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" hygiene-digest; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook hygiene-digest; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" subagent-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook subagent-inject; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" gap-promote; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook gap-promote; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" active-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook active-inject; exit 0plugins/codex/hooks/hooks.json:r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0
- Not scanned by AutoGineerThe author’s MCP configuration (.claude-plugin/plugin.json) also starts it in a way AutoGineer does not offer. Read that file: AutoGineer did not scan that path; what it runs is listed under “This brain runs”. https://github.com/itechmeat/open-second-brain/blob/233b91d1bdc7cf74c47ea1c1fd72e949887e1fc4/.claude-plugin/plugin.json
First committed by hermes-vps-agent. Published in itechmeat/open-second-brain. Licence: MIT License.
Usage
AutoGineer overview, from the brain’s own files
- Initialize a new vault for agent memory
- Connect agents like Claude Code or Codex via MCP
- Manage agent preferences and rules through automated dream passes
- Audit agent activity using hash-chained logs
- Perform semantic or keyword searches across agent-generated notes
- Enforce write-path integrity with staged review gates
- “o2b brain today”
- Displays the current day's view and active memory markers for the agent session.
- “o2b brain rollback”
- Restores the vault state from the last snapshot, reversing recent brain mutations.
Documentation by hermes-vps-agent
Open Second Brain
Image: Open Second Brain - your knowledge, amplified by AI
Open Second Brain is a memory layer for AI agents that lives in an Obsidian vault. Agents read and write it through MCP servers, lifecycle hooks and the o2b CLI. Everything the agent learns is a Markdown file under Brain/, so you can open, edit, grep and version it like any other note. A deterministic dream pass turns repeated signals into rules and retires the ones nothing uses; no model runs inside that algorithm.
How it fits
flowchart LR
subgraph Clients["Agent clients"]
Plug["Plugins: Claude Code, Codex,<br/>Hermes, opencode, Grok Build"]
Mcp["MCP config: Cursor, kiro, Copilot CLI,<br/>Gemini CLI, ZCode, any MCP host"]
Claw["OpenClaw<br/>in-process plugin"]
Cliu["Aider, Pi<br/>CLI and context file"]
end
subgraph Product["Open Second Brain"]
Srv["MCP servers<br/>open-second-brain, open-second-brain-writer"]
Hooks["Lifecycle hooks<br/>o2b-hook"]
Cli["CLI<br/>o2b"]
Core["Core"]
end
subgraph Vault["Your vault"]
Brain["Brain/<br/>Markdown the agent writes"]
Notes["Your notes"]
Index[(".open-second-brain/brain.sqlite<br/>rebuildable search index")]
end
Plug --> Srv
Plug --> Hooks
Mcp --> Srv
Claw --> Core
Cliu --> Cli
Srv --> Core
Hooks --> Core
Cli --> Core
Core --> Brain
Core --> Index
Core -. reads, writes a named note on request .-> NotesEach client reaches the same core through a plugin, an MCP registration or the CLI. The core keeps its own records under Brain/ and a generated search index under .open-second-brain/ that it can rebuild from the files. Your notes are read; the note tools write one only at a path the caller names.
flowchart LR
Agent["Agent"] -->|brain_feedback| Inbox["Brain/inbox/<br/>signal"]
Inbox -->|dream pass| Pref["Brain/preferences/<br/>rule"]
Pref -->|rendered into| Active["Brain/active.md"]
Active -->|read at session start| Agent
Agent -->|brain_apply_evidence| Log["Brain/log/"]
Log -->|next dream pass| PrefThe learning loop: signals become rules, rules reach the next session through Brain/active.md, and applied or violated evidence confirms, quarantines or retires them. Details: docs/how-it-works.md.
Install
Install Bun 1.1 or later:
install/prerequisites.md.Install the plugin for your client (guides below), or get the
o2bCLI:install/prerequisites.md.Create the vault files:
o2b init --vault /path/to/vault --name "My Second Brain" o2b brain init --vault /path/to/vaultConnect the client (table below), then check the result:
o2b doctor --vault /path/to/vault o2b install --check
The full router with readiness criteria is install.md; native Windows is covered in install/windows.md.
Supported clients
| Client | Integration | Guide |
|---|---|---|
| Claude Code | Marketplace plugin: MCP servers and hooks | install/claudecode.md |
| OpenAI Codex | Marketplace plugin, then o2b install --target codex --apply | install/codex.md |
| ChatGPT desktop | Reads the shared Codex config: o2b install --target codex --apply | install/codex.md |
| Hermes Agent | Plugin and memory provider | install/hermes.md |
| OpenClaw | Native plugin, no MCP | install/openclaw.md |
| opencode | o2b install --target opencode --apply: MCP servers and a native plugin | install/opencode.md |
| Grok Build | o2b install --target grok --apply: MCP servers and hooks | install/grok.md |
| Cursor | o2b install --target cursor --apply | install/cursor.md |
| kiro | o2b install --target kiro --apply | install/kiro.md |
| GitHub Copilot CLI | o2b install --target copilot-cli --apply | install/copilot-cli.md |
| Gemini CLI | o2b install --target gemini-cli --apply | install/gemini-cli.md |
| ZCode | MCP servers added to its config by hand | install/zcode.md |
| Aider | o2b install --target aider --apply: context file, no MCP | install/aider.md |
| Pi | o2b install --target pi --apply: skill over the CLI | install/pi.md |
| Any other MCP host | o2b install --target generic --apply --out - prints the server entries | install/generic.md |
o2b uninstall --target <name> --apply removes what o2b install wrote; the vault is never touched (install.md).
Capabilities
Preferences that learn and expire. Signals, a deterministic dream pass, confidence and retirement: how it works.
Reversible changes. Brain mutations take a snapshot first;
o2b brain rollbackrestores it: snapshots.Attributed writes.
o2b brain writeslists every note write by agent and device,writes revertplans a restore,o2b brain freezestops all writers, ando2b brain log verifychecks the hash-chained log: Brain CLI.Search. Keyword search with an optional semantic lane, result explanations, recall profiles and compact cards; an opt-in iterative loop (
search_iterative_recall_enabled, default off) keeps querying when coverage gaps survive, pausing with a plan the caller answers throughbrain_search's round trip; and an opt-in per-source-session cap (search_pool_max_per_session, default off) keeps one long imported session from filling the candidate window, so diversity spreads across sessions: search CLI, iterative recall, pool cap, ranking.Session recall. Import agent transcripts already on disk, search them, or select whole-thread coverage under a character budget (
o2b brain session-grep --operation coverage --char-budget <n>, with--entity-keyseeding the fan-out from mined entity mentions): session logs, coverage.Hygiene.
o2b brain hygiene scanfinds contested, duplicate, stale and unused memories;applyruns only the findings you pick: maintenance.Claim ledger.
o2b brain truth ingestrecords an entity claim with an optional validity window (--valid-from/--valid-until; with the flags absent, a window on the source record's frontmatter is adopted and frozen at ingest),truth eventsrecalls a windowed slice of the ledger, andtruth stategrounds an agent-stated claim with an anchoring verdict - committed, or reported back with reason codes.truth stateaccepts an optional opaque--sample-idnaming the context-pack sample the claim drew on, and claim reads report the receipt and outcome provenance behind it at the caller's reach - "receipt known, outcome unknown", never an invented verdict: entity truth CLI.Corrections.
o2b brain lifecycle correct <target>reports a correction's blast radius and retires the affected records - validity-closed by default, tombstoned with--flatly-wrong, dry run by default and--applyto write. A retired record that recall can still serve is served only beside its correction, never alone: belief lifecycle CLI.Sharing and backup. Knowledge packs export a chosen subset;
o2b brain bank-exportwrites a one-file backup bundle: knowledge packs.Secret custody. Credentials live encrypted in the vault's state dir behind a capability-gated store;
$secret:NAMEreferences resolve at the use sites,o2b brain secret unlockwraps the keyfile under a passphrase, andsecret export/importmove the store between installs as one passphrase-encrypted bundle: custody CLI.Today view and note markers.
o2b brain today,@osb loopand@osb setmarkers: Brain CLI.Self-inspection.
o2b version,o2b install --frictionando2b state statusreport what is installed and where state lives: CLI reference.MCP surface. Tools, tool profiles for hosts with tool limits, and the always-loaded writer server:
docs/mcp.md.
Optional features
Semantic search: an embedding provider plus
sqlite-vec; theembeddings-setupskill walks through it:skills/embeddings-setup/SKILL.md.Decision models: a typed judgment model that can rerank search and filter candidates, off by default per use:
docs/decision-models.md.Deep relational recall: a fourth search arm over typed links, off by default. Its traversal runs under width budgets - 8 seeds, 4 edges per node, 16 nodes in total, and a hub above 12 walked edges is reached but not expanded - each overridable through an
OPEN_SECOND_BRAIN_SEARCH_TRAVERSAL_*environment variable or asearch_traversal_*config key; entity co-occurrence bridges join the walk by default and switch off separately (OPEN_SECOND_BRAIN_SEARCH_ENTITY_BRIDGES): retrieval quality.Staged review for agent writes, off by default. With no key set every write publishes exactly as before.
write_approval.notes/OPEN_SECOND_BRAIN_WRITE_APPROVAL_NOTES_ENABLEDandwrite_approval.ingest/OPEN_SECOND_BRAIN_WRITE_APPROVAL_INGEST_ENABLED(each falling back to thewrite_approval.enabledmaster, default off) stage note creates and ingest summary pages intoBrain/pending/beside the staged signals, where they stay out of the search index until an operator runso2b brain pending listand applies or rejects them; write-path integrity.A permissions document, absent by default.
Brain/_permissions.yaml(an operator-edited vault file) resolvesallow/ask/denyper agent, role and target for the write, ingest and owner-write actions; with the file absent every check behaves exactly as today.askstages the write,denyrefuses with thewrite-refusedtoken and the next commando2b brain permissions show, and every ask/deny verdict lands in a queryable decision ledger underBrain/logs/decisions/;o2b brain permissions showdry-runs the decision table,ledgerreads the rows; Brain CLI.The owner-write gate, off by default.
integrity.owner_scope_writesinBrain/_brain.yaml(off|warn|fail, defaultoff) refuses a caller-named owner that differs from the caller's resolved identity on the preference and note lanes (warnallows and records one decision-ledger row); a documentowner_writeverdict composes most-restrictive-wins with the gate; write-time integrity.Named MCP tokens, optional.
o2b mcp token mint|rotate|revoke|listkeeps a hash-at-rest token per agent (.open-second-brain/secrets/mcp-tokens.json; material shown exactly once). Over HTTP a valid token authenticates as its agent per request, the shared--api-keystays valid as the operator master credential, andmcp_tokens_required/OPEN_SECOND_BRAIN_MCP_TOKENS_REQUIRED(defaultfalse) makes a non-empty token map refuse credential-less requests.o2b bootstrap --target <harness> [--token] [--rotate] [--check]provisions MCP registration, token and receipt in one idempotent command, ando2b bootstrap --remove <harness>tears a provision down again; the minted token authenticates HTTP clients configured by hand, while the registered stdio harness presents no credential and keeps its config-derived identity; core CLI.Ambient capture consent, opt-out.
guardrails.ambient_writeback: falsesuppresses the ambient extraction lane with a countedambient-withheldevent (absent keeps today's behavior), andguardrails.ambient_ttl_daysstamps anexpiration_dateon ambient-extracted signals so reads drop them after the window (absent stamps nothing); a TTL-stamped signal still stages when the review gate is on and survives apply verbatim.Open decisions.
o2b brain decision open --title <t> --question <q> --option <o> [...]parks a question with enumerated options atBrain/decisions/open-<slug>.md;resolvemints the realtype: decisionpage,discardcloses without deciding, and the morning brief renders up to five open questions: belief lifecycle.
What is new
Open Second Brain 1.98.0 is the agent-presence and self-improvement release. o2b install --target <host> --apply now writes the brain-usage block into the agent-instruction file the installed host reads at the vault root (AGENTS.md for codex, cursor, opencode and aider; GEMINI.md for gemini-cli) under the same consent prompt as the config writes, preserving every byte outside the block and recording its ownership so uninstall removes exactly the pair the tool wrote; the dream pass gains an operator-approved exit into those same files, where a lesson promotes into a managed lessons block only with a fresh approval pinned to the approved text, and the doctor grades every block state it can name. The skill lane gains a benchmark-gated rewrite loop: a skill is rewritten only after a candidate beats the incumbent on scored dev tasks with full coverage and survives the one held-out check - Open Second Brain never calls a model, the scorer reads structural predicates and nothing the agent claims about its own work, and the accept stays an operator step on the command line. And ChatGPT desktop is named where its operators look for it: it reads the shared Codex config, so one codex registration serves both. Every release is described in the CHANGELOG.
Documentation
| Topic | Page |
|---|---|
| Mental model, vault layout, dream pass, safety properties | docs/how-it-works.md |
| Every CLI verb and flag | docs/cli-reference.md |
| MCP protocol, tools, writer split | docs/mcp.md |
| Brain store: setup, sync, migration, the human view | docs/brain-store.md |
| Architecture and configuration model | docs/architecture.md |
Updating (o2b update) and upgrade notes | docs/updating.md |
| Observability events | docs/observability.md |
| Metrics data contract | docs/metrics.md |
| Cross-project pointer | docs/cross-project-pointer.md |
| Scheduled Brain digest (Hermes cron) | docs/hermes-cron.md |
| Stability policy | docs/stability.md |
| Origin of the idea | docs/idea.md |
Project
CHANGELOG · Security policy · Source on GitHub · MIT License
CHANGELOG.md
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
1.98.0 - 2026-11-02
Open Second Brain 1.98.0 is the agent-presence and self-improvement release: the vault states its usage where the agents that read it actually look, and its skills improve only under evidence. The install path closes its own gap - o2b install --target <host> --apply now writes the brain-usage block into the agent-instruction file the installed host reads at the vault root, through a shared managed-block writer that preserves every byte outside the block, refuses a hand-edited block by name, and records its ownership so uninstall removes exactly the pair the tool wrote. The dream pass gains an operator-approved exit into those same files: a lesson promotes into a managed lessons block only when the full qualification holds and a fresh approval pins the approved text, every non-member carries one named blocked reason, and the doctor grades every block state it can name. The skill lane gains a benchmark-gated rewrite loop where a skill is rewritten only after a candidate beats the incumbent on scored dev tasks with full coverage and survives the one held-out check - Open Second Brain never calls a model, the calling agent executes the task envelopes, the scorer reads structural predicates and nothing the agent claims about its own work, the accept stays an operator step on the command line, and the incumbent's bytes ride a content-restoring rollback journal until the sequence settles. And ChatGPT desktop is named where its operators look for it: it reads the shared Codex config, and the detect output and install docs say so.
Added
The installer writes the brain-usage block into the agent rules files.
o2b install --target <host> --applyfor a target whose host reads an agent-instruction file now also writes the canonical write-gate managed block into that file at the vault root - AGENTS.md for codex, cursor, opencode and aider, GEMINI.md for gemini-cli (name-implied only, the open question the precedence table's docblock already carries) - mapped through the oneHOST_INSTRUCTION_PRECEDENCEtable, never a second host-to-file map; copilot-cli, grok, kiro, pi and generic install no instruction step and the plan says so with a named skip. The block body is the canonical gate-and-usage text the write-back contract audits - the same-turn atomic-fact write gate merged with the cross-project-pointer usage content - so the readiness probe grades exactly what install wrote, and a file with no block at all staysmissing-block, graded skipped, not failed. Every byte goes through the shared rules-block writer, never a direct fs write: operator content outside the block is preserved byte-for-byte, the first write takes an exclusive-create backup, a re-apply is a byte no-op, and a hand-edited block refusesuser-modified-blockwith exit 4 unless--force. The apply is bound to the approved preview - the apply re-derives the plan from the live disk and refusespreview-driftwhen anything the consent was shown has moved - and the instruction write rides the same consent verdict as the adapter's config writes: one prompt above both, nothing new to consent to twice. Ownership lands in<vault>/.open-second-brain/instruction-block.lock.json- per file, the block id, the begin marker and the applied time, the applied time being what makes a hand-edit refusable - and that lock is whato2b uninstall --target <name> --applyreads to remove exactly the recorded pair, never a hand-placed one; an absent pair is a named skip and the ownership entry is cleared either way.Operator-approved lesson promotion into the lessons block.
o2b brain lessons candidates | approve <pref-id> | deny <pref-id> | refresh-blocksturns the dream pass's lessons into content the agents that read the rules files actually see, behind the operator's hand. A lesson qualifies only under the full conjunction: preference-derived (dead-end lessons never promote), the preferenceconfirmed, the confidence bandhigh, the lesson tierpreferredin the current digest, an explicitly enabled promotion scope (lessons.promotion.scopes, env twinOPEN_SECOND_BRAIN_LESSONS_PROMOTION_SCOPES; absent or empty disables promotion entirely), and a fresh approval - the ledger pins the content hash of the text the operator approved, so the operator approves text and not an id, and a preference whose text has moved since drops from the block until it is approved again. Every non-member carries exactly one named blocked reason from a closed vocabulary with a pinned precedence, an operator denial dominating. The block body is a deterministic render - no timestamp ever appears, ordering is tier, then scope, then id, and every rendered lesson is provably one line that cannot forge a managed-block marker: a scope or id carrying a line break or a marker-shaped sequence refuses the render by name rather than being silently rewritten.approvewrites through the same shared rules-block writer on its consent ladder (--apply,--yes,--force) into the named target files (--target AGENTS.md|CLAUDE.md|GEMINI.md, repeatable; defaulting to the ones that exist), andrefresh-blocksre-renders existing blocks dry-run by default and never creates one. The approval ledger is machine state at<vault>/.open-second-brain/lessons-approvals.json, deliberately in the derived store and outsideBrain/so an agent cannot read its own approval state as a note surface - schema-versioned, fail-closed on damage, with the audit history in thelessons-approved,lessons-deniedandlessons-promotedlog events. The doctor's fail-soft, read-only readiness check grades each managed lessons block against the same derivation a refresh would render -absent,current,drifted,stale-lessons- a damaged ledger surfaces aslessons-approvals-unreadable, and every finding names the verb that heals it; the brain status snapshot carries the approval counts (approved, denied, stale) report-only, fail-soft with a generalized read error that names the doctor as the surface for the cause, because machine paths stay out of MCP output.Benchmark-gated skill rewrites: a skill is rewritten only under evidence. The task set lives vault-side at
Brain/skill-benchmarks/<skill>/tasks.jsonwith its frozen receipt (spec.json), and the loop runs kernel by kernel:o2b brain skill-proposals rewrite-plan <skill>verifies the task set against the receipt and prints it with the dev envelopes, the calling agent executes and submits the transcripts, andrewrite-score <skill> [--rounds-dir <dir>]grades them. Open Second Brain never calls a model - the envelopes areneeds-llm-steppayloads, dev members only until the single held-out check after a promote - and the scorer validates the submitted shape and grades structural predicates only, refusing a submission that carries a self-reported success field, so nothing the agent claims about its own work can move the metric. Every transcript is bound to the candidate body it was produced against: each envelope carries the candidate's sha-256, and the scorer refuses - by name - a transcript whose binding is missing or stale. Selection is the preregistered three arms, and coverage is part of the preregistration: the dev arms decide only when every dev member has a scored transcript on both arms - a short arm isnot_enough_evidence, the honest outcome either way - a score for an id outside the receipt's membership is refused as what it is, and the held-out gate runs once, on the full held-out membership only, on the candidate that earned promotion; a miss downgrades it, and a refusal writes nothing, because a hold would record a verdict the one-shot check never earned and would leak which held-out tasks fail. The winning candidate stages as ascored_rewriteproposal whose acceptance is the operator's step: MCP never accepts one (a typed refusal points at the command line), the CLI accept requires--yesand re-verifies the live receipt and the scored contract at accept time, and the incumbentSKILL.mdbytes are captured to a vault-side prior-content copy before the overwrite, so the accept journal's rollback restores content and not just existence; the journal parser refuses a marker whose materialized path the accept sequence cannot have produced. Modes:skill_rewrite.mode(gated, the default - one candidate per gate round;iterative- a bounded edit loop over the dev tasks, still gated before any write) and an optionalskill_rewrite_judge_cmdjudge that stays advisory - it never moves thetasks_passed_ratemetric the gate decides on. A read-onlybrain_skill_rewrite_statusMCP tool mirrors one skill's lane - the frozen receipt and whether it still verifies, the completed rounds' verdicts and rates, the staged rewrite's slug - ids and rates only, never task or transcript text, with an unknown skill answering as a named empty status; theskill_rewrite_eval_gradetelemetry record is written once per completed gate run, and a run that stops before the gates complete writes nothing.ChatGPT desktop is named as a reader of the shared Codex config. ChatGPT desktop registers no MCP servers of its own; it reads the
$CODEX_HOME/config.tomlfile the codex target writes, so oneo2b install --target codex --applyserves both hosts and oneo2b uninstall --target codex --applyundoes it for both. The codex adapter's merge-lane detect note - the CLI-absent lane, where an operator with only the desktop app would otherwise have no surface telling them this target is theirs - names ChatGPT desktop, the README and plugin host tables gain the row, andinstall/codex.mdgains the ChatGPT desktop section. There is deliberately no ChatGPT install target id: a second target for one config file would claim bytes the codex target owns and report them as drift.Docs:
docs/cli-reference.mdgains theo2b brain lessonsfamily and therewrite-plan/rewrite-scoreforms on the skill-proposals row, states the instruction-block step on the install and uninstall rows and the lessons-block doctor checks on the doctor row;docs/mcp.mddocuments thebrain_skill_rewrite_statustool;docs/observability.mdnames the lessons log events and the rewrite-eval metric;install/codex.mdgains the ChatGPT desktop section;docs/cross-project-pointer.mdnames the installer as the managed way to keep the block written, with the manual snippet staying for hand-provisioned hosts; the README host tables name ChatGPT desktop.
1.97.0 - 2026-11-01
Open Second Brain 1.97.0 is the delivery-trust release: the surfaces that hand stored vault text to a remote agent now state whose words they are delivering, and the operator gains a signed way to vouch for the store state its surfaces stamp. Every MCP read that returns stored-record text - the search family, the report-shaped reads, the context and pinned-context tools and the osb:// preference, topic, active, lessons and backlinks resources - fences agent-origin or unverified text at remote reach inside the same wrapper the intake path uses, fails closed to the fence whenever provenance cannot be derived, and leaves local reads and the stored bytes untouched, with a doctor check naming the pages whose provenance nothing proves. Context receipts gain a verify face that re-hashes what a session was delivered against the vault it was drawn from, and a doctor check reports drift in recent receipts. The opencode lane closes most of its distance to the Claude Code lane: opt-in tool gating that blocks only on an explicit shim deny, and prompt-time recall, navigation and brief injection delivered exactly once per turn through the existing context hook, with a census pinning the surfaces that remain declared divergences. And the store can be signed: o2b brain attest measures the log heads and the state-surface stamps into an Ed25519-signed record under an operator-managed key - never auto-generated - with a read-only MCP mirror and a doctor check that reports stale or diverged attestations.
Added
Tier-keyed quote fencing on remote read deliveries. Stored text that reaches a caller below local reach arrives wrapped in the explicit
<untrusted_source origin="...">fence the intake path already uses - same tag, same body neutralization, no second delimiter vocabulary - with the origin attribute naming the page's derived provenance: anuntrusted_sourcemarker, acontent_class: prompt-derivedclass, or anorigin_channelofmcp-toolorimportreads agent-authored;origin_channel: cliwith no other marker is the one operator-authored answer that ships plain. The derivation fails closed - a page with no stamps, theunsetchannel literal, or one that cannot be read resolves fenced, never trusted - because absence of a stamp is a statement about knowledge, not authorship. The fence lives inside the delivered field -snippetandcontenton the search family (search and expand), the report-shaped reads (query, agent query, agent diff, decisions, the claim graph's principles and the unlinked-mention contexts),brain_contextandbrain_pinned_context, and theosb://preference, topic, active, lessons, digest and backlinks resources - so no output schema grows a marker field; withheld rows gain no fence and no count, and the Brain log and event bodies are the stated delivery-shaping boundary and stay verbatim. Read-time only: the fence itself never writes - local reach is byte-identical to the pre-wave bytes and the stored bytes the fence reads do not change (the one write-side change this release makes to stored bytes, the mutation channel restamp, is under Changed below). The fail-softdelivery-provenance-unknowndoctor check is the widening made observable: one warning counting the delivered pages that carry no derivable provenance - the complement of what a stamp proves - naming up to five paths and counting the rest, excluding the system-rendered Brain-root pages and never naming a page the caller may not read; the remediation is stamping, which every write path already does going forward.Context-receipt verify: what a session was delivered, re-checked against the vault.
o2b brain context-receipts verify <receipt-id>and a newverifyoperation on thebrain_context_receiptsMCP tool re-hash each item's recorded delivered-text digest against the live vault: the page is re-read, the delivery-time transforms re-applied - including the recorded per-item character budget, so an item delivered trimmed does not false-CHANGED - and each item answersmatch/changed/unavailable, the last with its reason (no_path,no_hash,file_missing,unreadable); a structural id, a hashless item and apre_compressprinciple line are never guessed into a verdict. MATCH is a claim about delivered bytes, never liveness: a retired note verifies against its retired body. Private and redacted receipts are refused symmetrically with the session fold, the MCP face answers at local reach only with a typed refusal below it (reach,receipt_withheld), the CLI exits 1 naming the changed items, and nothing persists - no new capture machinery, the read is the product. The fail-softreceipt-content-driftdoctor check rides the same kernel over a bounded recent window - the twenty most recent context receipts within thirty days - and reports one warning with per-verdict counts and the next command naming the newest drifted receipt; legacy hashless andsession_injectreceipts stay silent, and a window that cannot be read reaches the uncertain stream asreceipt-content-drift-unmeasuredrather than a clean bill.opencode lane parity: fail-soft tool gating and once-per-turn prompt injection. The native opencode plugin relays the opt-in
pretool-orient(hook_strict_enabled) andpretool-host-backup(host_settings_backup, file-mutating tools only) shims through the host'stool.execute.before/tool.hook("execute.before")event with synthetic PreToolUse payloads carrying the opencode session id, so the vault hook-state stamps and the audit trail behave exactly as on the Claude Code lane; only an explicit structuredpermissionDecision: "deny"blocks the tool call, by throwing with the shim's reason - a flag that is off (which also means zero spawns), a missing binary, a spawn failure, a timeout or malformed shim output passes the call untouched. On the prompt event (chat.message/session.hook("prompt")) the plugin relays the opt-inrecall-inject,nav-inject,counterparty-briefandreground-delivershims with synthetic UserPromptSubmit payloads into a single-slot per-turn queue that the existing context delivery points drain on the first model request of the turn - exactly once per turn, so the injection ledger dedupes (recall once per session, brief on the first qualifying turn, reground's take-once cursor) exactly as on the Claude Code lane. All six flags are default-OFF, and with them off the lanes spawn nothing. A hook-parity census (tests/core/install/adapters/opencode-hook-parity.test.ts) pins the surface: everyhooks/hooks.jsonrow must be either carried by the plugin or a declared divergence with a written reason, and the declared-divergence table ininstall/opencode.mdnames the thirteen rows that stay opencode-specific with why.Ed25519 attestations: the operator signs the store.
o2b brain attest keygen | sign | list | verifyturns store verification into evidence instead of a live answer.keygencreates the operator keypair under.open-second-brain/keys/attestation/(private half 0600, never synced; the optional passphrase arrives as an env var NAME via--passphrase-from-envor on stdin, never argv, and a lost key leaves every past attestation unverifiable);signmeasures the store - the vault identity, one chain head per Brain log JSONL shard collected under the appender's own lock, a file-name inventory of every ledger directory the doctor sweeps, and the search index's recorded embedding-ABI stamp, hashed to one token because a live WAL makes raw bytes meaningless - seals and signs one attestation record, and writes it as one JSON file under.open-second-brain/attestations/; signing never creates a key. The signature covers exactly those surfaces and never raw note bytes: an out-of-band edit to an existing note's body that moves no log head, no ledger file name and no index stamp is not something an attestation can see - per-note content drift is the context-receipt verify's question.verifyre-measures those surfaces against the newest attestation - per-shardintact/appended/diverged/missing, the signature, the store digest - and exits 0 with the attested state intact or legitimately appended, 1 on diverged or missing shards, an unexplained manifest drift (the vault identity or index stamp moved, an attested ledger entry is gone, a name the producer's grammar refuses, an addition to a strict directory such asBrain/preferences), a store it could not re-measure (a log lock held through the retries), or an invalid signature, 2 on usage errors; nothing here repairs a store. Post-signing ledger growth is the normal case and reads as such: a new file whose name parses under the swept directory's producer grammar and carries the kind the producer writes, and a known ledger subdirectory (a first continuity month, a first work-identity directory, a first per-preference trail), is growth rather than drift - removals, wrong kinds, non-grammar names and strict-directory additions stay drift. The MCP face is thebrain_secretssplit applied to signatures: a read-onlybrain_attestationstool mirrorslistandverifyand answers identically at every reach because it carries no note content, while keygen and sign have no tool surface anywhere - a key lifecycle is an operator decision, because an agent that could mint a signing key would attest nothing an operator vouches for. The fail-softattestation-stale-or-divergeddoctor check folds the facts that age badly into one warning naming the next command - the signing key gone while signed evidence exists, the newest attestation older than thirty days, a shard diverged or missing since it was signed, an unexplained manifest drift, or a signature that no longer verifies - while legitimately appended shards, a vault that never attests, and a clean fresh keyed attestation all stay silent; a store the check could not re-measure, and evidence that cannot be read, reach the uncertain stream rather than a clean bill.Docs:
docs/cli-reference.mdgains theo2b brain attestrow, theverifyform on the context-receipts row and the delivery-trust section naming the fenced surfaces and the two new doctor rows;docs/mcp.mddocuments thebrain_attestationstool, theverifyoperation onbrain_context_receiptsand the fence contract on resource bodies delivered at remote reach;install/opencode.mddocuments tool gating, prompt-time injection, the live-verification steps and the declared-divergences table.
Changed
A note mutation by a non-CLI lane re-stamps an inherited
origin_channel: cli. Update and append operations coming from any lane other than the CLI - an MCP tool call, an import replay, an unclaimed process (stampedunset) - rewrite an inheritedclistamp to the writing lane's own channel; a CLI-lane mutation keeps the stamp, and a page with no stamp stays unstamped. Until now mutation never touched the stamp, so text an agent wrote through MCP kept shipping as plain, operator-authoredcliprose from the first edit on; the restamp moves the page behind the delivery fence from that edit forward. Read roots are unchanged, and a page whose stamp already names a non-CLI lane is left alone.The shared frontmatter read cache refuses vault-escaping rels and folds Windows separators.
readCachedFrontmatterEntry- the one seam every ownership and reachability verdict reads through - now judges the joined path's containment BEFORE any read or cache hit, so a vault-relative rel that resolves outside the vault (a traversal spelling or a caller-supplied path) fails closed to the unreadable verdict instead of opening and parsing a file beside the vault; and a rel handed to the cache with\separators folds to its POSIX spelling where those are separators, so both spellings of one page share one cache entry and one read. Local reads that stay inside the vault are byte-identical; the refusal verdict is exactly the one every fail-open filter already handles.
1.96.0 - 2026-10-29
Open Second Brain 1.96.0 is the guarded seams and concurrent reads release: the places where other systems' text, other tools' state directories and the host's own instruction files meet the vault each state what they accept, and the stdio transport stops serializing what never needed to be. The write side gains an optional neutralization gate that escapes the structure-bearing markdown in imported third-party text at the capture and ingest seams - off by default as a deliberate operator choice, with a shadow mode that measures without writing and a read-only doctor audit naming the pages that already carry planted rows. The install side seeds a managed .gitignore block at o2b brain init so the derived state under .open-second-brain/ stays out of git, and a doctor error names every path git already tracks. The readiness probe fails a workspace whose write-back contract is installed into a file the operative host never reads. And the stdio MCP transport classifies every tool as read or write in the registry, runs reads concurrently up to four in flight, and keeps writes exclusive and arrival-ordered behind a dispatch-level gate.
Added
Write-time markdown neutralization at the third-party seams, gated off/shadow/enforce. Third-party text that carries structure-bearing markdown - wikilinks,
@osbinline markers, ```osb fence lines, ATX headings - lands in capture pages and untrusted-source ingest summaries as live vault structure: the link graph, the marker scanners and the section parsers read it as the vault's own.markdown_neutralize_mode(envOPEN_SECOND_BRAIN_MARKDOWN_NEUTRALIZE_MODE,off | shadow | enforce, defaultoff) runs the gate at the two seams - capture writes (o2b brain captureand the capture kernel) and untrusted ingest summaries:enforcebackslash-escapes the structure-bearing markdown in caller text before it is written, so no scanner treats it as the vault's own, and stamps aneutralized_markersfrontmatter key naming the escaped families - a write that changes nothing adds nothing, and vault-authored sections appended after caller text are never touched;shadowleaves every write byte-identical and lets the would-be families surface through the doctor audit below;off- the default, a deliberate operator choice - runs no gate and every write stays byte-identical. The env twin wins over the config key and an invalid value is refused by name at the seam. Because capture identity hashes the persisted bytes, flipping the mode between captures of the same text yields an honest duplicate rather than a silent mutation.A read-only doctor audit names the pages that already carry planted rows. The
o2b brain doctorpass gains the fail-softplanted-structure-rowswarning: one finding counting the live structure rows third-party text planted in capture pages, untrusted-stamped pages, quarantined entity pages and source summaries, naming up to five paths and counting the rest, deduplicatingBrain/pending/staged copies against their publish targets and skipping pages the caller may not read, silent on a vault with none. Undermarkdown_neutralize_mode: shadowthe finding appends the preview of what enforce would escape on the next capture or untrusted ingest of the same text. The audit is read-only by contract - the rows leave only by re-capturing the text with the gate inenforceor by a later repair pass - so the finding carries noo2bcommand.o2b brain initseeds a managed .gitignore block, and a doctor error names derived state git already tracks. A fresh init in a git repository now writes a managed guard block into the vault.gitignorecovering/.open-second-brain/- the derived runtime state, bench runs included - beside the identity stamp, so a fresh vault no longer leavesbrain.sqlitevisible togit status. The seeder merges with existing content and never clobbers a conforming block, checks exact-line membership first so an operator's own ignore line yields a skip, keeps CRLF endings, reports malformed markers as a named conflict and continues without writing, and--forcenever rewrites the guard block; the fresh-vault created count moves from seven to eight. Theo2b brain doctorpass answers for existing vaults with thegit-tracked-derived-statecheck: derived state under.open-second-brain/that git actually tracks - measured bygit ls-files, never inferred from status - is an ERROR naming one location-independentgit -C <vault> rm -r --cached -- .open-second-braincommand plus the missing/.open-second-brain/ignore line; a present-but-unignored untracked store is a warning; the check is silent outside a git worktree, when the store directory is absent, and when git cannot answer, and it never edits anything - the ignore half is remediated by init's guard block and the untracking is git's own.o2b doctor --readinessfails a shadowed instruction contract. A workspace where the file the operative host reads carries no managed write-back block while another candidate file carries it has the contract installed where it never runs. The readiness probe now grades that placement as a fail from a pure resolver over the audit rows alone: per the host-to-file precedence table, claude-code readsCLAUDE.md, codex, cursor, opencode and aider readAGENTS.md, and gemini-cli readsGEMINI.md; the finding names the host, the file the host reads, the file carrying the contract and the same read-only recovery clause the per-file findings carry, and the fail census counts findings, not files. Symlinked candidates keep their refused unknown treatment, and a workspace with no instruction file at all stays skipped - nothing installed is not a contract violation.Parallel reads with exclusive writes on the stdio MCP transport. Read-classified
tools/calls run concurrently, up to four in flight - a constant, not a knob, backpressured at the cap so the bound holds by construction - while write-classified calls run one at a time behind a dispatch-level FIFO gate and never beside a read: the loop reads no further line until a dispatched write has settled, and the write waits out the reads already in flight before it starts. The concurrency pays only where a handler awaits I/O: read handlers whose work runs synchronously between awaits (the measured SQLite, filesystem and scoring reads) finish no sooner than serialized dispatch, so a batch's total time does not shrink, its responses shift toward the end of the batch, and its first response can arrive later than arrival-order delivery would - time-to-first-response may grow - while write-classified tools still run one at a time (brain_searchamong them, write-classified by the write-mode-dominant rule). Every tool is classifiedreadorwritein the registry at registration - write-mode-dominant, so a tool that can write iswrite, decided per operation family and never from arguments at dispatch, and fail-soft telemetry appends do not make a toolwrite- and the classification is metadata only: it never reaches thetools/listwire payload, and a definition that arrives without one is treated aswrite, so it can only ever lose concurrency, never safety. Cross-request frame order is intentionally surrendered in one direction: responses may arrive out of arrival order, which is legal JSON-RPC - correlate by responseidand progress byprogressToken- while each request's own progress notifications all precede its response, every frame stays newline-terminated, and on EOF or drain the server still waits for every dispatched request before the process exits.Docs:
docs/cli-reference.mddocuments themarkdown_neutralize_modewrite-side trust switch, thegit-tracked-derived-stateandplanted-structure-rowsdoctor rows, the placement-shadowing failure on the doctor readiness row and the bench-runs ignore the init guard block seeds;docs/mcp.mddocuments the stdio request-ordering contract - concurrent reads, exclusive writes, out-of-order responses correlated by id;docs/observability.mdnames the guard block behind the bench-runs path.
Fixed
Backslash-escaped wikilink brackets no longer produce links. The link readers (
extractWikilinks, the backlink body walker, the page lint's link targets, the merged-link convergence and the Sources-section reader) now hold a[[or]]preceded by an odd backslash run to the same rule the write-time neutralizer's detector applies - escaped punctuation is literal text and opens or closes no link - so the escape-aware detection grammar and the readers that consume it answer with one truth, and the same shared helper serves both. Every link-graph input (backlinks, co-occurrence, portability, entity links, doctor checks) and the planted-rows audit inherit the rule.Digest maintenance actions now honor agent scope. The
actionslist inview: digestis collected vault-wide, and its rows were filtered by transport reach alone, so a caller delivering the digest underagent_scopecould receive maintenance rows naming preferences the sections beside them had already withheld. The enforced ownership rule now joins the reach view for the actions rows - the same judgmentbrain_doctor'ssuggested_actionsand the operator summary'stop_actionsalready apply - andosb://digest/latestthreads its resolved scope into the render the wayosb://preferences/activedoes. Withoutintegrity.owner_scope_delivery: fail, or with no scope requested, every output is byte-identical.The brief views and the digest's dream-parsed rows honor the enforced owner scope at every reach. The daily and weekly envelopes judged their rows by transport reach alone and stepped past the ownership rule at local reach, so under
integrity.owner_scope_delivery: faila caller with a resolved identity could receive another owner's preference ids, transition reasons and source pointers fromstatus_transitions,retiredandcontradictions; those rows are now kept through the same enforced-rule view the digest's actions use, with the counts recomputed from the same selection. The digest's confidence-shift and contradiction rows - parsed from dream log lines whose preference id the log-level filter cannot resolve - are kept through the same view. And a caller the enforced scope narrows now takes no report snapshot and is shown no delta, since a per-caller render must not land under the shared date key for the next caller's delta to be diffed against.view=monthly,view=todayand the operator summary were already scoped and are pinned by regression rows; without the gate, or underwarn, every output is byte-identical.
1.95.0 - 2026-10-27
Open Second Brain 1.95.0 is the verified delivery and honest write surfaces release: every surface that hands an agent context or writes on its behalf now states what it actually did - what was trimmed from a delivered pack, what was injected and never consulted by the answer, what a privacy reservation held back from an export, what failed part way through a multi-artifact write and how to finish it. The delivery side gains an opt-in score-aware tail trim that names every drop and offers the freed budget back by priority, and a reply-presence receipt that joins what a session injected to what its answer verifiably used. The capture side keeps its failures recoverable - the dead-letter record freezes the payload it failed on and one command re-runs it - and gains a standalone commit-blocking validation gate for writers no hook reaches. The organization side moves whole folders under the same occupancy-checked, failure-naming discipline the file surface set, every shared export names the pages a privacy reservation held back, and the operator surfaces state exactly what they can and cannot measure: the search writer lock reads held, free, or unknown - never a guessed free - and the doctor's client-version probe reports what it could not read instead of failing the machine.
Added
Score-aware tail trim for delivered context-pack evidence. With
tail_trim_context_pack: "true"in the machine-local main config (or the env overrideOPEN_SECOND_BRAIN_TAIL_TRIM_CONTEXT_PACK; default off),brain_context_pack- MCP only; the o2b brain context-pack verb never reads the gate - runs one score-aware trim over the unified tier-and-budget walk whenever the call carries a query: the top three delivered items are immune, any later item whose query-term coverage - the fraction of the query's tokens it carries - is at least 0.8 of the top delivered score is kept - it restates the question's terms nearly as fully as the top hit - and the rest drop whole with atail-trimskip reason, never partially re-cut. The freed budget is then offered back toover-budgetcandidates in priority order, the only path by which a page can cross tiers, spending the tokens the trim freed and no more, and never reordering or dropping the synthesized attention item. The response carries a conditionaltail_trimblock (dropped,kept_by_query_terms,rescued,tokens_saved) only when the trim actually ran, counted over the items that actually ship; the same four counters ride thecontext_receipt'sbudgetblock and thecontext_packtelemetry record'smetadata, and each dropped item's skip reason surfaces as the telemetry gaptail_trim. With the key off - the default - packs are byte-identical, and thepacking_policybranch never trims.Reply-presence receipts join what was injected to what the answer used. The answer-quote-check Stop hook now folds one session-keyed
reply_presencecontinuity record per checked turn, beside the annotation it already wrote: a verdict per injected note -verifiedwhen a reply span checked out against it, the strongest miss verdict otherwise, andabsentfor a note that was injected into context but that no reply span was ever measured against, the row this record exists to surface; a span that verifies records only the note that carries it, so an uncited span's other candidates are never named by a verdict that is not theirs. Exactly one record per checked turn, written even when every span verified, and only when the turn had injected candidates at all; the payload carries identifiers, the per-note verdicts, the reply-span count and a hash binding the verdicts to the exact answer text checked - never the answer text itself. The verdict vocabulary gains a sixth member,absent, which is per-note presence only: every span still gets one of the five original verdicts, so a consumer reading per-span outcomes never sees it. The receipt read surfaces join on it:o2b brain context-receipts showand the MCPbrain_context_receiptsshowanswer with a join matched by exactsession_idand folded over every presence record the session wrote at or after the receipt's own creation, chronologically, withverifiedwinning across turns -absentnever upgrades or downgrades one - and the folded rows restricted to the notes the receipt itself injected; a receipt with no readable record in that window names theunjoinedoutcome honestly, saying which of the two it was: a receipt that names no session at all, or one whose session wrote no record at or after it.liststays a summary.session_injectreceipts carry the session key. The SessionStart active-inject receipt records the payload's real session id as itssession_idmember - the join key the reply-presence read matches on - and stays absent when the host sent nothing real, never invented. Receipts written before this release carry no key and read as unjoined, which is what they are.Privacy-marker denials are named in every shared export. A preference the privacy deny markers reserve (
visibility: private, or an informal marker saying the same) now leaves theo2b brain exportjson and llms-txt preference dumps and theo2b brain bank-exportbundle before their bytes are composed, is blocked by the knowledge pack's selection step beside the existingvisibilityandownerblocks, and is withheld from the statico2b brain explorer --exportHTML too: the export narrows its graph by the same denial judgment over its preference and retired nodes, the node count answers for what was actually written, and the same stderr notice names what stayed out - while the live server, the operator's own view, keeps every node. Every lane names what it held back: the json envelope carries anomittedfield and the bundle apreferences_omittedreport (the pack names the page in itsblockedlist), the stderr notice names each denied row with the frontmatter key that denied it (denied_by:visibility,access,confidentialityorsensitivity), and every field is absent when nothing was denied - a vault with no reservations produces byte-identical output, so a smaller export is never a silent one.bank-export --include-reservedis the one override for the reservation itself, for this verb's documented purpose - the one-file personal backup - and carries the reserved rows into the bundle whole, with an inclusion notice naming each carried row and the flag where the denial notice would have been; the default run is unchanged. And the SDK facade carriesexportPreferencesLlmsTxtDetailedbeside its string-only form, returning the same text beside the omission report the json envelope names, so an in-process caller no longer watches reserved rows leave the text silently; the string form is deprecated and keeps its exact bytes.o2b brain dead-letters: the failed extraction write gets an exit. A multi-artifact extraction write that failed part way already left a durable dead-letter record; the letters were write-only. The dead-letter schema moves to v2 and freezes the failing payload into the letter, so what the write meant to land survives the failure - each frozen item redacted at freeze time by the same redaction the write path runs, with the raw-text dedup hash stored beside it so a recovery still dedups against the original identity, andlist --jsonrows carrying onlyhas_payload, so the bulk surface agents and cron logs read never prints a payload (show --jsonkeeps the frozen items for the operator's one-letter read); v1 letters (written before the payload was frozen) parse unchanged and classifyre-extract. The new verb lists every letter with its classification -recover(the session is still capture-classified with undelivered missing items),pending(the items sit staged in the write-approval queue),re-extract,not_recoverable,resolved(every missing key exists on disk) - and the exact copy-paste command that resolves it; a letter whose every undelivered item the durability gate will never accept classifiesnot_recoverablebefore any command promises a re-run, with the gate and its denial named.show <id>prints one letter in full, missing keys and frozen payload named;recover <id>re-runs a schema-2 letter's frozen payload through the extraction commit at CURRENT vault state, where a v2 item counts as delivered on its stored hash or - the v1 topic measure, extended to v2 - when the session already holds a signal page on its topic, and a topic-delivered item is left out of the plan and the apply entirely, so a re-extract's reworded text is never replayed as a second signal while already-written items still dedup away. Recover is dry-run by default and previews the per-item verdicts;--applyperforms the re-run and requires--yesunder--jsonor a non-TTY stdin, and an apply that comes back with nothing written and nothing staged exits 1 naming the accounting instead of printing a green zero-write success; a frozen vault refuses and an unknown id is a usage error. NOTHING here deletes a letter: a recovered letter is retained, and removing it stays an operator's hand action on the named file.o2b brain validate: a standalone, commit-blocking validation gate for hook-less writers. A writer with no hook harness had no way to block its own commit -brain lintalways exits 0. The new gate runs the exact write-receipt page lint over exactly one caller-named page set ---paths <path>(vault-relative, repeatable, one flag per page) or--dirty(every git-dirty Markdown note: modified, added and untracked) - and nothing else, so it is safe inside any gate. The exit code is the product, byte-for-byte the doctor contract: an error finding exits 1, warnings exit 2 under--strict, a named page the lint could not read exits 1, clean exits 0; a vault that is not inside a git worktree - or that its enclosing repository ignores, where git cannot measure dirt at all - refuses--dirtywith exit 2 naming--pathsas the remedy, and giving neither or both selectors is a usage error. Report-only: the fixes stay withbrain doctor --repair --applyandbrain lint --consolidate --apply, which every finding'snext_commandnames.Whole-folder move and rename on the note-lifecycle surface.
o2b brain note-lifecycle move-folderrelocates a whole folder under a destination parent andrename-folderrenames it within its parent - split for the same reason the filerenameandmoveare split, so a caller that meant one and spells the other is refusedwrong_actionrather than moved by accident - and the MCPbrain_note_lifecycletool accepts both through the same action enum. The folder surface mirrors the file envelope rung for rung (vault-relative, no traversal, theBrain/machinery root refused, the vault-identity write guard) minus the.mdrequirement, and every destination is checked free BEFORE the first rename - the closest thing to atomicity the operation honestly has - so an occupied destination refuses with nothing moved and the source untouched. The move set is the folder's Markdown only, judged by where a file really lives: a symlinked directory or note under the source is excluded and named in the receipt's residue instead of being pulled into the move or hard-linked into a dangling name, a source reached through an alias moves under its real name, and the destination-inside-source guard compares real paths, so an alias of the source is refused as nesting rather than linked into itself. The rename-intent reading applies only when the named destination directory does not exist - an existing sibling folder is a real move target, movable in one call. Because a folder move never changes a basename, the move set's inbound path-spelled wikilinks are rewritten in one pass over the successfully moved set, to the minimal spelling that still resolves; bare-basename links are untouched and keep resolving, and the receipt names the settled cost of a Markdown-only move: what stayed behind in the source folder (left_behind, absent when the folder emptied) and, per moved note, the relative Markdown links and images whose targets stayed behind (dangling_relative_links, absent when nothing dangles), so a zero rewrite count never reads as nothing broke. There is no rollback, on purpose and by name: a mid-set failure is named per file inmoveFailureswith the pairs computed from what actually moved, never a silently shorter move. Dry-run is the default.The operator surfaces state what they measure about the store and the clients.
o2b brain statusgains astoregroup: the search writer lock's three-state read -held(a live writer holds it, with its age),free(no live holder at the instant of the read),unknown(the probe could not be taken, with the reason) - a state that could not be measured is never rendered asfree;thisCaller: "read-only"states the snapshot's own standing, and while a lock is held the line scopes its refusal warning to the writes the lock gates - an index write (o2b search index/ reindex) would refuse withINDEX_LOCKED, while vault and memory writes serialize on the brain lane's own lock files and are not gated by it; andwriterLockOwnerisnullwhile a lock is held because the lock record carries no identity - a measurement, not an omission, and the renderer says so. A held lock is informational and never flips the snapshot unhealthy, and the MCPbrain_statuswithholds the resolved lock path below local reach while keeping every state field.o2b doctor --readinessgains theclient_versionsprobe: each detected agent client is asked its version through its own version argv under the per-probe timeout, one row per target -passwith the raw first stdout line,unknownnaming why (a thrown runner, a non-zero exit, a timeout, empty stdout), orskippedwhen the target has no version command. The ask settles on the child's own exit rather than on stdout closing, so a forked helper that inherits the pipe cannot turn an answered version into a timeout, and a client that outlives the cut is SIGKILLed after the TERM grace, so nothing the probe spawned outlives doctor. An all-unknown probe keeps doctor at exit 6, never 1, because a version it cannot read proves nothing broken.Docs:
docs/cli-reference.mdgains theo2b brain dead-lettersando2b brain validaterows, themove-folder/rename-folderactions on the note-lifecycle row, the tail trim on the context-pack row, the reply-presence join on the context-receipts row, the denial naming on the export and bank-export rows, the store group on the brain status row and the client_versions probe on the doctor row;docs/mcp.mddocuments thetail_trimblock onbrain_context_pack, thereply_presencejoin onbrain_context_receiptsshow, the folder actions onbrain_note_lifecycle, thebrain_statusstore group and the CLI-only validate/dead-letters pointer for hook-less agents;docs/observability.mdstates the tail-trim telemetry counters;docs/how-it-works.mddescribes the trim engine;hooks/README.mddocuments the reply-presence record on the Stop row, the session key on the SessionStart receipt and the transcript-availability caveat; the CLI help surfaces (command manifest ando2b brain --help) name the folder actions.
1.94.0 - 2026-10-26
Open Second Brain 1.94.0 gives the continuity ledger its intended readback path. The Hermes provider's native memory writes have been mirrored into the ledger as durable, attributable records for some time, but the ledger was write-only for recall purposes: a fact the host wrote could be exported for audit and never recalled by the memory system itself. The new o2b brain continuity promote verb turns those records into ordinary preference pages, dry-run by default and sealed by an approval digest on apply, so a mirrored fact becomes recallable only through a review that says exactly what will land. The ledger itself stays write-only audit substrate, and the full-fidelity export keeps its job.
Added
Mirrored host memory writes become recallable through an explicit, reviewable promotion verb. The Hermes provider's native memory writes are mirrored by the
on_memory_writebridge into the continuity ledger ashost_memory_writerecords (Brain/log/continuity/<YYYY-MM>[.<deviceId>].jsonl), and until now the only readback was the full-fidelitybrain continuity export, because the ledger is write-only to the rest of the memory system: the search index admits only Markdown pages, the session grep keeps only session kinds, and the context pack reads the preferences and retired lanes only.o2b brain continuity promote(the documented answer to GitHub issue #277) renders eachhost_memory_writerecord as a preference page underBrain/preferences/pref-hostmem-<record-id12>.md, deterministic per record id, so the mirrored fact becomes ordinarily recallable - page search finds it, and the context pack and its confirm flow treat it like any imported host memory. The verb is dry-run by default: it prints the plan - create, update, recreate, skip-unchanged or conflict per record, skips named with reasons, private records dropped and counted, redacted records promoted in masked form and counted - plus a plan digest. A malformed ledger row (a non-canonical createdAt or record id) skips by name and never aborts the run, records with identical content promote once and their later duplicates skip by name, and every record is re-scanned at promotion time under the export door's strict redaction grade - vendor tokens, bare high-entropy tokens, URL credentials - so the masked text is what lands and a credential a host wrote into its memory cannot reach the search index or the context pack through promote.--applywrites: an--approval-digest <hex>mismatch refuses with nothing written and no snapshot, mirroring the approval semantics ofo2b brain import-claude-memory; a matching digest takes a snapshot, writes the pages atomically, records the promotion manifest (Brain/.imports/host-memory-promotions.json, rewritten only when something was actually applied) and appends a log event, while an interactive apply without a digest lands the current plan and a non-interactive apply must carry the digest; a corrupt promotion manifest is reported by the dry run (manifest_corrupt: trueunder--json) and refuses every apply until the operator reconciles the file, and a conflict - apref-hostmem-*page the promotion manifest does not record, left behind by another writer or by a lost manifest, not an edit to a landed page, which is kept and plans skip-unchanged - throws after the safe writes land and exits 2. Pages land under the trial window (dream.unconfirmed_window_days): promoted pages start unconfirmed and are confirmed like any other preference, so a poisoned host write never becomes a live rule just by being promoted.--month YYYY-MMnarrows the source window to one ledger month. No supersession is inferred: areplacerecord promotes as its own page beside the one it superseded, because the ledger never says which record replaced which, and reconciling the pair is the confirm flow's job. The ledger itself stays write-only audit substrate, and the division of labor between the two verbs stays clean:brain continuity exportanswers "what happened", promote answers "make it recallable".
1.93.0 - 2026-10-23
Open Second Brain 1.93.0 closes the gap between what a configuration promises and what the model, the filesystem and the package manager actually do. The chunker now charges a long whitespace-free run such as a URL at a conservative token rate and splits it across chunks, so the declared budget bounds what the embedding model really receives. Retirement provenance lands in the log shard the device actually writes, so a fresh retirement no longer reads as a broken link. The decision model tries providers in a configured order with automatic fallback inside the existing budgets, and per-model enforce thresholds move out of the source tree into the installation, keyed by the model id the server reports and kept for every model ever tuned. The launchers repair a checkout that a Codex marketplace add or upgrade strips, so the documented install path starts the CLI. The release also closes all twenty-seven findings of the previous release's external review, among them two paths that let marker-private content leave the vault, a quadratic regex on the synchronous write path, a hold rule that withheld fitting chunks from embedding, and a ranking rule that let one entity match outrank higher-scoring results.
Fixed
Chunks fit the model window when a page is one long URL. The token estimator counted any whitespace-free run as one word, so a page holding a single long link produced a chunk declared at 4 tokens that the embedding model reads as more than 1,700 - past its 512-token input window, where the serving stack silently truncated it. A whitespace-free run of 16 characters or more is now charged one token per 3 characters, and the splitter cuts inside such a run, so the text is distributed across chunks that each fit the budget and every character stays indexed. Reducing
search_chunk_sizetherefore works for link-heavy pages, and the chunker version moves to 3, which schedules a one-time re-chunk of the index.Retirement provenance names the log shard the device writes. A merge retirement, a dream retirement and
brain rejectstampedretired_by: [[Brain/log/<date>]]while the log appender - for a vault with a configureddevice_id- writesBrain/log/<date>.<device>.md, so the doctor reported every fresh retirement as a broken link. All three now build the link through one helper that resolves the same shard id the appender resolves: sharded when a device id is configured, byte-identical to the historical shape when not. Historical records are untouched.A Codex marketplace add or upgrade leaves a launcher that starts. The marketplace flow replaces the checkout wholesale and takes
node_moduleswith it, so the first launcher command died importingproper-lockfilebeforeinstall-clicould repair anything. The bun launchers -o2b,o2b-mcp,o2b-aider,vault-logand their Windows twins - now probe for the dependency tree with one file stat and runbun install --frozen-lockfileonce when it is missing, refusing with the manual command named if that install fails; a warm checkout pays nothing. A cold-checkout regression drives the real entry point end to end.A page carrying an informal privacy marker is now withheld at every egress, not only at the remote read roots. The decision-model gates - rerank, recall injection, label suggestions and advisories - and the shared knowledge-pack export treat a marker page exactly like a formally reserved one, a marker-private source's ingest summary is stamped reserved before its derived sections can copy its rows, and marker keys match case-insensitively, so an imported
Access: privatereserves likeaccess: private. Existing search indexes re-measure the indexed visibility column of marker pages on upgrade (schema v16). Theupdate_noteguard refuses a marker-key write only when it would clear or forge the page's deny status -access: read-onlyand apublictoteammove are writable again - andbrain privacy-marker-restampnow says that a stamped page also leaves default-scope local search and how to reach it again.Chunks are held out of embedding only when even a conservative lower-bound estimate puts them over the declared window. The hold trigger was an average-rate estimate, so ordinary English chunks that fit the model's window were held and never embedded. Chunks merely estimated over the window are embedded again and reported in the undecided band,
search checkno longer recommends a vector backfill for chunks that are held, a declined embedding provider is reported as decided instead of not-configured, decline refusals name only the keys that recorded the decline, ando2b doctor --readinessreports an unreadable consent record as unknown and lists registered-provider and rerank credential variables by name. The decline this feature covers is the embedding provider's; rerank and decision-model endpoints keep their own settings' declines, recorded as follow-up work.Entity ranking can no longer let a single entity match outrank higher-scoring results. The declared-over-derived rule now breaks only exact score ties, so a keyword-strong chunk stays first and a weak entity hit does not jump ahead. The usual spellings
A. LovelaceandLovelace, Adaresolve, short operator nicknames likealresolve (with the doctor naming any entry it still skips), a derived form that reaches two people is reported as ambiguous with its own remedy instead of silently expanding to both, and the nickname-derived index is built once per vault change, which turns the doctor's entity scan from seconds to milliseconds at five thousand people.The undated-quantity detector no longer stalls the write path and no longer fires on non-claims. A comma-joined digit run used to backtrack quadratically - about forty-five seconds on a 256 KiB body, on the synchronous write-receipt path; the scan is now linear. Numbers inside URLs, markdown link destinations and wikilink targets, dates inside code regions, and dimension spellings like
3Dare no longer reported as undated claims, a date silences a page's quantities only where claims can stand, and the finding's offset now states that it is counted after frontmatter.The default OKF export no longer contradicts its own omissions. The export's log no longer carries entries the agent wrote over the MCP channel or entries naming the pages the class split left out, and kept pages' structured links, relations and citations to a left-out page - in the OKF manifest, the graph export, the bank bundle and the knowledge pack - are dropped and counted, with
--include-prompt-derivedcarrying all of it back. Source summaries ingested over MCP are now stampedcontent_class: prompt-derivedthemselves.
Added
Ordered decision-model providers with fallback, and per-model thresholds in the installation.
decision_model_providersaccepts an ordered list, with the singledecision_model_providerkept as the one-entry form: the first provider that answers serves the call, a connection error or timeout falls through to the next entry within the same budget, and everydecision_model_callrecord names the provider and model that actually answered, so reports never pool two models.o2b decision-model checkshows the order and which entries answer. Per-model enforce thresholds move from the compiledDECISION_THRESHOLD_PROFILEStable - now the hosted baseline family only - intodecision-thresholds.jsonbesideconfig.yaml, keyed by the model id the server reports rather than the provider preset, with the entries of every model ever tuned kept across switches and an optional per-vault override. A use with no stored thresholds for the active model runs asshadowandcheck/reportsay so explicitly, naming the tuning command;o2b decision-model thresholds fit --use <use>derives the threshold from the use's labelled report data against a false-action target and writes it for the answering model, andthresholds list/thresholds setround the flow out. Docs:docs/decision-models.mdanddocs/decision-models/providers.md.
1.92.0 - 2026-10-22
Open Second Brain 1.92.0 makes the write path and the egress path keep promises the rest of the system already made. Informal privacy markers in frontmatter - access: private, confidentiality: secret, sensitivity: confidential - now resolve to the reserved remote-deny token at every remote read root, and a preview-first verb stamps the formal token onto the pages that only ever carried the informal spelling. Chunks the declared embedding window cannot hold are held out of embedding under a persistent record instead of being truncated and indexed as something shorter, with the hold reported and released when its premise moves. People resolve through derived name forms - first initial plus surname, word order, and operator-supplied nicknames - while declared spellings keep precedence and ambiguity is reported rather than guessed. A fifth page-lint detector asks a count or an amount for its date, prompt-driven writes carry a server-stamped content class that shared exports honor by default, and a provider decline is a persisted decision honored before any network call. Review of the release closed four defects, among them an OKF bundle that left out its own reserved root files.
Fixed
An OKF bundle carries its reserved root files, and an index that agrees with the manifest.
o2b brain okf-exportcomposes its bundle from the pages the content-class split kept, andindex.mdandlog.mdwere not reaching the written directory, so a bundle arrived without the two files the rest of its layout assumes. Both now ride every bundle. The index is re-rendered from the filtered manifest by the same renderer the builder uses rather than copied from the source vault, so a page the export excluded is named neither inokf.jsonnor inindex.md- an index listing pages the shipped manifest does not carry would make the bundle disagree with itself - andlog.mdstays whole, because its entries are day-keyed events rather than page content, while still passing the same scan as every page file.The relational arm reads the indexed visibility column at remote reach before it names a candidate. A remote answer from the relational arm joined its candidates through the page index and resolved them against the vault, leaving a window in which a page whose remote reservation had just been lifted could still be named from rows the previous index pass had written. The arm now consults the indexed visibility column at remote reach, so the answer holds at the joined view rather than at one of its two sources, and a page readable at local reach is unaffected.
The undated-quantity exclusion scan is order-independent. The scan now settles its exclusions over the page as a whole before it reports, so the findings depend on what the page contains rather than on the order its regions were visited, and the same bytes yield the same findings on every run.
A research report page now carries the prompt-derived class stamp. The write path applies it at creation beside the rest of the report's frontmatter, and the byte-identity fixture is updated to the stamped shape as the only byte change. A report page therefore reads as prompt-derived to the shared exports, and the report lane's output is subject to the same default exclusion as a session import or an MCP note write.
Added
Informal privacy markers resolve to the reserved remote-deny token at every remote read root, and a preview-first verb writes the reservation the pages never carried. A page that says
access: private,confidentiality: secretorsensitivity: confidentialwas already withheld from a remote caller, but every consumer that readsvisibility:directly - a derived page stamped through the strictest-visibility fold, an export filtering on the token, an operator auditing reservations - could not see the claim. The three markers now resolve to the reserved token at every remote read root: search results, vault page lists and the indexed snippet column answer the same deny the formal key would.o2b brain privacy-marker-restampmakes the claim formal - dry run by default,--applyto write - unioning the token intovisibility:additively and idempotently, re-reading each candidate immediately before its write so an edit that landed in between is honored, refusing by name (in the verb'sskippedlist) a page whose frontmatter cannot be rewritten losslessly instead of quietly absorbing it into a lower updated count, never removing a token and never re-embedding, with the write guard asserted at the applying entry point only. A doctor finding counts the marker pages that are still unstamped, andupdate_noterefuses changes to the three marker keys exactly as it refuses changes to the reservedvisibilitykey, so the informal spelling cannot be removed from a page whose reservation depends on it.A chunk that cannot fit the embedding model's declared input window is held out of embedding instead of silently truncated, under a record that says why. The serving stack used to cut such a chunk to the window, so the index held an embedding of text the page does not contain, with nothing on any surface saying so. A chunk provably over the declared window is now skipped and recorded under a persistent hold keyed by the model, the window and the chunk signature, so the same chunk under the same configuration stays held without re-deciding, and the record is released automatically when the model, the window or the signature changes - a moved premise reconsiders, a static one stays quiet. A held chunk is excluded from the pending-vectors census, so no backfill is recommended for text this configuration cannot embed, and the estimator's undecided band is reported rather than resolved to one side.
o2b search status --jsoncarries a boundedheld_chunksreport - position-ordered, capped at 20 with the whole named bytotal, present only while the active configuration holds something - and a doctor check counts the holds, so the state is visible without reading the index.Person entities resolve through derived name forms, with a declared spelling always outranking a derived one and ambiguity reported instead of guessed. Two derivation families join the resolver: forms computed from a declared name - a first-initial-plus-surname spelling, and the word-order variants of a multi-part name - and operator-supplied nicknames read from
Brain/entities/person-nicknames.json; nothing ships a built-in word list, so a nickname exists only because the operator wrote it. The forms answer in query expansion and in registry lookups, and ranking is explicit: a match on a declared name or alias outranks a match that only a derived form reaches, so a derived form can widen recall but never displace the spelling the vault declares. Ambiguity is reported rather than resolved - a derived form that matches more than one person refuses and names the competing ids - write lanes stay exact-only, and a doctor warning names a derived variant that collides with another person's declared name.A fifth write-time page-lint detector warns when a count or an amount is written with no date it could come from. The recognized shapes are structural, never natural language: a currency amount by symbol or ISO code, a percentage, a number with a unit from the declared set, and a bare thousands-separated count. A page that supplies a date anywhere the quantity could take one stays quiet - a
valid_fromboundary in frontmatter, any ISO date in the body, or a[Source: <name>, YYYY-MM-DD]citation marker - and the finding carries the matched lexeme and its offset, never the authored line, so a receipt cannot repeat prose the caller did not ask it to carry. Each finding nameso2b brain claimsas the next command, where the quantity can be placed in time.Prompt-driven writes now carry a server-stamped
content_class: prompt-derived, and every shared export leaves those pages out by default. The stamp lands at creation on the deterministic prompt-driven paths - a note write that arrives over MCP, the session import, and the in-process report writers - and it is derived by the writer rather than accepted from the caller: a caller-supplied value is dropped, an operator-authored CLI write stays unmarked, and no path removes the stamp afterwards. The shared exports - bank, graph, OKF and knowledge packs - exclude class-marked pages by default and name every omission with the page's path and class, so a smaller export is never a silent one; the bank, graph and OKF verbs and the transcript export (o2b brain export --format transcripts-jsonl) take--include-prompt-derived, which carries the pages back in and says so on the run, while the knowledge pack always blocks class-marked pages and names them in itsblockedlist. Pages written before this release carry no mark and export exactly as they did; that era boundary is documented indocs/cli-reference.mdandSECURITY.mdrather than migrated, because a stamp applied by a later pass would describe the pass and not the writer.A provider decline is a persisted, fail-closed record honored before any network call.
search_semantic_enabled=falseandembedding_provider=disabledused to reach the same capability verdict as a machine that had never configured semantic search at all, so the two situations were indistinguishable to every surface that asked. A decline written in the configuration is now read as a record of its own, distinct from never-configured, and it is fail-closed: an unreadable or unparseable record is a decline, not consent. The decline is honored at provider construction, before a request is built, and the refusal names the keys that record it and the keys that would re-enable the provider.o2b doctor --readinessgains a consent probe that reports the decline as a decision rather than a fault, and lists the provider environment variables it found by NAME only, never by value. Beside the record, a new egress consent-stamp primitive ships with a presence-based kill switch,OPEN_SECOND_BRAIN_EGRESS_CONSENT_KILL; it declares no destination of its own, adds no egress registry row, and leaves every network path in this release unchanged.
1.91.0 - 2026-10-21
Open Second Brain 1.91.0 turns one-off promises into first-class memory. A promise made in prose becomes a pledge page under Brain/pledges/ - title, promise text, due date, the source page it was read from, and evidence quotes the kernel verified against that source at write time - with direct CRUD and a validate-everything-first extraction intake over MCP, a pledges section on the today dashboard, and two delivery surfaces for the moment of need: an opt-in context-pack rollup that groups what a query's entities already kept, and an opt-in counterparty brief that hands the agent at most three shareable facts about the person a prompt names, once per session. Both delivery surfaces are off by default and byte-identical when off, and nothing in the release calls a model.
Changed
The today dashboard and
brain_briefview="today"carry their new section additively. The rendered dashboard gains a## Pledgesblock between obligations and open loops and the totals linesPledges:andPledges overdue:; the--jsonenvelopes gainpledgesTotalandpledgesOverdueinsidetotals, and thebrain_briefview="today"response gains apledgesmember besideobligations. No existing key changed value or moved, and a pledge page the caller may not read drops from the section before totals are derived, identical to absent.
Added
One-off pledge pages with evidence verified at write time. A pledge lives at
Brain/pledges/<slug>.mdwith bare frontmatter -title,created_at,due,source,evidence,promised_to,fulfilled,agent- and the kernel refuses by name what would make one hollow: an empty evidence list, a source page the vault does not hold, and any quote that does not occur in the cited source (containment through the shared quote-span comparators, so a stored quote is one the source contained when the pledge was written; there is no unverified-evidence lane and no leniency knob - the agent re-quotes verbatim).fulfillstamps the fulfillment date once and a second stamp refuses;listsorts by due then slug with an overdue flag and whole days until due, open pledges only;removeretires the page intoBrain/pledges/archive/with a collision suffix, the obligations pattern. Counterparties ride thepromised_tofrontmatter key, a newpromised_torelation token in the shared vocabulary, so the existing frontmatter-relations ingest projects the typed edges and search admits the new directory automatically - the page family adds no projection code of its own.brain_pledgeandbrain_intake_pledges. The first is direct CRUD over the family -add,fulfill,list,show,remove- mirroring thebrain_obligationoperation shape, with schemas that reject unknown properties and answers that carry the pagepath; the second is the extraction intake, where the agent posts typed pledge extractions (title, due, evidence quotes, optional text and counterparty names) citing one source page. The intake validates EVERY item before writing anything - source readability at the caller's reach, quote containment, valid dates - so a batch with one invalid item writes no page, and commits idempotently: a repeat with the same payload is a reported no-op, the same identity with a changed payload is a named mismatch refusal. Counterparty names are anchored server-side against canonical-scope entities and never trusted from the caller; a name that anchors nowhere is reported and stored nowhere - the pledge exists without edges rather than with fake ones - and re-posting the same payload after the entity registers adds the edge. The kernel never calls a model. Both tools answer at the caller's reach: below local reach a withheld pledge page reads exactly as an absent one.A pledges section on the today dashboard.
o2b brain todayandbrain_briefview="today"now surface due-sorted open pledges beside obligations and open loops - title, slug, due date, and due today / due in N days / overdue by N days - inside the dashboard's fault isolation, so a failing pledges section reports an explicit error line and the rest of the dashboard still renders; an empty vault renders an empty section with no error. The section rides the same reach predicate as the obligations rows: a pledge page the caller may not read drops before anything counts it, so below local reach neither its text nor its totals cross. (See Changed for the additive envelope members.)An opt-in
entity_factssection onbrain_context_pack, a rollup over work the pack already did. Withentity_facts_context_pack: "true"(machine-local config, or envOPEN_SECOND_BRAIN_ENTITY_FACTS_CONTEXT_PACK; default off), the pack response gainsentity_facts: the canonical-scope entities anchored from the pack query, capped at three, each listing only the ids of candidates the pack already kept whose bodies anchor it - never a second recall lane, never an id the budget or the view dropped. An entity page that fails the caller's view is treated as absent, so a withheld entity never surfaces its rollup; entities sort by id and item ids follow the pack's own order. With the key off, every default pack is byte-identical and the other consumers of the same core are untouched.An opt-in counterparty brief at the moment of need, once per session. With
counterparty_brief_enabled: "true"(machine-local config, or envOPEN_SECOND_BRAIN_COUNTERPARTY_BRIEF_ENABLED; default off) and a non-empty operator-declaredcounterparty_brief_triggersvocabulary (a comma-separated machine-local list key in the operator's own language; nothing ships a built-in word list, and an absent or malformed list keeps the brief inert), a UserPromptSubmit hook reads the submitted prompt and injects one dated brief about the counterparty it names: at most three shareable items - open pledges promised to that entity first, then recent non-private preference pages whose bodies anchor the entity - each carrying its source path. The gates are conjunctive and fail closed: at least one trigger token must occur in the prompt, exactly one canonical-scope entity must match by name or alias (zero or several matches abstain), it must be the session's first qualifying prompt (the injection ledger suppresses repeats; no real session id means no emission and no ledger write), and a page whose visibility carries the reservedprivatetoken never surfaces whatever the caller's reach, with items derived from several sources inheriting the strictest visibility. The known limit is disclosed, not claimed closed: a readable page may name a private page, and no per-page gate rewrites prose. Flag off keeps the hook's stdout byte-identical to an install without it.
1.90.0 - 2026-10-20
Open Second Brain 1.90.0 turns the dream pass into a rule promoter, teaches every context budget to read the host model's window instead of guessing it, and makes two benchmarks honest about what they measure. The dream pass can promote one recurring cross-topic rule per owner per day out of confirmed preferences that state the same principle in different topics, behind an opt-in config block whose absence changes nothing. Context packs, preset suggestions, pre-compress guidance and the anticipatory cache resolve their token budgets from a model-aware window resolver - operator declarations first, then a published exact-id table, then a trailing -1m suffix, then the 200,000-token default - and each surface discloses which channel decided. The decision model gains a preregistered per-use eval with a measured promotion gate: a promote verdict writes a machine-side tuning mark, fail-closed against the spec hash, the fixture hash and the pinned thresholds, and only a valid mark moves a shadow-only use to enforce. Search names how far behind a stale index actually is - a measured document count on the index-stale degradation and a new index_behind line in search status - and the memory bench refuses, by name and before any state is created or any score published, every damaged pre-run and pre-report state it used to fail late on or silently reset. The search query language ships as a versioned JSON Schema artifact with export and check verbs, and a wikilink's spelling stops reading as ambiguous because an out-of-scope twin shares its basename.
Fixed
A wikilink's spelling stops reading as ambiguous because an out-of-scope twin shares its basename. Bridge discovery's basename census counted every vault page carrying the name, whatever the operator's scope rules say about it, so a note under an ignored directory - an
ignore_pathsentry, a frozen default ignore the narrow walk does not know, or outside theinclude_pathsallowlist - forced the full-path spelling onto links to a uniquely-named in-scope target, and a bare[[basename]]resolved to neither. The census now resolves the vault scope once and skips pages whose vault-relative path fails it, beside the entity-status rule it already applied (a quarantined entity page never counted): a page outside the operator's scope is not part of the vault by definition and can no more anchor a wikilink than a missing file would. In-scope twins keep forcing the longer spelling exactly as before, and the census decides the spelling only - naming an out-of-scope target explicitly behaves exactly as it did.
Changed
brain_context_presetssuggest (ando2b brain context-presets suggest) now ranks an explicitcontext_window_tokens- and a model the exact-id table knows - above a matchingmodel_hintstoken. This is a deliberate order change from the hint-first draft, not a regression: the hint check used to run first, sogpt-4o-miniwith an explicit 128,000 window answered tight-context from the "mini" hint, and the table-knowngpt-4.1-minianswered tight-context although every derived budget for it sizes from the declared 1M window - two mappings for one model on two surfaces. The window is authoritative: an explicitcontext_window_tokensis the caller's own number for the deployment at hand, the same figure a derived budget sizes from, and a table row is a published fact - while a hint is a heuristic read off the id, so it answers only when nothing published resolves and never claims a resolved window. The conservative fallback (tight-context, low confidence) is unchanged for ids nothing places - and for exactly those ids the pack budget surfaces refuse rather than budget the default window's long-context, so an unrecognised model never gets a budget again.The context-window resolver deliberately builds no window-proportional budgets: it selects between the two existing presets. The presets tile the window range at 32,000 tokens -
tight-contextbelow,long-contextabove - so a 128k, a 200k and a 1M window all land inlong-contextand receive the same derived guidance; the widened recognition (the[1m]bracket form, provider and region prefixes, dated snapshots,-vN:Nrevision suffixes) changes which bucket an id lands in, never the bucket count. Window-proportional budgeting is its own calibrated change and is not improvised here.
Added
Cross-topic rule promotion in the dream pass, behind an opt-in
cross_topic:block whose absence is byte-identical to before. The same-topic merge machinery buckets entries by topic before comparing, so a principle stated once per subject is invisible to it by construction; the new planner inverts that walk and considers pairs only ACROSS distinct topic buckets - confirmed preferences in at leastmin_topics(floor 2) distinct topics whose principle text reaches a jaccard similarity ofjaccard(floor 0.6) elect one recurring cross-topic rule. The promotion is bounded four ways: sources must share one normalized owner token (ownerless counts as the shared token), so owner-scoped recall never leaks one agent's rules into another's rule body; one promotion per owner token per calendar day, budgeted through aBrain/cross-topic-rules.jsonledger (a registered state surface) with the date taken from the pass's injected clock, so the same corpus elects the same rule on every machine and a rerun plans nothing; a budgeted-out pass plans nothing; and a source the same pass plans to retire is not eligible, so evidence is never hung off a file the run is about to move. The promoted artifact is an ordinary unconfirmed preference -provenance: "inferred",evidenced_bywikilinks to every source, arelatedlineage - written by the dream apply stage through the transactional preference writer with overwrite off, under a content-derivedcross-<sha256-12>slug so the same source set maps to one file and an already-present or retired slug skips. Sources are never modified or retired, the fired promotion counts as a state change (it rides the dream summary ascross_topic_rulesand lands one Brain log event beside the ledger write), and a below-threshold pass stays byte-identical: no-op summary, no log write.A model-aware context-window resolver, and every context budget that used to be a caller-declared guess now derives from it. One pure module turns a host model id plus the operator's declarations into a window number and states for each answer WHAT decided: precedence is the operator's
context.window_tokensconfig key, thenOPEN_SECOND_BRAIN_MODEL_CONTEXT_WINDOW, then a published exact-id table (no family substring ever resolves - "sonnet" matches nothing, which is the defect the module exists to remove; the table carries gpt-4o at 128,000, the gpt-4.1 family at 1,000,000 and the three shipped Claude ids at 200,000, each row with its provenance note, and the id normalizes before the lookup, so provider and region prefixes likeopenai/andus.anthropic., trailing date snapshots likegpt-4.1-2025-04-14and Bedrock-vN:Nrevision suffixes still reach the exact row), then the structural tier - a trailing, delimited1msuffix (-1m/_1m, or the trailing[1m]bracket form hosts report likeclaude-sonnet-5-5[1m]; a dot-led version fragment like the4.1mingpt-4.1mis a version, not a window) - then the 200,000-token default. First-hand declarations - the operator's config key, the environment value, a published table row - pass through unclamped, used exactly as given; only the guessed tiers (the1msuffix expansion and the unknown-model default) are held to the [100,000, 1,000,000] band, with any clamp disclosed. The consumers:brain_context_pack(ando2b brain context-pack) acceptsmodeland makesmax_tokensoptional - omitted, the budget is the matching preset'scontext_pack.max_tokensfor the resolved window and the response names the deciding channel inmax_tokens_source, while nothing resolvable - and no id the table, the suffix and the preset hints cannot place - is refused exactly as today, never a silent default;brain_context_presetssuggest (and the CLI twin) resolves a window from--modelalone with the source in the reason, an explicit window or a table-known id taking precedence over a hint match (deliberate; see Changed), and an unrecognised id answering the conservative fallback;brain_pre_compress_packcloses the unbounded-addendum case with resolver-derived caps, an unparseable window env value keeping the historical unbounded caps beside a namedwindow_warning; and the anticipatory cache scales its budget to the resolved window's hundredth part, floored at the 2,000-token constant so it only ever scales up, disclosed when derived and byte-identical when nothing resolves.A preregistered per-use decision-model eval, and a measured gate that is the only path out of shadow. Each use ships a preregistered spec - fixture reference, seed, split rule, metric, error ceiling, minimum counted n, and the pinned threshold values it measures - with a canonical spec hash; the take is a hash-order split (
sha256Hex(seed + ":" + id), dev = the first ceil(0.3 n), membership frozen at preregistration) and the runner verifies the split against the frozen receipt before grading.o2b decision-model eval run --use <use>grades the counted split through the configured adapter, cachingdecision_model_eval_graderecords stamped with the spec hash, profile, use, seed, item ids, boolean verdicts, metric and counted n (origineval, spend counted toward the daily cost gate, malformed records skipped by the lister with a named note);o2b decision-model eval listrenders them oldest first in prose or--json, ando2b decision-model checknames the measured state - untuned, measured under spec hash X, or baseline. The verdict is the preregistered three-arm rule - promote only when the counted error meets the ceiling, no-regression against the deterministic baseline holds, and the counted n is met; a short n is its own arm - and apromoteverdict, and only that, writes a machine-side tuning mark per (threshold profile, use) under the platform state root, outside every vault. Acceptance is fail-closed: the mark is honored only when its spec hash, fixture hash and pinned threshold values all match the installed spec, so a spec edited after the mark was written, a fixture that moved, or thresholds that drifted each put the use back in shadow; no mark file is exactly today's behavior, and a hand-registered tune always wins because the resolver consults the mark only where the source table says untuned.o2b decision-model doctorreflects the measured state.A measured index-behind count, on the stale note and in
search status. A search that names a stale index (index-stale, the index older than ten minutes) now also reports how far behind it is:detail.behindCountis the number of documents the next incremental run would add, update or delete, measured under the same gate that fires the code and cached for at most 60 seconds - invalidated by this process's own note writes, so a write from another process can leave the count up to a minute old - by a stat-only walk (onereaddirplus onestatper entry, no content read, no hashing) diffed against the index's stored rows. The count is deliberately an upper bound: a touch-only file (identical bytes, bumped mtime) counts as changed, because "behind" means "what the next run would re-read", not "notes missing from answers". A measured zero rides too; a failed measurement is absent - the note then names the age alone, and a fresh index performs no walk at all.o2b search statusprints the same census asindex_behind:in its freshen block (<n> doc(s), or(not measured)) and serializes it asindex_behindin the--jsonfreshen object, null when not measured - never a fabricated zero - measured only when an index exists, and without touching the doctor's own freshen check, which reads the state file and picks up no walk cost.Bench-run integrity guards: a closed refusal vocabulary over every damaged pre-run and pre-report state. Before a run exists, a preflight refuses - by name, on stderr, as
refused (<kind>): <detail>with a non-zero exit and no run directory left behind - an unusable runs directory (runs_dir_unusable), a fresh run whose identity collides with an existing run dir (run_dir_exists; the prior checkpoint survives and the detail names--resume, where the store used to silently reset it), a resume target whose checkpoint is unparseable (checkpoint_unparseable), names a phase this build does not know (checkpoint_phase_unknown) or lists phases out of order or duplicated (checkpoint_phase_order), a phase marked done whose artifacts are gone or truncated (run_artifact_missing- the materialized vault is required exactly while retrieval still has to run against it, the per-question result files once retrieve is done,report.jsononce report is), and a fixture that cross-references a note or session it never defines (fixture_cross_reference, which used to score as an ordinary missing-expected FAIL indistinguishable from a real regression). Config resolution and the capability gate moved above run creation, so those refusals leave no run dir behind either. Before any score is counted, and again before the report writes, the persisted trace is validated: a question with no result file (trace_result_missing), a truncated or non-JSON one (trace_result_unparseable), an id that differs from the question it is stored under (trace_id_mismatch), a category-required field absent from a record (trace_field_absent- present-but-empty is a legitimate miss and passes), a faulted proactive decision (trace_faulted_decisions- the report must never dress harness breakage up as memory behavior), files inresults/beyond the fixture's question ids (trace_extra_results), and a report whose counts disagree with its own trace (report_counts_mismatch, recomputed through the same aggregation the report builder uses, so the two cannot drift). Every detail names the repair. What is deliberately not a refusal: score content - an all-questions-failed run is the harness's purpose - and the external judge staying inerrorremains advisory. The guards add zero stdout on success, existing run ids, checkpoints and reports are byte-identical (fixtureHashstays content-only), and the per-question results andreport.jsonare now written atomically.The search query language ships as a versioned JSON Schema artifact, with
exportandcheckverbs.schemas/brain/search-query.schema.json($id brain.search_query.v1, draft 2020-12) is derived - never hand-edited - from the search-options descriptor, the same typed table that derives the servedbrain_searchinputSchema (byte-identically) and theo2b searchCLI flag table, so the published artifact cannot disagree with either surface; a$defs.search_query_documentblock publishes the grammar of the line-oriented query-document program (theintent:/lex:/vec:/hyde:lanes, the intent enum complete against the parser). The artifact documents the per-surface asymmetries the descriptor records instead of flattening them.o2b search schema export [--stdout]regenerates the committed file;o2b search schema checkre-derives and compares - JSON equality, not byte equality, so the repo formatter's layout never reads as drift - exiting 0 clean, 2 on drift with stderr naming which (the property set or the content) and the remedy, and 2 when the file is missing or unreadable, so CI and any operator share one named verdict. The verbs live under the search group deliberately:o2b brain schemais the vault schema pack, a wholly unrelated "schema".
1.89.0 - 2026-10-19
Open Second Brain 1.89.0 makes the derived truth follow the note write. A rename or move rewrites the inbound links it touches to the shortest spelling that still resolves, repairs the bare-name links its carrier-count change would silently repoint, and claims one detached search-index refresh the moment the write lands, so the staleness window starts closing at the write instead of at the next search. Entity anchoring matches the spelling a fact actually uses - "Ada Lovelace" anchors a registry entity reachable as "ada-lovelace" - while a leading-edge word-boundary rule stops the matcher from anchoring inside other words without silencing the inflected forms a token boundary admits. And the anchors a fact carries can follow its source page's outbound links: the anchoring kernel accepts caller-resolved link-derived ids, and a fifth maintenance lane task, relink, re-anchors the extracted signals already stored - every lane pass reports what the task would add, and the stored lines change only when the run asks for it.
Fixed
Entity anchoring matches the spelling a fact uses, and stops matching inside other words. The anchoring kernels compared literal normalized substrings: a fact saying "Ada Lovelace" never anchored an entity whose forms are "ada-lovelace" or "people/ada_lovelace", while a short form anchored inside any word that contained it - "Canada" anchored "Ada". Both sides of every comparison now expand through one pure match-form generator: runs of space, hyphen and underscore are interchangeable, so the spaced, hyphenated, underscored and slug-prefixed spellings of a name are one match class; a slug-prefixed form also matches at its path-segment boundary, the segment prefix being registry decoration rather than part of the mentioned name, and the tail spelling is minted only when that prefix is one of the registry's entity-type folders - the same vocabulary the registry mints entity ids and the merge guard's guarded categories from - so
people/ada_lovelaceanswersAda Lovelacewhile a slash-bearing form likeWarner/Chappellnever anchors through a bareChappell; and an occurrence anchors only when no letter or digit sits directly before it - the leading edge - while the trailing side stays open, a trailing character being an inflection until proven otherwise, so the Russian accusativeИвана, the German genitiveAnnasand a Korean particle run like김철수는anchor again; the adjacency of a script written without word spaces (Han, kana, Thai, Lao, Khmer, Myanmar) suspends the requirement, because there adjacency carries no boundary information, and a leading Hangul letter still blocks, Korean attaching no prefixes. The leading edge is what preserves the precision fix: the fold still never plants a mid-word hit literal matching did not already have -Canadanever anchorsAda. Forms are precomputed once per capture and each text expands once, not once per entity; identity keys are untouched - two spellings can remain two entities and one match class - and the merge guard and the contamination check adopt the shared rule verbatim, so the guard that vetoes a merge cannot disagree with the anchoring that fed it.A rename can no longer silently repoint the bare-name links it never rewrote. A bare spelling resolves only while exactly one nested note carries it, so a rename that brings a second carrier into the vault - another note moved onto a basename a unique bare spelling already resolved to - turned every
[[spelling]]aimed at the note that never moved into a link that resolves to neither, and nothing said so: the rewrite pass only touched links that named the moved note. Those third-party links are now repaired in the same pass, retargeted to the minimal unambiguous spelling of their pre-rename target through the same chooser and resolution ladder as the main rewrite; a repair that would write the spelling it already has is dropped, and everything unprovable stays untouched - a spelling several notes carried before the move was already ambiguous, a spelling with no carrier was already dangling, and a top-level note keeps owning its bare spelling through the exact-path rung whatever the nested count becomes.
Changed
Inbound wikilinks a relocation rewrites come out no longer than the vault still needs. The rewrite preserved each link's original spelling form, so a full-path link was rewritten to the new full path even where the bare basename would have resolved to the same note. Every old spelling of the source now collapses onto the one shortest form that still resolves to the destination under the store's own read-time resolution ladder - exact path first, then a bare basename (a top-level note owns its bare spelling outright, whatever nested notes also carry it), then a bare basename carried by exactly one nested note - so
[[Projects/Old]]comes out[[New]]whenNewis unambiguous and[[Projects/New]]only when it is not. The chooser mirrors the index's resolution rung for rung, so a spelling it calls minimal is a spelling search resolves to the same document; an ambiguous spelling is never an answer - it falls through to the next-longer form - and ambiguous basenames stay withheld exactly as before.
Added
An applied note write claims one detached index refresh, and the receipt names the claim and, on a refusal, the reason. The derived search index was only as fresh as its last indexing pass: a rename's receipt said so honestly, and the window stayed open until the next search happened to freshen on read. Applied note writes now claim one detached, claim-guarded incremental refresh through the freshen-on-read machinery - create, content edit and append alongside the four lifecycle operations (rename, move, archive, delete; a batch write's operations do not claim yet) - forced past the freshness interval because the write just aged the stamp, so the window starts closing at the write itself. The claim is the reader path's, verbatim: the same claim file, so a write's refresh and a reader's freshen exclude each other, and a write arriving while a run holds the claim starts nothing of its own but leaves a durable pending marker, which the running pass settles with one extra bounded incremental pass before it releases the claim - bounded at one, never a loop, so a write landing mid-refresh is picked up by that pass instead of staying stale for a whole interval. It is a report, not an invariant:
claimedsays a run is in flight, never that it has landed, and every refusal -search_freshen_interval_s: 0, another run holding the claim, no index to refresh, a failed spawn - answersnot_claimedwith the freshen machinery's own reason surfaced beside the token:index_refresh_reasonon the create, update and append results,refresh_reasonon the lifecycle receipts (the CLI's JSON and the MCP result, with the CLI's prose rendering it in parentheses beside the token), so a refusal names which refusal it was; the index evidence's next command remains the way the index reconciles. The CLI and the MCP note-lifecycle results carry the token beside the index evidence, the create and update receipts carry it under the same vocabulary, and both now expose the rewritten spellings the pass applied.Link-derived fact anchors, and a
relinklane task that reports the re-anchoring the stored signals need and applies it on request. A fact extracted from a source page anchored only to the entity names its own sentence used, so a page about an entity it linked but never named wrote signals that never reached that entity's recall surface. The anchoring kernel unions caller-resolved link-derived entity ids into the storedentities:line ahead of the text-derived anchors, the first body-order link leading when both mechanisms match the same fact: a page-sourced caller maps the page's outbound wikilinks through a resolver that answers the entity id (with or without the.mdsuffix), the entity page's own vault path, the slug-prefixed spelling folded and case-folded the way ids are minted, and a frontmatter alias no other visible entity claims - a directory-prefixed spelling answers only as that path rather than by basename, an alias two visible entities claim stays unresolved instead of first-wins, the captured link target is trimmed before the rungs answer it, the pages the caller's reach refuses are dropped before any id is produced, and the resolver answers empty rather than throwing when nothing resolves; a session-sourced capture passes nothing and writes byte-identical signals. Write-time attachment has no shipped caller yet - the extraction call exposes the seam and nothing threads it on the write path - so the path extracted signals actually gain link-derived anchors through is the maintenance lane: a fifth lane task,relink, recomputes the anchors of the extracted signals already stored with the shipped matchers and the same resolver, and unions them into the stored line additively. The resolution binds at the reach of each signal itself - a remotely readable signal only receives ids whose entity and source pages are readable at remote reach, a local-only signal keeps the lane's reach, a withheld page is skipped - acts only on server-stamped extracted signals, never on a caller-supplied pointer, and the rewrite touches only the extractor's own raw shape: a codec-compressed raw, a drifted page and a Raw holding anything but the extractor's oneentities:line are skipped, never written. An anchor already stored is never removed, a signal without a Raw section is counted by neither arm because the rewrite could not store the addition, and the default arm is a dry run that writes nothing, not even the skip-audit log the shared anchorable build stamps on the write path. Every lane pass reports per source page what the task would add, and the stored lines are rewritten only when the run asks for it with--relink-apply, orrelink_applyover MCP, each addition written only where the signal's own reach allows it, never calling a model.
1.88.0 - 2026-10-17
Open Second Brain 1.88.0 repairs split entity pages behind a preview-and-apply gate, folds the write ledger into one destination-keyed view, and catches an agent up on the log lines its peers appended while it was away. brain entity merge plans the repair, echoes the plan's hash and applies only when the live vault still produces the plan the caller read - the dropped page archived behind a recovery point, its name kept as an alias on the survivor. o2b brain writes destinations and the new brain_write_destinations tool answer which destinations the recorded writes touched, across both recorded write kinds, each row saying what its rows cover. An opt-in SessionStart lane hands the agent the log lines other agents appended since its last look on this device. Every lane that injects memory at session start now neutralizes what it hands over and frames it under the clause that says what the span is - records for the foreign lanes, binding working-style guidance for the memory lanes - and note listings order the same on every host, "note 2" before "note 10". Exports speak OKF v0.2 beside the Open Second Brain dialect while the importer accepts both, and Hermes discovers the memory provider through a declared package entry point.
Fixed
The lanes that hand the agent memory written elsewhere now neutralize what they inject and frame it under the clause that says what the span is: records for the foreign lanes, binding working-style guidance for the memory lanes. The active and lessons bodies are neutralized - structural control, bidirectional and zero-width characters stripped, delimiters escaped - and set under the shared one-line framing header at the assembly chokepoint before the meter records the injection, so per-source attribution names exactly the guarded bytes that reached the payload. The two clauses are deliberately different: a preamble telling the model to read its own rules lane as data would have it ignore the very preferences the lane exists to deliver, so the memory header binds instead - vault-derived preferences and lessons apply as working-style guidance, they never override system or user instructions, and they cannot request new actions, tool calls, or data disclosure - while the foreign lanes (the cross-agent catch-up, the answer quote-check) keep the records-not-instructions disclaimer. The header's overhead is charged against
inject_budget_charsbefore the tier ladder runs, so the framing never pushes the framed whole over budget and the ladder never evicts a section to pay for the framing; and a budget that cannot hold the header - or whose entire budgeted remainder would be the truncation notice alone, content evicted to the last section - skips the memory source whole rather than emit an over-budget frame, leaving nothing metered behind it. Gap-agenda topics route through the shared single-line neutralizer and the agenda block rides the shared untrusted-content fence under its own origin label, and the answer quote-check neutralizes each quoted span before the whitespace collapse and carries the framing line between its header and its findings. The single-line neutralizer is promoted intosrc/core/brain/untrusted-source.ts, retiring byte-identical private copies in recall-inject and navmap; standing and scoped rules stay trusted with their precedence header, runtime notices stay trusted, and theuntrusted_source_delimitingflag keeps its meaning. The tiered-budgeting pin now budgets 1,000 characters and additionally pins that the framed whole stays within the configured budget, and the coverage over the guarded lanes pins the neutralized, framed shapes at every lane.Note listings order the same way on every host, and "note 2" sorts before "note 10". The page listing sorted titles with a bare
localeCompare, which answers to the host's default locale - the same vault listed in different orders on different machines, and every lexicographic host reading "note 10" as sorting before "note 2". The single sort site now compares through a module-level fixed-localeIntl.Collator("en", { numeric: true }), keeping the case-insensitive pre-pass, and the doc comment names the host-independence contract. Observable output changes for multi-digit titles -o2b indexbytes andsecond_brain_queryordering - while the raw directory listings and the search pipeline stay untouched.
Changed
The write-ledger surfaces refuse a
since/untilthat is neither a bareYYYY-MM-DDnor a canonical UTC timestamp. The ledger readers (brain writes list,destinationsandrevert, and thebrain_writesandbrain_write_destinationstools) used to accept any string and compare it lexically against the log's canonical timestamps, so a bound in any other spelling -yesterday,03/01/2026- matched no log day and the query answered a clean, wrong empty. One shared validating parser now completes a bare date to the window's edge (sinceto that day's start,untilthrough the end of that day, calendar-checked by parsing) and passes through only a full timestamp already in the writer's canonical UTC shape; everything else is refused by name - a usage error on the CLI surfaces,INVALID_PARAMSblaming the argument on the MCP tools - so a mistyped window says so instead of answering empty.
Fixed
brain writes revert --device ""- and thebrain_writestool'sdevice: ""- selects the legacy un-sharded log instead of dropping the device filter. The flag readers treat emptiness as absence, so the empty device fell out of the revert selector: the bare flag was refused as an unbounded selector, and beside--paththe plan silently reverted the path's writes from EVERY device while looking device-scoped. Revert now reads the device through the same empty-aware selector the list and destinations surfaces use -""is a real filter value naming the un-sharded pair - so an empty device reverts only the legacy log, on the CLI and the MCP tool alike, and the plan is pinned by its selection rather than its echo.
Added
o2b brain entity merge <keep> <drop>, a preview-and-apply repair for two entity pages that denote one subject. Both subjects accept ids, names or aliases, so the doctor's always-onduplicate-entitycollision and its opt-inentity-alias-candidatenomination both resolve through the one verb. The repair is alias-first, never a bare delete: the survivor keeps its page and gains the dropped page's display name as an alias, the dropped body folds under a heading beside the survivor's, inbound relation edges retarget to the survivor so no broken edge survives, and the dropped page is removed behind the destructive-snapshot gate's recovery point. Claims stay append-only and aggregate by normalized name, so nothing claim-side is rewritten. The gate is the plan hash: the--dry-runpreview renders the plan and echoes its sha256 hash, and apply requires--expect-hash <hash>, re-planning from the live vault and refusing before any write when the vault no longer produces the plan the caller read; consent is a TTY y/N prompt,--forcefor agents, and a--jsonrun refuses without--forcebecause the prompt cannot render. The text-level merge guard runs over the two bodies unless--forcebypasses it explicitly, quarantine-lane records are refused by name rather than merged across, and a confirmed apply asserts vault identity and appends one reliability-audit record beside one Brainmergelog event after the bytes land. Thebrain_entityMCP tool stays read-only; writes remain CLI-only by convention.A destination-keyed view over the write ledger:
o2b brain writes destinationsand thebrain_write_destinationstool. One shared core reader walks the log and folds thenote-writeandwrite-sessionkinds into one row per vault-relative destination, each row carrying the contributing agents and devices, per-kind counts, the newest recorded touch and akindscoverage field that states honestly which kinds its rows cover - the slug-derived and fully-derived write-binding sites emit no event of either kind, so the answer carries a coverage note that a destination absent here may still have been written. The default window is the last seven days,--since/--untilnarrow the walk, and agent, device and path filters cut by exact identity; the tool pages by the shared offset cursor withtotal_matched, passes the log's warnings through and applies reach filtering, so a page the caller cannot read moves no count. The aggregate is log-derived at query time - no persisted table, no maintenance lane.An opt-in SessionStart lane that catches an agent up on the log lines its peers appended. With
log_catchup_enabled(envOPEN_SECOND_BRAIN_LOG_CATCHUP_ENABLED, default off), thelog-catchuplane hands the agent theBrain/loglines other agents appended since its identity's last look on this device - the newest 50 as one fencedadditionalContextblock with a count of older ones, every line attributed to the agent that wrote it, a per-lane character cap trimming oldest-first. "Me" resolves the configured agent name, else the device id; self-authored lines are filtered, and an unresolvable identity stays silent rather than replay the agent its own lines. The lane is a pure read -Brain/logstays byte-identical after every run, and the lane stays distinct from session-capture's lifecycle event - and the cursor is one un-synced(date, shardId, line)watermark with per-shard high-water marks per reading identity - several runtimes share one vault with different agent names, so "last look" is a fact about the identity, not the device - under<vault>/.open-second-brain/log-catchup.<identity>-<digest>.cursor.json(the identity sanitized into the file name, the digest keeping the mapping one-to-one - the digest also orphans the digest-less per-identity cursor files an intermediate build of this release wrote, a one-time reset: the first run after the upgrade replays the log once under the lane's caps and the old file stays behind unread), overwrite-only, symlink-guarded and fail-soft. It is rewritten on every run that actually scanned the log - silent runs included, so an idle vault does not rescan history at each start; the cursor holds back only when none of the collected lines fit the block, while the lines the 50-line limit or the character cap cut are counted as older and then skipped - keeping the cursor date's own marks, a three-date window of older dates, and the same marks for the first three future dates a clock-skewed writer may stamp, so a future-dated line on one of them shows once instead of every session until the clock reaches its date (a skew beyond that window resurfaces): the merge order interleaves shards, so a bare tuple would skip a same-second line landing in a differently-named shard. The scan is bounded on both ends of the cursor. The date-level skip is the calendar window, not the recorded marks: a parked cursor rescans every date within its three-date window whether or not a shard had synced to this device when it parked, so a day whose file arrives late still surfaces where marks-keyed skipping would drop it forever. A first run - no cursor yet - never walks the whole history: it scans a recent window, the same three dates plus two days of slack behind the newest log date of today or earlier (never behind a future-skewed stamp, and never behind today itself, so a vault whose newest entry is months old still catches up on its own recent tail), and names the days past it in the block header as "N older day(s) not scanned" instead of parsing them. That count is honest even when nothing else renders: a first run whose every scanned line turns out to be the reader's own emits the count once as a header-only block before the cursor parks, so the days the window never read are reported rather than dropped silently.OKF v0.2 emission beside the Open Second Brain dialect, and an importer that accepts both. The exporter keeps the manifest's dialect
schema: "1"and declaresokf_version: "0.2"in the bundle-rootindex.mdfrontmatter, the only frontmatter an index file may carry. Every page gains the spec's per-concept keys as derived fields: asourceslist translated from the citations the exporter already collects, withresourcerequired - URL targets pass through as-is, wikilink targets resolve through the page walk's bundle-path map, dangling targets drop and ambiguous basename collisions drop rather than guess - and ageneratedblock (by,at) stamped with the one shared instant the manifest'sgenerated_atcarries, so whole-bundle byte equality modulo that single instant stays the determinism contract. Page status lands on the spec's closeddraft | stable | deprecatedenum through a conservative map pinned to the frontmatterstatus:field:deprecated,retired,supersededandarchivedmap todeprecated,done,closedandresolvedmap tostable- completion is never deprecation -draftstaysdraft, and anything else reads asstable. The importer admits a bundle whose index declaresokf_version: "0.2"beside its ownschema: "1"dialect, consults the index only when the manifest does not name the dialect - sealed knowledge-pack halves carry no index and never grow an index dependency - preserves the on-disk schema value in the parsed manifest, and refuses everything else by name with the existing message shape. Beside the derived block, every page of a fresh export declaresokf_raw: trueand carries its nativestatus/sources/generatedvalues under the producer namespace asokf_status_raw/okf_sources_raw/okf_generated_raw- the derived status maps onto the closed enum and the derived sources are re-translated from citations, so exporting the natives in place would rewrite what the operator wrote. An own-producer import reads that declaration back: a marked page's derived block is stripped and its natives restored - a raw key absent beside a present one means the field never had a value, so a plain page comes back plain and a quarantined page comes back quarantined - on the trusted round-trip and review staging alike; a page carrying neither the marker nor raw keys is a legacy bundle's and imports as-is, and a foreign bundle's pages import as-is too, their v0.2 per-page keys surviving verbatim minus the raw machinery.A
hermes_agent.memory_providersentry point, so pip discovery lands on the plugin module.pyproject.tomldeclares the entry-point group withopen-second-brain = "plugins.hermes"- the module that already exposes theregistercallable - so Hermes's entry-point discovery reaches the provider instead of relying on directory-layout loading alone; the header comment no longer claims there are no entry points at all, the declared one being a plugin registration, not a CLI script. Tests parse the declaration back with tomllib, import the target module and pin the loader contract (aregistercallable there), and the standing pin that prefetch stays the single injection lane holds.install/hermes.mdnames the discovery path and its one caveat: the entry point resolves from the wheel by itself, but the provider bridge still needso2bon PATH or a checkout for the fallback launch, so "zero-config" holds only in those contexts.
1.87.0 - 2026-10-17
Open Second Brain 1.87.0 makes the host-config writes guarded and reversible, and the large answers walkable. Every guarded host-write lane - o2b install --target X --apply, brain protect --apply, brain doctor --repair --apply and brain unprotect, which joins the lanes this release - demands explicit consent before touching a file, shows the intended change as a unified diff, takes a timestamped backup beside an audit receipt before anything is written, and lands exactly the preview the consent approved, refusing the write when the file moved under it; an opt-in PreToolUse hook extends the same care to an agent's file writes against host Claude settings. Thirteen more MCP list tools walk any large result set by an offset cursor instead of stopping at one capped page. A fourth decision-model route speaks the bearer-authenticated Converse protocol (Bedrock-style) beside the existing three adapters. And the trigger queue learns to snooze - an open trigger defers for a bounded window or until its finding's grounding moves, whichever comes first - while a dismissal holds until its finding is grounded in something new.
Fixed
The applied change is the approved preview.
brain protect --applyandbrain unprotectcarry the before/after pair the consent ladder showed into the write: the write re-derives the change and refuses it (PREVIEW_DRIFT, nothing written, no backup taken) when the target file or one of the manifest inputs moved since the preview, and the write re-reads the target right before the rename, absent a target excepted, so the bytes that land are the bytes that were approved.
Changed
The three guarded host-write lanes now demand explicit consent before touching a file, which breaks headless callers that scripted the old write-on-
--applybehavior.o2b install --target X --apply,o2b brain protect --applyando2b brain doctor --repair --applyrefuse with a non-zero exit in a--jsonor non-TTY run unless--yesis passed; interactive runs prompt instead. Every apply's JSON output now carries the consent verdict, derived from what happened rather than hard-coded:yes-flag(a--yesconsent of a real write),interactive-yes(a confirmed prompt) orunchanged(a no-op that never reached the gate) on the CLI lanes, andcaller-applyon the MCPbrain_doctorapply: truepath, recorded in each fix's event; a--json/non-TTY refusal emits its own envelope ({ consent: "headless-refused", written: false, error }) on the payload stream, andrefusedstays reserved for a future interactive-no envelope. Bothbrain protect --applyandbrain doctor --repairprint a unified diff of the intended change before anything is written. Headless callers must add--yes(install exit code 6 names it).brain unprotectjoins the consent lanes, which breaks headless scripts that called it unattended.o2b brain unprotectused to rewrite the protected file on every call, with no consent, no backup and no record. Open Second Brain now makes plainbrain unprotecta read-only preview - under--jsonthe answer is a"preview": trueenvelope carrying the before/after pair, so a headless consumer can tell it from the apply envelope it shares keys with - and--yeswithout--applyis refused by name, because unprotect writes only through the consent ladder's apply step.brain unprotect --applyruns the same fail-closed ladder as the other guarded host-write lanes: a--jsonor non-TTY run refuses with a non-zero exit unless--yesis passed, an interactive run shows the unified diff and asks once before writing, and a confirmed run takes a timestamped backup of the file's current content beside it and appends the audit receipt before anything is written - so a refused consent or a failed backup leaves the protected file untouched. Headless callers must pass--apply --yes.A dismissed trigger no longer returns on a cooldown clock; it stays dismissed until its finding is grounded in something new. Dismissal used to silence a cooldown key for
trigger_cooldown_days(default 7) after the dismissal, so a finding nobody had touched resurfaced on a timer and a genuinely moved finding stayed hidden until the clock ran out. A dismissed finding now stays dismissed for however long the finding's grounding holds still, and returns - as a new record through the normal creation path, never as a status flip of the resolved one - only when the grounding moves: the re-fire introduces a source artifact the dismissed record does not name, or its grounding fingerprint differs from the one persisted at dismissal time (a sha256 of the finding's reason and context, normalized so the clock projections those texts embed - day counts, ages, scores - cannot read as movement). The artifact comparison is set-based and order-insensitive: a re-fire citing a subset of the dismissed grounding is not movement, and a record written before the grounding list existed never resurrects on a comparison it cannot support, while one dismissed before the fingerprint field existed reads its missing fingerprint as no baseline, never as movement. A bare re-scan of an unchanged finding keeps the silence whatever the calendar says. Scans name the silenced re-fires with a newdismissedskip reason. The acted status keeps its cooldown window, andsuppressstays clockless.
Added
A
conversedecision-model route for bearer-authenticated Converse-protocol (Bedrock-style) gateways. The adapter union gains a fourth adapter besidesystemone,vercel-evaluateandllm-emulation, with aconversepreset (decision_model_provider: converse) whose base URL, pinned model id and key variable are the operator's. The judgment travels the chat-shaped way - a system prompt and one user message carry the questions, the reply's JSON schema and the state fenced as untrusted content, and the reply's message text holds one JSON object keyed by question id, validated item by item by the same rules as every route; astopReasonthat is notend_turnrefuses the reply rather than half-parsing it. The route is declared uncalibrated - the reply is generated text whose probabilities the model wrote itself - so anenforcesetting is a config error until the operator opts in withdecision_model_allow_uncalibrated: true, and its ownconversethreshold profile tunes no use yet, so a permittedenforcestill runs asshadowuntil tuned from this route's own shadow data. The model rides the request path (POST <base>/model/<model_id>/converse; no model-id body field), and as a chat-shaped route the gateway's output tokens are billed beside input ones once both prices are configured - one price alone leaves the cost unknown. The request passes the shared egress guard under the newdecision-model-converseregistry id, the contract suite's fourth route applies the full failure matrix to the adapter, ando2b decision-model check,doctorand the setup skill name the new preset and profile.An opt-in PreToolUse hook that backs up host Claude settings before agent file writes. With
host_settings_backupon (envOPEN_SECOND_BRAIN_HOST_SETTINGS_BACKUPover the_brain.yamlkey, default off), a Write/Edit/MultiEdit/apply_patch call whose path canonically names a.claude/settings.jsonor.claude/settings.local.json- tilde-expanded, cwd-relative, symlink-resolved - gets its current content copied into the timestamped backup file<dir>/.o2b-backups/<name>/<UTC stamp>beside the target (created private at0600- the backup carries the same secrets the settings file does - and exclusive-create, so same-second backups ladder their names and exhaustion is a typed error, never an overwrite) and one audit receipt appended, before the call is allowed to proceed. A backup that cannot be taken denies the call via stdoutpermissionDecision: "deny"- the only channel that survives the fail-soft dispatcher - and a successful backup emits nothing, so the call proceeds through the normal permission flow. The wrapper checks the flag first, so the off path is a byte-identical no-op; its guard logic is restated stdlib-only - the sole core import is the two directory names of the audit root - and the honest hard floor (no deny when bun or the hook script is missing) and the uncovered lanes (Bash-class writes, temp-then-rename) are documented inhooks/README.md.Cursor pagination across the MCP list tools, so an agent can walk any large result set instead of getting one capped page. Thirteen limit-only list tools adopt the shared offset cursor beside the three surfaces that already paged (
brain_decisionaction=history,brain_session_expand,brain_search_expand), bringing the walking surface to sixteen:second_brain_query,brain_writes,brain_agent_query,brain_agent_diff,brain_unlinked_mentions,brain_write_sessionop=list,brain_generation_reportslist,brain_context_receiptslist,brain_event_trace,brain_analyticsview=timeline,brain_maintenancestatus journal,brain_scaffold_stublist and the ordered-walk diagnosticbrain_session_grep, whose coverage census stays single-pass and refuses a cursor Each declares acursorinput, cuts one offset window over the full filtered set through the one core cursor helper, and answersnext_cursorpresent in every response andnullwhen the walk is exhausted; a malformed cursor is refused as INVALID_PARAMS rather than silently restarting the walk, and existing count fields keep their meaning across pages (thebrain_maintenancestatus journal gainsjournal_total, the count its page cuts into). Three surfaces pin their default page to the historical answer: a limit-lessbrain_session_grepandbrain_scaffold_stublist return the bounded default (10 and 100) with anext_cursorfor the rest rather than the whole set - withinclude_raw,brain_session_grepruns its search undecorated and decorates only the page's items, so a cursor walk stays linear instead of re-decorating the whole match set on every page - andbrain_agent_diff, which caps nothing, states that itslimitbounds only the returned page while the comparison runs over the whole matched set; the agent surfaces compute their summary over that whole matched set, so aggregates hold still between pages.. Adoption follows a recorded census with teeth: every tool whose input carries alimitnow either walks by the cursor or carries a written report-cap reason pinned beside the census tests - full walks on the ordered logs and listings, recorded caps on the diagnostics whose rows are aggregates a second page could not continue (withbrain_truthkeeping its documented advance-sincetime-window paging), and the two ranked-retrieval toolsbrain_searchandbrain_file_contextdeliberately limit-only, because offset-paging a ranked, index-dependent answer would present an index race as data. Two of the walking scanners,brain_event_traceandbrain_unlinked_mentions, bound each page's scan one record past the page's reach, so a cursor walk costs one page of work per call instead of re-reading the whole filtered set on every page fetch; theirtotal(andbrain_event_trace'strace_total) is exact once the walk exhausts, and mid-walk reports the bound the fetched page reached - a bound the tool descriptions name, never a silent cap. A limit-lessbrain_event_tracecall still reads the day whole.A
snoozeaction on the trigger queue, withunsnoozeand a terminalsnoozedstatus.o2b brain trigger snooze <id>and the MCPbrain_triggersnoozeoperation defer an open trigger - legal from open states only, like acknowledge, dismiss and act - for a bounded window (three days) or until the finding's grounding moves, whichever comes first;unsnoozewakes it by hand, and the window's end or a grounding movement wakes it on its own, both restoring the exact status the snooze interrupted. The movement half persists the same grounding fingerprint the dismissal gate does: a scan that sees the finding again while it is snoozed records the recurrence - reported with a newsnoozedskip reason - but wakes the snoozed twin in place only when the re-fire introduces an artifact the record never saw or grounds the finding in changed content; a bare re-sighting keeps the snooze holding and only stamps the ledger. A snoozed trigger is terminal like a suppressed one: hidden fromlist, present inhistory, never in the brief, and governed by its snooze window rather than the TTL expiry clock, so a long snooze cannot be cut short by expiry. Re-snoozing a still-snoozed trigger is refused rather than treated as idempotent, andunsnoozeon a hand-edited record that lost its interrupted status refuses instead of inventing a lifecycle position to restore into.
1.86.2 - 2026-10-16
Open Second Brain 1.86.2 closes a concurrency defect in the per-session hook-state lock: after a stale lock was taken over, two or more hook processes could hold the same scope at once, so a queued re-grounding part could be delivered more than once.
Fixed
A stale hook-state lock takeover can no longer admit a second holder. A contender that judged an abandoned scope lock stale could act after another contender had already taken it over and re-acquired, move that live lock aside, and leave the lock name empty long enough for a third contender to acquire beside it; from then on each holder's release deleted the next holder's lock, and concurrent takers on the Windows runner each received every queued re-grounding part. A takeover now removes the judged file only for the one contender whose exclusive claim on that file's identity wins, and only while the name still holds that file, so a live holder's lock is never moved or removed.
A lock release removes only the lock its holder created. The release and the process-exit sweep of the shared lock primitive leave a lock file in place once its name holds another holder's file, so one lost lock can no longer cascade into several holders.
The same claim guards the other stale-lock breakers. The lineage gap-sidecar trim lock and the deferred session-capture worker lock break an abandoned lock through the same claim, and the capture worker's release removes only its own lock; the hook-state prune removes old scope locks through it too and sweeps old takeover claims. A claim older than a minute no longer blocks a takeover, so a winner that died between its claim and the removal, or a new abandoned lock that reuses an old inode, cannot leave a lock unbreakable, and every successful takeover sweeps that lock's old claims, so the stores without a prune of their own (the capture worker lock in the temp directory, the lineage gap lock) do not accumulate them.
1.86.1 - 2026-10-16
Open Second Brain 1.86.1 repairs what the review of v1.86.0 found and closes two embedding-loss defects: a vault that had the retrieval trust gate on keeps excluding after the upgrade, a barren session mine records its tombstone in the documented flow, the undistilled census reads the session store once so the SessionStart nudge stays inside its budget, and a search index left on an older schema is upgraded in place with every stored embedding kept instead of being rebuilt keyword-only.
Fixed
The legacy trust gate switch keeps excluding.
search_trust_gate_enabled: true(and its env var) maps toenforceagain, as it did before 1.86.0; measure-only mode is reached only through an explicitsearch_trust_gate_mode: shadow. The explain envelope names the resolved mode underenforceas well asshadow, and a refused legacy value names the key or env var it actually came from.The relational arm and the bench follow the gate mode in force. A relational query that names
contradictsedges returns them under the enforcing gate instead of an empty answer, the arm takes its mode from the caller's effective config, the bench pin acceptssearchTrustGateModeand refuses the obsoleteretrievalTrustGateEnabledby name, and an invalid mode in an override is refused.A barren session mine records its zero-yield tombstone without a relayed hash. A plan that hands out a mining envelope (and each mining step
--propose-backlog --jsonhands out) records the hash of the turns it offered as a pending survey, and a barren commit without a relay writes its tombstone over that material while the session still holds it, so the documented flow (plan, then commit) tombstones; a commit whose turns or ledger write fail keeps the survey for its retry. A turn imported after the plan, two pending plans over different turns, or no plan since the last commit withholds the tombstone and is named inmining_ledger_failure, so turns no model saw are never suppressed; a yield is recorded either way. A relayedsurveyed_turns_hashis validated (an empty one included), and one the session no longer matches writes no record and is named the same way.The undistilled census reads the session store once. The census and the mining ledger's prune share one session-turn index, so the SessionStart backlog nudge and
import-session --statusstay fast on vaults with thousands of imported sessions instead of growing with the square of the session count.The distill backlog drains.
import-session --propose-backloghands out extract-signals mining steps for the next five undistilled sessions (mining_steps, one per session identity, withmining_steps_remaining), and answering a step records the session as mined.--statusand--discoverkeep reporting coverage when the mining census fails, naming the census as unavailable. A session imported under--recall-session-idis counted correctly, and a corrupt session mining ledger names its remedy.An appended source whose last line has no newline is no longer re-queued by every batch plan, and an appended tail longer than 64 KiB is hashed over its real bytes rather than a reused read buffer.
Every capacity census honours an explicit config file. The maintenance capacity gate, the ingest admission wall, the capture inbox drain, the storage sampler and the storage report measure the store named by
--config; the capture inbox drain refuses ingest on an invalidstorage:block, asbrain_ingest_sourcedoes, and ignores errors in other blocks.Scan and removal failures no longer expose host paths to callers below local reach in
brain_doctor, the digest, the operator summary andbrain_delete_by_source.Receipt compaction keeps
skill_invokedrecords, so usage-ordered skill offers stay stable, and no longer counts a merged-away prior aggregate as kept.The explorer console refuses cross-origin requests before any route runs, so a web page can no longer make it run the dry-run dream through a blind request.
Smaller fixes: the semantic document dedup names the candidate cap once when it falls back to the lexical layer, the retirement marker in the content manifest is written under the manifest lock, and metric appends wait briefly for a busy surface lock instead of dropping the sample.
A keyword-only write keeps the recorded embedding prefixes of vectors it keeps, so the next semantic index run no longer clears compatible embeddings; a refused prefix-only change now names the prefix change instead of reporting a model change to the same model (#255).
An older-schema search index is upgraded in place. On the first search and at session start, an index left on an older schema by a previous release is migrated in place under the writer lock, keeping every stored embedding and its recorded model and prefixes, instead of being rebuilt keyword-only; the background self-heal run does the same, says it migrated rather than that the index was current, and session start still catches up files changed since the last index run. A self-heal rebuild re-checks the index once it holds the writer lock and skips work another process already finished. When a full rebuild is unavoidable (newer schema, missing tables, a chunker change, a corrupt or integrity-condemned file), Open Second Brain reports how many stored embeddings it dropped, where the previous index is kept and how to restore them, and a later rebuild never replaces a
.bakthat holds more stored embeddings than the index it retires, and a crash during that swap restores the live index rather than the older.bak(#256).
Changed
The 1.86.0 entry's statement that the legacy boolean maps
truetoshadowno longer holds: it maps toenforce.
1.86.0 - 2026-10-16
Open Second Brain 1.86.0 stops re-paying for sessions with nothing new to say and measures before it excludes: a session mine that ends with nothing written records a zero-yield tombstone that suppresses every later plan over the same transcript until the turn material changes, a distilled and undistilled census over the imported backlog feeds an ask-once distill envelope and an opt-in SessionStart nudge, the retrieval trust gate resolves an off | shadow | enforce mode where shadow measures every would-be refusal while excluding nothing, and the loopback explorer serves a live-counts console beside its graph. The release also repairs what the external review of v1.85.0 found in the shipped storage lifecycle: receipt compaction now folds only opt-in telemetry kinds, the capacity trend buckets its samples per day while the maintenance lane pauses a band before the wall, a retired document copy keeps its manifest marker, and the incremental extraction watermark stamps exactly the bytes the appended delta covered.
Added
A zero-yield mining tombstone, with a one-call force re-mine.
o2b brain extract-signalsand thebrain_extract_signalstool now remember a barren mine: a commit that ends with nothing written and nothing staged records a tombstone for the session in a session mining ledger beside the import ledger (<vault>/.open-second-brain/session-mining-ledger.json, schema-versioned, read fail-closed on a corrupt file or an unknown schema version - a silent reset would unsuppress every barren session at once - written under the vault lock, and keyed on a SHA-256 of the exact turn material the plan embeds), and the next plan over that session at unchanged content returns the recorded breakdown instead of an envelope -skipped { reason: mining_tombstone, mining_outcome { written, staged, deduped, durability_rejected } }withllm_step: nulland emptyturns_mined, text mode naming the breakdown - so an unchanged transcript is never re-offered to the model. The invalidation is the content: a re-import that changes the session's turns changes the hash and re-opens the session with no clearing step. Because removing a downstream preference does not change the turns,--force(MCPforce) bypasses the tombstone for exactly the one plan call it is given to - the remedy when the session should be re-proposed; a corrupt mining ledger refuses the plan by name even under force, and a commit that succeeds but cannot record its tombstone still exits 0, namingmining_ledger_failureon both surfaces.A distilled and undistilled census, an ask-once distill envelope and an opt-in SessionStart nudge. The census joins the import ledger to the mining record and classifies every imported session -
distilled(mined with accepted yield on its current material),known-nothing-new(mined barren on it, the tombstone), orundistilled(never mined, or mined on material that has since changed) - joining on the portable session identity and reading vault-side only, so the honest count never opens or re-hashes a source log and stays affordable inside a hook's budget.o2b brain import-session --propose-backlogturns the undistilled rows into one ask-once envelope: the feedback-marker candidates the sessions' mined turns raise are deduplicated against the vault's shared dedup index before the ask - candidates the vault already holds are dropped and named, the rest collapse to one proposal per distinct hash across sessions with every contributing session listed, and a session whose every candidate was already known is reported rather than re-proposed.backlog_nudge_enabled(envOPEN_SECOND_BRAIN_BACKLOG_NUDGE_ENABLED, default off) arms a SessionStart hook that reports the undistilled count as oneadditionalContextline, change-keyed rather than timed: injected when the count differs from the count injected last, an unchanged backlog never re-nagged, a drained backlog recorded without being announced; under its deadline the hook degrades to silence, never to a fabricated number, and flag off keeps the session preamble byte-identical.A measure-only shadow mode on the retrieval trust gate. The gate resolves a tri-state mode,
search_trust_gate_mode(envOPEN_SECOND_BRAIN_SEARCH_TRUST_GATE_MODE,off | shadow | enforce, default off): an explicit mode wins, with an invalid value refused naming the key actually in force, and absent the mode key the legacysearch_trust_gate_enabledboolean mapstruetoshadowandfalsetooff.offruns no gate at all and stays byte-identical;shadowruns the same deterministic controlled-vocabulary classification, excludes nothing, and records every would-be refusal with all of its structural reasons into the retrieval receipts per lane - the candidate pool and the relational arm never folded together - so an operator can read exactly what enforcement would drop before naming it;enforceis today's excluding behavior made explicit. The resolved mode rides the query-cache fingerprint so a mode change can never serve stale cached rows, the--explainenvelope names it, and the human explain transcript now prints the refusal counts and rows beside the exclusions, so a shadow run no longer reads like a clean enforce run on the surface the operator defaults to.An edge-level eligibility adjuster in the relational arm. Each walked node and its arriving edge are classified from the same controlled-vocabulary frontmatter signals the page gate reads, plus the validity window at query time and the relation-polarity vocabulary: a
contradictsarrival is the counted exclusion,superseded_bystays the annotated supersession backbone, and positive relations boost. Underenforcean ineligible arrival leaves the arm's contribution set; undershadowit is counted only. A seed is the caller's own entry point and is never refused for its own markers, the walk still expands through ineligible nodes - only the contribution set is gated at the arm boundary - and a refusal is a different act from the arm's identical-to-absent provenance convention: it is removed or kept-and-counted with named structural reason tokens on the arm lane of the same retrieval receipts, reach-filtered to pages the caller could read. The mode resolves from the same config the arm's other knobs answer to, so the arm and the page gate cannot disagree about the posture.A live-counts console on the loopback explorer. The explorer's live server gains two read-only routes beside the graph:
/counts.json- the recall-telemetry summary, the most-recalled artifacts under a named cap, a written-per-day histogram over a trailing 30-day window bucketed from continuity record timestamps, and the latest report-snapshot date per surface, which reads as disabled while the feature is off rather than as empty data an operator could mistake for enabled-and-quiet - and/status.json, the operator snapshot. Both re-read the vault on every GET (the status route re-runs the dry-run dream each time, a bounded cost stated rather than cached away), a failed read answers a 500 naming the route and the error instead of an empty payload, and the page's right panel renders Counts and Vault status sections fetching both client-side, so a refresh is a fresh read; a static export states that the live routes are unreachable instead of firing fetches that can only fail.
Fixed
Receipt compaction folds only opt-in telemetry kinds, never functional records. The protection rule is inverted into a closed allowlist -
FOLDABLE_CONTINUITY_KINDS, the pure telemetry kinds whose readers keep no per-record identity past the correlation window - withKEPT_CONTINUITY_KINDSnaming every kind a reader reads back as data; folding is opt-in per kind, and a kind the build does not know fails closed to keep, the way a future build's records always did. v1.85.0's clip-key rule never protectedsession_entity_mention(its payload carries asession_ref), so a confirmed compact folded the records the session-recall seed, the hallways pass and the mining dedupe read back, the by-idgeneration_reportlookups, the row-count-derived recent-turn sequence, andreceipt_compaction's own audit record - the next run folding the run before it.recall_observed_useandnegative_recallsit on the kept list too, their readers named: one is the lifetime observed-reuse rate search ranking consumes, the other the answer-audit trail a corpus statement rests on. Beside the allowlist: repeated folds merge into the existing per-kind, per-month aggregate instead of stacking a second line; shard names are validated up front so a malformed--shardanswers invalid params; scan offers and compact rewrites only this device's shard, a foreign shard refused by name on previews and confirmed runs alike - the fold's lock and compare-and-swap are local, so rewriting a peer's shard is exactly the divergent-copy conflict the per-device shard layout exists to prevent; and a text-modecompact --applywhose own audit record is lost in the rewrite reports the loss and exits 1.The capacity trend buckets per day, and the maintenance lane pauses a band before the wall. The forecast's slope is day-based: samples bucket to one point per UTC day (the day's last sample wins), a sample whose census was partial drops out of the fit entirely - an incomplete census understates the vault by exactly the surfaces it could not read, and the jump when they become readable again would otherwise fit as growth - and the fit needs three distinct days inside a trailing 30-day window before any
daysToCapfigure exists, so two calls a minute apart around one ordinary write no longer extrapolate the burst into a full vault, and a season-old slope (including a season-old reclamation) stops steering the line once the window slides past. The lane's capacity gate carriesstorage.pause_percent(template-documented, defaulting to the warn band, validated strictly belowcritical_percent- a pause at or past the refuse line could never pause anything): a measured percent at or past the line skips the pass with theskipped:capacityverdict while interactive ingest still works until the critical refusal. And the gate records its census as astorage_usagesample on every run where it evaluated, skip or proceed, so a vault where nobody runs the storage report still grows a trend the day-based tiers can fire on.A retired document copy keeps its manifest marker, and a copy that cannot retire cleanly stays in place. v1.85.0 dropped the retired copy's content-manifest entry as its resurrect guard, which re-classified the unchanged source as new and let the next batch plan quietly re-ingest it, undoing the retirement. The entry now stays, marked
retired_intothe canonical page (additive, the manifest schema stays at 3): an unchanged marked source answers aretiredbucket and queues out ontoskipped_retired, a real byte change re-admits it as modified, and a deliberate re-ingest re-records the entry without the marker. A copy whose inbound-link rewrite failed is refused before the removal - one locked citing note no longer leaves its[[stale-copy]]link pointing at a deleted page - and the apply detail carries the per-copy rewritten and failed link counts. A page elsewhere inBrain/carrying the same basename refuses the copy outright instead of retiring it, so no leftover[[basename]]is silently re-pointed to the namesake. The detector's bounds are named too: a candidate cap that bound the read set emits a review-only finding carrying the scanned and total counts instead of silently truncating, and the census that reads a retired-and-unchanged source reports it as found, never as a gap.The extraction watermark stamps exactly the bytes the delta announced, and the ingest answer reports the appended range. The v1.85.0 stamp always advanced
extracted_bytesto the full file size while the tail cut ended at the last complete newline, so an unterminated trailing fragment fell out of every delta: the stamp claimed bytes no delta ever announced, and the next append's delta started past the fragment instead of stitching it in. On a proven append the stamp now lands at the delta's boundary-safetoByteand records the digest of exactly those leading bytes, so the prefix proof stays consistent with the watermark the delta series announced and the fragment rides the next delta; every other shape - first ingest, rewrite, shrink - stays a whole-source pass stamped at the full current file.brain_ingest_sourcenow carriesextraction_delta- the named kind beside the boundary-safe byte range - absent on every shape that is not a proven append; extraction itself stays whole-source, so the field is a report of what the append added, never a claim that only the tail was processed. Beside the boundary: a first ingest no longer walks its source for a delta that cannot exist, an append past the read ceiling is named instead of answered like a rewrite, and the watermark's stat-and-hash happens inside the manifest lock.The censuses measure the vault they were pointed at, and every front door carries what its core collected. The storage census counts the derived store's directory as a surface - the hook audit, the dead letters, the checkpoints, the session ledgers and the maintenance journal all accumulated outside the meter, and on a busy vault the forecast read ok while the disk filled - with the store files on their own surface so no byte lands in two, and the directory's non-regenerable contents (secret custody, lockfiles) counted as vault-content, the dangerous direction being to overstate the deletable side. It measures the store the configured location (
search_db_pathwhen set) points at rather than the default, so the lane gate, the ingest refusal and the doctor check agree about the same vault.appendMetrictakes the shard's per-file lock the continuity appender holds, so a receipt fold's compare-and-swap rewrite can no longer move the shard out from under a concurrent append and silently drop the sample - including thestorage_usagesamples the gate and doctor read.deleteBySource's per-file failed list now reaches both front doors - theforget-sourceverb's JSON payload and text report, and the MCP answer - with the confirmed run exiting 1 when a file stayed behind, matching the page-dedup verb's rule. The maintenance collectors' error observer is passed by all three production callers - digest, operator summary and doctor - so an absorbed scan failure renders as a namedcollect_errorsrow on every surface instead of a thin report that reads as healthy. And the live explorer routes refuse a non-loopbackHostheader by name.
Changed
Docs:
docs/cli-reference.mdstates the five storage thresholds with thepause_percentband, the day-bucketed three-day-minimum trend and the whole-derived-store census, names the pause line in the maintenance-lane gate prose, states receipt compaction's opt-in-per-kind allowlist and this-device shard rule, documents the--propose-backlogmode with its conflicts, adds thebacklog_nudge_enabledhook flag, and extends theo2b brain explorerentry with the console routes;docs/how-it-works.mdstates the pause line beside the five gates; the README's "What is new" names the release; the shipped 1.85.0 entry is reworded where this review proved the claim false at ship - the document-dedup bullet names the lexical layer as the wired one and the semantic layer as unreachable wiring, and the incremental-extraction bullet and opening paragraph report the appended byte range instead of claiming the new text is extracted without a full reprocess.
1.85.0 - 2026-10-15
Open Second Brain 1.85.0 gives a long-lived vault the means to watch its own storage and to keep its append-only ledgers in shape: a measured per-surface byte census with a days-to-capacity forecast that warns in graduated tiers, background producers that pause behind a fifth maintenance gate before the wall while interactive writes keep working until a hard, validated refusal at the critical tier, receipt ledgers that compact behind an explicit preview-bound apply, near-duplicate document copies that retire behind the hygiene review gate keeping the newest copy, bulk removals that report per-file failures instead of aborting, and large appended sources whose re-ingest proves a pure append and reports the exact appended byte range. No cap is configured by default: an uncapped vault reads unbounded, nothing pauses and nothing refuses.
Added
A measured storage report:
o2b brain storageandbrain_storage. One builder feeds both surfaces, and each invocation takes one census and appends onestorage_usagesample, so the forecast series accumulates precisely because the report was asked for. The report answers three questions in one pass: what the vault's storage holds per surface (brain_tree, and the dot-directories.snapshots/,.artifacts/,.archives/,.payloads/,.state/, plus the derived store with its WAL/SHM sidecars - each measured or refused by name), where it is heading against the configured cap - a tier (ok,warnorcritical, orunbounded,unmeasurable,no_forecast) with the percent-used and days-to-cap figures it rests on, the trend linear over the recorded samples from two up - and which sample this invocation recorded, appended or named when it could not be. The verb is a report and never fails on a finding: a critical tier still exits 0, because enforcement lives at the gates that consume the tier. Below local reach the census is not taken: the answer names the reason and carries no figures.A storage policy block.
storage:joinsBrain/_brain.yamlthrough the config template and the resolver:cap_bytesplus four thresholds (warn_percent,critical_percent,warn_days,critical_days, defaulting to 80 and 95 percent of the cap and 30 and 7 days to it at the measured trend), with the critical band required to sit past the warn band - a larger percent, a smaller day horizon. An absentcap_bytesmeans the vault is unbounded; setting a cap is what turns on the forecast tiers, the capacity gate and the storage-capacity doctor check.The maintenance lane's fifth gate. A configured cap arms the lane's capacity gate, evaluated last because it is the most expensive - a fresh byte census of the whole vault - and it decides from that fresh census at decision time, never the stored aggregate: a sample taken yesterday says nothing about a vault an agent wrote into this morning. Only a measured critical tier closes it - an unmeasurable store leaves the gate open with its own
capacity:unmeasurablejournal row, the same honesty the host-pressure gate practices - and a closed gate skips the pass with its ownskipped:capacityverdict, the journal row carrying the tier and the reading behind it while the skipped run still appends its usage sample first, so the forecast series keeps accumulating exactly while producers pause. The lane's front doors carry the verdict and its remedy, and where the caller has no local reach the reading stays behind:brain_maintenance statusrenders the tier word and the unmeasurable reason without the three byte-and-percent figures.--force(MCPforce) bypasses the four soft gates - window, busy, pressure and capacity - and every streak refusal; it never bypasses the lease.Interactive writes keep working until the wall. The lane's gate pauses background producers; it does not pause a session.
brain_ingest_sourcerefuses withcapacity_exhaustedonce a fresh census measures the vault at the critical tier, and the check sits in the validate phase before the write path opens, so a refused ingest leaves the vault byte-identical. The refusal names the tier and the remedy at every reach; the whole-vault figures ride only where the census itself may be read.Receipt compaction:
o2b brain receiptsandbrain_receipts. The continuity and metrics ledgers grow one record per event and never shrink; compaction gives them a drain.scan(the CLI's default mode) inventories both ledgers read-only, per shard: records, foldable, protected, unparseable, aggregates, bytes.compactfolds expired records into per-kind, per-month aggregates inside the named shards - the mode and thefamily:nameshard spelling are one shared vocabulary, so the verb and the tool cannot drift - and never runs without explicit shard ids from a scan. A compact previews unless--apply(MCPapply) is passed, takes a recovery-point snapshot before a confirmed run, rewrites each shard atomically, and honors a count guard:--expect N(MCPexpect) asserts exactly N records will fold and aborts without writing on a mismatch, and--strictrefuses a confirmed compact that carries no guard at all. Only records older than the age threshold (--older-than-days/older_than_days, default 30) and in no protected class fold; the protected classes never fold whatever their age - thesource_invalidationaudit trail and the two context-compaction kinds, whose vocabulary a storage operation must not collide with - so their lines stay verbatim, and onereceipt_compactionaudit record closes a confirmed run. Below local reach compact is refused by name; scan stays readable at any reach, and thereceipt-ledger-growthdoctor check reports ledger growth the same way.Near-duplicate document retirement. The opt-in
document-deduphygiene detector nominates near-duplicate copies of ingest summary pages - a lexical similarity of 0.8; the semantic embedding layer (the 0.97 threshold) ships exported and tested but reaches no scan caller yet, because the synchronous scan registry cannot call it, so no surface reaches it until that wiring lands - and selects the newestcreated_atas the canonical, a page withoutcreated_atbeing ineligible rather than guessed at by mtime.applyroutes a selected retire finding through the retirement core: every inbound[[stale-copy]]wikilink inside the detector's scope retargets to the canonical spelling (a same-basename page elsewhere inBrain/keeps its links untouched, and the report names the skip), the copy is removed, its content-manifest entry is dropped - which is the resurrect guard, since with the entry gone the source can only return as a deliberate full re-ingest - and onesource_invalidationaudit record per copy names the canonical, the retired id, the dropped manifest key and the retained source-archive hash. The whole span sits behind the destructive-snapshot gate, and it fails soft per copy: one copy's failure is collected and named while the remaining copies still retire.Incremental extraction of appended sources. The content manifest's entries gain an optional extraction watermark (
extracted_bytes,extracted_at- how many leading bytes the last extracting pass covered, strictly additive, the manifest schema stays at 3, and an entry without the fields reprocesses fully the next time it actually changes, so nothing forces an eager mass reprocess). When a re-ingest meets a watermarked record, the delta proof first shows the growth is a pure append - the SHA-256 of the leading watermark bytes must still equal the recorded digest, read through one non-blocking descriptor - and only then is the tail read through a UTF-8-safe from-offset read whose start snaps forward past a partial multi-byte sequence, bounded by a ceiling; the appended{fromByte, toByte}range rides the ingest result and the CLI extract preview as a report of what the append added - extraction itself stays whole-source, so the range is honest reporting, never a claim that only the tail was processed. A rewritten source - even one that happens to grow - fails the prefix proof and answers no delta, and the full reprocess is the safe direction. The extraction contract stays at version 2.Two doctor checks join the lifecycle.
storage-capacityreports fullness for a capped vault - tier, percent and trend where the census can be taken, and below local reach it answers in the uncertain stream that the reading was not taken, no figures - and skips its own gate when the vault already measures critical.receipt-ledger-growthreads both receipt ledgers and reports their foldable share before compaction becomes the remedy worth running.
Fixed
A bulk source removal no longer aborts on the first blocked file. A confirmed
deleteBySource- the cleanupo2b brain forget-sourceand thebrain_delete_by_sourcetool run - removed derived entries one at a time and threw away the whole run on the first removal it could not complete, leaving the vault half-cleaned with no record of what stayed. Each derived entry and included original now removes through a collecting wrapper: the remaining entries still land, the result carries afailedarray of path and message records naming what stayed on disk, and thesource_invalidationaudit counts both sides honestly - removed N, failed M with the paths. The partial-progress counts persist even when the manifest write itself fails and the run rethrows.Page-dedup merges and maintenance collections fail soft per item. A page-dedup
--applywas a bare loop where one refused merge or one blocked wikilink write aborted every cluster queued behind it; the loop now collects each refused merge as a key-and-message pair, still applies the later clusters, reports the failures beside the merged and wikilink counts, and the CLI exits 1 when the failure list is non-empty. The maintenance action collection behind the doctor, digest and health surfaces absorbs one scan's failure as a named collected error while the remaining signals still collect, so a thin report because a scan refused is distinguishable from a thin report because the vault is healthy.
Changed
Docs:
docs/cli-reference.mdcorrects the maintenance-lane prose to the five-gate reality and the actual--forcesemantics, gains a storage-lifecycle section documentingo2b brain storageando2b brain receiptswith their flags and exit codes, and adds the opt-indocument-dedupdetector to the hygiene row;docs/mcp.mdraises its advertised tool count to 117 and addsbrain_storageandbrain_receiptsto the Tool Highlights table;docs/how-it-works.mdstates the lane's five gates;docs/metrics.mddocuments thestorage_usagesample surface; the README's "What is new" names the release.
1.84.1 - 2026-10-15
Fixed
A stored comma-separated scope no longer aborts the dream pass.
parsePreferencereadsscope: alpha,betaback verbatim, and records written before the v1.78.0 tag hardening can carry that shape, but the refresh writer composed the whole value as onebrain/scope/...tag, the shared tag rule refused the comma, anddreamthrewTagSyntaxErroron a record the reader accepts - whether the stored tag was the valid original or a legacy comma-bearing one. A comma-separated scope is now a list at the tag boundary: each trimmed, non-empty segment composes its ownbrain/scope/<segment>tag, duplicates collapse, and thescopefield stays verbatim, so the record keeps its meaning. Preferences and signals share the one rule (scopeTagValues). New caller input stays strict: a segment that fails the tag rule (a space, a special character) is refused unrewritten, and a preference write names the record (fix the scope field of pref-<slug>). Records the dream pass rewrites from disk - a refreshed preference, or a new preference promoted from stored signals - and the keep record ofbrain mergecompose leniently instead, because a vault written before v1.78.0 can hold a scope such asfront end: a failing segment is slugified the way a topic is (brain/scope/front-end), a segment whose slug still fails the rule composes no tag, and thescopefield stays verbatim either way. Reported by @Padde86 in #253.The MCP identity tests no longer read the developer's real config. Two test files cleared
OPEN_SECOND_BRAIN_CONFIGafter they ran instead of restoring it, so every later file in the same process lost the test preload's throwaway config and config discovery reached the real user config; the owner-scope fallback-identity test then saw a configured agent name instead of the unresolved placeholder and failed whenever it ran after them. Both files now restore the prior value, and the fallback-identity test pins its own config with noagent_nameand noVAULT_AGENT_NAME.The iterative-recall deadline test no longer races wall time. It simulated an expired deadline by shifting
Date.nowa constant 10 s, but the search reads its own start instant through the same clock, so the offset cancelled out and expiry still depended on real elapsed time; on a fast enough run the looped search reached the loop gate before the 1 ms budget lapsed and ran a round the plain search never runs. The stub now steps the clock forward on every read, so every check after the start is expired regardless of load. The search code was already correct.The overlap fallback test asserts the interval it can actually measure. For a provider that never reports a dispatch (the offline local embedder) the embed interval is the whole awaited call, and its resolution stamp is written only after the synchronous keyword lanes yield, so the interval encloses the keyword interval and
overlapped_msis that interval's length - 0 or 1 ms on the wall clock depending on whether the lanes crossed a tick. The test expected exactly 0 and failed under load; it now steps the clock the request reads and asserts the ordering (pre-call start before the keyword lanes, resolution after them) and that the overlap equals the keyword interval.
1.84.0 - 2026-10-14
Open Second Brain 1.84.0 hides the recall lane's embed round trip behind the keyword work and accounts for the overlap: hybrid search now starts the semantic lane before the keyword lanes run and joins it where the serial await stood, search --explain reports the overlapped milliseconds, and the bench latency family carries the same figure per question and in aggregate. Every surface is byte-identical when the new option is absent, and no configuration changes.
Added
The semantic lane starts before the keyword lanes. Hybrid search's one caller wait - the query embed - is now put in flight before
runKeywordLaneruns: the lane starts through a join seam that attaches its rejection handler in the same tick, and the join stands where the serial await stood, in both deadline arms. The lane start stays after the query-cache probe, so cached answers still cost nothing; an already-expired composite deadline starts no embed at all (the never-start rule moved to the start site); and a keyword-step throw with the embed in flight can no longer strand an unhandled rejection - with a deadline the lane's late, non-abort failure is named by thehybrid deadline: an abandoned lane failed after the answer:sentence, without one it is defused. The keyword pool size reaches the lane as a supplier read at lane completion, so thehybrid_degradedsignal keeps grading the same snapshot the serial pipeline captured; the post-join void and every later deadline checkpoint are untouched. Results, warning order and the query-cache keys are byte-identical: the deadline-degraded answer stays keyword-only, and the wall-clock budget is neither started earlier nor extended.overlapped_msonsearch --explain. The overlap of the embed and keyword intervals -max(0, min(embedEnd, keywordEnd) - max(embedStart, keywordStart)), zero whenever the embed never ran - rides a per-call timing sink on the search options, never the outcome: the query cache persists whole outcomes, so an outcome field would freeze one call's timings into every cached row served afterwards. With--explain, the JSON payload carries an integeroverlapped_msbesidesemantic_attempted, and the human transcript prints the overlapped line in the explain block; without the flag the payload is byte-identical (off means absent). A cross-vault union captures no timing, because a union merges one search per origin and no honest single figure exists for it.A caller-selectable packing policy over the pack's two candidate populations.
brain_context_packgainspacking_policy(facts_firstorretired_first), strictly opt-in and byte-identical when omitted: inside one pack the preference-derived pages and the retired pages already share the singlemax_tokensbudget, and the policy names which lane claims it first -facts_firstclaims for the distilled facts lane before the pages lane fills the exact remainder,retired_firsthands the first claim to the retired pages lane - query-ordered in therankedandsemanticquery modes, tier-then-recency ordered in the default substring mode, where the query filtered rather than ranked. Neither member is the omitted default: the omitted pack interleaves both populations by tier, while a named policy orders whole lanes - every preference before every retired page underfacts_first- reserves the attention charge up front instead of drawing it last, and refuses a vault where one lane holds no candidate in reach, a call the omitted pack succeeds on. Claiming is strictly sequential with no minimum-page floor, the total never grows, the per-memory character cap stays lane-internal, and the synthesized attention block stays a reserved charge neither lane draws; lane overflow keeps the shippedover-budgetskip reason. The pureallocateRecallBudgetprimitive lands beside the sharedapplyCharBudget(frozen vocabulary, narrowing guard, lane allocations plus any reserved charge always summing to the total), and the report'spackingaccounting - the policy name, the reserved attention charge, plus each lane's allocation, delivered spend, candidate count and item count - is wired through the one finalize funnel, so the char-budget path agrees with it; a zero budget with a policy named gets no accounting at all, because that call is refused before anything is returned. The refusals all fire by name and before any receipt or telemetry is written: a non-member value, a policy without a query (inert), a policy against a one-lane vault (INVALID_PARAMS naming the lane and the member, and both lanes when both are empty), a zero budget with a policy named, and a reserved attention charge that consumes the whole budget. The MCP argument mirrorsquery_modeargument for argument: the schema enum derives from the frozen tuple, the query pairing is declareddependentRequiredstyle, and every refusal arrives on the wire as INVALID_PARAMS carrying the named message; the response gainspackingonly when the policy was named. Receipt and telemetry budget metadata and the prompt-prefix metric segment carry the policy as a closed-vocabulary member under the same only-when-named rule. No CLI flag (thequery_modeMCP-first precedent) and no recall-inject or hook change - the hook runs a single lane, where a policy would be inert by the design's own refusal rule.overlapped_msin the bench latency family. The bench passes the per-call timing sink for every retrieval question, so a run whose suite declares the embeddings capability and pins the semantic lane reportsoverlapped_msper question and its mean in the aggregatelatency_msfamily - both present only when at least one question attempted the embed. Keyword-only runs (the default pin) add no key anywhere: the report JSON, the committed failure-modes baseline and the human output stay byte-identical, and the humanoverlapped_ms: avg Nline prints only when the aggregate carries the figure.Opt-in standing-instruction facets with first-mention provenance. The
standing_queryattention action gains astandingInstructionFacetsoption: unset, every emitted string is the legacypref-<slug> (scope)output byte for byte and sections carry no facet data; set, a surfaced preference whose provenance join resolves gains a first-mention annotation on its item string ([first mention turn <session>#<turn>], or[first mention event ...]for a capture-lifecycle origin, which is not guaranteed to be a turn id) plus a structuredStandingInstructionFacetentry - the preference-corpus sibling ofThreadFacet, same contract shape (stablepref:<id>key, ordered origins, pure derivation), distinct name so the coverage lane's meaning of facet stays unambiguous. The join is a new pure read-time module (src/core/brain/preference-provenance.ts, no I/O, the signal lookup injected) overevidenced_bywikilinks and producer-awaresession_refparsing: import-sessionbasename#turnresolves as a turn, capture-lifecyclesession:<id>#<event>as an origin event labeled as what it is, and the baresession:<id>the auto-extract path persists resolves to nothing - the never-guess rule covers every other shape, so an unknown ref yields no provenance rather than an invented coordinate. The option changes annotations, never membership: ownership and retirement exclusions apply before annotation, so a facet cannot surface a preference the plain lane withholds; a preference whose join does not resolve keeps the plain string and is absent from the facets - an honest zero, never an implied provenance; and the whole lane performs zero writes, standing-rules opacity and the vault-identity guard unchanged.A
standing_instructionbench category gates the facet lane. The new category joinsBENCH_CATEGORIESand drives the shipped surfacing call with the facet option on over the fixture vault, so the lane ships measured:evaluateQuestionasserts the expected facet keys -pref:<preference id>, the preference id itself - and the expected first-mention annotation, whose kind word keeps an event-annotated origin from satisfying a turn expectation; a surfaced preference whose join does not resolve is a named failure, never an implied provenance.BenchPinnedKnobsgains the lane's identity marker: it changes no behavior - the category is armed by the fixture - but folds into the checkpoint hash, so a resumed run that would mix a facet-gate arm with a plain one fails the identity guard, while the default pin serializes byte-for-byte unchanged. Reports without the category keep their bytes, and the humanstanding_instruction: <passed>/<total> (pass rate <n>)line prints only when the quality buckets carry the category; the exit code stays all-pass-or-nonzero with the category in play.
1.83.0 - 2026-10-13
Open Second Brain 1.83.0 wires four shipped seams, each opt-in and byte-identical when off: a per-source-session cap on the search candidate pool, usage-ordered skill offers on skills_attach, seeded question-type-proportional sampling for the LongMemEval bench suite, and the OpenCode V2 config shape for the opencode install. No new MCP tools, no new continuity record kinds, no new external dependencies.
Added
A per-source-session cap on the search candidate pool.
search_pool_max_per_session(OPEN_SECOND_BRAIN_SEARCH_POOL_MAX_PER_SESSION, integer, default0- off) bounds how many rows one source import session may contribute to the reranker-facing candidate pool. A row's session identity is a non-emptysession_reffrontmatter value (imported signal notes), else a non-emptysession_id(handoff notes), read through the same frontmatter cache the pool filters already use; rows without identity, and rows whose frontmatter is unreadable, pass through uncapped. When the cap is active it sits after the relevance floor and before the MMR diversity rerank and joins the pool-width widening, so the cap spreads the window across sessions instead of merely truncating it sooner. The backfill trigger is unchanged - the cap never adds a reason for the second assemble pass, and when that pass runs for its own pre-existing reasons the cap is re-applied over the wider pool, with the reported trail being the final pass's. Drops carry the new retrieval-trail codesource-session-capped-rows(detail.droppedagainstdetail.cap), noted once per answer. The knob rides thesearch_mmr_lambdaplumbing exactly: parsed inresolveSearchConfig, range-validated invalidateResolvedConfig(a negative value is refused), surfaced as the optionalResolvedRecallConfig.poolMaxPerSessionspread-omitted at0so exact-shape resolved-config assertions keep holding, with a per-querySearchOptions.poolMaxPerSessionoverride and both results-affecting folds - the search config fingerprint and the per-query cache key - so a knob change can never serve stale cached rows. A bench--pinaccepts the knob by name ({"recall":{"poolMaxPerSession":N}}). Off (0or absent), results and query-cache keys are byte-identical and no frontmatter reads are added; with the knob on, the resolved value folds into the search fingerprint so cached rows can never go stale, while in a vault with no session identity the results alone stay byte-identical at any value.Usage-ordered skill offers on
skills_attach. Withskills_attach_usage_ordering(OPEN_SECOND_BRAIN_SKILLS_ATTACH_USAGE_ORDERING, default off) on, observedskill_invokedusage reorders the survivors of the discriminating-term floor by the bounded compositescore * (1 + 0.5 * weight)-SKILL_USAGE_ORDERING_MAX_BOOST = 0.5,weightthe usage store's decay value in (0, 1] - with equal composites resolving by name ascending, so a skill an agent invokes daily can rise over a slightly hotter one-off while a wide relevance gap stays decisive, the floor is never crossed by weight, and every returnedscorestays the untouched lexical score. The pureskillUsageWeights(usage, knownNames)builder joins usage onto discovered names without rewriting any record: exact names join first, plugin-qualified keys (vendor:name, the shape the Claude CodeSkilltool records) join through the longest suffix after a colon that names a discovered skill, keys matching nothing are dropped, and the higher weight wins when several rows join one name. Private records never count: the continuity read dropsprivaterows the same way every reader does. The tool derives usage exactly once per attach call only when the flag is on and feeds the same weight map to both build paths, so the deterministic and decision-model offers stay coherent; the tool description names the knob in one clause. Flag off - or on, in a vault with no usage records - the offer is byte-identical to the lexical order.Seeded LongMemEval sampling.
o2b brain bench memory --suite longmemeval --sample-seed <value> --sample-cap <n>scores a deterministic, question-type-proportional subset of the dataset instead of running it wholesale. The flags are a pair (either alone is refused by name), the cap must be a positive integer (otherwise refused naming the value), and both together require a suite whose registration declares the newsamplingcapability - longmemeval is the only one - otherwise refused naming the suite, so the flags can never be silently ignored by a loader that cannot honor them. The pure sampler (src/core/bench/longmemeval-sample.ts) excludes abstention entries first (both spellings; their count still lands inabstention_excluded), stratifies the survivors by their presentquestion_typevalues sorted ascending, apportions the cap across strata by largest-remainder (Hamilton) apportionment clamped to the population with fractional ties broken by stratum name, and draws within a stratum by a seeded Fisher-Yates over its own mulberry32 PRNG keyed bysha256Hex(seed)- noMath.random, no wall clock - over entries sorted byq_id, so dataset file order cannot change the subset; the selection re-sorts byq_id, and a population of zero after abstention exclusion raises the namedLongMemEvalSampleError. Sampling happens before conversion, so a sampled run materializes fixture notes and continuity records only for the questions it scores; the receipt gainssample_population,sample_selectedand the per-stratumsample_stratabesideabstention_excluded, and the report line gainssample_selected Nonly on a sampled run. The seed and cap fold intobenchRunIdentityHashspread-omitted: run ids of runs without sampling are byte-stable (the committed pins hold unchanged), two runs differing only in seed or in cap get different ids even when the sampled fixture would be identical, and a resumed run whose sampling options changed fails the identity guard by name. Without the flags the loader path is byte-identical, and the frozen locomo and sample fixtures stay untouched.The opencode install learns the OpenCode V2 config shape. The adapter now detects, through one shared function used by apply, verify, detect and plan, whether the config carries the native V2
mcp.serversobject - and registers there - or keeps the legacy top-levelmcpkey every earlier release wrote. When onlyopencode.jsoncexists it is the resolved config path, and a vendored minimal JSONC editor (src/core/install/jsonc.ts, token-offset splices, no new dependency) edits comment-bearing files so comments and formatting survive byte for byte outside the changed entry; duplicate top-level keys are a namedJsoncEditErrorrefusal, and a BOM is stripped on read and restored on write. An existing healthy top-level registration is never migrated whenmcp.serversappears (V2 hosts keep reading a legacy declaration, so migration would churn installs for zero benefit); the manifest records the key actually written (mcp.servers.<entry>ormcp.<entry>), and uninstall removes exactly those keys under either shape. When the host addsmcp.serversto a config the adapter previously owned, the first--checkreports drift once and the first re-apply is refused through the user-modified-block guard; a forced re-apply (--force) registers at the native path and cleans the shadowed legacy copies of its own two keys, never user keys. V1-shaped plain-JSON configs keep the previous write path byte for byte, and the doctor's registered-command probe learned the nestedmcp.serverskey so a healthy V2 install passeso2b doctor.
Changed
Docs:
docs/cli-reference.mdgains thesearch_pool_max_per_sessionrow in the recall-quality config table, thesource-session-capped-rowsrow in the retrieval-trail vocabulary, a per-source-session pool cap section, and the bench sampling flags with their selection semantics and the resume-guard consequence;docs/how-it-works.mdcovers the cap in the recall-layers ranking paragraph;docs/mcp.mdaddsskills_attach_usage_orderingto theskills_attachopt-in list with the name-normalization rule, the private-records note and the per-call cost;docs/observability.mddocuments the sampling flags, the stratified selection, the receipt fields and the identity fold;install/opencode.mddocuments the V2 shape, the.jsoncpath, shape-aware uninstall and the one-time drift;docs/updating.mdgains the 1.83.0 upgrade note for that drift, following the 1.50.0 convention; the README names the release and extends the search capability (mirrored intoplugins/codex/README.mdby the mirror sync).
1.82.0 - 2026-10-12
Open Second Brain 1.82.0 is the recall-quality release: it widens what recall can reach on a hard question and makes every surfacing decision measurable. A bounded iterative recall loop keeps querying after the single self-correcting second pass and, when coverage gaps survive, pauses mid-answer with a plan the caller executes and commits back; session recall gains a coverage-first selection that answers for a whole thread instead of a keyword's hits; recall admission moves onto one shared graded surfacing key that the recall-inject hook and the recall gate grade identically; and the bench grows the harness to score all of it - a suite registry, a LongMemEval lane with a strict recall_all@5 receipt, and a surfacing-replay suite that measures the graded gate itself. The default posture of every surface is byte-identity: with the knobs off, every response and every query-cache key is exactly what it was.
Added
A bounded iterative recall loop behind
brain_search's plan/commit round-trip. With the operator knobsearch_iterative_recall_enabledon (OPEN_SECOND_BRAIN_SEARCH_ITERATIVE; the round capsearch_iterative_recall_max_rounds/OPEN_SECOND_BRAIN_SEARCH_ITERATIVE_MAX_ROUNDSdefaults to 2 and clamps to 8), a query markediterative: trueruns gap-driven rounds past the single second pass: each round re-measures pool coverage, emits follow-up sub-queries from the uncovered rare-term groups, and merges what they recover into the same ranked pool, stopping on coverage completeness, a round that recovers nothing, the marginal-value stop extracted from the retrieval advisor, the round cap or the composite deadline. The deterministic k-group coverage planner is the always-available floor - the mode is useful with no model in the loop - and the decider rounds are additive: when measurable gaps survive the deterministic budget, the call pauses with aniterativemarker atphase: "needs-llm-step"carrying the decider envelope, the candidate pool ids, the uncovered gaps and an opaque single-use round token, and the caller re-invokes withiterative_tokenplusiterative_payload({evidence, subQueries}), validated in full before any pull runs - unknown chunk ids, oversized or over-limit sub-queries, sub-queries carrying no significant terms, a replayed token (token_used), a mismatched one (token_mismatch) and a fully-formed commit arriving while the loop is disabled are typed refusals surfaced as caller errors. The round-trip runs on the single-vault path only: anyiterativefield combined withglobal: trueis refused by name at the argument layer, so no iterative field ever reaches the federated legs. Every loop that ran at least one round - a committed call, or a plain final - answers with the same marker atphase: "final", carrying the per-round records (terms, groups, added, elapsed, and whether the deterministic floor or the decider authored the sub-queries) and thestop_reason(coverage_complete,no_sub_query,no_progress,round_cap,marginal_value,deadline,decider_round); with the knob off, and on any call the mode did nothing for, the response and the query-cache keys are byte-identical, because the cache suffix that separates iterative rows exists only in the opt-in mode.Coverage-first session recall on
brain_session_grep, with CLI parity.operation: "coverage"replaces the grep's term matching with thread coverage over the same lineage-scoped record set: facets are the thread's own - distinct mined entity keys and depth-1 summary clusters, never a word list - and fan out under the adaptive topic budgetclamp(distinct facets, floor 12, cap 16), the selection returns in chronological order under the requiredchar_budgetbeside recomputable diagnostics that report the realized topic count honestly, andentity_keyoptionally seeds the fan-out from thesession_entity_mentionrows an import already mined - no new mining, no new record kind - with an entity no record mentions refused by name instead of answered with an empty result. The operations keep their arguments to themselves:char_budgetundermatch, andquery,since,before,limit,snippet_chars,include_rawandraw_budget_charsundercoverage, are each refused as not applicable. A budget that cannot hold the floor-weighted selection is a typed infeasibility mapped toINVALID_PARAMS, naming the requested topics, the kept ones and the required characters - a retry bound, never a silent truncation, with the floor enforced again after the real budget pass so a crowded-out floor topic cannot slip through the pre-check.brain_session_describeacceptstopics: true, adding thetopic_reportthe coverage operation fans out under (detected facets, the budget, the realized topics); without it the response is byte-identical. The CLI mirrors both surfaces:o2b brain session-grep --operation coverage --char-budget <n> [--entity-key <key>]ando2b brain session-describe --topics, with the same refusals.A bench suite registry, with LongMemEval and surfacing-replay suites.
o2b brain bench memory --suite <name>resolves through a registry mapping each suite name to its loader plus a declared capability set ({embeddings: boolean}), so what a suite may do is explicit in code rather than flag-implied; omitting--suitestays the byte-identical default path.--suite longmemevaladds the benchmark the memory field reports recall_all@5 on: a fail-fast parser over the official dataset shape with named parse errors, a converter into the unchanged bench fixture that drops abstention questions at conversion with the count recorded and joins gold sessions by direct id membership (an unknown id is a named parse error, andexpected_textis never set, so nothing silently scores nothing), a committed sample fixture certifying CI with the full dataset an operator-supplied path that is never downloaded at run time, and the strict named report familylongmemeval: {recall_all_at_5, abstention_excluded}beside the untouched quality, latency and context_cost families. The judge glue reusesbench_judge_cmd: when the suite provides them, the stdin payload's questions carryquestion,answer,goldand the evidence the run actually injected (path plus a bounded excerpt per retrieved note, in rank order) beside the usual result fields, and the judge prompts are house-written in the spirit of upstream'sevaluate_qa.pyrather than transcribed from it, so the external command stays a dumb model call; suites without judge input keep the payload byte for byte. Embeddings are a per-suite capability: the semantic lane is reachable only for a suite that declared it, the index builds through the apply-gated embedding path with the spend plan surfaced and never silently bypassed, a shared vector cache under the runs dir keyed by provider, model, dimension, prefixes and text hash lets both arms compare byte-identical vectors, and--pin <json>plus--recall-floor <n>expose pinning and the admission floor from the CLI.--suite surfacing-replayreplays a recorded prompt corpus - a committed session fixture in the replay shape, carrying each prompt's recorded predecessor for the structural gate - through the shipped decision core and scores the graded surfacing gate's know_to_ask and false_fire rates under the same denominations the proactive-recall metric uses, with per-turninjected_tokens, checkpoint and resume, and the abstain-costs-zero invariant asserted: the calibration becomes a measured number a re-run re-measures.Bench runs carry their configuration in the run identity. A
SearchConfigOverrides-shaped knob pin applies last throughresolveSearchConfigand folds into the checkpoint hash beside the fixture hash and the proactive-recall admission floor, so a resumed run that would mix configurations fails the identity guard by name instead of silently comparing two pipelines, and two runs with different pins get different run ids. The default pin is the semantic-off override the harness always applied, so locomo report numbers stay identical; the one-time consequence is that in-flight runs started before this change fail the resume guard loudly and restart.
Changed
Recall admission is one graded, channel-aware key. The recall gate's binary prompt-structural predicate stays the structural pre-tier with its named short-circuits (empty prompt, verbatim repeat, slash command, shell command) and keeps projecting the binary
{retrieve, reason}contract unchanged; admission now rides one shared calculator that grades absolute match quality - the IDF-weighted coverage a search outcome already reports, the quantity recall-adequacy grades - calibrated per transport channel, and orders by the existing fused composite, which is never a threshold basis.brain_recall_gateand the recall-inject hook's floor branches consume the same{key, tier, admit, reason}decision, so a tier boundary surfaces the same reason on every channel, and a measuredbrain_recall_gatecall returnskeyandtierbeside the binary decision, absent - byte-identical - without a measurement. Per-channel admission floors ship as exported module constants at the hook's 0.35, the hook'sRECALL_INJECT_CONFIDENCE_FLOORnow derived from that single source, with no new config knobs. Gate telemetry under the existingrecall_gate_telemetryopt-in records optionalkey,tierandchannelon measured decisions and summarizes counts by tier; the opt-in-off path records nothing, and no new record kind exists.Docs:
docs/mcp.mddocuments the iterative round-trip, the coverage operation with its argument partition, the graded surfacing key onbrain_recall_gateand the widened gate telemetry;docs/cli-reference.mddocuments the coverage flags with their CLI parity, the iterative-recall and graded-key sections, and the bench suites with the run identity;docs/observability.mdcovers the gate-telemetry key, tier and channel fields and the suite registry; the README names the release and extends the search and session-recall capabilities.
Fixed
The iterative loop answers to the reach rule everywhere. The round records counted raw pull hits, so at non-local reach a probing caller could read recovery counts for terms only withheld pages match - the same boundary the pause pool already honors; the reach filter now governs the loop's working set, so withheld candidates never enter the pool, the round accounting or the coverage gaps. An expired composite deadline at block entry also skipped the commit refusal, handing the caller a plain answer and an unspent token with no error; the commit is refused by name like every other half-executable commit, and the round token is claimed atomically so two racing commits cannot both spend it.
Coverage selection refuses honestly instead of silently degrading. The private-row rule the entity-seed resolver applies now covers the coverage facets too, so a private mention can no longer seed a facet whose key surfaces in the diagnostics or the topic report; the entity seed and its typed unknown-entity refusal reach both shipped surfaces (they were core-API only); and the echoed entity key is capped at the sibling argument bound.
The bench refuses the identities it cannot honor. A run id of dots passed the charset check and resolved into the runs-dir root, writing the checkpoint and vault beside the shared vector cache, and is refused by name; distinct LongMemEval session ids that sanitize to one evidence note are a named parse error instead of silently merging both sessions' evidence; and the embedding cost gate prices the misses census, so a second arm the shared cache serves whole is no longer refused under a positive gate against the zero-provider-contact purpose the cache exists for.
1.81.1 - 2026-10-12
Fixed
The snapshot exclusion no longer depends on the shape of a directory entry.
createSnapshotlists the top-levelBrain/entries withreaddirSync(dir, { encoding: "buffer" })and drops.snapshotsand.artifactsby name. The name was read withtoString(), which is right for aBufferbut renders a bareUint8Arrayas comma-joined byte values ("46,100,..."); on a runtime that returns that shape nothing matched,.snapshotswas packed into every archive, and each dream snapshot carried its predecessors until a run outgrew its timeout. Entries now decode throughBuffer.from, which reads either shape, and tests pin both the archive members and the bareUint8Arraydecode. Bun 1.4.0 and 1.4.2 on Linux returnBufferentries, so vaults there were not affected. Reported and fixed by @Iversondwx in #243.The explorer's port-in-use test no longer collides on busy runners. It held a random port and failed when another process already owned it; it now holds a port the OS assigns.
Changed
Dependencies:
oxfmt0.72.0,oxlint1.87.0,@types/node22.20.5 and@node-rs/jieba2.0.4; CI actionsactions/upload-artifact7.0.2 andhashgraph-online/ai-plugin-scanner-action1.2.772.
1.81.0 - 2026-10-11
Open Second Brain 1.81.0 is the answer-trust release: it narrows the gap between what an agent was given and what its answer can be believed on. A Stop-hook step now checks the quotations in the assistant's own answer against the note cited for them or the evidence the injection ledger actually delivered, a stated claim can name the context-pack sample it drew on and read the receipt and outcome provenance back, trusted local-file ingest keeps the source's bytes in a durable content-addressed archive instead of trusting a file to stay where it was, and a record with no session is read as no-session everywhere instead of pooling into one phantom bucket. A repair lane closes the outstanding review findings: attribute narrowing in search no longer silently excludes documents whose indexed snapshot has gone stale, and the wave's own review hardened the annotation channel, the provenance reach and the archive's write path.
Added
Answer-time quote verification, shadow by default. A new Stop-hook step registered after the stop-log guardrail walks the last assistant messages of the turn, finds quoted spans with the distill lane's quote helpers (imported, never forked), resolves each span's evidence - a note named in the span's context by wikilink or vault-relative path, else membership in the injection ledger's delivered notes for the phases actually running - and checks it through a pure module over five verdicts:
verified,not-in-note,source-truncated,note-unavailableandnot-text. Degradation never upgrades a verdict: a note whose body reached context truncated is matched against the prefix that was delivered, and a miss there issource-truncated, nevernot-in-note; a note that cannot be read isnote-unavailable, because "we could not check" is a finding, not an all-clear. OneadditionalContextannotation per turn lists the unverified spans as flattened, capped previews; Claude Code renders it and the other registered runtimes stay silent. The step never blocks, never edits a transcript, and reads a bounded message window under character and wall-clock caps, failing open with exit 0. Configuration is per phase in thequote_checkblock ofBrain/_brain.yaml-recall,activeandsubagent, eachoff|shadow|enforce, defaultshadow(annotate-only), scoped to the phases whose injection the vault enabled - andenforceadditionally appends exactly one Brain-lognoteevent per turn carrying the counts and the finding lines, so findings survive the session.Statement-level claim attribution.
brain_truth stateando2b brain truth stateaccept an optional opaquesample_id(CLI--sample-id) frozen on the committed claim event - schema version unchanged, omitted when absent, never invented - and claim reads hydrate the provenance behind it: the named context receipt's items plus the context-pack outcome row, joined read-only, one ledger read per report. Provenance is shaped by the caller's reach - below local reach, receipt items whose injections came from operator rules and item paths the caller cannot read stay hidden, so a claim'ssample_idnever leaks what the receipts surface refuses to say directly - and the join is omit-don't-invent in both directions: a claim whose outcome never arrived reports "receipt known, outcome unknown", a claim timestamped before its claimed receipt is flagged as an inversion rather than silently joined, and a row without asample_idrenders byte-identically to before.A durable source archive behind trusted local ingest. Ingesting a trusted local source now reads the file once and archives its bytes content-addressed at
Brain/.archives/<sha256>.txt- redacted before persist so credentials never land in a second file, refused past a 1 MiB ceiling rather than truncated (an oversized source is simply not captured, and absence on the page still means exactly "not captured"), deduplicated by its own hash, and never TTL-pruned: evidence, not scratch. Success stampssource_archive_hashandsource_archive_byteson the summary page and returnssource_archive_hashon the ingest result; visibility tokens hold, so a source whose digest is refused to remote readers is never archived and remote reach never reads archived bytes. The capture-scope hygiene detector names a page whose archive is missing or no longer matches its stamp,o2b brain doctorprints a count-only archive section (entries, bytes, missing, mismatched, uncited - no fix verb, pruning and recapture are operator calls),brain_hygienegainsarchive_get, which resolves a summary page's stamp to its archived bytes at local reach only, and delete-by-source cleanup removes the source's orphaned entries best-effort.Explicit no-session identity. A missing or non-string
session_idis treated as no-session explicitly at every reader: scoped recall excludes the record, grouping sites skip it instead of lumping it into a shared bucket, and the session-summary digest omits the key rather than answering""- dissolving the phantom session that quietly collected every unattributed record from every unrelated lane. A new count-only, fail-soft doctor check,blank-session-identity, reports one issue per affected record kind with a count; no payload is rewritten and no repair verb ships, because the continuity store is append-only and repair belongs to a backup-gated posture.
Changed
Docs:
docs/cli-reference.mddocuments thetruth events/truth stateverbs with--sample-idand provenance-hydrated reads, the widened capture-scope detector and the doctor's archive section;docs/mcp.mdcoversarchive_getonbrain_hygiene, the archive stamp on the ingest result and the claim provenance hydration;docs/observability.mddocuments the quote-check surface and the no-session rule with its doctor count;hooks/README.mdgains the new Stop step; the README names the release and extends the claim-ledger capability.
Fixed
A stale attribute snapshot no longer silently excludes a document from search. The property pushdown narrowed the candidate pool against the indexed
document_attributessnapshot while advertising the live file as the final judge, so a document whose indexed snapshot no longer matched its file - a draft flipped to done - dropped out of astatus=donequery before the live post-rank filter ever saw it. A store-level freshness gate now compares a candidate's indexed snapshot against the live document during narrowing: a proven-fresh candidate keeps its SQL exclusion without touching the filesystem, a stale one falls through to the lane read without the exclusion fragment and is judged by the live filter, and absent filters still compile byte-identical SQL.Review hardening across the wave. The external reviews of this wave hardened what landed: the annotation's findings render as flattened previews under a character cap, the cited-note read is bounded and the transcript tail-window read advances in pages instead of scanning whole files, the archive stats path resolves its root through the containment check before it stats anything, and the archive stamp's single-writer assumption is pinned as an architecture census beside the claim-attribution one.
Two damaged CHANGELOG entries read as written again. Whitespace that had collapsed inside the v1.72.0 cost entry (words running together around
estimated_cost_usdandsearch status) and the v1.19.0 post-compact-audit block (a lost indent) is repaired; prose only, no claim changed.
1.80.0 - 2026-10-11
Open Second Brain 1.80.0 raises what recall can reach and what the store can answer for itself, on both ends of the pipeline: what enters the graph is gated by what the vault itself has learned, and what the store can filter is answered by the index instead of by per-row file reads. Entity creation consults a store-learned lexicon with two new structural verdicts, session imports mine precision mentions into a hallways graph an operator reviews before anything is promoted, the search store's v14 migration pushes kind and promoted frontmatter keys into SQL on both lanes and hydrates in two tiers, the rerank eval reports per language, and recall telemetry names its dead weight. A repair lane closes every outstanding operator-review finding from the two previous release reviews, and two release-note claims those reviews falsified are corrected here.
Added
A store-learned entity lexicon behind the admission gate.
o2b brain entity lexicon rebuild [--apply]folds the vault's own pages (vault user pages only - the Brain root and session transcripts are excluded, and the artifact header binds that scope) intoBrain/entities/lexicon.json: schema-versioned, dataset-hashed, written through one shared derived-artifact helper, and fail-open - a stale, torn or expired artifact reads as absent, so admission degrades to the previous structural behavior and never mis-rejects. No word list ships with the binary; the vocabulary is the vault's own.Two tightened creation verdicts, advisory after admission. Entity creation (
upsertEntity, the one choke point every minter shares) now refuses a multi-token all-caps label with no prior stored occurrence (shouted-heading) and a caseful label whose every token the lexicon learned as a common word (function-word-run), while structural code shapes (a_,.,/, a digit, or internal uppercase - the symbols the session miner exists for) bypass both case rules, and caseless-script labels admit on the structural gate alone. The verdicts feed the existing heal triangle - the doctor'sentity-label-malformedfindings and the snapshot-gated, dry-run-defaulto2b brain entity prune- so already-admitted records are repair candidates, never auto-removed, and anchoring (buildAnchorables,anchorEntityForms) resolves the same handle so junk forms stop anchoring facts.Session hallways. Session import mines a precision-mention side ledger - code symbols from spans, fences and qualified-identifier shapes, URLs, file paths in both slash directions, qualified identifiers - refusing secret-shaped tokens outright and deduplicating at
(family, key, session)granularity with provenance keyed on the transcript basename, never a host absolute path.o2b brain hallways mine|show|promote|dismissfolds that ledger into pairwise-PMI associations written toBrain/link-graph/hallways.jsonbeside, never merged into, the vault-derived co-occurrence graph, with a reviewable proposals file and cross-run dismissals. Mining itself never writes an entity:promoteis the only bridge into the registry, offered solely for keys seen in at least N distinct sessions (--min-sessions, default 3), and a promoted label still answers to the tightened admission gate.The v14 store migration. One crash-safe first-open transaction adds a virtual generated
chunks.chunk_kindcolumn (pagefor chunk 0, elsesection) with taxonomy indexes, a boundeddocument_attributesside table backfilled from chunk 0 (64 keys per document, 16 values per key, 256 bytes per value; overflow decrements the set and never fails the index; the promoted keys are skipped, so none of them gains a second source of truth), and sargable path-prefix predicates (path >= prefix AND path < prefix || U+FFFFover the canonical POSIX spelling, which also fixes a latent Windows backslash prefix mismatch). The migration never rewrites the embedding tables.Kind and property filter pushdown on the query path. A
kindfilter (page/section, MCP argument and CLI--kind) pushes into the SQL of both lanes - beforeORDER BY/LIMITon the keyword lane, inside the overfetch window on the semantic lane - and the promoted frontmatter keys compile to SQL against their dedicated indexed storage where the push is faithful to the live filter's verdict:typepushes the scalar declared values as the one normalized token the index stores,pinnedpushes only fortrue, andvisibilitypushes its normalized tokens.tagsand every non-scalar shape stay post-rank filters judged against the live file, because the indexed tag corpus and a single token column cannot answer for what the frontmatter array says. Other property keys narrow the candidate pool in SQL against the boundeddocument_attributessnapshot - only a provable mismatch drops out, since the bounded table can be silent about an over-bound document - and the live filter still judges every surfaced row, so answers stay identical to the post-rank filter alone while a filtered query hydrates and reranks fewer candidates. Pushed filters answer AS INDEXED, a documented divergence from the file on disk, and absent filters compile byte-identical SQL.A two-tier hydration cut. The candidate pool hydrates metadata only; content hydrates for the rank cap after assembly, with final results byte-identical and peak hydration bounded by the rank cap rather than the pool.
Per-language rerank eval. Benchmark dataset queries may carry a
langfield (defaulten, validated likeanswer), ando2b search rerank-evalreports a per-language split beside the overall aggregates, each language folded with the same reduce over its per-query rows, so a bilingual dataset shows per-script lifts the blended number would average away. A bundled self-authored ZH fixture (separate from the untouched EN fixture, whose denominators and pins are preserved) gates Chinese tokenization, indexing and rerank ordering in CI on the offline stack.The dead-weight telemetry view.
brain_recall_telemetrygrows a seventh operation,dead_weight(CLIo2b brain recall-telemetry dead-weight), reporting pages currently in scope that no recorded recall within the telemetry window ever surfaced. The denominator is the vault walk's included paths - current on-disk truth, never the stale document table - so a deleted-but-indexed page is never reported; the retrieved set is the union of recorded recall artifacts and observed-use references,since/until-windowed; the payload carries the window, the counts, a capped dead list behind the usualvault_pathenvelope, and the gate caveat: telemetry is opt-in and records only surfaced top rows, so the report claims "no recorded retrieval surface within telemetry history", never absolute never. No new MCP tool - the tool-count pin holds.
Changed
Permission-rule targets canonicalize. A rule target written with backslashes or
.\\segments (Notes\a.md) matches the stored POSIX spelling via the same path canonicalization the write paths use, and an unnormalized target in the document draws a warning, so a Windows-spelled rule no longer silently matches nothing.Custody-flow documentation. The unlock flow prints an export-before-wrap nudge (a wrapped store's entries are unreachable to
secret exportuntil the keyfile is unwrapped), and the ingest-manifest documentation states that the schema-3 content-manifest move is one-way for older binaries.Bundle allowlists warn on
*. Importing a bundle whose exec allowlist contains the wildcard emits a named warning - an allow-everything allowlist is almost never what an operator meant - while bundle authentication (AAD) still refuses tampering, and non-wildcard allowlists behave exactly as before.Docs:
docs/cli-reference.mddocuments the hallways verb, the lexicon rebuild, the dead-weight verb and a kind-and-property-pushdown section stating the as-indexed semantics;docs/mcp.mdcovers thekindargument, the promoted-key pushdown and thedead_weightoperation;docs/observability.mddocuments the dead-weight fold; the README names the release.
Fixed
A staged ingest no longer registers itself as done. On a
stageverdict the ingest staged only the summary page while publishing the rest of the write's records: the intake's created entity pages landed straight in the canonical registry and the manifest row recorded the source as ingested current - so review approved half of what had already happened, and an operator rejection left a manifest claiming a done ingest no re-ingest would redo. The intake's disposition contract now covers every page and registration row of the write: created entity pages stage into the review queue beside the summary page (staged first, removed from the canonical tree second, so a crash leaves both copies and never a lost one), and the manifest row stays unrecorded until a publish arm lands. The deny arm still refuses before any write, and the update arm is untouched - the review boundary is entry, not mutation.delete_by_sourceand schema mutations consult the permissions document. A confirmed source cleanup destroyed the derived entries and the ingest manifest entry with no document consult, and a schema batch reshaped the vocabulary every later write validates against, so a caller a document had closed could still do both. Both lanes now resolve the document against the credential subject and refuse on deny through the same discipline as update and append - the same typed refusal, one decision-ledger row, before anything is read for destruction or written; the dry-run preview stays a read and stays ungated, an absent document changes nothing, and the store-integrity doctor'sdeny-consulted-lanesfinding names all the covered lanes. This entry also corrects two stale release-note claims the repairs falsified: v1.79.0's "denied agents cannot update or append to published notes" named the first two consult lanes only (delete_by_source and the schema-mutation lane were uncovered until now), and v1.78.0's research-pool claim ("research names the refusal instead of quietly yielding an empty pool") described a use site that had not been wired to the vault-resolved configuration - what ships here is the ownership measurement behind install and onboarding: it resolves a$secret:reference through the vault's own configuration by metadata alone, never a decrypt, so a raw reference no longer counts as a configured key, an unresolved one is reported unchecked with a reason naming it, and a store the process cannot read (locked, or missing its keyfile) reads as unchecked instead of aborting the statement; the research call site itself is not wired to the vault-resolved configuration yet, so that claim is not yet true on the live research path.
1.79.0 - 2026-10-10
Open Second Brain 1.79.0 gives an operator who lets agents write into a vault the answers the write paths never recorded: who made a given write, what rule allowed or refused it, and how to stop the next one. Agents authenticate as themselves through named per-agent MCP tokens, one operator-edited permissions document resolves allow/ask/deny at a single chokepoint, a queryable decision ledger records which rule decided, every gated write stages into a review queue that recall cannot see until an operator applies it, an owner-write gate keeps callers from publishing under a foreign owner, open questions get a durable artifact of their own, and the ambient extraction lane answers to operator consent and a time-to-live. The default posture is byte-identity: no tokens, no document, no gate keys and no flags means every existing write path behaves exactly as before.
Added
Named per-agent MCP tokens, hash at rest.
o2b mcp token mint|rotate|revoke|listmanages one token per agent in.open-second-brain/secrets/mcp-tokens.jsonbeside the custody store: the store keeps only the sha-256 hash of the material plus a non-secret prefix, verification compares hashes in constant time, the plaintext material is printed exactly once with a shown-once notice and never persisted, writes serialize under the secrets lock, names use themcp_token_<slug>spelling the$secret:grammar can address, and every mint, rotation and revocation appends a no-values custody audit record.Token authentication and request-scoped identity on the HTTP transport. A request whose credential matches a stored token authenticates as that token's agent for that one request - identity threads as a parameter through the dispatch path, never as server state, so concurrent callers with different tokens never observe each other's identity. The token map is consulted before the shared key, the shared
--api-keystays valid as the operator master credential with the process's configured identity, and a revoked or unknown token gets the same generic401body as a missing credential, so no oracle distinguishes them. Rotation and revocation take effect on the next request with no server restart. The newmcp_tokens_requireddevice key (defaultfalse, env twinOPEN_SECOND_BRAIN_MCP_TOKENS_REQUIRED) makes the endpoint refuse credential-less requests while a non-empty token map exists; the non-loopback bind rule accepts a key or a non-empty token map, and stdio identity stays config-derived. A token mints identity only, never reach.One bootstrap command.
o2b bootstrap --target <harness>runs the harness adapter's existing idempotent apply, optionally mints and prints the harness token once (--token, never on argv and never in a harness config), and writes a receipt at<vault>/.open-second-brain/bootstrap.lock.jsonrecording the owned entries, the token name and prefix, and the applied time. A second identical run is a byte-identical no-op,--rotatere-mints under the same name and reprints once with the new material authenticating on the next request,--checkverifies drift, and unsupported targets are refused naming the available list. The config-writing adapters (codex, grok, opencode) apply directly,genericprints the payload plus manual steps, and the plugin runtimes stay verify-only.One permissions document.
Brain/_permissions.yaml- an operator-edited vault file that rides vault sync like the freeze marker - resolvesallow/ask/denyper agent, operator-defined role and target-scoped entry for thewrite,ingestandowner_writeactions. The loader is strict:default_actionis required, unknown keys warn, aversionother than 1 hard-refuses naming the file, and a present-but-unreadable document fails closed with a field-named error instead of falling back to permissive. Resolution order is target entry, then agent override, then role, then default, with deny over ask over allow at equal specificity; when a document exists it is the only disposition source, so one write has exactly one deciding rule. With the file absent every check behaves exactly as today.A queryable decision ledger. Every ask/deny verdict plus the owner-write gate's warn rows lands as one JSONL row in the month/device shards under
Brain/logs/decisions/, carryingactor,via,action,target,verdict, the decidingsource(entry, agent, role, default or gate key), the reason token and optionaltoolandcorrelation_id; allow rows are written only when the document asks for them (ledger.record_allows: true). The append never throws - a failed write returns an audit reason for the caller to surface instead of blocking the verdict it records.o2b brain permissions showprints the effective document plus a dry-run decision table over the declared agents,ledgerreads the filtered rows, ando2b brain doctorreports an unreadable document as thepermissions-unreadablefinding with the show verb as the exit.Staged review for every writer lane. Beyond the existing signals queue, note creates stage at
Brain/pending/notes/<id>.md(the id carrying a reversible percent-encoding of the publish target, round-trip safe over CJK, spaces and dots, over-long targets refused by name) and ingest summary pages stage atBrain/pending/ingest/, while registration completes and cleanup removes the staged page. The signal gate now resolves insidewriteSignal, so every ungated caller - MCP and CLI feedback, inline scan, session import, lifecycle and checkpoint - inherits it without per-tool wiring; batch create ops stage per operation and report receipt statusstagedwith apending_id, and staged receipts on the writer tools carry the next commando2b brain pending apply <pending-id>.o2b brain pending listshows every lane sorted with--lanefiltering and named-unreadable entries, apply is an exclusive create with a typed conflict on an occupied target, and--dry-runruns every check and writes nothing. Toggles are per lane:write_approval.notesandwrite_approval.ingesteach fall back to thewrite_approval.enabledmaster (default off) when absent.Staged documents leave recall.
Brain/pending/is excluded from search-index admission (reasonreview-pending), sobrain_searchand recall-inject cannot surface a document no operator has admitted, while gate-off vaults without a pending directory are unaffected.The owner-write gate.
integrity.owner_scope_writes(off|warn|fail, defaultoff, unreadable config fails closed tofail) refuses a caller-named owner that differs from the caller's resolved identity: on the preference lane at the explicit-owner arm, and on the note lane whereownerjoins the refused frontmatter keys for creates and batch updates underfail. The refusal carries theowner-write-refusedtoken (owner_write_refusedon the write-batch and note-creation error vocabularies) naming both the named and the resolved owner and the fix - name your own identity or omit the owner.warnallows and appends exactly one decision-ledger row carrying the gate key as its source, and a document'sowner_writeverdict composes most-restrictive-wins with the gate in both directions. Gate off, behavior is byte-identical.Open decisions.
o2b brain decision open --title <t> --question <q> --option <o> [--option <o>...] [--context <c>]parks a question with enumerated options atBrain/decisions/open-<slug>.md- a frozen key table, JSON-quoted free text, a## Question/## Options/## Contextbody, a directory lock around transitions, and a typed duplicate refusal naming the existing id.list_openpartitions by status with unreadable records named,show_openreads one back,resolve <id> --choice <c>mints the realtype: decisionpage through the ordinary record path and stamps the open recordresolvedwith a[[decision-<slug>]]pointer plus exactly oneopen-resolvedreceipt, anddiscard <id> --reason <r>closes without deciding. Terminal records stay in place, the morning brief renders at most five open records plus an unreadable block and never mutates them, and thedecision-open,decision-resolvedanddecision-discardedlog kinds keep the lifecycle in the Brain log timeline.Consent and a TTL for ambient capture.
guardrails.ambient_writeback: falsesuppresses the ambient extraction lane with a counted, loggedambient-withheldevent per capture and no signal written (absent keeps today's behavior, since the lane already writes on capture);guardrails.ambient_ttl_daysstampsexpiration_dateat creation on ambient-extracted signals, which the expiration filter then drops at read. A TTL-stamped signal still stages when the review gate is on, and the expiration survives apply verbatim.
Changed
Document-backed dispositions close the
force_confirmedgap. With a permissions document present,force_confirmedwrites a confirmed preference only when the caller'swriteverdict resolves toallow; otherwise the refusal answers with theforce-confirmed-requires-allowtoken and theo2b brain permissions showexit, and document-absent behavior is unchanged. A documentdenyrefuses the write before any filesystem effect with the typedwrite-refusederror naming the principal, the action, the deciding rule and the next command, the lane keys cannot bypass a document in either direction, and each stage or refuse disposition appends exactly one ledger row naming the rule that decided.Docs:
docs/cli-reference.mddocumentso2b bootstrap,o2b mcp token,o2b brain permissions, the widenedo2b brain pendingand the open-decision actions;docs/mcp.mddocuments the per-agent token authentication, the new refusal tokens with their next commands and the open-decision actions onbrain_decision(no new MCP tool - the advertised surface is unchanged);docs/observability.mdgains the decision-ledger section and the new Brain log kinds; the README control section states every new gate with its config key and default-off posture.
Fixed
The passphrase-wrapped store is reachable again. Every key-bearing verb (
set,rm,run,export,import) ingests the passphrase through--passphrase-from-envor stdin exactly asunlockdoes; the MCP server unlocks once fromOPEN_SECOND_BRAIN_SECRETS_PASSPHRASEat its first key use and drops the variable;o2b brain secret unwraprestores the raw keyfile; and the wrap path verifies the envelope against the key and writes it durably before replacing the only copy, and a failed post-write restore reports the original read-back refusal beside a named warning instead of the restore's own error.Credential bundles authenticate their metadata. Each entry's value is sealed against its name, env mapping, allow patterns, the bundle version and the KDF parameters, so an edited or swapped bundle refuses on import before any write; the bundle schema moves to 2 and older bundles refuse naming the re-export remedy.
Approval digests bind the content that lands. The import plan seals each row's source and rendered-body hashes, so a file edited after the dry run fails the apply;
brain upgrade --applyaccepts--approval-digest, required when non-interactive and verified against the freshly computed plan.The ingest manifest carries the extraction contract per entry. Ingesting one source no longer cancels the owed reprocess of the others; a manifest without per-entry contracts degrades toward reprocessing, never toward skipping.
Tags accept the Obsidian grammar. Unicode letters and digit-led tags (
2fa,2024-notes) pass; only bare numbers refuse; legacy scopes carrying such tags stop breaking dream rewrites and preference writes.A refused capture no longer crash-loops the Telegram daemon. Contract refusals are recorded per update, the offset advances, and the run continues with the messages behind the refusal.
Session-summary divergence is a signal again. Records dedupe by content hash on read,
divergentfires only when distinct hashes coexist at one instant, and the revision count is reported separately.Secret references resolve the names the store holds. Hyphenated and mixed-case names resolve through the store leg while the env fallback keeps its case rules, and redaction covers env-resolved values without blanking error text on one- or two-character values.
The HTTP transport survives a corrupt token store. A store read error falls through to the shared-key compare with a named warning, while token enforcement that was requested by config stays on (an unreadable map refuses credential-less requests rather than reading as empty); a presented credential matching nothing is refused outright rather than read as anonymous; and revoking the last token on a keyless network bind no longer re-opens anonymous access.
Request identity reaches every write decision. A token caller's writes are judged as the token's agent: document deny entries refuse them, cross-owner claims are checked against the credential identity, a caller-supplied agent that differs from the token refuses by name, and the decision ledger names the true actor. A run launched with an explicit config path is consulted as the agent that config declares even when no transport identity is present, so a document rule naming that agent refuses it and the ledger row names it. The signal lane consults the permissions document like every other writer, denied agents cannot update or append to published notes, and the review door appends one resolution row per apply and reject.
Staging the same target twice refuses by name instead of silently replacing the earlier entry's bytes - and the retried
brain_create_note,brain_write_batchcreate op orbrain_ingest_sourceanswers thepending-stage-conflictcode carrying the queue entry that holds the target, not a generic internal error - and a failed decision-ledger append surfaces its audit reason instead of vanishing.Open decisions stay resolvable. Same-titled questions resolve into distinct decision pages even when the title's slug fills the 64-character cap - the per-record fallback candidates keep their distinguishing id inside the cap, so resolving one never spins forever against a foreign page at that slug - a resolved or discarded question can be parked again with the same wording, adversarial headings in question text round-trip byte-faithfully, callers below the record's reach cannot resolve or discard what they cannot list, and the CLI help says non-Latin titles hash to an unnamed id.
Bootstrap tells the truth about tokens. The minted token authenticates hand-configured HTTP clients while the registered harness keeps its config-derived identity (stated in the output and the docs); a failure anywhere in the registration half - the payload build refusing, the apply throwing - no longer loses the minted material; placeholder agent names refuse at mint; rotation keeps a ten-minute grace window for the previous material; and
o2b bootstrap --removetears a provisioning down through the receipt.The doctor sees a document that denies the locally configured agent, and sessions import reports the ambient-withheld count when consent suppresses capture.
Expired transient facts cannot consolidate. The dream topic and promotion planning readers drop signals past their ambient time-to-live before clustering, matching the recall path.
1.78.0 - 2026-10-09
Open Second Brain now puts credential custody under an operator passphrase and routes it by name: the vault's secrets keyfile wraps into an opt-in scrypt envelope behind a lock/unlock lifecycle, $secret:NAME references resolve through the custody store at every credential use site, one passphrase-encrypted bundle moves the store between installs, and every resolved credential literal is redacted at the error and config-mapping boundaries. The vault's own declarations bind its writers - composed tags must parse as Obsidian tags and a declared page vocabulary gates capture writes - and the plans an operator approves are sealed: a Claude-memory import or a brain upgrade applies exactly the approved plan or nothing. Reads name divergent session summaries, the model-answer verbs strip and name a leading think block before parsing, and a changed extraction contract reprocesses the sources it covers instead of absorbing the change as unchanged.
Added
A passphrase-wrapped keyfile with a lock/unlock lifecycle.
o2b brain secret unlockwraps the store's raw 32-byte keyfile in place under a passphrase - read from stdin or--passphrase-from-env, never argv - into a versioned scrypt envelope (per-envelope random salt, parameters bounded to the build's cost curve) and holds the unwrapped key in memory for that one process only;lockclears the holder and refuses a store that was never wrapped by name, instead of recording a protection that does not exist. There is no daemon: every other invocation and the MCP server stay locked, a key-bearing operation over a locked store refuses with a named error whose prose carries no paths and whose remedy is the unlock command, and a store that still holds entries but has lost its keyfile refuses the read-only resolve by name instead of minting a fresh key over the surviving ciphertext. Both lifecycle verbs land no-values custody records and sit behind vault identity, and the help and manifest state the loss warning: a lost passphrase is unrecoverable, and every stored value stays unreadable forever.Passphrase-encrypted credential bundles.
o2b brain secret export --out FILEre-encrypts every entry under a key the passphrase derives through the envelope KDF (fresh salt, the parameters recorded inside the bundle) and writes one file an operator can carry between installs - names, env-var mappings and allow patterns travel in the clear past the shared egress redactor, the values do not.secret import FILEverifies and decrypts every value before writing anything, reuses the store's own name, env-var and allow-pattern validation, and refuses a name the store already holds unless--replace; both operations land no-values custody records and run behind vault identity.$secret:NAMEreferences resolve at the use sites. The custody-store-backed named-secret provider answers credential consumers store-first with environment fallback - byte-identical when no reference is used, opt-in via the vault that joins the store: the embedding registry's provider probes, the decision-model key from config resolution through the provider factory (extract pre-filter, labels, recall-inject, skills, rerank), the research pool env, the Telegram capture token and the installation secret behind thevault_pathMCP field. A locked store or an unresolvable reference surfaces the named refusal - the decision-model key degrades into errors with no request leaving, the Telegram run fails naming the secret instead of handing the raw reference to the API, research names the refusal instead of quietly yielding an empty pool - and resolution stays read-only: nolast_used_atstamp, no exec custody record, no store mutation.o2b secrets listandstatusaccept--vault <path>and report availability from the merged provider, metadata only, a locked store still counting.Resolved credential literals are redacted at the guarded boundaries. The MCP error boundary scrubs every occurrence of a value the vault resolved from a reference - longest value first, ahead of the truncation guard, so no cut leaves a partial credential - and the OpenClaw status tool's config mapping takes the same values into its scan; a boundary whose store is absent, empty, unreadable or locked contributes nothing and answers byte-identically to the shape-only redactor.
Digest-sealed approval for import and upgrade plans.
o2b brain import-claude-memory --dry-runseals the adoption plan - plans, skipped entries, conflicts, unchanged rows, no wall-clock fields - and prints its digest; a non-interactive--applymust carry--approval-digest(exit 2 otherwise, mirroring the--yesguard, while an interactive terminal keeps the escape hatch) and lands exactly the approved plan or nothing: a stale digest refuses naming approved and current digests and stating that nothing was written, and a digest paired with--dry-runrefuses, because a dry run computes one and does not consume one.o2b brain upgradeseals its plan the same way and verifies the seal first at apply, so a plan object edited after it was measured is refused before the error rows, the no-op return or the snapshot; the CLI and the self-heal worker, which apply the plan they just planned, are unchanged.Divergent session summaries are named on reads. The single-session read (CLI and MCP) answers the latest digest plus
digest_count, and when one session id holds several content-differing digests reportsdivergent: truebeside a bounded newest-first-count, oldest-first sample ofid/created_at/content_hashrows - never a payload echo. Divergence is reported, never merged, deleted or reconciled.A named think-block strip before CLI JSON parses. The four model-answer verbs (extract-signals, design-note, skill-proposals page-draft, distill) parse plainly first and, on failure, strip one leading
<think>...</think>block and retry; every strip is named on the result - a note on the ok envelope under--json, a note line in text - and a payload that still refuses keeps the named exit-1 refusal with the attempt named. Clean JSON is byte-identical and genuinely malformed JSON is never rescued.The extraction contract fingerprint rides in the source-ingest manifest. The manifest records the fingerprint of the extraction contract (the schema-extractable allowlist plus the hand-bumped contract version), schema 2 with a v1 read path, and batch planning compares it against the live one: on a mismatch - a v1 manifest with no recorded contract included - byte-identical sources reprocess once under the distinct contract-changed status (
contract_changedandcontract_changed_fileson the wire, an info line in text) instead of being absorbed as unchanged, reconcile names those sources as the gap, and the fingerprint folds into the plan id so a stale checkpoint cannot resume-skip work extracted under a different contract.
Changed
Composed tags must parse as Obsidian tags. Every tag the core composes into frontmatter - a preference's topic, scope and extra tags, a signal's topic and scope, the Claude-memory import's derived topic and scope - is validated against the one rule the search index and the inline detector already read (starts with a letter or underscore, continues with letters, digits, dashes, underscores or slashes, never purely numeric), and a segment that fails it refuses the write with the offending field and the rule named. Existing content cannot be refused retroactively, so an opt-in
frontmatter-tagshygiene detector reports what the write-time gate would have rejected - one finding class, grouped per distinct value, informational, never auto-fixed.A declared page vocabulary gates capture writes. A vault that declares
schema.page_typesinBrain/_brain.yamlrefuses a capture whose kind it does not declare - staged captures, every inbox-drain route and the Telegram path - with a typed error naming the kind, the declared set and the remedy, before any filesystem effect: no directory is created and no allocator name is consumed. The idea route consults the gate at plan time, so a dry run previews the same refusal apply would land, and a vault with no declaration behaves byte-identically to before.Docs:
docs/cli-reference.mddocuments the lock/unlock/export/import verbs, the--vaultjoin ono2b secrets list/status,--approval-digeston the import and the contract-changed batch-plan report;docs/how-it-works.mdstates the passphrase envelope, the references and the bundles under the custody model; the README names the release and the custody capability. The plugin mirrors follow.
Fixed
Same-instant continuity records read in append order. The store tie-broke rows sharing a timestamp by content-hash id, which says nothing about which of two same-instant writes landed first, so a latest-wins reader - the session-summary read among them - could answer the older take arbitrarily. Within one shard the append-only file is now the authority; ties across shards still order by shard id, because two devices' arrival orders genuinely cannot be known.
1.77.0 - 2026-10-08
Open Second Brain now keeps its claim ledger in time and correctable: a claim can carry a validity window frozen from its source at ingest, the events recall reads the ledger over an assertion-time slice, an agent's stated claims commit only when they anchor in their assertion text, and two same-slot claims with disjoint windows stand in a succession channel instead of a conflict. One correct verb discovers a correction's blast radius and retires what it touches, and every serving surface serves a retired-but-serveable record only beside its resolved, readable chain-tip correction, fail-closed. The deep relational recall arm runs under width budgets with hub skipping, walks entity co-occurrence bridges, reports per-node path provenance gated at the caller's reach, and never lets a traversal-only row outrank what the lanes matched.
Added
Validity windows on the claim ledger. A claim event can carry optional
valid_from/valid_until(half-open[valid_from, valid_until), a bare ISO date or a canonical UTC timestamp; bare dates day-snap, relative phrases never parse, and a windowless line serializes byte-identically to before, so an old binary reads new lines on the assertion-time axis it knows).o2b brain truth ingestand thebrain_truthingestoperation accept the window (--valid-from/--valid-until; a malformed or inverted bound is refused by name, asINVALID_PARAMSon MCP and exit 2 on the CLI), and with the flags absent each open bound is resolved from the source record's frontmattervalid_from/valid_untiland frozen verbatim onto the event at ingest - explicit input wins outright, resolution is per bound, an unreadable or windowless source stores a windowless event, a malformed source bound resolves as absent so it can never refuse an ingest, and mtime is never consulted (an assertion-time proxy is not validity).Windowed truth events recall. The
brain_trutheventsoperation ando2b brain truth eventsrecall a slice of the ledger through one shared report, so shape, ascending-tsordering, gating and paging cannot drift between the surfaces.entityfilters by canonical name (display spellings accepted),since/untilbound the assertion time through the shared time-range grammar (a claim's validity window is a different axis and rides on the rows verbatim;sincefloors into its second so tail-following pagination re-sees a boundary event but never skips one), and paging defaults to 200 rows under a hard 1,000-row cap. Every row passes the same owner/reach gatebrain_claimsasks.Grounded agent-stated claims. The
brain_truthstateoperation ando2b brain truth statecommit an agent's stated claims -subject/relation/objectplus the assertiontextthey came from - into the ledger. The payload boundary refuses a whole call (unknown relation, missing text or source, malformed ts, empty agent) before anything is written, and the anchoring verdict is per claim: subject and object must both anchor in the text through the quality-gated match-form kernel, so a grounded claim commits as anagent_statedledger event while an ungrounded one is reported back with machine reason codes. The extractor tag is annotation only - conflict detection reads the ledger exactly as before.A succession channel beside conflicts. Two same-slot claims with present, non-intersecting validity windows classify as successions (the predecessor is the representative claim whose window closes first) and never as conflicts; intersecting windows or any windowless claim keeps the assertion-time contest rule verbatim, and an expired window never suppresses contestation on its own. The channel is a separate, presence-gated field on the truth state - never a conflict kind - so
ask_usersemantics, hygiene findings and conflict priority are structurally unreachable from it, and windowless output stays byte-identical.One correct verb.
brain_lifecyclegains thecorrectaction ando2b brain lifecycle correct <target>the matching subcommand. The dry run - the default - discovers the blast radius within the caller's reach and writes nothing: the claim-graph closure over replaced and contesting records, a wikilink mention scan, and the same-entity/aspect ledger claims, each row reach-gated by its source record.--applyretires each target through the end-state policy - validity closes at the explicit--window-endwhen the correction is time-scoped, else at the correction instant, while--flatly-wrongtombstones instead, for a claim that was never true - appends ledger correction events under the corrected record's own source (a same-source value change reads as self-correction in the fold, never a conflict), retargets mentions with per-write failures carried (the log and the receipt lines are testimony and never rewritten), and issues bundle-correlated receipts (correction_bundle:<id>inevidence_triggers, the existingsupersede/tombstonereason codes). Replay converges: an already-applied correction is a no-op withappended: false. A target the caller may not read is refused as missing before anything is written.Width budgets for deep relational recall. The relational arm's traversal runs under width budgets - 8 seeds, 4 expansions per node, 16 nodes in total, and a node whose walked-edge degree exceeds 12 is reached but not expanded - and abandons the frontier deterministically once the composite recall deadline fires, keeping every node already reached. Each budget is a named constant overridable through an
OPEN_SECOND_BRAIN_SEARCH_TRAVERSAL_*environment variable or itssearch_traversal_*config key, and an invalid value is refused naming the key in force.Entity co-occurrence bridges. Two documents that mention the same normalized entity gain one bridge edge over the index's
chunk_entities, so the walk follows co-occurrence beside the declared typed edges (labelledentity, machine-derived, never part of the query edge-type vocabulary). On by default, off throughOPEN_SECOND_BRAIN_SEARCH_ENTITY_BRIDGESorsearch_entity_bridges_enabled; the budgets, the transport reach and the bridges are wired through to the production search call site, byte-identical with the arm disabled.Per-node path provenance in the retrieval trail. A relational row's trail entry carries the ordered path of documents that reached it, each node gated at the caller's reach and owner scope, so the trail names only pages the caller can read and the reason string renders the walk shape alone.
Relational-only rows stay below the organic tail. A row the traversal found and no lane matched is admitted below the organic tail in the fused order, and a structural rung makes it impossible for a relational-only row to outrank a deep keyword direct hit: the traversal widens the pool, it never outranks what the lanes matched. With the arm off, the rung never fires.
Changed
A retired record is served only beside its correction. A validity-closed predecessor stays serveable inside its window and a superseded non-tip stays readable; only a tombstoned record drops out of recall everywhere. Every surface that serves memory - the search pool after ranking, recall by topic, context packs, the
Brain/active.mddigest and the dream scan - applies one rule to a serveable retired row: it is served only beside its chain-tip correction, resolved and readable at the caller's reach, and dropped otherwise. The drop is fail-closed: a chain walk that dangles, cycles, stops at the depth cap or answers the row itself is unresolved, and a correction pulled in beside its predecessor re-runs the caller's full filter set - status, visibility scope, agent scope, reach, and the property, degree and composite filters - because a successor the caller could not read on its own must not enter through the correction door. A pulled-in correction enters as a link-type row carrying a share of its predecessor's score, provenance-stampedcorrection_for:. Records with no pointer, and pools with neither pull-in nor retired row, are byte-identical to before.Docs: the README gains the claim-ledger and corrections capability bullets and the deep-relational-recall note with the budget keys and defaults, and
docs/architecture.mdstates the serve-with-correction rule and the agent-scope matrix's new recipe row (234 recipes over 101 tools). The plugin mirrors follow.
Fixed
Withheld rows no longer surface as counts. The events account covers only the rows that passed the gate (
totalis the gated count and no withheld key exists beside it), the relational path provenance carries the readable document ids alone, and its reason string renders the walk shape only, so a filtered answer is byte-identical to the answer over a vault that never held the withheld rows.A source page the caller cannot read answers exactly like an absent one. The truth
ingestandstatewindow resolution now runs through the caller's readable predicate: an unreadable source stores a windowless claim with no validity keys and a byte-identical response. The CLI verb runs at operator reach and resolves ungated.Extensionless page references resolve only to files that exist. The .md-twin resolution no longer answers with the twin when neither candidate exists and no longer accepts a directory as a page, so a slug convention in a log payload no longer drops the rows carrying it and the
Brain/inboxdirectory no longer resolves as a page; in the events gate, a slash-bearing extensionless spelling resolves through the same first-existing rule, so both spellings of one page answer as one.Slot claims gate on the extension-bearing source path. The correct verb's claims gate was asked over a fenced or extensionless path spelling it could never answer readable, so in-reach claims dropped from a remote caller's blast radius; the gate now hears the same path shape as the sibling callers, and a source naming no page under its literal spelling stays withheld.
The correct verb validates its window end before writing. A malformed
--window-endis refused with the same check the ledger append boundary applies - a bare ISO date or a canonical UTC instant - before anything is written, including the frontmatter-only correction that appends no ledger event.The coupling tip is probed through the caller's full filter set. With the opt-in property, degree or composite-scope filters active, a correction could pass the coupling verdict and then fall to the pool's re-filter, leaving its predecessor served bare; the tip candidate is now probed through the same pipeline the re-filter runs, and a tip the probe drops counts as unresolved, so the predecessor drops with it.
A self-pointing chain walk is unresolved. A blocked or stale
superseded_byindex edge left the post-rank walk no first hop and answered the start page as its own chain tip, the one corner where the serve-with-correction coupling failed open; a walk tip that is not distinct from the start page now classifies as unresolved and the retired row drops.Malformed validity windows answer as invalid params. A malformed or inverted
valid_from/valid_untilanswersINVALID_PARAMSon MCP and exit 2 on the CLI like every sibling mistyped input, and a stated-claims payload with a malformed ts or an empty agent is refused whole before the first write.
1.76.0 - 2026-10-08
Open Second Brain now keeps its search index current without a scheduler: a search or a session start that finds the index more than a minute old answers from it as it is and starts one low-priority background incremental run, so the next read sees the current vault. Nothing stays resident and nothing runs while no agent works. Two indexer changes make those frequent runs cheap: a run that changed no document writes almost nothing, and a changed document rewrites only the chunks that changed, so an edit to a 1 MB daily log costs about 0.2 MB of writes instead of about 9 MB.
Added
Freshen on read.
search()(every reading MCP tool, the recall-inject hook,o2b search query) andensureVaultCurrent(SessionStart and MCP start) start a detachedo2b search index --freshen <token>whenlast_indexed_atis older thansearch_freshen_interval_s(default60,0turns it off, envOPEN_SECOND_BRAIN_SEARCH_FRESHEN_INTERVAL_S). The reader never waits for the run. An exclusive claim next to the index (freshen.claim) keeps concurrent readers from starting a run each; a dead run's claim is taken over after ten minutes by exactly one reader, under an exclusive takeover lock. A held writer lock or an index directory that refuses the claim (read-only mount, no permission) skips the spawn, a run that still meets another writer ends as a skip rather than a failure, and the child is told where its state lives (--freshen-state), so even a failure before it resolves its config is recorded and releases the claim; and a read-only open (cross-vault, recall sources) never starts one. The child lowers its CPU priority and runs underionice -c3on Linux ortaskpolicy -bon macOS when the tool exists. It is keyword-only unlesssearch_freshen_embeddingsis on.Backoff and visibility. The child records its outcome in the per-device
freshen-state.jsonbeside the index (a new state surface that followsOPEN_SECOND_BRAIN_SEARCH_DB/search_db_path, soo2b state statusando2b state migratereport and leave it where a relocated index keeps it); a failed run backs off one minute, doubling to an hour.o2b search statusprintsfreshen,index_age,last_freshenand an activefreshen_backoff: until <time>(freshenobject under--json);o2b doctorwarns with the new codefreshen-failingafter three failures in a row; runs that changed documents or failed append one row to the newindex_freshenmetrics surface.Trail code
index-stale. A search over an index more than ten minutes old names it on the retrieval trail withdetail.ageSeconds. The code is added at read time, also to a query-cache hit, and never stored in the cache, so a cached answer reports the index age it is served at and an operator who reindexes on a schedule keeps the cache.
Changed
A no-change index run skips link and alias resolution. Both are a pure function of the documents and their links; rewriting every link row on every run was the only write a no-change run made. The relation-constraint and tier passes still run, so a schema-pack edit alone still takes effect.
IndexStats.linkResolutionSkippedreports it. A run killed between its document writes and link resolution leaves a pending marker (link_resolution_pending), so the next run resolves even if it changes nothing itself.Re-indexing a changed document keeps its unchanged chunks in place. A chunk at the same position with the same content hash, lines, token count, heading and stored full-text form keeps its row, id, full-text entry, entities and vector; only changed chunks are deleted and inserted, and moved ones still carry their vectors. Measured on a 5,812-note vault: one edit to a 1 MB daily log went from 9.3 MB written and 2.1 s to 0.2 MB and 0.17 s; 200 edited notes from 57 MB and 1.8 s to 28 MB and 0.58 s.
The doctor's repair plan sees the index.
o2b brain doctor --repairandbrain_doctorwithrepairnow hand the doctor the resolved index path, sofreshen-failingandtier-driftare listed as not auto-repairable, as the report already listed them.tier-driftis counted over the pages the caller may read, so below local reach a drifted page the caller cannot read moves the count no more than an absent one, in the report and the repair plan alike.Docs:
docs/cli-reference.mdgains "Freshen on read", the two keys, the--freshenflag, the status fields and theindex-stalerow;docs/how-it-works.md"Keeping the index current";docs/architecture.mdstates that background work starts only from agent activity;docs/hermes-cron.mdanddocs/updating.mdsay a scheduled reindex is now only needed for embeddings;docs/metrics.mdlistsindex_freshen. The README names this release.
1.75.0 - 2026-10-07
Open Second Brain now reaches the two moments the session-start injection never could: the delegated subagent that performs the writes, and the end of the turn. A carrier hands the operator's context to every delegated subagent once, on the SubagentStart event, before the subagent's first prompt, with the write-shaped PostToolUse registration kept as the fallback for runtimes without that event; an opt-in Stop-hook digest surfaces pending Brain hygiene findings as one line, once per change, on the runtimes whose Stop channel is non-blocking and stays silent elsewhere; and an injection body over its budget degrades its sections to headline tiers - the top three ranked bullets each - least important section first, until the body fits, before it drops a whole section.
Added
The operator's context reaches delegated subagents. A carrier (
hooks/subagent-inject.ts) delivers the operator's context payload into a delegated subagent's turn. The primary channel is the SubagentStart event, which fires when the host spawns the sub-agent: the payload is in place before the subagent's first prompt, and the host re-injects it if the subagent compacts. Runtimes without that event keep the fallback registration on the write-shaped PostToolUse matcher (Write|Edit|MultiEdit|apply_patch), which delivers at the subagent's first write-shaped tool call. Both registrations feed one delivery ledger (osb.subagent_inject.delivered, the most recent 200 agent ids, 24 h TTL), recorded after the stdout write in the lose-not-duplicate direction, so a host with both events delivers exactly once and a crash between write and record re-delivers rather than loses. The payload is the session-start composition, not the standing rules alone: the standing-rules block, the scoped operator rules for this project and device, and the budgetedBrain/active.md+Brain/lessons.mddigest under the sameactive.inject_budget_chars- assembled by the sharedhooks/lib/active-context.ts(extracted from the session-start lane), so the two surfaces cannot drift. The main thread gets nothing, and the carrier stays silent only when the entire payload is empty; a failed rules read still delivers the explicit failure block naming the path, so a subagent never mistakes "no rules were written" for "the rules could not be read". Every runtime receives thehookSpecificOutput.additionalContextenvelope, withhookEventNamenaming the event the payload rode in on (SubagentStartorPostToolUse);decision: "block"is never a context channel on these events. The block renderer is extracted tohooks/lib/standing-block.ts, so the session-start lane and the carrier render the operator's rules through exactly one code path with one cap policy.An end-of-turn hygiene digest, opt-in. With
hygiene_digest_enabledin the machine config (envOPEN_SECOND_BRAIN_HYGIENE_DIGEST_ENABLED, default off), the Stop hook runs after a turn that wrote an artifact and folds the default hygiene sweep plus the index-backed dangling-link count into ONE line: warning- and action-severity findings as per-detector counts (informational findings never emit), under a hard 300-character cap that degrades by dropping whole count segments. The line is counts-only by design - it names no targets; the detail lives behind the pointer, which names both actionable surfaces honestly:o2b brain hygiene scanlists the detector findings, while the dangling-link count is measured from the search index and reported only by the MCPbrain_hygienetool, a number the scan never prints. A hash ledger beside the hook-state directory (<vault>/.open-second-brain/hygiene-digest.hash, one overwrite-only SHA-256) keeps an unchanged finding set silent, so the line lands once per change, not once per turn, and re-arms in a new session; zero eligible findings and flag-off installs emit nothing. The flag resolves before the vault, so the per-turn cost when off is one config read. The line is delivered only where the runtime has a non-blocking Stop channel - Claude Code'shookSpecificOutput.additionalContext; on runtimes where a Stop block would force a continuation turn (Codex, Grok Build, unrecognised) the hook exits silently before any transcript work, because an opt-in digest never buys a continuation.Oversized sections degrade to headlines before they drop. When the injected body exceeds
active.inject_budget_chars, the shared budget core compacts sections below the keep-guard priority to a headline tier - its heading, its lead-in lines and its top three bullets, ranked by the inline application/confidence tags, with lines that carry no parseable tags kept in render order. The tier runs sequentially in the drop order, least important section first and the same selection the drop pass makes, recomputing the total after each compaction and stopping as soon as the body fits; only when no tierable section remains does the tiered body go to the drop pass. The keep-guard sections (the preamble and the confirmed rules) are exempt from tiering and still tail-trim as the ladder's last resort. The one-line budget notice names the sections reduced to headlines and those dropped, names as tiered only the sections the tier actually left behind, and quotes the original body size in its kept-of pair, so a body tiered before dropping still reports how much of what the vault holds survived. The doctor pressure probe mirrors the step through the same shared code, exemptions and top-N:tierFiresmarks the exact overflow condition under which the reactive tier runs, each eviction candidate carriestieredBytes, the byte footprint the tier would first degrade it to, and the warning says when headlines cannot help - every non-guard section already at or below the top-N, or keep-guard content alone over the budget - naming the drop-or-trim outcome instead of promising a reduction that cannot happen.
Changed
Grok installs register both new hooks. The grok asset registry adds
subagent-injectto the write-shaped PostToolUse matcher - grok has no SubagentStart event, so the fallback registration is its carrier, and the parity suite declares the divergence - andhygiene-digestto the Stop group, where the registration stands but the digest emits on Claude Code only.Docs:
docs/how-it-works.mdgains the subagent carrier and the hygiene digest and rewrites the budget paragraph around the headline tier,hooks/README.mdgains rows for the new SubagentStart hook, the PostToolUse fallback carrier and the Stop digest,docs/cli-reference.mdthehygiene_digest_enabledflag with its Claude Code-only emission, anddocs/mcp.mdnotes thatbrain_hygiene's pull surface doubles as a push channel when the digest is on.
1.74.1 - 2026-10-07
The bundled opencode plugin loads and works on OpenCode V2 hosts from the same installed file that already served V1. Contributed by @jkoelker in #190, with the maintainer-side completion of the V2 event names against the released 2.0.24 build.
Fixed
The opencode plugin supports the V1 and V2 plugin APIs from one file. The module now default-exports
{ id, server, setup }: OpenCode V1 keeps callingserver()exactly as before, and OpenCode V2 loads the same installed copy throughsetup(), which feature-detects every capability it needs (location.directory,session.hook/get/context,tool.hook,event.subscribe) and does nothing when one is missing, so hosts without a hook stay unaffected. Under V2, active-context injection rides the sessioncontexthook, the post-write reminder ridestool.hook("execute.after"), and session capture subscribes to the event stream and merges each snapshot with the earlier turns so history survives compactions; a committed revert removes the saved boundary and the turns after it, rebuilding from active context when the boundary was never captured. Verified live against opencode 2.0.24: plugin load, context hook, capture spool and post-write reminder.V2 capture listens on the turn-end events a released opencode emits. opencode 2.0.24 never fires
session.idle; a finished turn emitssession.execution.succeeded(failed and interrupted runs likewise). The capture set now covers those names alongside the v2-tree names, plussession.compaction.failed, so a turn that ends or fails on a released V2 host still lands its spool snapshot.
1.74.0 - 2026-10-07
Open Second Brain now defends against near-duplicate memory at the points a fact enters or leaves the vault: one reach-aware near-duplicate kernel scores the note-write receipt hint, the feedback advisory and a new retire-sibling list, so a context-driven retire names the active paraphrases that would otherwise survive it; the receipt hint can look beyond the written page's directory through the keyword index; every receipt lint finding answers at the caller's reach; extract-signals grounds time bounds in each turn's timestamp and refuses a payload that repeats a topic; and route metrics time each write stage without carrying any content.
Added
A reach-aware near-duplicate kernel. One pure lookup (
src/core/brain/near-duplicate.ts) scores the write-receipt hint, thebrain_feedbackwrite-conflict advisory and the retire siblings over one named threshold table. Itsreadablepredicate is required and runs before anything else, so a page the caller may not read is neither scored nor counted.Retire siblings. A context-driven retire (
superseded-by-context,rebutted,quarantine-violated,user-rejected) now nominates the active preferences that resemble it, at 0.7 Jaccard or above on the principle.brain_review_candidatesreturns them as an additiveretire_siblingsarray of{ retiring_id, sibling_id, score, method }pairs, the dream run summary carries the same key, ando2b brain rejectlists the siblings of the rejected preference with the command that rejects each (--jsonaddsretire_siblings). The list is advisory and accept-gated: nothing is retired by it, and each pair is accepted throughbrain_apply_evidencewithresult: outdatedoro2b brain reject. Decay retires, merges and retires the confirmed-evidence gate holds back nominate nothing, and a pair is kept only when the caller may read both preferences.A stored-vector tier for retire siblings. With a search config,
brain_review_candidatesalso compares the stored content vectors of the retiring and sibling preferences and adds pairs at 0.92 cosine or above asmethod: "embedding", with zero embedding spend.retire_siblings_semanticreports the tier's outcome (used,index_missing,vec_unavailable,not_embedded,model_mismatch), and the tier probes only the retires the answer keeps.near_duplicate_retire_siblings_enabled. The key (envOPEN_SECOND_BRAIN_NEAR_DUPLICATE_RETIRE_SIBLINGS_ENABLED, default off) gates every retire-sibling surface; off, the dream summary,brain_review_candidatesando2b brain rejectare unchanged.A wider near-duplicate receipt hint. With
near_duplicate_write_widening_enabled(envOPEN_SECOND_BRAIN_NEAR_DUPLICATE_WRITE_WIDENING_ENABLED, default off), the advisory near-duplicate hint on note-write receipts also compares the written pages with candidates from other directories, pulled from the keyword index (the top 20 chunks per written page) and re-read from disk, at no embedding spend. An index that cannot be opened or queried never fails the write: the lint report carrieswidening: "index_unavailable"and awidening_detailcode naming the failure, and the same-directory hint still runs.Turn timestamps in extract-signals. Every
turns_minedentry carries the turn's storedtimestamp(""when the source has none), and each prompt line names it. The envelope asks for a time bound as an ISO 8601 date or interval resolved against the stating turn's timestamp, in language-neutral terms, and the dream pass turns that bound into the preference'svalid_from/valid_until.Write-stage timings on route metrics.
mcp_route_latencyrecords gain an optionalstagesarray of{ name, ms }entries from a closed allowlist of nine stage names (validate,idempotency_lookup,near_duplicate_lookup,document_write,idempotency_remember,log_append,preference_write,write_receipt,lint); any other name and any negative or non-finite value is dropped, so no content reaches the record. Stages appear only in the continuity record, never in a tool response, andbrain_route_metricssummaryrolls them up per route (count, average and p95 per stage).
Changed
Wave playbooks. The feature-release playbook (1.4.0) records the picked cards for the wave's reviewers and merges the green pull request itself, squash and verified head, under the operator's 2026-10-06 merge authorization; the pr-prepare playbook (1.2.0) handles the bot review while CI runs, waits out its bot budget with one final CI watch, and reaches ready on a green gate when the bot review pushed nothing.
Extract payloads that repeat a topic are refused. Two items sharing a
topicrefuse the whole payload, naming both items, and nothing is written.Docs:
docs/mcp.mdgains the retire siblings, the receipt widening and reach, theturns_minedtimestamps and the write stages,docs/observability.mdthestagesfield, its allowlist and the routes that emit it, anddocs/cli-reference.mdthe extract-signals timestamps and topic refusal; theo2b brain rejecthelp names the retire siblings.
Fixed
Every receipt lint finding answers at the caller's reach. Note-write receipts used to apply the caller's reach to the near-duplicate check only; the broken-wikilink and merged-link checks now treat a withheld page as absent too, so a wikilink to it reads as broken and a merge chain ends at its first withheld hop.
1.73.1 - 2026-10-07
Sturdier input handling in three places, and a faster CI.
Fixed
The HTTP transport parses the
Authorizationheader in linear time. The bearer pattern no longer lets two groups claim the same run of whitespace, so a long run of spaces cannot make the parse backtrack. Any whitespace before the key is still accepted, and a header with nothing afterBeareris still refused.Shell completions escape backslashes. The fish, elvish and nushell scripts from
o2b completionsescape a backslash before quoting a word, so a word that ends in one can no longer swallow its closing quote.The brain explorer escapes every field it renders. The node status and confidence in the details panel go through the same HTML escaping as every other field, and that escaping now covers quotes, since the status also lands in an attribute. Both values are already limited to known words by the parser, so this changes nothing a vault can produce today.
Changed
CI: the Linux suite runs in parallel workers, and the Windows suite in three shards on separate runners balanced by recorded timings (about 11 minutes instead of 25). Windows is skipped only for pull requests that change nothing but prose, and the release workflow reuses a green CI result on the tagged commit instead of running the suites again. The Linux jobs run on Ubuntu 26.04.
1.73.0 - 2026-10-06
Open Second Brain now makes every query embed honest and every upgrade safe: one query-embed gateway applies the caller's reach and the embedding price gate before any provider is called, for the search lane behind six MCP tools and two hooks and for the brain_context_pack semantic belief order, and fits the query, instruction prefix included, to the model's input window, disclosing a refusal or a cut by trail code; an operator can declare the input window of an uncurated model and send extra request fields to an OpenAI-compatible endpoint; recall feedback, benchmark, eval and tune reports name the degradations their searches met; and o2b brain upgrade and the automatic upgrade worker apply exactly the plan they computed, refusing a managed file edited in between instead of overwriting it.
Added
One gate for every query embed. The search lane (reached by
o2b search,brain_search,brain_recall_feedback,brain_file_context,brain_eval,brain_benchmark,brain_tuneand the recall-inject and gap-promote hooks) and thebrain_context_packsemantic belief order now decide through one gateway whether, and with what text, a query is embedded. Under a positiveembedding_cost_gate_usd, a caller that is not local is refused the query embed of a model with no known price before any provider is called. A search that asked for the semantic lane by name fails withEMBEDDING_COST_UNPRICED; a hybrid search falls back to keyword-only, warns, and records the new trail codesemantic-cost-unpriced. The message names the model and the price pair that clears the refusal and says only that the gate is positive, never its amount. The CLI runs at local reach and is not gated.Queries fitted to the model's input window. The gateway cuts a query to the effective input window, counting the instruction prefix the provider sends, at a code-point boundary under the conservative token estimate, so it never sends more than the window and treats every language alike. A cut is disclosed by a warning naming the window and how much of the query was embedded, and by the new trail code
semantic-query-truncatedwithdetail.windowTokens. When the instruction prefix alone fills the window nothing is embedded: an explicit semantic search and the semantic belief order refuse withINVALID_INPUT, and a hybrid search falls back to keyword-only with the trail codesemantic-query-empty-fit, which, unlike a cut, always counts as a stopped semantic lane. An unknown window cuts nothing.embedding_input_window_tokens. The effective window is this key (envOPEN_SECOND_BRAIN_EMBEDDING_INPUT_WINDOW_TOKENS, an integer of at least 1), then the window the curated model table declares, then unknown. The chunk-window census of an index run and ofo2b search statusreads the same window, so a declared window enables it for an uncurated model. A blank value is refused rather than read as unset, and the key is refused for thelocalprovider, which has no window.embedding_extra_body. One JSON object (envOPEN_SECOND_BRAIN_EMBEDDING_EXTRA_BODY) whose fields are sent with every embedding request to an OpenAI-compatible endpoint. The owned fieldsmodel,inputandencoding_formatare refused by name in any spelling (case, separators, fullwidth and zero-width variants are folded first); a blank value, invalid JSON or a non-object is refused naming the key or env variable that supplied it. The key is refused for any provider other thanopenai-compat(disabledis exempt, it sends nothing), and adimensionsfield requiresembedding_dimensionand must agree with it. The checks run on the resolved config too, so a programmatic override meets them. The extra body is not part of the embedding identity and never triggers a reindex.Degradations reported where results are measured.
brain_recall_feedbackreturns an additivedegradedarray with the codes of its re-run search, andbrain_benchmark,brain_evalandbrain_tunereports carry adegradedunion (and per query or per grid row) when a run measured a smaller system than the configured one.Hook audit lines name the retrieval codes. The recall-inject and gap-promote hooks run at remote reach, so a gated query embed is now visible: their local audit lines carry
retrieval_degraded, the trail codes of the searches behind the decision, each once in first-seen order. The codes ride the local line only, never the syncedrecall_telemetryrecord.BrainUpgradeError.driftedand--apply --jsonrefusals. An upgrade refused for drift names every refused path, ando2b brain upgrade --apply --jsonprints{ "ok": false, "error", "run_id", "drifted" }for a refusal so a script can tell a drift from any other failure.
Changed
Upgrades apply the plan they showed.
o2b brain upgrade --applyand the automatic upgrade worker plan once and apply that plan; each planned row records the bytes it was read with, ornullwhen the file was absent (an absent file used to read as an empty one). A managed file that changed after the plan was read is refused before the snapshot is taken, with nothing written ando2b brain upgrade --dry-runadvised, and again right before each write, so an edit that lands mid-apply is left as it is. When earlier files were already rewritten, the message names them and advises the same dry-run re-plan; a rollback is deliberately not offered, because the pre-apply snapshot predates the rewritten files, so its own drift guard would refuse it, and forcing it would destroy the edit. The--dry-run --jsonplan still reportsbefore_size: 0for an absent file, and the text plan readsabsent.brain_context_packsemantic mode counts what it sends.semantic.query_tokensand the estimate count the text actually sent, instruction prefix included, and an omitted reach now resolves to remote like every other reader instead of local.brain_tunerefuses to save a keyword-only winner. A winner measured with the semantic lane missing is not saved:EMBEDDING_COST_UNPRICEDfor the cost gate,EMBEDDING_KEY_MISSINGorEMBEDDING_DISABLEDfor a blocked tier,EMBEDDING_PROVIDER_HTTPwhen the provider did not answer, and the sameEMBEDDING_PROVIDER_HTTPfor any other stop that left the hybrid caller keyword-only (the composite deadline, an empty query vector, an empty fit recorded assemantic-query-empty-fit), each with its remedy. An index with no embeddings or a machine without sqlite-vec is keyword-only by construction and still tunes.Recall feedback does not learn from a keyword-only answer. A feedback event whose re-run lost the semantic lane records zero layer contributions, so the vault's learned weights stay as they were.
Gated answers are never cached; cut answers are cached under their window. A search answer refused by the cost gate is not stored in the search cache, so declaring a price takes effect on the next search. A search answer cut to the input window is cached under a key that carries the effective window and the query prefix sent with it, so a repeated long query is not embedded again, and a declared or changed window or prefix takes effect on the next search.
Docs:
docs/cli-reference.mdgains the three trail-table rows, "Query embeds",embedding_input_window_tokens,embedding_extra_body, the extra-body note in the egress table and the upgrade drift behaviour,docs/how-it-works.mdthe query-embed gateway and the drift-checked upgrade,docs/mcp.mdthe gateway, thedegradedfields and the tune refusal,docs/observability.mdtheretrieval_degradedaudit field, anddocs/updating.md"Upgrading to 1.73.0"; theembeddings-setupskill gains the input window and extra body steps. The README names this release.
Fixed
A managed file edited during an upgrade is no longer overwritten. The upgrade used to plan again at apply time, so the plan an operator confirmed was not the plan applied, and an edit made in between was replaced without notice.
The query embed of an MCP search or a hook was not gated. The v1.72.0 cost gate covered embedding runs and the semantic belief order, but the search lane's query embed spent on an unpriced model for any caller; it now passes the same gate.
Notes
The rerank and decision-model spend paths stay outside the query-embed gate.
gap-promote verdicts are unchanged on degraded evidence: the codes are disclosed, the decisions are not altered.
An extra request body for the ZeroEntropy provider and for the reranker is not part of this release.
A width field under a name other than
dimensionsis not cross-checked againstembedding_dimension; the provider's response-width checks still catch a mismatch.
1.72.0 - 2026-10-05
Open Second Brain now prices embedding spend honestly: every estimate names where its price came from (builtin, operator or unknown), an operator can declare the price of a model the built-in table does not list, and one spend plan feeds the estimate, the cost gate, the spend receipt and search status, so an unknown price is reported as unknown instead of as $0 and a positive cost gate refuses it by name; edits re-embed only the chunks that changed, o2b search vector-backfill can be scoped to vault paths, o2b search check names an unpriced model, a stale price declaration and the places it looked for an embedding key, and brain_context_pack gains a semantic query mode that orders the curated belief notes by the stored vectors of their chunks.
Added
Price quotes that name their source. Every embedding estimate carries a price source:
builtin(the frozen price table, and the local embedder, which is free),operator(declared by the operator) orunknown. A price is not part of the embedding identity, so it never triggers a reindex.An operator price pair.
embedding_price_modelandembedding_price_usd_per_mtok(USD per million tokens, a plain non-negative decimal number no larger than 1,000,000,0declares the model free) bind a price to one model name, compared case-insensitively, and win over the table for that model. Set both or neither; a blank value counts as unset, so a blank half is refused as a half pair, never read as a free price. A blankembedding_cost_gate_usd(or its env twin) is refused withINVALID_INPUT, never read as a gate of 0, and a blanksearch_rerank_min_score(or its env twin) is refused the same way, never read as a floor of 0. A half pair, a negative rate, a non-decimal rate (0x10,1e3,Infinity) or a rate above 1,000,000 USD per million tokens fails config resolution withINVALID_INPUTnaming the key or env variable that supplied it. The env twinsOPEN_SECOND_BRAIN_EMBEDDING_PRICE_MODELandOPEN_SECOND_BRAIN_EMBEDDING_PRICE_USD_PER_MTOKwin as a pair: when either is set, both halves come from env, so an env model never pairs with a config rate. A loopbackembedding_base_urlis not assumed to be free.One embedding spend plan. The pending census (optionally scoped by path prefix), the model resolution, the token estimate, the price quote and the cost-gate verdict are one computation. The reindex task's embedding phase, the maintenance preview banner, the backfill dry run and the
search statusrefresh estimate all read it, so what is announced, refused, spent and receipted agrees.EMBEDDING_COST_UNPRICED. Under a positiveembedding_cost_gate_usd, an embedding run with pending chunks on a model with no known price is refused before the provider is contacted, because an unknown price cannot be checked against a cap. The message names the model, both price keys and--force-cost.--force-cost(MCPforce_cost) passes the refusal, and the receipt then recordsforced: true,price_source: unknownand a null estimate. The code joins the stable MCP error codes.Vector carry-over on edit. Replacing a document's chunks keeps the stored vector of every new chunk whose content hash matches an old chunk of the same document (duplicates match as a multiset), as long as the old vector was written by the model and dimension the index records; nothing is carried while that identity is unrecorded. A paragraph moved within a note is carried, one moved into another note is re-embedded, and only the vectors not carried are purged. The index run reports the count as
embeddingsReused.A repeatable
--pathono2b search vector-backfill. Each--path <prefix>scopes the run to chunks under a vault-relative prefix, and the census, the estimate, the cost gate and the spend receipt read the same scoped census, so--path Brain/preferences/prices and embeds only belief notes. The prefix is a raw string prefix (end a directory with/); a leading./is dropped,\becomes/, an empty prefix is refused withINVALID_INPUT, and an unsafe prefix is refused by name. The text report adds ascope:line and its next step keeps the scope;--jsonaddspath_prefixes, andunmatched_path_prefixeswith a stderr warning (scope <prefix> matches no indexed document) for a prefix that matches nothing.--jsonalso names the gate verdict and the spend receipt:gate_blockedandgate_reasonwhen the configured gate would refuse the run unforced, andspend(model,tokens,estimated_usd,price_source,forced) when an--applyrun reached the provider, each absent otherwise. A dry run the gate would refuse printscost gate: would refuse (<reason>)with the remedy; the remedy line names--force-cost, and the advised next step stays unforced. On Windows a scope prefix that would need quoting is left out of the advice:next_commandis omitted and the text saysnext: rerun this command with --apply and the same --path flagsinstead of naming a command.Price recommendations in
o2b search check. The recommendations name an active embedding model with no known price, with the price pair to declare and what a positive gate does with it, and a price pair that names a model other than the active one.Where
o2b search checklooked for an embedding key. When no embedding key resolves,key_sources_checked:lists the sources in probe order (OPEN_SECOND_BRAIN_EMBEDDING_KEY,embedding_api_key, then the env-key names of the registered profileembedding_providerselects), andkey_present_under:lists the other registered profiles whose env key is set, or saysnone.--jsoncarries the same ascredential_sources{consulted, present_elsewhere}. Only names the operator declared are consulted, no value is printed, nothing goes over the network, and a configured setup's output is unchanged.A semantic belief order on
brain_context_pack.query_mode: "semantic"(MCP only) embedsqueryonce and scores each curated belief note by the maximum cosine over its stored chunk vectors, counting only vectors written by the configured model at the query's dimension; nothing new is embedded for the notes themselves. Tier still decides first; within a tier a note without a usable vector sorts after the scored ones, then by recency. The response gains asemanticobject withmodel,price_source,query_tokens,estimated_usd(null for an unknown price),scoredandunembedded, counted after the reach filter. The mode refuses by stableerror.data.codewhen the semantic tier is blocked (EMBEDDING_DISABLED,EMBEDDING_KEY_MISSING), when sqlite-vec is not loadable (VEC_EXTENSION_UNAVAILABLE), and withBELIEF_VECTORS_MISSINGwhen no kept candidate has a usable vector, namingo2b search vector-backfill --path Brain/preferences/ --path Brain/retired/ --apply; a refusal that is certain in advance is decided before the query is embedded. The query embed is one paid call per request and is disclosed in the response; for a remote caller it is refused withEMBEDDING_COST_UNPRICEDwhen the model has no known price andembedding_cost_gate_usdis positive, while a local caller keeps the disclosed, ungated embed, as forsearch. A query vector refused after the embed (EMBEDDING_INVALID_VECTOR) names that spend, as a lateBELIEF_VECTORS_MISSINGdoes.
Changed
An unknown price reads as unknown, never as $0. The maintenance banner, the backfill dry run and
search statusprintprice unknownwhere they used to print$0.0000for an unlisted model, and the JSON estimates (estimated_cost_usd,estimated_usd, the status refresh estimate) arenull. The backfill'sestimated_cost_usdused to be omitted for an unknown price and is now always present. A fully embedded index has nothing pending, sosearch statusthen prints norefresh_cost_estline and its JSON estimate is0.A positive cost gate now refuses an unpriced model. Before this release a model missing from the price table was priced at $0, so a positive
embedding_cost_gate_usdnever stopped it; local servers (Ollama, LM Studio, llama.cpp) and any unlisted model are now refused withEMBEDDING_COST_UNPRICEDuntil their price is declared or the run is forced. With the gate at 0, the default, nothing is refused.Spend receipts carry the price source. Receipts, the
maintenance_spendmetric, the backfill JSON and the status JSON (refresh_price_source) gainprice_source, ando2b brain maintenance statuslists each receipt with it; journal rows written before this release readunrecorded, and a journaled receipt without a numeric estimate readsprice unknowninstead of failing the whole listing.brain_maintenancespend receipts carry a nullestimated_usdfor an unknown price.brain_context_packcapsqueryat 2000 characters. The cap is declared in the schema and enforced in every query mode, counted in Unicode code points as the schema'smaxLengthcounts them, and a blankqueryis refused withINVALID_PARAMSinsemanticmode.brain_searchnow counts its 2000-character cap the same way, so a query of characters outside the Basic Multilingual Plane is no longer refused at half the advertised length.brain_recall_feedback, which advertised the samemaxLengthwithout checking it, now refuses a longerquerywith the sameINVALID_PARAMSerror before it records anything.A refused path prefix names its argument and value. A search
path_prefix(and a backfill--path) that escapes the vault is refused with the argument name and the JSON-quoted value instead of a barepath_prefix escapes vault.Docs:
docs/cli-reference.mdgains "Embedding prices", "Vector carry-over", the--pathoption and the credential-source lines ofsearch check,docs/how-it-works.mdthe embedding spend, vector carry-over and semantic belief order entries,docs/mcp.mdthesemanticquery mode andEMBEDDING_COST_UNPRICED, anddocs/updating.md"Upgrading to 1.72.0"; theembeddings-setupskill gains the price declaration step and the scoped backfill. The README names this release.
Fixed
The managed-file upgrade worker never recreates a removed vault. The automatic upgrade worker checks again that the vault still exists right before it rewrites managed files or records a failure, and writes nothing when the vault was removed or moved while it ran.
The Hermes anti-drift test no longer fails intermittently on Windows. It waits for the live
o2b mcpserver and its launcher to exit, and kills the whole process tree if they do not, before removing its temporary directory, so a log file the server still holds open no longer breaks the cleanup.
Notes
Account-side model discovery and automatic model switching are deliberately not part of this release: both would reach the network or start a paid re-embed the operator did not request.
The hygiene dedup detector and the entity alias detector still embed short texts live, outside the spend plan, with no gate and no receipt, as before this release.
1.71.0 - 2026-10-04
Prompt-time recall now remembers what a session was already shown: the recall-inject hook skips a note span it injected earlier in the session or that the SessionStart digest already delivered, an operator can lay the brief out as named slices declared in Brain/_brain.yaml, and its four caps are config keys with env overrides; and an opt-in chunked re-grounding splits an oversized SessionStart payload into parts that a new reground-deliver hook hands out exactly once each on the following tool calls and prompts in Claude Code and Codex, so a post-compaction payload is no longer cut to a preview.
Added
Per-session recall dedupe. With
recall_inject_enabledon and a host that sends asession_id, the recall-inject hook keeps a ledger under.open-second-brain/hook-state/of the note spans this session was shown, keyed<origin>:<path>#L<start>-L<end>(an empty origin for the primary vault), so a different span of a long note stays eligible. Only the notes actually rendered into the brief are recorded, and only after the brief was written to stdout. Each entry expires 24 h after its last write, the set keeps at most the 2000 most recent keys, and every SessionStart that emits (startup, resume, clear, compact) starts a new injection epoch and clears the set; a run of the SessionStart hook on another event, such as an operator-registeredUserPromptSubmit, injects its payload whole and starts no epoch. Dedupe runs after the confidence floor and before the note cut, and when every candidate that cleared the floor was already shown the hook abstains with the new reasonall_already_injected. A host without a session id gets no dedupe.A cross-lane filter against the SessionStart digest. The SessionStart hook records in the same ledger the vault paths present in the payload it actually emitted (
Brain/active.md,Brain/lessons.mdandBrain/preferences/pref-<slug>.mdfor each rendered preference bullet; a lesson or any other line that only quotes apref-id does not count), and the recall hook drops a candidate from the active vault on any of those paths, on any line span. A profile or recall-source note on the same vault-relative path is a different note and stays eligible. It writes this set only whenrecall_inject_enabledorreground_parts_enabledis on and a session id is present.Operator-declared recall slices. A
recall_inject:block inBrain/_brain.yamldeclares up to 6 named slices (slices: [decisions, lessons]plusslice_<name>_heading,_path_prefix,_types,_limitand_max_chars), each retrieved with its own filters and rendered under its own heading inside the one fenced brief. Slices retrieve in parallel under the one shared time budget, the confidence floor and dedupe apply per slice, and the global note and character caps bound their sum, so a later slice is clamped to what the earlier ones left. A note an earlier slice placed is not repeated, a slice that places nothing is omitted, and when no slice places a note the hook abstains withall_slices_abstained. An unknown field, an undeclared or duplicate name, more than 6 slices, an out-of-range number or apath_prefixwith.., a leading/or a drive letter is a load error. The block is deliberately absent from the generated_brain.yamltemplate; without it the hook runs its single unsliced query byte for byte.Recall-inject caps in config.
recall_inject_max_notes(1..10, default 4),recall_inject_max_chars(200..8000, default 900),recall_inject_time_budget_ms(250..6000, default 2500) andrecall_inject_confidence_floor(0..1, default 0.35) sit next torecall_inject_enabled, each with anOPEN_SECOND_BRAIN_RECALL_INJECT_*env override that wins over the config value. A rejected value keeps the built-in default and is named on the hook's audit line asconfig_invalid; unset caps leave the brief byte-identical.Chunked re-grounding (off by default). With
reground_parts_enabledon (envOPEN_SECOND_BRAIN_REGROUND_PARTS_ENABLED), a joined standing-rules, scoped-rules and memory payload longer than the part ceiling is cut at block, then paragraph, then line boundaries into at most 8 parts. Part 1 is emitted at SessionStart, parts 2..n are queued in the session's hook state with a 24 h expiry, and the newreground-deliverhook hands out exactly one queued part perPostToolUseorUserPromptSubmitevent until the queue is empty or the next SessionStart replaces it; a part queued under an earlier SessionStart is never delivered, and a tool call inside a delegated sub-agent (the payload carriesagent_id) takes no part, so the queue stays for the main agent. When parts past the cap are dropped, the last delivered part ends with(context truncated: N further part(s) not delivered), and every part, that line included, fits the ceiling. The ceiling isreground_part_chars(2000..100000 UTF-16 code units, default 9000, the observed Claude Code preview threshold less 10%), overridable per runtime byreground_part_chars_claudecodeandreground_part_chars_codex, each with an env override. Only Claude Code and Codex payloads with a session id are split; every other runtime, and any payload that fits, gets the single payload as before.Re-grounding meters. With the flag on, the SessionStart receipt's
extra.injectioncarriesutf16_chars,part_ceiling_chars,parts_total,parts_droppedandover_budget, plusreground_fallback: "ledger_write_failed"when the queue could not be written and the whole payload went out as one part; the new epoch is then written once more with that whole payload, so a transient failure does not leave the previous SessionStart's queue and recall set behind. A part-ceiling key (reground_part_chars,reground_part_chars_claudecode,reground_part_chars_codex) whose value is out of range or not an integer falls through to the next level and is named in the receipt'sconfig_invalid, present only then. The epoch records the SessionStart source asstartup,resume,clearorcompact, any other value asunknown. Each delivered part writes one hook-audit linereground_part_deliveredwith its part number, total, epoch and sizes, and a take that fails for a reason other than lock contention writes onereground_take_failedline per epoch.Recall-inject audit fields. The hook's local audit line gains
dedupe_sets(the sizes of the recall and digest sets consulted),ledger_recorded: false(an injected brief whose ledger write failed, so its notes can repeat),slices(one{name, outcome, notes}per slice) andslices_config: "invalid". Slice names ride the local audit line only, never the syncedrecall_telemetryrecord.
Changed
The recall brief no longer repeats notes within a session.
recall_inject_dedupe(envOPEN_SECOND_BRAIN_RECALL_INJECT_DEDUPE) defaults totrue, so an operator who already runs withrecall_inject_enabledon sees each note span at most once per injection epoch instead of on every prompt, and a prompt whose candidates were all shown before now injects nothing. Set it to"false"or"0"to restore the previous behaviour.The decision-model filter judges the deduped, sliced brief. With
decision_model_uses: "recall_inject:..."the filter sees the brief after session dedupe and slice layout, so it never judges a note the session was already shown, and its sub-budget is bounded by the configuredrecall_inject_time_budget_msinstead of the fixed 2,500 ms.Two new hook registrations on Claude Code and Codex.
hooks/hooks.jsonand the Codex plugin registerreground-deliverforPostToolUse(matcher*) andUserPromptSubmit. Withreground_parts_enabledoff the hook checks the flag before resolving the vault and exits without output.Hook state is written atomically and pruned. Hook-state files are now written through an atomic rename, so a reader never sees a partial file, and a multi-key update runs under the per-session lock. A lockfile left behind by a hook killed mid-update is taken over after 30 s by moving it aside, so only one contender acquires it. On a
startupthat wrote the session ledger, the SessionStart hook prunes hook-state scope files older than 7 days, at most 200 per run; the sweep removes scope files and the temp files, abandoned locks and stale-lock asides a killed write left behind, and leaves symbolic links and linked state folders untouched.Hook state is private. The per-session files under
.open-second-brain/hook-state/are written with mode0600, because the re-grounding queue holds parts of the SessionStart payload (standing rules, scoped rules, the memory digest tail); existing files become private on their next write, and a file never written again stays as it was until the prune removes it. Thehook_session_statestate surface reportscarries_memory: true. When.open-second-brainorhook-stateis a symbolic link, the hooks neither read nor write through it (reads answer empty, writes fail open), and a state file that is itself a symbolic link is not followed. A session id that is not already a lowercase slug gets a file name with a 16-hex hash suffix, so ids that differ only by case or punctuation, or share a 64-character prefix, no longer share a file; Claude Code and Codex session ids keep their file names, and a host that sends other ids starts its hook state afresh once after the upgrade.The inject cache and the search focus get the same guards. The last-good SessionStart snapshot under
.open-second-brain/inject-cache/and the search session focus file are written with mode0600, and neither is read or written through a directory that is a symbolic link: a read answers empty, a cache write is skipped, and setting a focus througho2b search focusor the MCP focus path answersINVALID_INPUT. A cache or focus file that is itself a symbolic link is not followed on read, and a focus write replaces it instead of writing through it.A search
path_prefixthat starts with a drive letter is refused. The search request now answersINVALID_INPUTfor apath_prefixlikeC:/notes, as it already did for one containing..or starting with/, matching the recall slice load rule.Docs:
docs/cli-reference.mdgains the recall-inject tuning keys, therecall_inject:slice contract and the chunked re-grounding keys,docs/decision-models/recall-inject.mda "Session dedupe and slices" section,docs/observability.mdthe recall-inject audit fields and the re-grounding receipt and audit fields,hooks/README.mdthe tworeground-deliverrows and a per-session hook-state paragraph, anddocs/updating.md"Upgrading to 1.71.0"; the README names this release.
Notes
On
resumea new injection epoch starts, so a note can be injected once more.Dedupe does not over-fetch to refill a brief, because that would change the confidence-floor verdict for the same prompt; a deduped brief can carry fewer notes than the cap.
A
Brain/_brain.yamlthat fails to load takes no slice path and the search that reads the same file fails, so the decision ends inerrorwith faultretriever_failedand nothing is injected until the file is fixed.The per-runtime part ceiling for Codex uses the Claude Code value until the Codex threshold is measured.
1.70.0 - 2026-10-03
A setting or a rule meant for one context no longer carries over into another: on a Hermes gateway that multiplexes profiles, the Open Second Brain plugin reads every profile-scoped setting from the turn's profile scope and gives each profile its own MCP child, and the operator can now write standing rules for one project, one harness or one machine under Brain/standing-rules/, which the SessionStart hook and brain_context render below the operator standing rules at local reach; the today, monthly and operator brief views, the doctor's counts and repair preview, and the obligation, intention, health, trigger, stale-scan, review-candidates, intent-review, retention, context-receipts, tension, lifecycle, expire, evidence, feedback, derived-fact, decision, label, scaffold, dream, maintenance, dead-end, diarize, hygiene refresh and anticipatory-context tools now answer at the caller's reach.
Added
Scoped operator rules. Beside the vault-wide
Brain/standing-rules.md, the operator can write one file per scope value:Brain/standing-rules/project/<key>.md,Brain/standing-rules/harness/<harness id>.mdandBrain/standing-rules/host/<device id>.md. Each session renders the files whose key matches its own project, harness and device, under a## Scoped operator rulesheader that states the precedence (the operator standing rules above take precedence over them, and they take precedence over every recalled preference, lesson and context pack that follows) and one### Project: <key>,### Harness: <key>or### Host: <key>subheading per file. The operator's text is never rewritten. A file that cannot be read is replaced by oneUNAVAILABLE:line naming its vault-relative path and the error code (ESCAPEfor a file or folder behind a symbolic link that leaves the vault). The files are read on every render and never cached, and they render only at local reach.A server-resolved scope identity. No caller names its own scope. The project is the basename of the directory holding the nearest
.o2b-vault.jsonpointer found from the workspace directory (written byo2b brain project link); the harness comes from the launch-time--harnessoption, falling back to--host-target; the host is the device id. Keys are normalised (NFC, lowercase) to letters, combining marks, digits and-in any script, at most 64 characters. An axis that does not resolve matches no file.o2b mcp --harness <id>. Names the harness the server runs under, from a closed list (aider,claude-code,codex,copilot-cli,cursor,gemini-cli,generic,grok,hermes,kiro,openclaw,opencode,pi); an unknown value exits 2 and lists the accepted ones.scoped_rulesonbrain_context. At local reach, when at least one scoped file matched (or the host notice applies), the scoped block follows the standing rules incontentand the result carriesscoped_rules: {scope: {project, harness, host}, files: [{path, axis, truncated}]}with vault-relative paths. No input argument is added.active.scoped_rules_max_chars. Caps the scoped block (default 2000, at least 200); when the cap cuts anything the host file drops first, then the harness file, and a notice states the kept and total characters and the number of dropped files.Per-profile Hermes settings on a multiplexed gateway. With
gateway.multiplex_profileson,VAULT_DIR,VAULT_AGENT_NAME,VAULT_TIMEZONE,OPEN_SECOND_BRAIN_CONFIGandOPEN_SECOND_BRAIN_MCP_TIMEOUTare read from the turn's Hermes profile scope (the profile's.env), never from the gateway process environment, which belongs to the launch profile. A setting the profile does not hold falls through to the config chain (pointer, active profile, config key, default).XDG_CONFIG_HOMEandLOCALAPPDATAare read from the profile scope first and fall back to the gateway process environment when the profile does not set them, so a profile's own config directory locates its own config file. Without multiplexing every answer is unchanged.settings_source:inhermes open-second-brain config. The first line names the source this command resolved from:profile scope (multiplexed gateway), orprocess environment (this command; a gateway with gateway.multiplex_profiles reads each profile's .env).
Changed
The Claude Code and Hermes plugins launch
o2b mcpwith--harness. The Claude Code plugin registers both of its MCP servers with--harness claude-code, and the Hermes plugin launches its bridge with--harness hermes. Ano2bolder than 1.70.0 refuses the flag, so the Hermes bridge does not start untilo2bis updated together with the plugin; the Claude Code plugin runs theo2bit ships and needs no step.A refused
--host-targetvalue is echoed JSON-quoted.o2b mcpused to echo a refused--host-targetvalue raw on stderr; like a refused--harnessvalue, it is now a JSON string with every C0 and C1 control character escaped as\uXXXX(or its short JSON escape).The scoped block is charged against the injection budget. Unlike the operator standing rules, it is a budgeted source of the SessionStart hook: its rendered length is subtracted from
inject_budget_charsbefore the active context is assembled, its header and the room for its notices ride inside that budget and its section cap is never larger than what the budget leaves after them, and the receipt'sbudgetblock records it asscoped_rules_charsbeside the unchangedinject_budget_chars.The SessionStart hook renders project and host files. Harness-scoped files render on the MCP surfaces only in this release.
Each Hermes profile gets its own MCP child on a multiplexed gateway. The child's environment carries the profile's own agent name, timezone, config path and vault instead of the launch profile's, and two profiles that share a vault but differ in any of those, or in their config directories (
XDG_CONFIG_HOME,LOCALAPPDATA), no longer share one child. On a multiplexed gateway a bridge keeps the timeout it was built with, so a restart never reads the profile scope from a thread that has none; elsewhere the timeout is read at each start, as before.A Hermes setting ignored from the gateway environment is named once. Under multiplexing each profile-scoped variable set in the gateway process environment logs one WARNING per process from the plugin's config module, without its value, saying to set it in the profile's
.envinstead; the shadowing hint names the profile's.env.A turn with no bound profile scope degrades. When the gateway bound no profile scope, the settings readers raise a named
ProfileScopeError(aConfigReadError) instead of reading the launch profile's values, and the per-turn vault reminder is omitted with one WARNING instead of failing the turn. A Hermes whose profile-scope module is present but fails to import is treated the same way (one WARNING naming the exception type, and no further import attempt in that process), never as a gateway without profiles, andhermes memory statusleaves outconfig_filewhen no profile scope is bound instead of failing.The today, monthly and operator brief views and the doctor's counts answer at the caller's reach. Below local reach
brain_briefview="today"no longer lists an open loop or obligation on a page the caller cannot read and counts only the files it may scan;view="monthly"counts events, status transitions, retirements and contradictions from the events the caller may see, like the daily and weekly views;view="operator"computes its doctor and digest counts, its top actions, its verification entries and counts and its trust verdict from what the caller may see; andbrain_doctorfills its removed-tool warning cap and the per-code cap of itsuncertainstream, counts the stale-dependencystates_changedand each stale row's consumers, and runs its concept-gap and contradiction detectors from readable pages and records only. A local caller and the CLI see no change.Scoped rule files cannot be written by an agent. Every write path that refuses
Brain/standing-rules.mdnow also refuses any path insideBrain/standing-rules/, compared as written and after resolving symbolic links, so a not-yet-existing file reached through a linked folder is refused too. A rules folder or file behind a symbolic link that leaves the vault no longer makes the other guarded writes (labels, attributes, marker writeback,brain_write_session) fail.Docs:
docs/how-it-works.mdgains "Scoped operator rules",docs/mcp.mdthescoped_ruleskey and--harness,docs/cli-reference.mdtheo2b mcp --harnessline,docs/observability.mdthescoped_rules_charsfield,docs/stability.mdthe two new layers,install/hermes.md"Multiple Hermes profiles", anddocs/updating.md"Upgrading to 1.70.0"; the README names this release.
Fixed
The today view no longer lists an open loop from a page the caller cannot read. Below local reach
view="today"used to return the text and path of an open loop on a page withheld by visibility.The operator view no longer lists a verification entry the caller cannot read. Below local reach
view="operator"used to list verification entries by record id and page path for records withheld from the caller.brain_obligationanswers at the caller's reach. Below local reach it used to list, show, complete and remove an obligation page withheld by visibility; such a page is now treated as absent, andaddrefuses its slug as before.brain_healthandbrain_doctorcount concept gaps over readable principles. Below local reachconcept_gapsand the doctor'sconcept-gapwarnings used to name terms that only withheld preferences carried; the semantic-health detectors now read only the preferences and signals the caller may read.brain_triggerscans answer at the caller's reach. Below local reachoperation="scan"used to count, create and list triggers about withheld preferences; its sources now read only readable records, and every trigger row asks the caller's reach as well as the owner scope.brain_stale_scan,brain_review_candidatesandbrain_retentionanswer at the caller's reach. Below local reach they used to list withheld preferences and signals by id and path; such rows are now left out, and the retentionsummarycounts the rows returned.brain_intention,brain_intent_reviewand the dream preview's signal clusters answer at the caller's reach. Below local reachbrain_intentionused to list and show an intention chain withheld by visibility and fold it into a new version onset; such a chain is now treated as absent bylist,showandmove, andsetrefuses it;listjudges each chain by its own file name, so a readable chain whose file name is not scope-shaped stays listed and a file named without letters or digits no longer fails the call.brain_intent_reviewand theclusters_below_thresholdandintent_reviewsofbrain_review_candidatesused to fold withheld inbox signals into their topics and counts; they now fold only the records the caller may read.brain_obligationremoveandbrain_intentionmoveleave the archive name out of the answer below local reach.Retention and forget-plan rows carry the vault-relative path.
brain_retentionrecommendations and the forget-plan entries used to carry a path pointing out of the vault, so abrain_triggerscan below local reach never queued a retention trigger, even for a readable record.brain_doctorand the operator view leave out a withheld instruction file. Below local reachinstruction_file_warningsused to name a vault-rootAGENTS.md,CLAUDE.mdorGEMINI.mdwithheld by visibility, with its line count.A withheld page no longer moves a readable stale-dependency row or the
uncertaincap. Below local reach a withheld consumer could hide a readable state's row, and withheld pages could fill the per-code cap of the doctor'suncertainstream.brain_context_receiptsanswers at the caller's reach for the operator rules. Below local reach a SessionStart injection receipt used to carry the standing-rules and scoped-rules items and the figures that measure them; it now leaves them out, andsummaryleaves them out of its item totals and counts a receipt left with no item as empty. A receipt that recorded only the rules (a session with noactive.mdbody, or a degraded run with no cached body) is left out oflistandsummaryand answered byshowas an unknown id, and thebudgetblock leaves a receipt with no budgeted body left.The
brain_doctorrepair preview counts at the caller's reach. Below local reachrepairused to fill the removed-tool cap and count concept gaps over every page before its findings were filtered, so itsunfixablecounts moved with withheld pages; the checks behind the plan now read only the pages and records the caller may read.brain_tensionanswers at the caller's reach. Below local reachdetectused to read, count and pair notes withheld by visibility and return the tension, andlist,show,verifyand the transitions used to answer for it.detectnow reads only the notes the caller may read, a persisted tension page carries the strictervisibilityof its two source notes, and a tension page the caller cannot read is answered as absent (no tension: <slug>) before anything is written.brain_lifecycleandbrain_expirerefuse a page the caller cannot read. Below local reachtombstone,supersede,temporal-replaceandbrain_expireused to write to a page or record withheld by visibility; such a target is now refused with the error a missing one gets (note does not exist,no signal or preference with id), before anything is written, andcuratorleaves out the rows of such pages.The writers that name a preference, a decision or a page answer at the caller's reach. Below local reach
brain_apply_evidenceand theapply_evidenceoperation ofbrain_write_batchused to accept evidence against a preference withheld by visibility,brain_derive_facta withheld premise,brain_scaffold_stuba withheld source, andbrain_decision,brain_labelsassign/removeandbrain_lifecycletipused to show, rewrite or resolve a withheld page; such a page or record is now treated as absent before anything is written. Thebrain_feedbackconflict and routing hints andbrain_design_notegrounding read only readable records. Thebrain_scaffold_stubambiguity refusal no longer ends in an empty candidate list when none of the candidates is readable.brain_dreamandbrain_maintenancerun a real pass at local reach only. Below local reachbrain_dreamserves a dry run planned over the records the caller may read, and refuses a real pass, a step and the staged lifecycle;brain_maintenancerefusesrun. At local reachbrain_maintenancerunis allowed even whenintegrity.owner_scope_deliveryisfail.A preference drafted by the dream pass keeps the visibility of its sources. A preference drafted from signals reserved with
visibility, or superseding or rebutting a reserved record, used to be written without avisibilityline; it now carries the strictest visibility of those sources.brain_dead_ends,brain_diarize,brain_hygienerefreshandbrain_anticipatory_contextanswer at the caller's reach. Below local reach they used to list, profile, re-derive or archive pages and records withheld by visibility, or serve a context bundle built for the local operator; they now read only the pages and records the caller may read, andbrain_dead_endsrecordstill counts every active dead end against the cap but moves only readable ones into the archive.A non-finite Hermes MCP timeout is read as meant.
OPEN_SECOND_BRAIN_MCP_TIMEOUT=nanused to become the request deadline itself; it is now a malformed value that keeps the default deadline with one WARNING,infdisables the deadline like a non-positive value, and the WARNING for a malformed value no longer prints the value.
Notes
Other variables the TypeScript core reads from the environment (search settings, the MCP API key, embedding keys, the Telegram settings) still come from the gateway process environment on a multiplexed Hermes gateway.
The shared Hermes bridge resolves the gateway's working directory, so it usually matches no project-scoped file.
A Hermes gateway that serves a routed profile home without multiplexing keeps reading the process environment.
There are no combination scope files (for example project and host together); the matching single-axis files are joined instead.
The operator view's
dream_summarycounts (the dry-run dream's warnings, uncertain and quarantined entries), and the dream warnings its trust verdict folds in, are still taken over the whole Brain layer below local reach; they name no record.A create aimed at an existing page refuses whoever may read it:
brain_obligationadd,brain_intentionsetover a withheld chain,brain_decisionrecord,brain_feedbackforce_confirmed, thebrain_derive_factslug,brain_write_batchcreate_noteand thebrain_scaffold_stubtarget.The review obligation
brain_decisionrecordopens names the decision's title and does not take the decision page'svisibility.
1.69.0 - 2026-10-02
Ingest now reads more than Markdown: one format registry maps a file's extension to a format, CSV, TSV and HTML sources get a derived ## Table or ## Parts section and their content hash on the summary page, which carries the source's visibility, PDF, Office, EPUB, RTF and image files are named as format skips instead of counted as unclassifiable, and the read-only o2b brain extract previews what ingest derives from a file; the daily log pages, the daily and weekly briefs, the analytics, claims, backlinks, log, topic, preference, MOC audit and doctor readers, the source-cleanup tools and the recall verdict's root coverage now answer at the caller's reach, private regions and table cells are redacted in linear time, and brain_recall_gate accepts the turn_id the Hermes plugin sends.
Added
A source format registry. One registry maps a file's extension to a format and each format to an extractor: Markdown and the lightweight markups (
.md,.markdown,.txt,.text,.rst,.org) are read as they are, CSV, TSV and HTML (.csv,.tsv,.html,.htm) are extracted, and PDF, the Office formats (docx,xlsx,pptx,odt,ods,odp), EPUB, RTF and images are named but not extracted. No parser dependency and no external program is added.HTML parts on summary pages.
brain_ingest_sourceon an HTML file in the vault, in the trusted lane and readable at the caller's reach, reduces it to text with a linear scanner (comments, scripts, styles, embedded SVG and MathML dropped, entities decoded,<private>regions replaced by their placeholder, also inside a title or a textarea, no attribute value kept) and writes its headings under## Partsas one fenced block, one line per heading:h<level> <trail> | lines <a>-<b>. At most 256 parts; each heading and the title are redacted (key=valuecredentials and URL userinfo) over their first 4,096 code units, then capped at 200 characters, and a backslash and a|in a heading are escaped as\\and\|so no heading can forge a line span. The scan keeps at most one heading past the cap, so a page of millions of headings is read in bounded memory. The body text never reaches the page. The result gainsparts({extracted: true, count, omitted?}or{extracted: false, reason}).Table notes for CSV and TSV. The first record is the header; quoted fields follow RFC 4180 leniently, a
.csvfile is read as semicolon-delimited when its first record has at least two fields on semicolons and more than on commas (soname;"price, eur";qtyis three columns), and an unterminated quote is refused asmalformed-quotingwith the record number. Rows are rendered under## Tableas fenced plain text in groups of at most 50 rows and 600 tokens, each repeating the header, so every search chunk of the page carries the column names and aTable > Rows a-bheading path. Caps: 1,000 rows, 64 columns, 256 characters per cell, 256 KiB per section, each cut named; only the first 4,096 code units of a cell are read for redaction, and a longer cell counts as cut. A<private>region is hidden before the records are split, so a region spanning records hides every row between its tags. A column whose header names a credential has its values replaced and every other cell goes through the output redactor with URL credentials included. Inside a cell a backslash,|, a backtick, a line feed, a carriage return and a tab are escaped, and every other control character (C0, DEL and C1) is written as\u{XXXX}, so no cell can end the fence or reach a terminal raw. The result gainstable({rendered: true, format, delimiter, columns, rows, rows_rendered, truncated?, redacted_cells}or{rendered: false, format, reason, detail?}).New frontmatter on derived pages.
source_formatandsource_content_hash, then for tablestable_delimiter,table_columns,table_rows,table_rows_renderedandtable_truncated(absent when nothing was cut).o2b brain extract <file> [--json]. A read-only preview of what ingest derives from a file, chosen by its extension, up to 8 MiB: the HTML title and parts (with--json, the extracted text and each part's line span and source byte offset), or the table counts and the## Tablesection, ornot extracted: <reason>(format-read-verbatim,format-not-extractable,format-unknown,not-a-regular-file,source-too-large,not-utf8). A named or unknown format is answered from its extension without reading the file, and a leading frontmatter block is left out as ingest leaves it out. Exit code 0 for every answer; a missing path or a symbolic link exits 1.turn_idonbrain_recall_gate. The optional argument (at most 512 characters) the Hermes plugin sends is accepted and recorded on thegate_telemetryrecord.
Changed
CSV, TSV and HTML files are planned by default.
o2b brain batch-planandbrain_ingest_batch_plandiscover them beside Markdown and plain text, and a planned file of a non-text format carriesformatafterstatus(CLI:- <path> (<status>, <bytes>B, <format>)). The plan id of a tree holding such files changes, so a resume checkpoint written for it before the upgrade no longer matches; a Markdown-only tree plans byte for byte as before. The first ingest after the upgrade of a CSV, TSV or HTML source already ingested rewrites its summary page once with the derived section and keys.Named but unextracted formats are listed, not counted. A PDF, Office, EPUB, RTF or image file moves from
unclassifiabletoskipped_non_extractablewith reasonformat-not-extractableand its format asdetail, ahead of the schema skips, andskip_reason_countsgains that key. Anextensionsoverride that lists such an extension still plans it.A summary page is at most as visible as its source. A summary page with a derived section takes on the source's own
visibilitytokens; when both the page and the source declare tokens, the page gets the audience both the operator and the source allow, and when they share none, the page is withheld below local reach. The reserved token is kept whenever either side carries it. A leading frontmatter block in a CSV, TSV or HTML source is not data: it is never rendered as a row or as text.Over-long correlation arguments are refused up front.
brain_recall_gaterefuses aturn_idorsession_idover 512 characters or atelemetry_hostover 200 withINVALID_PARAMSbefore it runs, with telemetry on or off; such a value used to be dropped from the telemetry record silently, or not checked at all with telemetry off.The daily and weekly briefs answer at the caller's reach. Below local reach
brain_briefview="daily"andview="weekly"leave out status transitions, retirements and contradictions naming a record the caller cannot read (under itspref-orret-id), withhold a source pointer when every evidence event citing it is about such a record or when it names a page the caller cannot read, recomputeevents_by_kindandvault_deltafrom the events the caller may see, and take no report snapshot and show nodelta.More readers treat a page or record the caller cannot read at its reach as absent. A local caller and the CLI see no change.
Below local reach the daily log pages (
Brain/log/, the JSONL sidecars included) are not served bybrain_searchor any other generic page reader.brain_analyticsview="timeline",view="belief_evolution"andview="concept_synthesis",brain_briefview="today",brain_querywithsince,topicorpreference, and theosb://log/{date},osb://topic/{topic}andosb://preference/{id}resources judge every log entry and record under each id it answers to (pref-andret-), drop an entry naming such a record before any limit or total, and answer a refused preference as an absent one;osb://log/{date}re-renders the day from the entries it keeps.brain_claims(current and history),brain_backlinksandosb://backlinks/{id}judge a record under both its spellings, so a retired record is not named under its earlier id.brain_moc_auditanswers a hub the caller cannot read ashub note not found, and leaves such a member unlisted, unsized and out of the reference counts.Every
brain_doctorfinding is kept only when the caller can read each record it names;orphan-evidenceandmalformed-evidence-rangenow carrysources(the preference the evidence is about) andremoved-tool-referencecarriespath(vault-relative on the wire), with the messages unchanged.brain_search_by_sourceandbrain_delete_by_sourcehonour the reach of a derived summary page: such a page, and the content-manifest entry of a source the caller cannot read, are neither listed nor deleted. Abrain_ingest_sourcebelow local reach leaves a summary page the caller cannot read untouched and answers as a first ingest would.An entity intake of a source that is reserved against remote reads stays in the trusted lane but records no
source_content_hashon any Brain page.The recall verdict's root coverage in
brain_searchandbrain_recall_gatecounts a root as reached only through a page the caller can read.
An open private tag opens a region more readily. An open
<private>tag whose attributes hold a<, or whose>never comes, now opens a region in the redactor, and the session-recall and continuity detectors ask the redactor's own region walker, so an unclosed tag also flags a turn private there.The bounded source reader sizes its buffer to the file. It used to allocate the full read limit for every source; it now starts at the file's size and grows once when the file grew under the limit.
Docs:
docs/mcp.mdgains two "Since v1.69.0" bullets (non-Markdown ingest, and the readers that answer at the caller's reach),docs/cli-reference.mdtheextractandbatch-planlines,docs/how-it-works.mda "Non-Markdown sources" section with the limits of table notes and of the redactor, the architect clause and the doctor finding fields,docs/observability.mdtheturn_idfield, anddocs/updating.md"Upgrading to 1.69.0"; the README names this release.
Fixed
An operator-set
visibilityon a summary page survives a re-ingest. It used to be dropped whenever the source was ingested again, widening a page the operator had narrowed; it is now kept likecreated_at, for every source format.A byte-order mark no longer hides a frontmatter block. A page or source that starts with U+FEFF before its
---block used to be read as having no frontmatter, so itsvisibilitydid not apply; the mark is now skipped before the block is read.Private regions and frontmatter blocks are read in linear time. Stripping
<private>regions, matching an open tag that never ends, detecting a private region in a session turn or a continuity record and reading a frontmatter block that opens and never closes each took time quadratic in the input on some text; each now scans its input once, so a page of a few megabytes no longer stalls a read.The digest and
brain_event_tracekeep a dream shared with a readable record. Below local reach a dream that also names a record the caller can read used to be dropped whole; it is now kept with its readable transitions, and itsconfirmed,retiredandnew_unconfirmedlists keep only readable links.brain_event_tracedrops evidence on a retired record the caller cannot read. Below local reach anapply-evidenceevent logged before its preference was retired, which names the record only under itspref-id, used to be listed with its artifact and result once the record had moved to its reservedret-page; each record id is now judged under both spellings.o2b brain extractrefuses a symbolic link. A path that is a symbolic link is an error (exit code 1, the message names the link) before any format is named, as the reference states; the verb used to follow the link.Empty HTML comments no longer hide the rest of a page. The scanner closes
<!-->and<!--->where they stand; either used to hide every heading and line of text up to the next-->or the end of the source.A part's
source_offsetis counted in the source file. When a leading frontmatter block (and a byte-order mark before it) is left out,o2b brain extract --jsonused to count each part's byte offset from the end of that block; it now counts from the start of the file.The architecture overview no longer claims there are no module edges when it cannot draw them. When every declared edge touches a module whose name a link cannot carry, the
module-dependenciesregion names those edges instead of the no-edge sentence; with mixed edges one line under the diagram names the dropped ones, and the module note lists them underNot linked:.Multi-line prompts that start with a command name are admitted by the recall surfacing gate. The newline check ran on the normalised prompt, where line breaks were already gone; it now tests the raw prompt, so
git statusfollowed by a question on the next line retrieves, while a single-line command (also with only a trailing line break) is still skipped asshell_command.
1.68.0 - 2026-10-02
The architecture scan now reads the dependency manifests a project declares, at its root and at each module, and turns them into dependency regions, declared module edges and a typed depends_on link on module notes; the pre-extractor reads Terraform files into block seeds with depends_on and references edges, and redacts credentials in import specifiers; and the active digest, the activity and lessons digests, the morning brief, the pre-compress pack, brain_health and the brain_doctor repair now answer at the caller's reach, while the generic page readers withhold the compiled digest pages from a remote caller.
Added
Dependency manifests in the architecture scan.
o2b brain architectreadspackage.json,pyproject.toml(PEP 621 and Poetry),Cargo.tomlandgo.modat the project root and at each detected module, and reportspom.xml,build.gradle,Gemfileandcomposer.jsonasunsupportedby name. Each manifest gets one status:read,malformed(with a fixed reason such asinvalid JSON,invalid TOMLornesting too deep, never the manifest's own text),unreadable(with the error code, or because it is not a regular file or is larger than 1 MiB, which is not read) orunsupported, and one bad manifest never aborts the run. A manifest that is a symlink is not read, and a leading UTF-8 byte order mark is ignored. Runtime dependencies are listed per ecosystem under one canonical name each (PyPI names normalised per PEP 503, a renamed Cargo dependency by its crate name); dev, build, optional, peer and indirect groups are counted, not listed. The project name comes from the first root manifest read that names a project, in the orderpackage.json,pyproject.toml,Cargo.toml,go.mod.Declared module edges. A module whose manifest declares a runtime dependency on the manifest name of exactly one other module gets a declared edge (a name two modules carry binds nothing and stays listed as a dependency). The overview gains a
module-dependenciesregion (a Mermaid diagram, declared by manifests, not measured from imports), each module note gains adependenciesregion, and a module note with edges gets adepends_onfrontmatter key: a sorted YAML list of wikilinks that the indexer turns into typeddepends_onlinks. A module whose directory name holds a character a link cannot carry (a control character, DEL and the C1 range included,[,],|,#or^) is named once on one overview line and left out of every link and edge.manifestsono2b brain architect --json. The envelope always carriesmanifests: [{path, ecosystem, status, detail?}]sorted by path; text mode adds onemanifests not read: <path> (<status>), ...line only when some manifest was not read, with control characters in the path folded to a space.Terraform in the pre-extractor.
o2b brain pre-extractandbrain_ingest_sourcewithpre_extractread.tfand.tfvarsfiles:resource,data,module,variable,output,providerandlocalsblocks with identifier labels become entity seeds in Terraform address syntax (a label holding any other character opens no block), a modulesourcebecomes animportsseed (never resolved), single-linedepends_onlists becomedepends_onedges (the items of a multi-line list are seeded asreferences), andvar.,local.,module.,data.and same-file resource citations becomereferencesedges. Strings, heredocs and comments are skipped, and the$${and%%{escapes stay string text; a.tfvarsfile yields variable names, never values. The text output prints every seed on one line with control characters folded;--jsonkeeps the escaped value. The limits are named indocs/cli-reference.md(.hcland.tf.jsonfiles, multi-linedepends_on, nested blocks as entities,for_each/countexpansion and others).
Changed
Existing architecture notes are updated once after the upgrade. The first
o2b brain architectrun on an existing project appends themodule-dependenciesregion to the overview and adependenciesregion to each module note, adds thedepends_onkey to a module note with edges, and reports every such noteupdated; a later run with no change reports none.The generator owns the
depends_onkey on module notes. It rewrites the key on every run, removes it when a module has no edge and overwrites any value typed under it; every other frontmatter key is left alone. A module note whose frontmatter opens and never closes stops the run before anything is written, naming the note.A malformed root
package.jsonis reported asmalformed. It used to be read as no manifest at all; itsdetailis a fixed reason, not the parser's message.A dependency name that cannot be written into a note is counted, not listed. A declared name that is not a plausible package name (one holding a line break, a space or a bracket) shows only as an
unrepresentablecount on the group line of its ecosystem..tfand.tfvarssources are pre-extracted. They used to reportextracted: false; they now return block names and edges, never attribute values.Credentials in import specifiers are redacted. In every family's
importsseeds, the userinfo of an http(s),git+http(s)orgit::specifier (auser:passwordpair or a bare token) and the value of a named credential query parameter (sshkey,token,access_token,password,signature, the S3 and GCS access-key and signing parameters) are replaced by the redaction placeholder, the query parameter also on a specifier that is not a URL. A conventional login such asssh://git@is kept, and a credential in a path segment, a fragment or an unnamed query parameter is not recognised. A specifier longer than 2048 characters is replaced whole; specifiers without credentials are unchanged.The pre-extract pass on the ingest path reads a bounded source at the caller's reach.
brain_ingest_sourcewithpre_extractasks whether the caller may read the source before reading it, reads it through one descriptor, and skips a source that is not a regular file or is larger than 1 MiB with a reason that names the limit.A remote caller always gets the active digest as a render.
brain_context, theosb://preferences/activeresource andbrain_pre_compress_packhand a caller below local reach a render stamped with the file'sgenerated_at, without a preference or retired record that caller cannot read (its line, its count and its most-applied entry go, and the pack's top-K walk skips it), instead of the file's bytes. The resource also follows the owner gate underintegrity.owner_scope_delivery: fail, asbrain_contextdoes. A local caller with no owner scope still gets the file's own bytes.More readers treat a record the caller cannot read at its reach as absent.
brain_briefview="digest", theosb://digest/latestresource and theosb://lessonsresource leave out such a record, its rows, its counts, the log events naming it and their share of the agent summary, and a remote digest takes no report snapshot and shows no delta.brain_briefview="morning"leaves such a preference out of its list; below local reach it shows no pending-triggers section and marks no trigger delivered. Open questions keyed by a preference's topic follow the reach of that preference, and one whose topic has no preference file is shown.brain_healthdrops a finding any of whose members the caller cannot read, andbrain_doctorwithrepairplans, counts (unfixableincluded) and applies only from findings the caller can read. A local caller and the CLI see no change.The compiled digest pages are withheld from generic readers at remote reach.
brain_searchand every other tool that reads a page by path no longer returnBrain/active.mdorBrain/lessons.md, in any spelling the filesystem resolves to them, to a remote caller, because those pages compile records that may be reserved against remote reads; the active digest stays available remotely through its three readers above and the lessons digest throughosb://lessons. Local search is unchanged.A remote search with no match states no index counts. The answer of an empty
brain_searchand ofbrain_recall_gatebelow local reach carries no coverage receipt: anot_foundnames the index time only, and anunknownverdict gives one fixed, count-free reason for eachunknown_reason. A local caller and the CLI keep the receipt.Docs:
docs/how-it-works.mddescribes the manifest facts, statuses, regions and thedepends_onkey;docs/cli-reference.mdthe architect and pre-extract lines with the Terraform limits and the specifier redaction;docs/mcp.mdthe readers that answer at the caller's reach;docs/updating.mdgains "Upgrading to 1.68.0".
Fixed
Project fields and file names stay on one line in architecture notes. The project's name, version and description, module paths, file names and extensions are written on one line with control characters folded and
[[escaped, so a value read from a project can no longer end a generated region or open a link.o2b brain architect --helpnames--progress. The long help's usage line omitted the flag the verb accepts.An owner-scoped
brain_doctorrepair counts only that owner's findings.unfixableused to count findings over every owner's records while the fixes were already bounded.The scoped pre-compress head carries the digest's own stamp. It used to render with a fresh
generated_at.
1.67.0 - 2026-10-02
A distillation page now proves what it quotes and says how much of its source the vault holds: every quoted span in a claim is checked against the block it cites, or the whole source, at write time, a span that does not verify loses its quotation marks and is named in the result (or, on request, refuses the write with quote_unverified), and distillation, ingest summary and research report pages carry a capture_scope of full-local, bounded-local or url-only, with a default-on hygiene detector that warns when active knowledge rests only on url-only sources. Every link and mention reader now masks a fenced block up to its own closing fence, and a source the caller cannot read at its reach is treated as absent wherever a page cites one.
Added
Quotes in distilled claims are verified at write time.
brain_distill_sourceando2b brain distillcompare every span between quotation marks with the block the claim cites, or with the whole source when it cites none. Quotation marks are recognised by the UnicodeQuotation_Markproperty and paired by position, so no list of languages is involved: corner brackets in CJK running text pair, spaced guillemets pair across one no-break, narrow no-break or thin space, an apostrophe inside a word or closing a plural possessive (the students' work) is never a delimiter, an unclosed leading apostrophe ('90s) hides no later quote, and a quoted run with fewer than two letters or digits (rock 'n' roll) is not a quote. Both sides are normalised the same way for the comparison only (NFC, paired inline Markdown delimiters reduced to their text, a trailing^idremoved, quote variants folded, whitespace collapsed), and line-leading block markers are removed on the evidence side only, so a span's own leading3.,-or>is wording to match; case, punctuation and wording are never folded. A span must start and end on a word boundary of the evidence by the Unicode word rules, so"safe"does not verify insideunsafe, and an ellipsis splits a span into fragments that must appear in order, with an edge next to the ellipsis free to cut a word. A block-cited claim is never checked against the whole source: an unknown block id isblock-not-found, a duplicateblock-ambiguous. The comparison and block resolution run in linear time, and a source's blocks are indexed once per write.A failed quote is unquoted and named. A span that does not verify loses its two quotation marks, the words stay and the write lands; the result carries a
quotesreport (checked,verified_in_block,verified_in_source,unquoted,unpaired, cappedfindingswith claim index, outcome and a bounded span preview). The page recordsquotes_verifiedandquotes_unquotedin its frontmatter when a claim holds a span. Pages without quoted spans are unchanged.Strict quote refusal.
strict_quotes: trueonbrain_distill_sourceand--strict-quotesono2b brain distillrefuse the whole write when any span would be unquoted, and nothing is written. The MCP refusal isINVALID_PARAMSwitherror.data.codeset to the new registered codequote_unverified; its message names claim indices and outcomes, never claim text. The CLI exits 1.Capture scope on distilled, ingested and researched pages. Distillation pages, ingest summary pages and research reports record how much of their source the vault holds:
full-local(a file in the vault),bounded-local(no local bytes, but the page stores a verbatim excerpt with its digest) orurl-only(a URL, a path with no file behind it, or a vault file the caller cannot read at its reach). An extensionless note source (notes/meeting,[[Meeting]]) isfull-localwhen its.mdnote exists. The stamp (capture_scope, orcapture_scopesper source on a research report) is written only when it is notfull-local, so pages over local sources stay byte-identical.brain_distill_sourceandbrain_ingest_sourcereturncapture_scope,brain_research_reportreturnscapture_scopesin source order, ando2b brain distillprints[bounded-local](aurl-onlysource already prints[untrusted_source]) and[quotes verified:V unquoted:U], with--jsoncarryingcapture_scopeandquotes.A bounded excerpt for sources the vault does not hold.
excerptonbrain_distill_source(with a declaredmaxLength) and--excerpt-fileono2b brain distillstore the text the caller read from aurl-onlysource in a fenced## Excerptsection with anexcerpt_hash, make the pagebounded-local, and check its quotes against that excerpt. An excerpt for a local source, one that is blank or made only of control, format or whitespace characters, one holding NUL, or one over the byte cap is refused by name before anything is written; an--excerpt-filethat is not valid UTF-8 exits 2 withdistill: excerpt file is not valid UTF-8. A stored excerpt contributes no links or tags to the page that holds it.The
capture-scopehygiene detector. A default-onbrain_hygienedetector warns (proposed_action: review) when a retrievable page of active knowledge (a distillation, an ingest summary, a research report or a canonical entity) rests only on sources that areurl-onlyat scan time. It reads only the page's own last## Sourcessection, outside fenced blocks, and re-derives each cited source's scope from its identity at the caller's reach without reading bytes, so a deleted local source surfaces too; abounded-localpage counts as backed only while its excerpt still matches its digest, quarantined pages are skipped, and a cited source the filesystem refuses to stat counts as backing rather than ending the sweep. Thebrain_statushygiene count includes it.
Changed
Quotation marks in distilled claims may now be removed. A distillation whose claim quotes a span that the cited block or the source does not hold now lands with that span's quotation marks removed, and the result's
quotesreport names it; before, any quoted text was written as given. Setstrict_quotes(or--strict-quotes) to refuse such a write instead.New frontmatter keys appear only on pages that are not
full-local.capture_scope,capture_scopesandexcerpt_hashare written for abounded-localorurl-onlysource;quotes_verifiedandquotes_unquotedonly when a claim holds a quoted span. An existing distillation, ingest summary or research report over local sources is written byte-identical to before.A claim or a research title that spans more than one line is refused.
brain_distill_sourceando2b brain distillrefuse a claim that contains a line break (a claim is one bullet line) and accept at most 1000 claims per call;brain_research_reportrefuses a title that holds a line break (a title is the page's one heading line). Both used to be written as given.A fenced block is masked up to its own closing fence. Wikilink extraction (backlinks, unlinked mentions, the repair lane, the wikilink formatter) and search indexing of links and tags close a fenced block only on a run of its own character at least as long, the CommonMark rule, and stay linear on long runs. Links inside a tilde fence are no longer extracted, and a longer fence holding a shorter backtick run no longer leaks the rest of its content as links. An inline code span closes only on a backtick run of its own length, and a backtick run with no closer is plain text, so its last mark no longer masks the real links between it and an unrelated later backtick. A vault with such blocks may see different backlinks and link hits after the next index pass.
The default hygiene sweep includes
capture-scope. A defaultbrain_hygieneoro2b brain hygienescan may report new warnings on an existing vault whose active knowledge cites only URLs; callers that pin the list of detectors a default scan runs see the new id at the end.A cited source is asked about at the caller's reach before it is read.
brain_distill_source,brain_intake_entities,brain_ingest_source,brain_ingest_batch_planandbrain_research_reportdecide whether the caller may read a cited source on its resolved vault-relative path before it is stat-ed or read; a source the caller cannot read at its reach is treated as absent (untrusted lane, no digest,url-only, not planned, not recorded in the content manifest or the plan checkpoint). A caller that may read it still gets the size-ceiling or read refusal.brain_distill_sourcechecks quotes only against a source the caller may read (and, underintegrity.owner_scope_delivery: fail, its owner scope).More tools treat a page the caller cannot read at its reach as absent. The CLI is unchanged; at local reach every answer is as before.
brain_delete_by_sourceleaves such an original out of the plan,blast_radius,matchedandrecoverability, and a confirmed run withinclude_originalsleaves it on disk.brain_note_lifecycle,brain_append_note,brain_update_noteand the update and append operations ofbrain_write_batchrefuse such a page with the refusal a missing path gets (source_missing,target_missing), before any plan, reserved-key check or write;brain_create_notekeeps itsnote already existsrefusal on an occupied path and leaves the page untouched. At remote reach a note the host cannot read is also answered as missing; thetarget_unreadablediagnostic stays for local callers.brain_note_lifecyclenames and counts only files the caller may read in its reference report (inbound_files,files_scanned,files_rewritten,rewrite_failures, the delete cascade's lists and the recoverability derived from the rewrite). A cascade never deletes a file the caller cannot read; an applied move or rename still rewrites the links inside such files and reports nothing about them.brain_note_historyanswers for such a page as for a path no commit touches (commit_count: 0, no phases), before any history is read.brain_tiers checklists drift rows only for pages the caller may read, andrestoreandacceptrefuse any other page asnot indexedbefore reading or writing anything.brain_event_tracedrops an event that names such a page before thelimitand thetotalandtrace_totalcounts, and drops an attached trace whose handoff reference is withheld;brain_agent_queryandbrain_agent_diffwithhold the same contributions before the roster, topics and counts are built.brain_diarize,brain_design_noteand thebrain_dreamrollup step leave such a page out oflink_candidates, itstotaland the schema hint;brain_idea_discoveryleaves it out of its walk before ranking and capping;brain_procedural_memoryleaves such a procedure out oflistand thereconcilecounts, andmark_usedandmark_outcomerefuse its id as unknown before writing.brain_writes plan_revert, when it withholds an entry, returns adigestandnext_commandcomputed over the visible entries only;o2b brain writes revert --applyre-plans in full and refuses such a digest withdigest_mismatch.The
brain_hygienescan'slink_integrityblock reportsmeasured: falsewith reasonreachbelow local reach, because its counts are taken over the whole index, and thebrain_statushygiene problem count counts only the findingsbrain_hygienewould list for the same caller (owner scope and reach).brain_doctorand theschema_inspectlint,orphansandstatsviews leave out a Brain record the caller may not read, so a malformed private preference answers as an absent one;brain_doctorrestatesok,trust_verdictand its tier-drift count over what it keeps.
One source read feeds both the digest and the quote check. A source is read through one descriptor that refuses a final-component symlink, a non-regular file and growth past the 8 MiB ceiling after the size check, so a distillation's
source_hashand its quote verdicts always describe the same bytes; a source whose bytes are not valid UTF-8 verifies no quote (source-not-text) instead of being compared as mangled text.Docs:
docs/mcp.mdgains a "Source distillation" section (inputs, the quote check and its tolerance, unquoting, strict refusal, thequotesreport, capture scope and the excerpt) and thequote_unverifiedrow in the error-code table;docs/cli-reference.mddocumentso2b brain distillwith its flags and the new detector;docs/how-it-works.mdgains "Checked quotes and capture scope";docs/updating.mdnames what an existing vault sees after the upgrade; the README names this release.
Fixed
o2b brain distillcompletions andhelp --jsonlist its flags. The command manifest modelled no flags for the verb, so shell completion and the machine-readable help offered none; all six are now listed, and the usage line names--agent <name>.A path that resolves outside the vault is answered as absent without being opened. A caller-supplied path such as
../x.md(for example tobrain_note_historyorbrain_tiers), or a page reference a log entry carries (read bybrain_event_trace,brain_agent_queryandbrain_agent_diff), is no longer read from beside the vault at remote reach; such a request used to wait for good on a named pipe. The answers are unchanged: such a path already read as one no commit touches.
1.66.0 - 2026-10-01
A failed MCP call now says what failed in a token a client can branch on, and the optional cross-encoder rerank says why it stepped aside: every JSON-RPC error answer carries error.data.code and every isError tool result carries _meta["open-second-brain/error"].code, both drawn from one closed registry with the error prose left byte for byte as it was; a failed rerank endpoint is recorded on the retrieval trail as rerank-provider-unavailable with a typed category and is never cached, a model past its announced decommission date is skipped without a request (rerank-model-sunset), and o2b brain doctor checks the rerank configuration before a search has to fail on it.
Added
Every failed MCP call carries a stable code. A failure reaches the client on one of two channels, and each now names its code. On the JSON-RPC channel every error response carries
error.data.code: a code the refusing site chose (vault_frozen,budget_exceeded,unknown_argument, a search error code) wins, and otherwise the server fills in the default for the numeric code (parse_error,invalid_request,method_not_found,invalid_params,internal_error), after anydatamembers the answer already had. On the tool-result channel anisError: trueresult carries_meta["open-second-brain/error"] = { schema: "o2b.error.v1", code }, beside a progress refusal when the call has one, and never instructuredContent, which a strict client validates against the tool'soutputSchemaeven on an error. Every error message and text block reads as before, and a successful result never gains the envelope. The stdio and HTTP transport errors pass through the same single builder, so the rule holds by construction.One closed registry of wire codes.
TOOL_ERROR_CODESinsrc/mcp/tool-error-codes.tsis the union of the vocabularies the core already defines, passed through with their existing spelling (search errors stay UPPER_SNAKE, the write-binding, reach and owner-scope refusals stay kebab-case, thebrain_expirerefusals keep their class names), and 21 lower snake_case tokens of its own, nearly all for failures that had no code before: the five JSON-RPC defaults,safeguard_timeout,safeguard_aborted,output_contract_failed,config_unreadable,skill_not_found,skill_invalid_path,unknown_skill,unknown_operation,invalid_status,trigger_transition_refused,write_session_unknown,write_session_terminal,session_id_required,unknown_argument,brain_artifact_unparseableandargument_forbidden. Nothing already on the wire was renamed. A failure is classified by its error class over a closed table, never by reading an arbitrary.codeproperty, so an operating-system code such asENOENTnever reaches the wire; a class the table does not know is answered asinternal_errorand named on stderr by class name only (warning: unclassified tool error mapped to internal_error: <name>). A code a refusing site supplies outside the registry is still sent and is named on stderr, JSON-quoted and capped at 120 characters.Search, skill, workspace, write-session and argument refusals name their code.
SearchErrorcodes reacherror.data.codeonbrain_searchand the recall tools, where they used to be dropped or appended to the text only; skill tools answerunknown_skill,skill_not_foundorskill_invalid_path;brain_triggeranswersinvalid_statusandtrigger_transition_refused, with an absent trigger and one the caller may not see sharing one code and one message; the write-session tools answersession_id_required,write_session_unknownandwrite_session_terminal; the argument guard answersunknown_argument, and the reach and owner-scope refusals carrycaller-supplied-reach,foreign-ownerandunresolved-identity. Every tool that dispatches on anoperation,oporactionargument, abrain_write_batchoperation'sopincluded, answers an unknown one withunknown_operation; abrain_memory_bridgehost write keeps its existinginvalid_action. A Brain artifact that cannot be parsed answersbrain_artifact_unparseableinstead ofinternal_error.The Hermes bridge exposes the code.
BridgeErrorgains acodeattribute holdingerror.data.codeof a JSON-RPC error response, so Hermes can branch on the token without parsing the rendered message; it isNonefor a transport failure and for a response whose code is missing or not a string.str(err)is unchanged.A failed rerank endpoint is named on the retrieval trail. When the cross-encoder rerank endpoint cannot answer, the answer keeps the heuristic order as before, and the trail in
--jsonand onbrain_searchnow carriesrerank-provider-unavailablewithdetail.categorycomputed from the typed failure, never from the provider's message:auth(401, 403),quota(402),gone(404, 410),rejected(any other 4xx),transient(408, 429, 5xx),timeout,network(refused connection, DNS, TLS, a redirect, or a body that broke while it was read),malformed(not JSON, the wrong number of scores, an out-of-range or duplicate index) orunclassified. Thererank_degraded:warning stays the human signal.A rerank model past its announced decommission date is not called. A shipped rerank sunset survey, built on the embedding survey's shape and classifier, records the Cohere
rerank-english-v2.0andrerank-multilingual-v2.0shutdown of 2025-04-30 with its cited source; once a configuredopenai-compatmodel's date has passed, the stage sends no request and the trail carriesrerank-model-sunset. The ZeroEntropyzerankcheckpoints are recorded as open-weight negatives: their hosted API ended after 2026-09-04, but a self-hosted checkpoint keeps answering, so a dead hosted endpoint is reported asrerank-provider-unavailableinstead of being skipped.o2b brain doctorchecks the rerank configuration. Withsearch_rerank_enabledon and theopenai-compatkind, a config-only check that sends no request reportsrerank-endpoint-unconfigured(error) for a missing or blank base URL, model or key, a base URL the provider would refuse, or asearch_rerank_providerthat names no registered profile and so left one of them empty;rerank-model-sunset-announced(warning) for a model whose announced decommission date is 90 days away or fewer, or past; andrerank-model-sunset-unsurveyedorrerank-model-sunset-undetermined(uncertain) when the survey makes no statement or reaches no verdict. A base URL that carriesuser:password@credentials gets one fixed finding, and the doctor refuses it without repeating it: the configured value never appears in the finding, which shows<search_rerank_base_url>in its place.
Changed
Every JSON-RPC error answer carries
error.data. It is always an object with a stringcode, never absent and never an array; acodemember that is not a string is replaced by the default. A client that asserted"data" not in errorsees the change; existingdatamembers such astool,unknown_argumentsorerrorsare kept as they were.A failed rerank answer is served but never cached. The next identical query asks the endpoint again instead of replaying the failure. A
rerank-model-sunsetanswer depends only on the build and the date and is cached as usual; for an enabledopenai-compatrerank the query cache key now carries the survey's review date, so a corrected survey does not replay the old answer, and such an install sees one cache miss per query after the upgrade.argument_forbiddenand the threebrain_expirerefusals are registry members.brain_scaffold_stub'sargument_forbiddenandExpirationValueError,ExpirationTargetNotFoundErrorandInvalidExpirationTargetErrorkeep the spelling they already had on the wire.Docs:
docs/mcp.mdgains a "Tool errors" section with both channels, the generic code table and the casing rules;docs/cli-reference.mddocuments the two rerank trail codes, the category table, the missinghybrid-deadline-exceededrow and the rerank doctor check;docs/how-it-works.mddescribes the rerank trail and the sunset skip;docs/updating.mdnames what a client sees after the upgrade; the README names this release.
Fixed
A JSON-RPC refusal keeps the progress-token refusal. A
tools/callwith a progress token over a single-response transport that failed on the JSON-RPC channel gave no sign that its token was refused; the refusal now rides inerror.data["open-second-brain/progress"]besidecode, with the message and the numeric code unchanged.A throw that escapes the HTTP dispatch is answered like any internal error. The HTTP catch-all answered with the raw error message; it now answers
internal error: <message>with the message passed through the redactor for the bind's reach and a classifieddata.code, the same builder the dispatcher's own internal-error answer uses.The rerank timeout covers the response body. An endpoint that sent its headers and then stalled the body held the search past the rerank timeout, and a caller's cancellation during the stall was not honoured; the timeout and the cancellation now cover the body read, answering
timeoutor the caller's own reason, with the messages unchanged.o2b brain doctorno longer prints the plain-http rerank warning. Withsearch_rerank_allow_insecure_httpset, the rerank check printed the runtime's once-per-process plain-http warning, so a later search in the same process sent vault text without it; the check now answers that case from the configuration alone.
1.65.0 - 2026-10-01
The maintenance lane becomes something an unattended host can schedule and extend, and the MCP server survives the background faults that used to end it: o2b brain maintenance run --cron-template prints the recipe that schedules the lane, every recipe can be printed as a systemd user timer, an install can put its own upkeep commands under the lane's gates as custom:<name> tasks, and a served o2b mcp names and counts a stray promise rejection instead of dying on it, while an uncaught exception still exits, through the hooks that checkpoint the index and release the locks.
Added
o2b brain maintenance run --cron-templateprints the lane's schedule. The lane was designed as the cron entry point and had no recipe of its own.run --cron-template [--interval <N>m|h|d] [--format cron|systemd] [--window H-H --tz ZONE]prints a script (~/.local/bin/osb-maintenance-<hash>.sh, where<hash>is the first 8 hex characters of the SHA-256 of the resolved vault path, so each vault gets its own script, Hermes job and systemd units) and its scheduler lines, default interval1h: the lane's window, busy, pressure and lease gates decide whether work happens and a gate skip exits 0, so an hourly schedule plus--windowis the intended pattern. The script embeds the resolved vault, single-quote escaped, because cron runs without the operator's environment; it runso2b brain maintenance run --vault '<vault>' --json, stays silent on exit 0 and prints the captured JSON and keeps the exit code on any other exit, so cron mail or a Hermes job surfaces a failure, a timeout (6) or a refusal (7). It prints and installs nothing: it returns before the lease, the gates, the journal and the metrics. The window and the zone are pasted into the script's command line, so the renderer itself refuses a window that is notH-Hwith hours 0..23 and a zone that is not a plain zone name, rather than printing shell text; it also refuses a zoneIntldoes not know and a vault path with a line break, by name, and its verify line names the vault. A bad interval, window or format,--intervalor--formatwithout--cron-template, andstatus --cron-templateexit with the lane's usage code 2.Every recipe prints a systemd user timer with
--format systemd.o2b search reindex --cron-template,o2b partner codegraph resync --cron-templateand the new maintenance recipe take--format cron|systemd(defaultcron, whose output is byte-identical to before).systemdprints the same script section plus a.service/.timerpair to save under~/.config/systemd/user/(OnBootSec=,OnUnitActiveSec=<N><unit>; noPersistent=, which systemd ignores on a monotonic timer), thesystemctl --user daemon-reloadandenable --nowcommands, theloginctl enable-lingernote and the same verify footer. One spec feeds both renderers, so a recipe cannot say one thing in cron and another in systemd; an interval the cron grammar refuses is refused with the same message in both formats, and an unknown format is refused namingcronandsystemd.Install-owned custom lane tasks. An install can declare its own upkeep commands in the machine config file:
maintenance_custom_<name>: <command>, with optionalmaintenance_custom_<name>_cwd(default the running user's home directory, never the vault unless named) andmaintenance_custom_<name>_timeout_seconds(default 120, a whole number from 1 to 1200); the declared timeouts together stay within 1200 s, leaving 600 s of the 30-minute lease for the built-in tasks, and a task over that budget is refused by name. They run only with the master switchmaintenance_custom_tasks: true(envOPEN_SECOND_BRAIN_MAINTENANCE_CUSTOM_TASKS, default off,0turns it off on one host), after the four built-in tasks, under the same window, busy, pressure, lease and streak gates, and they journal ascustom:<name>. A task runs throughsh -c(cmd.exe /d /s /con Windows) withO2B_VAULTset, stdin closed, stdout discarded and every variable whose name declares a credential dropped from its environment; a_cwdthat does not exist or is not a directory fails the task by name before anything is spawned; a non-zero exit fails the task asexit <N>with the redacted tail of its stderr, capped at 4096 bytes, and the outcome is the shell's exit status: background processes the command leaves are killed with its process group (POSIX) at its timeout or when the o2b process exits. A custom task makes no model call and records no spend receipt. Commands are never read from the vault, which syncs between devices and is writable through the MCP write tools, and no MCP parameter can add, edit or read one.--retry custom:<name>andretry_tasksaccept a declared task; at most 8 can be declared./healthon the HTTP transport carries the server's fault counts. Afaultsblock (unhandled_rejection,uncaught_exception,last_fault_at) joins the drain fields, so a supervisor can see a server that is surviving rejections before an operator reads its log.
Changed
A served
o2b mcpsurvives an unhandled promise rejection. Both served transports install a fault guard: a rejection nobody awaited is logged by name on stderr ([mcp] unhandled_rejection #N: <reason> (server keeps serving)with the first stack frame, a non-Error reason named too, tokens and URL credentials masked in every line the guard writes), rate-limited to 5 full lines per 60 seconds and then one summary line, counted, and the server goes on answering. An uncaught exception is logged by name and exits 70: the process state is unknown after one, so the server does not keep serving, and it exits rather than dying so theexithooks still checkpoint the search index and release the locks. It does not drain first, because a drain would keep serving from that unknown state.o2b mcp --probeinstalls no guard.One task builder serves both maintenance surfaces. The CLI verb and
brain_maintenanceused two hand-kept copies of the dream, reindex, bridges and clusters task bodies; both now build their tasks, including declared custom tasks, from one shared builder, so the two surfaces cannot drift apart again.--retryandretry_tasksvalidate against the tasks this install actually registered, and a request longer than that is refused with the count; theretry_tasksschemamaxItemsadvertises the built-ins plus the custom-task cap.brain_maintenancereturnscustom_task_errors. A refused custom declaration (a bad name, an empty command, an out-of-range timeout, a relative_cwd, a_cwdor_timeout_secondskey without its command, a ninth task, a timeout over the budget) is named there, and on the CLI ascustom task refused: <reason>on stderr; the valid tasks still run.statussayscustom tasks declared but maintenance_custom_tasks is offwhen keys are declared and the switch is off, and namesOPEN_SECOND_BRAIN_MAINTENANCE_CUSTOM_TASKSwhen the environment turned it off.A timeout counts toward a custom task's failure streak. A built-in task keeps the rule that a safeguard timeout neither counts toward nor resets its streak, because its budget is per operation and a slow vault is not a broken task; a custom command that always hangs would otherwise never be refused and would spend its whole timeout of lease time on every pass.
Docs:
docs/cli-reference.mddocuments the maintenance recipe,--format cron|systemdon every recipe, custom lane tasks, the Hermes jobs path and exit code 70 ofo2b mcp;docs/mcp.mdgains a "Background faults" section and documents custom tasks onbrain_maintenance;docs/updating.mdnames the new opt-ins;docs/hermes-cron.mdpoints at the maintenance recipe and the systemd format; the README names this release.
Fixed
The codegraph resync recipe single-quotes the project path.
o2b partner codegraph resync --cron-templatenow quotes the repository path like the other recipe arguments.o2b discipline installworks on hosts where Hermes does not run as root. The default jobs file was fixed at/root/.hermes/cron/jobs.json; it is now~/.hermes/cron/jobs.jsonof the running user, still overridden byOSB_HERMES_JOBS, and a home directory that cannot be resolved is a named error that points atOSB_HERMES_JOBS. Root hosts see no change. An emptyOSB_HERMES_JOBSnow reads as unset instead of as a path.
1.64.0 - 2026-10-01
Search ranks by what the operator marked and remembers when things happened, diagnostics prove that this machine can still act instead of only that its files look right, and the write path names the residue it lands: the index persists each document's resolved event-time window and pinned flag (schema v13), a bounded pinned boost joins ranking beside two opt-in guards and one wall-clock deadline over the composite hybrid path, o2b doctor selftest drives a throwaway store end to end, --readiness probes the commands the installer registered and the write-back contract the workspace instruction files carry, the maintenance lane distinguishes a pass that failed from one that did not finish and, once opted in, announces and receipts its embedding spend, and write receipts name near-duplicate bodies while hygiene scans report slug-collision residue and inline-tag drift.
Added
The index remembers when each document's events happened. The indexer persists every document's resolved event-time window - the shared rung order (frontmatter validity window, then the event anchor), now spelled once for index time and query time so the two cannot drift - into two new
documentscolumns (event_time_min/event_time_max, schema v13), and the store answers how many documents fall in a window as one SQL aggregate over those bounds: declared, intersecting, and anmtimeFallbackbucket that counts the rows with no persisted window rather than pretending they are none.o2b search statusreports the persisted event-time windows: anevent_timeline (JSONevent_timeblock withdocuments,with_event_time,earliest,latest,recent_window_days,in_recent_window) counts the documents that carry a window, the span those windows cover, and how many intersect the last 30 days. One side NULL is a declared open window; the migration is additive and reindex-safe, with existing rows refreshed lazily on their next content change and a NULL window still judged by storage mtime at query time, exactly as before.A page whose frontmatter carries
pinned: trueearns a bounded ranking boost. The flag is persisted at index time intodocuments.pinned(same schema and lazy story as the window bounds), and the composite rerank gains a pinned layer capped at 0.05 - the reinforce scale, a bounded nudge over an already-relevant candidate set that never overrides BM25's lead. Only the candidates' pinned rows are read; an unpinned result, measured or not, adds nothing and carries nopinnedbreakdown key, the boost names itself in the result's reasons and breakdown, and the layer is always-on rather than config-gated: a bounded signal over explicit operator state, inspectable through its reason strings.Two opt-in guards keep a rescore from overriding evidence.
search_relational_rerank_pin(envOPEN_SECOND_BRAIN_SEARCH_RELATIONAL_RERANK_PIN, default off) lets rerank promote a relational-origin candidate but never sink it below its pre-rerank heuristic order - a protect rule at the cross-encoder hand-off, not a second floor: the relevance floor and the keyword lane's own ordering are untouched.search_metadata_boost_gate(envOPEN_SECOND_BRAIN_SEARCH_METADATA_BOOST_GATE, default off) reduces every additive metadata and structural boost layer (link with its tag half, freshness, entity, activation, co-access, observed reuse, temporal intent, session focus) to exactly zero for a query whose keyword lane returned no hits, so a vector-only answer is never floated by layers calibrated over a lexical candidate set; the relevance terms, the tier and trend multipliers and the pinned layer are not metadata boosts and stay live, and a gated run names its suppressed layers inreasons/breakdown. Either off leaves every score byte-identical.The composite hybrid path has one wall-clock deadline. The named lane budgets (embedding timeout, rerank budget) summed, so a slow semantic path could stack waits into a stall.
search_hybrid_deadline_ms(envOPEN_SECOND_BRAIN_SEARCH_HYBRID_DEADLINE, default 15000 ms,0disables) bounds embed, semantic top-k, rerank and the second pass as a whole; on expiry the semantic lane and the rerank call are raced, abandoned and aborted, the unbudgeted second-pass retry is skipped at its checkpoints, and the search completes with what it has (keyword-only when the semantic lane did not finish, the pre-rerank order when the rerank did not) while reporting the degradation (hybridDeadlineExceeded), so the caller sees what it did not wait for.o2b doctor selftestproves the store, not just the configuration. Plaino2b doctorcan pass on a machine that cannot init, migrate, index, search or survive concurrent writes. The new verb drives a real throwaway store end to end - open and migrate, an index pass, a document upsert, keyword and trigram search, a concurrent-write roundtrip through the real writer lock, delete, close - stopping at the first failure with one doctor-check-shaped entry per stage that names the fault and the fix. It never opens the configured vault (every write path asserts vault identity first; the store is pinned inside the temp tree and removed on every exit path), the semantic lane is forced off because the harness is network-free (o2b search checkstays the judge of semantic capability), and--jsoncarries no timestamps, paths or durations, so two healthy runs on one machine stringify byte-identically. Exit codes reuse the doctor vocabulary: 0 every stage passed, 1 a stage failed, 6 the harness itself could not run and established nothing.o2b doctor --readinessprobes what the installer registered and what the workspace promises.registered_commandsre-reads the install manifest and every recorded client config, extracts the Open Second Brain command and arguments each install wrote (JSONmcpServers/mcpkeys and the[mcp_servers.*]TOML tables), and probes each command word: a proved-absent absolute path fails with the client named and the exacto2b install <target> --applyrecovery line, while a bare name this process's PATH does not carry reports unknown - the host client spawns with its own PATH, which may still resolve it - a relative path is unresolved, because the client resolves it from its own working directory, and a recorded config that is gone is unknown, never broken.writeback_contractaudits every workspace agent-instruction file that exists (AGENTS.md,CLAUDE.md,GEMINI.md) for the marker-delimited managed block whose body states the same-turn atomic-fact write gate, reporting conforming, missing-block, malformed-block, missing-clauses, absent, symlink or unreadable, with the missing clause named; a file without the block is graded skipped, because the block is not installed, while a broken or incomplete block fails. Both probes are read-only, and a symlink is refused without being followed even for the read.o2b brain doctorreports a danglingmerged_into:pointer. A de-canonicalised page points at its canonical through amerged_into:field, and every duplicate detector asks only the pointer-presence question - so a page whose canonical was later deleted dropped out of dedup candidacy forever, with nothing named why. The newmerge-chain-danglingcheck walks the chains, including ones that crossBrain/retired/, and reports exactly the dangling class: cycle, depth and malformed intermediates already surface ino2b brain lint --consolidate's unresolved list, and a second code for them would duplicate a finding an operator can already see. The repair is a content judgement - re-point the page at a surviving canonical, remove the pointer to un-merge, or restore the deleted file - so the finding prints that judgement rather than a next command, and no fixer exists for it.The maintenance lane can opt into its embedding phase, and then announces and receipts the spend. With
maintenance_embeddings: true(envOPEN_SECOND_BRAIN_MAINTENANCE_EMBEDDINGS, default off) the quiet-window reindex requests the embedding phase when the resolved semantic config can reach a provider, on the CLI lane and the MCP lane alike; without it the lane stays keyword-only and contacts no provider. The pass announces its predicted spend before it runs, and journals the phase's own cost-gate result as the per-run receipt - on the task row, the journal line and themaintenance_spendmetrics surface - carrying model, tokens, estimated cost and whether a bypass fired. The preview is an estimate by position; the receipt prices what the completed pass actually embedded, and a run killed mid-spend receipts nothing, its row carrying the named failure instead. A positiveembedding_cost_gate_usdrefuses the phase unless--force-costis set (MCPbrain_maintenancetakesforce_cost), and a bypass that overrode a refusing gate says so on the receipt.A near-duplicate authored body is named on the write receipt. Exact duplicates were caught at the write; close ones landed silently. The write-time page lint gains a
near-duplicatefinding: a Jaccard score of at least 0.8 over the authored body against an existing page in the same directory and scope bucket, carried on thelintkey of every note-write receipt. The score is computed over the body exactly as authored, frontmatter is never compared or touched, the finding never gates the write, at most the 200 newest sibling pages are read, with the rest counted ascandidates_skippedand an unreadable sibling named incandidates_unreadable, and a caller-supplied../spelling cannot walk the candidate search outside the vault.Hygiene scans name slug-collision residue, and an opt-in audit reads inline tags. Writes cannot clobber - allocation runs the exclusive-create suffix ladder and caller-named writes refuse occupied destinations - but the residue an operator sees anyway,
topic.mdbesidetopic-2.mdwith nothing saying both were one intended slug, was invisible. The newslug-collisionsdetector reports one info finding per same-stem group within a directory (the ladder suffixes-2,-3, ... stripped; a group needs its bare-base member, a date-named slug is a whole name, and cross-directory same-stem is not a collision), is default-on, and never re-runs allocation logic or mutates anything. The opt-intagsdetector audits inline body tags through the one shared rule the link table extracts with:malformedtokens the looser Obsidian-compatible rule accepts but the index rule rejects (so they are silently absent from the link table),inconsistentcase and hierarchy families (#Foovs#foo,#a/bvs#a) grouped one finding per family, andorphantags carried by exactly one document - all info; frontmattertags:arrays are out of scope by design, and the default sweep is unchanged for vaults that do not ask for the audit. The scan's--detectorslist, the MCP enum and the help text are derived from one detector registry, so the next registration is the same small change again.A captured idea stages its area-hub candidate at inbox drain. A routed idea used to land without its area link, and the MOC audit only noticed the missing edge after the fact. On an applied idea route, the page's scope bucket is matched against structurally validated hubs (one shared hub definition with the MOC audit) by the unique-match rule: exactly one hub stages an
area_membershipcandidate in the repair lane's apply-gated store, zero stage askip-no-hubrefusal and several stageskip-ambiguous-hublisting every candidate - no degree tie-break, no new write path.o2b brain repair-lanemerges the staged candidates and refusals with the graph-collected ones before planning, so a hub proposal made at intake reaches the lane's own apply + confirm + holdout gate; a store failure aborts the drain loudly before the archive, and the still-staged capture makes a rerun recover it. The store keeps one record per routed page, prunes records whose page is gone, and is declared as therepair_candidatesstate surface.
Changed
A maintenance task killed at its safeguard deadline exits 6, not 1. The timeout already names itself, and its budget, in the task row's error; collapsing it into the generic failure told a nightly cron that the pass is broken when the only proved fact is that it did not finish.
o2b brain maintenanceandbrain_maintenancenow distinguishprobeIncomplete(6), keyed ontimed_outrows alone, from failed (1) and refused (7), with the same precedence the doctor uses: a proved failure outranks a timeout, a timeout outranks a refusal, and the render says TIMED OUT with the safeguard detail.The maintenance lane (
o2b brain maintenance run,brain_maintenance) stays keyword-only by default. Setmaintenance_embeddings: true(envOPEN_SECOND_BRAIN_MAINTENANCE_EMBEDDINGS) to let its reindex run the embedding phase; the embedding cost gate still applies, and the spend is announced and receipted as before.writeback_contractreadiness grades an agent-instruction file without the managed block as skipped (the block is not installed). Soo2b doctor --readinessexits 0 for a plain AGENTS.md, CLAUDE.md or GEMINI.md; a broken block (malformed-block) or one missing its clauses still fails.registered_commandsgrades a relative command path as unresolved instead of absent.Docs:
docs/cli-reference.mddocumentso2b doctor selftest, the two new readiness probes, the maintenance exit codes and--force-cost, the extended detector list with its default sweep, the staged hub records, the three new search config keys,maintenance_embeddingsand theevent_timeline ofo2b search status;docs/mcp.mddocumentsforce_cost, themaintenance_embeddingsopt-in and the spend block onbrain_maintenance, the widened detector set onbrain_hygieneand the near-duplicate finding on the write receipts;docs/updating.mdnames the opt-in for the maintenance lane's embedding phase; the README names this release.
Fixed
A staged rebuild under an unnamed embedding model never resumes. The staging marker's embedding identity is compared through the same unknown-identity rule as the stored signature, so an unnamed model is compatible with nothing, not even its own interrupted build.
Staged reindex resumes on default embedding configs. A null
embedding_dimensionno longer makes every staged rebuild restart: the local embedder resolves to its fixed default and a named remote model to its native width; only an unnamed model stays an unknown identity that never resumes.A model clear can no longer destroy vectors the current config cannot rebuild. When stored embeddings exist and the configured tier is blocked (a missing credential, a disabled provider), both embedding-model clear paths keep the vectors and the recorded model, log the refusal under the tier's error code (
EMBEDDING_KEY_MISSING/EMBEDDING_DISABLED), and open the store anyway, so keyword indexing, watch and MCP writes go on. The named-to-null model transition, previously silent, now warns naming the last recorded model; a local provider's implicit model is excluded from the warning, because that transition is its healthy steady state.The hybrid deadline cuts only the rerank call and stops what it abandons. Past
search_hybrid_deadline_msthe answer keeps the caller's exclusions, the reach filter, the retrieval trust gate, supersede fade, relation polarity and reinforce on the pre-rerank order. The query embed, the cross-encoder request and the decision-model rerank request are aborted, not left running, and a deadline-degraded answer is never written to the query cache.A maintenance task stopped by its safeguard deadline no longer counts toward the failure streak that refuses it.
A maintenance run that a gate skips no longer reports a spend banner over MCP; a failed spend-metric write is named as
spend.receipt_metric_error.The slug-collisions detector no longer flags date-named notes or numbered series without a bare base.
The inbox drain parses the vault once per drain, keeps one staged hub record per page, prunes records for deleted pages, and reports a corrupt store line as
skipped_corruptinstead of failing.The write-time near-duplicate lint reads at most the 200 newest sibling pages and reports the rest as
candidates_skipped, naming unreadable siblings incandidates_unreadable.
1.63.0 - 2026-09-30
Integrity and control on the agent-facing memory surface: a prose mention binds to a page only when exactly one page carries the term, relative imports and JSX component usage become code-structure edges, a brain_write_batch call can carry a request ID that makes a retry apply once, keyword search gets a selectable match breadth, and every append-only Brain ledger that several synced machines write now keeps one file per device.
Added
The repair lane binds alias mentions, and only to a single page.
o2b brain repair-lanepools each corpus page's title with every frontmatter alias that page claims. A note that mentions a term (title or alias, at least four characters, at a word boundary) carried by exactly one page gets the explicit-reference candidate a title mention always had, with the same 0.9 confidence, and the reason names the term that matched. Threshold, ordering, write cap, dry-run default and idempotent reruns are unchanged.Relative imports bind to the ingested file they name. With
pre_extractonbrain_ingest_source, animportsseed whose specifier is relative (./or../in TypeScript and JavaScript, a leading-dotfrom .x importin Python) gainsresolved_to(resolvedToon the core result), the vault-relative path of the one ingested file it names, checked against the ingest content manifest. TypeScript and JavaScript probe the path as written, then each TypeScript/JavaScript extension and/indexplus each; Python probes<name>.pyand<name>/__init__.py(a dots-only specifier only the package__init__.py). No match, several matches or a specifier that climbs above the vault root leave the seed exactly as before,toalways keeps the raw specifier, and a Python import never binds to a TypeScript file or the reverse.o2b brain pre-extractreads no manifest and never binds.JSX component usage edges. In
.tsxand.jsxsources the pre-extract pass emits ausesedge from the file to each component an opening tag names. The rule is structural, with no tag vocabulary: only names starting with an uppercase letter,_or$count, so DOM tags (<div>), member-expression tags (<Nav.Item>), type-argument lists (Array<Foo>), type-parameter lists (<T,>,<K extends string>) and generic call signatures (<T>(value: T) => T) produce nothing. As a line grammar it misses a generic component (<Foo<T> />) and an element whose text opens with(on the same line, and closing tags never match. Thepre_extractdescription onbrain_ingest_sourcenow namesusesedges andresolved_to.brain_write_batchrequest receipts. An optionalrequest_id(requestIdonapplyWriteBatch) makes a retried batch apply once. The result then carriesrequest_idandreceipt:appliedwhen this call committed;duplicatewhen the same ID with the same operations was already recorded (the retained original result comes back, nothing is written, re-validated or linted);concurrent_duplicatewhen this call committed but a concurrent call recorded the same ID and payload in between (both writes landed; the fields are this call's own); andpayload_conflictwhen this call committed but a concurrent call recorded the same ID for a different payload in between (the writes landed, this call's receipt is not recorded, and areceipt_notesays not to resend the batch under a newrequest_id). The same ID with different operations found before the commit is refused asINVALID_PARAMSwith the idempotency ledger's mismatch message, a blank ID or one over 256 characters after trimming is refused asINVALID_PARAMSbefore any write, and an ID already recorded without a readable batch receipt is refused by name rather than answered with an empty result. The receipt is recorded in the idempotency ledger after the commit under its ownwrite_batchnamespace, so a request ID never collides with abrain_feedbackor apply-evidenceidempotency_keyof the same text; records without a namespace read exactly as before. The stored receipt keeps log paths vault-relative, and a duplicate rebuilds them under the vault serving the retry. While a synced peer still runs 1.62.0, which ignores the key space, that peer refuses a feedback key equal to a synced batch request ID with the named idempotency mismatch error rather than deduplicating it. Withoutrequest_idthe call and its payload are unchanged.Keyword match breadth.
brain_searchacceptsmatch_mode(all, the default, orany), the search APImatchMode, ando2b search--match-mode all|any.anyjoins the cleaned query tokens withOR, so a document carrying any one term is a keyword candidate; caller-typedAND,OR,NOTandNEARtokens are dropped in both modes, and query expansion composes with either. Any other value is refused with the accepted list (a CLI error,INVALID_PARAMSon MCP,INVALID_INPUTfrom the API). The query cache keys ananysearch apart from the default, so neither mode is served the other's rows. Absent, the query and its cache key are byte-identical to before; the semantic lane is unaffected.
Changed
The
brain_searchdescription tells the agent when to widen. It now ends with one sentence: when a query misses, consultbrain_recall_gatebefore widening recall, since it classifies whether widening is warranted.o2b brain doctorsweeps the newly sharded directories for Syncthing conflict copies. Thesync-conflict-logcheck now also listsBrain/log/session-lifecycle/,hygiene/,secret-custody/,watchdog/andschema-mutations/,.open-second-brain/hook-audit/and.open-second-brain/watchdog-audit/,Brain/skill-proposals/,Brain/preferences/and each repository directory underBrain/projects/git/, with the same union-and-dedup instruction. In the directories that also hold whole files (Brain/skill-proposals/,Brain/preferences/and the git repository directories), a conflict copy that is not a.jsonlshard, such as a proposal file, a preference note or a repositorystate.json, is reported under the samesync-conflict-logcode with its own remedy: compare it with the original, keep the right version by hand, then delete the copy. A parent it cannot list is reported underuncertainby name, never read as clean.o2b state statussees sharded ledgers. The query-demand, recurrence-support, capture-decision and maintenance-journal surfaces, and the session lineage ledger, report present when the legacy file or any device shard exists, so a device with an id that never writes the legacy name no longer reads as absent. Conflict copies never count.A source-layout test keeps append-only writers on the shard grammar. A file that appends with
appendFileSyncoropenSync(..., "a")fails the suite unless it uses the shard helpers or sits on a named allow-list with a reason (such as the per-run dream workrun files and the doctor repair that appends to them).--disclosureanddisclosurerefusals list the accepted modes from one shared vocabulary, the same waymatch_modedoes.Docs:
docs/mcp.mddocumentsrequest_idand the receipt,match_mode, the search cue and the widened doctor sweep;docs/cli-reference.mdlistso2b search --match-mode, theusesedge and the repair lane'sskip-ambiguousdecision;docs/observability.mdanddocs/how-it-works.mddescribe the per-device audit weeks and fixed-name ledgers; the README names this release.
Fixed
A mention of a term several pages carry no longer links all of them. The repair lane used to propose an edge from a note to every page whose title it mentioned, so a title two pages share produced two edges. Such a term, title or an alias two pages claim, now binds to none: every (mentioning page, carrying page) pair comes back as a named
skip-ambiguousdecision in the dry-run and apply reports (and under--json), which is never written, never counted against the write cap and never sent to the apply holdout gate. A note that already links one of the carriers has resolved the mention itself and is not refused, and a page naming its own shared title is a self-reference that gets no refusal.Two synced machines no longer split an audit week or a fixed-name ledger into conflict copies. Every writer of an ISO-week audit file (session lifecycle, hygiene and targeted recompiles, schema mutations, secret custody, the watchdog and its fallback directory, and the hook audit) now appends to
<ISO-week>[.<device-id>].jsonl, and the fixed-name ledgers follow the same grammar:Brain/log/query-demand,recurrence-supportandcapture-decisions,Brain/skill-proposals/verifier-rejections,Brain/preferences/pref-<slug>.history,Brain/projects/git/<repo>/commitsand.open-second-brain/maintenance-runs, each<stem>[.<device-id>].jsonl. The empty device id keeps every legacy name, so existing vaults need no migration. Readers merge every shard and never parse a*.sync-conflict-*copy: the maintenance journal, the git store and the recurrence-support ledger interleave shards by row time without reordering any one shard (so a purge on one device is never replayed before another device's earlier event), the git store keeps one copy of each commit and tag two devices both ingested, the query-demand read keeps its timestamp order, and the schema-pack digest reads every week shard. Query-demand compaction and the maintenance-journal cap rewrite only the shard this device writes, so the journal cap now applies per device.The release stage script compares asset names in byte order. The expected asset list now sorts under
LC_ALL=C, the orderjquses, so names such as-source.svgand[email protected]no longer fail the draft check.
1.62.0 - 2026-09-29
Wider egress redaction, honest write receipts, an ingest plan that accounts for what it sets aside, a search store that keeps its journal mode honest on remote filesystems, and two more brain-doctor checks with a confirm-gated repair verb.
Fixed
URL credentials whose username is empty are now redacted. The URL pass required a username, so the password-only spelling connection strings use (
scheme://:pass@host) was left verbatim wherever the pass runs, including the export boundary where it is on by default. The username half is optional now; the colon between the halves stays mandatory and adjacent, so a URL whose path carries an@but no userinfo colon is untouched, and the pass stays linear.URL credentials whose password carries a slash are now redacted. The password class stopped at a slash, and one unmatched character failed the whole match - the username was left verbatim too. The class now runs to the
@anchor, so a generated database password with a slash is redacted wherever the URL pass runs. A colon followed by a port and a path is never read as the userinfo colon, so a URL such ashttp://localhost:5173/@vite/clientorhttps://registry.npmjs.org:443/@types/nodestays untouched (a slash password that itself begins with 1-5 digits is the known miss, as on main).Bare JSON Web Tokens are redacted by default. A JWT pass runs next to the bearer pass on every surface the shared redactor guards: it is anchored on the header prefix - compact JSON (
eyJ) or a header with whitespace after the brace (eyA,ewo,ew0,ewk) - with segments bounded at 64 KiB each, so a bare token - not only aBearer-prefixed one - is redacted.A secret named beside its value is redacted in structured data. The structured walk reads a
{name, value}pair whose sibling name names a secret - an environment block such as{"name": "DB_PASSWORD", "value": "..."}- and redacts the value.A byte-identical note rewrite no longer bumps the note's mtime. The atomic rename landed a fresh inode on every write, so re-applying identical bytes moved the file's
mtimewhile the receipt claimedupdated: truefor a write that changed nothing - poisoning recency ranking, validity windows and vault-delta counts. Both write chokepoints now skip a byte-identical rewrite, andupdated/appendedcarry the write's own verdict: a skipped write stores no before-image and records no note-write event. The skip is decided before anything is written, so a rewrite that does change the bytes still stores its before-image before the rename, and a failed store leaves the note untouched.The search index opens with journal mode DELETE on a remote filesystem, and says so once. An index file on a network filesystem (on Linux nfs, cifs, SMB2/SMB3, smbfs, ceph, afs and 9p; on Windows a UNC path; macOS is not probed) opens without WAL, and the write open prints one clear warning naming the filesystem and the slower writes; every later open makes the same decision instead of enabling WAL there. Leaving WAL needs exclusive access, so while another connection has an index in WAL open the switch is kept for later instead of failing: the read open stays in WAL silently (never an
INDEX_UNREADABLEthat would trigger a reindex), and the write open keeps WAL, warns once that it stays until other connections close, and retries on its next open.A batch write lints only the pages it actually committed.
brain_write_batchnamed a page for the write-time lint whenever a result carried a path, which included skipped rewrites; a skipped operation now names no page, matching the single-write receipts.
Changed
Docs:
docs/cli-reference.mdlistso2b brain orphan-repair;docs/mcp.mddocuments the plan'sskip_reason_countsandunclassifiableaggregates, the optionalpath_advisoryon note receipts, and the new doctor findings;docs/observability.mdanddocs/how-it-works.mddescribe the before-image ordering and the journal-mode decision as the code now makes them.
Added
A note write receipt can carry a non-blocking
path_advisory. An authored note body that mentions an absolute host path is advised on the receipt (frontmatter values are never scanned) through the hygiene scan's grammar, as-is; the key is absent when the content is clean, log-line operations are never scanned, and the advisory never blocks the write.The ingest plan accounts for what it sets aside. Every file the extractable gate sets aside (
skipped_non_extractable) carries a reason from a closed, typed set and the value behind it (the legacy free-text sentence still parses, throughparseSkippedPageReason; the type guard accepts members only), whileskippedstays a list of unchanged paths; the plan aggregates per-reason counts, and extensionless regular files nothing can classify are counted by extension; both aggregates appear on the MCP plan and the CLI rendering only when non-empty, so a plan with nothing set aside serializes exactly as before.o2b brain doctorchecks embedding health. A fail-soft, read-only check reports an embedding backlog, an unrecorded census and an unmeasured health verdict; an unrecorded census lands inuncertain, never in healthy. The stale count resolves the dimension the way the indexer does (configured, then the local embedder's default, then the stored value) and compares by model alone when none resolves, so a model switch on a default configuration is counted - ino2b search statustoo; a provider with no configured model isuncertain, and the check reads the index the doctor pass was pointed at.o2b brain doctorreports orphaned session references, ando2b brain orphan-repairdetaches them. An observation signal whosesession_refresolves against neither the continuity ledger nor the Brain log's session lifecycle is a warning that names the repair verb on itsfixfield. A ref written byo2b brain sessions import(<transcript basename>#<turn>) points at a transcript outside the vault that the import keeps no guaranteed record of, so it is never judged; the capture form and the extract-signals form are. A log day or shard the pass could not read makes the check reportuncertainand no finding, because a session whose lifecycle lived there would otherwise read as an orphan. The verb removes only thesession_refkey, and only while it still holds the value the scan resolved under the signal's per-file lock (a reference rewritten in between is reportedskip-changed, a contended lockskip-locked) - the observation body, topic and every other field stay - dry-run by default, behind an exact confirmation phrase, with a hard per-run cap, and it is never reachable from the doctor pass itself.
1.61.0 - 2026-09-28
MCP server start-up off the Brain upgrade path, fair ingest locks, the Claude Code MCP manifest inside plugin.json, and an inbox archive (issues #216, #198, #196, #195).
Fixed
The full MCP server answers
initializewithout waiting for a Brain upgrade (#216). After a plugin update,ensureVaultCurrentplanned and applied a pending_brain.yaml/_BRAIN.mdupgrade, snapshot included, synchronously insideo2b mcpand theSessionStarthook, before the server read its first request; on a large vault that took 7 to 35 s and could miss the host's 30 s connect timeout. The upgrade now runs in a detachedo2b brain upgrade --self-healworker behind a per-vault lock (.open-second-brain/self-heal-upgrade.lock, handed to the worker and held until it exits). Measured on a vault with a 315 MBBrain/tar: a fullinitializetook 823 to 1037 ms and hit ENOBUFS on every start before, and 223 to 259 ms once the worker had applied the upgrade.Brain snapshots stream through files instead of a 256 MB buffer (#216). tar writes a staging file that the compressor reads, the in-process gzip fallback compresses in bounded chunks (a multi-member gzip file every reader accepts), and the zstd restore path decompresses to a staging file, so a
Brain/tree of any size no longer fails withENOBUFS.A snapshot archive appears under its final name only once it is complete. Both compressors write a partial file in
.snapshots/, and the archive is published by an exclusive hard link (a checked rename where the filesystem has no hard links); the manifest is written after it. A process killed during compression, or a failed compressor, no longer leaves a truncated<run_id>.tar.zstwithout a manifest that listing, retention and rollback would treat as a recovery point. The partial file of a killed snapshot is never listed, and retention removes it once it is a day old.One vault directory keeps one
vault_namespelling (#216). On a case-insensitive filesystem two spellings of the same vault path rendered two different_BRAIN.mdfiles, so each runtime saw the other's manual as a pending upgrade. Avault_namethat differs only by case and names the same directory (same device and inode) is kept.Contended ingest locks are handed over to their waiters (#198). A writer that released a sync lock and came straight back for it could win every draw while a waiter slept, so a healthy four-process ingest race was refused on the Windows runner. Waiters now mark the held lock, a holder that releases a marked lock steps back before re-acquiring, and a long-waiting waiter polls more often; the exclusive create still grants the lock and an expired budget is still a named
ELOCKED. At the Windows runner's pace (emulated on Linux) the race's longest wait fell from 5017 ms with 4 refusals to 753 ms with none, and the race tests no longer retry refused writes.Starting Claude Code in a checkout of this repository no longer shows two failed MCP servers (#196). The plugin's servers are declared inline in
.claude-plugin/plugin.json(mcpServers, same commands and arguments,alwaysLoadon the writer server), and the repository-root.mcp.json, which Claude Code also loaded as a project config with an emptyCLAUDE_PLUGIN_ROOT, is removed.Session capture stays well under the hook timeout on a large inbox on a slow mount (#195). The signal dedup index is revalidated against a machine-local cache (per directory: listing digest, count and mtime; per file: size and mtime; git's racy-clean window), so a sync tool that replaces a file under the same name is still re-read, and its async callers stat and read in parallel. With 4000 signals on WSL
/mnt/cthe index took 16 s before, 1.9 s cold and 0.17 s warm now; an inline session capture fell from 17 s to 2.9 s.The session-capture process ceiling reaches the dedup walk (#195). The hook hands the walk a cooperative deadline equal to its ceiling, checked between files, so a synchronous walk stops before the host timeout instead of running past it.
Added
Inbox archive (#195). Every dream pass moves inbox signals older than
dream.contradiction_window_daysthat it did not consume intoBrain/inbox/archived/, byte for byte (archived_signalsin the summary and thedreamlog event, previewed by--dry-run, idempotent, never deleted). Such a signal can no longer count toward a candidate, so promotion outcomes inside the window are unchanged. Archived signals stay readable by query, backlinks, sources, expiration, forget, the claim graph and the doctor record checks, a preference evidenced by one keeps its sign, and the dedup index covers them so a re-imported session does not re-create one. The archive clock never runs ahead of the wall clock, so--nowin the future archives nothing that is still inside the window today, a name already taken inarchived/is reported instead of re-planned, and a staged bundle does not count newly archivable signals as drift.dream.archive_stale_signals: falsekeeps the previous behaviour.o2b brain doctorreportsinbox-archivable(#195) while inbox signals older than the window wait for a dream pass, with the inbox size, the archivable count (exactly what the next pass archives) ando2b brain dreamas the next step.Failed automatic upgrades are visible and back off (#216). A failed attempt is recorded per device in
.open-second-brain/self-heal-upgrade.jsonand on theself_heal_upgrademetrics surface;o2b doctorgains aself_heal_upgradecheck,o2b brain statusaself-heal-upgrade-failedproblem, ando2b brain upgrade --dry-run/--checkprint the last failure (JSONself_heal_failure). The next automatic attempt waits one hour, doubling per consecutive failure up to 24 hours;o2b brain upgrade --apply --yesignores the cooldown and clears the record.Environment variables
OPEN_SECOND_BRAIN_LOCK_WAIT_MS(the ingest lock wait, default 5000 ms; the interactive wait is unchanged),OPEN_SECOND_BRAIN_DEDUP_CACHE_DIR(where the dedup index cache lives) andOPEN_SECOND_BRAIN_DEDUP_CACHE=0(turns the cache off), listed together indocs/cli-reference.md. The dedup index cache is listed in the data-ownership statement.
Changed
A refused ingest write says what to do next (#198): how long it waited, then retry the ingest, run the parallel ingests one at a time, or raise
OPEN_SECOND_BRAIN_LOCK_WAIT_MS.Docs:
docs/how-it-works.md,docs/architecture.md,docs/observability.md,docs/updating.md,docs/metrics.md,docs/mcp.md,docs/cli-reference.mdandREADME.mddescribe the start-up worker, streaming and atomic snapshots, the inbox archive, the dedup index cache and the inline MCP manifest; the state-surface counts indocs/cli-reference.mdare current (45 surfaces, 17 memory-bearing).
1.60.0 - 2026-09-27
Optional decision-model support, second part: every remaining use, more routes, threshold profiles and a setup skill (issue #213, Parts 3 to 10).
Every use is off by default and stays off unless the operator enables the feature in machine config, the key variable is set (a self-hosted laya or openjev server on loopback needs none) and the use is set to shadow or enforce in decision_model_uses. With a use off, no key, an invalid config or the vault opt-out, every tool, hook and CLI output is byte-identical to 1.59.0, no request is sent and no record is written; each use's tests prove it. Any failure (timeout, network, HTTP error, invalid reply, state budget, cost gate) falls back silently to the deterministic result and is recorded only in the decision_model_call record. No decision writes to the vault.
Added
skillsuse: two-stage skill selection forskills_attach. The BM25 shortlist (cut at 40 instead ofmax_skills) goes to one request that asks which skill fits and whether the turn needs one at all; when that answer is confident, a second request asks per finalist, over its SKILL.md body, whether it applies. Both stages stay withindecision_model_timeout_ms.enforceoffers the decision subset or reorder of the shortlist; any failure in either stage returns the BM25 offer. The result gainsdecision_model: { mode, applied, degraded?, model? }only while the use is notoff, and theoffer_idis computed over what is actually offered, so theskill_invokedjoin works in both modes. Private or unreadable SKILL.md files are never sent. Seedocs/decision-models/skills.md.extract_prefilteruse: turn pre-filter before thebrain_extract_signalsenvelope. One yes/no question per mined user turn.shadowrecords the probabilities and returns the full plan;enforceleaves turns below the threshold out of the envelope (turns_dropped) and, when every turn is below it, returnsskipped: { reason: "decision_model_prefilter" }withllm_step: null, saving the round trip. Commit items accept an optionalsource_turn, which feeds a regret metric. Seedocs/decision-models/extract-prefilter.md.dedupandtensionuses: advisory pair verdicts. Onechoiceper pair (same | related | differentfor dedup findings ofbrain_hygiene scanand entity alias candidates of the doctor;contradicts | compatible | unrelatedfor tensions).enforceannotates each item withdecision_model: { verdict, probabilities, model, calibrated }and lists a confident low-priority verdict last withdecision_model_low_priority: true; nothing is hidden, merged, dismissed or resolved. New read-onlybrain_tensionactionverifyand CLI verbo2b brain tension verify [<slug>]. Seedocs/decision-models/dedup-tension.md.labelsuse: label suggestions. New read-onlybrain_labelsoperationsuggest(path, optionaldimensions) ando2b brain label <path> --suggest [--dimensions a,b]: one request per note over each declared vocabulary plusnone.shadowreturns null suggestions;suggestnever assigns, and a private note is refused. Seedocs/decision-models/labels.md.answerableuse: advisory signal for search and the recall gates. Carried by the rerank request:brain_searchando2b search --jsongaindecision_model.answerable, andbrain_recall_gateandbrain_context_packacceptdecision_answerablenext to the scores andmatch_qualityand annotate the verdict with{ probability, disagrees }. The level and action never change. Seedocs/decision-models/answerable.md.recall_injectuse: recall-inject hook filter. One request per injecting prompt, withindecision_model_hook_budget_msand the hook's existing retrieval budget;enforcedrops notes that would not help or withholds the brief (decision_model_abstain), never adds material. Any failure, including the sub-budget running out, injects today's brief. Seedocs/decision-models/recall-inject.md.Routes: a
vercel-evaluateadapter and preset for the Vercel AI Gateway/v1/evaluatevariant; self-hostedlayaandopenjevpresets on loopback (no key needed there, small state defaults with a raisable ceiling, per-presetchoicelimits, a licence note incheck); and anllm-emulationadapter that asks an OpenAI-compatible chat model for probabilities only,calibrated: falseeverywhere, never chosen implicitly, refused inenforcewithoutdecision_model_allow_uncalibrated. Shared transport rules live intransport.ts, and one contract suite runs the same fixtures against every adapter. Seedocs/decision-models/providers.md.Threshold profiles per model family. Every threshold is held to the family it was tuned against (
jev-1.13for the hosted routes).enforceon a profile without tuned thresholds for the use (laya,openjev,llm-emulation, or acompatibleserver without a named family) runs asshadowfor every use; onlychecksays so.Config keys
decision_model_threshold_profile(the family acompatibleserver serves) anddecision_model_output_price_usd_per_mtok(read only forllm-emulation, whose cost isunknownunless both prices are set).decision_model_hook_budget_ms, reserved in 1.59.0, now drives therecall_injectsub-budget.o2b decision-model report --use <use>adds the use's own evaluation report after the summary (JSONuse_report) for every use: offer-hit and needless-offer rates (skills), regret, drop and skip shares (extract_prefilter), verdict bands against the operator's later decisions (dedup,tension), acceptance (labels), level against advisory band and outcomes (answerable), added latency and hook timeouts (recall_inject), and the shadow agreement (rerank).token_impactsamples gain an optionalsourceattribution (decision_model:skills,decision_model:extract_prefilter,decision_model:recall_inject) for host-side savings.Skill
decision-model-setup(mirrored into the Codex plugin): routes, the key by name only,checkand--ping, one use in shadow,reportandrerank-evalbeforeenforce, turning a use off, and the privacy and terms notes.Docs:
docs/decision-models.mdis the hub, with a per-use table (surface, whatenforcechanges, failure policy) linking a page per use underdocs/decision-models/and the providers page, retention links per processor and a note on non-English vaults;README.md,docs/architecture.md,docs/mcp.md,docs/cli-reference.mdanddocs/observability.mdupdated.
Changed
Rerank kind
decision-modelfollow-ups from 1.59.0: each passage now carries the notetitleand, when declared, itsstatusandupdatedvalues, so an archived copy can be told from the current one; a chunk that holds only frontmatter is not sent and keeps its position; the injection questions are asked only inenforce, which saves roughly 15 to 20 percent of input tokens inshadow; the query-cache key names the passage shape.A
compatibleprovider enforces only oncedecision_model_threshold_profilenames its model family; until then itsenforceuses run asshadow(in 1.59.0 every preset enforced with the Jev thresholds).The agent-scope probe covers the new
brain_tension verifyandbrain_labels suggestcalls (233 call recipes).The decision-model state builder also withholds a text slice that starts inside a
<private>region and continues publicly on the same line (a chunk or sentence boundary inside an inline region), for every use.llm-emulationrecords a cost the route reports, so the daily cost gate counts it without configured prices;checkwarns when neither is available.
1.59.0 - 2026-09-27
Optional decision-model support, first part: the provider core and a decision-model rerank kind (issue #213, Parts 1 and 2).
A decision model is a typed judgment model: it answers questions about a text state with probabilities over options Open Second Brain supplies, and never generates text. The feature is off by default. It becomes active only when the operator sets decision_model_enabled: "true" in machine config AND the environment variable named by decision_model_env_key is set. Without both, every surface behaves exactly as before: no network request, no record, and no change in tool, hook or CLI output, apart from the explicit diagnostics (o2b decision-model check and a decision_model line in o2b doctor --readiness that reads skipped while the feature is off).
Added
Decision-model provider core in
src/core/decision-model/: the contract (noul,choiceandscorequestions; answers with probabilities and avalidflag), asystemoneadapter forPOST <base>/v1/systemone(TypeSafe, OpenRouter, Vercel AI Gateway compatible route, OpenCode Zen, compatible servers), presetstypesafe,openrouter,vercel,opencode-zenandcompatible, andrunDecision, the one entry point every use goes through. The adapter uses rawfetchwithredirect: "error", validates each answer (known ids and option keys, probabilities in range and summing to one, argmax matching the reported choice), recomputes a missing confidence, retries once only on 408, 409, 429 and any 5xx within the timeout, never retries after a reset connection, and refuses a reply body over 1 MiB.Config keys
decision_model_*(enabled, provider, base_url, id, env_key, allow_insecure_http, timeout_ms, hook_budget_ms, max_state_tokens, uses, cost_gate_usd, input_price_usd_per_mtok, allow_uncalibrated), each with anOPEN_SECOND_BRAIN_DECISION_MODEL_*override. The config holds only the NAME of the key variable; anenv_keythat is not a variable name, and abase_urlwithuser:password@, make the config invalid without printing the value. Invalid values keep the feature off and are reported bycheck, naming the key. The model id must be pinned; a moving alias such asjev-latestis refused.Privacy rules for the state: candidates whose page carries
visibility: private, or whose visibility cannot be resolved, are never sent;<private>regions are stripped, and a search chunk that carries part of a region split across chunks is withheld; ids and paths are masked asP0..Pn; texts are clipped; the state is kept withindecision_model_max_state_tokens. The whole body passes the shared egress guard, and the adapter is declared in the egress registry asdecision-model-systemonewith statusshared_redactor.Vault opt-out:
decision_model: { enabled: false }inBrain/_brain.yamldisables every use for that vault. A vault can only narrow:enabled: true, uses, modes or endpoints there are ignored with a warning.Accounting: a
decision_model_callcontinuity record per request (use, mode, provider, answering model, counts, tokens, cost and its source, latency, outcome, a hash of the redacted state, and per-use identifiers; never text), a daily cost gate (decision_model_cost_gate_usd, default 0.50 USD per UTC day; eval and--pingrequests count toward it and carry anorigin), and an optionaldecision_mscomponent onmcp_route_latencyrecords.o2b decision-model check [--ping]shows the configured state, the key variable's name and whether it is set (never the value), per-use modes, vault opt-out, cost gate, today's spend and the processor;--pingsends one synthetic request and records it.o2b decision-model reportsummarises records per use, including rerank shadow agreement over ordinary shadow records.o2b doctor --readinessgains adecision_modelline on every install.Rerank kind
decision-model(search_rerank_kind: decision-model, mode fromdecision_model_usesrerank): one request per rerank carrying a relevance question per candidate, an injection question per candidate, and ananswerablequestion while that use is notoff.shadowrecords and returns the heuristic order unchanged;enforcereorders the head by relevance and keeps private, unsent and invalid candidates in place; a suspected injection is taggeddecision_model_injection_suspected(advisory, it never moves). Any failure returns the heuristic order and is recorded, with no warning in the search output, and that search is not written to the query cache. The recall-inject hook does not run this kind.o2b search rerank-eval --dataset <path> [--kind local|decision-model|openai-compat] [--compare-local], the operator entry point for the rerank eval gate, reporting hit@1/3/5/10, hit@k and MRR for rerank off and the chosen kind with deltas, per-query wins, losses and ties, a recommendation, and for the decision-model kind the run's calls by outcome, cost and latency.runRerankEvalGateacceptskind: "decision-model"and measures it inenforce.Docs: a short section in
README.md,docs/decision-models.md(a quick start, configuration reference, presets, privacy and retention notes, the vault opt-out, the rerank kind and how to measure it), plus sections indocs/architecture.md,docs/cli-reference.mdanddocs/observability.md.
Changed
search_rerank_kindacceptsdecision-model, and its error message names the three kinds.o2b search rerank-fitreports thedecision-modelkind as inapplicable and points torerank-eval.The data-ownership statement counts an active decision-model reranker as a networked rerank endpoint.
1.58.2 - 2026-09-26
A quieter end-of-turn reminder in Claude Code.
When a turn changed files but recorded no brain event, the Stop guardrail continued the turn with decision: "block". Claude Code renders that as a red "Stop hook error" and prints the whole reason, about 25 lines, in the user's transcript on every guarded turn.
Changed
The Stop guardrail uses Claude Code's non-error feedback channel and one line of text. In Claude Code the hook now returns
hookSpecificOutput.additionalContextfor the Stop event (supported since Claude Code 2.1.163), which continues the turn as "Stop hook feedback" instead of a hook error. Codex documents onlydecision: "block"for Stop, where the reason becomes the continuation prompt, so Codex, Grok Build and unrecognised runtimes keep that shape. In every runtime the text is now a single line (STOP_GUARDRAIL_TEXT): "Open Second Brain: this turn changed files but recorded no brain event. Call brain_feedback, brain_apply_evidence or brain_note if one fits (brain-memory skill); otherwise just finish." The guardrail still fires at most once per turn throughstop_hook_active, and the per-runtime cadence lines are gone. The opencode plugin has no Stop guardrail and is unchanged.The
brain-memoryskill has an "End-of-turn check" section with the decision the one-line reminder asks for, which the Stop reason used to spell out. The Codex plugin copy is regenerated.Claude Code is recognised on Stop when its transcripts live outside
~/.claude(a customCLAUDE_CONFIG_DIR): a Stop payload withstop_hook_activeandlast_assistant_messagebut no Codexturn_idresolves to Claude Code, so such installs get the feedback channel too.The
brain-memoryskill documentsresult: outdatedforbrain_apply_evidence, next toappliedandviolated.
1.58.1 - 2026-09-26
Session capture off the hook path, and registry scanner hygiene.
A session-capture event that needs duplicate checking read every signal in the inbox inside the hook itself. On a large inbox on a slow mount that took longer than the host's 10 s hook timeout. Those captures now run in a background worker.
Plugin registries such as Awesome AI Plugins run the HOL AI Plugin Scanner over a repository before they list it. Its secret patterns matched test fixtures, schema tokens and two constant names, none of which is a credential, and it asked for the repository files a published plugin is expected to carry. The hygiene changes do not change runtime behaviour. The scanner (plugin-scanner 3.0.123, default profile) now reports 0 critical, high, medium or low findings and a score of 99/100, up from 73/100 with 132 high findings.
Fixed
Session capture no longer exceeds the host hook timeout on large inboxes: captures that need duplicate checking run in a background worker. A UserPromptSubmit or PostToolUse event that may need the signal dedup index (a feedback marker, an extractable fact such as a URL or a percentage, a replayed
brain_feedbackcall, or an interrupted SessionEnd) is written to a spool file created with mode 0600 and handed to a detached worker, and the hook returns at once. Workers for one vault run one at a time, so two deferred captures still see each other in the dedup index. Every other event is still captured inline, and a failed hand-off falls back to an inline capture.OPEN_SECOND_BRAIN_SESSION_CAPTURE_DEFER=0restores inline capture for every event. The spool is listed with the other out-of-vault state, as memory-bearing: a worker that dies before reading its spool leaves that payload in the temp directory.A note rename on Windows no longer mistakes an occupied destination for the source. The case-only-rename check compared
devandinoas numbers. NTFS file IDs are 64-bit, and above 2^53 two different files can round to the same value, so a rename onto an existing note could skip thedestination_occupiedrefusal. The check now reads exactbigintstats. The Windows CI job surfaced it as an intermittent test failure.
Changed
Test fixtures build placeholder credentials at runtime.
tests/helpers/fake-credentials.tsassembles each fake key from parts, so no source line holds a literal shaped like a real credential. The assembled values are the ones the tests used before, so the redactor tests still exercise realistic shapes. Schema tokens that tests pass astoken: "..."moved into named constants.Two internal constants renamed.
SCAN_TRUNCATED_TOKENinsrc/core/redactor.tsis nowSCAN_TRUNCATED_SENTINEL(SCAN_TRUNCATED_MARKERalready names the exported marker that embeds it), andUNRECORDED_TOKENinsrc/core/integrity/stamp.tsis nowUNRECORDED_MARKER. Neither is exported and their values are unchanged; the OpenClaw bundle is rebuilt.The codegraph install recommendation names npm. The
codegraph-partnerskill no longer quotes acurl ... | shinstaller. It suggestsnpm i -g @colbymchenry/codegraphand links to codegraph's own install section.A brainstorm recon note uses an obvious placeholder (
sk-proj-<example-key>) where it quoted a realistic-looking key.
Added
SECURITY.md, with private GitHub vulnerability reporting as the reporting channel..github/dependabot.yml: weekly, grouped updates for GitHub Actions and for the Bun dependencies, with major JavaScript upgrades left to a person..codexignore, listing local state, secrets and build output.uv.lockfor the Hermes Python shim, which has no dependencies.scripts/sync-version.tskeeps the project's own version in it in step withpackage.json.Codex metadata. The root
.codex-plugin/plugin.jsoncarries the author, homepage, repository and license thatplugins/codexalready declared; the Codex manifest adds itswebsiteURL; and the marketplace entry declares"authentication": "ON_INSTALL".The Codex plugin subtree carries
LICENSE,README.md,SECURITY.mdand.codexignore.bun run sync-plugin-mirrorsgenerates them from the root as real files:LICENSEand.codexignorebyte for byte, and the two Markdown files with repository-relative links made absolute, since those links resolve to nothing two levels down. The--checkform reports a symlinked single-file mirror as drift, as it already did for the mirrored directories.A plugin-scanner CI job (
.github/workflows/plugin-scanner.yml) runs the scanner's GitHub Action, pinned by commit SHA, on every pull request and on pushes to main. It fails on a high or critical finding or a score below 80, holds onlycontents: read, and needs no secrets.
1.58.0 - 2026-09-26
Two features that 0.28.0 shipped as preview foundations, and that no verb, tool or hook ever reached, are now wired end to end: the oversized-payload registry and knowledge packs. Around them, this release hardens the boundaries that decide what a remote MCP caller can see, which paths a write may land on, how much trust imported content arrives with, and where a credential may travel. It also fixes chunking for Chinese, Japanese, Korean, Thai and similar text, which rebuilds each existing search index once, and stops the prompt hook from reading the whole inbox on every prompt.
Several changes refuse input that earlier releases accepted. They are listed under Changed, and docs/updating.md has a 1.58.0 section with what to check and how to opt back where an opt-out exists.
Security
Remote callers no longer receive private pages through side surfaces. A page carrying the reserved
visibility: privatetoken is withheld at remote reach everywhere page content or page identity can travel:brain_context_packapplies the reach rule to its candidates before ranking and budgeting, so a withheld page contributes nothing to any lane, the skipped list,deduped_fromor the items. A tension that touches a withheld page is still reported, without its id.brain_writes(list,plan_revert),brain_labels showandbrain_unlinked_mentionsask the reach view per target. A withheld note is answered exactly like a missing one.brain_writesandbrain_unlinked_mentionsmove to covered in the visibility registry.The reserved token dominates its co-tags: a page tagged
visibility: [private, team]is no longer returned to a caller whose scope names onlyteam.brain_searchrefuses the reserved token as a caller-supplied scope at any reach.Search and chunk drill-down honour both the page's current visibility and the visibility the index recorded for that path, so a public page written at the path of a deleted or moved private page does not surface the older text before the next index run.
A bare artifact id resolves against the resolved vault path, so the artifact visibility rule applies when the vault path is spelled with a trailing separator or a
./..segment.
Private session rows stay local. At remote reach
brain_session_grep,brain_session_describe,brain_session_expandandbrain_idea_lineagewithhold continuity rows flaggedprivate, and the session recall tools also withhold the summary nodes built from them. A withheld row is answered exactly like a missing one.Error text sent to a remote caller names no host paths. Both the tool-level error envelope and the
INTERNAL_ERRORchannel replace the vault, home and temp roots (with their real-path and slash variants) by<vault>,<home>and<tmp>before the shared redactor runs. Local callers keep the paths their remediation needs.One reading of paths backs every
Brain/boundary check. Backslash is a separator,.and..segments collapse, leading separators drop, and theBrainsegment is compared case-folded, the way macOS and Windows filesystems resolve it. Graph import resolves paths before theBrain/boundary check in every mode and re-checks where the bytes land, so a vault folder symlinked intoBrain/is refused, and a node whose landing path cannot be resolved is rejected while the rest of the graph imports. Note create, update, append and batch writes use the same reading and also re-check where the bytes land, refusing a target that resolves intoBrain/, outside the vault, or not at all (a note used as a folder, a link loop).Caller-named frontmatter writes answer to the write binding and stay on notes.
brain_labels,o2b brain label,o2b brain attrand marker write-back require a Markdown note, and underBrain/only the page lanes (sources/,reports/,distillations/). The operator config, standing rules and the other machinery files are refused, both as spelled and where the bytes would land.apply-markersreports a refused target as a per-markerrefusedverdict in report and apply mode alike, and the markers after it still run.Write sessions commit only into the lanes that hold agent pages.
brain_write_sessionando2b brain sessionaccept a target underBrain/sources/,reports/,distillations/,notes/ordecisions/panels/, read with the shared case-folded path reading, and refuse the rest ofBrain/, including the standing rules and the active context. The commit re-checks where the bytes land, refuses the standing rules file by identity and applies the write binding, so a symlinked folder or note cannot carry a session into machinery.Imported content arrives with the trust the operator gave it.
OKF import validates every recorded path before joining it (relative POSIX, no
..), so the review lane stays insideOKF Review/. The manifest'sproducerfield is a label, not a credential: machinery frontmatter (_status,owner,origin_channel,_revision,_confidence*,knowledge_packand the rest) is stripped on every import not run with--trusted, and theBrain/sources/andBrain/reports/lanes open on--trustedalone.bank-importrestores every preference row untrusted by default, including rows the bundle already marked unconfirmed.Claude memory imports land
unconfirmedon a real trial window instead of confirmed at high confidence.brain_update_noterefuses the reserved frontmatter keysvisibilityandorigin_channelwithreserved_frontmatter_key.brain_ingest_sourcereads apre_extractsource only inside the vault; anything else is reportedextracted: false.Snapshot restore checks every extracted symbolic link, including one whose target does not exist yet, and restores only links that stay inside the extracted tree.
Every content writer answers to the freeze.
brain_tiersrestore and accept, ando2b brain links --write, now refuse on a frozen vault like every other writer; the links dry run stays available.Secret custody. The secrets directory carries a
.gitignore, so a vault that is a git repository never commits the keyfile or the ciphertext. On POSIX the owner-only modes of the keyfile and the store are re-applied on every load. On Windowssecret setrefuses when the owner-only ACL cannot be applied, and the firstsecret setin a fresh vault works. A.gitignoredoes not reach Syncthing, which reads only the.stignoreat its folder root: the newsecrets-sync-exposeddoctor check, and a warning onsecret set, name the exact.stignoreline when a Syncthing folder would carry the directory to its peers. The check reads real paths, so a vault opened through a symbolic link is checked against the folder its target sits in. Neither edits.stignore. An unknown secret name no longer lists the stored names in its error.brain_secrets runmatches its allowlist against the whole argv. Pattern tokens align element by element with the argument vector, and returned output also runs the bare-token redaction pass, so an encoded echo of the value is scrubbed too.Credentials travel only where the operator configured them. Embedding and rerank endpoints must be https (loopback excepted), and all three provider transports refuse redirects. The research fetch helper refuses non-https URLs and private, link-local, CGNAT and unspecified IP literals before it attaches a key, and does not follow redirects.
An unknown
--tool-profilefails closed.o2b mcpexits 2 naming the bad value, where it came from and the known profiles, instead of serving the full tool surface.parseBrainYamlrefuses__proto__,constructorandprototypekeys at every level, and the snapshot manifest sidecar validates its run id the way the snapshot path does.
Added
The payload registry, wired end to end. Session recall import runs every turn through the registry before the continuity row is written. Data URIs and base64 runs longer than
sessions.payload_max_inline_chars(default 512), and text longer thansessions.payload_max_text_chars(default 32000), move toBrain/.payloads/<sha256>.txt. The row keeps a 1,000-character head preview and anosb-payload://placeholder. Every stored payload is private-region-stripped and redacted before it is written. A turn that needs no externalization imports byte-identically.o2b brain payload get <ref> [--offset N] [--limit N]pages the exact stored content;listshows stored payloads with reference counts, orphans and missing files;gcis a dry run unless--apply, which removes only unreferenced payloads older than ten minutes, behind apayload-gcrecovery point, re-planning inside it under the payload store lock that session import also holds from writing a payload to appending the row that names it. Younger orphans are reported as deferred, and an import that finds its payload already stored refreshes the file's age.brain_session_expandgains a payload mode (payload,payload_chars,cursor) that pages a placeholder's content. Remote callers read only a payload that a non-private session turn references, directly or through another remotely readable payload. Pages, the Brain log included, keep a payload alive for the gc but grant no remote read.The doctor reports
payload-orphan,payload-missingandcontinuity-row-oversized. Snapshots archive the store, the search index never admits it, and bank and OKF exports carry placeholders, never payload bytes.
Knowledge packs, the install and uninstall follow-up 0.28.0 left open.
o2b brain knowledge-pack export|preview|install|uninstall|listpacks a selected subset of rules and pages into an OKF-based bundle sealed with a sha256 per file and a digest over name and version. Export blocks pages scoped byvisibility:, carrying anowner:or still under review, and redacts through the shared egress guard before sealing.previewreports samples, integrity, conflicts and injection or privacy warnings.installrefuses a tampered pack and lands everything untrusted: rules on a fresh trial window with the source vault's evidence, counters and pins cleared, pages staged inOKF Review/, each stampedknowledge_pack: <name>@<digest>.uninstallremoves one pack's entries and keeps rules that gained local evidence, pages an operator promoted, and staged pages whose content changed since install (reported asedited, against aknowledge_pack_shafingerprint the installer records on each staged page). Theknowledge-pack-uninstallsnapshot archivesBrain/only, so it holds the removed rules; a removed staged page is one the operator never changed, and installing the pack again restores it. Installed provenance shows inbrain_searchtrust metadata (trust.knowledge_pack) ando2b brain query. CLI only, becausepreviewreads a directory outside the vault.embedding_allow_insecure_httpandsearch_rerank_allow_insecure_http(and theirOPEN_SECOND_BRAIN_*environment forms) accept plain http for that one endpoint, for a local server on another machine such as LM Studio on the Windows host seen from WSL or Ollama on the LAN. Both default to false, apply only to a base URL from the operator's config or environment, never to one from the in-vault provider registry, and warn once per endpoint.OPEN_SECOND_BRAIN_MCP_API_KEYsupplies the MCP HTTP bearer when--api-keyis absent, so the key stays out of the process list.bun run check:hermes-scanand ahermes-plugin-scanCI job.hermes plugins installscans the whole clone and blocks a dangerous verdict with no override (#188). The script fetches Hermes' plugin scanner at a pinned commit, checks its digests, and scans the repository root andplugins/hermesthe way an install does.
Changed
These can change what an existing setup sees. docs/updating.md covers each one.
Embedding and rerank endpoints must be https unless the host is loopback or the endpoint's
*_allow_insecure_httpopt-out is set in the operator's config or environment.An unknown tool profile exits 2.
--tool-profileormcp_tool_profilenaming a profile that does not exist stopso2b mcpinstead of serving every tool.bank-importresets every preference row by default: unconfirmed, unpinned, low confidence, on a fresh trial window dated from the restore, and each reset row is named.--trusted-restorekeeps the carried state for a backup you vouch for.OKF import strips machinery frontmatter unless
--trusted, whatever the bundle'sproducersays, and only--trustedadmits theBrain/sources/andBrain/reports/lanes.Labels and attributes refuse
Brain/machinery and non-Markdown targets. UnderBrain/, onlysources/,reports/anddistillations/pages accept them.Write sessions commit only under
Brain/sources/,reports/,distillations/,notes/anddecisions/panels/. Any other target underBrain/is refused withtarget-reservedwhen the session opens.A top-level folder named
brainin any letter case is treated asBrain/. Note writes and graph import refuse it as they refuse the machinery root. On a case-sensitive filesystem that holds a separate user folder of that name, rename it.Every existing search index is rebuilt once, in the background at the next start, because its chunks were cut by older chunking rules (see Fixed). With embeddings on, the chunks that changed are re-embedded, which includes multi-chunk notes in every language, not only text in scripts written without spaces.
brain_secrets runallowlist patterns match the argv element by element. Each space-separated pattern token matches one argument,*inside a token globs within that argument, and a trailing*token means one or more further arguments. A pattern such astool*that relied on matching across the joined command line now matches only a single-element argv; writetool *.brain_update_noterefusesvisibilityandorigin_channelin its frontmatter. Edit them in the file.Claude memory imports land unconfirmed and go through the dream pass like every other candidate.
Knowledge-pack preview lives in portability. The never-wired
src/core/brain/packs/pack.tspreview is folded intopreviewKnowledgePack, with its guarded samples and integrity hash.
Fixed
The prompt hook no longer reads the inbox for a prompt with nothing to capture.
session-capturebuilt the signal dedup index, a read and parse of everyBrain/inbox/sig-*.md, before it knew whether the prompt carried a marker or a fact. On a vault with about 4000 signals on WSL's mount of a Windows drive that took 6 to 12 s and ran into the UserPromptSubmit hook's 10 s host timeout. The index is now built on first use and shared by the rest of the event; dedup is unchanged when there is something to capture.Chunking for scripts written without spaces. The chunker counted whitespace-separated words, so a paragraph of Chinese, Japanese, Thai or similar text counted as one token and a long note became one chunk that embedding providers reject. Han, Hiragana, Katakana, Hangul, Bopomofo, Thai, Lao, Khmer, CJK punctuation and fullwidth forms now count one token per character, a block too large for one chunk is split by line, then after sentence punctuation, then at a token boundary, and every chunk stays within the cap. Short space-separated notes chunk identically; longer notes may split at slightly different points because overlap now counts toward the cap, and their changed chunks are re-embedded. The index records
chunker_version, and the startup self-heal rebuilds an index cut under older rules once. Reported by @Iversondwx in #186.A failed reindex swap puts the previous index back. When the final rename fails after the live index was moved aside, the old index is restored before the error is raised, instead of leaving no index until the next open. From the analysis by @rayblennerhassett-byte in #187.
o2b search reindexon a vault that is not there refuses withvault not foundinstead of creating the directory with an empty index. The detached self-heal child no longer recreates a vault that was moved or deleted after it was spawned.Hermes installs are no longer blocked by the plugin scanner (#188). The lines Hermes' scanner flagged were all prose, and are reworded without changing their meaning. The one runtime string among them, the reason text of the context guard's instruction-override detector, now says "disregard" where it said "ignore"; its code and pattern are unchanged.
OpenClaw no longer calls Bun inside its Node bundle. A contended first use of the installation secret or device id slept with
Bun.sleepSync, which is undefined under Node and threw out ofsecond_brain_status. Both waits use the runtime-neutral sleep, and a test drives the committed bundle under a realnode.Git remote identities keep
#,?and%. Path remotes were placed into URLs unescaped, so two repositories whose paths differed after a#or?shared one identity. The drive letter of afile:identity is folded to upper case, and a UNC remote gets an identity instead of none. The session lineage ledger re-canonicalizes recorded remotes on read; an older line for a path holding one of those characters reads as a different repository, and continuation refuses it.Snapshot restore keeps in-tree links relative. The restore copy rewrote every relative symbolic link to an absolute path inside the temporary extraction, which the cleanup then removed, so in-tree links restored dangling. Links now copy verbatim.
Label and attribute writes work on Windows with a write binding declared. The binding matcher received a backslash-separated path and refused every target.
The startup search self-heal finishes on native Windows. A rebuild started by a hook was ended together with the hook process, so it never ran and never recorded an outcome. The child is now detached on Windows; POSIX is unchanged.
A note used as a folder is refused on Windows too. Windows reports a path below a file as missing rather than as not a directory, so the landing-path check read
note.md/child.mdas a note still to be created. The shared path resolver now gives the POSIX answer on both.brain_context_packstopped writing its item count to stderr, which landed in MCP client logs on every call.
Removed
src/core/brain/schema-contracts.ts, the TypeScript mirror ofschemas/brain/*.schema.json. Nothing in production read it. The 0.17.0 entry described it as a registry "used by tests and agent-facing envelopes"; only its own tests used it, and it had drifted from the published files. A contract test now runs the report generators and the MCP payloads ofbrain_intent_review,brain_retentionandbrain_brief view=monthlyagainst the published schemas, and a new schema file fails the suite until it gets a generator check. No drift between the generators and the published schemas was found.Two other modules nothing called:
activityMarker()in the activity-line renderer and themock-embeddingtest helper.
Internal
A temp entry that outlives the test run fails it (#194). The test preload points
TMPDIR,TMPandTEMPat one per-run root and fails the run naming every leftover (a warning on Windows, where a just-exited process can still hold a handle). Every test file that minted temp directories or pinnedHOMEnow removes them. Closes #194.Flaky tests fixed: lock races retry a write the lock refused, so they count only lost updates; the live explorer test waits for its listening line; hook tests wait for the reindex child they start; the state-surfaces suite releases the writer lock it takes; the capability-tier provider probe hits a server that answers; context-pack round trips follow the preview envelope.
Weak pins replaced by measured ones: the advertised MCP tool list is pinned by name set against the tool table, note-lifecycle and scaffold-stub flags ratchet against what each verb parses, the advisory claims are driven through the CLI, and the OpenClaw per-turn reminder is driven through the real plugin entry.
A cleanup pass first removed the payload registry and the knowledge-pack preview as dead code. Both were unfinished features, so they were restored and finished instead (see Added), and the write-site census attribution was corrected with them.
Every workflow action is pinned to a full commit SHA, with the version as a comment, and a test keeps a new action from arriving by tag.
The opencode plugin's spool directory is chosen by an if/else chain rather than a nested ternary, with tests for each fallback.
The git hooks check the working tree they are committing from, and lint and fmt skip
.claude/worktrees/.An agentic-playbooks workspace (
.apb/) holds this repository's release and PR-preparation playbooks. Run state stays ignored, as do the local.zvec-grep/index and.ai-notes/.
1.57.1 - 2026-09-25
Two faults that surfaced while wiring Codex CLI 0.157.0 to this plugin. Neither was visible from inside the repository. Codex installs a plugin by copying its marketplace source, plugins/codex/, into ~/.codex/plugins/cache/, and that copy drops symlinks. plugins/codex/hooks and plugins/codex/skills were both symlinks into the repository root, so every Codex install had no hooks and no skills. Every Codex run also warned failed to read plugin hooks config .../hooks/hooks.json: No such file or directory. Separately, o2b install --check reported a correctly registered Codex as mcp-unreachable, because the host probe discarded every successful answer.
Fixed
The Codex plugin ships real files.
plugins/codex/skills/is a byte copy ofskills/.plugins/codex/hooks/hooks.jsonis generated fromhooks/hooks.json. SessionEnd timeouts are capped at the 3 s that Codex enforces anyway, and that it otherwise warns about on every run.Only
hooks.jsonis mirrored, never thehooks/*.tsscripts. Codex exportsCLAUDE_PLUGIN_ROOTas its cache dir. A cached script there would wino2b-hook's resolution and then fail on its../srcimports. Without one, resolution falls through to the real checkout, as before.Every mirrored hook also carries a
commandWindows. On Windows Codex runs a hook as%COMSPEC% /C "<command>", which cannot parse the POSIX form. The Windows form runs the PATHo2b-hook.cmdthato2b install-cliwrites, setsNoDefaultCurrentDirectoryInExePathfirst and askswhere /q $PATH:o2b-hookrather than a barewhere, so a launcher planted in the project Codex opened never runs. It always exits 0. The sync refuses a hook command that does not end in theo2b-hook <name>fallback, rather than ship one that cmd.exe cannot run.The same change retires the only symlinks in the tree. A Windows checkout without Developer Mode or
core.symlinksmaterialised them as text files holding the target path.Verified against Codex 0.157.0 in a throwaway
CODEX_HOME:codex plugin marketplace addpluscodex plugin addputhooks/hooks.jsonand all five skills in the cache.hooks/listreports 16 plugin hooks with no warnings or errors, andskills/listreports the fiveopen-second-brain:*skills. Once trusted, the SessionStart, UserPromptSubmit and SessionEnd hooks fire througho2b-hook.
install/codex.mdlooks for the launcher underCODEX_HOME. Step 2 searched~/.codexwhateverCODEX_HOMEsaid, so a relocated Codex home stopped the install or picked a stale launcher. It now searches the marketplace clone in${CODEX_HOME:-$HOME/.codex}/.tmp/marketplaces.o2b install --checkstops calling a working Codexmcp-unreachable. The host probe testedsignalCode !== nullto detect a killed child. Bun leavessignalCodeundefined, not null, on a normal exit, so every successfulcodex mcp listwas treated as killed and its stdout was thrown away. The check is now!= null, and a test drives a real child that exits normally, since a stubbed runner cannot reproduce a runtime quirk. No other spawn site has the pattern: thenode:child_processcall sites getnullfrom Bun, as Node documents.
Added
bun run sync-plugin-mirrorsrebuilds the Codex mirrors.bun run sync-plugin-mirrors:checkis gated in the pre-commit hook and in two CI jobs:validate, andwindows, whose checkout is where a line-ending rewrite or a symlink materialised as a text file would show. It reports a missing, changed or extra mirrored file, and any symlink, by name. A test also asserts that the git index tracks no symlink underplugins/or.agents/.
1.57.0 - 2026-09-25
Open Second Brain now runs natively on Windows 10 and 11. Until this release the resolver refused win32 by name, and the refusal was the honest part: every entry point was a bash script, every durable path assumed $HOME/.config, and the code underneath had habits that POSIX forgives and Windows does not. A SQLite connection closed without finalizing its statements keeps its file locked. A rename onto a file another process holds fails. A tar walk follows readdir order, and that order differs between filesystems. A path split on / misses \. The first full test run under Windows Bun failed 2127 of 12,388 tests. This release closes those gaps, gives the platform its own layout, and ends with the same suite at 0 failures on Windows and on Linux. 162 tests are skipped on Windows, each with a stated reason; they assert something Windows cannot express, such as a chmod that denies reading. Several of the fixes were never Windows bugs. They are nondeterminism and leaks that every platform had, which a second platform happened to expose.
Added
Native Windows layout.
src/core/platform-dirs.tsanswers one question for every surface that builds a path: where this tool keeps its config, data, state, cache and launchers.Where things live on Windows:
%LOCALAPPDATA%\open-second-brain\, withconfig.yaml, the opencode session spool and the other per-user files under it. It is%LOCALAPPDATA%rather than the roaming%APPDATA%because those files are machine-bound: the vault path and an agent name that carries the host would be wrong on another machine. Hermes Agent makes the same choice.Overrides and other platforms:
XDG_*_HOMEandOPEN_SECOND_BRAIN_CONFIGstill win on every platform, and POSIX is byte-identical.Kept in step: the Hermes plugin's Python resolver and the self-contained opencode plugin mirror the rule, and a
~\vaultconfig value expands like~/vault.
.cmdlaunchers.scripts\o2b.cmd,o2b-mcp.cmd,o2b-hook.cmd,vault-log.cmdando2b-aider.cmdsit beside their bash twins.They resolve Bun the way
_bun-precheck.shdoes: PATH first, then%USERPROFILE%\.bun\bin.They never run a
bunfrom the current directory. cmd.exe looks for a bare command name in the current directory before PATH, and an agent host starts them in the project it opened, so a repository that ships abun.cmdwould have run it. Each launcher setsNoDefaultCurrentDirectoryInExePath=1right aftersetlocal, and turns delayed expansion off so a!in the checkout path stays a character.The hook launcher keeps the fail-soft contract and always exits 0. It passes an empty
""argument through instead of stopping at it, and reports a hook that exits with a negative code (a crash) as well as a positive one.A new
.gitattributespins them to CRLF and everything else to LF, so a Windows checkout (Git for Windows defaults tocore.autocrlf=true) holds the same bytes as a POSIX one.
o2b install-clion Windows writes.cmdlaunchers into%USERPROFILE%\.local\bin, because a symlink there needs Developer Mode or an elevated token.Each launcher carries an ownership marker, so install, heal and uninstall follow the same policy as the symlink path: repoint our own stale launcher, refuse a foreign file, heal only one that rotated out of a plugin cache, and remove only one that points into this checkout.
A
%in the checkout path is doubled on the launcher's command line, where cmd.exe would expand it. A launcher of ours whose command line is not the current body is rewritten and reported asupdated.When the directory is not on PATH, the verb says so and names the Environment Variables dialog (
rundll32.exe sysdm.cpl,EditEnvironmentVariables). It prints no command that rewrites the value: Windows PowerShell'sSetEnvironmentVariablestores the user Path back asREG_SZwith its%VAR%entries expanded.
MCP host configs on Windows start the server as
cmd /d /c o2b mcp ..., thecmd /c <name>form several hosts document fornpx.Node refuses to spawn a
.cmdwithout a shell since the 2024 BatBadBut fix, and Rust and Bun resolve only.exefrom a bare name./dskips cmd's AutoRun, whose output would corrupt the JSON-RPC stream.Every Windows entry carries
NoDefaultCurrentDirectoryInExePath=1in itsenv. Without it cmd.exe resolveso2bin the current directory first, and a host starts its servers in the project it opened, so ano2b.cmdcommitted to any repository ran at MCP start. An absolute launcher path was the alternative; cmd's quote stripping breaks it for a profile or vault path with a space.Because hosts quote only arguments that contain whitespace, the Windows payload refuses a vault path containing
& | < > ^ % " !by name, rather than writing a config that cmd would split, expand or execute at spawn time.Grok Build keeps its absolute
bun.exe run main.tsentries, with the arguments now built without thecmdprefix.
Cursor's Windows session root (
%APPDATA%\Cursor\User\workspaceStorage). Session roots can now be scoped to a platform, so a POSIX host is not handed a Windows-shaped path to probe.An owner-only ACL for the secrets keyfile on Windows.
icacls /inheritance:r /grant:rgrants access to the current user only, on the key directory (inheritable), the keyfile and an existingsecrets.json. It is the Windows counterpart of the POSIX 0700 and 0600 modes. The grant names the user by SID, read from the process token withwhoami /user, not by%USERDOMAIN%\name, which a parent process can rewrite andicaclsmay not map. It is applied on every load, once per process, not only when the key is created, so a secrets directory restored or copied in with a vault is brought to the same ACL.icacls.exeandwhoami.exerun fromSystem32by absolute path.icacls /resetruns first, because/inheritance:rremoves only inherited entries: an explicit entry for another account, which a directory restored with its ACL can carry, would otherwise survive. Where SYSTEM or Administrators entries remain, as they can on GitHub's elevated Windows runners, the threat model already counts administrators as able to read the key. It is best effort: a failedicaclswarns and never fails the write.A
windows-latestCI job runs the typecheck, the full TypeScript suite, the Hermes plugin's Python tests and the.cmdlauncher, with a 75-minute timeout. Like the Linux job, it asserts that the Hermes anti-drift test actually ran rather than skipped. The suite runs the generated and checked-in launchers through a realcmd.exe: end to end from the MCP payload's command, and with abun.cmdand ano2b.cmdplanted in the working directory, which must not run.install/windows.md, plus Windows sections ininstall/prerequisites.md,install/claudecode.md,install/hermes.mdand the README.
Changed
Closing SQLite releases the file immediately.
closeDatabase()callsclose(true): Bun's plainclose()turns a connection with unfinalized statements into a zombie that keepsbrain.sqliteand its-wal/-shmopen until garbage collection. POSIX never noticed. On Windows the open handle blocks every later delete and rename, which includes the index swap and snapshot restore. This one pattern caused more than half of the original Windows failures. All twenty close sites use the helper.Renames ride out transient sharing violations. On Windows,
renameWithRetryretries EPERM, EACCES and EBUSY for up to two seconds, which is the window in which Syncthing, an editor, an antivirus scan or anothero2bholds a file. On POSIX it is plainrenameSync. Atomic writes and every rename site use it. The destructive-site and write-site censuses count it as therenameSyncit wraps.Snapshots need no
gzipbinary. Withoutzstd, the archive is gzip-compressed in-process throughnode:zlib, and restore inflates in-process too, so the only external tool istar. Windows 10 and later shiptarasSystem32\tar.exe. The byte format is unchanged, so older archives restore as before.Vault-relative paths in MCP responses use
/on every host. They are portable, Obsidian-style and identical across the devices that share a vault. Paths outside the vault are returned unchanged.The operator command bridge runs under
cmd.exe /d /s /con Windows and undersh -celsewhere.The self-heal reindex spawns
bun run main.tson Windows without a console window, and it passes the live environment to the child: Bun otherwise gives a child the environment the process started with.The Hermes memory provider on Windows starts the MCP bridge without a console window, which matters because the Hermes desktop app is a GUI process. Its last-resort branch resolves
o2b.cmdto an absolute path, becausePopen(["o2b", ...])cannot start a batch file. Its executable lookup ignores abunoro2bthatshutil.whichfinds in the current directory, which Windows searches before PATH, unless that directory is on PATH itself.bun installworks on Windows. The inlineshprepare script is nowscripts/prepare.ts, run asbun scripts/prepare.ts || exit 0, so annpm installfrom a git URL on a machine without Bun still never fails on it.
Fixed
Snapshot member order was not deterministic. Tar walked each directory in
readdirorder, which is hash order on ext4 and name order on NTFS and APFS, so two copies of the same tree archived their members in different orders. Every member is now listed explicitly, depth-first in byte order, and handed to tar with--no-recursion --null -T, which GNU tar and bsdtar both accept. The member order is deterministic; the bytes are not, because tar headers still carry mtimes and owners.The walk fails the snapshot on any error other than a vanished entry. With
--no-recursion, a directory it could not read would otherwise be archived empty, and bsdtar (macOS, Windowstar.exe) reports success for that.Member names travel as raw bytes, so a file name that is not valid UTF-8 (a latin1
café.mdon Linux) is archived under its real name instead of failing GNU tar.
The dream pass wrote filesystem-dependent output. It listed inbox signals in
readdirorder, somoved_to_processedand the evidence list of a promoted preference differed between machines for the same corpus. The listing is now sorted by code unit.o2b brain protect --target claudecodeprotected nothing on any platform. Claude Code reads a single leading/in a permission rule as relative to the settings source, not as the filesystem root. The rules are now written as//abs/pathon POSIX and//c/...on Windows, the form the Claude Code permission docs give for each.Several paths were compared or split on
/alone:hidden artifact references were shown on Windows;
inbox-drain created an idea note as a directory;
an explicit session import was never recorded in the ledger;
novelty scores were null;
freshness findings leaked host paths past owner scope;
o2b indexlisted\notes\a.md;a drive-letter git remote parsed as a URL scheme.
Each now uses
node:pathor the existing containment helpers, and each has a test.Snapshots failed under Git for Windows' tar. Inside Git Bash, and on GitHub's Windows runners, the
tarfirst on PATH is MSYS GNU tar, which cannot extract into a backslashedC:\...directory. Every path handed to tar is now written with forward slashes, which it andSystem32\tar.exeboth accept.Updating a note with the Windows read-only attribute failed with a raw EPERM after the before-image was stored. It is now refused up front as
target_unreadable, the code POSIX already produces for EACCES.Piped stdin could be lost on Windows (
capture,codec,secret): the process exited before the pipe drained. It is now read through one helper that waits for EOF.A lock file being deleted by another process reported EPERM on Windows, which failed a concurrent ingest instead of waiting. It is now treated as "lock busy". An EPERM with nothing at the lock's name is a directory that refuses to create files (an ACL, Controlled Folder Access), and it fails at once with its own error instead of waiting out the budget as "lock busy".
The search index swap deletes its
.bakand.newfiles with the same Windows retry as its renames, so a stale file held by an antivirus scan no longer fails the reindex.closeDatabasenever throws. If the fallback lazy close throws too, the error is dropped, so a caller'sfinallycannot replace the original failure.Tests leaked into the real user profile. A lineage test and the self-heal child could write into the operator's real config, on Linux as well as on Windows. The test preload now also redirects
LOCALAPPDATAandAPPDATA, and spawned children inherit the hermetic environment.The Hermes anti-drift test always skipped itself on Windows, because
Popen(["o2b", ...])cannot starto2b.cmd. It now resolves the launcher first and runs.
Notes
Verified on a real Windows 11 machine, not only in tests:
Claude Code 2.1.280: the plugin's MCP servers connect. Claude Code starts the extensionless
scripts/o2bthroughcmd.exe, which resolveso2b.cmdvia PATHEXT, so.mcp.jsonneeds no Windows variant.Hooks: they run under Git Bash and return valid JSON.
Hermes Agent: its memory provider completes the MCP handshake with 115 tools, with Bun off PATH.
install-cli: the launchers pass stdin through the exactcmd /d /cform hosts use.The current-directory hijack: before the fix,
cmd /d /c o2b --versionrun from a directory holding a plantedo2b.cmdran the planted file, and the absolutescripts\o2b.cmdran a plantedbun.cmd. After it, both reach the real CLI.
Claude Code hooks need Git for Windows. Without it Claude Code runs hook commands through PowerShell, the POSIX command strings fail to parse, and the hooks are skipped without blocking the session; the MCP tools are unaffected. A PowerShell variant was not added because Claude Code has no per-platform hook command.
Unverified: Grok Build's hook execution on Windows. xAI does not document which shell runs it, and an unquoted absolute command breaks on a path containing a space.
162 tests are skipped on Windows, each with a one-line reason; a host without a real Python adds the five Hermes resolver-parity cases. More than half depend on chmod denying access or on an unreadable-file fixture; the rest need bash scripts, POSIX signals, FIFOs, file names Windows forbids, or deleting the process's working directory.
1.56.0 - 2026-09-12
Open Second Brain answers a great deal about the vault it owns and, until this release, very little about itself. An operator with a shell could not ask which version was installed, though the MCP handshake had carried the same fact as serverInfo.version since the server existed. An agent on the MCP surface could not ask which projects point at this vault or whether the hosts this install wrote registrations into can still be reached, though both answers had shipped as typed readers with CLI-only consumers. And the first block of text every agent reads at connect time was three frozen strings chosen by scope alone, so a host that disabled a tool still received a paragraph instructing the agent to call it - a confident answer that was wrong before the session began. Nothing in this release computes a new fact. Four surfaces start reading what four readers already produce, and the machinery that made them unreachable is the whole of what changed.
Every card in scope was read against the live source before anything was designed, and three of the five verdicts amended the card. The version card asked to reuse an existing packageJson.version import and understated the problem: three independent reads of that field existed in the tree, so adding a fourth would have written the defect into the change meant to answer it. The linked-projects card is smaller than it states - both readers are already pure, typed and tolerant, and the absolute-path question it raises is answered by the shipped expose_host_paths contract rather than by a new rule. The connector-health card is largely refuted: per-target status is already a closed vocabulary, an unverified probe is already a named member carrying its reason rather than an assumed success, the deadline is already enforced at ten seconds per probing target, and the aggregate view its own validator believed was the remaining gap already ships as o2b install --check. What survives is narrower and of a different shape than the card states, and it is stated in the Notes below along with the one card that was refused outright and not built.
Added
o2b version, and--versionas its synonym. The verb prints the installed version and exits 0;--jsonrenders{"version":"…"}for a caller that parses. The root flag is routed to the same renderer rather than printing a line of its own, because two spellings of one question that answer differently are worse than one spelling - the difference is invisible until someone pastes the wrong one into a bug report. Both are declared in the command manifest, so shell completions ando2b help --jsonoffer them.o2b version latestis a usage error rather than a print of the local version: nothing in this build learns what any other version is, and answering that question with the installed number would be the wrong answer wearing the right shape.second_brain_wiring: what this install is wired into. One read-only tool with a requiredviewand no aggregate member, so a projects read never pays for a host probe and every payload stays bounded - the same idiombrain_brief,brain_analyticsandschema_inspectalready use, for the same token reason.view=projectsreturns one entry per registered project link carrying the four-value pointer state and whether the vault it names still exists, offlinkedProjectsStatus, which had one consumer and none undersrc/mcp.view=hostsreturns one entry per registered install target carrying the target, theVerifyStatusmember, the adapter's details and its fix hint, from the sameverify()call over the same registry and the sameInstallEnvthato2b install --checkmakes. An absent or unknown view is refused with-32602naming the accepted members, and so is ahostsread with no resolved vault. Paths do not travel:project_refandvault_refrender through the shippedexpose_host_pathscontract, and because the hosts view carries the adapter's own sentences - whichverify()composes from the host home, where no store reference applies - the home prefix in those is folded to~under the same flag. An MCP response lands in model context, so the payload that hashes the vault path must not name the operator's account beside it.The handshake instructions are rendered from the live capability report. This server has evaluated a runtime capability window in its constructor for several releases and knows, per tool, whether it is available or withheld and why.
initialize.instructionsdid not read it. Guidance whose tool this runtime withheld is now REMOVED rather than contradicted - an instruction and its retraction in one document leave the reader deciding which half is true, and the half printed first is the half that shapes the plan - and a block at the end names each removed tool with the reason the evaluator produced, inventing no second vocabulary. Each scope body is an ordered list of paragraphs of tool-tagged segments; a segment renders when every tool it names is available, the separator of the preceding rendered segment joins the survivors so a dropped clause takes its punctuation with it, and a paragraph whose every tool-bearing segment dropped renders nothing at all, its lead-in included, because "Five tools live here" is a claim that stops being true the moment one of them is withheld.
Changed
One version constant, and the tree's only manifest import.
src/core/version.tsexportsOPEN_SECOND_BRAIN_VERSION; the continuity export header, the MCP handshake and the stamped opencode plugin read it. A census oversrc/holds the import to exactly one module - an equality with a positive control, not a floor - so the fourth copy cannot appear quietly. The same import was previously written two ways, with and without thewith { type: "json" }attribute, and is now written once.The install environment is built in one place.
buildInstallEnvmoved from theo2b installverb intosrc/core/install/env.ts, so the MCP hosts view verifies the same machine the CLI does. Two builders would be two answers to which machine is under verification, which is the split that constructor's own documentation was written to close. The CLI verb keeps its argument resolution and delegates the construction, and the sentence a run with no vault refuses with is now one constant both surfaces read.The path policy generalises rather than repeats.
vaultPathFieldis nowhostPathReference(path, source)applied to the vault;vaultStoreReferencenever required the path to be the vault. Both sides of a project link render through it, soproject_refandvault_reffollow the sameexpose_host_pathscontractvault_pathobeys - an opaquevault://<hex>by default, the raw path when the operator sets the flag - and a config that cannot be read renders{ "error": … }rather than degrading to the path the reference exists to hide.The instruction block's fully-available render is byte-identical to what shipped before, pinned against fixtures captured from the strings it replaces, so a reshaping of three bodies cannot become a rewrite of their meaning without a test saying so. One sentence in the
catalogbody is the declared exception, and it is listed under Fixed below. A segment naming a tool its scope does not register fails an orphan census in the same file, so a renamed tool cannot silently take its guidance with it.
Fixed
The dream observer-equivalence test normalised a field but not the hashes derived from it.
attaching an observer changes nothing the pass writescompares two vault trees and already normalised the snapshot archive'ssize_bytes, which is nondeterministic because a tar carries the mtimes of the tree it was taken from and the two copies straddle a second under load. The day log's chain hashes are computed over the payload including that real size, and every later line chains off the first throughprev, so one straddled second re-keyed the whole file in the two places the field had just been removed from. It passed on every developer box and failed on a loaded CI runner. The chain fields are now normalised alongside the size, with the reason stated: the chain has its own end-to-end integrity suites, and what this test asserts is that the observer changed nothing the pass authored.A before-image stamped in the future was never pruned.
pruneWriteImagesmeasures each image withfileAgeMsand floors the result to whole days.fileAgeMsdeliberately does not clamp - a file stamped ahead of the clock is a real condition it reports rather than hides - so an age of -1ms became -1 whole day, which is below every retention window including zero, and the image was kept forever. The condition is ordinary rather than exotic:nowMsis sampled once before the sweep, and mtime granularity on a synced or networked filesystem rounds a write made just before it up past that sample. It turned CI red on one image of two. The sweep now floors a non-positive age to zero at the call site, where the floor is part of retention's meaning rather than of the measurement, and a test stages the future-stamped image deterministically instead of waiting for the race.view=projectsreturned an unbounded list. The tool takes a preview-budget exemption whose reason claimed both views were bounded by construction. That is true ofview=hosts- one entry per adapter in a registry this build closes - and false ofview=projects, which reads a registry nothing caps: everyo2b brain project linkappends toprojects.json, and the serialized response grows with it, inline, into model context. The list is now capped at twenty-five and carriestotal,returnedandtruncatedbeside it, the same self-declaring shape the page-lint report uses, so a capped answer cannot be read as a complete one. Capped rather than budgeted because the preview budget is per tool and not per view: at ~2000 characters it would park the hosts view, which renders around 1700 with nothing installed, in an artifact on nearly every call. The exemption reason now states what actually bounds each view.The unresolvable path reference reported the config path into model context. When the device-local config cannot be read,
vault_path- and nowproject_refandvault_refwith it - degrade to{ "error": … }rather than raising or falling back to the raw path. The reason carried wasConfigReadError's own message, which names the config file twice, once in the condition and once in thechmodremedy. That message is right on the four channels it was written for; this is not one of them. It is the field forty-five tools emit precisely so that an absolute path under the operator's home does not travel into model context, so reporting the condition by naming that path reintroduced, in the degraded branch, the leak the resolved branch prevents. The field now carries a path-safe reason and points atsecond_brain_status, which names the file from a field whose contract is to name it.The catalog handshake told the agent that a withheld tool was still callable. The frozen string said every tool omitted from
tools/liststays CALLABLE throughtools/call, which is true of the tools the catalog surface hides and false of the ones a runtime capability window withholds: the window removes them from the very tabletools/callroutes on. Carrying that sentence unchanged into a rendering whose whole subject is the window would ship a handshake contradicting the runtime it was rendered for - the defect this release exists to remove, one paragraph over. The sentence now separates the two cases, the pinnedcatalogfixture carries the corrected text, and the exception to byte-identity is stated in the fixture test, in the module that renders it and indocs/mcp.md.Two places said the catalog advertises seven tools without saying when.
evaluateToolCapabilitiesapplies the runtime window before counting, so a window withholding one of those seven lowersadvertised_tool_countwith it. Theadvertised_tool_countdocblock and the matchingdocs/mcp.mdparagraph now state that seven is the no-window count, and name the capability diagnostic as the one tool no window withholds.docs/mcp.mdsaid the full server advertises 111 tools. It advertised 114 before this release and 115 after. Two further counts in the same file were stale in the same way and are fixed with it: the full surface was also given as 49 in the.mcp.jsonsection, which now points at the one place the number is stated, and thecataloggap was given as a figure that goes stale on the next tool added. That one is restated as the relation it always was - the catalog advertises seven whatever the tool table holds - in the doc and in theadvertised_tool_countdocblock it mirrors, so there is no number left there to drift.
Notes
Release awareness was scoped, verified against the source, and not built. The card asked for a surface reporting whether a newer version is available "by reading only cached update state". There is no producer for that cache: a search across
src/forlatestVersion,latest_version,update-check,updateCheckandregistry.npmjsreturns nothing, so nothing in this tree ever learns what the latest release is. Filling the cache requires a network check, andrenderDataOwnershipinsrc/core/install/ownership.tsrenders to the operator, verbatim, that this tool has no server it phones home to, no sync endpoint and no update check. Building the card as written means either making that statement false or shipping a surface whose only possible answer is that it has nothing to report. A collision the card did not see:o2b updatealready exists and means re-applying managed install blocks to host configs, so the obvious verb is taken and the obvious name would mislead about an existing one. The two honest alternatives - registering an outbound release check in the egress registry as a network site like every other outbound path, or reading a version fact some other component already fetched and saying where it came from - both amend an operator-facing promise about network behaviour. That is an operator policy decision rather than an engineering one, and it is recorded here for the operator to take rather than taken on their behalf. The upgrade-plan half of the card needs nothing:src/core/brain/upgrade.tsis already a plan and apply migrator.The
hostsview may ask host CLIs, and its description says so. Two of the registered targets declare a host probe, so a worst-case call adds two subprocess round trips, each bounded by the ten-second deadline--checkalready enforces; a diagnostic that can take twenty seconds and does not say so is a surprise. A probe that could not run renders its named reason instead of an assumed success, which is the property the probe module already held and this view carries through unchanged.An unresolved vault is refused by name on the
hostsview rather than reported as ten absent runtimes.verify()reads the per-vault sidecar manifest, so an unset vault producesnot-installedfor every target off a path that does not exist - a wrong answer wearing the authority of a completed check.The withheld block in the instructions lists the tools that text would have instructed, not the whole withheld set: a
--max-toolswindow can withhold over a hundred tools, and pasting that list into the first thing every agent reads would cost more than the guidance it explains. It points atsecond_brain_capabilitieson the surfaces that register it. No capability window ever withholds that tool, but the writer scope never registers it at all, and a handshake sending an agent to a tool its own server answersunknown toolfor would be the defect this rendering removes, reintroduced by the sentence explaining the removal; there the block ends with the names.One test limit is stated rather than implied.
os.homedir()in this runtime does not follow a laterprocess.env.HOME, so an in-process test cannot redirect the home thehostsview resolves. The named-probe-skip case therefore builds theInstallEnvby hand, as the adapter suites do, and asserts the view's own mapping over the realverify()answer; a separate deep-equality test is what ties that mapping to the environment the view builds.
1.55.0 - 2026-09-10
Open Second Brain has known who wrote a signal or a preference since the log shipped, has taken a snapshot before every destructive pass since 0.10, and has sharded the Brain log per device since 0.10.8 so that two machines syncing one vault never fight over one file. What it could not say was who wrote a note. A brain_create_note, an update_note, an append_note or a write_batch left no event in the log and no row in any roster, and the bulk of what an agent produces was therefore invisible to attribution, to brain_event_trace, and to any rollback finer than the whole Brain/ tree at a snapshot instant. This release closes that, and the four things that follow from closing it. Every note write now emits one note-write event carrying a write id, the operation, the target, the content hash and byte size on both sides, the caller-asserted agent and the server-derived origin channel, with the device riding on the shard name as it does for every event, and the bytes the write replaced kept in a content-addressed store so that a revert has something to restore from. A revert is a plan over those events for one agent, device or path in a window - restore, delete, or a refusal by name when another writer sits on top - sealed by a digest, applied only as the exact plan the operator read, under a snapshot, and recorded as a write of its own so that a revert is itself attributable and revertible. A freeze marker at Brain/.state/frozen.json, carried by Syncthing to every device, is refused by name at the one guard every content writer already passes, while the Brain log keeps recording so the refusals are auditable; o2b brain freeze and o2b brain unfreeze are the operator verbs and o2b brain status, brain_status and brain_doctor all say so. The shard rule the log has followed for a year is now the rule for every append-only ledger in the vault - continuity, idempotency, preference audit, metrics and session lineage each write <base>.<device-id>.jsonl and merge every shard at read, and the doctor names a sync-conflict copy under any of those directories, not only the log. And because the log is now the attribution record for every write, every JSONL line it appends carries a per-shard hash chain: a line that was edited, removed or stripped after it was written has a file and a line number, reported by o2b brain log verify and the doctor and never used to gate a read.
The wave was scoped from a comparison against a published control-model checklist for agents with delegated authority whose fleet layer - a kill path, a causal log attributable per agent and per machine, and rollback - is described in prose and not shipped. The comparison found that Open Second Brain already had most of the described log and none of the described stop, and that its own log discipline had not been applied to its own other ledgers; the five kanban cards behind this release are those findings, each verified against the live source before anything was designed.
Added
Every note write is an event.
createNoteand the write-batch kernel'supdate_noteandappend_notecommits each append exactly onenote-writelog event after the bytes land:write_id(nw_<14 timestamp digits>_<16 hex>, derived from the event body so two devices that record the same write agree on its id),op,target,hash_before(the literalabsentwhen the target did not exist, deliberately not the hash of zero bytes),hash_after,bytes_before,bytes_after, andagent. The bytes an update or append replaced are kept atBrain/.state/write-images/<sha256>, written before the atomic write and skipped when an image of that content already exists, so a note toggled between two states costs two files and not two per write. A log failure never fails a write that has already landed; the result and the MCP receipt carrywrite_id: nulland anaudit_reasoninstead, and a create that skipped an occupied target records nothing because it authored nothing.o2b brain writesandbrain_writes. The record read back: filter by agent, device, path, window and operation, newest first, with the device reported as the shard the event came from.brain_agent_queryandbrain_agent_diffgain thenotecontribution kind off the same events, so "which notes did this agent touch" has an answer with no new reader.o2b brain writes prune-images [--older-than-days N] [--dry-run]bounds the image store by file age, default 30 days; an image is a copy of note content the snapshot region already archives, so removing one loses no content and only narrows how far back a revert can reach, which a revert plan then reports by name.o2b brain writes revertandbrain_writes action=plan_revert. A selector - at least one of agent, device or path, with an optional window - resolves every target the selected writes touched to one action:restoreto the bytes the oldest selected write replaced,deletewhen the oldest selected write created the note and nothing preceded it, or a refusal by name:driftwhen the note on disk is not what the newest selected write left,interleavedwhen an unselected write sits between the selected ones,image-missingwhen the before-image was pruned,already-revertedwhen the target already holds the bytes the plan would restore,unrecordedwhen the target cannot be read at all. A plan carries the log's parse warnings beside its entries, outside the digest, because an operator deciding what to undo has to be told when the history is incomplete. The plan is sealed by a digest over the selector and every entry, refusals included;--apply <digest>re-plans, refuses before any byte when the digests differ, runs under anote-revertsnapshot, and records each restore and delete as anote-writeof operationrevert. Apply is CLI-only; the MCP tool plans and does not apply.A fleet freeze.
o2b brain freeze [--reason]writesBrain/.state/frozen.jsonando2b brain unfreezeremoves it; the marker records who set it, from which device, when and why.assertVaultIdentityForWrite- the guard every content writer in the tree already calls before its first byte, ninety-eight call sites across sixty modules - refuses the content lane by name while the marker exists, withfrozen_at,by,reasonand the next command. The guard gains a write lane: the Brain log appender and the marker writer ask for the audit lane and are the only two that do, pinned by a source scan, sofreeze,unfreezeandwrite-refusedevents are recorded while the freeze is in force. A malformed marker is a freeze, not an absence. The MCP boundary maps the refusal to a structured error and appends onewrite-refusedevent naming the tool.o2b brain rollbackon a frozen vault is refused like every other writer; the recovering operator is exactly the operator who should decide the freeze is over.Per-device shards for every ledger. The shard grammar, listing, sync-conflict exclusion and device-id resolution move out of the log reader into
ledger-shards.ts, byte-preserving for the log, and the continuity store, idempotency ledger, preference audit, metrics and session-lineage ledger write<base>.<device-id>.jsonland merge every shard at read in a deterministic order. The preference audit is the one ledger that changes shape: a flat<pref-id>.<device-id>.jsonlis a name another valid preference id could own, because a preference id may itself contain a dot, so new audit lines land in one directory per preference,Brain/log/pref-audit/<pref-id>/device[.<device-id>].jsonl, the legacy flat trail stays readable, and a row whosepref_idis not the one asked for is dropped with a warning naming both. The bare name is the shard with the empty id everywhere, so no file is renamed and a device with an id never appends to a bare file again. The lineage ledger keeps one hash chain and one sequence per shard, compacts per shard, shards its gap sidecar the same way, and its verifier reports per shard path. The doctor'ssync-conflict-logfinding sweeps every ledger directory and names the one the copy sits in.The log chain. Every JSONL row appended from this release on carries
h, the sha-256 of the canonical JSON of{ prev, ts, kind, payload }, andprev, thehof the previous chained line in the same shard ornullat that shard's genesis. Editing any earlier line changes every later hash, so a tampered history cannot be re-linked without rewriting the shard from the edit forward. One chain per shard, because two machines append to two files that Syncthing delivers in whatever order it likes. Rows written before the chain are legacy and clean while they precede it; the same shape after a chained line is a stripped line and is reported, and a first chained line naming a predecessor means the head was cut off.o2b brain log verify [--json]reports the first break per shard ashash-mismatch,prev-mismatchormalformedwith the line number;brain_doctoremits onelog-chain-brokenwarning per broken shard. Report-only:readLogDayreadsts,kindandpayloadand nothing else, so a broken shard still yields every event it holds. The markdown twin is a derived rendering and is not chained.
Changed
readLogDayreports the shard beside each entry.entryShardIdsis a parallel array at the same index asentries, the empty string being the legacy un-sharded pair. A parallel array rather than a payload field, because the shard is a fact about the file and not a claim the writer made.createNote,applyWriteBatchandscaffoldStubaccept aconfigPath. Without it the note-write record resolved the agent against no config and attributed every MCP write to the unconfigured default instead of the identity the server's own config names. Attribution is the deliverable, so the three signatures grew one optional field.The state-surface catalogue has 44 rows, up from 42: the freeze marker and the before-image store, each with a written recovery story. The write-site census admits the two new writers as shared-helper rows.
Notes
The
agenton a note-write event is a caller-asserted claim, exactly assrc/core/write-binding/index.tshas stated since 1.52.0;origin_channeland the device shard are the facts. No gate in this release keys on the agent name, and the freeze keys on the vault, not on who is asking. A policy layer over who may write remains a separate card.The chain is the one place this release changes bytes for a vault that enables nothing: two keys on every new log line. Legacy lines read exactly as before.
Revert refuses on doubt and never merges. A target another writer touched after the selected writes is reported, not reconciled; the plan digest covers the refusals, so the operator who applies a digest applies the plan they read, refusals and all.
The lineage state-surface row still derives the bare file name, which on a device with an id does not exist; the row's reason says so. Deriving a shard name there touches the inventory contract and is left for a later card, as is re-pointing the two ledgers that already carried their own copy of the shard grammar (
truth/store.ts,decisions/receipts.ts) at the shared module.An independent review of the finished branch returned three blocking findings and six non-blocking ones, all applied: the before-image prune threw on any foreign file name in its directory and now reports such names as
skipped; the preference-audit shard grammar let one preference read another's trail, closed by the per-preference directory above; the idempotency ledger and the continuity store had started swallowing every shard read error where the unsharded reader propagated it, so a permission fault read as "key never seen" - both now rethrow anything but a missing file; a craftedtargeton a note-write event aborted the whole revert plan with an unnamed error and is now the per-target refusaltarget-invalid; one throwing entry in a revert apply aborted the rest after the snapshot and is now a namedfailedbucket the others continue past; and the audit-lane pin reads both spellings of the lane.Note-write events carry no session or turn id: the MCP server context does not hold them today, and inventing one at the seam would be a claim with nothing behind it.
1.54.0 - 2026-08-27
Open Second Brain has shipped a visibility: frontmatter field, a predicate that reads it, and a filter that applies it - and until this release the field was decorative. The filter was wired into exactly one call site, and the scope it filtered against came from the caller, so a remote caller that asked for the private scope was handed it. Of the 71 callable surfaces the project's own visibility census enumerated, 3 consulted the field and 68 did not. The previous wave measured that rather than closing it: 1.52.0 shipped the census, the registry behind it, and an o2b search check finding that stated in the operator's own diagnostics that this boundary did not exist. This release is the enforcement that census was built as the coverage map for. One reserved token is denied by default at remote reach and enforced at the three roots every read surface reaches page content through - the ranked search pipeline, the page walker, and the by-path and by-chunk-id read primitives - and a new sweep proves mechanically that there is no fourth root. The reach that decides is minted by the transport and refused by name when a caller supplies it, because a trust claim echoed back from the request is not a trust claim. A vault that never wrote the token sees no change at all, and the schema-12 backfill is what guarantees that rather than an assumption.
Both source cards had premises refuted against the live source before anything was designed, and the verdicts ship as deliverables rather than as obstacles that were quietly designed around. The first card's claim that no visibility: handling existed in the read surfaces is refuted by src/core/graph/visibility.ts and src/core/search/result-filters.ts, and the carrier it proposed - extending gatedOwnerScopeView - is refused as an approach: that view short-circuits every predicate to true unless integrity.owner_scope_delivery is set to fail, so riding it would have shipped privacy that is off in every default install. The second card's claim that no registry-driven harness and no fail-closed gate existed is refuted by the agent-scope matrix and the surface census, both of which already fail on an unclassified newcomer; what survives is that the harness's axis is ownership rather than visibility, which is the axis this release adds. The wave's own stated risk is corrected too: indexRevision is a query-cache generation counter consumed by computeCorpusGeneration, not a schema or index-format revision, and moving it forces no reindex on anyone.
Added
One reserved visibility token, denied by default at remote reach.
isRemotelyReadableis the kernel the rest of the wave consumes: it denies exactly one token -REMOTE_DENY_VISIBILITY_TOKEN, written once in the normal formpageVisibilityreads tags into, so a page cannot spell it a second way and mean something else - and answers true for everything else at every reach. Every other token, reserved-adjacent or not, keeps the caller-liftable semanticsisVisiblehas always given it; this constant reserves one identifier and defines no policy for any other. The caller's ownvisibilityargument still works and can still narrow. What it can no longer do is name the reserved token and be handed the page. There is no configuration key to switch this on, because a boundary an operator has to enable is the defect the previous release's honesty finding named.The reach a request arrived at is minted by the transport and refused from the caller.
TransportReachis a closed two-member vocabulary -localandremote- and it is a transport fact rather than a request field. stdio and the CLI mintlocalbecause the caller already started the process and holds whatever filesystem access it runs with; an HTTP bind mints fromisLoopbackHostalone, keyed on the bind rather than on a caller-suppliedHostheader, so every non-loopback bind isremote. Each value is set after the caller's runtime options are spread, the same guardsendNotificationalready uses, so a runtime option cannot claim a reach the bind did not establish. A caller that puts a reach-shaped argument in atools/callis refused with-32602and the tokencaller-supplied-reach, ahead of the unknown-argument gate and without consulting the tool's schema, so an open schema is covered too; names are compared case-folded and separator-free, soreach,Reach,transport_reach,transport-reachandtransportReachare one argument rather than five holes.disclosureis deliberately not reserved - it is the unrelated result-depth mode on the recall surfaces. What thelocalbypass proves is written down where it is defined rather than overclaimed: filesystem-equivalent access, or a request that originated on this host. It does not prove operator intent, since a remote host can spawn a stdio subprocess. The alternative - no bypass at all - would take an operator's private notes away from the operator's own shell.Enforcement at three read roots, and a sweep that proves there is no fourth. Root A is the ranked pipeline, where
applyVisibilityScopeis reimplemented on the predicate rather than left beside it, and the reach rides onSearchOptions.transportReachwith an absent value resolving to the narrowest. Root B islistVaultPages, where the reach is a required option rather than a defaulted one - the two ways to get it wrong fail in opposite directions - so each of its 23 call sites states which caller it walks for and the compiler is what asks the question; the maintenance lanes answer once, throughMAINTENANCE_LANE_REACH, because a repair lane that stopped seeing reserved pages would corrupt the link graph it repairs. Root C is the key-addressed reads, where both key spaces are enumerable - a chunk id is a sequential integer and a preference id is a topic slug - so a withheld read answers with the message an absent one produces, byte for byte, rather than with a distinguishable refusal that would be an existence oracle over exactly the population being hidden.DIRECT_VAULT_READ_REGISTRYand its architecture sweep make the closure claim checkable instead of prose: every file undersrc/mcp/,src/cli/andsrc/openclaw/that opens a vault path throughnode:fsrather than a root must carry a row with a closed category and a written reason, and the sweep fails in both directions. It found two on its first run -brain_clusterslistingBrain/clusters/*.mdandbrain_bridgesreadingBrain/proposals/bridges.md- and both now ask the rule at the site of the read. What the sweep cannot see is written down beside it rather than implied.documents.visibility: what the index measured, backfilled by schema 12. Three distinct states - a JSON array of the page's normalised tokens,[]for a page that declares none, and NULL for a document the index holds no frontmatter chunk for. The backfill is the whole of why this is more than anALTER TABLE: a column added without one is NULL for every existing row, and the only fail-closed reading of unmeasured is reserved, which would withhold every document in every vault on upgrade. The frontmatter is already in the index as its own chunk atchunk_index = 0, so the backfill is one table scan and a parse per document with no vault walk and no operator action. The design's open question is answered by measurement: it runs inside the migration transaction, and not for speed - 86 ms over 5000 documents, against 8.9 seconds to index the same corpus - but for atomicity, since a half-filled column leaves measurable rows reading as unmeasured. The column reports and the live file decides: moving the read boundary onto the column would trade promptness for a saved frontmatter read, so a page reserved a minute ago would stay readable until the next index run. What the column earns is exactness -anyVisibilityTagPresentwas aLIKE '%visibility:%'scan that read a quoted key or a body line as a tagged vault, and both false positives are gone - plus the two populationso2b search checknow reports rather than absorbs.The OpenClaw page walker joins the boundary and the census.
src/openclaw/index.tswas the one surface the previous census named as un-swept, which meant the place already identified as a gap was the place the sweep could not have found it. Its page search takes the narrowest reach, and the distinction from the CLI is the caller rather than the machine: a CLI verb is typed by the operator, while these tools are called by a model over a conversation the plugin cannot see.second_brain_query'stotal_pagesis now counted after the filter, closing a difference betweentotal_pagesandreturnedthat the status-scope filter had already made observable.
Changed
The
o2b search checkvisibility finding states what is now true. It used to say the field was a caller-supplied view filter and not a privacy boundary, which was true and is not any more. It now reports the three things an operator needs in order to size the change rather than infer it from a search that came back shorter: how many indexed documents reserve themselves against remote reads, how many of the enumerated note-returning surfaces still do not consult the field, and how many documents the index holds no measurable frontmatter for - reported rather than folded into "declares nothing". Every number is read at render time off the registry and off the index's own column; none is written down here. The finding fires whenever there is something to be honest about, which includes an index whose column is NULL for every row - the state that count exists to surface, and the state in which an earlier gating bug printed no line at all.The visibility registry reclassifies, and the counts fall out of it. 24 of the 76 enumerated callable surfaces now consult the field, up from 3 of 71: 13 MCP tools, 7 CLI verbs and the 4 templated
osb://readers, each with a written reason naming which root covers it. The 52 that remain are the honest remainder, each carrying what it actually discloses rather than being waved through. Theindex_storerow is kept and reworded rather than dropped: a reserved page's text is still inchunksandchunk_fts, deliberately, because excluding it would take an operator's private notes out of the operator's own local search. That is the same trust boundary as the vault's own Markdown files, and this release does not close it.
Fixed
The query cache is partitioned by the reach that computed it.
buildCacheKeyenumerates every result-affecting option explicitly, and the reach became one the moment it decided which pages are in the pool. It was not in the key, so alocaloutcome and aremoteoutcome collided on one row of the sharedquery_cachetable that the operator's CLI and the HTTP server both write: whichever warmed the row first served the other, handing a remote caller the reserved pages it had just been denied, and poisoning the operator's own shell in the reverse direction. Folded in resolved rather than raw, and unconditionally, because every row written before this boundary existed was computed without it.The retrieval trail stopped counting withheld rows.
scope-filters-dropped-rowsrides out on everybrain_searchresponse and is not gated behindexplain. Its baseline was taken in front of both rules, so a remote caller searching a term nobody public wrote about got zero results and a trail saying two rows had been removed - an existence oracle enumerable one query term at a time. The two rules are separate functions now, the reach rule runs first, and the trail's baseline is taken between them, so the number it publishes is exactly what the caller's own arguments removed. The assembly's one-shot backfill keeps the other count in its own field, because a reserved page crowding a narrow rank cap still has to widen the window.The successor pull-in re-asks the rule. Relation polarity runs after the pool filters and does not only reorder: it resolves typed
superseded_byedges to documents that were never in the filtered pool, fetches their chunks, and appends them with full path, title and content. A public page declaring a reserved successor was therefore a route to that successor's body, and the pool filter could not have caught it because the row was not in the pool. Root A is asked again over the whole pool. The identical gap for the caller's ownvisibilityscope andagentScopepredates this boundary and is recorded at the seam rather than silently carried.A caller-supplied benchmark is scored at the caller's reach.
runRecallBenchmarkpinnedlocalon every search it made under a comment calling itself an internal lane. It is not one -datasetis a required argument ofbrain_benchmarkandbrain_tune, both callable over any transport. The reach is an option now, resolving to the narrowest when absent. The reported premise is refuted and recorded rather than dropped: a tagged page is outside every benchmark run at every reach because the benchmark passes no visibility scope, so the observable effect is the unmeasurable page - a document still in the index whose file was deleted or made unreadable - and that is what the pinned tests measure.captureRecallFeedbackhad the same defect in sharper form, re-running the judged query unscoped and returningresultFound.Root B fails closed on a page it could not read. Roots A and C substitute the reserved token for a page whose file cannot be read, on the stated ground that an unreadable visibility claim is not the absence of one. Root B decided off the parsed map alone, and the parser resolves an unreadable file to an empty map - byte-indistinguishable from a page that declares nothing - so
listVaultPageslisted it at remote reach with its path and its filename-derived title. The trigger is routine rather than exotic: a page deleted, renamed, moved to a failing mount or made unreadable between one call and the next. Toggling a reserved page's readability was itself the oracle. The walk now carries the read verdict beside the parsed map and substitutes the same token its twin does, spelled from the same exported constant so the two cannot drift.Two counts that reported what they had withheld.
brain_query mode=topicfiltered its signals and log events and then reported the pre-filter count - a number written to the recall-telemetry log and handed back verbatim bybrain_recall_telemetry, so a caller that saw an empty topic could read through a second tool how many rows it had been withheld from.brain_clusters operation=runreported community membership filtered by owner scope alone; both rules are ANDed now, and a community either rule withholds is dropped whole, because a community of seven reported as four is not a narrower true finding but a false one.A withheld preference names the id an absent one names. Both refusals claimed byte-identity with the absent form in their own comments and neither had it.
brain_query mode=preferencepassed an already-formatted sentence into a constructor that formats the sentence itself, andosb://preference/{id}echoed the raw id on the withheld branch while the absent branch rethrew the normalised one - so for the bare-slug key shape the presence of thepref-prefix was a one-bit existence oracle over the whole reserved population. Both key shapes are pinned on both surfaces now.brain_bridgesandbrain_backlinksask the rule about what they name, not only about what they return.brain_bridgeswas classified covered off itslistmode alone, whilediscoverreturned proposals naming two pages each by vault-relative path; a proposal is dropped whole when either end is withheld.brain_backlinksand its CLI mirror published every ref the index returned, and a ref names its source artifact, so a withheld target now answers with the empty backlink document and a visible target no longer names sources the caller may not see.The reference grammar stopped failing open on spelling and on shape. A reference the resolver could not place was read as naming nothing and let its row through - a fail-open on a rule whose whole posture is fail-closed. Three cases were live: aliased, anchored and line-ranged forms such as
pref-x|aliasandpref-x#Raw, whichbrain_apply_evidencedocuments for its ownartifactargument; multi-artifact payload values, which arestring | ReadonlyArray<string>and answered undefined as arrays; and an enumeration of four payload keys where a census of everyappendLogEventbody in the tree finds artifact references under thirteen. Every string in the payload is offered now, because enumerating a payload whose writers are open-ended is the defect rather than the particular keys that were missing, and offering too much is safe in the way offering too little is not.Two surfaces that were outside the denominator. The census sweeps a file only when it imports a note-content primitive directly, and
brain_hygieneandbrain_skill_proposalsreach page content through asrc/core/helper - so neither had a registry row, neither was in the numbero2b search checkreports, and both were returning reserved page paths at remote reach. Both are filtered at the handler seam the owner rule already sits on, so a hygiene detector registered later inherits the rule, and both recompute their own count from the visible rows.
Notes
What this release deliberately does not close is stated rather than implied. A reserved page's body stays in the index, so anyone with file access to
brain.sqlitereads it - the same trust boundary as the vault's Markdown files. Owner-scope delivery and this boundary remain independent rules over different subjects, and neither is folded into the other. Non-reserved tokens keep exactly today's caller-liftable semantics. And 52 enumerated surfaces still do not consult the field; the visibility matrix measures which of them can still name a reserved page at remote reach and asserts that list in both directions, so a surface leaving it is the wave that covered it and one joining it is a regression.The design called the closed vocabulary
disclosure. That word was already taken in this tree -SearchOptions.disclosureis the progressive result-depth mode - and this value rides on the same options bag, so the transport fact is named for what it measures instead. Two fields with one name meaning different things is the defect this suite exists to remove.The new visibility matrix is a sibling of the owner matrix rather than a second axis inside it, decided on evidence the design left open: the two cannot share one fixture, because the owner matrix builds its contexts without a reach and every one of its assertions would have had to say which reach it was asking at. The recipes are shared through one catalogue rather than copied, since a second copy of "what is the whole surface" is the enumeration defect the first matrix was built to remove. Every probe goes through the real
tools/callpath and asserts over the whole serialised response, which coversstructuredContent, the rendered text, a thrownmessageand the_metamember in one check.One recorded limit on the new column: an unterminated frontmatter block is measured as declaring nothing rather than as unmeasured, because the chunker leaves chunk zero of such a page byte-indistinguishable from that of a page with no frontmatter. It does not matter, because the live parse on the real file reads an unterminated block the same way - the column and the boundary agree rather than one of them being wrong in private.
A pre-existing test flake was root-caused and fixed on the way.
dream progress > attaching an observer changes nothing the pass writesfailed about once in twelve loaded runs and never in isolation; the whole difference was the snapshot entry'ssize_bytes, republished into the day log the assertion compares, and whether it tripped depended on whether the copy straddled a second. Verified againstmain's own source at the same rate before being fixed by normalising the one field rather than excluding the pass's main authored artifact.
1.53.1 - 2026-08-27
Reported as #180 by @BlackKite84: o2b brain page-dedup merges a duplicate pair, stamps merged_into on the secondary, and then proposes the same merge again on the next scan, forever. Confirmed as filed, and the seam the report named - findDuplicateCandidates, which never read the pointer the merge writes - is the right one; it is the single chokepoint behind page-dedup, brain actions, the digest, brain_doctor, vault_health and the operator trust verdict, so one filter closes all six. Verification found two escalations the report did not mention, and both raise this from a noisy digest to a silent vault mutation: on pages with no created_at the canonical ordering falls back to an mtime the merge itself moves, so a second --apply reverses the pair and writes a merge CYCLE that leaves lint --consolidate unable to repair any link to either page; and setMergedInto overwrote an existing merged_into without asking, so a deliberate operator merge was repointed at whatever canonical the next run picked. One side-claim in the report is rejected: identical text under different scopes cannot re-collapse, because the composite scope is already folded into the dedup key and a test has pinned that since v1.37.0.
Fixed
A page a merge already resolved leaves the dedup candidate pool. Any
merged_intopointer takes the page out of candidacy, not just one aimed at another member of the same group - the narrower reading the issue proposed leaves the two cases that cost something. A pointer aimed OUTSIDE the group kept the page a candidate and let the next--applyoverwrite the operator's own merge decision, and a chain that leaves the group and comes back (b -> x -> a) survived a one-hop test. A dangling target counts as resolved too: the canonical was lost, which is a lint problem and never a licence to re-merge the page somewhere else. Members are dropped individually rather than whole groups skipped, so a third page duplicating the canonical after an earlier merge is still proposed. The predicate is defined once and the two sibling detectors with the same blind spot adopt it - the digest's## Merge suggestionsand the hygienededupdetector, both of which repeated the finding in their own sections - which neededparsePreferenceto exposemerged_intoso the digest's pre-parsed path reaches the same verdict as the filesystem one.The merged-into write seam refuses cycles and silent repoints.
setMergedIntochecked for a self-pointer and nothing else. It now reads the existing pointer first: re-setting the same canonical writes nothing and reportschanged: false, a different canonical is refused by name unless the caller passes an explicit repoint, and a canonical whose chain already resolves back through the secondary is refused before a byte is written. The guard stays even though the dedup pass can no longer propose either write - the proposer is one caller, and this is where the damage happened. The chain walk behind the resolver and the new cycle guard is now one function, with the resolver'sCYCLE/DEPTH/MALFORMEDbehaviour, dangling targets included, unchanged.
Notes
The N2 idempotency test pinned the defect: it asserted the merged pair was still reported on the rerun. The invariant it exists to defend - "a rerun re-collapses nothing new" - is strengthened rather than broken by dropping resolved members, so it keeps its title and now asserts zero clusters.
page-id.test.ts's "updates an existing pointer" pinned the silent overwrite and is rewritten as the refusal / explicit-repoint pair it should always have been.A
merged-into-danglingdoctor lint is deferred. A new doctor code needs an entry inDIAGNOSTIC_SIGNALSorDOCTOR_EXIT_EXCLUSIONSunder the exit census, plus a frontmatter pass the doctor context does not carry - registration machinery rather than the small reporting-only addition it looked like from outside.
1.53.0 - 2026-08-27
Hermes injected recall on every gated turn without ever telling the vault what the turn was about, and @Yori940619 named it in #181: the pack was query-blind, and the pages that would have mattered lost the budget to whichever confirmed preferences happened to sort first. The diagnosis is right and the preference-first ordering the pull request argued for is the right instinct. The mechanism it proposed is the part that changed. Recall would have moved onto raw brain_search, and that path is the one place a curated injection must not go: it runs no prompt-injection guard, post-filters a general ranker instead of enumerating the preference directory, returns retired and merely-demoted superseded pages, never consults owner-scope delivery, enforces no response-level token budget, and issues no receipt. The fix belongs one layer down, inside the lane, and that is where it landed.
Added
brain_context_packreads its query two ways, and the second one ranks. The tool has always accepted aqueryand has always meant a case-insensitive SUBSTRING match againsttopic + principle, so a natural-language turn missed every candidate and came back with an empty pack and afilter-missfor each page.query_mode: "ranked"takes the query out of the filter and into the sort: the already-collected candidates are ORDERED by structural token overlap - the same deterministic, stopword-free, language-agnostic kernel the rated-decision matcher uses, with no model and no embedding, so it works on an install that has never indexed a vector - and none of them is excluded. The budget decides what is dropped, not a lexical accident. Relevance sits between session focus and density in the comparator and under tier, so a bound focus still dominates and a peripheral page still never outranks a core one. An omittedquery_modeis byte-identical to every release before this one and a test pins it; the mode requires a query, stated in the schema asdependentRequiredand refused by the handler rather than accepted as a silent no-op. Because the change is inside the lane, the containment guard, the curated pool, the tombstone and supersession-tip filters, owner-scope delivery,max_tokens, the named skip reasons, and the server-issuedreceipt_idare all inherited rather than bypassed.
Fixed
Hermes recall is query-aware without leaving the lane that makes it safe.
prefetchcalls one tool,brain_context_pack, and now carries the turn on it in ranked mode. The sample id in the injected metadata line is the server-issuedreceipt_idagain rather than a locally hashed string: an id with no receipt behind it resolvesunresolved / sample_absenton every outcome an agent posts, which would have undone what #179 shipped one release earlier. The local character budget is gone with it -max_tokensis a TOKEN budget the server enforces against the bodies it emits, and re-spending it in Python at four bytes per token over-injected by two to four times on Cyrillic and CJK vaults and cut the joined output mid-string with no marker. The two helpers that formatted search rows are deleted; one of them scanned a whole result for a line beginningprinciple:and returned that line alone, so an ordinary note carrying such a line in its body was reduced to it.Two silences on the Hermes boundary, both named now.
brain_context_packcomputed injection-time tension warnings and the owner-scope observation and its MCP projection discarded both, so the one surface that puts vault text in front of a model every turn was the one surface that could not say a memory it injected is contested; the tool forwards them asbrain_pre_compress_packalways has, absent when empty. On the provider side those warnings reach the log, and a gated turn that recalls nothing states that rather than passing for a healthy injection. The correlation defaultshandle_tool_callsupplies forbrain_context_pack_outcomeare also scoped tooperation: "post"-hostandsession_idare read filters forlistandsummary, and defaulting them there narrowed an agent's own query to this host's rows without telling it.
Notes
The three regression pins #179 added still pass, but they were passing for the wrong reason while the search lane existed: the test bridge answers
{}for an unregistered tool, which sent them down the fallback path instead of the shipped one. Each now asserts by name which lane ran, so a future rerouting fails them instead of sliding past.
1.52.1 - 2026-08-24
The context-pack outcome ledger shipped with receipts on the read side and nothing on the Hermes side to carry them, so a deployment that injected recall on every turn still recorded zero outcomes. Contributed by @Yori940619 (#179), with one course correction along the way that is worth recording: the first revision inferred success and repair from hardcoded phrase lists in a single language, and the rework removed raw-text inference entirely - the honest shape, and now the pinned one.
Fixed
The Hermes provider closes the context-pack outcome loop - structurally, not linguistically.
prefetchnow requests a context receipt and opt-in telemetry from the samebrain_context_packcall and appends a small structured metadata line (sample id, tool name,outcome: unknown_until_explicit_tool_call) to the injected recall, so the calling agent - the party that actually understands the user's language - postsbrain_context_pack_outcomeitself when the user explicitly confirms or corrects. The provider infers nothing from raw turn text, in any language, and a regression test pins that; an unsignaled turn stays unknown rather than being guessed into a vanity metric.brain_context_pack_outcomejoins the curated Hermes tool surface with its schema vendored against the live server.
1.52.0 - 2026-08-23
Nine tracker cards read against the live source before anything was designed, and the reading did most of the work. One card described a feature that already shipped end to end - closed, not built. One asked for privacy by riding a predicate that is inert in every default install - refused as designed, redesigned down to the truth it can tell today. Four more had premises the source contradicted: the "localized" stamp would have touched sixty-eight writers, the counter had no chokepoint to count at, the resume half already existed on the ingest lane, and the raise-on-attach the card wanted was already there behind a documented default. What survived is one rule applied eight ways: nothing writes silently, nothing degrades silently.
The units share their machinery on purpose. One pure reconciliation vocabulary - attempted, found, and missing with every missing key named, no bare-count constructor - is spoken by the import read-back census, the multi-artifact write accounting, and the embedder record audit, and is pinned by the same verdict-vocabulary census that guards the project's other closed vocabularies. Two facts the wave needed - which process a write came through, and which transport carried a request - are derived by the server at the entry point, never accepted from a caller: twenty-five tool schemas already take a caller-supplied agent string verbatim, and this wave declined to add one more unverifiable claim beside them.
Added
o2b search checkmeasures the pending-vector count instead of inferring it. The old recommendation branch fired on a provider-reachable proxy with no term for existing vectors, so a fully embedded, healthy vault was told to "compute the first vectors" - a false statement on the happy path, and one no test pinned. A COUNT(*) anti-join through the read-only seam replaces it: a fully embedded vault gets no recommendation, a partial one gets the count and the dry-run backfill verb that prices it, and an absent or unreadable index reports unrecorded - never zero, because an unmeasured quantity is not a measured nothing.An embedder record audit, and a restamp for the drift that costs nothing. Nothing compared the recorded index dimension against the width of the vectors actually stored; a meta row claiming one dimension over data holding another was invisible. The audit reports that disagreement as its own third state - the record contradicts the data - distinct from drifted and from unrecorded.
o2b search restamprepairs the one drift that needs no provider, a stalevec_versiontoken: dry-run by default,--applywrites one cell, and a real model or dimension disagreement refuses by name because that repair costs money and belongs to the apply-gated backfill.Note updates refuse to destroy what they could not read. An existing-but-unreadable note - permissions, a directory standing where a file should be, a mid-sync fault - parsed as an empty one, so an update destroyed frontmatter and body while reporting
updated: true, and an append destroyed the prior body the same way. The read now raisestarget_unreadable(and a scanner-lossy frontmatter raisestarget_frontmatter_lossy) before anything is projected, and a blank body over a non-empty note refuses unless the caller passes the newallow_emptyflag onbrain_update_noteorbrain_write_batch. Genuinely new empty notes are untouched; the guard's docblock records the three placements it deliberately refused.Every structured Brain record carries a server-derived
origin_channel.mcp-tool,cli, orimport, resolved once at the process entry point and threaded as an option - never a tool or CLI argument, and a census test proves no schema offers one. Log events, signals, continuity records, and caller-named notes are stamped; existing records are not rewritten, because backfilled provenance is the opposite of provenance; and the write-site census asserts the measured remainder that carries no stamp rather than describing it.Brain status derives its maintenance debt instead of counting it.
log_events_since_dreamis computed from the log shards the status reader already opens, and is named for what it measures - caller-named note writes emit no log event, and the docblock says so instead of borrowing a bigger name. An unreadable shard turns the figure into a named lower bound, a dry-run dream moves nothing (pinned by a test), and the always-loadedbrain_contextcarries only a newest-shard overdue flag so the bootstrap path stays cheap.The three note-producing envelope lanes hand the model its wikilink vocabulary. Rollup, diarization, and design-note envelopes now carry a bounded
link_candidatesmanifest - built from the basename walker with no embedding, filtered through the ownership rule so a withheld note is indistinguishable from an absent one, and carrying its own total and truncation flag so a partial list cannot read as complete. The envelope spine is byte-identical; the three lanes that produce no wikilinked note carry nothing new.Multi-artifact envelope writes account for themselves, durably. The one lane that commits several artifacts from one validated payload reports attempted, found, and missing in the shared vocabulary with the first real error - never a partial success - and a mid-batch failure now also leaves a dead letter under the vault's state directory, content-addressed so a retry rewrites one file instead of accumulating stamps. A caller that drops the response is no longer the only record that a partial write happened.
Imports read back what they claim. Both import lanes re-read the vault after committing and report claimed, found, and missing with the keys named - a count alone is the misleading no-op this wave exists to remove. The sessions lane also gains resume on the ingest lane's existing checkpoint substrate - extended, not introduced - so an interrupted transcript import continues at the turn boundary instead of re-reading everything, a resumed run re-collects what its recall analysis needs so resumption never loses data, and a malformed checkpoint discards itself by name instead of silently resetting to zero.
A visibility surface census, and the honesty it enforces.
visibility: privatefrontmatter is a caller-supplied view filter, not a privacy boundary - any caller may lift it, one read lane honors it, and the index stores tagged content regardless. A standing architecture census now enumerates every note-returning surface as covered or excluded-with-reason, ando2b search checkstates the boundary's absence on vaults that use the field, with the surface counts derived from the census itself rather than hand-written. The census corrected the wave's own reconnaissance on two surfaces, once in each direction.
Notes
The wikilink-stub card is closed as already shipped:
scaffold-stublanded earlier end to end - core, tool, verb, repair-lane integration, tests - and the one unbuilt clause, auto-materialize on capture, is a silent write the shipped design declined in writing. The per-page privacy card stays open with its redesign verdict: real enforcement must reach the walker, the search pipeline, and the indexer, and belongs to a wave of its own; the census above is the coverage map that wave starts from.Also refused, each with its reason recorded in the design documents: a stored writes counter (no transaction, no chokepoint, and a counter would be a new state surface where a derivation cannot desync), a caller-supplied channel field (an unverifiable stamp is a lie with a schema), backfilled provenance, a flipped embedding-ABI default (the rebuild-loop argument stands), and any re-embed verification outside the explicit apply gate.
An independent reviewer was given the finished branch with no knowledge of how it was built and returned two blocking findings and eleven non-blocking ones, all applied: a resumed import's recall analysis silently dropped every turn before the checkpoint boundary while reporting success; a frontmatter-only update deleted a line the scanner could not express while reporting success; a malformed checkpoint silently reset to zero; the visibility census could not see a tool registered under a constant name; the origin-channel vocabulary was invisible to the vocabulary census that should have pinned it; dead letters accumulated one file per retry against their own docblock; an unreadable log shard under-counted the maintenance debt with no signal; the dream pass built link-candidate manifests it then discarded; and a bad ingest plan id disabled resume through a bare catch. The other blocking finding was the version bump this entry is part of.
1.51.0 - 2026-08-22
Eight tracker cards picked by priority, and reconnaissance against the real source before any design - a pass that corrected three of the eight rather than following them. The salience gate the first card asked for had nothing to stand in front of: the dream pass owns no per-item model lane, only a count-triggered rollup ladder, so the gate governs what enters that fold instead. The architecture diagram the second card wanted could not draw dependencies, because the scanner's own design doc excludes import analysis and its module facts carry no edges - so the diagram draws what the scan truthfully knows, containment, and says so. And the codegraph enrichment the third card described would have required inventing a partner query surface against a constant whose docstring calls its four tokens the entire surface, absolute - so what ships is the honest half, a verdict region that states what the partner index holds without pretending to read symbols it cannot see.
What the eight had in common only became visible against the source: this wave adds three new lanes where a model writes into the vault, and the project already carried three needs-llm-step envelope shapes sharing five field names and no shared type. So the first unit of the wave is the type those six shapes now share, plus a registry for the payload constraints the deliberately shallow shape language cannot express - a design-note payload must carry exactly one recommended alternative, an extraction batch must respect a cap and a confidence floor, and both rules now live in one fail-closed mechanism instead of two ad-hoc ones.
Added
A deterministic salience gate in front of dream synthesis.
dream.salience_thresholdpartitions the rollup fold set by a score built from three signals the system already computes - decayed evidence mass, the Wilson confidence bound, observed reuse - with no model call, no cache, and no second round trip. Every excluded fact is named in the run summary with its score and all three raw signals; unset threshold reproduces the previous behavior byte-for-byte, proven by running the same vault both ways. Aretriageaction on the staged-bundle verbs reports what a changed threshold would admit or exclude without mutating the bundle.o2b brain extract-signals- model-mined session signals that cannot skip the trial lane. A two-phase batch verb over an already-imported session: phase one emits one envelope carrying the user turns to mine, phase two validates the returned payload against a declared shape plus a per-session cap and per-item confidence floor, then routes survivors through the capture boundary, dedup, the durability denylist, and write-approval staging intoBrain/inbox/as speculative signals under a new closed-vocabularysource_type: auto_extract. Confirmed preferences remain reachable only through the dream pass's own promotion. The live hook path hosts none of this - it runs under a process ceiling that has no room for a model round trip, and the import command's output is now pinned byte-identical to prove it stayed out.--delete-linked- a note delete that can follow provenance, and nothing else. The cascade adopts the source-cleanup blast-radius rule: only a single-purpose derived file whose structured provenance traces solely to the deleted note joins the deletion set; a page that merely mentions the note in prose, or cites any second source, is reported and preserved. One snapshot covers the whole set, the count guard asserts its exact size, and the dry-run lists deletions and reports separately before any confirm.Capture notes carry operator guidance, and the operator finally has a capture verb.
WriteCaptureInputgains a free-formguidancefield - part of the capture identity hash, so two captures differing only in guidance are two captures - ando2b brain capturebecomes the first ingress beside the Telegram bridge: body from argument or stdin, source, sender, timestamp, guidance.Expiration is now settable, and later changeable, from every writing surface.
--expireson the feedback verb and anexpiresparameter on the feedback tool stamp a lifetime at creation; the newo2b brain expire/brain_expiremutate or explicitly clear it afterwards, addressed by artifact id because the note-path surfaces refuse theBrain/root by design. Every path funnels through the one normalizer, clearing is a word rather than an empty string, and an equivalent re-set reportschanged: falseand writes nothing.Agent Skill drafts mined from the pages that earned them. A
mature_pagepattern kind in the skill-proposals pipeline: the page-meta trio gates candidates, observed reuse supplies the evidence floor, installed-skill coverage vetoes duplicates with the veto named per page, and each admitted page yields one envelope whose validated draft stages as a pending proposal inside the vault. Accepting it materializes aSKILL.mdunder the configured skills root through the existing write-ahead journal - the only write that may leave the vault, and only on explicit accept.The architect overview now names its grounding. A
codegraphregion states the partner graph verdict -graph-presentwith node, file, and edge counts and health findings, orgraph-absent, lexical-onlywith the specific state and reason - from the report the partner already publishes, through zero new partner invocations. Amodule-mapregion renders a deterministic Mermaid containment diagram from the scanner's own facts, and a decisions note lists the repository's mined ADR candidates by therepo_keyboth generators already share, with an explicit empty state when there are none.o2b brain design-note- a one-shot grounded decision artifact. Given a topic, the verb assembles tension, decision, and truth records matched through the vault's own similarity kernel, reports an empty grounding by name rather than failing on a young vault, and emits one envelope whose payload must carry named alternatives with exactly one recommended - a cardinality the semantic-check registry enforces, since the shape language deliberately cannot. The validated note lands beside the panel outputs underBrain/decisions/.
Notes
The local transformer embedding provider (the ninth card) is refused rather than shipped. A written invariant stands against bundling a model runtime, the lexical provider already gives a key-less install working recall, and the only runtime-free version - a registered provider name that cannot embed - is a stub wearing a feature's name. The refusal is the explicit choice the card itself asked for; revisit only if the invariant is deliberately lifted.
The salience gate is deterministic on purpose. An LLM triage scorer with a verdict cache was designed and rejected: all three inputs already exist as pure functions, recomputation is free, and a gate the kernel can evaluate without a model keeps the pass's no-model property intact.
An independent reviewer was given the finished branch with no knowledge of how it was built and returned one blocking finding - a caller-supplied-agent census pinned one wave behind the tools that declare it - and eleven non-blocking ones, all applied: two tools reported absolute host paths where their siblings redact, the cascade's inherited rule read prose mentions as provenance, a corrupted retriage manifest produced a spurious drift note instead of a refusal, the human-readable dream output was the one path that never mentioned the gate, an equivalent expiration re-set logged a change, the envelope builder's stamped status could be overridden by a hostile payload, guidance containing its own heading split at the wrong seam, and a mid-batch write failure reported nothing about the items already landed.
1.50.3 - 2026-08-22
Reported as #173: in a Hermes deployment the memory tools were advertised and every call to them was refused, with nothing anywhere naming a cause. The gateway starts its providers with a minimal inherited PATH, and Open Second Brain already knew how to work around that for itself - the fallback scan finds an absolute o2b that PATH cannot see. What it did not do was carry that knowledge into the process it launched, so the wrapper checked its own PATH for the Bun runtime, missed, and exited before writing a byte. The transport reported the consequence. The reason was on the child's standard error, which this bridge read and discarded by design.
Fixed
The MCP child is launched with a
PATHthat contains Bun. Resolving an absoluteo2bwas only half the job: the wrapper is a shell script whose first act iscommand -v bun, run against whatever environment it inherits. WhenPATHcannot see Bun and the fallback scan can, the child now starts with that Bun's directory first on itsPATH. Theo2bwrapper remains the preferred command - it is what sources the macOS sqlite-vec shim - and is skipped only when no Bun exists anywhere for its precheck to find, which is the one case where preferring it buys a command guaranteed to exit 127.A failed handshake carries what the child said. The stderr drain existed to stop a chatty child from blocking on a full pipe, and read every line into the void, so
error: 'bun' is not on PATH.was discarded one buffer away from an operator who saw only "unexpected EOF from MCP server". The last twenty lines are kept, capped at 8 KiB, and appended to the transport error. The drain is still a daemon thread that never blocks and never grows without bound.The Bun precheck looks before it advises.
command -v bunmissing means "not on thisPATH", not "not installed", and the script answered the second question with install instructions for a runtime already present. Both the precheck and theo2b-hookwrapper, which does not source it, now adopt the standard install location before deciding Bun is absent. The version gate still runs against whatever was adopted, and a machine with no Bun anywhere still gets the instructions and exit 127.An empty context pack no longer leaks its own JSON into the prompt. Per-turn recall read the structured item bodies and dropped to the legacy text channel whenever it assembled nothing - but an empty pack is an ordinary answer, and that text channel carries the raw pack JSON, local vault path included, or the transport's preview envelope. A result carrying an
itemskey is now taken at its word; the text fallback is reserved for a result that omits it, which is the only shape that really is an older server.
Notes
Two children launched with different search paths are now distinct entries in the gateway's shared-bridge registry. One bridge per server configuration is still the rule; the environment is part of that configuration.
The end-to-end repair was measured against the reported shape: a minimal
PATH, Bun installed at~/.bun/bin,o2bsymlinked into~/.local/bin. Before, the handshake failed at EOF; after, the bridge starts and lists the full live tool surface.
1.50.2 - 2026-08-20
Hermes asked Open Second Brain for a token-bounded context pack before each substantive turn, then read only the MCP transport text. The transport deliberately replaces a large result with a 2,000-character preview envelope, even though the complete structured context pack is present beside it, so the provider injected transport metadata instead of the recalled memory bodies.
Fixed
Hermes per-turn recall now uses the structured, token-budgeted context pack. The provider joins non-empty
structuredContent.items[*].bodyvalues and bypasses thepreview_truncatedtransport envelope. The existing 1,024-token context-pack ceiling is unchanged, and text-only older servers retain their existing fallback.The transport-boundary regression has a test that fails on the old path. The fixture returns both structured memory bodies and the exact preview-envelope shape; it requires both bodies to reach the prompt and every envelope marker to stay out.
1.50.1 - 2026-08-17
The Hermes dashboard could not save this provider's configuration, and reported the failure in the one way an operator cannot act on: a provider that was fully configured showed as needing setup, and every attempt to configure it from the panel returned HTTP 400. Reported as #170 with the root cause already traced to where the two sides look for the same values. Two unrelated repairs ride along, both surfaced by running the suite on a later day than the one it was written on: a test pinned to a date literal, and the build step that was meant to notice the vendored-schema test skipping.
Fixed
A configured Hermes memory provider is no longer reported as needing setup. This provider keeps its configuration in the canonical Open Second Brain file, outside
$HERMES_HOME, because the bridge spawnso2b mcpand that is whereo2breads it. The host looks for provider state in three locations it owns, finds nothing for this provider, and concludes it is unconfigured - permanently, since a save writes the file the host still cannot read and the save handler then re-derives readiness through the same blind path and refuses its own write. The two readback hooks the host does consult now report the resolved values: the schema field'sdefault, which is the documented way a provider reports a value it holds, andget_status_config, which is whathermes memory statusprints. Measured against a live hermes-agent with nothing written under$HERMES_HOME:configured: false, status: needs_configbefore,configured: true, status: readyafter, with the vault, agent name and timezone rendered in the panel.hermes memory statusprints the configuration that is in force. It was passing the provider its ownmemory.<name>block, which nothing writes for this provider by design, and printing the empty result.A test that could only pass on the day it was written. The session-discovery byte-identity test pinned the signal filename and the two daily log names to a date literal, and all three are stamped from the wall clock, so the suite went red the next morning - on the one test whose job is to catch a redaction difference between the discovered and the explicit import path. The date is derived from the clock now.
A test file could fail inside another file's configuration. The fake HTTP server the embedding tests run against identified itself by its ephemeral port, and the operating system hands that number back as soon as a server closes. The embedding concurrency ceiling is process-scoped and keyed by endpoint URL, and refuses by design when two configurations disagree about the limit for one endpoint - so two unrelated test files met inside one ceiling whenever a port came back around, and the file that configured
embedding_concurrencysecond failed on a limit the first one set. Each fake server now carries a per-instance path segment, which is what it already was: a distinct endpoint.The anti-drift test had still never run in continuous integration. 1.50.0 put the command-line entry point on PATH for it and added a build step to fail if it skipped. It kept skipping, for the other of its two reasons:
o2b mcprefuses to start with no vault configured, no runner has one, so the handshake saw EOF - and the build step matched only the first skip message, so that skip still read as a pass. Three repairs, because the test was hiding behind all three: it gives the server a throwaway vault (tools/listreads nothing and writes nothing), it names the server's own last words when it does skip instead of sending them to/dev/null, and the build step now fails on any skip. On a clean HOME the suite goes fromOK (skipped=1)toOK, which is the first time the vendored schemas have been compared against a live server anywhere but a developer machine that happened to have a vault.
Notes
No copy of the configuration is written anywhere. Mirroring the values into the host's own store is the workaround in the report and it works, but it leaves two stores that can disagree about which vault is in force, and only one of them is the one
o2breads.The value reported is the effective one, not the config file's line, because readiness has to mean what the bridge will actually use: a vault supplied by
VAULT_DIR, a project pointer or an active profile is a configured provider.One thing the fix does not buy: clearing a field from the dashboard. The host coerces an empty submission back to the reported default before the provider sees it, so a cleared field keeps its value.
hermes memory setupand editing the config file still unset one.
1.50.0 - 2026-08-16
Nine tracker items, picked off the board by priority and read against the real source before anything was designed, turned out to be one question seen from nine sides: what does Open Second Brain know about the machine it is installed on, where is that knowledge declared, and what proves it is true. Three of the nine were not missing features. They were contracts already written into the type system with nothing behind them - InstallAdapter.sessionPaths with ten declarations and no caller, ExportFormat imported nowhere while the command inlined its two literals, and four maintenance task names that were not a vocabulary at all but two independent lists in two files, which is why the command line and the tool surface had drifted apart two releases running.
Reconnaissance corrected three task bodies before a line was written. A committed vault-level configuration already existed; what was missing was that install and hook generation never read it, and that no resolver anywhere could say which layer a value came from. The tool-profile mechanism already existed; what was missing was the one adapter whose host publishes a limit selecting nothing. And the tool count on the card was 90 against a measured 110.
Then the finished branch went to six reviewers who did not know how it was built, four by area, one told only to find tests that cannot fail, and one to fix what they found. They returned forty-one confirmed defects with reproductions, and the release is what survived that. Two were secret leaks inside the code that exists to stop secret leaks. Three were data-loss paths in a command whose entire purpose is moving data safely. One reviewer finding was rejected with an argument, and the duplication underneath it fixed instead.
Added
One declaration per runtime. A closed target vocabulary and a fact table stating, per agent runtime, its published tool ceiling, the profile that fits under it, the session adapter it maps to, where it keeps session logs, and how its binary can be asked what it has registered. Session roots are pure functions of an injected home and environment, and a test poisons the real environment to prove nothing leaks in. A census derives the population from the live adapter registry and compares it to the table in both directions, so neither an adapter without a row nor a row without an adapter can ship.
A capped host gets a surface that fits it. Cursor publishes a per-workspace ceiling of about forty tools and was being handed 110. Its generated registration now names the
catalogprofile: seven advertised, every verb still reachable throughtool_hydrate. The count is not asserted from a fixture - the test builds a real server and callstools/list. An unchecked ceiling is a third state, never folded into "no limit": eight runtimes publish nothing anyone has found, they get no profile, and the capability report says so with the reason rather than staying quiet.o2b install --frictioncompares hosts by capability dimension, every cell derived from the fact table and the adapter's own plan rather than a hand-written column. Three further dimensions were dropped rather than faked, each with the reason in the module: they have no structural source, and a column read from one adapter's private constant is a hand-written table in disguise.Hosts that can be asked are asked. Every configuration comparison used to end with the same sentence about no handshake being attempted. A runtime that declares a probe now gets a keyless, free
mcp list-style query - no model turn, a ten-second cap - and prints either the host's own answer or a named skip. The blanket sentence survives only where no probe exists, which is where it is true.A Codex install adapter, so a four-step manual procedure becomes
o2b install --target codex --apply. It reuses the existing TOML editor rather than growing a second one, and every unrelated table in the operator's configuration survives byte for byte. Its probe is deliberately not the JSON form: the shared parser reads the first token of each line, which under--jsonis the same key on every line, so a JSON probe would report every host as empty - a false negative dressed as an answer.o2b state statusreports all forty declared state surfaces with the path each resolved to, whether this run could reach it, and which configuration layer put it there. Reachability is a tri-state: a permission error reports unchecked with its errno rather than being rounded down to absent, because "it is not there" and "I could not look" have different repairs. Rows are tiered by recovery story rather than by location.o2b state migrateando2b state rollbackmove that state and put it back, binding source type, roots, path inventory, byte counts and per-file digests into a manifest. Every check runs before anything is committed, and the whole plan is refused rather than half applied - on a symlink, a special file, an occupied destination, insufficient space, a reserved namespace, an unreadable surface, or a held index writer lock, the one loss a digest cannot undo. Within the apply, every file is copied and re-digested at the destination before a single source byte is removed.Both transports drain. On a termination signal the server stops accepting, reports draining with an in-flight count on the unauthenticated health endpoint, waits a bounded ten seconds, names any still-open request on standard error rather than dropping it, and exits with the signal's code so the hooks that checkpoint the index and release locks still run.
o2b brain import-session --statusand--discoveranswer what session logs this machine holds and which were never imported, keyed by content digest so an unchanged log is reported as imported without being parsed again. Found equals imported plus gap plus unparsable, and a Cursor database counts unparsable rather than gap, because a gap is work an operator can close.o2b brain export --format transcripts-jsonlemits one JSON record per conversation. Not instruction-and-response pairs: that is a lossy projection needing three judgements the runtime never recorded, and while a harness can derive pairs from a conversation, nothing derives the conversation back. Tool names travel; tool inputs never do.A committed
install:block in the vault configuration supplies the generated hook timeout and the tool profile, and outranks the machine-local tier on purpose - two machines cloning one vault must not generate different hooks because one of them has a stale local key.
Changed
Config values can say which layer produced them. A provenance-returning resolver was added and the existing one delegates to it, so none of its roughly fifty-five call sites changed and a test asserts the two agree on every branch. The alternative - widening the shared function - was the largest risk in the design and bought nothing.
The environment tier no longer reaches a written registration. The invariant is not how many tiers there are but which survive to the next invocation: a committed file and a machine-local file do, a shell variable does not, and a verifier that reconstructs from one cannot be fed by the other. The variable still selects the surface of a server you start yourself.
The maintenance lane's task names are a vocabulary, and its values are operation references rather than fresh strings, so a lane task is by construction an operation that has a deadline budget. Both surfaces build their lists from it, and a census derives the command-line flag set and the tool property set from source and requires each member to reach the other or carry a written exemption - now including the bounds, since the two accepted different ranges for the same knob.
brain_dreamwith a single step runs under a deadline. It was the one path that did not, and the pass is synchronous, so an unbounded step held the whole server. The two functions it calls take a guard now. What is not interruptible is stated precisely rather than glossed: the vault listing and the phrase build are single phases, so the guarantee is one page plus whichever of those is running.brain_maintenancereaches the escapes the command line already had. Its streak refusal used to tell every caller to use a flag half of them cannot reach; it names both now. Theforcedescription named two gates while force disabled four.Session parsing streams. All five adapters read whole files and split them, so peak retention was about twice the file before the first turn was yielded. Measured on a 10.79 MB session: 12,058,624 bytes retained before turn one, against a 262,144-byte window now. Byte identity was proved rather than claimed, by diffing the old and new adapters over sixty file pairs including a character straddling a chunk boundary.
Session log roots are declared once. The discipline scanners independently knew where three runtimes keep their logs and swept them for a different question; both consumers read one table now, proved by a test that relocates a root and watches both follow.
The egress site is renamed from a name that said preferences to one that says export, because it now guards whole recorded conversations.
Fixed
A refusal printed the secret it was refusing to write. When the transcript export declined because a session filename was secret-shaped, it prefixed the refusal with that filename - into CI logs and shell scrollback, on the one path whose purpose is not letting it out. The guard's own message says locations rather than values; the command printed the value in front of it.
The redactor could not see a prefixed environment assignment. Its rule opened with a word boundary, and an underscore is a word character, so
API_KEY=xwas caught andANTHROPIC_API_KEY=xwas not. Everything that appeared to work was being caught by the vendor-prefix rule on the value instead. That is the most common shape a key takes in an agent transcript, and this release is what first ships transcripts off the machine.A bare high-entropy identifier left verbatim. A session filename and a turn id are named by a foreign harness, and the reasoning that lets vault-authored identifiers skip the entropy scan does not transfer to them.
Rollback could destroy the only surviving copy. It read the source root from the manifest without checking it still existed, so an operator who migrated and then renamed their vault had the old path recreated, the destination copy removed, the manifest deleted, and a success exit. It refuses by name now, and
--tonames where the vault went.Rollback overwrote a file written between plan and apply, deciding "already in place" once and copying later without re-checking - the one thing the module says it must never do.
The reserved-namespace checks compared resolved strings without following links, so a symlinked destination ancestor moved the whole state tree inside the vault it was migrating.
o2b updatehashed a payload it does not write, so it reported no change whileo2b install --checkreported drift on the same target, and a Cursor install kept its oversized registration. Two commands contradicting each other about one file.The host probe asked the ambient machine rather than the one being verified, so a relocated Codex home could verify against a different home's registrations.
A directory import recorded files that failed to import as imported, making them permanently invisible to the coverage report until their bytes changed - including after a future release ships an adapter that could read them.
A ledger failure after a successful import reported the import as failed and suppressed its report, so the machine-readable consumer got nothing while the signal was on disk.
One empty transcript aborted the whole export. Agent runtimes leave zero-length files routinely, and the offered remedy was to move them out of a directory holding a thousand.
--sincesilently dropped transcripts with no clock, which is precisely the case its own comment forbade: the guarantee held only for a malformed timestamp, which never occurs, while the synthesised epoch sentinel took the forbidden path.An unreadable vault configuration escaped as a stack trace and took every install target with it, because the detection sweep runs all adapters and one of them reads that file.
o2b install --frictioncrashed on the same file, though it is documented as a report that never exits non-zero.A failed removal still dropped the install manifest entry, so the retry demanded a force flag to clean up what it had just forgotten.
A commented-out table counted as a declared server, so an operator who commented both out and removed the binary from the path was told the install was fine.
One tool declared the pairing and the other only enforced it. v1.49.0 made
scoresandmatch_qualityall-or-nothing onbrain_recall_gateandbrain_context_pack, and said in the changelog and in the upgrade notes that the schema states it asdependentRequired. It said it on one of the two. Both refuse an incomplete pair through the same coercion, but only the gate declared the keyword, so for the context pack the rule stayed exactly what that keyword exists to end - discoverable only by making the call the server refuses. The pairing is now built beside the function that enforces it and parameterised on the same argument name, so a tool's property key, itsdependentRequiredentry and its handler read one constant. A new test derives its population from the live tool table rather than a list: every tool acceptingmatch_qualitymust declare the pairing on its own scores key and must refuse each half alone when driven, so a third tool cannot gain the property without the rule. It fails against the shipped code with no mutation.The vendored Hermes schemas had drifted from the live server, and the test that guards them was skipping itself. The drift was structural, not stale prose:
brain_recall_gate's vendored copy was missing thematch_qualityproperty and thedependentRequiredkeyword outright, andbrain_context_pack's was missing the property - so a provider holding those copies could believe a payload was legal that the server rejects with-32602, which the provider's own call wrapper swallows. Latent rather than live, because neither payload is built with scores today. The reason nobody was told is worse than the drift: the anti-drift test needs a live server, skips when the command-line entry point is not on PATH, and nothing put it there in continuous integration - so it had been reportingOK (skipped=1)on every run, and the one guard against a vendored copy diverging fired only on a developer machine that happened to have the symlink. The entry point is on PATH there now, and a following step fails the build if the test skips again, because a skip and a pass read identically in the summary line.The snapshot gate reported a cause it invented and hid the one it had. Reported from outside as a Windows failure, root-caused here as a platform-independent defect and reproduced on Linux. The retry key matched any
EEXISTanywhere in an error's cause chain, without regard for which path collided - and the first thing a snapshot does is create its own archive directory. So a failure to makeBrain/.snapshotsread as "another process took this candidate run id", laddered through all sixty-four candidates, and reported that every one was claimed, while the real error sat in acauseneither command-line wrapper printed. Nothing had claimed anything, and the directory need not have existed at all. The retry now matches only a collision naming this candidate's own archive; an error carrying the code but no path no longer counts, because treating unknown as "yours" is what produced the fiction. The directory is diagnosed before it is written into, naming what was actually found there, and error causes reach the operator through one renderer rather than being dropped at the last line.
Notes
Some of what the reviewers asked for is not here, and each absence is a decision.
Three adapters answer a refuting probe differently and were not collapsed into one verdict, because collapsing them would make one wrong: one leaves no artifact, so the host's registry is the record and its no means the registration is gone; the other two compared an artifact and found it correct, so the host has not reloaded. The defect was that one rule was argued three times in three bodies. It is argued once now.
The streaming bound is asserted on the reader's own accounting rather than on resident memory, and the suite says so and records the adapter it cannot catch - one that streams correctly and also holds the file. Repeated measurement showed resident size swinging between zero and sixty-six megabytes on the same code path, because mid-iteration heap is dominated by parse garbage rather than by the reader's window. A bound that reports thirty megabytes for a reader holding two hundred and fifty-six kilobytes is not a bound.
Two progress emitters cannot produce an advance record in a test - one needs an embedding provider and a network call, the other owns no loop - so they carry declared exceptions checked in both directions, and an exception that outlives its condition fails.
With --recall set, session import still accumulates every kept turn in memory, about 1.4 times the file, because the recall store takes a materialised array. The adapters underneath it no longer do. Bounding that means changing the store's signature, which is its own unit.
A defect that could not be given a failing test is named rather than quietly fixed: the platform refusal in the grok adapter's detection path reads the process platform directly with no seam, so it cannot be driven from this one.
The Windows half of the report that produced the snapshot fix is not fixed, and will not be claimed. Whether this runtime raises EEXIST when asked to create a directory that already exists on that platform is a question no machine here can answer, there is no Windows job in continuous integration, and shipping an unverifiable fix is the thing this release exists to stop doing. What did ship makes the next report falsifiable: the command now names the directory and what is in it instead of a run-id ladder that never ran.
That report also stood inside a boundary this project declares and does not hold. Windows is listed as an unsupported platform, but the refusal sits on the third branch of configuration-path resolution, after two environment variables have been consulted - so setting either one runs the whole product on a platform it says it refuses, and the code already documents that as intentional. It is a sign on an open door. Making it a real boundary would break two documented ways people are using today, and calling it advisory means saying so in the README and stopping treating those reports as in-scope. That is a decision for the operator, and it is recorded rather than taken.
1.49.0 - 2026-08-16
Ten tracker items, picked by priority off the board and read against the real source before anything was designed, turned out to describe one defect seen from ten sides: the product declares a boundary and puts nothing behind it. An owner-isolation filter whose field no writer ever emitted. A gate that enforced whichever scope the caller asked for. A confidence floor compared against a number that could not vary with match quality. An egress registry built to stop an unguarded export, with an unguarded export outside it. A census whose docblock claimed to have closed the mis-parse class that was live inside it.
Reconnaissance corrected most of the ten task bodies before a line was written. Six corrections changed what the unit is rather than how it is worded, two findings turned out to be the opposite of what was filed, and two described a live defect rather than an improvement: a credential leaving verbatim through a shipped export path, and an authorisation check that read back whatever the caller claimed. Each unit then takes one declared boundary and does exactly one of three things, chosen by evidence: ENFORCE it and put a census behind the check, RE-MEASURE it against the signal that actually varies with what the label claims, or RETRACT it and say so.
Then two reviewers were given the finished branch with no knowledge of how it was built, and told that a comment is a claim rather than evidence and that a test which cannot fail proves nothing. They turned the release's own thesis on it and it did not survive contact. The isolation probe this release is proudest of — one two-owner fixture driving every classified tool — was itself a label with nothing behind it: flipping it to run with the gate OPEN and demanding the marker appear gave 15 pass and 81 fail. Eighty-one of the ninety-six entries were driven against arguments naming artifacts the fixture never created, so they could not have failed under any setting. Three more surfaces were leaking behind that green: brain_hygiene mode=apply retired another owner's preference and named both hidden ids, brain_analytics filtered one view of four, and brain_health returned a verdict folded over findings it had just withheld. The MCP resources surface had no ownership filtering at all. The central measurement the four rethreaded thresholds rest on was wrong by 0.05 in seven places. Everything named below is what shipped after that.
Added
Ownership is written, or it is not a boundary. With
integrity.owner_scope_deliveryon, every production preference writer stamps the server-resolved agent identity onto a new record. A rewrite never re-owns, so a page created before the gate stays shared rather than being retro-owned by whoever rewrote it first. With the gate off nothing is stamped and a vault that never opted in is byte-identical, measured with a whole-tree digest. "Every writer" is enforced by a census derived from source rather than a hand-written list: a module that builds a preference path and writes bytes must reach the stamp or carry a declared exemption. The hand-written list is what letimport-claude-memorywrite an unstamped preference through its own renderer.A caller-supplied owner scope is checked. Under the gate, an
agent_scopenaming an owner other than the resolved identity is refused by name, never silently narrowed — a narrowing would leave the caller believing it had read what it asked for. An unconfigured install cannot become an owner either: the placeholder identityagentis refused by the same predicate on the write side that already refused it on the read side, so the two halves cannot disagree.The scope matrix asserts correctness, and asserts that it asserts anything. Ninety-six classified tools carry 220 call recipes, one per mode, view or operation, because one executed view is not an executed classification. Every recipe runs twice against the same two-owner fixture — once with the gate closed, once open — and the classification decides what the open run must show. The 31 recipes claiming to reach owner-taggable content must surface the marker with the gate off, which is what stops the closed run from passing vacuously; the other 189 carry a claim of unreachability, executed directly where nothing is hidden. All three numbers are pinned as equalities.
The MCP resources surface honours the gate.
osb://preference/{id},osb://topic/{slug},osb://backlinks/{id}andosb://log/{date}returned another owner's file verbatim, including itsowner:line, while the delivery path correctly withheld it. A withheld resource now answers with the same not-found message an absent one produces. The topic scope reaches the selection rather than filtering after it, so a shared record is not hidden merely because another owner's sorted first.The delegation boundary the host already delivers.
SubagentStopis registered, and a session turn carries the delegatedagentIdandisSidechain— the only things separating a sub-agent's turns from its parent's, since a sub-agent transcript reuses the parent's session id. Both are emitted only where the transcript records them, so a flat transcript keeps its pre-delegation shape byte for byte. The Grok installer registered six of eight hook events and captured nothing from a delegated sub-agent; a parity test now derives the expectation fromhooks/hooks.jsonitself.An unreachable search origin is reported rather than dropped, with a four-piece vocabulary distinguishing reachable, unreachable and unknown, each carrying its own reason where it has one — "it is not there" and "I could not look" have different repairs, and neither may read as a zero contribution.
The
vault_pathcontract has an enumeration behind it. Forty-four sites emit the field and three honoured the contract stated in prose above them. A census lexes every emission out of the tool surface and requires it to come from the one producer. The producer moved to its own leaf module, because importing it back from the tool aggregator would have closed an import cycle the architecture ratchet gates — and that move is also what let the OpenClaw plugin, a second runtime with three emissions of its own, reach it.
Changed
Four thresholds stop reading a number that cannot vary with match quality. Whenever the keyword lane is non-empty, the top row of any recall scores at or above the configured
search_keyword_weight— shipped at 0.6, identical torecall_adequacy_sufficient— and 0.65 measured end to end on a freshly written keyword-only vault, where the ranker adds the freshness prior on top of the min-max normalised lane. Nothing about how well the row matched can move it below that floor. So every keyword recall gradedsufficient / proceed, a gap task auto-closed on a hit of any quality, and the cross-vault chain stop meant opposite things under the two fusion modes. All four now read IDF-weighted coverage. No threshold value changed; what they measure did.Match quality can say it does not know.
idfWeightedCoverageisnumber | null, andnullexactly when there is no IDF mass to weigh. It used to return 1.0 — maximum confidence — for any query it could not weigh, which included every one- or two-character CJK query, because "significant term" carried a three-character floor that was an English-shaped stopword rule in structural disguise. That floor is gone; a token qualifies if it carries a word character, and nothing branches on script. The same floor sat one layer up in the demand log, where it made a CJK query unable to form a bucket at all, so its recall gaps were invisible to the gap-promotion loop.nullis nullable rather than a flag so the compiler forces every consumer to choose: the targeted retry does not fire, completeness gains anunmeasurableverdict rather than folding intosparse, the chain stop does not stop, the gap loop does not close, and recall-inject abstains with a named reason.What match quality is, stated narrowly enough to be true. It is absolute as against a score — no rank position, no lane magnitude, no fusion mode — so two retrievals for one query are comparable. It is NOT independent of which rows were delivered: it is computed over the returned window, so the same query at
limit: 1andlimit: 20differs, and that is pinned by a test. The first version of this release claimed pool-independence in five docblocks.The
tier:frontmatter knob reaches the ranker. It shipped with a weight table, a breakdown line and no producer, so a page stampedtier: peripheralscored identically to an untagged one. An untagged vault emits no tier map at all and its ranking is unchanged, pinned by an ordered(path, score)window over the committed benchmark corpus.brain_recall_gateandbrain_context_packtakematch_qualitybeside their scores, all-or-nothing: the level is decided by the quality alone, and deriving one from the scores is the substitution this release removes. The tool description advertised the call the handler now refuses; it names the pairing, the schema states it asdependentRequired, and the verdict returnsmatch_qualityso the quantity that decided the level travels with it.A scoped call on an unreadable config is refused rather than delivered. An unreadable config resolves to the strict integrity fallback precisely so it cannot loosen a gate, only close one. That rationale is about READERS, and this release first wired a writer to it — one bad token in the YAML silently began stamping ownership onto every new preference, permanently. The writer now refuses and names the parse failure: it cannot infer an operator's intent to enable ownership from a file it could not read.
One lexer for every census that reads this repository's own source. Four census files had grown their own copy; one read a backtick inside a regex literal as a template opener, blanked 9,706 bytes of a shipped module and hid what its census could not otherwise see. Its own offset invariant was then broken by an astral character — both views were built by code point while the scanner indexed code units — so four instruments were reading one shipped module one character out of step. Six further single-file assertions that stripped comments by naive regex now route through it too.
Census counts become equalities where a floor hid a drifted reality. The direct continuity-store reader floor sat at twenty against a real twenty-one, and the observability doc had inherited the wrong number from it. The
vault_pathand write-site censuses carried the same shape and are pinned too.
Fixed
brain_hygiene mode=applywrote across the owner boundary. The owner view was constructed inside thescanbranch, so a caller whose scan correctly returned nothing could handapplya finding id and retire another owner's preference — and the response named both hidden artifacts. Finding ids are a digest of their targets, derivable rather than secret, so withholding them was never the boundary. The plan is built from the filtered report, which also closes the existence oracle: a hidden id now answers exactly as an id nobody issued.brain_doctor repairhad the same shape and rewrote another owner's frontmatter.Three surfaces disclosed what they had just withheld.
brain_analyticsfilteredtimelineand leftconcept_synthesis,belief_evolutionanddedupopen — the first returned another owner's preference id and a verbatim line of its body.brain_healthreturned a verdict folded over the unfiltered findings while the arrays beside it were empty, telling the caller a hidden artifact had tripped a detector.brain_sourceswas reclassified rather than filtered: it groups signals by author, and signals carry no owner in this product, so the reason was wrong and not the payload. The widened probe found five more —brain_clusters,brain_dream,brain_trigger,brain_doctor repairandbrain_agent_diff— all closed, none moved to the unscoped bucket.An unredacted vault export.
o2b brain explorer --exportwrote vault content to an operator-named file without the redactor every other export path uses. The egress census missed it because its population rule keyed on a hardcoded list of destination flags — the same defect class the census exists to catch, one level up. Its two synthetic positive controls could not have caught the fix either: both appended a fabricated path string to an already-derived population, and blinding the derivation to an empty list left them green. All three now run the real derivation over the intruder's real source.The egress registry could not express the largest egress in the product. A second population rule finds modules POSTing vault-derived bytes to an operator-configured URL. Five such sites exist, not one: two embedding providers, the cross-encoder reranker — which POSTs the query and the candidate documents — the Telegram capture reply, and the research transport. Each is declared with the reason redaction would corrupt rather than protect the result, and an operator-facing table in the CLI reference names what leaves and when. A default install reaches none of them.
An export that could not parse a preference reported success, silently omitting records. It now collects every failure in one run and refuses by name with a non-zero exit, and the message is vault-relative rather than carrying the host path. The bare
catch { return [] }on the directory listing above it, which reported a permission fault as an empty Brain, raises with the reason.o2b brain bank-importno longer aborts on a malformed rule in the destination. The refusal's justification — no caller for whom a shorter list is the right answer — is true of an export and false of a prior-topic map, where a destination-side defect has nothing to do with the bundle being imported. The scan runs on the readable rules and reports each file it could not read, because a collision list over a partial map is a partial answer and not an empty one.The redactor ate signal ids, so an exported preference lost the link it points through. The carve-out keys on the shape rather than the prefix. It also surfaced that the CLI
--jsonredactor has no high-entropy pass at all, so the two surfaces answered differently about the same string — only one of them ever ate it, and the tests now pin both.log.mdwas empty on every default install. The OKF export's date pattern matched only the legacy un-sharded log name while the writer has produced per-device shards since sharding landed. It reads the recogniser in the module that owns the layout, and shards of one day fold into one section.A backlink index reported a confident zero on a legacy-frontmatter preference, swallowed by a
catch { continue }. The failures ride on the returned index, so its call sites are unaffected, and the CLI verb — the surface an operator debugging a legacy vault reaches first — renders them instead of printing the unmeasured zero.The gap loop measured coverage over rows its own gate rejects. The path exclusion that stops a gap-task note closing itself applied to only half the condition, so the note the loop is trying not to count still inflated the number the floor read. The membership rule now travels with the query. The hook that drives it was then passing a retriever of the old shape — which TypeScript accepts, since a function taking fewer parameters is assignable to one taking more — so the hook threw and its outer catch swallowed it, doing nothing silently. It reports a distinct failed action rather than a zeroed healthy one.
schema_inspect view=lintandview=orphansdied on the artifact they exist to report, with an absolute host path in the message. Both report it instead, through one shared rewrite the export refusal now uses too, and the orphan review carries those rows because "nothing uses this token" is a claim over every artifact.An absent argument was answered with a sentence about its shape.
brain_labelsandbrain_tiersanswered a missingpathwith "path must be a vault-relative string", so a caller who sent the wrong argument name read it as a malformed value.The owner-scope view failed open on a reference it could not resolve, against its own stated convention, for two reasons: three directories holding owner-taggable id-addressable pages were missing from its list, and a bracketed wikilink matched neither branch.
Notes
Some of what was asked for is not here, and each absence is a decision with a measurement behind it.
There is no git-visibility ladder: there is no git transport, every git invocation under src/ is read-only, and the egress registry states in prose that there deliberately never will be one. A fail-closed check needs something to guard. There is no staging copy and no history purge: there is no history to purge, and the export bundle already is the staging copy. There is no off identity mode: the bottom of the identity chain is already the literal agent, itself a placeholder. The passthrough / managed mode names are not adopted — passthrough is the shipped, ungated default across every tool that accepts a caller-supplied agent, and the owner-scope refusal is what makes it gateable. There is no per-agent instruction block: neither transport can reach one, since both resolve identity from process environment and config rather than from the request, and the config reader has no block-scalar form. The shared observation-scope member stays retracted: a positive scope no reader requests narrows visibility rather than widening it.
private: true in frontmatter is retracted as a privacy claim rather than enforced. Nothing under portability/ reads the key, the export composers are content composers rather than visibility filters, and the <private> region marker is the only content-derived privacy primitive. A test asserts that such a page exports in full, so the label cannot be re-read as a boundary.
The keyword lane is not de-normalised: every score in the product would move and four thresholds would flip at once.
Three things this release does NOT achieve, stated because the obvious reading of the bullets above is wider than the truth.
On a default keyword-only install, match quality is effectively binary. The keyword lane matches with implicit AND, so every returned row contains every query term and coverage is 1 whenever anything comes back and 0 when nothing does. The measurement is honest — that material really does cover the query — but weak remains unreachable on that configuration, for a different reason than before. Partial values need the semantic lane or the broadened second pass. A consequence worth naming rather than burying: the cross-vault chain stop has inverted. It never fired in linear mode and now fires on the first origin that returns anything. It is opt-in and off by default.
Computing match quality on every search is a new cost, not a recovered saving. An earlier draft of these notes claimed the number was already being computed and thrown away; it was reachable only in evidence-pack mode, which is off by default. Measured on the committed benchmark corpus it adds about 0.15 ms to a 5.9 ms query — roughly 2.5%, inside run-to-run noise at that size — and it scales with the query's term count rather than with the corpus.
warn is inert on the surfaces that now filter by the server-resolved identity. Reporting how many rows fail would withhold is itself the disclosure, so warn's observability there is the owner: stamp appearing on new files, not a count in any response. The docblock that promised otherwise says so now.
1.48.0 - 2026-08-15
Nine units picked by priority off the tracker, and reconnaissance against the real source before any of them was designed. That pass corrected six of the nine task bodies rather than following them, because they describe a different system: one with a resident daemon, a chat-model client of its own, and an ingest that locks too often. This one has none of those. What it does have is long, resource-hungry work that reports nothing while it runs, and surfaces that print claims nobody checked.
The two are the same defect from two sides. o2b brain dream produced its first observable character after it had already finished, so a caller could not tell a slow pass from a hung one. A cancellation contract - signal, SafeguardAbortError, abort checked in priority over the deadline - was fully written and passed by no production call site, which made that error class dead code. A herd of detached reindexes was spawned from startup with stderr: "ignore", so every loser of the writer-lock race threw INDEX_LOCKED into a discarded stream. And the install success path was asked to state that every memory is a file in the operator's own vault - a claim with a known counterexample, a machine-local secret keying the vault identity every tool returns, and a vault-resolution chain that disagreed with the rest of the CLI about which directory it was talking about.
Then five reviewers were given the finished branch with no knowledge of how it was built, told that a comment is a claim rather than evidence and that a test which cannot fail proves nothing. Two of them were asked to break the production code in a scratch copy and report whether the test caught it; one ran thirty-seven such mutations, of which ten went green. They returned forty-four reproduced findings, and the release's own argument caught it six times.
Three of the six are worth naming, because each is the release doing precisely what it accuses the codebase of. Three of the five long operations emitted progress and then simply stopped arriving - which is the shape of a completed run, a crashed run and a hung run at once. docs/mcp.md stated that four long tools ran under a cooperative deadline; four of the six call paths reaching a consolidation pass had none, and a synchronous pass with no deadline holds the MCP event loop for its whole duration. And the census that the previous release's entire argument rests on - declaration plus a census is what makes a fix durable - turned out to read no source at all: it is a hand-maintained import list, and a reviewer added a vocabulary carrying three of the exact defects it audits for and got 63 pass, 0 fail.
Three findings were resolved the opposite way to the obvious one, and in each case a measurement decided it rather than an argument. Ctrl-C cannot stop a synchronous pass: 27.3 million checkpoints ran with the signal pending and the handler never fired. The obvious fix - make the pass async and await at checkpoints - does not work either, and would hide the bug: 4.8 million awaits with a signal pending, still nothing, because microtasks drain without yielding to the event loop. Only a macrotask yield works, at 184 thousand per second against 13.7 million synchronous checkpoints per second, across 137 call sites where any plain await silently recreates the same false claim. So the four synchronous verbs install no signal handler at all, Ctrl-C keeps killing them as it always did, and nothing claims otherwise. The retrying ingest lock was going to have its budget cut, until the wait was measured: the tail is queueing, not hold length, and a shorter budget made this branch's own designed-contention test fail intermittently. And the over-release guard a reviewer proposed for the shared semaphore cannot see the case that matters, because with a waiter queued the pool count is zero and a stray release passes the check and is handed straight to that waiter.
Added
A long pass says what it is doing while it does it. A synchronous progress sink, optional on the same options interface that already carries the operation's deadline, emitted from the checkpoints that already existed.
totalis optional and its absence means something: the index walk consumes a generator, so counting first would cost a second full traversal, while the embedding phase holds an array and knows its denominator exactly. One event shape carries both, and a renderer that sees no total shows a counter rather than inventing a percentage.--progressono2b brain dream,bridges discover,clusters run,maintenance run,architect,search index,search reindexandsearch vector-backfill, as newline-delimited records on stderr - never stdout, which twelve top-level commands own as a payload a caller parses.Progress over MCP, where the transport can carry it. The stdio transport emits
notifications/progressagainst the client's token. The HTTP transport writes one response and closes, so a token supplied there receives a typed refusal naming the transport rather than events that never arrive. A call with no token is byte-identical to before.Ctrl-C reaches the passes that can observe it, and nothing pretends about the rest.
o2b search index,search reindex,search vector-backfillandbrain maintenance runstop at a checkpoint and exit 130. The four fully synchronous passes install no handler, because a synchronous function never yields the event loop and a handler that cannot run is worse than none - it swallows the keystroke that used to work.A fourth gate on the maintenance lane, which can say it cannot see. Host pressure joins the window, busy and lease gates. Where the metric is degenerate - Windows, or a CPU quota in force - the gate does not evaluate and says so on its own journal line, distinct from the line a gate that evaluated and let the work through writes.
A consecutive-failure streak, and a narrow way past it. A task that has failed three times running is refused rather than spending the lane's lease every pass;
--retry <task>attempts that one task with every other gate still in force, where--forcedisables them all.The install says only what it checked. The ownership statement measures the vault, what filesystem backs it, whether the search index is inside it, and each of four outbound integrations. What it cannot measure - ten kinds of state the tool can leave outside the vault - is worded as a catalogue rather than a scan, and every row names the verb that created it and the verb that removes it.
A doctor check for a model with an announced decommission date, and one for the age of the newest recovery point. The sunset survey ships sixteen sourced entries from one published deprecation page and an operator can declare a date the shipped table cannot know; a model outside the survey is reported as unsurveyed, which is a different statement from no shutdown announced.
A switch for the codegraph partner check (
partner_codegraph_disabled), whichdoctor()accepted and nothing could set.
Changed
embedding_concurrencybounds requests in flight process-wide, per embedding identity against one endpoint, rather than perembed()call. Two models on one host are two budgets, so that host sees up toembedding_concurrencytimes the number of identities at once.The project scanner walks once, and stops walking what the repository ignores. Measured on this repository: 24148 files visited became 2729, and 530.6 ms became 57.5 ms. The
.gitignorequestion was settled by measurement - a dot-directory rule removes 88% of the files, and full.gitignoresupport adds 207 more, every one of them already inside a dot directory.The ingest path takes the lock it never took. Three shared read-modify-writes were unlocked; a four-process race behind a wall-clock barrier loses 74 of 100 entries without them.
A language tie-break in the scanner no longer depends on the host locale, in a module whose whole contract is deterministic regeneration.
The prose recognizer on the write path carries no word list, which its sibling module's rule had forbidden all along.
Breaking
o2b installresolves the vault through the same chain as every other verb. It previously used--vault, then the configvaultkey, thenVAULT_DIR. It now uses--vault, thenVAULT_DIR, then a project pointer, then the active profile, then the config key, and it expands a leading tilde where it previously passed one through literally. Where the answer changes, the new one is whato2b statusando2b brain doctorwere already using - the old behaviour let the installer write one vault into an agent's config while the rest of the CLI read another, with nothing reporting the split.o2b doctor --readinessexits 6 for a probe that did not complete, where it exited 1. A timed-out probe was reported as a failure, so on a loaded machine the doctor declared a healthy system broken. Code 6 is the numbero2b search checkalready spends on the same condition, and a test asserts the two cannot drift.o2b brain maintenance runexits 7 when a task is refused for a failure streak. Not 1, which would report a failure this run did not have; not 0, which would tell a nightly cron that a lane that has stopped doing heavy work is healthy.Two new
_brain.yamlblocks,maintenance:andembeddings:, both absent by default and templated commented. A vault that declares only one ofembeddings.sunset_model/sunset_atfails to load rather than ignoring the half it got.Two new doctor warnings and three new uncertain codes. Under
--strict, a vault that previously exited 0 can now exit 2.The hook self-ceiling default falls from 55 s to 8 s, derived from the 10-second host timeout every hook entry declares. The old default could never fire, because the host killed the process first.
brain_token_impactno longer reportsmethod: "exact". It counts nothing - it subtracts integers a caller supplied - so the vocabulary now names provenance (tokenizer/heuristic) rather than an accuracy nobody verified.The bench report is schema v2:
context_cost.est_tokenschanged meaning, not only name.o2b brain doctorreports the codegraph check as deliberately not consulted when it is switched off, where it previously omitted the row. Omission made a setting that had just taken effect indistinguishable from one that had done nothing.listSnapshotsreturns a listing rather than an array. A per-entry read failure was dropped with a barecontinue, so a recovery-point history whose archives could not be stat'ed read as a history that never existed. The skipped entries are now returned beside the snapshots,o2b brain snapshot log --jsongainsskipped, andpruneSnapshots'sretainedno longer claims "still on disk" about entries it could not see.o2b search vector-backfillnow runs under a deadline. It declaredsafeguard,signalandonProgressand had a producer for none of them; wiring two of the three would have left the third declared and unreachable. The budget is thereindexkey off the same laddero2b search indexreads, because this pass is that run's embedding phase alone.EMBEDDING_SUNSET_UNDETERMINED_REASONlosesdeclaration_malformed. Nothing could produce it: the config parser validates the declared instant with the same predicate and refuses the vault before any check runs, so the member described a state that could not exist.
Fixed
Three long operations emitted progress and never terminated the stream. The rule is written once now, because five copies of one rule are five chances to forget it - which is what happened.
The first Ctrl-C was swallowed and the verb exited 0. Registering a handler suppresses default termination, so a signal landing outside a checkpointed region was absorbed and the command reported success for a pass the operator stopped.
o2b search vector-backfillgrew the entire progress spine and no way to reach it. An options field, a counter and a terminator in core, with no--progressflag, no caller passing a sink, no signal and no deadline - every line of it unreachable from any surface an operator has, which is the exact defect this release is about. Found by the new execution census, which drives each entry point for real; the source census could not see it, because a declared sink is not a passed one.A shared semaphore could be released by code holding nothing, admitting four concurrent holders against a ceiling of two while its own high-water counter still reported two.
release()is gone;acquire()returns a one-shot permit, and a second call on it throws.A retrying lock spun forever on a
NaNbudget and froze the event loop for the whole of a valid one.The doctor could not see two of the three ingest locks, because the stale-lock scan walked only one of the two roots locks live under.
A recovery-point check reported a vault whose archives could not be read as a vault that never had any.
The host-pressure probe read the root cgroup rather than the one governing the process, so a CPU quota in force yielded a confident percentage computed from the host's run queue.
brain_dream, its staged actions,brain_brief view=operatorandbrain_review_candidatesran without a deadline while the documentation said otherwise.The self-heal rows paired on a process id in a file that syncs between devices and whose own docblock forbids machine-local values in a row.
Bun.whichandBun.spawnSyncresolved against the PATH the process started with, so a long-lived server would answer with a stale binary's result.The test suite's verdict was a function of the machine it ran on, for the third release running.
doctor()consults the codegraph partner whenever that binary is on PATH, and the partner's cache lives under HOME - which this suite pins to a clean directory.tests/cli/cli.test.tswent from 96.03 s to 1.09 s with the partner switched off, same tests green. CI never saw it, because the binary is absent there.
Notes
Two censuses were rewritten because reviewers evaded them. The progress census went from five regular expressions to a lexer and three parsers: fourteen evasions now fail it, each with a permanent positive control, including one where a brace inside a generic constraint did not merely miss an interface but silently parsed the wrong one. Four limits it still has are written into its docblock rather than left implied. The verdict-vocabulary census now reads the tree instead of a hand-kept import list. And a new execution census runs each entry point for real and asserts records arrive, which no syntactic bypass can defeat.
README.md recorded, one release ago, a decision this release reverses: there was to be no registry of embedding-provider shutdown dates, because a hand-maintained table over an open set of endpoints would rot in place and be believed. The objection was right, and the design that ships answers both halves of it. A surveyed negative expires and degrades to undetermined; a positive carries the date it was reviewed; a model the survey does not know is reported as unsurveyed rather than as having no announced shutdown; and an operator who has read a vendor's announcement can declare it without waiting for a release. The reversal is stated rather than quietly performed.
Some of what was asked for is not here. There is no daemon, no scheduler and no supervisor, so the startup grace period and crash-loop recovery two task bodies asked for have no subject; the portable analogue is a gate evaluated at the top of a one-shot command with its state on disk. There is no cap on model inference, because this process makes no chat-model call - it hands model work back to the caller as typed envelopes - and naming a ceiling "inference" would be a hardcoded falsehood about what the code does. There is no concurrency in the project scanner, because rendering and writing are 7.9 ms of a 396 ms run. There is no whole-ingest writer lease, because the batch-planning surface exists so a caller can dispatch each batch as a parallel subagent and the sync lock has no retry by design.
Three things are stated rather than fixed. The confidence floor the new bench leans on cannot fire through the shipped retrieval path: min-max normalisation pins the first result at the keyword weight regardless of match quality, so the abstain that actually fires is "no matches". The reindex herd's thinning is exactly zero in the scenario it was built for - four parents probing inside the 0.71-0.75 s window all spawn - and helps only a caller arriving later. And brain_dream step still runs without a deadline, because bounding it means threading a guard into two functions that do not accept one; the documentation now says so on its own row instead of implying otherwise.
1.47.0 - 2026-08-15
Nine units picked by priority off the tracker, and reconnaissance against the real source before any of them was designed. That pass found they were not nine problems. In every one the capability already existed in this codebase - exported, tested, used elsewhere - and the defect was that the site needing it did not call it.
The clearest instance is the destructive-operation gate. snapshot-gate.ts opens by stating its guarantee: no destructive brain mutation runs without a recovery point on disk first. It had two call sites against roughly twenty-five destructive operations, and the consolidation pass took its own snapshot inline, which that same header names as the anti-pattern it exists to prevent. The second clearest is the ranker, which read a document's authoring instant nine lines after it had finished ranking on the file's modification time. Search classified sources through two functions the third write path never called. Five of six export paths never called a redactor with eighteen importers. A live embedding probe ran on every check and its answer was downgraded to a warning the exit code ignored. The consolidation pass compared raw strings where the read path canonicalised. A bundle carried preferences and the import counted them.
So the argument is not a subsystem. It is that a mechanism which must be called by hand is a mechanism that will be missed - and the answer is declaration plus a census test, an idiom already running here for raw fs write sites, for vault-identity assertions, and for doctor exit codes. Two registries were added on that pattern rather than routing twenty-five operations through new shared code, which is the change least compatible with the byte-identity this project measures rather than asserts.
Two of the nine task bodies were corrected by the reconnaissance rather than followed. A credential scanner was filed against the git commit path; the vault has no git transport and deliberately never will, a constraint written verbatim in seven design documents, so the intent landed on the boundary that exists - the export surface. An alias task was filed against the sub-recalls the consolidation pass issues; that pass is synchronous and issues none, and the real asymmetry was its clustering key.
Then five reviewers were given the finished branch with no knowledge of how it was built, and told that a comment is a claim rather than evidence. They returned thirty-two findings, twenty-seven reproduced against real vaults, and the release's own argument caught it out three times. The gate's docblock claimed its retention pass was safe "by arithmetic rather than by luck" - the archive listing sorts by modification time, so a pre-existing archive with a clock-skewed timestamp survived and the recovery point just written was pruned, after which the live tree was destroyed and the verdict read covered. The destructive-site census, the thing that was supposed to make the fix last, had six syntactic bypasses and a gate flag that was per file rather than per site, so one commented-out mention exempted every removal in a module. And a tool description promised its pages were excluded from ordinary reads while the retrieval trust gate defaults off. A release arguing that uncalled mechanisms get missed had shipped three of them.
Added
Note files have a lifecycle. Rename, move, archive and delete over MCP and the CLI, wikilink-aware. Both the source and the destination go through the same nine-step path envelope every other note write obeys, so a
..segment, an absolute path, a POSIX backslash, a symlink leaving the vault and a destination insideBrain/are refused identically on each. The relocation relinks rather than re-reading:linkSyncattaches the source inode to the destination name, so byte preservation is a property of the operation rather than of a guessed encoding, and a note that is not valid UTF-8 survives intact. A case-only rename is decided by inode identity rather than by platform, and stages a third name so the bytes are reachable under some name at every instant. Delete sits at the top of the confirmation ladder - dry run by default, an explicit confirm, and a blast-radius count guard.A rename says how fresh the evidence it acted on was. It cannot honestly claim to have fixed every inbound reference: no write path updates the link table, so an index that exists is stale with respect to a relocation that just happened. The result names the files it rewrote, the ones it could not, the bare-basename spellings it withheld because they were ambiguous, and the state of the index it consulted. A withheld rewrite is visible rather than absent.
A dangling wikilink can be materialised. The gap was reported in five places and closed programmatically in none: the repair lane decided
skip-missing-target, the synthesis report emitted the advice "write the missing note", the doctor emitted structured targets and sources, and a stub renderer already existed. Scaffolding is opt-in, never a side effect of a repair pass, and it refuses when the index is only partially resolved rather than reporting no dangling links from an index that has not finished resolving.A typed recoverability verdict, and a census over every destructive site. The gate expressed "could not prove recoverability" as an untyped throw, so an operation deleting files no archive covers still reported a snapshot path. The verdict is a frozen object with sorted tokens, in the family the trust gates already use, and it distinguishes full coverage from partial, from unproven, from nothing at risk. The census enumerates removal sites syntactically - the write-site census's rule with a different call set - and each is routed through the gate or carries a written declaration of the coverage it actually has.
The export boundary redacts, and refuses twice. A payload the redactor could only partially scan refuses rather than writing a file that cannot claim to be clean. A secret-shaped identifier - a filename, an id, a mapping key - refuses rather than being redacted, because redacting an identifier does not hide it, it renames it, and two names that redact to the same placeholder collapse onto one path.
o2b brain doctorsurfaces preference topics that collide once folded, so the consolidation pass's contention warning has a tool behind it rather than an instruction to go looking.
Changed
The freshness prior reads the content's authoring instant, falling back to storage modification time. A batch of historical conversations imported today no longer all receive the maximum freshness boost as though captured this morning. A declared instant later than the reading clock is refused at both index time and rank time, with different dispositions: the index drops the value so a poisoned row cannot outlive many index runs behind the content-hash fastpath, and the ranker falls back without touching what is stored. Falling back rather than clamping is deliberate - a clamp leaves the note at the top of the decay curve forever, which is the defect. On a corpus declaring no authoring instants, ranking is byte-identical to the previous release, measured rather than asserted.
The consolidation pass folds the topic key it clusters on, with the same normaliser the read path already used - Unicode normal form, case, whitespace, quote variants, no language-specific rule anywhere. Two distinct preferences landing on one folded key plan nothing at all rather than letting filesystem enumeration order decide which rule gets retired, and the contention is named in both summary shapes. Exactly five code points of 1,112,064 fold differently from before.
Preferences restore from a bank bundle through the audited transaction that owns their lifecycle rather than being counted and discarded, so the confidence band, trial window, revision counter and audit trail are maintained. A row whose trial window is inert restores on a window derived closed from its own timestamps and is listed as derived, never folded into the restored count as though the backup had carried it. A row whose deadline is live is still refused, because no field it carries determines one and a fabricated deadline silently re-dates a promotion.
The distillation write path classifies its source. It wrote claims under the top authority tier from a caller-supplied path with no trust classification at all, while the two paths hardened in the previous release both classified theirs. It now routes through the same two functions, records absence as a verdict rather than as the literal string
missing, and bounds the source read to the vault - it was resolving../../etc/passwd, hashing it into the page, and leaking a raw errno through the boundary.
Breaking
o2b search checkexits 5 for an embedding provider that is configured and was proved unreachable, and 6 when the probe did not complete. It exited 0 over a provider it had already proved dead, so a script gating on the exit code read it as healthy. Code 5 deliberately matcheso2b install --checkfor the same condition, and a test asserts the two cannot drift. A provider that is not configured still exits 0 - absent is not broken. The--jsonkeyprovider_reachableis replaced byprovider_probe; a boolean had to answer four questions with two values, so a provider that refused and one that never answered were the samefalse.o2b brain bank-importexits 1 when a rule was genuinely left on the floor, where it previously always exited 0, and its result gainsderived,failedandtopicKeyCollisions.A second top-level directory,
Archive/, receives archived notes.Brain/is refused for every note path, and an operator whose backup rule covers onlyBrain/should extend it.mergePagefails closed on a malformedBrain/_brain.yaml, because the link-rewrite walk now resolves vault scope like every other walker.The config-surface redaction placeholder is now
***REDACTED***, matching every other redaction, where it was[REDACTED].
Fixed
The gate destroyed the recovery point it had just written, and reported success. The retention pass sorts archives by modification time, so one pre-existing archive stamped in the future - the state
rsync -t,cp -p, a clock-skewed server or a stepped system clock leaves behind - survived and the new point was pruned. Reproduced end to end: the live tree gone, the verdictcovered, the named archive absent. Every gate user was exposed, not only rollback. Protection is now by run id rather than by sort position, so equal timestamps cannot decide it either, and the archive is probed on disk before the operation runs.A rollback pruned the archive it was still restoring from. Only the tar is extracted before the discard; the manifest sidecar and the derived-store archive are read afterwards. At the default retention this was reachable whenever the target was the tenth-oldest or older, and with derived-store coverage on it silently returned "coverage unknown" over a store that was never rolled back.
The destructive-site census could not fail. Default imports, a
promisesbinding, a barefsspecifier, dynamic imports, computed member access and.mtsfiles all passed it, and its gate flag was per file, so a mention inside a comment exempted every removal in the module. All fifteen shapes are now fixtures.A rename could write outside the vault through a symlinked directory - a common Obsidian layout - and could rewrite paths the operator had excluded from indexing, returning their filenames across the MCP boundary. It also rewrote
Brain/log/, which is an append-only record of what was said at a time.A partial scan could be defeated by content quoting the marker, so a 1.4 MiB payload exported with roughly 700 KB silently dropped and a success exit. Truncation is now a fact the scan returns rather than a substring search on its own output.
okf-exportmerged pages whose filenames were secret-shaped into one bundle path, destroying two page bodies and exiting 0 with the pre-merge count.A redacted markdown page's frontmatter was not valid YAML - an unquoted placeholder opens an alias node - and the same expression consumed the line following an empty secret key, deleting a key that was never a secret. Caught only by asserting with a strict parser; this repository's own parser is lenient enough to accept it.
o2b export-configmangled the operator's own paths. Folding the key-name-only copy into the shared redactor brought value scanning to a surface whose entire purpose is handing a support person the resolved paths, so a container id or a hashed cache directory was redacted out of the answer.continuity exportleaked exactly the tokens the guard exists to catch, because it called the redactor with no options, and the census actively failed if the call was corrected. Its scan also ran at write time only, so a record already on disk was never examined.An import restored a record whose identity had been redacted away. A bundle carrying
***REDACTED***where a preference id, topic or alias belonged was written into the vault under that name, and two such records collided on it. Identity-bearing fields now refuse per record, on both the preference and the page-graph halves, while a placeholder in a principle or a body still restores - that is an honest redaction, and the difference is the same line the export side draws.A prune that could not remove an archive removed its manifest and derived-store companions anyway, orphaning the survivor.
Both new CLI verbs were missing from the help surface, and
--helpon them was a usage error.The stub tool leaked the absolute host vault path into model context, and accepted arguments belonging to the other action without reading them.
An AWS secret access key passed the export boundary verbatim, because the entropy detector's character class excludes the two characters base64 needs.
Two readiness tests that spawn the CLI twice were budgeted; they fitted the default only on an idle machine, so the suite's verdict depended on the load of the box it ran on.
Notes
Some of what was asked for is not here. There is no repo-visibility verification, because there is no push path whose destination could be verified. There is no cascade delete of linked memories: a cascade is strictly more dangerous than the single-note delete this release introduces, and building it on a recoverability gate that has not shipped once inverts the order of risk. The trust-classification registry the consultant proposed was not built, because there are three such write paths and a census over three items costs more than it protects. The distillation quarantine admits its limit rather than overstating it: the marker is written and read correctly, but the retrieval trust gate that acts on it defaults off, and turning it on changes three signals, the result shape and a cache key at once - a release-wide decision that does not belong to a tool description. The description names the setting, and a test fails if the default changes and the wording does not. What the destructive-site census does not hold is stated in its header rather than implied: the atomic-overwrite class is excluded, one member of it overwrites user-authored bytes that no producer can regenerate, and a test measures that the gap is real rather than leaving the claim to rot.
1.46.0 - 2026-08-15
Nine units, and the same argument nine times: a claim the code could not back up. Four came from the public tracker and five from an upstream mining pass, and reconnaissance against the real source ran before any of them was designed. That pass changed the shape of three fixes and killed a fourth. The one the issue asked for - let the MCP host supply the trust verdict instead of the caller - turned out to be unbuildable, because there is no unforgeable caller identity anywhere in this surface, a conclusion this project had already reached and written down in src/core/write-binding/index.ts and then not applied here.
What the nine have in common is not a subsystem. It is that the information needed to tell two cases apart either existed already and was discarded, or cost one filesystem call nobody made. A closed union classifying a store fault was computed and then interpolated into a sentence. An embedding error carried a category that was stringified and dropped. A rank cap and a pre-visibility count were computed and thrown away. Trust was decided from the shape of a string with no file behind it. Readiness was asserted from the presence of files rather than read off disk. Freshness compared two modification times and had no clock at all.
Extraction happened only where duplication was measured rather than merely suspected. One segment-wise path predicate replaced six copies. One wall-clock age helper replaced five, which spelled a day three different ways and disagreed about whether a failure to read meant fresh or stale. One write-result envelope replaced four hand-built shapes across the note-write tools. Nothing was extracted because two things looked alike.
Some of what was asked for is not here. There is no embedding-provider sunset registry, because the only implementation is a hand-maintained table of shutdown dates over an open set of endpoints and no provider exposes the metadata, so it would rot in place and be believed. There is no per-caller filtering of the advertised tool catalog, because it depends on per-client surface state that has not shipped. The schema-completeness guard covers input schemas only: the output vocabulary declares union-typed fields with no type on purpose, so the rule would demand a lie there, and 202 of 202 output property nodes carry no description, which is a separate decision rather than a side effect of this one. The new argument gate enforces the declared closure at the top level only, and the fifteen nested nodes it does not reach are named in its module header rather than left as a second unenforced promise. And distill-source.ts has the same defect as the intake trust fix, with no trust classification at all; it has no reporter, it is not in this scope, and it is filed rather than quietly included.
Four reviewers were then given the finished branch with no knowledge of how it was built, and told that comments in the diff are claims rather than evidence. They found twelve defects, most of them reproduced against real vaults, and two were regressions this branch had introduced: a snapshot gate whose lost-race retry was dead against the real creator, recovered by the previous release and not by this one, and a rank-cap degradation that fired whenever a corpus ended exactly on the cap, which broke the byte-identity this release claims for a healthy search and filed an invented recall gap into telemetry. The branch's own tests could not see either: the first injected a synthetic error instead of calling the real creator, the second used a two-document vault against a cap of thirty. Every one is fixed below.
Added
A positive
vault.include_pathsallowlist. Vault scope was exclude-only, so indexing nothing butBrain/meant naming every other top-level directory and every stray root note, and anything created later was indexed silently until someone noticed. Absent, the key changes nothing, and that is measured rather than asserted: a recorder ran against the previous release over a fixture vault and pinned what each of the three walkers returned, and those literals are the test. An empty list is refused, because a list admitting no path is an off switch rather than a boundary; a..segment is refused, because it matches nothing the walkers produce and would defeat the check meant to catch it by resolving outside the vault. Requested by @lymeswold in #155.A retrieval trail on every search answer. Fifteen degradation signals existed and none reached a caller as anything a machine could read. Each code names a site that exists, the detail carries identifiers and integers only, and both the CLI and the MCP envelope get it through one function with the codes declared in the output schema, so an undeclared one fails the contract rather than travelling. An empty result now names its cause: from the trail when a degradation attributes it, and otherwise from the corpus classifier that already existed and was reachable from exactly one surface.
Write operations return the lint findings for the pages they just wrote. The vault lint was a separate pass measured at 110 ms over 2200 pages, so an agent that wrote a broken wikilink learned about it from a sweep that might never run. The per-page pass reuses the three detectors that already existed, costs 6.5 ms for one page and 6.9 ms for five, and never walks vault content. It runs after the commit and never gates the write. Update, append and batch previously ran no document validation at all, so callers will now see findings on pages that were accepted silently before.
A channel dimension on recall-delivery telemetry, and a doctor check that reads it. A "recall never fires" report was undiagnosable, because a hook that was never installed and a hook that runs and decides not to inject produce identical evidence, which is none. The channel is required rather than optional, so every emit site had to name itself to compile, and the injecting hook now emits for all three of its decisions, since a hook that ran and abstained is exactly the signal that separates the two silences.
Readiness reads installed state off disk, through each adapter's own verify, and gained an
unknownstatus, because a manifest can exist and refuse to parse and among pass, fail and skipped there was no answer for that which was not a lie in one direction.A wall-clock ceiling on materialisation freshness,
health.materialize_max_age_days, and a three-state verdict with named reasons in place of a boolean.Every advertised tool parameter is documented, and CI says so. Fifty-nine parameters across thirteen tools carried no description and nothing guarded it. The other five completeness dimensions were already clean at 108 of 108, measured rather than assumed, and are now ratchets. A composition keyword the walker cannot follow is a violation rather than a silent skip, which matters precisely because there are none today.
An unknown tool argument is named back to the caller with a suggestion from pure edit distance over the schema's own property names, instead of being silently ignored. All 108 tools declared a closed argument set that the server enforced on none, so a caller who sent
quierygot a success envelope computed from defaults.
Fixed
A concurrent write dropped its record. Two writers raced the same free-name probe, both picked the same candidate, one won the exclusive create and the other exited with "already exists". Replaying the pre-fix sequence across eight subprocesses lost twelve events out of twenty-four in three rounds. The defect was never signal-local: dead ends and capture notes had it too, and the signal writer alone has eight callers, three of which write many records per run on the same date. Reported by @daltondellinger1-coder in #161.
Intake trust was decided from a claim nobody checked.
brain_intake_entitiesclassified a source by the shape of a caller-supplied string and never asked whether the file existed, and the caller is the same agent that extracted the entities from the content being classified. What the fix does not buy is stated in the code, because it matters: an attacker forced to name a real file namesREADME.md. The free bypass is gone and the claim is auditable through a recorded content hash; the lie is not. Reported by @daltondellinger1-coder in #160.The Hermes plugin and the core resolved different vaults. The Python resolver's docstring claimed to mirror the TypeScript one exactly and mirrored two of its four steps: no project-pointer walk-up, no active-profile lookup, no tilde expansion, and the first duplicate key won where the core takes the last. A parity table driven through both implementations found eighteen disagreeing rows, two of which nobody had named: a value with backslashes came back JSON-encoded and therefore resolved to a different path, and
agentNamebeatagent_nameby file position rather than key priority. An unreadable config no longer reads as unconfigured. Reported by @kaykayyali in #130.A snapshot collision was unrecognisable to the gate written to retry it, so a destructive operation aborted where the previous release recovered.
A malformed
merged_intovalue produced neither a fix nor a finding, printing a clean vault over a link pointing at a page that had been merged away.The Hermes resolver parity check applied to every user of the command line, because
plugins/ships in the published package, so a machine without Python had a permanently failingo2b doctortelling the operator to install Python for a plugin they do not use.A dead-include-root lint fired at error severity on a legal bare-name root, asking
existsSyncat the vault root about a rule whose grammar matches at any depth.clusters run --if-staleswallowed a configuration it could not parse and answeredfresh, exiting zero over a vault every other verb refuses loudly.The new unknown-argument suggestion ran an unbounded edit distance over a caller-supplied name: one call carrying a one-megabyte argument key blocked the event loop for fourteen seconds, inside the transport's own body cap. The bound is exact rather than approximate, so it changes no answer.
A raw retriever error message reached a synced, model-readable telemetry payload, and raw operator paths reached the four write tools' responses and an intake error, which also made that tool an existence-and-permission oracle over an arbitrary path.
An unreadable continuity store silently erased the entire recall-channel check, producing exactly the undiagnosable silence that check exists to remove.
Five copies of wall-clock age from a modification time disagreed about the failure: one treated an unreadable file as stale, one returned age zero so an unreadable note read as brand new, one let the raw errno abort a whole scan.
Two operator surfaces over-reported index coverage: the scope walker and the path inspector never applied the admission filter the indexer applies.
The readiness module promised a per-check timeout it cannot honour for a probe that blocks the event loop, and now says so instead.
1.45.1 - 2026-08-15
Fixed
Every Hermes pre-compaction flush has been discarded at the boundary since 0.32.0.
brain_pre_compact_extractdeclaresturn_startandturn_endas turn ids - strings - and the provider sent the buffer's integer offsets. The tool reads them with the argument reader that does not coerce, so the call was rejected with-32602before extraction ran; the lifecycle hook swallows bridge errors by design, so neither side said anything and the buffered turns were simply gone. The provider sends strings, which is what the sibling Aider bracket has always sent. Reported and fixed by @GMDD59 in #158.The vendored schema copy carried a comment instructing the next reader to leave that mismatch alone, on the theory that the server coerced it. It does not, and the comment now says so.
A payload conformance suite drives the provider's lifecycle hooks and validates what actually crossed the bridge against the vendored schemas - required keys, declared types, enums, string bounds, and the closed property set - and fails when a hook stops calling out at all, because a flush that never happens is the failure mode being guarded. Nothing checked the shape of a provider-built payload before; the Hermes test double accepts anything, which is why a payload the real server always refused looked healthy in every run.
The anti-drift gate on the vendored schemas has been failing since 1.45.0, which reworded the
brain_recall_gatescoresdescription without re-vendoring the copy Hermes advertises before its bridge is up. Re-vendored.A starter vault went stale ninety days after the bundle was authored. The dropped bundle carries a fictional five-day history with fixed May 2026 dates, one of them the evidence behind a
pinnedpreference, so from 13 August 2026 on every vault initialised with--starterreportedpinned-without-recent-evidenceon its firsto2b brain doctorrun. The bundle is now aged to the day it is dropped: every date moves by the same whole number of days, so the trial window still open, the evidence two days back and the retired entries months behind all survive the move, and the starter is clean whenever it is installed rather than for one quarter after it was written.bootstrapBrain'snowseam, reserved for exactly this and until now unused, is what the tests pin it with.A test that deletes
OPEN_SECOND_BRAIN_CONFIGfrom the real process environment never put it back, and every test file the runner ordered after it lost the hermetic default the suite's preload installs. On a developer machine the fallback to a real~/.config/open-second-brain/config.yamlhid it completely; on a bare runner it was forty-two failures or four, depending on where the ordering landed, and it turned the suite's verdict into a function of the machine it ran on.
1.45.0 - 2026-08-10
Eight units on one seam: what this system does when it cannot answer. A check that could not run reported the same thing as a check that passed. An archive that omitted a file reported the same thing as one that contained it. A search over a stale index returned the same nothing as a search over a complete one. A rule dropped under budget pressure was reported as dropped without saying which rule. A finding an operator had judged benign had no state meaning benign, only a seven-day timer. A retired preference left every decision built on it looking current.
In six of the eight, the information needed to tell the two cases apart was already computed and then discarded. The section budgeter returned the list of keys it dropped and its caller kept only the text. The manifest walker hashed a tree whose sibling store it did not mention. The retirement path wrote the alias that makes the reverse lookup possible and nothing performed the lookup. The trigger store computed a recurrence and held it for the duration of one scan. A grounding pass over the code preceded the design and falsified a load-bearing claim in six of the eight task descriptions, so what shipped has a shape none of them predicted.
Two independent reviewers then read the finished branch against the base, with no knowledge of how the work was done, one told to treat every claim as unproven. They found this release's own theme broken by its own code in five places, and each is fixed below: a cron health gate that passed on input its parser could not read, a test-runner guard that reported its own setup failures as concurrency, a staleness audit that discarded a computable finding and then said nothing was knowable, an unreadable optional manifest field that voided the record drift detection runs on, and a per-record refusal with no per-record boundary that took a whole queue down with one corrupt file.
Some of what was asked for is not here. There is no maintenance-lane task that runs the external code indexer: it would be the first lane task to spawn a foreign binary and the first to touch state outside the vault, and the cron recipe delivers the capability without either. There are no snapshots at session, plan or decision boundaries; the history is typed and has a log surface, but changing when snapshots happen interacts with retention and with the derived store's size and should be measured first. Retention still orders by modification time, so a replicated archive arriving from another device can evict a locally newer recovery point - real, and a data-loss path, but the fix changes which snapshot a retention pass deletes and deserves its own release rather than a line in this one. The doubled injection ceiling is untouched: two preamble sources are each budgeted against the full configured limit, so the effective ceiling is twice the configured number, and halving an operator's context window is not a silent change. And the sixteen sites that inline a hash with an ad-hoc truncation are left alone, because they persist nothing this release reads.
Added
Schema-pack integrity is reported as
ok,modifiedorunverifiedwith a named reason, on both read surfaces and ino2b brain schema report. The expectation it compares against is new: applying a mutation now seals the pack by writing a digest of the rendered schema block into its audit record, so a hand-edit after an apply is detectable rather than indistinguishable from the pack that apply produced. Digesting the rendered block rather than the file is what makes it invariant to comment and whitespace churn elsewhere in the config.Negative recall is typed.
not_found,unknownanddid_not_happenare separate answers, and a complete negative requires a digest-bound receipt over the document set actually searched - the index side, with the divergence from the configured note roots reported rather than folded in. Every negative therefore says how far it was checked: an absent index, stale embeddings, an unreadable index instant and a configured root the index does not cover each produceunknownwith their own reason instead of a confident nothing. A vault that never configured an embedding model still receives anot_found, with the vector count and the absent model signature inside the receipt and its digest, because running without embeddings is a supported configuration rather than an unfinished job - and a verdict nobody can obtain stops being read. An interrupted embedding run is the case that is genuinely indeterminate, and it says so.A reverse stale-dependency audit joins the instant a state stopped being current against the instant each consumer was written, and names the context receipts, decision receipts and live records that rest on it. The join needs no new store: retirement already writes the former identifier into the retired file's aliases, so the backlink index answers across the rename. It plugs into
o2b brain doctoras a warning. The arm that reads the backlink graph needs no telemetry and always runs, so a vault with the receipt gates off still gets every finding the evidence supports; what an absent receipt trail costs is named separately, and only when something actually stopped being current.o2b partner codegraph resync --cron-templateemits an installable recipe that re-indexes an external code graph after the watched repository moves. Open Second Brain writes nothing: the recipe is text, and every write in it is a shell command the operator's own scheduler runs on their host, which keeps the module invariant that this project never writes into the partner's store.o2b partner codegraph report --fail-on-healthgives the health verdict an exit code, so a script can gate on it - the report returned success unconditionally before, including for an index built against the wrong root.The proactive trigger queue gains
suppressandunsuppress. Suppression is indefinite by construction rather than by a second timer, because expiry applies only to open statuses, and it records the status it interrupted so restoring returns the finding to exactly where it was, with its original cooldown arithmetic intact. Alongside it, the recurrence ledger the audit trail needed: a blocked finding now increments a persisted count inside the directory lock, so the operator can ask how often a silenced finding has fired since they silenced it.o2b brain snapshotcan cover the derived store, off by default. What it protects is spend rather than information - a rebuild restores everything except the embeddings, which cost an API key - and retention keeps ten copies inside a vault that replicates to every peer, so inclusion is a cost decision and the live store size is reported in every manifest whether or not it was included. Every incompleteness is named: an absent store, a store the integrity scanner has condemned, and one over the configured ceiling each refuse with their own reason, and the store is archived before the tar so a refusal leaves nothing behind.Every recovery point carries a typed reason, the event log gains the
snapshotkind it had been missing sincerollbackwas added, ando2b brain snapshot logcompletes log, diff and revert over the snapshot family. A reason written by a build that did not know it reads as unrecorded rather than being guessed from the run-id prefix.An operator-authored
Brain/standing-rules.mdis injected first, ahead of everything the agent learned about itself, and is exempt from the adaptive budget. It survives a memory-layer failure because it is read outside that boundary, and it is never written to the injection cache, so a stale constitution cannot be served from disk. The file is treated as opaque bytes throughout - read, trim, and cut at a line boundary - so it behaves identically whatever language the operator writes in, and its truncation notice is built from counts alone.Truncation says what it dropped. The section budgeter already returned the list of dropped keys and the caller kept only the text, so the notice named nothing; it now names the sections that were cut and the character delta.
Fixed
On a host without
zstd, the gzip fallback wrote the snapshot archive with a plain file write and silently overwrote an existing one, while thezstdpath refused - and the run-id collision logic depended on that refusal. Two concurrent destructive operations could therefore destroy each other's recovery point with no error. Both paths now refuse through one guard ahead of either compressor.A rollback replaced every entry under
Brain/and only then looked for the derived-store archive, so a missing companion threw over work that had already completed: exit 1, nothing logged, and the vault rolled back regardless. Both conditions are checked before anything is touched, which is the ordering the create path already used.An unreadable optional field in a snapshot's sidecar failed the whole manifest closed, file map included, which reproduced the loss that keeping the manifest version unchanged exists to prevent: a peer on a later build writes a value this one does not know, this one discards the record, and the rollback gate then reports no sidecar at all and skips drift detection. Each optional field now has three states - valid, absent, or present and unreadable - and the third is recorded rather than folded into either of the others.
o2b brain schema syncreturned a success report and performed no work at all; it did not even take a vault. It refuses now, because what a schema backfill should write is undefined and an invented one would rewrite notes nobody asked it to touch.An absent
Brain/_brain.yamlwas substituted with a valid empty schema pack, so a vault with no ontology was indistinguishable from one that declares nothing.A malformed grounding-artifact list on a trigger degraded to empty, so an unparseable finding presented as an ungrounded one. It refuses now - and the refusal is confined to the record that is broken, because the first form of it had no per-record boundary and one corrupt file took down list, history, the morning brief, delivery marking, every transition and the scan that records new findings. Two callers then swallowed that into a bare catch and the brief rendered with its pending section absent, which is indistinguishable from an empty queue. Every surface now reports what it could not read alongside what it could.
A trigger record with no creation instant was written back with an empty last-seen field and refused on the next read, so its first recurrence write made it permanently unreadable.
The occurrence counter was race-free only against itself: the two writers outside the scan path wrote the whole record from an earlier snapshot without the directory lock, so a scan interleaving with a suppress lost one of the two.
A decision-receipt directory that could not be read reported as a vault that had never recorded a decision, which additionally defeated the duplicate-append guard directly above it.
The snapshot diff classifier predated per-device log sharding and mislabelled both the sharded Markdown and the machine-primary log - the surface a pre-restore diff is most likely to be showing.
Brain/.artifacts/is documented as never backed up and was archived in every snapshot and hashed into every manifest, so ephemeral tool output tripped the drift gate.The derived-store archiver resolved the store by its default location and ignored a configured override, so a vault with one either refused as absent for the wrong reason or archived a stale database and reported success.
An operator configuration error in the vault-instruction path was swallowed into a null field: the key simply vanished from the response and the operator was told nothing.
The emitted resync recipe gated on
if jq -e, and a bareifcollapses every non-zero exit into one answer - so a parse error read as no mismatch and the recipe re-indexed over a root nobody had verified, against the comment three lines above it. The status is read explicitly and only a literal falsy result is a pass.Shell completions hand-listed the three root commands with subcommands, so every
o2b partnerverb had been offered by no shell since that surface was added, and an absent completion reads as a command that does not exist. The groups are derived from the command manifest at every depth now, and a test asserts each shell's output covers them.o2b brain trigger's verb map routed any accepted verb that was notackordismissto the act transition, ando2b search reindexdeclared none of its eight flags in the command manifest, so help and completions could not offer the cron template.Two test files ran CLI invocations concurrently. An in-process run swaps the environment, the working directory and both output streams and restores them in a
finally, so overlapping runs restored each other's swapped state: one left a temporary config path installed for the rest of the process and seventeen assertions in unrelated files failed with a vault that could not be found, at a stack with nothing to do with the test that broke them. The runner refuses to overlap now, which immediately surfaced a second such race that had been silently wrong.The tree-wide format check walked into nested git worktrees, because the vendored bundle it excludes was matched by a root-relative pattern, and failed for changes that were entirely clean.
Changed
One digest encoding. Ten modules carried a byte-identical private SHA-256 helper and three reimplemented canonical JSON serialization independently, which was survivable while every digest stayed inside the process that computed it and stops being survivable the moment one is persisted: this release persists two digests, into a vault that replicates between machines. The three canonical-JSON copies disagreed about an object entry whose value is
undefined. In the two that feed a persisted digest the divergence was unreachable, because every parser building their input uses a conditional spread, so no stored value changes. The third keyed an in-process request cache and rendered such an entry as an explicit null, which was distinguishing two requests that are byte-identical once they leave throughJSON.stringify- so absorbing it closes a latent cache miss rather than changing a value.Every closed verdict vocabulary this release introduces registers in one census that asserts its frozen object, its membership list and its type guard agree, with synthetic negative cases proving each assertion bites. The status partition behind the trigger queue was three hand-maintained copies across core, CLI and the tool layer, with a fourth copy of the status list as a literal in a tool schema and nothing asserting the two matched; there is one owner now.
1.44.1 - 2026-08-08
Fixed
Reuse and reap Hermes MCP bridges. A Hermes memory provider is constructed per agent while the MCP server is a gateway resource, so every new session spawned its own Bun child - and cache eviction released the client without shutting the provider down, so nothing reaped it. A long-lived backend accumulated processes and descriptors until it hit
EMFILEand lost its listening socket. Providers now share one bridge per effective vault, repository and command; shutdown terminates, escalates, reaps and closes the child's pipes rather than only waiting on the process; a failed handshake no longer leaves its child alive; and POSIX children run under the host's parent-death watchdog so a hard gateway exit cannot orphan them. Reported and fixed by @DanBennettUK in #156.One shared bridge is one stdio stream, so its calls are serialised - which makes a child that stops answering everyone's problem rather than one session's. Every request therefore carries a deadline, and a bridge that misses one is torn down and restarted rather than holding the gateway behind an unbounded read. There was no read timeout anywhere in this transport before, and with a bridge per session that was survivable; with a shared one it is not.
The committed OpenClaw bundle was two releases stale, and the gate that would have said so only ran on a tag.
openclaw/index.jswas last rebuilt at 1.37.0 while its source changed at 1.43.0, so the plugin bundle OpenClaw users install did not contain that release's quarantine filter - the release workflow failed on exactly this check for both 1.43.0 and 1.44.0 and skipped its publish job, which is why the releases were cut by hand and the breakage went unread. The bundle is rebuilt, and the same check now runs on every pull request so the change that staled it fails rather than the tag that ships it.The parent-death watchdog is optional by construction - the host module may be absent in a standalone checkout - and its absence was silent, so descendant cleanup could disappear in a rename without a word. It says so now, once per process.
1.44.0 - 2026-08-08
Thirteen units on the retrieval path and the store beneath it. A reconnaissance pass ran before any design and falsified the load-bearing premise of nine of the eleven tasks in scope, and what survived had a shape none of the task descriptions predicted: with two exceptions these were not missing capabilities but values the system already computed and discarded before anything could see them, and checks that were never run at all. A structure recording which candidates were evaluated, surfaced and excluded was built on every gated search and serialized by nothing. A content hash had been on every chunk since the first schema version and was never read back. A match-centred snippet function was written, tested, and mounted on the wrong surface. An expiry filter took an as-of date as a parameter and no caller ever passed one. A ranker accepted an injected clock and the stage above it did not inject.
Some of what was asked for is not here, and its absence is a decision rather than an omission. There is no runtime probe of an embedding model's input window, because no provider in this system exposes model metadata, no local model file exists, and the provider contract has no window field - the only implementations are a network round trip or a hardcoded table over an open set of endpoints. There is no second index-health counter surface, because o2b search status is already the eighth and the stall reasons the request described belong to a background drain loop this project does not run. There is no column type-drift check, because SQLite accepts a text value into an integer column and still reports the column as integer. There is no daily snapshot of the derived store, because o2b brain snapshot and o2b brain rollback already ship the checksum sidecar, the verification, the pre-restore diff, the confirmation and the retention that request called its differentiating pieces. And two of seven proposed recall-quality signals - acquisition risk and expected regret - are refused outright, with a census proving their names appear nowhere in the source, because there is no data behind either and an invented number is worse than an absent one.
Five reviewers were then given the finished branch and the claims made for it, with no knowledge of how the work was done, one told to treat every test as guilty until proven innocent. They found defects this wave had introduced, including one that turned a harmless refusal into silent data loss, one mechanism sealed by another unit in the same branch, and three claims in the branch's own comments and design document that were not true. Every one is fixed or named below.
Added
Exact-duplicate passages merge into one result naming the locations they were folded from, on the full and card surfaces alike. The identity is the pair of whole-file bytes and passage bytes, not the passage alone: frontmatter is stripped before chunking, so two records with the same prose but different dates, tiers or titles hash their chunks identically and are not duplicates.
Every snippet surface anchors its window on the match instead of the head of the chunk, through one function with four callers. A passage that fits its cap is shown whole rather than slid past.
The retrieval decision trace and the memory trust assessment - built by the trust gate and serialized by neither surface - are returned under the existing
explainflag, absent rather than null when it is off, and naming the switch that produces them when the gate is not engaged.o2b search check --integrityruns the full structural scan on demand and records its verdict where the write-open gate reads it, so an index that only ever gets read can still be condemned.An oversize-chunk census compares the configured chunk size against the configured model's declared input window, which the curated presets now carry. The shipped default chunk size overflows the shipped default model by roughly two to three times, and nothing had noticed. The estimate is two-sided - a floor charging every code point a quarter token and a ceiling charging every non-ASCII code point a whole one - because a quarter-token-per-character proxy under-reports Han text roughly fourfold, and a census that stayed silent there would be claiming a pass it had not earned. Between the two bounds the verdict is
estimate-undecided, which is reported rather than rounded to clean. The instruction prefix the provider prepends is measured into both, and which backends send one is an exhaustive switch, so a new backend fails to compile rather than silently inheriting a zero.A gated search records four computable recall-quality signals on the telemetry record it already writes, one record per query.
o2b brain querygains--atand--show-expired, threading the as-of instant the expiry filter has accepted as a parameter all along.The paired holdout harness a release note has described since 1.36.0 now gates
o2b brain repair-lane --apply, which is what makes that claim true. It had no production caller at all.
Fixed
A transient failure while opening the store for reading was classified as an unreadable index, which is self-healable, so a locked pragma triggered a silent rebuild that dropped every embedding. The busy timeout is now set before the statement that needs it, and an unrecognised failure is no longer grounds for a rebuild.
A recorded integrity fault was honoured on read and ignored on write, and a completed scan refreshed the interval stamp, so a condemned index kept accepting writes for a day while every query refused it.
The ranker read the wall clock itself because the stage above never passed the instant the request had already resolved. One instant now governs a whole search. The vector lane returned an arbitrary member of a tie band - and its two branches disagreed about which - because a
knearest selection happens before any SQL ordering; three candidate queries now end their ordering in a unique column.Reciprocal-rank fusion discarded the per-query intent profile, so a quoted phrase expressed a preference the opt-in fusion mode silently ignored, and the learned weights from operator feedback went with it.
The entity identity kernel treated a typographic apostrophe and an ASCII one as different entities. The fold that joins them is derived from Unicode categories, not a character list, and the collision it creates between two records that already existed is refused with the command that resolves it rather than silently rewriting one label.
Learned recall weights were validated for finiteness but not range, so a hand-edited file could inject a zero or negative lane multiplier - which under rank fusion deletes a lane or inverts its order.
show_expiredwas silently ignored outside topic mode while its own schema text said topic mode only.The store verified its schema version integer and nothing else, so an index missing a table opened cleanly and failed later inside a query with a raw SQL error. The expected objects are derived by running the migrations, not by a second hand-maintained list.
A corrupt-but-parseable index passed every check and served wrong results indefinitely; there was no integrity check anywhere in the codebase.
The CLI truncation marker was keyed on the raw chunk length, so a chunk that fitted after whitespace collapse still claimed there was more.
totalechoed the number of rows returned, which is no information at all. It is the ranked pool the limit sliced from.The MCP tool descriptions still told every agent that
sinceanduntilfilter on when a document was modified. They have filtered on event time since 1.43.0.Entity extraction backtracked catastrophically on any unbroken run of letters carrying no digits - which is what an ordinary Han, Thai or Lao paragraph is. Measured at 11.7 seconds and zero matches for a single 2160-character chunk, growing cubically, so a Chinese vault of any size could not be indexed at all. The two mixed-script alternatives now consume their leading run atomically: 20 milliseconds on the same input, with identical output on every mixed-script sample.
The explain surface named a cross-vault union as the reason there was no trace even when the trust gate was also off, sending the operator to a single-vault query that produces nothing either. When both hold, both are named.
Changed
The vendored Hermes tool schemas were re-vendored; three properties added in earlier releases had never reached them.
o2b search query's flags are declared in the command manifest, which generates the help and the completions. None of them had been.
1.43.0 - 2026-08-02
Ten units on one seam: the boundary where content, authority and claims enter the vault. Open Second Brain could say a great deal about what it holds and almost nothing about where any of it came from, who was entitled to put it there, or what backs a claim made about it. An entity extracted from a scraped page became a first-class Brain entity on the same terms as one the operator typed. A write named any path it liked. A note was created with whatever frontmatter the caller assembled. A record was ranked by when its file was touched rather than by what it is about. An agent posted its own success and nothing recomputed it.
A reconnaissance pass over the code preceded the design and changed the work substantially: load-bearing premises were falsified in nine of the ten tasks. Two described a capability that cannot be built honestly on this architecture and were scoped down rather than faked. An independent verifier is not definable here - agent identity is an environment variable, else a config key, else the literal string agent, with any caller-supplied string accepted verbatim, and the server is a child process of the agent it labels, so two records asserting two names chosen by one process are not two actors. A schema pack fetched by URL is not implementable either - a pack is a block inside one config file, the listing returns a single hardcoded entry, and there is no registry to install into and no signature verification anywhere. Three tasks described gaps that are already closed while the open gap sat one layer away unmentioned: ranking already used a content-derived event time, skill invocation was already observed, and a document validator already existed.
Five reviewers were then given the finished branch and the claims made for it, with no knowledge of how the work was done, one of them told to treat every test as guilty until proven innocent. They found thirty defects, nine introduced by this wave. Every one is fixed or named below. Three were a mechanism nothing could make fire, which is the shape this repository reverted a unit for one release ago, and one of those three was removed rather than repaired.
Added
Untrusted intake lands in a quarantine lane instead of the registry. Trust is originated where content arrives and derived structurally from the source identity's shape, with no host list, scheme list or word list in any language. The entity status filter, previously re-implemented at every caller and absent at eight of them, is one shared predicate a census keeps singular, and a status nothing can leave would be a dead end so release from quarantine is the named exit.
A write boundary the operator declares in the vault config. A caller-named write outside the declared path prefixes is refused through a registered code. Its authority is the config file, never the caller: identity here is self-asserted and additionally overridable by an argument on twenty-two tool schemas, so a fence keyed to it is bypassed by passing a different string. This is a write boundary over caller-named paths and not a security boundary, and nothing describes it as one.
Note creation gains an idempotent skip whose result says which happened, pre-write validation over the existing artifact validator, and a template mode with a closed two-construct grammar - typed variables, and presence-driven sections with list iteration - parsed by recursive descent with no dependency.
A body-derived date anchor, materialised at index time with the rung that won recorded beside it as a registered token, so a back-dated note ranks by what it is about.
o2b search event-anchor-backfillresolves anchors for documents indexed before this release; an index run counts what is still pending and names that command as its exit.embedding_batch_tokenscloses an embedding batch on accumulated tokens as well as on item count, on both batching providers.One capability-tier resolver shared by the five sites that computed those facts separately, behind registered codes, with a contract predicate replacing a provider-name string comparison duplicated at five sites.
o2b search vector-backfillruns the vector phase alone.schema_apply_mutationsando2b brain schema applygain a dry run returning the pack that would result and its diff, with the same validator rejections and no write.A second ledger record carrying the kernel's own on-disk evidence against the acting agent's claim, joined by the existing sample id. It carries no verifier identity and no surface calls it independently witnessed, because it cannot be.
A skill offer now has a content-addressed identity an invocation can be joined back to, a discriminating-term ranking floor derived from corpus document frequency, and retained provenance where a skill is shadowed by a same-named one in a later root.
Fixed
o2b brain schema apply --dry-runparsed the flag and wrote the vault schema anyway. It previews now.An untrusted source that guessed an entity name captured it permanently: every later trusted write landed inside the quarantined record, returned a not-created result, and read back as absent. Trusted and untrusted writes are separate lanes that never resolve across.
The trust classifier had the wrong polarity in three ways. A bare host classified as trusted, an intake naming no source classified as trusted, and a vault filename containing a colon parsed as an external URI and quarantined everything it touched. It trusts only what is shaped like a location inside this vault, and an intake naming no source is refused before any write.
A symlink under a declared write prefix widened the binding to the whole vault while the response named a path the bytes were not at. A backslash was a separator to the prefix matcher and an ordinary character to the filesystem, so a binding admitted a write that landed at the vault root - and the same mismatch had been hiding the Brain machinery root from its own guard with no config present at all. Both are pre-existing and both are closed.
The anchor ladder had promoted two frontmatter keys into event time, and event time feeds the hard
since/untilfilter rather than only the ranking boost, so a note carrying an oldcreated_atand edited today disappeared from a query for recent work. Those keys stay recorded with their provenance; they are no longer consulted by the query side.A frontmatter key containing a newline emitted extra lines, and because an update merges last-wins a caller could overwrite a key it was not asked to touch. Keys are refused at the emitter rather than escaped: the reader has no quoted-key form to escape into, so a quoted key would be lost on the next parse, trading an injection for a silent loss.
An explicit semantic request degraded to lexical results with a success exit on one rung of the capability ladder. A token budget above the 32-bit boundary truncated to one item per request, and the sibling count cap had the same defect; both are refused rather than quietly reinterpreted.
Quarantined records were readable through page walkers, which no entity-reader rule can see because entity records are ordinary vault pages.
A capability probe made an outbound request with no credential whose only possible outcome was an authentication error, a working local provider was reported as having an unresolved key and advised to set one it does not use, and a fourth branch of the deferred-reason function was unreachable because its only call site passed a constant.
Changed
The declared continuity scope was built and removed before release rather than shipped. Nothing could supply one, and its single vocabulary member would have made a record less visible, since a declared scope is dropped by every reader that does not name it. The record envelope is unchanged, dedup id included, and is now pinned as a known answer.
Three censuses were added and two of them were the disease they were written to cure. The write-site census read only the first
node:fsimport statement per file and was already missing a live vault append. The entity-read census missed the registry API whose explicit status branch bypasses the shared predicate, had the quarantine value absent from its own vocabulary, and its second rule reached nothing at all. Every rule now has to reach modules no other rule reaches, and that is asserted rather than described.Four tests claimed byte-identity without comparing bytes, one by sorting the keys whose order was the property. Each compares a golden now and each was shown failing against a real mutation of the code it pins.
Notes
Three modules carried literal NUL bytes as key separators, which makes
grepclassify the file as binary and print nothing for it. They were invisible to every grep-based sweep, including this release's own reconnaissance, which is why it undercounted the continuity readers as five of twenty-one when the measured figure is four of twenty-four. All three now use the two-character escape.Found and deliberately not closed: roughly forty test sites report a silent pass when the vector extension is absent, so a host without it loses the coverage while the suite reads green - two files were converted and the shared helper is a follow-up. Twenty continuity readers bypass the masking read model, nineteen of them predating this release and the twentieth added by it. Page walkers outside the link-graph tree are not asserted by the new rule. The concurrency cap has the same 32-bit truncation as the batch caps, left alone because there the clamp is protective and a maximum is a policy decision. And
brain_token_impactstill has no actor wiring - the asymmetry one unit set out to close did not exist in the direction stated.Two review findings were measured false and no fix was written for them.
The vault schema goes to version 11. The migration is additive and runs in place; it does not reindex, which is what the new backfill verb and the index run's changed exit exist to say out loud.
1.42.0 - 2026-07-27
A structural wave with no new capability: the six modules that cost the most to change, plus the two import cycles in the graph, plus the silent fallbacks the split uncovered on the way. The targets were chosen by measurement rather than by impression - a maintainability index, a cognitive-complexity count and a fan-in count per file - and the six worst were a config validator with a single twelve-hundred-line function, the SQLite store as one class holding fourteen tables, the recall pipeline as one function running twelve stages, every search subcommand in one file, the vault doctor as twenty-six unrelated diagnostics in one body, and the dream pass with its orchestration fused into the mechanics it orchestrates.
The rule for the refactoring commits was that behaviour is byte-identical, and the existing suite is what proves it, so no test may be edited to make one pass. Where that guarantee was load-bearing it was checked directly rather than left to the suite: the config validator was run against its predecessor over 3135 generated configs comparing resolved values, key order, warnings and thrown errors; the search command line was run through 92 invocations on identical fresh vaults with stdout, stderr and exit code diffed byte for byte; the store's schema statements and the recall pipeline's cache-key composition were diffed line for line against their previous versions, because either one changing would strand data already on disk without failing a test.
Three ratchets grew rather than being excused. The terminal-state census followed the code out of the file it used to read, and three exclusions that predate this wave turned out to be detectable once it did. The doctor's code census now enumerates its check modules from disk. A new census walks the static import graph and fails on any cycle at all.
Independent review of the finished branch - four reviewers given the diff and the claims, with no knowledge of how the work was done, one of them tasked with treating every changed test as guilty until proven innocent - found fourteen further defects, four of them regressions this wave introduced. Two are worth naming here because they are the shape this exercise is supposed to catch. Narrowing a swallowed database error to the one condition it documented turned a lane that only ADDS search candidates into one that could fail the whole query. And the new import-cycle ratchet was proved to pass on an empty set twice over - with its source root pointed at an empty directory, and with its patterns altered to match nothing - so it would have reported an acyclic graph over a fully cyclic one, permanently and silently. Every finding is fixed and named below; each ratchet now carries a demonstration that it can fail.
The silent fallbacks are the part with user-visible behaviour, and review turned one correction into a theme: absence and inability to look are not the same answer, and nearly everywhere this code conflated them the result was not imprecision but destructive advice. An unreadable vault root was told to run brain init over a populated Brain layer; an unreadable index was told to re-initialise an index that already existed; a manifest behind an untraversable parent was told to run an update that regenerates the file and leaves the permission fault in place. Nine such sites across six subsystems are closed.
Changed
The vault config validator is one module per
_brain.yamlblock, each owning its default table, its read-side resolver and its parser together, above a shared constraint vocabulary and key index. The public path and all fifty-five exports are unchanged, so none of the seventy importing modules moved. Nine duplications and dead branches went with the split: a YAML-safety loop written three times, an array guard written at five sites, two bound checks with one message, a deadSetbranch, a ternary whose arms were identical repeated across six blocks, a default parameter no call site overrode, a boolean check split out of the loop handling its four siblings, validation sitting a thousand lines from the construction it guarded, and three parameters threaded loose through twenty call sites.The search index store keeps its class and its full method surface - twenty-five modules hold that abstraction - and the bodies moved into seventeen modules, one per table or lifecycle concern, each taking the database handle as an explicit parameter. The on-disk format is untouched: the schema module has an empty diff, the version is still 10, and no table-creating statement exists anywhere in the split.
The recall pipeline reads as the stages it runs. Each stage is a module with an explicit input and output type, and the gate deciding whether a stage runs is visible at the call site rather than buried inside it. Warning order is preserved exactly, because it is observable: each stage returns its own list and the coordinator drains them in the original sequence.
Every
o2b searchverb is its own module under the layout the brain verbs already established, rather than a second layout invented for it. Six duplications went with it, including the vault, config and database flag schema written out fourteen times and the whole report-emission block copy-pasted betweenindexandreindex, comment included.The vault doctor is a registry: a twenty-six entry array a new check joins by being appended to, over one module per check family. Each check declares its own fail-soft flag beside its implementation, encoding exactly the guard placement the single body had. The forty-one diagnostic codes are unchanged, verified by running the census's own patterns over both trees.
The dream pass separates deciding from writing. The apply module owns the order of writes that keeps the on-disk invariants true and decides nothing; the planning modules decide and write nothing. Moving the rename out made the new module write-capable and the vault-guard census correctly failed on it - the fix was to make the guard honest by asserting write identity on entry, not to add an exclusion.
The two import cycles are gone. Both existed because one large module played two roles at once: declaring a value everyone needs, and aggregating types from its own dependents. The search error vocabulary, the ranked-row shape and the vault-relative path constants now live in leaf modules that import nothing, re-exported from the modules that used to declare them. Both export surfaces were compared symbol by symbol against the previous commit through the TypeScript checker rather than by eye.
Fixed
A
_brain.yamlthat is present but malformed no longer reads as one that is absent. Five config readers shared a blanket catch that collapsed the two, so an operator whose configuration had a typo was served the documented defaults while every surface reported healthy. Absent stays byte-identical and no longer even opens the file; malformed, structurally invalid and unreadable each raise with the path named. No new diagnostic code or configuration key was needed, because every call site already had a visible channel: the feedback scope raises before the durable write, since a signal recorded under a silently wrong scope is a record no later fix reaches, and the snapshot retention read raises inside the gate's existing best-effort catch so the destructive prune is skipped rather than run at a guessed retention. The one reader that still resolves rather than raises is the integrity gate, which now documents why - the search layer reads it on every store open inside a fail-soft path, and its strict fallback means an unreadable config can only close a gate, never open one. A test pins that asymmetry so the two shapes are not later unified by someone tidying.The doctor now says when it could not look. Two sweeps caught every read failure and returned nothing, and for a diagnostic no findings renders as a clean bill of health - the operator was told the vault was fine exactly when the check was unable to examine it. A missing path stays silent, since a path that is not there was never a subtree to sweep; a permission failure, an I/O error or a file standing where a directory was expected now names the path, the reason and the specific claim the sweep can no longer make, through the doctor's existing uncertainty stream rather than by raising, because a doctor that dies on the first odd directory reports less than one that continues. Two further catches of the same shape were found in the removed-tool scan and fixed with it.
One of those guards could never have fired. The vault walk beneath it swallowed every directory failure internally, while accepting a notices sink in its own options type and dropping it - so the guard above was inert and the real silence was one level down. The walk now honours the sink its sibling branch already did.
An unreadable search index no longer narrows recall while looking healthy, and no longer fails the query either. The trigram candidate lookup caught every database error and returned an empty candidate list; narrowing that to the one condition it documented then raised for everything else, which review showed could kill a query the keyword and semantic lanes had already answered - an indexing pass holding the write lock past the busy timeout is enough, and so is an index whose trigram tokenizer the running build does not have. The lane only ever ADDS candidates, so it degrades and the query carries the reason, from a closed vocabulary that never forwards the database message: the full-text engine reports a rejected match by echoing the expression, which is built from the caller's own query terms. The absence predicate is also anchored now - a missing shadow table means the index has lost one, which is corruption, and it used to read as the table never having been built.
A neutral fold of the learned recall weights could attach a ranking reason to itself. Two booleans were derived separately from one read, one gating the composition and the other gating its explanation, so using the wrong one credited a fold that did not move the ranking. They are one nullable value that is non-null exactly when the fold applied.
A
void parseFrontmatter;statement in the doctor claimed in its comment to prevent a dependency being stripped, while the import was genuinely used the whole time; avoid basenamein the dream pass claimed a barrel re-export that never existed; and a planning function there was a pure stub. All three deleted.A vault that cannot be examined is no longer reported as one that was never set up. An unreadable root made the doctor advise
brain initover a populated Brain layer; an unreadable index made the code-graph partner advise re-initialising an index that already existed; a manifest behind an untraversable parent was reported missing under a fix that regenerates the file and leaves the permission fault. The status payload'svault_existsfield, which also gated three blocks, answeredfalsefor a vault it could not stat, so an operator saw four confident negatives for one missing execute bit. The field carries a named reason on that branch only - both answerable cases stay byte-identical - and the blocks it gates degrade themselves rather than being omitted, since an omitted block reads as the same confident negative. The second runtime publishes the same field from its own payload and now shares one formulation of the condition rather than a second copy of it.The plugin config read failure had nowhere to land. The command-line dispatcher formats two error classes and rethrows the rest, so an unreadable config printed a source excerpt and eight stack frames with zero checks reported - worst on
doctor, which prints the correct diagnosis the moment the crash is out of the way - and the JSON surface produced empty output plus a trace. It refuses with a formatted message now, both JSON surfaces stay parseable, and the remedy travels in the error because it escapes through four unrelated channels. Two tool surfaces computed their whole payload and then threw on the one field that reads the config, discarding the report that names the broken file; both report it in the field instead. The server resolved the agent identity from that same config before any handler ran, which made those repairs unreachable - including in the handshake, so a real client would never have issued a call at all. The identity is resolved where it is used now, still raising, still never substituting a placeholder name.An existence check in front of the config read returned false for every errno, so a readable config behind an untraversable parent silently reverted every setting - in the commit that claimed to have fixed exactly that. It is one stat now, which makes the split free: the absent case pays the syscall it always did and constructs no error. Invalid UTF-8 was never really read as missing either; it produced an empty config that every resolver reads as "the operator set nothing", while the write path would have persisted the file with each undecodable byte replaced.
The pre-compaction memory pack died on a broken config instead of degrading. The integrity gate's strict fallback resolves owner-scope delivery to
fail, which takes the scoped branch, which renders the active head, which now raises - so one bad line cost a scoped agent the whole pack. Compaction happens whether or not the tool answered, so a refusal deletes the memory at the one moment it cannot be re-fetched: the pack is delivered, the scope-narrowed head is withheld, and the reason rides on the payload.One uncapped, undeduplicated uncertainty entry per sweep meant four identical lines for one unreadable subtree, and an existence check in front of every sweep sent parent-directory failures past the classifier entirely. Three ordinary broken-vault shapes also made the whole diagnostic pass throw with zero findings, which the hoisted shared context had made the first thing it did.
The
pref|ret|sigartifact id grammar had two homes and could drift. It is now a predicate on the module that already declares itself the one place the wikilink grammar lives - the other direction would have closed an import cycle, which the new ratchet catches.
Notes
Three things were found and deliberately not changed, because fixing them alters output and that belongs in its own release: the same constraint renders two different messages depending on which config block declares it; the same command-line flag is refused with different wording by different subcommands; and several reads in the recall pipeline swallow a failure to keep a single unreadable file ranking neutrally rather than failing the query.
The measured effect: no file in the repository now scores worse than the seventh-worst did before, the worst cognitive-complexity count in the touched set falls from 441 to 32, mean cognitive complexity across the tree falls from 34.9 to 30.7, and the import graph is acyclic.
Three ratchets can now be shown to fail. The import-cycle census pins the size of the graph it searched, with separate floors for the two narrow declaration kinds, since the total is dominated by plain imports and either could stop matching without moving it; a type-only import stays an edge and a deferred dynamic import stays excluded, both asserted rather than left as comments, because counting the deferred ones closes two cycles on this tree - which is to say those deferrals exist to break them. The terminal-state census applies the same reason, class and cap gates to both of its inventories, so a state rescued by the widened module walk can no longer be excused without a written argument. Its hop limit was measured inert beyond one and is one now.
One gap is stated rather than closed: seven degradation codes reach the doctor's uncertainty stream with neither a registered command nor a written exclusion. Closing it needs a judgement per condition.
The next tier by the same measurement, untouched here: the preference parser, the skill proposal store, the search and recall tool surfaces, the indexer, the digest and the command-line entry point.
1.41.0 - 2026-07-27
A signals-that-survive wave: eight units that carry a signal the kernel already computes the last few metres, to the decision it should have changed. A repository declared which files it considers noise and the ingest planner ignored the declaration, while the hygiene scan beside it had honoured git for releases. A recall attempt was graded weak and the grade evaporated when the call returned, so nothing could tell that the same question had come back insufficient four times. A query named a period and the ranker went on applying a freshness prior pointing the other way. A parse error carried its file path in prose and as a field, so the doctor printed it twice. Three write paths accepted model-authored payloads and parsed them loosely, straight into the vault.
A ninth unit was written, reviewed, and then reverted rather than shipped, and that is worth stating plainly: a tie-break layer for the write router turned out to be unable to fire at all. Proving it byte-identical when its gate is absent also proved its reach — only a rival claiming a captured span in full can enter its margin, and the three pattern families this router chooses between cannot do that, because a complete match of one carries a character the others do not admit. The routing ambiguity the task describes is real, but it lives on a different write surface; this router chooses between structurally disjoint families and has nothing to disambiguate. Shipping the mechanism would have shipped a decision layer that never decides and a telemetry record that is never written.
Reconnaissance against the code invalidated a premise in four of the nine scoped tasks before a line was written. Two more were falsified during the release itself: one recorded during scoping and corrected in the design document rather than quietly dropped, and the one above. Independent review of the finished branch — reviewers given the diff and the claims, with no knowledge of how the work was done — found fourteen further defects that the implementing agents' own passing tests had not, most of them in error paths and option branches those tests did not model. Every one is fixed in this release and named below. The kernel still calls no model, no natural-language word list was added in any language, and every new flag and configuration key is byte-identical when absent.
Added
Git-aware ingest source discovery. The batch planner honours the repository's own root and nested
.gitignore,.git/info/exclude, and submodule boundaries, through a discovery module extracted from the hygiene scan so both paths compose one implementation and the submodule predicate - the only net-new piece - exists once rather than in two twins that drift. The layers stack repository-declared first and the operator--excludelayer last, re-stacked at every directory so it wins at any depth; a submodule boundary is a boundary rather than an ignore rule, and--excludecannot re-include it. A malformed pattern in a repository's own ignore file is returned as a structured warning and surfaced on both operator surfaces - carrying source, line, pattern and reason as fields - rather than printed from the kernel or failing the plan; a malformed operator--excludestill throws. Under--subpaththe directories between the source and the walk root were never visited, so their.gitignorefiles were silently skipped; they are now layered. A tree with no ignore files produces the same plan identifier it produced before.A routing hint on an unroutable capture. A feedback signal recorded with no scope now names
scopeas the missing routing signal and lists the scope slugs the vault actually contains, ranked by document frequency over confirmed preferences and the unconsumed inbox - so recency is structural rather than clocked, and candidates come from frontmatter rather than from a word list. A vault with no scope corpus returns nothing rather than empty prose. The forward pointer resolves a registered diagnostic code through the advisory rail, resolved at module scope so registry drift fails at import instead of silently printing nothing, and the hint is recorded under its own log event kind - a hint nobody counts cannot show whether unrouted captures fell. The write always lands; no configuration key was added.Query-side temporal intent. A query that declares a window - from ISO tokens or
since:/until:field tokens, never a natural-language phrase in any language - biases ranking toward that window through a capped additive layer over the existing proximity function, and damps the freshness prior when the window is historical. Field values accept only language-neutral forms, checked by shape rather than by a second parser, so the word grammar the explicit parameter has kept for releases is not exposed to free query text. A bare token that merely looks like a date but does not resolve declares nothing and stays in the query as content, rather than failing the search. Freshness and temporal intent stay separate entries in the reasons and in the score breakdown, so a ranking change is attributable to one of them. A tie-break rung sits above the freshness rungs and above the keyword-score rung, because that score is already a component of the total and re-applying it on an exact tie double-counts a signal the temporal contribution cannot outweigh once clamped. Detection runs on the raw query before expansion or any other lexical restructuring, and the directive is stripped from the text lanes before matching: the full-text builder joins tokens by implicit AND, so a directive left in the query is a required term no document carries and the search returns nothing. A query that declares a window and no terms is an input error naming what is missing.Weak recall becomes tracked work. The recall-adequacy verdict, already computed deterministically from numeric scores at two sites, is stamped onto the demand record under the bucket key the demand log already derives from document frequency, with its existing redaction and caps. The knowledge-gap loop gains that as a second signal source beside its structural one; the two are never summed, the task key is namespaced by source, and the agenda labels which is which, so a coarse telemetry bucket and a fine term bucket stay distinguishable. Promotion is capped per run and a skip costs no budget, so an established backlog cannot starve a new gap.
factandskillmarker kinds on the existing inline grammar, so plain text can seed all three destinations rather than only a taste signal. No second grammar and no new entry point: fence-awareness, the consumed sentinel and the rewrite machinery apply unchanged. A fact lands as an unconfirmed preference through the existing derive path with premise validation still fail-closed; a skill lands in the pending proposal store, never in accepted. A declared skill deliberately bypasses the mining verifier rather than satisfying it - that gate demands three distinct pieces of structural evidence and a confidence floor, which a single declaration can never meet, so running it would reject every marker and ship the feature inert. For the same reason the declared frontmatter writes no confidence key: an absent claim already reads as zero self-reported support, and writing a number would fabricate a statistic.Response-shape validation on the three model-authored write paths. Each ingress checks its payload against a frozen descriptor before any field is read; a violation raises a named error carrying code, path and message as fields, nothing is coerced, and a batch whose third item is malformed writes none of the first two. Validation is unconditional - a validator that can be switched off is one the write path cannot rely on, and a strict path shipped inert behind a permissive default would be exactly the silent no-op this project forbids. The validator is not new: the JSON-subset checker in the Model Context Protocol output contract was promoted to core and both sides consume one definition, replacing the hand-rolled coercion checks that had been duplicated between each command-line verb and its tool.
Durable work identity above the timing crutch. A session may declare a work id and a lane, and a rung above the fifteen-minute window links two entries sharing a work id on identity alone - no freshness bound, no working-directory or branch predicate - so resumed work re-attaches after a model, account, branch or worktree switch. Identity is declared, never derived, through three rungs where the first to declare either field supplies both; deriving it from remote and merge-base into a union registry was rejected because that places mutable, effectively irreversible state inside a peer-replicated vault with no arbiter. A declared lane mismatch excludes a candidate at both rungs rather than only at the identity rung - a separator one rung ignores is not a separator - and the refusal is a named outcome reported ahead of predicates a candidate merely fell short of. The abstention layer from 1.39.0 is consumed rather than re-implemented.
Changed
A parse-error finding names its file once. The path moved out of the message and into a field on a typed error that composes its message from a bare detail plus that path, so every caller surfacing the message directly receives the same string it received before and none needed editing. Both branches were duplicating, not one: the status-folder-mismatch error was documented as already path-free and was not - its constructor appended the path to its own message. That error carries its location in the middle of its historical sentence rather than at the end, so it composes its own message through a single declared seam instead of having its layout rearranged.
Four optional-field helpers in the preference parser threw with no path at all, so a malformed list-valued field named no file on any surface. They now carry the location they never had.
Repeated keys inside a marker fence silently last-won, and one kind carried a bespoke duplicate guard to compensate. Both marker shapes now funnel through one collector with per-field arity, so a repeated single-valued field is refused as ambiguous for every kind and a repeated multi-valued field keeps all its values.
A malformed value in an explicit
since:/until:search field is an input error rather than being matched as literal text, which is how the equivalent parameter already behaved. A bare date-shaped token in ordinary query text is not a declaration and is left alone.The ranking boost caps were unnamed literals with no single place to compare them, so the new temporal cap had nothing to be proportioned against. They are extracted as named constants and the proportion is asserted against those rather than re-hardcoded.
Fixed
A
--src-subpathwalked past exclusions and boundaries the unscoped walk honoured. Pruning only ran on directories reached as a directory entry, so a subpath starting below an ignored directory re-entered it, and one starting below a nested repository crossed into it - the same tree answering differently depending on how it was addressed, which also forked its plan identifier. The ignored case is refused and reported with the override that opens it; the boundary case is refused outright, because continuing would attribute another repository's files to this plan and an empty plan would be indistinguishable from nothing to do.An unreadable repository ignore file was swallowed by a catch that returned without touching the warnings array in its own signature, so a file the repository declared excluded was queued for ingest with no signal on any surface. Only a genuine absence stays quiet now; every other failure names its reason, and only the error code is reported so no host path reaches a payload. The same code was inherited verbatim by the hygiene scan and is fixed in both. A symlinked ignore file is refused rather than followed, and the read is size-capped.
A declared search window was silently voided under query expansion, and the search returned nothing. Detection read the text after expansion, whose tokenizer destroys the field grammar, while stripping still ran on the original - so the window vanished and its fragments became required terms. The expansion path had no test at all, which is why it shipped.
A scan preview hid every routed marker. The whole routed branch sat behind the write guard and the counters only counted the older kind, so
--dry-runreported zero pending work while writes were queued and the next real run performed them. Preview and write now share one planner, and a test runs both over the same fixtures and compares refusals so they cannot drift apart on what is acceptable.A skill re-declared under a name a human had already rejected or accepted vanished: nothing counted, nothing reported, and the marker was consumed with its pointer aimed at a file whose body differed from the one just declared. The outcome is named and reported, and the marker is not consumed - the queue stored nothing, so the marker is the operator's only copy of that body.
A repeated single-valued optional field in a marker was silently discarded while a required one was correctly refused, so a signal declaring a scope twice was written unscoped and unroutable. Both are refused as ambiguous now; the inline shape had the same drop before this release and is fixed too.
An inherited work identity never expired, so every later session in a directory chained onto the previous one and lineage depth grew without bound. The bound applies to the inherited rung only: an explicit declaration by something live still re-attaches after a month, and the marker directory is capped in the same idiom the lineage ledger already uses.
The response-shape descriptor for research reports accepted blank strings the parser it replaced had rejected, writing a report whose citations rendered as empty links. Descriptors can now require a non-blank string, with the replaced parser's own wording.
The gap-task directory had no bound: its topic space used to be a fixed set of telemetry codes and the new source makes it arbitrary normalised query text. Closed tasks past a retention window are pruned and minting past a cap is refused, both reported.
Two different opt-ins gated one feature - a configuration flag on one surface and a per-call argument on the other, so the demand log wrote when it should not and stayed silent when it should not. One key now gates both.
The rolling-cap test in the demand log took five seconds against a five-second limit and would have flaked under parallel load. Its cost was append count rather than bytes, so it reaches the same byte-budget branch with 890 appends instead of 20,000 - and it now asserts that compaction actually ran, which it never did.
1.40.0 - 2026-07-27
A no-dead-ends wave: eleven units that make the system say what to do next, do it where the fix is provably mechanical, and refuse by name where it is not. Reconnaissance against the code invalidated at least one premise in eight of the ten scoped sources before a line was written, so most of this release is not new machinery but delivery of machinery that already existed and did not reach the caller. A registry of structural next commands had twenty entries and two consumers; the notice channel carried its command inside an English sentence; three repair pipelines each decided privately what they could fix; a repair planner shared a vocabulary with the detector while re-scanning the vault independently; the dream journal marked phases complete before their work began; the command manifest omitted a third of the real verb surface that shell completions are generated from; and two advisories stated things that were not true, one of them contradicting the same screen it printed on. The kernel still calls no LLM, and every new flag, argument, and configuration key is byte-identical when absent.
Added
One advisory rail for every forward pointer the command-line interface prints. It takes a diagnostic code, never a sentence, so prose cannot enter the surface, and it owns the question of whether advisory output is legal on the current stream - the twelve commands that render their own JSON receive nothing on stdout, while the envelope-wrapped family is safe because the envelope buffers. A suppressed emission still resolves, so a machine caller carries the command as a field instead of losing it. A ratchet fails the build when a forward pointer is hand-written outside the rail; four sites remain sanctioned, each with a written reason and a pinned count so a new pointer cannot hide behind a neighbour's justification.
o2b brain dream run --step <scan|heal-enrich>and a repeatable--gate <name>=<true|false>, withstepandgateson thebrain_dreamtool. The override applies to one run and never touches_brain.yaml. Any other step, including all five phase labels, is refused with an error that names the coupling literally: two planning steps mutate each other's accumulators specifically so a preference is never both refreshed and retired, and running the refresh half alone with writes enabled would write a preference the same pass had decided to retire.o2b brain skill-proposals recover [--discard-unreadable]and arecoveroperation onbrain_skill_proposals. The accept transaction added in this release could strand a vault two ways, and both now name their exit: an unparseable journal marker blocked every future accept with no operator-facing recovery path, and a crash left an accept lock that no command mentioned. The lock is not broken automatically - a live writer cannot be told from a dead one - so the refusal names the exact file and the command that inspects it.A skill contract on acceptance: prerequisites, rollback, side effects, and verification are written and read back by the procedural-memory reader, so they are a contract rather than decoration. A proposal's self-reported evidence is now resolved against the independently recorded procedural outcome ledger, joined by a key derived from the proposal's own slug that the accept path already causes to exist. Three states stay distinguishable and are never conflated: recorded successes, recorded failures, and no recorded outcome at all.
--dry-runono2b brain pending apply- the one repair path that had no preview. It runs every check the apply runs and stops before the two disk calls, and a conflict an apply would reject is now reported by the preview rather than surfacing only at write time.Ingest dedup counts as a queryable signal: a new
ingest_dedupcontinuity record with abrain_analyticsview over it. The counters existed and were reported one call deep, then discarded, so a re-ingest spike could not be seen over time. The record is dimensioned by surface and source, and a run that deduped nothing writes nothing rather than a zero row that would make a real zero indistinguishable from an absent measurement. The semantic layers are reported as what they are - nominations, never drops.A
projectscope on session digests, reusing the existing scope axis, its slug rule, and theproject_scopeargument spelling already carried by search rather than introducing a parallel dimension.An applier capability table: per finding code, whether repair is mechanical - naming the fixer - or refused, naming the reason. The three repair pipelines consume it instead of each deciding privately, and a test re-derives the population from the fixer registry and the three planners so the table and the code cannot disagree in either direction.
Machine-readable broken-link findings: the field name, the target, and the source list are carried as fields, where a consumer previously had to match the message text.
Expected-output blocks in every per-runtime install document, bound to the adapters by a conformance test that installs each target into an injected environment, calls its verify, renders through the command-line interface's own renderer, and names the document in the failure.
Ratchets that keep the release honest after it ships: a terminal-state census enumerated from the dispatchers, a manifest-completeness ratchet, a configuration-key ratchet over the generated template, a doctor-exit census, and the forward-pointer ratchet above. Each fails until a new surface is classified, and none can be satisfied by an empty reason.
Changed
Every issue
o2b brain doctorreports now carries its structural next command on the human surface and as a field under--jsonand onbrain_doctor. Where no single command resolves a class, the doctor says so instead of falling silent: an additiveno_exitobject gives the reason once per code, so an operator can tell "there is no command for this" from "the system forgot". All forty-one doctor codes are classified - three gained a genuine exit, twenty-four are excluded with written reasons - and four of them were assembled by string concatenation and existed nowhere in the source as readable literals.The command manifest gained sixty-four entries the dispatchers already handled - two top-level commands, seven search subcommands, and fifty-five brain subcommands - and
o2b helpis now rendered from that manifest instead of a parallel hand-written constant. Shell completions ando2b help --jsonwiden accordingly. The two help surfaces previously described materially different command sets.The dream workrun journal now marks a phase complete where its durable writes actually land. Three markers were previously appended before a single preference was written and four after every mutation had finished, so a crash mid-write left a journal claiming work that had not started. Emission order is now execution order rather than the reporting order, which moves one marker to the end; any consumer outside this repository treating marker position as monotonic progress should re-read it. A fifth safeguard checkpoint was added before finalize, covering the log writes, the ledger write, snapshot pruning, and digest regeneration that ran unguarded.
The repair planner derives its plan from the findings the doctor already produced, instead of re-scanning the vault independently and sharing only a vocabulary of codes with it. No hidden re-scan was kept: one finding population turned out to be a strict superset of the other, and that is recorded rather than papered over.
_brain.yamlis generated from the resolver's own default tables. Sixteen of twenty-three top-level keys resolved silently from defaults because a hand-maintained template and a set of resolver tables had drifted apart with nothing keeping them in sync. Newly surfaced keys are emitted commented, so the resolver stays authoritative and a later default change is not silently frozen into every vault created today.Repair-path guards were unified: assert at the applier's entry point, before any other work, and only when the call will write. One of the three pipelines refused dry runs and no longer does.
o2b brain initcreates the two directories it declared but never made, and the runtime notice channel carries its command as a field rather than inside a sentence a consumer had to match.The stale search-index notice was corrected. It claimed recall returns nothing without an index; the read path builds the index and retries, so recall returns late rather than empty.
--is honoured when detecting a JSON request, so a value after the terminator is a positional rather than a flag.
Fixed
A read-only verb could fail with a write-path error: the consolidation lint asserted vault identity before its apply branch, so
o2b brain actionscould refuse a pure read on a vault root whose marker differed from the pinned one.Two advisories stated things that were not true. The empty-Brain pointer fired after every initialization, including re-runs where nothing was created, and
o2b initcontradicted itself two lines apart on one screen by naming a command its own checklist then asked for again. Each now fires only where its claim holds, and a table-driven test asserts that for every occurrence of every registered terminal state.A repair item with no fixer was silently skipped and reported as success; it now raises a named error.
The profile registry silently overwrote an existing entry, performed no directory check at create time although switching did, and did a lock-free read-modify-write so two concurrent creates lost an update. All three are closed, and both concurrent creates now land.
A configuration key that gates a destructive path reached block merging but was dropped when the typed configuration was assembled, so it could not be switched on from
_brain.yamlat all, and no warning said so.Preferences created with forced confirmation wrote an absent confidence value that read back as null, which ranks below zero on the two surfaces that rank on the number. They now write an explicit zero, matching what the dream pass already does for new preferences, and the doctor check that detects this population names recording real evidence as the exit. Reported as #149. Both fixes proposed in that report were declined with measured reasons: one writes an apply-evidence row asserting a rule was applied to an artifact when it was not, which is defined in this codebase as the canonical durable signal that a preference was exercised against real work; the other cannot see the timestamp it needs, breaks two assertions, and drifts about one ten-thousandth per hour against a one-millionth no-op threshold, so every dream pass would rewrite every zero-evidence preference forever. Measured: the reported feedback loop has no code path, because the injected context applies no confidence cutoff and orders by band rather than by value, so both proposed values would have left the preference in the same position.
An emission record did not carry the code it was made for, while the batch form's own documentation promised unregistered codes come back "by name". The test covering it asserted only the outcome, so the gap was invisible from both sides.
1.39.0 - 2026-07-26
A context-integrity wave: ten units that convert a silent failure in the read and delivery path into an explicit, named one. Owner-scoped isolation was enforced on one surface out of thirteen; a context pack carried no record of the vault state it was built from; session continuation resolved ambiguity by picking the most recent candidate; the lineage ledger lost history on compaction and had no writer lock; an embedding store written against a different model, dimension, or sqlite-vec ABI returned zero or garbage neighbours with no check on the read path; a frontmatter line the parser did not understand was dropped without a trace; nothing counted broken links vault-wide, nothing measured the SessionStart injection, nothing joined retrieval receipts across a session, and a mis-resolved vault root was materialized by the first write while the doctor reported it clean. Two shared seams carry the wave: a closed-vocabulary degradation channel and a stamp-and-compare module with three-mode gates. The kernel still calls no LLM, and every new surface is byte-identical when its flag, argument, or config key is absent.
Added
Typed degradation channel: a closed
DegradationCodeunion with a notice record (code,site,path?,detail) and a one-line English renderer, carried by the report types that already exist. The union is exhaustive-switchable, so a consumer with anever-typed default arm fails to compile on an unhandled member.Stamp-and-compare seam and the
integrity:block in_brain.yaml:owner_scope_delivery(defaultoff),embedding_abi(defaultwarn), andpack_validity_seconds(default 900). An unrecognised mode or a non-positive validity raisesBrainConfigErrornaming the dotted field; an absent block resolves to the documented defaults. Underoffthe gate does not compare at all and returns no evidence.Owner-scope isolation on every content-returning surface, not only search. Preference-backed surfaces (context pack, pre-compress pack, morning brief, digest,
active.md) route through one shared collector and are gated byintegrity.owner_scope_delivery; search-backed surfaces filter whenever the caller passes a scope. An optionalagent_scopeargument joins eleven MCP tools and an--agent-scopeflag joinso2b search query,o2b search expand, ando2b brain sgrep. A table-driven test partitions all 108 tools into scoped, deliberately-unscoped-with-a-reason, and non-content, and fails until a new tool is classified.brain_search_expandon another owner's chunk now raises the samechunk not founderror as a genuinely missing chunk, closing an enumeration surface over sequential integer ids.Context pack provenance: a stamp pairing the index corpus generation with a digest over path, mtime, and size for the directories the pack walks, plus an expiry from
integrity.pack_validity_seconds. The anticipatory cache stores both and refuses a drifted or expired entry as a miss naming the reason; its schema version bumped so pre-existing cache files read as misses.Embedding ABI detection: the sqlite-vec version joins the model and dimension already stamped in
index_state, and the three are compared on the read-mode open, where previously nothing checked.o2b search checkreports drifted fields with a copy-pasteable fix, ungated. Nothing is ever rebuilt automatically - the existing self-heal path rebuilds keyword data only and would leave stale vectors in place.Frontmatter diagnostics: a note whose frontmatter uses grammar the line scanner does not support still indexes, and now leaves a trace.
o2b search indexreportsfrontmatter_notices,o2b brain doctorreportsuncertainentries, each naming the file, the line number, and the offending text. The index run reports files it re-read on that run; the doctor sweeps the whole Brain tree and is the standing report.Vault-wide broken-link ratchet: a deterministic count of link rows the read-time resolution ladder cannot resolve, sharing that ladder's SQL from one constant.
bun run link-ratchet:checkis wired into both CI workflows and exits 1 on a rise, namingbun run link-ratchetas the fix. The committed ceiling inlink-ratchet.jsonmeasures thetemplates/brain-starterrepository template, not any operator's vault, and encodes the link rot present there today.SessionStart injection meter: one receipt per injection with per-source byte and token attribution for the runtime-notice block, the budgeted
active.mdbody, and thelessons.mddigest, measured after the fail-open loader returns, together with the loader source, the hook event, and the char budget each sub-body was charged against. The meter runs after the context is emitted and inside its own try/catch.Session-scoped retrieval receipt summary: a third
summaryoperation onbrain_context_receiptsfolding existing records into receipt count, item total, distinct items, and per-item injection frequency and token cost. Receipt emission is opt-in, so a window with no receipts returns{recorded: false}carrying no counters at all - the return type is a discriminated union, so "nobody enabled receipts" can never be read as "retrieval injected nothing".Fail-closed session continuation: the crutch resolver returns a discriminated outcome - linked, or an abstention naming
no-candidate,self-known,no-workspace,ambiguous,stale, orevidence-missing- instead of a barenull. Two concurrent sessions in one directory inside the freshness window both abstain where one was previously stitched to the other. Repo, branch, and commit joincwdas required-match predicates, captured through boundedgitinvocations; a remote carrying credentials canonicalizes to the same identity as the same repository cloned without them, and the credential never reaches a recorded field.Lineage ledger integrity: sequence numbers and a hash chain, appends under a writer lock, and compaction that retains recent lines verbatim instead of folding one summary per session - so
firstSeenMs, per-session event history, and any field a later writer adds all survive. A dropped observation is recorded as a named gap inBrain/.state/session-lineage-gaps.jsonland excluded from continuation matching, so a lost write can never become a false stitch. A tampered, truncated, or unreadable ledger is reported by verification and still resolves; the read path stays fail-soft.Vault identity guard:
o2b brain initstamps an idempotentBrain/vault-id.jsoncarrying an opaque id and no absolute path. Brain writers compare the root's marker against the identity the process pinned on its first write - a present-and-different marker refuses with a named error ahead of the first byte, an absent one warns through the degradation channel and proceeds. Of 106 write-capable modules, 97 carry the guard and 9 are excluded with a stated reason, measured by a census test that names any unguarded module.
Changed
o2b brain doctorreports aBrain-less root as a namedbrain-root-absentwarning with anuncertainentry recording that every check below was skipped, instead of certifying it clean.o2b brain doctor --stricton such a root now exits 2 where it previously exited 0.vault profile switchrefuses a profile whose vault is not an existing directory, so activation fails at switch time instead of in a later process. A profile can no longer be switched to before its directory exists.The lineage ledger's compaction retention changed from the most recent 256 sessions to the most recent 256 lines, so a chatty session ages other sessions out sooner. The writer lock has no retry: concurrent sessions on one vault can drop lifecycle observations, each named in the gap sidecar rather than lost.
Twelve MCP tools gained input arguments -
agent_scopeon eleven, andsince,until,max_receiptsplus a widenedoperationonbrain_context_receipts. No tool was added or removed (the count stays 108) and no output schema changed.brain_hygienegains an additive top-levellink_integritykey. It refuses to produce a number unless the index's last run was a forced full pass, reportingmeasured: falsewith reasonpartial-resolutionrather than a misleading zero.Retirement keeps ownership.
BrainRetiredcarries anownerfield and every retired-page surface applies the same visibility predicate as an active preference - retiring a memory withdraws it from the active set, it does not publish it.Five
brain_briefviews that cannot honour a scope (daily,weekly,monthly,operator,today) now reject an explicitagent_scopewithINVALID_PARAMSnaming the view, rather than accepting and ignoring it. The server-identity fallback is unaffected.
Fixed
Ownership resolution fails closed. A page whose frontmatter cannot be read, and a page whose
owner:is present but not a plain string (a block-style or inline YAML list, or a nested mapping - the shape Obsidian Properties writes), are withheld from a scoped caller instead of being treated as shared.A per-request scope no longer drives a shared write:
brain_contextfilters what it returns at render time and leavesBrain/active.mdcomplete, and the anticipatory cache path keys on the enforced scope, so a warm cache written by the SessionStart hook is found by the MCP reader under the default gate.The vault write guard runs ahead of the first byte everywhere. The secrets store, the schema mutator, page archiving, hygiene apply, the pinned-context batch path, tension transitions, gap auto-close, and snapshot pruning previously wrote or deleted before their assertion ran.
An unreadable
_brain.yamlno longer silently disables an isolation gate: an absent config resolves to defaults, while a present-but-unreadable one falls back to the strictest mode and surfaces abrain_config_unreadablenotice.The link-ratchet gate no longer passes on a subject that measures nothing: an empty or fully-excluded subject is an explicit unmeasurable state rather than a ceiling of zero.
Four source files carried literal NUL bytes that made git treat them as binary and made
grepskip them silently.
A semantic-health baselining release (#147): an optional acknowledge-before watermark lets an operator declare "I have seen and accepted the state up to this date", so a one-time bulk seed of preferences no longer pins the health verdict at watch forever through the concept-gap and batch-concept-inflation advisories. Detection and storage are unchanged; the watermark only filters which findings are surfaced, the verdict is computed from the surfaced set, and suppression is always reported explicitly. With no watermark set, every output is byte-identical to 1.37.0.
Added
Semantic-health baseline watermark: an optional
health.silence_beforedate (date-only or full ISO-8601 timestamp) in_brain.yamlsuppresses advisory findings whose underlying entries are entirely older than the watermark - abatch-concept-inflationburst whose window ended before it, aconcept-gapterm whose corpus mentions all predate it. Conservative by construction: an entry with no parseable timestamp counts as newer than any watermark, and a concept-gap term with even one post-baseline mention surfaces with its full frequency. An invalid value is an explicit config error, never silently ignored.o2b brain health-baseline set <date>|now,get, andclearrecord, show, and remove the watermark without hand-editing_brain.yaml; the upsert preserves unrelated config content byte-for-byte under the repository's file-lock plus atomic-rename convention. Usage errors exit 2 and--jsonoperational failures emit{ok:false}envelopes.Explicit suppression reporting: whenever the watermark hides at least one finding,
o2b brain healthprintssuppressed: N finding(s) older than baseline <date>and the report carries an additivesuppressedobject with per-detector counts and the baseline date, mirrored on MCPbrain_healthassuppressed: { concept_gaps, batch_inflation, baseline }; the field is absent whenever nothing is hidden.
Changed
The four detector-local ISO timestamp parsers (stale-claim, batch-inflation, thesis, and the new baseline code) consolidated into one shared
iso-timehelper.
1.37.0 - 2026-07-19
A retrieval quality and context delivery wave: nine units that make search answer relationship-shaped and summary-shaped questions deterministically, explain and plan retrieval without touching live policy, and put the right context in front of the agent while stale operational state stays out. Relationship queries traverse typed edges as a fourth RRF arm, dedup and search respect composite scopes, operational state moves to an overwrite-only lane with a retrieval-time staleness barrier, prompts gain a cadence-controlled navigation tier, an opt-in strict hook redirects the first raw vault read to the search index, and codegraph partnering covers every project in the workspace. Two shared seams carry the wave: a composite scope-key module and an index-admission predicate. The kernel still calls no LLM, and every new surface is byte-identical when its flag or param is omitted.
Added
Typed-edge relational retrieval arm: a vocabulary-driven relational-query parser (schema-pack
link_typeswith subset validation, no natural-language word lists) and bounded depth-2 typed-edge fan-out join hybrid search as a fourth RRF arm behindsearch_relational_arm_enabled(envOPEN_SECOND_BRAIN_SEARCH_RELATIONAL_ARM, default off, byte-identical when disabled); RRF and dedup keys carry source identity through the sharedrrfKey, and the query cache scopes by canonical source-set key.Composite scope keys with per-scope dedup and search scoping: one shared module owns the source-identity and owner/session/project scope vocabulary; page dedup folds the scope in additively (scopeless writes keep their existing keys, reruns over old rows collapse nothing new), and
brain_searchgains optionalsession_scopeandproject_scopefilters that leave unfiltered results byte-identical.Exact-state lane with a retrieval-time staleness barrier:
o2b brain state set|get|list|clearmaintains an overwrite-only, per-aspect operational-state lane underBrain/state/, excluded from FTS, vector, and graph layers by a new index-admission predicate (which defaults to admit, so no existing content leaves the index), while a retrieval barrier drops superseded exact-state rows from recall;Brain/pinned.mddeliberately remains a searchable scratchpad, and the lane is the supported home for current-state values.Deterministic summary-search router: query plans carry a
surfaceverdict that routes structurally summary-shaped questions (source-targeted, artifact-kind, summary-typed pages) to the summary surface; non-summary routing stays byte-identical, the verdict never enters the plan hash or ranking, and the retrieval skill names the intended surface explicitly.Per-store reranker fit check:
o2b search rerank-fitsamples recorded demand-log queries, computes the correlation between reranker scores and the base retrieval signal, and reports fits, out-of-domain, or inverted verdicts with a disable-or-swap recommendation; strictly read-only (probes disable cache and self-heal) and explicitly inapplicable on rerankerless vaults.Shadow-only retrieval plan advisor: the
brain_retrieval_planMCP tool composes query intent and weights, impact-per-token density allocation, the calibrated token-impact ledger, and observed route latency into a read-only per-question plan with source strategy, token-budget allocation, graph-expansion advice, reliability with p95 latency, and a marginal-value stop that tightens with latency; the tool exposes no mutating parameters.Tiered context injection with a cadence-controlled navigation tier: an additive navmap (top hubs by link degree plus vault size, neutralized and fenced as untrusted) injects on
UserPromptSubmitonly when the cadence is due, behindnav_tier_enabled(envOPEN_SECOND_BRAIN_NAV_TIER_ENABLED, default off) withnav_tier_cadence_minutes(envOPEN_SECOND_BRAIN_NAV_TIER_CADENCE_MINUTES, default 30); every inclusion decision is audited with its reason and added character count, and the every-turn kernel stays byte-identical.Opt-in strict PreToolUse orientation hook: with
hook_strict_enabled(envOPEN_SECOND_BRAIN_HOOK_STRICT_ENABLED), the first raw vault-file read of a session is denied once with a redirect naming the brain search surface, then downgrades to a soft nudge; any brain query or search refreshes a short-lived orientation stamp that suppresses the block; detection is structural (the tool call's path resolving inside the vault root), and every failure path fails open.Workspace-wide codegraph partnering:
checkCodegraphaggregates status across every discovered code project and threadsproject_pathper query when the partnered codegraph supports it (feature-detected from the status usage text), degrading to the previous single-project behavior with an explicit note when it does not; single-project workspaces are byte-identical.
Changed
Hook stamps (
osb.nav_tier.*,osb.oriented.*) live in a minimal per-session file-backed store under.open-second-brain/hook-state/with epoch-ms expiry; missing, malformed, or expired stamps read as absent and never throw.The MCP tool surface grows from 107 to 108 tools (
brain_retrieval_plan).
1.36.0 - 2026-07-19
A knowledge intake and consolidation wave: eight units that carry content from the operator's pocket into the vault and turn what accumulates into trustworthy insight. Phone captures arrive through an inbound Telegram bot and drain through a report-then-apply routing pass, keyed web-research providers feed the citation-constrained pipeline, facts roll up into higher tiers on pure counters, entities gain diarized profiles with a stated-vs-evidenced gap, synthesis findings carry causal context and decomposed confidence behind an evidence-identity gate, the link graph gets a capped idempotent repair lane with efficacy holdouts, and skill proposals pass a deterministic verifier before reaching the pending queue. Two shared seams carry the wave: a capture-note contract and a keyed external-fetch helper. The kernel still calls no LLM, and every new surface is byte-identical when unused.
Added
Inbound Telegram capture bot (
o2b brain telegram-capture run | catchup): an explicit long-polling runner (fetch-based getUpdates, no new dependencies) gated bytelegram_bot_token(envTELEGRAM_BOT_TOKEN) and a chat-id allowlisttelegram_chat_allowlist(envTELEGRAM_CHAT_ALLOWLIST, empty accepts nothing); each accepted message becomes one staged capture note through a new capture-note contract (kind, provenance, capture timestamp) underBrain/captures/; rejected or malformed updates log one decision each to a capture-decisions sidecar;catchupreplays captures since the last acknowledged one; a missing token is a typed startup error.Inbox-drain pass (
o2b brain inbox-drain [--apply]): walks staged captures through the contract, classifies each structurally (URL-shaped body as source reference, explicit obligation marker as task, otherwise atomic idea), routes on apply (source ingest, note create incaptured/, obligation open), archives processed captures, and reports every item with action and reason; dry-run is the default and writes nothing; rerun after apply is a no-op.Keyed web-research providers plus full-page extract: Brave and Tavily join the research pool only when
BRAVE_API_KEYorTAVILY_API_KEYis set, through a shared external-fetch helper (env-gated, Bearer or named-header auth, typed network/auth/http/payload errors, response cache keyed by normalized request, keys never in cache keys or error text); a keyless pool reports itself empty and stays byte-identical; the extract step feeds verbatim page text into the existing citation-constrained pipeline.Count-triggered fact rollup ladder in the dream synthesize phase: when new facts at a tier reach the threshold (optional
rollup:config block withfact_thresholdandidentity_threshold), one needs-llm-step rollup envelope is emitted and the counter reset is recorded in the dream report over a persisted ledger (Brain/rollup-ladder.json); rungs compose from facts to rollups to the identity tier; below threshold the dream output is byte-identical.Subject diarization (
o2b brain diarize, MCPbrain_diarize): assembles an entity's document set from the registry and ingested source pages, computes a stated-vs-evidenced section deterministically (stated claims versus evidence frequency and recency, each line carrying an evidence identity and classified stated_corroborated, stated_unevidenced, or evidenced_unstated), and emits a profile skeleton plus one needs-llm-step envelope; unknown entities are a typed error.Synthesis findings hardening: every deep-synthesis finding carries an additive causal-context field and decomposed confidence components (support, opposition, freshness, coverage, all deterministic); findings without an evidence identity are excluded and counted with reasons; the exported evidence-identity type and predicate are shared with diarization; the steelman seed selection is unchanged; both the CLI
--jsonpayload and the MCP structured content expose the new fields additively.Memory-graph repair lane (
o2b brain repair-lane [--apply --confirm]): candidate edges ordered by identity strength (explicit references, session continuity, same-topic evidence; inferred candidates opt-in), gated by a confidence threshold and a hard per-run write cap, dry-run by default, apply requires the exact confirmation phrase, and idempotent forward-scan batching makes reruns converge to zero writes; the paired holdout harness measures graph lift separately from direct recall and fails the gate on any dangling or unhydrated target.Skill-proposal verification and evolution: a deterministic pre-promotion verifier validates each draft against its own supporting records before it reaches pending (rejections recorded with reasons in a ledger), accepted skills carry a version that increments on evolution, and a same-name collision merges support instead of forking; the human accept and reject flow is unchanged and legacy proposals default to version 1.
Changed
The MCP tool surface grows from 106 to 107 tools (
brain_diarize).Deep-synthesis reports gain additive fields (causal context, decomposed confidence, excluded-finding counters) on the core report, the CLI JSON payload, and the MCP structured content; prior fields and consumers are unchanged.
1.35.0 - 2026-07-19
A trusted recall and memory write surface wave: ten units that carry vault knowledge to the agent at the moment it matters and make every step accountable. An opt-in bounded recall brief lands on each prompt, retrieval zero-ranks quarantined material and hands back receipts naming every exclusion, a supersede relation on a successor fades the superseded note without editing it, and MCP agents gain the full atomic note lifecycle: update, append, and all-or-nothing mixed batches. Two shared kernels carry the wave: a deterministic retrieval rank-adjustment stage and an atomic multi-operation write core. The kernel still calls no LLM, and every new surface is byte-identical when its flag or param is omitted.
Added
Bounded recall inject on UserPromptSubmit: with
recall_inject_enabled: "true"(envOPEN_SECOND_BRAIN_RECALL_INJECT_ENABLED) a hook relevance-recalls a small brief of vault notes into each prompt under hard caps (4 notes, 900 chars, a fixed time budget), abstains below a confidence floor, injects nothing on any internal error or timeout, and writes one audit line per decision (inject, abstain, error) recording counts and scores, never content. The brief is fenced as untrusted content with neutralized titles; the hook process itself stays fail-open for the session.Retrieval trust gate plus per-pack receipts: with
search_trust_gate_enabled: "true"(envOPEN_SECOND_BRAIN_SEARCH_TRUST_GATE), material carrying quarantine signals (self-approval quarantine state, untrusted-source provenance, entity-contamination marker) is zero-ranked out of both semantic and lexical search through a new deterministic rank-adjustment stage, and every search outcome carries two compact-reference receipts,memory_trust_assessmentandretrieval_decision_trace, counting each exclusion with its reason so nothing is dropped silently.Relation-only supersede fade: with
search_supersede_fade_enabled: "true"(envOPEN_SECOND_BRAIN_SEARCH_SUPERSEDE_FADE), an inboundsupersedesorsuperseded_byrelation authored on a successor fades the unchanged superseded note by a named multiplier on both search paths, so deprecating a note takes one edit on its replacement; the existing tombstone drop is untouched.brain_update_noteandbrain_append_noteMCP tools: update an existing note's body and/or merge frontmatter keys, or append to its body, under the exact create-note safety envelope (path traversal, Brain machinery root, vault-scope exclusions), with atomic write semantics and a typed error when the target does not exist.brain_write_batchMCP tool: an ordered mixed batch (create note, update note body or frontmatter, append note, apply evidence, append log line) validated and projected in memory first, then committed all-or-nothing; the first invalid operation aborts with a typed error naming its index and nothing touches disk; a duplicate note target within one batch is refused.include_rawon session recall (brain_session_grep): responses carry the original raw capture inline beside each derived record, and every item is stamped with anextractedboolean discriminator (derived versus raw); an optionalraw_budget_charsclips raw payloads while identity fields survive.Knowledge-gap loop: with
gap_loop_enabled: "true"(envOPEN_SECOND_BRAIN_GAP_LOOP_ENABLED, threshold viagap_loop_threshold), recurring recall gaps aggregated from existing gap-count telemetry promote at session end into durable task notes underBrain/gap-tasks/(stable-key dedup, never the kanban board), open gap tasks render as a session-start agenda, and a later confident recall on the topic auto-closes the task; gap tasks never match their own text.Typed age-labeled session-start timeline: the morning brief renders recalled items as one chronological Recent activity timeline with a per-item structural type marker and relative age label built by a pure rendering helper; the underlying data arrays are unchanged.
Clip-protected pack identity:
agent_idjoinssession_idon pack and continuity identity, and the newclipPayloadToBudgetprimitive retains both fields under any output-budget truncation while dropping non-protected keys.Fail-fast doctor readiness probes:
o2b doctor --readinessruns three functional probes (model-inference key resolvable, embedding provider loadable with model and dims, runtime-adapter wiring) with per-check timeouts and outcomes pass, fail with a reason, or skipped-not-configured; any failure exits non-zero, and without the flag doctor output stays byte-identical.
Changed
The MCP tool surface grows from 103 to 106 tools (
brain_update_note,brain_append_note,brain_write_batch);brain_session_grepgains optionalinclude_rawandraw_budget_charsparams and stays byte-identical when they are omitted.Search ranking flows through a deterministic rank-adjustment stage (
src/core/search/rank-adjust.ts); with no adjusters registered the stage returns its input unchanged.
Fixed
The temporal-replace lifecycle test derived its expected log day from the wall clock and failed on any date after its authoring day; it now derives the day from the operation result.
1.34.0 - 2026-07-18
A source pipeline integrity and operator tooling wave: eleven units that make vault intake trustworthy end to end (scoped and gated discovery, reconciled dispatch, deterministic pre-extraction, citation provenance), extend the query surface (configurable FTS tokenizer, graph-degree predicates), and give the operator a repairing doctor plus one consolidated status snapshot. Two shared kernels carry the wave: a gitignore-semantics path-scope engine and a diagnostics-signal registry whose next-command hints travel with the issue definitions. Everything stays deterministic (the kernel calls no LLM) and every new surface is byte-identical when unused.
Added
Nested
.gitignoresupport in the hygiene file scan through a new path-scope engine (src/core/fs/ignore.ts): root and nested ignore files compose with git semantics (a deeper file scopes its own subtree, a nearer!re-include wins,.git/info/excludeparticipates); repositories without ignore files scan exactly as before and malformed patterns warn explicitly.Source ingest scoping:
--src-subpathand--excludeono2b brain batch-plan(andsrc_subpath/excludeonbrain_ingest_batch_plan) restrict a monorepo ingest to one subtree with gitignore-style excludes through the same shared engine; a subpath escaping the source root rejects with a typed error.The schema
extractableflag is enforced during page discovery: with a non-empty allowlist, pages whoseschema_typeis not listed are skipped before extraction, reported with a reason on the plan (skipped_non_extractable) and logged; an empty allowlist keeps today's behavior byte for byte and untyped pages are never gated.Deterministic no-LLM code-structure pre-extraction (
o2b brain pre-extract,pre_extractonbrain_ingest_source): classes, functions, imports, and inheritance edges from TypeScript, JavaScript, and Python sources as JSON entity/edge seeds handed to the agent ingest step; unknown languages reportextracted: falsewith a reason, never a fake empty success.Dispatched-vs-ingested reconciliation (
o2b brain batch-plan --reconcile,reconcileonbrain_ingest_batch_plan): diffs a plan's dispatched set against checkpoint completions and names every silently-lost source; a complete plan reports an explicit empty gap; read-only and idempotent, a report rather than a retry.Inline citation promotion (
o2b brain scan-citations): structural[Source: <name>, YYYY-MM-DD]markers in note prose become datedsource-citationevents on the temporal timeline, stamped at the citation date and deduplicated on normalized name plus date; malformed markers are reported and skipped (--strictexits nonzero).Configurable FTS tokenizer:
search_fts_diacritics(0 | 1 | 2) andsearch_fts_stemmer(none | porter) assemble the FTS5 tokenizer clause from validated allowlists; invalid values reject with a typed error naming the allowed set, changing the config requires an explicito2b search reindex, and the CJK trigram path is untouched.Graph-degree cardinality predicates in the search filter DSL:
--degreeono2b search(anddegreeonbrain_search) filters by backlink and outlink counts with=,!=,>,>=,<,<=(orphansbacklinks=0, hubsoutlinks>=N), backed by directed degree data on the existing link-graph index.Doctor repair mode (
o2b brain doctor --repair,repair/applyonbrain_doctor): previews targeted fixes for doctor-detected issue classes (dangling workrun checkpoints, dead evidence links) without writing;--repair --applyperforms them idempotently and logs one typeddoctor-repairevent per fix; structural lifecycle links and append-only audit history are reported needs-review or unfixable rather than half-fixed; plain doctor and--strictstay read-only.Unified operator status snapshot (
o2b brain status, MCPbrain_status): doctor, semantic health, hygiene, stale scan, review queue depth, active profile, and state-file health in one readable view; every problem line carries the exact next command supplied by the shared diagnostics-signal registry; a healthy vault prints a compact all-clear.Clean exit on early-closed stdout pipes:
o2b ... | head(andvault-log) exits 0 on EPIPE, while any other stdout error now fails loudly instead of being silently swallowed.
Changed
The MCP tool surface grows from 102 to 103 tools (
brain_status);brain_ingest_batch_plan,brain_ingest_source,brain_doctor, andbrain_searchgain the optional params listed above and stay byte-identical when the params are omitted.The hygiene scan replaces its static skip-dir list with gitignore-aware composition via the shared path-scope engine.
Doctor diagnostics flow through a shared diagnostics-signal registry (issue class, detector, optional fixer, next-command hint) consumed by both
doctor --repairando2b brain status.
1.33.0 - 2026-07-18
A belief lifecycle and decision memory wave: eleven units that give memories a full lifecycle (tombstone, supersede, temporal replacement, tension triage) and make decisions first-class, auditable citizens of the brain (records, ratings, commitment tiers, change receipts, governed recall). Everything is deterministic (the kernel still calls no LLM), every mutation is logged or receipted, and vaults that do not opt in behave byte-identically.
Added
Cross-type tombstone and supersede lifecycle (
o2b brain lifecycle, MCPbrain_lifecycle): idempotent soft delete via frontmatter (_status: tombstoned, reason, timestamp, optionalsuperseded_by) across preferences, signals, and learnings; tombstoned and superseded-non-tip entries leave recall, inject, andBrain/active.mdbut stay on disk for audit; curator slices list injected-never-used, contradicted, and high-used memories from observed-use verdicts.Atomic temporal fact-replacement (
o2b brain lifecycle temporal-replace): closes the predecessor and opens the successor at one shared instant with half-open[valid_from, valid_to)intervals, whole-day semantics for date-only facts, and both-or-neither write atomicity; the successor link reusessuperseded_by.Persisted claim-graph query surface (
o2b brain claims, MCPbrain_claims): one bounded, deterministically rebuildable projection (Brain/claim-graph.json) over existingsuperseded_by/contradictsrelations and bi-temporal validity, answering current truth, truth-at-instant, what-replaced, and what-contests; current-truth by default, history opt-in.Supersedes-chain consumer policy: context packs inject only chain tips under budget (an explicit historical flag keeps the chain), recall annotates superseded hits with a pointer to their replacement, and the dream pass accelerates decay of low-recall superseded ancestors (
chain-decayevents, named threshold constants).Decision-record note family (
o2b brain decision, MCPbrain_decision):type: decisionnotes underBrain/decisions/with chosen, assumption,review_date(opens exactly one review obligation, idempotently), outcome backfill, optional pre-mortem, and similar-decision recall with recorded outcomes.Rated decision capture:
rating(1-5) plusrationaleround-trip through decision frontmatter,list --ratedsorts by rating,comparereads decisions side by side; rated decisions stay out of signal and preference recall.Commitment tier vocabulary: optional
commitment: exploring | leaning | decided | lockedon preferences, theses, and decision records; injected text renders the tier label in place of the raw confidence float when set and is byte-identical when unset.Decision-change receipts:
decision_change.v1JSONL alongside the truth ledger recording before, after, evidence, confidence delta, alternatives, actor, and reason code with durable idempotency keys; emitted on decision record/outcome/rating, lifecycle tombstone/supersede, and material confidence-band drops;o2b brain decision historypages the trail with an opaque cursor and normalized subject matching.Rated-decision recall governor: deterministic token-overlap prompt matching resurfaces rated decisions verbatim under
decision_recall.max_per_sessionanddecision_recall.min_spacing_turns(config plus env twins); disabled and byte-identical when unset, with governor state flags on both CLI and MCP.Conversation chronology: session imports stamp
authored_aton turn signals, the search index (schema v10) carries and returns it (authored_atin CLI JSON,authoredAtin MCP results), exact hybrid-score ties break toward more recently authored content,session-grepaccepts--since/--before, ando2b brain authored-at-backfillupgrades existing vaults idempotently (dry-run default, no re-embedding).Tension objects (
o2b brain tension, MCPbrain_tension): detected contradictions persist underBrain/tensions/with a deterministic dedup key and an open -> confirmed/dismissed/resolved state machine; re-detection refreshes instead of duplicating; context packs warn when a subject note of an unresolved tension is injected.
Changed
The MCP tool surface grows from 98 to 102 tools (
brain_lifecycle,brain_claims,brain_decision,brain_tension);brain_session_grepgainssince/beforeparameters andbrain_searchresults exposeauthoredAt.Search store schema advances to v10 with a nullable
authored_atcolumn; existing indexes migrate in place and documents without a turn instant are unchanged.Context-pack reports gain an additive
warningsarray (unresolved-tension notices) and items carry optionalcommitment; packs without tensions or tiers stay byte-identical.
1.32.0 - 2026-07-18
A memory write-path integrity and store safety wave: eleven units across four subsystems that make every durable write choke point validate its input, every destructive vault operation snapshot first, and every embeddings failure surface as a typed, actionable outcome. All gates are deterministic (the kernel still calls no LLM), every rejection is a typed error or a logged, visible skip, and no new dependency is added.
Added
Vector validity gate. A new
EMBEDDING_INVALID_VECTORerror rejects non-finite (NaN/Infinity) and all-zero vectors at the store choke points (vecUpsert,semanticTopK) and in unit normalisation, so a broken embedder can no longer poison cosine distances silently. Previously only the dimension was checked and a NaN vector inserted cleanly.e5 instruction prefixes at index and query time. Instruction-tuned embedders get the prompts they require: the embed path now distinguishes
queryfrompassageinputs, the multilingual-e5 preset carries"query: "/"passage: "defaults, andembedding_prefix_query/embedding_prefix_passageconfig keys (with env twins) override them; an explicit empty string disables. The active prefix pair is persisted in index meta, and changing it surfaces the same reindex-required path as a model change, so prefixed queries are never compared against unprefixed stored vectors.Embeddings quota classification. HTTP 402, and 429 responses carrying provider quota tokens (for example
insufficient_quota), now classify as a dedicated non-retriableEMBEDDING_QUOTA_EXHAUSTEDoutcome with an actionable message (quota or billing exhausted, semantic search degraded, check provider billing) instead of folding into a generic HTTP failure with blind retries. The classifier reads structured status fields on the error, not message strings.Retry-After honoring and graceful semantic degrade. Rate-limit and transient retries honor the provider's
Retry-Afterheader (bounded by a 30-second cap); quota outcomes fail fast with zero retries; implicit semantic search degrades to the lexical lane with a warning that names the failure category, while explicit--semanticrequests keep throwing the typed error rather than silently falling back.Snapshot-before-destructive-write gate. A shared
withDestructiveSnapshotwrapper over the existing tar+zst snapshot engine now frontso2b brain forget-source --confirm(previously an unrecoverable bulk delete) and the new entity prune. The snapshot is taken before any deletion, the run id and archive path are reported as the recovery point, and dry runs take no snapshot.Entity label quality gate and prune.
sanitizeEntityLabelstrips surrounding Markdown decoration and punctuation before normalization at every label-intake boundary, and a structural validator (no letter or digit in any script, empty after stripping, over-length, or on the operator-suppliedentities.label_denylist) rejects junk labels: a typed error at creation, a logged skip at anchoring.o2b brain entity prunelists malformed historical nodes (dry-run default) and on--confirmremoves them and their edges behind the snapshot gate; a newentity-label-malformeddoctor lint surfaces candidates. Identity keys for all currently-valid labels are byte-stable.Deterministic durability gate for extracted facts. A structural classifier (temp paths, progress counters, run-id and timestamp shapes, measurement-token dominance, exit-status shapes; extendable via
durability.denylistregexes) rejects transient operational content before it persists as durable memory. Rejections are counted and logged asdurability-skipevents, never dropped silently; operator feedback writes are not gated. The classifier ships zero built-in natural-language word lists.Opt-in write-approval queue. With
write_approval.enabled: true, extracted signals stage intoBrain/pending/instead of landing directly in the inbox;o2b brain pending list | apply <id> | reject <id> --reason <text>reviews the queue. Apply moves the unchanged document (anchors and dedup hash preserved) into the inbox; reject moves it toBrain/retired/with the reason. Default off preserves the direct-write behavior byte for byte.Write-time conflict advisory. Recording feedback whose principle closely resembles a confirmed same-scope preference now returns an advisory naming the conflicting preference id and similarity evidence, in both the
brain_feedbackMCP response and the CLI output, and logs awrite-conflict-advisoryevent. The write always proceeds; the advisory tightens the feedback loop that previously waited for the next health pass.Signal retire lifecycle.
o2b brain signal retire <id> --reason <text> [--superseded-by <id>]moves an inbox signal toBrain/retired/with retire frontmatter (_status,retired_at,retired_reason, optionalsuperseded_by, old-id alias), mirroring the preference retire conventions, so an outdated extracted fact can be superseded without deleting its history. Retired signals leave the dream-pass intake but stay queryable.Store hardening. A
harden-permissionsauto-safe remediation step (idempotent, step-capped, dry-run first) tightens existingBrain/files to owner-only modes; asymlink-escapedoctor lint reports vault-internal symlinks resolving outside the vault root.
Changed
MCP responses no longer expose absolute host paths by default. The
vault_pathfields returned by the core MCP tools now carry a stable opaque reference (vault://<hash>) instead of the raw filesystem path, because tool responses land in model context. Setexpose_host_paths: true(orOPEN_SECOND_BRAIN_EXPOSE_HOST_PATHS=true) to restore the raw value.Zero-norm and non-finite vectors now throw during normalisation.
unitNormaliseInPlacepreviously returned an all-zero vector unchanged on zero norm and propagated NaN silently; both cases now raise the typedEMBEDDING_INVALID_VECTORerror, because a silent all-zero embedding is a misleading no-op.
1.31.0 - 2026-07-17
A today-operator-surface release: one read-only dashboard answering "what matters right now", a chronologically merged activity timeline, prose-native open loops, and a guarded write-back that turns prose markers into schema-validated frontmatter mutations. Every read surface is live-derived on demand and never stored; the single write path is opt-in, deterministic, and audited. No new dependency; the kernel still calls no LLM.
Added
Today dashboard (
o2b brain today,brain_brief view=today). A read-only, multi-section operator overview: due and overdue obligations (reusing the obligation store's own due math), open loops, a recent-activity timeline, and totals. Every section is an independent live query re-derived on demand - nothing is stored, so nothing drifts. Sections are fault-isolated: a section that fails to compute reports an explicit per-section error entry while the rest still render, never a silent healthy-looking blank. Text or--json;--lookback-daysand--limitbound the activity window and both accept0(empty activity section) consistently across the core builder, the CLI, and the MCPview=todayhandler.Merged chronological activity timeline.
buildActivityTimelinerenders the Brain log'sTimelineIndexas one newest-first typed bullet list across all event kinds -- [<kind>] <field>=<value> pairs · <relative age>- instead of the section-grouped brief. Tags are the event-kind strings themselves and text derivation is a fixed structural field-priority walk, so the surface stays language-agnostic with no display-name table.Open-loop prose markers with survive-until-closed semantics. The
@osbmarker grammar gains aloopkind:@osb loop <text>(optionalid=) keeps an informal intention visible on the dashboard until a structural close token@osb loop close id=<id>appears anywhere in scanned prose. Loop ids derive deterministically from normalized text (sha256 prefix) or an explicitid=; loop markers are never consumed by any scanner; duplicates and orphan closes are reported, not guessed away. Lighter-weight than an obligation - the gap between formal cadence commitments and one-off notes.Guarded marker write-back (
o2b brain apply-markers). The grammar also gains asetkind:@osb set note=<target> field=<field> value=<value>resolves its target fail-closed (a[[Title]]resolves by Obsidian basename semantics; zero or multiple matches list candidates instead of guessing; paths pass the vault-containment check) and applies the mutation through the existing schema-pack-validated attribute path. Report mode is the default and writes nothing;--applyrequires the new opt-inguardrails.marker_writebackflag (default off), emits oneattribute-writeBrain log event per applied mutation carrying the prior value, and consumes exactly the applied markers so a re-run applies nothing. Per-marker isolation: one failing marker never aborts the rest. A mutation that lands but whose post-write audit or consumption step fails is reported honestly asapplied-unconsumed(never a cleanapplied) with the cause; the CLI renders it, adds anunconsumedcount, and exits non-zero so the operator can reconcile the still-live marker before a retry re-applies it.
Changed
The note-space walker is a shared module. The read-path/ignore-path/size-cap walking rules previously private to the inline scan now live in
src/core/brain/notes/note-walk.tsand back the inline scan, the new title resolver, the open-loop scan, and the write-back file list - one set of rules instead of per-feature copies. Existing scan behavior is pinned unchanged by its tests.Marker grammar validation is per-kind.
@osbmarkers validate against a per-kind required-field table (feedback,loop,set) instead of feedback-specific hardcoded checks. Feedback parsing, downstream signal emission, and unknown-kind rejection are byte-identical; the three existing marker consumers now explicitly process only feedback markers.
1.30.1 - 2026-07-17
A structural maintenance release with zero behavior change: every import cycle in the TypeScript source tree is removed and the largest source file is decomposed. The analysis and the verification gate come from code-ranker (v5.0.3); the full suite (5687 tests) is byte-for-byte green before and after.
Changed
All seven import cycles removed from
src/. Dependency inversion at the type level, cycle by cycle: doctor/trust and procedural-* shapes move to the leaf modulesrc/core/brain/types.ts;LinkOutputFormatmoves fromconfig.tsto its only consumerwikilink.ts; the ~35-filesrc/mcpcomponent dissolves into a one-directional layering over a newsrc/mcp/tool-contract.tscontract leaf;src/core/searchgainsembeddings/contract.ts,rerank/contract.ts, andembeddings/configured-provider.tswhilesearch/types.tsbecomes a pure leaf; and a newsearch/tuning-store.tssplits the tuned-parameter read side from the grid optimizer, closing the lastsearch -> tuning -> benchmarktriangle.code-rankernow reports zerocycle.chainviolations and zero cycles in the file graph.src/core/search/search.tsdecomposed. The worst-ranked file in the tree (maintainability index -85.8, cognitive complexity 442, 1722 lines) keeps only thesearch()orchestration; progressive disclosure (cards.ts), structured-lane application (structured-lanes.ts), the semantic phase (semantic-phase.ts), frontmatter result filters (result-filters.ts), graph expansion phases (graph-phases.ts), and evidence-pack coverage (evidence-verification.ts) move to focused leaf modules with byte-identical function bodies.
1.30.0 - 2026-07-11
A governance-visibility release that adds two aggregate views over the preference set the existing per-item hygiene lints could not answer, plus an efficiency refinement to how one of them sources its data. Both new surfaces are additive and read-only over Brain/preferences/; the release adds no new dependency and the kernel still calls no LLM.
Added
Aggregate governance scorecard (
o2b brain vitals). A single read-only scorecard over confirmed preferences that the per-item lints (doctor,health,moc-audit) do not surface:domain_diversity(normalised Shannon entropy of thescopedistribution),connectivity_index(meanevidenced_bycount per preference),orphan_preferences(confirmed preferences below an evidence threshold, default 2,--orphan-threshold), andgap_pressure(openconcept-gapfindings, reused from the semantic-health pass, divided by preference count). Text or--json; records onevault_vitalsmetric per run, following the existingclusters/benchmarksurface pattern.Batch-concept-inflation lint. The existing
duplicate-preferenceslint only catches near-identical pairs (Jaccard >= 0.7 within one topic/scope).detectBatchInflationis a deterministic, non-overlapping window scan over confirmed preferences'confirmed_attimestamps (default 24h window, burst size >= 5, both configurable) that flags a burst of individually-distinct preferences confirmed together - the signal that a batch or concurrent ingestion session skipped its dedup/consolidation pass. Wired end to end:reconcileSemanticHealth, thebatch-concept-inflationdoctor lint,o2b brain health(CLI) andbrain_health(MCP), and a newbatch_inflationWorkspace Insight trigger kind.
Changed
Vault vitals sources
gap_pressurefrom a semantic-only health pass.computeVaultVitalsnow calls a new exportedcomputeSemanticHealthinstead of the fullrunDoctorsweep to read the open concept-gap count. Both route through the same detector over the same inputs, sogap_pressureis byte-for-byte identical (proven by an equivalence test), but vitals no longer pays for the structural config, signals, backlinks, logs, and entities checks it never reads.runDoctor's own report is unchanged.
1.29.0 - 2026-07-10
An operability, safety, and first-run release that makes Open Second Brain robust and pleasant to set up and operate: a guided first-run checklist, actionable config diagnostics, resilient hooks and rate-limit handling, safe mutating operations, a hardened optional HTTP transport, and usage visibility. Every new surface is additive and off by default or byte-identical when its condition is absent; the release adds no new dependency and the kernel still calls no LLM.
Added
Guided first-run onboarding checklist.
o2b initnow prints a state-aware, ordered set of next steps beyond search indexing (vault config, Brain scaffold, first index, agent identity, an optional embedding key, a first feedback signal, session import, and a health check), each with a done flag and a copy-pasteable command. A re-runnableo2b onboardingverb (text or--json) shows the same checklist any time. It reuses the runtime-notice and doctor-remediation surfaces; read-only, no network, no model.Config-validator remediation. The install/config doctor's
CheckResultgains an optionalfixfield carrying a copy-pasteable remediation command, populated in every failing check (mkdir/chmodfor vault and config,o2b updatefor a missing or invalid manifest).o2b doctorrenders it under failing checks, gains a scriptable--jsonreport (per-checkfixplus an aggregate{total, failed}summary) and a summary line, and surfacesfixthroughvault_healthand the OpenClaw doctor. The 0/1 exit stays the scriptable gate - the doctor becomes a self-service repair tool.Runtime-state notice channel. A deterministic, no-network, no-LLM probe surfaces OSB's own transient conditions - semantic search degraded (enabled but no embedding key resolved), the search index missing or rebuilding, and a read-only vault - as notices that ride the existing SessionStart injection surface (prepended only when a condition holds, so a healthy vault stays byte-identical) and fold into
vault_healthfor pull consumers. Default on; opt out withOPEN_SECOND_BRAIN_RUNTIME_NOTICES=false.Hook process self-watchdog and fail-open context load. A lifecycle hook now arms a hard time ceiling on itself (default 55s, override
OPEN_SECOND_BRAIN_HOOK_CEILING_MS): a healthy run exits normally, and a hung hook self-terminates at the deadline with an audit line instead of orphaning a process or blocking the agent. SessionStart context assembly runs fail-open - a slow or failing assembly degrades to the last-good cached body (or empty), never a partial or poisoned write. Distinct from thebrain_watchdogvault-health probe.Expectation and strict count guards on mutating ops.
--expect Nasserts how many items a mutation will touch and--strictrefuses a guardless mutation; on a mismatch the op aborts before writing and surfaces the matched list. Wired into delete-by-source, hygiene apply, and dream run (MCP and CLI), each reporting honestmatchedvschangedcounts. The atomic writer gains an opt-in content-equality short-circuit that skips an identical write (no mtime churn). No block-query DSL; guards default off.Per-skill invocation telemetry. How often each installed skill is actually invoked is captured as an append-only
skill_invokedcontinuity record, emitted from the session-import tool-call scan across Claude Code and opencode logs (get_skilland the host-nativeSkill; discovery and attach are not invocations). Counts are derived read-side, deduped by content-address id so re-import is idempotent, and surfaced viabrain_skill_proposalsoperation: usageando2b brain skill-proposals usage. Deterministic, no LLM; distinct from proposal ranking and outcome-weighted recall.
Changed
Configurable embedding retry budget for 429/5xx. The embedding provider's per-batch transient-retry budget is now runtime-configurable via
embedding_max_retries/OPEN_SECOND_BRAIN_EMBEDDING_MAX_RETRIES, with the default raised from a hardcoded 3 to 6, threaded into both the OpenAI-compatible and ZeroEntropy providers, so a reindex against a strict-RPM account no longer exhausts the budget and silently drops chunks.ping()still probes once and multi-key auth failover stays independent; the concurrency knob already exists asembedding_concurrency.Hardened optional loopback HTTP transport. The opt-in
o2b mcp --transport httpserver gains a mandatory, non-bypassable Host/Origin DNS-rebinding guard (a foreign Host or cross-origin/opaque Origin is rejected before auth) and an unauthenticatedGET /healthliveness probe. The bearer token is now optional on a loopback bind (loopback + guards are the baseline defence) but mandatory on a non-loopback host - the server refuses to expose an unauthenticated endpoint on the network, with no permissive fallback. stdio stays the default transport.
1.28.0 - 2026-07-10
A retrieval and ranking quality release: embedding-layer robustness, faster and smarter candidate retrieval, a fully offline reranker with a per-store eval gate, ranking signals learned from real outcomes, and a named benchmark to score the whole. Every new surface is additive and off by default where it changes an existing path; the release adds no new dependency and no ML runtime, and the kernel still calls no LLM.
Added
Curated embedding-model presets and a recommended default. A static, shippable catalog of known-good multilingual-first embedding models (with native dimensions and guidance) is surfaced at provider registration via
o2b search provider presets;provider adddefaults--modelto the recommended model when omitted. Advisory only - a custom--modelstays first-class and verbatim. No server, no network.Native ZeroEntropy embedding provider.
zeroentropyis now a first-class embedding provider calling the nativePOST {base}/models/embedAPI directly overfetch(no SDK, no dependency), with Bearer auth, input-order-preserving vectors, and Matryoshkadimensionswhen configured. It reuses the shared retry/backoff andpingcontract; the resolved-provider union stays closed.Trigram candidate prefilter for large-vault search. A native SQLite FTS5
trigramshadow index (schema v9, populated in-place by a reindex-safe migration, no dependency) backs an opt-in prefilter that merges substring / partial-token candidates the word tokenizer misses into the keyword pool - a strict superset of substring matches, so it never drops a result. It engages only past a corpus-size floor and skips short, CJK, and low-selectivity queries. Off by default (byte-identical); knobs fold into the query-cache fingerprint.Graph-index query pre-pass with multi-hop BFS. A new read planner (
o2b search plan) ranks seed notes by title match and wikilink degree over index metadata, connects them through multi-hop BFS on the link graph, and returns ashould_readshortlist - surfacing notes reachable only via several hops that the 1-hop link boost misses. Anindex_onlymode answers from title / degree / link metadata with zero note bodies hydrated. Pure-stdlib.Bundled offline reranker and per-store reranker eval gate. A deterministic, dependency-free
localreranker (coverage / proximity / density blend) behind the existing rerank interface makes reranking work fully offline with no model runtime or network, selected viasearch_rerank_kind=local. A new evaluation gate runs the recall benchmark rerank-off vs rerank-on and recommends enabling only when it lifts ranking without regressing hit@k.Session-end observed-use verdict feeding recall ranking. A new
recall_observed_usecontinuity record captures a USED / IGNORED / CONTRADICTED verdict per injected memory - classified deterministically from transcript token overlap plus a structural stance flip (no LLM) or supplied by the host via the newbrain_observed_useMCP tool (mirroringbrain_apply_evidence). The folded observed-reuse rate becomes a preferred, capped recall-ranking signal so demonstrably reused memories outrank merely-predicted-important ones; contradictions demote. Inspectable viabrain_recall_telemetry observed_reuse. Byte-identical when no verdicts exist.LoCoMo benchmark suite.
o2b brain bench memory --suite locomoconverts a LoCoMo-shaped dataset into the existing staged harness's fixture and runs it as a parallel named suite (locomo-*), so runs are comparable to the published long-conversation-memory literature. OSB's six categories stay canonical; the run is deterministic and network-free by default, with the LLM judge opt-in. Ships a small committed sample dataset.
Changed
Multi-key fallback for the embeddings API key. A provider profile's
envKeymay now be an ordered probe list (a single string still works and stays byte-identical); resolution takes the first present key and theOpenAICompatProviderfails over to the next on an HTTP 401/403, pinning the first that authenticates.o2b search provider add --env-key A,Bregisters a probe list. Rerank stays single-key. Removes the single point of failure in the only networked path without touching the LLM-free kernel.Outcome-validated procedural recall. Procedural entries and their usage sidecar gain additive success / failure counters (outcome-free vaults stay byte-identical), a host-supplied
recordProceduralOutcome(deterministic, no LLM), and success-rate ranking that surfaces proven procedures and sinks failing ones, with the usage count as the neutral prior for unproven ones. Exposed viabrain_procedural_memory mark_outcomeandlist ranked:true.
Fixed
Resolve
o2b/bunwhen the memory provider inherits a tinyPATH. When Hermes starts the provider from a process with a minimal inheritedPATH,shutil.which("o2b")cannot see a user-local install andsubprocess.Popenlater fails withNo such file or directory: 'o2b'. Resolution now falls back to a curated scan of user-local and system executable directories (~/.local/bin,~/.bun/bin,~/.hermes/node/bin,/opt/homebrew/bin,/usr/local/bin,/usr/bin,/bin) and uses the resolved absolute path for botho2bandbun.shutil.whichstill wins first, the POSIX-onlyo2bbranch and the repo-localbun run <entry> mcpfallback are preserved, and on Windows the scan matchesPATHEXTsuffixes rather than gating on the POSIX execute bit.
1.27.0 - 2026-07-10
An ingestion and import robustness layer that hardens and extends the source pipeline in one coherent scope: interrupted batches resume, more memory stores import, deterministic refresh skips when nothing changed, and sources distill into citeable claims. Every new surface is additive and off by default where it changes an existing path; the kernel still calls no LLM.
Added
Resumable per-item batch checkpointing. A plan-scoped checkpoint under
<vault>/.open-second-brain/ingest-checkpoints/<plan_id>.jsonrecords completed items union-as-you-go, so an interrupted large-folder ingest resumes at the item boundary instead of re-planning from scratch.planBatchesacceptsresume, which excludes checkpointed items before content-hash classification (a fast-path that avoids re-hashing completed work on a large-vault resume) and returns a stableplan_idplus aresumed_completedcount;ingestSourcerecords completion when handed aplan_id. Thebrain_ingest_batch_planMCP tool gainsresumeand returnsplan_id, clearing a drained plan;brain_ingest_sourceand theo2b brain batch-plan --resumeCLI carry it through. The content-hash manifest stays the authoritative final state. Opt out withOSB_INGEST_NO_CHECKPOINT.mem0 and generic-JSON memory-store importers. The
MemorySourceBackendseam widened from one-file-one-entry to discover-files plus parse-entries, so a single JSON export maps to many Brain preferences. Newmem0andgenericbackends parse a memory export (a top-level array or aresults/memories/entriesenvelope) into preferences on the shared render path; theclaudebackend stays byte-identical. Theo2b brain import-claude-memoryCLI gains--from/--backendto select a non-default backend and can be pointed at a single export file. Unknown ids fail loudly with the registered list.Staleness fast-path for deterministic refresh. A reusable mtime staleness gate plus an opt-in
o2b brain clusters run --if-stalethat no-ops when the materializedBrain/clusters/notes are already newer than every input note, recording a freshness-skip metric and skipping the recompute entirely. Default behavior is unchanged; the>=comparison errs toward recompute, and a missing input never forces a needless rebuild.Source distillation into atomic claims. A new
o2b brain distillCLI verb andbrain_distill_sourceMCP tool condense a source into agent-supplied atomic claims, each with an optional source block id, and write an idempotentBrain/distillations/page that cites each claim as[[source#^block]]alongside a Sources provenance section and a content sha256 the verifier can reproduce. Provider-agnostic: the core runs no model, validates claims structurally, and rewrites one page per source in place.
[1.26.1] - 2026-07-08
An upstream-alignment guard release. Two regression suites now pin Open Second Brain's Hermes-facing surface against two landed upstream Hermes changes, with no source edits and no runtime behavior changes. The kernel still calls no LLM.
Added
Structural invariant tests for the memory-provider tool schemas. A new
NormalizeContractTestssuite intests/python/test_static_schemas.pyasserts that every entryget_tool_schemas()exports, on both the static-fallback and the live-bridge code paths, is flat (no{type: function}wrapper nested anywhere) and carries a non-empty top-levelname, exactly the preconditions core's new sharednormalize_tool_schema()(upstream Hermes PR #52140) relies on. A companionProviderNormalizeSurvivalTestscase drives the provider's live-listing path with a mixed tool list (a curated tool, a non-curated tool, and an accidentally pre-wrapped entry) and asserts only the curated, flat, named tools survive, so a strict-provider turn (DeepSeek) can no longer be disabled by a malformed schema. TheinputSchematoparametersremap is also shown to preserve the curated name set, count, and order, and to be idempotent with deep-copied output.Repeated in-place-compaction regression under a stable session id. A new
InPlaceCompactionLifecycleTestssuite intests/python/test_memory_provider.pydrivessync_turnfollowed by repeatedon_pre_compressandon_session_endwith one fixed session id (the newcompression.in_placedefault flipped by upstream Hermes PR #52658) and asserts exactly-once flush semantics:brain_pre_compact_extractfires once per boundary that has buffered turns, the in-memory buffer is cleared between flushes with no double-flush or clobber, and no duplicate Brain session writes accumulate across repeated in-place compaction. The provider is shown to make no assumption that the session id rotates.
[1.26.0] - 2026-07-08
A memory-signal provenance and lifecycle integrity layer that closes five gaps in one coherent scope. Every new surface is additive and byte-identical when off, and the kernel still calls no LLM.
Added
Content-hash skip-unchanged manifest. Ingest now classifies source files as new, modified, unchanged, or missing by SHA-256 over file bytes, distinct from identity dedup which hashes the source path. A machine manifest at
<vault>/.open-second-brain/ingest-manifest.jsonrecords post-ingest hashes; a rerun where every source is unchanged skips the LLM extraction pass and rewrites nothing. Atomic write; idempotent.Per-row event-time on batch remember and import.
WriteSignalInputgains optionalvalid_fromandrecorded_atthat flow into the same frontmatter keysreadBiTemporalSlots()already parses; session-import prefersSessionTurn.timestampforcreated_atandrecorded_atand falls back tonowonly when absent or unparseable, so backfilled signals are stamped with the original turn time instead of the import moment.brain_feedbackand the batch import path forward an optional per-rowevent_time. Absent by default, existing records byte-identical.Batch-plan step for large-folder ingest. A new
planBatcheshelper discovers ingestible files, skips unchanged ones through the content manifest, then splits the remainder into size- and count-bounded batches for the caller (agent or CLI) to dispatch as parallel subagents. Deterministic sort-by-path greedy fill; no parallel execution inside the kernel.Idempotent writes via client idempotency keys.
writeSignal,writePreference, andappendApplyEvidenceaccept an optionalidempotency_key; the writer hashes the would-be payload and consults a month-sharded JSONL ledger underBrain/logs/idempotency/. Same key and same hash is a deduped no-op; same key with a different hash is an explicitpayload_mismatcherror, never a silent overwrite.brain_feedbackforwards an optionalidempotency_key.Dry-run extraction preview.
extractPreCompactRecordsand the other extraction entry points accept an optionaldryRunflag that returns the exact records the real path would append without callingappendContinuityRecord(no vault mutation, no log event, no dream or retire trigger). Mirrors the existingopts.dryRunidiom inimport.ts; a parity test asserts dry-run candidates equal real-run records for the same input.Bounded verbatim last-N-turns buffer. A hard-capped ring buffer of the last N turns (default 20) persisted as a new
recent_turncontinuity kind (additive; legacy records read as v1).appendRecentTurnevicts oldest beyond N; reads are bounded. An opt-in post-compaction flag appends the buffer so the agent can recover recent verbatim wording after compaction. Default off; its own kind and reader, clearly separated from curated memory.Batch checkpoint save for whole sessions. A new
brain_session_checkpointMCP tool saves a whole session signals, learnings, and summary in one round-trip. Idempotency is session-id plus content-hash, reusing the idempotency ledger; a checkpoint that needs review returns a{ status: "ok" | "mixed", partial: [...] }result and never silently drops an item.Caller-settable per-memory expiration date.
WritePreferenceInputandWriteSignalInputaccept an optional ISOexpiration_date. The read path silently drops anything past its expiration by default; an opt-inshowExpiredre-includes expired records for audit. Orthogonal to dream heuristic retirement: an expired-by-date memory is filtered, not moved toBrain/retired/(audit trail preserved).Delete and search by exact source file. Search accepts an exact
source_filefilter matching the source-ref frontmatter, and a newdelete_by_sourcecommand is dry-run by default, reporting the blast radius (which derived signals, preferences, index entries, and summary pages trace back to the source) without deleting. An explicit--confirmdeletes the derived entries and index artifacts; original user notes are removed only with--include-originals. Every cleanup is recorded as a continuity event.Signed source-diversity grounding score. A new
computeGroundingScoreprojection over the sameClaimEvents that feedcomputeTruthStatereturns{ score, confidence, band, supportingSources, contradictingSources }. Confirming vs contradicting evidence is counted across independent sources (distinctsourceandagent), not raw mention count, so N mentions in one document count far below N mentions across N independent sources. Signed: plus for confirming, minus for contradicting; confidence captures how much evidence backs the direction. Pure projection, order-insensitive, never mutates the append-only truth ledger; replaces nothing (the CONTESTED flag stays, the score quantifies it).Note-position contradiction detection.
detectNoteContradictionspairs same-subject permanent notes asserting opposite stances, deriving position-sign from note prose rather thanevidenced_bysignals. Reuses the existing language-agnostic token-overlap machinery; emitsask_userfindings quoting the relevant span from each note and never auto-resolves.Declared-thesis register with new-note monitor. A new
Brain/theses/frontmatter kind records operator positions (statement, supporting and counter evidence, last-updated, falsification criterion). A monitor evaluates each newly-ingested note against active theses, flagging support or contradiction, reuses the obligations cadence machinery for a staleness check and a thesis-graveyard pass, and alerts when incoming evidence matches a thesis documented falsification scenario.Session-bracketing memory wrapper for Aider. A new
scripts/o2b-aiderwrapper (alsoo2b aider wrap) brackets an Aider session: it re-renders the static context sidecar at start, execs the Aider binary with the injectedread:context, and at session end runs the write-back half (capture and persist the session into the Brain), mirroring Hermesprefetch,sync_turn, andon_session_end. The install-time sidecar adapter stays as the fallback for users who do not run through the wrapper.
Fixed
Shared preference no longer deletable by
forget-source.deleteBySourcenow reads a preference's managed_evidenced_bylinks (not only the legacyevidenced_bykey) and applies the foreign-evidence guard to preferences matched by a[[source]]wikilink, so a preference folded from any foreign signal is reported, never auto-deleted on--confirm.Provenance folded into idempotency hashes. The preference idempotency hash now includes
owner, and the session-checkpoint hash now includeshostandsourceTurnIds, so a retry that changes only owner-visibility or checkpoint provenance raisesIdempotencyPayloadMismatchErrorinstead of silently deduping under the wrong scope.Apply-evidence honors the idempotency ledger verdict.
appendApplyEvidencenow acts onrememberKey's return value: a concurrent same-key write with a different payload raises the mismatch error, and a same-payload race returns the winner's row as a deduped result instead of a second normal write.Partial
forget-sourcecleanup stays auditable. AremoveFilefailure mid-cleanup now still writes thesource_invalidationaudit record for the paths already removed before rethrowing, so a retry can reconstruct the state.CLI error contracts.
brain batch-planwith no<source-dir>now exits 2 as a usage error, andbrain forget-source --jsonemits a parseable{ ok: false, message }envelope on a runtime failure instead of plain-text stderr.
[1.25.0] - 2026-07-07
An economics and observability layer for the context pack. Five new opt-in surfaces make the value of memory measurable and close the quality feedback loop, each off by default with byte-identical output when unconfigured. The kernel still calls no LLM.
Added
Value-per-token density ranking in the context pack. With
density_ranking_context_packon (default off), the context-pack builder adds a deterministic density comparator that breaks within-tier ties after session focus and before recency, ordering candidates by structural signal per estimated token rather than only by freshness. Tier stays the coarse gate so a peripheral page never outranks a core one; off/absent keeps the density field off the item and the ordering byte-identical. The score is a language-agnostic count (evidence-ref grounding plus[[wikilink]]connectivity plus a fixed epistemic weight), never a wordlist or LLM judgement.Durable token-impact ledger. With
token_impact_ledger_enabledon (default off), every context pack posts onetoken_impactsample carrying the tokenizer-exact prompt-token delta (baseline_tokens−packed_tokens, labelledmethod: exactorfallback) plus an optional modeled inference-avoidance estimate.brain_token_impactrecordposts a sample,outcomeposts afirst_pass/repair/retryresult used only to calibrate the modeled figure, andsummarykeeps the two ledgers strictly separate:prompt_token_deltareports the measured net, saved, and added tokens with a per-method split, whilemodeled_inference_avoidancereports the raw modeled figure and acalibrated_savings_tokensvalue dampened by the observed first-pass rate (null until an outcome exists, uncalibrated not zero).listreads raw samples. Writes are payload-safe (counts and an opaquepack_idonly); reads ignore the gate. Aggregates are recomputed from the continuity store on every read, so they survive restarts.Agent-operable context-pack outcome loop. With
context_pack_outcome_enabledon (default off),brain_context_pack_outcomepostrecords one compact outcome row for a carried context-pack quality-sample id: first-pass, repair, and retry counters plus three strictly separate token signals (exact_prompt_token_savings,modeled_inference_avoidance,observed_provider_tokens), and composes the token-impact ledger by posting a matching first-pass, repair, and retry calibration outcome.listandsummaryread the rows keeping the signals separate. A field the caller omits is never invented; writes are payload-safe (counters and an opaquesample_idonly); reads ignore the gate.Route-level MCP tool latency metrics. With
mcp_route_metrics_enabledon (default off), the MCP server emits onemcp_route_latencycontinuity record per tool call: tool name, scope, status (ok/error), duration, and the sorted set of argument key names only, never argument values.brain_route_metricslistreturns raw records andsummaryrolls each tool up into count, error count, min, avg, max, and p50, p95, p99 latency ordered slowest-first, so an operator can identify which endpoint blocks an agent turn by route rather than by aggregate benchmark. Read-only; the emit is gated and fail-open.Proactive active-memory budget-pressure watermark. The active budget now reports a pressure signal before the pack is forced to trim, so an operator or agent can react to approaching limits instead of only observing the trim after it happens. The signal is derived from existing budget metadata; behavior is byte-identical when no consumer reads it.
1.24.0 - 2026-07-06
A hardening and correctness pass over the PR #121 refactor. Thirteen fixes
across four classes land as one minor because the basename column addition
forces a LATEST_SCHEMA_VERSION bump and reindex on upgrade, and because the
created_at validation tightening turns previously-accepted garbage into
INVALID_PARAMS. The kernel still calls no LLM, and every new field is off or
absent by default with byte-identical output when unconfigured.
Added
Self-heal on an unreadable search index.
openReadOrSelfHealnow rebuilds onINDEX_UNREADABLEalongside the existingINDEX_MISSINGandSCHEMA_MISMATCHcases, so a corrupted but present index self-heals instead of surfacing a raw read error.
Changed
basenamecolumn and index in the search store. Resolved-wikilink discovery is now an equality join over a populateddocuments.basenamecolumn instead of adocumentsSUBSTR scan per link, removing a scan that grew with vault age. The column requires aLATEST_SCHEMA_VERSIONbump, so every existing install reindexes once on next open. The reindex is absorbed together with the Han-bigram tokenizer change below, so users pay one reindex, not two.Hot-path memoization. The entity index, backlink index, and heal regex are now memoized with mtime and generation keys so they are not rebuilt on every query. The memos are opt-in by construction: they introduce a bounded staleness window (a write inside mtime granularity can serve one stale read), which is documented at the call site and pinned by write-then-read invalidation tests.
links normalize --mode shortrewritten over a basename-to-paths Map. Output is byte-identical to before, captured by a characterization fixture.Continuity latest-record queries exit early in reverse chronological order. The working-memory recompute no longer scans the full history when only the most recent record is needed.
Fixed
Search index rebuild race and crash-restore clobber. A separate reindex lock (
acquireReindexLock/isReindexInProgress, distinct from the per-Store writer lock) now guards the staging build so a concurrent reindex cannot swap an empty staging database over the live index, and the crash-restore preamble no longer clobbers a freshly built index with a stale.bak. Three regression tests pin the path.created_atvalidation tightened. Continuity and query-demand records now require a full canonical UTC timestamp; aYYYY-MM-prefix timestamp that was previously accepted now fails with a structuredINVALID_PARAMS. This is the contract-tightening gate that places these fixes behind a minor version.Surrogate-safe card snippet truncation. Snippet truncation now cuts on code points instead of UTF-16 code units, so an emoji or other astral character at the boundary no longer splits into a lone U+FFFD on the
cardsdisclosure surface.Second-precision
since/untilfiltering. Filter bounds run through timestamp normalization before the lexical compare, so millisecond and second precision are handled consistently and up to a second of records is no longer dropped at each boundary.Secret-exec audit redaction. The
commandfield logged by the secret-exec path is now redacted before it reaches the audit trail, so a foreign bare credential is no longer persisted in cleartext.Han-bigram tokenizer restricted to contiguous Han spans. The CJK bigram pass no longer emits cross-script bigrams (
n实) for mixed tokens, matching the docstring's Han-only scope. Pure-Han and pure-ASCII tokenization are unchanged.
1.23.1 - 2026-07-06
Fixed
o2b initfails on native Windows --resolve()produces backslash paths thatsetConfigValue()rejects viaCONFIG_VALUE_REJECTED_CHARS. Vault path is now normalized to forward slashes before persisting; forward slashes work identically in Node/Bun fs APIs on Windows. (#119)Hermes memory provider bridge fails on Windows --
scripts/o2bis a bash script thatsubprocess.Popencannot execute directly (Windows delegates tocmd.exe, not a POSIX shell). Added_resolve_command()which detects the platform and falls back tobun run <entry> mcpwheno2bis not available as a native executable.detectTooling()fails to find tools on Windows -- PATH was split on:only (POSIX); now uses;on Windows. Executable probing now honoursPATHEXT(.exe,.cmd,.bat,.com) instead of hardcoding.exe.MCP
inputSchemanot recognized by Hermes adapters (all platforms) -- Hermes adapters (Anthropic, OpenAI, Bedrock) expect tool schemas underparameters, but MCP serves them asinputSchema. Without this remap brain tools had no visible arguments on any OS. Both live and static fallback schemas now remapinputSchema->parametersat the provider boundary.
1.23.0 - 2026-07-05
Two optional precision layers that complete the retrieval-precision loop started in 1.22.0. Each is off by default with byte-identical output when unconfigured, and the kernel still calls no LLM.
Added
Proposal-only semantic entity dedup. With
entity_semantic_dedup_enabledset,brain_doctor(and thedoctorlint surface) surfaces lexical entity-name variants ("Google LLC" vs "Google Inc") as PROPOSAL-ONLY alias-merge candidates through a deterministic jaccard layer (entity_semantic_dedup_lexical_threshold, default 0.8). An embedding-cosine reader (entity_semantic_dedup_threshold, default 0.92) reuses the configured embedding provider and is exposed as a library reader for apply plans. The deterministicentityIdentityKeyis never rewritten and no entity is ever auto-merged; a human or apply plan owns the actual merge, preserving audit friendliness. Optionalidentity_typetagging derives from frontmatter and structural signals only (language-agnostic by construction). Registry and search behavior is byte-identical until opted in.Optional cross-encoder rerank stage. A learned final reader step re-scores the top-K fused search candidates jointly against the query, appended after every heuristic rerank. Off by default (
search_rerank_enabled); when on, it resolves an OpenAI-compatible/rerankendpoint (search_rerank_base_url/search_rerank_model/search_rerank_env_key, or a profile registered viao2b search rerank-provider add) and re-orders thesearch_rerank_top_kcandidates by the returned relevance, promoting a genuinely-relevant hit the heuristic ranker placed deep. Zero HTTP cost and byte-identical ordering when disabled; a request-time endpoint error degrades to the heuristic ordering (arerank_degraded:warning, never a throw). Most valuable on thethoroughprofile.
1.22.0 - 2026-07-05
A retrieval-precision and quality-loop pass. Four new surfaces close the loop between what recall surfaces, how honestly it reports its own grounding, and how observed failures turn into a prioritized backlog. Each new field is optional and absent by default, so every call site whose caller does not opt in produces byte-identical output, and the kernel still calls no LLM.
Added
Epistemic provenance markers on packed and recalled items. Every item emitted by
brain_context_pack,brain_context_receipts, and recall results now carries a structuralepistemicstatus (observed|derived|hypothesis|plan|unknown) plusevidence_refs, derived from existing graph metadata with no manual tagging and no natural-language word lists. A consuming model can now tell a source-backed fact from a conjecture or a plan. The status is language-agnostic by construction; fields are absent when the status isunknown, so the off-path is byte-identical to today.Recall adequacy verdict with explicit low-adequacy actions. A thin verdict layer classifies each recall attempt as
sufficient,weak, orinsufficientand emits a recommended action (proceed,re_recall,abstain) with an optionalescalateflag, reusing the existing gate-telemetry relevance scores plus the new epistemic mix. The verdict appears inbrain_recall_gateoutput and the context-receipts summary when the caller passes recall scores; otherwise the field is absent and output is unchanged. Thresholds are configurable (recall_adequacy_sufficient,recall_adequacy_weak,recall_adequacy_min_results).Cross-query demand log surfacing recurring poorly-answered queries. A rolling, size-capped demand log under
Brain/log/query-demand.jsonlrecords each recall (normalized terms plus timestamp, result count, and the existing coverage score) and an aggregation reader ranks recurring queries the vault answers poorly, ranked by frequency times (1 minus IDF-weighted coverage). Surfaced read-only as thebrain_knowledge_gapsMCP tool ando2b brain knowledge-gapsCLI. The log is written only when recall gate telemetry is opt-in (default off) and queries are normalized before append; the reader is always available and never writes.Unified outcome-tagged lessons loop. A
Brain/lessons.mddigest folds positive knowledge (preferences and their applied evidence) and negative knowledge (violated/outdatedrows andBrain/dead-ends/) into one outcome-tagged corpus scored by a signed, recency-decayed weight (shared 30-day half-life from working-memory continuity). Each lesson lands in a tier:preferred(net-positive and corroborated by at least N distinct results),tentative(net-positive but under the corroboration floor),contested(both positive and negative recent evidence, recency-wins), oravoid(dead-ends and net-negative). The digest is regenerated at the tail of everydreampass alongsideactive.md, exposed as theosb://lessonsMCP resource, and injected on SessionStart and PostCompact. Tunable vialessons.limit,lessons.half_life_days, andlessons.corroboration_min.
1.21.0 - 2026-07-04
A safety-hardening pass around the surfaces that write to or trust the vault and the codegraph. Each change tightens a boundary or closes a silent-loss gap; none changes default behaviour when the new path is unused, and the kernel still calls no LLM.
Added
Fail-closed redactor scan window and opt-in infra-topology detectors. The secret redactor now enforces a hard 1 MiB input window and fails closed instead of silently truncating oversized input, so a too-large document can no longer slip past redaction. Two opt-in detector families (hostnames that resolve to a private range, and well-known infrastructure tokens) extend coverage beyond secrets when enabled; off by default, the shipped redaction set is unchanged.
Read-only codegraph health gate before labeling and import. Before the partner surface mutates the codegraph (labeling, edge import), it runs a read-only
codegraph healthcheck and refuses the write when the index reports stale, partial, or inconsistent state, instead of importing into a graph it cannot trust.Report-only hardcoded-path check for shipped surfaces. A new
check-hardcoded-pathsscript scans the shipped source for absolute or machine-local paths and reports them, without failing a build on pre-existing hits, so path leakage stays visible without blocking history it did not create.Memory cost meter: write-volume versus read accounting. The recall-telemetry surface now separates memory write volume from read volume, so a runaway writer (a provider looping on store) is distinguishable from a busy reader in the same numbers.
Changed
Stable
unnamed-<hash>slug fallback for punctuation-only titles. A note whose title collapses to empty after slugification now gets a deterministicunnamed-<hash>slug instead of colliding into a single sharedunnamednote, so distinct punctuation-only titles no longer overwrite one another.Portable session-provenance key. Session import records its provenance as a stable logical key rather than the absolute session-file path, so a vault moved between machines no longer breaks session provenance lookups.
1.20.0 - 2026-06-28
Two optional, opt-in config keys widen the agent skill surface without
changing any default behaviour: skill discovery can point at an external
directory, and skill auto-attach can score on a triggers keyword field.
Added
Configurable skill discovery root (
skills_dir/OPEN_SECOND_BRAIN_SKILLS_DIR). Pointslist_skills/get_skill/skills_attachat an arbitrary directory (e.g. an external~/.hermes/skills/) instead of vault-localBrain/skills/, with no symlinks or vault restructuring.~is expanded; a relative value is anchored to the resolved config file's directory so the root is deterministic regardless of the process working directory. Unset leaves discovery byte-identical to before.Trigger-keyword scoring for skill auto-attach (
skills_attach_triggers/OPEN_SECOND_BRAIN_SKILLS_ATTACH_TRIGGERS). When"true"or"1", each skill'striggersfrontmatter field (a scalar string or an inline array) participates in the lexical scorer as a 2x BM25 tag signal, alongside name (3x) and description (1x). Default off:triggersis ignored and scoring stays name + description only. The tokenizer also emits overlapping bigrams for runs of Han characters, so a spaceless CJK query can match a trigger keyword.
1.19.1 - 2026-06-26
Three correctness and hygiene follow-ups deferred from the v1.18.0 review,
shipped together. No behaviour changes beyond the fixes; the kernel still
calls no LLM and the default full search path stays byte-identical.
Fixed
Cross-vault search now returns
disclosure: "cards"results (t_fd411665). In--global(cross-vault) mode a cards search returned nothing: single-vaultsearch()puts cards onoutcome.cardsand leavesoutcome.resultsempty, butsearchAcrossVaultsmerged onlyresults, so every origin's cards were dropped. The union now merges each origin's cards (labelled by origin, ranked and capped like results) and returns them onoutcome.cardswithresultsempty, mirroring single-vault semantics. The normalized-confidence chain-stop now gates on the top card score when cards mode leavesresultsempty.A genuine event-trace IO error is a runtime failure, not a usage error (
t_27ea0daa).o2b brain event-tracemapped any throw from the resolver to a usage error (exit 2). A real IO failure from the log-shard reader (an existing-but-unreadableBrain/log: EACCES / EIO / ENOTDIR) is now exit 1, while a bad--date/--at/--kindselector stays exit 2. The MCP twinbrain_event_tracelikewise maps a runtime IO error toINTERNAL_ERRORand a bad selector toINVALID_PARAMS, via a sharedEventTraceSelectorErrortag thrown only by the pre-IO selector checks.Registry-guard exempt membership is own-key only (
t_6fbdba4b).auditPreviewBudgetstested exemption withname in PREVIEW_BUDGET_EXEMPT, which walks the prototype chain - so a tool named like anObject.prototypemember (constructor,toString, ...) would be falsely treated as exempt and escape the unbudgeted-output guard. Membership now uses a name set hoisted once at module load, removing the per-callObject.keysas well.
1.19.0 - 2026-06-24
Session-boundary capture durability and a post-compaction survival audit for pinned context. Three durability gaps at session boundaries are closed, and each new behaviour is off by default or otherwise byte-identical until opted in. The kernel still calls no LLM.
Added
Post-compaction pinned-anchor survival audit and selective re-assertion (
t_post_compact_audit). A deterministic, LLM-free audit symmetric to the existing pre-compaction capture (extractPreCompactRecords). After a Hermes context-compaction demotes a pinned anchor's text into the background summary block, nothing previously verified the anchor still lived in the ACTIVE region or re-surfaced it.detectCompactionscans the conversation for Hermes compressor handoff markers (Unicode SUMMARY_PREFIX, ASCII fallback, and legacy[CONTEXT SUMMARY]:; overridable per-host) and splits summary versus active. A per-session summary-body hash (sessionId:summaryHashdedupe key on a newpost_compact_auditcontinuity record) ensures only a genuinely new compaction triggers an audit, so re-running each turn cannot churn the prompt cache. Locale-agnostic keyword probes (length-based, no stopword list) classify each pinned and static anchor as survived, drifted, or absent; only drifted anchors (present in the summary, gone from active) are re-asserted viaappendPinnedContext, so survivors cost zero tokens. Fail-open throughout (every recoverable error lands inerrors, never thrown), with a bounded drift log. Surfaced as theo2b brain post-compact-auditCLI verb and gated bypost_compact_survival_audit(default OFF, env/config), with--forcefor diagnostics; unchanged installs stay byte-identical.
Changed
Interrupted sessions are captured instead of dropped (
t_interrupted_capture). Since Hermes PRs #50004/#50003/#50312, a SIGHUP/SIGTERM/force-quit/restart-drain now flushes the in-flight transcript and fireson_session_end(interrupted=True). Previously Open Second Brain swallowed theinterruptedflag into_kwargsand these sessions left no memory trace.session-lifecycle.tsnow threads an absent-by-defaultinterruptedflag throughNormalizedPayload; on an interruptedSessionEndit consumes the persisted pre-restart transcript via the session adapters and runs its user turns through the same marker/fact extraction as a live prompt. Double-counting on resume is prevented by the existing content-keyed dedupe seams (signaldedup_hashand the fact dedup index).interruptedandtranscript_consumedare recorded honestly in the audit record/result; an unreadable transcript surfacestranscript_consumed: falserather than coercing to a clean close. The Hermesprovider.pysurfaces theinterruptedkwarg fromon_session_endonto the flush payload (absent by default, so a clean close is byte-identical) and makes no capture decision. Thepre-compact-extractpath and thebrain_pre_compact_extractMCP schema accept an optionalinterruptedflag recorded on the continuity record so an interrupted capture is marked honestly. Fail-soft throughout.
1.18.1 - 2026-06-24
Fixed
Cross-vault chain-stop reads the max normalized score, not the first result (
t_23c1b929). The normalized-confidence chain-stop gated onresults[0].score, assuming a score-descending order, butrerankand MMR reorder results by relevance/diversity, so the positional first element is not always the score maximum. It now gates on the maximum normalized score across the origin's results, so an enabled chain-stop short-circuits exactly when an origin truly answers confidently. Off by default, and byte-identical on the default ranking path where the first result already is the maximum.
1.18.0 - 2026-06-24
Recall precision, coverage, and provenance hardening. Six related improvements make recall more accurate, more complete, and more auditable: an end-to-end provenance reader, exact line-span citations, progressive disclosure for all recall, normalized-confidence cross-vault early termination, coverage-driven targeted self-correction, and a configurable chunk floor. Every new decision is deterministic and LLM-free, and each new behaviour is off by default or otherwise byte-identical until opted in.
Added
Configurable chunk minimum size (
t_122b2cbc). The markdown chunker's packing floor (minTokens) is now operator-tunable per vault viasearch_chunk_min_size/OPEN_SECOND_BRAIN_SEARCH_CHUNK_MIN_SIZE, threaded throughchunkMarkdownand the indexer alongside the existingsearch_chunk_sizeandsearch_chunk_overlapknobs. Default100is unchanged, so vaults that do not set it hash identical chunks across Syncthing peers.Event-to-context provenance reader (
t_8da11868). A new read-only correlation-join surface attaches a logged event's context traces via its correlation IDs, so an operator can answer "why did the agent do this" from one surface: theo2b brain event-traceCLI verb and thebrain_event_traceMCP tool. Read-only; joins existing records and writes nothing.Read-time line-span citations (
t_0b83c97b). A sharedLinePointertype andline-numbering.tshelpers resolve recalled content to exactpath:Lstart-Lendspans at read time (no Markdown mutation), and evidence records and session-recall hits now carry those line anchors for sharper citation-depth and answer-containment signals.Progressive disclosure for all recall (
t_468190f5). The compact-card to expand to raw-transcript disclosure already used by session-recall is generalized to the main vault search viadisclosure: "cards", so callers can pay layer-1 token cost up front and drill deeper only when needed. The defaultfullpath stays byte-identical.Normalized-confidence cross-vault chain-stop (
t_23c1b929). Opt-in viasearch_chain_stop_enabled/OPEN_SECOND_BRAIN_SEARCH_CHAIN_STOP:searchAcrossVaultsstops querying further origins once a completed origin's top NORMALIZED[0,1]result score reachessearch_chain_stop_score(default0.8), recording the skipped origins on the newSearchOutcome.chainStop. The gate reads the normalized result score, never the raw lane score, so a tiny-corpus origin with a high raw score never short-circuits. Off by default and byte-identical when no origin is skipped.Targeted retry on partial coverage (
t_8eb5ca32). In evidence-pack mode, when the first pass returns candidates but their IDF-weighted coverage of the query is belowCOMPLETENESS_COMPLETE_THRESHOLDwith at least one RARE significant term still uncovered,search()issues exactly ONE follow-up FTS pass built from those uncovered rare terms (the specifically-missing high-signal facts), merges the recovered candidates into the pool, and lets them flow through the normal ranking and a regenerated evidence pack. The new pure helperplanTargetedRetry(coverage)(incoverage.ts) is the deterministic trigger: it fires only on a below-threshold coverage WITH uncovered rare terms (a rare gate keeps it off when only corpus-common terms are missing) and returns exactly those terms to re-query. This retry is mutually exclusive with the existing zero-candidate broadened OR retry (one needs an empty pool, the other a non-empty one), so at most one retry fires per query — the same single-retry discipline. The regenerated pack still abstains on any term left uncovered after the retry, so the conservative-on-final-miss posture is unchanged.SearchOutcome.secondPass.kind(t_8eb5ca32). The two-pass marker now carrieskind: "broadened" | "targeted"to distinguish the two triggers, plustargetedTerms(the uncovered rare terms re-queried) for the targeted variant. Recovered results carry asecond_pass: targeted retry on uncovered rare termsreason; first-pass hits they merge with do not.
Fixed
MCP search output contracts. Aligned the MCP search tool output with the new line-span and disclosure result shapes so the typed-relation and evidence fields surface consistently across the CLI and MCP surfaces.
1.17.0 - 2026-06-21
CodeGraph link-graph depth and MCP exposure. A set of strictly additive leaves inspired by Graphify: richer Markdown link resolution, MCP transport surfaces, and an explicit offline-first guarantee for keyless processing. No default behavior changes when the new options are unused, and the kernel still calls no LLM.
Added
Reference-style Markdown links become link-graph edges (
t_13c92d85). The deterministic link extractor now resolves CommonMark reference definitions into the existingmarkdown_linkrow shape: full ([text][label]), collapsed ([text][]), and shortcut ([text]) references match labels case-insensitively with collapsed whitespace, reuse the same external-URL /mailto:filtering and#anchorstripping as inline links, and ignore image embeds and code fences. Inline-link behavior remains unchanged while reference-heavy notes now contribute real graph edges.Standalone
graphify-mcp-style console entry (t_da6321a9). Packages now expose ano2b-mcpbin that mirrors the existingo2blauncher and injects themcpsubcommand, soo2b-mcp --vault Xis equivalent too2b mcp --vault X. The shim is transport-agnostic and forwards all flags verbatim, including the new HTTP transport flags.Streamable HTTP MCP transport with API-key auth (
t_31dfae18).o2b mcpaccepts--transport stdio|http(defaultstdio) plus--host,--port, and--api-keyfor HTTP. The HTTP transport is a thin additive peer to stdio: it authenticates every request with a generic constant-time API-key check, rejects unauthenticated traffic with the same401 Unauthorizedresponse, rejects JSON-RPC batches per the 2025-06-18 MCP contract, supports JSON or single-event SSE responses based onAccept, and dispatches every accepted request through the existingMCPServer.handleRequestcore.Explicit offline/deferred backend resolution for indexing (
t_85252236). The structuredIndexStatsnow declares which backend processed a run:backend: "offline"when only the deterministic lexical pipeline ran and no provider credentials were resolved, orbackend: "semantic"when the embedding backend was actually engaged. An accompanyingdeferredReasonstring explains why the semantic backend was not used (embeddings not requested, semantic search disabled, orembedding_api_keynot configured); it is null on a semantic run. Backend resolution is lazy - the credential check stays inside the explicitly requested embedding path, so a deterministic-only corpus indexes to completion with no key and never hard-fails for a missing credential. Borrowing Graphify's offline-first idea, this makes the already-keyless code path an explicit, tested guarantee rather than an implicit side effect:importSessionis verified to never read provider-credential environment variables, and keyless indexing is verified to reportoffline. The new fields are additive; existingIndexStatsfields are byte-identical when no option changes.
Memory subsystem alignment: the write paths the Hermes memory provider
declares now match the live host memory semantics. The release makes
pinned-context budget handling honest, adds atomic batch writes for
pinned context and continuity, and connects the on_memory_write host
bridge so the vault is the durable backing store for native built-in
memory writes. No new LLM, search, dream, or recall behavior; the
storage model stays vault-first Markdown and JSONL.
Added
Atomic batch writes for pinned context and continuity. An agent can now free space and add content in one turn without a partial write leaking to disk.
brain_pinned_contextaccepts an optionaloperationsarray of orderedwrite/append/clear/replacesteps; the whole batch is projected and validated in memory first, and a malformed operation, an absentreplacetarget, or an over-budget final state throwsPinnedBatchErrorbefore any write, leavingBrain/pinned.mdbyte-for-byte unchanged. Successful writes return a terminaldonemarker so an agent does not redundantly re-call. The continuity store gainsappendContinuityRecords, which builds and validates every record before any disk write and then appends per month shard under a single lock; single-month batches are fully atomic, while cross-shard atomicity is deliberately not guaranteed and is documented as such. Single-operation pinned and continuity writers stay byte-identical when nooperationsarray is passed.Hermes
on_memory_writehost bridge. The provider declared theon_memory_writememory-provider hook but only mirrored writes tobrain_note; the vault was never the durable backing store for native host memory writes. The hook is now a thin adapter over the shared continuity substrate:plugins/hermes/provider.pyforwards verified(action, target, content, metadata)writes to the newbrain_memory_bridgeMCP tool (a deliberate v1.x surface addition, pinned in the parity guard; hidden tools are banned by the 1.0.0 sweep), which delegates to a deterministic core that persists each write as ahost_memory_writecontinuity record viaappendContinuityRecord(s). The contract was verified against Hermes source (on_memory_write(action, target, content, metadata=None), action in{add, replace}, target in{memory, user}, batches decomposed host-side so a provider never receives an array) before implementation. Unknown action, unknown target, and empty content are rejected explicitly with nothing written; the path is fail-soft and a no-op when the host never calls the hook. The tool is advertised but kept out of the provider'sMEMORY_TOOLSallowlist, so the Hermes agent surface is unchanged and only the hook calls it.
Fixed
Pinned-context budget is honest. Single-operation
write/appendonbrain_pinned_contextpreviously normalized pinned content throughsanitiseTextField'smaxLencap, silently dropping the tail and returningdone: true: a deceptive truncated-success the project brief forbids.writePinnedContext/appendPinnedContextnow enforce the budget explicitly through a sharedassertWithinPinnedBudgetthat throwsPinnedBatchError(code: budget_exceeded) before any write, carryingoperation,length,budget,over_by, and a consolidate-then-retry hint; the MCP layer maps it to a structuredINVALID_PARAMS, mirroring the batch path (which now reuses the same assertion). Within-budget writes are byte-for-byte unchanged.
1.15.0 - 2026-06-19
Added
Calendar-aware obligations and agenda synthesis. Two deterministic, vault-native surfaces inspired by the obsidian-second-brain calendar commands, built to Open Second Brain's contract: the kernel never reaches a calendar API or calls a model - the host runtime (e.g. the google-workspace skill) fetches events and the Brain owns the durable, deterministic record and analysis.
Recurring obligations (
brain_obligation,o2b brain obligation). First-class Brain pages underBrain/obligations/<slug>.mdtracking a periodic commitment (weekly review, monthly report, quarterly audit) with a cadence and a deterministically computednext_duedate.addcreates the page (next-due starts at the anchor),donerecords a completion and advances next-due by exactly one cadence interval from the completion date,list(optionally--overdue) sorts by next-due with an overdue flag and days-until-due,showreads one,removearchives intoBrain/obligations/archive/. Cadences:daily,weekly,biweekly,monthly,quarterly,yearly, andevery-<N>-days; month-based cadences clamp to the last day of short months (Jan 31 + 1 month -> Feb 28/29). Markdown-first and operator readable in Obsidian; cadence arithmetic is pure UTC calendar math, so the same inputs always yield the same next-due date.Agenda synthesis (
brain_agenda,o2b brain agenda). A stateless analysis over caller-provided calendar events (a JSON array, or piped on stdin for the CLI): overlap conflicts between events, free focus blocks (the gaps a scheduler would slot work into, optionally clipped to a--workday-start/--workday-endwindow), and external-organizer flags for events organised outside the operator's own email domain(s). Pure function of its input - no vault writes, no clock, no model - so a given event list always yields the same snapshot.MCP surface. Adds
brain_agendaandbrain_obligationto the frozen tool set; both carry input schemas and the deliberate surface-change is pinned in the parity guard.
Open Knowledge Format (OKF) export and import. A portable, producer-agnostic bundle for moving a Brain (or part of one) between vaults and producers, with a deterministic export-then-import round-trip and a review gate for untrusted sources.
Export (
brain okf-export,o2b brain okf-export). Writes a self-contained OKF bundle to a directory:concepts/,queries/, andreferences/pages plus a date-groupedlog.mdand anokf.jsonmanifest. Read-only on the vault;--forceoverwrites a non-empty target directory.Import (
brain okf-import,o2b brain okf-import). Reads an OKF bundle directory. By default pages land underOKF Review/stampedokf_review: pendingas review candidates;--trustedwrites each page straight to its recorded vault-relative path. Foreign-producer bundles get producer and raw-type provenance stamped, with producer-specific (x-*) frontmatter preserved.
Obsidian Bases view definitions stamped at vault init. Four native
.basefiles are now written intoBrain/bases/whenever a vault is bootstrapped (o2b brain init), giving operators structured, performant views over the Brain collections without any Dataview plugin dependency. Inspired by the obsidian-second-brain/obsidian-projects+ Bases templates work, adapted to Open Second Brain's real frontmatter rather than copied verbatim:projects.base→ canonical entities withcategory: project(Brain/entities/project/), with Active / Archived / All table views, a status icon, and a "stale since update" formula.people.base→ canonical entities withcategory: person(Brain/entities/person/), surfacing optional operator-addedrole/companycolumns alongside status and freshness.tasks.base→ recurring obligations (Brain/obligations/), ordered by anoverdueformula andnext_due, with days-until-due.daily.base→ log days (Brain/log/) as a date-sorted table plus a calendar view.Files are inert structural scaffolding (no plugin required; ignored by editors that do not render Bases) and are stamped like the operating manual: written only when absent, overwritten under
--force, never clobbering operator edits on a plain re-run. The templates ship undersrc/core/brain/templates/bases/so they travel with the publishedsrc/tree.
Strongest-objection steelman in synthesis outputs. Both the
osb://topic/{slug}resource andbrain_deep_synthesisnow surface the single best-formed argument against their own implicit conclusion, rather than only enumerating tensions within the source material. Inspired by obsidian-wiki's steelman section, adapted to Open Second Brain's deterministic-core contract (no generated prose — the strongest counter-finding is selected by fixed priority and framed as a seed for the calling agent to develop).brain_deep_synthesis(ando2b brain deep-synthesis) add astrongest_objectionfield withbasis(contradiction → superseded → stale → knowledge_gap → thin_evidence),statement, andsource_artifacts. It isnullonly for a larger internally-consistent body, andstrongest_objectionjoins the dossier'scheckeddimensions.osb://topic/{slug}gains an always-present Strongest objection section steelmanning the current preference: a previously-retired rule, a quarantined rule, a recorded negative counter-signal, or an unconfirmed-trial caveat, falling back to an explicit "no standing objection" line.
1.14.0 - 2026-06-18
Added
Configurable default scope for feedback signals (
feedback.default_scope). A vault-local default applied tobrain_feedback/o2b brain feedbackwrites that pass no explicitscope, so agent-recorded signals can land in a consistent category (for examplecoding) instead of staying uncategorized. The rule is a single precedence at the signal write boundary: an explicit per-call scope always wins; otherwise the configured default is used; otherwise thescopefield is omitted exactly as before. With nofeedback.default_scopeconfigured and no explicit scope, signal output is byte-identical to prior behaviour.Config block. Optional
feedback:block inBrain/_brain.yamlwith adefault_scopestring, validated through the normal Brain config policy against the same constraints as a signalscopefield (non-empty after trim, single-line, at most 128 characters). Invalid values are rejected by config validation and surfaced byo2b brain doctorrather than silently ignored.Parity across surfaces. The effective scope is computed once and reused for the inbox signal, its shared-namespace mirror, and any force-confirmed preference, so a preference never diverges in scope from the signal that produced it. Distinct from the
owner_scoped_factsand vault guardrail settings, which govern fact visibility rather than feedback categorization.
Security
Write-containment backstop at the write-session commit chokepoint. The single point where a write-session lands an agent-supplied artifact now re-resolves its target through
ensureInsideVaultbefore any directory creation, read, or write. The earlier target check is purely lexical and runs at session-open time, so it cannot see a symlinked ancestor (for example aBrain/<symlink>/note.mdwhose ancestor links outside the vault) and is decoupled from the write by the persisted session record. The backstop fails closed: a target that resolves outside the configured vault root is rejected and nothing is written. Audit confirmed every other caller-derived vault path (slugs, ids, device shards, artifacts) already funnels through the guardedpaths.tsconstructors orensureInsideVault, andvalidateSlugcontinues to admit@and+so email-style and plus-addressed identifiers remain valid file basenames while dot-traversal tokens stay rejected. Regression tests pin both invariants.
1.13.0 - 2026-06-16
Added
Per-handoff LLM generation tracing (
generation_report). An additive, opt-in, fail-open way to record the real LLM usage an agent performs on behalf of a brain handoff, without the kernel ever calling an LLM. Open Second Brain owns sequencing and the atomic commit; the calling agent owns generation, so tracing is an INBOUND path: after the agent fulfils a write-session step, a context-pack consume, or a dream-stage proposal, it optionally reports back the usage and Open Second Brain stores it as ageneration_reportcontinuity record.Inbound report surface (
brain_generation_reportsactionrecord,o2b brain generation-reports record). Gated (default off) by a per-callenableflag or thegeneration_trace_enabledconfig /OPEN_SECOND_BRAIN_GENERATION_TRACE_ENABLEDenv. With the gate off no payload is built and nothing is written; a throwing build is swallowed so tracing never fails the primary operation (emitGatedTelemetry).Payload-safe by construction. The handoff prompt is hashed and counted but never persisted - only
prompt_hash(SHA-256 hex) andprompt_chars, plus token counts, reach disk, and the whole payload still passessafeContinuityPayloadredaction. The local token estimate (local_estimate.input_tokens) is always present; the agent-reportedusageblock is present only when supplied and absent is reported as absent, never fabricated.Memory to trace linkage.
sourceRefsjoin each report to the handoff ref (write-session session id, context-receipt id, or dream run id) and the memory paths involved, lifted to first-classhandoffKind/handoffRefread-model fields. Thesummaryread (brain_generation_reportsactionsummary,o2b brain generation-reports summary) rolls up call counts, per-handoff-kind breakdown, the local estimate, reported usage, and a per-path map so a memory path resolves back to the reports that produced or consumed it.Read surfaces.
o2b brain generation-reports list|summary|showand the matchingbrain_generation_reportsactions (listandsummary;showis CLI-only). The kernel never adds an outboundfetch/provider HTTP call - a grep-guarded regression test pins this, and a persisted-file assertion confirms no raw prompt survives. Default behaviour is byte-identical: the write-session envelope, context_receipt, dream_stage metric, and recall_telemetry keep their current shapes when the gate is off. Documented indocs/observability.md,docs/cli-reference.md, anddocs/mcp.md.
Structural prompt-prefix stability (
prompt_prefixmetric). A deterministic prefix layer plus a run-level metric that reports how stable the prompt preamble was across the repeated generation handoffs of one brain operation. Open Second Brain cannot port Hindsight's provider prompt-prefix caching verbatim - the kernel never calls an LLM, so it has no outbound request to cache - but it can guarantee the precondition a provider prefix cache rewards: a byte-stable, cache- eligible prefix across a pass.Deterministic prefix helper (
src/core/brain/prompt-prefix.ts).deterministicPrefixcertifies a stable preamble (prefix, full SHA-256hash, code-pointchars) from stable inputs only - no clock, no random, sorted keys viacanonicalSegment. The decision- panel builder routes its sharedDecision topic:frame through it; the bytes of the persona and synthesis prompts are unchanged.Run-level
prompt_prefixmetric. A decision-panel commit (opt-inpromptPrefixMetric) and a context-pack consume (opt-inpromptPrefix) emit one record underBrain/metrics/prompt_prefix.jsonlcarryingkind,prefix_hash,prefix_chars,call_count, andstable_count. The reader is surface-agnostic (nometrics.tschange), exactly likedream_stage.Stability, not cache-hit rate. The metric measures STRUCTURAL prefix stability (what the kernel handed the agent), never a provider's cache-hit rate (which the kernel cannot observe). The raw prompt is never stored - only its hash and length. Emission is opt-in and fail-soft; with the gate off no record is written and the panel commit, write-session envelope, and context_pack report are byte-identical. Documented in
docs/metrics.mdanddocs/observability.md.
1.12.0 - 2026-06-15
Added
CodeGraph and MCP Operational Readability. Additive, deterministic operator surfaces over the optional Codegraph partner and vault community detection. With no new option in use, existing reads stay byte-identical and the kernel never calls an LLM.
CodeGraph report surface (
o2b partner codegraph report,brain_codegraph_report). A read-only report that resolves the in-scope code project, reports the partner index state (no_project,absent,not_indexed,indexedwith node/file/edge counts, orerror), and structurally parsesCargo.tomlfor Rust workspace members. It never installs, initializes, extracts, or mutates a codegraph or Graphify index: a missing CLI, a missing.codegraph/index, and a non-Rust project are all honest report states (the latter returnscargo_workspace: nullwith an explicit reason) rather than failures or silent no-ops. The report is schema-versioned (schema_version: 1) and the same shape backs both the CLI (--json) and the MCP tool. This surfaces the operationally useful kernel of Graphify's Cargo workspace work (workspace membership) without adopting the extraction binary as a dependency or adding acrate_depends_onedge type to the Open Second Brain graph schema.Batched community materialization (
o2b brain clusters run --batch-size N,brain_clustersbatch_size). Borrowing Graphify's multi-batch labeling for large graphs, cluster note materialization can now run in fixed-size, order-preserving chunks. Batched mode returns the same community summaries plus abatchesarray, each entry carryingindex,start,end,written,removed, and an optionalerror; a batch that fails is isolated and reported instead of dropping the whole pass. The stale sweep keys off the full detected set, so a failed batch leaves its prior note in place rather than deleting it, and the single global sweep is attributed to the final batch. Detection stays deterministic and label-free (no LLM labeling, no natural-language classification, no Graphify dependency). Without--batch-size/batch_sizethe run is byte-identical to before, and invalid sizes are rejected with the usual usage/INVALID_PARAMSerrors.
1.11.0 - 2026-06-14
Added
Session Knowledge Synthesis Suite. Three additive, deterministic, language-agnostic units that turn temporal activity into structured, queryable, provenance-traced knowledge. Every unit is a new surface; with no new surface in use, reads are byte-identical to before, and the kernel never calls an LLM (agent-extracted data is stored verbatim; everything else is a pure read).
Structured session summary (
o2b brain session-summary,brain_session_summary). A session-scoped digest over four canonical categories - request, decisions, learnings, next_steps - stored as one append-only continuity record (session_summary_digest), distinct from thesession_summary_noderecall rollup and the per-linepre_compact_extract. The agent supplies the already-extracted categories; the kernel validates, dedupes by content hash, and appends, never parsing prose into categories. An all-empty digest is rejected; absent a write, reads return null.Idea-lineage provenance tracer (
o2b brain idea-lineage,brain_idea_lineage). A read-only tracer reconstructing how a derived artifact was reached as an observation -> synthesis -> conclusion graph. A continuity id walks thesourceRefsgraph (raw turns are observations, summaries/extracts/digests synthesis, the queried record the conclusion), resolving edges by record id or turn id, cycle-guarded and depth-bounded; a preference id adapts the existingbuildBeliefEvolutionlifecycle into the same shape. Unknown ids fail with a typed error rather than a silent empty chain.Episodic note-history decomposition (
o2b brain note-history,brain_note_history). Splits a note's git commit chain into recallable episodic phases on a deterministic commit-time gap (default 72h) - a language-agnostic rule that never depends on commit-message wording. Each phase carries the commit subjects, dates, and authors. A missing repo reportsavailable: false(no fabricated phase); a path with no commits reportsavailable: truewith zero phases (empty is distinct from broken). The sanitized read-only git reader gains an additivepathpathspec filter.
1.10.0 - 2026-06-14
Added
Recall & Working-Memory Quality Suite. Four additive, deterministic, language-agnostic units on top of the mature search/recall subsystem. Every unit is flag- or surface-gated; with no profile, flag, or new surface in use, search and continuity reads are byte-identical to before.
Selectable recall profiles (
o2b search --profile,brain_searchprofilefield). Named presetsfast | balanced | thoroughexpand to a fixed knob tuple over the same bounded axes the self-tuning grid ranges over (candidate pool, traversal depth, learned weights, query expansion) and are applied through the sameapplyTunedParametersmachinery, so a profile and a learned grid point stay coherent. An explicitly selected profile takes precedence over a persisted self-tuning grid point; an unknown profile name fails loud with a typedSearchError. No profile selected leaves ranking on the existing config path, bit-for-bit.Usage-driven working-memory decay (
o2b brain continuity rank). Continuity records are append-only and immutable, so decay is a pure read-side weight in(0, 1], never a mutation.decayWeightcombines record age (exponential half-life) with a usage signal derived only from existingrecall_telemetrysurfaced-artifact references; a record whose sources were never surfaced - such as a session-scopedpre_compact_extractdecision - has no usage and decays by age alone, with no fabricated coupling.ranklists working-memory records freshest and most-recalled first.Language-agnostic co-occurrence auto-relate (
o2b brain co-occurrence). Entities repeatedly co-referenced from the same notes are scored with a structural PMI / document-frequency metric over the wikilink graph - link incidence only, no natural-language word list in any language, so a non-Latin vault scores identically for the same structure. Output is a derived, schema-versioned, hashed suggestion artifact (re-validated on read, fail-soft); notes are never mutated and an already directly-linked pair is never re-suggested.--writepersistsBrain/link-graph/co-occurrence.json.File-context recall (
o2b brain file-context,brain_file_contextMCP tool). Given a file path, surface prior vault work that mentions it by querying the existing index with terms derived structurally from the path (basename + stem, no natural-language processing, no LLM). A size gate skips trivial files (default 1500 bytes) and returns an explicit reason rather than a fabricated empty hit.
1.9.0 - 2026-06-14
Added
Brain Portability & Interop Suite. Brain content becomes portable in and out of a vault, with provenance, and programmatically writable - composing the existing portability helpers rather than adding a new subsystem. Every unit is an additive new surface; no existing exporter, importer, CLI verb, or MCP tool changes behaviour.
Whole-vault bank export/import (
o2b brain bank-export/bank-import).exportBankBundlecomposes the existing exporters - preferences, the page link-graph, the page interchange contract, and the read-only sources dashboard - into one deterministic, schema-versioned envelope for backup, cross-instance migration, or downstream-tool ingest.importBankBundlereconstructs the part that round-trips (the page graph, delegated toimportVaultGraphunder a conflict mode) and reports preferences, page contracts, and the sources dashboard as carried-not-restored - no silent partial restore. An unsupported bundle schema fails loudly with a typedBankImportError.Page interchange contract (
projectPageContracts). A pure, read-only projection of every user vault page to a stable, schema-versioned record (path,kind, advisoryconfidence/provenance, flattenedcitations,aliases,freshness) a downstream importer can consume without knowing Open Second Brain internals. Derivation is structural only (frontmatter fields, body wikilinks, typed-relation targets, file mtime); an absent advisory field is reported asnull, never synthesised.brain_create_noteMCP tool. Writes an actual vault note file (path + frontmatter + content) atomically throughensureInsideVault- distinct frombrain_note, which only appends a log line. Refuses path traversal, the Brain machinery root, vault-scope-excluded paths, and overwriting an existing note, each with a typed error mapped to INVALID_PARAMS.In-process SDK (
createBrain(vault)). A thin façade over the existing core functions - bank export/import, graph export/import, preference export,ingestSource, andcreateNote- plus source-backed reads (listSources/getSource/deleteSource) over thekind: brain-sourcesummary pages. Every method is a one-line delegation; the upstreamwriteStatusmaps toingestSource(Open Second Brain has no separate source status lifecycle). A source id that resolves outsideBrain/sourcesis treated as not-found and never deleted.
1.8.0 - 2026-06-13
Added
Indexer Durability & Resilience Suite. Interrupting a long index run no longer risks losing work or wedging the index. The suite makes cancellation cooperative, the watcher shutdown graceful, and a full rebuild resumable - reusing the existing
Safeguardand per-vault lock rather than adding a new lifecycle subsystem. Every behavioural change defaults to today's behaviour; a vault that sets nothing is byte-identical.Cooperative abort (
Safeguard+AbortSignal). The cooperative deadline gains an optionalAbortSignal: onecheckpoint()now trips on either an aborted signal (a newSafeguardAbortError, checked first) or the existing timeout. The signal threads throughindexVaultandpopulateEmbeddings, checked at the same boundaries the deadline uses - between files and between embed batches, never mid-write - so a run can be cancelled on demand. Bun's SQLite is synchronous, so abort stays cooperative; the deletion sweep runs only on full completion, so an aborted run leaves a consistent, partially-refreshed index.Graceful
o2b search watchshutdown. SIGINT/SIGTERM now stops accepting new flushes, aborts the in-flight pass, and awaits it to settle at a cooperative boundary before exiting - bounded bysearch_shutdown_grace_seconds(default 5). A signal no longer kills a pass mid-write. The flush/shutdown coordination lives in a testableIndexWatchRunner; a second signal falls back to the default terminate.0grace exits immediately after signalling.Opt-in resumable reindex (
search_resume_reindex). An interrupted fullreindexVaultrebuild no longer discards all progress: a compatible in-progressbrain.sqlite.newstaging build is resumed via the incremental fastpath instead of rebuilt from scratch. Resume is gated on a signature marker (schema version + chunk parameters + embedding signature) stored in the staging DB'sindex_stateKV - no schema migration - so a drifted or unreadable staging DB is discarded and rebuilt, never trusted. The marker is cleared before the atomic swap, so the live index never carries staging state. Default off keeps the always-fresh rebuild.Writer-lock heartbeat + WAL-flush-on-exit. The async writer lock refreshes its mtime mid-run (explicit heartbeat below the 60s stale window) so a long index is never mistaken for a stale lock. A process-exit registry consolidates each open writer's WAL on a bypassed
close(), mirroring the existing sync-lock cleanup hook.
Honest multi-instance story (no new daemon): the MCP server is stdio-only, so isolation comes from the per-
dbPathwriter lock. Two instances on different vaults run conflict-free; a second writer on the same vault gets a typedINDEX_LOCKED. Pinned by tests; no--port/--instancemodel was fabricated.
1.7.0 - 2026-06-13
Added
Knowledge Provenance Suite. Six features share one theme: every piece of knowledge in the brain carries its origin (which source, which premises, which owner, whether stated or inferred), and the operator can steer what surfaces. All generation lives on the agent side of the MCP boundary - Open Second Brain is provider-agnostic and bundles no model. Every behavioural change is opt-in; a vault that enables nothing is byte-identical in results, ordering, and shape.
Source-ingest pipeline (
brain_ingest_source). One text-bearing source (document / note / URL text) becomes entity and concept pages plus a per-source summary page that backlinks the source, lists the entities it introduced, and lists its connections to pre-existing material. Idempotent on the source path. No OCR or binary/media path.Parameterized research pipeline (
brain_research_report). N consulted sources plus an agent synthesis become one dated report page where each finding cites the source that flagged it. A finding with no source, or one citing an unconsulted source, is rejected - no uncited claims.Model-based entity intake (
brain_intake_entities). The agent extracts entities from note text (it owns the recognition; no ML model is bundled) and submits them; Open Second Brain validates the typed payload and commits it through one shared, idempotent intake into the entity registry. Opt-in and non-blocking: a plain note write never triggers it.Derived-fact synthesis (
brain_derive_fact). The agent reasons a second-order conclusion from existing premises; Open Second Brain validates each premise exists and commits an unconfirmed preference carrying adeduced/inferredprovenance level and premise links. Opt-in behind thederived_fact_synthesisguardrail. Recall trust ordering (provenance_trust_orderingguardrail) ranks stated > deduced > inferred in the active digest.Owner-scoped canonical facts. A preference may declare an
owner:token;brain_querygains anagent_scopeargument (gated by theowner_scoped_factsguardrail) that returns an owner-private fact only to its owner and keeps shared facts visible. Reuses the v1.6 owner-visibility model; fails closed.Operator-editable standing-query attention layer. The attention-flows mechanism gains a
standing_queryaction: an operator declares scope tokens in a flow doc and the matching confirmed preferences always surface into the assembled context. Structural selector, language-agnostic.
1.6.0 - 2026-06-13
Added
Vault Integrity & Trust Suite. Five language-agnostic units that harden the trust and identity boundaries of the vault. Every behavioural unit is opt-in, so a vault that enables nothing keeps byte-identical results and output shape; the one unconditional change (NFC path identity) is idempotent on already-NFC input, so the dominant platform is unaffected.
Untrusted-source containment. A new guardrail flag
untrusted_source_delimiting(default off) wraps each untrusted memory body surfaced into an agent-facing context pack in a provenance-carrying<untrusted_source path="..." sha256="...">delimiter and structurally neutralizes it (invisible/control characters stripped, delimiter breakouts escaped) instead of matching it against an English-only injection blocklist and blanking it. Lossless and identical across every language: neutralization keys off structure, never a natural-language word list.Canonical NFC note-path identity. The vault walker funnels every note's vault-relative identity through
canonicalNotePath(POSIX + Unicode NFC), so the same note has one identity across a macOS (NFD) / Linux (NFC) Syncthing peer set. Kills the cross-device re-index churn and phantom duplicates that a byte-different path produced. Idempotent on already-NFC input.File-watcher index sync.
o2b search watchwatches the vault for.mdedits and incrementally re-indexes after a debounced quiet window, reusing the existing incrementalindexVaultfastpath. A single-flight guard prevents overlapping passes; recursivefs.watchunavailability fails loudly with guidance rather than silently no-op-ing. No new dependency.O(1) graph stats/queries. A precomputed link-graph side-index (resolved adjacency, degree, top-degree) is memoized on the store and invalidated on the index revision, so community detection and the new
brain clustersgraph summary stop rebuilding the adjacency on every call. Byte-identical community output; in-memory only, never persisted.Agent-scoped recall isolation.
brain_searchaccepts anagent_scopeinput: a page that declares anowner:frontmatter token is returned only to its own owner, while shared (ownerless) pages always match. Absent scope = no ownership filtering, so existing callers are byte-identical. Fails closed on an unparseable page under an active scope.
1.5.0 - 2026-06-13
Added
Search & Recall Quality Suite. Seven language-agnostic enhancements to the retrieval core. The five
brain_searchenhancements are each gated behind their own flag, so a call that sets none of them returns byte-identical results, ordering, and shape to before; the hybrid-degrade warning fires only in a genuinely degraded state, andbrain_evalis a new read-only tool.Structured score breakdown.
brain_searchacceptsexplain: trueand returns a per-resultscore_breakdownobject (keyword, semantic, rrf, entity, activation, coAccess, link, recency, tier, trend, sessionFocus) alongside the existing human-readablereasons[]. The ranker now emits the breakdown first-class, and the learned-weights fold reads it directly instead of re-parsing reason strings.Hybrid-degrade warning. When a caller wants the semantic lane but it cannot run (missing embeddings, unloaded vec extension, unconfigured key), the result set carries one greppable
hybrid_degraded:warning instead of silently serving keyword-only. A keyword-only-by-choice query and a semantic-disabled vault never carry it.Inline trust metadata.
brain_searchacceptstrust: trueand stamps each hit with a computed-at-read-timetrustobject:age_daysfrom the document mtime, plussupersededandconflictderived from thesuperseded_by/contradictstyped relations the recall path already surfaces. Never stored.Relevance threshold and rerank.
threshold(a[0,1]score floor applied before the diversity rerank) drops weak hits so an irrelevant query returns no match instead of noise;rerankre-orders the qualified set by core textual relevance. Both off by default.Self-tuning reinforce.
reinforce: [paths]records proven-useful memories to a conflict-free one-file-per-signal ledger underBrain/search/reinforce/and lifts ledger-strong memories by a bounded boost before the top_k cut. Surfaced-only frequency is never recorded and never boosts; the ledger is resettable.Answer-containment@k benchmark metric. The recall benchmark accepts an optional per-query
answerand reportsanswerContainmentAtK- whether the answer text appears (folded substring) in the retrieved content within the top k. The committed fixture gains answer-bearing queries with a CI floor; datasets withoutanswerkeep hit@k / MRR behaviour unchanged.brain_evalMCP tool. A read-only tool that scores retrieval quality over a caller-supplied dataset against the active vault, returning hit@k, MRR, answer-containment@k, source-utilization@k, citation-depth, and a source-warnings count a CI gate can cap. The fast path needs no embedding key.
1.4.0 - 2026-06-12
Added
Native Grok Build (xAI
grok) integration.o2b install --target grok --applyregisters the two Open Second Brain MCP servers in${GROK_HOME:-~/.grok}/config.tomlunder[mcp_servers.*](grok's highest-priority MCP source) and writes lifecycle hooks to${GROK_HOME:-~/.grok}/hooks/open-second-brain.json(grok's native hooks dir), each with an absolutebun run <repo>/src/cli/main.ts …command. Verified against live grok 0.2.45 (session debug log): grok spawns MCP and hook processes with a restricted PATH that excludes~/.local/bin, so a bareo2bcommand fails to spawn - the absolute form is what actually handshakes in a session (71 + 5 MCP tools registered; active-context inject, post-write reminder, session capture, and the stop-log guardrail dispatched). The shared hook layer normalizes grok's camelCase stdin payload (hookEventName/sessionId/toolName/toolInput) into the internal shape, detects thegrokruntime, and counts grok'ssearch_replaceas a file-mutating tool. A newgroksession adapter imports grok's ACPupdates.jsonlstream into the Brain viao2b brain import-session(autodetected). The integration uses grok's own config, not the~/.claude/namespace. Seeinstall/grok.md.Per-runtime host-qualified Brain identity. Every runtime that registers the MCP servers (grok, opencode, openclaw, and Codex via
codex mcp add) now attributes its Brain writes to its OWN host-qualified name rather than the shared operatoragent_name. The host segment of the operator name is kept and the vendor token is swapped to the runtime's own id (claude-vps-agent->grok-vps-agent/opencode-vps-agent; a name outside the<vendor>-<host>-agentshape is prefixed with<runtime>-). This keeps a shared multi-device vault (e.g. Syncthing across a VPS, a dev box, and a Mac) able to tell the same runtime apart both across runtimes and across devices. The derived name is read from the payload's operator name, soapplyandverifyproduce the identical value ando2b install --checkreports no drift after an apply. For grok, both theconfig.tomlMCP env and the hooks file carry the same derived name.
1.3.1 - 2026-06-11
Fixed
Hermes provider returned a dict where the contract requires a string (
plugins/hermes/provider.py):handle_tool_callforwarded the raw MCPtools/callresult dict straight back to Hermes, which feeds it to the model as tool-message content. Lenient providers (Anthropic) accept a dict, so the bug stayed hidden; strict ones (DeepSeek) reject it with HTTP 400 (content should be a string or a list) on the next API call. The bridge result is now serialized at the boundary through a single_as_tool_contentseam (lossless JSON,default=str, string results passed through), andhandle_tool_callis typed-> strto match the base-class contract. Contract tests now assert the return type against every result shape, closing the gap that let the leak through unnoticed. Reported in #82.
1.3.0 - 2026-06-10
Continuity, Hygiene & Freshness Suite: one conversation stays one session across host context compressions, and the Brain gains a remediation pipeline for the entropy it accumulates - contested facts, near-duplicate rules, stale derived pages, never-recalled memories. Two kernels carry the release: a session-lineage resolver consumed by capture, recall, and the new anticipatory cache, and a hygiene findings pipeline composing pure detectors behind one scan/apply surface.
Native opencode integration: the install adapter now writes the config opencode actually reads, and a bundled plugin brings the runtime to near-parity with the Claude Code and Codex integrations - live active context injection, lifecycle-driven session capture, and the post-write logging reminder.
Added
Bundled opencode plugin (
plugins/opencode/open-second-brain.ts): a single zero-dependency file installed into~/.config/opencode/plugins/byo2b install --target opencode --apply. On each chat request it appends the renderedBrain/active.mddigest to the system prompt (via the sameo2b-hook active-injectshim the Claude Code and Codex hook layers use, cached with a 5 minute TTL); onsession.idle/session.compacted/session.deletedit snapshots the session as a deterministic JSONL spool under${XDG_DATA_HOME:-~/.local/share}/open-second-brain/opencode/; after file-mutating tools it appends the standard logging nudge to the tool output. Every hook is fail-soft - a missing vault, missing binary, or SDK error never breaks the opencode session.opencode session adapter (
src/core/brain/sessions/opencode.ts): fourth adapter in the session registry. Imports the plugin-owned spool format (format: 1, originatoropen-second-brain-opencode-plugin) through the sameo2b brain import-sessionflow as Claude, Codex, and Hermes transcripts; a spool written by a newer plugin fails with a versioned error instead of silently dropping fields.Pluggable MCP entry shape in the shared JSON-merge install layer:
createJsonMcpAdapteracceptsserializeEntry/entryEquals, so runtimes whose MCP config schema differs from the default{command, args, env}entry reuse the same detect/plan/apply/verify/uninstall body with drift detection intact. Existing targets (Cursor, kiro, Gemini CLI) are byte-identical.Session lineage kernel (
src/core/brain/lineage/): every session id resolves to a lineage (root id, parent segment, compression depth). Native path reads the new optionalparent_session_id/root_session_id/compression_depthhook-payload fields (ready for upstream Hermes PR #42940); until the deployed host emits them, a conservative interim inference - isolated in one file and markedCRUTCH(t_1459706f)- links a brand-new session to a predecessor only on compression evidence plus same cwd inside a bounded window. Time proximity alone never stitches; every ambiguous case stays flat.Compression-aware session recall: imported session records carry additive lineage payload fields, and
brain_session_grep/brain_session_describeresolve any segment id to the whole stitched conversation. Describe exposes the segment chain with its continuity edges; never-compacted sessions keep the exact pre-lineage shapes.Staged degradation ladder for over-budget recall entries:
recall.degradation: staged
This document is long; the rest is at the source.
512 files · 486 not served here
- .agents/plugins/marketplace.json (not served here)
- .ai-notes/.gitignore (not served here)
- .apb/.gitignore (not served here)
- .apb/config.yaml (not served here)
- .apb/playbooks/osb-feature-release/1.1.0/playbook.yaml (not served here)
- .apb/playbooks/osb-feature-release/1.2.0/playbook.yaml (not served here)
- .apb/playbooks/osb-feature-release/1.3.0/playbook.yaml (not served here)
- .apb/playbooks/osb-feature-release/1.4.0/playbook.yaml (not served here)
- .apb/playbooks/osb-feature-release/current (not served here)
- .apb/playbooks/osb-feature-release/lifecycle (not served here)
- .apb/playbooks/osb-feature-release/meta/1.1.0.yaml (not served here)
- .apb/playbooks/osb-feature-release/meta/1.2.0.yaml (not served here)
- .apb/playbooks/osb-feature-release/meta/1.3.0.yaml (not served here)
- .apb/playbooks/osb-pr-prepare/1.0.0/playbook.yaml (not served here)
- .apb/playbooks/osb-pr-prepare/1.0.0/scripts/arch-gate.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.0.0/scripts/check-context.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.0.0/scripts/check-pr.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.0.0/scripts/check-release.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.0.0/scripts/ci-wait.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.0.0/scripts/lib.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.0.0/scripts/qa-gates.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.0.0/scripts/zg-index.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/playbook.yaml (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/scripts/arch-gate.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/scripts/check-context.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/scripts/check-pr.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/scripts/check-release.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/scripts/ci-wait.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/scripts/final-gates.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/scripts/lib.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/scripts/qa-gates.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.1.0/scripts/zg-index.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/playbook.yaml (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/scripts/arch-gate.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/scripts/check-context.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/scripts/check-pr.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/scripts/check-release.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/scripts/ci-wait.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/scripts/final-gates.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/scripts/lib.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/scripts/qa-gates.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.2.0/scripts/zg-index.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/playbook.yaml (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/arch-gate.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/check-context.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/check-pr.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/check-release.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/ci-wait.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/cycle-run.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/final-gates.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/lib.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/qa-gates.sh (not served here)
- .apb/playbooks/osb-pr-prepare/1.4.0/scripts/zg-index.sh (not served here)
- .apb/playbooks/osb-pr-prepare/current (not served here)
- .apb/playbooks/osb-pr-prepare/lifecycle (not served here)
- .apb/playbooks/osb-pr-prepare/meta/1.0.0.yaml (not served here)
- .apb/playbooks/osb-pr-prepare/meta/1.1.0.yaml (not served here)
- .apb/playbooks/osb-pr-prepare/meta/1.4.0.yaml (not served here)
- .apb/playbooks/osb-release/1.0.0/playbook.yaml (not served here)
- .apb/playbooks/osb-release/1.0.0/scripts/discard.sh (not served here)
- .apb/playbooks/osb-release/1.0.0/scripts/lib.sh (not served here)
- .apb/playbooks/osb-release/1.0.0/scripts/publish.sh (not served here)
- .apb/playbooks/osb-release/1.0.0/scripts/release-image-style.md (not served here)
- .apb/playbooks/osb-release/1.0.0/scripts/render.sh (not served here)
- .apb/playbooks/osb-release/1.0.0/scripts/render.ts (not served here)
- .apb/playbooks/osb-release/1.0.0/scripts/scope.sh (not served here)
- .apb/playbooks/osb-release/1.0.0/scripts/stage.sh (not served here)
- .apb/playbooks/osb-release/current (not served here)
- .apb/playbooks/osb-release/lifecycle (not served here)
- .apb/playbooks/osb-release/meta/1.0.0.yaml (not served here)
- .apb/playbooks/osb-triage-validator/1.2.0/playbook.yaml (not served here)
- .apb/playbooks/osb-triage-validator/1.3.0/playbook.yaml (not served here)
- .apb/playbooks/osb-triage-validator/current (not served here)
- .apb/playbooks/osb-triage-validator/lifecycle (not served here)
- .apb/playbooks/osb-triage-validator/meta/1.2.0.yaml (not served here)
- .apb/playbooks/osb-triage-validator/meta/1.3.0.yaml (not served here)
- .apb/playbooks/osb-upstream-watch/1.0.0/playbook.yaml (not served here)
- .apb/playbooks/osb-upstream-watch/1.0.0/scripts/materialize-collector.sh (not served here)
- .apb/playbooks/osb-upstream-watch/current (not served here)
- .apb/playbooks/osb-upstream-watch/lifecycle (not served here)
- .apb/playbooks/osb-upstream-watch/meta/1.0.0.yaml (not served here)
- .apb/profiles/osb-collector/SOUL.md (not served here)
- .apb/profiles/osb-collector/profile.yaml (not served here)
- .apb/profiles/osb-developer/SOUL.md (not served here)
- .apb/profiles/osb-developer/profile.yaml (not served here)
- .apb/profiles/osb-reviewer/SOUL.md (not served here)
- .apb/profiles/osb-reviewer/profile.yaml (not served here)
- .apb/profiles/osb-validator/SOUL.md (not served here)
- .apb/profiles/osb-validator/profile.yaml (not served here)
- .claude-plugin/marketplace.json (not served here)
- .claude-plugin/plugin.json
- .codex-plugin/plugin.json (not served here)
- .codexignore (not served here)
- .gitattributes (not served here)
- .githooks/pre-commit (not served here)
- .githooks/pre-push (not served here)
- .github/dependabot.yml (not served here)
- .github/scripts/ci-scope.sh (not served here)
- .github/scripts/windows-timings.sh (not served here)
- .github/windows-test-timings.json (not served here)
- .github/workflows/ci.yml (not served here)
- .github/workflows/plugin-scanner.yml (not served here)
- .github/workflows/release.yml (not served here)
- .gitignore (not served here)
- .oxfmtrc.json (not served here)
- ATTRIBUTION.md
- AUTOGINEER-RUNS.md
- CHANGELOG.md
- CLAUDE.md
- LICENSE
- README.md
- SECURITY.md
- __init__.py (not served here)
- after-install.md
- bin/o2b-discipline-report (not served here)
- bun.lock (not served here)
- bunfig.toml (not served here)
- cli.py (not served here)
- docs/architecture.md
- docs/brain-store.md
- docs/brainstorm/a-label-is-not-a-boundary/cli-output/claude.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/cli-output/prompt.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/design.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/plan.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/recon/census-lexer.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/recon/conventions-and-release-machinery.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/recon/egress-boundary.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/recon/fleet-and-shared-scope.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/recon/identity-and-attribution.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/recon/mcp-instruction-surface.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/recon/owner-scope-isolation.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/recon/retrieval-confidence-floor.md (not served here)
- docs/brainstorm/a-label-is-not-a-boundary/variants.md (not served here)
- docs/brainstorm/actionable-now-batch/cli-output/consultant.md (not served here)
- docs/brainstorm/actionable-now-batch/cli-output/prompt.md (not served here)
- docs/brainstorm/actionable-now-batch/design.md (not served here)
- docs/brainstorm/actionable-now-batch/plan.md (not served here)
- docs/brainstorm/actionable-now-batch/variants.md (not served here)
- docs/brainstorm/agent-boundary-control-surfaces/README.md (not served here)
- docs/brainstorm/agent-boundary-control-surfaces/cli-output/claude.md (not served here)
- docs/brainstorm/agent-boundary-control-surfaces/cli-output/prompt.md (not served here)
- docs/brainstorm/agent-boundary-control-surfaces/design.md (not served here)
- docs/brainstorm/agent-boundary-control-surfaces/plan.md (not served here)
- docs/brainstorm/agent-boundary-control-surfaces/variants.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/README.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/adr.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/cli-output/claude.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/cli-output/prompt.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/context.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/design.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/implementation-notes.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/notes.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/phase-log.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/plan.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/pr-notes.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/scope.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/task-body.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/test-matrix.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/variants.md (not served here)
- docs/brainstorm/agent-capability-cli-integration/verification.md (not served here)
- docs/brainstorm/agent-presence-self-improvement/cli-output/consultant.md (not served here)
- docs/brainstorm/agent-presence-self-improvement/cli-output/prompt.md (not served here)
- docs/brainstorm/agent-presence-self-improvement/design.md (not served here)
- docs/brainstorm/agent-presence-self-improvement/plan.md (not served here)
- docs/brainstorm/agent-presence-self-improvement/variants.md (not served here)
- docs/brainstorm/agent-surface-suite/cli-output/claude.md (not served here)
- docs/brainstorm/agent-surface-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/agent-surface-suite/design.md (not served here)
- docs/brainstorm/agent-surface-suite/plan.md (not served here)
- docs/brainstorm/agent-surface-suite/variants.md (not served here)
- docs/brainstorm/agent-write-contract-suite/cli-output/claude.md (not served here)
- docs/brainstorm/agent-write-contract-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/agent-write-contract-suite/design.md (not served here)
- docs/brainstorm/agent-write-contract-suite/plan.md (not served here)
- docs/brainstorm/agent-write-contract-suite/variants.md (not served here)
- docs/brainstorm/answer-trust/cli-output/consultant.md (not served here)
- docs/brainstorm/answer-trust/cli-output/prompt.md (not served here)
- docs/brainstorm/answer-trust/design.md (not served here)
- docs/brainstorm/answer-trust/plan.md (not served here)
- docs/brainstorm/answer-trust/variants.md (not served here)
- docs/brainstorm/auto-migrate-on-upgrade/design.md (not served here)
- docs/brainstorm/auto-migrate-on-upgrade/plan.md (not served here)
- docs/brainstorm/auto-migrate-on-upgrade/variants.md (not served here)
- docs/brainstorm/belief-lifecycle-decision-memory/cli-output/claude.md (not served here)
- docs/brainstorm/belief-lifecycle-decision-memory/cli-output/prompt.md (not served here)
- docs/brainstorm/belief-lifecycle-decision-memory/design.md (not served here)
- docs/brainstorm/belief-lifecycle-decision-memory/plan.md (not served here)
- docs/brainstorm/belief-lifecycle-decision-memory/variants.md (not served here)
- docs/brainstorm/brain-centric-layout/design.md (not served here)
- docs/brainstorm/brain-centric-layout/plan.md (not served here)
- docs/brainstorm/brain-integrity-safety-hardening/cli-output/claude.md (not served here)
- docs/brainstorm/brain-integrity-safety-hardening/cli-output/prompt.md (not served here)
- docs/brainstorm/brain-integrity-safety-hardening/design.md (not served here)
- docs/brainstorm/brain-integrity-safety-hardening/plan.md (not served here)
- docs/brainstorm/brain-integrity-safety-hardening/variants.md (not served here)
- docs/brainstorm/brain-integrity-suite/cli-output/claude.md (not served here)
- docs/brainstorm/brain-integrity-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/brain-integrity-suite/design.md (not served here)
- docs/brainstorm/brain-integrity-suite/plan.md (not served here)
- docs/brainstorm/brain-integrity-suite/pr-body.md (not served here)
- docs/brainstorm/brain-integrity-suite/variants.md (not served here)
- docs/brainstorm/brain-lifecycle-review-suite/cli-output/claude.md (not served here)
- docs/brainstorm/brain-lifecycle-review-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/brain-lifecycle-review-suite/design.md (not served here)
- docs/brainstorm/brain-lifecycle-review-suite/plan.md (not served here)
- docs/brainstorm/brain-lifecycle-review-suite/variants.md (not served here)
- docs/brainstorm/brain-lifecycle-suite/cli-output/claude.md (not served here)
- docs/brainstorm/brain-lifecycle-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/brain-lifecycle-suite/design.md (not served here)
- docs/brainstorm/brain-lifecycle-suite/plan.md (not served here)
- docs/brainstorm/brain-lifecycle-suite/variants.md (not served here)
- docs/brainstorm/brain-model-semantics-foundation/adr.md (not served here)
- docs/brainstorm/brain-model-semantics-foundation/cli-output/claude.md (not served here)
- docs/brainstorm/brain-model-semantics-foundation/cli-output/prompt.md (not served here)
- docs/brainstorm/brain-model-semantics-foundation/design.md (not served here)
- docs/brainstorm/brain-model-semantics-foundation/plan.md (not served here)
- docs/brainstorm/brain-model-semantics-foundation/variants.md (not served here)
- docs/brainstorm/brain-portability-interop/cli-output/claude.md (not served here)
- docs/brainstorm/brain-portability-interop/cli-output/prompt.md (not served here)
- docs/brainstorm/brain-portability-interop/design.md (not served here)
- docs/brainstorm/brain-portability-interop/plan.md (not served here)
- docs/brainstorm/brain-portability-interop/variants.md (not served here)
- docs/brainstorm/brain-safety-governance-suite/cli-output/claude.md (not served here)
- docs/brainstorm/brain-safety-governance-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/brain-safety-governance-suite/design.md (not served here)
- docs/brainstorm/brain-safety-governance-suite/plan.md (not served here)
- docs/brainstorm/brain-safety-governance-suite/variants.md (not served here)
- docs/brainstorm/cjk-schema-lifecycle-recovery/cli-output/claude.md (not served here)
- docs/brainstorm/cjk-schema-lifecycle-recovery/cli-output/prompt.md (not served here)
- docs/brainstorm/cjk-schema-lifecycle-recovery/design.md (not served here)
- docs/brainstorm/cjk-schema-lifecycle-recovery/plan.md (not served here)
- docs/brainstorm/cjk-schema-lifecycle-recovery/variants.md (not served here)
- docs/brainstorm/codegraph-link-depth-mcp/cli-output/claude.md (not served here)
- docs/brainstorm/codegraph-link-depth-mcp/cli-output/prompt.md (not served here)
- docs/brainstorm/codegraph-link-depth-mcp/design.md (not served here)
- docs/brainstorm/codegraph-link-depth-mcp/plan.md (not served here)
- docs/brainstorm/codegraph-link-depth-mcp/variants.md (not served here)
- docs/brainstorm/codegraph-mcp-operational-readability/cli-output/claude.md (not served here)
- docs/brainstorm/codegraph-mcp-operational-readability/cli-output/prompt.md (not served here)
- docs/brainstorm/codegraph-mcp-operational-readability/design.md (not served here)
- docs/brainstorm/codegraph-mcp-operational-readability/plan.md (not served here)
- docs/brainstorm/codegraph-mcp-operational-readability/variants.md (not served here)
- docs/brainstorm/context-continuity-receipts-suite/cli-output/claude.md (not served here)
- docs/brainstorm/context-continuity-receipts-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/context-continuity-receipts-suite/design.md (not served here)
- docs/brainstorm/context-continuity-receipts-suite/plan.md (not served here)
- docs/brainstorm/context-continuity-receipts-suite/variants.md (not served here)
- docs/brainstorm/context-injection-pipeline/cli-output/consultant.md (not served here)
- docs/brainstorm/context-injection-pipeline/cli-output/prompt.md (not served here)
- docs/brainstorm/context-injection-pipeline/design.md (not served here)
- docs/brainstorm/context-injection-pipeline/plan.md (not served here)
- docs/brainstorm/context-injection-pipeline/variants.md (not served here)
- docs/brainstorm/context-integrity-gates/cli-output/claude.md (not served here)
- docs/brainstorm/context-integrity-gates/cli-output/prompt.md (not served here)
- docs/brainstorm/context-integrity-gates/design.md (not served here)
- docs/brainstorm/context-integrity-gates/plan.md (not served here)
- docs/brainstorm/context-integrity-gates/variants.md (not served here)
- docs/brainstorm/context-pack-economics-observability/cli-output/claude.md (not served here)
- docs/brainstorm/context-pack-economics-observability/cli-output/prompt.md (not served here)
- docs/brainstorm/context-pack-economics-observability/design.md (not served here)
- docs/brainstorm/context-pack-economics-observability/plan.md (not served here)
- docs/brainstorm/context-pack-economics-observability/variants.md (not served here)
- docs/brainstorm/continuity-hygiene-freshness/cli-output/claude.md (not served here)
- docs/brainstorm/continuity-hygiene-freshness/cli-output/prompt.md (not served here)
- docs/brainstorm/continuity-hygiene-freshness/design.md (not served here)
- docs/brainstorm/continuity-hygiene-freshness/plan.md (not served here)
- docs/brainstorm/continuity-hygiene-freshness/variants.md (not served here)
- docs/brainstorm/cross-agent-query-foundation/cli-output/claude.md (not served here)
- docs/brainstorm/cross-agent-query-foundation/cli-output/prompt.md (not served here)
- docs/brainstorm/cross-agent-query-foundation/design.md (not served here)
- docs/brainstorm/cross-agent-query-foundation/plan.md (not served here)
- docs/brainstorm/cross-agent-query-foundation/variants.md (not served here)
- docs/brainstorm/crossvault-cards-eventtrace-fixes/design.md (not served here)
- docs/brainstorm/crossvault-cards-eventtrace-fixes/plan.md (not served here)
- docs/brainstorm/crossvault-cards-eventtrace-fixes/variants.md (not served here)
- docs/brainstorm/cycle-20261020/cli-output/claude.md (not served here)
- docs/brainstorm/cycle-20261020/cli-output/prompt.md (not served here)
- docs/brainstorm/cycle-20261020/design.md (not served here)
- docs/brainstorm/cycle-20261020/plan.md (not served here)
- docs/brainstorm/cycle-20261020/variants.md (not served here)
- docs/brainstorm/dated-commitments/cli-output/claude.md (not served here)
- docs/brainstorm/dated-commitments/cli-output/prompt.md (not served here)
- docs/brainstorm/dated-commitments/design.md (not served here)
- docs/brainstorm/dated-commitments/plan.md (not served here)
- docs/brainstorm/dated-commitments/variants.md (not served here)
- docs/brainstorm/default-scope-feedback/cli-output/claude.md (not served here)
- docs/brainstorm/default-scope-feedback/cli-output/prompt.md (not served here)
- docs/brainstorm/default-scope-feedback/design.md (not served here)
- docs/brainstorm/default-scope-feedback/plan.md (not served here)
- docs/brainstorm/default-scope-feedback/variants.md (not served here)
- docs/brainstorm/delivery-trust/cli-output/consultant.md (not served here)
- docs/brainstorm/delivery-trust/cli-output/prompt.md (not served here)
- docs/brainstorm/delivery-trust/design.md (not served here)
- docs/brainstorm/delivery-trust/plan.md (not served here)
- docs/brainstorm/delivery-trust/variants.md (not served here)
- docs/brainstorm/distilled-provenance/cli-output/claude.md (not served here)
- docs/brainstorm/distilled-provenance/cli-output/expected-output.md (not served here)
- docs/brainstorm/distilled-provenance/cli-output/prompt.md (not served here)
- docs/brainstorm/distilled-provenance/design.md (not served here)
- docs/brainstorm/distilled-provenance/plan.md (not served here)
- docs/brainstorm/distilled-provenance/variants.md (not served here)
- docs/brainstorm/embedding-provider-suite/cli-output/claude.md (not served here)
- docs/brainstorm/embedding-provider-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/embedding-provider-suite/design.md (not served here)
- docs/brainstorm/embedding-provider-suite/plan.md (not served here)
- docs/brainstorm/embedding-provider-suite/variants.md (not served here)
- docs/brainstorm/entity-truth-dream-suite/cli-output/claude.md (not served here)
- docs/brainstorm/entity-truth-dream-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/entity-truth-dream-suite/design.md (not served here)
- docs/brainstorm/entity-truth-dream-suite/plan.md (not served here)
- docs/brainstorm/entity-truth-dream-suite/variants.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/cli-output/claude.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/cli-output/prompt.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/design.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/plan.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/0e79f0-lint-on-write.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/130-hermes-readiness.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/155-include-paths.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/160-source-trust.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/161-slug-race.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/3309-retrieval-trail.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/a3254-readiness-liveness.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/conventions.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/e24c6d-schema-completeness.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/recon/e5f447-recall-channels.md (not served here)
- docs/brainstorm/evidence-at-the-boundary/variants.md (not served here)
- docs/brainstorm/grok-native-integration/cli-output/claude.md (not served here)
- docs/brainstorm/grok-native-integration/cli-output/prompt.md (not served here)
- docs/brainstorm/grok-native-integration/design.md (not served here)
- docs/brainstorm/grok-native-integration/plan.md (not served here)
- docs/brainstorm/grok-native-integration/variants.md (not served here)
- docs/brainstorm/hermes-memory-provider/cli-output/claude.md (not served here)
- docs/brainstorm/hermes-memory-provider/cli-output/prompt.md (not served here)
- docs/brainstorm/hermes-memory-provider/design.md (not served here)
- docs/brainstorm/hermes-memory-provider/plan.md (not served here)
- docs/brainstorm/hermes-memory-provider/variants.md (not served here)
- docs/brainstorm/hermes-static-tool-schemas/cli-output/claude.md (not served here)
- docs/brainstorm/hermes-static-tool-schemas/cli-output/prompt.md (not served here)
- docs/brainstorm/hermes-static-tool-schemas/design.md (not served here)
- docs/brainstorm/hermes-static-tool-schemas/plan.md (not served here)
- docs/brainstorm/hermes-static-tool-schemas/variants.md (not served here)
- docs/brainstorm/hindsight-brain-loop-ops/cli-output/claude.md (not served here)
- docs/brainstorm/hindsight-brain-loop-ops/cli-output/prompt.md (not served here)
- docs/brainstorm/hindsight-brain-loop-ops/design.md (not served here)
- docs/brainstorm/hindsight-brain-loop-ops/plan.md (not served here)
- docs/brainstorm/hindsight-brain-loop-ops/variants.md (not served here)
- docs/brainstorm/honest-embedding-spend/cli-output/claude.md (not served here)
- docs/brainstorm/honest-embedding-spend/cli-output/prompt.md (not served here)
- docs/brainstorm/honest-embedding-spend/design.md (not served here)
- docs/brainstorm/honest-embedding-spend/plan.md (not served here)
- docs/brainstorm/honest-embedding-spend/variants.md (not served here)
- docs/brainstorm/honest-query-embed-and-safe-upgrades/cli-output/claude.md (not served here)
- docs/brainstorm/honest-query-embed-and-safe-upgrades/cli-output/prompt.md (not served here)
- docs/brainstorm/honest-query-embed-and-safe-upgrades/design.md (not served here)
- docs/brainstorm/honest-query-embed-and-safe-upgrades/plan.md (not served here)
- docs/brainstorm/honest-query-embed-and-safe-upgrades/variants.md (not served here)
- docs/brainstorm/hook-update-resilience/design.md (not served here)
- docs/brainstorm/hook-update-resilience/plan.md (not served here)
- docs/brainstorm/hook-update-resilience/variants.md (not served here)
- docs/brainstorm/index-freshness/design.md (not served here)
- docs/brainstorm/index-freshness/plan.md (not served here)
- docs/brainstorm/indexer-durability/cli-output/claude.md (not served here)
- docs/brainstorm/indexer-durability/cli-output/prompt.md (not served here)
- docs/brainstorm/indexer-durability/design.md (not served here)
- docs/brainstorm/indexer-durability/plan.md (not served here)
- docs/brainstorm/indexer-durability/variants.md (not served here)
- docs/brainstorm/ingestion-import-robustness/design.md (not served here)
- docs/brainstorm/ingestion-import-robustness/plan.md (not served here)
- docs/brainstorm/ingestion-import-robustness/variants.md (not served here)
- docs/brainstorm/integrity-control-agent-memory-surface/cli-output/claude.md (not served here)
- docs/brainstorm/integrity-control-agent-memory-surface/cli-output/prompt.md (not served here)
- docs/brainstorm/integrity-control-agent-memory-surface/design.md (not served here)
- docs/brainstorm/integrity-control-agent-memory-surface/plan.md (not served here)
- docs/brainstorm/integrity-control-agent-memory-surface/variants.md (not served here)
- docs/brainstorm/knowledge-intake-and-consolidation/cli-output/claude.md (not served here)
- docs/brainstorm/knowledge-intake-and-consolidation/cli-output/prompt.md (not served here)
- docs/brainstorm/knowledge-intake-and-consolidation/design.md (not served here)
- docs/brainstorm/knowledge-intake-and-consolidation/plan.md (not served here)
- docs/brainstorm/knowledge-intake-and-consolidation/variants.md (not served here)
- docs/brainstorm/knowledge-provenance/cli-output/claude.md (not served here)
- docs/brainstorm/knowledge-provenance/cli-output/prompt.md (not served here)
- docs/brainstorm/knowledge-provenance/design.md (not served here)
- docs/brainstorm/knowledge-provenance/plan.md (not served here)
- docs/brainstorm/knowledge-provenance/variants.md (not served here)
- docs/brainstorm/language-agnostic-fact-extraction/cli-output/claude.md (not served here)
- docs/brainstorm/language-agnostic-fact-extraction/cli-output/prompt.md (not served here)
- docs/brainstorm/language-agnostic-fact-extraction/design.md (not served here)
- docs/brainstorm/language-agnostic-fact-extraction/plan.md (not served here)
- docs/brainstorm/language-agnostic-fact-extraction/variants.md (not served here)
- docs/brainstorm/link-graph-surfaces/cli-output/claude.md (not served here)
- docs/brainstorm/link-graph-surfaces/cli-output/prompt.md (not served here)
- docs/brainstorm/link-graph-surfaces/design.md (not served here)
- docs/brainstorm/link-graph-surfaces/plan.md (not served here)
- docs/brainstorm/link-graph-surfaces/variants.md (not served here)
- docs/brainstorm/link-recall-intelligence/cli-output/claude.md (not served here)
- docs/brainstorm/link-recall-intelligence/cli-output/prompt.md (not served here)
- docs/brainstorm/link-recall-intelligence/design.md (not served here)
- docs/brainstorm/link-recall-intelligence/plan.md (not served here)
- docs/brainstorm/link-recall-intelligence/variants.md (not served here)
- docs/brainstorm/manifest-dependency-facts/cli-output/claude.md (not served here)
- docs/brainstorm/manifest-dependency-facts/cli-output/expected-output.md (not served here)
- docs/brainstorm/manifest-dependency-facts/cli-output/prompt.md (not served here)
- docs/brainstorm/manifest-dependency-facts/design.md (not served here)
- docs/brainstorm/manifest-dependency-facts/plan.md (not served here)
- docs/brainstorm/manifest-dependency-facts/variants.md (not served here)
- docs/brainstorm/mcp-context-economy/cli-output/claude.md (not served here)
- docs/brainstorm/mcp-context-economy/cli-output/prompt.md (not served here)
- docs/brainstorm/mcp-context-economy/design.md (not served here)
- docs/brainstorm/mcp-context-economy/plan.md (not served here)
- docs/brainstorm/mcp-context-economy/variants.md (not served here)
- docs/brainstorm/mcp-error-codes/cli-output/claude.md (not served here)
- docs/brainstorm/mcp-error-codes/cli-output/prompt.md (not served here)
- docs/brainstorm/mcp-error-codes/design.md (not served here)
- docs/brainstorm/mcp-error-codes/plan.md (not served here)
- docs/brainstorm/mcp-error-codes/variants.md (not served here)
- docs/brainstorm/memory-integrity/cli-output/claude.md (not served here)
- docs/brainstorm/memory-integrity/cli-output/prompt.md (not served here)
- docs/brainstorm/memory-integrity/design.md (not served here)
- docs/brainstorm/memory-integrity/plan.md (not served here)
- docs/brainstorm/memory-integrity/variants.md (not served here)
- docs/brainstorm/memory-observability-suite/atof-atif-mapping.md (not served here)
- docs/brainstorm/memory-observability-suite/cli-output/claude.md (not served here)
- docs/brainstorm/memory-observability-suite/cli-output/prompt.md (not served here)
- docs/brainstorm/memory-observability-suite/design.md (not served here)
- docs/brainstorm/memory-observability-suite/plan.md (not served here)
- docs/brainstorm/memory-observability-suite/variants.md (not served here)
- docs/brainstorm/memory-signal-provenance-lifecycle/cli-output/claude.md (not served here)
- docs/brainstorm/memory-signal-provenance-lifecycle/cli-output/prompt.md (not served here)
- docs/brainstorm/memory-signal-provenance-lifecycle/design.md (not served here)
- docs/brainstorm/memory-signal-provenance-lifecycle/plan.md (not served here)
- docs/brainstorm/memory-signal-provenance-lifecycle/variants.md (not served here)
- docs/brainstorm/memory-subsystem-alignment/cli-output/claude.md (not served here)
- docs/brainstorm/memory-subsystem-alignment/cli-output/prompt.md (not served here)
- docs/brainstorm/memory-subsystem-alignment/design.md (not served here)
- docs/brainstorm/memory-subsystem-alignment/plan.md (not served here)
- docs/brainstorm/memory-subsystem-alignment/variants.md (not served here)
- docs/brainstorm/memory-write-path-integrity/cli-output/claude.md (not served here)
- docs/brainstorm/memory-write-path-integrity/cli-output/prompt.md (not served here)
- docs/brainstorm/memory-write-path-integrity/design.md (not served here)
- docs/brainstorm/memory-write-path-integrity/plan.md (not served here)
- docs/brainstorm/memory-write-path-integrity/variants.md (not served here)
- docs/brainstorm/near-duplicate-defense/cli-output/claude.md (not served here)
- docs/brainstorm/near-duplicate-defense/cli-output/codex.md (not served here)
- docs/brainstorm/near-duplicate-defense/cli-output/opencode.md (not served here)
- docs/brainstorm/near-duplicate-defense/cli-output/prompt.md (not served here)
- docs/brainstorm/near-duplicate-defense/cli-output/second-claude.md (not served here)
- docs/brainstorm/near-duplicate-defense/design.md (not served here)
- docs/brainstorm/near-duplicate-defense/plan.md (not served here)
- docs/brainstorm/near-duplicate-defense/variants.md (not served here)
- docs/brainstorm/no-dead-ends/cli-output/claude.md (not served here)
- docs/brainstorm/no-dead-ends/cli-output/prompt.md (not served here)
- docs/brainstorm/no-dead-ends/design.md (not served here)
- docs/brainstorm/no-dead-ends/plan.md (not served here)
- docs/brainstorm/no-dead-ends/variants.md (not served here)
- docs/brainstorm/non-markdown-ingest/cli-output/claude.md (not served here)
- docs/brainstorm/non-markdown-ingest/cli-output/expected-output.md (not served here)
- docs/brainstorm/non-markdown-ingest/cli-output/prompt.md (not served here)
- docs/brainstorm/non-markdown-ingest/design.md (not served here)
- docs/brainstorm/non-markdown-ingest/plan.md (not served here)
- docs/brainstorm/non-markdown-ingest/variants.md (not served here)
- docs/brainstorm/note-change-truth-maintenance/cli-output/claude.md (not served here)
- docs/brainstorm/note-change-truth-maintenance/cli-output/prompt.md (not served here)
- docs/brainstorm/note-change-truth-maintenance/cli-output/task-bodies.md (not served here)
- docs/brainstorm/note-change-truth-maintenance/design.md (not served here)
- docs/brainstorm/note-change-truth-maintenance/plan.md (not served here)
- docs/brainstorm/note-change-truth-maintenance/variants.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/cli-output/claude.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/cli-output/prompt.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/design.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/plan.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/recon/architect-module.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/recon/concurrency-and-cancellation.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/recon/durability-and-ownership.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/recon/failure-mode-bench.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/recon/ingest-adapters-and-lease.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/recon/progress-reporting.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/recon/provider-sunset.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/review/claims-and-refusals.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/review/cross-cutting.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/review/locks-and-races.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/review/progress-and-cancellation.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/review/tests-and-docs.md (not served here)
- docs/brainstorm/nothing-runs-unwatched/variants.md (not served here)
- docs/brainstorm/nothing-writes-silently/cli-output/claude.md (not served here)
- docs/brainstorm/nothing-writes-silently/cli-output/prompt.md (not served here)
- docs/brainstorm/nothing-writes-silently/design.md (not served here)
- docs/brainstorm/nothing-writes-silently/plan.md (not served here)
- docs/brainstorm/nothing-writes-silently/variants.md (not served here)
- docs/brainstorm/opencode-native-integration/cli-output/claude.md (not served here)
- docs/brainstorm/opencode-native-integration/cli-output/prompt.md (not served here)
- docs/brainstorm/opencode-native-integration/design.md (not served here)
- docs/brainstorm/opencode-native-integration/plan.md (not served here)
- docs/brainstorm/opencode-native-integration/variants.md (not served here)
- docs/brainstorm/operability-first-run/design.md (not served here)
- docs/brainstorm/operability-first-run/plan.md (not served here)
- docs/brainstorm/operability-first-run/variants.md (not served here)
- docs/brainstorm/operational-friction-reduction/design.md (not served here)
- docs/cli-reference.md
- docs/cross-project-pointer.md
- docs/decision-models.md
- docs/hermes-cron.md
- docs/how-it-works.md
- docs/idea.md
- docs/ingest-manifest.md
- docs/mcp.md
- docs/metrics.md
- docs/observability.md
- docs/stability.md
- docs/updating.md
- hooks/hooks.json
- install.md
- plugins/codex/LICENSE
Advantages
- Human-readable and editable memory stored as standard Markdown files in an Obsidian vault. evidence: README.md
- Deterministic dream pass ensures rules are derived from repeated signals without requiring additional model runs. evidence: README.md
- Supports reversible changes through snapshots and rollback capabilities. evidence: README.md
Limitations
- Requires Bun runtime for the CLI and core operations. evidence: README.md
- Advanced features like semantic search require additional setup with sqlite-vec. evidence: README.md
- Some integrations depend on specific client-side plugin support or MCP host capabilities. evidence: README.md
Security
Risk level: Low risk
This brain runs
MCP servers (2)
.claude-plugin/plugin.json(open-second-brain):${CLAUDE_PLUGIN_ROOT}/scripts/o2b mcp --harness claude-code.claude-plugin/plugin.json(open-second-brain-writer):${CLAUDE_PLUGIN_ROOT}/scripts/o2b mcp --scope writer --harness claude-code
Git hooks (2)
.githooks/pre-commit(pre-commit):runs on git pre-commit.githooks/pre-push(pre-push):runs on git pre-push
Agent instructions that run or fetch something (46)
CLAUDE.md:bun run scripts/sync-version.tsCLAUDE.md:CI gates on `bun run scripts/sync-version.ts --check` (the `validate` job, step "Verify manifest version sync").CLAUDE.md:Run the `--check` form locally before pushing a version change.CLAUDE.md:Bump `package.json` (and run `sync-version.ts`) on the feature branch and include it in the SAME pull request as the feature, before you push.CLAUDE.md:Bump `package.json` to `X.Y.Z` and run `bun run scripts/sync-version.ts`.CLAUDE.md:After changing a skill or `hooks/hooks.json`, run:CLAUDE.md:bun run sync-plugin-mirrorsCLAUDE.md:CI gates on `bun run sync-plugin-mirrors:check` (the `validate` and `windows` jobs), and the pre-commit hook runs the same check.docs/legacy-skills/agent-event-log/SKILL.md:Read-only queries (running `git log`, `o2b status`, `vault_health`, `second_brain_query`).docs/legacy-skills/agent-event-log/SKILL.md:**Technical identifiers stay verbatim regardless of surrounding language**: command names (`bun test`, `o2b doctor`), file paths (`src/core/event-log.ts`), package and library names (`proper-lockfile`, `@anthropic-ai/sdk`), API symbols, error messages quoted from logs.plugins/codex/skills/brain-memory/SKILL.md:Read-only inspection (running `git log`, `o2b status`, `vault_health`).plugins/codex/skills/brain-memory/SKILL.md:After scan, the source line becomes `@osb✓ [[sig-...]]` so a second run is idempotent.plugins/codex/skills/codegraph-partner/SKILL.md:Run these checks in order.plugins/codex/skills/codegraph-partner/SKILL.md:No install talk.plugins/codex/skills/codegraph-partner/SKILL.md:| | not_indexed (CLI present, no `.codegraph/` or `initialized: false`) | Offer one line: "run `codegraph init <path>` to index this repo".plugins/codex/skills/codegraph-partner/SKILL.md:Whenever you run `codegraph init` in any repository on the user's behalf, immediately read that repo's `.gitignore` and append a `.codegraph/` line if one is not already present.plugins/codex/skills/decision-model-setup/SKILL.md:That is the operator's command, and only the operator can run it: the key is operator-only, so the `brain_settings` tool refuses an agent's write to it with `operator_only_setting` on every machine, switched or not.plugins/codex/skills/decision-model-setup/SKILL.md:Ask the operator to run the command;plugins/codex/skills/decision-model-setup/SKILL.md:Does not install servers or download model weights.plugins/codex/skills/embeddings-setup/SKILL.md:Every run that uses it prints one stderr warning per endpoint.plugins/codex/skills/embeddings-setup/SKILL.md:If the install path missed it, force a rebuild:plugins/codex/skills/embeddings-setup/SKILL.md:bun pm ls | grep sqlite-vecplugins/codex/skills/embeddings-setup/SKILL.md:bun install --forceplugins/codex/skills/embeddings-setup/SKILL.md:When the index already exists and only some chunks lack vectors, price the work first with the dry run, then apply it:plugins/codex/skills/embeddings-setup/SKILL.md:Does not invoke `brew install` on the user's behalf.plugins/codex/skills/schema-author/SKILL.md:Preview the batch first with `schema_apply_mutations` (`dry_run=true`) or `o2b brain schema apply --dry-run --mutation ...`: it returns the pack that would result and its diff and writes nothing at all - no config write, no audit record, no lock file.plugins/codex/skills/schema-author/SKILL.md:It does not run the two checks that exist only because the apply writes: the vault-identity write guard, and the atomic writer's re-parse of the rendered YAML.plugins/codex/skills/schema-author/SKILL.md:Re-run lint/stats after mutation and report changed tokens plus any remaining findings.skills/brain-memory/SKILL.md:Read-only inspection (running `git log`, `o2b status`, `vault_health`).skills/brain-memory/SKILL.md:After scan, the source line becomes `@osb✓ [[sig-...]]` so a second run is idempotent.skills/codegraph-partner/SKILL.md:Run these checks in order.skills/codegraph-partner/SKILL.md:No install talk.skills/codegraph-partner/SKILL.md:| | not_indexed (CLI present, no `.codegraph/` or `initialized: false`) | Offer one line: "run `codegraph init <path>` to index this repo".skills/codegraph-partner/SKILL.md:Whenever you run `codegraph init` in any repository on the user's behalf, immediately read that repo's `.gitignore` and append a `.codegraph/` line if one is not already present.skills/decision-model-setup/SKILL.md:That is the operator's command, and only the operator can run it: the key is operator-only, so the `brain_settings` tool refuses an agent's write to it with `operator_only_setting` on every machine, switched or not.skills/decision-model-setup/SKILL.md:Ask the operator to run the command;skills/decision-model-setup/SKILL.md:Does not install servers or download model weights.skills/embeddings-setup/SKILL.md:Every run that uses it prints one stderr warning per endpoint.skills/embeddings-setup/SKILL.md:If the install path missed it, force a rebuild:skills/embeddings-setup/SKILL.md:bun pm ls | grep sqlite-vecskills/embeddings-setup/SKILL.md:bun install --forceskills/embeddings-setup/SKILL.md:When the index already exists and only some chunks lack vectors, price the work first with the dry run, then apply it:skills/embeddings-setup/SKILL.md:Does not invoke `brew install` on the user's behalf.skills/schema-author/SKILL.md:Preview the batch first with `schema_apply_mutations` (`dry_run=true`) or `o2b brain schema apply --dry-run --mutation ...`: it returns the pack that would result and its diff and writes nothing at all - no config write, no audit record, no lock file.skills/schema-author/SKILL.md:It does not run the two checks that exist only because the apply writes: the vault-identity write guard, and the atomic writer's re-parse of the rendered YAML.skills/schema-author/SKILL.md:Re-run lint/stats after mutation and report changed tokens plus any remaining findings.
Hooks (54)
hooks/hooks.json(PostToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json(PostToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" post-write-reminder; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook post-write-reminder; exit 0hooks/hooks.json(PostToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" subagent-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook subagent-inject; exit 0hooks/hooks.json(PostToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" reground-deliver; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook reground-deliver; exit 0hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" active-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook active-inject; exit 0hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" gap-agenda; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook gap-agenda; exit 0hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" backlog-nudge; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook backlog-nudge; exit 0hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" log-catchup; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook log-catchup; exit 0hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" view-render; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook view-render; exit 0hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" recall-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook recall-inject; exit 0hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" nav-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook nav-inject; exit 0hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" counterparty-brief; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook counterparty-brief; exit 0hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" reground-deliver; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook reground-deliver; exit 0hooks/hooks.json(PreToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" pretool-orient; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook pretool-orient; exit 0hooks/hooks.json(PreToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" pretool-host-backup; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook pretool-host-backup; exit 0hooks/hooks.json(Stop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json(Stop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" stop-log-guardrail; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook stop-log-guardrail; exit 0hooks/hooks.json(Stop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" answer-quote-check; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook answer-quote-check; exit 0hooks/hooks.json(Stop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" hygiene-digest; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook hygiene-digest; exit 0hooks/hooks.json(SubagentStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" subagent-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook subagent-inject; exit 0hooks/hooks.json(SubagentStop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json(SessionEnd):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0hooks/hooks.json(SessionEnd):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" gap-promote; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook gap-promote; exit 0hooks/hooks.json(PostCompact):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" active-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook active-inject; exit 0hooks/hooks.json(PostCompact):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json(PostToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json(PostToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" post-write-reminder; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook post-write-reminder; exit 0plugins/codex/hooks/hooks.json(PostToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" subagent-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook subagent-inject; exit 0plugins/codex/hooks/hooks.json(PostToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" reground-deliver; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook reground-deliver; exit 0plugins/codex/hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" active-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook active-inject; exit 0plugins/codex/hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" gap-agenda; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook gap-agenda; exit 0plugins/codex/hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" backlog-nudge; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook backlog-nudge; exit 0plugins/codex/hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" log-catchup; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook log-catchup; exit 0plugins/codex/hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" view-render; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook view-render; exit 0plugins/codex/hooks/hooks.json(SessionStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" recall-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook recall-inject; exit 0plugins/codex/hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" nav-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook nav-inject; exit 0plugins/codex/hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" counterparty-brief; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook counterparty-brief; exit 0plugins/codex/hooks/hooks.json(UserPromptSubmit):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" reground-deliver; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook reground-deliver; exit 0plugins/codex/hooks/hooks.json(PreToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" pretool-orient; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook pretool-orient; exit 0plugins/codex/hooks/hooks.json(PreToolUse):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" pretool-host-backup; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook pretool-host-backup; exit 0plugins/codex/hooks/hooks.json(Stop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json(Stop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" stop-log-guardrail; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook stop-log-guardrail; exit 0plugins/codex/hooks/hooks.json(Stop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" answer-quote-check; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook answer-quote-check; exit 0plugins/codex/hooks/hooks.json(Stop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" hygiene-digest; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook hygiene-digest; exit 0plugins/codex/hooks/hooks.json(SubagentStart):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" subagent-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook subagent-inject; exit 0plugins/codex/hooks/hooks.json(SubagentStop):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json(SessionEnd):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0plugins/codex/hooks/hooks.json(SessionEnd):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" gap-promote; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook gap-promote; exit 0plugins/codex/hooks/hooks.json(PostCompact):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" active-inject; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook active-inject; exit 0plugins/codex/hooks/hooks.json(PostCompact):r="$CLAUDE_PLUGIN_ROOT"; if [ -n "$r" ] && [ -x "$r/scripts/o2b-hook" ]; then exec "$r/scripts/o2b-hook" session-capture; fi; command -v o2b-hook >/dev/null 2>&1 && exec o2b-hook session-capture; exit 0
Package scripts (1)
package.json(prepare):bun scripts/prepare.ts || exit 0
- Low risk: scripts-present (.githooks/pre-commit)
- Low risk: scripts-present (.githooks/pre-push)
- Low risk: network-instructions (plugins/codex/skills/codegraph-partner/SKILL.md:115)
- Low risk: scripts-present (scripts/prepare.ts)
- Low risk: network-instructions (skills/codegraph-partner/SKILL.md:115)
Attribution and licence
- First committed by
hermes-vps-agent
- Declared authors
- Open Second Brain contributors (declared in .claude-plugin/plugin.json)
- Open Second Brain contributors (declared in pyproject.toml)
- Publisher
- Serg Eroshe (Community)
- Source
- itechmeat/open-second-brain · commit 233b91d
- Licence
- MIT License (files served here)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Contributors
- solaitken, itechmeat, Yori940619, hermes-vps-agent, DanBennettUK
AutoGineer is not affiliated with or endorsed by hermes-vps-agent or Serg Eroshe.
Install from source
486 of its files are not served here, so there is no ZIP. itechmeat/open-second-brain at 233b91d