Generated banner for Open Second Brain
Generated logo for Open Second Brain

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.
— the description by hermes-vps-agent

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.

Folders
FolderRoleFiles
.githubagent-config7
binscripts1
docsdocs774
hookshooks35
scriptsscripts22
skillsagent-instructions6
srcsource-code1496
templatestemplates23
teststests2101
.agentsfolder1
.ai-notesfolder1
.apbfolder87
.claude-pluginfolder2
.codex-pluginfolder1
.githooksfolder2
installfolder16
openclawfolder1
openspecfolder33
pluginsfolder21
schemasfolder5

The agent reads

The agent writes

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

  1. 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-push
    • 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
    • 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 0
    • 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 0
    • 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 0
    • 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 0
    • 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 0
    • 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 0
    • 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 0
    • 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 0
    • 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
    • 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
    • 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 0
    • 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 0
    • 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 0
    • 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 0
    • 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 0
    • 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 0
    • 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
    • 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 0
    • 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 0
    • 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 0
    • 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 0
    • 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
    • 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
    • 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 0
    • 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 0
    • 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
    • package.json: bun scripts/prepare.ts || exit 0
    • plugins/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
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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
    • plugins/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
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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 0
    • plugins/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
    • plugins/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
    • plugins/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 0
    • plugins/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 0
    • plugins/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
  2. 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
  3. 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 .-> Notes

Each 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| Pref

The 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

  1. Install Bun 1.1 or later: install/prerequisites.md.

  2. Install the plugin for your client (guides below), or get the o2b CLI: install/prerequisites.md.

  3. Create the vault files:

    o2b init       --vault /path/to/vault --name "My Second Brain"
    o2b brain init --vault /path/to/vault
  4. Connect 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

ClientIntegrationGuide
Claude CodeMarketplace plugin: MCP servers and hooksinstall/claudecode.md
OpenAI CodexMarketplace plugin, then o2b install --target codex --applyinstall/codex.md
ChatGPT desktopReads the shared Codex config: o2b install --target codex --applyinstall/codex.md
Hermes AgentPlugin and memory providerinstall/hermes.md
OpenClawNative plugin, no MCPinstall/openclaw.md
opencodeo2b install --target opencode --apply: MCP servers and a native plugininstall/opencode.md
Grok Buildo2b install --target grok --apply: MCP servers and hooksinstall/grok.md
Cursoro2b install --target cursor --applyinstall/cursor.md
kiroo2b install --target kiro --applyinstall/kiro.md
GitHub Copilot CLIo2b install --target copilot-cli --applyinstall/copilot-cli.md
Gemini CLIo2b install --target gemini-cli --applyinstall/gemini-cli.md
ZCodeMCP servers added to its config by handinstall/zcode.md
Aidero2b install --target aider --apply: context file, no MCPinstall/aider.md
Pio2b install --target pi --apply: skill over the CLIinstall/pi.md
Any other MCP hosto2b install --target generic --apply --out - prints the server entriesinstall/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 rollback restores it: snapshots.

  • Attributed writes. o2b brain writes lists every note write by agent and device, writes revert plans a restore, o2b brain freeze stops all writers, and o2b brain log verify checks 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 through brain_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-key seeding the fan-out from mined entity mentions): session logs, coverage.

  • Hygiene. o2b brain hygiene scan finds contested, duplicate, stale and unused memories; apply runs only the findings you pick: maintenance.

  • Claim ledger. o2b brain truth ingest records 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 events recalls a windowed slice of the ledger, and truth state grounds an agent-stated claim with an anchoring verdict - committed, or reported back with reason codes. truth state accepts an optional opaque --sample-id naming 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 --apply to 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-export writes a one-file backup bundle: knowledge packs.

  • Secret custody. Credentials live encrypted in the vault's state dir behind a capability-gated store; $secret:NAME references resolve at the use sites, o2b brain secret unlock wraps the keyfile under a passphrase, and secret export/import move the store between installs as one passphrase-encrypted bundle: custody CLI.

  • Today view and note markers. o2b brain today, @osb loop and @osb set markers: Brain CLI.

  • Self-inspection. o2b version, o2b install --friction and o2b state status report 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; the embeddings-setup skill 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 a search_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_ENABLED and write_approval.ingest / OPEN_SECOND_BRAIN_WRITE_APPROVAL_INGEST_ENABLED (each falling back to the write_approval.enabled master, default off) stage note creates and ingest summary pages into Brain/pending/ beside the staged signals, where they stay out of the search index until an operator runs o2b brain pending list and applies or rejects them; write-path integrity.

  • A permissions document, absent by default. Brain/_permissions.yaml (an operator-edited vault file) resolves allow/ask/deny per agent, role and target for the write, ingest and owner-write actions; with the file absent every check behaves exactly as today. ask stages the write, deny refuses with the write-refused token and the next command o2b brain permissions show, and every ask/deny verdict lands in a queryable decision ledger under Brain/logs/decisions/; o2b brain permissions show dry-runs the decision table, ledger reads the rows; Brain CLI.

  • The owner-write gate, off by default. integrity.owner_scope_writes in Brain/_brain.yaml (off | warn | fail, default off) refuses a caller-named owner that differs from the caller's resolved identity on the preference and note lanes (warn allows and records one decision-ledger row); a document owner_write verdict composes most-restrictive-wins with the gate; write-time integrity.

  • Named MCP tokens, optional. o2b mcp token mint|rotate|revoke|list keeps 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-key stays valid as the operator master credential, and mcp_tokens_required / OPEN_SECOND_BRAIN_MCP_TOKENS_REQUIRED (default false) 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, and o2b 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: false suppresses the ambient extraction lane with a counted ambient-withheld event (absent keeps today's behavior), and guardrails.ambient_ttl_days stamps an expiration_date on 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 at Brain/decisions/open-<slug>.md; resolve mints the real type: decision page, discard closes 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

TopicPage
Mental model, vault layout, dream pass, safety propertiesdocs/how-it-works.md
Every CLI verb and flagdocs/cli-reference.md
MCP protocol, tools, writer splitdocs/mcp.md
Brain store: setup, sync, migration, the human viewdocs/brain-store.md
Architecture and configuration modeldocs/architecture.md
Updating (o2b update) and upgrade notesdocs/updating.md
Observability eventsdocs/observability.md
Metrics data contractdocs/metrics.md
Cross-project pointerdocs/cross-project-pointer.md
Scheduled Brain digest (Hermes cron)docs/hermes-cron.md
Stability policydocs/stability.md
Origin of the ideadocs/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> --apply for 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 one HOST_INSTRUCTION_PRECEDENCE table, 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 stays missing-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 refuses user-modified-block with exit 4 unless --force. The apply is bound to the approved preview - the apply re-derives the plan from the live disk and refuses preview-drift when 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 what o2b uninstall --target <name> --apply reads 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-blocks turns 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 preference confirmed, the confidence band high, the lesson tier preferred in the current digest, an explicitly enabled promotion scope (lessons.promotion.scopes, env twin OPEN_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. approve writes 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), and refresh-blocks re-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 outside Brain/ so an agent cannot read its own approval state as a note surface - schema-versioned, fail-closed on damage, with the audit history in the lessons-approved, lessons-denied and lessons-promoted log 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 as lessons-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.json with 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, and rewrite-score <skill> [--rounds-dir <dir>] grades them. Open Second Brain never calls a model - the envelopes are needs-llm-step payloads, 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 is not_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 a scored_rewrite proposal whose acceptance is the operator's step: MCP never accepts one (a typed refusal points at the command line), the CLI accept requires --yes and re-verifies the live receipt and the scored contract at accept time, and the incumbent SKILL.md bytes 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 optional skill_rewrite_judge_cmd judge that stays advisory - it never moves the tasks_passed_rate metric the gate decides on. A read-only brain_skill_rewrite_status MCP 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; the skill_rewrite_eval_grade telemetry 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.toml file the codex target writes, so one o2b install --target codex --apply serves both hosts and one o2b uninstall --target codex --apply undoes 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, and install/codex.md gains 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.md gains the o2b brain lessons family and the rewrite-plan / rewrite-score forms 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.md documents the brain_skill_rewrite_status tool; docs/observability.md names the lessons log events and the rewrite-eval metric; install/codex.md gains the ChatGPT desktop section; docs/cross-project-pointer.md names 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: an untrusted_source marker, a content_class: prompt-derived class, or an origin_channel of mcp-tool or import reads agent-authored; origin_channel: cli with no other marker is the one operator-authored answer that ships plain. The derivation fails closed - a page with no stamps, the unset channel 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 - snippet and content on 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_context and brain_pinned_context, and the osb:// 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-soft delivery-provenance-unknown doctor 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 new verify operation on the brain_context_receipts MCP 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 answers match / changed / unavailable, the last with its reason (no_path, no_hash, file_missing, unreadable); a structural id, a hashless item and a pre_compress principle 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-soft receipt-content-drift doctor 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 and session_inject receipts stay silent, and a window that cannot be read reaches the uncertain stream as receipt-content-drift-unmeasured rather 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) and pretool-host-backup (host_settings_backup, file-mutating tools only) shims through the host's tool.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 structured permissionDecision: "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-in recall-inject, nav-inject, counterparty-brief and reground-deliver shims 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: every hooks/hooks.json row must be either carried by the plugin or a declared divergence with a written reason, and the declared-divergence table in install/opencode.md names the thirteen rows that stay opencode-specific with why.

  • Ed25519 attestations: the operator signs the store. o2b brain attest keygen | sign | list | verify turns store verification into evidence instead of a live answer. keygen creates 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-env or on stdin, never argv, and a lost key leaves every past attestation unverifiable); sign measures 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. verify re-measures those surfaces against the newest attestation - per-shard intact / 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 as Brain/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 the brain_secrets split applied to signatures: a read-only brain_attestations tool mirrors list and verify and 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-soft attestation-stale-or-diverged doctor 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.md gains the o2b brain attest row, the verify form on the context-receipts row and the delivery-trust section naming the fenced surfaces and the two new doctor rows; docs/mcp.md documents the brain_attestations tool, the verify operation on brain_context_receipts and the fence contract on resource bodies delivered at remote reach; install/opencode.md documents 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 (stamped unset) - rewrite an inherited cli stamp 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-authored cli prose 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, @osb inline 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 (env OPEN_SECOND_BRAIN_MARKDOWN_NEUTRALIZE_MODE, off | shadow | enforce, default off) runs the gate at the two seams - capture writes (o2b brain capture and the capture kernel) and untrusted ingest summaries: enforce backslash-escapes the structure-bearing markdown in caller text before it is written, so no scanner treats it as the vault's own, and stamps a neutralized_markers frontmatter key naming the escaped families - a write that changes nothing adds nothing, and vault-authored sections appended after caller text are never touched; shadow leaves 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 doctor pass gains the fail-soft planted-structure-rows warning: 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, deduplicating Brain/pending/ staged copies against their publish targets and skipping pages the caller may not read, silent on a vault with none. Under markdown_neutralize_mode: shadow the 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 in enforce or by a later repair pass - so the finding carries no o2b command.

  • o2b brain init seeds 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 .gitignore covering /.open-second-brain/ - the derived runtime state, bench runs included - beside the identity stamp, so a fresh vault no longer leaves brain.sqlite visible to git 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 --force never rewrites the guard block; the fresh-vault created count moves from seven to eight. The o2b brain doctor pass answers for existing vaults with the git-tracked-derived-state check: derived state under .open-second-brain/ that git actually tracks - measured by git ls-files, never inferred from status - is an ERROR naming one location-independent git -C <vault> rm -r --cached -- .open-second-brain command 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 --readiness fails 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 reads CLAUDE.md, codex, cursor, opencode and aider read AGENTS.md, and gemini-cli reads GEMINI.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_search among them, write-classified by the write-mode-dominant rule). Every tool is classified read or write in the registry at registration - write-mode-dominant, so a tool that can write is write, decided per operation family and never from arguments at dispatch, and fail-soft telemetry appends do not make a tool write - and the classification is metadata only: it never reaches the tools/list wire payload, and a definition that arrives without one is treated as write, 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 response id and progress by progressToken - 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.md documents the markdown_neutralize_mode write-side trust switch, the git-tracked-derived-state and planted-structure-rows doctor rows, the placement-shadowing failure on the doctor readiness row and the bench-runs ignore the init guard block seeds; docs/mcp.md documents the stdio request-ordering contract - concurrent reads, exclusive writes, out-of-order responses correlated by id; docs/observability.md names 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 actions list in view: digest is collected vault-wide, and its rows were filtered by transport reach alone, so a caller delivering the digest under agent_scope could 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 judgment brain_doctor's suggested_actions and the operator summary's top_actions already apply - and osb://digest/latest threads its resolved scope into the render the way osb://preferences/active does. Without integrity.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: fail a caller with a resolved identity could receive another owner's preference ids, transition reasons and source pointers from status_transitions, retired and contradictions; 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=today and the operator summary were already scoped and are pinned by regression rows; without the gate, or under warn, 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 override OPEN_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 a tail-trim skip reason, never partially re-cut. The freed budget is then offered back to over-budget candidates 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 conditional tail_trim block (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 the context_receipt's budget block and the context_pack telemetry record's metadata, and each dropped item's skip reason surfaces as the telemetry gap tail_trim. With the key off - the default - packs are byte-identical, and the packing_policy branch 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_presence continuity record per checked turn, beside the annotation it already wrote: a verdict per injected note - verified when a reply span checked out against it, the strongest miss verdict otherwise, and absent for 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 show and the MCP brain_context_receipts show answer with a join matched by exact session_id and folded over every presence record the session wrote at or after the receipt's own creation, chronologically, with verified winning across turns - absent never 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 the unjoined outcome 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. list stays a summary.

  • session_inject receipts carry the session key. The SessionStart active-inject receipt records the payload's real session id as its session_id member - 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 the o2b brain export json and llms-txt preference dumps and the o2b brain bank-export bundle before their bytes are composed, is blocked by the knowledge pack's selection step beside the existing visibility and owner blocks, and is withheld from the static o2b brain explorer --export HTML 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 an omitted field and the bundle a preferences_omitted report (the pack names the page in its blocked list), the stderr notice names each denied row with the frontmatter key that denied it (denied_by: visibility, access, confidentiality or sensitivity), 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-reserved is 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 carries exportPreferencesLlmsTxtDetailed beside 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, and list --json rows carrying only has_payload, so the bulk surface agents and cron logs read never prints a payload (show --json keeps the frozen items for the operator's one-letter read); v1 letters (written before the payload was frozen) parse unchanged and classify re-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 classifies not_recoverable before 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; --apply performs the re-run and requires --yes under --json or 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 lint always 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 --dirty with exit 2 naming --paths as the remedy, and giving neither or both selectors is a usage error. Report-only: the fixes stay with brain doctor --repair --apply and brain lint --consolidate --apply, which every finding's next_command names.

  • Whole-folder move and rename on the note-lifecycle surface. o2b brain note-lifecycle move-folder relocates a whole folder under a destination parent and rename-folder renames it within its parent - split for the same reason the file rename and move are split, so a caller that meant one and spells the other is refused wrong_action rather than moved by accident - and the MCP brain_note_lifecycle tool accepts both through the same action enum. The folder surface mirrors the file envelope rung for rung (vault-relative, no traversal, the Brain/ machinery root refused, the vault-identity write guard) minus the .md requirement, 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 in moveFailures with 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 status gains a store group: 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 as free; 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 with INDEX_LOCKED, while vault and memory writes serialize on the brain lane's own lock files and are not gated by it; and writerLockOwner is null while 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 MCP brain_status withholds the resolved lock path below local reach while keeping every state field. o2b doctor --readiness gains the client_versions probe: each detected agent client is asked its version through its own version argv under the per-probe timeout, one row per target - pass with the raw first stdout line, unknown naming why (a thrown runner, a non-zero exit, a timeout, empty stdout), or skipped when 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.md gains the o2b brain dead-letters and o2b brain validate rows, the move-folder/rename-folder actions 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.md documents the tail_trim block on brain_context_pack, the reply_presence join on brain_context_receipts show, the folder actions on brain_note_lifecycle, the brain_status store group and the CLI-only validate/dead-letters pointer for hook-less agents; docs/observability.md states the tail-trim telemetry counters; docs/how-it-works.md describes the trim engine; hooks/README.md documents 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 and o2b 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_write bridge into the continuity ledger as host_memory_write records (Brain/log/continuity/<YYYY-MM>[.<deviceId>].jsonl), and until now the only readback was the full-fidelity brain 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 each host_memory_write record as a preference page under Brain/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. --apply writes: an --approval-digest <hex> mismatch refuses with nothing written and no snapshot, mirroring the approval semantics of o2b 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: true under --json) and refuses every apply until the operator reconciles the file, and a conflict - a pref-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-MM narrows the source window to one ledger month. No supersession is inferred: a replace record 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 export answers "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_size therefore 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 reject stamped retired_by: [[Brain/log/<date>]] while the log appender - for a vault with a configured device_id - writes Brain/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_modules with it, so the first launcher command died importing proper-lockfile before install-cli could repair anything. The bun launchers - o2b, o2b-mcp, o2b-aider, vault-log and their Windows twins - now probe for the dependency tree with one file stat and run bun install --frozen-lockfile once 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: private reserves like access: private. Existing search indexes re-measure the indexed visibility column of marker pages on upgrade (schema v16). The update_note guard refuses a marker-key write only when it would clear or forge the page's deny status - access: read-only and a public to team move are writable again - and brain privacy-marker-restamp now 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 check no 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, and o2b doctor --readiness reports 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. Lovelace and Lovelace, Ada resolve, short operator nicknames like al resolve (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 3D are 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-derived carrying all of it back. Source summaries ingested over MCP are now stamped content_class: prompt-derived themselves.

Added
  • Ordered decision-model providers with fallback, and per-model thresholds in the installation. decision_model_providers accepts an ordered list, with the single decision_model_provider kept 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 every decision_model_call record names the provider and model that actually answered, so reports never pool two models. o2b decision-model check shows the order and which entries answer. Per-model enforce thresholds move from the compiled DECISION_THRESHOLD_PROFILES table - now the hosted baseline family only - into decision-thresholds.json beside config.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 as shadow and check/report say 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, and thresholds list / thresholds set round the flow out. Docs: docs/decision-models.md and docs/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-export composes its bundle from the pages the content-class split kept, and index.md and log.md were 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 in okf.json nor in index.md - an index listing pages the shipped manifest does not carry would make the bundle disagree with itself - and log.md stays 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: secret or sensitivity: confidential was already withheld from a remote caller, but every consumer that reads visibility: 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-restamp makes the claim formal - dry run by default, --apply to write - unioning the token into visibility: 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's skipped list) 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, and update_note refuses changes to the three marker keys exactly as it refuses changes to the reserved visibility key, 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 --json carries a bounded held_chunks report - position-ordered, capped at 20 with the whole named by total, 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_from boundary 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 names o2b brain claims as 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 its blocked list. Pages written before this release carry no mark and export exactly as they did; that era boundary is documented in docs/cli-reference.md and SECURITY.md rather 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=false and embedding_provider=disabled used 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 --readiness gains 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_brief view="today" carry their new section additively. The rendered dashboard gains a ## Pledges block between obligations and open loops and the totals lines Pledges: and Pledges overdue:; the --json envelopes gain pledgesTotal and pledgesOverdue inside totals, and the brain_brief view="today" response gains a pledges member beside obligations. 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>.md with 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). fulfill stamps the fulfillment date once and a second stamp refuses; list sorts by due then slug with an overdue flag and whole days until due, open pledges only; remove retires the page into Brain/pledges/archive/ with a collision suffix, the obligations pattern. Counterparties ride the promised_to frontmatter key, a new promised_to relation 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_pledge and brain_intake_pledges. The first is direct CRUD over the family - add, fulfill, list, show, remove - mirroring the brain_obligation operation shape, with schemas that reject unknown properties and answers that carry the page path; 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 today and brain_brief view="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_facts section on brain_context_pack, a rollup over work the pack already did. With entity_facts_context_pack: "true" (machine-local config, or env OPEN_SECOND_BRAIN_ENTITY_FACTS_CONTEXT_PACK; default off), the pack response gains entity_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 env OPEN_SECOND_BRAIN_COUNTERPARTY_BRIEF_ENABLED; default off) and a non-empty operator-declared counterparty_brief_triggers vocabulary (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 reserved private token 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_paths entry, a frozen default ignore the narrow walk does not know, or outside the include_paths allowlist - 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_presets suggest (and o2b brain context-presets suggest) now ranks an explicit context_window_tokens - and a model the exact-id table knows - above a matching model_hints token. This is a deliberate order change from the hint-first draft, not a regression: the hint check used to run first, so gpt-4o-mini with an explicit 128,000 window answered tight-context from the "mini" hint, and the table-known gpt-4.1-mini answered 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 explicit context_window_tokens is 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-context below, long-context above - so a 128k, a 200k and a 1M window all land in long-context and receive the same derived guidance; the widened recognition (the [1m] bracket form, provider and region prefixes, dated snapshots, -vN:N revision 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 least min_topics (floor 2) distinct topics whose principle text reaches a jaccard similarity of jaccard (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 a Brain/cross-topic-rules.json ledger (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_by wikilinks to every source, a related lineage - written by the dream apply stage through the transactional preference writer with overwrite off, under a content-derived cross-<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 as cross_topic_rules and 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_tokens config key, then OPEN_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 like openai/ and us.anthropic., trailing date snapshots like gpt-4.1-2025-04-14 and Bedrock -vN:N revision suffixes still reach the exact row), then the structural tier - a trailing, delimited 1m suffix (-1m / _1m, or the trailing [1m] bracket form hosts report like claude-sonnet-5-5[1m]; a dot-led version fragment like the 4.1m in gpt-4.1m is 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 (the 1m suffix 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 (and o2b brain context-pack) accepts model and makes max_tokens optional - omitted, the budget is the matching preset's context_pack.max_tokens for the resolved window and the response names the deciding channel in max_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_presets suggest (and the CLI twin) resolves a window from --model alone 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_pack closes the unbounded-addendum case with resolver-derived caps, an unparseable window env value keeping the historical unbounded caps beside a named window_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, caching decision_model_eval_grade records stamped with the spec hash, profile, use, seed, item ids, boolean verdicts, metric and counted n (origin eval, spend counted toward the daily cost gate, malformed records skipped by the lister with a named note); o2b decision-model eval list renders them oldest first in prose or --json, and o2b decision-model check names 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 a promote verdict, 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 doctor reflects 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.behindCount is 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 (one readdir plus one stat per 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 status prints the same census as index_behind: in its freshen block (<n> doc(s), or (not measured)) and serializes it as index_behind in the --json freshen 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.json once 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 in results/ 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 in error remains advisory. The guards add zero stdout on success, existing run ids, checkpoints and reports are byte-identical (fixtureHash stays content-only), and the per-question results and report.json are now written atomically.

  • The search query language ships as a versioned JSON Schema artifact, with export and check verbs. 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 served brain_search inputSchema (byte-identically) and the o2b search CLI flag table, so the published artifact cannot disagree with either surface; a $defs.search_query_document block publishes the grammar of the line-oriented query-document program (the intent: / 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 check re-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 schema is 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_lovelace answers Ada Lovelace while a slash-bearing form like Warner/Chappell never anchors through a bare Chappell; 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 genitive Annas and 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 - Canada never anchors Ada. 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]] when New is 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: claimed says 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 - answers not_claimed with the freshen machinery's own reason surfaced beside the token: index_refresh_reason on the create, update and append results, refresh_reason on 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 relink lane 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 stored entities: 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 .md suffix), 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 one entities: 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, or relink_apply over 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_chars before 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 into src/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 the untrusted_source_delimiting flag 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-locale Intl.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 index bytes and second_brain_query ordering - while the raw directory listings and the search pipeline stay untouched.

Changed
  • The write-ledger surfaces refuse a since / until that is neither a bare YYYY-MM-DD nor a canonical UTC timestamp. The ledger readers (brain writes list, destinations and revert, and the brain_writes and brain_write_destinations tools) 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 (since to that day's start, until through 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_PARAMS blaming the argument on the MCP tools - so a mistyped window says so instead of answering empty.

Fixed
  • brain writes revert --device "" - and the brain_writes tool's device: "" - 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 --path the 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-on duplicate-entity collision and its opt-in entity-alias-candidate nomination 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-run preview 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, --force for agents, and a --json run refuses without --force because the prompt cannot render. The text-level merge guard runs over the two bodies unless --force bypasses 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 Brain merge log event after the bytes land. The brain_entity MCP tool stays read-only; writes remain CLI-only by convention.

  • A destination-keyed view over the write ledger: o2b brain writes destinations and the brain_write_destinations tool. One shared core reader walks the log and folds the note-write and write-session kinds into one row per vault-relative destination, each row carrying the contributing agents and devices, per-kind counts, the newest recorded touch and a kinds coverage 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 / --until narrow the walk, and agent, device and path filters cut by exact identity; the tool pages by the shared offset cursor with total_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 (env OPEN_SECOND_BRAIN_LOG_CATCHUP_ENABLED, default off), the log-catchup lane hands the agent the Brain/log lines other agents appended since its identity's last look on this device - the newest 50 as one fenced additionalContext block 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/log stays 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 declares okf_version: "0.2" in the bundle-root index.md frontmatter, the only frontmatter an index file may carry. Every page gains the spec's per-concept keys as derived fields: a sources list translated from the citations the exporter already collects, with resource required - 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 a generated block (by, at) stamped with the one shared instant the manifest's generated_at carries, so whole-bundle byte equality modulo that single instant stays the determinism contract. Page status lands on the spec's closed draft | stable | deprecated enum through a conservative map pinned to the frontmatter status: field: deprecated, retired, superseded and archived map to deprecated, done, closed and resolved map to stable - completion is never deprecation - draft stays draft, and anything else reads as stable. The importer admits a bundle whose index declares okf_version: "0.2" beside its own schema: "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 declares okf_raw: true and carries its native status / sources / generated values under the producer namespace as okf_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_providers entry point, so pip discovery lands on the plugin module. pyproject.toml declares the entry-point group with open-second-brain = "plugins.hermes" - the module that already exposes the register callable - 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 (a register callable there), and the standing pin that prefetch stays the single injection lane holds. install/hermes.md names the discovery path and its one caveat: the entry point resolves from the wheel by itself, but the provider bridge still needs o2b on 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 --apply and brain unprotect carry 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---apply behavior. o2b install --target X --apply, o2b brain protect --apply and o2b brain doctor --repair --apply refuse with a non-zero exit in a --json or non-TTY run unless --yes is 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 --yes consent of a real write), interactive-yes (a confirmed prompt) or unchanged (a no-op that never reached the gate) on the CLI lanes, and caller-apply on the MCP brain_doctor apply: true path, 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, and refused stays reserved for a future interactive-no envelope. Both brain protect --apply and brain doctor --repair print a unified diff of the intended change before anything is written. Headless callers must add --yes (install exit code 6 names it).

  • brain unprotect joins the consent lanes, which breaks headless scripts that called it unattended. o2b brain unprotect used to rewrite the protected file on every call, with no consent, no backup and no record. Open Second Brain now makes plain brain unprotect a read-only preview - under --json the answer is a "preview": true envelope carrying the before/after pair, so a headless consumer can tell it from the apply envelope it shares keys with - and --yes without --apply is refused by name, because unprotect writes only through the consent ladder's apply step. brain unprotect --apply runs the same fail-closed ladder as the other guarded host-write lanes: a --json or non-TTY run refuses with a non-zero exit unless --yes is 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 new dismissed skip reason. The acted status keeps its cooldown window, and suppress stays clockless.

Added
  • A converse decision-model route for bearer-authenticated Converse-protocol (Bedrock-style) gateways. The adapter union gains a fourth adapter beside systemone, vercel-evaluate and llm-emulation, with a converse preset (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; a stopReason that is not end_turn refuses the reply rather than half-parsing it. The route is declared uncalibrated - the reply is generated text whose probabilities the model wrote itself - so an enforce setting is a config error until the operator opts in with decision_model_allow_uncalibrated: true, and its own converse threshold profile tunes no use yet, so a permitted enforce still runs as shadow until 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 new decision-model-converse registry id, the contract suite's fourth route applies the full failure matrix to the adapter, and o2b decision-model check, doctor and 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_backup on (env OPEN_SECOND_BRAIN_HOST_SETTINGS_BACKUP over the _brain.yaml key, default off), a Write/Edit/MultiEdit/apply_patch call whose path canonically names a .claude/settings.json or .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 at 0600 - 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 stdout permissionDecision: "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 in hooks/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_decision action=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_session op=list, brain_generation_reports list, brain_context_receipts list, brain_event_trace, brain_analytics view=timeline, brain_maintenance status journal, brain_scaffold_stub list and the ordered-walk diagnostic brain_session_grep, whose coverage census stays single-pass and refuses a cursor Each declares a cursor input, cuts one offset window over the full filtered set through the one core cursor helper, and answers next_cursor present in every response and null when 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 (the brain_maintenance status journal gains journal_total, the count its page cuts into). Three surfaces pin their default page to the historical answer: a limit-less brain_session_grep and brain_scaffold_stub list return the bounded default (10 and 100) with a next_cursor for the rest rather than the whole set - with include_raw, brain_session_grep runs 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 - and brain_agent_diff, which caps nothing, states that its limit bounds 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 a limit now 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 (with brain_truth keeping its documented advance-since time-window paging), and the two ranked-retrieval tools brain_search and brain_file_context deliberately limit-only, because offset-paging a ranked, index-dependent answer would present an index race as data. Two of the walking scanners, brain_event_trace and brain_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; their total (and brain_event_trace's trace_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-less brain_event_trace call still reads the day whole.

  • A snooze action on the trigger queue, with unsnooze and a terminal snoozed status. o2b brain trigger snooze <id> and the MCP brain_trigger snooze operation 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; unsnooze wakes 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 new snoozed skip 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 from list, present in history, 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, and unsnooze on 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 to enforce again, as it did before 1.86.0; measure-only mode is reached only through an explicit search_trust_gate_mode: shadow. The explain envelope names the resolved mode under enforce as well as shadow, 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 contradicts edges 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 accepts searchTrustGateMode and refuses the obsolete retrievalTrustGateEnabled by 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 --json hands 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 in mining_ledger_failure, so turns no model saw are never suppressed; a yield is recorded either way. A relayed surveyed_turns_hash is 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 --status stay 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-backlog hands out extract-signals mining steps for the next five undistilled sessions (mining_steps, one per session identity, with mining_steps_remaining), and answering a step records the session as mined. --status and --discover keep reporting coverage when the mining census fails, naming the census as unavailable. A session imported under --recall-session-id is 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 invalid storage: block, as brain_ingest_source does, 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 and brain_delete_by_source.

  • Receipt compaction keeps skill_invoked records, 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 .bak that 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 true to shadow no longer holds: it maps to enforce.

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-signals and the brain_extract_signals tool 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 } } with llm_step: null and empty turns_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 (MCP force) 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, naming mining_ledger_failure on 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), or undistilled (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-backlog turns 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 (env OPEN_SECOND_BRAIN_BACKLOG_NUDGE_ENABLED, default off) arms a SessionStart hook that reports the undistilled count as one additionalContext line, 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 (env OPEN_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 legacy search_trust_gate_enabled boolean maps true to shadow and false to off. off runs no gate at all and stays byte-identical; shadow runs 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; enforce is 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 --explain envelope 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 contradicts arrival is the counted exclusion, superseded_by stays the annotated supersession backbone, and positive relations boost. Under enforce an ineligible arrival leaves the arm's contribution set; under shadow it 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 - with KEPT_CONTINUITY_KINDS naming 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 protected session_entity_mention (its payload carries a session_ref), so a confirmed compact folded the records the session-recall seed, the hallways pass and the mining dedupe read back, the by-id generation_report lookups, the row-count-derived recent-turn sequence, and receipt_compaction's own audit record - the next run folding the run before it. recall_observed_use and negative_recall sit 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 --shard answers 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-mode compact --apply whose 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 daysToCap figure 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 carries storage.pause_percent (template-documented, defaulting to the warn band, validated strictly below critical_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 the skipped:capacity verdict while interactive ingest still works until the critical refusal. And the gate records its census as a storage_usage sample 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_into the canonical page (additive, the manifest schema stays at 3): an unchanged marked source answers a retired bucket and queues out onto skipped_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 in Brain/ 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_bytes to 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-safe toByte and 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_source now carries extraction_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_path when set) points at rather than the default, so the lane gate, the ingest refusal and the doctor check agree about the same vault. appendMetric takes 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 the storage_usage samples the gate and doctor read. deleteBySource's per-file failed list now reaches both front doors - the forget-source verb'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 named collect_errors row on every surface instead of a thin report that reads as healthy. And the live explorer routes refuse a non-loopback Host header by name.

Changed
  • Docs: docs/cli-reference.md states the five storage thresholds with the pause_percent band, 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-backlog mode with its conflicts, adds the backlog_nudge_enabled hook flag, and extends the o2b brain explorer entry with the console routes; docs/how-it-works.md states 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 storage and brain_storage. One builder feeds both surfaces, and each invocation takes one census and appends one storage_usage sample, 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, warn or critical, or unbounded, 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: joins Brain/_brain.yaml through the config template and the resolver: cap_bytes plus 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 absent cap_bytes means 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:unmeasurable journal row, the same honesty the host-pressure gate practices - and a closed gate skips the pass with its own skipped:capacity verdict, 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 status renders the tier word and the unmeasurable reason without the three byte-and-percent figures. --force (MCP force) 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_source refuses with capacity_exhausted once 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 receipts and brain_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. compact folds expired records into per-kind, per-month aggregates inside the named shards - the mode and the family:name shard 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 (MCP apply) is passed, takes a recovery-point snapshot before a confirmed run, rewrites each shard atomically, and honors a count guard: --expect N (MCP expect) asserts exactly N records will fold and aborts without writing on a mismatch, and --strict refuses 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 - the source_invalidation audit trail and the two context-compaction kinds, whose vocabulary a storage operation must not collide with - so their lines stay verbatim, and one receipt_compaction audit record closes a confirmed run. Below local reach compact is refused by name; scan stays readable at any reach, and the receipt-ledger-growth doctor check reports ledger growth the same way.

  • Near-duplicate document retirement. The opt-in document-dedup hygiene 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 newest created_at as the canonical, a page without created_at being ineligible rather than guessed at by mtime. apply routes 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 in Brain/ 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 one source_invalidation audit 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-capacity reports 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-growth reads 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 cleanup o2b brain forget-source and the brain_delete_by_source tool 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 a failed array of path and message records naming what stayed on disk, and the source_invalidation audit 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 --apply was 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.md corrects the maintenance-lane prose to the five-gate reality and the actual --force semantics, gains a storage-lifecycle section documenting o2b brain storage and o2b brain receipts with their flags and exit codes, and adds the opt-in document-dedup detector to the hygiene row; docs/mcp.md raises its advertised tool count to 117 and adds brain_storage and brain_receipts to the Tool Highlights table; docs/how-it-works.md states the lane's five gates; docs/metrics.md documents the storage_usage sample 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. parsePreference reads scope: alpha,beta back verbatim, and records written before the v1.78.0 tag hardening can carry that shape, but the refresh writer composed the whole value as one brain/scope/... tag, the shared tag rule refused the comma, and dream threw TagSyntaxError on 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 own brain/scope/<segment> tag, duplicates collapse, and the scope field 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 of brain merge compose leniently instead, because a vault written before v1.78.0 can hold a scope such as front 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 the scope field 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_CONFIG after 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 no agent_name and no VAULT_AGENT_NAME.

  • The iterative-recall deadline test no longer races wall time. It simulated an expired deadline by shifting Date.now a 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_ms is 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 runKeywordLane runs: 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 the hybrid 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 the hybrid_degraded signal 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_ms on search --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 integer overlapped_ms beside semantic_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_pack gains packing_policy (facts_first or retired_first), strictly opt-in and byte-identical when omitted: inside one pack the preference-derived pages and the retired pages already share the single max_tokens budget, and the policy names which lane claims it first - facts_first claims for the distilled facts lane before the pages lane fills the exact remainder, retired_first hands the first claim to the retired pages lane - query-ordered in the ranked and semantic query 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 under facts_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 shipped over-budget skip reason. The pure allocateRecallBudget primitive lands beside the shared applyCharBudget (frozen vocabulary, narrowing guard, lane allocations plus any reserved charge always summing to the total), and the report's packing accounting - 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 mirrors query_mode argument for argument: the schema enum derives from the frozen tuple, the query pairing is declared dependentRequired style, and every refusal arrives on the wire as INVALID_PARAMS carrying the named message; the response gains packing only 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 (the query_mode MCP-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_ms in 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 reports overlapped_ms per question and its mean in the aggregate latency_ms family - 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 human overlapped_ms: avg N line prints only when the aggregate carries the figure.

  • Opt-in standing-instruction facets with first-mention provenance. The standing_query attention action gains a standingInstructionFacets option: unset, every emitted string is the legacy pref-<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 structured StandingInstructionFacet entry - the preference-corpus sibling of ThreadFacet, same contract shape (stable pref:<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) over evidenced_by wikilinks and producer-aware session_ref parsing: import-session basename#turn resolves as a turn, capture-lifecycle session:<id>#<event> as an origin event labeled as what it is, and the bare session:<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_instruction bench category gates the facet lane. The new category joins BENCH_CATEGORIES and drives the shipped surfacing call with the facet option on over the fixture vault, so the lane ships measured: evaluateQuestion asserts 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. BenchPinnedKnobs gains 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 human standing_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, default 0 - 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-empty session_ref frontmatter value (imported signal notes), else a non-empty session_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 code source-session-capped-rows (detail.dropped against detail.cap), noted once per answer. The knob rides the search_mmr_lambda plumbing exactly: parsed in resolveSearchConfig, range-validated in validateResolvedConfig (a negative value is refused), surfaced as the optional ResolvedRecallConfig.poolMaxPerSession spread-omitted at 0 so exact-shape resolved-config assertions keep holding, with a per-query SearchOptions.poolMaxPerSession override 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 --pin accepts the knob by name ({"recall":{"poolMaxPerSession":N}}). Off (0 or 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. With skills_attach_usage_ordering (OPEN_SECOND_BRAIN_SKILLS_ATTACH_USAGE_ORDERING, default off) on, observed skill_invoked usage reorders the survivors of the discriminating-term floor by the bounded composite score * (1 + 0.5 * weight) - SKILL_USAGE_ORDERING_MAX_BOOST = 0.5, weight the 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 returned score stays the untouched lexical score. The pure skillUsageWeights(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 Code Skill tool 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 drops private rows 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 new sampling capability - 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 in abstention_excluded), stratifies the survivors by their present question_type values 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 by sha256Hex(seed) - no Math.random, no wall clock - over entries sorted by q_id, so dataset file order cannot change the subset; the selection re-sorts by q_id, and a population of zero after abstention exclusion raises the named LongMemEvalSampleError. Sampling happens before conversion, so a sampled run materializes fixture notes and continuity records only for the questions it scores; the receipt gains sample_population, sample_selected and the per-stratum sample_strata beside abstention_excluded, and the report line gains sample_selected N only on a sampled run. The seed and cap fold into benchRunIdentityHash spread-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.servers object - and registers there - or keeps the legacy top-level mcp key every earlier release wrote. When only opencode.jsonc exists 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 named JsoncEditError refusal, and a BOM is stripped on read and restored on write. An existing healthy top-level registration is never migrated when mcp.servers appears (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> or mcp.<entry>), and uninstall removes exactly those keys under either shape. When the host adds mcp.servers to a config the adapter previously owned, the first --check reports 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 nested mcp.servers key so a healthy V2 install passes o2b doctor.

Changed
  • Docs: docs/cli-reference.md gains the search_pool_max_per_session row in the recall-quality config table, the source-session-capped-rows row 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.md covers the cap in the recall-layers ranking paragraph; docs/mcp.md adds skills_attach_usage_ordering to the skills_attach opt-in list with the name-normalization rule, the private-records note and the per-call cost; docs/observability.md documents the sampling flags, the stratified selection, the receipt fields and the identity fold; install/opencode.md documents the V2 shape, the .jsonc path, shape-aware uninstall and the one-time drift; docs/updating.md gains 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 into plugins/codex/README.md by 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 knob search_iterative_recall_enabled on (OPEN_SECOND_BRAIN_SEARCH_ITERATIVE; the round cap search_iterative_recall_max_rounds / OPEN_SECOND_BRAIN_SEARCH_ITERATIVE_MAX_ROUNDS defaults to 2 and clamps to 8), a query marked iterative: true runs 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 an iterative marker at phase: "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 with iterative_token plus iterative_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: any iterative field combined with global: true is 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 at phase: "final", carrying the per-round records (terms, groups, added, elapsed, and whether the deterministic floor or the decider authored the sub-queries) and the stop_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 budget clamp(distinct facets, floor 12, cap 16), the selection returns in chronological order under the required char_budget beside recomputable diagnostics that report the realized topic count honestly, and entity_key optionally seeds the fan-out from the session_entity_mention rows 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_budget under match, and query, since, before, limit, snippet_chars, include_raw and raw_budget_chars under coverage, are each refused as not applicable. A budget that cannot hold the floor-weighted selection is a typed infeasibility mapped to INVALID_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_describe accepts topics: true, adding the topic_report the 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>] and o2b 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 --suite stays the byte-identical default path. --suite longmemeval adds 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, and expected_text is 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 family longmemeval: {recall_all_at_5, abstention_excluded} beside the untouched quality, latency and context_cost families. The judge glue reuses bench_judge_cmd: when the suite provides them, the stdin payload's questions carry question, answer, gold and 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's evaluate_qa.py rather 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-replay replays 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-turn injected_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 through resolveSearchConfig and 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_gate and 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 measured brain_recall_gate call returns key and tier beside 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's RECALL_INJECT_CONFIDENCE_FLOOR now derived from that single source, with no new config knobs. Gate telemetry under the existing recall_gate_telemetry opt-in records optional key, tier and channel on measured decisions and summarizes counts by tier; the opt-in-off path records nothing, and no new record kind exists.

  • Docs: docs/mcp.md documents the iterative round-trip, the coverage operation with its argument partition, the graded surfacing key on brain_recall_gate and the widened gate telemetry; docs/cli-reference.md documents the coverage flags with their CLI parity, the iterative-recall and graded-key sections, and the bench suites with the run identity; docs/observability.md covers 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. createSnapshot lists the top-level Brain/ entries with readdirSync(dir, { encoding: "buffer" }) and drops .snapshots and .artifacts by name. The name was read with toString(), which is right for a Buffer but renders a bare Uint8Array as comma-joined byte values ("46,100,..."); on a runtime that returns that shape nothing matched, .snapshots was packed into every archive, and each dream snapshot carried its predecessors until a run outgrew its timeout. Entries now decode through Buffer.from, which reads either shape, and tests pin both the archive members and the bare Uint8Array decode. Bun 1.4.0 and 1.4.2 on Linux return Buffer entries, 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: oxfmt 0.72.0, oxlint 1.87.0, @types/node 22.20.5 and @node-rs/jieba 2.0.4; CI actions actions/upload-artifact 7.0.2 and hashgraph-online/ai-plugin-scanner-action 1.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-unavailable and not-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 is source-truncated, never not-in-note; a note that cannot be read is note-unavailable, because "we could not check" is a finding, not an all-clear. One additionalContext annotation 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 the quote_check block of Brain/_brain.yaml - recall, active and subagent, each off|shadow|enforce, default shadow (annotate-only), scoped to the phases whose injection the vault enabled - and enforce additionally appends exactly one Brain-log note event per turn carrying the counts and the finding lines, so findings survive the session.

  • Statement-level claim attribution. brain_truth state and o2b brain truth state accept an optional opaque sample_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's sample_id never 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 a sample_id renders 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 stamps source_archive_hash and source_archive_bytes on the summary page and returns source_archive_hash on 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 doctor prints a count-only archive section (entries, bytes, missing, mismatched, uncited - no fix verb, pruning and recapture are operator calls), brain_hygiene gains archive_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_id is 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.md documents the truth events/truth state verbs with --sample-id and provenance-hydrated reads, the widened capture-scope detector and the doctor's archive section; docs/mcp.md covers archive_get on brain_hygiene, the archive stamp on the ingest result and the claim provenance hydration; docs/observability.md documents the quote-check surface and the no-session rule with its doctor count; hooks/README.md gains 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_attributes snapshot 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 a status=done query 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_usd and search 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) into Brain/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's entity-label-malformed findings and the snapshot-gated, dry-run-default o2b 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|dismiss folds that ledger into pairwise-PMI associations written to Brain/link-graph/hallways.json beside, never merged into, the vault-derived co-occurrence graph, with a reviewable proposals file and cross-run dismissals. Mining itself never writes an entity: promote is 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_kind column (page for chunk 0, else section) with taxonomy indexes, a bounded document_attributes side 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+FFFF over 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 kind filter (page/section, MCP argument and CLI --kind) pushes into the SQL of both lanes - before ORDER BY/LIMIT on 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: type pushes the scalar declared values as the one normalized token the index stores, pinned pushes only for true, and visibility pushes its normalized tokens. tags and 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 bounded document_attributes snapshot - 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 lang field (default en, validated like answer), and o2b search rerank-eval reports 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_telemetry grows a seventh operation, dead_weight (CLI o2b 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 usual vault_path envelope, 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 export until 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.md documents the hallways verb, the lexicon rebuild, the dead-weight verb and a kind-and-property-pushdown section stating the as-indexed semantics; docs/mcp.md covers the kind argument, the promoted-key pushdown and the dead_weight operation; docs/observability.md documents the dead-weight fold; the README names the release.

Fixed
  • A staged ingest no longer registers itself as done. On a stage verdict 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_source and 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's deny-consulted-lanes finding 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|list manages one token per agent in .open-second-brain/secrets/mcp-tokens.json beside 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 the mcp_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-key stays valid as the operator master credential with the process's configured identity, and a revoked or unknown token gets the same generic 401 body as a missing credential, so no oracle distinguishes them. Rotation and revocation take effect on the next request with no server restart. The new mcp_tokens_required device key (default false, env twin OPEN_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.json recording the owned entries, the token name and prefix, and the applied time. A second identical run is a byte-identical no-op, --rotate re-mints under the same name and reprints once with the new material authenticating on the next request, --check verifies drift, and unsupported targets are refused naming the available list. The config-writing adapters (codex, grok, opencode) apply directly, generic prints 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 - resolves allow/ask/deny per agent, operator-defined role and target-scoped entry for the write, ingest and owner_write actions. The loader is strict: default_action is required, unknown keys warn, a version other 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/, carrying actor, via, action, target, verdict, the deciding source (entry, agent, role, default or gate key), the reason token and optional tool and correlation_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 show prints the effective document plus a dry-run decision table over the declared agents, ledger reads the filtered rows, and o2b brain doctor reports an unreadable document as the permissions-unreadable finding 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 at Brain/pending/ingest/, while registration completes and cleanup removes the staged page. The signal gate now resolves inside writeSignal, 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 status staged with a pending_id, and staged receipts on the writer tools carry the next command o2b brain pending apply <pending-id>. o2b brain pending list shows every lane sorted with --lane filtering and named-unreadable entries, apply is an exclusive create with a typed conflict on an occupied target, and --dry-run runs every check and writes nothing. Toggles are per lane: write_approval.notes and write_approval.ingest each fall back to the write_approval.enabled master (default off) when absent.

  • Staged documents leave recall. Brain/pending/ is excluded from search-index admission (reason review-pending), so brain_search and 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, default off, unreadable config fails closed to fail) 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 where owner joins the refused frontmatter keys for creates and batch updates under fail. The refusal carries the owner-write-refused token (owner_write_refused on 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. warn allows and appends exactly one decision-ledger row carrying the gate key as its source, and a document's owner_write verdict 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 at Brain/decisions/open-<slug>.md - a frozen key table, JSON-quoted free text, a ## Question/## Options/## Context body, a directory lock around transitions, and a typed duplicate refusal naming the existing id. list_open partitions by status with unreadable records named, show_open reads one back, resolve <id> --choice <c> mints the real type: decision page through the ordinary record path and stamps the open record resolved with a [[decision-<slug>]] pointer plus exactly one open-resolved receipt, and discard <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 the decision-open, decision-resolved and decision-discarded log kinds keep the lifecycle in the Brain log timeline.

  • Consent and a TTL for ambient capture. guardrails.ambient_writeback: false suppresses the ambient extraction lane with a counted, logged ambient-withheld event per capture and no signal written (absent keeps today's behavior, since the lane already writes on capture); guardrails.ambient_ttl_days stamps expiration_date at 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_confirmed gap. With a permissions document present, force_confirmed writes a confirmed preference only when the caller's write verdict resolves to allow; otherwise the refusal answers with the force-confirmed-requires-allow token and the o2b brain permissions show exit, and document-absent behavior is unchanged. A document deny refuses the write before any filesystem effect with the typed write-refused error 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.md documents o2b bootstrap, o2b mcp token, o2b brain permissions, the widened o2b brain pending and the open-decision actions; docs/mcp.md documents the per-agent token authentication, the new refusal tokens with their next commands and the open-decision actions on brain_decision (no new MCP tool - the advertised surface is unchanged); docs/observability.md gains 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-env or stdin exactly as unlock does; the MCP server unlocks once from OPEN_SECOND_BRAIN_SECRETS_PASSPHRASE at its first key use and drops the variable; o2b brain secret unwrap restores 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 --apply accepts --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, divergent fires 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_batch create op or brain_ingest_source answers the pending-stage-conflict code 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 --remove tears 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 unlock wraps 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; lock clears 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 FILE re-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 FILE verifies 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:NAME references 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 the vault_path MCP 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: no last_used_at stamp, no exec custody record, no store mutation. o2b secrets list and status accept --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-run seals the adoption plan - plans, skipped entries, conflicts, unchanged rows, no wall-clock fields - and prints its digest; a non-interactive --apply must carry --approval-digest (exit 2 otherwise, mirroring the --yes guard, 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-run refuses, because a dry run computes one and does not consume one. o2b brain upgrade seals 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 reports divergent: true beside a bounded newest-first-count, oldest-first sample of id / created_at / content_hash rows - 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_changed and contract_changed_files on 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-tags hygiene 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_types in Brain/_brain.yaml refuses 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.md documents the lock/unlock/export/import verbs, the --vault join on o2b secrets list/status, --approval-digest on the import and the contract-changed batch-plan report; docs/how-it-works.md states 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 ingest and the brain_truth ingest operation accept the window (--valid-from/--valid-until; a malformed or inverted bound is refused by name, as INVALID_PARAMS on MCP and exit 2 on the CLI), and with the flags absent each open bound is resolved from the source record's frontmatter valid_from / valid_until and 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_truth events operation and o2b brain truth events recall a slice of the ledger through one shared report, so shape, ascending-ts ordering, gating and paging cannot drift between the surfaces. entity filters by canonical name (display spellings accepted), since / until bound the assertion time through the shared time-range grammar (a claim's validity window is a different axis and rides on the rows verbatim; since floors 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 gate brain_claims asks.

  • Grounded agent-stated claims. The brain_truth state operation and o2b brain truth state commit an agent's stated claims - subject / relation / object plus the assertion text they 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 an agent_stated ledger 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_user semantics, hygiene findings and conflict priority are structurally unreachable from it, and windowless output stays byte-identical.

  • One correct verb. brain_lifecycle gains the correct action and o2b 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. --apply retires each target through the end-state policy - validity closes at the explicit --window-end when the correction is time-scoped, else at the correction instant, while --flatly-wrong tombstones 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> in evidence_triggers, the existing supersede / tombstone reason codes). Replay converges: an already-applied correction is a no-op with appended: 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 its search_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 (labelled entity, machine-derived, never part of the query edge-type vocabulary). On by default, off through OPEN_SECOND_BRAIN_SEARCH_ENTITY_BRIDGES or search_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.md digest 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-stamped correction_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.md states 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 (total is 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 ingest and state window 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/inbox directory 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-end is 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_by index 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_until answers INVALID_PARAMS on 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) and ensureVaultCurrent (SessionStart and MCP start) start a detached o2b search index --freshen <token> when last_indexed_at is older than search_freshen_interval_s (default 60, 0 turns it off, env OPEN_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 under ionice -c3 on Linux or taskpolicy -b on macOS when the tool exists. It is keyword-only unless search_freshen_embeddings is on.

  • Backoff and visibility. The child records its outcome in the per-device freshen-state.json beside the index (a new state surface that follows OPEN_SECOND_BRAIN_SEARCH_DB / search_db_path, so o2b state status and o2b state migrate report and leave it where a relocated index keeps it); a failed run backs off one minute, doubling to an hour. o2b search status prints freshen, index_age, last_freshen and an active freshen_backoff: until <time> (freshen object under --json); o2b doctor warns with the new code freshen-failing after three failures in a row; runs that changed documents or failed append one row to the new index_freshen metrics surface.

  • Trail code index-stale. A search over an index more than ten minutes old names it on the retrieval trail with detail.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.linkResolutionSkipped reports 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 --repair and brain_doctor with repair now hand the doctor the resolved index path, so freshen-failing and tier-drift are listed as not auto-repairable, as the report already listed them. tier-drift is 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.md gains "Freshen on read", the two keys, the --freshen flag, the status fields and the index-stale row; docs/how-it-works.md "Keeping the index current"; docs/architecture.md states that background work starts only from agent activity; docs/hermes-cron.md and docs/updating.md say a scheduled reindex is now only needed for embeddings; docs/metrics.md lists index_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 budgeted Brain/active.md + Brain/lessons.md digest under the same active.inject_budget_chars - assembled by the shared hooks/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 the hookSpecificOutput.additionalContext envelope, with hookEventName naming the event the payload rode in on (SubagentStart or PostToolUse); decision: "block" is never a context channel on these events. The block renderer is extracted to hooks/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_enabled in the machine config (env OPEN_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 scan lists the detector findings, while the dangling-link count is measured from the search index and reported only by the MCP brain_hygiene tool, 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's hookSpecificOutput.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: tierFires marks the exact overflow condition under which the reactive tier runs, each eviction candidate carries tieredBytes, 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-inject to the write-shaped PostToolUse matcher - grok has no SubagentStart event, so the fallback registration is its carrier, and the parity suite declares the divergence - and hygiene-digest to the Stop group, where the registration stands but the digest emits on Claude Code only.

  • Docs: docs/how-it-works.md gains the subagent carrier and the hygiene digest and rewrites the budget paragraph around the headline tier, hooks/README.md gains rows for the new SubagentStart hook, the PostToolUse fallback carrier and the Stop digest, docs/cli-reference.md the hygiene_digest_enabled flag with its Claude Code-only emission, and docs/mcp.md notes that brain_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 calling server() exactly as before, and OpenCode V2 loads the same installed copy through setup(), 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 session context hook, the post-write reminder rides tool.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 emits session.execution.succeeded (failed and interrupted runs likewise). The capture set now covers those names alongside the v2-tree names, plus session.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, the brain_feedback write-conflict advisory and the retire siblings over one named threshold table. Its readable predicate 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_candidates returns them as an additive retire_siblings array of { retiring_id, sibling_id, score, method } pairs, the dream run summary carries the same key, and o2b brain reject lists the siblings of the rejected preference with the command that rejects each (--json adds retire_siblings). The list is advisory and accept-gated: nothing is retired by it, and each pair is accepted through brain_apply_evidence with result: outdated or o2b 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_candidates also compares the stored content vectors of the retiring and sibling preferences and adds pairs at 0.92 cosine or above as method: "embedding", with zero embedding spend. retire_siblings_semantic reports 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 (env OPEN_SECOND_BRAIN_NEAR_DUPLICATE_RETIRE_SIBLINGS_ENABLED, default off) gates every retire-sibling surface; off, the dream summary, brain_review_candidates and o2b brain reject are unchanged.

  • A wider near-duplicate receipt hint. With near_duplicate_write_widening_enabled (env OPEN_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 carries widening: "index_unavailable" and a widening_detail code naming the failure, and the same-directory hint still runs.

  • Turn timestamps in extract-signals. Every turns_mined entry carries the turn's stored timestamp ("" 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's valid_from / valid_until.

  • Write-stage timings on route metrics. mcp_route_latency records gain an optional stages array 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, and brain_route_metrics summary rolls 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 topic refuse the whole payload, naming both items, and nothing is written.

  • Docs: docs/mcp.md gains the retire siblings, the receipt widening and reach, the turns_mined timestamps and the write stages, docs/observability.md the stages field, its allowlist and the routes that emit it, and docs/cli-reference.md the extract-signals timestamps and topic refusal; the o2b brain reject help 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 Authorization header 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 after Bearer is still refused.

  • Shell completions escape backslashes. The fish, elvish and nushell scripts from o2b completions escape 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_tune and the recall-inject and gap-promote hooks) and the brain_context_pack semantic belief order now decide through one gateway whether, and with what text, a query is embedded. Under a positive embedding_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 with EMBEDDING_COST_UNPRICED; a hybrid search falls back to keyword-only, warns, and records the new trail code semantic-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-truncated with detail.windowTokens. When the instruction prefix alone fills the window nothing is embedded: an explicit semantic search and the semantic belief order refuse with INVALID_INPUT, and a hybrid search falls back to keyword-only with the trail code semantic-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 (env OPEN_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 of o2b search status reads 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 the local provider, which has no window.

  • embedding_extra_body. One JSON object (env OPEN_SECOND_BRAIN_EMBEDDING_EXTRA_BODY) whose fields are sent with every embedding request to an OpenAI-compatible endpoint. The owned fields model, input and encoding_format are 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 than openai-compat (disabled is exempt, it sends nothing), and a dimensions field requires embedding_dimension and 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_feedback returns an additive degraded array with the codes of its re-run search, and brain_benchmark, brain_eval and brain_tune reports carry a degraded union (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 synced recall_telemetry record.

  • BrainUpgradeError.drifted and --apply --json refusals. An upgrade refused for drift names every refused path, and o2b brain upgrade --apply --json prints { "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 --apply and the automatic upgrade worker plan once and apply that plan; each planned row records the bytes it was read with, or null when 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 and o2b brain upgrade --dry-run advised, 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 --json plan still reports before_size: 0 for an absent file, and the text plan reads absent.

  • brain_context_pack semantic mode counts what it sends. semantic.query_tokens and 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_tune refuses to save a keyword-only winner. A winner measured with the semantic lane missing is not saved: EMBEDDING_COST_UNPRICED for the cost gate, EMBEDDING_KEY_MISSING or EMBEDDING_DISABLED for a blocked tier, EMBEDDING_PROVIDER_HTTP when the provider did not answer, and the same EMBEDDING_PROVIDER_HTTP for any other stop that left the hybrid caller keyword-only (the composite deadline, an empty query vector, an empty fit recorded as semantic-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.md gains 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.md the query-embed gateway and the drift-checked upgrade, docs/mcp.md the gateway, the degraded fields and the tune refusal, docs/observability.md the retrieval_degraded audit field, and docs/updating.md "Upgrading to 1.73.0"; the embeddings-setup skill 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 dimensions is not cross-checked against embedding_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) or unknown. A price is not part of the embedding identity, so it never triggers a reindex.

  • An operator price pair. embedding_price_model and embedding_price_usd_per_mtok (USD per million tokens, a plain non-negative decimal number no larger than 1,000,000, 0 declares 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 blank embedding_cost_gate_usd (or its env twin) is refused with INVALID_INPUT, never read as a gate of 0, and a blank search_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 with INVALID_INPUT naming the key or env variable that supplied it. The env twins OPEN_SECOND_BRAIN_EMBEDDING_PRICE_MODEL and OPEN_SECOND_BRAIN_EMBEDDING_PRICE_USD_PER_MTOK win as a pair: when either is set, both halves come from env, so an env model never pairs with a config rate. A loopback embedding_base_url is 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 status refresh estimate all read it, so what is announced, refused, spent and receipted agrees.

  • EMBEDDING_COST_UNPRICED. Under a positive embedding_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 (MCP force_cost) passes the refusal, and the receipt then records forced: true, price_source: unknown and 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 --path on o2b 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 with INVALID_INPUT, and an unsafe prefix is refused by name. The text report adds a scope: line and its next step keeps the scope; --json adds path_prefixes, and unmatched_path_prefixes with a stderr warning (scope <prefix> matches no indexed document) for a prefix that matches nothing. --json also names the gate verdict and the spend receipt: gate_blocked and gate_reason when the configured gate would refuse the run unforced, and spend (model, tokens, estimated_usd, price_source, forced) when an --apply run reached the provider, each absent otherwise. A dry run the gate would refuse prints cost 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_command is omitted and the text says next: rerun this command with --apply and the same --path flags instead 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 check looked 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 profile embedding_provider selects), and key_present_under: lists the other registered profiles whose env key is set, or says none. --json carries the same as credential_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) embeds query once 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 a semantic object with model, price_source, query_tokens, estimated_usd (null for an unknown price), scored and unembedded, counted after the reach filter. The mode refuses by stable error.data.code when the semantic tier is blocked (EMBEDDING_DISABLED, EMBEDDING_KEY_MISSING), when sqlite-vec is not loadable (VEC_EXTENSION_UNAVAILABLE), and with BELIEF_VECTORS_MISSING when no kept candidate has a usable vector, naming o2b 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 with EMBEDDING_COST_UNPRICED when the model has no known price and embedding_cost_gate_usd is positive, while a local caller keeps the disclosed, ungated embed, as for search. A query vector refused after the embed (EMBEDDING_INVALID_VECTOR) names that spend, as a late BELIEF_VECTORS_MISSING does.

Changed
  • An unknown price reads as unknown, never as $0. The maintenance banner, the backfill dry run and search status print price unknown where they used to print $0.0000 for an unlisted model, and the JSON estimates (estimated_cost_usd, estimated_usd, the status refresh estimate) are null. The backfill's estimated_cost_usd used to be omitted for an unknown price and is now always present. A fully embedded index has nothing pending, so search status then prints no refresh_cost_est line and its JSON estimate is 0.

  • 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_usd never stopped it; local servers (Ollama, LM Studio, llama.cpp) and any unlisted model are now refused with EMBEDDING_COST_UNPRICED until 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_spend metric, the backfill JSON and the status JSON (refresh_price_source) gain price_source, and o2b brain maintenance status lists each receipt with it; journal rows written before this release read unrecorded, and a journaled receipt without a numeric estimate reads price unknown instead of failing the whole listing. brain_maintenance spend receipts carry a null estimated_usd for an unknown price.

  • brain_context_pack caps query at 2000 characters. The cap is declared in the schema and enforced in every query mode, counted in Unicode code points as the schema's maxLength counts them, and a blank query is refused with INVALID_PARAMS in semantic mode. brain_search now 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 same maxLength without checking it, now refuses a longer query with the same INVALID_PARAMS error 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 bare path_prefix escapes vault.

  • Docs: docs/cli-reference.md gains "Embedding prices", "Vector carry-over", the --path option and the credential-source lines of search check, docs/how-it-works.md the embedding spend, vector carry-over and semantic belief order entries, docs/mcp.md the semantic query mode and EMBEDDING_COST_UNPRICED, and docs/updating.md "Upgrading to 1.72.0"; the embeddings-setup skill 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 mcp server 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_enabled on and a host that sends a session_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-registered UserPromptSubmit, 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 reason all_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.md and Brain/preferences/pref-<slug>.md for each rendered preference bullet; a lesson or any other line that only quotes a pref- 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 when recall_inject_enabled or reground_parts_enabled is on and a session id is present.

  • Operator-declared recall slices. A recall_inject: block in Brain/_brain.yaml declares up to 6 named slices (slices: [decisions, lessons] plus slice_<name>_heading, _path_prefix, _types, _limit and _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 with all_slices_abstained. An unknown field, an undeclared or duplicate name, more than 6 slices, an out-of-range number or a path_prefix with .., a leading / or a drive letter is a load error. The block is deliberately absent from the generated _brain.yaml template; 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) and recall_inject_confidence_floor (0..1, default 0.35) sit next to recall_inject_enabled, each with an OPEN_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 as config_invalid; unset caps leave the brief byte-identical.

  • Chunked re-grounding (off by default). With reground_parts_enabled on (env OPEN_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 new reground-deliver hook hands out exactly one queued part per PostToolUse or UserPromptSubmit event 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 carries agent_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 is reground_part_chars (2000..100000 UTF-16 code units, default 9000, the observed Claude Code preview threshold less 10%), overridable per runtime by reground_part_chars_claudecode and reground_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.injection carries utf16_chars, part_ceiling_chars, parts_total, parts_dropped and over_budget, plus reground_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's config_invalid, present only then. The epoch records the SessionStart source as startup, resume, clear or compact, any other value as unknown. Each delivered part writes one hook-audit line reground_part_delivered with its part number, total, epoch and sizes, and a take that fails for a reason other than lock contention writes one reground_take_failed line 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) and slices_config: "invalid". Slice names ride the local audit line only, never the synced recall_telemetry record.

Changed
  • The recall brief no longer repeats notes within a session. recall_inject_dedupe (env OPEN_SECOND_BRAIN_RECALL_INJECT_DEDUPE) defaults to true, so an operator who already runs with recall_inject_enabled on 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 configured recall_inject_time_budget_ms instead of the fixed 2,500 ms.

  • Two new hook registrations on Claude Code and Codex. hooks/hooks.json and the Codex plugin register reground-deliver for PostToolUse (matcher *) and UserPromptSubmit. With reground_parts_enabled off 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 startup that 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 mode 0600, 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. The hook_session_state state surface reports carries_memory: true. When .open-second-brain or hook-state is 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 mode 0600, 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 through o2b search focus or the MCP focus path answers INVALID_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_prefix that starts with a drive letter is refused. The search request now answers INVALID_INPUT for a path_prefix like C:/notes, as it already did for one containing .. or starting with /, matching the recall slice load rule.

  • Docs: docs/cli-reference.md gains the recall-inject tuning keys, the recall_inject: slice contract and the chunked re-grounding keys, docs/decision-models/recall-inject.md a "Session dedupe and slices" section, docs/observability.md the recall-inject audit fields and the re-grounding receipt and audit fields, hooks/README.md the two reground-deliver rows and a per-session hook-state paragraph, and docs/updating.md "Upgrading to 1.71.0"; the README names this release.

Notes
  • On resume a 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.yaml that fails to load takes no slice path and the search that reads the same file fails, so the decision ends in error with fault retriever_failed and 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>.md and Brain/standing-rules/host/<device id>.md. Each session renders the files whose key matches its own project, harness and device, under a ## Scoped operator rules header 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 one UNAVAILABLE: line naming its vault-relative path and the error code (ESCAPE for 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.json pointer found from the workspace directory (written by o2b brain project link); the harness comes from the launch-time --harness option, 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_rules on brain_context. At local reach, when at least one scoped file matched (or the host notice applies), the scoped block follows the standing rules in content and the result carries scoped_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_profiles on, VAULT_DIR, VAULT_AGENT_NAME, VAULT_TIMEZONE, OPEN_SECOND_BRAIN_CONFIG and OPEN_SECOND_BRAIN_MCP_TIMEOUT are 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_HOME and LOCALAPPDATA are 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: in hermes open-second-brain config. The first line names the source this command resolved from: profile scope (multiplexed gateway), or process environment (this command; a gateway with gateway.multiplex_profiles reads each profile's .env).

Changed
  • The Claude Code and Hermes plugins launch o2b mcp with --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. An o2b older than 1.70.0 refuses the flag, so the Hermes bridge does not start until o2b is updated together with the plugin; the Claude Code plugin runs the o2b it ships and needs no step.

  • A refused --host-target value is echoed JSON-quoted. o2b mcp used to echo a refused --host-target value raw on stderr; like a refused --harness value, 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_chars before 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's budget block records it as scoped_rules_chars beside the unchanged inject_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 .env instead; 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 (a ConfigReadError) 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, and hermes memory status leaves out config_file when 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_brief view="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; and brain_doctor fills its removed-tool warning cap and the per-code cap of its uncertain stream, counts the stale-dependency states_changed and 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.md now also refuses any path inside Brain/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.md gains "Scoped operator rules", docs/mcp.md the scoped_rules key and --harness, docs/cli-reference.md the o2b mcp --harness line, docs/observability.md the scoped_rules_chars field, docs/stability.md the two new layers, install/hermes.md "Multiple Hermes profiles", and docs/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_obligation answers 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, and add refuses its slug as before.

  • brain_health and brain_doctor count concept gaps over readable principles. Below local reach concept_gaps and the doctor's concept-gap warnings 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_trigger scans answer at the caller's reach. Below local reach operation="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_candidates and brain_retention answer 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 retention summary counts the rows returned.

  • brain_intention, brain_intent_review and the dream preview's signal clusters answer at the caller's reach. Below local reach brain_intention used to list and show an intention chain withheld by visibility and fold it into a new version on set; such a chain is now treated as absent by list, show and move, and set refuses it; list judges 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_review and the clusters_below_threshold and intent_reviews of brain_review_candidates used to fold withheld inbox signals into their topics and counts; they now fold only the records the caller may read. brain_obligation remove and brain_intention move leave the archive name out of the answer below local reach.

  • Retention and forget-plan rows carry the vault-relative path. brain_retention recommendations and the forget-plan entries used to carry a path pointing out of the vault, so a brain_trigger scan below local reach never queued a retention trigger, even for a readable record.

  • brain_doctor and the operator view leave out a withheld instruction file. Below local reach instruction_file_warnings used to name a vault-root AGENTS.md, CLAUDE.md or GEMINI.md withheld by visibility, with its line count.

  • A withheld page no longer moves a readable stale-dependency row or the uncertain cap. 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's uncertain stream.

  • brain_context_receipts answers 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, and summary leaves 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 no active.md body, or a degraded run with no cached body) is left out of list and summary and answered by show as an unknown id, and the budget block leaves a receipt with no budgeted body left.

  • The brain_doctor repair preview counts at the caller's reach. Below local reach repair used to fill the removed-tool cap and count concept gaps over every page before its findings were filtered, so its unfixable counts moved with withheld pages; the checks behind the plan now read only the pages and records the caller may read.

  • brain_tension answers at the caller's reach. Below local reach detect used to read, count and pair notes withheld by visibility and return the tension, and list, show, verify and the transitions used to answer for it. detect now reads only the notes the caller may read, a persisted tension page carries the stricter visibility of its two source notes, and a tension page the caller cannot read is answered as absent (no tension: <slug>) before anything is written.

  • brain_lifecycle and brain_expire refuse a page the caller cannot read. Below local reach tombstone, supersede, temporal-replace and brain_expire used 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, and curator leaves 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_evidence and the apply_evidence operation of brain_write_batch used to accept evidence against a preference withheld by visibility, brain_derive_fact a withheld premise, brain_scaffold_stub a withheld source, and brain_decision, brain_labels assign/remove and brain_lifecycle tip used to show, rewrite or resolve a withheld page; such a page or record is now treated as absent before anything is written. The brain_feedback conflict and routing hints and brain_design_note grounding read only readable records. The brain_scaffold_stub ambiguity refusal no longer ends in an empty candidate list when none of the candidates is readable.

  • brain_dream and brain_maintenance run a real pass at local reach only. Below local reach brain_dream serves a dry run planned over the records the caller may read, and refuses a real pass, a step and the staged lifecycle; brain_maintenance refuses run. At local reach brain_maintenance run is allowed even when integrity.owner_scope_delivery is fail.

  • 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 a visibility line; it now carries the strictest visibility of those sources.

  • brain_dead_ends, brain_diarize, brain_hygiene refresh and brain_anticipatory_context answer 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, and brain_dead_ends record still 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=nan used to become the request deadline itself; it is now a malformed value that keeps the default deadline with one WARNING, inf disables 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_summary counts (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_obligation add, brain_intention set over a withheld chain, brain_decision record, brain_feedback force_confirmed, the brain_derive_fact slug, brain_write_batch create_note and the brain_scaffold_stub target.

  • The review obligation brain_decision record opens names the decision's title and does not take the decision page's visibility.

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_source on 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 ## Parts as 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=value credentials 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 gains parts ({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 .csv file is read as semicolon-delimited when its first record has at least two fields on semicolons and more than on commas (so name;"price, eur";qty is three columns), and an unterminated quote is refused as malformed-quoting with the record number. Rows are rendered under ## Table as 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 a Table > Rows a-b heading 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 gains table ({rendered: true, format, delimiter, columns, rows, rows_rendered, truncated?, redacted_cells} or {rendered: false, format, reason, detail?}).

  • New frontmatter on derived pages. source_format and source_content_hash, then for tables table_delimiter, table_columns, table_rows, table_rows_rendered and table_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 ## Table section, or not 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_id on brain_recall_gate. The optional argument (at most 512 characters) the Hermes plugin sends is accepted and recorded on the gate_telemetry record.

Changed
  • CSV, TSV and HTML files are planned by default. o2b brain batch-plan and brain_ingest_batch_plan discover them beside Markdown and plain text, and a planned file of a non-text format carries format after status (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 unclassifiable to skipped_non_extractable with reason format-not-extractable and its format as detail, ahead of the schema skips, and skip_reason_counts gains that key. An extensions override 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 visibility tokens; 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_gate refuses a turn_id or session_id over 512 characters or a telemetry_host over 200 with INVALID_PARAMS before 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_brief view="daily" and view="weekly" leave out status transitions, retirements and contradictions naming a record the caller cannot read (under its pref- or ret- 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, recompute events_by_kind and vault_delta from the events the caller may see, and take no report snapshot and show no delta.

  • 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 by brain_search or any other generic page reader.

    • brain_analytics view="timeline", view="belief_evolution" and view="concept_synthesis", brain_brief view="today", brain_query with since, topic or preference, and the osb://log/{date}, osb://topic/{topic} and osb://preference/{id} resources judge every log entry and record under each id it answers to (pref- and ret-), 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_backlinks and osb://backlinks/{id} judge a record under both its spellings, so a retired record is not named under its earlier id.

    • brain_moc_audit answers a hub the caller cannot read as hub note not found, and leaves such a member unlisted, unsized and out of the reference counts.

    • Every brain_doctor finding is kept only when the caller can read each record it names; orphan-evidence and malformed-evidence-range now carry sources (the preference the evidence is about) and removed-tool-reference carries path (vault-relative on the wire), with the messages unchanged.

    • brain_search_by_source and brain_delete_by_source honour 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. A brain_ingest_source below 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_hash on any Brain page.

    • The recall verdict's root coverage in brain_search and brain_recall_gate counts 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.md gains two "Since v1.69.0" bullets (non-Markdown ingest, and the readers that answer at the caller's reach), docs/cli-reference.md the extract and batch-plan lines, docs/how-it-works.md a "Non-Markdown sources" section with the limits of table notes and of the redactor, the architect clause and the doctor finding fields, docs/observability.md the turn_id field, and docs/updating.md "Upgrading to 1.69.0"; the README names this release.

Fixed
  • An operator-set visibility on 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 like created_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 its visibility did 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_trace keep 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 its confirmed, retired and new_unconfirmed lists keep only readable links.

  • brain_event_trace drops evidence on a retired record the caller cannot read. Below local reach an apply-evidence event logged before its preference was retired, which names the record only under its pref- id, used to be listed with its artifact and result once the record had moved to its reserved ret- page; each record id is now judged under both spellings.

  • o2b brain extract refuses 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_offset is counted in the source file. When a leading frontmatter block (and a byte-order mark before it) is left out, o2b brain extract --json used 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-dependencies region 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 under Not 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 status followed by a question on the next line retrieves, while a single-line command (also with only a trailing line break) is still skipped as shell_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 architect reads package.json, pyproject.toml (PEP 621 and Poetry), Cargo.toml and go.mod at the project root and at each detected module, and reports pom.xml, build.gradle, Gemfile and composer.json as unsupported by name. Each manifest gets one status: read, malformed (with a fixed reason such as invalid JSON, invalid TOML or nesting 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) or unsupported, 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 order package.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-dependencies region (a Mermaid diagram, declared by manifests, not measured from imports), each module note gains a dependencies region, and a module note with edges gets a depends_on frontmatter key: a sorted YAML list of wikilinks that the indexer turns into typed depends_on links. 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.

  • manifests on o2b brain architect --json. The envelope always carries manifests: [{path, ecosystem, status, detail?}] sorted by path; text mode adds one manifests 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-extract and brain_ingest_source with pre_extract read .tf and .tfvars files: resource, data, module, variable, output, provider and locals blocks with identifier labels become entity seeds in Terraform address syntax (a label holding any other character opens no block), a module source becomes an imports seed (never resolved), single-line depends_on lists become depends_on edges (the items of a multi-line list are seeded as references), and var., local., module., data. and same-file resource citations become references edges. Strings, heredocs and comments are skipped, and the $${ and %%{ escapes stay string text; a .tfvars file yields variable names, never values. The text output prints every seed on one line with control characters folded; --json keeps the escaped value. The limits are named in docs/cli-reference.md (.hcl and .tf.json files, multi-line depends_on, nested blocks as entities, for_each/count expansion and others).

Changed
  • Existing architecture notes are updated once after the upgrade. The first o2b brain architect run on an existing project appends the module-dependencies region to the overview and a dependencies region to each module note, adds the depends_on key to a module note with edges, and reports every such note updated; a later run with no change reports none.

  • The generator owns the depends_on key 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.json is reported as malformed. It used to be read as no manifest at all; its detail is 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 unrepresentable count on the group line of its ecosystem.

  • .tf and .tfvars sources are pre-extracted. They used to report extracted: false; they now return block names and edges, never attribute values.

  • Credentials in import specifiers are redacted. In every family's imports seeds, the userinfo of an http(s), git+http(s) or git:: specifier (a user:password pair 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 as ssh://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_source with pre_extract asks 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, the osb://preferences/active resource and brain_pre_compress_pack hand a caller below local reach a render stamped with the file's generated_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 under integrity.owner_scope_delivery: fail, as brain_context does. 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_brief view="digest", the osb://digest/latest resource and the osb://lessons resource 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_brief view="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_health drops a finding any of whose members the caller cannot read, and brain_doctor with repair plans, counts (unfixable included) 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_search and every other tool that reads a page by path no longer return Brain/active.md or Brain/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 through osb://lessons. Local search is unchanged.

  • A remote search with no match states no index counts. The answer of an empty brain_search and of brain_recall_gate below local reach carries no coverage receipt: a not_found names the index time only, and an unknown verdict gives one fixed, count-free reason for each unknown_reason. A local caller and the CLI keep the receipt.

  • Docs: docs/how-it-works.md describes the manifest facts, statuses, regions and the depends_on key; docs/cli-reference.md the architect and pre-extract lines with the Terraform limits and the specifier redaction; docs/mcp.md the readers that answer at the caller's reach; docs/updating.md gains "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 --help names --progress. The long help's usage line omitted the flag the verb accepts.

  • An owner-scoped brain_doctor repair counts only that owner's findings. unfixable used 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_source and o2b brain distill compare 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 Unicode Quotation_Mark property 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 ^id removed, quote variants folded, whitespace collapsed), and line-leading block markers are removed on the evidence side only, so a span's own leading 3. , - 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 inside unsafe, 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 is block-not-found, a duplicate block-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 quotes report (checked, verified_in_block, verified_in_source, unquoted, unpaired, capped findings with claim index, outcome and a bounded span preview). The page records quotes_verified and quotes_unquoted in its frontmatter when a claim holds a span. Pages without quoted spans are unchanged.

  • Strict quote refusal. strict_quotes: true on brain_distill_source and --strict-quotes on o2b brain distill refuse the whole write when any span would be unquoted, and nothing is written. The MCP refusal is INVALID_PARAMS with error.data.code set to the new registered code quote_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) or url-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]]) is full-local when its .md note exists. The stamp (capture_scope, or capture_scopes per source on a research report) is written only when it is not full-local, so pages over local sources stay byte-identical. brain_distill_source and brain_ingest_source return capture_scope, brain_research_report returns capture_scopes in source order, and o2b brain distill prints [bounded-local] (a url-only source already prints [untrusted_source]) and [quotes verified:V unquoted:U], with --json carrying capture_scope and quotes.

  • A bounded excerpt for sources the vault does not hold. excerpt on brain_distill_source (with a declared maxLength) and --excerpt-file on o2b brain distill store the text the caller read from a url-only source in a fenced ## Excerpt section with an excerpt_hash, make the page bounded-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-file that is not valid UTF-8 exits 2 with distill: excerpt file is not valid UTF-8. A stored excerpt contributes no links or tags to the page that holds it.

  • The capture-scope hygiene detector. A default-on brain_hygiene detector 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 are url-only at scan time. It reads only the page's own last ## Sources section, 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; a bounded-local page 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. The brain_status hygiene 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 quotes report names it; before, any quoted text was written as given. Set strict_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_scopes and excerpt_hash are written for a bounded-local or url-only source; quotes_verified and quotes_unquoted only 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_source and o2b brain distill refuse a claim that contains a line break (a claim is one bullet line) and accept at most 1000 claims per call; brain_research_report refuses 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 default brain_hygiene or o2b brain hygiene scan 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_plan and brain_research_report decide 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_source checks quotes only against a source the caller may read (and, under integrity.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_source leaves such an original out of the plan, blast_radius, matched and recoverability, and a confirmed run with include_originals leaves it on disk.

    • brain_note_lifecycle, brain_append_note, brain_update_note and the update and append operations of brain_write_batch refuse such a page with the refusal a missing path gets (source_missing, target_missing), before any plan, reserved-key check or write; brain_create_note keeps its note already exists refusal on an occupied path and leaves the page untouched. At remote reach a note the host cannot read is also answered as missing; the target_unreadable diagnostic stays for local callers.

    • brain_note_lifecycle names 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_history answers for such a page as for a path no commit touches (commit_count: 0, no phases), before any history is read.

    • brain_tiers check lists drift rows only for pages the caller may read, and restore and accept refuse any other page as not indexed before reading or writing anything.

    • brain_event_trace drops an event that names such a page before the limit and the total and trace_total counts, and drops an attached trace whose handoff reference is withheld; brain_agent_query and brain_agent_diff withhold the same contributions before the roster, topics and counts are built.

    • brain_diarize, brain_design_note and the brain_dream rollup step leave such a page out of link_candidates, its total and the schema hint; brain_idea_discovery leaves it out of its walk before ranking and capping; brain_procedural_memory leaves such a procedure out of list and the reconcile counts, and mark_used and mark_outcome refuse its id as unknown before writing.

    • brain_writes plan_revert, when it withholds an entry, returns a digest and next_command computed over the visible entries only; o2b brain writes revert --apply re-plans in full and refuses such a digest with digest_mismatch.

    • The brain_hygiene scan's link_integrity block reports measured: false with reason reach below local reach, because its counts are taken over the whole index, and the brain_status hygiene problem count counts only the findings brain_hygiene would list for the same caller (owner scope and reach).

    • brain_doctor and the schema_inspect lint, orphans and stats views leave out a Brain record the caller may not read, so a malformed private preference answers as an absent one; brain_doctor restates ok, trust_verdict and 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_hash and 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.md gains a "Source distillation" section (inputs, the quote check and its tolerance, unquoting, strict refusal, the quotes report, capture scope and the excerpt) and the quote_unverified row in the error-code table; docs/cli-reference.md documents o2b brain distill with its flags and the new detector; docs/how-it-works.md gains "Checked quotes and capture scope"; docs/updating.md names what an existing vault sees after the upgrade; the README names this release.

Fixed
  • o2b brain distill completions and help --json list 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 to brain_note_history or brain_tiers), or a page reference a log entry carries (read by brain_event_trace, brain_agent_query and brain_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 any data members the answer already had. On the tool-result channel an isError: true result carries _meta["open-second-brain/error"] = { schema: "o2b.error.v1", code }, beside a progress refusal when the call has one, and never in structuredContent, which a strict client validates against the tool's outputSchema even 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_CODES in src/mcp/tool-error-codes.ts is 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, the brain_expire refusals 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_unparseable and argument_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 .code property, so an operating-system code such as ENOENT never reaches the wire; a class the table does not know is answered as internal_error and 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. SearchError codes reach error.data.code on brain_search and the recall tools, where they used to be dropped or appended to the text only; skill tools answer unknown_skill, skill_not_found or skill_invalid_path; brain_trigger answers invalid_status and trigger_transition_refused, with an absent trigger and one the caller may not see sharing one code and one message; the write-session tools answer session_id_required, write_session_unknown and write_session_terminal; the argument guard answers unknown_argument, and the reach and owner-scope refusals carry caller-supplied-reach, foreign-owner and unresolved-identity. Every tool that dispatches on an operation, op or action argument, a brain_write_batch operation's op included, answers an unknown one with unknown_operation; a brain_memory_bridge host write keeps its existing invalid_action. A Brain artifact that cannot be parsed answers brain_artifact_unparseable instead of internal_error.

  • The Hermes bridge exposes the code. BridgeError gains a code attribute holding error.data.code of a JSON-RPC error response, so Hermes can branch on the token without parsing the rendered message; it is None for 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 --json and on brain_search now carries rerank-provider-unavailable with detail.category computed 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) or unclassified. The rerank_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.0 and rerank-multilingual-v2.0 shutdown of 2025-04-30 with its cited source; once a configured openai-compat model's date has passed, the stage sends no request and the trail carries rerank-model-sunset. The ZeroEntropy zerank checkpoints 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 as rerank-provider-unavailable instead of being skipped.

  • o2b brain doctor checks the rerank configuration. With search_rerank_enabled on and the openai-compat kind, a config-only check that sends no request reports rerank-endpoint-unconfigured (error) for a missing or blank base URL, model or key, a base URL the provider would refuse, or a search_rerank_provider that 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; and rerank-model-sunset-unsurveyed or rerank-model-sunset-undetermined (uncertain) when the survey makes no statement or reaches no verdict. A base URL that carries user: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 string code, never absent and never an array; a code member that is not a string is replaced by the default. A client that asserted "data" not in error sees the change; existing data members such as tool, unknown_arguments or errors are 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-sunset answer depends only on the build and the date and is cached as usual; for an enabled openai-compat rerank 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_forbidden and the three brain_expire refusals are registry members. brain_scaffold_stub's argument_forbidden and ExpirationValueError, ExpirationTargetNotFoundError and InvalidExpirationTargetError keep the spelling they already had on the wire.

  • Docs: docs/mcp.md gains a "Tool errors" section with both channels, the generic code table and the casing rules; docs/cli-reference.md documents the two rerank trail codes, the category table, the missing hybrid-deadline-exceeded row and the rerank doctor check; docs/how-it-works.md describes the rerank trail and the sunset skip; docs/updating.md names what a client sees after the upgrade; the README names this release.

Fixed
  • A JSON-RPC refusal keeps the progress-token refusal. A tools/call with 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 in error.data["open-second-brain/progress"] beside code, 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 classified data.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 timeout or the caller's own reason, with the messages unchanged.

  • o2b brain doctor no longer prints the plain-http rerank warning. With search_rerank_allow_insecure_http set, 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-template prints 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 interval 1h: the lane's window, busy, pressure and lease gates decide whether work happens and a gate skip exits 0, so an hourly schedule plus --window is the intended pattern. The script embeds the resolved vault, single-quote escaped, because cron runs without the operator's environment; it runs o2b 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 not H-H with hours 0..23 and a zone that is not a plain zone name, rather than printing shell text; it also refuses a zone Intl does 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, --interval or --format without --cron-template, and status --cron-template exit 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-template and the new maintenance recipe take --format cron|systemd (default cron, whose output is byte-identical to before). systemd prints the same script section plus a .service/.timer pair to save under ~/.config/systemd/user/ (OnBootSec=, OnUnitActiveSec=<N><unit>; no Persistent=, which systemd ignores on a monotonic timer), the systemctl --user daemon-reload and enable --now commands, the loginctl enable-linger note 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 naming cron and systemd.

  • Install-owned custom lane tasks. An install can declare its own upkeep commands in the machine config file: maintenance_custom_<name>: <command>, with optional maintenance_custom_<name>_cwd (default the running user's home directory, never the vault unless named) and maintenance_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 switch maintenance_custom_tasks: true (env OPEN_SECOND_BRAIN_MAINTENANCE_CUSTOM_TASKS, default off, 0 turns it off on one host), after the four built-in tasks, under the same window, busy, pressure, lease and streak gates, and they journal as custom:<name>. A task runs through sh -c (cmd.exe /d /s /c on Windows) with O2B_VAULT set, stdin closed, stdout discarded and every variable whose name declares a credential dropped from its environment; a _cwd that does not exist or is not a directory fails the task by name before anything is spawned; a non-zero exit fails the task as exit <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> and retry_tasks accept a declared task; at most 8 can be declared.

  • /health on the HTTP transport carries the server's fault counts. A faults block (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 mcp survives 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 the exit hooks 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 --probe installs no guard.

  • One task builder serves both maintenance surfaces. The CLI verb and brain_maintenance used 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. --retry and retry_tasks validate against the tasks this install actually registered, and a request longer than that is refused with the count; the retry_tasks schema maxItems advertises the built-ins plus the custom-task cap.

  • brain_maintenance returns custom_task_errors. A refused custom declaration (a bad name, an empty command, an out-of-range timeout, a relative _cwd, a _cwd or _timeout_seconds key without its command, a ninth task, a timeout over the budget) is named there, and on the CLI as custom task refused: <reason> on stderr; the valid tasks still run. status says custom tasks declared but maintenance_custom_tasks is off when keys are declared and the switch is off, and names OPEN_SECOND_BRAIN_MAINTENANCE_CUSTOM_TASKS when 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.md documents the maintenance recipe, --format cron|systemd on every recipe, custom lane tasks, the Hermes jobs path and exit code 70 of o2b mcp; docs/mcp.md gains a "Background faults" section and documents custom tasks on brain_maintenance; docs/updating.md names the new opt-ins; docs/hermes-cron.md points 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-template now quotes the repository path like the other recipe arguments.

  • o2b discipline install works 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.json of the running user, still overridden by OSB_HERMES_JOBS, and a home directory that cannot be resolved is a named error that points at OSB_HERMES_JOBS. Root hosts see no change. An empty OSB_HERMES_JOBS now 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 documents columns (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 an mtimeFallback bucket that counts the rows with no persisted window rather than pretending they are none. o2b search status reports the persisted event-time windows: an event_time line (JSON event_time block with documents, 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: true earns a bounded ranking boost. The flag is persisted at index time into documents.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 no pinned breakdown 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 (env OPEN_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 (env OPEN_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 in reasons/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 (env OPEN_SECOND_BRAIN_SEARCH_HYBRID_DEADLINE, default 15000 ms, 0 disables) 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 selftest proves the store, not just the configuration. Plain o2b doctor can 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 check stays the judge of semantic capability), and --json carries 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 --readiness probes what the installer registered and what the workspace promises. registered_commands re-reads the install manifest and every recorded client config, extracts the Open Second Brain command and arguments each install wrote (JSON mcpServers/mcp keys and the [mcp_servers.*] TOML tables), and probes each command word: a proved-absent absolute path fails with the client named and the exact o2b install <target> --apply recovery 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_contract audits 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 doctor reports a dangling merged_into: pointer. A de-canonicalised page points at its canonical through a merged_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 new merge-chain-dangling check walks the chains, including ones that cross Brain/retired/, and reports exactly the dangling class: cycle, depth and malformed intermediates already surface in o2b 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 (env OPEN_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 the maintenance_spend metrics 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 positive embedding_cost_gate_usd refuses the phase unless --force-cost is set (MCP brain_maintenance takes force_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-duplicate finding: 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 the lint key 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 as candidates_skipped and an unreadable sibling named in candidates_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.md beside topic-2.md with nothing saying both were one intended slug, was invisible. The new slug-collisions detector 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-in tags detector audits inline body tags through the one shared rule the link table extracts with: malformed tokens the looser Obsidian-compatible rule accepts but the index rule rejects (so they are silently absent from the link table), inconsistent case and hierarchy families (#Foo vs #foo, #a/b vs #a) grouped one finding per family, and orphan tags carried by exactly one document - all info; frontmatter tags: 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 --detectors list, 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_membership candidate in the repair lane's apply-gated store, zero stage a skip-no-hub refusal and several stage skip-ambiguous-hub listing every candidate - no degree tie-break, no new write path. o2b brain repair-lane merges 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 the repair_candidates state 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 maintenance and brain_maintenance now distinguish probeIncomplete (6), keyed on timed_out rows 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. Set maintenance_embeddings: true (env OPEN_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_contract readiness grades an agent-instruction file without the managed block as skipped (the block is not installed). So o2b doctor --readiness exits 0 for a plain AGENTS.md, CLAUDE.md or GEMINI.md; a broken block (malformed-block) or one missing its clauses still fails.

  • registered_commands grades a relative command path as unresolved instead of absent.

  • Docs: docs/cli-reference.md documents o2b 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_embeddings and the event_time line of o2b search status; docs/mcp.md documents force_cost, the maintenance_embeddings opt-in and the spend block on brain_maintenance, the widened detector set on brain_hygiene and the near-duplicate finding on the write receipts; docs/updating.md names 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_dimension no 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_ms the 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_corrupt instead 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 in candidates_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-lane pools 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_extract on brain_ingest_source, an imports seed whose specifier is relative (./ or ../ in TypeScript and JavaScript, a leading-dot from .x import in Python) gains resolved_to (resolvedTo on 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 /index plus each; Python probes <name>.py and <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, to always keeps the raw specifier, and a Python import never binds to a TypeScript file or the reverse. o2b brain pre-extract reads no manifest and never binds.

  • JSX component usage edges. In .tsx and .jsx sources the pre-extract pass emits a uses edge 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. The pre_extract description on brain_ingest_source now names uses edges and resolved_to.

  • brain_write_batch request receipts. An optional request_id (requestId on applyWriteBatch) makes a retried batch apply once. The result then carries request_id and receipt: applied when this call committed; duplicate when the same ID with the same operations was already recorded (the retained original result comes back, nothing is written, re-validated or linted); concurrent_duplicate when 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); and payload_conflict when 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 a receipt_note says not to resend the batch under a new request_id). The same ID with different operations found before the commit is refused as INVALID_PARAMS with the idempotency ledger's mismatch message, a blank ID or one over 256 characters after trimming is refused as INVALID_PARAMS before 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 own write_batch namespace, so a request ID never collides with a brain_feedback or apply-evidence idempotency_key of 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. Without request_id the call and its payload are unchanged.

  • Keyword match breadth. brain_search accepts match_mode (all, the default, or any), the search API matchMode, and o2b search --match-mode all|any. any joins the cleaned query tokens with OR, so a document carrying any one term is a keyword candidate; caller-typed AND, OR, NOT and NEAR tokens are dropped in both modes, and query expansion composes with either. Any other value is refused with the accepted list (a CLI error, INVALID_PARAMS on MCP, INVALID_INPUT from the API). The query cache keys an any search 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_search description tells the agent when to widen. It now ends with one sentence: when a query misses, consult brain_recall_gate before widening recall, since it classifies whether widening is warranted.

  • o2b brain doctor sweeps the newly sharded directories for Syncthing conflict copies. The sync-conflict-log check now also lists Brain/log/session-lifecycle/, hygiene/, secret-custody/, watchdog/ and schema-mutations/, .open-second-brain/hook-audit/ and .open-second-brain/watchdog-audit/, Brain/skill-proposals/, Brain/preferences/ and each repository directory under Brain/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 .jsonl shard, such as a proposal file, a preference note or a repository state.json, is reported under the same sync-conflict-log code 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 under uncertain by name, never read as clean.

  • o2b state status sees 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 appendFileSync or openSync(..., "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).

  • --disclosure and disclosure refusals list the accepted modes from one shared vocabulary, the same way match_mode does.

  • Docs: docs/mcp.md documents request_id and the receipt, match_mode, the search cue and the widened doctor sweep; docs/cli-reference.md lists o2b search --match-mode, the uses edge and the repair lane's skip-ambiguous decision; docs/observability.md and docs/how-it-works.md describe 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-ambiguous decision 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-support and capture-decisions, Brain/skill-proposals/verifier-rejections, Brain/preferences/pref-<slug>.history, Brain/projects/git/<repo>/commits and .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 order jq uses, so names such as -source.svg and [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 as http://localhost:5173/@vite/client or https://registry.npmjs.org:443/@types/node stays 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 a Bearer-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 mtime while the receipt claimed updated: true for a write that changed nothing - poisoning recency ranking, validity windows and vault-delta counts. Both write chokepoints now skip a byte-identical rewrite, and updated / appended carry 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_UNREADABLE that 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_batch named 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.md lists o2b brain orphan-repair; docs/mcp.md documents the plan's skip_reason_counts and unclassifiable aggregates, the optional path_advisory on note receipts, and the new doctor findings; docs/observability.md and docs/how-it-works.md describe 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, through parseSkippedPageReason; the type guard accepts members only), while skipped stays 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 doctor checks embedding health. A fail-soft, read-only check reports an embedding backlog, an unrecorded census and an unmeasured health verdict; an unrecorded census lands in uncertain, 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 - in o2b search status too; a provider with no configured model is uncertain, and the check reads the index the doctor pass was pointed at.

  • o2b brain doctor reports orphaned session references, and o2b brain orphan-repair detaches them. An observation signal whose session_ref resolves against neither the continuity ledger nor the Brain log's session lifecycle is a warning that names the repair verb on its fix field. A ref written by o2b 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 report uncertain and no finding, because a session whose lifecycle lived there would otherwise read as an orphan. The verb removes only the session_ref key, and only while it still holds the value the scan resolved under the signal's per-file lock (a reference rewritten in between is reported skip-changed, a contended lock skip-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 initialize without waiting for a Brain upgrade (#216). After a plugin update, ensureVaultCurrent planned and applied a pending _brain.yaml / _BRAIN.md upgrade, snapshot included, synchronously inside o2b mcp and the SessionStart hook, 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 detached o2b brain upgrade --self-heal worker 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 MB Brain/ tar: a full initialize took 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 with ENOBUFS.

  • 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.zst without 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_name spelling (#216). On a case-insensitive filesystem two spellings of the same vault path rendered two different _BRAIN.md files, so each runtime saw the other's manual as a pending upgrade. A vault_name that 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, alwaysLoad on the writer server), and the repository-root .mcp.json, which Claude Code also loaded as a project config with an empty CLAUDE_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/c the 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_days that it did not consume into Brain/inbox/archived/, byte for byte (archived_signals in the summary and the dream log 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 --now in the future archives nothing that is still inside the window today, a name already taken in archived/ is reported instead of re-planned, and a staged bundle does not count newly archivable signals as drift. dream.archive_stale_signals: false keeps the previous behaviour.

  • o2b brain doctor reports inbox-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) and o2b brain dream as 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.json and on the self_heal_upgrade metrics surface; o2b doctor gains a self_heal_upgrade check, o2b brain status a self-heal-upgrade-failed problem, and o2b brain upgrade --dry-run / --check print the last failure (JSON self_heal_failure). The next automatic attempt waits one hour, doubling per consecutive failure up to 24 hours; o2b brain upgrade --apply --yes ignores 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) and OPEN_SECOND_BRAIN_DEDUP_CACHE=0 (turns the cache off), listed together in docs/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.md and README.md describe the start-up worker, streaming and atomic snapshots, the inbox archive, the dedup index cache and the inline MCP manifest; the state-surface counts in docs/cli-reference.md are 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
  • skills use: two-stage skill selection for skills_attach. The BM25 shortlist (cut at 40 instead of max_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 within decision_model_timeout_ms. enforce offers the decision subset or reorder of the shortlist; any failure in either stage returns the BM25 offer. The result gains decision_model: { mode, applied, degraded?, model? } only while the use is not off, and the offer_id is computed over what is actually offered, so the skill_invoked join works in both modes. Private or unreadable SKILL.md files are never sent. See docs/decision-models/skills.md.

  • extract_prefilter use: turn pre-filter before the brain_extract_signals envelope. One yes/no question per mined user turn. shadow records the probabilities and returns the full plan; enforce leaves turns below the threshold out of the envelope (turns_dropped) and, when every turn is below it, returns skipped: { reason: "decision_model_prefilter" } with llm_step: null, saving the round trip. Commit items accept an optional source_turn, which feeds a regret metric. See docs/decision-models/extract-prefilter.md.

  • dedup and tension uses: advisory pair verdicts. One choice per pair (same | related | different for dedup findings of brain_hygiene scan and entity alias candidates of the doctor; contradicts | compatible | unrelated for tensions). enforce annotates each item with decision_model: { verdict, probabilities, model, calibrated } and lists a confident low-priority verdict last with decision_model_low_priority: true; nothing is hidden, merged, dismissed or resolved. New read-only brain_tension action verify and CLI verb o2b brain tension verify [<slug>]. See docs/decision-models/dedup-tension.md.

  • labels use: label suggestions. New read-only brain_labels operation suggest (path, optional dimensions) and o2b brain label <path> --suggest [--dimensions a,b]: one request per note over each declared vocabulary plus none. shadow returns null suggestions; suggest never assigns, and a private note is refused. See docs/decision-models/labels.md.

  • answerable use: advisory signal for search and the recall gates. Carried by the rerank request: brain_search and o2b search --json gain decision_model.answerable, and brain_recall_gate and brain_context_pack accept decision_answerable next to the scores and match_quality and annotate the verdict with { probability, disagrees }. The level and action never change. See docs/decision-models/answerable.md.

  • recall_inject use: recall-inject hook filter. One request per injecting prompt, within decision_model_hook_budget_ms and the hook's existing retrieval budget; enforce drops 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. See docs/decision-models/recall-inject.md.

  • Routes: a vercel-evaluate adapter and preset for the Vercel AI Gateway /v1/evaluate variant; self-hosted laya and openjev presets on loopback (no key needed there, small state defaults with a raisable ceiling, per-preset choice limits, a licence note in check); and an llm-emulation adapter that asks an OpenAI-compatible chat model for probabilities only, calibrated: false everywhere, never chosen implicitly, refused in enforce without decision_model_allow_uncalibrated. Shared transport rules live in transport.ts, and one contract suite runs the same fixtures against every adapter. See docs/decision-models/providers.md.

  • Threshold profiles per model family. Every threshold is held to the family it was tuned against (jev-1.13 for the hosted routes). enforce on a profile without tuned thresholds for the use (laya, openjev, llm-emulation, or a compatible server without a named family) runs as shadow for every use; only check says so.

  • Config keys decision_model_threshold_profile (the family a compatible server serves) and decision_model_output_price_usd_per_mtok (read only for llm-emulation, whose cost is unknown unless both prices are set). decision_model_hook_budget_ms, reserved in 1.59.0, now drives the recall_inject sub-budget.

  • o2b decision-model report --use <use> adds the use's own evaluation report after the summary (JSON use_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_impact samples gain an optional source attribution (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, check and --ping, one use in shadow, report and rerank-eval before enforce, turning a use off, and the privacy and terms notes.

  • Docs: docs/decision-models.md is the hub, with a per-use table (surface, what enforce changes, failure policy) linking a page per use under docs/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.md and docs/observability.md updated.

Changed
  • Rerank kind decision-model follow-ups from 1.59.0: each passage now carries the note title and, when declared, its status and updated values, 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 in enforce, which saves roughly 15 to 20 percent of input tokens in shadow; the query-cache key names the passage shape.

  • A compatible provider enforces only once decision_model_threshold_profile names its model family; until then its enforce uses run as shadow (in 1.59.0 every preset enforced with the Jev thresholds).

  • The agent-scope probe covers the new brain_tension verify and brain_labels suggest calls (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-emulation records a cost the route reports, so the daily cost gate counts it without configured prices; check warns 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, choice and score questions; answers with probabilities and a valid flag), a systemone adapter for POST <base>/v1/systemone (TypeSafe, OpenRouter, Vercel AI Gateway compatible route, OpenCode Zen, compatible servers), presets typesafe, openrouter, vercel, opencode-zen and compatible, and runDecision, the one entry point every use goes through. The adapter uses raw fetch with redirect: "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 an OPEN_SECOND_BRAIN_DECISION_MODEL_* override. The config holds only the NAME of the key variable; an env_key that is not a variable name, and a base_url with user:password@, make the config invalid without printing the value. Invalid values keep the feature off and are reported by check, naming the key. The model id must be pinned; a moving alias such as jev-latest is 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 as P0..Pn; texts are clipped; the state is kept within decision_model_max_state_tokens. The whole body passes the shared egress guard, and the adapter is declared in the egress registry as decision-model-systemone with status shared_redactor.

  • Vault opt-out: decision_model: { enabled: false } in Brain/_brain.yaml disables 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_call continuity 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 --ping requests count toward it and carry an origin), and an optional decision_ms component on mcp_route_latency records.

  • 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; --ping sends one synthetic request and records it. o2b decision-model report summarises records per use, including rerank shadow agreement over ordinary shadow records. o2b doctor --readiness gains a decision_model line on every install.

  • Rerank kind decision-model (search_rerank_kind: decision-model, mode from decision_model_uses rerank): one request per rerank carrying a relevance question per candidate, an injection question per candidate, and an answerable question while that use is not off. shadow records and returns the heuristic order unchanged; enforce reorders the head by relevance and keeps private, unsent and invalid candidates in place; a suspected injection is tagged decision_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. runRerankEvalGate accepts kind: "decision-model" and measures it in enforce.

  • 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 in docs/architecture.md, docs/cli-reference.md and docs/observability.md.

Changed
  • search_rerank_kind accepts decision-model, and its error message names the three kinds.

  • o2b search rerank-fit reports the decision-model kind as inapplicable and points to rerank-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.additionalContext for 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 only decision: "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 through stop_hook_active, and the per-runtime cadence lines are gone. The opencode plugin has no Stop guardrail and is unchanged.

  • The brain-memory skill 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 custom CLAUDE_CONFIG_DIR): a Stop payload with stop_hook_active and last_assistant_message but no Codex turn_id resolves to Claude Code, so such installs get the feedback channel too.

  • The brain-memory skill documents result: outdated for brain_apply_evidence, next to applied and violated.

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_feedback call, 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=0 restores 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 dev and ino as 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 the destination_occupied refusal. The check now reads exact bigint stats. The Windows CI job surfaced it as an intermittent test failure.

Changed
  • Test fixtures build placeholder credentials at runtime. tests/helpers/fake-credentials.ts assembles 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 as token: "..." moved into named constants.

  • Two internal constants renamed. SCAN_TRUNCATED_TOKEN in src/core/redactor.ts is now SCAN_TRUNCATED_SENTINEL (SCAN_TRUNCATED_MARKER already names the exported marker that embeds it), and UNRECORDED_TOKEN in src/core/integrity/stamp.ts is now UNRECORDED_MARKER. Neither is exported and their values are unchanged; the OpenClaw bundle is rebuilt.

  • The codegraph install recommendation names npm. The codegraph-partner skill no longer quotes a curl ... | sh installer. It suggests npm i -g @colbymchenry/codegraph and 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.lock for the Hermes Python shim, which has no dependencies. scripts/sync-version.ts keeps the project's own version in it in step with package.json.

  • Codex metadata. The root .codex-plugin/plugin.json carries the author, homepage, repository and license that plugins/codex already declared; the Codex manifest adds its websiteURL; and the marketplace entry declares "authentication": "ON_INSTALL".

  • The Codex plugin subtree carries LICENSE, README.md, SECURITY.md and .codexignore. bun run sync-plugin-mirrors generates them from the root as real files: LICENSE and .codexignore byte for byte, and the two Markdown files with repository-relative links made absolute, since those links resolve to nothing two levels down. The --check form 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 only contents: 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: private token is withheld at remote reach everywhere page content or page identity can travel:

    • brain_context_pack applies the reach rule to its candidates before ranking and budgeting, so a withheld page contributes nothing to any lane, the skipped list, deduped_from or the items. A tension that touches a withheld page is still reported, without its id.

    • brain_writes (list, plan_revert), brain_labels show and brain_unlinked_mentions ask the reach view per target. A withheld note is answered exactly like a missing one. brain_writes and brain_unlinked_mentions move 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 only team. brain_search refuses 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_expand and brain_idea_lineage withhold continuity rows flagged private, 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_ERROR channel 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 the Brain segment is compared case-folded, the way macOS and Windows filesystems resolve it. Graph import resolves paths before the Brain/ boundary check in every mode and re-checks where the bytes land, so a vault folder symlinked into Brain/ 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 into Brain/, 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 attr and marker write-back require a Markdown note, and under Brain/ 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-markers reports a refused target as a per-marker refused verdict 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_session and o2b brain session accept a target under Brain/sources/, reports/, distillations/, notes/ or decisions/panels/, read with the shared case-folded path reading, and refuse the rest of Brain/, 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 inside OKF Review/. The manifest's producer field is a label, not a credential: machinery frontmatter (_status, owner, origin_channel, _revision, _confidence*, knowledge_pack and the rest) is stripped on every import not run with --trusted, and the Brain/sources/ and Brain/reports/ lanes open on --trusted alone.

    • bank-import restores every preference row untrusted by default, including rows the bundle already marked unconfirmed.

    • Claude memory imports land unconfirmed on a real trial window instead of confirmed at high confidence.

    • brain_update_note refuses the reserved frontmatter keys visibility and origin_channel with reserved_frontmatter_key.

    • brain_ingest_source reads a pre_extract source only inside the vault; anything else is reported extracted: 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_tiers restore and accept, and o2b 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 Windows secret set refuses when the owner-only ACL cannot be applied, and the first secret set in a fresh vault works. A .gitignore does not reach Syncthing, which reads only the .stignore at its folder root: the new secrets-sync-exposed doctor check, and a warning on secret set, name the exact .stignore line 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 run matches 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-profile fails closed. o2b mcp exits 2 naming the bad value, where it came from and the known profiles, instead of serving the full tool surface.

  • parseBrainYaml refuses __proto__, constructor and prototype keys 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 than sessions.payload_max_text_chars (default 32000), move to Brain/.payloads/<sha256>.txt. The row keeps a 1,000-character head preview and an osb-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; list shows stored payloads with reference counts, orphans and missing files; gc is a dry run unless --apply, which removes only unreferenced payloads older than ten minutes, behind a payload-gc recovery 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_expand gains 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-missing and continuity-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|list packs 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 by visibility:, carrying an owner: or still under review, and redacts through the shared egress guard before sealing. preview reports samples, integrity, conflicts and injection or privacy warnings. install refuses 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 in OKF Review/, each stamped knowledge_pack: <name>@<digest>. uninstall removes one pack's entries and keeps rules that gained local evidence, pages an operator promoted, and staged pages whose content changed since install (reported as edited, against a knowledge_pack_sha fingerprint the installer records on each staged page). The knowledge-pack-uninstall snapshot archives Brain/ 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 in brain_search trust metadata (trust.knowledge_pack) and o2b brain query. CLI only, because preview reads a directory outside the vault.

  • embedding_allow_insecure_http and search_rerank_allow_insecure_http (and their OPEN_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_KEY supplies the MCP HTTP bearer when --api-key is absent, so the key stays out of the process list.

  • bun run check:hermes-scan and a hermes-plugin-scan CI job. hermes plugins install scans 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 and plugins/hermes the 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_http opt-out is set in the operator's config or environment.

  • An unknown tool profile exits 2. --tool-profile or mcp_tool_profile naming a profile that does not exist stops o2b mcp instead of serving every tool.

  • bank-import resets 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-restore keeps the carried state for a backup you vouch for.

  • OKF import strips machinery frontmatter unless --trusted, whatever the bundle's producer says, and only --trusted admits the Brain/sources/ and Brain/reports/ lanes.

  • Labels and attributes refuse Brain/ machinery and non-Markdown targets. Under Brain/, only sources/, reports/ and distillations/ pages accept them.

  • Write sessions commit only under Brain/sources/, reports/, distillations/, notes/ and decisions/panels/. Any other target under Brain/ is refused with target-reserved when the session opens.

  • A top-level folder named brain in any letter case is treated as Brain/. 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 run allowlist 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 as tool* that relied on matching across the joined command line now matches only a single-element argv; write tool *.

  • brain_update_note refuses visibility and origin_channel in 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.ts preview is folded into previewKnowledgePack, with its guarded samples and integrity hash.

Fixed
  • The prompt hook no longer reads the inbox for a prompt with nothing to capture. session-capture built the signal dedup index, a read and parse of every Brain/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 reindex on a vault that is not there refuses with vault not found instead 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 of second_brain_status. Both waits use the runtime-neutral sleep, and a test drives the committed bundle under a real node.

  • 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 a file: 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.md as a note still to be created. The shared path resolver now gives the POSIX answer on both.

  • brain_context_pack stopped 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 of schemas/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 of brain_intent_review, brain_retention and brain_brief view=monthly against 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 the mock-embedding test helper.

Internal
  • A temp entry that outlives the test run fails it (#194). The test preload points TMPDIR, TMP and TEMP at 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 pinned HOME now 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 of skills/.

    • plugins/codex/hooks/hooks.json is generated from hooks/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.json is mirrored, never the hooks/*.ts scripts. Codex exports CLAUDE_PLUGIN_ROOT as its cache dir. A cached script there would win o2b-hook's resolution and then fail on its ../src imports. 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 PATH o2b-hook.cmd that o2b install-cli writes, sets NoDefaultCurrentDirectoryInExePath first and asks where /q $PATH:o2b-hook rather than a bare where, 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 the o2b-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.symlinks materialised them as text files holding the target path.

    • Verified against Codex 0.157.0 in a throwaway CODEX_HOME: codex plugin marketplace add plus codex plugin add put hooks/hooks.json and all five skills in the cache. hooks/list reports 16 plugin hooks with no warnings or errors, and skills/list reports the five open-second-brain:* skills. Once trusted, the SessionStart, UserPromptSubmit and SessionEnd hooks fire through o2b-hook.

  • install/codex.md looks for the launcher under CODEX_HOME. Step 2 searched ~/.codex whatever CODEX_HOME said, 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 --check stops calling a working Codex mcp-unreachable. The host probe tested signalCode !== null to detect a killed child. Bun leaves signalCode undefined, not null, on a normal exit, so every successful codex mcp list was 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: the node:child_process call sites get null from Bun, as Node documents.

Added
  • bun run sync-plugin-mirrors rebuilds the Codex mirrors. bun run sync-plugin-mirrors:check is gated in the pre-commit hook and in two CI jobs: validate, and windows, 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 under plugins/ 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.ts answers 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\, with config.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_*_HOME and OPEN_SECOND_BRAIN_CONFIG still 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 ~\vault config value expands like ~/vault.

  • .cmd launchers. scripts\o2b.cmd, o2b-mcp.cmd, o2b-hook.cmd, vault-log.cmd and o2b-aider.cmd sit beside their bash twins.

    • They resolve Bun the way _bun-precheck.sh does: PATH first, then %USERPROFILE%\.bun\bin.

    • They never run a bun from 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 a bun.cmd would have run it. Each launcher sets NoDefaultCurrentDirectoryInExePath=1 right after setlocal, 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 .gitattributes pins them to CRLF and everything else to LF, so a Windows checkout (Git for Windows defaults to core.autocrlf=true) holds the same bytes as a POSIX one.

  • o2b install-cli on Windows writes .cmd launchers 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 as updated.

    • 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's SetEnvironmentVariable stores the user Path back as REG_SZ with its %VAR% entries expanded.

  • MCP host configs on Windows start the server as cmd /d /c o2b mcp ..., the cmd /c <name> form several hosts document for npx.

    • Node refuses to spawn a .cmd without a shell since the 2024 BatBadBut fix, and Rust and Bun resolve only .exe from a bare name.

    • /d skips cmd's AutoRun, whose output would corrupt the JSON-RPC stream.

    • Every Windows entry carries NoDefaultCurrentDirectoryInExePath=1 in its env. Without it cmd.exe resolves o2b in the current directory first, and a host starts its servers in the project it opened, so an o2b.cmd committed 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.ts entries, with the arguments now built without the cmd prefix.

  • 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:r grants access to the current user only, on the key directory (inheritable), the keyfile and an existing secrets.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 with whoami /user, not by %USERDOMAIN%\name, which a parent process can rewrite and icacls may 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.exe and whoami.exe run from System32 by absolute path. icacls /reset runs first, because /inheritance:r removes 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 failed icacls warns and never fails the write.

  • A windows-latest CI job runs the typecheck, the full TypeScript suite, the Hermes plugin's Python tests and the .cmd launcher, 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 real cmd.exe: end to end from the MCP payload's command, and with a bun.cmd and an o2b.cmd planted in the working directory, which must not run.

  • install/windows.md, plus Windows sections in install/prerequisites.md, install/claudecode.md, install/hermes.md and the README.

Changed
  • Closing SQLite releases the file immediately. closeDatabase() calls close(true): Bun's plain close() turns a connection with unfinalized statements into a zombie that keeps brain.sqlite and its -wal/-shm open 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, renameWithRetry retries EPERM, EACCES and EBUSY for up to two seconds, which is the window in which Syncthing, an editor, an antivirus scan or another o2b holds a file. On POSIX it is plain renameSync. Atomic writes and every rename site use it. The destructive-site and write-site censuses count it as the renameSync it wraps.

  • Snapshots need no gzip binary. Without zstd, the archive is gzip-compressed in-process through node:zlib, and restore inflates in-process too, so the only external tool is tar. Windows 10 and later ship tar as System32\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 /c on Windows and under sh -c elsewhere.

  • The self-heal reindex spawns bun run main.ts on 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.cmd to an absolute path, because Popen(["o2b", ...]) cannot start a batch file. Its executable lookup ignores a bun or o2b that shutil.which finds in the current directory, which Windows searches before PATH, unless that directory is on PATH itself.

  • bun install works on Windows. The inline sh prepare script is now scripts/prepare.ts, run as bun scripts/prepare.ts || exit 0, so an npm install from 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 readdir order, 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, Windows tar.exe) reports success for that.

    • Member names travel as raw bytes, so a file name that is not valid UTF-8 (a latin1 café.md on Linux) is archived under its real name instead of failing GNU tar.

  • The dream pass wrote filesystem-dependent output. It listed inbox signals in readdir order, so moved_to_processed and 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 claudecode protected 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/path on 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 index listed \notes\a.md;

    • a drive-letter git remote parsed as a URL scheme.

    Each now uses node:path or 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 tar first on PATH is MSYS GNU tar, which cannot extract into a backslashed C:\... directory. Every path handed to tar is now written with forward slashes, which it and System32\tar.exe both 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 .bak and .new files with the same Windows retry as its renames, so a stale file held by an antivirus scan no longer fails the reindex.

  • closeDatabase never throws. If the fallback lazy close throws too, the error is dropped, so a caller's finally cannot 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 LOCALAPPDATA and APPDATA, and spawned children inherit the hermetic environment.

  • The Hermes anti-drift test always skipped itself on Windows, because Popen(["o2b", ...]) cannot start o2b.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/o2b through cmd.exe, which resolves o2b.cmd via PATHEXT, so .mcp.json needs 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 exact cmd /d /c form hosts use.

    • The current-directory hijack: before the fix, cmd /d /c o2b --version run from a directory holding a planted o2b.cmd ran the planted file, and the absolute scripts\o2b.cmd ran a planted bun.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 --version as its synonym. The verb prints the installed version and exits 0; --json renders {"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 and o2b help --json offer them. o2b version latest is 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 required view and no aggregate member, so a projects read never pays for a host probe and every payload stays bounded - the same idiom brain_brief, brain_analytics and schema_inspect already use, for the same token reason. view=projects returns one entry per registered project link carrying the four-value pointer state and whether the vault it names still exists, off linkedProjectsStatus, which had one consumer and none under src/mcp. view=hosts returns one entry per registered install target carrying the target, the VerifyStatus member, the adapter's details and its fix hint, from the same verify() call over the same registry and the same InstallEnv that o2b install --check makes. An absent or unknown view is refused with -32602 naming the accepted members, and so is a hosts read with no resolved vault. Paths do not travel: project_ref and vault_ref render through the shipped expose_host_paths contract, and because the hosts view carries the adapter's own sentences - which verify() 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.instructions did 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.ts exports OPEN_SECOND_BRAIN_VERSION; the continuity export header, the MCP handshake and the stamped opencode plugin read it. A census over src/ 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 the with { type: "json" } attribute, and is now written once.

  • The install environment is built in one place. buildInstallEnv moved from the o2b install verb into src/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. vaultPathField is now hostPathReference(path, source) applied to the vault; vaultStoreReference never required the path to be the vault. Both sides of a project link render through it, so project_ref and vault_ref follow the same expose_host_paths contract vault_path obeys - an opaque vault://<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 catalog body 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 writes compares two vault trees and already normalised the snapshot archive's size_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 through prev, 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. pruneWriteImages measures each image with fileAgeMs and floors the result to whole days. fileAgeMs deliberately 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: nowMs is 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=projects returned an unbounded list. The tool takes a preview-budget exemption whose reason claimed both views were bounded by construction. That is true of view=hosts - one entry per adapter in a registry this build closes - and false of view=projects, which reads a registry nothing caps: every o2b brain project link appends to projects.json, and the serialized response grows with it, inline, into model context. The list is now capped at twenty-five and carries total, returned and truncated beside 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 now project_ref and vault_ref with it - degrade to { "error": … } rather than raising or falling back to the raw path. The reason carried was ConfigReadError's own message, which names the config file twice, once in the condition and once in the chmod remedy. 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 at second_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/list stays CALLABLE through tools/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 table tools/call routes 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 pinned catalog fixture carries the corrected text, and the exception to byte-identity is stated in the fixture test, in the module that renders it and in docs/mcp.md.

  • Two places said the catalog advertises seven tools without saying when. evaluateToolCapabilities applies the runtime window before counting, so a window withholding one of those seven lowers advertised_tool_count with it. The advertised_tool_count docblock and the matching docs/mcp.md paragraph now state that seven is the no-window count, and name the capability diagnostic as the one tool no window withholds.

  • docs/mcp.md said 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.json section, which now points at the one place the number is stated, and the catalog gap 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 the advertised_tool_count docblock 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/ for latestVersion, latest_version, update-check, updateCheck and registry.npmjs returns nothing, so nothing in this tree ever learns what the latest release is. Filling the cache requires a network check, and renderDataOwnership in src/core/install/ownership.ts renders 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 update already 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.ts is already a plan and apply migrator.

  • The hosts view 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 --check already 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 hosts view rather than reported as ten absent runtimes. verify() reads the per-vault sidecar manifest, so an unset vault produces not-installed for 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-tools window 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 at second_brain_capabilities on 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 answers unknown tool for 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 later process.env.HOME, so an in-process test cannot redirect the home the hosts view resolves. The named-probe-skip case therefore builds the InstallEnv by hand, as the adapter suites do, and asserts the view's own mapping over the real verify() 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. createNote and the write-batch kernel's update_note and append_note commits each append exactly one note-write log 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 literal absent when the target did not exist, deliberately not the hash of zero bytes), hash_after, bytes_before, bytes_after, and agent. The bytes an update or append replaced are kept at Brain/.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 carry write_id: null and an audit_reason instead, and a create that skipped an occupied target records nothing because it authored nothing.

  • o2b brain writes and brain_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_query and brain_agent_diff gain the note contribution 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 revert and brain_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: restore to the bytes the oldest selected write replaced, delete when the oldest selected write created the note and nothing preceded it, or a refusal by name: drift when the note on disk is not what the newest selected write left, interleaved when an unselected write sits between the selected ones, image-missing when the before-image was pruned, already-reverted when the target already holds the bytes the plan would restore, unrecorded when 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 a note-revert snapshot, and records each restore and delete as a note-write of operation revert. Apply is CLI-only; the MCP tool plans and does not apply.

  • A fleet freeze. o2b brain freeze [--reason] writes Brain/.state/frozen.json and o2b brain unfreeze removes 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, with frozen_at, by, reason and 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, so freeze, unfreeze and write-refused events 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 one write-refused event naming the tool. o2b brain rollback on 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>.jsonl and 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>.jsonl is 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 whose pref_id is 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's sync-conflict-log finding 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 }, and prev, the h of the previous chained line in the same shard or null at 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 as hash-mismatch, prev-mismatch or malformed with the line number; brain_doctor emits one log-chain-broken warning per broken shard. Report-only: readLogDay reads ts, kind and payload and nothing else, so a broken shard still yields every event it holds. The markdown twin is a derived rendering and is not chained.

Changed
  • readLogDay reports the shard beside each entry. entryShardIds is a parallel array at the same index as entries, 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, applyWriteBatch and scaffoldStub accept a configPath. 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 agent on a note-write event is a caller-asserted claim, exactly as src/core/write-binding/index.ts has stated since 1.52.0; origin_channel and 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 crafted target on a note-write event aborted the whole revert plan with an unnamed error and is now the per-target refusal target-invalid; one throwing entry in a revert apply aborted the rest after the snapshot and is now a named failed bucket 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. isRemotelyReadable is the kernel the rest of the wave consumes: it denies exactly one token - REMOTE_DENY_VISIBILITY_TOKEN, written once in the normal form pageVisibility reads 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 semantics isVisible has always given it; this constant reserves one identifier and defines no policy for any other. The caller's own visibility argument 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. TransportReach is a closed two-member vocabulary - local and remote - and it is a transport fact rather than a request field. stdio and the CLI mint local because the caller already started the process and holds whatever filesystem access it runs with; an HTTP bind mints from isLoopbackHost alone, keyed on the bind rather than on a caller-supplied Host header, so every non-loopback bind is remote. Each value is set after the caller's runtime options are spread, the same guard sendNotification already uses, so a runtime option cannot claim a reach the bind did not establish. A caller that puts a reach-shaped argument in a tools/call is refused with -32602 and the token caller-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, so reach, Reach, transport_reach, transport-reach and transportReach are one argument rather than five holes. disclosure is deliberately not reserved - it is the unrelated result-depth mode on the recall surfaces. What the local bypass 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 applyVisibilityScope is reimplemented on the predicate rather than left beside it, and the reach rides on SearchOptions.transportReach with an absent value resolving to the narrowest. Root B is listVaultPages, 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, through MAINTENANCE_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_REGISTRY and its architecture sweep make the closure claim checkable instead of prose: every file under src/mcp/, src/cli/ and src/openclaw/ that opens a vault path through node:fs rather 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_clusters listing Brain/clusters/*.md and brain_bridges reading Brain/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 an ALTER 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 at chunk_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 - anyVisibilityTagPresent was a LIKE '%visibility:%' scan that read a quoted key or a body line as a tagged vault, and both false positives are gone - plus the two populations o2b search check now reports rather than absorbs.

  • The OpenClaw page walker joins the boundary and the census. src/openclaw/index.ts was 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's total_pages is now counted after the filter, closing a difference between total_pages and returned that the status-scope filter had already made observable.

Changed
  • The o2b search check visibility 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. The index_store row is kept and reworded rather than dropped: a reserved page's text is still in chunks and chunk_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. buildCacheKey enumerates 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 a local outcome and a remote outcome collided on one row of the shared query_cache table 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-rows rides out on every brain_search response and is not gated behind explain. 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_by edges 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 own visibility scope and agentScope predates this boundary and is recorded at the seam rather than silently carried.

  • A caller-supplied benchmark is scored at the caller's reach. runRecallBenchmark pinned local on every search it made under a comment calling itself an internal lane. It is not one - dataset is a required argument of brain_benchmark and brain_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. captureRecallFeedback had the same defect in sharper form, re-running the judged query unscoped and returning resultFound.

  • 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 listVaultPages listed 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=topic filtered its signals and log events and then reported the pre-filter count - a number written to the recall-telemetry log and handed back verbatim by brain_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=run reported 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=preference passed an already-formatted sentence into a constructor that formats the sentence itself, and osb://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 the pref- prefix was a one-bit existence oracle over the whole reserved population. Both key shapes are pinned on both surfaces now.

  • brain_bridges and brain_backlinks ask the rule about what they name, not only about what they return. brain_bridges was classified covered off its list mode alone, while discover returned proposals naming two pages each by vault-relative path; a proposal is dropped whole when either end is withheld. brain_backlinks and 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|alias and pref-x#Raw, which brain_apply_evidence documents for its own artifact argument; multi-artifact payload values, which are string | ReadonlyArray<string> and answered undefined as arrays; and an enumeration of four payload keys where a census of every appendLogEvent body 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_hygiene and brain_skill_proposals reach page content through a src/core/ helper - so neither had a registry row, neither was in the number o2b search check reports, 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.sqlite reads 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.disclosure is 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/call path and asserts over the whole serialised response, which covers structuredContent, the rendered text, a thrown message and the _meta member 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 writes failed about once in twelve loaded runs and never in isolation; the whole difference was the snapshot entry's size_bytes, republished into the day log the assertion compares, and whether it tripped depended on whether the copy straddled a second. Verified against main'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_into pointer 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 --apply overwrite 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 suggestions and the hygiene dedup detector, both of which repeated the finding in their own sections - which needed parsePreference to expose merged_into so the digest's pre-parsed path reaches the same verdict as the filesystem one.

  • The merged-into write seam refuses cycles and silent repoints. setMergedInto checked for a self-pointer and nothing else. It now reads the existing pointer first: re-setting the same canonical writes nothing and reports changed: 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's CYCLE / DEPTH / MALFORMED behaviour, 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-dangling doctor lint is deferred. A new doctor code needs an entry in DIAGNOSTIC_SIGNALS or DOCTOR_EXIT_EXCLUSIONS under 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_pack reads its query two ways, and the second one ranks. The tool has always accepted a query and has always meant a case-insensitive SUBSTRING match against topic + principle, so a natural-language turn missed every candidate and came back with an empty pack and a filter-miss for 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 omitted query_mode is byte-identical to every release before this one and a test pins it; the mode requires a query, stated in the schema as dependentRequired and 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-issued receipt_id are all inherited rather than bypassed.

Fixed
  • Hermes recall is query-aware without leaving the lane that makes it safe. prefetch calls 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-issued receipt_id again rather than a locally hashed string: an id with no receipt behind it resolves unresolved / sample_absent on every outcome an agent posts, which would have undone what #179 shipped one release earlier. The local character budget is gone with it - max_tokens is 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 beginning principle: 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_pack computed 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 as brain_pre_compress_pack always 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 defaults handle_tool_call supplies for brain_context_pack_outcome are also scoped to operation: "post" - host and session_id are read filters for list and summary, 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. prefetch now requests a context receipt and opt-in telemetry from the same brain_context_pack call 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 - posts brain_context_pack_outcome itself 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_outcome joins 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 check measures 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 restamp repairs the one drift that needs no provider, a stale vec_version token: dry-run by default, --apply writes 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 raises target_unreadable (and a scanner-lossy frontmatter raises target_frontmatter_lossy) before anything is projected, and a blank body over a non-empty note refuses unless the caller passes the new allow_empty flag on brain_update_note or brain_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, or import, 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_dream is 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-loaded brain_context carries 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_candidates manifest - 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: private frontmatter 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, and o2b search check states 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-stub landed 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_threshold partitions 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. A retriage action 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 into Brain/inbox/ as speculative signals under a new closed-vocabulary source_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. WriteCaptureInput gains a free-form guidance field - part of the capture identity hash, so two captures differing only in guidance are two captures - and o2b brain capture becomes 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. --expires on the feedback verb and an expires parameter on the feedback tool stamp a lifetime at creation; the new o2b brain expire / brain_expire mutate or explicitly clear it afterwards, addressed by artifact id because the note-path surfaces refuse the Brain/ root by design. Every path funnels through the one normalizer, clearing is a word rather than an empty string, and an equivalent re-set reports changed: false and writes nothing.

  • Agent Skill drafts mined from the pages that earned them. A mature_page pattern 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 a SKILL.md under 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 codegraph region states the partner graph verdict - graph-present with node, file, and edge counts and health findings, or graph-absent, lexical-only with the specific state and reason - from the report the partner already publishes, through zero new partner invocations. A module-map region renders a deterministic Mermaid containment diagram from the scanner's own facts, and a decisions note lists the repository's mined ADR candidates by the repo_key both 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 under Brain/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 PATH that contains Bun. Resolving an absolute o2b was only half the job: the wrapper is a shell script whose first act is command -v bun, run against whatever environment it inherits. When PATH cannot see Bun and the fallback scan can, the child now starts with that Bun's directory first on its PATH. The o2b wrapper 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 bun missing means "not on this PATH", not "not installed", and the script answered the second question with install instructions for a runtime already present. Both the precheck and the o2b-hook wrapper, 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 items key 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, o2b symlinked 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[*].body values and bypasses the preview_truncated transport 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 spawns o2b mcp and that is where o2b reads 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's default, which is the documented way a provider reports a value it holds, and get_status_config, which is what hermes memory status prints. Measured against a live hermes-agent with nothing written under $HERMES_HOME: configured: false, status: needs_config before, configured: true, status: ready after, with the vault, agent name and timezone rendered in the panel.

  • hermes memory status prints the configuration that is in force. It was passing the provider its own memory.<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_concurrency second 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 mcp refuses 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/list reads 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 from OK (skipped=1) to OK, 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 o2b reads.

  • 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 setup and 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 catalog profile: seven advertised, every verb still reachable through tool_hydrate. The count is not asserted from a fixture - the test builds a real server and calls tools/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 --friction compares 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 --json is 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 status reports 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 migrate and o2b state rollback move 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 --status and --discover answer 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-jsonl emits 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_dream with 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_maintenance reaches 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. The force description 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=x was caught and ANTHROPIC_API_KEY=x was 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 --to names 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 update hashed a payload it does not write, so it reported no change while o2b install --check reported 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.

  • --since silently 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 --friction crashed 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 scores and match_quality all-or-nothing on brain_recall_gate and brain_context_pack, and said in the changelog and in the upgrade notes that the schema states it as dependentRequired. 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, its dependentRequired entry and its handler read one constant. A new test derives its population from the live tool table rather than a list: every tool accepting match_quality must 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 the match_quality property and the dependentRequired keyword outright, and brain_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 reporting OK (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 EEXIST anywhere 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 make Brain/.snapshots read 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 a cause neither 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_delivery on, 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 let import-claude-memory write an unstamped preference through its own renderer.

  • A caller-supplied owner scope is checked. Under the gate, an agent_scope naming 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 identity agent is 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} and osb://log/{date} returned another owner's file verbatim, including its owner: 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. SubagentStop is registered, and a session turn carries the delegated agentId and isSidechain — 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 from hooks/hooks.json itself.

  • 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_path contract 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 to recall_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 graded sufficient / 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. idfWeightedCoverage is number | null, and null exactly 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. null is nullable rather than a flag so the compiler forces every consumer to choose: the targeted retry does not fire, completeness gains an unmeasurable verdict rather than folding into sparse, 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: 1 and limit: 20 differs, 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 stamped tier: peripheral scored 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_gate and brain_context_pack take match_quality beside 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 as dependentRequired, and the verdict returns match_quality so 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_path and write-site censuses carried the same shape and are pinned too.

Fixed
  • brain_hygiene mode=apply wrote across the owner boundary. The owner view was constructed inside the scan branch, so a caller whose scan correctly returned nothing could hand apply a 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 repair had the same shape and rewrote another owner's frontmatter.

  • Three surfaces disclosed what they had just withheld. brain_analytics filtered timeline and left concept_synthesis, belief_evolution and dedup open — the first returned another owner's preference id and a verbatim line of its body. brain_health returned 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_sources was 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 repair and brain_agent_diff — all closed, none moved to the unscoped bucket.

  • An unredacted vault export. o2b brain explorer --export wrote 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-import no 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 --json redactor 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.md was 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=lint and view=orphans died 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_labels and brain_tiers answered a missing path with "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. total is 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. --progress on o2b brain dream, bridges discover, clusters run, maintenance run, architect, search index, search reindex and search 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/progress against 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-backfill and brain maintenance run stop 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 --force disables 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), which doctor() accepted and nothing could set.

Changed
  • embedding_concurrency bounds requests in flight process-wide, per embedding identity against one endpoint, rather than per embed() call. Two models on one host are two budgets, so that host sees up to embedding_concurrency times 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 .gitignore question was settled by measurement - a dot-directory rule removes 88% of the files, and full .gitignore support 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 install resolves the vault through the same chain as every other verb. It previously used --vault, then the config vault key, then VAULT_DIR. It now uses --vault, then VAULT_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 what o2b status and o2b brain doctor were 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 --readiness exits 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 number o2b search check already spends on the same condition, and a test asserts the two cannot drift.

  • o2b brain maintenance run exits 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.yaml blocks, maintenance: and embeddings:, both absent by default and templated commented. A vault that declares only one of embeddings.sunset_model / sunset_at fails 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_impact no longer reports method: "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_tokens changed meaning, not only name.

  • o2b brain doctor reports 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.

  • listSnapshots returns a listing rather than an array. A per-entry read failure was dropped with a bare continue, 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 --json gains skipped, and pruneSnapshots's retained no longer claims "still on disk" about entries it could not see.

  • o2b search vector-backfill now runs under a deadline. It declared safeguard, signal and onProgress and had a producer for none of them; wiring two of the three would have left the third declared and unreachable. The budget is the reindex key off the same ladder o2b search index reads, because this pass is that run's embedding phase alone.

  • EMBEDDING_SUNSET_UNDETERMINED_REASON loses declaration_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-backfill grew the entire progress spine and no way to reach it. An options field, a counter and a terminator in core, with no --progress flag, 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 NaN budget 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=operator and brain_review_candidates ran 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.which and Bun.spawnSync resolved 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.ts went 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 inside Brain/ are refused identically on each. The relocation relinks rather than re-reading: linkSync attaches 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 doctor surfaces 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 check exits 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 matches o2b install --check for 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 --json key provider_reachable is replaced by provider_probe; a boolean had to answer four questions with two values, so a provider that refused and one that never answered were the same false.

  • o2b brain bank-import exits 1 when a rule was genuinely left on the floor, where it previously always exited 0, and its result gains derived, failed and topicKeyCollisions.

  • A second top-level directory, Archive/, receives archived notes. Brain/ is refused for every note path, and an operator whose backup rule covers only Brain/ should extend it.

  • mergePage fails closed on a malformed Brain/_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 verdict covered, 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 promises binding, a bare fs specifier, dynamic imports, computed member access and .mts files 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-export merged 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-config mangled 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 export leaked 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 --help on 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_paths allowlist. Vault scope was exclude-only, so indexing nothing but Brain/ 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 unknown status, 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 quiery got 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_entities classified 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 names README.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 agentName beat agent_name by 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_into value 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 failing o2b doctor telling 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 existsSync at the vault root about a rule whose grammar matches at any depth.

  • clusters run --if-stale swallowed a configuration it could not parse and answered fresh, 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_extract declares turn_start and turn_end as 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 -32602 before 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_gate scores description 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 pinned preference, so from 13 August 2026 on every vault initialised with --starter reported pinned-without-recent-evidence on its first o2b brain doctor run. 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's now seam, reserved for exactly this and until now unused, is what the tests pin it with.

  • A test that deletes OPEN_SECOND_BRAIN_CONFIG from 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.yaml hid 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, modified or unverified with a named reason, on both read surfaces and in o2b 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, unknown and did_not_happen are 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 produce unknown with their own reason instead of a confident nothing. A vault that never configured an embedding model still receives a not_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 doctor as 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-template emits 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-health gives 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 suppress and unsuppress. 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 snapshot can 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 snapshot kind it had been missing since rollback was added, and o2b brain snapshot log completes 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.md is 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 the zstd path 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 sync returned 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.yaml was 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 bare if collapses 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 partner verb 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 not ack or dismiss to the act transition, and o2b search reindex declared 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 through JSON.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 EMFILE and 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.js was 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 explain flag, absent rather than null when it is off, and naming the switch that produces them when the gate is not engaged.

  • o2b search check --integrity runs 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 query gains --at and --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 k nearest 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_expired was 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.

  • total echoed 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 since and until filter 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-backfill resolves anchors for documents indexed before this release; an index run counts what is still pending and names that command as its exit.

  • embedding_batch_tokens closes 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-backfill runs the vector phase alone.

  • schema_apply_mutations and o2b brain schema apply gain 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-run parsed 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/until filter rather than only the ranking boost, so a note carrying an old created_at and 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:fs import 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 grep classify 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_impact still 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.yaml block, 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 dead Set branch, 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 search verb 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 between index and reindex, 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.yaml that 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; a void basename in 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 init over 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's vault_exists field, which also gated three blocks, answered false for 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|sig artifact 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 --exclude layer last, re-stacked at every directory so it wins at any depth; a submodule boundary is a boundary rather than an ignore rule, and --exclude cannot 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 --exclude still throws. Under --subpath the directories between the source and the walk root were never visited, so their .gitignore files 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 scope as 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.

  • fact and skill marker 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-subpath walked 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-run reported 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>, with step and gates on the brain_dream tool. 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 a recover operation on brain_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-run on o2b 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_dedup continuity record with a brain_analytics view 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 project scope on session digests, reusing the existing scope axis, its slug rule, and the project_scope argument 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 doctor reports now carries its structural next command on the human surface and as a field under --json and on brain_doctor. Where no single command resolves a class, the doctor says so instead of falling silent: an additive no_exit object 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 help is now rendered from that manifest instead of a parallel hand-written constant. Shell completions and o2b help --json widen 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.yaml is 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 init creates 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 actions could 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 init contradicted 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.yaml at 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 DegradationCode union 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 a never-typed default arm fails to compile on an unhandled member.

  • Stamp-and-compare seam and the integrity: block in _brain.yaml: owner_scope_delivery (default off), embedding_abi (default warn), and pack_validity_seconds (default 900). An unrecognised mode or a non-positive validity raises BrainConfigError naming the dotted field; an absent block resolves to the documented defaults. Under off the 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 by integrity.owner_scope_delivery; search-backed surfaces filter whenever the caller passes a scope. An optional agent_scope argument joins eleven MCP tools and an --agent-scope flag joins o2b search query, o2b search expand, and o2b 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_expand on another owner's chunk now raises the same chunk not found error 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 check reports 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 index reports frontmatter_notices, o2b brain doctor reports uncertain entries, 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:check is wired into both CI workflows and exits 1 on a rise, naming bun run link-ratchet as the fix. The committed ceiling in link-ratchet.json measures the templates/brain-starter repository 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.md body, and the lessons.md digest, 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 summary operation on brain_context_receipts folding 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, or evidence-missing - instead of a bare null. Two concurrent sessions in one directory inside the freshness window both abstain where one was previously stitched to the other. Repo, branch, and commit join cwd as required-match predicates, captured through bounded git invocations; 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 in Brain/.state/session-lineage-gaps.jsonl and 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 init stamps an idempotent Brain/vault-id.json carrying 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 doctor reports a Brain-less root as a named brain-root-absent warning with an uncertain entry recording that every check below was skipped, instead of certifying it clean. o2b brain doctor --strict on such a root now exits 2 where it previously exited 0.

  • vault profile switch refuses 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_scope on eleven, and since, until, max_receipts plus a widened operation on brain_context_receipts. No tool was added or removed (the count stays 108) and no output schema changed.

  • brain_hygiene gains an additive top-level link_integrity key. It refuses to produce a number unless the index's last run was a forced full pass, reporting measured: false with reason partial-resolution rather than a misleading zero.

  • Retirement keeps ownership. BrainRetired carries an owner field 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_brief views that cannot honour a scope (daily, weekly, monthly, operator, today) now reject an explicit agent_scope with INVALID_PARAMS naming 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_context filters what it returns at render time and leaves Brain/active.md complete, 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.yaml no 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 a brain_config_unreadable notice.

  • 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 grep skip 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_before date (date-only or full ISO-8601 timestamp) in _brain.yaml suppresses advisory findings whose underlying entries are entirely older than the watermark - a batch-concept-inflation burst whose window ended before it, a concept-gap term 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, and clear record, 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 --json operational failures emit {ok:false} envelopes.

  • Explicit suppression reporting: whenever the watermark hides at least one finding, o2b brain health prints suppressed: N finding(s) older than baseline <date> and the report carries an additive suppressed object with per-detector counts and the baseline date, mirrored on MCP brain_health as suppressed: { 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-time helper.

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_types with subset validation, no natural-language word lists) and bounded depth-2 typed-edge fan-out join hybrid search as a fourth RRF arm behind search_relational_arm_enabled (env OPEN_SECOND_BRAIN_SEARCH_RELATIONAL_ARM, default off, byte-identical when disabled); RRF and dedup keys carry source identity through the shared rrfKey, 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_search gains optional session_scope and project_scope filters that leave unfiltered results byte-identical.

  • Exact-state lane with a retrieval-time staleness barrier: o2b brain state set|get|list|clear maintains an overwrite-only, per-aspect operational-state lane under Brain/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.md deliberately remains a searchable scratchpad, and the lane is the supported home for current-state values.

  • Deterministic summary-search router: query plans carry a surface verdict 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-fit samples 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_plan MCP 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 UserPromptSubmit only when the cadence is due, behind nav_tier_enabled (env OPEN_SECOND_BRAIN_NAV_TIER_ENABLED, default off) with nav_tier_cadence_minutes (env OPEN_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 (env OPEN_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: checkCodegraph aggregates status across every discovered code project and threads project_path per 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 by telegram_bot_token (env TELEGRAM_BOT_TOKEN) and a chat-id allowlist telegram_chat_allowlist (env TELEGRAM_CHAT_ALLOWLIST, empty accepts nothing); each accepted message becomes one staged capture note through a new capture-note contract (kind, provenance, capture timestamp) under Brain/captures/; rejected or malformed updates log one decision each to a capture-decisions sidecar; catchup replays 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 in captured/, 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_KEY or TAVILY_API_KEY is 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 with fact_threshold and identity_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, MCP brain_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 --json payload 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" (env OPEN_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" (env OPEN_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_assessment and retrieval_decision_trace, counting each exclusion with its reason so nothing is dropped silently.

  • Relation-only supersede fade: with search_supersede_fade_enabled: "true" (env OPEN_SECOND_BRAIN_SEARCH_SUPERSEDE_FADE), an inbound supersedes or superseded_by relation 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_note and brain_append_note MCP 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_batch MCP 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_raw on session recall (brain_session_grep): responses carry the original raw capture inline beside each derived record, and every item is stamped with an extracted boolean discriminator (derived versus raw); an optional raw_budget_chars clips raw payloads while identity fields survive.

  • Knowledge-gap loop: with gap_loop_enabled: "true" (env OPEN_SECOND_BRAIN_GAP_LOOP_ENABLED, threshold via gap_loop_threshold), recurring recall gaps aggregated from existing gap-count telemetry promote at session end into durable task notes under Brain/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_id joins session_id on pack and continuity identity, and the new clipPayloadToBudget primitive retains both fields under any output-budget truncation while dropping non-protected keys.

  • Fail-fast doctor readiness probes: o2b doctor --readiness runs 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_grep gains optional include_raw and raw_budget_chars params 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 .gitignore support 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/exclude participates); repositories without ignore files scan exactly as before and malformed patterns warn explicitly.

  • Source ingest scoping: --src-subpath and --exclude on o2b brain batch-plan (and src_subpath / exclude on brain_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 extractable flag is enforced during page discovery: with a non-empty allowlist, pages whose schema_type is 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_extract on brain_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 report extracted: false with a reason, never a fake empty success.

  • Dispatched-vs-ingested reconciliation (o2b brain batch-plan --reconcile, reconcile on brain_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 dated source-citation events on the temporal timeline, stamped at the citation date and deduplicated on normalized name plus date; malformed markers are reported and skipped (--strict exits nonzero).

  • Configurable FTS tokenizer: search_fts_diacritics (0 | 1 | 2) and search_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 explicit o2b search reindex, and the CJK trigram path is untouched.

  • Graph-degree cardinality predicates in the search filter DSL: --degree on o2b search (and degree on brain_search) filters by backlink and outlink counts with =, !=, >, >=, <, <= (orphans backlinks=0, hubs outlinks>=N), backed by directed degree data on the existing link-graph index.

  • Doctor repair mode (o2b brain doctor --repair, repair / apply on brain_doctor): previews targeted fixes for doctor-detected issue classes (dangling workrun checkpoints, dead evidence links) without writing; --repair --apply performs them idempotently and logs one typed doctor-repair event per fix; structural lifecycle links and append-only audit history are reported needs-review or unfixable rather than half-fixed; plain doctor and --strict stay read-only.

  • Unified operator status snapshot (o2b brain status, MCP brain_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 (and vault-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, and brain_search gain 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 --repair and o2b 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, MCP brain_lifecycle): idempotent soft delete via frontmatter (_status: tombstoned, reason, timestamp, optional superseded_by) across preferences, signals, and learnings; tombstoned and superseded-non-tip entries leave recall, inject, and Brain/active.md but 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 reuses superseded_by.

  • Persisted claim-graph query surface (o2b brain claims, MCP brain_claims): one bounded, deterministically rebuildable projection (Brain/claim-graph.json) over existing superseded_by/contradicts relations 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-decay events, named threshold constants).

  • Decision-record note family (o2b brain decision, MCP brain_decision): type: decision notes under Brain/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) plus rationale round-trip through decision frontmatter, list --rated sorts by rating, compare reads decisions side by side; rated decisions stay out of signal and preference recall.

  • Commitment tier vocabulary: optional commitment: exploring | leaning | decided | locked on 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.v1 JSONL 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 history pages 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_session and decision_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_at on turn signals, the search index (schema v10) carries and returns it (authored_at in CLI JSON, authoredAt in MCP results), exact hybrid-score ties break toward more recently authored content, session-grep accepts --since/--before, and o2b brain authored-at-backfill upgrades existing vaults idempotently (dry-run default, no re-embedding).

  • Tension objects (o2b brain tension, MCP brain_tension): detected contradictions persist under Brain/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_grep gains since/before parameters and brain_search results expose authoredAt.

  • Search store schema advances to v10 with a nullable authored_at column; existing indexes migrate in place and documents without a turn instant are unchanged.

  • Context-pack reports gain an additive warnings array (unresolved-tension notices) and items carry optional commitment; 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_VECTOR error 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 query from passage inputs, the multilingual-e5 preset carries "query: "/"passage: " defaults, and embedding_prefix_query/embedding_prefix_passage config 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-retriable EMBEDDING_QUOTA_EXHAUSTED outcome 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-After header (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 --semantic requests keep throwing the typed error rather than silently falling back.

  • Snapshot-before-destructive-write gate. A shared withDestructiveSnapshot wrapper over the existing tar+zst snapshot engine now fronts o2b 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. sanitizeEntityLabel strips 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-supplied entities.label_denylist) rejects junk labels: a typed error at creation, a logged skip at anchoring. o2b brain entity prune lists malformed historical nodes (dry-run default) and on --confirm removes them and their edges behind the snapshot gate; a new entity-label-malformed doctor 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.denylist regexes) rejects transient operational content before it persists as durable memory. Rejections are counted and logged as durability-skip events, 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 into Brain/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 to Brain/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_feedback MCP response and the CLI output, and logs a write-conflict-advisory event. 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 to Brain/retired/ with retire frontmatter (_status, retired_at, retired_reason, optional superseded_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-permissions auto-safe remediation step (idempotent, step-capped, dry-run first) tightens existing Brain/ files to owner-only modes; a symlink-escape doctor lint reports vault-internal symlinks resolving outside the vault root.

Changed
  • MCP responses no longer expose absolute host paths by default. The vault_path fields 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. Set expose_host_paths: true (or OPEN_SECOND_BRAIN_EXPOSE_HOST_PATHS=true) to restore the raw value.

  • Zero-norm and non-finite vectors now throw during normalisation. unitNormaliseInPlace previously returned an all-zero vector unchanged on zero norm and propagated NaN silently; both cases now raise the typed EMBEDDING_INVALID_VECTOR error, 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-days and --limit bound the activity window and both accept 0 (empty activity section) consistently across the core builder, the CLI, and the MCP view=today handler.

  • Merged chronological activity timeline. buildActivityTimeline renders the Brain log's TimelineIndex as 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 @osb marker grammar gains a loop kind: @osb loop <text> (optional id=) 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 explicit id=; 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 a set kind: @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; --apply requires the new opt-in guardrails.marker_writeback flag (default off), emits one attribute-write Brain 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 as applied-unconsumed (never a clean applied) with the cause; the CLI renders it, adds an unconsumed count, 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.ts and 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. @osb markers 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 module src/core/brain/types.ts; LinkOutputFormat moves from config.ts to its only consumer wikilink.ts; the ~35-file src/mcp component dissolves into a one-directional layering over a new src/mcp/tool-contract.ts contract leaf; src/core/search gains embeddings/contract.ts, rerank/contract.ts, and embeddings/configured-provider.ts while search/types.ts becomes a pure leaf; and a new search/tuning-store.ts splits the tuned-parameter read side from the grid optimizer, closing the last search -> tuning -> benchmark triangle. code-ranker now reports zero cycle.chain violations and zero cycles in the file graph.

  • src/core/search/search.ts decomposed. The worst-ranked file in the tree (maintainability index -85.8, cognitive complexity 442, 1722 lines) keeps only the search() 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 the scope distribution), connectivity_index (mean evidenced_by count per preference), orphan_preferences (confirmed preferences below an evidence threshold, default 2, --orphan-threshold), and gap_pressure (open concept-gap findings, reused from the semantic-health pass, divided by preference count). Text or --json; records one vault_vitals metric per run, following the existing clusters/benchmark surface pattern.

  • Batch-concept-inflation lint. The existing duplicate-preferences lint only catches near-identical pairs (Jaccard >= 0.7 within one topic/scope). detectBatchInflation is a deterministic, non-overlapping window scan over confirmed preferences' confirmed_at timestamps (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, the batch-concept-inflation doctor lint, o2b brain health (CLI) and brain_health (MCP), and a new batch_inflation Workspace Insight trigger kind.

Changed
  • Vault vitals sources gap_pressure from a semantic-only health pass. computeVaultVitals now calls a new exported computeSemanticHealth instead of the full runDoctor sweep to read the open concept-gap count. Both route through the same detector over the same inputs, so gap_pressure is 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 init now 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-runnable o2b onboarding verb (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 CheckResult gains an optional fix field carrying a copy-pasteable remediation command, populated in every failing check (mkdir/chmod for vault and config, o2b update for a missing or invalid manifest). o2b doctor renders it under failing checks, gains a scriptable --json report (per-check fix plus an aggregate {total, failed} summary) and a summary line, and surfaces fix through vault_health and 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_health for pull consumers. Default on; opt out with OPEN_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 the brain_watchdog vault-health probe.

  • Expectation and strict count guards on mutating ops. --expect N asserts how many items a mutation will touch and --strict refuses 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 honest matched vs changed counts. 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_invoked continuity record, emitted from the session-import tool-call scan across Claude Code and opencode logs (get_skill and the host-native Skill; discovery and attach are not invocations). Counts are derived read-side, deduped by content-address id so re-import is idempotent, and surfaced via brain_skill_proposals operation: usage and o2b 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 as embedding_concurrency.

  • Hardened optional loopback HTTP transport. The opt-in o2b mcp --transport http server gains a mandatory, non-bypassable Host/Origin DNS-rebinding guard (a foreign Host or cross-origin/opaque Origin is rejected before auth) and an unauthenticated GET /health liveness 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 add defaults --model to the recommended model when omitted. Advisory only - a custom --model stays first-class and verbatim. No server, no network.

  • Native ZeroEntropy embedding provider. zeroentropy is now a first-class embedding provider calling the native POST {base}/models/embed API directly over fetch (no SDK, no dependency), with Bearer auth, input-order-preserving vectors, and Matryoshka dimensions when configured. It reuses the shared retry/backoff and ping contract; the resolved-provider union stays closed.

  • Trigram candidate prefilter for large-vault search. A native SQLite FTS5 trigram shadow 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 a should_read shortlist - surfacing notes reachable only via several hops that the 1-hop link boost misses. An index_only mode 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 local reranker (coverage / proximity / density blend) behind the existing rerank interface makes reranking work fully offline with no model runtime or network, selected via search_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_use continuity 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 new brain_observed_use MCP tool (mirroring brain_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 via brain_recall_telemetry observed_reuse. Byte-identical when no verdicts exist.

  • LoCoMo benchmark suite. o2b brain bench memory --suite locomo converts 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 envKey may now be an ordered probe list (a single string still works and stays byte-identical); resolution takes the first present key and the OpenAICompatProvider fails over to the next on an HTTP 401/403, pinning the first that authenticates. o2b search provider add --env-key A,B registers 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 via brain_procedural_memory mark_outcome and list ranked:true.

Fixed
  • Resolve o2b/bun when the memory provider inherits a tiny PATH. When Hermes starts the provider from a process with a minimal inherited PATH, shutil.which("o2b") cannot see a user-local install and subprocess.Popen later fails with No 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 both o2b and bun. shutil.which still wins first, the POSIX-only o2b branch and the repo-local bun run <entry> mcp fallback are preserved, and on Windows the scan matches PATHEXT suffixes 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>.json records completed items union-as-you-go, so an interrupted large-folder ingest resumes at the item boundary instead of re-planning from scratch. planBatches accepts resume, 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 stable plan_id plus a resumed_completed count; ingestSource records completion when handed a plan_id. The brain_ingest_batch_plan MCP tool gains resume and returns plan_id, clearing a drained plan; brain_ingest_source and the o2b brain batch-plan --resume CLI carry it through. The content-hash manifest stays the authoritative final state. Opt out with OSB_INGEST_NO_CHECKPOINT.

  • mem0 and generic-JSON memory-store importers. The MemorySourceBackend seam widened from one-file-one-entry to discover-files plus parse-entries, so a single JSON export maps to many Brain preferences. New mem0 and generic backends parse a memory export (a top-level array or a results/memories/entries envelope) into preferences on the shared render path; the claude backend stays byte-identical. The o2b brain import-claude-memory CLI gains --from/--backend to 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-stale that no-ops when the materialized Brain/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 distill CLI verb and brain_distill_source MCP tool condense a source into agent-supplied atomic claims, each with an optional source block id, and write an idempotent Brain/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 NormalizeContractTests suite in tests/python/test_static_schemas.py asserts that every entry get_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-level name, exactly the preconditions core's new shared normalize_tool_schema() (upstream Hermes PR #52140) relies on. A companion ProviderNormalizeSurvivalTests case 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. The inputSchema to parameters remap 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 InPlaceCompactionLifecycleTests suite in tests/python/test_memory_provider.py drives sync_turn followed by repeated on_pre_compress and on_session_end with one fixed session id (the new compression.in_place default flipped by upstream Hermes PR #52658) and asserts exactly-once flush semantics: brain_pre_compact_extract fires 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.json records 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. WriteSignalInput gains optional valid_from and recorded_at that flow into the same frontmatter keys readBiTemporalSlots() already parses; session-import prefers SessionTurn.timestamp for created_at and recorded_at and falls back to now only when absent or unparseable, so backfilled signals are stamped with the original turn time instead of the import moment. brain_feedback and the batch import path forward an optional per-row event_time. Absent by default, existing records byte-identical.

  • Batch-plan step for large-folder ingest. A new planBatches helper 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, and appendApplyEvidence accept an optional idempotency_key; the writer hashes the would-be payload and consults a month-sharded JSONL ledger under Brain/logs/idempotency/. Same key and same hash is a deduped no-op; same key with a different hash is an explicit payload_mismatch error, never a silent overwrite. brain_feedback forwards an optional idempotency_key.

  • Dry-run extraction preview. extractPreCompactRecords and the other extraction entry points accept an optional dryRun flag that returns the exact records the real path would append without calling appendContinuityRecord (no vault mutation, no log event, no dream or retire trigger). Mirrors the existing opts.dryRun idiom in import.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_turn continuity kind (additive; legacy records read as v1). appendRecentTurn evicts 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_checkpoint MCP 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. WritePreferenceInput and WriteSignalInput accept an optional ISO expiration_date. The read path silently drops anything past its expiration by default; an opt-in showExpired re-includes expired records for audit. Orthogonal to dream heuristic retirement: an expired-by-date memory is filtered, not moved to Brain/retired/ (audit trail preserved).

  • Delete and search by exact source file. Search accepts an exact source_file filter matching the source-ref frontmatter, and a new delete_by_source command 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 --confirm deletes 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 computeGroundingScore projection over the same ClaimEvents that feed computeTruthState returns { score, confidence, band, supportingSources, contradictingSources }. Confirming vs contradicting evidence is counted across independent sources (distinct source and agent), 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. detectNoteContradictions pairs same-subject permanent notes asserting opposite stances, deriving position-sign from note prose rather than evidenced_by signals. Reuses the existing language-agnostic token-overlap machinery; emits ask_user findings 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-aider wrapper (also o2b aider wrap) brackets an Aider session: it re-renders the static context sidecar at start, execs the Aider binary with the injected read: context, and at session end runs the write-back half (capture and persist the session into the Brain), mirroring Hermes prefetch, sync_turn, and on_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. deleteBySource now reads a preference's managed _evidenced_by links (not only the legacy evidenced_by key) 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 includes host and sourceTurnIds, so a retry that changes only owner-visibility or checkpoint provenance raises IdempotencyPayloadMismatchError instead of silently deduping under the wrong scope.

  • Apply-evidence honors the idempotency ledger verdict. appendApplyEvidence now acts on rememberKey'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-source cleanup stays auditable. A removeFile failure mid-cleanup now still writes the source_invalidation audit record for the paths already removed before rethrowing, so a retry can reconstruct the state.

  • CLI error contracts. brain batch-plan with no <source-dir> now exits 2 as a usage error, and brain forget-source --json emits 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_pack on (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_enabled on (default off), every context pack posts one token_impact sample carrying the tokenizer-exact prompt-token delta (baseline_tokens − packed_tokens, labelled method: exact or fallback) plus an optional modeled inference-avoidance estimate. brain_token_impact record posts a sample, outcome posts a first_pass / repair / retry result used only to calibrate the modeled figure, and summary keeps the two ledgers strictly separate: prompt_token_delta reports the measured net, saved, and added tokens with a per-method split, while modeled_inference_avoidance reports the raw modeled figure and a calibrated_savings_tokens value dampened by the observed first-pass rate (null until an outcome exists, uncalibrated not zero). list reads raw samples. Writes are payload-safe (counts and an opaque pack_id only); 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_enabled on (default off), brain_context_pack_outcome post records 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. list and summary read the rows keeping the signals separate. A field the caller omits is never invented; writes are payload-safe (counters and an opaque sample_id only); reads ignore the gate.

  • Route-level MCP tool latency metrics. With mcp_route_metrics_enabled on (default off), the MCP server emits one mcp_route_latency continuity 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_metrics list returns raw records and summary rolls 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. openReadOrSelfHeal now rebuilds on INDEX_UNREADABLE alongside the existing INDEX_MISSING and SCHEMA_MISMATCH cases, so a corrupted but present index self-heals instead of surfacing a raw read error.

Changed
  • basename column and index in the search store. Resolved-wikilink discovery is now an equality join over a populated documents.basename column instead of a documents SUBSTR scan per link, removing a scan that grew with vault age. The column requires a LATEST_SCHEMA_VERSION bump, 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 short rewritten 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_at validation tightened. Continuity and query-demand records now require a full canonical UTC timestamp; a YYYY-MM-prefix timestamp that was previously accepted now fails with a structured INVALID_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 cards disclosure surface.

  • Second-precision since / until filtering. 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 command field 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 init fails on native Windows -- resolve() produces backslash paths that setConfigValue() rejects via CONFIG_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/o2b is a bash script that subprocess.Popen cannot execute directly (Windows delegates to cmd.exe, not a POSIX shell). Added _resolve_command() which detects the platform and falls back to bun run <entry> mcp when o2b is 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 honours PATHEXT (.exe, .cmd, .bat, .com) instead of hardcoding .exe.

  • MCP inputSchema not recognized by Hermes adapters (all platforms) -- Hermes adapters (Anthropic, OpenAI, Bedrock) expect tool schemas under parameters, but MCP serves them as inputSchema. Without this remap brain tools had no visible arguments on any OS. Both live and static fallback schemas now remap inputSchema -> parameters at 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_enabled set, brain_doctor (and the doctor lint 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 deterministic entityIdentityKey is never rewritten and no entity is ever auto-merged; a human or apply plan owns the actual merge, preserving audit friendliness. Optional identity_type tagging 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 /rerank endpoint (search_rerank_base_url / search_rerank_model / search_rerank_env_key, or a profile registered via o2b search rerank-provider add) and re-orders the search_rerank_top_k candidates 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 (a rerank_degraded: warning, never a throw). Most valuable on the thorough profile.

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 structural epistemic status (observed | derived | hypothesis | plan | unknown) plus evidence_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 is unknown, 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, or insufficient and emits a recommended action (proceed, re_recall, abstain) with an optional escalate flag, reusing the existing gate-telemetry relevance scores plus the new epistemic mix. The verdict appears in brain_recall_gate output 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.jsonl records 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 the brain_knowledge_gaps MCP tool and o2b brain knowledge-gaps CLI. 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.md digest folds positive knowledge (preferences and their applied evidence) and negative knowledge (violated / outdated rows and Brain/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), or avoid (dead-ends and net-negative). The digest is regenerated at the tail of every dream pass alongside active.md, exposed as the osb://lessons MCP resource, and injected on SessionStart and PostCompact. Tunable via lessons.limit, lessons.half_life_days, and lessons.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 health check 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-paths script 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 deterministic unnamed-<hash> slug instead of colliding into a single shared unnamed note, 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). Points list_skills / get_skill / skills_attach at an arbitrary directory (e.g. an external ~/.hermes/skills/) instead of vault-local Brain/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's triggers frontmatter 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: triggers is 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-vault search() puts cards on outcome.cards and leaves outcome.results empty, but searchAcrossVaults merged only results, 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 on outcome.cards with results empty, mirroring single-vault semantics. The normalized-confidence chain-stop now gates on the top card score when cards mode leaves results empty.

  • A genuine event-trace IO error is a runtime failure, not a usage error (t_27ea0daa). o2b brain event-trace mapped any throw from the resolver to a usage error (exit 2). A real IO failure from the log-shard reader (an existing-but-unreadable Brain/log: EACCES / EIO / ENOTDIR) is now exit 1, while a bad --date / --at / --kind selector stays exit 2. The MCP twin brain_event_trace likewise maps a runtime IO error to INTERNAL_ERROR and a bad selector to INVALID_PARAMS, via a shared EventTraceSelectorError tag thrown only by the pre-IO selector checks.

  • Registry-guard exempt membership is own-key only (t_6fbdba4b). auditPreviewBudgets tested exemption with name in PREVIEW_BUDGET_EXEMPT, which walks the prototype chain - so a tool named like an Object.prototype member (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-call Object.keys as 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. detectCompaction scans 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:summaryHash dedupe key on a new post_compact_audit continuity 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 via appendPinnedContext, so survivors cost zero tokens. Fail-open throughout (every recoverable error lands in errors, never thrown), with a bounded drift log. Surfaced as the o2b brain post-compact-audit CLI verb and gated by post_compact_survival_audit (default OFF, env/config), with --force for 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 fires on_session_end(interrupted=True). Previously Open Second Brain swallowed the interrupted flag into _kwargs and these sessions left no memory trace. session-lifecycle.ts now threads an absent-by-default interrupted flag through NormalizedPayload; on an interrupted SessionEnd it 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 (signal dedup_hash and the fact dedup index). interrupted and transcript_consumed are recorded honestly in the audit record/result; an unreadable transcript surfaces transcript_consumed: false rather than coercing to a clean close. The Hermes provider.py surfaces the interrupted kwarg from on_session_end onto the flush payload (absent by default, so a clean close is byte-identical) and makes no capture decision. The pre-compact-extract path and the brain_pre_compact_extract MCP schema accept an optional interrupted flag 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 on results[0].score, assuming a score-descending order, but rerank and 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 via search_chunk_min_size / OPEN_SECOND_BRAIN_SEARCH_CHUNK_MIN_SIZE, threaded through chunkMarkdown and the indexer alongside the existing search_chunk_size and search_chunk_overlap knobs. Default 100 is 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: the o2b brain event-trace CLI verb and the brain_event_trace MCP tool. Read-only; joins existing records and writes nothing.

  • Read-time line-span citations (t_0b83c97b). A shared LinePointer type and line-numbering.ts helpers resolve recalled content to exact path:Lstart-Lend spans 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 via disclosure: "cards", so callers can pay layer-1 token cost up front and drill deeper only when needed. The default full path stays byte-identical.

  • Normalized-confidence cross-vault chain-stop (t_23c1b929). Opt-in via search_chain_stop_enabled / OPEN_SECOND_BRAIN_SEARCH_CHAIN_STOP: searchAcrossVaults stops querying further origins once a completed origin's top NORMALIZED [0,1] result score reaches search_chain_stop_score (default 0.8), recording the skipped origins on the new SearchOutcome.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 below COMPLETENESS_COMPLETE_THRESHOLD with 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 helper planTargetedRetry(coverage) (in coverage.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 carries kind: "broadened" | "targeted" to distinguish the two triggers, plus targetedTerms (the uncovered rare terms re-queried) for the targeted variant. Recovered results carry a second_pass: targeted retry on uncovered rare terms reason; 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 existing markdown_link row 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 #anchor stripping 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 an o2b-mcp bin that mirrors the existing o2b launcher and injects the mcp subcommand, so o2b-mcp --vault X is equivalent to o2b 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 mcp accepts --transport stdio|http (default stdio) plus --host, --port, and --api-key for 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 same 401 Unauthorized response, rejects JSON-RPC batches per the 2025-06-18 MCP contract, supports JSON or single-event SSE responses based on Accept, and dispatches every accepted request through the existing MCPServer.handleRequest core.

  • Explicit offline/deferred backend resolution for indexing (t_85252236). The structured IndexStats now declares which backend processed a run: backend: "offline" when only the deterministic lexical pipeline ran and no provider credentials were resolved, or backend: "semantic" when the embedding backend was actually engaged. An accompanying deferredReason string explains why the semantic backend was not used (embeddings not requested, semantic search disabled, or embedding_api_key not 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: importSession is verified to never read provider-credential environment variables, and keyless indexing is verified to report offline. The new fields are additive; existing IndexStats fields 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_context accepts an optional operations array of ordered write / append / clear / replace steps; the whole batch is projected and validated in memory first, and a malformed operation, an absent replace target, or an over-budget final state throws PinnedBatchError before any write, leaving Brain/pinned.md byte-for-byte unchanged. Successful writes return a terminal done marker so an agent does not redundantly re-call. The continuity store gains appendContinuityRecords, 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 no operations array is passed.

  • Hermes on_memory_write host bridge. The provider declared the on_memory_write memory-provider hook but only mirrored writes to brain_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.py forwards verified (action, target, content, metadata) writes to the new brain_memory_bridge MCP 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 a host_memory_write continuity record via appendContinuityRecord(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's MEMORY_TOOLS allowlist, so the Hermes agent surface is unchanged and only the hook calls it.

Fixed
  • Pinned-context budget is honest. Single-operation write / append on brain_pinned_context previously normalized pinned content through sanitiseTextField's maxLen cap, silently dropping the tail and returning done: true: a deceptive truncated-success the project brief forbids. writePinnedContext / appendPinnedContext now enforce the budget explicitly through a shared assertWithinPinnedBudget that throws PinnedBatchError (code: budget_exceeded) before any write, carrying operation, length, budget, over_by, and a consolidate-then-retry hint; the MCP layer maps it to a structured INVALID_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 under Brain/obligations/<slug>.md tracking a periodic commitment (weekly review, monthly report, quarterly audit) with a cadence and a deterministically computed next_due date. add creates the page (next-due starts at the anchor), done records 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, show reads one, remove archives into Brain/obligations/archive/. Cadences: daily, weekly, biweekly, monthly, quarterly, yearly, and every-<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-end window), 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_agenda and brain_obligation to 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/, and references/ pages plus a date-grouped log.md and an okf.json manifest. Read-only on the vault; --force overwrites a non-empty target directory.

    • Import (brain okf-import, o2b brain okf-import). Reads an OKF bundle directory. By default pages land under OKF Review/ stamped okf_review: pending as review candidates; --trusted writes 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 .base files are now written into Brain/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 with category: project (Brain/entities/project/), with Active / Archived / All table views, a status icon, and a "stale since update" formula.

    • people.base → canonical entities with category: person (Brain/entities/person/), surfacing optional operator-added role / company columns alongside status and freshness.

    • tasks.base → recurring obligations (Brain/obligations/), ordered by an overdue formula and next_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 under src/core/brain/templates/bases/ so they travel with the published src/ tree.

  • Strongest-objection steelman in synthesis outputs. Both the osb://topic/{slug} resource and brain_deep_synthesis now 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 (and o2b brain deep-synthesis) add a strongest_objection field with basis (contradiction → superseded → stale → knowledge_gap → thin_evidence), statement, and source_artifacts. It is null only for a larger internally-consistent body, and strongest_objection joins the dossier's checked dimensions.

    • 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 to brain_feedback / o2b brain feedback writes that pass no explicit scope, so agent-recorded signals can land in a consistent category (for example coding) 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 the scope field is omitted exactly as before. With no feedback.default_scope configured and no explicit scope, signal output is byte-identical to prior behaviour.

    • Config block. Optional feedback: block in Brain/_brain.yaml with a default_scope string, validated through the normal Brain config policy against the same constraints as a signal scope field (non-empty after trim, single-line, at most 128 characters). Invalid values are rejected by config validation and surfaced by o2b brain doctor rather 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_facts and 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 ensureInsideVault before 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 a Brain/<symlink>/note.md whose 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 guarded paths.ts constructors or ensureInsideVault, and validateSlug continues 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 a generation_report continuity record.

    • Inbound report surface (brain_generation_reports action record, o2b brain generation-reports record). Gated (default off) by a per-call enable flag or the generation_trace_enabled config / OPEN_SECOND_BRAIN_GENERATION_TRACE_ENABLED env. 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) and prompt_chars, plus token counts, reach disk, and the whole payload still passes safeContinuityPayload redaction. The local token estimate (local_estimate.input_tokens) is always present; the agent-reported usage block is present only when supplied and absent is reported as absent, never fabricated.

    • Memory to trace linkage. sourceRefs join 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-class handoffKind / handoffRef read-model fields. The summary read (brain_generation_reports action summary, 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|show and the matching brain_generation_reports actions (list and summary; show is CLI-only). The kernel never adds an outbound fetch/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 in docs/observability.md, docs/cli-reference.md, and docs/mcp.md.

  • Structural prompt-prefix stability (prompt_prefix metric). 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). deterministicPrefix certifies a stable preamble (prefix, full SHA-256 hash, code-point chars) from stable inputs only - no clock, no random, sorted keys via canonicalSegment. The decision- panel builder routes its shared Decision topic: frame through it; the bytes of the persona and synthesis prompts are unchanged.

    • Run-level prompt_prefix metric. A decision-panel commit (opt-in promptPrefixMetric) and a context-pack consume (opt-in promptPrefix) emit one record under Brain/metrics/prompt_prefix.jsonl carrying kind, prefix_hash, prefix_chars, call_count, and stable_count. The reader is surface-agnostic (no metrics.ts change), exactly like dream_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.md and docs/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, indexed with node/file/edge counts, or error), and structurally parses Cargo.toml for 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 returns cargo_workspace: null with 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 a crate_depends_on edge type to the Open Second Brain graph schema.

    • Batched community materialization (o2b brain clusters run --batch-size N, brain_clusters batch_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 a batches array, each entry carrying index, start, end, written, removed, and an optional error; 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_size the run is byte-identical to before, and invalid sizes are rejected with the usual usage/INVALID_PARAMS errors.

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 the session_summary_node recall rollup and the per-line pre_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 the sourceRefs graph (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 existing buildBeliefEvolution lifecycle 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 reports available: false (no fabricated phase); a path with no commits reports available: true with zero phases (empty is distinct from broken). The sanitized read-only git reader gains an additive path pathspec 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_search profile field). Named presets fast | balanced | thorough expand 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 same applyTunedParameters machinery, 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 typed SearchError. 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. decayWeight combines record age (exponential half-life) with a usage signal derived only from existing recall_telemetry surfaced-artifact references; a record whose sources were never surfaced - such as a session-scoped pre_compact_extract decision - has no usage and decays by age alone, with no fabricated coupling. rank lists 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. --write persists Brain/link-graph/co-occurrence.json.

    • File-context recall (o2b brain file-context, brain_file_context MCP 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). exportBankBundle composes 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. importBankBundle reconstructs the part that round-trips (the page graph, delegated to importVaultGraph under 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 typed BankImportError.

    • Page interchange contract (projectPageContracts). A pure, read-only projection of every user vault page to a stable, schema-versioned record (path, kind, advisory confidence/provenance, flattened citations, 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 as null, never synthesised.

    • brain_create_note MCP tool. Writes an actual vault note file (path + frontmatter + content) atomically through ensureInsideVault - distinct from brain_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, and createNote - plus source-backed reads (listSources/getSource/deleteSource) over the kind: brain-source summary pages. Every method is a one-line delegation; the upstream writeStatus maps to ingestSource (Open Second Brain has no separate source status lifecycle). A source id that resolves outside Brain/sources is 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 Safeguard and 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 optional AbortSignal: one checkpoint() now trips on either an aborted signal (a new SafeguardAbortError, checked first) or the existing timeout. The signal threads through indexVault and populateEmbeddings, 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 watch shutdown. SIGINT/SIGTERM now stops accepting new flushes, aborts the in-flight pass, and awaits it to settle at a cooperative boundary before exiting - bounded by search_shutdown_grace_seconds (default 5). A signal no longer kills a pass mid-write. The flush/shutdown coordination lives in a testable IndexWatchRunner; a second signal falls back to the default terminate. 0 grace exits immediately after signalling.

    • Opt-in resumable reindex (search_resume_reindex). An interrupted full reindexVault rebuild no longer discards all progress: a compatible in-progress brain.sqlite.new staging 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's index_state KV - 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-dbPath writer lock. Two instances on different vaults run conflict-free; a second writer on the same vault gets a typed INDEX_LOCKED. Pinned by tests; no --port/--instance model 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 a deduced/inferred provenance level and premise links. Opt-in behind the derived_fact_synthesis guardrail. Recall trust ordering (provenance_trust_ordering guardrail) ranks stated > deduced > inferred in the active digest.

    • Owner-scoped canonical facts. A preference may declare an owner: token; brain_query gains an agent_scope argument (gated by the owner_scoped_facts guardrail) 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_query action: 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 watch watches the vault for .md edits and incrementally re-indexes after a debounced quiet window, reusing the existing incremental indexVault fastpath. A single-flight guard prevents overlapping passes; recursive fs.watch unavailability 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 clusters graph summary stop rebuilding the adjacency on every call. Byte-identical community output; in-memory only, never persisted.

    • Agent-scoped recall isolation. brain_search accepts an agent_scope input: a page that declares an owner: 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_search enhancements 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, and brain_eval is a new read-only tool.

    • Structured score breakdown. brain_search accepts explain: true and returns a per-result score_breakdown object (keyword, semantic, rrf, entity, activation, coAccess, link, recency, tier, trend, sessionFocus) alongside the existing human-readable reasons[]. 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_search accepts trust: true and stamps each hit with a computed-at-read-time trust object: age_days from the document mtime, plus superseded and conflict derived from the superseded_by / contradicts typed 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; rerank re-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 under Brain/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 answer and reports answerContainmentAtK - 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 without answer keep hit@k / MRR behaviour unchanged.

    • brain_eval MCP 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 --apply registers the two Open Second Brain MCP servers in ${GROK_HOME:-~/.grok}/config.toml under [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 absolute bun 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 bare o2b command 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 the grok runtime, and counts grok's search_replace as a file-mutating tool. A new grok session adapter imports grok's ACP updates.jsonl stream into the Brain via o2b brain import-session (autodetected). The integration uses grok's own config, not the ~/.claude/ namespace. See install/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 operator agent_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>-agent shape 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, so apply and verify produce the identical value and o2b install --check reports no drift after an apply. For grok, both the config.toml MCP 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_call forwarded the raw MCP tools/call result 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_content seam (lossless JSON, default=str, string results passed through), and handle_tool_call is typed -> str to 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/ by o2b install --target opencode --apply. On each chat request it appends the rendered Brain/active.md digest to the system prompt (via the same o2b-hook active-inject shim the Claude Code and Codex hook layers use, cached with a 5 minute TTL); on session.idle / session.compacted / session.deleted it 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, originator open-second-brain-opencode-plugin) through the same o2b brain import-session flow 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: createJsonMcpAdapter accepts serializeEntry / 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 optional parent_session_id / root_session_id / compression_depth hook-payload fields (ready for upstream Hermes PR #42940); until the deployed host emits them, a conservative interim inference - isolated in one file and marked CRUTCH(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_describe resolve 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

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

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.ts
  • CLAUDE.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-mirrors
  • CLAUDE.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-vec
  • plugins/codex/skills/embeddings-setup/SKILL.md: bun install --force
  • plugins/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-vec
  • skills/embeddings-setup/SKILL.md: bun install --force
  • 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:
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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 0
  • 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
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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 0
  • plugins/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
GitHub avatar of hermes-vps-agenthermes-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