[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M78CDGADXPR9WBQ05C10EJSW":3,"skill-same:01M78CDGADXPR9WBQ05C10EJSW":344},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":17,"publishedAt":18,"linkOnly":19,"markdownContent":20,"markdownBody":21,"frontMatter":22,"refName":23,"commitSha":24,"skillDirectory":25,"forksCount":26,"repositoryHtmlUrl":27,"repositoryHomepageUrl":6,"ownerHtmlUrl":28,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":29,"install":30,"resources":43},"01M78CDGADXPR9WBQ05C10EJSW","memory",null,"ZMem — ZCode multi-tier memory system. Provides cross-session recall, capture, and management of lessons, facts, conventions, and preferences. Use when you need to recall prior knowledge before a non-trivial task, capture a lesson at task end, or manage the memory store. Tier 0 (core.md\u002FAGENTS.md) is auto-injected by the SessionStart hook; this skill operates Tier 2 (semantic store).","zaxbysauce\u002Fzmem","zaxbysauce","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F10211642?v=4","01M78CD0J751DPXGF3YVNH6VCH","01M78CD0JZPBW7BJHV1MST3CTZ",0,"MIT","https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002FSKILL.md",[],[],"2026-11-15 14:19:17",false,"---\nname: memory\ndescription: >\n  ZMem — ZCode multi-tier memory system. Provides cross-session recall, capture,\n  and management of lessons, facts, conventions, and preferences. Use when you\n  need to recall prior knowledge before a non-trivial task, capture a lesson at\n  task end, or manage the memory store. Tier 0 (core.md\u002FAGENTS.md) is auto-injected\n  by the SessionStart hook; this skill operates Tier 2 (semantic store).\n---\n\n# Memory (ZMem)\n\nZCode's memory system has three tiers:\n- **Tier 0 — Core:** `core.md` (user-level, in the canonical shared store dir)\n  + `\u003Crepo>\u002FAGENTS.md` (project-level on ZCode). Auto-injected every session by\n  the SessionStart hook. Edit `core.md` directly for stable rules\u002Fpreferences.\n  Keep \u003C2KB.\n- **Tier 2 — Semantic:** `store.sqlite` (in the plugin data dir). Cross-task lessons,\n  facts, conventions, preferences. Operated by this skill via `scripts\u002Fstore.py`.\n- **Tier 4 — Procedural:** the skills library. (Extended later with evals + index.)\n\n## Finding store.py\nThe SessionStart hook injects a ready-to-run store command into context each\nsession (look for the `# Memory skill: invoke ...` note). Run the command it\nshows as-is: it names the shell-quoted interpreter the hook itself resolved\nahead of the absolute `store.py` path, so it cannot hit a stub `python` on\nWindows. The `store.py` path is the second quoted token in that command.\nOn Windows it will be a\nWindows-format path like\n`C:\\Users\\...\\plugins\\data\\zmem@...\\skills\\memory\\scripts\\store.py`\n(the `...` segments are elision placeholders, not real paths).\nIf you cannot find the injected path, the script is at the plugin root under\n`skills\u002Fmemory\u002Fscripts\u002Fstore.py` — invoke it through an explicitly\nresolved interpreter (`py -3` on Windows, `python3` on macOS\u002FLinux), never\nas a bare path and never bare `python3` on Windows (Store stub).\n\n## Local vs remote mode (issue #289)\nWith `ZMEM_REMOTE_URL` set, this machine's memory store lives on a hub and\nevery `store.py` subcommand routes through the fleet backend — no local\n`store.sqlite` is ever created or read:\n\n- **Writes** (`add`, `update`, `supersede`, `invalidate`) enqueue a durable\n  op and print a receipt line `{\"ok\": true, \"op_id\": ..., \"status\":\n  \"pending\", ...}` — the op flushes to the hub on the next read moment.\n  The routed write accepts `--namespace --type --content --tags --signal\n  --source-ref --capture-mode` and, since 0.129.0 (issue #326), the expiry\n  pair `--expiry-class --inject-until` on `add` and `update` (a state add\n  requires `--expiry-class`; an `update` may omit `--type` and the target's\n  type resolves when the hub applies the op — issue #317). Occurrence\n  flags (`--occurred-*`, `--mentioned-at`) and `--type event` still exit 2\n  naming the flag (occurrence semantics do not travel); so do\n  `--confidence --taint --evidence --no-dedup --reason`, an update that\n  names a non-state type while carrying expiry flags, and a state add\n  without `--expiry-class`. `--content -` still reads stdin.\n- **Reads** (`recall`, `search`, `recent`, `list`, `get`) always print the\n  JSON envelope (with or without `--json`) plus a `[PENDING]` trailer for\n  ops queued but not yet confirmed by the hub. Reads accept only the\n  query flag, `--namespace`, `--limit` (and `--json`); search's query\n  flag is `--text`. On failure they print `{\"ok\": false, \"reason\":\n  \"hub-unreachable\"}` (or `hub-response-invalid`) and exit 1.\n- **Store-independent commands keep running locally**: `failures`,\n  `signals`, `corrections`, `queue-*`, `source-exists`,\n  `source-ref-exists`, `ops-append`, `retain`, `retain-adapt`,\n  `ledger-clear`, `delivery-clear`, `hermes-reflect`, `hermes-context\n  --action commit-cursor`.\n- **Everything else** (maintenance and admin: `consolidate`, `organize`,\n  `reembed`, `purge`, `export-*`, `init`, `backup`, `evidence`,\n  `apply-ops`, …) exits 2 with `this command runs on the hub;\n  ZMEM_REMOTE_URL is set` — run it on the hub machine.\n- Config rule: `ZMEM_STORE` together with `ZMEM_REMOTE_URL` is a\n  configuration error (exit 2). `ZMEM_DATA` alone is fine — it names the\n  local data dir (op queue, sidecars).\n\n**Offline degrade (issue #290):** when the hub is unreachable, reads fall\nback to the machine's broker snapshot — a read-only, principal-scoped\ncopy under `\u003Cdata>\u002Fbroker\u002Fsnapshot.sqlite` that expires 72 h after issue\nand is refreshed hourly and on every hub reconnect by the broker (never\nby hooks). A served answer carries the label\n`[OFFLINE SNAPSHOT r\u003Crev>, \u003Cage> old]` (broker-emitted fence header on\nthe Inject path; an additive `offline_snapshot` provenance key on CLI\nreads, with a stderr note — stdout stays JSON-parseable). An expired or\nmissing snapshot keeps the plain `hub-unreachable` error on the CLI; the\nInject path logs `snapshot-expired` instead. A row served offline is\nadvisory and may be re-delivered live after reconnect; the snapshot never\nmerges with live data — a reconnect supersedes it.\n\nWith `ZMEM_REMOTE_URL` unset, every command is local and unchanged.\n\n## When to use\n- **Before a non-trivial task:** `recall` relevant past lessons. High-precision-first:\n  if a retrieved lesson does not clearly apply, ignore it.\n- **After a failure with a generalizable lesson:** `add` a lesson grounded in an\n  external signal (test\u002Fcompile\u002Flint\u002Freviewer\u002Fuser). The reflection Stop hook will\n  prompt you automatically when failures are detected.\n- **When you learn a stable fact or convention:** `add` it.\n- **When a memory is stale\u002Fwrong:** `supersede` it for a general tombstone; use the\n  dedicated **`invalidate`** command when a fact is *no longer true* (it REQUIRES a\n  reason so the correction is auditable); use **`update`** to revise a memory\n  append-only (the old row is tombstoned, a new live row links back via\n  `update_of`, and point-in-time `--as-of` recall still sees the old content).\n\n## Commands\n\nStore commands run `\u003Cinterpreter> \u003Cstore.py path> \u003Csubcommand>` — use the\ninterpreter the injected command names, or an explicitly resolved one\n(`py -3` on Windows). Never bare `python3` on Windows — that is a Windows\nStore stub — and never a bare `store.py` path (pin the interpreter explicitly; a bare .py path depends on platform dispatch).\n`doctor` is the one separate diagnostic script shown below.\n\n### doctor — read-only install diagnostics\n```\npython \u003Cdoctor.py path> [--project \u003Crepo>] [--repo-root \u003Czmem-repo>] [--format human|json|both]\n```\nRead-only preflight for cutover and operator debugging. It never mutates the\nstore or host config. Checks:\n- resolved store path and split-brain env\u002Fconfig risk\n- local\u002Fnon-OneDrive store path safety\n- Python version (supported floor 3.11) + SQLite FTS5\n- Node and a usable Git Bash\u002FCygwin shell on Windows\n- best-effort read\u002Fwrite access to the store path\n- schema compatibility against current v19\n- v9 append-only lineage columns present (`valid_until`\u002F`update_of`\u002F`taint`)\n- v10 entity identity tables present and non-vacuous (`entity`\u002F`entity_alias`\u002F\n  `memory_entity`); inspect deeper with `store.py entity-list`\n- v11 link surface present (`memory_link` table + `memory.trust_score` in\n  range [0,1]); inspect deeper with `store.py links --id \u003Cuuid>`\n- v13 episode storage present with counts (`episode-tables` check), and the\n  MCP token scope advisory (`mcp-token` check: warns `unscoped_token: true`\n  on full-access operator tokens; in principal-map mode (`ZMEM_MCP_TOKENS_FILE`)\n  reports scoped\u002Funscoped\u002Frevoked counts instead — never a token value)\n- Claude\u002FCodex native-memory conflicts via read-only config inspection\n- ZCode native memory (`zcode-native-memory` check, issue #185):\n  `~\u002F.zcode\u002Fv2\u002Fsetting.json` `memoryEnabled: true` fails cutover, explicit\n  `false` passes; unreadable\u002Fmissing setting warns — never auto-edited\n- host install-skew (issue #185): `duplicate-install` (fail) when more than\n  one enabled user-scope zmem install is registered for a host;\n  `marketplace-skew` (warn) when an installed cache version differs from the\n  marketplace version its registry entry points at; `project-pin` (warn)\n  when a project-scoped zmem pin is behind the enabled user-scope install.\n  Registries are read through the #184 strict codecs; missing registries\n  skip and malformed ones warn — doctor never edits host state\n- Codex manifest hook trust (`untrusted-hook` check, issue #185): compares\n  the pre-approval events the repo manifest registers (SessionStart,\n  PreToolUse) with the events the Codex config records trusted for the repo\n  (falling back to the box-wide union when no entry names this repo — on a\n  multi-repo box another repo's approval can stand in, so treat a pass as\n  inventory, not proof). Missing registered events warn\n  `untrusted-hook \u003Cids>`; reapproval is always manual\n- orphan-store inventory (issue #185): warns with `schema=`\u002F`rows=` for\n  every non-canonical SQLite store on the known host paths (plugin-data env\n  dirs, `~\u002F.zcode\u002Fmemory\u002Fstore.sqlite`); inspect then merge with\n  `promote-store --from \u003Cpath>` — doctor never deletes or migrates\n- canonical namespace for the provided project\n- host surface presence (Claude plugin, Codex plugin, ZCode plugin, memory\n  skill — all four are REQUIRED surfaces; a missing Codex plugin file fails\n  `host-surfaces`, and the repo-local Codex adapter lane never shipped)\n- served-tree drift vs `release-manifest.json` (`served-drift` check, issue\n  #107): `matched`\u002F`drifted` (warn — first 10 differing paths + both digests)\u002F\n  `unknown`-skip when the tree predates 0.17.0 and carries no manifest, and\n  `unknown`-WARN when a manifest is present but corrupt (failed its\n  algorithm\u002Fdigest integrity gate — the served mirror itself is damaged).\n  Log-only;\n  a drifted session start also appends one `zmem-drift` line to `zmem-bg.log`\n  and shows the operator a systemMessage — never model context.\n\nUse it before first install, before cutover, and after any store-path or hook\nsurface change.\n\n#### doctor `--miss-rate` — the miss-rate join + false-injection counter (issues #94, #129)\n\n```\npython \u003Cdoctor.py> --miss-rate --store \u003Csnapshot-store.sqlite> [--miss-db PATH]\n                   [--miss-transcripts GLOB ...] [--miss-bg-log PATH]\n                   [--miss-window-before 1800] [--miss-window-after 300]\n                   [--miss-limit 200] [--miss-min-overlap 2] [--miss-verbose]\n                   [--format json]\n```\nOpt-in check that measures the miss rate — \"a failure occurred in a session,\na matching memory existed in the store, and nothing surfaced at that moment\".\nIt joins mined tool failures (ZCode episodic db via `--miss-db`, transcript\nJSONL globs via `--miss-transcripts`) against a snapshot of the store\n(recall with telemetry fully disabled, `link_hops=0`) and the bg log's\ninjection decision lines — BOTH shapes: writer A's `reason=injected` and\nthe session-start writer's `status=injected` lines (that writer has never\ncarried `reason=`). Definitions (pinned):\n- `missed` — failure + floor-passing store match + no injection of a matched\n  row in the window ⇒ counts toward the miss rate.\n- `capture-gap` — failure + NO store match (write-side problem, counted\n  separately).\n- `surfaced (sid)` — an injected line whose `sid=` proves it is this\n  session's decision carries a matched row id.\n- `surfaced (legacy)` — same evidence on a pre-#94 sid-less line or a\n  `sid=unknown` line (window+overlap attribution only;\n  `miss_rate_strict_sid` excludes it).\n- `no-query` — nothing derivable for the failure (no recovered operation and\n  no ops ring): unmeasurable, excluded from every rate. On boxes where the\n  ops-ring lane is not yet deployed this bucket can dominate until rings\n  accumulate.\n\nIssue #129 adds the OTHER gate direction to the same report: the\n**false-injection rate** — of the injected decision lines, how many were\nNEVER referenced by any later same-session operation, prompt, or captured\nfailure. A line counts as USED when a later reference (mined failure\noperation\u002Ferror text, the session's ops-ring events, or transcript\nprompts) contains one of its memory ids literally, or shares >=\n`--miss-min-overlap` (default 2) distinct ops tokens with the injected\nrow's content. Rates print overall AND per moment (`session_start`,\n`user_prompt`, `pretool`, `subagent`, `precompact`; pre-#129 lines\nwithout `moment=` report under `legacy` with a caveat). Each decision\nLINE counts once — the same row re-injected at two moments is two\ndenominator lines, never double-counted.\n\nDecision telemetry lives in `\u003Cdata dir>\u002Fzmem-decisions.log` (issue #129\nsplit): maintenance output and `zmem-drift` lines stay in `zmem-bg.log`.\nBoth logs rotate instead of truncating — `ZMEM_BG_LOG_MAX_BYTES` (default\n262144) caps the active file and `ZMEM_LOG_ROTATIONS` (default 3) keeps\nbounded segments named `\u003Cname>.1`..`\u003Cname>.N`, each stamped with a\n`# zmem-seq=` marker; the join and the counter read rotated segments.\nLegacy deployments with only `zmem-bg.log` keep working: readers fall\nback to it when no decisions log exists, and an explicit `--miss-bg-log`\nwins over both.\n\nREFUSES to run without an explicit `--store`, and refuses the\nhost-default store even when given explicitly — the join reads session\ndata, so snapshot first: copy `store.sqlite` AND any\n`store.sqlite-wal`\u002F`-shm` beside it into a temp dir (plus\n`zmem-decisions.log` and its rotated `.1`..`.N` segments, `zmem-bg.log`,\nand the `ops\u002F` ring dir when present), then pass that path. exit 1 from\n`--miss-rate` most often means exactly that:\nsnapshot the store and re-run with `--store`; exit 1 from\nother checks means a real diagnostic failed.\nRead-only everywhere: the\nstore\u002Fdb open `mode=ro`, the report never writes, and a missing\u002Fold store is\nan error (never created, never migrated). Memory content stays out of the\ndefault output (ids\u002Fnamespaces only; `--miss-verbose` adds a short preview).\n\n### page — read, list, and refresh curated pages (issue #138)\n\n```\n\u003Cinterpreter> \u003Cstore.py> page read --id \u003Cpage-id> [--version \u003Cversion-id>]\n\u003Cinterpreter> \u003Cstore.py> page list [--namespace \u003Cnamespace>]\n\u003Cinterpreter> \u003Cstore.py> page refresh --id \u003Cpage-id> --query \"\u003Csource query>\" \\\n  --namespace \u003Cnamespace> [--tag \u003Ctag>]... [--llm-local]\n```\n\nCurated pages are local filesystem artifacts grounded in canonical memory.\n`read` and `list` are SQLite-independent and never create, migrate, or open a\nstore. `refresh` requires an existing store, uses the normal read-only and\nmaintenance coordination, and writes no canonical memory rows. The namespace,\nresolved data root, page ID, and regular contained page paths are validated\nbefore source selection. New candidates without evidence are excluded before\npublication; if a previously published live source loses its evidence during\nrevalidation, refresh refuses and preserves the committed pointer.\n\n`--tag` is repeatable. Each tag is one literal over stored comma-separated\ntokens: only delimiter-edge whitespace is trimmed, while internal spaces and\ncase are exact. A comma in a requested tag is literal input, not a wildcard,\nand stored commas have no escaping extension. A blank or whitespace-only query\nkeeps the existing recent-query selector behavior; a nonblank query that\nnormalizes to zero terms has zero lexical coverage. The refresh snapshot and\nits source\u002Fevidence associations are revalidated before publication, so a\nchanged source causes a refusal and leaves the prior committed pointer intact.\n\nThe optional `--llm-local` adapter is enabled only by setting\n`ZMEM_PAGE_ADAPTER_ACTIONS` to a non-empty local JSON action file. It is a\nbounded data-only input for offline maintenance; it cannot run commands, load\ncode, access the network, or name files. The page CLI does not populate or drop\na cache as part of these operations. `ZMEM_PAGE_ADAPTER_ACTIONS` must be a\nnonblank path to an existing file; a missing or unusable configured path is\nrefused before the store is opened. An existing recording is opened and\nvalidated during refresh, so unreadable bytes, malformed JSON, and over-limit\nactions follow the normal atomic refresh refusal. An adapter failure leaves\ncanonical memory and the last committed page unchanged.\n\nPassive page verification uses a finite cost policy. Metadata discovery may\nscale with the page directory, but expensive source\u002Fevidence verification is\nlimited to the 50 namespace-eligible committed pages with the greatest\ncanonical source snapshot watermark (maximum source `ingestion_ts` plus\nsnapshot hash), ordered descending and then by page ID ascending. The eligible\nset is the requested namespace plus `user:global`, and both share this one\n50-page cap; a newer global page can therefore displace a page from the\nrequested namespace. A 51st eligible page is deterministically withheld. It\nre-enters only when a later refresh advances its canonical source snapshot\nwatermark sufficiently; refreshing an unchanged snapshot preserves its\nwatermark and may leave it withheld. Foreign namespaces remain outside the\neligible set. This is a selection bound, not a universal filesystem latency\nguarantee.\n\nPublication stages into a nonce directory and replaces `current.json` last.\nDefault and passive readers use only the valid pointer, so interrupted staging\nor an unreferenced version is residue for operator maintenance. An explicit\n`page read --version` selects and validates the requested version file directly;\nit can read a valid unreferenced version left by interrupted publication and\ndoes not prove that the version is the committed current pointer. There is no\nautomatic garbage collector. First-publication recovery takes a fresh\nauthority snapshot and preserves immutable history. Cleanup removes a whole\ninspected artifact and retries the operation; it never deletes an individual\nhistorical version as a repair. Directory fsync is used where supported, with\nexplicit Windows directory durability limits and no universal crash-atomicity\nclaim.\n\nVersion-2 pages privately bind each represented source to its UTF-8 content\nhash. The public shapes remain stable: `page_read` exposes the existing version\nkeys plus `content` and `namespace` (and optional `bullet_sources`), while\n`page_list` and `page_for_injection` retain their existing key sets. The\nprivate `format_version` and `source_content_hashes` fields never appear in\nthose public results. Legacy version-1 pages remain operator-readable but are\nwithheld from passive delivery until a normal refresh creates a bound version;\nunknown formats are refused. Content changes withhold the page and require\nrefresh, without rewriting old history.\n\nBefore canonical `purge` deletes live rows, it preflights derived needles\nagainst the whole pages artifact directory. An existing needle, unsafe path,\nor unscannable artifact fails closed before the row-deletion transaction. The\noperator inspects and removes the whole affected page or staging artifact,\nthen retries; historical page versions are never individually deleted as\ncleanup. When page artifacts exist, the preflight intentionally snapshots the\nfull live SQLite database into an in-memory connection, reapplies the pending\npurge there, and derives needles from the exact post-purge survivor set. This\nprivacy check has memory and latency that scale with the store; no performance\nbound is promised. If the snapshot or artifact scan cannot complete, purge\nrefuses before live row deletion. Passive credential withholding controls\ndelivery only; it does not promise encryption or redaction of page content at\nrest.\n\nThe current observation gate is recorded as `reject`, so page metadata omits\n`runbook` (and since 0.107.0 the `ALLOWED_TYPES` vocabulary is the EIGHT-type\nset — `entity_card` joined as the card-refresh-only type; see \"Entity cards\n(issue #179)\" below).\n\n### event — occurrence-typed memories + the temporal recall arm (v18, issue #178)\n```\n\u003Cinterpreter> \u003Cstore.py> add --type event --namespace NS --content TEXT   --occurred-start 2026-09-01T08:00:00Z --occurred-end 2026-09-03T08:00:00Z   [--mentioned-at TS]\n\u003Cinterpreter> \u003Cstore.py> recall --query \"last week's outage\" --namespace NS   --prompt-timestamp 2026-09-10T12:00:00Z [--as-of TS]\n```\nSchema 18 adds the `event` memory type and the `memory_occurrence` side\ntable (occurred_start \u002F occurred_end \u002F mentioned_at beside the memory id).\nEvent rows require BOTH occurrence endpoints (clamped to the temporal\nwindow: the future clamps to the prompt timestamp, older than 3650 days\nclamps to the floor); non-event rows refuse any occurrence field (exit 2,\nno partial row). Occurrence rows live with their parent and SURVIVE soft\ntombstones of USER event rows (update \u002F supersede \u002F consolidate-absorb) —\nhistorical as-of recall surfaces them inside the tombstoned row's validity\nwindow; generated-row writers (organize summaries, entity reconcile) keep\nsame-transaction cleanup; hard deletes (purge, import-store) remove\noccurrence rows through the side-table registry; event->event updates\nadditionally inherit the occurrence onto the replacement row\n(non-event->event via update is refused; dedup folds keep the keeper's\noccurrence).\n\nThe temporal recall arm resolves relative phrases in the query (the fixed\n20-phrase vocabulary: today\u002Fyesterday\u002Ftomorrow, this\u002Flast\u002Fnext week,\nthis\u002Flast\u002Fnext month, this\u002Flast year, past 24 hours\u002F7 days\u002F30 days,\ntwo\u002Fthree days ago, in two days, earlier today, this\u002Flast quarter —\nlongest-match-first, so \"earlier today\" wins over \"today\") against the\n`--prompt-timestamp` (default: the `ZMEM_TEST_NOW` seam \u002F wall clock),\nselects occurrence-overlapping rows under the unchanged `--as-of`\nvalidity predicate, and fuses them as the capped fifth RRF arm\n(`ZMEM_ARM_CAP_TEMPORAL`, default 4). Knowledge validity (valid_from\ninclusive \u002F valid_until exclusive) is SEPARATE from occurrence time.\nRecall rows reached via the temporal arm carry additive occurred_start \u002F\noccurred_end \u002F mentioned_at envelope fields (the time the event HAPPENED),\ndistinct from valid_from (the time it was WRITTEN). The three keys ride\nonly temporal-arm rows (never non-temporal rows, never overwriting an\nexisting key), so projections that select known fields are unaffected.\nA phrase-free query or an occurrence-empty store fuses byte-identically.\nThe Hermes\u002FMCP `add` surfaces do not advertise `event` (they take no\noccurrence endpoints — the #173 `state` precedent).\n\n### recall — surface relevant memories (high-precision)\n```\n\u003Cinterpreter> \u003Cstore.py> recall --query \"\u003Cquery>\" [--namespace NS] [--limit 5]\n  [--link-hops 0|1] [--link-budget N]\n                          [--include-global] [--global-limit 3] [--hybrid]\n                          [--no-hybrid] [--no-mmr] [--no-bump]\n                          [--as-of ISO-8601] [--no-unfold] [--json]\n\u003Cinterpreter> \u003Cstore.py> recall --query \"\u003Cquery>\" --explain [--target ID|FRAGMENT]\n                          [--json]\n```\nReturns live (non-superseded) memories matching the query, filtered by confidence\nfloor (>=0.25) and namespace — unless `--as-of ISO-8601` is given, in which case\nit is a point-in-time read that returns rows **valid at that instant** (a row\nsuperseded after that instant may be included; a row created after it is not;\nsee the `--as-of` notes below). Prefer `--namespace project:\u003Cbasename>` to scope to\nthe current project; use `user:global` for cross-project.\n\n#### Retrieval debugger: `--explain` \u002F `--target` (issue #82)\n\n`recall --explain` re-runs the REAL pipeline (same lanes, same floors, same\nmerge) and prints one blameline per verdict explaining why a row did or did\nnot surface. It is an operator diagnostic in the same spirit as `doctor.py`:\nZERO writes (the telemetry path is never reached — stricter than\n`no_telemetry`), it never unfolds, it never fails a recall (a thrown tracer\ndegrades to the `explain_unavailable` verdict and the results still print).\n`--target` takes a memory id (full or unambiguous prefix) or a content\nfragment (case-insensitive substring, then token overlap >= 0.7); multiple\nmatches produce one verdict per id, never a guess. With `--json` the read\nenvelope gains an `explain` object: `query`, `query_shape` (the\nnormalized terms + exact FTS MATCH expression, #112), `target`, the\neffective settings (`namespace`, `limit`, `include_global`,\n`global_limit`, `no_mmr`, `no_bump`, `as_of`, `hybrid`), and\n`verdicts[]` with `id`\u002F`reason`\u002F`rank`\u002F`score`\u002F`detail`.\n\nVerdict reasons are a CLOSED set (`EXPLAIN_REASONS` in `storelib\u002Frecall.py`):\n\n| reason | meaning |\n|---|---|\n| `found` | in the presented result set at `rank` (1-based) |\n| `below_limit` | retrieved and scored but beyond `--limit` |\n| `below_floor` | the row's confidence is below the effective floor (the recall `min_confidence` parameter, default `CONFIDENCE_FLOOR`) |\n| `omitted_injection` | would have ranked; dropped because `no_bump` and prompt-injection risk |\n| `omitted_untrusted_web` | would have ranked; dropped because `no_bump` and `taint=untrusted_web` |\n| `namespace` | lives outside the query's expanded namespace set (and `--include-global` did not admit it) |\n| `superseded` | live-only recall and `superseded_at` is set (`detail.successor_id` names the live replacement when one exists) |\n| `not_valid_at_as_of` | `--as-of` set and the validity interval does not contain it |\n| `vec_lane_miss` | `--as-of` + hybrid: the row was valid then, but the vec KNN pool is live-rows-only (the SKILL.md as-of caveat — this is why history-on-hooks is not \"just pass `--as-of`\") |\n| `not_in_pool` | not in the FTS, vec, or entity candidate pool at over-fetch depth |\n| `not_in_db` | no row matched `--target` (`detail.neighbors` lists up to 5 nearest live rows by token overlap) |\n| `explain_unavailable` | the tracer threw; results still returned (fail-open) |\n\nFor an injection-path explanation, combine `--for-injection --explain`. This\nis a read-only replay: it follows the same omit, selective-inject,\nscore-margin, and token-budget stages as a passive injection, but never writes\nsurface or retrieval telemetry. A target removed by the score-margin stage is\nreported with the closed-set reason `margin_pruned`; its detail includes the\nobserved relative margin (formatted to six decimals), the configured threshold,\nand the retained top-row id. The existing omit, selective, scope, validity,\nand budget reasons remain unchanged for rows rejected by those stages.\n\n#### Change-intent lineage unfold — explicit recall only (issue #82)\n\nAlready-stored lineage (`update_of` from `update`) becomes visible on the\nEXPLICIT path: when the query reads as change-intent (\"what changed about X\",\n\"why did we switch to Y\", \"we used to Z\") AND the presented results contain a\nlive head of a tombstoned predecessor, recall appends the predecessor chain as\nbudgeted extra rows tagged `[PREVIOUSLY]` (JSON keys `unfold_of` +\n`unfold_hop`, 1 = immediate predecessor). Extras never count against\n`--limit`, never enter the telemetry bump set (popularity rewards query\nmatches, not neighbors), never cross namespaces, and are never silently\ndropped for taint — the explicit surface prefixes `[INJECTION RISK]` \u002F\n`[UNTRUSTED WEB]` like any other row.\n\nIt runs ONLY when all of these hold: not `--no-bump` (hooks, PreCompact,\nHermes prefetch, and eval defaults never unfold), not `--no-unfold`, the\nquery matches the compiled change-intent regexes (deterministic, no LLM), and\n`link_hops >= 1` (the same contract that keeps `search` — CLI, MCP, and\nHermes `_tool_search` — byte-identical; MCP `recall` unfolds for free).\n`--explain` describes lineage in verdict detail but never injects rows.\n\n| Env var | Default | Meaning |\n|---|---|---|\n| `ZMEM_UNFOLD_TOP_K` | `3` | max presented hits to walk backward from (clamped 1-10) |\n| `ZMEM_UNFOLD_MAX_HOPS` | `3` | max `update_of` hops per chain (clamped 1-10) |\n| `ZMEM_UNFOLD_BUDGET` | `4` | hard cap on total `[PREVIOUSLY]` extras per recall (clamped 1-20) |\n| `ZMEM_FAILURES_DB_TIMEOUT_S` | `1.0` | Stop-hook db reader busy-wait budget in seconds (clamped 0.1-5.0; invalid → default; `failures --db-timeout` overrides) |\n| `ZMEM_ZCODE_DB` | `~\u002F.zcode\u002Fcli\u002Fdb\u002Fdb.sqlite` | ZCode episodic db the Stop-hook detector reads (empty\u002Funset = default) |\n| `ZMEM_REFLECT` | enabled | set to exactly `0` to disable the Stop hook entirely |\n\n#### Confidence floors (issue #58, 3.8)\n\nFive distinct floors live on the recall path. Each reflects a different\nsurface's precision-vs-coverage tradeoff. They are env-overridable; the\nconstants live in `schema_meta.py`.\n\n| Constant | Default | Env override | Used by |\n|---|---|---|---|\n| `INJECT_FLOOR_PROMPT_DEFAULT` | 0.25 | `ZMEM_INJECT_FLOOR_PROMPT` | `recall` (UserPromptSubmit \u002F PreCompact). Hard floor on FTS\u002Fvec results — anything below is dropped before scoring. |\n| `INJECT_FLOOR_RECENT_DEFAULT` | 0.5 | `ZMEM_INJECT_FLOOR_RECENT` | `recent` (SessionStart \u002F subagent recall). Tighter because the surface is high-confidence recent material, not query-best match. |\n| `INJECT_FLOOR_GATE_NONE_DEFAULT` | 0.4 | `ZMEM_INJECT_FLOOR_GATE_NONE` | Hook selective-inject gate. `signal=none` rows must clear this floor; grounded-signal rows (`test`\u002F`compile`\u002F`lint`\u002F`reviewer`\u002F`user`) keep the 0.25 floor. |\n| `INJECT_FLOOR_TRUST_DEFAULT` | 0.2 | `ZMEM_INJECT_FLOOR_TRUST` | Selective-inject gate (issue #115). Hard floor on `trust_score`: a row the contradiction ledger has driven below 0.2 (nine or more distinct `contradict` events) can no longer ride the passive lane; `compute_score` also multiplies every composite by `trust_score` (identity at the default 1.0, so uncontradicted rankings do not move — explicit recall\u002Fsearch keep retrieving the row, just ranked lower). When E-5 (#124) starts recording violations automatically, they must feed THIS ledger via trust deltas — `violated_count` never becomes a second independent gate input. |\n| `INJECT_FLOOR_USER_GLOBAL_DEFAULT` | 0.5 | `ZMEM_INJECT_FLOOR_USER_GLOBAL` | user:global tier floor on the passive injection lane (issue #235). On gated moments (`pretool`\u002F`subagent` — the hooks' posttoolbatch maps to `pretool` store-side) a global-tier injection candidate whose MAX measured relevance lane sits below 0.5 is withheld and its reserved slot returns empty rather than being backfilled; the envelope reports the count as `global_withheld` (present only when a withhold happened). A global candidate with NO measured lane (query-less recent pulls) keeps the not-measured exemption — never withheld by this floor; the user:global floor is armed on the subagent moment for every pull; a bridged task-keyed SubagentStart recall (issue #253) additionally gives that surface a measured relevance lane, while query-less pulls keep the not-measured exemption. `session_start`\u002F`user_prompt`\u002F`precompact` keep their current global composition. `0` disables (relevance values are >= 0, so nothing is ever below it). |\n\nThe floors are intentional. Do not silently unify them. The\nselective-inject gate (3.8) is a passive-lane filter; the trust floor\napplies symmetrically to link-expansion neighbors that ride the passive\nlane (a once-contradicted neighbor at trust 0.9 still renders with its\n`[CONTESTED LINK: \u003Cid>]` marker naming the other end of the\ncontradiction).\n\n#### Score-margin gate (issue #182)\n\n`ZMEM_INJECT_MARGIN` is an opt-in score-separation gate between the existing\nselective-inject filter and token-budget admission. It defaults to `0.0`,\nwhich disables the gate and preserves the legacy injection bytes. The setting\nis read dynamically for each injection decision. A missing, negative,\nnon-numeric, NaN, or infinite value fails open to `0.0`; values above `1.0`\nare clamped to `1.0`. The recommended initial rollout is `0.05`.\n\nWith a positive threshold and at least two usable post-selective rows, the\ngate compares the two highest scores using the stable score-descending view\nand computes the raw `(top - second) \u002F top`. Every candidate score must be\nusable; a malformed, scoreless, or non-finite score anywhere fails open. When\nthe raw margin is strictly below the threshold, ordinary rows after the winner\nare pruned before the token budget runs. A `decision` or `constraint` in either\nleading position is protected: no rows are pruned, but the valid observed\nmargin is still reported. Fewer than two rows and a non-positive top score\nalso fail open. The original presentation order is retained for surviving\nrows.\n\nWhen a valid score pair was observed, injection JSON adds `margin` formatted\nto six decimal places and `margin_pruned_ids` in prune order; no fields are\nadded when the gate could not make a decision. The hook decision line appends\nthe same diagnostics, in the order `margin=` then `margin_pruned=[...]`, after\nthe existing optional fields. Empty pruned lists are omitted from the hook\nline, and malformed optional values are ignored independently so legacy\nlogging remains fail-open and byte-compatible.\n\nWhen a passive inject surfaces nothing, it names WHICH gate fired (issue #87 \u002F\n#85 direction 1): `no durable memories retrieved for this prompt.` means the\ncandidate pool was empty OR every retrieved row was dropped by the passive\ninjection-risk filter — the one-liner is shared by design (the model is never\ntaught that omitted rows existed); the `zmem-bg.log` line distinguishes them.\n`no durable memories met the inject bar.` means rows reached the\nselective-inject gate and none passed. A token-budget wipe says so in its own\nwords (`memories withheld: the injection token budget (ZMEM_INJECT_TOKEN_BUDGET)\ndropped every candidate row.`). Hermes\u002FMCP `session_start` use the session\nvariants (`no durable memories retrieved for this session.` and\n`session memories withheld: ...`).\n`zmem-bg.log` carries the same cut per decision line: every\n`zmem-hook` line has `reason=` (`reason=empty-pool`, `reason=omitted`,\n`reason=below-bar`, `reason=budget-drop`, `reason=below-relevance`,\n`reason=already-delivered`, `reason=injected`), plus `omitted=N`\nwhen the passive injection-risk filter dropped rows. The closed set lives in\n`schema_meta.py` (`INJECT_SILENT_REASONS`). Since issue #94 every line also\nends with `sid=\u003Csanitized session id>` (`[^A-Za-z0-9._-]` → `_`, cap 128;\n`sid=unknown` when the host sent none) — the session key doctor's\n`--miss-rate` join binds failures to injections with. The order is `status`, `reason`, `omitted=`, `ids=`, `all=`, `tokens=`, `rendered_estimate=`, `admission_budget=`, `budget_dropped=`, `budget_truncated=`, `budget_dropped_protected=`, `sid=` (the retired `ops=` ring count rode before `sid=` in releases \u003C= 0.37; issue #158 moved operation context into the query, not the log). Current lines append `exc=`, `moment=`, optional `lane=`, `ver=`, `t_ms=`, then `arms=`, `batch=`, `tools=`, `paths=`, and margin fields (the budget fields are issue #116's distinct labeled numbers and ride only when budget accounting ran).\n\n#### Decision attribution and report projection (issue #153)\n\nThe additive decision suffix is ordered `lane=`, `ver=`, `t_ms=` after the\nhistorical `moment=` field and before later additive tails (`arms=`,\n`batch=`, `tools=`, `paths=`, and margin fields). Closed lanes are `claude`,\n`codex`, `zcode`, `deepseek`, `hermes-provider`, and `hermes-compat`;\nruntime moments are\n`session_start`, `user_prompt`, `pretool`, `subagent`, and `precompact`.\nSilent reasons are exactly `empty-pool`, `omitted`, `below-bar`, `budget-drop`,\n`below-relevance`, `already-delivered`, and `expired` (`expired` names a pool\nthat was eligible before the live-expiry gate drained it — issue #174's\nproducer; every other condition keeps its precedence). The decision line can\nadditionally carry the transport tokens `reason=store-error` — a store\nchild that exited nonzero (e.g. a partial-upgrade store rejecting an\nunknown flag) — and `reason=envelope-error` — a store child that exited\nzero whose stdout was not a JSON envelope (issue #435); both are emitted\nby the hook body and the hermes-provider lane, and miss-rate reporting\nexcludes both from the missed count like the\nother negotiation refusals. `reason=injected` is a\nsuccessful decision and `reason=disabled` is the separate kill-switch marker.\n\n`already-delivered` is selected when pre-ledger candidate ids are nonempty but\nthe post-ledger pool is empty, after `budget-drop` in the precedence order;\ndual-empty remains `empty-pool`. `ver=` and `t_ms=` are an all-or-nothing\nenrichment pair: `ver=` is the release-manifest semver and `t_ms=` is a\nnonnegative rounded `perf_counter` duration. A manifest\u002Fversion failure keeps\nthe complete legacy line and omits all attribution fields. A lane is optional\nfor compatibility callers, but an explicit lane must be closed-set.\n\n`parse_bg_log` accepts legacy lines without attribution. If any attribution\ntoken appears, it requires valid `ver=` and decimal nonnegative `t_ms=` and\nrefuses malformed enrichment; `moment=` remains open for compatibility values.\nLane-less enriched lines and moments outside the report set stay aggregate-only.\nThe named report is always a sorted, zero-filled 20-row matrix: five lanes ×\nthe four report moments `session_start`, `user_prompt`, `pretool`, and\n`precompact`. The runtime `subagent` moment and `session_start_compact` are\nintentionally excluded from that 5×4 projection but remain visible in\naggregate statistics. The local Hermes provider writes `hermes-provider`; the\nremote compatibility path writes `hermes-compat`, and invalid explicit lanes\nreturn structured status 2 before store work while absent lanes stay omitted.\n\n#### `store.py report` — per-session repeat and composition report (issue #249)\n\n`store.py report [--decision-log \u003Cpath>] [--json]` aggregates the decision log\ninto the repeat rate and composition tables, read-only. It is the instrumentation\nWorkstream P PRs 2-4 use as their before\u002Fafter baseline.\n\n- `rows[\u003Cid>]` — `delivered` (total deliveries), `sessions` (distinct `sid=`\n  count), `max_per_session` (busiest single session), and the row's `tier`.\n  Lines carrying no usable `sid=` count toward `delivered` but contribute no\n  session, and the run says so in `caveats`. **The writers' `sid=unknown`\n  placeholder counts as no session**: it is what every host without a session id\n  emits, so pooling it would invent one session and attribute unrelated\n  deliveries to it. This matches how the sibling readers already treat it.\n- `by_tier_moment[\u003Ctier>][\u003Cmoment>]` — `delivered` (the `ids=` entries) against\n  `withheld` (the `all=` candidates that were not injected). `delivered` counts\n  only lines the shared injected-line predicate accepts, so ids on a\n  `status=silent` line are never reported as deliveries; their candidate pool is\n  still counted as `withheld`, because a candidate was offered either way.\n\nTop-level JSON keys: `decision_log`, `decision_lines`, `ts_first`, `ts_last`,\n`rows`, `by_tier_moment`, `caveats`. `ts_first`\u002F`ts_last` are the decision\ntimestamps actually covered, so two runs can be checked for an overlapping window\nbefore a before\u002Fafter difference is read as a behaviour change.\n\n`--decision-log` defaults to `\u003Cdata dir>\u002Fzmem-decisions.log`, falling back to the\nlegacy `zmem-bg.log`. Rotated segments (`.1` … `.N`) are included. Without\n`--json` a human table prints; with `--json` stdout is exactly one compact JSON\ndocument, so it can be piped into `jq` unchanged — **including on a refusal**,\nwhere stdout carries `{\"ok\": false, \"error\": \"...\"}` at exit code 2 and the\nhuman `[zmem] report:` line goes to stderr. A missing or unreadable log is\nreported as a `caveats` entry rather than a silent empty report, because an\nempty report and an unreadable log are otherwise indistinguishable.\n\n**Tier labels** come from the row's stored namespace: a `project:` namespace is\n`project`, `user:global` is `global`, and anything else — including an id no\nlonger in the store — is `unknown`. The live five-tier scoped recall model spells\nits global tier `user_global` and adds `domain` \u002F `fleet_host` \u002F\n`cross_project`; its cross-project tiers depend on the CALLER's current project,\nwhich an offline report does not have, so it does not invent them.\n\n**Read it with `caveats`.** Tier labels come from a point-in-time snapshot; an id\nmissing from that snapshot is reported as `unknown`. The log-derived figures and\nthe tier-summed totals are unaffected, but the per-tier split can shift, so\n`unknown`-bucket movement between two runs is not evidence of tier migration.\n\nThe command never writes to the store, the ledger, or the log. It opens the store\nthrough an `immutable=1` handle only when there is no residue at all, or through\na private staged copy of `store.sqlite` + `-wal` + `-journal` when there is; a\nplain read-only open would create sidecars and mutate `-shm` in place. A hot\nrollback `-journal` also forces the staged path on purpose: `immutable=1` ignores\nthe journal entirely and would report the uncommitted pages of an in-flight\ntransaction as real rows. The staging directory is created owner-only and is\nremoved on every path including failures; a directory that survives removal is\nnever left silent — on a successful run it is appended to `caveats`, and on a\nrefusal it is reported alongside it (folded into the refusal message, or on its\nown `[zmem] report:` stderr line for a non-`ReportError` failure). `-shm` is\nnever copied — SQLite rebuilds it from the `-wal`, and copying it fails\noutright under a live writer on Windows.\n#### Query context (prior-turn operation tokens) — issue #88 \u002F #85 direction 2\n\nDecision-point prompts are prose with zero lexical overlap with the\noperation-adjacent lessons that matter; the retrieval signal lives in the\ntool commands the session is executing. The PostToolUse hooks\n(convention-capture on the coding hosts, `post_tool_call` on Hermes) append\neach Edit\u002FWrite\u002FBash event to a per-session ring at\n`\u003Cdata>\u002Fops\u002F\u003Csession>.log`, storing ONLY the tool name plus allowlisted\ntokens (git subcommand chains, test-runner verbs, edited-path basenames) —\nnever a raw command dump, stdout, or argument values that are bare words;\nsecret-shaped tokens (common credential prefixes like `ghp_`\u002F`sk-`\u002F`xox?-`)\nare dropped as well. The ring is byte-capped: past 64 KB it is trimmed to\nthe newest 64 lines. The store-owned passive selector composes that ring only\nfor the `pretool` moment, with the ops tail occupying a fixed reserved slice\nINSIDE the 500-char cap (never appended past it; the separator shares the\nslice, so no token is severed). UserPromptSubmit and Hermes prefetch no\nlonger append a caller-owned ops tail.\n`ZMEM_QUERY_CONTEXT=0` is the lane kill switch — it stops BOTH composition\nand ring collection (an operator disabling the lane expects no sidecar\nwrites). The ring resolves through the same unified data-dir chain as the\ndecision log (`hooks\u002Flib\u002Fzmem-recall-body.py::_data_dir()`, which resolves\nthe decision-log data directory: a directory-qualified `ZMEM_STORE` parent\nfirst, then `ZMEM_DATA` \u002F `CLAUDE_PLUGIN_DATA` \u002F `ZCODE_PLUGIN_DATA`, then\n`~\u002F.zmem`) — `read_ops_ring(data_dir, ...)` reads the ring from exactly the\ndirectory it is given, including plugin-data directories by #88 design.\nOperation derivation remains store-owned; no operation count crosses\nthe exact hook envelope. Explicit surfaces (`recall --query`, `search`) are unchanged.\nLimitation (deliberate, see #88): this helps LATER turns only — the first\ntool call of a turn still runs before any operation context exists.\nCost note (#93 B4): credential-prefix-shaped tokens (`sk-`, `npm_`, `AKIA`,\n…) are dropped from the ops query EVEN when they are legitimate filenames —\nfail-safe direction (signal loss, never a leak); rename a real path that\ncollides. The eval composer ignores `ZMEM_QUERY_CONTEXT` by design (#93 B6):\nevals must stay deterministic and immune to ambient env, so the kill switch\ndoes not change their queries.\n\n#### Pre-tool checkpoint phrases — issue #99\n\nPreToolUse passive recall also enriches the query from the current raw\n`tool_input` object before the tool runs. The store serializes that object as\nsorted compact JSON (`sort_keys=True`, separators `,` and `:`), examines\nexactly its first 150 Unicode characters, and selects at most one entry from\nthis immutable ordered table:\n\n| Event | Checkpoint phrase |\n|---|---|\n| stash-consume | `foreign-stash conflict verify stash list` |\n| reset | `stale tree fetch main rebase verify diff` |\n| force-push | `stale tree fetched base force-with-lease` |\n| branch-publication | `stale tree fetched base force-with-lease` |\n| base-rewrite | `base drift citation re-pin` |\n| path-test | `basename ratchet citation re-pin local battery` |\n\nThe recognized forms are `git stash pop|apply|drop`, `git reset --soft|--hard`,\n`git push --force|--force-with-lease|-f`, ordinary `git push`, `git merge\n--squash`, and test\u002Fcitation paths. `git stash list`, unknown operations,\nnon-object input, and matches beyond character 150 add no phrase. Current-event\noperation tokens take precedence over an older session ring; the ring remains\nthe fallback. The operation-token tail retains its 150-character reservation,\nthe checkpoint phrase is never split, and the final query remains bounded to\n500 Unicode characters. Raw tool input crosses only the private bounded\nhook-to-store stdin channel and is neither logged nor persisted. The\n`ZMEM_QUERY_CONTEXT=0` kill switch disables both token and phrase enrichment.\n\nThis feature shipped under issue #155's published measurement decision (the\n2026-09-19 fail-closed INSUFFICIENT outcome); #155's predeclared measurement\nof record has since landed (`eval\u002Freal-corpus-2026-09-19.json` — an audit\nover a frozen cohort, not a live efficacy claim), so the gate is satisfied\nwithout an efficacy claim by this feature.\n\n#### Passive-injection kill switch (ZMEM_INJECT=0) — issue #110 \u002F P0-5\n\n`ZMEM_INJECT=0` disables every passive recall-injection surface: the shared\nrecall body (all modes — user_prompt, pretool, subagent, precompact\u002Frecent),\nthe SessionStart hook (Tier 2 recall AND Tier 0 — under the switch\nSessionStart emits its empty `{}` envelope), the Hermes provider\n(`prefetch`, the `zmem_session_start` tool twin, and the system-prompt\n`core.md` block), the Hermes reflect hook's delivery paths, and MCP\n`session_start`. Each silenced surface emits its empty envelope and logs\n`status=silent reason=disabled` (`reason=disabled` is written only by this\nswitch, never by silent-reason classification — it lives beside `injected`\nas `INJECT_REASON_DISABLED`, not in `INJECT_SILENT_REASONS`). Only the\nliteral `0` (whitespace-tolerated) disables — the `ZMEM_QUERY_CONTEXT`\nconvention; `false`\u002F`no`\u002Fempty leave injection enabled. Capture paths never\nconsult the switch: correction capture, the ops ring, convention counters,\nfailure capture, and session-cadence maintenance keep writing — and the\ncapture-side PROMPT hooks (`zmem-reflect.sh`, `zmem-subagent-reflect.sh`,\n`zmem-convention-capture.sh`, capture-failure\u002Fcorrection prompts) stay\nactive by design (#110: they prompt capture, they do not inject recalled\nmemory; the Hermes reflect hook is the documented divergence — a named\ngated surface whose delivery carries recalled query-context). Parked\npre-tool fences and armed nudge markers are left in place and deliver on the\nfirst enabled run. `doctor` shows the state (`inject-switch` line, WARN when\ndisabled). Near-miss spellings (`0.0`, `00`, `False`, `off`) do NOT disable\n— the parser matches the exact literal `0`; verify with `doctor` after\nsetting.\n\n#### Capture kill switch (ZMEM_CAPTURE=0) — issue #123\n\n`ZMEM_CAPTURE` is a string environment variable with default `\"1\"`. The\nfailure, convention, Stop, SubagentStop, and Hermes compatibility surfaces\nread it before payload parsing, marker\u002Fqueue writes, lesson queries, operation\nappends, or other stateful work. Only a trimmed literal `0` disables capture;\nundefined, empty, whitespace, `false`, and `00` stay enabled. Disabled shell\nsurfaces emit their exact empty sentinel and Hermes emits `{}`. Audits and\nprobes set `ZMEM_CAPTURE=0` so those five covered surfaces cannot teach the\nsystem.\n\nThis is independent of `ZMEM_INJECT` (delivery) and `ZMEM_QUERY_CONTEXT`\n(operation-ring collection). Under `ZMEM_INJECT=0`, capture can still run;\nunder `ZMEM_CAPTURE=0`, no capture subprocess or state mutation runs on those\nfive surfaces. The pre-existing UserPromptSubmit correction queue and Hermes\nconvention-compatibility cadence are separate legacy paths with their own\ncapture-mode\u002Finterval controls. Complete\n`\u003C\u003C\u003CZMEM_UNTRUSTED_FENCE>>>` through `\u003C\u003C\u003CEND_ZMEM_UNTRUSTED_FENCE>>>` blocks\nare removed before transcript-derived content reaches deduplication, queue\nsynthesis, or closeout review.\n\n#### Pre-tool repository-state predicate — issue #397\n\nSince 0.124.0 the pretool lane carries a bounded, fail-open\nrepository-state predicate. When the DERIVED pretool query contains any\nrestore\u002Fdestructive verb as a substring (checkout, restore, reset, stash,\nclean, rebase, merge, push, cherry-pick, revert, filter-branch — a\nsubstring match, so benign text like \"cleanup\" also arms it; the cost is\none extra `git status`), the hook body runs\n`git status --porcelain --untracked-files=no` once — TRACKED-change dirt\nonly, exactly the wiped-by-`git checkout --` hazard — in the EVENT cwd\nwhen it names an existing directory (process cwd otherwise), with the\ntimeout read from `ZMEM_STATE_PROBE_TIMEOUT_S`\n(float seconds, default 2.0, clamped at 8.0; malformed values fall back\nto the default). When the verb gate does not arm, the probe does not run\nand the decision line carries NO `state=` field at all — absence means\n\"no probe ran\", distinct from `state=unavailable` (\"ran and failed\").\nThe outcome travels as `--state-hint dirty|clean` on the pretool recall\nargv (omitted entirely when the probe fails — never an unknown value) and\nis recorded on the decision line as `state=dirty|clean|unavailable`.\nStore-side, a candidate row receives a +0.15 rank bump — on both the sort\nscore and `_score` — ONLY when the hint is exactly `dirty` AND the row's\ncontent matches the closed hazard lexicon (`uncommitted`, `dirty tree`,\n`dirty working tree`) AND carries a condition marker AND negation\npolarity. Clean, unavailable, and absent hints are byte-identical to\ntoday; the global relevance floor and trust gates still withhold gated\nrows (a bump never rescues them). The hint rides the recall query branch\nonly — the queryless `recent` lane stays state-blind. Test seam:\n`ZMEM_TEST_REPO_STATE=dirty|clean|unavailable` overrides the probe\nentirely.\n\n#### Snapshot-refresh refusal decision lines — issue #449\n\nSince 0.146.0 a refused snapshot refresh (any shipped refusal class:\n`hub-unreachable`, `hub-refused`, `payload-invalid`) appends ONE bounded\nline to the guarded decisions log beside the unchanged #437 stderr line:\n`[\u003Cts>] zmem-hook status=silent reason=\u003Cclass> ids=[] all=[]\nsnapshot_refresh=refused`. The line is parseable by the shipped\ndecision-log reader via the additive TERMINAL `snapshot_refresh=` optional\ngroup (appended after `state=` so every earlier positional group index is\npreserved); parsed rows carry `snapshot_refresh` verbatim (None on lines\nthat predate the field). No enrichment trio (`lane=`\u002F`ver=`\u002F`t_ms=`) rides\nthe line, so the reader's enrichment veto stays untriggered. The two\nwrites are failure-independent: rotation problems and a closed\u002Fraising\nstderr never suppress the persistent line (and vice versa); the whole\nwriter body is guarded so a refused\u002Freadonly data dir only loses the line,\nnever the refresh ladder.\n\nKnown bounds (PR #451 review, disclosed): a FULL hub outage writes ZERO\nrefusal lines — the broker takes its offline-snapshot branch and returns\nBEFORE the refresh attempt, so the stamp (not the decision log) is the\noutage-window evidence; refusal lines appear only when a refresh was\nATTEMPTED and then refused (fresh-pair transport failures retry per\nmoment, one line each — the rotation family bounds the growth). The rows\ndilate hook-decision aggregates that count all parsed lines\n(`sid_coverage_pct`, period bounds, attribution counters — a refusal row\ncarries no `sid=`); no shipped report branches on `snapshot_refresh` yet\n(parsing is the shipped contract; a dedicated refusal surface is future\nwork). Mixed version: a pre-0.146 decision-log READER skips these lines\nwhole, and a pre-0.146 `scripts\u002Feval_replay.py` REFUSES the log\n(ReplayError on the unknown terminal field) — do not point an older\nreplay evaluator at a 0.146 data dir.\n\n#### Write-path capture guards and the session closeout — issues #398 \u002F #246 \u002F #247\n\n- **Conditional-contradiction notice (#398)**: an `add` whose content\n  opposes a recent same-namespace row (negation-polarity disagreement +\n  condition marker + >= 2 shared significant tokens, 200-row window)\n  stores normally with a STRUCTURED notice naming both rows — notice\n  only, never an edge or trust mutation.\n- **Lesson phrasing advisory (#398)**: a `--type lesson` row with no\n  trigger-condition marker (when\u002Fwhenever\u002Fif\u002Fwhile\u002Fbefore\u002Fafter\u002Funtil)\n  stores with an advisory suggesting the conditional phrasing.\n- **Hostless namespace near-miss (#246)**: a `project:\u003Cowner>\u002F\u003Crepo>`\n  key with a dotless first component stores, with a\n  `namespace_project_near_miss` warning naming the canonical\n  `project:github.com\u002F\u003Cowner>\u002F\u003Crepo>` form (assumption A7:\n  warn-and-store — rejecting would break legitimate hierarchical\n  namespaces).\n- **Session closeout (#398)**: every successful attributed store write\n  (`add`\u002F`update`\u002F`supersede`\u002F`invalidate`\u002F`entity-card` when a card was\n  written) stamps `\u003Csha256(session-id)[:32]>.wrote` under `\u003Cdata>\u002Fops\u002F`;\n  refused writes stamp nothing. The session hooks surface exactly one\n  bounded closeout reminder while `.wrote` exists without `.closeout` —\n  at session end, and (because SessionEnd output has no live model turn\n  on most hosts) re-surfaced once at the NEXT SessionStart in\n  model-visible context. A recorded closeout silences it:\n  `store.py closeout-ack --session-id \u003Cid>` (store-independent; the\n  closeout workflow's Step 7 runs it). The ops age sweep reaps\n  `.wrote`\u002F`.closeout`\u002F`.nudged` markers after\n  `ZMEM_SENTINEL_SWEEP_DAYS`.\n- **Harvest-paste gate (#247)**: a user message over 400 chars whose\n  dominant fenced block parses as JSON (array or single object) carrying\n  >= 3 of the harvest keys (`namespace`, `type`, `content`, `signal`) is\n  NOT captured as a correction candidate — it belongs to\n  `\u002Fingest-harvest`. The gate runs on the manual correction hook lane and\n  the auto-retain and history-mine lanes; line endings are normalized\n  (CRLF pastes are gated too).\n\n#### Pre-tool inject — issue #90 \u002F #85 direction C\n\nOn hosts whose pre-tool contract was probed and confirmed (ZCode: documented;\nClaude: emitted — issue #117 retired the sidecar default in favor of the\nper-session delivery ledger), a\nPreToolUse hook (`zmem-pretool-recall.sh`, matcher\n`Edit|Write|MultiEdit|NotebookEdit|Bash|apply_patch|Agent|Task` since 0.31.0 (apply_patch added later; #338) — issue #119;\nmatching the live registration in `hooks\u002Fhooks.claude.json`.\nthe Agent branch passes task text when the event carries it AND,\nwhen it does not, parks the delegating prompt store-side for the child's\nSubagentStart (issue #253), otherwise using the store-owned recent path)\nderives the recall query from the\ntool input ITSELF — the command or file path about to run — and injects\nmatching hazard lessons before the tool executes. Pre-tool\n`additionalContext` is documented on Claude Code (since 2.1.9 it lands\nalongside the tool result; pausing is `permissionDecision`-driven only) —\nissue #158 makes the store-owned selector the only passive path: delivered\nids now live in a bounded per-session ledger\n(`\u003Cdata>\u002Fops\u002F\u003Csha256-of-session-id>.ledger`, atomic, window- and\ncap-bounded — plus per-call `\u003Chash>.ledger.seg.\u003Chex>` JSONL segments that\npublish without a lock and are merged into the base by a best-effort\ncompaction; reads always merge base + segments, issue #252) that EVERY\ninjection moment consults, so the same row is not\nre-delivered within the window — with one deliberate exception: PreToolUse\nre-delivers when the operation tokens about to run strongly match the row\n(the session-start hazard still fires before the dangerous command). The\nledger clears at PreCompact and the following SessionStart uses the ordinary\n`session_start` selector moment. The retired `ZMEM_PENDING_SIDECAR` fallback\nis no longer produced or consumed. The host event's `session_id` (without\nit the adapter stays fail-open) is passed to the selector. The hook NEVER\ndenies (a surfaced hazard is information, not\ngrounds to block a legitimate command) and stays fully silent when nothing\nqualified. `ZMEM_QUERY_CONTEXT=0` silences every query-context lane, this\none included. Hermes delivers the equivalent on `pre_llm_call` (after the\nfact of the producing call), best-effort per ring CURSOR\n`(ts, event-count)` — same-second events still deliver; a transient store\nfailure after the at-most-once marker skips that cursor's delivery. Hermes\ndelivery's namespace follows the hook chain `ZMEM_MCP_NAMESPACE` →\n`ZMEM_NAMESPACE` → `user:global` (issue #71 review: one chain for prefetch,\nrecall, and correction capture — Hermes hook events themselves carry no\nnamespace); project-scoped operation context delivers\non the coding-host PreToolUse surface. All query-context persistence\n(rings, delivery markers, and the session delivery ledger) lives under `\u003Cdata>\u002Fops\u002F`\nsidecars and never grows the store's tables. Codex pre-tool injection is\nWIRED (issue #95): `hooks.codex.json` registers `PreToolUse` with matcher\n`Bash|apply_patch` — dumped live from codex-cli 0.153.0 (Windows,\n2026-09-09: shell operations emit the hook tool name `Bash`, file patches\nemit `apply_patch`; Codex treats an all-alphanumeric\u002Fpipe matcher string as\nEXACT alternation, so it matches precisely those two names). MCP tools\n(`mcp__\u003Cserver>__\u003Ctool>`) and `write_stdin` are deliberately OUT — pre-tool\nrecall derives its query from shell\u002Ffile-patch inputs, not arbitrary MCP\narguments. Upstream accepts `hookSpecificOutput.additionalContext` on\nPreToolUse (model-visible, non-blocking); Codex hooks reference:\nhttps:\u002F\u002Flearn.chatgpt.com\u002Fdocs\u002Fhooks. Codex envelopes are additionally\ncapped at 8000 encoded bytes (≈2000 tokens at the plugin's 4-chars\u002Ftoken\nestimator; dense multi-byte (CJK) content has less real headroom) — 20% margin under upstream's 2,500-token hook-output spill\nlimit (`DEFAULT_HOOK_OUTPUT_TOKEN_LIMIT`, codex-rs output_spill.rs,\nverified against tag rust-v0.153.0); the cap applies even when an operator\nsets a larger `ZMEM_CTX_BUDGET`. The unset `ZMEM_CTX_BUDGET` default is\nhost-dependent: 25000 on the ZCode launcher path, 9000 on the Claude Code\npath and the ZCode non-launcher path. Keep the verification-first convention: re-probe\nwith a live tool_name dump before changing the matcher.\n\nInject surface parity (host facts, not aspirations): Claude Code registers\nSubagentStart (task-text recall — issue #119, probe 2026-09-10: neither\nhost's SubagentStart payload carries task text, so the query ladder is\npayload-field-if-present → the delegating prompt parked store-side by the\nparent's PreToolUse(`Agent`) call when the event yielded nothing (issue\n#253) → the queryless recent selector last). The store-side bridge restores\nthe #119 intent inside the #158 architecture: the hook body passes the\ndelegating prompt to the store subprocess it already makes\n(`--delegation-prompt`, argv surface bounded at 4000 chars so the store can\nredact before capping the parked bytes at 800, never a storelib import in\nthe adapter), the CLI parks it via `park_task_text`\n(secret-redacted through the canonical `redact_text`, advisory and\nfail-open; 16-entry cap, and a stale entry stops being CONSUMED after the\n6h window — that prune is in-memory only; issue #336 (0.124.0) added the\ndisk bounds: the TERMINAL session-end clear now unlinks the stash beside\nthe data dir it serves (the mid-session `ledger-clear` keeps it), an\nall-expired consume unlinks immediately, and the real bound on untrusted\nhost prose on disk remains the ops age sweep,\n`ZMEM_SENTINEL_SWEEP_DAYS`, default 7 days; `purge`\nscrubs the stash by needle and reaps orphaned `*.tasktext.tmp.*`;\n0600-at-open + atomic replace, with owner-only access on Windows resting\non the operator-profile ACL rather than chmod; since 0.138.0 written\nWITHOUT fsync (#340), so a host crash can lose the whole sidecar (the\nreader discards an unreadable stash) or re-deliver an already-consumed\ndelegation — disclosed, regenerable-cache posture), and the selector consumes it\nFIFO at the subagent moment only when the payload ladder produced no query.\nCorrelation key (issue #428, 0.148.0): a host that names the child's id in\nthe delegating call (`tool_input.agent_id`, forwarded by the hook body as\n`--delegation-agent-id`) parks a TAGGED entry, and the child's SubagentStart\n`agent_id` (forwarded as `--consume-agent-id` beside `--consume-tasktext`)\ntakes exactly its own tagged prompt regardless of age or newer siblings.\nIds are normalized identically on both sides (string, stripped, 1–128\nchars, no control characters, no leading `-`; anything else is treated as\nabsent). RESERVATION RULE: a consume without a matching id never takes,\nages out, or counts a TAGGED entry — the FIFO + aged-head rule below runs\nover UNTAGGED entries only. No probed host supplies an id at PARK time\ntoday (Claude Code assigns the child id at SubagentStart), so every\nproduction stash is untagged and the rule below is byte-identical to\n0.138.0; the id the child carries simply finds no tag and falls through.\nFor untagged entries FIFO is the\ncorrelation rule and out-of-order arrival of truly concurrent children is\nthe documented limitation; the consume lands before the selector runs\n(#337), so a selector failure after a successful consume loses that\ndelegation (disclosed residual); the accepted (unlocked, #330) races are the\nparallel-park last-writer-wins (jittered N=8 parks left 2-7 survivors;\nCORRECTED from \"drop-not-swap\" in 0.138.0), a parallel-consume overlap\n(two concurrent children can load the same head entry before either\nrewrite lands — on UNTAGGED entries both receive that same delegation\n(duplicated delivery — the text is served twice, not lost); on TAGGED\nentries each consume drops its own index and rewrites the whole file, so\nthe loser's stale rewrite resurrects the winner's already-consumed entry\n(last-writer-wins; a tagged entry stays inert until its own id arrives);\ndegraded relevance only, never a\ncrash), and the parked-against-consume overlap in BOTH directions: a fresh park\nloses its entry to a concurrent rewrite (unlink-wipe) or an\nalready-consumed entry is re-delivered (resurrection). An ORPHANED park\n— a delegation whose child never starts (permission denied, cancelled,\ntool errored) — would sit at the FIFO head and shift EVERY later child's\nprompt by one; since 0.138.0 the consume applies an AGED-HEAD SKIP\n(issue #329) over untagged entries: a leading entry older than 300 s is\ndropped in favor of a strictly newer one (an exact agent_id match still\noutranks the heuristic; malformed timestamps are never aged; a sole entry\ninside the 6h window is always served — the window prune still applies).\nTwo residuals stand for UNTAGGED entries (issue #329, maintainer-ratified\n2026-11-02): an orphan younger than 300 s still shifts by one until it\nages out, and a live child delayed past 300 s can lose its prompt to a\nstrictly newer delegation - a failure mode the heuristic introduces\n(pre-0.138.0 a delayed child consumed its own head entry). A host that\nsupplies the correlation id on both sides retires both residuals for its\ndelegations (issue #428); the 300 s band is unchanged. PR #453 review\nresiduals, disclosed: the 16-entry park cap evicts UNTAGGED entries first\n(a tagged entry is only evicted when the stash holds no untagged entry to\ndrop — an ALL-TAGGED stash over the cap still drops its oldest), and a\nTAGGED entry with a corrupted unparseable timestamp survives the 6h\nwindow prune (park always writes a finite timestamp; bounded by the cap,\nthe session-end unlink and the ops age sweep). The 6h window and\n16-entry cap bound how long any\nshift lasts, not how many children it affects. Since 0.138.0 the consume\nis CALLER-GATED (issue #339): only the SubagentStart hook lane\n(`--consume-tasktext` on its recall\u002Frecent argv) and the fleet broker's\nSubagentStart prefetch are INTENDED to pass the grant; the gate is\ndefault-off but CALLER-ASSERTED — any caller (including any\nauthenticated hub principal via the MCP prefetch param) may self-grant,\nso the boundary is convention, not enforcement (the stash is keyed by\nsession id only; trusted-fleet assumption). An ungranted `--moment\nsubagent` empty-query caller is byte-identical to a no-stash session. `ZMEM_QUERY_CONTEXT=0` or `ZMEM_INJECT=0`\ndisables parking via the early exit. A bridged child is a fresh context:\nits pull drops the delivered-derived exclusions entirely, so the parent's\npretool deliveries cannot starve it and a fresh child re-reads rows an\nearlier sibling already rendered (over-delivery of already-seen rows,\nnever starvation — deliberately NOT narrowed to sibling-moment rows,\nwhich would let one sibling starve the next; issue #331). Claude's matcher remains\n`Edit|Write|MultiEdit|NotebookEdit|Bash|apply_patch|Agent|Task` since 0.31.0 (apply_patch added later; #338) — issue\n#119; `Task` is accepted as the pre-rename delegation tool name per community\nissue 29677, closed stale and not vendor-confirmed. Claude Code also registers\nPostToolBatch (issue #120, 2026-09-11 — post-edit checkpoint recall, the\nbatch sibling of PreToolUse): after one completed batch, the hook parses\nONLY `tool_uses[].name` plus `input.command` \u002F `file_path` \u002F\n`notebook_path` \u002F `path` (plus the singular `tool_name`\u002F`tool_input`\ncompatibility shape), bounds every retained field to 150 chars and the\njoined query to 500, derives the existing operation tokens, and makes ONE\n`store.py recall --for-injection --no-bump` decision with the #117 ledger\nexclusion — raw `tool_response`\u002F`result` payloads are never parsed, stored,\nor logged (the decision log carries tool NAMES and path BASENAMES only).\nThe internal mode is `posttoolbatch`, but the runtime moment recorded in\nthe decision log and ledger is the closed-set `pretool` — no\n`posttoolbatch` moment is emitted anywhere. PostToolBatch is deliberately\nUNREGISTERED on Codex (no such upstream event exists in codex-rs hooks as\nof the 0.153.0 probe) and is a documented host gap on ZCode (see the\nseven-event note below); no lane reads `tool_response` — convention-capture\non PostToolUse parses the same tool_name\u002Ftool_input-shaped fields, not the\nresponse payload. Codex registers SessionStart,\nUserPromptSubmit,\nPreToolUse (matcher `Bash|apply_patch`, probe 2026-09-09, codex-cli\n0.153.0), PostToolUse (matcher `Edit|Write|MultiEdit|NotebookEdit|Bash|apply_patch`,\nper the shipped `hooks\u002Fhooks.codex.json` manifest), Stop, SubagentStart,\nSubagentStop, PreCompact, and SessionEnd —\nthe Codex DELEGATION tool surface is UNVERIFIED by #119 (2026-09-10): the\n#95 dump covered only the shell and patch\u002Fapply tools and cannot say\nwhether a delegation facility fires PreToolUse at all, so no matcher entry\nwas added and Codex SubagentStart rides the payload-field \u002F recent rungs\n(there is no transcript-tail rung — that parser is retired; a fresh\ndelegation-tool dump is deferred to the live-probe owner #96).\nUpstream drops `additionalContext` on PreCompact (decision control only,\nverified 2026-09-09 from codex-rs source), so zmem's Codex PreCompact\nentry exists to clear the delivery ledger before compaction;\npost-compaction re-injection rides\nthe registered SessionStart, which upstream fires with `source=compact`\nafter every compaction. PostCompact stays UNREGISTERED on Codex (issue\n#118 decision, 2026-09-10: upstream Codex PostCompact carries only\n`trigger: manual|auto` — no compact_summary, so there is nothing to\nstash). The compact branch (issue #158): SessionStart branches on `source ==\n\"compact\"` only — the launcher exports the payload field as\n`ZMEM_SESSION_SOURCE`, clears the delivery ledger at PreCompact, and then\ncalls the store-owned selector with moment `session_start` on the next\nSessionStart. The local decision log may retain\n`moment=session_start_compact` for diagnostics, but that label is never sent\nto storelib. There is no compact-summary sidecar or special second budget.\nThe payload block lives in\n`hooks\u002Flib\u002Fzmem-session-start-payload.py` — NEVER inline it back as\n`python -c`: the string outgrew the Windows ~32K CreateProcess\ncommand-line limit and silently degraded the hook to `{}`. Whether the PreCompact fence itself survives a live\n`\u002Fcompact` is UNPROBED — no claim either way until #96's live canary\nlands (the host-capability rot convention from #103\u002F#104). **ZCode supports exactly\nseven hook events — SessionStart, UserPromptSubmit, PreToolUse,\nPermissionRequest, PostToolUse, PostToolUseFailure, Stop — so SubagentStart,\nPreCompact, and PostCompact are host gaps on ZCode** (an unsupported event name would be\ndead config under the host's strict schema, so they are documented here\ninstead of registered; likewise ZCode's PreToolUse matcher deliberately\nomits `Agent` — with no SubagentStart event, event-provided task text has no\nconsumer). If ZCode grows either event, wire\n`zmem-subagent-recall.sh` \u002F `zmem-precompact.sh` \u002F `zmem-postcompact.sh`\nimmediately. PostToolBatch shares that ZCode gap (issue #120): until the\nhost grows the event, `zmem-posttoolbatch-recall.sh` stays\nClaude-registered only, and the launcher's translation mapping for the\nverb is inert on hosts whose manifest omits the entry; if ZCode grows the\nevent, wire `zmem-posttoolbatch-recall.sh` immediately (same convention).\n\n#### Decision-point checkpoints (REQUIRED skill contract) — #85 direction E\n\nWhen an agent is about to run one of the named hazardous operations, the\nskill\u002Fworkflow driving it MUST first run an explicit recall and treat any\nhit as blocking review (read the lesson before proceeding; do not skip it\nbecause the task feels urgent):\n\n- before `git stash pop` \u002F any stash-consume — `recall --query \"git stash\n  pop foreign stash conflict\" --include-global` (a blind pop can apply a\n  foreign stash)\n- before `git reset --soft` (squash assembly) — `recall --query \"git reset\n  soft origin main stale tree\" --include-global` (a fetch may have moved the\n  base)\n- before `git push` — `recall --query \"git push stale tree fetch rebase\n  verify\" --include-global` (verify the tree against the fetched base first)\n- before editing any file named by a stored citation\u002Fratchet lesson —\n  `recall --query \"\u003Cpath basename> ratchet citation re-pin\" --include-global`\n  (cited-file edits have local gate batteries)\n\nThese checkpoints complement the pre-tool hook; they are not a substitute\nfor it (an agent improvising a raw command is exactly who the hook covers).\n\n#### Schema forward-compatibility (issue #65 follow-up)\n\nA client refuses a store whose `schema_version` is above its ceiling.\n`FORWARD_COMPAT_SCHEMA_VERSION` (schema_meta) is that ceiling: a maintenance\nrelease of an older client line may raise it to an ADDITIVE-ONLY newer schema\n(new side tables, no `memory` changes), letting the older client keep storing\nand recalling memories — with a one-time stderr NOTICE — until it updates.\nAbove the ceiling the refusal names the update, and\n`ZMEM_ALLOW_NEWER_SCHEMA=1` overrides at the operator's own risk.\n\n#### Schema 16 — entity kinds v2 (issue #175, UPGRADE-REQUIRED)\n\nSchema v16 widens the `entity` kind vocabulary from five to exactly twelve\nkinds, enforced by a SQL CHECK: `person`, `project`, `tool`, `preference`,\n`host`, `service`, `model`, `repo`, `file`, `command`, `error_class`,\n`other` (schema_meta `ENTITY_KINDS`; the deterministic extractor admits each\nkind as a `kind:Name` tag prefix). The v15→v16 migration REBUILDS the\n`entity` table (create `entity_v16` with the new CHECK, copy every row, drop the old\ntable, recreate `entity` with the new CHECK, copy back) in one BEGIN\nIMMEDIATE transaction with `PRAGMA foreign_key_check` before commit; every\n`entity`, `entity_alias`, and `memory_entity` row\nsurvives unchanged. Since 0.136.0 every entity-kind CHECK is rendered from\nthe single `schema_meta.ENTITY_KINDS` authority (issue #356) — no DDL site\nhand-copies the vocabulary — and the schema lock gained holder-liveness\n(issue #357): the first open after an upgrade runs the rebuild under the\nlock (expect seconds, proportional to entity count; later commands wait up\nto `ZMEM_SCHEMA_LOCK_WAIT_SECONDS` then refuse with a clear message), while\na holder that DIED mid-migration has its lock broken automatically by PID\nliveness (SQLite rolls back the uncommitted transaction, so the store is\nnever half-migrated). Recovery path for a bad migration: stop sessions,\n`store.py restore` the newest good snapshot from `ZMEM_BACKUP_DIR`\n(default `\u003Cstore dir>\u002Fbackups`), re-run; `import-store.py` only replaces\nits destination after every acceptance gate passes. This is NOT an additive bump: a changed SQL constraint is\noutside the forward-compat window, so per the v16 contract\n`SUPPORTED_SCHEMA_VERSION` is 16 while `FORWARD_COMPAT_SCHEMA_VERSION`\ndeliberately stays 15 — every client older than the v16 release REFUSES a\nmigrated v16 store (the actionable refusal\nnames `ZMEM_ALLOW_NEWER_SCHEMA=1`, the explicit operator override that\nproceeds at the operator's own risk and never downgrades or migrates the\nstore). The #172 taxonomy decision for `entity_card` is REJECT: that memory\ntype does not exist and `ALLOWED_TYPES` is unchanged by this release.\nThe #172 decision was reject; the #179 workstream contract reverses it and\nships the type — cards are born only through the refresh path. Since 0.107.0\n`ALLOWED_TYPES` carries `entity_card` (the #175 kinds precedent: the RFC map\nis a frozen record, not a veto); see \"Entity cards (issue #179)\" below for\nthe full card contract.\n\nSchema 18 (issue #178) is purely additive (the `event` type plus the\n`memory_occurrence` side table — no `memory` column or constraint change),\nso the forward-compat window REOPENS: `SUPPORTED_SCHEMA_VERSION` is 18 and\n`FORWARD_COMPAT_SCHEMA_VERSION` is 18. Clients whose forward ceiling is\nstill 15 (every 0.10x-era install) refuse a migrated v18 store until\nupdated — on shared-store machines, update every zmem client before the\nfirst v18 client opens the store (`ZMEM_ALLOW_NEWER_SCHEMA=1` is the\nstopgap; see CUTOVER.md and the CHANGELOG operator note).\n\nSchema 19 (issue #288) adds the `applied_ops` op-queue table —\npurely additive, so SUPPORTED and FORWARD both moved to 19. The compat\ngate refuses any store newer than the client's FORWARD ceiling: a\n0.116-era client (FORWARD 18) REFUSES a v19 store with the standard\n`ZMEM_ALLOW_NEWER_SCHEMA=1` message — update all zmem clients on\nshared-store machines before the first v19 client opens the store\n(a FORWARD-15 client refuses it the same way).\nNUMBERING: #179's `entity_card` bump owns schema 17 as a STAMP-ONLY step\n(no DDL); this PR's `memory_occurrence` DDL therefore renumbered to 18 at\nrebase — one integer can never carry two schema definitions, or the\n`ver \u003C N` guard would skip the DDL on every store a released client had\nalready stamped.\n\n#### Injection token budget (issues #65 10.9, #116)\n\n`ZMEM_INJECT_TOKEN_BUDGET` (default 1500, measured at 4 chars\u002Ftoken — no\ntokenizer is bundled) is a HARD CEILING on the memories injected by the hooks\n(UserPromptSubmit \u002F PreCompact \u002F SubagentStart \u002F SessionStart Tier 2) and by\nthe `session_start` MCP\u002FHermes tools. Admission charges each row its FULL\nrendered fence contribution plus the fence shell, with the same estimator the\nreporting uses — the rendered fence never exceeds the budget. When the\nceiling is hit, admission skips past rows that do not fit (a later smaller\nrow is still admitted); `decision`\u002F`constraint` rows are never dropped while\nthey can carry information: a row that fits renders whole, one that does not\nis TRUNCATED with an explicit `…[budget-truncated]` marker, and only a row\ntoo large for even a minimal stub is dropped — never silently. When anything\nwas omitted, the fence carries a machine-readable line\n`# [budget: dropped N rows, truncated M]` and the shared hook body's\ndecision line (UserPromptSubmit \u002F PreCompact \u002F SubagentStart \u002F SessionStart\ntier-2 python path) reports `tokens=\u003Crendered>\u002F\u003Cbudget>` plus the distinct\nlabeled fields `rendered_estimate=`, `admission_budget=`, `budget_dropped=`,\n`budget_truncated=`, `budget_dropped_protected=` (the same order as the\nfield-order list above; emitted only when budget accounting ran). The\nseparate `zmem-session-start.sh` bash writer keeps its legacy\n`tokens=\u003Cused>\u002F\u003Cbudget>`-only line. Every read `--json` envelope reports\n`tokens_used`\u002F`tokens_budget` and, on the `--for-injection` lane,\n`budget_dropped`\u002F`budget_admission`\u002F`budget_truncated`\u002F\n`budget_dropped_protected`\u002F`budget_note`. The B-1 report (`doctor --miss-rate`) counts\n`over-budget N` decisions so the ceiling can be verified on a live log\nwindow. `ZMEM_CTX_BUDGET` (character cap) remains the hard outer truncation\non the rendered block.\n\n`--include-global` (opt-in) ALSO surfaces up to `--global-limit` query-relevant\nrows from the `user:global` tier, merged project-first so a global row never\ncrowds out a project row. The three automatic hooks pass this so cross-project\nlessons reach project-scoped sessions. Without it, behaviour is strict-namespaced\n(byte-identical to before). When you want the global tier unioned in but still\nwant a per-tier budget, use `recall --namespace project:\u003Cx> --include-global`\nrather than going unscoped.\n\n#### Scoped five-tier recall (issue #167)\n\nOrdinary implicit `recall` and `recent` calls without `--namespace` resolve the\ncurrent project, fleet, and host through the shared #166 scope resolver and use\nfive independent reservations in this order; hostnames are normalized to\nlowercase for `host:` scope lookup:\n`project`, `domain`, `fleet_host`, `cross_project`, `user_global`. The default\nslot caps are `5\u002F2\u002F2\u002F2\u002F3`. Library callers opt in explicitly with a `scopes=` map\non `recall_memory`, `recent_memory`, or the read-only `explain_recall` API; the\nresolver's `agent` value is ignored. `user_global` is admitted only when\n`include_global=True`.\n\n`ZMEM_TIER_SLOTS` overrides the caps per call as exactly five comma-separated\nASCII nonnegative integers in that order. Empty, malformed, signed, or Unicode\nnumerals fail before the scoped pipeline opens a SQL lane. The scoped\n`cross_project` slots remain reserved until a public admission policy is\navailable. While no policy is wired, recall and recent skip the unfiltered\ncandidate scan. This is separate from the legacy #98\n`--include-cross-project` hazard lane. Scoped rows are stable-deduplicated by id\nand carry their tier in JSON; generic fenced output prefixes `[tier=\u003Cname>]`,\nand a tierless generic row is explicitly prefixed `[tier=unknown]`. Ordinary\nplain-text recall\u002Frecent output includes that marker too. Legacy passive\ninjection retains its established tierless wire bytes. The legacy\n`tier=cross` renderer marker remains a suffix. Explicit `--namespace`, search,\nhook, and injection calls retain their legacy route and limit arguments\n(issue #235 adds only the gated-moment `user_global` relevance floor on top:\nbelow-floor global candidates are withheld before the merge, the slot stays\nempty, and the envelope reports the count as `global_withheld`).\nOn implicit ordinary recall and recent, `--include-global` opts into the\nscoped `user_global` reservation and preserves tier labels. Explicit\n`--namespace` calls retain their legacy union behavior; `--include-cross-project`\nretains its legacy hazard semantics. Direct scoped\n`recall_memory`\u002F`recent_memory` calls reject `for_injection=True`; scoped explain\nremains read-only and accepts it. Programmatic callers use `scopes=` and\n`include_global=True` for the same labeled global tier.\nThe `domain` reservation is available to programmatic callers that supply a\ndomain scope; shipped CLI, hook, and MCP resolvers do not currently create one.\nScoped recall\u002Frecent reject the legacy `include_cross_project=True` flag.\n\n#### Cross-project hazard tier (issue #98)\n\nA fourth, precision-gated tier (`--include-cross-project`, wired automatically\non the passive injection surface) can deliver up to 2 live rows from FOREIGN\n`project:*` namespaces — a lesson another project already paid for, surfaced\nexactly when you are about to repeat its incident. Admission requires ALL of:\n\n- the running operation is hazardous: the derived ops tokens\n  (`derive_ops_tokens`, the `#88`\u002F`#123` allowlist) whole-token-intersect the\n  hazard-verb set — `ops_tokens._HAZARDOUS_SUBS` (`git push\u002Freset\u002Fstash pop\u002F\n  rebase\u002F...`) by default, overridable via `ZMEM_CROSS_PROJECT_HAZARD_VERBS`\n  (comma-separated, trimmed, case-folded, de-duplicated; unknown or empty\n  verbs are dropped with a one-shot stderr warning and an override with no\n  usable verb falls back to the default set);\n- the row's `signal` is grounded: one of `test`, `compile`, `lint`,\n  `reviewer`;\n- the row passes the standard score\u002Fconfidence floor (#113) and is live\n  (`superseded_at IS NULL`);\n- the row's namespace matches `project:*` and is OUTSIDE the current\n  project's alias set (`user:global` rows stay in their own tier).\n\nSurface policy (`ZMEM_CROSS_PROJECT`): unset → `pretool` only (PostToolBatch\nmaps to the `pretool` moment); `0` → off everywhere (wins even over an\nexplicit `--include-cross-project`); `1` → `pretool` and `user_prompt` (on an\nenv-enabled `user_prompt` surface the store-side selector derives ops tokens\nfrom the prompt event itself — the #158 hook boundary keeps the hook a thin\nflag forwarder); any other non-empty value → `pretool` only plus a one-shot\nstderr warning. The tier is query-time — the queryless `recent` pull never\nadmits cross rows.\nScoped MCP principals keep their `ops_tokens`; the selector's cross-project\nlane intersects its foreign namespace set with the principal's authorized\nallow-list (issue #283), so foreign projects outside the allow-list are\nexcluded before rendering, context construction, or delivery-ledger\nupdates, while allow-listed cross-project rows can be admitted when\noperation tokens ground them.\n\nNo-copy rule: a cross row is never copied, rewritten, or mirrored — the\nstore's own row renders in place, inside the untrusted fence, tagged\n`[ns=\u003Csource namespace>] [tier=cross]` (with `tier: \"cross\"` on the JSON\nrow), and cross rows never consume project or global slots. Delivered cross\nrows count once in `surfaced_count` under the same telemetry law as every\nother tier. The #155 real-corpus replay baseline has landed: the lane ships\nwith these conservative defaults, and the predeclared measurement record\n`eval\u002Freal-corpus-2026-09-19.json` (see the read-only replay evaluator section)\nis the calibration reference — an audit over a frozen cohort, not a live\nefficacy claim. `recall --explain`\ndoes not include the cross tier (the read-only debugger predates it and is\nnot extended by #98).\n\n**Hybrid is the DEFAULT when embeddings are available** (issue #58 3.3): the\nquery is embedded and matched against stored embeddings (sqlite-vec KNN),\nthen both lanes' rankings are fused with Reciprocal Rank Fusion (RRF, k=60).\n`--hybrid` is kept as an explicit no-op alias for older invocations;\n`--no-hybrid` forces the lexical (FTS5\u002FBM25-only) lane. The embedding\nruntime (onnxruntime + tokenizers, model lazy-downloaded and\nchecksum-verified) is optional and fails open: when it is unavailable,\nrecall silently uses plain keyword ranking — never an error.\n\n**Entity matching is the THIRD RRF list and needs no model** (v10, issue #60\n5.3): at recall the query is run through the same deterministic entity\nextractor used on write, plus plain query tokens are matched against stored\nentity aliases. Memories linked to a matched entity join the fusion as a\nthird ranked list (rank = number of matched entities, then recency). This is\nhow the alias `rg` recalls `ripgrep`-linked memories even when BM25 terms\nmiss — and it is live on model-absent stores by default. An unknown alias is\nsimply an empty third list; the lexical (and, when available, vector) lanes\nfuse exactly as before.\n\n**MMR diversity is the DEFAULT** (v10, issue #60 5.5): after RRF fusion and\ncomposite scoring, the candidate set is re-ordered with Maximal Marginal\nRelevance before `--limit` is applied, so a cluster of near-paraphrase\nduplicates cannot crowd out a distinct fact. Similarity is embedding cosine\nwhen both rows have embeddings, else Jaccard on normalized content tokens —\nso MMR also works model-absent. `--no-mmr` returns pure composite-score\norder (four paraphrases instead of three + the distinct fact). The tradeoff\nknob is lambda: default 0.7, env `ZMEM_MMR_LAMBDA` (0.0 = maximize\n\n**Embedding profiles & rerank** (`ZMEM_EMBED_PROFILE`, `ZMEM_CROSS_ENCODER`,\n`ZMEM_CROSS_ENCODER_MODEL`): `ZMEM_EMBED_PROFILE` selects a row from the\nregistry in `skills\u002Fmemory\u002Fscripts\u002Fembed_profiles.py` — unknown values refuse\nwith exit 2 before any store work, and a value whose dimension differs from\nthe store's committed vectors refuses until `reembed --all` converts it.\nThe cross-encoder rerank is enabled by default (issue #126 flip); set `ZMEM_CROSS_ENCODER=0` to disable it. The\nEXPLICIT lane covers CLI `recall` invocations that are not `--no-bump`\u002F\n`--no-hybrid` (never `recent` \u002F `search`-aliases); pointing\n`ZMEM_CROSS_ENCODER_MODEL` at a LOCAL pair-scoring `.onnx` plus sibling\n`tokenizer.json` keeps the operator-vouched load path. Issue #125 adds a\nchecked-in `mini-pair-scorer` profile\n(`skills\u002Fmemory\u002Fscripts\u002Fcross_encoder_profiles.py`, exact SHA-256 pins for\nBOTH the model and its tokenizer) used\nwhen `ZMEM_CROSS_ENCODER_MODEL` is unset — resolved from `ZMEM_MODELS_DIR`\nor the shared models dir, and loaded ONLY after the digest of the exact\nBUFFER each runtime object is constructed from matches its profile pin at\nscorer BUILD time (#418; there is no verify-then-reopen window — the\npath-level `verify_profile_file` helper is a diagnostic API, no longer the\nload gate); `ZMEM_CROSS_ENCODER_MODEL_URL` plus exact\n`ZMEM_MODEL_AUTODOWNLOAD=1` permits one digest-verified, atomic-download\nattempt at scorer-load time. A second opt-in, `ZMEM_CROSS_ENCODER_PASSIVE=1`,\nlets the PASSIVE injection lane score its final admitted set through\n`rerank_final_injection_set` (issue #125) in shadow-only mode:\n`ZMEM_CROSS_ENCODER_SHADOW=1` logs bounded rank deltas to\n`${ZMEM_DATA}\u002Fcross-encoder-shadow.jsonl` and the lane's order is unchanged\nuntil the recorded promotion gate (`PASSIVE_PROMOTION_GATE`:\n#111 precision > 0.8978333333333333, p95 \u003C= 250 ms, #129 false-injection \u003C= 0.0) is\nmeasured and flipped. Every rerank attempt is budgeted (250 ms default,\n`ZMEM_CROSS_ENCODER_BUDGET_MS`) and emits exactly one terminal\n`[zmem] cross-encoder reason=\u003C...>` line on stderr; any failure degrades to\nun-reranked results and rerank can never fail a recall. There is still NO\nunverified-load escape hatch (`ZMEM_MODEL_ALLOW_UNVERIFIED` does not\nexist).\ndiversity, 1.0 = no diversity — identical ordering to `--no-mmr`).\n`--no-mmr` and `--no-hybrid` are independent flags and can be combined.\n\nRecall rows carry **entity cards**: `recall --json` rows include\n`entities: [{id, kind, name}]`, and the fenced hook render shows at most\nthree entity NAMES per row (never ids). `get --id` shows the same links.\n\n**1-hop link expansion is the DEFAULT** (v11, issue #61 6.3): after MMR,\neach recalled memory's `related`\u002F`supports` links are walked one hop and\nup to `--link-budget` (default 2) extra neighbor rows are appended — a\nmemory that never matched the query but is associative-neighbor of a\nmatch still surfaces. `contradicts` neighbors are included only when\nthey survive the confidence floor and are tagged `[CONTESTED LINK: \u003Cid>]`\nin the fenced render — the id names the opposing row, the parent the\nneighbour was expanded from (`contested_link: true` + `link_of` in JSON;\nissue #254). `contradicts` edges are stored in both directions, so the\nnamed id is one end of the contradiction, not a fixed direction; when\nthe id is not a single safe token the bare `[CONTESTED LINK]` renders\ninstead, and a named row that a floor or the token budget dropped from\nthe fence can appear in a marker without its own bullet. Expansion rows\ncarry\n`link_relation` \u002F `link_of` \u002F `link_score` \u002F `contested_link` keys; rows\nthat matched the query themselves never carry them, and expansion rows\nnever advance telemetry (popularity rewards query matches, not link\nneighbors — so expansion cannot distort later rankings). On the passive\ninjection lane an already-delivered parent contributes no neighbours\n(`exclude_ids` seed skip, issue #250); the plain recall path is unaffected\nbecause its pre-filter has already removed those rows. `--link-hops 0`\ndisables the walk; `--link-budget 0` is equivalent. `search` and `recent`\nnever expand.\n\n`--no-bump` (opt-in) makes a recall **passive**: it records a surface event\n(`surfaced_count`\u002F`last_surfaced`) instead of advancing `retrieval_count`\u002F\n`last_retrieved`. The explicit-vs-passive split is the system contract: the\nthree automatic hooks and the Hermes provider prefetch are passive surfaces\n(a background injection is not a usefulness signal); explicit recall — this\nCLI without `--no-bump`, the MCP `recall`\u002F`search` tools, and the Hermes\n`MemoryProvider` search tool — intentionally bumps `retrieval_count`, because\nan explicit read IS evidence the memory was useful (issue #21).\n\nIssue #114 sharpened that contract in two ways. First, the automatic hooks and\nSessionStart now pass `--for-injection` (with `--no-bump`): the selective\ninject gate and the token budget run INSIDE that one store call, the returned\nrows are exactly the rendered set, and `surfaced_count` advances only for\nrendered QUERY-MATCHED rows (link neighbors render but never count; unfold\nis explicit-recall-only and never runs on this lane) —\none subprocess, one decision, no second ack process. Second, ranking\npopularity now reads `retrieval_count` ONLY: passive surfaces are still\nrecorded (promote\u002Fprune\u002Fconsolidate consume them) but they no longer feed the\ncomposite score, so passive pulls cannot inflate their own ranking (the\nlive-store shape `surfaced_count=371, retrieval_count=0` was this loop). The\nweight itself is unchanged; #124 will repoint it at applied\u002Fviolated counters.\n\n#### Store-owned passive selector and envelope — issue #158\n\nAll passive consumers use the single storelib entry point\n`select_and_budget_for_injection`. It owns selection, delivery-ledger\nexclusion and recording, pre-tool operation-token composition, the hard token\nbudget, and the canonical untrusted fenced rendering. The recall hooks,\nSessionStart, and Hermes provider are subprocess-only adapters: they consume\nonly the envelope's string `rendered` field and never read SQLite, the\noperation ring, a delivery ledger, or memory rows themselves. The selector's\nclosed moments are `session_start`, `user_prompt`, `pretool`, `subagent`, and\n`precompact`; its lanes are `claude`, `codex`, `zcode`, `deepseek`, `hermes-provider`,\nand\n`hermes-compat`. A compact restart is logged locally as\n`session_start_compact`, but the selector and ledger always use\n`session_start`.\n\nThe exact session-aware CLI forms are:\n\n```\n\u003Cinterpreter> \u003Cstore.py> recall --query \"\u003Ctext>\" --for-injection --json \\\n  --session-id=\u003Cid> --moment \u003Csession_start|user_prompt|pretool|subagent|precompact> \\\n  --lane \u003Cclaude|codex|zcode|hermes-provider|hermes-compat> \\\n  [--ops-token \u003Ctoken>]... [--authorized-namespace \u003Cns>]...\n\u003Cinterpreter> \u003Cstore.py> recent --for-injection --json \\\n  --session-id=\u003Cid> --moment \u003Csession_start|user_prompt|pretool|subagent|precompact> \\\n  --lane \u003Cclaude|codex|zcode|hermes-provider|hermes-compat> \\\n  [--ops-token \u003Ctoken>]... [--authorized-namespace \u003Cns>]...\n\u003Cinterpreter> \u003Cstore.py> ledger-clear --session-id=\u003Cid> [--principal \u003Cid>] [--harness \u003Cid>]\n\u003Cinterpreter> \u003Cstore.py> delivery-clear --session-id=\u003Cid> [--principal \u003Cid>] [--harness \u003Cid>]\n\u003Cinterpreter> \u003Cstore.py> closeout-ack --session-id=\u003Cid>\n```\n\nThe passive `recall` and `recent` commands accept the additive attribution\nflags `--session-id=\u003Cid>`, `--moment`, `--lane`, and repeatable `--ops-token` when\ncalled with `--for-injection --json`, plus the payload flag\n`--delegation-prompt` (issue #253), `--consume-tasktext` (issue #339:\nSubagentStart-lane-only grant of the store-side task-text consume), the\nissue #428 correlation ids `--delegation-agent-id` (tags the park) and\n`--consume-agent-id` (also on `prefetch`; selects the tagged entry) — both\nrefused by name in remote mode (`ZMEM_REMOTE_URL` set), as is recall's\n`--state-hint`, because the hub read tools carry none of them and the fleet\nlane never parks — and\nrepeatable `--authorized-namespace`\n(issue #283: the caller's namespace allow-list — the selector's\n`user:global` and cross-project lanes are filtered to it; omitted means\nunscoped). On the pretool passive lane, recall additionally accepts\n`--state-hint dirty|clean` (issue #397): it REQUIRES the passive shape\n(`--for-injection --json --session-id`) and `--moment pretool` on recall —\neither missing is a stable exit-2 refusal — and is refused on any moment\nother than pretool; the hook is the intended caller. `--delegation-prompt` is honored ONLY on\n`--moment pretool` with a session id, is secret-redacted before it touches the\nsidecar, and exists to park the delegating Agent\u002FTask prompt for the subagent\nmoment; every other moment ignores it. An empty query dispatches to recent\nselection. An omitted `--ops-token` list lets the store read the pre-tool ring;\nthe ring is composed only for `pretool`, not for UserPromptSubmit or other\nmoments. For passive `recall` and `recent`, a session id requires a moment, and\na moment requires a session id. The equals form keeps session ids beginning\nwith `-` unambiguous. `ledger-clear --session-id=\u003Cid>` clears one\nsession's delivery ledger without opening SQLite. `delivery-clear\n--session-id=\u003Cid>` clears the ledger and legacy pending sidecar for SessionEnd\ncleanup, also without opening SQLite; both commands are idempotent when their\ntarget sidecars are absent.\n\nThe old hook-owned pending and compact-summary sidecars are no longer\nproduced or consumed by these adapters (task-text parking returned in 0.98.0\nas a STORE-side bridge — issue #253: the pretool store call parks via\n`park_task_text`, the selector consumes at the subagent moment; the hook\nadapters themselves still never touch any sidecar). The D-2 #118\ncompaction-snapshot handoff is documented above. This reflects the ownership\nhistory: issue #117 superseded the pending sidecar\nwith the per-session delivery ledger, and #158 completes that retirement for the passive adapters.\n`ZMEM_PENDING_SIDECAR=1`\nremains only as a documented no-op for operators upgrading from the\ncompatibility path. Precompact clears the ledger;\nthe next SessionStart performs ordinary `session_start` selection. The MCP\nserver's passive surface (`prefetch`, the reworked `session_start` below, and\nthe Hermes twins) now rides the same store-owned selector. This ownership move is schema-free: neither schema-version\nconstant changes.\n\n#### Query-aware passive prefetch — issue #159\n\n`prefetch` is the query-aware passive lane: one\n`select_and_budget_for_injection` call and one complete JSON envelope — JSON\nis the command's only output mode, and there is no consumer-side renderer or\nsecond budget run.\n\n```\n\u003Cinterpreter> \u003Cstore.py> prefetch --query \"\u003Ctext>\" --namespace \u003Cns> \\\n  --session-id \u003Cid> --moment \u003Csession_start|user_prompt|pretool|subagent|precompact> \\\n  [--lane \u003Cclaude|codex|zcode|hermes-provider|hermes-compat>] \\\n  [--ops-token \u003Ctoken>]... [--exclude \u003Cmemory-id>]... \\\n  [--authorized-namespace \u003Cns>]... [--reserve] \\\n  [--principal \u003Cid>] [--harness \u003Cid>] [--consume-tasktext]\n  [--consume-agent-id \u003Cid>]\n\u003Cinterpreter> \u003Cstore.py> prefetch-ack --reservation \u003Cid>\n```\n\n`--query`, `--namespace`, `--session-id`, and `--moment` are required;\n`--exclude` (memory ids the caller already holds) is repeatable.\n`--for-injection`, `--no-bump`, and `--json` are accepted as explicit markers\nof the (only) passive mode the selector pins. The selector owns the\nrelevance\u002Ftrust gate and the 1,500-token budget; prefetch never advances\n`retrieval_count` (only `surfaced_count` and the delivery ledger may move).\n`--reserve` (issue #284, T-05) returns the selection with a `reservation`\nid and records nothing in the delivery ledger — commit with\n`prefetch-ack --reservation \u003Cid>` (exactly-once; duplicate ack is a\nno-op within the reservation TTL — default 120 s via\n`ZMEM_RESERVATION_TTL_S`; an expired reservation commits nothing and an\nunknown or malformed id exits 2);\n`--principal`\u002F`--harness` key the delivery ledger as\n`principal|harness|session` (omitted keeps the legacy session-only key).\nDelivery is session-attributed: a second turn for the same session whose\nledger already holds the candidate rows returns the silent\n`already-delivered` envelope; `ledger-clear --session-id` resets the ledger,\nwhile SessionEnd uses `delivery-clear --session-id` to retire both sidecars.\n\nThe MCP server exposes the same one-call envelope as the `prefetch` tool\n(`query`, `namespace`, `session_id`, `moment` required; optional `lane` —\nvalidated against the five-value tuple, never defaulted to a host lane —,\n`ops_tokens`, and (issue #339) the caller-asserted `consume_tasktext`), enforcing namespace scope and the `ZMEM_INJECT=0` kill switch\nbefore any store subprocess, and returning the complete selector envelope\nplus the additive `context` alias equal to `rendered`. Scoped principals keep\ntheir `ops_tokens`: the selector's cross-project lane intersects its foreign\nnamespace set with the principal's authorized allow-list (issue #283), so\nforeign projects outside the allow-list are never admitted before rendering\nor ledger updates, while allow-listed cross-project rows can be admitted\nwhen operation tokens ground them. The MCP\n`session_start` tool rides the same store-owned queryless selector path via\n`recent --for-injection --json --session-id \u003Cid> --moment session_start`,\nreturning that envelope (additive `context` alias; `result`\u002F`namespace`\u002F\n`ids` retained as back-compat aliases); an omitted `session_id` falls back\nto the legacy for-injection envelope without the ledger key.\n\n### add — capture a memory\n```\n\u003Cinterpreter> \u003Cstore.py> add \\\n  --namespace \"project:\u003Cbasename>\" \\\n  --type \u003Cfact|lesson|convention|preference|decision|constraint|state> \\\n  --content \"\u003Cthe knowledge, specific and actionable>\" \\\n  --tags \"comma,separated\" \\\n  --signal \u003Ctest|compile|lint|reviewer|user|none> \\\n  [--taint \u003Ctrusted_internal|untrusted_tool|untrusted_web>] \\\n  [--source-ref \"file:\u003Cpath>\" | \"session:\u003Cid>\" | \"db:\u003Ctable>:\u003Crowid>\"] \\\n  [--expiry-class \u003Cendpoint|setting|metric>] \\\n  [--inject-until \u003CUTC ISO8601, e.g. 2026-11-12T09:00:00Z>]\n```\n\n**`state` and injection expiry (v15, issue #173).** A `state` row records a\npiece of the operating environment — an endpoint URL, a setting, a metric —\nthat is only worth injecting while it is CURRENT, unlike a lesson or fact that\nstays true. `--type state` requires `--expiry-class`, one of:\n\n| class | default TTL | env override |\n|---|---|---|\n| `endpoint` | 30 days | `ZMEM_STATE_ENDPOINT_DAYS` |\n| `setting` | 90 days | `ZMEM_STATE_SETTING_DAYS` |\n| `metric`  | 14 days | `ZMEM_STATE_METRIC_DAYS` |\n\nThe override must be a positive decimal integer. With no `--inject-until`, the\nexpiry is computed as `now + TTL` and stored in the additive `memory_expiry`\nside table (`memory_id`, `inject_until`, `expiry_class`); an explicit\n`--inject-until` must be UTC ISO-8601 (`...Z` or `+00:00`, normalized to `Z`)\nand bypasses the TTL. The side table is INJECTION-ONLY: it records when a row\nshould stop being injected, it never deletes the row or hides it from explicit\n`recall`\u002F`search`. Passing either flag with a non-state type, or omitting\n`--expiry-class` on a state add, is refused exit 2. `update` carries the same\ntwo flags and the same rules, and (issue #317) resolves the TARGET's\neffective type at action time: `--type` may be omitted on an update that\ncarries the expiry flags — a state target accepts them, and a genuinely\nnon-state target is refused by the writer with the same exit code.\n\nSince 0.98.0 (issue #174) the recorded expiry is ENFORCED on read: passive\ninjection (`recall --for-injection`, `recent --for-injection`, the Hermes\nprefetch lane) never delivers a row whose `inject_until \u003C= now` — the\ncomparison is by parsed true instant (fractional seconds order by their real\ntime), inclusive at the boundary. Expired rows never reach results, budget\naccounting, or surfaced\u002Fretrieval telemetry; on an engaged lane the envelope\nreports the pre-expiry candidate pool — `candidate_ids` (arrival order) and\n`candidate_lanes` over the SAME population — plus an `expired` count over\nthat population, and a fully-expired-but-eligible pool names the `expired`\nsilent reason. Since 0.119.0 (issues #342\u002F#343\u002F#349) the gate also fails\nCLOSED on unknown expiry state: a side-table read failure (lock\u002Finterrupt\nwith the table present) withholds the un-read candidates, keys\n`expiry_read_failed: \u003Ccount>` into the envelope (registered in\n`INJECTION_ENVELOPE_OPTIONAL`; mirrored in the Hermes fallback set; keyed\nonly when nonzero — a store without the failure keeps byte-identical wire),\nand prints one `[zmem]` stderr line; belief heads resolve their deadline\nthrough their source rows' earliest `inject_until` (a head whose sources\nare all live still delivers; an all-sources-expired head is withheld and\ncounted). `doctor --expiring` keys `expiry_read_error` on a failed\nside-table read (the scan is per-leg guarded, so a successful first read is\nnever discarded) and names unparseable deadlines in\n`unreadable_expiry_states` (memory_id, raw value, namespace, preview;\nkeyed only when nonempty; both keys scoped to `--expiring` invocations).\nA pool whose every candidate was withheld by a read failure reports\n`expiry_read_failed` carrying the explanation; the `reason` label follows\nthe pre-existing classifier (a never-engaged gate names `empty-pool`, an\nengaged drained gate may name `expired` — the read-failure count is the\nauthoritative disambiguator). Present-time explicit `recall`\u002F`search` return expired rows\nmarked `[EXPIRED] ` with `expired: true` instead of filtering; `--as-of`\nneither filters nor marks. `--for-injection` + `--as-of` is refused exit 2\nbefore the store opens. Two read-only operators complete the surface:\n`doctor --expiring 7d` (rows with `now \u003C inject_until \u003C= now + N days`,\nparsed-instant ordered) and `store.py classify-state --dry-run [--json]`\n(deterministic endpoint\u002Fmetric\u002Fsetting proposal; read-only URI +\n`PRAGMA query_only=ON`, never a writer lease).\n\n**Fake-clock seam.** Reads honor `ZMEM_TEST_NOW` (the scoring clock\n`_now_epoch`): the expiry gate, the `[EXPIRED]` markers, and\n`doctor --expiring` are all deterministic under it. WRITES never do — the\nwriter clock is `schema.now_iso()` alone (issue #173's contract), so a pinned\n`ZMEM_TEST_NOW` can only move read-time judgments, never a stored timestamp.\nSignal sets default confidence: test\u002Fcompile=0.9, lint=0.85, reviewer\u002Fuser=0.6\n(medium), none=0.2 — deliberately BELOW the 0.25 recall floor: an ungrounded\nlesson is the agent's self-opinion and never surfaces in default recall (still\nreachable via `search --text`, which applies no confidence floor, or\n`recent --min-confidence 0`) (#36 M3); for a verbatim probe of a stop-word-only\nstring, `search --exact` reaches it literally.\nDedup-on-write: near-identical live content in the same namespace refreshes the\nexisting entry instead of duplicating.\nDedup has a polarity guard (v11, issue #61 6.2): a near-identical hit whose\nnegation polarity DISAGREES (\"always X\" vs \"never X\") is NOT merged — both\nrows stay live, link as `contradicts`, and each loses 0.10 trust. A\npolarity-AGREEING duplicate re-add is corroboration: the existing entry\nrefreshes as before AND gains +0.05 trust.\nDedup also has a subject guard (issue #376): two rows whose `task:\u003Ckey>` tags\ncarry explicitly DIFFERENT task subjects are never merged by similarity\nALONE — each statement, its tags, source and evidence stay independently\naddressable on `add`, `update`, ingest and consolidation, the veto is logged\nwith the stable code `subject_veto`, and a subject-compatible runner-up can\nstill absorb the fold. Absent\u002Fmalformed identity is compatible (unknown\nsubjects dedupe as before), two rows BOTH typed `lesson` generalize across\ntask origins (a single lesson row does not opt itself out of the guard), and\nsubject-incompatible neighbors no longer receive each other's tags through\nlink propagation. An admitted merge unions the survivor's task identities\n(bridge growth): a row that legitimately absorbed across compatible subjects\ncan later merge with either subject.\nLink generation on write (v11, issue #61 6.2): after every `add`\u002F`update`,\nthe same namespace-aware neighbors dedup already computed (embedding cosine\nwhen the model is present, else Jaccard token overlap — model-absent stores\nlink too) above `ZMEM_LINK_THRESHOLD` (default 0.75) become `related` edges\n(stored both directions). Each linked neighbor also absorbs the new row's\nTAGS and re-derives its entity links (attribute evolution) — content,\nconfidence, signal, and retrieval_count are never touched. Deterministic;\nno LLM (a `ZMEM_LINK_LLM` knob deliberately does not exist).\n`add --json` (v13) prints a structured write result — `{\"id\", \"result\":\n\"stored\"|\"deduped\", \"warnings\"}` — with warnings as structured objects\n(`{\"type\": \"redacted\", \"count\": N}` for automatic secret redactions); the\nMCP and Hermes add surfaces use it. Default output is unchanged.\nNamespace validation: obvious misspellings of the global namespace (`global`,\n`userglobal`, `users:global`, …) are rejected at `add` time AND on `ingest-jsonl`\nsync import with a message naming the canonical `user:global` — such rows would\nbe unreachable from the automatic hooks. New writes accept `project:\u003Cx>` and\n`user:\u003Cx>` (including later colons in their values), plus strict\n`fleet:\u003Cx>`, `host:\u003Cx>`, `agent:\u003Cx>`, and `domain:\u003Cx>` forms. The latter four\nrequire one non-empty, whitespace-free, colon-free value. `ingest-jsonl`\nretains its legacy near-miss-only validation until the explicit rekey work.\nLegacy rows already stranded under a near-miss namespace\n(before this guard existed) can be remediated with `rekey-namespace\n--near-miss-global --confirm` (see below).\n\n### update \u002F invalidate — append-only revision and contradiction corrections (v9, issue #59)\n```\n\u003Cinterpreter> \u003Cstore.py> update --id \u003Cfull-uuid> --content \"\u003Cnew content>\" \\\n  [--namespace NS] [--type ...] [--tags ...] [--source-ref ...] [--confidence F]\n  [--signal ...] [--taint ...] [--capture-mode manual|reviewed|auto]\n  [--expiry-class \u003Cendpoint|setting|metric>] [--inject-until \u003CUTC ISO8601>]\n\u003Cinterpreter> \u003Cstore.py> invalidate --id \u003Cfull-uuid> --reason \"\u003Cwhy the fact is no longer true>\"\n```\n- **`update`** is append-only knowledge revision: it creates a NEW live row with the\n  new content, tombstones the target row (`superseded_at=now, valid_until=now,\n  supersede_reason='updated'`), and links the new row back to it via `update_of`.\n  Namespace\u002Ftype\u002Ftags\u002Fsource_ref\u002Fconfidence\u002Fsignal inherit from the target unless\n  overridden; the old row's content is NEVER mutated. An unknown or\n  already-superseded id is refused (exit 2, nothing written). Dedup runs against\n  OTHER live rows (the replaced row is excluded, so even an unchanged-content\n  \"update\" creates history). Point-in-time recall (`--as-of` before the update)\n  returns the OLD content; after returns the NEW.\n- **`invalidate`** is `supersede` with a REQUIRED `--reason` — the preferred way to\n  record \"this fact is no longer true\" so the correction is auditable (issue #59,\n  4.3). `supersede` remains for general tombstones (consolidated\u002Fpruned rows)\n  where a reason is optional. The reason must be NON-EMPTY (whitespace-only is\n  refused, exit 2), and tombstones are write-once: a second `invalidate`\u002F\n  `supersede` on an already-tombstoned row is refused (exit 2) — re-tombstoning\n  would move `valid_until` forward and falsify point-in-time history.\n- **Large content**: `--content -` (the literal dash) reads the content from\n  stdin on both `add` and `update`. Windows caps command-line argv far below the\n  65536-char content limit, so pipe near-limit payloads instead of passing them\n  as an argument (the Hermes\u002FMCP surfaces do this automatically). Piped bytes\n  are always decoded as UTF-8, whatever the process stdio codec (cp1252 on a\n  default Windows install), locally and in remote mode; invalid UTF-8 bytes\n  store as U+FFFD. Line endings are normalized: `\\r\\n` (and a lone `\\r`) store\n  as `\\n`, so a text-mode parent's piped LF text is stored as LF (issue #460).\n  The argv path performs no translation: an argument containing `\\r\\n` is\n  stored verbatim.\n\n### Provenance trust (`--taint`) — v9 (issue #59, 4.7)\nEvery memory carries a **taint** rank marking the trust of its ORIGIN:\n`trusted_internal` (human\u002Fcloseout\u002Ftest-grounded) \u003C `untrusted_tool` (agent or\nMCP\u002FHermes-authored) \u003C `untrusted_web` (web-fetched). There is deliberately no\nfourth rank: an unknown taint value is **refused** at every write surface (the\nwrite path never silently coerces it). The closed enum is enforced by a CHECK\nconstraint AND by application-side validation (CLI argparse choices, ingest\nvalidator, Hermes\u002FMCP boundary checks) — all sourced from the single\n`schema_meta.ALLOWED_TAINTS`.\n\nThe taint default depends on the surface:\n- CLI `add`: derived from `--signal` — grounded signals (`test`\u002F`compile`\u002F`lint`\u002F\n  `reviewer`\u002F`user`) default to `trusted_internal`; `signal=none` (the agent's\n  self-opinion) defaults to `untrusted_tool`. An explicit `--taint` overrides.\n- Hermes\u002FMCP tools (`zmem_add` \u002F the MCP `add` tool): default to EXPLICIT\n  `untrusted_tool` unless the caller passes a taint (e.g. `untrusted_web` for a\n  web fetch, or `trusted_internal` for a human-grounded note).\nTaint propagates **worst-of forward through lineage**: `update` re-creation and\n`consolidate` absorb both set the surviving row's taint to the worse of the two\nmerged sources (a merged row never DOWNGRADES a riskier member), and a\nduplicate `add` \u002F `ingest-jsonl` row that folds into an existing keeper applies\nthe same worst-of to the keeper. A tombstone\n(`supersede`\u002F`invalidate`) preserves the row's taint — it creates no new row.\n\nRecall surfaces the taint so trust is visible, mirroring prompt-injection-risk:\n- The **explicit** text path prefixes rows with `[UNTRUSTED TOOL]` (taint\n  `untrusted_tool`) or `[UNTRUSTED WEB]` (taint `untrusted_web`).\n- The **passive\u002Fauto-inject** path (`--no-bump` — used by the automatic hooks and\n  Hermes prefetch) OMITS `untrusted_web` rows entirely, exactly like\n  `prompt-injection-risk` rows (an untrusted web-sourced memory is not trusted\n  enough to inject passively). `untrusted_tool` rows DO surface passively — they\n  are agent-authored and thus safer than web content — and are flagged on the\n  explicit path instead. The hook scripts need no edits: they already pass\n  `--no-bump` and inherit this store-side filter.\n\n### recent \u002F search \u002F supersede \u002F update \u002F invalidate \u002F list \u002F get \u002F stats\n```\n\u003Cinterpreter> \u003Cstore.py> recent [--namespace NS] [--limit 5] [--min-confidence 0.5]\n                         [--include-global] [--global-limit 3] [--as-of ISO-8601]\n                         [--json]\n\u003Cinterpreter> \u003Cstore.py> search --text \"\u003Ctext>\" [--exact] [--namespace NS] [--limit 10]\n                        [--include-global] [--global-limit 3] [--no-bump]\n                        [--as-of ISO-8601] [--json]\n\u003Cinterpreter> \u003Cstore.py> supersede --id \u003Cfull-uuid> [--reason \"...\"] [--expected-namespace NS]\n\u003Cinterpreter> \u003Cstore.py> invalidate --id \u003Cfull-uuid> --reason \"...\" [--expected-namespace NS]\n\u003Cinterpreter> \u003Cstore.py> update --id \u003Cfull-uuid> --content \"\u003Cnew content>\" [overrides...]\n                       [--expected-old-namespace NS]\n\u003Cinterpreter> \u003Cstore.py> list [--namespace NS] [--include-superseded]\n\u003Cinterpreter> \u003Cstore.py> get --id \u003Cuuid>\n\u003Cinterpreter> \u003Cstore.py> stats\n```\nRead envelope (v13, issue #65 10.8): `recall`\u002F`recent`\u002F`search --json` print\n`{\"results\", \"count\", \"omitted\", \"injection_risk\", \"tokens_used\",\n\"tokens_budget\"}` instead of a bare list — `omitted` counts rows the\n`--no-bump` inject filter dropped (injection-risk + `untrusted_web`), so\nhosts stop guessing from stderr. Library callers still get the bare row list\nfrom the Python API.\n\n`update --json` mirrors `add --json` (`{\"id\", \"result\", \"created_new\",\n\"warnings\"}`). `get` (always JSON) includes the v13 `episodes` key — the\nepisodes this memory belongs to (always a list, possibly empty).\n\n`recent`\u002F`search` accept the same `--include-global`\u002F`--global-limit` pair as\n`recall` (project-first global tier union). `recent` now ALSO honours v5\nmigration aliases (so `recent --namespace \u003Cold pre-v5 key>` finds rows migrated\nto the new key). `search` now accepts `--no-bump` for a *passive* query that records a\nsurface on `surfaced_count` (never advancing `retrieval_count`) instead of bumping like\n`recall` (issue #21).\n\n`search --exact` (issue #263) switches the matcher from FTS5 to a **literal\nsubstring existence check**: `--text` is matched verbatim (case-sensitive,\ncontiguous) against `memory.content`, so stop-word-only literals, FTS5 query\nsyntax (`OR`, quotes, leading `-k`), and word order all stay literal instead\nof being normalized into a token query. Use it to confirm an exact id, path,\nerror string, or redaction marker is or is not present verbatim; plain\n`search` (no `--exact`) is unchanged byte-for-byte. `--exact` honors the same\n`--namespace`, `--include-global`\u002F`--global-limit`, `--as-of`, and `--exclude`\nfilters and the same `--no-bump` (issue #21) semantics as plain search, needs\nno embedding model, never link-expands, and orders results most-recent-first.\nThe literal match defeats every index (SQLite `instr` byte-scans each row in\nscope and the sort runs before `LIMIT`), so cost scales with the scoped row\ncount — fine for operator existence probes, not a hot-path substitute for\nranked search.\nAn empty `--text` trivially matches every row in scope (Python `\"\" in content`\nsemantics).\n\n`--as-of ISO-8601` (v9, issue #59 4.4) on `recall`\u002F`recent`\u002F`search` returns rows\n**valid at that instant**: `valid_from \u003C= as_of AND (valid_until empty OR\nvalid_until > as_of)`. `valid_from` is INCLUSIVE, `valid_until` is EXCLUSIVE, and\nempty `valid_until` means \"never expires\". Because a tombstone writes\n`valid_until` at the same instant as `superseded_at`, a point-in-time as-of may\nreturn rows that were later superseded (they were valid then) — only rows whose\nvalidity interval contains `as_of` surface. This is what makes `update` history\nreadable: `--as-of` before the update returns the OLD content.\n\n`--as-of` recoverability is **lane-bounded** in hybrid mode: a historically\ntombstoned row that was valid at `as_of` is reachable only through the lexical\n(FTS5\u002FBM25) lane. The vector lane's candidate pool always filters\n`superseded_at IS NULL` (live-only), so a row superseded before `as_of` cannot\nsurface via semantic KNN even though it was valid at that instant. Model-absent\nruns are lexical-only and unaffected; a hybrid run that misses old content via\nthe vec lane should be re-run with `--no-hybrid` to include the FTS lane.\n\n\n### reembed — backfill or rebuild semantic embeddings\n```\n\u003Cinterpreter> \u003Cstore.py> reembed\n\u003Cinterpreter> \u003Cstore.py> reembed --all [--profile NAME] [--batch N] [--dry-run]\n\u003Cinterpreter> \u003Cstore.py> reembed --check\n```\nFlagless form (unchanged contract): backfills embeddings for live memories that\nare MISSING them when the optional embedding runtime and model are available.\nExisting embeddings are preserved; no runtime means a graceful skip.\n\n`--all` rebuilds EVERY live memory's vector under the selected profile —\nthe operator-grade converter when you switch profiles:\n- `--profile NAME` selects from the shipped registry (`minilm`, `fake`; see\n  \"Embedding profiles\" below). Default: active `ZMEM_EMBED_PROFILE` or\n  `minilm`. Requires `--all` — `--profile` without it refuses with the exact\n  conversion command, never a silent no-op. `--all --profile fake`\n  requires `--confirm` ONLY when the store already holds non-fake committed\n  vectors (the overwrite is irreversible-in-practice without a working model\n  later); fresh\u002Fempty stores convert freely.\n- If the profile's dimension differs from what the store holds, `memory_vec`\n  is recreated at the new dimension INSIDE one transaction — a crash mid-run\n  rolls back to the pristine pre-run state, so a half-dim index is impossible.\n  Every command then re-verifies profile-vs-store dimension before it embeds\n  (mismatch = exit 2 with the exact remediation line).\n- Idempotent: a second identical run changes 0 rows. `retrieval_count`,\n  `content`, and every other column stay untouched — only embedding bytes,\n  their `embedding_model` marker, and `embedded_at` are rewritten, plus the\n  `embedding_profile` meta key recording the last completed conversion.\n- `--batch N` paces stderr progress lines ONLY (display chunks inside the\n  single transaction — batches are never separate commits). For very large\n  stores run during an idle window: the single transaction briefly grows the\n  WAL by roughly the size of all rebuilt vectors (~1.5 KB\u002Frow at 384-dim);\n  the default batch size is 64.\n- `--dry-run` reports how many of the live rows would change and writes\n  nothing (no writer lease, no meta write).\n\nIdempotency detail: change detection keys on\n`embedding IS NULL OR blob dim mismatch OR embedding_model marker differs`,\nso switching `--profile` back and forth always reports honestly instead of\nsilently treating rows as current.\n\n`--check` is an exclusive, read-only consistency census. It reports live rows\nmissing a `memory_vec` row, vector rows whose memory no longer exists, and live\nembedding blobs whose exact byte length differs from the declared vec0 dimension.\nIt exits 0 with `reembed check: 0 inconsistencies`; otherwise it prints the three\ncounts and exits 1. It never downloads a model, takes a writer lease, performs\nauto-rekey, or changes the store. A store with an active `-wal` sidecar is refused\nwith exit 2 so the census cannot recover or create shared-memory files; checkpoint\nthe store first.\n\n### Embedding profiles — shipped registry (`embed_profiles.py`)\n\n| Profile | hf_id | dim | sha256 | Purpose |\n|---|---|---|---|---|\n| `minilm` | `Xenova\u002Fall-MiniLM-L6-v2` | 384 | `bbd7b466…46f0c5` (original operator-box ONNX export; see notes) | Operator default; semantic recall\u002Fdedup |\n| `fake` | — | 16 | — (no files\u002Fnetwork by design) | Model-absent tests\u002FCI ONLY; deterministic placeholders; doctor warns on non-temporary stores |\n\nNotes:\n- The minilm pair's SECOND executed artifact, `tokenizer.json`, ships its\n  own pin: `TOKENIZER_SHA256 = da0e7993…` (the full 64-hex constant in\n  `embed_profiles.py` — the checked-in tokenizer artifact's byte digest).\n  The loader digest-verifies it from the same buffer the `Tokenizer` is\n  constructed from (`from_buffer`), and `embeddings._TOKENIZER_SHA256`\n  re-exports the same constant (#418).\n- The published pin covers the ORIGINAL OPERATOR-BOX ONNX export of\n  all-MiniLM-L6-v2 — preserved byte-exact in this repo's Git LFS history at\n  commit `3240ea1a0cf3d972f6a7d472cee73abaddfc038d`\n  (`skills\u002Fmemory\u002Fmodels\u002Fminilm.onnx`, 90,387,630 bytes) and served as the\n  never-retargeted GitHub release asset `model-minilm-bbd7b466\u002Fminilm.onnx`\n  (the sha-256 check after fetching — not platform immutability — is the\n  trust boundary). It is\n  NOT the public Xenova HF-repo export (`onnx\u002Fmodel.onnx`, oid\n  `759c3cd2b7fe7e93933ad23c4c9181b7396442a2ed746ec7c1d46192c469c46e`,\n  90,387,606 bytes) and NOT the sentence-transformers PyTorch weights —\n  different builds of the same checkpoint hash differently.\n  `checksum_mismatch` therefore means \"wrong build installed\"; restore the\n  pinned bytes from the release asset (or point `ZMEM_MODEL_URL` at it);\n  doctor prints this note verbatim. Verification has NO escape hatch:\n  `ZMEM_MODEL_ALLOW_UNVERIFIED` does not exist and never will.\n- A third ONNX profile was evaluated and deliberately OMITTED per the release\n  rule: no Qwen3\u002FNomic local ONNX artifact with a personally verified\n  hf_id+dim+sha256 could be pinned at authoring time. \"A name with empty\n  sha256 is a stub\" is forbidden — add one only with verified facts.\n\n#### Compatibility ledger for profile conversion (read before switching)\n\n- **Older zmem releases on a converted store:** versions without this change\n  hardcode `float[384]` and do not know about `embedding_profile`. On a store\n  converted to another dimension they keep opening it, keep writing rows, and\n  silently lose\u002Fdegrade vector recall (the vec lane's failures are swallowed\n  there). Conversion is per-machine deliberate maintenance — coordinate box\n  upgrades around it.\n- **Hook surfaces during mismatch:** while the active profile's dim differs\n  from committed data, every hook that recalls (`--no-bump`) receives an EMPTY\n  inject envelope; the refusal reason lands only on the child's stderr, which\n  hooks do not surface. This refuse-over-wrong-vectors posture is deliberate:\n  lexical-only degradation inside a wrong-dim store would surface misplaced\n  confidence rather than silence. Run `reembed --all` (or restore the prior\n  profile) to clear the state.\n- **`doctor` default-run behavior changed by design** in the same release:\n  the embeddings check now deep-verifies the model pin (mtime-keyed cached\n  hashing), so its JSON can newly report `checksum_ok:false` where older\n  versions stayed silently `null`.\n- **Concurrency during `reembed --all`:** the whole rebuild holds one SQLite\n  write transaction for its duration; concurrent writers wait (busy-timeout),\n  readers proceed on WAL snapshots. Schedule large conversions for idle\n  windows.\n\n**Cross-encoder trust note** (issue #63 review round; updated by issues\n#125 and #418): the operator-supplied `ZMEM_CROSS_ENCODER_MODEL` path stays\noperator-vouched (no checksum pin — treat it with the same caution as any\nexecutable), while the DEFAULT `mini-pair-scorer` profile path digest-gates\nBOTH artifacts (model AND tokenizer) at scorer BUILD time — each runtime\nobject is constructed only from the buffer whose SHA-256 just matched its\nprofile pin, closing the verify-then-reopen window (#418) — and any\ndownload is digest-verified before an atomic install. doctor's\n`embeddings_health.cross_encoder` block now surfaces\n`profile`\u002F`model_path`\u002F`tokenizer_path`\u002F`checksum_state`\n(`verified|mismatch|missing|unreadable`)\u002F`tokenizer_state`\n(`not-applicable|missing|unreadable|verified|mismatch`)\u002F`load_policy`\n(`explicit-operator-vouched`|`profile-pinned`)\u002F`passive`\u002F`shadow`\u002F\n`autodownload` so a missing-model silent degrade and the passive\u002Fshadow\nopt-in state stay visible.\n\n### episode-open \u002F episode-add \u002F episode-close \u002F episode-list — session containers (v13, issue #65 10.7)\n\n```\n\u003Cinterpreter> \u003Cstore.py> episode-open --namespace \"project:\u003Cbasename>\" [--json]\n\u003Cinterpreter> \u003Cstore.py> episode-add --episode \u003Cuuid> --memory \u003Cuuid> [--json]\n\u003Cinterpreter> \u003Cstore.py> episode-close --episode \u003Cuuid> [--summary] [--json]\n\u003Cinterpreter> \u003Cstore.py> episode-list [--namespace NS] [--json]\n```\n\nAn **episode** groups the memories captured during one working session\n(schema v13: `episode` + `episode_memory` tables). It is a CONTAINER, not a\nmemory type — `episode` is deliberately absent from `--type` values.\n\n- `episode-open` creates the row (`ended_at` empty until close) and prints\n  its id; this is the only creator — no empty-table seeding.\n- `episode-add` attaches a LIVE memory (`superseded_at IS NULL`; a\n  tombstoned id is refused exit-2) to an OPEN episode. Idempotent per pair;\n  memberships are append-only (a later supersede never removes one).\n- `episode-close` sets `ended_at`, computes `token_count` (the same\n  `row_token_cost` the injection budget uses, summed over LIVE members at\n  close), and with `--summary` writes one extractive first-sentence summary\n  row via the standard `add` path (capture-mode auto, so the shared\n  redaction helper runs) linked by `summary_memory_id`. A closed episode\n  cannot be re-closed or re-opened (append-only).\n- `episode-list` prints episodes newest-first with member counts\n  (`--json` for rows). `get --json` on a memory lists its episodes.\n\nEpisodes round-trip through `export-jsonl`\u002F`ingest-jsonl` via a `kind`\ndiscriminator (`\"memory\"` on memory rows; separate `episode` and\n`episode_memory` records). Legacy kind-less files still ingest as\nmemory rows; an older client consuming a new export should filter to\n`kind == \"memory\"` lines. Doctor reports counts (`episode-tables`).\n\n### session_start \u002F session_end — MCP + Hermes pairing tools (v13, issue #65 10.5)\n\nBoth surfaces expose `session_start` \u002F `session_end` (MCP tools) and\n`zmem_session_start` \u002F `zmem_session_end` (Hermes tools) with the same\ncontract:\n\n- **`session_start(namespace?, limit=3, session_id?, lane?)`** — passive\n  prefetch returning a fenced, provenance-tagged context block (Phase 3\n  fence + selective-inject rules, 0.5 recent floor). It runs\n  `recent --for-injection --json`: with a `session_id` the store-side\n  selector (moment `session_start`) owns the gate, the budget, the delivery\n  ledger, and the `rendered` fence, and the tool returns that complete\n  envelope plus the additive `context` alias and the back-compat\n  `result`\u002F`namespace`\u002F`ids` fields (issue #159); an omitted `session_id`\n  takes the legacy for-injection envelope. `retrieval_count`\n  NEVER advances (only the surface event is recorded — pinned by\n  tests\u002Ftest_session_tools.py), injection-risk and `untrusted_web` rows are\n  omitted, and `ZMEM_INJECT_TOKEN_BUDGET` is honored. Namespace omitted\n  resolves to the surface's own default — `user:global` on MCP, the session\n  namespace on Hermes (a deliberate divergence: the Hermes provider is\n  session-scoped, the network server is not).\n- **`session_end(note?, namespace?)`** — default is a NO-WRITE\n  acknowledgement so clients can pair start\u002Fend freely; it NEVER organizes\n  or consolidates (run the explicit CLI for that). With a `note`, exactly\n  one row is written via the standard `add` path (`fact`, `signal none`,\n  `taint untrusted_tool`, capture-mode auto so redaction runs), defaulting\n  to the server\u002Fsession namespace.\n\n### Scoped MCP tokens (v13, issue #65 10.2)\n\n`ZMEM_MCP_TOKEN` (env) is always an UNSCOPED operator token — full access to\nevery namespace, exactly the pre-v13 behavior. To scope it, point\n`ZMEM_MCP_TOKEN_FILE` at a JSON object:\n\n```json\n{\"token\": \"\u003Csecret>\", \"namespaces\": [\"project:zmem\", \"user:global\"]}\n```\n\nAllowed namespace forms are project:name, user:name, fleet:name, host:name,\nagent:name, and domain:name. Project and user values retain later-colon\ncompatibility; existing local project path keys may also contain internal\nspaces. The fleet, host, agent, and domain forms require one non-empty value\nwithout whitespace or colons.\n\nRequests outside the allow-list fail closed with the stable\n`namespace_not_allowed` error. `supersede`\u002F`invalidate` are namespace-guarded\ntoo (issue #109): for a scoped token the server reads the target row's\nnamespace, denies with the same `namespace_not_allowed` shape when it is\noutside the allow-list, and pins the verified namespace on the store\nmutation via `--expected-namespace` (a new `supersede`\u002F`invalidate` CLI flag\nthat makes the tombstone UPDATE conditional on the row's namespace — refuse\nexit 2 with the stable `[zmem] namespace guard:` stderr line, nothing\nwritten, on mismatch; `update --expected-old-namespace` pins the replaced\nrow the same way; omit for the historical unguarded local-operator\nbehavior). The empty string counts as a supplied (never-matching)\nexpectation and fails closed, not as \"no guard\". Unscoped operator tokens\nkeep full tombstone powers everywhere. Scoped tokens MUST pass an allowed namespace\nexplicitly on every read — a namespace-less read spans the whole store and\nis denied — and the implicit `user:global` union on scoped reads is\nsuppressed unless `user:global` is itself in the list. Malformed JSON, an\nempty `namespaces` list, or near-miss scopes like `\"global\"` are hard\nstartup errors (exit 2). A bare (non-JSON) token file stays unscoped, as does\na JSON object with `namespaces` absent or `null`.\nDoctor warns `unscoped_token: true` on operator tokens (issue #65, 10.10)\nand never prints the token value.\n\n### promote — review and install a reusable skill\n```\n\u003Cinterpreter> \u003Cstore.py> promote --dry-run [--namespace NS]\n\u003Cinterpreter> \u003Cstore.py> promote --id \u003Cuuid> --confirm [--description \"...\"]\n\u003Cinterpreter> \u003Cstore.py> promote --id \u003Cuuid> --confirm --install-approved\n```\n`--confirm` writes a review candidate; `--install-approved` is the additional\nexplicit gate that installs the reviewed skill into Codex, Claude Code, Zcode,\nand OpenCode skill directories. Promotion is never an unattended hook action.\n\nThe promote ladder (issue #64, schema v12) decides which lessons are\ncandidates. A lesson is eligible only when its EXPLICIT usage feedback says it\nworks and nothing says it doesn't:\n- `applied_count >= 3` AND `violated_count == 0` → eligible (see `feedback`).\n- Any `violated_count > 0` blocks eligibility; the 2nd violation has already\n  dropped the row's `trust_score` by `TRUST_VIOLATION_FLOOR_DROP` (0.15,\n  clamped at 0.0, applied exactly once at the violated_count 1→2 crossing).\n  `signal` is NEVER changed by feedback — trust_score is the usage ledger,\n  signal is provenance.\n- Passive exposure does NOT count: `retrieval_count`\u002F`surfaced_count`\n  (hooks, `--no-bump` recall) are not eligibility inputs. Only the explicit\n  `feedback` CLI advances the counters, so nothing auto-promotes.\n- The other candidate bars are unchanged: `type=lesson`, grounded signal\n  (test\u002Fcompile\u002Flint), `confidence >= 0.85`, live row.\n`promote --dry-run` with no eligible rows is a no-op that prints\n`[zmem] no promotion candidates found` (exit 0).\n\n### feedback — record explicit usage feedback (Voyager counters)\n```\n\u003Cinterpreter> \u003Cstore.py> feedback --id \u003Cuuid> --applied\n\u003Cinterpreter> \u003Cstore.py> feedback --id \u003Cuuid> --violated\n```\nIncrements exactly one of the row's `applied_count` \u002F `violated_count`\n(schema v12) and prints a one-line JSON summary\n(`id`, `verdict`, both counters, `trust_score`, `trust_dropped`). This is the\nONLY writer of those counters anywhere — hooks, `--no-bump` recall,\nPreCompact, and Hermes prefetch never advance them.\nExit codes: 0 success; 1 target missing or not live (tombstoned rows refuse);\n2 usage error (both or neither of `--applied`\u002F`--violated`, missing `--id`).\n\n### eval — offline recall-quality harness\n```\npython scripts\u002Feval_runner.py --store \u003Cpath> [--gold eval\u002Fgold.jsonl] [--k 5] [--fail-under X] [--json-out PATH]\n```\n\n### eval-inject — injection precision gold (issue #111)\n```\npython scripts\u002Feval_inject_runner.py --store \u003Cpath> [--gold eval\u002Finjection_gold.jsonl] [--k 5] [--fail-under-precision X] [--fail-under-false-injection X] [--compare-baseline eval\u002Fbaseline-injection.json] [--json-out PATH]\n```\nRuns every injection-gold item through the REAL `--for-injection` lane —\nthe same selective-inject gate and token budget the hooks execute — and\nscores the RENDERED set with negative controls: `precision_at_k`,\n`false_injection_rate`, `empty_pool_rate`, `hit_at_k`, overall and per\nmoment (user prompt, pre-tool operation tokens, subagent task text,\npre-compaction). Refuses (exit 2) if the gate or budget is stubbed out.\n`--gold` accepts a local labeled JSONL (e.g. derived from your own decision\nlog) without code changes. Record-only by default; the ratchet flags are\nthe one-switch CI gate.\n\nThe score-margin replay\u002Fbaseline evaluator is available for the #155\u002F#183\nread-only audit. CI runs the committed `scripts\u002Feval_replay.py` snapshot with\n`eval\u002Fbaseline-replay.json` on both platform legs. The evaluator is\nrecord-only unless an explicit `--fail-under` ratchet is supplied; it never\nresolves the operator store or claims live-host efficacy.\nRuns every gold item through the REAL recall pipeline and prints one JSON\nreport: `hit_at_k`, `mrr`, `as_of_accuracy`, `injection_omit_rate` (+ per-bucket\nand per-item detail). `--store` is REQUIRED — the runner never resolves the\ndefault home store; point it at a throwaway path and it auto-builds the\ndeterministic 64-row fixture corpus (the ids `eval\u002Fgold.jsonl` names) there.\nThe gold set covers the six issue-#64 buckets (as-of knowledge updates,\ninjection omission, entity aliases, namespace isolation, contested\u002Fsuperseded\nguidance, ordinary FTS hits; >= 5 items each) plus the issue-#82 honesty\nbuckets (>= 3 items each): `retraction` (an `invalidate --reason`-ed fact must\nvanish at current time yet surface under `--as-of` before the tombstone),\n`polarity` (two LIVE contradicting rows — both sides surface), and\n`change-intent` (explicit-path lineage unfold). An optional `\"explicit\": true`\ngold flag runs an item on the explicit path (`no_bump=false`, still zero-write\nvia `no_telemetry`) so the change-intent unfold is measured; passive twins on\nthe same rows pin that hooks never unfold. The original 30 ids (rowids 1-50)\nare the frozen #64 contract; #82 appends rowids 51-64.\nExit 0 regardless of scores (quality is collected, never\ngating); `--fail-under` is optional and OFF in CI; 2 on operational errors\n(invalid gold set names the offending item).\nThe run is model-absent by construction: the fake embedder\n(`ZMEM_EMBED_PROFILE=fake`) and a pinned clock (`ZMEM_TEST_NOW`) make ranking\ndeterministic on any machine. Evaluation is passive (`no_bump`) and measures\nretrieval quality — link expansion and MMR presentation are excluded\n(`link_hops=0`, `no_mmr`).\nSelf-corpus probe — measure recall against YOUR corpus (issue #82):\n```\npython scripts\u002Feval_self_corpus.py --store \u003Cstore-copy> [--k 5,20] [--limit 100] [--json-out PATH]\n```\n`--store` is REQUIRED and the runner REFUSES the host default store path\n(exit 2 with a remediation: take a `store.py backup` snapshot and probe the\ncopy) and refuses nonexistent paths before creating anything. Probes are\ntemplated deterministically from the store's own live rows (first sentence \u002F\ndistinctive tokens \u002F entity aliases — domain-agnostic, no LLM). Every probe\nis fully passive (`no_telemetry`, `no_bump`, `link_hops=0`, `no_mmr`; the\nunfold structurally cannot fire) so the probed store stays byte-identical.\nThe report may embed operator content — `--json-out` is the only write and\nreports are gitignored; never commit one.\nPublic-corpus adapters convert on-disk corpora into gold JSONL and skip\ncleanly when the corpus is absent (CI downloads nothing):\n```\npython scripts\u002Feval_adapters.py --adapter longmemeval --input \u003Cpath> --out \u003Cpath>\npython scripts\u002Feval_adapters.py --adapter locomo --input \u003Cpath> --out \u003Cpath>\n```\nA missing `--input` prints `skipped: ...` and exits 0. Synthetic 3-row toy\nfixtures under `tests\u002Ffixtures\u002Fadapters\u002F` prove both converters.\n\n### tune-weights — suggest recall scoring weights (dry-run only)\n```\n\u003Cinterpreter> \u003Cstore.py> tune-weights --dry-run --gold eval\u002Fgold.jsonl [--k 5]\n```\nEvaluates the shipped composite weights (W_BM25\u002FW_CONFIDENCE\u002FW_RECENCY\u002F\nW_POPULARITY) against a gold set, hill-climbs a small deterministic candidate\nset, and prints JSON with `current` and `suggested` weight vectors (always\nsumming to 1.0) plus the metrics each achieves. The evaluation is read-only\nand it WRITES NOTHING — there is deliberately no `--apply` (the store must\nalready be at the current schema: opening any store.py subcommand migrates\nit, as always). Applying a suggestion is a MANUAL edit of the W_*\nconstants at the top of `skills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Frecall.py`, keeping\nthe sum at 1.0. Uses the CURRENT store (env-resolved like every subcommand —\nin tests\u002FCI that is a fixture store via `ZMEM_STORE`, never the operator\nstore). Exit 0 even when the current weights already win; 2 on an invalid\ngold set, a missing `--dry-run`, or an evaluation failure.\n\n### consolidate — merge near-duplicate memories\n```\n\u003Cinterpreter> \u003Cstore.py> consolidate [--threshold 0.80] [--prune] [--dry-run] [--namespace NS] [--force] [--merge-contested] [--json]\n```\nClusters live memories by embedding cosine similarity (Jaccard token overlap when\nembeddings are unavailable), picks a keeper, merges metadata, and supersedes the\nrest with a persisted reason. The automatic SessionStart cadence now invokes\n**`organize`** (issue #62, 7.7) — which runs consolidate's EXACT same cluster\nlogic on a bounded episode — so the 7-day \u002F 20%-growth gate below protects the\nschedule REGARDLESS of entry point: organize and consolidate share the same\n`last_consolidation` meta keys and the same single-flight lock. An automatic\nrun is skipped only when the last run was \u003C7 days ago AND growth was \u003C20%; both\nbounds are env-tunable via `ZMEM_CONSOLIDATE_MIN_INTERVAL_DAYS` \u002F\n`ZMEM_CONSOLIDATE_GROWTH_THRESHOLD`. The `consolidate` CLI remains available for\nexplicit\u002Fmanual scope runs. When the gate declines, the run prints a\nsingle `[zmem] consolidate: skipped by cadence gate (...)` line (never silent)\nand changes nothing. Use `--dry-run` to preview clusters and the exact merge\ndecision per row without mutating the store — `--dry-run` models the cadence\ngate too, so a gated dry run reports `would skip by cadence gate` rather than\n`would merge N`, and a dry run that says \"would merge\" implies a real run that\nmerges. Pass `--force` to bypass the cadence gate and run consolidation now\n(this is the only intentional bypass; `--threshold` does not affect the gate).\n\nKeeper selection: within a cluster, the survivor is the row with the highest\n`confidence * (retrieval_count + surfaced_count)` product — total surface events,\nblend-aware for hook-surfaced memories (ties broken by confidence, then earliest\ningestion) (issue #21). Absorbed rows are NOT lost on merge: a near-duplicate that is not\nfully subsumed by the keeper has its ENTIRE text appended to the keeper's content\n(live-recallable + FTS-indexed), and its id is recorded in the keeper's\n`merged_from` provenance column, so consolidated memory is non-lossy\n(v11, issue #61 6.6: the column is maintained as a DE-DUPLICATED,\nfirst-seen-order id list — the same absorbed id is never recorded twice,\nand the v11 migration normalized pre-existing duplicates losslessly). Note this\nis a whole-text append, not a unique-tail diff — a partially-overlapping row\nduplicates the span it already shares with the keeper (deliberate\nover-preservation, bounded by the size cap below). Reordered same-token phrasing\n(e.g. \"call foo before bar\" vs \"call bar before foo\") is likewise preserved as\ndistinct content — only genuinely subsumed\u002Fduplicate text is omitted.\n\nSize cap: a keeper's content is never allowed to exceed\n`INGEST_MAX_CONTENT_CHARS` (the JSONL round-trip ceiling, 65536). An absorb that\nwould push the keeper over the cap is NOT appended — its id is recorded with a\n`:truncated` marker in `merged_from` and the absorbed row is still superseded;\nthat is the one bounded case where an absorbed row's text is not migrated live\n(it remains only in the tombstoned history row). `--dry-run` shows would-lose\ntokens for this case.\n`--prune` also removes low-value NEVER-surfaced and never-retrieved memories\n(`retrieval_count = 0` AND `surfaced_count = 0`, low confidence, old, `signal = none`;\nopt-in, never automatic). A hook-surfaced memory is protected — `retrieval_count = 0`\nalone is not evidence of unused (issue #21). Unrecalled extension (issue #62,\n7.6): a live row whose `last_surfaced` is OLDER than `ZMEM_UNRECALLED_DAYS`\n(default 30) qualifies EVEN with `surfaced_count > 0` — a surface event that has\nitself gone stale loses its issue #21 protection; `NULL last_surfaced` keeps the\n`surfaced_count = 0` rule unchanged and `signal != none` is never pruned.\n\nConsolidation is **single-flighted**: concurrent runs (several sessions starting\nat once) take an advisory lockfile in the store dir, and the losers skip cleanly\nand exit 0 rather than clustering the same rows twice.\n\n### organize — sleep-time organization (SessionStart cadence job)\n```\n\u003Cinterpreter> \u003Cstore.py> organize [--prune] [--dry-run] [--force] [--json]\n```\nThe SessionStart sleep-time maintenance job (issue #62, 7.7): the\n`session-cadence` batch that the start hook launches detached runs ORGANIZE, not\nconsolidate. It bounds an \"episode\" of work to the most recent live rows,\nbackfills any enrichment those rows are missing, runs consolidate's identical\ncluster\u002Fabsorb\u002Fcontested machinery on exactly that set, then adds sleep-time\ndeliverables: a topic hierarchy, hierarchical extractive summaries, and\nextractive compression of the rows consolidation just grew. Everything is\ndeterministic and LLM-free by default; there is no `--threshold`\u002F`--namespace`\nto keep the two maintenance entry points aligned (see the consolidate section:\norganize and consolidate SHARE one cadence gate implementation, one cadence\nclock, and one single-flight \"consolidate\" lock, so on a given store at most\none maintenance run happens per cadence window — whether it was triggered\nfrom SessionStart, a manual CLI call, or the Hermes session-end hook).\n\nPipeline (in order):\n1. **Shared cadence gate** — the SAME 7-day \u002F 20%-growth gate as consolidate\n   (ONE shared implementation, `_cadence_gate_skipped`, so the two entry\n   points can never drift), using consolidate's `last_consolidation` meta keys\n   (two entry points, one clock). `--force` is the only bypass. A gated run\n   prints `[zmem] organize: skipped by cadence gate (...)` and changes\n   nothing; `--dry-run` models the gate (`would skip by cadence gate`).\n2. **Optional idle gate** — `ZMEM_ORGANIZE_IDLE_HOURS` (default 0 = off). When\n   set, organize refuses to run unless the store has had no live-memory activity\n   (max of ingestion_ts\u002Flast_retrieved\u002Flast_surfaced) for at least that many\n   hours: sleep-time jobs should not churn a store that is mid-use. A skip prints\n   `[zmem] organize: skipped by idle gate (...)`.\n3. **Working set \u002F episode bound** — the N most recent live rows\n   (`ZMEM_ORGANIZE_EPISODE_BOUND`, default 256, clamped to the per-namespace\n   consolidation cap), box-wide. 0 disables real work. organize's own summary\n   rows are EXCLUDED from the episode — keyed on the structural\n   `source_ref` `organize:` prefix, never the (mutable) tags column, so\n   summaries are the pipeline's OUTPUT and never its input. The `organize:`\n   prefix is therefore RESERVED for summary rows: a manually-added row whose\n   `--source-ref` begins with `organize:` is excluded from episodes the same\n   way, and consolidate additionally EXCLUDES every live `organize:%` row\n   from its candidate set so the pipeline's output can never become its input\n   (issue #77). Every CLI write surface (`add`, `update`, `ingest-jsonl`,\n   including per-row `source_ref` fields in ingested JSONL) warns when a\n   non-empty source_ref carries the reserved prefix —\n   `[zmem] WARNING: source_ref prefix organize: is reserved for organize\n   summaries` on stderr; the write is NOT rejected and stdout (including\n   `--json` output) is untouched.\n4. **Entity backfill** — every working row missing `memory_entity` links gets\n   them via the deterministic extractor (idempotent; rows with nothing\n   extractable are counted as candidates but link nothing). The card sweep's\n   env configuration (`ZMEM_CARD_MIN_MENTIONS` \u002F `ZMEM_CARD_TOP_N`) resolves\n   BEFORE this loop — before ANY write of the run (PR #385 review F-4); an\n   invalid value prints one stderr line, skips the card stage with zero\n   counts, and never aborts the run. Immediately after the backfill\n   (issue #179): **the entity-card cadence sweep** — see \"Entity cards\n   (issue #179)\" below. Per entity (ascending entity-id order) one\n   deterministic card is created, refreshed, or tombstoned; the report (and\n   `organize --json`) carries the five counts under `entity_cards`:\n   `created` \u002F `refreshed` \u002F `unchanged` (SHA-256 no-op — no SQL write) \u002F\n   `removed` (a live card that fell below the threshold) \u002F `skipped`\n   (non-qualifying entity with no live card, AND per-entity apply failures —\n   one bad entity rolls back only its own transaction, logs one stderr\n   line, and the sweep continues). `--dry-run` computes the same\n   counts read-only; the session-cadence JSON envelope carries them under\n   `entity_cards` too.\n5. **Link backfill** — every working row missing `memory_link` edges gets them\n   via the standard write-time linker (`related`\u002F`contradicts` at\n   `ZMEM_LINK_THRESHOLD`).\n6. **Episodic consolidation** — `consolidate` on EXACTLY the working set\n   (keeper selection, absorb, contested guard + optional NLI judge, bounded by\n   the same per-namespace cap). The optional NLI judge is bounded by a\n   RUN-WIDE budget (issue #77): `ZMEM_NLI_MAX_CALLS` (default 64) judge\n   subprocess invocations and `ZMEM_NLI_MAX_SECONDS` (default 30.0) seconds\n   of wall time per run; invalid, non-finite, or non-positive values fall\n   back to the defaults. When the budget is exhausted the judge stops, one\n   `[zmem] consolidate: NLI judge budget exhausted (...)` warning names the\n   reason, and affected contested clusters stay unmerged — even under\n   `--merge-contested`. Judge diagnostics carry member ids, pair indexes, and\n   the verdict only, never memory content. organize's synthesized writes\n   (compression replacements and summaries) declare `capture_mode=\"auto\"`. The vector neighbor lookup is bounded to the\n   episode too — an out-of-episode near-duplicate in the global vec0 index is\n   never pulled in. See the consolidate section for semantics.\n7. **Compression** — the keepers consolidation actually GREW, when their\n   content exceeds `ZMEM_KEEPER_COMPRESS_CHARS` (default 4000; 0\u002Fnegative\n   degrades to the default — it is a cap, not a switch), are compressed\n   deterministically: order-preserving UNIQUE sentences, dropping only from\n   the TAIL on whole-sentence boundaries (never mid-sentence; the pre-compress\n   text stays on the superseded history row and `merged_from` provenance is\n   carried to the replacement). Compression runs BEFORE topic identity is\n   recorded: compression replaces the keeper's id (append-only update), so\n   keying topics first would leave every later run with a different key and a\n   duplicate summary.\n8. **Topics + summaries** — a related-graph over the POST-COMPRESSION live\n   working rows using the SAME neighbor predicate at the same threshold\n   consolidate would use; leftover singletons are grouped by SHARED entity\n   (A-MEM lite). Every live NON-summary working row lands in exactly one\n   topic. Topics of ≥3 members get a **hierarchical extractive summary**: a\n   REAL row (`type=fact`, tags exactly `summary,topic`, `signal=none`,\n   confidence 0.5, `source_ref=organize:\u003Cmembers>`,\n   `merged_from=\u003Cmembers>`), built from the first sentence of each member\n   (de-duplicated, capped, never truncated mid-sentence). Members stay live;\n   the summary is FTS\u002Fentity\u002Fsearch recallable. Idempotent: a re-run finds the\n   existing summary by its STRUCTURAL identity (`source_ref` + `merged_from` —\n   never the mutable tags column) and UPDATES it (Phase 4 of the issue)\n   instead of duplicating; when a topic's membership legitimately changed,\n   the stale overlapping summary is superseded (`reorganized into a changed\n   topic`) rather than left as a live orphan. Summary writes never propagate\n   the `summary,topic` marker onto neighboring user rows. If the\n   update\u002Fcreate would fold into a dedup target, organize logs and skips\n   rather than rewriting a stranger's row.\n9. **Unrecalled prune (pass-through)** — `--prune` enables the `consolidate\n   --prune` rule, extended (issue #62, 7.6): a live row also qualifies when its\n   `last_surfaced` is OLDER than `ZMEM_UNRECALLED_DAYS` (default 30) even with\n   `surfaced_count > 0` — a surface event that has itself gone stale loses its\n   issue #21 protection (both boundary comparisons are made in a\n   format-agnostic way, so the store's ISO-8601 timestamps compare correctly\n   against the cutoff). `signal != none` is never pruned; never automatic.\n\n`--dry-run` writes NOTHING (no meta, no backfill, no summaries) and the `--json`\nreport carries per-step would-be counts (`entity_backfill`, `link_backfill`,\n`entity_cards`, `topics`, `summaries`, `compressed`, `pruned`, `episode_ids`,\n`consolidate` sub-report).  A concurrent organize\u002Fconsolidate loses cleanly (exit 0, a\n`- skipped` notice, or `{\"error\": \"organize lock busy\"}` under `--json`).\n\nOptional local NLI judge (7.5): when `ZMEM_NLI_CMD` is set to an argv template\n(e.g. `ZMEM_NLI_CMD='python \u002Fpath\u002Fto\u002Fyour\u002Flocal-judge.py'`), consolidate's\ncontested branch consults it for mixed-polarity clusters; only an `entailment`\nverdict on EVERY polarity-flagged pair — ANY two members whose negation\npolarity differs, not only pairs anchored on the keeper — un-parks the merge,\nand any other verdict or failure parks it (never auto-merges; a failure is\nreported on stderr as a distinguishable NLI diagnostic). The judge receives\nthe two first-sentences on stdin (one per line, UTF-8) and prints a verdict\nword. On Windows, backslashes in the template are normalized to forward\nslashes before parsing (POSIX-style backslash stripping would otherwise\nmangle `C:\\path\\judge.py`; quote paths containing spaces). Unset =\nbyte-identical behavior (no NLI); a deterministic regex polarity remains the\nfallback for a response you judge ambiguous.\n\nSchema compatibility: organize NEVER changes the schema. All of its outputs use\npre-existing columns (`merged_from`, `source_ref`, `tags`, `confidence`,\n`source_hash`), so a store stamped at an older schema version keeps opening\nnormally in an older client until the new version is installed — verify a\ntemplate store with `store.py organize --dry-run` before upgrading.\n\n### Entity cards (issue #179)\n\nOne deterministic `entity_card` memory row per qualifying entity — a\ncontinuously refreshed summary card with provenance, built and maintained\nentirely store-side. The #172 taxonomy decision REJECTED this type; the #179\nworkstream contract reverses that decision (the #175 kinds precedent: the RFC\nmap is a frozen record, not a veto), and `ALLOWED_TYPES` carries\n`entity_card` as the 8th type. A card is an ordinary `memory` row keyed by\n`source_ref=card:\u003Centity_id>`; the schema stamp is **17** (0.107.0, PR #385\nreview F-1): the bump is STAMP-ONLY (no DDL), but it is upgrade-required —\nsee the operator note below.\n\n**KEY OPERATOR NOTE (schema 17, upgrade-required; FORWARD since\nsuperseded — see the schema 18 note above).** For THAT bump\n`FORWARD_COMPAT` deliberately stayed at 15: every pre-0.107 client (\u003C= 0.106) sharing a store\n**refuses** it, loudly at open, once the first 0.107 client migrates the\nstamp to 17. Without the bump those clients would fail SILENT on\n`entity_card` rows (an unknown type raises in their type preference and\ntheir hooks swallow the error — a fleet-wide memory blackout the review\nround executed end-to-end). On shared-store machines update every client\nBEFORE the first 0.107 host runs a cadence; `ZMEM_ALLOW_NEWER_SCHEMA=1` is\nthe operator stopgap (proceed at your own risk; it never downgrades the\nstore).\n\n**Type and birth invariant.** `entity_card` is CARD-REFRESH-ONLY: card rows\nare born ONLY through `store.py entity-card build|refresh` and the organize\ncadence sweep. Every interactive add surface refuses the type (`add --type`\nchoices derive from ALLOWED_TYPES minus `entity_card`; the Hermes\u002FMCP add\ntools validate against the same exclusion), jsonl ingest refuses it\nline-numbered (ingest bypasses add_memory and must never mint cards), an\nordinary `add_memory` call with the type but without the refresh path's\n`force_append=True` raises ValueError, every ordinary write rejects any tag\ncarrying the reserved `entity_card_sha256=` prefix BEFORE mutation, and\n(since the PR #385 review round) the `card:` source_ref prefix is reserved\nthe same way — an ordinary `add`\u002F`update` cannot claim a card identity.\nThe SAME guards cover the update path (review F-2): `update --type\nentity_card` on an ordinary row is refused (type conversion into the\nreserved type), an explicitly supplied watermark-prefix tag is refused, and\na `card:` source_ref claim is refused. Content-only updates of\npre-existing card rows keep the full type enum and succeed — the `state`\nprecedent; a transient watermark desync from such an edit self-heals at the\nnext refresh, which recomputes the digest over the new bytes (disclosed).\n\n**Configuration.** `ZMEM_CARD_MIN_MENTIONS` (default 5, inclusive) is the\nlive non-card mention count an entity needs to qualify;\n`ZMEM_CARD_TOP_N` (default 8) caps the card's provenance rows. An absent\nvariable selects its default; a present empty, non-integer, zero, or\nnegative value raises before any card write starts (CLI exit 2, store bytes\nunchanged).\n\n**Rendering.** The card is UTF-8, LF-terminated, fixed field order:\n`Entity: \u003Ccanonical_name>` \u002F `Kind: \u003Ckind>` \u002F `Aliases: \u003Csorted alias_norm>`\n\u002F `Mentions:` (one `- score=\u003Csix decimals> last_seen=\u003Cingestion_ts>\nid=\u003Cmemory_id> source_ref=\u003Cref> text=\u003Csingle-line>` per selected row) \u002F\n`Last seen: \u003Cmax ingestion_ts over EVERY live non-card source row linked to\nthe entity>` \u002F `Sources:` (one `- \u003Cmemory_id>` per selected row, same\norder). Mention text collapses CR\u002FLF\u002Fwhitespace runs to one space. Source\nrows are live (`superseded_at IS NULL`, empty `valid_until`), non-card,\nlinked to the entity across ALL namespaces, NULL confidence reads 0.0, and\nordered confidence DESC, ingestion_ts DESC, id ASC.\n\n**Provenance and lifecycle.** Every card row lives in `user:global` with\n`source_ref=card:\u003Centity_id>`, `signal=none`, confidence 0.5 (above the\nrecall floor — a signal=none row's inherited 0.2 would be invisible; the\nsummary precedent), `capture_mode=manual`, and the reserved\n`entity_card_sha256=\u003Chex>` tag (SHA-256 over the card's exact UTF-8 bytes —\nthe no-op watermark). The card's TAINT is the WORST OF its own\nsignal-derived default (signal=none -> `untrusted_tool`) and every SELECTED\nsource row's taint (PR #385 review F-8): one `untrusted_web` mention makes\nthe card itself `untrusted_web`, so the passive untrusted fence withholds\nthe card exactly as it withholds the source it aggregates. The card row is linked DIRECTLY to its entity —\nthat is its only entity link. REDACTION\nTRADEOFF (disclosed): the card is written with `capture_mode=manual` — the\npinned mode that keeps the watermark tag from being dropped — so its\nmachine-assembled content is NOT re-redacted at card write time; the source\nrows were already redacted at their own writes, and their rendered mention\ntext is the already-redacted form. Refresh is append-only and fail-closed:\nidentical content bytes AND matching watermark return `(existing_id,\nFalse)` with NO SQL write; changed content tombstones the old row\n(`superseded_at` AND `valid_until` both set — history stays queryable) and\ninserts the replacement with `update_of=\u003Cold_id>`; any failure (tombstone,\ninsert, link, watermark) raises and rolls back the whole transaction. The\nliveness decision is made on a RE-READ inside the `BEGIN IMMEDIATE` (the\npre-lock read is only the no-op fast path), and the `entity-card` CLI\nsingle-flights on the shared `consolidate` advisory lock with the cadence\nsweep, so two card writers cannot interleave (PR #385 review F-6). Writer\ntimestamps come from one `schema.now_iso()` value per sweep and NEVER read\n`ZMEM_TEST_NOW`. The cadence sweep (organize step 4b) enumerates entity ids\nin ascending byte order and returns exactly the five counts\n`created\u002Frefreshed\u002Funchanged\u002Fremoved\u002Fskipped`; a live card whose entity\nfell below the threshold is tombstoned and counted `removed`; `skipped`\nalso covers per-entity apply failures (an oversized card, a stale-plan\nsupersede) — one failure logs one stderr line, rolls back only that\nentity's transaction, and the sweep CONTINUES (PR #385 review F-4).\n\n**CLI.** `store.py entity-card build ENTITY --json` creates a missing\nqualifying card (`created`), refuses a live card with exit 2 and no write,\nand reports `no_card` (no write) below the threshold;\n`entity-card refresh ENTITY --json` returns `created` \u002F `updated` \u002F\n`unchanged` \u002F `no_card` (below-threshold refresh tombstones the live card\nfirst). `ENTITY` resolves an exact entity id first, then an exact canonical\nname (zero or multiple name matches: exit 2). `--json` stdout is one object\nwith the exact key order `result, id, entity_id, source_ref, warnings`\n(id\u002Fsource_ref are JSON null for `no_card`); errors before the write exit 2\nwith store bytes unchanged. The command single-flights on the shared\n`consolidate` advisory lock with the organize cadence (a lock-busy run\nexits 1 with a stderr notice, never a silent skip).\n\n**Cards never travel.** `export-jsonl` omits `entity_card` rows from every\nexport shape (live-only and `--include-superseded`): cards are store-local\nderived state, every ingest surface refuses the type, and the destination\nre-derives them via its own organize cadence sweep (PR #385 review F-3).\n\n**Same-fence recall suppression.** With `prefer_cards=True` (the default,\n`entity_match_ids`), a live card of a query-matched entity enters the\nentity arm with that entity's relevance. One suppression pass resolves the\nselected card rows by exact `source_ref=card:\u003Centity_id>` plus a direct\nentity link — never by matching card text — and removes ALL live non-card\nrows linked to those entities from the fence under the active namespace and\n`as_of` predicates: the fence renders the card, not the mentions it\nsummarizes. The pass runs at EVERY merge site (complete enumeration, PR\n#385 review F-5): (1) the scoped five-tier pools when the global tier is\nincluded (`--include-global`); (2) the cross-tier merge (namespace +\n`--include-global`); (3) the UNSCOPED default surface (`namespace=None`,\nthe default `recall --query` path) — its single all-namespace pool renders\none fence, so the card's represented sources leave it too; (4) the\ncross-project hazard pool, cleaned before its rows splice into the fence;\n(5) `--explain`'s presented pools, mirroring (1)-(3), AND its deep\n\"scored but beyond --limit\" pools, so the verdict stream agrees with the\npresented fence; and (6) the suppressed-id set is threaded into\n`expand_recall_links`' exclusion, so a removed source row cannot re-enter\nthe fence as a 1-hop link neighbour of a surviving row. The PASSIVE\nprefetch lane (`recent` \u002F SessionStart pulls) never renders cards at all —\n`entity_card` rows are excluded from every recent pool, so the query-less\nbranch can never render card and sources together. A query with no selected\ncard keeps its current result; a project-only or scoped-without-global\nrecall selects no card and applies no suppression. (Disclosed placement:\nthe pass runs after per-tier arm caps, RRF fusion, and MMR — suppressed\nrows may already have consumed tier slots; per-tier placement is impossible\nbecause the card lives in `user:global` while its represented sources live\nin project fences.) Cards are NOT in `PROTECTED_INJECT_TYPES` — they are\nordinary recall rows for explicit id retrieval.\n\n### rekey-namespace — remediate stranded namespace rows (admin)\n```\n\u003Cinterpreter> \u003Cstore.py> rekey-namespace --near-miss-global [--to user:global] [--dry-run] --confirm\n\u003Cinterpreter> \u003Cstore.py> rekey-namespace --from \u003Cold-namespace> --to \u003Cnew-namespace> [--dry-run] --confirm\n\u003Cinterpreter> \u003Cstore.py> rekey-namespace --map \u003Cmap.yaml> --dry-run\n\u003Cinterpreter> \u003Cstore.py> rekey-namespace --map \u003Cmap.yaml> --confirm\n```\nRewrites the `namespace` column of live rows. The primary use is remediating\nlegacy rows stranded under a global near-miss namespace (`global`,\n`userglobal`, …) that pre-date the `add`\u002F`ingest-jsonl` write-time guard — such\nrows are unreachable from the automatic hooks. `--near-miss-global` rekeys EVERY\nlive near-miss row to `--to` (default `user:global`); `--from` rekeys one exact\nnamespace. `--to` may not itself be a near-miss. `--confirm` is required to\nwrite (without it, or with `--dry-run`, the command only previews). Superseded\nrows are left untouched (history preserved).\n\nSince issue #71 C this remediation also runs AUTOMATICALLY: every\nstore-opening command rekeys global-near-miss rows to `user:global` before\ndispatching (silent on healthy stores; `ZMEM_AUTO_REKEY=0` or\n`--no-auto-rekey` opts out). The explicit command remains for previews and\nfor kill-switch deployments. Namespace contract: fleet facts live in\n`user:global`; project facts in `project:\u003Ccanonical-git-remote>`; MCP `add`\nwithout a namespace uses `ZMEM_MCP_DEFAULT_NS` when the operator set it,\nelse `user:global` — a bare `global` is never invented.\n\n`--map` is separate from the legacy selectors. Its file is UTF-8 without a BOM\nand contains only unindented quoted pairs such as\n`\"db:source-prefix\": \"fleet:scope\"`; blank lines and full-line comments are\nallowed. Entries are evaluated in file order, so the first matching `source_ref`\nprefix wins. A source prefix must also exclude C0\u002FC1 controls, whitespace, and\n`=` so it cannot forge key\u002Fvalue fields in the decision log. Every target must\nsatisfy the shared namespace grammar; its value is percent-encoded in the log,\nso valid project\u002Fuser names containing spaces or `=` remain safe. Map mode\nrequires exactly one of `--dry-run` or `--confirm` and cannot be combined with\n`--from`, `--to`, or `--near-miss-global`.\n\nBoth map modes enforce the store's forward-schema compatibility gate through\nthe connection they use for the operation. If the store has a newer unsupported\nschema, upgrade this plugin or use the documented compatibility override before\npreviewing or applying the map.\n\nThe preview opens the existing store read-only and prints one count per map entry\nplus `unmapped`, with no backup, log, lease, migration, or auto-rekey. Apply takes\na verified snapshot before its single transaction, updates and relinks only IDs\nwhose namespace changes, then appends one decision-log line per map entry with\nseparate `matched` and `moved` counts. A post-commit decision-log failure reports\nexit 3 and explicitly says that the map changes are committed; pre-commit\nfailures return nonzero without that committed-state diagnostic. Restore the\nverified snapshot if recovery is required. Apply loads the optional sqlite-vec\nextension when available and checks vector bytes in the transaction; link\nendpoints are checked on every apply.\n\n### promote-store — merge a leftover second store (admin, issue #71 E)\n```\n\u003Cinterpreter> \u003Cstore.py> promote-store --from \u003Cpath-to-store.sqlite> [--dry-run]\n```\nOne-shot merge of a leftover second store (e.g. `~\u002F.zcode\u002Fmemory\u002Fstore.sqlite`)\ninto the canonical one. Read-only on the source; source ids are PRESERVED so\nre-runs are no-ops; a NEWER source schema is refused. doctor's\n`second-stores` check fails when canonical is missing live rows that exist\noutside it (rows already tombstoned in canonical are only a WARN — nothing\nto promote) and recommends this command.\n\n### backup — verified snapshot with retention\n```\n\u003Cinterpreter> \u003Cstore.py> backup [--retention 7] [--out-dir DIR] [--if-due]\n```\nWrites `store-\u003CUTC timestamp>.sqlite` into the backup dir (`--out-dir`, else\n`$ZMEM_BACKUP_DIR`, else `\u003Cstore dir>\u002Fbackups`) using SQLite's Online Backup API,\nwhich is safe while other sessions are writing to the store. The snapshot is\nverified (`PRAGMA integrity_check` **and** a total\u002Flive row-count comparison\nagainst the source) before it counts as a backup — if either check fails the\nsnapshot file is deleted, the command exits non-zero, and neither the\n`last_backup` marker nor retention rotation happens.\n\nRetention deletes only the **oldest** files matching `store-*.sqlite` beyond the\nnewest N. Nothing else in the directory is ever touched — including the\n`prerestore-*` safety copies `restore` leaves behind. `--retention 0` disables\npruning.\n\n`--if-due` makes the command a no-op unless `$ZMEM_BACKUP_INTERVAL_DAYS`\n(default 1) has elapsed since the last successful backup; the SessionStart hook\nuses it so the automatic snapshot is cheap almost every session. Without the\nflag the backup always runs. Also single-flighted (its own lockfile).\n\n### restore — recover the store from a snapshot\n```\n\u003Cinterpreter> \u003Cstore.py> restore --from \u003Csnapshot.sqlite> [--force] [--out-dir DIR]\n```\nRefuses unless `--force` when a store already exists. Verifies the snapshot's\nown `integrity_check` **before** touching the destination, then takes a\n`prerestore-\u003Ctimestamp>.sqlite` copy of the current store (your rollback path —\ndeliberately outside the retention glob so rotation can never prune it), clears\nstale `-wal`\u002F`-shm` sidecars, copies, and re-verifies the restored store.\n\nTakes the **full maintenance lock ladder** — `maintenance`, `schema`, `backup`,\nand `consolidate` — for its whole duration, and refuses when a live writer\nlease exists, so it cannot race the automated background snapshot\u002Fconsolidation\nthe SessionStart hook fires, and refuses a destination that is not on a local\nfilesystem (no UNC\u002Fnetwork\u002FOneDrive path). If any lock is held it exits **2**\nwithout touching the destination — a skipped restore must never look like a\ncompleted one. A live interactive session's own `add`\u002F`recall` writes are\nblocked for the restore's duration (writers wait on the maintenance gate and\nfail clearly after a short timeout): still run `restore` when no session is\nactively writing.\n\n### Passive-lane credential withhold (issue #256)\nThe hook-driven `recall --for-injection` path re-scans every selected row\nwith the **credential** patterns from the write-time registry\n(`SECRET_CREDENTIAL_PATTERNS` — key=value shapes, PEM headers, `gh*_`\ntokens, AKIA keys; plus whatever shapes issue #180 adds). A matching row is\ndelivered as an id+type-only bullet marked `[WITHHELD: SECRET]`: its\ncontent, source_ref, tags, and entity names never reach the hook payload or\nthe model context, it is not counted in `surfaced_count`, and the delivery\nledger entry for it carries no credential text. The JSON envelope gains\n`secret_withheld` (a count) **only when a withhold happened** — clean runs\nstay byte-identical. Ordinary rows are untouched: rows carrying plain\n40-hex git SHAs render normally (the generic hex\u002Fbase64 detectors are\ndeliberately NOT used at read time). The re-scan runs BEFORE the token\nbudget, so a credential straddling the budget cut is withheld whole — never\nleaked as a clipped fragment — and `--for-injection` applies it on the\nno-session lane too. Since 0.142.0 the explicit lanes are filtered as well:\n`recall`\u002F`recent` reduce credential-shaped rows to the `[WITHHELD: SECRET]`\nstub (PR #440, issue #352 AC3); since 0.144.0 `list` (both the human line and\nthe `--json` payload, including the hub\u002Fremote read route) and `export-pack`\n(body bullets, the pack title, and the empty-pack refusal) reduce\ncredential-shaped rows the same way and blank a credential-shaped namespace\nat render (issue #443). `get` by id remains an unfiltered operator surface.\nSibling envelopes the selector\nbuilds for silent\u002Fkill-switch reasons never carry the key. Known residuals:\na credential stored ONLY as an entity `canonical_name`\u002Falias is not\ndetected — it renders in the fence's entity line and rides into the\ndelivery ledger via the entry text (issue #180-adjacent; purge covers the\nstore rows, not the entity tables, for those shapes); and the withheld\nbullet's metadata (id, type, namespace, confidence, signal) plus the\n`secret_withheld` counter still reveal to the model THAT a secret exists,\nby design. To remove a\nconfirmed secret from the store entirely, use `purge` (#255); rotation of\nany credential that was ever passively delivered remains mandatory.\n\n### rescan-secrets — live secret rescan\u002Fremediation (issue #181)\n```\n\u003Cinterpreter> \u003Cstore.py> rescan-secrets --dry-run | --apply\n```\nRe-evaluates every LIVE row through `apply_capture_policy` (auto mode).\n`--dry-run` prints one compact JSON line (`mode`, `rows_scanned`,\n`rows_needing_review`, ascending `ids`) through a read-only handle — the\nscanned store stays byte-identical. `--apply` redacts each candidate via\n`update`'s append-only lineage (tombstone + `[REDACTED_SECRET]` successor\ncarrying `update_of`) in ONE transaction; any policy refusal or dedup fold\nrolls the whole batch back (exit 2, store untouched). The tombstoned\npredecessor keeps its plaintext (FTS-indexed, `--as-of` reachable) — run\n`purge` (below) on the reported ids for durable removal.\n\n### embedding-health — print embedding lane health as JSON (issue #131)\n```\n\u003Cinterpreter> \u003Cstore.py> embedding-health [--json]\n```\nRead-only vector-lane diagnostic. `--json` prints exactly one compact\nsorted-key JSON line (reason, vector_status, notice_created,\nnotice_present, models_dir, profile, last_successful_embed,\nlast_successful_index, unembedded_backlog) through a read-only store\nhandle — the scanned store stays byte-identical. When the lane is\ndegraded the command creates the once-per-data-dir sentinel\n(`\u003Cdata dir>\u002Fembedding-degraded.notice`, canonical four lines:\nreason\u002Fmodels_dir\u002Fprofile\u002Ftimestamp) via an O_CREAT|O_EXCL write, prints\n`vector lane: degraded (\u003Creason>)` to stderr, and still exits 0 — a\ndegraded vector lane is a supported state; only an unreadable store\nexits 1. The sentinel is never rewritten (the O_EXCL write records the\nfirst degradation in a data directory exactly once), but the\n`vector lane: degraded (\u003Creason>)` stderr line prints on EVERY degraded\nrun.\n\n### purge — durably remove a memory's content (issue #255)\n```\n\u003Cinterpreter> \u003Cstore.py> purge --id \u003Cid> [--id \u003Cid2> ...] [--scrub-backups --out-dir DIR] [--json]\n```\n`update`\u002F`invalidate` **tombstone** a row — the plaintext survives in the row,\nthe FTS index, side tables, derived copies and ledger files. `purge` is the\noperator primitive that actually removes content, and it takes **ids ONLY**.\n**Never pass secret text on a command line in a hooked session**: the evidence\nwriter records tool-call input into `evidence` rows and would re-insert the\nfragment.\n\nWhat one purge does:\n- resolves the id plus its verified `update_of` predecessor chain and deletes\n  those `memory` rows, their FTS entries, their `memory_vec` rows (by\n  `memory_id`) and every id-keyed side-table row (`memory_link`, both\n  endpoints; `memory_entity`, `episode_memory`, `memory_evidence`,\n  `belief_head_source`, `belief_head_evidence`);\n- deletes evidence rows and entity\u002Falias rows the purge orphaned (evidence a\n  surviving row still references is kept);\n- **rewrites, deletes, or refuses** derived copies that carry the text:\n  consolidation keepers (the `\\n\\n--- merged from \u003Cid> ---\\n` block\n  is stripped and entities re-linked), belief heads (rebuilt from surviving\n  sources, or deleted when sourceless), and extractive episode-summary rows\n  (episodes that contained a purged member lose their summary row — expect\n  deletion, not surgical editing). A keeper refuses the whole purge (exit 4,\n  naming the row, nothing deleted) when `merged_from` names a purged id but\n  its stored block header is missing (the content was compressed or drifted),\n  or when an 8+-character token of the absorbed block still appears in the\n  keeper's own text — the byte-verify suppresses needles that a surviving row\n  holds, so that residue could not otherwise be distinguished from a clean\n  rewrite;\n- compacts the store (FTS `'optimize'`, `VACUUM`, WAL checkpoint) and\n  **byte-verifies** the result: a case-insensitive scan of `store.sqlite` and\n  `-wal` must find no copy of the purged content. Exit **5** means the text\n  still lives somewhere outside the memory rows (the message names the\n  needle): typically an evidence row no memory references — the rescan that\n  owns those is issue #181;\n- scrubs the ids (and any needle-bearing entry) from the `\u003Cdata>\u002Fops\u002F*.ledger`\n  delivery ledgers **and the issue-#252 JSONL segments\n  (`\u003Chash>.ledger.seg.\u003Chex>`)**, and removes orphaned `.ledger.tmp.*` and\n  `.signals.tmp.*` partial writes. The\n  `.pending`\u002F`.compact`\u002F`.feedback.jsonl`\u002F`.signals` ops\n  sidecars are NOT scrubbed (outside the ledger contract; `.signals`\n  carries only signal names\u002Fstatuses\u002Fcounts, and the finished file is\n  swept on the shared ops mtime policy) — treat them as residue surfaces.\n  The `\u003Chash>.tasktext` delegation stash IS in scope since issue #253: purge\n  needle-scrubs its entries (they carry untrusted host prose and have no\n  memory ids) and reaps orphaned `*.tasktext.tmp.*`. The residue verify\n  step scans the full delivery-state scope (all eight registered families\n  plus the broad legacy ledger\u002Freserve globs, issue #322), so a stash the\n  scrub SKIPPED because it was unreadable\u002Ftorn (the reader tolerates those\n  states by design) is no longer exempt: needle bytes in any scanned\n  family fail the purge closed; see #336 for the historical verify-glob\n  trade-off;\n- records the ids in the `purged_id` deny-list so `ingest-jsonl` will not\n  re-insert them from a peer export (child records referencing a purged id\n  are skipped or blanked, never fatal);\n- if a post-commit compaction, ledger scrub, or snapshot scrub fails, a later\n  `purge --id` for the deny-listed id resumes the cleanup phases. The retry\n  derives its scrub needles from each surviving snapshot row, so it does not\n  require the original plaintext as a command-line argument. A scrub retry\n  never re-verifies the live store (the purged rows are absent, so no live\n  needles can be derived) and says so instead of claiming a clean verify;\n- `--scrub-backups --out-dir DIR` applies the same removal inside every\n  `store-*.sqlite` **and** `prerestore-*.sqlite` snapshot in DIR, rewriting\n  files **in place** (never deleted, truncated, or renamed) and re-verifying\n  each with `integrity_check`.\n\nVector erasure is fail-closed: when a `memory_vec` table exists, purge must\nload sqlite-vec and verify that no vector rows remain for the deleted ids. If\nthe extension cannot be loaded, the purge refuses rather than claiming the\nvectors were erased. `--scrub-backups --out-dir DIR` also requires DIR to be\nan existing local directory.\n\nLocking: the full `restore` posture (maintenance → schema → backup →\nconsolidate locks, plus a live-writer refusal), so nothing can write between\nthe deletes and the byte verification.\n\nExit codes: **0** purged and verified clean; **2** bad usage; **3** unknown\nid (named); **4** refused (maintenance\u002Fschema\u002Fbackup\u002Fconsolidate lock or live\nwriter); **5** residue remains in the store or ledgers — OR transient\ndelivery-ledger lock contention (re-run; the message names which); **6**\ncompaction\u002Fscrub step failed.\n\nPost-commit cleanup failures leave the deny-list entry in place by design;\nre-run purge for that id after fixing the named surface. `import-store.py\n--force` and `restore` from a pre-purge snapshot are explicit whole-store\nreplacements — they supersede both the removal and the deny-list, so **re-run\npurge after either** (a restore's `prerestore-*` safety\ncopy of the pre-restore store preserves the deny-list rows recorded between\nsnapshot and restore, so the ids to re-purge are recoverable by SQL from that\ncopy). Purge is also honest about a structural carve-out in the byte\nverification: memory content that is a substring of SQLite's own schema DDL\ntext (contrived single-word contents like `namespace`) cannot be\ndistinguished from schema bytes, so no needle is scanned for it. Surviving\nrows that still quote purged-content tokens (e.g. a mid-chain `update`\nsuccessor, kept by design) are listed as WARNINGs in the summary. Exit **5**\nhas two shapes: (a) residue detected after deletion — the deny-list rows are\nalready recorded while content may remain: clean the named surface, then\ndelete those rows from `purged_id` (or re-purge the surviving carrier) so\npeer imports stay denied only while content exists; (b) the delivery-state\nskew refusal (issue #322) — `\u003Cdata>\u002Fops` held delivery-state files from a\nfamily this client does not support, and the purge refused BEFORE touching\nthe store: nothing was deleted, no `purged_id` rows exist, and the named\nfiles must be handled per the refusal's recovery text (upgrade this client,\nor remove the listed files after quiescing writers). Exit **2** also covers\nan invalid `ZMEM_TEST_LEDGER_FAMILIES` value (test seam misconfiguration).\nDetection scope note: the read-time-style scan here covers the row's\ncontent, source_ref and tags; a credential stored ONLY as an entity\nname is not detected by purge's needle scan or by the passive lane's\nwithhold (issue #180-adjacent residual). Keeper rewrite note: a\nconsolidation keeper whose own base text shares words or short phrases\nwith the absorbed text keeps those shared fragments after the strip —\nthey are indistinguishable from the keeper's own vocabulary; the\nfull purged text is always removed or the purge refuses, and the\nbyte-verify's novel-token needles cover genuinely novel residue. Peer\nimports skip evidence rows that were junctioned to a purged memory\n(including their episode junctions), so purged excerpts stay gone.\n\n### sweep — prune stale per-session cooldown sentinels\n```\n\u003Cinterpreter> \u003Cstore.py> sweep [--marker-dir DIR] [--max-age-days 7] [--dry-run]\n```\nRemoves the `.capture-prompted-\u003Csession>` \u002F `.convention-prompted-\u003Csession>`\ncooldown markers the capture\u002Fconvention hooks leave in the data dirs (issue #23).\nA marker is only meaningful for the session named in its filename, so anything\nolder than `--max-age-days` (default `$ZMEM_SENTINEL_SWEEP_DAYS`, else 7) is\ngarbage. Sweeps the union of every dir the two hooks can write into (their\nresolution chains differ), never touching anything that is not a sentinel-prefixed\nfile. Idempotent and fail-open. The SessionStart hook fires it detached each\nsession so the markers stay bounded; `--dry-run` counts without deleting.\n\n### corrections — mine user corrections from a transcript (read-only)\n```\n\u003Cinterpreter> \u003Cstore.py> corrections --transcript \u003Cpath> [--json]\n```\nScans a Claude Code transcript JSONL for user corrections. (User *rejections*\nare a separate signal — reported by `store.py failures` and surfaced by the\nStop \u002F SubagentStop reflection hooks.) Prints `{\"count\": N, \"items\": [{message,\ntype, patterns, confidence, sentiment, decay_days}]}`. **Read-only** — this\ncommand never opens or writes the store; candidates are reviewed by an\nagent\u002Fhuman before any `add` (signal honesty). Corrections are detected by the\nported correction-pattern library (`skills\u002Fmemory\u002Fscripts\u002Fcorrections.py`, from\nthe MIT claude-reflect project). This parses **Claude Code transcript format\nonly**; other hosts' histories are out of scope. In `ZMEM_CAPTURE_MODE=auto`\nlikely-secret text is redacted; in `manual` matching items are annotated\n`\"secret_warning\": true` but kept verbatim for review. (`--json` is accepted for\nparity with the issue's syntax; output is always JSON.)\n\n### signals — recognized runner signals for a session (read-only)\n```\n\u003Cinterpreter> \u003Cstore.py> signals [--session SESSION] [--transcript PATH] [--db PATH]\n```\nReports the session's tracked runner signals as\n`{\"signals\": {\"test\": \"pass\", \"lint\": \"fail\", ...}}` — the LAST status of\neach recognized runner family in the transcript, classified by\n`capture_quality.infer_signal` (`test`\u002F`compile`\u002F`lint`; everything else,\nincluding the db substrate where `tool_usage` carries no command text, is\nuntracked). A user-REJECTED runner call records no signal (the run never\nexecuted — same rejection split as `failures`), and an error-shaped\n`toolUseResult` sibling counts as `fail` even when the block's `is_error`\nflag is false. This is the read command behind the Stop hook's\nsignal-change gate (issue #258). **Read-only and store-independent** like\n`failures` — dispatched before `connect()`. Exit-code contract: checked results\n(including a missing transcript path, which falls to the db leg) exit 0; a\ngenuine substrate error (transcript present but unreadable) prints\n`{\"signals\": {}, \"error\": ...}` and exits 2, so the hook can suppress\nwithout touching its persisted state.\n\n### source-exists \u002F ops-append — capture adapter bridges\n```\n\u003Cinterpreter> \u003Cstore.py> source-exists --namespace NS --source-ref REF --json\n\u003Cinterpreter> \u003Cstore.py> ops-append --namespace NS --session-id SESSION --tool TOOL --operation OP --json\n```\n`source-exists` prints `{\"exists\":false}` with exit 0 for a missing store and\notherwise opens the store read-only without creation or migration. `ops-append`\nprints `{\"ok\":true}` after appending the bounded operation-ring record; a\nno-token operation is the declared successful no-op (issue #163).\n`--namespace` is REQUIRED (old invocations without it exit 2 with no stdout\npayload). Invalid arguments or unavailable state print `{\"ok\":false}` and\nreturn nonzero; adapters translate that through their documented fail-open\nenvelopes. With `memory.provider=zmem` the plugin's native post_tool_call \u002F\npre_llm_call \u002F pre_verify callbacks run BESIDE any installed compatibility\nshell hooks (both paths are operational; suppression is operator-managed). Neither command authorizes a hook to\nimport store internals directly.\n\n`ZMEM_CONVENTION_INTERVAL` remains the legacy cadence for the Hermes\nconvention-compatibility hook only. It does not control the commit-only\n`zmem-convention-capture.sh` prompt.\n\n### queue-list \u002F queue-clear \u002F queue-add — review live-captured corrections (read-only \u002F clear \u002F author a note)\n```\n\u003Cinterpreter> \u003Cstore.py> queue-list --namespace NS [--json]\n\u003Cinterpreter> \u003Cstore.py> queue-clear --namespace NS [--id ID ...] [--all] [--drop-stale]\n\u003Cinterpreter> \u003Cstore.py> queue-add --namespace NS --message TEXT --type lesson \\\n  [--patterns P] [--confidence 0.7] [--sentiment note] [--decay-days 90] [--json]\n```\nThe `capture-correction` hook (UserPromptSubmit, Claude Code \u002F ZCode \u002F Codex)\nqueues mid-session user corrections (\"no, use X\", \"remember: ...\") into a\nnamespace-scoped sidecar file (`\u003Cstore-data-dir>\u002Fqueue\u002F\u003Cns>.json`, default\n`~\u002F.zmem\u002Fqueue\u002F\u003Cns>.json`; follows the store's `ZMEM_STORE`\u002F`ZMEM_DATA` override\nchain, not a fixed path) — these recall\u002Fcapture hooks never `add` rows and\nwrite nothing else to the store themselves (the one sanctioned exception is\nthe DETACHED SessionStart `session-cadence` maintenance task, which runs\norganize\u002Fbackup\u002Fsweep on its own lease). `queue-list` shows the pending candidates for review (emit them into the\nstore via `add --signal user` only if they clear the closeout rubric);\n`queue-clear` removes processed ids (`--id`), empties the queue (`--all`), or\nprunes stale low-confidence items (`--drop-stale`) — exactly one selector is\nrequired (a flag-less invocation is rejected, not a silent full wipe). All three are store-independent\n(they never open or write the store), so they work even if the store is locked or\nmissing.\n\n`queue-add` (issue #260) is the write-side sibling: a closeout session can jot\na cheap, low-ceremony note for a FUTURE closeout to review — cheap now, fully\nprocessed later — instead of running the full `add` ceremony (embedding,\ndedup\u002Fsupersede matching) on a note whose whole point is to be reviewed first.\nIt appends through the same capture queue machinery the hook uses (same\natomic write, same `MAX_QUEUE_SIZE` oldest-drop cap) and stamps\n`source: \"closeout-note\"` so `queue-list` consumers can tell a\ncloseout-authored note from a live-captured (`source: \"live-capture\"`) or\nmined (`source: \"history-mine\"`) candidate. Secret redaction covers the whole\nnote: the message follows the capture policy (`ZMEM_CAPTURE_MODE` — auto\nstores the redacted form, manual keeps the original with `secret_warning`),\nand the free-text `--patterns`\u002F`--sentiment` metadata is always stored\nredacted (flagging `secret_warning` when redaction fired). Input guards\nmirror the promotion path: `--message` is bounded by the store's\n`MAX_CONTENT_CHARS`, `--confidence` must be a finite value in [0, 1], and\n`--decay-days` must be at least 1 so every item stays reachable by\n`queue-clear --drop-stale`. Because `--message` rides the process command\nline, route secret-bearing content through the `capture-correction` hook\n(stdin transport) rather than pasting tokens into a note. A `queue-add` item\nis reviewed under the SAME closeout Step 0.5 rubric as any other queue item —\nthere is no fast track; `--type` accepts exactly the `add` command's type\nenum, so an accepted note needs no type conversion when it is promoted.\n\n### retain \u002F retain-adapt — evidence-gated automatic retention (issue #177, default off)\n```\n\u003Cinterpreter> \u003Cstore.py> retain [--input PATH|-] --namespace NS --session ID \\\n  --host claude|codex|zcode|hermes-provider|hermes-compat [--dry-run]\n\u003Cinterpreter> \u003Cstore.py> retain-adapt --input PATH|- --namespace NS \\\n  --session ID --host claude|codex|zcode|hermes-provider|hermes-compat\n```\nThe automatic-retention surface (Workstream K PR 6). `retain` classifies\nnormalized JSONL evidence rows through the deterministic extractor\n(`storelib\u002Fretain.py::extract_candidates`) into REVIEW-QUEUE CANDIDATES —\nnever memory rows: explicit `remember:` rows (class `explicit`), decisions\ncarrying a rationale marker (`because` \u002F `so that` \u002F `therefore` \u002F\n`rationale:` \u002F `reason:`, class `decision`), `Actually,`-style corrections\n(class `correction`, priced at the classifier's pattern-table confidence —\nthe live-capture short-message boost deliberately does not apply), and\ntwice-repeated runner errors (class `outcome` via `aggregate_errors`). Every\ncandidate keeps the full legacy review shape plus `class`, `evidence_ids`,\n`signal=none`, a stable 32-hex SHA-256 id, and a `retain_hash`; writes go\nthrough the same capped, atomic queue sidecar as manual capture (a failed\nwrite keeps prior writes and reports\n`[zmem] retain: queue write failed; written=\u003Ck-1>; total=\u003CN>`).\n\n**The gate (default OFF):** retention runs only when `\u003Cstore_dir>\u002Fconfig.json`\n(the file sits beside the store file; the queue itself lives in a `queue\u002F`\nsubdirectory beside it — read by the store-side `storelib\u002Fconfig.py` reader)\ncontains a literal boolean `{\"auto_retain\": true}`, or `ZMEM_AUTO_RETAIN=1`\nforces it on. Exact `ZMEM_AUTO_RETAIN=0` forces off; any other non-empty value\nprints `[zmem] retain: invalid ZMEM_AUTO_RETAIN` with\n`{\"count\":0,\"ok\":false}` and exits 0. A non-boolean `auto_retain` in\n`config.json` is REFUSED and leaves the gate CLOSED — a config meant to\ndisable retention cannot enable it. Exact `ZMEM_CAPTURE=0` is byte-passive — no\ninput read, no queue file, no store file. The Hermes provider's `auto_retain`\nconfig key (issue #161) is a SEPARATE provider-home setting that `sync_turn`\nconsults BEFORE spawning a child; the store subprocess never reads provider\nhome. Both commands dispatch before the store opens: a disabled run never\ncreates or migrates the store, and the stage driver checks the gate BEFORE it\nreads the transcript, so a default install never parses a transcript. The\nStop\u002FSessionEnd seams still start one interpreter to evaluate the gate before\ndeclining, because the shell cannot read the store-side config without one.\n\nBoth retention verbs answer with `{\"count\":N,\"ok\":bool}` on stdout. `ok` is\nfalse on a partial write, a classify failure or an invalid override, so a\ncaller can tell a successful no-op from a failure even though every hook path\nstill exits 0.\n\n`retain-adapt` normalizes a raw host payload (Claude transcript records —\n`isMeta` may be absent and `message.content` may be block-form —, Codex\n`id`\u002F`timestamp`\u002F`role`\u002F`content` records, ZCode\n`event_id`\u002F`ts`\u002F`role`\u002F`text` records, or a Hermes payload) into evidence rows\nstamped with the CLI provenance triple. Timestamps are accepted with or\nwithout millisecond precision and canonicalized to `...THH:MM:SSZ`.\n\n**Host wiring — which lanes share the subprocess seam:**\n- Claude\u002FCodex\u002FZCode Stop and Claude SessionEnd run\n  `hooks\u002Flib\u002Fzmem-retain-process.py` (10s per-child deadline, exit 124 on\n  timeout), which spawns `store.py retain-adapt` then `store.py retain`.\n- The Hermes provider (`sync_turn`, `on_pre_compress` checkpoint API v2) calls\n  `store.py retain` DIRECTLY with its own duplicate deadline copy; it does not\n  import the shared seam.\n- The Hermes compatibility `pre_llm_call` hook uses `subprocess.run` with its\n  own `ZMEM_MCP_TIMEOUT` clamp (8s default), not the shared seam.\n- The shared SessionEnd script does NOT run on the Codex SessionEnd lane — the\n  Codex fast path returns before that script spawns — so the retention stage\n  there is currently unreachable for Codex.\n\nHooks and providers consume child JSON only — no storelib import, no SQLite,\nno direct queue access. Every failure path exits 0 so a retention problem can\nnever break a host hook. Diagnostics are appended to a `zmem-retain.log`\nsidecar in the store data directory and echoed to stderr where the host\npreserves it — the Stop hook suppresses its own stderr, so the sidecar is the\nchannel that always survives.\n\nRetention is idempotent: candidates are keyed by a stable id, already-present\nids are skipped, and the batch runs under a per-namespace lock, so repeated\nStop\u002FSessionEnd fires over one transcript add nothing on the second run. The\n`remember:` classifier is unanchored, so material that reads as quoted or\nmachine-authored (a pasted answer, a project instruction file, a diff) is\nskipped rather than promoted as the user's own standing fact. The `decision`\nclassification needs a `kind` field no wired adapter emits, so it is\ncurrently reachable only by callers that set `kind` explicitly.\n\n### convention-drift — surface conventions a queued correction contradicts (read-only)\n```\n\u003Cinterpreter> \u003Cstore.py> convention-drift --namespace NS [--json]\n```\nRead-only drift scan (issue #261): loads the namespace's queued correction\nitems (the same sidecar `queue-list` reads — every item, regardless of its\n`stale` flag) and the namespace's live rows, keeps `type == \"convention\"`\n(filtered in Python — `list` has no `--type` flag), and reports each\nconvention row some queued item's message CONTRADICTS: the write-time dedupe\nguard's negation-polarity signal (polarity flip) plus at least one shared\ncontent stem (the stem term is specific to this scan; token-disjoint notes\nnever flag). `--json` emits `{\"candidates\": [{\"id\": ...}, ...]}`; plain mode\nprints one `- \u003Cid> :: \u003Cpreview>` line per candidate, or\n`(no convention-drift candidates)` when there are none; both modes exit 0.\nOn an existing store it writes nothing — no invalidate, update, supersede,\nor telemetry, and the store file stays byte-identical (a missing store is\ncreated by the ordinary open path, exactly like `list`) — surfaced ids are\nupdate candidates for the reviewer's deliberate `update`\u002F`invalidate` at\ncloseout (false positives are acceptable; a missed real contradiction is the\nfailure mode it minimizes). The queue loader fails open: a missing or\ncorrupt queue file reads as an empty result. Unlike the queue family it\nopens the store (it reads live rows), so it dispatches after connect like\n`list`; it is exempt from the automatic near-miss rekey (a pure-read surface\nmust not trigger the rekey's writes). Works without an embedding model — the\npredicate is lexical only.\n\n### export-pack — render a Tier 1 markdown memory pack\n```\n\u003Cinterpreter> \u003Cstore.py> export-pack --namespace NS [--out FILE] [--project-limit 50] \\\n  [--global-limit 15] [--min-confidence 0.6] [--max-bytes 32768]\n```\nRenders live memories from `--namespace` and `user:global` (confidence DESC,\nretrieval_count + surfaced_count DESC, ingestion_ts DESC) as a hand-off markdown pack,\ne.g. for a cloud\u002Fremote session with no store access. `--max-bytes` is a UTF-8\nbyte budget over the whole rendered pack (structural framing counts toward\nthe cap): a bullet that would push past it is omitted whole (never\ntruncated), smaller rows behind it are still emitted, and only framing\nappended after the walk (an empty section's heading and the trailing\nomitted-count note) can exceed the budget. Refuses (exit 2) if\nboth sections are empty.\n\n### export-jsonl — export Tier 3 sync JSONL\n```\n\u003Cinterpreter> \u003Cstore.py> export-jsonl [--out FILE] [--namespace NS] [--include-superseded]\n```\nWrites one memory row per line (no embeddings) for box-to-box sync via\n`ingest-jsonl`. Default: all namespaces, live rows only; `--namespace` scopes\nto one namespace, `--include-superseded` also exports tombstoned rows.\nEntity links are deliberately NOT carried in the JSONL (they are store-local\nderived data, like embeddings and content_norm): the receiving store rebuilds\nthem by re-running the deterministic extractor on ingest — see below.\n\n### Governed training capture and export (issue #135)\n\nAutomatic Claude, Codex, ZCode, and Hermes hooks may create a partial training\ncapture. Capture governance defaults to deny and a denied or missing policy\nstores metadata only; local capture consent permits redacted partial storage\nbut does not authorize export. A capture is exportable only after the actual\ninjection result has been snapshotted, a trusted workflow has acknowledged the\ndelivery, and a verifier has completed the capture with a supported outcome,\nevidence id, scoped memory ids, current export governance, and any required\nreviewer correction fields. Hook observations, Stop, tool success\u002Ffailure,\nand displayed text never complete a capture.\n\nCompletion evidence must match the outcome: test, compile, and lint require\ntest_result evidence; user_acceptance requires turn evidence; reviewer_acceptance\naccepts turn or correction evidence. correction_closeout requires\nreviewer_acceptance with correction evidence.\n\nThe explicit trusted local adapters accept reviewed JSON input and keep the\nstate transitions separate:\n\n```bash\n\u003Cinterpreter> \u003Cstore.py> capture-training-delivery --input delivery.json\n\u003Cinterpreter> \u003Cstore.py> capture-training-acknowledge --input acknowledgement.json\n\u003Cinterpreter> \u003Cstore.py> capture-training-completion --input completion.json\n```\n\nEvery reviewer-acceptance outcome requires a separate local review transition,\nincluding turn evidence. Reviewer-acceptance corrections additionally require\ncorrection evidence:\n\n```bash\nZMEM_TRAINING_CALLER_ID=operator-a \\\nZMEM_TRAINING_REVIEWER_IDS=operator-b \\\n\u003Cinterpreter> \u003Cstore.py> capture-training-review --input review.json\n```\n\n`ZMEM_TRAINING_CALLER_ID` is the local caller identity and\n`ZMEM_TRAINING_REVIEWER_IDS` is the comma-separated reviewer allow-list. The\nreviewer must differ from the verifier. These values protect the local\nworkflow against accidental self-review; they do not provide an external\nreviewer service or a boundary against a hostile local process. Capture ids,\nsession keys, and optional host task ids are correlation metadata only.\n\nThe delivery response contains the local `capture_id` and immutable\n`delivery_snapshot_id`; later adapters resolve the capture through the snapshot\nid. `host_task_id` is correlation metadata only when the host supplies it.\nReplayed compatible inputs are idempotent, while conflicting snapshots or\ncompletion data are refused. The current redaction masks credential-like,\nemail, and filesystem-path classes; it is not comprehensive PII anonymization.\nReviewer trust on this local completion path means a local operator's\nattestation; no external reviewer registry is implied.\n\nFinalized and revoked capture records, their local delivery\u002Fcompletion\u002F\nobservation rows, remain for 30 days. The detached `session-cadence` task\nattempts the purge before its backup step; a successful purge keeps expired\nrecords out of new backups. Doctor reports an intentional expired-retention\nwarning with output status `warn` and stays read-only; an operator can run the\nexplicit cleanup instead:\n\n```bash\n\u003Cinterpreter> \u003Cstore.py> purge-training-captures --confirm\n```\n\nBefore enabling export, install PyArrow in the store-host interpreter:\n\n```bash\npython -m pip install --disable-pip-version-check \\\n  -r skills\u002Fmemory\u002Fscripts\u002Frequirements-training.txt\n```\n\nThe Hermes server environment can install its packaged MCP requirements without\ninstalling the optional exporter:\n\n```bash\npython -m pip install --disable-pip-version-check \\\n  -r hermes-plugin\u002Fserver\u002Frequirements.txt\n```\n\nInstall `requirements-training.txt` separately in the interpreter that runs\n`export-training`; the training extra pins PyArrow `25.0.1` for deterministic\nParquet bytes.\n\nThe read-only doctor reports the exact requirements-file command when PyArrow\nis unavailable. Reload the host after updating a plugin cache and run doctor\nbefore exporting.\n\n### export-training — write reviewed SFT and preference views\n\n```bash\n\u003Cinterpreter> \u003Cstore.py> export-training DIR --snapshot-id ID \\\n  --reviewer-confirmed [--namespace NS] [--quarantine-raw]\n```\n\n`--snapshot-id` selects the immutable source export snapshot and\n`--reviewer-confirmed` is a required export authorization. Both are checked\nbefore SQLite opens or output is created. `DIR` is the training output\ndirectory itself and contains the fixed `sft-000.parquet`,\n`preferences-000.parquet`, `manifest.json`, and `deletion-map.json` artifacts\ndirectly. `deletion-map.json` is derived deduplication and exclusion metadata;\nit does not delete canonical SQLite rows or prior operator-owned output\nfolders. Empty eligible selections still produce schema-only Parquet files. A\nmissing PyArrow dependency or unavailable local model for a nonempty semantic\nexport fails closed before publish.\n\n`--quarantine-raw` writes a separate bounded, redacted derived dump under\n`DIR\u002Fquarantine\u002F` with `DIR\u002Fquarantine-manifest.json`. It uses the same\nverified, acknowledged completion gate as the Parquet views; partial, denied,\nrevoked, incomplete, or capture-quarantined records never reach any derived\nartifact. Training vectors are in-memory only and never sync or back up.\nOutput is published from a validated same-volume staging directory, with one\nadvisory per-output lock and the final manifest written last; consumers must\nreject output without a valid manifest. Parquet, quarantine, deletion-map, and\nstaging files are operator-owned disposable artifacts outside canonical-row\nretention; purging SQLite capture rows does not delete prior published output\nfolders. Doctor reports abandoned staging directories but is read-only by\ndefault. After confirming no export is running, an operator can\nexplicitly remove only stale direct, non-symlink `.training-staging-*` siblings\nof `DIR`:\n\n```bash\npython \u003Cdoctor.py> --project . --training-output .\u002Ftraining \\\n  --cleanup-training-staging --confirm-no-training-export\n```\n\nThe cleanup skips candidates newer than 24 hours and rechecks mtimes before\nremoval. For `.\u002Ftraining`, a candidate is `.\u002F.training-staging-*`. The remedy\nfor an expired-retention warning is `\u003Cinterpreter> \u003Cstore.py>\npurge-training-captures --confirm`, followed by another doctor run. The\nexporter bounds work at 10,000 source captures by default. Set\nZMEM_TRAINING_MAX_ROWS to a lower positive limit when needed; exceeding the\nlimit fails before publication and leaves no new manifest. The exporter has no\ncross-process lock that doctor could use to prove an export is\ninactive, so the confirmation flag is an operator assertion. Revoked captures\nare reported as an informational count and remain ineligible for export.\n\nv13 (issue #65, 10.7): every row carries a `kind` discriminator (`\"memory\"`);\nepisodes round-trip as additional `\"episode\"` and `\"episode_memory\"`\nrecords when any exist (memberships are emitted only when both endpoints are\nin the exported set, so the file is self-consistent). `ingest-jsonl` treats\na missing `kind` as `\"memory\"` (legacy files ingest unchanged) and applies\nrecords in dependency order (memory → episode → membership) regardless of\nfile order; re-ingesting the same file is an exact no-op.\n\n### ingest-jsonl — import Tier 3 sync JSONL\n```\n\u003Cinterpreter> \u003Cstore.py> ingest-jsonl --in FILE [--source-ref REF] [--allow-tombstones] [--capture-mode auto|reviewed|manual] [--trust authoritative|untrusted-peer] [--allow-namespace NS ...] [--attribute-host HOST]\n```\nImports a JSONL file written by `export-jsonl`. Every row is validated before\ntouching the store, and a bad row is counted and reported by line number\nrather than aborting the file. For files whose sender is NOT authoritative\nover this store, pass `--trust untrusted-peer` with one or more\n`--allow-namespace` values (issue #282): trust claims in the rows are\nclamped (taint, signal grounding kept only as `claimed_signal:` tags,\nconfidence\u002Ftrust_score ceiling 0.6), `host:`\u002F`agent:` attribution and\nsource provenance are stamped from the caller (`--attribute-host`,\n`peer:\u003Chost>`), every record kind is gated against the allow-list (evidence\nrecords refused outright; tombstone authority refused), and the peer-mode\nsummary line carries `namespace_refused=`, `authorship_refused=` and\n`trust_clamped=` counts (`authorship_refused` counts side records aimed at a\nrow this import did not author — a peer is never authoritative over a row it\ndid not land, though restating a lifetime the store already holds is a no-op;\nabsent trust_score resolves 1.0, so trust_clamped fires on most peer rows\nby design — it reports \"the policy altered this row\"). `--source-ref` overrides source_ref on every\nrow inserted this run (default: keep each row's own incoming source_ref).\n`--allow-tombstones` lets an incoming superseded row tombstone a live local\nrow with the same id — off by default; use it only when the file is an\nexport of a store you trust as authoritative for those ids.\n\nv11 (issue #61) notes: rows carry `trust_score` (restored verbatim, default\n1.0 for pre-v11 files; a PRESENT but non-numeric\u002Fnon-finite value makes the\nrow malformed — refused, counted, not stored) and outgoing `links` (validated fail-closed, applied\nafter every row lands so an edge may reference a later row;\n`links_added`\u002F`links_skipped` in the summary). Ingest deliberately applies\nNO trust deltas and NO auto link generation — it is deterministic state\nrestore, so re-ingesting the same file is an exact no-op (the +0.05\ncorroboration is an organic `add`\u002F`update`-only signal). The dedup path DOES\napply the write-time polarity guard: a remote row that contradicts its\ndedup hit inserts as its own live row instead of merging (contradictions\nnever merge, on any surface).\n\n`--capture-mode` routes every inserted row through the same capture policy as\n`add` (a sync file is remote-authored data that can otherwise plant a poisoned\nmemory surfacing verbatim into model context, or store secret-like text). The\ndefault resolves like `add` (`ZMEM_CAPTURE_MODE` env or `manual`): verbatim\ncontent with prompt-injection-risk tagging (a row matching an injection\npattern is tagged `prompt-injection-risk` so it can be reviewed, in ALL modes;\nissue #82 widened the pattern set with four high-precision\ninstruction-to-the-model shapes — role hijack (\"you are now the ...\"),\nconcealment (\"do not mention this\"), instruction-override paraphrases\n(\"ignore all previous rules\u002Fguidelines\"), and store-mutation imperatives\n(\"update your knowledge base \u002F the memory store\") — precision-first, so\nordinary coding lessons like \"update the lockfile\" never tag; the read path\nre-classifies EVERY row at emit time regardless of store age).\nUse `--capture-mode auto` when ingesting an untrusted\u002Fremote sync file: it\nadditionally redacts secret-like VALUES in content\u002Ftags (value-span\nreplacement, tagged `auto-redacted`) and QUARANTINES rows whose `source_ref`\nlooks like a secret (issue #180: counted as `quarantined` in the summary —\nafter `capture_refused` — with the original row appended to\n`\u003Cstore-dir>\u002Fquarantine\u002F\u003CUTC-date>.jsonl`, NOT stored; the run still exits 0).\n`reviewed` RAISES on those refusals (counted as `capture_refused`, never\nquarantined — plain `ingest-jsonl` still exits 0 with the count since the\nfile keeps going; exit 2 applies via `--strict` or the `add`\u002F`update` CLI\npaths); `manual` keeps the original text with an advisory notice.\nA quarantine write failure counts `quarantine_failed`, stores nothing for\nthat row, and exits 1.\n\nIssue #71 F: in `auto` mode, `source_ref`s with a structured provenance\nscheme — `db:`, `hindsight:`, `session:`, `zmem-queue:`, and `file:` with a\nRELATIVE path (or a well-known stem like `codex-MEMORY.md`) — skip the\ngeneric hash-shape refusal (a 32-hex db row id is legitimate provenance).\nCredential shapes (key=value pairs, PEM headers, `gh*_`\u002FAKIA tokens) still\nrefuse on allowlisted refs, `file:` absolute remainders still refuse, and\ncontent\u002Ftags scanning is unchanged. The write result carries a structured\n`source_ref_allowlisted` warning so the relaxation is visible.\n\n### Capture policy and quarantine (issue #180)\n\n`apply_capture_policy` (storelib.write) is THE single capture-policy entry\npoint. Every importer routes through it: CLI `add`\u002F`update` (via\n`add_memory`\u002F`update_memory`), `ingest-jsonl` and `ingest-jsonl --strict`,\n`import-store.py`, `rescan-secrets` (the store-wide rescan\u002Fremediation\nscan), `scripts\u002Fingest_harvest.py`, MCP `add`, Hermes `zmem_add`\n(the last three via the `store.py` subprocess boundary — no adapter\nre-implements pattern matching). The pattern registry lives in\n`redaction.py` as the only source; `redact_text` and the read-time\ncredential withhold (`recall`'s classifier) iterate the same lists.\n\nDetectors and span semantics — value-span patterns replace ONLY the secret\nvalue and keep the surrounding syntax; everything else whole-match-replaces:\n\n| shape | matches | does NOT match |\n|---|---|---|\n| `sshpass -p \u003Cvalue>` (any whitespace, incl. tabs) | value replaced | dangling `sshpass -p` |\n| `sudo -S` | whole-row refusal (`unredactable_secret`) | `sudo -s` (flag case matters) |\n| `--password=\u003Cvalue>` (attached) | value replaced | `--password \u003Cvalue>` spaced; bare `--password` |\n| `ssh`\u002F`mysql`\u002F`psql` `-p\u003Cvalue>` (attached) | value replaced | `-P\u003Ctoken>`; other commands' `-p`; spaced `-p \u003Cvalue>` |\n| `scheme:\u002F\u002Fuser:password@host` | password replaced | URLs without userinfo |\n| key=value (`password=`, `api_key=`, … compound names like `DB_PASSWORD=` included) | value replaced (8+ chars) | bare keywords with no `[:=]` |\n| PEM block (BEGIN…END) | whole block replaced | — |\n| token prefixes (`gh*_`, `sk-`, `AKIA`, JWT, …) | whole token replaced | — |\n\nKnown accepted over-redaction (issue-mandated matrix, fail-safe direction):\nfor `ssh` and `psql` the lowercase `-p` is actually the PORT flag, so\n`ssh -p2222 user@host` IS redacted as if the port were a secret; the matrix\npositives (`ssh -ppw180F`) require it. Do not \"fix\" this without changing the\nissue contract.\n\nCommand names and URL syntax match case-insensitively; option letters are\ncase-SENSITIVE (`-S` vs `-s`, `-p` vs `-P`). `[REDACTED_SECRET]` is\nidempotent: a value already equal to the marker is detected but never\nrewritten, so re-running auto mode on a redacted row is a fixed point for\ncontent, source_ref, and tags (warnings are identical for inputs whose\ndetection set is stable across a re-scan — every issue fixture row; a\nplaceholder value long enough to trip a second pattern re-counts, and an\noutput matching no pattern emits no redaction warning). Known safe-direction\ncaveat: a URL of the shape `scheme:\u002F\u002Fhost:port\u002Fa@b` over-redacts the\n`port\u002Fa` span, and an attached `ssh -p\u003Cport>` is treated as a password\n(both are the price of the issue's exact matrix) — and because the\nread-time credential classifier consumes the same shapes, such a stored\nrow is ALSO withheld from passive injection (issue #256's read-time\ndefense), not merely masked at write time.\n\nQuarantine contract: an auto-mode refusal with reason\n`source_ref_secret_like`, `source_ref_unsafe_path`, or\n`unredactable_secret` is quarantineable. The importer that caught it\nappends the ORIGINAL row to `\u003Cstore-data-dir>\u002Fquarantine\u002F\u003CUTC-date>.jsonl`\n(the directory of the resolved STORE — every importer writer resolves the\nsame sink, never a divergent `ZMEM_DATA`; directory 0o700, file 0o600) —\none compact JSON object per line, keys in order `quarantined_at`,\n`reason`, `source_ref`, `row` — via `quarantine_import_row` (write\nfailures raise `OSError`: the row is rolled back, counted\n`quarantine_failed`, and the run exits nonzero). The store's writer lease\ndoes NOT cover the quarantine file; writers serialize on an exclusive\nadvisory file lock (`fcntl.flock` on POSIX, `msvcrt.locking` on Windows)\nheld across each append — bare `O_APPEND` and the buffered text-mode\n`open(\"a\")` idiom both measurably dropped or tore records under concurrent\nWin32 writers (review-round probe). Review quarantine files with\nrestricted filesystem access.\n\nImporter results and counters:\n- `store.py add --capture-mode auto --json` prints\n  `{\"id\": null, \"result\": \"quarantined\", \"warnings\": [{\"type\": \"quarantined\", \"reason\": \"\u003Creason>\"}]}`\n  and exits 0 on a successful quarantine; reviewed-mode and namespace\n  refusals exit 2 with `[zmem] capture policy refused: \u003Creason>` (namespace\n  refusals keep their prose guidance; `manual` never raises on capture\n  grounds — namespace validation can refuse in any mode).\n- `ingest-jsonl` summary carries `quarantined=` and `quarantine_failed=`\n  after `capture_refused=`; `quarantine_failed>0` exits 1.\n  `ingest-jsonl --strict` treats a quarantined row as a rejection\n  (all-or-nothing stays fail-closed).\n- `import-store.py` stages a sanitized copy (source read-only\n  `mode=ro`+`query_only=1`; every memory row through the policy in id\n  order; refused rows deleted with their related rows — belief heads\n  sourced only by a refused row are deleted with it, shared heads rebuild\n  from a surviving source — and quarantined; redacted rows keep ids with\n  FTS maintained by trigger and stale `memory_vec` embeddings dropped),\n  verifies `PRAGMA integrity_check` and\n  `staged_count = source_count - quarantined_count`, flushes its refusal\n  records (one locked, all-or-nothing batch; records the ledger already\n  holds — compared without the per-call timestamp — are skipped, so\n  re-imports never duplicate them) BEFORE the destination is touched, and\n  only then atomically replaces the destination (`os.replace`); the\n  source's sha256\u002Fsize\u002Frow-count are proven unchanged before and after.\n  Its report carries `added`, `redacted`, `quarantined`,\n  `quarantine_failed`.\n- `scripts\u002Fingest_harvest.py` subprocesses\n  `store.py add --capture-mode auto --json` per row (a record may carry its\n  own `source_ref`; the CLI `--source-ref` is the fallback), classifies\n  each child result as `stored`\u002F`deduped`\u002F`quarantined`\u002F`failed`, and\n  reports all four counts; any `failed` exits 1.\n- MCP `add` and Hermes `zmem_add` forward the store envelope verbatim —\n  `result: \"quarantined\"`, `id: null`, one structured warning — and never\n  expose the local quarantine path, the source row, the credential value,\n  or any filesystem detail.\n\nOperator runbook (a stored credential is captured — the field trigger was\nrow `b513b21c`): invalidate the row with reason `credential leak`; rotate\nthe affected SSH password AND every credential that shared it; search\nbackups, exports, quarantine files, and logs for copies; verify the row is\ntombstoned and absent from live recall; never print the credential; review\nfuture whole-row quarantine records with restricted filesystem access.\nInvalidate tombstones rather than purges (durable purge is #255); read-time\ncredential withhold on the passive lane is #256. This runbook is\noperator-only and never targets a test store.\n\nEvery ingested row ALSO runs the deterministic entity extractor (the same\none `add` uses), so entity identity is rebuilt locally instead of carried\n(v10, issue #60): two stores ingesting the same rows derive the same\nentities, keyed by normalized alias, with no cross-store id collisions.\n\n### Evidence (schema v14, issues #169\u002F#170)\n\nEvidence is bounded observation data, not instructions for the model. The\nadditive `evidence`, `episode_evidence`, and `memory_evidence` tables use the\nclosed lane\u002Fmoment\u002Fkind sets and second-precision UTC timestamps.\n`evidence write` validates the payload before insert; it redacts first —\nexcept the flagged delegation mode (issue #164), whose validated digest-only\npayload skips redaction — caps the\nfinal excerpt at 400 Unicode characters, hashes `kind|ts|final_excerpt`, and\nleaves the transaction commit to its caller. The strict JSONL importer\nverifies a row's hash against its exported excerpt verbatim first, so\nredaction-exempt rows round-trip. The CLI writer commits its own\none-row transaction. When the zmem provider is active (issue #163), the plugin natively registers\n`post_tool_call`, `pre_llm_call`, and `pre_verify`. The registered\n`post_tool_call` sends a bounded private payload through the existing local\nstore bridge and fails open on malformed input, unavailable host support,\nqueue saturation, or writer failure; `pre_llm_call` and `pre_verify` complete\nthe #163 provider surface (transport recall with strict envelope validation,\nand the once-per-session reflect-before-stop nudge). Since issue #164,\n`on_delegation` records each subagent delegation as one bounded, fail-open\nevidence write (task\u002Fresult SHA-256 digests plus child\u002Fparent session ids).\nAll of these fail open.\n\n```text\n\u003Cinterpreter> \u003Cstore.py> evidence write \u003C payload.json\n\u003Cinterpreter> \u003Cstore.py> evidence write --kind delegation --session-id PARENT --lane hermes-provider --moment subagent --ref-path delegation:PARENT:CHILD \u003C payload.json  # issue #164; answers {\"id\": ..., \"ok\": true}\n\u003Cinterpreter> \u003Cstore.py> evidence list --namespace NS [--session-id SID] [--lane LANE] [--moment MOMENT] [--json]\n\u003Cinterpreter> \u003Cstore.py> evidence show --namespace NS --id UUID [--json]\n\u003Cinterpreter> \u003Cstore.py> evidence for MEMORY_ID --json\n\u003Cinterpreter> \u003Cstore.py> evidence for --memory-id MEMORY_ID --json  # legacy alias\n\u003Cinterpreter> \u003Cstore.py> evidence scoped-show --namespace NS --id UUID [--json]\n\u003Cinterpreter> \u003Cstore.py> evidence associations --id UUID --json  # hidden operator helper\n```\n\nThe required `--namespace` argument on legacy CLI `list`\u002F`show` is a\ncompatibility\u002Fcontext marker and is ignored: evidence rows have no namespace\ncolumn, so this unscoped list\u002Fshow path has no namespace filter or\nauthorization boundary. `evidence list` filters only by `--session-id`,\n`--lane`, and `--moment`; `evidence show` selects by `--id`. The hidden\nunscoped `evidence associations` helper is used by the operator MCP bridge to\nexpose association rows for an evidence ID. `evidence scoped-show` is the\nstore-owned scoped lookup:\nit requires both `--namespace` and `--id`, returns the row only when an\nassociation reaches that namespace, and emits `namespace_not_allowed` for a\nmissing, unassociated, or foreign ID.\n\nThe Hermes MCP `evidence_for(memory_id)` tool derives the memory namespace.\nFor a scoped token, a missing or foreign memory returns the same structured\n`namespace_not_allowed` response (`namespace: null`, with detail that the\nmemory is not associated with an allowed namespace); an allowed memory returns\nits hash-free evidence rows. The MCP `evidence_show(id, namespace)` tool uses\nthe scoped lookup for scoped tokens, so missing, unassociated, and foreign\nevidence IDs all return the same structured `namespace_not_allowed` response\nfor the requested namespace. Unscoped operators retain the exact missing-ID\nresponses (`memory id not found` and `evidence id not found`) and legacy\nassociation behavior.\n\nThe session-cadence maintenance transaction runs evidence retention after its\nnormal organize\u002Fbackup work. `ZMEM_EVIDENCE_DAYS` defaults to 30 and removes\nrows strictly older than the supplied cadence clock minus that many days;\n`ZMEM_EVIDENCE_CAP`\ndefaults to 50,000 and keeps the newest rows by stable `ts,id` order.\nAssociations are deleted with their evidence and stale associations are swept.\nInvalid retention settings disable the evidence sweep rather than selecting a\nsurprising limit.\n\n`add` and `update` accept `--evidence ID[,ID...]` with at most 256 evidence\nIDs per memory write. Capture-policy advisories are emitted before the\ntransaction opens; the writer then validates each supplied ID before memory\nmutation and attaches the ids to the resulting memory before commit; existing\npairs are ignored. Explicit JSON `recall`, `recent`, and `recall --explain`\nrows include sorted `evidence_ids`. Passive injection and human fenced output\ndo not gain that field. `evidence for MEMORY_ID --json` (or the legacy\n`evidence for --memory-id MEMORY_ID --json` form) returns the memory namespace\nwith its hash-free evidence rows; if both forms are supplied, conflicting IDs\nexit 2. Strict import prevalidates\nall `memory_evidence` endpoints and reports a missing endpoint using its\nphysical source line before association insertion.\n\n`export-jsonl` reads one consistent snapshot. An unscoped export includes all\nevidence rows and only associations whose parent rows are in that export. A\nnamespace-scoped export includes only evidence\nreachable through the memory\u002Fepisode rows selected for that namespace and\nomits unassociated evidence, since it cannot be attributed to a namespace.\nRecords with the new top-level `table` discriminator use one staged read,\nduplicate-key\u002Fhash\u002Freference validation, and one outer transaction. Use the\nexplicit all-or-nothing form with bounded staging for evidence transfers:\n\n```text\n\u003Cinterpreter> \u003Cstore.py> ingest-jsonl --in evidence.jsonl --strict\n```\n\nThe automatic discriminator path also chooses strict staging when it can parse\na table record. Legacy memory-only JSONL retains its best-effort per-row\nbehavior; a wholly malformed source cannot be classified automatically.\nStrict staging parses and applies the same captured bytes, never reopens a\nmutable source between validation and mutation.\n\n#### Deterministic passive-query rewrite (issue #183)\n\nThe 0.43 passive `user_prompt` path may append bounded local context before\ncalling the existing selector. A prompt is ambiguous only when it has fewer\nthan the positive configured minimum of case-folded content terms and no exact\nanchor. The default minimum is 4 and the positive override is\n`ZMEM_AMBIG_MIN_TERMS`. Slash\u002Fbackslash\u002Fdot\u002Funderscore, namespace, flag, `Error`, and\n`Exception` tokens are exact anchors and bypass the rewrite. Context is\ndeduplicated in source order from the first 12 safe operation tokens and the\nnewest three safe edit basenames; it is capped at 150 context characters and\nthe total query at 500 characters. Host consumers fail open silently on\nmissing\u002Finvalid sessions or evidence, read\u002Ftimeout failures, and malformed\noutput. The standalone query-rewrite CLI preserves the original query with\n`rewrite=0` and emits one sanitized warning when its store or context is\nunavailable. `ZMEM_QUERY_CONTEXT=0` is an exact kill switch at each host\u002Fstore\nboundary. Explicit search and non-`user_prompt` moments are unchanged, and\nthis bounded capability is not a claim to implement all of issue #163.\n\n#### Read-only replay evaluator (issue #155\u002F#183)\n\nReplay uses explicitly supplied offline inputs (the CI invocation uses the\ncommitted fixture) and is not a live efficacy test:\n\n```text\npython scripts\u002Feval_replay.py --store tests\u002Ffixtures\u002Freplay\u002Fstore.sqlite \\\n  --log tests\u002Ffixtures\u002Freplay\u002Fdecisions.log --days 30 \\\n  --compare-baseline eval\u002Fbaseline-replay.json --json-out replay-report.json\n```\n\nThe closed report is exactly two lanes (`claude`, `hermes-provider`) crossed\nwith `session_start`, `user_prompt`, `pretool`, and `precompact` (eight rows).\nRepeatable `--transcript PATH` inputs are explicit regular bounded JSONL files\n(at most 16 files, 4 MiB per file, 32 MiB total, and 100,000 physical lines per\nfile); there is no glob, rotation, failure-database, ring, or ambient\noperator-store discovery. The evaluator derives its clock from the latest valid decision-log\ntimestamp, clears ambient routing\u002Frecall `ZMEM_*` values before store imports,\nand verifies input digests before publication. If no eligible later same-session\nobservation exists, observation-dependent zero fields are empty-denominator\ncompatibility values and stderr says metrics are unavailable; they are not a\nmeasured success, failure, or efficacy claim. See\n`tests\u002Ffixtures\u002Freplay\u002FREADME.md` for bounds and reproducibility details.\n\nObservational action matching (issue #156) adds a report-only `--actions` mode\nto the same evaluator:\n\n```text\npython scripts\u002Feval_replay.py --store tests\u002Ffixtures\u002Freplay\u002Fstore.sqlite \\\n  --log tests\u002Ffixtures\u002Freplay\u002Fdecisions.log --days 30 \\\n  --actions --actions-input tests\u002Ffixtures\u002Freplay\u002Factions.json \\\n  --json-out actions-report.json\n```\n\n`--actions-input` is an explicit JSON file of recorded `delivered_rows` and\n`evidence_rows` (delivered rows carry `id`, `session_id`, `timestamp`, and `operation`;\nevidence rows carry `session_id`, `timestamp`, `event_kind`, and\n`operation`). For each delivered row the matcher selects the first\nlater same-session evidence event inside the fixed `ZMEM_MATCH_WINDOW_S =\n1800` second window whose `derive_ops_tokens` normalization shares at least\n`ZMEM_MATCH_MIN_OVERLAP = 2` tokens with the delivered trigger, then labels\nthe row `applied` (success), `violated` (failure), or `ignored` (no\nqualifying event). Both constants are fixed with no environment overrides and\nare recorded in the report next to the sorted `results`. The boundary is\nstrictly report-only: no action counter is written, no write transaction is\nopened, and the store SHA-256 is unchanged (durable counters belong to\nissue #124). The oracle pair `tests\u002Ffixtures\u002Freplay\u002Factions.json` \u002F\n`actions-expected.json` is written only by\n`tests\u002Ffixtures\u002Freplay\u002Fgenerate_actions.py`, which refuses to replace\ncommitted bytes when they drift; malformed rows or non-UTC timestamps exit 2\nbefore any output file is replaced.\n\nCounterfactual replay (issue #157) measures with\u002Fwithout-memory divergence:\n\n```text\npython scripts\u002Feval_counterfactual.py \\\n  --tasks tests\u002Ffixtures\u002Fcounterfactual\u002Ftasks.json \\\n  --store tests\u002Ffixtures\u002Fcounterfactual\u002Fstore.sqlite \\\n  --model-id recorded-stub-v1 \\\n  --json-out counterfactual-report.json\n```\n\n(`--allow-model-calls` exists to opt a non-stub model id into real adapter\nresolution; without it the run takes the skip path described below.)\n\nEach recorded session replays twice — `ZMEM_INJECT=1` through the real\npassive injection lane (delivery ledger redirected to a scratch directory;\nread-only store URI; store SHA-256 verified before and after) and\n`ZMEM_INJECT=0` with delivery disabled — through the pinned\n`recorded-stub-v1` model path, which returns the task's recorded successful\naction exactly when the delivered fence contains that task's `memory_row_id`\nand the recorded no-memory action otherwise. The report carries\n`repeated_failure_rate` and `first_action_agreement` per condition (0.0\u002F1.0\nwith memory, 1.0\u002F0.0 without on the committed fixture) against the Draft\n2020-12 contract `eval\u002Fcounterfactual-schema.json`. The evaluator refuses the\noperator home store before opening anything. A real model id is only resolved\nwith `--allow-model-calls`; without it the run prints exactly\n`SKIPPED: model calls disabled` and exits 0 with `skipped: true` (writing the\nskipped report when `--json-out` is given) — no adapter\nimport, no download, no network. The committed task\u002Fstore\u002Fexpected fixture\ntrio is written only by `tests\u002Ffixtures\u002Fcounterfactual\u002Fgenerate.py`\n(developer-side; CI compares against the committed bytes and never\nregenerates).\n\nThe predeclared private real-corpus measurement of record for issue #155 is\ncommitted at `eval\u002Freal-corpus-2026-09-19.json`: a cohort frozen at\ndeclaration time (standalone store snapshot, `ver=0.49.0` projection of the\nfrozen decision log, and the cohort's one Claude-shaped transcript), declared\nwith SHA-256 digests on the issue before the evaluation ran. The file carries\naggregates and digests only; `tests\u002Ftest_eval_real_corpus_record.py` pins its\ndigests, schema, and privacy boundary. Future real-corpus replays must freeze\nthe store snapshot and declare its SHA-256 before the measurement — replays\nover live, undeclared, or outcome-selected inputs are not baselines.\n\n### entity-list — inspect entity identity (v10, issue #60)\n```\n\u003Cinterpreter> \u003Cstore.py> entity-list [--kind person|project|tool|preference|host|service|model|repo|file|command|error_class|other] [--json]\n```\nLists the entities the deterministic extractor minted: id, kind, canonical\nname, all normalized aliases, and how many memories link to each. `--kind`\nfilters to one kind; `--json` emits `[{id, kind, name, aliases, links}]`.\nEntity identities are GLOBAL: this listing (and every other entity surface)\nspans ALL namespaces — the entity tables carry no namespace column, and\n`entity_alias.alias_norm` is globally unique — so an alias resolves to\nexactly one entity no matter which project mentioned it (review PRR-011,\nPR #454: uniqueness is per ALIAS; canonical names are not unique-keyed —\n`resolve_entity` refuses with exit 2 when a name matches several entities).\nThis is the inspection\nsurface for humans and doctor — use it to see what the entity lane actually\nmatches. Issue #455 display masking: credential-shaped canonical names and\naliases render as the whole-value marker `[REDACTED_SECRET]` (full contract\nunder the classifier decision record below).\n\n### entity-merge — reconcile duplicate entities (v10, issue #60)\n```\n\u003Cinterpreter> \u003Cstore.py> entity-merge --from \u003Centity-id> --to \u003Centity-id> [--confirm]\n```\nMerges two entities: the `--from` entity's aliases and memory links move to\n`--to`, then `--from` is deleted. DRY RUN BY DEFAULT — without `--confirm`\nnothing is written and the plan (moves, alias\u002Flink collisions with the\ntarget, the deletion) is printed. With `--confirm` the write happens in one\ntransaction; a target that already has an alias or link keeps its own\n(collisions are counted, never overwritten).\n\nRefusals (exit 2, nothing written): unknown id on either side, `--from` ==\n`--to`, or a kind mismatch — merging a `person` into a `tool` would silently\nre-classify history. No write path auto-merges entities as a side effect\n(the issue's \"ZMem will not auto-merge people\" rule): merges happen only\nthrough the two EXPLICIT operator commands — this one (person→person merges\nare allowed here, manual and `--confirm`-gated) and\n`reconcile-entity-aliases --apply` (whose dry-run previews every merge; a\nlegacy `other:person:Name` row can merge into a person entity through it).\n\nTypical flow: `entity:Name` tags or backticked mentions created two\nentities for the same thing (e.g. `rg` and `ripgrep`); find both ids with\n`entity-list`, dry-run the merge, then apply with `--confirm`. Afterwards\nthe alias `rg` resolves to the surviving entity and recall's third list\nreturns all its linked memories.\n\nIssue #455 display masking: the dry-run plan masks credential-shaped\nfrom\u002Fto canonical names and the alias-collision join (full contract\nunder the classifier decision record below).\n\n### entity-card — build or refresh one entity's card (issue #179)\n```\n\u003Cinterpreter> \u003Cstore.py> entity-card build ENTITY [--json]\n\u003Cinterpreter> \u003Cstore.py> entity-card refresh ENTITY [--json]\n```\nDirect card operations on one entity (`ENTITY` = entity id, or an exact\ncanonical name; zero\u002Fmultiple name matches exit 2). `build` creates a\nmissing qualifying card and REFUSES a live card (exit 2, no write);\n`refresh` returns `created`\u002F`updated`\u002F`unchanged`\u002F`no_card` and tombstones\na live card whose entity fell below `ZMEM_CARD_MIN_MENTIONS`. `--json`\nemits one object with the exact key order `result, id, entity_id,\nsource_ref, warnings`. Full contract: \"Entity cards (issue #179)\" above.\n\n### reclassify-entities — rekind stored `other` entities (issue #176)\n```\n\u003Cinterpreter> \u003Cstore.py> reclassify-entities --dry-run|--apply [--json]\n```\nRuns the DETERMINISTIC classifier (pure string rules — no LLM, no shell, no\nnetwork, no DNS, no filesystem; precedence host → service → model → repo →\nfile → command → error_class → other over the case-folded name) over every\nentity row currently kinded `other` and proposes or applies new kinds.\nExactly one of `--dry-run`\u002F`--apply` is required (both or neither exits 2).\nNon-`other` rows are never touched; entity ids, aliases, and memory links\nare preserved; `--apply` writes in ONE transaction under the writer lease\nand rolls back completely on error. Like every entity surface, the scan\nspans ALL namespaces (there is no namespace filter — entity identities are\nglobal). `--apply` also refuses an unmigrated store up front (a schema\nstamp below the supported version exits 2 with the migration hint instead\nof a mid-apply sqlite error). `--dry-run` runs before the lease on a\nread-only connection, changes zero database bytes (no `-wal`\u002F`-shm`\nsidecars), and prints the same canonical JSON as apply: `{\"scanned\",\n\"changed\", \"before_other_link_share\", \"after_other_link_share\",\n\"changes\":[{id,name,old_kind,new_kind}]}` with changes ordered by entity id;\nfor `--apply` the scanned\u002Fbefore numbers describe the locked pre-update\nsnapshot inside the write transaction.\n\nIssue #455 display masking: change-row `name` fields render as the\nwhole-value marker `[REDACTED_SECRET]` when credential-shaped (full\ncontract under the classifier decision record below).\n\n**Writer lease and concurrent applies.** The writer lease is advisory — it\nmarks a live writer so `restore` observes it; it is NOT mutual exclusion. A\nsecond apply run after the first commits harmlessly reports `changed=0`\n(idempotent re-run); an apply whose targets were re-kinded by a writer\nbetween its scan and its transaction fails closed with a ValueError and zero\nchanges.\n\n**Backup before `--apply`.** `--apply` re-kinds rows in one transaction but\ntakes no snapshot of its own (unlike `rekey-namespace`). Take a verified\nbackup first — `\u003Cinterpreter> \u003Cstore.py> backup` — and keep it until the new\nkinds are confirmed; undo is `restore` from that snapshot (rows re-kinded by\nmistake come back with their old kinds; there is no per-row undo).\n\nAt write time the same classifier assigns kinds to unqualified extracted\nvalues (plain tag tokens, untyped `entity:Name`, CamelCase identifiers);\nexplicit typed tags, the `project:` namespace rule, and backtick→`tool`\nstay authoritative.\n\n**Per-kind entity-lane weights.** Recall multiplies each memory's entity-lane\nRRF contribution by the max weight across the kinds of the entities the\nQUERY matched (`ZMEM_ENTITY_KIND_WEIGHTS`, one JSON object such as\n`{\"other\":0.6,\"host\":1.0}`; defaults 1.0 for every kind except\n`other=0.6`; an invalid value logs one warning and uses the complete\ndefaults — recall never crashes on the knob). BM25, vector, and graph\ncontributions are unchanged; `recall --explain` reports the effective\nweights under `entity_kind_weights`.\n\n**Operator snapshot block.** `tests\u002Ffixtures\u002Fentity\u002Foperator-snapshot.json`\nrecords the motivating measurement and the fixture apply's fixed outcome:\n\n```json\n{\"as_of\": \"2026-09-10\", \"memory_entity_links\": 26511, \"other_links\": 24826,\n \"fixture\": {\"memory_entity_rows\": 3, \"before_other_link_share\": 1.0,\n             \"after_other_link_share\": 0.0, \"changed\": 4}}\n```\n\nThe 24,826-of-26,511 `other` share (0.9364…, exactly 24826\u002F26511) is why\n`other` defaults below\n1.0; the `fixture` block is test-pinned\n(`tests\u002Ftest_entity_fixture_provenance.py` cross-checks it against\n`reclassify-expected.json`) — the committed proof\nthe reclassification moves the fixture store's other-link share from 1.0 to\n0.0 in one apply. Regenerate the three fixed fixtures with\n`python tests\u002Ffixtures\u002Fentity\u002Fmake_reclassify_fixture.py` (deterministic). The\nreviewed `reclassify-expected.json` regenerates OUTSIDE any test in two steps:\nbuild the seeded fixture store with\n`python tests\u002Ffixtures\u002Fentity\u002Fmake_reclassify_fixture.py --emit-store \u003Cdir>`,\nthen run the exact dry-run against it —\n`ZMEM_STORE=\u003Cdir>\u002Fstore.sqlite \u003Cinterpreter> \u003Cstore.py> reclassify-entities\n--dry-run --json > tests\u002Ffixtures\u002Fentity\u002Freclassify-expected.json` — the same\nseeding `ReclassifyTest` uses (one source, `emit_store`).\n\n### entity-kind classifier decision record (issue #366)\n\nOwner-confirmed 2026-11-11 (verbatim directive: \"confirmed on 366\",\ntranscribed with provenance in the issue comment; proposed record\nadjudicated by an independent plan critic). The five precision rows observed\nin the #365 review are all **kept spec-literal** — each output matches its\ndocumented rule in `classify_entity_kind`'s docstring, and no guard is added\nahead of host\u002Fservice\u002Fcommand (guards would complicate the precedence chain\nissue #176 owns):\n\n1. Hyphenated one-dot basenames (`my-script.py`, `build-helper.sh`,\n   `claude-code.md`, `app-config.json`) stay `host` — the one-dot host rule\n   runs before file, and persisted kinds are sticky under\n   first-seen-kind-wins.\n2. The slash\u002Fdoc-suffix asymmetry stands: `docs\u002Freadme.md` → `other` while\n   `README.md` → `file` (the doc-suffix rule is gated on `\u002F` not in the\n   value).\n3. Bare runner-head words (`make`, `go`, `node`, `python`) stay `command`\n   (first whitespace token in RUNNER_HEADS).\n4. Service nouns matching filenames stay `service`: `server.log` and\n   `server.py` are `service` (whole-word rule); `access.log` remains `file`.\n5. CamelCase service identifiers (`UserService`, `DatabaseServer`) stay\n   `other` — the whole-word rule runs on the casefolded token; CamelCase\n   splitting is an issue #176 spec change and stays a non-goal.\n\nThe IPv4 fixture sub-decision: classify.json keeps its frozen 30 rows — the\nASCII-strict behavior (Arabic-Indic\u002Ffullwidth quads classify `other`) stays\npinned by the direct test `test_ipv4_octets_are_ascii_strict`, and no\nfixture row is added. The hardening items that motivated #366 landed via\nPR #365 (squash `a3e75178`, which carries the review-round fixes): the\nlinear host one-dot rewrite and the ASCII-strict IPv4 octet class.\n\nCompanion record from the PR #454 feedback ledger (F-30 \u002F PRR-013): recall\n`--json`\u002F`--explain` carry no LF-only byte contract (PRR-013: an\nLF-normalized golden exists only for the `--for-injection` arm and is\nunaffected) — recorded here so the decision line lives with the other dated\nrecords.\n\n**Display masking (issue #455).** Each entity report surface masks\ncredential-shaped display values (its own names\u002Faliases; the merge\ndry-run also the alias-collision join; reclassify the change-row names;\nreconcile the orphan name\u002Fsuffix\u002Fowner_name and merged-row names) with\nthe whole-value marker `[REDACTED_SECRET]` at render time — display only;\nstored rows, the classifier, and entity-card construction are untouched, and\nlegal `label:port` names (`localhost:8080`) render unmasked. Detection uses\nNFKC-normalized exact secret-head tokens over whitespace-trimmed\n`[:_=\"'-]`-delimited segments (colon, underscore, hyphen, equals, quote —\nso attribute-assignment shapes like `DB_PASSWORD=12345` and\n`DB_PASSWORD = 12345` mask, as do quoted-key shapes like\n`{\"password\": \"value\"}`; bare whitespace without punctuation does not\nseparate; fullwidth `：`\u002F`＝` fold to ASCII and EVERY Unicode format\ncharacter (category Cf — ZWJ, soft hyphen, word joiner, BOM) is\nstripped, so glued forms like `pass\\u200dword:12345` and\n`pass\\u00adword:12345` mask) and, for EVERY assignment-separator\noccurrence, over the\nDOT-segments of its key prefix (`db.password=12345`,\n`spring.datasource.password:5432`, earlier-separator forms\n`redis:db.password=12345` \u002F `--config=db.password=12345`, the dot-joined\nhead spelling `api.key=12345`, whitespace around\u002Finside key material\n(`db. password=…`, `db password=12345`, `--config password=12345`), and\ntrailing separators with an empty value (`db.password=`); never a global\ndot split — `api.example.com` and `acme.corp:8080` stay unmasked;\n`-`\u002F`_` are hard joins, so `db.password-prod=1` stays unmasked like\n`tokenized-service`; bare whitespace WITHOUT any assignment separator\n(`password 12345`) is not a key\u002Fvalue boundary and stays unmasked) plus\nthe shared redaction registry. The local\nhead list is REGISTRY-PARITY: every key-head family of\nredaction.py's credential pattern — `password`, `passwd`, `pwd`,\n`secret`, `token`, `api-key`\u002F`api_key`\u002F`apikey`,\n`private-key`\u002F`private_key`, `credential`, `auth` — is an exact local\ntoken, so short values below the registry's 8-character value floor\n(`pwd:12345`, `private_key:9`) mask locally too. Still unmasked by the\nexact-token boundary: prefix-shaped words (`tokenized-service`,\n`author`, `secretariat`) and glued NON-registry spellings\n(`secretkey=…`, `passphrase=…`, `pgpassword=…`). Masking is fail-closed:\nthe reports never fall back to raw output — a registry that becomes\nunavailable after startup (or whose call fails at render time) makes the\nrenderer refuse (dry-run exits 2 with empty stdout; apply reports from\nthe committed state), and a registry missing at process start fails the\nCLI at import before any report runs. The masking predicate is LINEAR in\nthe input — pinned by DisplayMaskPerfTest's scaling-ratio assertion\n(the pre-feedback form was quadratic in the separator count).\n\n### reconcile-entity-aliases — merge pre-widening orphan identities (issue #358)\n```\n\u003Cinterpreter> \u003Cstore.py> reconcile-entity-aliases --dry-run|--apply [--json]\n```\nA vocabulary widening silently re-keys stored aliases on the next relink: a store\nwritten before v16 stored a tag like `host:acme-prod` as one `other`-kind\nentity whose alias is the WHOLE token; after the widening, re-deriving that\nmemory mints `{host, \"acme-prod\"}` and leaves the old `other` entity with\nzero links (the 0.101.0 CHANGELOG discloses the behavior — only the entity\nlane's grouping changes; content and FTS recall are unaffected). This\ncommand reports — and, behind `--apply`, merges — those orphans back onto\nthe entity that now owns the suffix alias, so BOTH alias forms resolve to\none identity. Issue #455 display masking: orphan `name`\u002F`suffix`\u002F`owner_name` and\nmerged-row `name` render as the whole-value marker `[REDACTED_SECRET]`\nwhen credential-shaped (full contract under the classifier decision\nrecord above). (Dry-run's `mergeable` counts every orphan that HAS an owner;\napply additionally refuses cyclic chains, so `merged` can be smaller than\n`mergeable` by exactly the `skipped_cycle` count.) Run it AFTER upgrading past a widening and BEFORE the next\n`purge` (a purge reaps linkless entities and would delete the old alias form\nwith the orphan).\n\n`--dry-run` is strictly read-only (zero database bytes, no writer lease) and\nreports every orphaned `\u003Ckind>:\u003Cname>` other-entity with its prefix, suffix,\nand owner (null when no entity owns the suffix yet — EXCEPT two cases:\na no-owner orphan that another orphan's chain terminates ON stays alive\nand IS that chain's merge target; and an orphan that OWNS ITS OWN SUFFIX\nis reported with owner null and left untouched — self-merge is not a\nreconciliation). The\n`orphans` array means pre-apply candidates in dry-run and the remaining\nunmerged rows after apply. `--apply` takes the writer lease, refuses an\nunmigrated store up front, and performs every merge in ONE transaction:\nterminal-owner resolution first (a chain like `host:service:foo` →\n`service:foo` → `foo` merges into the final live owner; any chain that\nreaches a cycle is refused WHOLE — the cycle's members and any tail feeding\ninto it — reported as `skipped_cycle`\u002F`cycle_ids`), then per orphan: aliases\nmove first-delete-then-reinsert onto the owner, links move with\n`UNIQUE(memory_id, entity_id)` dedup, and the orphan row is deleted. A merged\norphan's live entity_card is tombstoned in the same transaction (a live card\nnaming a deleted entity would render without its recall suppression; the\nowner's card rebuilds from the owner's links via the cadence sweep). The old\nfull-token alias survives on the owner. Non-`other` rows are never touched,\nand the same ALL-namespaces scope as every entity surface applies.\n\n### links — inspect (or curate) a memory's associative links (v11, issue #61)\n```\n\u003Cinterpreter> \u003Cstore.py> links --id \u003Cuuid> [--json]           # list every edge\n\u003Cinterpreter> \u003Cstore.py> links --add --id \u003Ca> --id \u003Cb> \\\n    --relation \u003Crelated|supports|contradicts|updates|extends|derives> \\\n    [--score S] [--reason \"...\"]\n```\nList mode prints every `memory_link` edge touching the memory, both\ndirections (`out` = the memory is the source; JSON rows carry\n`{src, dst, direction, other, relation, score, created_at}`). A missing id\nexits 1 with the same stable stderr line as `get`.\n\n`--add` is the operator-curated insertion path for the typed relations\n(and for the trust-carrying `supports`\u002F`contradicts`). Symmetric\nrelations (`related`\u002F`supports`\u002F`contradicts`) insert BOTH directions;\ntyped relations (`updates`\u002F`extends`\u002F`derives`) keep their one authored\ndirection. Self-links and cross-namespace pairs are refused; re-adding an\nexisting edge is an exact no-op (idempotent, no second trust delta).\n`--relation contradicts|supports` adjusts `trust_score`, so those inserts\nREQUIRE `--reason` — the same deliberate-use guard `contradict` enforces\n(validated and echoed, not persisted).\n\nLinks are generated automatically on every `add`\u002F`update` (see\n`ZMEM_LINK_THRESHOLD` below) — deterministic, no LLM (no `ZMEM_LINK_LLM`\nknob exists; link generation never calls a model).\n\n### contradict — record a contradiction (v11, issue #61)\n```\n\u003Cinterpreter> \u003Cstore.py> contradict --id \u003Ca> --id \u003Cb> --reason \"\u003Cwhy they conflict>\"\n```\nInserts a `contradicts` pair (both directions) and applies the trust event\nto BOTH rows: `trust_score` −0.10 each, clamped to [0.0, 1.0] (ten\ncontradictions land a row at exactly 0.0, never below). Neither row is\nmerged, deleted, or rewritten — content, confidence, and signal are\nuntouched. `--reason` is REQUIRED (the `invalidate` deliberateness\nconvention); the v11 schema has no reason column, so the reason is\nvalidated and echoed but deliberately not persisted. Re-running the same\ncontradict is an exact no-op. A polarity-disagreeing `add` that lands in\nthe dedup window does this automatically (see `add`).\n\n**trust_score semantics** (v11): every memory starts at 1.0. A\n`contradicts` event lowers BOTH rows by 0.10; a `supports` link or a\npolarity-agreeing duplicate re-add (corroborating add, CLI `add`\u002F`update`\nonly — sync ingest never re-applies deltas) raises the keeper by 0.05.\nAlways clamped to [0.0, 1.0]; visible in `get --json`, `export-jsonl`,\nand `doctor`. `confidence`\u002F`signal` are never changed by linking — they\nare provenance inputs; trust_score is the contradiction ledger.\n\n## Operation feedback counters (issue #124)\n\n`applied_count`\u002F`violated_count` have ONE writer\n(`storelib\u002Fwrite.py::feedback_memory`) with TWO callers: the explicit\n`store.py feedback --id --applied|--violated` CLI and the observational\n`store.py operation-feedback` command that the capture-failure and\nposttoolbatch hooks invoke after each operation outcome. The command matches\nthe event against the session's delivered rows (1,800 s window, >= 2 token\noverlap), requires the event's evidence id to be associated with the memory,\nand records every verdict in a per-session sidecar\n(`\u003Cdata>\u002Fops\u002F\u003Csha256(session)[:32]>.feedback.jsonl`, one compact sorted-key\nJSON line with exactly `event_id`, `evidence_id`, `memory_id`, `overlap`,\n`session_id`, `timestamp`, `verdict` — verdict is applied, violated, or\nunmatched). Ranking popularity is usefulness only:\n`0.15*sqrt(applied_count) - 0.25*sqrt(violated_count)` clamped to\n[0.0, 1.0]; retrieval\u002Fsurfaced telemetry never feed ranking. `stats`,\ndoctor's voyager-counters check, and the miss-rate report expose seven keys\nin this order: `total_applied`, `total_violated`, `nonzero_applied`,\n`nonzero_violated`, `matched_applied`, `matched_violated`,\n`unmatched_operations`; the miss-rate JSON adds `feedback_associations`\n(session_id, event_id, memory_id, overlap, evidence_id per row).\n\n## Live host canary lanes (issue #96)\n\n`scripts\u002Fhost_canary.py` also owns nine LIVE host-canary lanes. Each lane\nruns against an isolated store, writes a schema-valid artifact\n(`canary\u002F\u003Clane>.json`, contract in `scripts\u002Fcanary-schema.json`), and always\nexits 0 for a completed run — the verdict lives in the artifact, never in\nthe exit code. A negative observation is a **structured fail** with the\nmeasured values and the exact command outcome in `notes`; it is never a\ncrash and never a faked pass. Lanes: `hermes-gateway`,\n`hermes-provider-mode`, `hermes-compat-mode`, `claude-compact`,\n`codex-trust`, `zcode-duplicate`, `exec-form-claude`, `exec-form-codex`,\n`exec-form-zcode`. Validate any artifact with\n`python scripts\u002Fhost_canary.py --validate-result canary\u002F\u003Clane>.json`.\nMeasured SHA\u002Fversion values are recorded only when the host's documented\nsurface emits them (`version-unavailable` otherwise); hook identifiers are\nderived from the host's real manifest (`PreCompact:zmem-launch.js` shape),\nnever invented; four before\u002Fafter inventory maps prove the run only touched\nthe isolated canary store. Canonical fence bytes come from the store's own\n`rendered` envelope (#158) — Hermes wraps them in `\u003Cmemory-context>`, so no\nzmem fixture ever contains that wrapper.\n\n## Timeout budget\n\nIssue #121: the hook path runs inside the host's hook timeout (the host\nconfigs give SessionStart 15 s) with an internal budget. Canonical shared\ninteger values live in `hooks\u002Ftimeout-budget.json`; the private\ntraining-capture allocations below are implemented in `hooks\u002Fzmem-launch.js`.\nRuntime overrides read env vars and fall back to those defaults (an invalid\nvalue falls back with exactly one warning).\n\n| Stage | Value | Override env var | Notes |\n|---|---|---|---|\n| Launcher watchdog | 12000 ms | `ZMEM_LAUNCHER_WATCHDOG_MS` | Kills the child tree at the deadline, emits the retained Tier 0 sentinel, logs `outer_timeout=1 reason=omitted`, exits 0. |\n| Automatic capture start | 5000 ms | - | Synchronous only when the store-issued capture id is needed; observe\u002Fsnapshot helpers are detached and fail open. The translated launcher clamps this private allocation to the remaining 12,000 ms watchdog deadline minus a 500 ms output reserve, and never asserts acknowledgement or outcome. |\n| Hermes capture subprocess | 5000 ms | - | `sync_turn` and session-end capture use their own per-operation cap and may add up to 5 seconds to that callback or maintenance path. The launcher’s full 5-second start allocation can leave roughly 6.5–7 seconds inside the 12,000 ms watchdog, below the 8-second recall budget, so watchdog preemption is an expected degradation. No whole-callback wall-clock guarantee is made; capture remains fail open. |\n| Namespace resolution | 2000 ms | `ZMEM_NAMESPACE_RESOLVE_MS` | Per interpreter attempt; successful non-empty REMOTE namespaces are cached per process. |\n| Namespace cache TTL | 60000 ms | `ZMEM_NAMESPACE_CACHE_TTL_MS` | Entry expires at exactly TTL; path-key resolutions are never cached. |\n| Store recall | 8000 ms (8.0 s) | `ZMEM_STORE_RECALL_TIMEOUT_S` | SessionStart + the shared recall body. Finite positive float; values above 8.0 clamp to 8.0 (one warning); values below 8.0 are honored. ONE store attempt at SessionStart (no retry loop). |\n| SQLite busy timeout | 5000 ms | - | `storelib\u002Fschema.py` (`PRAGMA busy_timeout=5000`); unchanged. |\n| Hermes manager join | 8000 ms | - | The Hermes MemoryManager joins external-provider prefetch at 8.0 s (external runtime, not vendored in this repo). |\n| Hermes provider deadline | 6000 ms | `ZMEM_HERMES_DEADLINE_S` | Issue #160: every provider prefetch runs under this deadline (default 6.0; invalid or `>= 8.0` values resolve to 6.0 with exactly one warning). |\n\nSessionStart emits a complete Tier 0 sentinel BEFORE the first store\nsubprocess (the fast path), so a store stall inside the watchdog window\nstill delivers Tier 0 to the host. The launcher watchdog arms before\nstartup, so namespace resolution counts against the 12,000 ms budget. The\n`scripts\u002Fbench_hook_latency.py` benchmark reports deterministic\ninjected-clock p50\u002Fp95 schedules for the seven stages (launcher,\nnamespace, store, embed, fuse, render, time-last-capture) — a\ncontract-regression gate whose `--compare-baseline` pins stage keys,\nper-stage values, and a machine-independent `input_digest`; it does not\nmeasure wall-clock latency.\n\n## Store hygiene (read-only, issue #97)\n\n`store.py hygiene` audits a SNAPSHOT of the store — it never opens or writes\nthe live store, and it never mutates a memory, vector row, namespace, or\nsignal. Hermes-origin MUTATIONS remain owned by issue #168's explicit\noperator map; this command is strictly report-only.\n\nOperator workflow:\n\n1. **Snapshot copy first.** The live store runs in WAL journal mode, so a\n   bare `cp` of `store.sqlite` can miss commits still sitting in the `-wal`\n   file (or open malformed mid-checkpoint). Use a crash-consistent snapshot\n   instead — either the built-in verified backup or SQLite's own backup API:\n\n   ```bash\n   # Option A (preferred): the store's verified backup writes a\n   # crash-consistent store-\u003Cstamp>.sqlite snapshot.\n   \u003Cinterpreter> skills\u002Fmemory\u002Fscripts\u002Fstore.py backup\n   # Option B: SQLite's online backup API.\n   sqlite3 ~\u002F.zmem\u002Fstore.sqlite \".backup '\u002Ftmp\u002Fsnapshot.sqlite'\"\n   ```\n\n   Then run the report against that snapshot (the command opens it with\n   SQLite `mode=ro`, so even an accident cannot write):\n\n   ```bash\n   \u003Cinterpreter> skills\u002Fmemory\u002Fscripts\u002Fstore.py hygiene \\\n     --store \u002Ftmp\u002Fsnapshot.sqlite \\\n     --origin-map origin-map.json --evidence-map evidence-map.json \\\n     --out report.json --format json\n   ```\n\n2. **Review the report.** It contains the snapshot's SHA-256, row totals and\n   live counts, sorted namespaces\u002Fsignals, the reviewed Hermes-origin ids,\n   the six known junk namespaces (`ns1`, `ns2`, `project:`, `test`,\n   `unfoldtest`, `user:t`), duplicate logical keys (live rows sharing a\n   `content_norm`), and a sorted `signal=none` upgrade action plan.\n\n3. **Verified backup** (`store.py backup`) before any remediation, then a\n   **dry-run rekey** (`rekey-namespace --dry-run`) if namespaces must move.\n\n4. **Mutation boundary.** The action plan emits `store.py update ...`\n   commands as REVIEW ARTIFACTS — verbatim and unquoted by contract, so read\n   them before running anything; it executes nothing. Every proposed upgrade\n   requires a live later grounded row, a live `supports`\u002F`updates`\u002F\n   `extends`\u002F`derives` link, a non-empty proof source reference, and a\n   justification. A rerun omits a target whose current row is superseded.\n\nInvalid input (unreadable files, malformed JSON, duplicate or unknown mapped\nids, SQLite errors) exits 2 with `[zmem] hygiene: invalid input` and creates\nno output file.\n\nNamespace resolution (host.py `resolve_namespace`) now distinguishes git\n`remote` \u002F `absent` \u002F `error`: a successful remote key is cached to disk\n(`\u003Cdata-dir>\u002Fnamespace-cache\u002F`, TTL 3600 s), an absent checkout keeps its\npath key, and a git ERROR inside a checkout resolves to the cached remote\nkey or `user:global` — never a path-shaped key. Cache failures fail open\nsilently. (This is the host-layer disk cache, distinct from the hook's\nper-process namespace cache in the timeout budget above.)\n\n## Hard rules\n- **Never put secrets\u002Fcredentials\u002FPII in the store.** It is a local plaintext sqlite\n  file. The write-time filter is advisory only (regex heuristic), not a guarantee.\n- **`doctor.py` is read-only.** It should inspect, never repair. Do not let it\n  create the store, rewrite config, or \"fix\" hook trust for the operator.\n- **Legacy namespace cutover:** before first v5 use, set\n  `ZMEM_NS_MIGRATION_MAP` to a JSON object mapping each old namespace to a live\n  checkout path, for example `{\"project:oldname\":\"C:\u002Fsrc\u002Fowner\u002Frepo\"}`. The\n  migration re-derives canonical remote-based keys from those checkouts; it\n  retries the map on later opens, but an omitted entry remains under its old key\n  until the map is supplied.\n- **Signal honesty:** `signal=none` means no external grounding — the lesson is the\n  agent's self-opinion. Never set `signal=test` unless a test actually ran.\n- **Wrap, do not replace:** this skill never writes to `tasks\u002F\u003Cslug>\u002F*.md` or\n  `issue-traces\u002F\u003Cissue>\u002F*.md`. Those are durable-session-state's source of truth.\n- **High-precision-first:** if a recalled memory does not clearly apply, ignore it.\n  Retrieved-wrong memory hurts more than retrieved-nothing.\n- **Source refs:** prefer immutable sources (`db:`, `session:`). For mutable markdown\n  (`file:`), a content hash is stored and checked on recall — if the source changed\n  since extraction, the memory's confidence is halved and flagged `[STALE SOURCE]`.\n  `file:` paths may be Windows (`C:\\Users\\...`) or Cygwin (`\u002Fc\u002FUsers\u002F...`) — both are\n  auto-normalized. If a `file:` ref cannot be opened, a stderr warning is emitted.\n\n## How recall works\nQuery hygiene (#112): the lexical query is normalized before it becomes the\nFTS5 MATCH — tokens are casefolded, deduped, stripped of edge punctuation,\nclosed-class English stop words are dropped, tokens under 3 characters match\nexactly instead of prefix-wildcard, the term list is capped at 24 (the last\nterms — the ops-token tail — always survive), and the MATCH is column-filtered\nto `{content tags}` so namespace text alone can never make a row a candidate.\n`recall --explain` records the resulting shape in its `query_shape` field.\n\nRetrieval is a **four-signal pipeline**: FTS5\u002FBM25 keyword match (always),\nvector KNN over stored embeddings (when the optional embedding runtime is\navailable), entity matching (v10 — always, no model needed; the query's\nidentifiers and plain tokens are matched against stored entity aliases),\nand the graph-seed arm (issue #136 — always, no model needed): the matched\nentities seed one bounded hop over seed-safe links (`related`, `supports`,\n`updates`, `derives`) so a linked neighbor can enter the pool without\nsharing any query token. The\nlanes' rankings are fused with Reciprocal Rank Fusion (RRF, k=60,\nper-id additive: a memory appearing in several lanes accumulates each lane's\ncontribution), then re-ranked by a **composite score** that combines:\n\n- **Relevance** (55%) — issue #113 lane composition: the max of the lexical\n  lane (term coverage x pool-normalized rank ratio; >= 2 matched terms),\n  embedding cosine, and entity-match fraction (see the per-lane subsection\n  below)\n- **Confidence** (20%) — grounded by signal tier (test\u002Fcompile > reviewer\u002Fuser > none)\n- **Recency** (15%) — exponential decay with a 90-day half-life\n- **Popularity** (10%) — retrieval frequency with diminishing returns (sqrt dampening)\n\nCandidates are namespace-filtered (the tier's expanded alias set) and subject\nto the confidence floor (below 0.25 is dropped before scoring). Staleness\ndemotion halves confidence, which feeds into the confidence component.\n\n**Per-lane relevance composition and inject floors (issue #113)**: a\ncandidate's relevance term is the MAX of its measured lanes — lexical\n(matched-terms\u002Ftotal-terms x row-|bm25|\u002Fpool-best-|bm25|), vec cosine, or the\nentity fraction — replacing the saturated `|bm25|\u002F(1+|bm25|)` back-solve, so a\nlexically-matched row keeps the cosine it earned and the score carries rank\ninformation from every lane. The lexical leg counts only when the row matches\n>= 2 distinct query terms or covers all of them — one shared generic token is\nnot relevance (the measured \"git status\" failure shape); an ineligible-but-\nranked row carries a measured 0.0, a lane with no signal carries `null`\n(absent). The inject gate consumes these values with DISJUNCTIVE floors: a\ntrusted row is admitted when ANY measured lane clears its own floor — lex\n0.30 \u002F cos 0.50 \u002F entity 0.50 (env `ZMEM_INJECT_FLOOR_LEX` \u002F\n`ZMEM_INJECT_FLOOR_COS` \u002F `ZMEM_INJECT_FLOOR_ENT`, calibrated starting\nvalues); a measured-but-failing lane does not veto a row another lane\nadmits, and ABSENT lanes are exempt (link-expansion rows carry none). A\nsilent inject whose rows passed the trust gate but failed these floors names\nthe new closed-set reason `below-relevance` (\"nothing relevant\") instead of\n`below-bar` (\"nothing trusted\"). `recall --explain` shows the numbers:\n`found`\u002F`below_limit` verdicts carry `detail.lanes`, the envelope reports the\nresolved `lane_floors`, and a row that missed every pool but would ride the\n1-hop link walk reports a `link_expansion` verdict (parent, relation, score)\ninstead of `not_in_pool`. Recall rows also carry the underscore-prefixed\ndiagnostic keys `_rel_lex` \u002F `_rel_cos` \u002F `_rel_ent` \u002F `_rel_graph` — the\nmeasured lane values the inject gate floors judge (same family as the\nexisting `_score` key).\n\n**Graph-seed arm and per-arm caps (issue #136)**: recall runs FOUR candidate\narms — FTS, vector, entity, and graph-seed — each capped BEFORE fusion so one\nover-generous arm cannot fill the fused window (named defaults equal the\npre-#136 windows, so default behavior is unchanged; `ZMEM_ARM_CAP_FTS` \u002F\n`ZMEM_ARM_CAP_VEC` \u002F `ZMEM_ARM_CAP_ENTITY` \u002F `ZMEM_ARM_CAP_GRAPH` override,\n`ZMEM_GRAPH_SEED=0` disables the graph arm). The graph-seed arm resolves the\nquery's entities (and ops tokens, which ride the same alias path) to seed\nmemories, then walks ONE bounded hop over `related`\u002F`supports`\u002F`updates`\u002F\n`derives` edges — never `contradicts` as a seed — and emits the capped,\ndeterministically ranked neighbors as the fourth RRF list. Each\ngraph-arm row carries a MEASURED `_rel_graph` lane: the best entry-edge\nscore, judged against the graph floor (default 0.75 = the write path's own\n`LINK_THRESHOLD`; env `ZMEM_INJECT_FLOOR_GRAPH`) by the same disjunctive\ninject gate — a graph-rescued row injects only when its edge to the\nquery-anchored seed is strong, never by the absent-lane exemption. `--explain`\nand the `--for-injection` envelope report per-arm `pre`\u002F`post`\u002F`cap` counts\n(`arms`), and the hook's decision line carries `arms=fts:P\u002FQ,vec:P\u002FQ,...`\nso the miss-rate report can attribute a hit to the arm that carried it.\n\n**Entity matching (v10, issue #60 5.3)** is the third fused list (after\nFTS\u002FBM25 and vector; the graph-seed arm above is the fourth): the query runs\nthrough the same deterministic extractor used at write time, and plain query\ntokens are additionally matched against stored entity aliases. Memories\nlinked to matched entities join RRF ranked by (number of matched entities,\nrecency), with the same namespace filter and the same `--as-of` temporal\npredicate as the other lanes — like the vector lane, an entity match in a\nFOREIGN namespace never leaks into the querying tier (it is found by its own\ntier's run, e.g. the global tier under `--include-global`). Works\nmodel-absent by design; an unknown alias contributes nothing.\n\n**Link expansion (v11, issue #61 6.3)**: after MMR and the passive-surface\nfilters, each result's `related`\u002F`supports` edges are walked ONE hop and up\nto `--link-budget` (default 2) neighbors are appended to the result set\n(never duplicating an already-present row, never crossing the tier's\nnamespace set, and always respecting the `--as-of` predicate — a neighbor\ninvalid at the requested instant does not surface). `contradicts`\nneighbors must additionally survive the confidence floor and are tagged\n`[CONTESTED LINK: \u003Cid>]` naming the other end of the contradiction.\nExpansion is bounded (one indexed lookup per result\nrow, budget-capped) — no PageRank or graph propagation of any kind.\n\n**MMR diversity (v10, issue #60 5.5)**: after the composite sort, and before\n`--limit`, Maximal Marginal Relevance re-orders each tier's candidates —\npicking the best-scoring row first, then trading relevance against\nsimilarity-to-already-picked (lambda: 0.7 default, `ZMEM_MMR_LAMBDA` env,\n`--no-mmr` to disable, 1.0 == no diversity). Row-to-row similarity is\nembedding cosine when both rows have embeddings, else Jaccard on normalized\ncontent tokens — so near-paraphrase clusters stop crowding out distinct\nfacts even on model-absent stores.\n\n**Entity cards (v10, issue #60 5.4)**: every recall row carries its linked\nentities — `entities: [{id, kind, name}]` in `recall --json`, at most three\nnames per row in the fenced hook render. `entity-list` inspects the entity\ntables; `entity-merge` reconciles duplicates.\n\n**Hybrid recall is the default** when the embedding runtime is available\n(auto-enabled; `--no-hybrid` forces lexical-only): a vector\u002Fembedding lane\nruns on top of this keyword pipeline and its ranking fuses with the lexical\nand entity lanes via the RRF above, so a memory BM25 missed can still\nsurface via embedding similarity. It needs the optional\nonnxruntime\u002Ftokenizers runtime and embedding model; without them recall\nfails open to plain keyword + entity ranking (see the `recall` command\nsection above for the full contract).\n\n**Entity extraction at write time is deterministic** (no LLM): explicit\n`entity:Name` \u002F `entity:\u003Ckind>:Name` tags, the `project:\u003Csuffix>` namespace\nsuffix, `--tags` tokens (`kind:Name` or plain), backtick-quoted spans in\ncontent (kind `tool`), and CamelCase identifiers (2+ humps). Every\nsource that carries NO explicit kind — untyped `entity:Name`, plain\n`--tags` tokens, CamelCase identifiers — is resolved by the\ndeterministic classifier (`reclassify-entities` documents its\nprecedence); a typed `entity:\u003Ckind>:Name`\u002F`kind:Name` tag and the\nbacktick-`tool` rule stay authoritative.\nStopwords (`the`, `and`, `use`, …) and path\u002FURL-shaped tokens are never\nentities; `person` entities exist only via an explicit `entity:person:`\ntag. The first kind seen for an alias wins; reconcile with `entity-merge`.\n\nThe `rebuild-fts` subcommand rebuilds the FTS5 index from scratch (useful after\nbulk imports or if the index drifts):\n```\n\u003Cinterpreter> \u003Cstore.py> rebuild-fts\n```\n\n## Sole memory system on Claude Code (replace native)\nWhen running under Claude Code with native memory turned off (`autoMemoryEnabled:\nfalse` in `~\u002F.claude\u002Fsettings.json`, or `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1`), ZMem\nis the **only** memory system in play — it is not layered on top of CC's own\nmemory. In that mode:\n- `store.py add` \u002F `recall` (this skill) is the canonical, sole path for\n  capturing and retrieving durable knowledge. Treat it exactly as you would\n  native memory on any other host.\n- **Do not hand-write** into CC's native `~\u002F.claude\u002F...\u002Fmemory\u002F` files or\n  `MEMORY.md` — that mechanism is intentionally disabled, and writing to it\n  would silently resurrect the duplicate-memory problem \"replace native\" exists\n  to avoid.\n- Project-level Tier 0 is `CLAUDE.md` (CC's own always-on mechanism, untouched\n  by ZMem) — not `AGENTS.md`. The SessionStart hook does not inject `AGENTS.md`\n  on Claude Code for this reason; it still does on ZCode.\n- If a SessionStart nudge appears telling you native memory still looks\n  enabled, that is informational for the user (a plugin cannot flip the\n  setting itself) — no action needed from you beyond surfacing it once.\n\n## Codex shared-store mode\nFor Codex, the safe cutover shape is:\n- **Disable Codex native memories yourself** in `~\u002F.codex\u002Fconfig.toml`; do not\n  let zmem auto-edit Codex config.\n- **Use one canonical physical store path** shared with the plugin hosts.\n- **Treat `ZMEM_TIER0=native` as the target shape**: keep Codex's own project\n  instruction surface native, and use zmem for shared Tier 2 durable memory.\n- **Do not assume Codex can write `~\u002F.zmem`.** If the shared store path is\n  outside Codex's writable roots, add a writable root or use a local broker\n  that owns the store and mediates read\u002Fwrite operations.\n- **Reapprove hooks after hook-surface changes.** Codex trusts hooks by a\n  SHA-256 over each normalized hook entry (event name + matcher + handler\n  fields). Every new or changed hook entry — this plugin's hooks file\n  changed in 0.28.0 (#95: PreToolUse + PreCompact entries) — carries a new\n  trusted hash, and an untrusted entry is SILENTLY SKIPPED: the hook simply\n  never runs. After upgrading, re-approve the hook surface in the Codex TUI\n  `\u002Fhooks` review (or re-record the `hooks.state` trusted hashes); for\n  vetted automation only, `codex exec --dangerously-bypass-hook-trust`\n  runs hooks without persisted trust for that invocation.\n\n## The reflection loop (Loop 1)\nThe `zmem-reflect.sh` Stop hook checks the episodic db for failed tool calls\n(status=error or exit_code!=0 on non-read-only tools) in the current session.\nIf found and no lesson references this session, it injects an additionalContext\nprompt at stop time. It is **non-blocking** (exit 0) — it only reminds you.\n\nThree env vars control the Stop hook's ZCode-db reader (#194): the reader opens\nthe db read-only (`mode=ro` URI plus `PRAGMA query_only`);\n`ZMEM_FAILURES_DB_TIMEOUT_S` bounds how long it waits on a busy ZCode db before\nreporting a substrate error (default 1.0; invalid or unset falls back to the\ndefault; clamped to 0.1-5.0; `store.py failures --db-timeout` overrides);\n`ZMEM_ZCODE_DB` overrides the ZCode db path (tests and operators can point the\ndetector at a scratch copy without touching `~\u002F.zcode`; empty\u002Funset means the\ndefault `~\u002F.zcode\u002Fcli\u002Fdb\u002Fdb.sqlite`); `ZMEM_REFLECT=0` (exactly `0`) disables\nthe whole Stop hook — unset, empty, or any other value keeps it enabled.\n\n### The no-failure nudge fires only on a signal change (#258)\nWhen the session has no tool failures and no rejections, the success nudge\n(\"you may have learned something worth capturing\") is gated on a state\nTRANSITION, not the state itself: it fires on the first Stop of a session\nor when a recognized runner signal appears or flips (`test`\u002F`compile`\u002F`lint`\nvia `capture_quality.infer_signal` over the transcript's Bash commands,\nread through `store.py signals`), and otherwise stays silent. Rejections\nneed no separate trigger — they have their own always-fire branch above\nthis gate (review PRR-001). \"Recognized\" is prefix-exact: bare `pytest`,\n`python -m pytest|unittest`, `python -m compileall`, `ruff check`, and\n`biome check` classify; compound commands (`cd x && pytest`, `uv run\npytest`) do not, so sessions that only invoke runners that way still see\nthe once-per-session behavior. The last-seen signal state is persisted per\nsession at `\u003CZMEM_DATA>\u002Fops\u002F\u003Csha256(session)[:32]>.signals` (atomic\ntmp-file + `os.replace` write; the finished file is swept on the shared\n7-day ops mtime policy, crashed-write `*.signals.tmp.*` orphans are\nreaped by `purge`) and re-recorded on every quiet Stop until a lesson is\ncaptured for the session — the lesson-dedup exit precedes the gate, so\nthe record freezes from that point (silent still means no\n`additionalContext`, never suppression of the signal itself): the\ncloseout skill's Step 0.5 candidate-review pass\n(`skills\u002Fcloseout\u002FSKILL.md`) is the intended future reader of that record;\na later PR extends it to surface suppressed signals. Substrate notes: on\nthe ZCode db substrate `tool_usage` has no command text, so no runner\nsignals are tracked and the nudge fires once per session; a missing\ntranscript path reads as a checked-empty scan (the hook treats it as a\nfresh quiet session); if `signals` cannot read the substrate (nonzero\nexit), the hook emits nothing and leaves the persisted state untouched —\na broken substrate must neither nag every Stop nor blank the record.\n\n### Subagent reflection is parent-side (#204)\nThe `zmem-subagent-reflect.sh` SubagentStop hook NEVER prompts the finishing\nsubagent. Claude Code honors additionalContext on (Subagent)Stop by continuing\nthe conversation, so a stop-time prompt would re-run the subagent turn and its\nreply (\"Memory captured\", \"blocked by sandbox guard, skipping\", …) would\nreplace the deliverable the orchestrator receives as the subagent's `\u003Cresult>`.\nInstead, when the subagent's own transcript shows failures (or rendered user\nrejections) and no lesson exists for `session:\u003Csid>:agent:\u003Caid>`, the hook\nwrites a hand-off sidecar under `\u003CZMEM_DATA>\u002Fsubagent-reflections\u002F` and emits\nthe empty envelope. The PARENT's own Stop hook scans that directory for this\nsession's sidecars, renders one reflection prompt covering all pending\nsubagent failures (pending sidecars alone are sufficient — a clean parent\ntranscript still prompts), and deletes the sidecars it rendered. Sidecars are\nconsumed on read, refreshed per agent on re-fire, and pruned when older than\n14 days, so at most one parent prompt surfaces per dispatched agent batch.\n`ZMEM_REFLECT=0` disables the subagent hook too (no sidecar). The Stop hook\nalso no-ops on Stop payloads carrying Claude subagent markers\n(`agent_id`\u002F`agent_transcript_path`) — defense-in-depth for hosts where Stop\nfires bare inside subagent contexts. Retention note: a sidecar persists the\nsubagent's failure details and rendered rejection reasons as plaintext JSON\nin this directory until the parent consumes it or the 14-day prune removes\nit (the prune runs from both the parent's Stop scan and each sidecar write).\n\nCapture a lesson only if it generalizes to a future session facing a similar\nsituation. If the failure was a one-off (typo, transient), do nothing — the prompt\nexplicitly allows that. Do not capture in-trajectory refinement tweaks as durable\nlessons; only capture what would help next time.\n","# Memory (ZMem)\n\nZCode's memory system has three tiers:\n- **Tier 0 — Core:** `core.md` (user-level, in the canonical shared store dir)\n  + `\u003Crepo>\u002FAGENTS.md` (project-level on ZCode). Auto-injected every session by\n  the SessionStart hook. Edit `core.md` directly for stable rules\u002Fpreferences.\n  Keep \u003C2KB.\n- **Tier 2 — Semantic:** `store.sqlite` (in the plugin data dir). Cross-task lessons,\n  facts, conventions, preferences. Operated by this skill via `scripts\u002Fstore.py`.\n- **Tier 4 — Procedural:** the skills library. (Extended later with evals + index.)\n\n## Finding store.py\nThe SessionStart hook injects a ready-to-run store command into context each\nsession (look for the `# Memory skill: invoke ...` note). Run the command it\nshows as-is: it names the shell-quoted interpreter the hook itself resolved\nahead of the absolute `store.py` path, so it cannot hit a stub `python` on\nWindows. The `store.py` path is the second quoted token in that command.\nOn Windows it will be a\nWindows-format path like\n`C:\\Users\\...\\plugins\\data\\zmem@...\\skills\\memory\\scripts\\store.py`\n(the `...` segments are elision placeholders, not real paths).\nIf you cannot find the injected path, the script is at the plugin root under\n`skills\u002Fmemory\u002Fscripts\u002Fstore.py` — invoke it through an explicitly\nresolved interpreter (`py -3` on Windows, `python3` on macOS\u002FLinux), never\nas a bare path and never bare `python3` on Windows (Store stub).\n\n## Local vs remote mode (issue #289)\nWith `ZMEM_REMOTE_URL` set, this machine's memory store lives on a hub and\nevery `store.py` subcommand routes through the fleet backend — no local\n`store.sqlite` is ever created or read:\n\n- **Writes** (`add`, `update`, `supersede`, `invalidate`) enqueue a durable\n  op and print a receipt line `{\"ok\": true, \"op_id\": ..., \"status\":\n  \"pending\", ...}` — the op flushes to the hub on the next read moment.\n  The routed write accepts `--namespace --type --content --tags --signal\n  --source-ref --capture-mode` and, since 0.129.0 (issue #326), the expiry\n  pair `--expiry-class --inject-until` on `add` and `update` (a state add\n  requires `--expiry-class`; an `update` may omit `--type` and the target's\n  type resolves when the hub applies the op — issue #317). Occurrence\n  flags (`--occurred-*`, `--mentioned-at`) and `--type event` still exit 2\n  naming the flag (occurrence semantics do not travel); so do\n  `--confidence --taint --evidence --no-dedup --reason`, an update that\n  names a non-state type while carrying expiry flags, and a state add\n  without `--expiry-class`. `--content -` still reads stdin.\n- **Reads** (`recall`, `search`, `recent`, `list`, `get`) always print the\n  JSON envelope (with or without `--json`) plus a `[PENDING]` trailer for\n  ops queued but not yet confirmed by the hub. Reads accept only the\n  query flag, `--namespace`, `--limit` (and `--json`); search's query\n  flag is `--text`. On failure they print `{\"ok\": false, \"reason\":\n  \"hub-unreachable\"}` (or `hub-response-invalid`) and exit 1.\n- **Store-independent commands keep running locally**: `failures`,\n  `signals`, `corrections`, `queue-*`, `source-exists`,\n  `source-ref-exists`, `ops-append`, `retain`, `retain-adapt`,\n  `ledger-clear`, `delivery-clear`, `hermes-reflect`, `hermes-context\n  --action commit-cursor`.\n- **Everything else** (maintenance and admin: `consolidate`, `organize`,\n  `reembed`, `purge`, `export-*`, `init`, `backup`, `evidence`,\n  `apply-ops`, …) exits 2 with `this command runs on the hub;\n  ZMEM_REMOTE_URL is set` — run it on the hub machine.\n- Config rule: `ZMEM_STORE` together with `ZMEM_REMOTE_URL` is a\n  configuration error (exit 2). `ZMEM_DATA` alone is fine — it names the\n  local data dir (op queue, sidecars).\n\n**Offline degrade (issue #290):** when the hub is unreachable, reads fall\nback to the machine's broker snapshot — a read-only, principal-scoped\ncopy under `\u003Cdata>\u002Fbroker\u002Fsnapshot.sqlite` that expires 72 h after issue\nand is refreshed hourly and on every hub reconnect by the broker (never\nby hooks). A served answer carries the label\n`[OFFLINE SNAPSHOT r\u003Crev>, \u003Cage> old]` (broker-emitted fence header on\nthe Inject path; an additive `offline_snapshot` provenance key on CLI\nreads, with a stderr note — stdout stays JSON-parseable). An expired or\nmissing snapshot keeps the plain `hub-unreachable` error on the CLI; the\nInject path logs `snapshot-expired` instead. A row served offline is\nadvisory and may be re-delivered live after reconnect; the snapshot never\nmerges with live data — a reconnect supersedes it.\n\nWith `ZMEM_REMOTE_URL` unset, every command is local and unchanged.\n\n## When to use\n- **Before a non-trivial task:** `recall` relevant past lessons. High-precision-first:\n  if a retrieved lesson does not clearly apply, ignore it.\n- **After a failure with a generalizable lesson:** `add` a lesson grounded in an\n  external signal (test\u002Fcompile\u002Flint\u002Freviewer\u002Fuser). The reflection Stop hook will\n  prompt you automatically when failures are detected.\n- **When you learn a stable fact or convention:** `add` it.\n- **When a memory is stale\u002Fwrong:** `supersede` it for a general tombstone; use the\n  dedicated **`invalidate`** command when a fact is *no longer true* (it REQUIRES a\n  reason so the correction is auditable); use **`update`** to revise a memory\n  append-only (the old row is tombstoned, a new live row links back via\n  `update_of`, and point-in-time `--as-of` recall still sees the old content).\n\n## Commands\n\nStore commands run `\u003Cinterpreter> \u003Cstore.py path> \u003Csubcommand>` — use the\ninterpreter the injected command names, or an explicitly resolved one\n(`py -3` on Windows). Never bare `python3` on Windows — that is a Windows\nStore stub — and never a bare `store.py` path (pin the interpreter explicitly; a bare .py path depends on platform dispatch).\n`doctor` is the one separate diagnostic script shown below.\n\n### doctor — read-only install diagnostics\n```\npython \u003Cdoctor.py path> [--project \u003Crepo>] [--repo-root \u003Czmem-repo>] [--format human|json|both]\n```\nRead-only preflight for cutover and operator debugging. It never mutates the\nstore or host config. Checks:\n- resolved store path and split-brain env\u002Fconfig risk\n- local\u002Fnon-OneDrive store path safety\n- Python version (supported floor 3.11) + SQLite FTS5\n- Node and a usable Git Bash\u002FCygwin shell on Windows\n- best-effort read\u002Fwrite access to the store path\n- schema compatibility against current v19\n- v9 append-only lineage columns present (`valid_until`\u002F`update_of`\u002F`taint`)\n- v10 entity identity tables present and non-vacuous (`entity`\u002F`entity_alias`\u002F\n  `memory_entity`); inspect deeper with `store.py entity-list`\n- v11 link surface present (`memory_link` table + `memory.trust_score` in\n  range [0,1]); inspect deeper with `store.py links --id \u003Cuuid>`\n- v13 episode storage present with counts (`episode-tables` check), and the\n  MCP token scope advisory (`mcp-token` check: warns `unscoped_token: true`\n  on full-access operator tokens; in principal-map mode (`ZMEM_MCP_TOKENS_FILE`)\n  reports scoped\u002Funscoped\u002Frevoked counts instead — never a token value)\n- Claude\u002FCodex native-memory conflicts via read-only config inspection\n- ZCode native memory (`zcode-native-memory` check, issue #185):\n  `~\u002F.zcode\u002Fv2\u002Fsetting.json` `memoryEnabled: true` fails cutover, explicit\n  `false` passes; unreadable\u002Fmissing setting warns — never auto-edited\n- host install-skew (issue #185): `duplicate-install` (fail) when more than\n  one enabled user-scope zmem install is registered for a host;\n  `marketplace-skew` (warn) when an installed cache version differs from the\n  marketplace version its registry entry points at; `project-pin` (warn)\n  when a project-scoped zmem pin is behind the enabled user-scope install.\n  Registries are read through the #184 strict codecs; missing registries\n  skip and malformed ones warn — doctor never edits host state\n- Codex manifest hook trust (`untrusted-hook` check, issue #185): compares\n  the pre-approval events the repo manifest registers (SessionStart,\n  PreToolUse) with the events the Codex config records trusted for the repo\n  (falling back to the box-wide union when no entry names this repo — on a\n  multi-repo box another repo's approval can stand in, so treat a pass as\n  inventory, not proof). Missing registered events warn\n  `untrusted-hook \u003Cids>`; reapproval is always manual\n- orphan-store inventory (issue #185): warns with `schema=`\u002F`rows=` for\n  every non-canonical SQLite store on the known host paths (plugin-data env\n  dirs, `~\u002F.zcode\u002Fmemory\u002Fstore.sqlite`); inspect then merge with\n  `promote-store --from \u003Cpath>` — doctor never deletes or migrates\n- canonical namespace for the provided project\n- host surface presence (Claude plugin, Codex plugin, ZCode plugin, memory\n  skill — all four are REQUIRED surfaces; a missing Codex plugin file fails\n  `host-surfaces`, and the repo-local Codex adapter lane never shipped)\n- served-tree drift vs `release-manifest.json` (`served-drift` check, issue\n  #107): `matched`\u002F`drifted` (warn — first 10 differing paths + both digests)\u002F\n  `unknown`-skip when the tree predates 0.17.0 and carries no manifest, and\n  `unknown`-WARN when a manifest is present but corrupt (failed its\n  algorithm\u002Fdigest integrity gate — the served mirror itself is damaged).\n  Log-only;\n  a drifted session start also appends one `zmem-drift` line to `zmem-bg.log`\n  and shows the operator a systemMessage — never model context.\n\nUse it before first install, before cutover, and after any store-path or hook\nsurface change.\n\n#### doctor `--miss-rate` — the miss-rate join + false-injection counter (issues #94, #129)\n\n```\npython \u003Cdoctor.py> --miss-rate --store \u003Csnapshot-store.sqlite> [--miss-db PATH]\n                   [--miss-transcripts GLOB ...] [--miss-bg-log PATH]\n                   [--miss-window-before 1800] [--miss-window-after 300]\n                   [--miss-limit 200] [--miss-min-overlap 2] [--miss-verbose]\n                   [--format json]\n```\nOpt-in check that measures the miss rate — \"a failure occurred in a session,\na matching memory existed in the store, and nothing surfaced at that moment\".\nIt joins mined tool failures (ZCode episodic db via `--miss-db`, transcript\nJSONL globs via `--miss-transcripts`) against a snapshot of the store\n(recall with telemetry fully disabled, `link_hops=0`) and the bg log's\ninjection decision lines — BOTH shapes: writer A's `reason=injected` and\nthe session-start writer's `status=injected` lines (that writer has never\ncarried `reason=`). Definitions (pinned):\n- `missed` — failure + floor-passing store match + no injection of a matched\n  row in the window ⇒ counts toward the miss rate.\n- `capture-gap` — failure + NO store match (write-side problem, counted\n  separately).\n- `surfaced (sid)` — an injected line whose `sid=` proves it is this\n  session's decision carries a matched row id.\n- `surfaced (legacy)` — same evidence on a pre-#94 sid-less line or a\n  `sid=unknown` line (window+overlap attribution only;\n  `miss_rate_strict_sid` excludes it).\n- `no-query` — nothing derivable for the failure (no recovered operation and\n  no ops ring): unmeasurable, excluded from every rate. On boxes where the\n  ops-ring lane is not yet deployed this bucket can dominate until rings\n  accumulate.\n\nIssue #129 adds the OTHER gate direction to the same report: the\n**false-injection rate** — of the injected decision lines, how many were\nNEVER referenced by any later same-session operation, prompt, or captured\nfailure. A line counts as USED when a later reference (mined failure\noperation\u002Ferror text, the session's ops-ring events, or transcript\nprompts) contains one of its memory ids literally, or shares >=\n`--miss-min-overlap` (default 2) distinct ops tokens with the injected\nrow's content. Rates print overall AND per moment (`session_start`,\n`user_prompt`, `pretool`, `subagent`, `precompact`; pre-#129 lines\nwithout `moment=` report under `legacy` with a caveat). Each decision\nLINE counts once — the same row re-injected at two moments is two\ndenominator lines, never double-counted.\n\nDecision telemetry lives in `\u003Cdata dir>\u002Fzmem-decisions.log` (issue #129\nsplit): maintenance output and `zmem-drift` lines stay in `zmem-bg.log`.\nBoth logs rotate instead of truncating — `ZMEM_BG_LOG_MAX_BYTES` (default\n262144) caps the active file and `ZMEM_LOG_ROTATIONS` (default 3) keeps\nbounded segments named `\u003Cname>.1`..`\u003Cname>.N`, each stamped with a\n`# zmem-seq=` marker; the join and the counter read rotated segments.\nLegacy deployments with only `zmem-bg.log` keep working: readers fall\nback to it when no decisions log exists, and an explicit `--miss-bg-log`\nwins over both.\n\nREFUSES to run without an explicit `--store`, and refuses the\nhost-default store even when given explicitly — the join reads session\ndata, so snapshot first: copy `store.sqlite` AND any\n`store.sqlite-wal`\u002F`-shm` beside it into a temp dir (plus\n`zmem-decisions.log` and its rotated `.1`..`.N` segments, `zmem-bg.log`,\nand the `ops\u002F` ring dir when present), then pass that path. exit 1 from\n`--miss-rate` most often means exactly that:\nsnapshot the store and re-run with `--store`; exit 1 from\nother checks means a real diagnostic failed.\nRead-only everywhere: the\nstore\u002Fdb open `mode=ro`, the report never writes, and a missing\u002Fold store is\nan error (never created, never migrated). Memory content stays out of the\ndefault output (ids\u002Fnamespaces only; `--miss-verbose` adds a short preview).\n\n### page — read, list, and refresh curated pages (issue #138)\n\n```\n\u003Cinterpreter> \u003Cstore.py> page read --id \u003Cpage-id> [--version \u003Cversion-id>]\n\u003Cinterpreter> \u003Cstore.py> page list [--namespace \u003Cnamespace>]\n\u003Cinterpreter> \u003Cstore.py> page refresh --id \u003Cpage-id> --query \"\u003Csource query>\" \\\n  --namespace \u003Cnamespace> [--tag \u003Ctag>]... [--llm-local]\n```\n\nCurated pages are local filesystem artifacts grounded in canonical memory.\n`read` and `list` are SQLite-independent and never create, migrate, or open a\nstore. `refresh` requires an existing store, uses the normal read-only and\nmaintenance coordination, and writes no canonical memory rows. The namespace,\nresolved data root, page ID, and regular contained page paths are validated\nbefore source selection. New candidates without evidence are excluded before\npublication; if a previously published live source loses its evidence during\nrevalidation, refresh refuses and preserves the committed pointer.\n\n`--tag` is repeatable. Each tag is one literal over stored comma-separated\ntokens: only delimiter-edge whitespace is trimmed, while internal spaces and\ncase are exact. A comma in a requested tag is literal input, not a wildcard,\nand stored commas have no escaping extension. A blank or whitespace-only query\nkeeps the existing recent-query selector behavior; a nonblank query that\nnormalizes to zero terms has zero lexical coverage. The refresh snapshot and\nits source\u002Fevidence associations are revalidated before publication, so a\nchanged source causes a refusal and leaves the prior committed pointer intact.\n\nThe optional `--llm-local` adapter is enabled only by setting\n`ZMEM_PAGE_ADAPTER_ACTIONS` to a non-empty local JSON action file. It is a\nbounded data-only input for offline maintenance; it cannot run commands, load\ncode, access the network, or name files. The page CLI does not populate or drop\na cache as part of these operations. `ZMEM_PAGE_ADAPTER_ACTIONS` must be a\nnonblank path to an existing file; a missing or unusable configured path is\nrefused before the store is opened. An existing recording is opened and\nvalidated during refresh, so unreadable bytes, malformed JSON, and over-limit\nactions follow the normal atomic refresh refusal. An adapter failure leaves\ncanonical memory and the last committed page unchanged.\n\nPassive page verification uses a finite cost policy. Metadata discovery may\nscale with the page directory, but expensive source\u002Fevidence verification is\nlimited to the 50 namespace-eligible committed pages with the greatest\ncanonical source snapshot watermark (maximum source `ingestion_ts` plus\nsnapshot hash), ordered descending and then by page ID ascending. The eligible\nset is the requested namespace plus `user:global`, and both share this one\n50-page cap; a newer global page can therefore displace a page from the\nrequested namespace. A 51st eligible page is deterministically withheld. It\nre-enters only when a later refresh advances its canonical source snapshot\nwatermark sufficiently; refreshing an unchanged snapshot preserves its\nwatermark and may leave it withheld. Foreign namespaces remain outside the\neligible set. This is a selection bound, not a universal filesystem latency\nguarantee.\n\nPublication stages into a nonce directory and replaces `current.json` last.\nDefault and passive readers use only the valid pointer, so interrupted staging\nor an unreferenced version is residue for operator maintenance. An explicit\n`page read --version` selects and validates the requested version file directly;\nit can read a valid unreferenced version left by interrupted publication and\ndoes not prove that the version is the committed current pointer. There is no\nautomatic garbage collector. First-publication recovery takes a fresh\nauthority snapshot and preserves immutable history. Cleanup removes a whole\ninspected artifact and retries the operation; it never deletes an individual\nhistorical version as a repair. Directory fsync is used where supported, with\nexplicit Windows directory durability limits and no universal crash-atomicity\nclaim.\n\nVersion-2 pages privately bind each represented source to its UTF-8 content\nhash. The public shapes remain stable: `page_read` exposes the existing version\nkeys plus `content` and `namespace` (and optional `bullet_sources`), while\n`page_list` and `page_for_injection` retain their existing key sets. The\nprivate `format_version` and `source_content_hashes` fields never appear in\nthose public results. Legacy version-1 pages remain operator-readable but are\nwithheld from passive delivery until a normal refresh creates a bound version;\nunknown formats are refused. Content changes withhold the page and require\nrefresh, without rewriting old history.\n\nBefore canonical `purge` deletes live rows, it preflights derived needles\nagainst the whole pages artifact directory. An existing needle, unsafe path,\nor unscannable artifact fails closed before the row-deletion transaction. The\noperator inspects and removes the whole affected page or staging artifact,\nthen retries; historical page versions are never individually deleted as\ncleanup. When page artifacts exist, the preflight intentionally snapshots the\nfull live SQLite database into an in-memory connection, reapplies the pending\npurge there, and derives needles from the exact post-purge survivor set. This\nprivacy check has memory and latency that scale with the store; no performance\nbound is promised. If the snapshot or artifact scan cannot complete, purge\nrefuses before live row deletion. Passive credential withholding controls\ndelivery only; it does not promise encryption or redaction of page content at\nrest.\n\nThe current observation gate is recorded as `reject`, so page metadata omits\n`runbook` (and since 0.107.0 the `ALLOWED_TYPES` vocabulary is the EIGHT-type\nset — `entity_card` joined as the card-refresh-only type; see \"Entity cards\n(issue #179)\" below).\n\n### event — occurrence-typed memories + the temporal recall arm (v18, issue #178)\n```\n\u003Cinterpreter> \u003Cstore.py> add --type event --namespace NS --content TEXT   --occurred-start 2026-09-01T08:00:00Z --occurred-end 2026-09-03T08:00:00Z   [--mentioned-at TS]\n\u003Cinterpreter> \u003Cstore.py> recall --query \"last week's outage\" --namespace NS   --prompt-timestamp 2026-09-10T12:00:00Z [--as-of TS]\n```\nSchema 18 adds the `event` memory type and the `memory_occurrence` side\ntable (occurred_start \u002F occurred_end \u002F mentioned_at beside the memory id).\nEvent rows require BOTH occurrence endpoints (clamped to the temporal\nwindow: the future clamps to the prompt timestamp, older than 3650 days\nclamps to the floor); non-event rows refuse any occurrence field (exit 2,\nno partial row). Occurrence rows live with their parent and SURVIVE soft\ntombstones of USER event rows (update \u002F supersede \u002F consolidate-absorb) —\nhistorical as-of recall surfaces them inside the tombstoned row's validity\nwindow; generated-row writers (organize summaries, entity reconcile) keep\nsame-transaction cleanup; hard deletes (purge, import-store) remove\noccurrence rows through the side-table registry; event->event updates\nadditionally inherit the occurrence onto the replacement row\n(non-event->event via update is refused; dedup folds keep the keeper's\noccurrence).\n\nThe temporal recall arm resolves relative phrases in the query (the fixed\n20-phrase vocabulary: today\u002Fyesterday\u002Ftomorrow, this\u002Flast\u002Fnext week,\nthis\u002Flast\u002Fnext month, this\u002Flast year, past 24 hours\u002F7 days\u002F30 days,\ntwo\u002Fthree days ago, in two days, earlier today, this\u002Flast quarter —\nlongest-match-first, so \"earlier today\" wins over \"today\") against the\n`--prompt-timestamp` (default: the `ZMEM_TEST_NOW` seam \u002F wall clock),\nselects occurrence-overlapping rows under the unchanged `--as-of`\nvalidity predicate, and fuses them as the capped fifth RRF arm\n(`ZMEM_ARM_CAP_TEMPORAL`, default 4). Knowledge validity (valid_from\ninclusive \u002F valid_until exclusive) is SEPARATE from occurrence time.\nRecall rows reached via the temporal arm carry additive occurred_start \u002F\noccurred_end \u002F mentioned_at envelope fields (the time the event HAPPENED),\ndistinct from valid_from (the time it was WRITTEN). The three keys ride\nonly temporal-arm rows (never non-temporal rows, never overwriting an\nexisting key), so projections that select known fields are unaffected.\nA phrase-free query or an occurrence-empty store fuses byte-identically.\nThe Hermes\u002FMCP `add` surfaces do not advertise `event` (they take no\noccurrence endpoints — the #173 `state` precedent).\n\n### recall — surface relevant memories (high-precision)\n```\n\u003Cinterpreter> \u003Cstore.py> recall --query \"\u003Cquery>\" [--namespace NS] [--limit 5]\n  [--link-hops 0|1] [--link-budget N]\n                          [--include-global] [--global-limit 3] [--hybrid]\n                          [--no-hybrid] [--no-mmr] [--no-bump]\n                          [--as-of ISO-8601] [--no-unfold] [--json]\n\u003Cinterpreter> \u003Cstore.py> recall --query \"\u003Cquery>\" --explain [--target ID|FRAGMENT]\n                          [--json]\n```\nReturns live (non-superseded) memories matching the query, filtered by confidence\nfloor (>=0.25) and namespace — unless `--as-of ISO-8601` is given, in which case\nit is a point-in-time read that returns rows **valid at that instant** (a row\nsuperseded after that instant may be included; a row created after it is not;\nsee the `--as-of` notes below). Prefer `--namespace project:\u003Cbasename>` to scope to\nthe current project; use `user:global` for cross-project.\n\n#### Retrieval debugger: `--explain` \u002F `--target` (issue #82)\n\n`recall --explain` re-runs the REAL pipeline (same lanes, same floors, same\nmerge) and prints one blameline per verdict explaining why a row did or did\nnot surface. It is an operator diagnostic in the same spirit as `doctor.py`:\nZERO writes (the telemetry path is never reached — stricter than\n`no_telemetry`), it never unfolds, it never fails a recall (a thrown tracer\ndegrades to the `explain_unavailable` verdict and the results still print).\n`--target` takes a memory id (full or unambiguous prefix) or a content\nfragment (case-insensitive substring, then token overlap >= 0.7); multiple\nmatches produce one verdict per id, never a guess. With `--json` the read\nenvelope gains an `explain` object: `query`, `query_shape` (the\nnormalized terms + exact FTS MATCH expression, #112), `target`, the\neffective settings (`namespace`, `limit`, `include_global`,\n`global_limit`, `no_mmr`, `no_bump`, `as_of`, `hybrid`), and\n`verdicts[]` with `id`\u002F`reason`\u002F`rank`\u002F`score`\u002F`detail`.\n\nVerdict reasons are a CLOSED set (`EXPLAIN_REASONS` in `storelib\u002Frecall.py`):\n\n| reason | meaning |\n|---|---|\n| `found` | in the presented result set at `rank` (1-based) |\n| `below_limit` | retrieved and scored but beyond `--limit` |\n| `below_floor` | the row's confidence is below the effective floor (the recall `min_confidence` parameter, default `CONFIDENCE_FLOOR`) |\n| `omitted_injection` | would have ranked; dropped because `no_bump` and prompt-injection risk |\n| `omitted_untrusted_web` | would have ranked; dropped because `no_bump` and `taint=untrusted_web` |\n| `namespace` | lives outside the query's expanded namespace set (and `--include-global` did not admit it) |\n| `superseded` | live-only recall and `superseded_at` is set (`detail.successor_id` names the live replacement when one exists) |\n| `not_valid_at_as_of` | `--as-of` set and the validity interval does not contain it |\n| `vec_lane_miss` | `--as-of` + hybrid: the row was valid then, but the vec KNN pool is live-rows-only (the SKILL.md as-of caveat — this is why history-on-hooks is not \"just pass `--as-of`\") |\n| `not_in_pool` | not in the FTS, vec, or entity candidate pool at over-fetch depth |\n| `not_in_db` | no row matched `--target` (`detail.neighbors` lists up to 5 nearest live rows by token overlap) |\n| `explain_unavailable` | the tracer threw; results still returned (fail-open) |\n\nFor an injection-path explanation, combine `--for-injection --explain`. This\nis a read-only replay: it follows the same omit, selective-inject,\nscore-margin, and token-budget stages as a passive injection, but never writes\nsurface or retrieval telemetry. A target removed by the score-margin stage is\nreported with the closed-set reason `margin_pruned`; its detail includes the\nobserved relative margin (formatted to six decimals), the configured threshold,\nand the retained top-row id. The existing omit, selective, scope, validity,\nand budget reasons remain unchanged for rows rejected by those stages.\n\n#### Change-intent lineage unfold — explicit recall only (issue #82)\n\nAlready-stored lineage (`update_of` from `update`) becomes visible on the\nEXPLICIT path: when the query reads as change-intent (\"what changed about X\",\n\"why did we switch to Y\", \"we used to Z\") AND the presented results contain a\nlive head of a tombstoned predecessor, recall appends the predecessor chain as\nbudgeted extra rows tagged `[PREVIOUSLY]` (JSON keys `unfold_of` +\n`unfold_hop`, 1 = immediate predecessor). Extras never count against\n`--limit`, never enter the telemetry bump set (popularity rewards query\nmatches, not neighbors), never cross namespaces, and are never silently\ndropped for taint — the explicit surface prefixes `[INJECTION RISK]` \u002F\n`[UNTRUSTED WEB]` like any other row.\n\nIt runs ONLY when all of these hold: not `--no-bump` (hooks, PreCompact,\nHermes prefetch, and eval defaults never unfold), not `--no-unfold`, the\nquery matches the compiled change-intent regexes (deterministic, no LLM), and\n`link_hops >= 1` (the same contract that keeps `search` — CLI, MCP, and\nHermes `_tool_search` — byte-identical; MCP `recall` unfolds for free).\n`--explain` describes lineage in verdict detail but never injects rows.\n\n| Env var | Default | Meaning |\n|---|---|---|\n| `ZMEM_UNFOLD_TOP_K` | `3` | max presented hits to walk backward from (clamped 1-10) |\n| `ZMEM_UNFOLD_MAX_HOPS` | `3` | max `update_of` hops per chain (clamped 1-10) |\n| `ZMEM_UNFOLD_BUDGET` | `4` | hard cap on total `[PREVIOUSLY]` extras per recall (clamped 1-20) |\n| `ZMEM_FAILURES_DB_TIMEOUT_S` | `1.0` | Stop-hook db reader busy-wait budget in seconds (clamped 0.1-5.0; invalid → default; `failures --db-timeout` overrides) |\n| `ZMEM_ZCODE_DB` | `~\u002F.zcode\u002Fcli\u002Fdb\u002Fdb.sqlite` | ZCode episodic db the Stop-hook detector reads (empty\u002Funset = default) |\n| `ZMEM_REFLECT` | enabled | set to exactly `0` to disable the Stop hook entirely |\n\n#### Confidence floors (issue #58, 3.8)\n\nFive distinct floors live on the recall path. Each reflects a different\nsurface's precision-vs-coverage tradeoff. They are env-overridable; the\nconstants live in `schema_meta.py`.\n\n| Constant | Default | Env override | Used by |\n|---|---|---|---|\n| `INJECT_FLOOR_PROMPT_DEFAULT` | 0.25 | `ZMEM_INJECT_FLOOR_PROMPT` | `recall` (UserPromptSubmit \u002F PreCompact). Hard floor on FTS\u002Fvec results — anything below is dropped before scoring. |\n| `INJECT_FLOOR_RECENT_DEFAULT` | 0.5 | `ZMEM_INJECT_FLOOR_RECENT` | `recent` (SessionStart \u002F subagent recall). Tighter because the surface is high-confidence recent material, not query-best match. |\n| `INJECT_FLOOR_GATE_NONE_DEFAULT` | 0.4 | `ZMEM_INJECT_FLOOR_GATE_NONE` | Hook selective-inject gate. `signal=none` rows must clear this floor; grounded-signal rows (`test`\u002F`compile`\u002F`lint`\u002F`reviewer`\u002F`user`) keep the 0.25 floor. |\n| `INJECT_FLOOR_TRUST_DEFAULT` | 0.2 | `ZMEM_INJECT_FLOOR_TRUST` | Selective-inject gate (issue #115). Hard floor on `trust_score`: a row the contradiction ledger has driven below 0.2 (nine or more distinct `contradict` events) can no longer ride the passive lane; `compute_score` also multiplies every composite by `trust_score` (identity at the default 1.0, so uncontradicted rankings do not move — explicit recall\u002Fsearch keep retrieving the row, just ranked lower). When E-5 (#124) starts recording violations automatically, they must feed THIS ledger via trust deltas — `violated_count` never becomes a second independent gate input. |\n| `INJECT_FLOOR_USER_GLOBAL_DEFAULT` | 0.5 | `ZMEM_INJECT_FLOOR_USER_GLOBAL` | user:global tier floor on the passive injection lane (issue #235). On gated moments (`pretool`\u002F`subagent` — the hooks' posttoolbatch maps to `pretool` store-side) a global-tier injection candidate whose MAX measured relevance lane sits below 0.5 is withheld and its reserved slot returns empty rather than being backfilled; the envelope reports the count as `global_withheld` (present only when a withhold happened). A global candidate with NO measured lane (query-less recent pulls) keeps the not-measured exemption — never withheld by this floor; the user:global floor is armed on the subagent moment for every pull; a bridged task-keyed SubagentStart recall (issue #253) additionally gives that surface a measured relevance lane, while query-less pulls keep the not-measured exemption. `session_start`\u002F`user_prompt`\u002F`precompact` keep their current global composition. `0` disables (relevance values are >= 0, so nothing is ever below it). |\n\nThe floors are intentional. Do not silently unify them. The\nselective-inject gate (3.8) is a passive-lane filter; the trust floor\napplies symmetrically to link-expansion neighbors that ride the passive\nlane (a once-contradicted neighbor at trust 0.9 still renders with its\n`[CONTESTED LINK: \u003Cid>]` marker naming the other end of the\ncontradiction).\n\n#### Score-margin gate (issue #182)\n\n`ZMEM_INJECT_MARGIN` is an opt-in score-separation gate between the existing\nselective-inject filter and token-budget admission. It defaults to `0.0`,\nwhich disables the gate and preserves the legacy injection bytes. The setting\nis read dynamically for each injection decision. A missing, negative,\nnon-numeric, NaN, or infinite value fails open to `0.0`; values above `1.0`\nare clamped to `1.0`. The recommended initial rollout is `0.05`.\n\nWith a positive threshold and at least two usable post-selective rows, the\ngate compares the two highest scores using the stable score-descending view\nand computes the raw `(top - second) \u002F top`. Every candidate score must be\nusable; a malformed, scoreless, or non-finite score anywhere fails open. When\nthe raw margin is strictly below the threshold, ordinary rows after the winner\nare pruned before the token budget runs. A `decision` or `constraint` in either\nleading position is protected: no rows are pruned, but the valid observed\nmargin is still reported. Fewer than two rows and a non-positive top score\nalso fail open. The original presentation order is retained for surviving\nrows.\n\nWhen a valid score pair was observed, injection JSON adds `margin` formatted\nto six decimal places and `margin_pruned_ids` in prune order; no fields are\nadded when the gate could not make a decision. The hook decision line appends\nthe same diagnostics, in the order `margin=` then `margin_pruned=[...]`, after\nthe existing optional fields. Empty pruned lists are omitted from the hook\nline, and malformed optional values are ignored independently so legacy\nlogging remains fail-open and byte-compatible.\n\nWhen a passive inject surfaces nothing, it names WHICH gate fired (issue #87 \u002F\n#85 direction 1): `no durable memories retrieved for this prompt.` means the\ncandidate pool was empty OR every retrieved row was dropped by the passive\ninjection-risk filter — the one-liner is shared by design (the model is never\ntaught that omitted rows existed); the `zmem-bg.log` line distinguishes them.\n`no durable memories met the inject bar.` means rows reached the\nselective-inject gate and none passed. A token-budget wipe says so in its own\nwords (`memories withheld: the injection token budget (ZMEM_INJECT_TOKEN_BUDGET)\ndropped every candidate row.`). Hermes\u002FMCP `session_start` use the session\nvariants (`no durable memories retrieved for this session.` and\n`session memories withheld: ...`).\n`zmem-bg.log` carries the same cut per decision line: every\n`zmem-hook` line has `reason=` (`reason=empty-pool`, `reason=omitted`,\n`reason=below-bar`, `reason=budget-drop`, `reason=below-relevance`,\n`reason=already-delivered`, `reason=injected`), plus `omitted=N`\nwhen the passive injection-risk filter dropped rows. The closed set lives in\n`schema_meta.py` (`INJECT_SILENT_REASONS`). Since issue #94 every line also\nends with `sid=\u003Csanitized session id>` (`[^A-Za-z0-9._-]` → `_`, cap 128;\n`sid=unknown` when the host sent none) — the session key doctor's\n`--miss-rate` join binds failures to injections with. The order is `status`, `reason`, `omitted=`, `ids=`, `all=`, `tokens=`, `rendered_estimate=`, `admission_budget=`, `budget_dropped=`, `budget_truncated=`, `budget_dropped_protected=`, `sid=` (the retired `ops=` ring count rode before `sid=` in releases \u003C= 0.37; issue #158 moved operation context into the query, not the log). Current lines append `exc=`, `moment=`, optional `lane=`, `ver=`, `t_ms=`, then `arms=`, `batch=`, `tools=`, `paths=`, and margin fields (the budget fields are issue #116's distinct labeled numbers and ride only when budget accounting ran).\n\n#### Decision attribution and report projection (issue #153)\n\nThe additive decision suffix is ordered `lane=`, `ver=`, `t_ms=` after the\nhistorical `moment=` field and before later additive tails (`arms=`,\n`batch=`, `tools=`, `paths=`, and margin fields). Closed lanes are `claude`,\n`codex`, `zcode`, `deepseek`, `hermes-provider`, and `hermes-compat`;\nruntime moments are\n`session_start`, `user_prompt`, `pretool`, `subagent`, and `precompact`.\nSilent reasons are exactly `empty-pool`, `omitted`, `below-bar`, `budget-drop`,\n`below-relevance`, `already-delivered`, and `expired` (`expired` names a pool\nthat was eligible before the live-expiry gate drained it — issue #174's\nproducer; every other condition keeps its precedence). The decision line can\nadditionally carry the transport tokens `reason=store-error` — a store\nchild that exited nonzero (e.g. a partial-upgrade store rejecting an\nunknown flag) — and `reason=envelope-error` — a store child that exited\nzero whose stdout was not a JSON envelope (issue #435); both are emitted\nby the hook body and the hermes-provider lane, and miss-rate reporting\nexcludes both from the missed count like the\nother negotiation refusals. `reason=injected` is a\nsuccessful decision and `reason=disabled` is the separate kill-switch marker.\n\n`already-delivered` is selected when pre-ledger candidate ids are nonempty but\nthe post-ledger pool is empty, after `budget-drop` in the precedence order;\ndual-empty remains `empty-pool`. `ver=` and `t_ms=` are an all-or-nothing\nenrichment pair: `ver=` is the release-manifest semver and `t_ms=` is a\nnonnegative rounded `perf_counter` duration. A manifest\u002Fversion failure keeps\nthe complete legacy line and omits all attribution fields. A lane is optional\nfor compatibility callers, but an explicit lane must be closed-set.\n\n`parse_bg_log` accepts legacy lines without attribution. If any attribution\ntoken appears, it requires valid `ver=` and decimal nonnegative `t_ms=` and\nrefuses malformed enrichment; `moment=` remains open for compatibility values.\nLane-less enriched lines and moments outside the report set stay aggregate-only.\nThe named report is always a sorted, zero-filled 20-row matrix: five lanes ×\nthe four report moments `session_start`, `user_prompt`, `pretool`, and\n`precompact`. The runtime `subagent` moment and `session_start_compact` are\nintentionally excluded from that 5×4 projection but remain visible in\naggregate statistics. The local Hermes provider writes `hermes-provider`; the\nremote compatibility path writes `hermes-compat`, and invalid explicit lanes\nreturn structured status 2 before store work while absent lanes stay omitted.\n\n#### `store.py report` — per-session repeat and composition report (issue #249)\n\n`store.py report [--decision-log \u003Cpath>] [--json]` aggregates the decision log\ninto the repeat rate and composition tables, read-only. It is the instrumentation\nWorkstream P PRs 2-4 use as their before\u002Fafter baseline.\n\n- `rows[\u003Cid>]` — `delivered` (total deliveries), `sessions` (distinct `sid=`\n  count), `max_per_session` (busiest single session), and the row's `tier`.\n  Lines carrying no usable `sid=` count toward `delivered` but contribute no\n  session, and the run says so in `caveats`. **The writers' `sid=unknown`\n  placeholder counts as no session**: it is what every host without a session id\n  emits, so pooling it would invent one session and attribute unrelated\n  deliveries to it. This matches how the sibling readers already treat it.\n- `by_tier_moment[\u003Ctier>][\u003Cmoment>]` — `delivered` (the `ids=` entries) against\n  `withheld` (the `all=` candidates that were not injected). `delivered` counts\n  only lines the shared injected-line predicate accepts, so ids on a\n  `status=silent` line are never reported as deliveries; their candidate pool is\n  still counted as `withheld`, because a candidate was offered either way.\n\nTop-level JSON keys: `decision_log`, `decision_lines`, `ts_first`, `ts_last`,\n`rows`, `by_tier_moment`, `caveats`. `ts_first`\u002F`ts_last` are the decision\ntimestamps actually covered, so two runs can be checked for an overlapping window\nbefore a before\u002Fafter difference is read as a behaviour change.\n\n`--decision-log` defaults to `\u003Cdata dir>\u002Fzmem-decisions.log`, falling back to the\nlegacy `zmem-bg.log`. Rotated segments (`.1` … `.N`) are included. Without\n`--json` a human table prints; with `--json` stdout is exactly one compact JSON\ndocument, so it can be piped into `jq` unchanged — **including on a refusal**,\nwhere stdout carries `{\"ok\": false, \"error\": \"...\"}` at exit code 2 and the\nhuman `[zmem] report:` line goes to stderr. A missing or unreadable log is\nreported as a `caveats` entry rather than a silent empty report, because an\nempty report and an unreadable log are otherwise indistinguishable.\n\n**Tier labels** come from the row's stored namespace: a `project:` namespace is\n`project`, `user:global` is `global`, and anything else — including an id no\nlonger in the store — is `unknown`. The live five-tier scoped recall model spells\nits global tier `user_global` and adds `domain` \u002F `fleet_host` \u002F\n`cross_project`; its cross-project tiers depend on the CALLER's current project,\nwhich an offline report does not have, so it does not invent them.\n\n**Read it with `caveats`.** Tier labels come from a point-in-time snapshot; an id\nmissing from that snapshot is reported as `unknown`. The log-derived figures and\nthe tier-summed totals are unaffected, but the per-tier split can shift, so\n`unknown`-bucket movement between two runs is not evidence of tier migration.\n\nThe command never writes to the store, the ledger, or the log. It opens the store\nthrough an `immutable=1` handle only when there is no residue at all, or through\na private staged copy of `store.sqlite` + `-wal` + `-journal` when there is; a\nplain read-only open would create sidecars and mutate `-shm` in place. A hot\nrollback `-journal` also forces the staged path on purpose: `immutable=1` ignores\nthe journal entirely and would report the uncommitted pages of an in-flight\ntransaction as real rows. The staging directory is created owner-only and is\nremoved on every path including failures; a directory that survives removal is\nnever left silent — on a successful run it is appended to `caveats`, and on a\nrefusal it is reported alongside it (folded into the refusal message, or on its\nown `[zmem] report:` stderr line for a non-`ReportError` failure). `-shm` is\nnever copied — SQLite rebuilds it from the `-wal`, and copying it fails\noutright under a live writer on Windows.\n#### Query context (prior-turn operation tokens) — issue #88 \u002F #85 direction 2\n\nDecision-point prompts are prose with zero lexical overlap with the\noperation-adjacent lessons that matter; the retrieval signal lives in the\ntool commands the session is executing. The PostToolUse hooks\n(convention-capture on the coding hosts, `post_tool_call` on Hermes) append\neach Edit\u002FWrite\u002FBash event to a per-session ring at\n`\u003Cdata>\u002Fops\u002F\u003Csession>.log`, storing ONLY the tool name plus allowlisted\ntokens (git subcommand chains, test-runner verbs, edited-path basenames) —\nnever a raw command dump, stdout, or argument values that are bare words;\nsecret-shaped tokens (common credential prefixes like `ghp_`\u002F`sk-`\u002F`xox?-`)\nare dropped as well. The ring is byte-capped: past 64 KB it is trimmed to\nthe newest 64 lines. The store-owned passive selector composes that ring only\nfor the `pretool` moment, with the ops tail occupying a fixed reserved slice\nINSIDE the 500-char cap (never appended past it; the separator shares the\nslice, so no token is severed). UserPromptSubmit and Hermes prefetch no\nlonger append a caller-owned ops tail.\n`ZMEM_QUERY_CONTEXT=0` is the lane kill switch — it stops BOTH composition\nand ring collection (an operator disabling the lane expects no sidecar\nwrites). The ring resolves through the same unified data-dir chain as the\ndecision log (`hooks\u002Flib\u002Fzmem-recall-body.py::_data_dir()`, which resolves\nthe decision-log data directory: a directory-qualified `ZMEM_STORE` parent\nfirst, then `ZMEM_DATA` \u002F `CLAUDE_PLUGIN_DATA` \u002F `ZCODE_PLUGIN_DATA`, then\n`~\u002F.zmem`) — `read_ops_ring(data_dir, ...)` reads the ring from exactly the\ndirectory it is given, including plugin-data directories by #88 design.\nOperation derivation remains store-owned; no operation count crosses\nthe exact hook envelope. Explicit surfaces (`recall --query`, `search`) are unchanged.\nLimitation (deliberate, see #88): this helps LATER turns only — the first\ntool call of a turn still runs before any operation context exists.\nCost note (#93 B4): credential-prefix-shaped tokens (`sk-`, `npm_`, `AKIA`,\n…) are dropped from the ops query EVEN when they are legitimate filenames —\nfail-safe direction (signal loss, never a leak); rename a real path that\ncollides. The eval composer ignores `ZMEM_QUERY_CONTEXT` by design (#93 B6):\nevals must stay deterministic and immune to ambient env, so the kill switch\ndoes not change their queries.\n\n#### Pre-tool checkpoint phrases — issue #99\n\nPreToolUse passive recall also enriches the query from the current raw\n`tool_input` object before the tool runs. The store serializes that object as\nsorted compact JSON (`sort_keys=True`, separators `,` and `:`), examines\nexactly its first 150 Unicode characters, and selects at most one entry from\nthis immutable ordered table:\n\n| Event | Checkpoint phrase |\n|---|---|\n| stash-consume | `foreign-stash conflict verify stash list` |\n| reset | `stale tree fetch main rebase verify diff` |\n| force-push | `stale tree fetched base force-with-lease` |\n| branch-publication | `stale tree fetched base force-with-lease` |\n| base-rewrite | `base drift citation re-pin` |\n| path-test | `basename ratchet citation re-pin local battery` |\n\nThe recognized forms are `git stash pop|apply|drop`, `git reset --soft|--hard`,\n`git push --force|--force-with-lease|-f`, ordinary `git push`, `git merge\n--squash`, and test\u002Fcitation paths. `git stash list`, unknown operations,\nnon-object input, and matches beyond character 150 add no phrase. Current-event\noperation tokens take precedence over an older session ring; the ring remains\nthe fallback. The operation-token tail retains its 150-character reservation,\nthe checkpoint phrase is never split, and the final query remains bounded to\n500 Unicode characters. Raw tool input crosses only the private bounded\nhook-to-store stdin channel and is neither logged nor persisted. The\n`ZMEM_QUERY_CONTEXT=0` kill switch disables both token and phrase enrichment.\n\nThis feature shipped under issue #155's published measurement decision (the\n2026-09-19 fail-closed INSUFFICIENT outcome); #155's predeclared measurement\nof record has since landed (`eval\u002Freal-corpus-2026-09-19.json` — an audit\nover a frozen cohort, not a live efficacy claim), so the gate is satisfied\nwithout an efficacy claim by this feature.\n\n#### Passive-injection kill switch (ZMEM_INJECT=0) — issue #110 \u002F P0-5\n\n`ZMEM_INJECT=0` disables every passive recall-injection surface: the shared\nrecall body (all modes — user_prompt, pretool, subagent, precompact\u002Frecent),\nthe SessionStart hook (Tier 2 recall AND Tier 0 — under the switch\nSessionStart emits its empty `{}` envelope), the Hermes provider\n(`prefetch`, the `zmem_session_start` tool twin, and the system-prompt\n`core.md` block), the Hermes reflect hook's delivery paths, and MCP\n`session_start`. Each silenced surface emits its empty envelope and logs\n`status=silent reason=disabled` (`reason=disabled` is written only by this\nswitch, never by silent-reason classification — it lives beside `injected`\nas `INJECT_REASON_DISABLED`, not in `INJECT_SILENT_REASONS`). Only the\nliteral `0` (whitespace-tolerated) disables — the `ZMEM_QUERY_CONTEXT`\nconvention; `false`\u002F`no`\u002Fempty leave injection enabled. Capture paths never\nconsult the switch: correction capture, the ops ring, convention counters,\nfailure capture, and session-cadence maintenance keep writing — and the\ncapture-side PROMPT hooks (`zmem-reflect.sh`, `zmem-subagent-reflect.sh`,\n`zmem-convention-capture.sh`, capture-failure\u002Fcorrection prompts) stay\nactive by design (#110: they prompt capture, they do not inject recalled\nmemory; the Hermes reflect hook is the documented divergence — a named\ngated surface whose delivery carries recalled query-context). Parked\npre-tool fences and armed nudge markers are left in place and deliver on the\nfirst enabled run. `doctor` shows the state (`inject-switch` line, WARN when\ndisabled). Near-miss spellings (`0.0`, `00`, `False`, `off`) do NOT disable\n— the parser matches the exact literal `0`; verify with `doctor` after\nsetting.\n\n#### Capture kill switch (ZMEM_CAPTURE=0) — issue #123\n\n`ZMEM_CAPTURE` is a string environment variable with default `\"1\"`. The\nfailure, convention, Stop, SubagentStop, and Hermes compatibility surfaces\nread it before payload parsing, marker\u002Fqueue writes, lesson queries, operation\nappends, or other stateful work. Only a trimmed literal `0` disables capture;\nundefined, empty, whitespace, `false`, and `00` stay enabled. Disabled shell\nsurfaces emit their exact empty sentinel and Hermes emits `{}`. Audits and\nprobes set `ZMEM_CAPTURE=0` so those five covered surfaces cannot teach the\nsystem.\n\nThis is independent of `ZMEM_INJECT` (delivery) and `ZMEM_QUERY_CONTEXT`\n(operation-ring collection). Under `ZMEM_INJECT=0`, capture can still run;\nunder `ZMEM_CAPTURE=0`, no capture subprocess or state mutation runs on those\nfive surfaces. The pre-existing UserPromptSubmit correction queue and Hermes\nconvention-compatibility cadence are separate legacy paths with their own\ncapture-mode\u002Finterval controls. Complete\n`\u003C\u003C\u003CZMEM_UNTRUSTED_FENCE>>>` through `\u003C\u003C\u003CEND_ZMEM_UNTRUSTED_FENCE>>>` blocks\nare removed before transcript-derived content reaches deduplication, queue\nsynthesis, or closeout review.\n\n#### Pre-tool repository-state predicate — issue #397\n\nSince 0.124.0 the pretool lane carries a bounded, fail-open\nrepository-state predicate. When the DERIVED pretool query contains any\nrestore\u002Fdestructive verb as a substring (checkout, restore, reset, stash,\nclean, rebase, merge, push, cherry-pick, revert, filter-branch — a\nsubstring match, so benign text like \"cleanup\" also arms it; the cost is\none extra `git status`), the hook body runs\n`git status --porcelain --untracked-files=no` once — TRACKED-change dirt\nonly, exactly the wiped-by-`git checkout --` hazard — in the EVENT cwd\nwhen it names an existing directory (process cwd otherwise), with the\ntimeout read from `ZMEM_STATE_PROBE_TIMEOUT_S`\n(float seconds, default 2.0, clamped at 8.0; malformed values fall back\nto the default). When the verb gate does not arm, the probe does not run\nand the decision line carries NO `state=` field at all — absence means\n\"no probe ran\", distinct from `state=unavailable` (\"ran and failed\").\nThe outcome travels as `--state-hint dirty|clean` on the pretool recall\nargv (omitted entirely when the probe fails — never an unknown value) and\nis recorded on the decision line as `state=dirty|clean|unavailable`.\nStore-side, a candidate row receives a +0.15 rank bump — on both the sort\nscore and `_score` — ONLY when the hint is exactly `dirty` AND the row's\ncontent matches the closed hazard lexicon (`uncommitted`, `dirty tree`,\n`dirty working tree`) AND carries a condition marker AND negation\npolarity. Clean, unavailable, and absent hints are byte-identical to\ntoday; the global relevance floor and trust gates still withhold gated\nrows (a bump never rescues them). The hint rides the recall query branch\nonly — the queryless `recent` lane stays state-blind. Test seam:\n`ZMEM_TEST_REPO_STATE=dirty|clean|unavailable` overrides the probe\nentirely.\n\n#### Snapshot-refresh refusal decision lines — issue #449\n\nSince 0.146.0 a refused snapshot refresh (any shipped refusal class:\n`hub-unreachable`, `hub-refused`, `payload-invalid`) appends ONE bounded\nline to the guarded decisions log beside the unchanged #437 stderr line:\n`[\u003Cts>] zmem-hook status=silent reason=\u003Cclass> ids=[] all=[]\nsnapshot_refresh=refused`. The line is parseable by the shipped\ndecision-log reader via the additive TERMINAL `snapshot_refresh=` optional\ngroup (appended after `state=` so every earlier positional group index is\npreserved); parsed rows carry `snapshot_refresh` verbatim (None on lines\nthat predate the field). No enrichment trio (`lane=`\u002F`ver=`\u002F`t_ms=`) rides\nthe line, so the reader's enrichment veto stays untriggered. The two\nwrites are failure-independent: rotation problems and a closed\u002Fraising\nstderr never suppress the persistent line (and vice versa); the whole\nwriter body is guarded so a refused\u002Freadonly data dir only loses the line,\nnever the refresh ladder.\n\nKnown bounds (PR #451 review, disclosed): a FULL hub outage writes ZERO\nrefusal lines — the broker takes its offline-snapshot branch and returns\nBEFORE the refresh attempt, so the stamp (not the decision log) is the\noutage-window evidence; refusal lines appear only when a refresh was\nATTEMPTED and then refused (fresh-pair transport failures retry per\nmoment, one line each — the rotation family bounds the growth). The rows\ndilate hook-decision aggregates that count all parsed lines\n(`sid_coverage_pct`, period bounds, attribution counters — a refusal row\ncarries no `sid=`); no shipped report branches on `snapshot_refresh` yet\n(parsing is the shipped contract; a dedicated refusal surface is future\nwork). Mixed version: a pre-0.146 decision-log READER skips these lines\nwhole, and a pre-0.146 `scripts\u002Feval_replay.py` REFUSES the log\n(ReplayError on the unknown terminal field) — do not point an older\nreplay evaluator at a 0.146 data dir.\n\n#### Write-path capture guards and the session closeout — issues #398 \u002F #246 \u002F #247\n\n- **Conditional-contradiction notice (#398)**: an `add` whose content\n  opposes a recent same-namespace row (negation-polarity disagreement +\n  condition marker + >= 2 shared significant tokens, 200-row window)\n  stores normally with a STRUCTURED notice naming both rows — notice\n  only, never an edge or trust mutation.\n- **Lesson phrasing advisory (#398)**: a `--type lesson` row with no\n  trigger-condition marker (when\u002Fwhenever\u002Fif\u002Fwhile\u002Fbefore\u002Fafter\u002Funtil)\n  stores with an advisory suggesting the conditional phrasing.\n- **Hostless namespace near-miss (#246)**: a `project:\u003Cowner>\u002F\u003Crepo>`\n  key with a dotless first component stores, with a\n  `namespace_project_near_miss` warning naming the canonical\n  `project:github.com\u002F\u003Cowner>\u002F\u003Crepo>` form (assumption A7:\n  warn-and-store — rejecting would break legitimate hierarchical\n  namespaces).\n- **Session closeout (#398)**: every successful attributed store write\n  (`add`\u002F`update`\u002F`supersede`\u002F`invalidate`\u002F`entity-card` when a card was\n  written) stamps `\u003Csha256(session-id)[:32]>.wrote` under `\u003Cdata>\u002Fops\u002F`;\n  refused writes stamp nothing. The session hooks surface exactly one\n  bounded closeout reminder while `.wrote` exists without `.closeout` —\n  at session end, and (because SessionEnd output has no live model turn\n  on most hosts) re-surfaced once at the NEXT SessionStart in\n  model-visible context. A recorded closeout silences it:\n  `store.py closeout-ack --session-id \u003Cid>` (store-independent; the\n  closeout workflow's Step 7 runs it). The ops age sweep reaps\n  `.wrote`\u002F`.closeout`\u002F`.nudged` markers after\n  `ZMEM_SENTINEL_SWEEP_DAYS`.\n- **Harvest-paste gate (#247)**: a user message over 400 chars whose\n  dominant fenced block parses as JSON (array or single object) carrying\n  >= 3 of the harvest keys (`namespace`, `type`, `content`, `signal`) is\n  NOT captured as a correction candidate — it belongs to\n  `\u002Fingest-harvest`. The gate runs on the manual correction hook lane and\n  the auto-retain and history-mine lanes; line endings are normalized\n  (CRLF pastes are gated too).\n\n#### Pre-tool inject — issue #90 \u002F #85 direction C\n\nOn hosts whose pre-tool contract was probed and confirmed (ZCode: documented;\nClaude: emitted — issue #117 retired the sidecar default in favor of the\nper-session delivery ledger), a\nPreToolUse hook (`zmem-pretool-recall.sh`, matcher\n`Edit|Write|MultiEdit|NotebookEdit|Bash|apply_patch|Agent|Task` since 0.31.0 (apply_patch added later; #338) — issue #119;\nmatching the live registration in `hooks\u002Fhooks.claude.json`.\nthe Agent branch passes task text when the event carries it AND,\nwhen it does not, parks the delegating prompt store-side for the child's\nSubagentStart (issue #253), otherwise using the store-owned recent path)\nderives the recall query from the\ntool input ITSELF — the command or file path about to run — and injects\nmatching hazard lessons before the tool executes. Pre-tool\n`additionalContext` is documented on Claude Code (since 2.1.9 it lands\nalongside the tool result; pausing is `permissionDecision`-driven only) —\nissue #158 makes the store-owned selector the only passive path: delivered\nids now live in a bounded per-session ledger\n(`\u003Cdata>\u002Fops\u002F\u003Csha256-of-session-id>.ledger`, atomic, window- and\ncap-bounded — plus per-call `\u003Chash>.ledger.seg.\u003Chex>` JSONL segments that\npublish without a lock and are merged into the base by a best-effort\ncompaction; reads always merge base + segments, issue #252) that EVERY\ninjection moment consults, so the same row is not\nre-delivered within the window — with one deliberate exception: PreToolUse\nre-delivers when the operation tokens about to run strongly match the row\n(the session-start hazard still fires before the dangerous command). The\nledger clears at PreCompact and the following SessionStart uses the ordinary\n`session_start` selector moment. The retired `ZMEM_PENDING_SIDECAR` fallback\nis no longer produced or consumed. The host event's `session_id` (without\nit the adapter stays fail-open) is passed to the selector. The hook NEVER\ndenies (a surfaced hazard is information, not\ngrounds to block a legitimate command) and stays fully silent when nothing\nqualified. `ZMEM_QUERY_CONTEXT=0` silences every query-context lane, this\none included. Hermes delivers the equivalent on `pre_llm_call` (after the\nfact of the producing call), best-effort per ring CURSOR\n`(ts, event-count)` — same-second events still deliver; a transient store\nfailure after the at-most-once marker skips that cursor's delivery. Hermes\ndelivery's namespace follows the hook chain `ZMEM_MCP_NAMESPACE` →\n`ZMEM_NAMESPACE` → `user:global` (issue #71 review: one chain for prefetch,\nrecall, and correction capture — Hermes hook events themselves carry no\nnamespace); project-scoped operation context delivers\non the coding-host PreToolUse surface. All query-context persistence\n(rings, delivery markers, and the session delivery ledger) lives under `\u003Cdata>\u002Fops\u002F`\nsidecars and never grows the store's tables. Codex pre-tool injection is\nWIRED (issue #95): `hooks.codex.json` registers `PreToolUse` with matcher\n`Bash|apply_patch` — dumped live from codex-cli 0.153.0 (Windows,\n2026-09-09: shell operations emit the hook tool name `Bash`, file patches\nemit `apply_patch`; Codex treats an all-alphanumeric\u002Fpipe matcher string as\nEXACT alternation, so it matches precisely those two names). MCP tools\n(`mcp__\u003Cserver>__\u003Ctool>`) and `write_stdin` are deliberately OUT — pre-tool\nrecall derives its query from shell\u002Ffile-patch inputs, not arbitrary MCP\narguments. Upstream accepts `hookSpecificOutput.additionalContext` on\nPreToolUse (model-visible, non-blocking); Codex hooks reference:\nhttps:\u002F\u002Flearn.chatgpt.com\u002Fdocs\u002Fhooks. Codex envelopes are additionally\ncapped at 8000 encoded bytes (≈2000 tokens at the plugin's 4-chars\u002Ftoken\nestimator; dense multi-byte (CJK) content has less real headroom) — 20% margin under upstream's 2,500-token hook-output spill\nlimit (`DEFAULT_HOOK_OUTPUT_TOKEN_LIMIT`, codex-rs output_spill.rs,\nverified against tag rust-v0.153.0); the cap applies even when an operator\nsets a larger `ZMEM_CTX_BUDGET`. The unset `ZMEM_CTX_BUDGET` default is\nhost-dependent: 25000 on the ZCode launcher path, 9000 on the Claude Code\npath and the ZCode non-launcher path. Keep the verification-first convention: re-probe\nwith a live tool_name dump before changing the matcher.\n\nInject surface parity (host facts, not aspirations): Claude Code registers\nSubagentStart (task-text recall — issue #119, probe 2026-09-10: neither\nhost's SubagentStart payload carries task text, so the query ladder is\npayload-field-if-present → the delegating prompt parked store-side by the\nparent's PreToolUse(`Agent`) call when the event yielded nothing (issue\n#253) → the queryless recent selector last). The store-side bridge restores\nthe #119 intent inside the #158 architecture: the hook body passes the\ndelegating prompt to the store subprocess it already makes\n(`--delegation-prompt`, argv surface bounded at 4000 chars so the store can\nredact before capping the parked bytes at 800, never a storelib import in\nthe adapter), the CLI parks it via `park_task_text`\n(secret-redacted through the canonical `redact_text`, advisory and\nfail-open; 16-entry cap, and a stale entry stops being CONSUMED after the\n6h window — that prune is in-memory only; issue #336 (0.124.0) added the\ndisk bounds: the TERMINAL session-end clear now unlinks the stash beside\nthe data dir it serves (the mid-session `ledger-clear` keeps it), an\nall-expired consume unlinks immediately, and the real bound on untrusted\nhost prose on disk remains the ops age sweep,\n`ZMEM_SENTINEL_SWEEP_DAYS`, default 7 days; `purge`\nscrubs the stash by needle and reaps orphaned `*.tasktext.tmp.*`;\n0600-at-open + atomic replace, with owner-only access on Windows resting\non the operator-profile ACL rather than chmod; since 0.138.0 written\nWITHOUT fsync (#340), so a host crash can lose the whole sidecar (the\nreader discards an unreadable stash) or re-deliver an already-consumed\ndelegation — disclosed, regenerable-cache posture), and the selector consumes it\nFIFO at the subagent moment only when the payload ladder produced no query.\nCorrelation key (issue #428, 0.148.0): a host that names the child's id in\nthe delegating call (`tool_input.agent_id`, forwarded by the hook body as\n`--delegation-agent-id`) parks a TAGGED entry, and the child's SubagentStart\n`agent_id` (forwarded as `--consume-agent-id` beside `--consume-tasktext`)\ntakes exactly its own tagged prompt regardless of age or newer siblings.\nIds are normalized identically on both sides (string, stripped, 1–128\nchars, no control characters, no leading `-`; anything else is treated as\nabsent). RESERVATION RULE: a consume without a matching id never takes,\nages out, or counts a TAGGED entry — the FIFO + aged-head rule below runs\nover UNTAGGED entries only. No probed host supplies an id at PARK time\ntoday (Claude Code assigns the child id at SubagentStart), so every\nproduction stash is untagged and the rule below is byte-identical to\n0.138.0; the id the child carries simply finds no tag and falls through.\nFor untagged entries FIFO is the\ncorrelation rule and out-of-order arrival of truly concurrent children is\nthe documented limitation; the consume lands before the selector runs\n(#337), so a selector failure after a successful consume loses that\ndelegation (disclosed residual); the accepted (unlocked, #330) races are the\nparallel-park last-writer-wins (jittered N=8 parks left 2-7 survivors;\nCORRECTED from \"drop-not-swap\" in 0.138.0), a parallel-consume overlap\n(two concurrent children can load the same head entry before either\nrewrite lands — on UNTAGGED entries both receive that same delegation\n(duplicated delivery — the text is served twice, not lost); on TAGGED\nentries each consume drops its own index and rewrites the whole file, so\nthe loser's stale rewrite resurrects the winner's already-consumed entry\n(last-writer-wins; a tagged entry stays inert until its own id arrives);\ndegraded relevance only, never a\ncrash), and the parked-against-consume overlap in BOTH directions: a fresh park\nloses its entry to a concurrent rewrite (unlink-wipe) or an\nalready-consumed entry is re-delivered (resurrection). An ORPHANED park\n— a delegation whose child never starts (permission denied, cancelled,\ntool errored) — would sit at the FIFO head and shift EVERY later child's\nprompt by one; since 0.138.0 the consume applies an AGED-HEAD SKIP\n(issue #329) over untagged entries: a leading entry older than 300 s is\ndropped in favor of a strictly newer one (an exact agent_id match still\noutranks the heuristic; malformed timestamps are never aged; a sole entry\ninside the 6h window is always served — the window prune still applies).\nTwo residuals stand for UNTAGGED entries (issue #329, maintainer-ratified\n2026-11-02): an orphan younger than 300 s still shifts by one until it\nages out, and a live child delayed past 300 s can lose its prompt to a\nstrictly newer delegation - a failure mode the heuristic introduces\n(pre-0.138.0 a delayed child consumed its own head entry). A host that\nsupplies the correlation id on both sides retires both residuals for its\ndelegations (issue #428); the 300 s band is unchanged. PR #453 review\nresiduals, disclosed: the 16-entry park cap evicts UNTAGGED entries first\n(a tagged entry is only evicted when the stash holds no untagged entry to\ndrop — an ALL-TAGGED stash over the cap still drops its oldest), and a\nTAGGED entry with a corrupted unparseable timestamp survives the 6h\nwindow prune (park always writes a finite timestamp; bounded by the cap,\nthe session-end unlink and the ops age sweep). The 6h window and\n16-entry cap bound how long any\nshift lasts, not how many children it affects. Since 0.138.0 the consume\nis CALLER-GATED (issue #339): only the SubagentStart hook lane\n(`--consume-tasktext` on its recall\u002Frecent argv) and the fleet broker's\nSubagentStart prefetch are INTENDED to pass the grant; the gate is\ndefault-off but CALLER-ASSERTED — any caller (including any\nauthenticated hub principal via the MCP prefetch param) may self-grant,\nso the boundary is convention, not enforcement (the stash is keyed by\nsession id only; trusted-fleet assumption). An ungranted `--moment\nsubagent` empty-query caller is byte-identical to a no-stash session. `ZMEM_QUERY_CONTEXT=0` or `ZMEM_INJECT=0`\ndisables parking via the early exit. A bridged child is a fresh context:\nits pull drops the delivered-derived exclusions entirely, so the parent's\npretool deliveries cannot starve it and a fresh child re-reads rows an\nearlier sibling already rendered (over-delivery of already-seen rows,\nnever starvation — deliberately NOT narrowed to sibling-moment rows,\nwhich would let one sibling starve the next; issue #331). Claude's matcher remains\n`Edit|Write|MultiEdit|NotebookEdit|Bash|apply_patch|Agent|Task` since 0.31.0 (apply_patch added later; #338) — issue\n#119; `Task` is accepted as the pre-rename delegation tool name per community\nissue 29677, closed stale and not vendor-confirmed. Claude Code also registers\nPostToolBatch (issue #120, 2026-09-11 — post-edit checkpoint recall, the\nbatch sibling of PreToolUse): after one completed batch, the hook parses\nONLY `tool_uses[].name` plus `input.command` \u002F `file_path` \u002F\n`notebook_path` \u002F `path` (plus the singular `tool_name`\u002F`tool_input`\ncompatibility shape), bounds every retained field to 150 chars and the\njoined query to 500, derives the existing operation tokens, and makes ONE\n`store.py recall --for-injection --no-bump` decision with the #117 ledger\nexclusion — raw `tool_response`\u002F`result` payloads are never parsed, stored,\nor logged (the decision log carries tool NAMES and path BASENAMES only).\nThe internal mode is `posttoolbatch`, but the runtime moment recorded in\nthe decision log and ledger is the closed-set `pretool` — no\n`posttoolbatch` moment is emitted anywhere. PostToolBatch is deliberately\nUNREGISTERED on Codex (no such upstream event exists in codex-rs hooks as\nof the 0.153.0 probe) and is a documented host gap on ZCode (see the\nseven-event note below); no lane reads `tool_response` — convention-capture\non PostToolUse parses the same tool_name\u002Ftool_input-shaped fields, not the\nresponse payload. Codex registers SessionStart,\nUserPromptSubmit,\nPreToolUse (matcher `Bash|apply_patch`, probe 2026-09-09, codex-cli\n0.153.0), PostToolUse (matcher `Edit|Write|MultiEdit|NotebookEdit|Bash|apply_patch`,\nper the shipped `hooks\u002Fhooks.codex.json` manifest), Stop, SubagentStart,\nSubagentStop, PreCompact, and SessionEnd —\nthe Codex DELEGATION tool surface is UNVERIFIED by #119 (2026-09-10): the\n#95 dump covered only the shell and patch\u002Fapply tools and cannot say\nwhether a delegation facility fires PreToolUse at all, so no matcher entry\nwas added and Codex SubagentStart rides the payload-field \u002F recent rungs\n(there is no transcript-tail rung — that parser is retired; a fresh\ndelegation-tool dump is deferred to the live-probe owner #96).\nUpstream drops `additionalContext` on PreCompact (decision control only,\nverified 2026-09-09 from codex-rs source), so zmem's Codex PreCompact\nentry exists to clear the delivery ledger before compaction;\npost-compaction re-injection rides\nthe registered SessionStart, which upstream fires with `source=compact`\nafter every compaction. PostCompact stays UNREGISTERED on Codex (issue\n#118 decision, 2026-09-10: upstream Codex PostCompact carries only\n`trigger: manual|auto` — no compact_summary, so there is nothing to\nstash). The compact branch (issue #158): SessionStart branches on `source ==\n\"compact\"` only — the launcher exports the payload field as\n`ZMEM_SESSION_SOURCE`, clears the delivery ledger at PreCompact, and then\ncalls the store-owned selector with moment `session_start` on the next\nSessionStart. The local decision log may retain\n`moment=session_start_compact` for diagnostics, but that label is never sent\nto storelib. There is no compact-summary sidecar or special second budget.\nThe payload block lives in\n`hooks\u002Flib\u002Fzmem-session-start-payload.py` — NEVER inline it back as\n`python -c`: the string outgrew the Windows ~32K CreateProcess\ncommand-line limit and silently degraded the hook to `{}`. Whether the PreCompact fence itself survives a live\n`\u002Fcompact` is UNPROBED — no claim either way until #96's live canary\nlands (the host-capability rot convention from #103\u002F#104). **ZCode supports exactly\nseven hook events — SessionStart, UserPromptSubmit, PreToolUse,\nPermissionRequest, PostToolUse, PostToolUseFailure, Stop — so SubagentStart,\nPreCompact, and PostCompact are host gaps on ZCode** (an unsupported event name would be\ndead config under the host's strict schema, so they are documented here\ninstead of registered; likewise ZCode's PreToolUse matcher deliberately\nomits `Agent` — with no SubagentStart event, event-provided task text has no\nconsumer). If ZCode grows either event, wire\n`zmem-subagent-recall.sh` \u002F `zmem-precompact.sh` \u002F `zmem-postcompact.sh`\nimmediately. PostToolBatch shares that ZCode gap (issue #120): until the\nhost grows the event, `zmem-posttoolbatch-recall.sh` stays\nClaude-registered only, and the launcher's translation mapping for the\nverb is inert on hosts whose manifest omits the entry; if ZCode grows the\nevent, wire `zmem-posttoolbatch-recall.sh` immediately (same convention).\n\n#### Decision-point checkpoints (REQUIRED skill contract) — #85 direction E\n\nWhen an agent is about to run one of the named hazardous operations, the\nskill\u002Fworkflow driving it MUST first run an explicit recall and treat any\nhit as blocking review (read the lesson before proceeding; do not skip it\nbecause the task feels urgent):\n\n- before `git stash pop` \u002F any stash-consume — `recall --query \"git stash\n  pop foreign stash conflict\" --include-global` (a blind pop can apply a\n  foreign stash)\n- before `git reset --soft` (squash assembly) — `recall --query \"git reset\n  soft origin main stale tree\" --include-global` (a fetch may have moved the\n  base)\n- before `git push` — `recall --query \"git push stale tree fetch rebase\n  verify\" --include-global` (verify the tree against the fetched base first)\n- before editing any file named by a stored citation\u002Fratchet lesson —\n  `recall --query \"\u003Cpath basename> ratchet citation re-pin\" --include-global`\n  (cited-file edits have local gate batteries)\n\nThese checkpoints complement the pre-tool hook; they are not a substitute\nfor it (an agent improvising a raw command is exactly who the hook covers).\n\n#### Schema forward-compatibility (issue #65 follow-up)\n\nA client refuses a store whose `schema_version` is above its ceiling.\n`FORWARD_COMPAT_SCHEMA_VERSION` (schema_meta) is that ceiling: a maintenance\nrelease of an older client line may raise it to an ADDITIVE-ONLY newer schema\n(new side tables, no `memory` changes), letting the older client keep storing\nand recalling memories — with a one-time stderr NOTICE — until it updates.\nAbove the ceiling the refusal names the update, and\n`ZMEM_ALLOW_NEWER_SCHEMA=1` overrides at the operator's own risk.\n\n#### Schema 16 — entity kinds v2 (issue #175, UPGRADE-REQUIRED)\n\nSchema v16 widens the `entity` kind vocabulary from five to exactly twelve\nkinds, enforced by a SQL CHECK: `person`, `project`, `tool`, `preference`,\n`host`, `service`, `model`, `repo`, `file`, `command`, `error_class`,\n`other` (schema_meta `ENTITY_KINDS`; the deterministic extractor admits each\nkind as a `kind:Name` tag prefix). The v15→v16 migration REBUILDS the\n`entity` table (create `entity_v16` with the new CHECK, copy every row, drop the old\ntable, recreate `entity` with the new CHECK, copy back) in one BEGIN\nIMMEDIATE transaction with `PRAGMA foreign_key_check` before commit; every\n`entity`, `entity_alias`, and `memory_entity` row\nsurvives unchanged. Since 0.136.0 every entity-kind CHECK is rendered from\nthe single `schema_meta.ENTITY_KINDS` authority (issue #356) — no DDL site\nhand-copies the vocabulary — and the schema lock gained holder-liveness\n(issue #357): the first open after an upgrade runs the rebuild under the\nlock (expect seconds, proportional to entity count; later commands wait up\nto `ZMEM_SCHEMA_LOCK_WAIT_SECONDS` then refuse with a clear message), while\na holder that DIED mid-migration has its lock broken automatically by PID\nliveness (SQLite rolls back the uncommitted transaction, so the store is\nnever half-migrated). Recovery path for a bad migration: stop sessions,\n`store.py restore` the newest good snapshot from `ZMEM_BACKUP_DIR`\n(default `\u003Cstore dir>\u002Fbackups`), re-run; `import-store.py` only replaces\nits destination after every acceptance gate passes. This is NOT an additive bump: a changed SQL constraint is\noutside the forward-compat window, so per the v16 contract\n`SUPPORTED_SCHEMA_VERSION` is 16 while `FORWARD_COMPAT_SCHEMA_VERSION`\ndeliberately stays 15 — every client older than the v16 release REFUSES a\nmigrated v16 store (the actionable refusal\nnames `ZMEM_ALLOW_NEWER_SCHEMA=1`, the explicit operator override that\nproceeds at the operator's own risk and never downgrades or migrates the\nstore). The #172 taxonomy decision for `entity_card` is REJECT: that memory\ntype does not exist and `ALLOWED_TYPES` is unchanged by this release.\nThe #172 decision was reject; the #179 workstream contract reverses it and\nships the type — cards are born only through the refresh path. Since 0.107.0\n`ALLOWED_TYPES` carries `entity_card` (the #175 kinds precedent: the RFC map\nis a frozen record, not a veto); see \"Entity cards (issue #179)\" below for\nthe full card contract.\n\nSchema 18 (issue #178) is purely additive (the `event` type plus the\n`memory_occurrence` side table — no `memory` column or constraint change),\nso the forward-compat window REOPENS: `SUPPORTED_SCHEMA_VERSION` is 18 and\n`FORWARD_COMPAT_SCHEMA_VERSION` is 18. Clients whose forward ceiling is\nstill 15 (every 0.10x-era install) refuse a migrated v18 store until\nupdated — on shared-store machines, update every zmem client before the\nfirst v18 client opens the store (`ZMEM_ALLOW_NEWER_SCHEMA=1` is the\nstopgap; see CUTOVER.md and the CHANGELOG operator note).\n\nSchema 19 (issue #288) adds the `applied_ops` op-queue table —\npurely additive, so SUPPORTED and FORWARD both moved to 19. The compat\ngate refuses any store newer than the client's FORWARD ceiling: a\n0.116-era client (FORWARD 18) REFUSES a v19 store with the standard\n`ZMEM_ALLOW_NEWER_SCHEMA=1` message — update all zmem clients on\nshared-store machines before the first v19 client opens the store\n(a FORWARD-15 client refuses it the same way).\nNUMBERING: #179's `entity_card` bump owns schema 17 as a STAMP-ONLY step\n(no DDL); this PR's `memory_occurrence` DDL therefore renumbered to 18 at\nrebase — one integer can never carry two schema definitions, or the\n`ver \u003C N` guard would skip the DDL on every store a released client had\nalready stamped.\n\n#### Injection token budget (issues #65 10.9, #116)\n\n`ZMEM_INJECT_TOKEN_BUDGET` (default 1500, measured at 4 chars\u002Ftoken — no\ntokenizer is bundled) is a HARD CEILING on the memories injected by the hooks\n(UserPromptSubmit \u002F PreCompact \u002F SubagentStart \u002F SessionStart Tier 2) and by\nthe `session_start` MCP\u002FHermes tools. Admission charges each row its FULL\nrendered fence contribution plus the fence shell, with the same estimator the\nreporting uses — the rendered fence never exceeds the budget. When the\nceiling is hit, admission skips past rows that do not fit (a later smaller\nrow is still admitted); `decision`\u002F`constraint` rows are never dropped while\nthey can carry information: a row that fits renders whole, one that does not\nis TRUNCATED with an explicit `…[budget-truncated]` marker, and only a row\ntoo large for even a minimal stub is dropped — never silently. When anything\nwas omitted, the fence carries a machine-readable line\n`# [budget: dropped N rows, truncated M]` and the shared hook body's\ndecision line (UserPromptSubmit \u002F PreCompact \u002F SubagentStart \u002F SessionStart\ntier-2 python path) reports `tokens=\u003Crendered>\u002F\u003Cbudget>` plus the distinct\nlabeled fields `rendered_estimate=`, `admission_budget=`, `budget_dropped=`,\n`budget_truncated=`, `budget_dropped_protected=` (the same order as the\nfield-order list above; emitted only when budget accounting ran). The\nseparate `zmem-session-start.sh` bash writer keeps its legacy\n`tokens=\u003Cused>\u002F\u003Cbudget>`-only line. Every read `--json` envelope reports\n`tokens_used`\u002F`tokens_budget` and, on the `--for-injection` lane,\n`budget_dropped`\u002F`budget_admission`\u002F`budget_truncated`\u002F\n`budget_dropped_protected`\u002F`budget_note`. The B-1 report (`doctor --miss-rate`) counts\n`over-budget N` decisions so the ceiling can be verified on a live log\nwindow. `ZMEM_CTX_BUDGET` (character cap) remains the hard outer truncation\non the rendered block.\n\n`--include-global` (opt-in) ALSO surfaces up to `--global-limit` query-relevant\nrows from the `user:global` tier, merged project-first so a global row never\ncrowds out a project row. The three automatic hooks pass this so cross-project\nlessons reach project-scoped sessions. Without it, behaviour is strict-namespaced\n(byte-identical to before). When you want the global tier unioned in but still\nwant a per-tier budget, use `recall --namespace project:\u003Cx> --include-global`\nrather than going unscoped.\n\n#### Scoped five-tier recall (issue #167)\n\nOrdinary implicit `recall` and `recent` calls without `--namespace` resolve the\ncurrent project, fleet, and host through the shared #166 scope resolver and use\nfive independent reservations in this order; hostnames are normalized to\nlowercase for `host:` scope lookup:\n`project`, `domain`, `fleet_host`, `cross_project`, `user_global`. The default\nslot caps are `5\u002F2\u002F2\u002F2\u002F3`. Library callers opt in explicitly with a `scopes=` map\non `recall_memory`, `recent_memory`, or the read-only `explain_recall` API; the\nresolver's `agent` value is ignored. `user_global` is admitted only when\n`include_global=True`.\n\n`ZMEM_TIER_SLOTS` overrides the caps per call as exactly five comma-separated\nASCII nonnegative integers in that order. Empty, malformed, signed, or Unicode\nnumerals fail before the scoped pipeline opens a SQL lane. The scoped\n`cross_project` slots remain reserved until a public admission policy is\navailable. While no policy is wired, recall and recent skip the unfiltered\ncandidate scan. This is separate from the legacy #98\n`--include-cross-project` hazard lane. Scoped rows are stable-deduplicated by id\nand carry their tier in JSON; generic fenced output prefixes `[tier=\u003Cname>]`,\nand a tierless generic row is explicitly prefixed `[tier=unknown]`. Ordinary\nplain-text recall\u002Frecent output includes that marker too. Legacy passive\ninjection retains its established tierless wire bytes. The legacy\n`tier=cross` renderer marker remains a suffix. Explicit `--namespace`, search,\nhook, and injection calls retain their legacy route and limit arguments\n(issue #235 adds only the gated-moment `user_global` relevance floor on top:\nbelow-floor global candidates are withheld before the merge, the slot stays\nempty, and the envelope reports the count as `global_withheld`).\nOn implicit ordinary recall and recent, `--include-global` opts into the\nscoped `user_global` reservation and preserves tier labels. Explicit\n`--namespace` calls retain their legacy union behavior; `--include-cross-project`\nretains its legacy hazard semantics. Direct scoped\n`recall_memory`\u002F`recent_memory` calls reject `for_injection=True`; scoped explain\nremains read-only and accepts it. Programmatic callers use `scopes=` and\n`include_global=True` for the same labeled global tier.\nThe `domain` reservation is available to programmatic callers that supply a\ndomain scope; shipped CLI, hook, and MCP resolvers do not currently create one.\nScoped recall\u002Frecent reject the legacy `include_cross_project=True` flag.\n\n#### Cross-project hazard tier (issue #98)\n\nA fourth, precision-gated tier (`--include-cross-project`, wired automatically\non the passive injection surface) can deliver up to 2 live rows from FOREIGN\n`project:*` namespaces — a lesson another project already paid for, surfaced\nexactly when you are about to repeat its incident. Admission requires ALL of:\n\n- the running operation is hazardous: the derived ops tokens\n  (`derive_ops_tokens`, the `#88`\u002F`#123` allowlist) whole-token-intersect the\n  hazard-verb set — `ops_tokens._HAZARDOUS_SUBS` (`git push\u002Freset\u002Fstash pop\u002F\n  rebase\u002F...`) by default, overridable via `ZMEM_CROSS_PROJECT_HAZARD_VERBS`\n  (comma-separated, trimmed, case-folded, de-duplicated; unknown or empty\n  verbs are dropped with a one-shot stderr warning and an override with no\n  usable verb falls back to the default set);\n- the row's `signal` is grounded: one of `test`, `compile`, `lint`,\n  `reviewer`;\n- the row passes the standard score\u002Fconfidence floor (#113) and is live\n  (`superseded_at IS NULL`);\n- the row's namespace matches `project:*` and is OUTSIDE the current\n  project's alias set (`user:global` rows stay in their own tier).\n\nSurface policy (`ZMEM_CROSS_PROJECT`): unset → `pretool` only (PostToolBatch\nmaps to the `pretool` moment); `0` → off everywhere (wins even over an\nexplicit `--include-cross-project`); `1` → `pretool` and `user_prompt` (on an\nenv-enabled `user_prompt` surface the store-side selector derives ops tokens\nfrom the prompt event itself — the #158 hook boundary keeps the hook a thin\nflag forwarder); any other non-empty value → `pretool` only plus a one-shot\nstderr warning. The tier is query-time — the queryless `recent` pull never\nadmits cross rows.\nScoped MCP principals keep their `ops_tokens`; the selector's cross-project\nlane intersects its foreign namespace set with the principal's authorized\nallow-list (issue #283), so foreign projects outside the allow-list are\nexcluded before rendering, context construction, or delivery-ledger\nupdates, while allow-listed cross-project rows can be admitted when\noperation tokens ground them.\n\nNo-copy rule: a cross row is never copied, rewritten, or mirrored — the\nstore's own row renders in place, inside the untrusted fence, tagged\n`[ns=\u003Csource namespace>] [tier=cross]` (with `tier: \"cross\"` on the JSON\nrow), and cross rows never consume project or global slots. Delivered cross\nrows count once in `surfaced_count` under the same telemetry law as every\nother tier. The #155 real-corpus replay baseline has landed: the lane ships\nwith these conservative defaults, and the predeclared measurement record\n`eval\u002Freal-corpus-2026-09-19.json` (see the read-only replay evaluator section)\nis the calibration reference — an audit over a frozen cohort, not a live\nefficacy claim. `recall --explain`\ndoes not include the cross tier (the read-only debugger predates it and is\nnot extended by #98).\n\n**Hybrid is the DEFAULT when embeddings are available** (issue #58 3.3): the\nquery is embedded and matched against stored embeddings (sqlite-vec KNN),\nthen both lanes' rankings are fused with Reciprocal Rank Fusion (RRF, k=60).\n`--hybrid` is kept as an explicit no-op alias for older invocations;\n`--no-hybrid` forces the lexical (FTS5\u002FBM25-only) lane. The embedding\nruntime (onnxruntime + tokenizers, model lazy-downloaded and\nchecksum-verified) is optional and fails open: when it is unavailable,\nrecall silently uses plain keyword ranking — never an error.\n\n**Entity matching is the THIRD RRF list and needs no model** (v10, issue #60\n5.3): at recall the query is run through the same deterministic entity\nextractor used on write, plus plain query tokens are matched against stored\nentity aliases. Memories linked to a matched entity join the fusion as a\nthird ranked list (rank = number of matched entities, then recency). This is\nhow the alias `rg` recalls `ripgrep`-linked memories even when BM25 terms\nmiss — and it is live on model-absent stores by default. An unknown alias is\nsimply an empty third list; the lexical (and, when available, vector) lanes\nfuse exactly as before.\n\n**MMR diversity is the DEFAULT** (v10, issue #60 5.5): after RRF fusion and\ncomposite scoring, the candidate set is re-ordered with Maximal Marginal\nRelevance before `--limit` is applied, so a cluster of near-paraphrase\nduplicates cannot crowd out a distinct fact. Similarity is embedding cosine\nwhen both rows have embeddings, else Jaccard on normalized content tokens —\nso MMR also works model-absent. `--no-mmr` returns pure composite-score\norder (four paraphrases instead of three + the distinct fact). The tradeoff\nknob is lambda: default 0.7, env `ZMEM_MMR_LAMBDA` (0.0 = maximize\n\n**Embedding profiles & rerank** (`ZMEM_EMBED_PROFILE`, `ZMEM_CROSS_ENCODER`,\n`ZMEM_CROSS_ENCODER_MODEL`): `ZMEM_EMBED_PROFILE` selects a row from the\nregistry in `skills\u002Fmemory\u002Fscripts\u002Fembed_profiles.py` — unknown values refuse\nwith exit 2 before any store work, and a value whose dimension differs from\nthe store's committed vectors refuses until `reembed --all` converts it.\nThe cross-encoder rerank is enabled by default (issue #126 flip); set `ZMEM_CROSS_ENCODER=0` to disable it. The\nEXPLICIT lane covers CLI `recall` invocations that are not `--no-bump`\u002F\n`--no-hybrid` (never `recent` \u002F `search`-aliases); pointing\n`ZMEM_CROSS_ENCODER_MODEL` at a LOCAL pair-scoring `.onnx` plus sibling\n`tokenizer.json` keeps the operator-vouched load path. Issue #125 adds a\nchecked-in `mini-pair-scorer` profile\n(`skills\u002Fmemory\u002Fscripts\u002Fcross_encoder_profiles.py`, exact SHA-256 pins for\nBOTH the model and its tokenizer) used\nwhen `ZMEM_CROSS_ENCODER_MODEL` is unset — resolved from `ZMEM_MODELS_DIR`\nor the shared models dir, and loaded ONLY after the digest of the exact\nBUFFER each runtime object is constructed from matches its profile pin at\nscorer BUILD time (#418; there is no verify-then-reopen window — the\npath-level `verify_profile_file` helper is a diagnostic API, no longer the\nload gate); `ZMEM_CROSS_ENCODER_MODEL_URL` plus exact\n`ZMEM_MODEL_AUTODOWNLOAD=1` permits one digest-verified, atomic-download\nattempt at scorer-load time. A second opt-in, `ZMEM_CROSS_ENCODER_PASSIVE=1`,\nlets the PASSIVE injection lane score its final admitted set through\n`rerank_final_injection_set` (issue #125) in shadow-only mode:\n`ZMEM_CROSS_ENCODER_SHADOW=1` logs bounded rank deltas to\n`${ZMEM_DATA}\u002Fcross-encoder-shadow.jsonl` and the lane's order is unchanged\nuntil the recorded promotion gate (`PASSIVE_PROMOTION_GATE`:\n#111 precision > 0.8978333333333333, p95 \u003C= 250 ms, #129 false-injection \u003C= 0.0) is\nmeasured and flipped. Every rerank attempt is budgeted (250 ms default,\n`ZMEM_CROSS_ENCODER_BUDGET_MS`) and emits exactly one terminal\n`[zmem] cross-encoder reason=\u003C...>` line on stderr; any failure degrades to\nun-reranked results and rerank can never fail a recall. There is still NO\nunverified-load escape hatch (`ZMEM_MODEL_ALLOW_UNVERIFIED` does not\nexist).\ndiversity, 1.0 = no diversity — identical ordering to `--no-mmr`).\n`--no-mmr` and `--no-hybrid` are independent flags and can be combined.\n\nRecall rows carry **entity cards**: `recall --json` rows include\n`entities: [{id, kind, name}]`, and the fenced hook render shows at most\nthree entity NAMES per row (never ids). `get --id` shows the same links.\n\n**1-hop link expansion is the DEFAULT** (v11, issue #61 6.3): after MMR,\neach recalled memory's `related`\u002F`supports` links are walked one hop and\nup to `--link-budget` (default 2) extra neighbor rows are appended — a\nmemory that never matched the query but is associative-neighbor of a\nmatch still surfaces. `contradicts` neighbors are included only when\nthey survive the confidence floor and are tagged `[CONTESTED LINK: \u003Cid>]`\nin the fenced render — the id names the opposing row, the parent the\nneighbour was expanded from (`contested_link: true` + `link_of` in JSON;\nissue #254). `contradicts` edges are stored in both directions, so the\nnamed id is one end of the contradiction, not a fixed direction; when\nthe id is not a single safe token the bare `[CONTESTED LINK]` renders\ninstead, and a named row that a floor or the token budget dropped from\nthe fence can appear in a marker without its own bullet. Expansion rows\ncarry\n`link_relation` \u002F `link_of` \u002F `link_score` \u002F `contested_link` keys; rows\nthat matched the query themselves never carry them, and expansion rows\nnever advance telemetry (popularity rewards query matches, not link\nneighbors — so expansion cannot distort later rankings). On the passive\ninjection lane an already-delivered parent contributes no neighbours\n(`exclude_ids` seed skip, issue #250); the plain recall path is unaffected\nbecause its pre-filter has already removed those rows. `--link-hops 0`\ndisables the walk; `--link-budget 0` is equivalent. `search` and `recent`\nnever expand.\n\n`--no-bump` (opt-in) makes a recall **passive**: it records a surface event\n(`surfaced_count`\u002F`last_surfaced`) instead of advancing `retrieval_count`\u002F\n`last_retrieved`. The explicit-vs-passive split is the system contract: the\nthree automatic hooks and the Hermes provider prefetch are passive surfaces\n(a background injection is not a usefulness signal); explicit recall — this\nCLI without `--no-bump`, the MCP `recall`\u002F`search` tools, and the Hermes\n`MemoryProvider` search tool — intentionally bumps `retrieval_count`, because\nan explicit read IS evidence the memory was useful (issue #21).\n\nIssue #114 sharpened that contract in two ways. First, the automatic hooks and\nSessionStart now pass `--for-injection` (with `--no-bump`): the selective\ninject gate and the token budget run INSIDE that one store call, the returned\nrows are exactly the rendered set, and `surfaced_count` advances only for\nrendered QUERY-MATCHED rows (link neighbors render but never count; unfold\nis explicit-recall-only and never runs on this lane) —\none subprocess, one decision, no second ack process. Second, ranking\npopularity now reads `retrieval_count` ONLY: passive surfaces are still\nrecorded (promote\u002Fprune\u002Fconsolidate consume them) but they no longer feed the\ncomposite score, so passive pulls cannot inflate their own ranking (the\nlive-store shape `surfaced_count=371, retrieval_count=0` was this loop). The\nweight itself is unchanged; #124 will repoint it at applied\u002Fviolated counters.\n\n#### Store-owned passive selector and envelope — issue #158\n\nAll passive consumers use the single storelib entry point\n`select_and_budget_for_injection`. It owns selection, delivery-ledger\nexclusion and recording, pre-tool operation-token composition, the hard token\nbudget, and the canonical untrusted fenced rendering. The recall hooks,\nSessionStart, and Hermes provider are subprocess-only adapters: they consume\nonly the envelope's string `rendered` field and never read SQLite, the\noperation ring, a delivery ledger, or memory rows themselves. The selector's\nclosed moments are `session_start`, `user_prompt`, `pretool`, `subagent`, and\n`precompact`; its lanes are `claude`, `codex`, `zcode`, `deepseek`, `hermes-provider`,\nand\n`hermes-compat`. A compact restart is logged locally as\n`session_start_compact`, but the selector and ledger always use\n`session_start`.\n\nThe exact session-aware CLI forms are:\n\n```\n\u003Cinterpreter> \u003Cstore.py> recall --query \"\u003Ctext>\" --for-injection --json \\\n  --session-id=\u003Cid> --moment \u003Csession_start|user_prompt|pretool|subagent|precompact> \\\n  --lane \u003Cclaude|codex|zcode|hermes-provider|hermes-compat> \\\n  [--ops-token \u003Ctoken>]... [--authorized-namespace \u003Cns>]...\n\u003Cinterpreter> \u003Cstore.py> recent --for-injection --json \\\n  --session-id=\u003Cid> --moment \u003Csession_start|user_prompt|pretool|subagent|precompact> \\\n  --lane \u003Cclaude|codex|zcode|hermes-provider|hermes-compat> \\\n  [--ops-token \u003Ctoken>]... [--authorized-namespace \u003Cns>]...\n\u003Cinterpreter> \u003Cstore.py> ledger-clear --session-id=\u003Cid> [--principal \u003Cid>] [--harness \u003Cid>]\n\u003Cinterpreter> \u003Cstore.py> delivery-clear --session-id=\u003Cid> [--principal \u003Cid>] [--harness \u003Cid>]\n\u003Cinterpreter> \u003Cstore.py> closeout-ack --session-id=\u003Cid>\n```\n\nThe passive `recall` and `recent` commands accept the additive attribution\nflags `--session-id=\u003Cid>`, `--moment`, `--lane`, and repeatable `--ops-token` when\ncalled with `--for-injection --json`, plus the payload flag\n`--delegation-prompt` (issue #253), `--consume-tasktext` (issue #339:\nSubagentStart-lane-only grant of the store-side task-text consume), the\nissue #428 correlation ids `--delegation-agent-id` (tags the park) and\n`--consume-agent-id` (also on `prefetch`; selects the tagged entry) — both\nrefused by name in remote mode (`ZMEM_REMOTE_URL` set), as is recall's\n`--state-hint`, because the hub read tools carry none of them and the fleet\nlane never parks — and\nrepeatable `--authorized-namespace`\n(issue #283: the caller's namespace allow-list — the selector's\n`user:global` and cross-project lanes are filtered to it; omitted means\nunscoped). On the pretool passive lane, recall additionally accepts\n`--state-hint dirty|clean` (issue #397): it REQUIRES the passive shape\n(`--for-injection --json --session-id`) and `--moment pretool` on recall —\neither missing is a stable exit-2 refusal — and is refused on any moment\nother than pretool; the hook is the intended caller. `--delegation-prompt` is honored ONLY on\n`--moment pretool` with a session id, is secret-redacted before it touches the\nsidecar, and exists to park the delegating Agent\u002FTask prompt for the subagent\nmoment; every other moment ignores it. An empty query dispatches to recent\nselection. An omitted `--ops-token` list lets the store read the pre-tool ring;\nthe ring is composed only for `pretool`, not for UserPromptSubmit or other\nmoments. For passive `recall` and `recent`, a session id requires a moment, and\na moment requires a session id. The equals form keeps session ids beginning\nwith `-` unambiguous. `ledger-clear --session-id=\u003Cid>` clears one\nsession's delivery ledger without opening SQLite. `delivery-clear\n--session-id=\u003Cid>` clears the ledger and legacy pending sidecar for SessionEnd\ncleanup, also without opening SQLite; both commands are idempotent when their\ntarget sidecars are absent.\n\nThe old hook-owned pending and compact-summary sidecars are no longer\nproduced or consumed by these adapters (task-text parking returned in 0.98.0\nas a STORE-side bridge — issue #253: the pretool store call parks via\n`park_task_text`, the selector consumes at the subagent moment; the hook\nadapters themselves still never touch any sidecar). The D-2 #118\ncompaction-snapshot handoff is documented above. This reflects the ownership\nhistory: issue #117 superseded the pending sidecar\nwith the per-session delivery ledger, and #158 completes that retirement for the passive adapters.\n`ZMEM_PENDING_SIDECAR=1`\nremains only as a documented no-op for operators upgrading from the\ncompatibility path. Precompact clears the ledger;\nthe next SessionStart performs ordinary `session_start` selection. The MCP\nserver's passive surface (`prefetch`, the reworked `session_start` below, and\nthe Hermes twins) now rides the same store-owned selector. This ownership move is schema-free: neither schema-version\nconstant changes.\n\n#### Query-aware passive prefetch — issue #159\n\n`prefetch` is the query-aware passive lane: one\n`select_and_budget_for_injection` call and one complete JSON envelope — JSON\nis the command's only output mode, and there is no consumer-side renderer or\nsecond budget run.\n\n```\n\u003Cinterpreter> \u003Cstore.py> prefetch --query \"\u003Ctext>\" --namespace \u003Cns> \\\n  --session-id \u003Cid> --moment \u003Csession_start|user_prompt|pretool|subagent|precompact> \\\n  [--lane \u003Cclaude|codex|zcode|hermes-provider|hermes-compat>] \\\n  [--ops-token \u003Ctoken>]... [--exclude \u003Cmemory-id>]... \\\n  [--authorized-namespace \u003Cns>]... [--reserve] \\\n  [--principal \u003Cid>] [--harness \u003Cid>] [--consume-tasktext]\n  [--consume-agent-id \u003Cid>]\n\u003Cinterpreter> \u003Cstore.py> prefetch-ack --reservation \u003Cid>\n```\n\n`--query`, `--namespace`, `--session-id`, and `--moment` are required;\n`--exclude` (memory ids the caller already holds) is repeatable.\n`--for-injection`, `--no-bump`, and `--json` are accepted as explicit markers\nof the (only) passive mode the selector pins. The selector owns the\nrelevance\u002Ftrust gate and the 1,500-token budget; prefetch never advances\n`retrieval_count` (only `surfaced_count` and the delivery ledger may move).\n`--reserve` (issue #284, T-05) returns the selection with a `reservation`\nid and records nothing in the delivery ledger — commit with\n`prefetch-ack --reservation \u003Cid>` (exactly-once; duplicate ack is a\nno-op within the reservation TTL — default 120 s via\n`ZMEM_RESERVATION_TTL_S`; an expired reservation commits nothing and an\nunknown or malformed id exits 2);\n`--principal`\u002F`--harness` key the delivery ledger as\n`principal|harness|session` (omitted keeps the legacy session-only key).\nDelivery is session-attributed: a second turn for the same session whose\nledger already holds the candidate rows returns the silent\n`already-delivered` envelope; `ledger-clear --session-id` resets the ledger,\nwhile SessionEnd uses `delivery-clear --session-id` to retire both sidecars.\n\nThe MCP server exposes the same one-call envelope as the `prefetch` tool\n(`query`, `namespace`, `session_id`, `moment` required; optional `lane` —\nvalidated against the five-value tuple, never defaulted to a host lane —,\n`ops_tokens`, and (issue #339) the caller-asserted `consume_tasktext`), enforcing namespace scope and the `ZMEM_INJECT=0` kill switch\nbefore any store subprocess, and returning the complete selector envelope\nplus the additive `context` alias equal to `rendered`. Scoped principals keep\ntheir `ops_tokens`: the selector's cross-project lane intersects its foreign\nnamespace set with the principal's authorized allow-list (issue #283), so\nforeign projects outside the allow-list are never admitted before rendering\nor ledger updates, while allow-listed cross-project rows can be admitted\nwhen operation tokens ground them. The MCP\n`session_start` tool rides the same store-owned queryless selector path via\n`recent --for-injection --json --session-id \u003Cid> --moment session_start`,\nreturning that envelope (additive `context` alias; `result`\u002F`namespace`\u002F\n`ids` retained as back-compat aliases); an omitted `session_id` falls back\nto the legacy for-injection envelope without the ledger key.\n\n### add — capture a memory\n```\n\u003Cinterpreter> \u003Cstore.py> add \\\n  --namespace \"project:\u003Cbasename>\" \\\n  --type \u003Cfact|lesson|convention|preference|decision|constraint|state> \\\n  --content \"\u003Cthe knowledge, specific and actionable>\" \\\n  --tags \"comma,separated\" \\\n  --signal \u003Ctest|compile|lint|reviewer|user|none> \\\n  [--taint \u003Ctrusted_internal|untrusted_tool|untrusted_web>] \\\n  [--source-ref \"file:\u003Cpath>\" | \"session:\u003Cid>\" | \"db:\u003Ctable>:\u003Crowid>\"] \\\n  [--expiry-class \u003Cendpoint|setting|metric>] \\\n  [--inject-until \u003CUTC ISO8601, e.g. 2026-11-12T09:00:00Z>]\n```\n\n**`state` and injection expiry (v15, issue #173).** A `state` row records a\npiece of the operating environment — an endpoint URL, a setting, a metric —\nthat is only worth injecting while it is CURRENT, unlike a lesson or fact that\nstays true. `--type state` requires `--expiry-class`, one of:\n\n| class | default TTL | env override |\n|---|---|---|\n| `endpoint` | 30 days | `ZMEM_STATE_ENDPOINT_DAYS` |\n| `setting` | 90 days | `ZMEM_STATE_SETTING_DAYS` |\n| `metric`  | 14 days | `ZMEM_STATE_METRIC_DAYS` |\n\nThe override must be a positive decimal integer. With no `--inject-until`, the\nexpiry is computed as `now + TTL` and stored in the additive `memory_expiry`\nside table (`memory_id`, `inject_until`, `expiry_class`); an explicit\n`--inject-until` must be UTC ISO-8601 (`...Z` or `+00:00`, normalized to `Z`)\nand bypasses the TTL. The side table is INJECTION-ONLY: it records when a row\nshould stop being injected, it never deletes the row or hides it from explicit\n`recall`\u002F`search`. Passing either flag with a non-state type, or omitting\n`--expiry-class` on a state add, is refused exit 2. `update` carries the same\ntwo flags and the same rules, and (issue #317) resolves the TARGET's\neffective type at action time: `--type` may be omitted on an update that\ncarries the expiry flags — a state target accepts them, and a genuinely\nnon-state target is refused by the writer with the same exit code.\n\nSince 0.98.0 (issue #174) the recorded expiry is ENFORCED on read: passive\ninjection (`recall --for-injection`, `recent --for-injection`, the Hermes\nprefetch lane) never delivers a row whose `inject_until \u003C= now` — the\ncomparison is by parsed true instant (fractional seconds order by their real\ntime), inclusive at the boundary. Expired rows never reach results, budget\naccounting, or surfaced\u002Fretrieval telemetry; on an engaged lane the envelope\nreports the pre-expiry candidate pool — `candidate_ids` (arrival order) and\n`candidate_lanes` over the SAME population — plus an `expired` count over\nthat population, and a fully-expired-but-eligible pool names the `expired`\nsilent reason. Since 0.119.0 (issues #342\u002F#343\u002F#349) the gate also fails\nCLOSED on unknown expiry state: a side-table read failure (lock\u002Finterrupt\nwith the table present) withholds the un-read candidates, keys\n`expiry_read_failed: \u003Ccount>` into the envelope (registered in\n`INJECTION_ENVELOPE_OPTIONAL`; mirrored in the Hermes fallback set; keyed\nonly when nonzero — a store without the failure keeps byte-identical wire),\nand prints one `[zmem]` stderr line; belief heads resolve their deadline\nthrough their source rows' earliest `inject_until` (a head whose sources\nare all live still delivers; an all-sources-expired head is withheld and\ncounted). `doctor --expiring` keys `expiry_read_error` on a failed\nside-table read (the scan is per-leg guarded, so a successful first read is\nnever discarded) and names unparseable deadlines in\n`unreadable_expiry_states` (memory_id, raw value, namespace, preview;\nkeyed only when nonempty; both keys scoped to `--expiring` invocations).\nA pool whose every candidate was withheld by a read failure reports\n`expiry_read_failed` carrying the explanation; the `reason` label follows\nthe pre-existing classifier (a never-engaged gate names `empty-pool`, an\nengaged drained gate may name `expired` — the read-failure count is the\nauthoritative disambiguator). Present-time explicit `recall`\u002F`search` return expired rows\nmarked `[EXPIRED] ` with `expired: true` instead of filtering; `--as-of`\nneither filters nor marks. `--for-injection` + `--as-of` is refused exit 2\nbefore the store opens. Two read-only operators complete the surface:\n`doctor --expiring 7d` (rows with `now \u003C inject_until \u003C= now + N days`,\nparsed-instant ordered) and `store.py classify-state --dry-run [--json]`\n(deterministic endpoint\u002Fmetric\u002Fsetting proposal; read-only URI +\n`PRAGMA query_only=ON`, never a writer lease).\n\n**Fake-clock seam.** Reads honor `ZMEM_TEST_NOW` (the scoring clock\n`_now_epoch`): the expiry gate, the `[EXPIRED]` markers, and\n`doctor --expiring` are all deterministic under it. WRITES never do — the\nwriter clock is `schema.now_iso()` alone (issue #173's contract), so a pinned\n`ZMEM_TEST_NOW` can only move read-time judgments, never a stored timestamp.\nSignal sets default confidence: test\u002Fcompile=0.9, lint=0.85, reviewer\u002Fuser=0.6\n(medium), none=0.2 — deliberately BELOW the 0.25 recall floor: an ungrounded\nlesson is the agent's self-opinion and never surfaces in default recall (still\nreachable via `search --text`, which applies no confidence floor, or\n`recent --min-confidence 0`) (#36 M3); for a verbatim probe of a stop-word-only\nstring, `search --exact` reaches it literally.\nDedup-on-write: near-identical live content in the same namespace refreshes the\nexisting entry instead of duplicating.\nDedup has a polarity guard (v11, issue #61 6.2): a near-identical hit whose\nnegation polarity DISAGREES (\"always X\" vs \"never X\") is NOT merged — both\nrows stay live, link as `contradicts`, and each loses 0.10 trust. A\npolarity-AGREEING duplicate re-add is corroboration: the existing entry\nrefreshes as before AND gains +0.05 trust.\nDedup also has a subject guard (issue #376): two rows whose `task:\u003Ckey>` tags\ncarry explicitly DIFFERENT task subjects are never merged by similarity\nALONE — each statement, its tags, source and evidence stay independently\naddressable on `add`, `update`, ingest and consolidation, the veto is logged\nwith the stable code `subject_veto`, and a subject-compatible runner-up can\nstill absorb the fold. Absent\u002Fmalformed identity is compatible (unknown\nsubjects dedupe as before), two rows BOTH typed `lesson` generalize across\ntask origins (a single lesson row does not opt itself out of the guard), and\nsubject-incompatible neighbors no longer receive each other's tags through\nlink propagation. An admitted merge unions the survivor's task identities\n(bridge growth): a row that legitimately absorbed across compatible subjects\ncan later merge with either subject.\nLink generation on write (v11, issue #61 6.2): after every `add`\u002F`update`,\nthe same namespace-aware neighbors dedup already computed (embedding cosine\nwhen the model is present, else Jaccard token overlap — model-absent stores\nlink too) above `ZMEM_LINK_THRESHOLD` (default 0.75) become `related` edges\n(stored both directions). Each linked neighbor also absorbs the new row's\nTAGS and re-derives its entity links (attribute evolution) — content,\nconfidence, signal, and retrieval_count are never touched. Deterministic;\nno LLM (a `ZMEM_LINK_LLM` knob deliberately does not exist).\n`add --json` (v13) prints a structured write result — `{\"id\", \"result\":\n\"stored\"|\"deduped\", \"warnings\"}` — with warnings as structured objects\n(`{\"type\": \"redacted\", \"count\": N}` for automatic secret redactions); the\nMCP and Hermes add surfaces use it. Default output is unchanged.\nNamespace validation: obvious misspellings of the global namespace (`global`,\n`userglobal`, `users:global`, …) are rejected at `add` time AND on `ingest-jsonl`\nsync import with a message naming the canonical `user:global` — such rows would\nbe unreachable from the automatic hooks. New writes accept `project:\u003Cx>` and\n`user:\u003Cx>` (including later colons in their values), plus strict\n`fleet:\u003Cx>`, `host:\u003Cx>`, `agent:\u003Cx>`, and `domain:\u003Cx>` forms. The latter four\nrequire one non-empty, whitespace-free, colon-free value. `ingest-jsonl`\nretains its legacy near-miss-only validation until the explicit rekey work.\nLegacy rows already stranded under a near-miss namespace\n(before this guard existed) can be remediated with `rekey-namespace\n--near-miss-global --confirm` (see below).\n\n### update \u002F invalidate — append-only revision and contradiction corrections (v9, issue #59)\n```\n\u003Cinterpreter> \u003Cstore.py> update --id \u003Cfull-uuid> --content \"\u003Cnew content>\" \\\n  [--namespace NS] [--type ...] [--tags ...] [--source-ref ...] [--confidence F]\n  [--signal ...] [--taint ...] [--capture-mode manual|reviewed|auto]\n  [--expiry-class \u003Cendpoint|setting|metric>] [--inject-until \u003CUTC ISO8601>]\n\u003Cinterpreter> \u003Cstore.py> invalidate --id \u003Cfull-uuid> --reason \"\u003Cwhy the fact is no longer true>\"\n```\n- **`update`** is append-only knowledge revision: it creates a NEW live row with the\n  new content, tombstones the target row (`superseded_at=now, valid_until=now,\n  supersede_reason='updated'`), and links the new row back to it via `update_of`.\n  Namespace\u002Ftype\u002Ftags\u002Fsource_ref\u002Fconfidence\u002Fsignal inherit from the target unless\n  overridden; the old row's content is NEVER mutated. An unknown or\n  already-superseded id is refused (exit 2, nothing written). Dedup runs against\n  OTHER live rows (the replaced row is excluded, so even an unchanged-content\n  \"update\" creates history). Point-in-time recall (`--as-of` before the update)\n  returns the OLD content; after returns the NEW.\n- **`invalidate`** is `supersede` with a REQUIRED `--reason` — the preferred way to\n  record \"this fact is no longer true\" so the correction is auditable (issue #59,\n  4.3). `supersede` remains for general tombstones (consolidated\u002Fpruned rows)\n  where a reason is optional. The reason must be NON-EMPTY (whitespace-only is\n  refused, exit 2), and tombstones are write-once: a second `invalidate`\u002F\n  `supersede` on an already-tombstoned row is refused (exit 2) — re-tombstoning\n  would move `valid_until` forward and falsify point-in-time history.\n- **Large content**: `--content -` (the literal dash) reads the content from\n  stdin on both `add` and `update`. Windows caps command-line argv far below the\n  65536-char content limit, so pipe near-limit payloads instead of passing them\n  as an argument (the Hermes\u002FMCP surfaces do this automatically). Piped bytes\n  are always decoded as UTF-8, whatever the process stdio codec (cp1252 on a\n  default Windows install), locally and in remote mode; invalid UTF-8 bytes\n  store as U+FFFD. Line endings are normalized: `\\r\\n` (and a lone `\\r`) store\n  as `\\n`, so a text-mode parent's piped LF text is stored as LF (issue #460).\n  The argv path performs no translation: an argument containing `\\r\\n` is\n  stored verbatim.\n\n### Provenance trust (`--taint`) — v9 (issue #59, 4.7)\nEvery memory carries a **taint** rank marking the trust of its ORIGIN:\n`trusted_internal` (human\u002Fcloseout\u002Ftest-grounded) \u003C `untrusted_tool` (agent or\nMCP\u002FHermes-authored) \u003C `untrusted_web` (web-fetched). There is deliberately no\nfourth rank: an unknown taint value is **refused** at every write surface (the\nwrite path never silently coerces it). The closed enum is enforced by a CHECK\nconstraint AND by application-side validation (CLI argparse choices, ingest\nvalidator, Hermes\u002FMCP boundary checks) — all sourced from the single\n`schema_meta.ALLOWED_TAINTS`.\n\nThe taint default depends on the surface:\n- CLI `add`: derived from `--signal` — grounded signals (`test`\u002F`compile`\u002F`lint`\u002F\n  `reviewer`\u002F`user`) default to `trusted_internal`; `signal=none` (the agent's\n  self-opinion) defaults to `untrusted_tool`. An explicit `--taint` overrides.\n- Hermes\u002FMCP tools (`zmem_add` \u002F the MCP `add` tool): default to EXPLICIT\n  `untrusted_tool` unless the caller passes a taint (e.g. `untrusted_web` for a\n  web fetch, or `trusted_internal` for a human-grounded note).\nTaint propagates **worst-of forward through lineage**: `update` re-creation and\n`consolidate` absorb both set the surviving row's taint to the worse of the two\nmerged sources (a merged row never DOWNGRADES a riskier member), and a\nduplicate `add` \u002F `ingest-jsonl` row that folds into an existing keeper applies\nthe same worst-of to the keeper. A tombstone\n(`supersede`\u002F`invalidate`) preserves the row's taint — it creates no new row.\n\nRecall surfaces the taint so trust is visible, mirroring prompt-injection-risk:\n- The **explicit** text path prefixes rows with `[UNTRUSTED TOOL]` (taint\n  `untrusted_tool`) or `[UNTRUSTED WEB]` (taint `untrusted_web`).\n- The **passive\u002Fauto-inject** path (`--no-bump` — used by the automatic hooks and\n  Hermes prefetch) OMITS `untrusted_web` rows entirely, exactly like\n  `prompt-injection-risk` rows (an untrusted web-sourced memory is not trusted\n  enough to inject passively). `untrusted_tool` rows DO surface passively — they\n  are agent-authored and thus safer than web content — and are flagged on the\n  explicit path instead. The hook scripts need no edits: they already pass\n  `--no-bump` and inherit this store-side filter.\n\n### recent \u002F search \u002F supersede \u002F update \u002F invalidate \u002F list \u002F get \u002F stats\n```\n\u003Cinterpreter> \u003Cstore.py> recent [--namespace NS] [--limit 5] [--min-confidence 0.5]\n                         [--include-global] [--global-limit 3] [--as-of ISO-8601]\n                         [--json]\n\u003Cinterpreter> \u003Cstore.py> search --text \"\u003Ctext>\" [--exact] [--namespace NS] [--limit 10]\n                        [--include-global] [--global-limit 3] [--no-bump]\n                        [--as-of ISO-8601] [--json]\n\u003Cinterpreter> \u003Cstore.py> supersede --id \u003Cfull-uuid> [--reason \"...\"] [--expected-namespace NS]\n\u003Cinterpreter> \u003Cstore.py> invalidate --id \u003Cfull-uuid> --reason \"...\" [--expected-namespace NS]\n\u003Cinterpreter> \u003Cstore.py> update --id \u003Cfull-uuid> --content \"\u003Cnew content>\" [overrides...]\n                       [--expected-old-namespace NS]\n\u003Cinterpreter> \u003Cstore.py> list [--namespace NS] [--include-superseded]\n\u003Cinterpreter> \u003Cstore.py> get --id \u003Cuuid>\n\u003Cinterpreter> \u003Cstore.py> stats\n```\nRead envelope (v13, issue #65 10.8): `recall`\u002F`recent`\u002F`search --json` print\n`{\"results\", \"count\", \"omitted\", \"injection_risk\", \"tokens_used\",\n\"tokens_budget\"}` instead of a bare list — `omitted` counts rows the\n`--no-bump` inject filter dropped (injection-risk + `untrusted_web`), so\nhosts stop guessing from stderr. Library callers still get the bare row list\nfrom the Python API.\n\n`update --json` mirrors `add --json` (`{\"id\", \"result\", \"created_new\",\n\"warnings\"}`). `get` (always JSON) includes the v13 `episodes` key — the\nepisodes this memory belongs to (always a list, possibly empty).\n\n`recent`\u002F`search` accept the same `--include-global`\u002F`--global-limit` pair as\n`recall` (project-first global tier union). `recent` now ALSO honours v5\nmigration aliases (so `recent --namespace \u003Cold pre-v5 key>` finds rows migrated\nto the new key). `search` now accepts `--no-bump` for a *passive* query that records a\nsurface on `surfaced_count` (never advancing `retrieval_count`) instead of bumping like\n`recall` (issue #21).\n\n`search --exact` (issue #263) switches the matcher from FTS5 to a **literal\nsubstring existence check**: `--text` is matched verbatim (case-sensitive,\ncontiguous) against `memory.content`, so stop-word-only literals, FTS5 query\nsyntax (`OR`, quotes, leading `-k`), and word order all stay literal instead\nof being normalized into a token query. Use it to confirm an exact id, path,\nerror string, or redaction marker is or is not present verbatim; plain\n`search` (no `--exact`) is unchanged byte-for-byte. `--exact` honors the same\n`--namespace`, `--include-global`\u002F`--global-limit`, `--as-of`, and `--exclude`\nfilters and the same `--no-bump` (issue #21) semantics as plain search, needs\nno embedding model, never link-expands, and orders results most-recent-first.\nThe literal match defeats every index (SQLite `instr` byte-scans each row in\nscope and the sort runs before `LIMIT`), so cost scales with the scoped row\ncount — fine for operator existence probes, not a hot-path substitute for\nranked search.\nAn empty `--text` trivially matches every row in scope (Python `\"\" in content`\nsemantics).\n\n`--as-of ISO-8601` (v9, issue #59 4.4) on `recall`\u002F`recent`\u002F`search` returns rows\n**valid at that instant**: `valid_from \u003C= as_of AND (valid_until empty OR\nvalid_until > as_of)`. `valid_from` is INCLUSIVE, `valid_until` is EXCLUSIVE, and\nempty `valid_until` means \"never expires\". Because a tombstone writes\n`valid_until` at the same instant as `superseded_at`, a point-in-time as-of may\nreturn rows that were later superseded (they were valid then) — only rows whose\nvalidity interval contains `as_of` surface. This is what makes `update` history\nreadable: `--as-of` before the update returns the OLD content.\n\n`--as-of` recoverability is **lane-bounded** in hybrid mode: a historically\ntombstoned row that was valid at `as_of` is reachable only through the lexical\n(FTS5\u002FBM25) lane. The vector lane's candidate pool always filters\n`superseded_at IS NULL` (live-only), so a row superseded before `as_of` cannot\nsurface via semantic KNN even though it was valid at that instant. Model-absent\nruns are lexical-only and unaffected; a hybrid run that misses old content via\nthe vec lane should be re-run with `--no-hybrid` to include the FTS lane.\n\n\n### reembed — backfill or rebuild semantic embeddings\n```\n\u003Cinterpreter> \u003Cstore.py> reembed\n\u003Cinterpreter> \u003Cstore.py> reembed --all [--profile NAME] [--batch N] [--dry-run]\n\u003Cinterpreter> \u003Cstore.py> reembed --check\n```\nFlagless form (unchanged contract): backfills embeddings for live memories that\nare MISSING them when the optional embedding runtime and model are available.\nExisting embeddings are preserved; no runtime means a graceful skip.\n\n`--all` rebuilds EVERY live memory's vector under the selected profile —\nthe operator-grade converter when you switch profiles:\n- `--profile NAME` selects from the shipped registry (`minilm`, `fake`; see\n  \"Embedding profiles\" below). Default: active `ZMEM_EMBED_PROFILE` or\n  `minilm`. Requires `--all` — `--profile` without it refuses with the exact\n  conversion command, never a silent no-op. `--all --profile fake`\n  requires `--confirm` ONLY when the store already holds non-fake committed\n  vectors (the overwrite is irreversible-in-practice without a working model\n  later); fresh\u002Fempty stores convert freely.\n- If the profile's dimension differs from what the store holds, `memory_vec`\n  is recreated at the new dimension INSIDE one transaction — a crash mid-run\n  rolls back to the pristine pre-run state, so a half-dim index is impossible.\n  Every command then re-verifies profile-vs-store dimension before it embeds\n  (mismatch = exit 2 with the exact remediation line).\n- Idempotent: a second identical run changes 0 rows. `retrieval_count`,\n  `content`, and every other column stay untouched — only embedding bytes,\n  their `embedding_model` marker, and `embedded_at` are rewritten, plus the\n  `embedding_profile` meta key recording the last completed conversion.\n- `--batch N` paces stderr progress lines ONLY (display chunks inside the\n  single transaction — batches are never separate commits). For very large\n  stores run during an idle window: the single transaction briefly grows the\n  WAL by roughly the size of all rebuilt vectors (~1.5 KB\u002Frow at 384-dim);\n  the default batch size is 64.\n- `--dry-run` reports how many of the live rows would change and writes\n  nothing (no writer lease, no meta write).\n\nIdempotency detail: change detection keys on\n`embedding IS NULL OR blob dim mismatch OR embedding_model marker differs`,\nso switching `--profile` back and forth always reports honestly instead of\nsilently treating rows as current.\n\n`--check` is an exclusive, read-only consistency census. It reports live rows\nmissing a `memory_vec` row, vector rows whose memory no longer exists, and live\nembedding blobs whose exact byte length differs from the declared vec0 dimension.\nIt exits 0 with `reembed check: 0 inconsistencies`; otherwise it prints the three\ncounts and exits 1. It never downloads a model, takes a writer lease, performs\nauto-rekey, or changes the store. A store with an active `-wal` sidecar is refused\nwith exit 2 so the census cannot recover or create shared-memory files; checkpoint\nthe store first.\n\n### Embedding profiles — shipped registry (`embed_profiles.py`)\n\n| Profile | hf_id | dim | sha256 | Purpose |\n|---|---|---|---|---|\n| `minilm` | `Xenova\u002Fall-MiniLM-L6-v2` | 384 | `bbd7b466…46f0c5` (original operator-box ONNX export; see notes) | Operator default; semantic recall\u002Fdedup |\n| `fake` | — | 16 | — (no files\u002Fnetwork by design) | Model-absent tests\u002FCI ONLY; deterministic placeholders; doctor warns on non-temporary stores |\n\nNotes:\n- The minilm pair's SECOND executed artifact, `tokenizer.json`, ships its\n  own pin: `TOKENIZER_SHA256 = da0e7993…` (the full 64-hex constant in\n  `embed_profiles.py` — the checked-in tokenizer artifact's byte digest).\n  The loader digest-verifies it from the same buffer the `Tokenizer` is\n  constructed from (`from_buffer`), and `embeddings._TOKENIZER_SHA256`\n  re-exports the same constant (#418).\n- The published pin covers the ORIGINAL OPERATOR-BOX ONNX export of\n  all-MiniLM-L6-v2 — preserved byte-exact in this repo's Git LFS history at\n  commit `3240ea1a0cf3d972f6a7d472cee73abaddfc038d`\n  (`skills\u002Fmemory\u002Fmodels\u002Fminilm.onnx`, 90,387,630 bytes) and served as the\n  never-retargeted GitHub release asset `model-minilm-bbd7b466\u002Fminilm.onnx`\n  (the sha-256 check after fetching — not platform immutability — is the\n  trust boundary). It is\n  NOT the public Xenova HF-repo export (`onnx\u002Fmodel.onnx`, oid\n  `759c3cd2b7fe7e93933ad23c4c9181b7396442a2ed746ec7c1d46192c469c46e`,\n  90,387,606 bytes) and NOT the sentence-transformers PyTorch weights —\n  different builds of the same checkpoint hash differently.\n  `checksum_mismatch` therefore means \"wrong build installed\"; restore the\n  pinned bytes from the release asset (or point `ZMEM_MODEL_URL` at it);\n  doctor prints this note verbatim. Verification has NO escape hatch:\n  `ZMEM_MODEL_ALLOW_UNVERIFIED` does not exist and never will.\n- A third ONNX profile was evaluated and deliberately OMITTED per the release\n  rule: no Qwen3\u002FNomic local ONNX artifact with a personally verified\n  hf_id+dim+sha256 could be pinned at authoring time. \"A name with empty\n  sha256 is a stub\" is forbidden — add one only with verified facts.\n\n#### Compatibility ledger for profile conversion (read before switching)\n\n- **Older zmem releases on a converted store:** versions without this change\n  hardcode `float[384]` and do not know about `embedding_profile`. On a store\n  converted to another dimension they keep opening it, keep writing rows, and\n  silently lose\u002Fdegrade vector recall (the vec lane's failures are swallowed\n  there). Conversion is per-machine deliberate maintenance — coordinate box\n  upgrades around it.\n- **Hook surfaces during mismatch:** while the active profile's dim differs\n  from committed data, every hook that recalls (`--no-bump`) receives an EMPTY\n  inject envelope; the refusal reason lands only on the child's stderr, which\n  hooks do not surface. This refuse-over-wrong-vectors posture is deliberate:\n  lexical-only degradation inside a wrong-dim store would surface misplaced\n  confidence rather than silence. Run `reembed --all` (or restore the prior\n  profile) to clear the state.\n- **`doctor` default-run behavior changed by design** in the same release:\n  the embeddings check now deep-verifies the model pin (mtime-keyed cached\n  hashing), so its JSON can newly report `checksum_ok:false` where older\n  versions stayed silently `null`.\n- **Concurrency during `reembed --all`:** the whole rebuild holds one SQLite\n  write transaction for its duration; concurrent writers wait (busy-timeout),\n  readers proceed on WAL snapshots. Schedule large conversions for idle\n  windows.\n\n**Cross-encoder trust note** (issue #63 review round; updated by issues\n#125 and #418): the operator-supplied `ZMEM_CROSS_ENCODER_MODEL` path stays\noperator-vouched (no checksum pin — treat it with the same caution as any\nexecutable), while the DEFAULT `mini-pair-scorer` profile path digest-gates\nBOTH artifacts (model AND tokenizer) at scorer BUILD time — each runtime\nobject is constructed only from the buffer whose SHA-256 just matched its\nprofile pin, closing the verify-then-reopen window (#418) — and any\ndownload is digest-verified before an atomic install. doctor's\n`embeddings_health.cross_encoder` block now surfaces\n`profile`\u002F`model_path`\u002F`tokenizer_path`\u002F`checksum_state`\n(`verified|mismatch|missing|unreadable`)\u002F`tokenizer_state`\n(`not-applicable|missing|unreadable|verified|mismatch`)\u002F`load_policy`\n(`explicit-operator-vouched`|`profile-pinned`)\u002F`passive`\u002F`shadow`\u002F\n`autodownload` so a missing-model silent degrade and the passive\u002Fshadow\nopt-in state stay visible.\n\n### episode-open \u002F episode-add \u002F episode-close \u002F episode-list — session containers (v13, issue #65 10.7)\n\n```\n\u003Cinterpreter> \u003Cstore.py> episode-open --namespace \"project:\u003Cbasename>\" [--json]\n\u003Cinterpreter> \u003Cstore.py> episode-add --episode \u003Cuuid> --memory \u003Cuuid> [--json]\n\u003Cinterpreter> \u003Cstore.py> episode-close --episode \u003Cuuid> [--summary] [--json]\n\u003Cinterpreter> \u003Cstore.py> episode-list [--namespace NS] [--json]\n```\n\nAn **episode** groups the memories captured during one working session\n(schema v13: `episode` + `episode_memory` tables). It is a CONTAINER, not a\nmemory type — `episode` is deliberately absent from `--type` values.\n\n- `episode-open` creates the row (`ended_at` empty until close) and prints\n  its id; this is the only creator — no empty-table seeding.\n- `episode-add` attaches a LIVE memory (`superseded_at IS NULL`; a\n  tombstoned id is refused exit-2) to an OPEN episode. Idempotent per pair;\n  memberships are append-only (a later supersede never removes one).\n- `episode-close` sets `ended_at`, computes `token_count` (the same\n  `row_token_cost` the injection budget uses, summed over LIVE members at\n  close), and with `--summary` writes one extractive first-sentence summary\n  row via the standard `add` path (capture-mode auto, so the shared\n  redaction helper runs) linked by `summary_memory_id`. A closed episode\n  cannot be re-closed or re-opened (append-only).\n- `episode-list` prints episodes newest-first with member counts\n  (`--json` for rows). `get --json` on a memory lists its episodes.\n\nEpisodes round-trip through `export-jsonl`\u002F`ingest-jsonl` via a `kind`\ndiscriminator (`\"memory\"` on memory rows; separate `episode` and\n`episode_memory` records). Legacy kind-less files still ingest as\nmemory rows; an older client consuming a new export should filter to\n`kind == \"memory\"` lines. Doctor reports counts (`episode-tables`).\n\n### session_start \u002F session_end — MCP + Hermes pairing tools (v13, issue #65 10.5)\n\nBoth surfaces expose `session_start` \u002F `session_end` (MCP tools) and\n`zmem_session_start` \u002F `zmem_session_end` (Hermes tools) with the same\ncontract:\n\n- **`session_start(namespace?, limit=3, session_id?, lane?)`** — passive\n  prefetch returning a fenced, provenance-tagged context block (Phase 3\n  fence + selective-inject rules, 0.5 recent floor). It runs\n  `recent --for-injection --json`: with a `session_id` the store-side\n  selector (moment `session_start`) owns the gate, the budget, the delivery\n  ledger, and the `rendered` fence, and the tool returns that complete\n  envelope plus the additive `context` alias and the back-compat\n  `result`\u002F`namespace`\u002F`ids` fields (issue #159); an omitted `session_id`\n  takes the legacy for-injection envelope. `retrieval_count`\n  NEVER advances (only the surface event is recorded — pinned by\n  tests\u002Ftest_session_tools.py), injection-risk and `untrusted_web` rows are\n  omitted, and `ZMEM_INJECT_TOKEN_BUDGET` is honored. Namespace omitted\n  resolves to the surface's own default — `user:global` on MCP, the session\n  namespace on Hermes (a deliberate divergence: the Hermes provider is\n  session-scoped, the network server is not).\n- **`session_end(note?, namespace?)`** — default is a NO-WRITE\n  acknowledgement so clients can pair start\u002Fend freely; it NEVER organizes\n  or consolidates (run the explicit CLI for that). With a `note`, exactly\n  one row is written via the standard `add` path (`fact`, `signal none`,\n  `taint untrusted_tool`, capture-mode auto so redaction runs), defaulting\n  to the server\u002Fsession namespace.\n\n### Scoped MCP tokens (v13, issue #65 10.2)\n\n`ZMEM_MCP_TOKEN` (env) is always an UNSCOPED operator token — full access to\nevery namespace, exactly the pre-v13 behavior. To scope it, point\n`ZMEM_MCP_TOKEN_FILE` at a JSON object:\n\n```json\n{\"token\": \"\u003Csecret>\", \"namespaces\": [\"project:zmem\", \"user:global\"]}\n```\n\nAllowed namespace forms are project:name, user:name, fleet:name, host:name,\nagent:name, and domain:name. Project and user values retain later-colon\ncompatibility; existing local project path keys may also contain internal\nspaces. The fleet, host, agent, and domain forms require one non-empty value\nwithout whitespace or colons.\n\nRequests outside the allow-list fail closed with the stable\n`namespace_not_allowed` error. `supersede`\u002F`invalidate` are namespace-guarded\ntoo (issue #109): for a scoped token the server reads the target row's\nnamespace, denies with the same `namespace_not_allowed` shape when it is\noutside the allow-list, and pins the verified namespace on the store\nmutation via `--expected-namespace` (a new `supersede`\u002F`invalidate` CLI flag\nthat makes the tombstone UPDATE conditional on the row's namespace — refuse\nexit 2 with the stable `[zmem] namespace guard:` stderr line, nothing\nwritten, on mismatch; `update --expected-old-namespace` pins the replaced\nrow the same way; omit for the historical unguarded local-operator\nbehavior). The empty string counts as a supplied (never-matching)\nexpectation and fails closed, not as \"no guard\". Unscoped operator tokens\nkeep full tombstone powers everywhere. Scoped tokens MUST pass an allowed namespace\nexplicitly on every read — a namespace-less read spans the whole store and\nis denied — and the implicit `user:global` union on scoped reads is\nsuppressed unless `user:global` is itself in the list. Malformed JSON, an\nempty `namespaces` list, or near-miss scopes like `\"global\"` are hard\nstartup errors (exit 2). A bare (non-JSON) token file stays unscoped, as does\na JSON object with `namespaces` absent or `null`.\nDoctor warns `unscoped_token: true` on operator tokens (issue #65, 10.10)\nand never prints the token value.\n\n### promote — review and install a reusable skill\n```\n\u003Cinterpreter> \u003Cstore.py> promote --dry-run [--namespace NS]\n\u003Cinterpreter> \u003Cstore.py> promote --id \u003Cuuid> --confirm [--description \"...\"]\n\u003Cinterpreter> \u003Cstore.py> promote --id \u003Cuuid> --confirm --install-approved\n```\n`--confirm` writes a review candidate; `--install-approved` is the additional\nexplicit gate that installs the reviewed skill into Codex, Claude Code, Zcode,\nand OpenCode skill directories. Promotion is never an unattended hook action.\n\nThe promote ladder (issue #64, schema v12) decides which lessons are\ncandidates. A lesson is eligible only when its EXPLICIT usage feedback says it\nworks and nothing says it doesn't:\n- `applied_count >= 3` AND `violated_count == 0` → eligible (see `feedback`).\n- Any `violated_count > 0` blocks eligibility; the 2nd violation has already\n  dropped the row's `trust_score` by `TRUST_VIOLATION_FLOOR_DROP` (0.15,\n  clamped at 0.0, applied exactly once at the violated_count 1→2 crossing).\n  `signal` is NEVER changed by feedback — trust_score is the usage ledger,\n  signal is provenance.\n- Passive exposure does NOT count: `retrieval_count`\u002F`surfaced_count`\n  (hooks, `--no-bump` recall) are not eligibility inputs. Only the explicit\n  `feedback` CLI advances the counters, so nothing auto-promotes.\n- The other candidate bars are unchanged: `type=lesson`, grounded signal\n  (test\u002Fcompile\u002Flint), `confidence >= 0.85`, live row.\n`promote --dry-run` with no eligible rows is a no-op that prints\n`[zmem] no promotion candidates found` (exit 0).\n\n### feedback — record explicit usage feedback (Voyager counters)\n```\n\u003Cinterpreter> \u003Cstore.py> feedback --id \u003Cuuid> --applied\n\u003Cinterpreter> \u003Cstore.py> feedback --id \u003Cuuid> --violated\n```\nIncrements exactly one of the row's `applied_count` \u002F `violated_count`\n(schema v12) and prints a one-line JSON summary\n(`id`, `verdict`, both counters, `trust_score`, `trust_dropped`). This is the\nONLY writer of those counters anywhere — hooks, `--no-bump` recall,\nPreCompact, and Hermes prefetch never advance them.\nExit codes: 0 success; 1 target missing or not live (tombstoned rows refuse);\n2 usage error (both or neither of `--applied`\u002F`--violated`, missing `--id`).\n\n### eval — offline recall-quality harness\n```\npython scripts\u002Feval_runner.py --store \u003Cpath> [--gold eval\u002Fgold.jsonl] [--k 5] [--fail-under X] [--json-out PATH]\n```\n\n### eval-inject — injection precision gold (issue #111)\n```\npython scripts\u002Feval_inject_runner.py --store \u003Cpath> [--gold eval\u002Finjection_gold.jsonl] [--k 5] [--fail-under-precision X] [--fail-under-false-injection X] [--compare-baseline eval\u002Fbaseline-injection.json] [--json-out PATH]\n```\nRuns every injection-gold item through the REAL `--for-injection` lane —\nthe same selective-inject gate and token budget the hooks execute — and\nscores the RENDERED set with negative controls: `precision_at_k`,\n`false_injection_rate`, `empty_pool_rate`, `hit_at_k`, overall and per\nmoment (user prompt, pre-tool operation tokens, subagent task text,\npre-compaction). Refuses (exit 2) if the gate or budget is stubbed out.\n`--gold` accepts a local labeled JSONL (e.g. derived from your own decision\nlog) without code changes. Record-only by default; the ratchet flags are\nthe one-switch CI gate.\n\nThe score-margin replay\u002Fbaseline evaluator is available for the #155\u002F#183\nread-only audit. CI runs the committed `scripts\u002Feval_replay.py` snapshot with\n`eval\u002Fbaseline-replay.json` on both platform legs. The evaluator is\nrecord-only unless an explicit `--fail-under` ratchet is supplied; it never\nresolves the operator store or claims live-host efficacy.\nRuns every gold item through the REAL recall pipeline and prints one JSON\nreport: `hit_at_k`, `mrr`, `as_of_accuracy`, `injection_omit_rate` (+ per-bucket\nand per-item detail). `--store` is REQUIRED — the runner never resolves the\ndefault home store; point it at a throwaway path and it auto-builds the\ndeterministic 64-row fixture corpus (the ids `eval\u002Fgold.jsonl` names) there.\nThe gold set covers the six issue-#64 buckets (as-of knowledge updates,\ninjection omission, entity aliases, namespace isolation, contested\u002Fsuperseded\nguidance, ordinary FTS hits; >= 5 items each) plus the issue-#82 honesty\nbuckets (>= 3 items each): `retraction` (an `invalidate --reason`-ed fact must\nvanish at current time yet surface under `--as-of` before the tombstone),\n`polarity` (two LIVE contradicting rows — both sides surface), and\n`change-intent` (explicit-path lineage unfold). An optional `\"explicit\": true`\ngold flag runs an item on the explicit path (`no_bump=false`, still zero-write\nvia `no_telemetry`) so the change-intent unfold is measured; passive twins on\nthe same rows pin that hooks never unfold. The original 30 ids (rowids 1-50)\nare the frozen #64 contract; #82 appends rowids 51-64.\nExit 0 regardless of scores (quality is collected, never\ngating); `--fail-under` is optional and OFF in CI; 2 on operational errors\n(invalid gold set names the offending item).\nThe run is model-absent by construction: the fake embedder\n(`ZMEM_EMBED_PROFILE=fake`) and a pinned clock (`ZMEM_TEST_NOW`) make ranking\ndeterministic on any machine. Evaluation is passive (`no_bump`) and measures\nretrieval quality — link expansion and MMR presentation are excluded\n(`link_hops=0`, `no_mmr`).\nSelf-corpus probe — measure recall against YOUR corpus (issue #82):\n```\npython scripts\u002Feval_self_corpus.py --store \u003Cstore-copy> [--k 5,20] [--limit 100] [--json-out PATH]\n```\n`--store` is REQUIRED and the runner REFUSES the host default store path\n(exit 2 with a remediation: take a `store.py backup` snapshot and probe the\ncopy) and refuses nonexistent paths before creating anything. Probes are\ntemplated deterministically from the store's own live rows (first sentence \u002F\ndistinctive tokens \u002F entity aliases — domain-agnostic, no LLM). Every probe\nis fully passive (`no_telemetry`, `no_bump`, `link_hops=0`, `no_mmr`; the\nunfold structurally cannot fire) so the probed store stays byte-identical.\nThe report may embed operator content — `--json-out` is the only write and\nreports are gitignored; never commit one.\nPublic-corpus adapters convert on-disk corpora into gold JSONL and skip\ncleanly when the corpus is absent (CI downloads nothing):\n```\npython scripts\u002Feval_adapters.py --adapter longmemeval --input \u003Cpath> --out \u003Cpath>\npython scripts\u002Feval_adapters.py --adapter locomo --input \u003Cpath> --out \u003Cpath>\n```\nA missing `--input` prints `skipped: ...` and exits 0. Synthetic 3-row toy\nfixtures under `tests\u002Ffixtures\u002Fadapters\u002F` prove both converters.\n\n### tune-weights — suggest recall scoring weights (dry-run only)\n```\n\u003Cinterpreter> \u003Cstore.py> tune-weights --dry-run --gold eval\u002Fgold.jsonl [--k 5]\n```\nEvaluates the shipped composite weights (W_BM25\u002FW_CONFIDENCE\u002FW_RECENCY\u002F\nW_POPULARITY) against a gold set, hill-climbs a small deterministic candidate\nset, and prints JSON with `current` and `suggested` weight vectors (always\nsumming to 1.0) plus the metrics each achieves. The evaluation is read-only\nand it WRITES NOTHING — there is deliberately no `--apply` (the store must\nalready be at the current schema: opening any store.py subcommand migrates\nit, as always). Applying a suggestion is a MANUAL edit of the W_*\nconstants at the top of `skills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Frecall.py`, keeping\nthe sum at 1.0. Uses the CURRENT store (env-resolved like every subcommand —\nin tests\u002FCI that is a fixture store via `ZMEM_STORE`, never the operator\nstore). Exit 0 even when the current weights already win; 2 on an invalid\ngold set, a missing `--dry-run`, or an evaluation failure.\n\n### consolidate — merge near-duplicate memories\n```\n\u003Cinterpreter> \u003Cstore.py> consolidate [--threshold 0.80] [--prune] [--dry-run] [--namespace NS] [--force] [--merge-contested] [--json]\n```\nClusters live memories by embedding cosine similarity (Jaccard token overlap when\nembeddings are unavailable), picks a keeper, merges metadata, and supersedes the\nrest with a persisted reason. The automatic SessionStart cadence now invokes\n**`organize`** (issue #62, 7.7) — which runs consolidate's EXACT same cluster\nlogic on a bounded episode — so the 7-day \u002F 20%-growth gate below protects the\nschedule REGARDLESS of entry point: organize and consolidate share the same\n`last_consolidation` meta keys and the same single-flight lock. An automatic\nrun is skipped only when the last run was \u003C7 days ago AND growth was \u003C20%; both\nbounds are env-tunable via `ZMEM_CONSOLIDATE_MIN_INTERVAL_DAYS` \u002F\n`ZMEM_CONSOLIDATE_GROWTH_THRESHOLD`. The `consolidate` CLI remains available for\nexplicit\u002Fmanual scope runs. When the gate declines, the run prints a\nsingle `[zmem] consolidate: skipped by cadence gate (...)` line (never silent)\nand changes nothing. Use `--dry-run` to preview clusters and the exact merge\ndecision per row without mutating the store — `--dry-run` models the cadence\ngate too, so a gated dry run reports `would skip by cadence gate` rather than\n`would merge N`, and a dry run that says \"would merge\" implies a real run that\nmerges. Pass `--force` to bypass the cadence gate and run consolidation now\n(this is the only intentional bypass; `--threshold` does not affect the gate).\n\nKeeper selection: within a cluster, the survivor is the row with the highest\n`confidence * (retrieval_count + surfaced_count)` product — total surface events,\nblend-aware for hook-surfaced memories (ties broken by confidence, then earliest\ningestion) (issue #21). Absorbed rows are NOT lost on merge: a near-duplicate that is not\nfully subsumed by the keeper has its ENTIRE text appended to the keeper's content\n(live-recallable + FTS-indexed), and its id is recorded in the keeper's\n`merged_from` provenance column, so consolidated memory is non-lossy\n(v11, issue #61 6.6: the column is maintained as a DE-DUPLICATED,\nfirst-seen-order id list — the same absorbed id is never recorded twice,\nand the v11 migration normalized pre-existing duplicates losslessly). Note this\nis a whole-text append, not a unique-tail diff — a partially-overlapping row\nduplicates the span it already shares with the keeper (deliberate\nover-preservation, bounded by the size cap below). Reordered same-token phrasing\n(e.g. \"call foo before bar\" vs \"call bar before foo\") is likewise preserved as\ndistinct content — only genuinely subsumed\u002Fduplicate text is omitted.\n\nSize cap: a keeper's content is never allowed to exceed\n`INGEST_MAX_CONTENT_CHARS` (the JSONL round-trip ceiling, 65536). An absorb that\nwould push the keeper over the cap is NOT appended — its id is recorded with a\n`:truncated` marker in `merged_from` and the absorbed row is still superseded;\nthat is the one bounded case where an absorbed row's text is not migrated live\n(it remains only in the tombstoned history row). `--dry-run` shows would-lose\ntokens for this case.\n`--prune` also removes low-value NEVER-surfaced and never-retrieved memories\n(`retrieval_count = 0` AND `surfaced_count = 0`, low confidence, old, `signal = none`;\nopt-in, never automatic). A hook-surfaced memory is protected — `retrieval_count = 0`\nalone is not evidence of unused (issue #21). Unrecalled extension (issue #62,\n7.6): a live row whose `last_surfaced` is OLDER than `ZMEM_UNRECALLED_DAYS`\n(default 30) qualifies EVEN with `surfaced_count > 0` — a surface event that has\nitself gone stale loses its issue #21 protection; `NULL last_surfaced` keeps the\n`surfaced_count = 0` rule unchanged and `signal != none` is never pruned.\n\nConsolidation is **single-flighted**: concurrent runs (several sessions starting\nat once) take an advisory lockfile in the store dir, and the losers skip cleanly\nand exit 0 rather than clustering the same rows twice.\n\n### organize — sleep-time organization (SessionStart cadence job)\n```\n\u003Cinterpreter> \u003Cstore.py> organize [--prune] [--dry-run] [--force] [--json]\n```\nThe SessionStart sleep-time maintenance job (issue #62, 7.7): the\n`session-cadence` batch that the start hook launches detached runs ORGANIZE, not\nconsolidate. It bounds an \"episode\" of work to the most recent live rows,\nbackfills any enrichment those rows are missing, runs consolidate's identical\ncluster\u002Fabsorb\u002Fcontested machinery on exactly that set, then adds sleep-time\ndeliverables: a topic hierarchy, hierarchical extractive summaries, and\nextractive compression of the rows consolidation just grew. Everything is\ndeterministic and LLM-free by default; there is no `--threshold`\u002F`--namespace`\nto keep the two maintenance entry points aligned (see the consolidate section:\norganize and consolidate SHARE one cadence gate implementation, one cadence\nclock, and one single-flight \"consolidate\" lock, so on a given store at most\none maintenance run happens per cadence window — whether it was triggered\nfrom SessionStart, a manual CLI call, or the Hermes session-end hook).\n\nPipeline (in order):\n1. **Shared cadence gate** — the SAME 7-day \u002F 20%-growth gate as consolidate\n   (ONE shared implementation, `_cadence_gate_skipped`, so the two entry\n   points can never drift), using consolidate's `last_consolidation` meta keys\n   (two entry points, one clock). `--force` is the only bypass. A gated run\n   prints `[zmem] organize: skipped by cadence gate (...)` and changes\n   nothing; `--dry-run` models the gate (`would skip by cadence gate`).\n2. **Optional idle gate** — `ZMEM_ORGANIZE_IDLE_HOURS` (default 0 = off). When\n   set, organize refuses to run unless the store has had no live-memory activity\n   (max of ingestion_ts\u002Flast_retrieved\u002Flast_surfaced) for at least that many\n   hours: sleep-time jobs should not churn a store that is mid-use. A skip prints\n   `[zmem] organize: skipped by idle gate (...)`.\n3. **Working set \u002F episode bound** — the N most recent live rows\n   (`ZMEM_ORGANIZE_EPISODE_BOUND`, default 256, clamped to the per-namespace\n   consolidation cap), box-wide. 0 disables real work. organize's own summary\n   rows are EXCLUDED from the episode — keyed on the structural\n   `source_ref` `organize:` prefix, never the (mutable) tags column, so\n   summaries are the pipeline's OUTPUT and never its input. The `organize:`\n   prefix is therefore RESERVED for summary rows: a manually-added row whose\n   `--source-ref` begins with `organize:` is excluded from episodes the same\n   way, and consolidate additionally EXCLUDES every live `organize:%` row\n   from its candidate set so the pipeline's output can never become its input\n   (issue #77). Every CLI write surface (`add`, `update`, `ingest-jsonl`,\n   including per-row `source_ref` fields in ingested JSONL) warns when a\n   non-empty source_ref carries the reserved prefix —\n   `[zmem] WARNING: source_ref prefix organize: is reserved for organize\n   summaries` on stderr; the write is NOT rejected and stdout (including\n   `--json` output) is untouched.\n4. **Entity backfill** — every working row missing `memory_entity` links gets\n   them via the deterministic extractor (idempotent; rows with nothing\n   extractable are counted as candidates but link nothing). The card sweep's\n   env configuration (`ZMEM_CARD_MIN_MENTIONS` \u002F `ZMEM_CARD_TOP_N`) resolves\n   BEFORE this loop — before ANY write of the run (PR #385 review F-4); an\n   invalid value prints one stderr line, skips the card stage with zero\n   counts, and never aborts the run. Immediately after the backfill\n   (issue #179): **the entity-card cadence sweep** — see \"Entity cards\n   (issue #179)\" below. Per entity (ascending entity-id order) one\n   deterministic card is created, refreshed, or tombstoned; the report (and\n   `organize --json`) carries the five counts under `entity_cards`:\n   `created` \u002F `refreshed` \u002F `unchanged` (SHA-256 no-op — no SQL write) \u002F\n   `removed` (a live card that fell below the threshold) \u002F `skipped`\n   (non-qualifying entity with no live card, AND per-entity apply failures —\n   one bad entity rolls back only its own transaction, logs one stderr\n   line, and the sweep continues). `--dry-run` computes the same\n   counts read-only; the session-cadence JSON envelope carries them under\n   `entity_cards` too.\n5. **Link backfill** — every working row missing `memory_link` edges gets them\n   via the standard write-time linker (`related`\u002F`contradicts` at\n   `ZMEM_LINK_THRESHOLD`).\n6. **Episodic consolidation** — `consolidate` on EXACTLY the working set\n   (keeper selection, absorb, contested guard + optional NLI judge, bounded by\n   the same per-namespace cap). The optional NLI judge is bounded by a\n   RUN-WIDE budget (issue #77): `ZMEM_NLI_MAX_CALLS` (default 64) judge\n   subprocess invocations and `ZMEM_NLI_MAX_SECONDS` (default 30.0) seconds\n   of wall time per run; invalid, non-finite, or non-positive values fall\n   back to the defaults. When the budget is exhausted the judge stops, one\n   `[zmem] consolidate: NLI judge budget exhausted (...)` warning names the\n   reason, and affected contested clusters stay unmerged — even under\n   `--merge-contested`. Judge diagnostics carry member ids, pair indexes, and\n   the verdict only, never memory content. organize's synthesized writes\n   (compression replacements and summaries) declare `capture_mode=\"auto\"`. The vector neighbor lookup is bounded to the\n   episode too — an out-of-episode near-duplicate in the global vec0 index is\n   never pulled in. See the consolidate section for semantics.\n7. **Compression** — the keepers consolidation actually GREW, when their\n   content exceeds `ZMEM_KEEPER_COMPRESS_CHARS` (default 4000; 0\u002Fnegative\n   degrades to the default — it is a cap, not a switch), are compressed\n   deterministically: order-preserving UNIQUE sentences, dropping only from\n   the TAIL on whole-sentence boundaries (never mid-sentence; the pre-compress\n   text stays on the superseded history row and `merged_from` provenance is\n   carried to the replacement). Compression runs BEFORE topic identity is\n   recorded: compression replaces the keeper's id (append-only update), so\n   keying topics first would leave every later run with a different key and a\n   duplicate summary.\n8. **Topics + summaries** — a related-graph over the POST-COMPRESSION live\n   working rows using the SAME neighbor predicate at the same threshold\n   consolidate would use; leftover singletons are grouped by SHARED entity\n   (A-MEM lite). Every live NON-summary working row lands in exactly one\n   topic. Topics of ≥3 members get a **hierarchical extractive summary**: a\n   REAL row (`type=fact`, tags exactly `summary,topic`, `signal=none`,\n   confidence 0.5, `source_ref=organize:\u003Cmembers>`,\n   `merged_from=\u003Cmembers>`), built from the first sentence of each member\n   (de-duplicated, capped, never truncated mid-sentence). Members stay live;\n   the summary is FTS\u002Fentity\u002Fsearch recallable. Idempotent: a re-run finds the\n   existing summary by its STRUCTURAL identity (`source_ref` + `merged_from` —\n   never the mutable tags column) and UPDATES it (Phase 4 of the issue)\n   instead of duplicating; when a topic's membership legitimately changed,\n   the stale overlapping summary is superseded (`reorganized into a changed\n   topic`) rather than left as a live orphan. Summary writes never propagate\n   the `summary,topic` marker onto neighboring user rows. If the\n   update\u002Fcreate would fold into a dedup target, organize logs and skips\n   rather than rewriting a stranger's row.\n9. **Unrecalled prune (pass-through)** — `--prune` enables the `consolidate\n   --prune` rule, extended (issue #62, 7.6): a live row also qualifies when its\n   `last_surfaced` is OLDER than `ZMEM_UNRECALLED_DAYS` (default 30) even with\n   `surfaced_count > 0` — a surface event that has itself gone stale loses its\n   issue #21 protection (both boundary comparisons are made in a\n   format-agnostic way, so the store's ISO-8601 timestamps compare correctly\n   against the cutoff). `signal != none` is never pruned; never automatic.\n\n`--dry-run` writes NOTHING (no meta, no backfill, no summaries) and the `--json`\nreport carries per-step would-be counts (`entity_backfill`, `link_backfill`,\n`entity_cards`, `topics`, `summaries`, `compressed`, `pruned`, `episode_ids`,\n`consolidate` sub-report).  A concurrent organize\u002Fconsolidate loses cleanly (exit 0, a\n`- skipped` notice, or `{\"error\": \"organize lock busy\"}` under `--json`).\n\nOptional local NLI judge (7.5): when `ZMEM_NLI_CMD` is set to an argv template\n(e.g. `ZMEM_NLI_CMD='python \u002Fpath\u002Fto\u002Fyour\u002Flocal-judge.py'`), consolidate's\ncontested branch consults it for mixed-polarity clusters; only an `entailment`\nverdict on EVERY polarity-flagged pair — ANY two members whose negation\npolarity differs, not only pairs anchored on the keeper — un-parks the merge,\nand any other verdict or failure parks it (never auto-merges; a failure is\nreported on stderr as a distinguishable NLI diagnostic). The judge receives\nthe two first-sentences on stdin (one per line, UTF-8) and prints a verdict\nword. On Windows, backslashes in the template are normalized to forward\nslashes before parsing (POSIX-style backslash stripping would otherwise\nmangle `C:\\path\\judge.py`; quote paths containing spaces). Unset =\nbyte-identical behavior (no NLI); a deterministic regex polarity remains the\nfallback for a response you judge ambiguous.\n\nSchema compatibility: organize NEVER changes the schema. All of its outputs use\npre-existing columns (`merged_from`, `source_ref`, `tags`, `confidence`,\n`source_hash`), so a store stamped at an older schema version keeps opening\nnormally in an older client until the new version is installed — verify a\ntemplate store with `store.py organize --dry-run` before upgrading.\n\n### Entity cards (issue #179)\n\nOne deterministic `entity_card` memory row per qualifying entity — a\ncontinuously refreshed summary card with provenance, built and maintained\nentirely store-side. The #172 taxonomy decision REJECTED this type; the #179\nworkstream contract reverses that decision (the #175 kinds precedent: the RFC\nmap is a frozen record, not a veto), and `ALLOWED_TYPES` carries\n`entity_card` as the 8th type. A card is an ordinary `memory` row keyed by\n`source_ref=card:\u003Centity_id>`; the schema stamp is **17** (0.107.0, PR #385\nreview F-1): the bump is STAMP-ONLY (no DDL), but it is upgrade-required —\nsee the operator note below.\n\n**KEY OPERATOR NOTE (schema 17, upgrade-required; FORWARD since\nsuperseded — see the schema 18 note above).** For THAT bump\n`FORWARD_COMPAT` deliberately stayed at 15: every pre-0.107 client (\u003C= 0.106) sharing a store\n**refuses** it, loudly at open, once the first 0.107 client migrates the\nstamp to 17. Without the bump those clients would fail SILENT on\n`entity_card` rows (an unknown type raises in their type preference and\ntheir hooks swallow the error — a fleet-wide memory blackout the review\nround executed end-to-end). On shared-store machines update every client\nBEFORE the first 0.107 host runs a cadence; `ZMEM_ALLOW_NEWER_SCHEMA=1` is\nthe operator stopgap (proceed at your own risk; it never downgrades the\nstore).\n\n**Type and birth invariant.** `entity_card` is CARD-REFRESH-ONLY: card rows\nare born ONLY through `store.py entity-card build|refresh` and the organize\ncadence sweep. Every interactive add surface refuses the type (`add --type`\nchoices derive from ALLOWED_TYPES minus `entity_card`; the Hermes\u002FMCP add\ntools validate against the same exclusion), jsonl ingest refuses it\nline-numbered (ingest bypasses add_memory and must never mint cards), an\nordinary `add_memory` call with the type but without the refresh path's\n`force_append=True` raises ValueError, every ordinary write rejects any tag\ncarrying the reserved `entity_card_sha256=` prefix BEFORE mutation, and\n(since the PR #385 review round) the `card:` source_ref prefix is reserved\nthe same way — an ordinary `add`\u002F`update` cannot claim a card identity.\nThe SAME guards cover the update path (review F-2): `update --type\nentity_card` on an ordinary row is refused (type conversion into the\nreserved type), an explicitly supplied watermark-prefix tag is refused, and\na `card:` source_ref claim is refused. Content-only updates of\npre-existing card rows keep the full type enum and succeed — the `state`\nprecedent; a transient watermark desync from such an edit self-heals at the\nnext refresh, which recomputes the digest over the new bytes (disclosed).\n\n**Configuration.** `ZMEM_CARD_MIN_MENTIONS` (default 5, inclusive) is the\nlive non-card mention count an entity needs to qualify;\n`ZMEM_CARD_TOP_N` (default 8) caps the card's provenance rows. An absent\nvariable selects its default; a present empty, non-integer, zero, or\nnegative value raises before any card write starts (CLI exit 2, store bytes\nunchanged).\n\n**Rendering.** The card is UTF-8, LF-terminated, fixed field order:\n`Entity: \u003Ccanonical_name>` \u002F `Kind: \u003Ckind>` \u002F `Aliases: \u003Csorted alias_norm>`\n\u002F `Mentions:` (one `- score=\u003Csix decimals> last_seen=\u003Cingestion_ts>\nid=\u003Cmemory_id> source_ref=\u003Cref> text=\u003Csingle-line>` per selected row) \u002F\n`Last seen: \u003Cmax ingestion_ts over EVERY live non-card source row linked to\nthe entity>` \u002F `Sources:` (one `- \u003Cmemory_id>` per selected row, same\norder). Mention text collapses CR\u002FLF\u002Fwhitespace runs to one space. Source\nrows are live (`superseded_at IS NULL`, empty `valid_until`), non-card,\nlinked to the entity across ALL namespaces, NULL confidence reads 0.0, and\nordered confidence DESC, ingestion_ts DESC, id ASC.\n\n**Provenance and lifecycle.** Every card row lives in `user:global` with\n`source_ref=card:\u003Centity_id>`, `signal=none`, confidence 0.5 (above the\nrecall floor — a signal=none row's inherited 0.2 would be invisible; the\nsummary precedent), `capture_mode=manual`, and the reserved\n`entity_card_sha256=\u003Chex>` tag (SHA-256 over the card's exact UTF-8 bytes —\nthe no-op watermark). The card's TAINT is the WORST OF its own\nsignal-derived default (signal=none -> `untrusted_tool`) and every SELECTED\nsource row's taint (PR #385 review F-8): one `untrusted_web` mention makes\nthe card itself `untrusted_web`, so the passive untrusted fence withholds\nthe card exactly as it withholds the source it aggregates. The card row is linked DIRECTLY to its entity —\nthat is its only entity link. REDACTION\nTRADEOFF (disclosed): the card is written with `capture_mode=manual` — the\npinned mode that keeps the watermark tag from being dropped — so its\nmachine-assembled content is NOT re-redacted at card write time; the source\nrows were already redacted at their own writes, and their rendered mention\ntext is the already-redacted form. Refresh is append-only and fail-closed:\nidentical content bytes AND matching watermark return `(existing_id,\nFalse)` with NO SQL write; changed content tombstones the old row\n(`superseded_at` AND `valid_until` both set — history stays queryable) and\ninserts the replacement with `update_of=\u003Cold_id>`; any failure (tombstone,\ninsert, link, watermark) raises and rolls back the whole transaction. The\nliveness decision is made on a RE-READ inside the `BEGIN IMMEDIATE` (the\npre-lock read is only the no-op fast path), and the `entity-card` CLI\nsingle-flights on the shared `consolidate` advisory lock with the cadence\nsweep, so two card writers cannot interleave (PR #385 review F-6). Writer\ntimestamps come from one `schema.now_iso()` value per sweep and NEVER read\n`ZMEM_TEST_NOW`. The cadence sweep (organize step 4b) enumerates entity ids\nin ascending byte order and returns exactly the five counts\n`created\u002Frefreshed\u002Funchanged\u002Fremoved\u002Fskipped`; a live card whose entity\nfell below the threshold is tombstoned and counted `removed`; `skipped`\nalso covers per-entity apply failures (an oversized card, a stale-plan\nsupersede) — one failure logs one stderr line, rolls back only that\nentity's transaction, and the sweep CONTINUES (PR #385 review F-4).\n\n**CLI.** `store.py entity-card build ENTITY --json` creates a missing\nqualifying card (`created`), refuses a live card with exit 2 and no write,\nand reports `no_card` (no write) below the threshold;\n`entity-card refresh ENTITY --json` returns `created` \u002F `updated` \u002F\n`unchanged` \u002F `no_card` (below-threshold refresh tombstones the live card\nfirst). `ENTITY` resolves an exact entity id first, then an exact canonical\nname (zero or multiple name matches: exit 2). `--json` stdout is one object\nwith the exact key order `result, id, entity_id, source_ref, warnings`\n(id\u002Fsource_ref are JSON null for `no_card`); errors before the write exit 2\nwith store bytes unchanged. The command single-flights on the shared\n`consolidate` advisory lock with the organize cadence (a lock-busy run\nexits 1 with a stderr notice, never a silent skip).\n\n**Cards never travel.** `export-jsonl` omits `entity_card` rows from every\nexport shape (live-only and `--include-superseded`): cards are store-local\nderived state, every ingest surface refuses the type, and the destination\nre-derives them via its own organize cadence sweep (PR #385 review F-3).\n\n**Same-fence recall suppression.** With `prefer_cards=True` (the default,\n`entity_match_ids`), a live card of a query-matched entity enters the\nentity arm with that entity's relevance. One suppression pass resolves the\nselected card rows by exact `source_ref=card:\u003Centity_id>` plus a direct\nentity link — never by matching card text — and removes ALL live non-card\nrows linked to those entities from the fence under the active namespace and\n`as_of` predicates: the fence renders the card, not the mentions it\nsummarizes. The pass runs at EVERY merge site (complete enumeration, PR\n#385 review F-5): (1) the scoped five-tier pools when the global tier is\nincluded (`--include-global`); (2) the cross-tier merge (namespace +\n`--include-global`); (3) the UNSCOPED default surface (`namespace=None`,\nthe default `recall --query` path) — its single all-namespace pool renders\none fence, so the card's represented sources leave it too; (4) the\ncross-project hazard pool, cleaned before its rows splice into the fence;\n(5) `--explain`'s presented pools, mirroring (1)-(3), AND its deep\n\"scored but beyond --limit\" pools, so the verdict stream agrees with the\npresented fence; and (6) the suppressed-id set is threaded into\n`expand_recall_links`' exclusion, so a removed source row cannot re-enter\nthe fence as a 1-hop link neighbour of a surviving row. The PASSIVE\nprefetch lane (`recent` \u002F SessionStart pulls) never renders cards at all —\n`entity_card` rows are excluded from every recent pool, so the query-less\nbranch can never render card and sources together. A query with no selected\ncard keeps its current result; a project-only or scoped-without-global\nrecall selects no card and applies no suppression. (Disclosed placement:\nthe pass runs after per-tier arm caps, RRF fusion, and MMR — suppressed\nrows may already have consumed tier slots; per-tier placement is impossible\nbecause the card lives in `user:global` while its represented sources live\nin project fences.) Cards are NOT in `PROTECTED_INJECT_TYPES` — they are\nordinary recall rows for explicit id retrieval.\n\n### rekey-namespace — remediate stranded namespace rows (admin)\n```\n\u003Cinterpreter> \u003Cstore.py> rekey-namespace --near-miss-global [--to user:global] [--dry-run] --confirm\n\u003Cinterpreter> \u003Cstore.py> rekey-namespace --from \u003Cold-namespace> --to \u003Cnew-namespace> [--dry-run] --confirm\n\u003Cinterpreter> \u003Cstore.py> rekey-namespace --map \u003Cmap.yaml> --dry-run\n\u003Cinterpreter> \u003Cstore.py> rekey-namespace --map \u003Cmap.yaml> --confirm\n```\nRewrites the `namespace` column of live rows. The primary use is remediating\nlegacy rows stranded under a global near-miss namespace (`global`,\n`userglobal`, …) that pre-date the `add`\u002F`ingest-jsonl` write-time guard — such\nrows are unreachable from the automatic hooks. `--near-miss-global` rekeys EVERY\nlive near-miss row to `--to` (default `user:global`); `--from` rekeys one exact\nnamespace. `--to` may not itself be a near-miss. `--confirm` is required to\nwrite (without it, or with `--dry-run`, the command only previews). Superseded\nrows are left untouched (history preserved).\n\nSince issue #71 C this remediation also runs AUTOMATICALLY: every\nstore-opening command rekeys global-near-miss rows to `user:global` before\ndispatching (silent on healthy stores; `ZMEM_AUTO_REKEY=0` or\n`--no-auto-rekey` opts out). The explicit command remains for previews and\nfor kill-switch deployments. Namespace contract: fleet facts live in\n`user:global`; project facts in `project:\u003Ccanonical-git-remote>`; MCP `add`\nwithout a namespace uses `ZMEM_MCP_DEFAULT_NS` when the operator set it,\nelse `user:global` — a bare `global` is never invented.\n\n`--map` is separate from the legacy selectors. Its file is UTF-8 without a BOM\nand contains only unindented quoted pairs such as\n`\"db:source-prefix\": \"fleet:scope\"`; blank lines and full-line comments are\nallowed. Entries are evaluated in file order, so the first matching `source_ref`\nprefix wins. A source prefix must also exclude C0\u002FC1 controls, whitespace, and\n`=` so it cannot forge key\u002Fvalue fields in the decision log. Every target must\nsatisfy the shared namespace grammar; its value is percent-encoded in the log,\nso valid project\u002Fuser names containing spaces or `=` remain safe. Map mode\nrequires exactly one of `--dry-run` or `--confirm` and cannot be combined with\n`--from`, `--to`, or `--near-miss-global`.\n\nBoth map modes enforce the store's forward-schema compatibility gate through\nthe connection they use for the operation. If the store has a newer unsupported\nschema, upgrade this plugin or use the documented compatibility override before\npreviewing or applying the map.\n\nThe preview opens the existing store read-only and prints one count per map entry\nplus `unmapped`, with no backup, log, lease, migration, or auto-rekey. Apply takes\na verified snapshot before its single transaction, updates and relinks only IDs\nwhose namespace changes, then appends one decision-log line per map entry with\nseparate `matched` and `moved` counts. A post-commit decision-log failure reports\nexit 3 and explicitly says that the map changes are committed; pre-commit\nfailures return nonzero without that committed-state diagnostic. Restore the\nverified snapshot if recovery is required. Apply loads the optional sqlite-vec\nextension when available and checks vector bytes in the transaction; link\nendpoints are checked on every apply.\n\n### promote-store — merge a leftover second store (admin, issue #71 E)\n```\n\u003Cinterpreter> \u003Cstore.py> promote-store --from \u003Cpath-to-store.sqlite> [--dry-run]\n```\nOne-shot merge of a leftover second store (e.g. `~\u002F.zcode\u002Fmemory\u002Fstore.sqlite`)\ninto the canonical one. Read-only on the source; source ids are PRESERVED so\nre-runs are no-ops; a NEWER source schema is refused. doctor's\n`second-stores` check fails when canonical is missing live rows that exist\noutside it (rows already tombstoned in canonical are only a WARN — nothing\nto promote) and recommends this command.\n\n### backup — verified snapshot with retention\n```\n\u003Cinterpreter> \u003Cstore.py> backup [--retention 7] [--out-dir DIR] [--if-due]\n```\nWrites `store-\u003CUTC timestamp>.sqlite` into the backup dir (`--out-dir`, else\n`$ZMEM_BACKUP_DIR`, else `\u003Cstore dir>\u002Fbackups`) using SQLite's Online Backup API,\nwhich is safe while other sessions are writing to the store. The snapshot is\nverified (`PRAGMA integrity_check` **and** a total\u002Flive row-count comparison\nagainst the source) before it counts as a backup — if either check fails the\nsnapshot file is deleted, the command exits non-zero, and neither the\n`last_backup` marker nor retention rotation happens.\n\nRetention deletes only the **oldest** files matching `store-*.sqlite` beyond the\nnewest N. Nothing else in the directory is ever touched — including the\n`prerestore-*` safety copies `restore` leaves behind. `--retention 0` disables\npruning.\n\n`--if-due` makes the command a no-op unless `$ZMEM_BACKUP_INTERVAL_DAYS`\n(default 1) has elapsed since the last successful backup; the SessionStart hook\nuses it so the automatic snapshot is cheap almost every session. Without the\nflag the backup always runs. Also single-flighted (its own lockfile).\n\n### restore — recover the store from a snapshot\n```\n\u003Cinterpreter> \u003Cstore.py> restore --from \u003Csnapshot.sqlite> [--force] [--out-dir DIR]\n```\nRefuses unless `--force` when a store already exists. Verifies the snapshot's\nown `integrity_check` **before** touching the destination, then takes a\n`prerestore-\u003Ctimestamp>.sqlite` copy of the current store (your rollback path —\ndeliberately outside the retention glob so rotation can never prune it), clears\nstale `-wal`\u002F`-shm` sidecars, copies, and re-verifies the restored store.\n\nTakes the **full maintenance lock ladder** — `maintenance`, `schema`, `backup`,\nand `consolidate` — for its whole duration, and refuses when a live writer\nlease exists, so it cannot race the automated background snapshot\u002Fconsolidation\nthe SessionStart hook fires, and refuses a destination that is not on a local\nfilesystem (no UNC\u002Fnetwork\u002FOneDrive path). If any lock is held it exits **2**\nwithout touching the destination — a skipped restore must never look like a\ncompleted one. A live interactive session's own `add`\u002F`recall` writes are\nblocked for the restore's duration (writers wait on the maintenance gate and\nfail clearly after a short timeout): still run `restore` when no session is\nactively writing.\n\n### Passive-lane credential withhold (issue #256)\nThe hook-driven `recall --for-injection` path re-scans every selected row\nwith the **credential** patterns from the write-time registry\n(`SECRET_CREDENTIAL_PATTERNS` — key=value shapes, PEM headers, `gh*_`\ntokens, AKIA keys; plus whatever shapes issue #180 adds). A matching row is\ndelivered as an id+type-only bullet marked `[WITHHELD: SECRET]`: its\ncontent, source_ref, tags, and entity names never reach the hook payload or\nthe model context, it is not counted in `surfaced_count`, and the delivery\nledger entry for it carries no credential text. The JSON envelope gains\n`secret_withheld` (a count) **only when a withhold happened** — clean runs\nstay byte-identical. Ordinary rows are untouched: rows carrying plain\n40-hex git SHAs render normally (the generic hex\u002Fbase64 detectors are\ndeliberately NOT used at read time). The re-scan runs BEFORE the token\nbudget, so a credential straddling the budget cut is withheld whole — never\nleaked as a clipped fragment — and `--for-injection` applies it on the\nno-session lane too. Since 0.142.0 the explicit lanes are filtered as well:\n`recall`\u002F`recent` reduce credential-shaped rows to the `[WITHHELD: SECRET]`\nstub (PR #440, issue #352 AC3); since 0.144.0 `list` (both the human line and\nthe `--json` payload, including the hub\u002Fremote read route) and `export-pack`\n(body bullets, the pack title, and the empty-pack refusal) reduce\ncredential-shaped rows the same way and blank a credential-shaped namespace\nat render (issue #443). `get` by id remains an unfiltered operator surface.\nSibling envelopes the selector\nbuilds for silent\u002Fkill-switch reasons never carry the key. Known residuals:\na credential stored ONLY as an entity `canonical_name`\u002Falias is not\ndetected — it renders in the fence's entity line and rides into the\ndelivery ledger via the entry text (issue #180-adjacent; purge covers the\nstore rows, not the entity tables, for those shapes); and the withheld\nbullet's metadata (id, type, namespace, confidence, signal) plus the\n`secret_withheld` counter still reveal to the model THAT a secret exists,\nby design. To remove a\nconfirmed secret from the store entirely, use `purge` (#255); rotation of\nany credential that was ever passively delivered remains mandatory.\n\n### rescan-secrets — live secret rescan\u002Fremediation (issue #181)\n```\n\u003Cinterpreter> \u003Cstore.py> rescan-secrets --dry-run | --apply\n```\nRe-evaluates every LIVE row through `apply_capture_policy` (auto mode).\n`--dry-run` prints one compact JSON line (`mode`, `rows_scanned`,\n`rows_needing_review`, ascending `ids`) through a read-only handle — the\nscanned store stays byte-identical. `--apply` redacts each candidate via\n`update`'s append-only lineage (tombstone + `[REDACTED_SECRET]` successor\ncarrying `update_of`) in ONE transaction; any policy refusal or dedup fold\nrolls the whole batch back (exit 2, store untouched). The tombstoned\npredecessor keeps its plaintext (FTS-indexed, `--as-of` reachable) — run\n`purge` (below) on the reported ids for durable removal.\n\n### embedding-health — print embedding lane health as JSON (issue #131)\n```\n\u003Cinterpreter> \u003Cstore.py> embedding-health [--json]\n```\nRead-only vector-lane diagnostic. `--json` prints exactly one compact\nsorted-key JSON line (reason, vector_status, notice_created,\nnotice_present, models_dir, profile, last_successful_embed,\nlast_successful_index, unembedded_backlog) through a read-only store\nhandle — the scanned store stays byte-identical. When the lane is\ndegraded the command creates the once-per-data-dir sentinel\n(`\u003Cdata dir>\u002Fembedding-degraded.notice`, canonical four lines:\nreason\u002Fmodels_dir\u002Fprofile\u002Ftimestamp) via an O_CREAT|O_EXCL write, prints\n`vector lane: degraded (\u003Creason>)` to stderr, and still exits 0 — a\ndegraded vector lane is a supported state; only an unreadable store\nexits 1. The sentinel is never rewritten (the O_EXCL write records the\nfirst degradation in a data directory exactly once), but the\n`vector lane: degraded (\u003Creason>)` stderr line prints on EVERY degraded\nrun.\n\n### purge — durably remove a memory's content (issue #255)\n```\n\u003Cinterpreter> \u003Cstore.py> purge --id \u003Cid> [--id \u003Cid2> ...] [--scrub-backups --out-dir DIR] [--json]\n```\n`update`\u002F`invalidate` **tombstone** a row — the plaintext survives in the row,\nthe FTS index, side tables, derived copies and ledger files. `purge` is the\noperator primitive that actually removes content, and it takes **ids ONLY**.\n**Never pass secret text on a command line in a hooked session**: the evidence\nwriter records tool-call input into `evidence` rows and would re-insert the\nfragment.\n\nWhat one purge does:\n- resolves the id plus its verified `update_of` predecessor chain and deletes\n  those `memory` rows, their FTS entries, their `memory_vec` rows (by\n  `memory_id`) and every id-keyed side-table row (`memory_link`, both\n  endpoints; `memory_entity`, `episode_memory`, `memory_evidence`,\n  `belief_head_source`, `belief_head_evidence`);\n- deletes evidence rows and entity\u002Falias rows the purge orphaned (evidence a\n  surviving row still references is kept);\n- **rewrites, deletes, or refuses** derived copies that carry the text:\n  consolidation keepers (the `\\n\\n--- merged from \u003Cid> ---\\n` block\n  is stripped and entities re-linked), belief heads (rebuilt from surviving\n  sources, or deleted when sourceless), and extractive episode-summary rows\n  (episodes that contained a purged member lose their summary row — expect\n  deletion, not surgical editing). A keeper refuses the whole purge (exit 4,\n  naming the row, nothing deleted) when `merged_from` names a purged id but\n  its stored block header is missing (the content was compressed or drifted),\n  or when an 8+-character token of the absorbed block still appears in the\n  keeper's own text — the byte-verify suppresses needles that a surviving row\n  holds, so that residue could not otherwise be distinguished from a clean\n  rewrite;\n- compacts the store (FTS `'optimize'`, `VACUUM`, WAL checkpoint) and\n  **byte-verifies** the result: a case-insensitive scan of `store.sqlite` and\n  `-wal` must find no copy of the purged content. Exit **5** means the text\n  still lives somewhere outside the memory rows (the message names the\n  needle): typically an evidence row no memory references — the rescan that\n  owns those is issue #181;\n- scrubs the ids (and any needle-bearing entry) from the `\u003Cdata>\u002Fops\u002F*.ledger`\n  delivery ledgers **and the issue-#252 JSONL segments\n  (`\u003Chash>.ledger.seg.\u003Chex>`)**, and removes orphaned `.ledger.tmp.*` and\n  `.signals.tmp.*` partial writes. The\n  `.pending`\u002F`.compact`\u002F`.feedback.jsonl`\u002F`.signals` ops\n  sidecars are NOT scrubbed (outside the ledger contract; `.signals`\n  carries only signal names\u002Fstatuses\u002Fcounts, and the finished file is\n  swept on the shared ops mtime policy) — treat them as residue surfaces.\n  The `\u003Chash>.tasktext` delegation stash IS in scope since issue #253: purge\n  needle-scrubs its entries (they carry untrusted host prose and have no\n  memory ids) and reaps orphaned `*.tasktext.tmp.*`. The residue verify\n  step scans the full delivery-state scope (all eight registered families\n  plus the broad legacy ledger\u002Freserve globs, issue #322), so a stash the\n  scrub SKIPPED because it was unreadable\u002Ftorn (the reader tolerates those\n  states by design) is no longer exempt: needle bytes in any scanned\n  family fail the purge closed; see #336 for the historical verify-glob\n  trade-off;\n- records the ids in the `purged_id` deny-list so `ingest-jsonl` will not\n  re-insert them from a peer export (child records referencing a purged id\n  are skipped or blanked, never fatal);\n- if a post-commit compaction, ledger scrub, or snapshot scrub fails, a later\n  `purge --id` for the deny-listed id resumes the cleanup phases. The retry\n  derives its scrub needles from each surviving snapshot row, so it does not\n  require the original plaintext as a command-line argument. A scrub retry\n  never re-verifies the live store (the purged rows are absent, so no live\n  needles can be derived) and says so instead of claiming a clean verify;\n- `--scrub-backups --out-dir DIR` applies the same removal inside every\n  `store-*.sqlite` **and** `prerestore-*.sqlite` snapshot in DIR, rewriting\n  files **in place** (never deleted, truncated, or renamed) and re-verifying\n  each with `integrity_check`.\n\nVector erasure is fail-closed: when a `memory_vec` table exists, purge must\nload sqlite-vec and verify that no vector rows remain for the deleted ids. If\nthe extension cannot be loaded, the purge refuses rather than claiming the\nvectors were erased. `--scrub-backups --out-dir DIR` also requires DIR to be\nan existing local directory.\n\nLocking: the full `restore` posture (maintenance → schema → backup →\nconsolidate locks, plus a live-writer refusal), so nothing can write between\nthe deletes and the byte verification.\n\nExit codes: **0** purged and verified clean; **2** bad usage; **3** unknown\nid (named); **4** refused (maintenance\u002Fschema\u002Fbackup\u002Fconsolidate lock or live\nwriter); **5** residue remains in the store or ledgers — OR transient\ndelivery-ledger lock contention (re-run; the message names which); **6**\ncompaction\u002Fscrub step failed.\n\nPost-commit cleanup failures leave the deny-list entry in place by design;\nre-run purge for that id after fixing the named surface. `import-store.py\n--force` and `restore` from a pre-purge snapshot are explicit whole-store\nreplacements — they supersede both the removal and the deny-list, so **re-run\npurge after either** (a restore's `prerestore-*` safety\ncopy of the pre-restore store preserves the deny-list rows recorded between\nsnapshot and restore, so the ids to re-purge are recoverable by SQL from that\ncopy). Purge is also honest about a structural carve-out in the byte\nverification: memory content that is a substring of SQLite's own schema DDL\ntext (contrived single-word contents like `namespace`) cannot be\ndistinguished from schema bytes, so no needle is scanned for it. Surviving\nrows that still quote purged-content tokens (e.g. a mid-chain `update`\nsuccessor, kept by design) are listed as WARNINGs in the summary. Exit **5**\nhas two shapes: (a) residue detected after deletion — the deny-list rows are\nalready recorded while content may remain: clean the named surface, then\ndelete those rows from `purged_id` (or re-purge the surviving carrier) so\npeer imports stay denied only while content exists; (b) the delivery-state\nskew refusal (issue #322) — `\u003Cdata>\u002Fops` held delivery-state files from a\nfamily this client does not support, and the purge refused BEFORE touching\nthe store: nothing was deleted, no `purged_id` rows exist, and the named\nfiles must be handled per the refusal's recovery text (upgrade this client,\nor remove the listed files after quiescing writers). Exit **2** also covers\nan invalid `ZMEM_TEST_LEDGER_FAMILIES` value (test seam misconfiguration).\nDetection scope note: the read-time-style scan here covers the row's\ncontent, source_ref and tags; a credential stored ONLY as an entity\nname is not detected by purge's needle scan or by the passive lane's\nwithhold (issue #180-adjacent residual). Keeper rewrite note: a\nconsolidation keeper whose own base text shares words or short phrases\nwith the absorbed text keeps those shared fragments after the strip —\nthey are indistinguishable from the keeper's own vocabulary; the\nfull purged text is always removed or the purge refuses, and the\nbyte-verify's novel-token needles cover genuinely novel residue. Peer\nimports skip evidence rows that were junctioned to a purged memory\n(including their episode junctions), so purged excerpts stay gone.\n\n### sweep — prune stale per-session cooldown sentinels\n```\n\u003Cinterpreter> \u003Cstore.py> sweep [--marker-dir DIR] [--max-age-days 7] [--dry-run]\n```\nRemoves the `.capture-prompted-\u003Csession>` \u002F `.convention-prompted-\u003Csession>`\ncooldown markers the capture\u002Fconvention hooks leave in the data dirs (issue #23).\nA marker is only meaningful for the session named in its filename, so anything\nolder than `--max-age-days` (default `$ZMEM_SENTINEL_SWEEP_DAYS`, else 7) is\ngarbage. Sweeps the union of every dir the two hooks can write into (their\nresolution chains differ), never touching anything that is not a sentinel-prefixed\nfile. Idempotent and fail-open. The SessionStart hook fires it detached each\nsession so the markers stay bounded; `--dry-run` counts without deleting.\n\n### corrections — mine user corrections from a transcript (read-only)\n```\n\u003Cinterpreter> \u003Cstore.py> corrections --transcript \u003Cpath> [--json]\n```\nScans a Claude Code transcript JSONL for user corrections. (User *rejections*\nare a separate signal — reported by `store.py failures` and surfaced by the\nStop \u002F SubagentStop reflection hooks.) Prints `{\"count\": N, \"items\": [{message,\ntype, patterns, confidence, sentiment, decay_days}]}`. **Read-only** — this\ncommand never opens or writes the store; candidates are reviewed by an\nagent\u002Fhuman before any `add` (signal honesty). Corrections are detected by the\nported correction-pattern library (`skills\u002Fmemory\u002Fscripts\u002Fcorrections.py`, from\nthe MIT claude-reflect project). This parses **Claude Code transcript format\nonly**; other hosts' histories are out of scope. In `ZMEM_CAPTURE_MODE=auto`\nlikely-secret text is redacted; in `manual` matching items are annotated\n`\"secret_warning\": true` but kept verbatim for review. (`--json` is accepted for\nparity with the issue's syntax; output is always JSON.)\n\n### signals — recognized runner signals for a session (read-only)\n```\n\u003Cinterpreter> \u003Cstore.py> signals [--session SESSION] [--transcript PATH] [--db PATH]\n```\nReports the session's tracked runner signals as\n`{\"signals\": {\"test\": \"pass\", \"lint\": \"fail\", ...}}` — the LAST status of\neach recognized runner family in the transcript, classified by\n`capture_quality.infer_signal` (`test`\u002F`compile`\u002F`lint`; everything else,\nincluding the db substrate where `tool_usage` carries no command text, is\nuntracked). A user-REJECTED runner call records no signal (the run never\nexecuted — same rejection split as `failures`), and an error-shaped\n`toolUseResult` sibling counts as `fail` even when the block's `is_error`\nflag is false. This is the read command behind the Stop hook's\nsignal-change gate (issue #258). **Read-only and store-independent** like\n`failures` — dispatched before `connect()`. Exit-code contract: checked results\n(including a missing transcript path, which falls to the db leg) exit 0; a\ngenuine substrate error (transcript present but unreadable) prints\n`{\"signals\": {}, \"error\": ...}` and exits 2, so the hook can suppress\nwithout touching its persisted state.\n\n### source-exists \u002F ops-append — capture adapter bridges\n```\n\u003Cinterpreter> \u003Cstore.py> source-exists --namespace NS --source-ref REF --json\n\u003Cinterpreter> \u003Cstore.py> ops-append --namespace NS --session-id SESSION --tool TOOL --operation OP --json\n```\n`source-exists` prints `{\"exists\":false}` with exit 0 for a missing store and\notherwise opens the store read-only without creation or migration. `ops-append`\nprints `{\"ok\":true}` after appending the bounded operation-ring record; a\nno-token operation is the declared successful no-op (issue #163).\n`--namespace` is REQUIRED (old invocations without it exit 2 with no stdout\npayload). Invalid arguments or unavailable state print `{\"ok\":false}` and\nreturn nonzero; adapters translate that through their documented fail-open\nenvelopes. With `memory.provider=zmem` the plugin's native post_tool_call \u002F\npre_llm_call \u002F pre_verify callbacks run BESIDE any installed compatibility\nshell hooks (both paths are operational; suppression is operator-managed). Neither command authorizes a hook to\nimport store internals directly.\n\n`ZMEM_CONVENTION_INTERVAL` remains the legacy cadence for the Hermes\nconvention-compatibility hook only. It does not control the commit-only\n`zmem-convention-capture.sh` prompt.\n\n### queue-list \u002F queue-clear \u002F queue-add — review live-captured corrections (read-only \u002F clear \u002F author a note)\n```\n\u003Cinterpreter> \u003Cstore.py> queue-list --namespace NS [--json]\n\u003Cinterpreter> \u003Cstore.py> queue-clear --namespace NS [--id ID ...] [--all] [--drop-stale]\n\u003Cinterpreter> \u003Cstore.py> queue-add --namespace NS --message TEXT --type lesson \\\n  [--patterns P] [--confidence 0.7] [--sentiment note] [--decay-days 90] [--json]\n```\nThe `capture-correction` hook (UserPromptSubmit, Claude Code \u002F ZCode \u002F Codex)\nqueues mid-session user corrections (\"no, use X\", \"remember: ...\") into a\nnamespace-scoped sidecar file (`\u003Cstore-data-dir>\u002Fqueue\u002F\u003Cns>.json`, default\n`~\u002F.zmem\u002Fqueue\u002F\u003Cns>.json`; follows the store's `ZMEM_STORE`\u002F`ZMEM_DATA` override\nchain, not a fixed path) — these recall\u002Fcapture hooks never `add` rows and\nwrite nothing else to the store themselves (the one sanctioned exception is\nthe DETACHED SessionStart `session-cadence` maintenance task, which runs\norganize\u002Fbackup\u002Fsweep on its own lease). `queue-list` shows the pending candidates for review (emit them into the\nstore via `add --signal user` only if they clear the closeout rubric);\n`queue-clear` removes processed ids (`--id`), empties the queue (`--all`), or\nprunes stale low-confidence items (`--drop-stale`) — exactly one selector is\nrequired (a flag-less invocation is rejected, not a silent full wipe). All three are store-independent\n(they never open or write the store), so they work even if the store is locked or\nmissing.\n\n`queue-add` (issue #260) is the write-side sibling: a closeout session can jot\na cheap, low-ceremony note for a FUTURE closeout to review — cheap now, fully\nprocessed later — instead of running the full `add` ceremony (embedding,\ndedup\u002Fsupersede matching) on a note whose whole point is to be reviewed first.\nIt appends through the same capture queue machinery the hook uses (same\natomic write, same `MAX_QUEUE_SIZE` oldest-drop cap) and stamps\n`source: \"closeout-note\"` so `queue-list` consumers can tell a\ncloseout-authored note from a live-captured (`source: \"live-capture\"`) or\nmined (`source: \"history-mine\"`) candidate. Secret redaction covers the whole\nnote: the message follows the capture policy (`ZMEM_CAPTURE_MODE` — auto\nstores the redacted form, manual keeps the original with `secret_warning`),\nand the free-text `--patterns`\u002F`--sentiment` metadata is always stored\nredacted (flagging `secret_warning` when redaction fired). Input guards\nmirror the promotion path: `--message` is bounded by the store's\n`MAX_CONTENT_CHARS`, `--confidence` must be a finite value in [0, 1], and\n`--decay-days` must be at least 1 so every item stays reachable by\n`queue-clear --drop-stale`. Because `--message` rides the process command\nline, route secret-bearing content through the `capture-correction` hook\n(stdin transport) rather than pasting tokens into a note. A `queue-add` item\nis reviewed under the SAME closeout Step 0.5 rubric as any other queue item —\nthere is no fast track; `--type` accepts exactly the `add` command's type\nenum, so an accepted note needs no type conversion when it is promoted.\n\n### retain \u002F retain-adapt — evidence-gated automatic retention (issue #177, default off)\n```\n\u003Cinterpreter> \u003Cstore.py> retain [--input PATH|-] --namespace NS --session ID \\\n  --host claude|codex|zcode|hermes-provider|hermes-compat [--dry-run]\n\u003Cinterpreter> \u003Cstore.py> retain-adapt --input PATH|- --namespace NS \\\n  --session ID --host claude|codex|zcode|hermes-provider|hermes-compat\n```\nThe automatic-retention surface (Workstream K PR 6). `retain` classifies\nnormalized JSONL evidence rows through the deterministic extractor\n(`storelib\u002Fretain.py::extract_candidates`) into REVIEW-QUEUE CANDIDATES —\nnever memory rows: explicit `remember:` rows (class `explicit`), decisions\ncarrying a rationale marker (`because` \u002F `so that` \u002F `therefore` \u002F\n`rationale:` \u002F `reason:`, class `decision`), `Actually,`-style corrections\n(class `correction`, priced at the classifier's pattern-table confidence —\nthe live-capture short-message boost deliberately does not apply), and\ntwice-repeated runner errors (class `outcome` via `aggregate_errors`). Every\ncandidate keeps the full legacy review shape plus `class`, `evidence_ids`,\n`signal=none`, a stable 32-hex SHA-256 id, and a `retain_hash`; writes go\nthrough the same capped, atomic queue sidecar as manual capture (a failed\nwrite keeps prior writes and reports\n`[zmem] retain: queue write failed; written=\u003Ck-1>; total=\u003CN>`).\n\n**The gate (default OFF):** retention runs only when `\u003Cstore_dir>\u002Fconfig.json`\n(the file sits beside the store file; the queue itself lives in a `queue\u002F`\nsubdirectory beside it — read by the store-side `storelib\u002Fconfig.py` reader)\ncontains a literal boolean `{\"auto_retain\": true}`, or `ZMEM_AUTO_RETAIN=1`\nforces it on. Exact `ZMEM_AUTO_RETAIN=0` forces off; any other non-empty value\nprints `[zmem] retain: invalid ZMEM_AUTO_RETAIN` with\n`{\"count\":0,\"ok\":false}` and exits 0. A non-boolean `auto_retain` in\n`config.json` is REFUSED and leaves the gate CLOSED — a config meant to\ndisable retention cannot enable it. Exact `ZMEM_CAPTURE=0` is byte-passive — no\ninput read, no queue file, no store file. The Hermes provider's `auto_retain`\nconfig key (issue #161) is a SEPARATE provider-home setting that `sync_turn`\nconsults BEFORE spawning a child; the store subprocess never reads provider\nhome. Both commands dispatch before the store opens: a disabled run never\ncreates or migrates the store, and the stage driver checks the gate BEFORE it\nreads the transcript, so a default install never parses a transcript. The\nStop\u002FSessionEnd seams still start one interpreter to evaluate the gate before\ndeclining, because the shell cannot read the store-side config without one.\n\nBoth retention verbs answer with `{\"count\":N,\"ok\":bool}` on stdout. `ok` is\nfalse on a partial write, a classify failure or an invalid override, so a\ncaller can tell a successful no-op from a failure even though every hook path\nstill exits 0.\n\n`retain-adapt` normalizes a raw host payload (Claude transcript records —\n`isMeta` may be absent and `message.content` may be block-form —, Codex\n`id`\u002F`timestamp`\u002F`role`\u002F`content` records, ZCode\n`event_id`\u002F`ts`\u002F`role`\u002F`text` records, or a Hermes payload) into evidence rows\nstamped with the CLI provenance triple. Timestamps are accepted with or\nwithout millisecond precision and canonicalized to `...THH:MM:SSZ`.\n\n**Host wiring — which lanes share the subprocess seam:**\n- Claude\u002FCodex\u002FZCode Stop and Claude SessionEnd run\n  `hooks\u002Flib\u002Fzmem-retain-process.py` (10s per-child deadline, exit 124 on\n  timeout), which spawns `store.py retain-adapt` then `store.py retain`.\n- The Hermes provider (`sync_turn`, `on_pre_compress` checkpoint API v2) calls\n  `store.py retain` DIRECTLY with its own duplicate deadline copy; it does not\n  import the shared seam.\n- The Hermes compatibility `pre_llm_call` hook uses `subprocess.run` with its\n  own `ZMEM_MCP_TIMEOUT` clamp (8s default), not the shared seam.\n- The shared SessionEnd script does NOT run on the Codex SessionEnd lane — the\n  Codex fast path returns before that script spawns — so the retention stage\n  there is currently unreachable for Codex.\n\nHooks and providers consume child JSON only — no storelib import, no SQLite,\nno direct queue access. Every failure path exits 0 so a retention problem can\nnever break a host hook. Diagnostics are appended to a `zmem-retain.log`\nsidecar in the store data directory and echoed to stderr where the host\npreserves it — the Stop hook suppresses its own stderr, so the sidecar is the\nchannel that always survives.\n\nRetention is idempotent: candidates are keyed by a stable id, already-present\nids are skipped, and the batch runs under a per-namespace lock, so repeated\nStop\u002FSessionEnd fires over one transcript add nothing on the second run. The\n`remember:` classifier is unanchored, so material that reads as quoted or\nmachine-authored (a pasted answer, a project instruction file, a diff) is\nskipped rather than promoted as the user's own standing fact. The `decision`\nclassification needs a `kind` field no wired adapter emits, so it is\ncurrently reachable only by callers that set `kind` explicitly.\n\n### convention-drift — surface conventions a queued correction contradicts (read-only)\n```\n\u003Cinterpreter> \u003Cstore.py> convention-drift --namespace NS [--json]\n```\nRead-only drift scan (issue #261): loads the namespace's queued correction\nitems (the same sidecar `queue-list` reads — every item, regardless of its\n`stale` flag) and the namespace's live rows, keeps `type == \"convention\"`\n(filtered in Python — `list` has no `--type` flag), and reports each\nconvention row some queued item's message CONTRADICTS: the write-time dedupe\nguard's negation-polarity signal (polarity flip) plus at least one shared\ncontent stem (the stem term is specific to this scan; token-disjoint notes\nnever flag). `--json` emits `{\"candidates\": [{\"id\": ...}, ...]}`; plain mode\nprints one `- \u003Cid> :: \u003Cpreview>` line per candidate, or\n`(no convention-drift candidates)` when there are none; both modes exit 0.\nOn an existing store it writes nothing — no invalidate, update, supersede,\nor telemetry, and the store file stays byte-identical (a missing store is\ncreated by the ordinary open path, exactly like `list`) — surfaced ids are\nupdate candidates for the reviewer's deliberate `update`\u002F`invalidate` at\ncloseout (false positives are acceptable; a missed real contradiction is the\nfailure mode it minimizes). The queue loader fails open: a missing or\ncorrupt queue file reads as an empty result. Unlike the queue family it\nopens the store (it reads live rows), so it dispatches after connect like\n`list`; it is exempt from the automatic near-miss rekey (a pure-read surface\nmust not trigger the rekey's writes). Works without an embedding model — the\npredicate is lexical only.\n\n### export-pack — render a Tier 1 markdown memory pack\n```\n\u003Cinterpreter> \u003Cstore.py> export-pack --namespace NS [--out FILE] [--project-limit 50] \\\n  [--global-limit 15] [--min-confidence 0.6] [--max-bytes 32768]\n```\nRenders live memories from `--namespace` and `user:global` (confidence DESC,\nretrieval_count + surfaced_count DESC, ingestion_ts DESC) as a hand-off markdown pack,\ne.g. for a cloud\u002Fremote session with no store access. `--max-bytes` is a UTF-8\nbyte budget over the whole rendered pack (structural framing counts toward\nthe cap): a bullet that would push past it is omitted whole (never\ntruncated), smaller rows behind it are still emitted, and only framing\nappended after the walk (an empty section's heading and the trailing\nomitted-count note) can exceed the budget. Refuses (exit 2) if\nboth sections are empty.\n\n### export-jsonl — export Tier 3 sync JSONL\n```\n\u003Cinterpreter> \u003Cstore.py> export-jsonl [--out FILE] [--namespace NS] [--include-superseded]\n```\nWrites one memory row per line (no embeddings) for box-to-box sync via\n`ingest-jsonl`. Default: all namespaces, live rows only; `--namespace` scopes\nto one namespace, `--include-superseded` also exports tombstoned rows.\nEntity links are deliberately NOT carried in the JSONL (they are store-local\nderived data, like embeddings and content_norm): the receiving store rebuilds\nthem by re-running the deterministic extractor on ingest — see below.\n\n### Governed training capture and export (issue #135)\n\nAutomatic Claude, Codex, ZCode, and Hermes hooks may create a partial training\ncapture. Capture governance defaults to deny and a denied or missing policy\nstores metadata only; local capture consent permits redacted partial storage\nbut does not authorize export. A capture is exportable only after the actual\ninjection result has been snapshotted, a trusted workflow has acknowledged the\ndelivery, and a verifier has completed the capture with a supported outcome,\nevidence id, scoped memory ids, current export governance, and any required\nreviewer correction fields. Hook observations, Stop, tool success\u002Ffailure,\nand displayed text never complete a capture.\n\nCompletion evidence must match the outcome: test, compile, and lint require\ntest_result evidence; user_acceptance requires turn evidence; reviewer_acceptance\naccepts turn or correction evidence. correction_closeout requires\nreviewer_acceptance with correction evidence.\n\nThe explicit trusted local adapters accept reviewed JSON input and keep the\nstate transitions separate:\n\n```bash\n\u003Cinterpreter> \u003Cstore.py> capture-training-delivery --input delivery.json\n\u003Cinterpreter> \u003Cstore.py> capture-training-acknowledge --input acknowledgement.json\n\u003Cinterpreter> \u003Cstore.py> capture-training-completion --input completion.json\n```\n\nEvery reviewer-acceptance outcome requires a separate local review transition,\nincluding turn evidence. Reviewer-acceptance corrections additionally require\ncorrection evidence:\n\n```bash\nZMEM_TRAINING_CALLER_ID=operator-a \\\nZMEM_TRAINING_REVIEWER_IDS=operator-b \\\n\u003Cinterpreter> \u003Cstore.py> capture-training-review --input review.json\n```\n\n`ZMEM_TRAINING_CALLER_ID` is the local caller identity and\n`ZMEM_TRAINING_REVIEWER_IDS` is the comma-separated reviewer allow-list. The\nreviewer must differ from the verifier. These values protect the local\nworkflow against accidental self-review; they do not provide an external\nreviewer service or a boundary against a hostile local process. Capture ids,\nsession keys, and optional host task ids are correlation metadata only.\n\nThe delivery response contains the local `capture_id` and immutable\n`delivery_snapshot_id`; later adapters resolve the capture through the snapshot\nid. `host_task_id` is correlation metadata only when the host supplies it.\nReplayed compatible inputs are idempotent, while conflicting snapshots or\ncompletion data are refused. The current redaction masks credential-like,\nemail, and filesystem-path classes; it is not comprehensive PII anonymization.\nReviewer trust on this local completion path means a local operator's\nattestation; no external reviewer registry is implied.\n\nFinalized and revoked capture records, their local delivery\u002Fcompletion\u002F\nobservation rows, remain for 30 days. The detached `session-cadence` task\nattempts the purge before its backup step; a successful purge keeps expired\nrecords out of new backups. Doctor reports an intentional expired-retention\nwarning with output status `warn` and stays read-only; an operator can run the\nexplicit cleanup instead:\n\n```bash\n\u003Cinterpreter> \u003Cstore.py> purge-training-captures --confirm\n```\n\nBefore enabling export, install PyArrow in the store-host interpreter:\n\n```bash\npython -m pip install --disable-pip-version-check \\\n  -r skills\u002Fmemory\u002Fscripts\u002Frequirements-training.txt\n```\n\nThe Hermes server environment can install its packaged MCP requirements without\ninstalling the optional exporter:\n\n```bash\npython -m pip install --disable-pip-version-check \\\n  -r hermes-plugin\u002Fserver\u002Frequirements.txt\n```\n\nInstall `requirements-training.txt` separately in the interpreter that runs\n`export-training`; the training extra pins PyArrow `25.0.1` for deterministic\nParquet bytes.\n\nThe read-only doctor reports the exact requirements-file command when PyArrow\nis unavailable. Reload the host after updating a plugin cache and run doctor\nbefore exporting.\n\n### export-training — write reviewed SFT and preference views\n\n```bash\n\u003Cinterpreter> \u003Cstore.py> export-training DIR --snapshot-id ID \\\n  --reviewer-confirmed [--namespace NS] [--quarantine-raw]\n```\n\n`--snapshot-id` selects the immutable source export snapshot and\n`--reviewer-confirmed` is a required export authorization. Both are checked\nbefore SQLite opens or output is created. `DIR` is the training output\ndirectory itself and contains the fixed `sft-000.parquet`,\n`preferences-000.parquet`, `manifest.json`, and `deletion-map.json` artifacts\ndirectly. `deletion-map.json` is derived deduplication and exclusion metadata;\nit does not delete canonical SQLite rows or prior operator-owned output\nfolders. Empty eligible selections still produce schema-only Parquet files. A\nmissing PyArrow dependency or unavailable local model for a nonempty semantic\nexport fails closed before publish.\n\n`--quarantine-raw` writes a separate bounded, redacted derived dump under\n`DIR\u002Fquarantine\u002F` with `DIR\u002Fquarantine-manifest.json`. It uses the same\nverified, acknowledged completion gate as the Parquet views; partial, denied,\nrevoked, incomplete, or capture-quarantined records never reach any derived\nartifact. Training vectors are in-memory only and never sync or back up.\nOutput is published from a validated same-volume staging directory, with one\nadvisory per-output lock and the final manifest written last; consumers must\nreject output without a valid manifest. Parquet, quarantine, deletion-map, and\nstaging files are operator-owned disposable artifacts outside canonical-row\nretention; purging SQLite capture rows does not delete prior published output\nfolders. Doctor reports abandoned staging directories but is read-only by\ndefault. After confirming no export is running, an operator can\nexplicitly remove only stale direct, non-symlink `.training-staging-*` siblings\nof `DIR`:\n\n```bash\npython \u003Cdoctor.py> --project . --training-output .\u002Ftraining \\\n  --cleanup-training-staging --confirm-no-training-export\n```\n\nThe cleanup skips candidates newer than 24 hours and rechecks mtimes before\nremoval. For `.\u002Ftraining`, a candidate is `.\u002F.training-staging-*`. The remedy\nfor an expired-retention warning is `\u003Cinterpreter> \u003Cstore.py>\npurge-training-captures --confirm`, followed by another doctor run. The\nexporter bounds work at 10,000 source captures by default. Set\nZMEM_TRAINING_MAX_ROWS to a lower positive limit when needed; exceeding the\nlimit fails before publication and leaves no new manifest. The exporter has no\ncross-process lock that doctor could use to prove an export is\ninactive, so the confirmation flag is an operator assertion. Revoked captures\nare reported as an informational count and remain ineligible for export.\n\nv13 (issue #65, 10.7): every row carries a `kind` discriminator (`\"memory\"`);\nepisodes round-trip as additional `\"episode\"` and `\"episode_memory\"`\nrecords when any exist (memberships are emitted only when both endpoints are\nin the exported set, so the file is self-consistent). `ingest-jsonl` treats\na missing `kind` as `\"memory\"` (legacy files ingest unchanged) and applies\nrecords in dependency order (memory → episode → membership) regardless of\nfile order; re-ingesting the same file is an exact no-op.\n\n### ingest-jsonl — import Tier 3 sync JSONL\n```\n\u003Cinterpreter> \u003Cstore.py> ingest-jsonl --in FILE [--source-ref REF] [--allow-tombstones] [--capture-mode auto|reviewed|manual] [--trust authoritative|untrusted-peer] [--allow-namespace NS ...] [--attribute-host HOST]\n```\nImports a JSONL file written by `export-jsonl`. Every row is validated before\ntouching the store, and a bad row is counted and reported by line number\nrather than aborting the file. For files whose sender is NOT authoritative\nover this store, pass `--trust untrusted-peer` with one or more\n`--allow-namespace` values (issue #282): trust claims in the rows are\nclamped (taint, signal grounding kept only as `claimed_signal:` tags,\nconfidence\u002Ftrust_score ceiling 0.6), `host:`\u002F`agent:` attribution and\nsource provenance are stamped from the caller (`--attribute-host`,\n`peer:\u003Chost>`), every record kind is gated against the allow-list (evidence\nrecords refused outright; tombstone authority refused), and the peer-mode\nsummary line carries `namespace_refused=`, `authorship_refused=` and\n`trust_clamped=` counts (`authorship_refused` counts side records aimed at a\nrow this import did not author — a peer is never authoritative over a row it\ndid not land, though restating a lifetime the store already holds is a no-op;\nabsent trust_score resolves 1.0, so trust_clamped fires on most peer rows\nby design — it reports \"the policy altered this row\"). `--source-ref` overrides source_ref on every\nrow inserted this run (default: keep each row's own incoming source_ref).\n`--allow-tombstones` lets an incoming superseded row tombstone a live local\nrow with the same id — off by default; use it only when the file is an\nexport of a store you trust as authoritative for those ids.\n\nv11 (issue #61) notes: rows carry `trust_score` (restored verbatim, default\n1.0 for pre-v11 files; a PRESENT but non-numeric\u002Fnon-finite value makes the\nrow malformed — refused, counted, not stored) and outgoing `links` (validated fail-closed, applied\nafter every row lands so an edge may reference a later row;\n`links_added`\u002F`links_skipped` in the summary). Ingest deliberately applies\nNO trust deltas and NO auto link generation — it is deterministic state\nrestore, so re-ingesting the same file is an exact no-op (the +0.05\ncorroboration is an organic `add`\u002F`update`-only signal). The dedup path DOES\napply the write-time polarity guard: a remote row that contradicts its\ndedup hit inserts as its own live row instead of merging (contradictions\nnever merge, on any surface).\n\n`--capture-mode` routes every inserted row through the same capture policy as\n`add` (a sync file is remote-authored data that can otherwise plant a poisoned\nmemory surfacing verbatim into model context, or store secret-like text). The\ndefault resolves like `add` (`ZMEM_CAPTURE_MODE` env or `manual`): verbatim\ncontent with prompt-injection-risk tagging (a row matching an injection\npattern is tagged `prompt-injection-risk` so it can be reviewed, in ALL modes;\nissue #82 widened the pattern set with four high-precision\ninstruction-to-the-model shapes — role hijack (\"you are now the ...\"),\nconcealment (\"do not mention this\"), instruction-override paraphrases\n(\"ignore all previous rules\u002Fguidelines\"), and store-mutation imperatives\n(\"update your knowledge base \u002F the memory store\") — precision-first, so\nordinary coding lessons like \"update the lockfile\" never tag; the read path\nre-classifies EVERY row at emit time regardless of store age).\nUse `--capture-mode auto` when ingesting an untrusted\u002Fremote sync file: it\nadditionally redacts secret-like VALUES in content\u002Ftags (value-span\nreplacement, tagged `auto-redacted`) and QUARANTINES rows whose `source_ref`\nlooks like a secret (issue #180: counted as `quarantined` in the summary —\nafter `capture_refused` — with the original row appended to\n`\u003Cstore-dir>\u002Fquarantine\u002F\u003CUTC-date>.jsonl`, NOT stored; the run still exits 0).\n`reviewed` RAISES on those refusals (counted as `capture_refused`, never\nquarantined — plain `ingest-jsonl` still exits 0 with the count since the\nfile keeps going; exit 2 applies via `--strict` or the `add`\u002F`update` CLI\npaths); `manual` keeps the original text with an advisory notice.\nA quarantine write failure counts `quarantine_failed`, stores nothing for\nthat row, and exits 1.\n\nIssue #71 F: in `auto` mode, `source_ref`s with a structured provenance\nscheme — `db:`, `hindsight:`, `session:`, `zmem-queue:`, and `file:` with a\nRELATIVE path (or a well-known stem like `codex-MEMORY.md`) — skip the\ngeneric hash-shape refusal (a 32-hex db row id is legitimate provenance).\nCredential shapes (key=value pairs, PEM headers, `gh*_`\u002FAKIA tokens) still\nrefuse on allowlisted refs, `file:` absolute remainders still refuse, and\ncontent\u002Ftags scanning is unchanged. The write result carries a structured\n`source_ref_allowlisted` warning so the relaxation is visible.\n\n### Capture policy and quarantine (issue #180)\n\n`apply_capture_policy` (storelib.write) is THE single capture-policy entry\npoint. Every importer routes through it: CLI `add`\u002F`update` (via\n`add_memory`\u002F`update_memory`), `ingest-jsonl` and `ingest-jsonl --strict`,\n`import-store.py`, `rescan-secrets` (the store-wide rescan\u002Fremediation\nscan), `scripts\u002Fingest_harvest.py`, MCP `add`, Hermes `zmem_add`\n(the last three via the `store.py` subprocess boundary — no adapter\nre-implements pattern matching). The pattern registry lives in\n`redaction.py` as the only source; `redact_text` and the read-time\ncredential withhold (`recall`'s classifier) iterate the same lists.\n\nDetectors and span semantics — value-span patterns replace ONLY the secret\nvalue and keep the surrounding syntax; everything else whole-match-replaces:\n\n| shape | matches | does NOT match |\n|---|---|---|\n| `sshpass -p \u003Cvalue>` (any whitespace, incl. tabs) | value replaced | dangling `sshpass -p` |\n| `sudo -S` | whole-row refusal (`unredactable_secret`) | `sudo -s` (flag case matters) |\n| `--password=\u003Cvalue>` (attached) | value replaced | `--password \u003Cvalue>` spaced; bare `--password` |\n| `ssh`\u002F`mysql`\u002F`psql` `-p\u003Cvalue>` (attached) | value replaced | `-P\u003Ctoken>`; other commands' `-p`; spaced `-p \u003Cvalue>` |\n| `scheme:\u002F\u002Fuser:password@host` | password replaced | URLs without userinfo |\n| key=value (`password=`, `api_key=`, … compound names like `DB_PASSWORD=` included) | value replaced (8+ chars) | bare keywords with no `[:=]` |\n| PEM block (BEGIN…END) | whole block replaced | — |\n| token prefixes (`gh*_`, `sk-`, `AKIA`, JWT, …) | whole token replaced | — |\n\nKnown accepted over-redaction (issue-mandated matrix, fail-safe direction):\nfor `ssh` and `psql` the lowercase `-p` is actually the PORT flag, so\n`ssh -p2222 user@host` IS redacted as if the port were a secret; the matrix\npositives (`ssh -ppw180F`) require it. Do not \"fix\" this without changing the\nissue contract.\n\nCommand names and URL syntax match case-insensitively; option letters are\ncase-SENSITIVE (`-S` vs `-s`, `-p` vs `-P`). `[REDACTED_SECRET]` is\nidempotent: a value already equal to the marker is detected but never\nrewritten, so re-running auto mode on a redacted row is a fixed point for\ncontent, source_ref, and tags (warnings are identical for inputs whose\ndetection set is stable across a re-scan — every issue fixture row; a\nplaceholder value long enough to trip a second pattern re-counts, and an\noutput matching no pattern emits no redaction warning). Known safe-direction\ncaveat: a URL of the shape `scheme:\u002F\u002Fhost:port\u002Fa@b` over-redacts the\n`port\u002Fa` span, and an attached `ssh -p\u003Cport>` is treated as a password\n(both are the price of the issue's exact matrix) — and because the\nread-time credential classifier consumes the same shapes, such a stored\nrow is ALSO withheld from passive injection (issue #256's read-time\ndefense), not merely masked at write time.\n\nQuarantine contract: an auto-mode refusal with reason\n`source_ref_secret_like`, `source_ref_unsafe_path`, or\n`unredactable_secret` is quarantineable. The importer that caught it\nappends the ORIGINAL row to `\u003Cstore-data-dir>\u002Fquarantine\u002F\u003CUTC-date>.jsonl`\n(the directory of the resolved STORE — every importer writer resolves the\nsame sink, never a divergent `ZMEM_DATA`; directory 0o700, file 0o600) —\none compact JSON object per line, keys in order `quarantined_at`,\n`reason`, `source_ref`, `row` — via `quarantine_import_row` (write\nfailures raise `OSError`: the row is rolled back, counted\n`quarantine_failed`, and the run exits nonzero). The store's writer lease\ndoes NOT cover the quarantine file; writers serialize on an exclusive\nadvisory file lock (`fcntl.flock` on POSIX, `msvcrt.locking` on Windows)\nheld across each append — bare `O_APPEND` and the buffered text-mode\n`open(\"a\")` idiom both measurably dropped or tore records under concurrent\nWin32 writers (review-round probe). Review quarantine files with\nrestricted filesystem access.\n\nImporter results and counters:\n- `store.py add --capture-mode auto --json` prints\n  `{\"id\": null, \"result\": \"quarantined\", \"warnings\": [{\"type\": \"quarantined\", \"reason\": \"\u003Creason>\"}]}`\n  and exits 0 on a successful quarantine; reviewed-mode and namespace\n  refusals exit 2 with `[zmem] capture policy refused: \u003Creason>` (namespace\n  refusals keep their prose guidance; `manual` never raises on capture\n  grounds — namespace validation can refuse in any mode).\n- `ingest-jsonl` summary carries `quarantined=` and `quarantine_failed=`\n  after `capture_refused=`; `quarantine_failed>0` exits 1.\n  `ingest-jsonl --strict` treats a quarantined row as a rejection\n  (all-or-nothing stays fail-closed).\n- `import-store.py` stages a sanitized copy (source read-only\n  `mode=ro`+`query_only=1`; every memory row through the policy in id\n  order; refused rows deleted with their related rows — belief heads\n  sourced only by a refused row are deleted with it, shared heads rebuild\n  from a surviving source — and quarantined; redacted rows keep ids with\n  FTS maintained by trigger and stale `memory_vec` embeddings dropped),\n  verifies `PRAGMA integrity_check` and\n  `staged_count = source_count - quarantined_count`, flushes its refusal\n  records (one locked, all-or-nothing batch; records the ledger already\n  holds — compared without the per-call timestamp — are skipped, so\n  re-imports never duplicate them) BEFORE the destination is touched, and\n  only then atomically replaces the destination (`os.replace`); the\n  source's sha256\u002Fsize\u002Frow-count are proven unchanged before and after.\n  Its report carries `added`, `redacted`, `quarantined`,\n  `quarantine_failed`.\n- `scripts\u002Fingest_harvest.py` subprocesses\n  `store.py add --capture-mode auto --json` per row (a record may carry its\n  own `source_ref`; the CLI `--source-ref` is the fallback), classifies\n  each child result as `stored`\u002F`deduped`\u002F`quarantined`\u002F`failed`, and\n  reports all four counts; any `failed` exits 1.\n- MCP `add` and Hermes `zmem_add` forward the store envelope verbatim —\n  `result: \"quarantined\"`, `id: null`, one structured warning — and never\n  expose the local quarantine path, the source row, the credential value,\n  or any filesystem detail.\n\nOperator runbook (a stored credential is captured — the field trigger was\nrow `b513b21c`): invalidate the row with reason `credential leak`; rotate\nthe affected SSH password AND every credential that shared it; search\nbackups, exports, quarantine files, and logs for copies; verify the row is\ntombstoned and absent from live recall; never print the credential; review\nfuture whole-row quarantine records with restricted filesystem access.\nInvalidate tombstones rather than purges (durable purge is #255); read-time\ncredential withhold on the passive lane is #256. This runbook is\noperator-only and never targets a test store.\n\nEvery ingested row ALSO runs the deterministic entity extractor (the same\none `add` uses), so entity identity is rebuilt locally instead of carried\n(v10, issue #60): two stores ingesting the same rows derive the same\nentities, keyed by normalized alias, with no cross-store id collisions.\n\n### Evidence (schema v14, issues #169\u002F#170)\n\nEvidence is bounded observation data, not instructions for the model. The\nadditive `evidence`, `episode_evidence`, and `memory_evidence` tables use the\nclosed lane\u002Fmoment\u002Fkind sets and second-precision UTC timestamps.\n`evidence write` validates the payload before insert; it redacts first —\nexcept the flagged delegation mode (issue #164), whose validated digest-only\npayload skips redaction — caps the\nfinal excerpt at 400 Unicode characters, hashes `kind|ts|final_excerpt`, and\nleaves the transaction commit to its caller. The strict JSONL importer\nverifies a row's hash against its exported excerpt verbatim first, so\nredaction-exempt rows round-trip. The CLI writer commits its own\none-row transaction. When the zmem provider is active (issue #163), the plugin natively registers\n`post_tool_call`, `pre_llm_call`, and `pre_verify`. The registered\n`post_tool_call` sends a bounded private payload through the existing local\nstore bridge and fails open on malformed input, unavailable host support,\nqueue saturation, or writer failure; `pre_llm_call` and `pre_verify` complete\nthe #163 provider surface (transport recall with strict envelope validation,\nand the once-per-session reflect-before-stop nudge). Since issue #164,\n`on_delegation` records each subagent delegation as one bounded, fail-open\nevidence write (task\u002Fresult SHA-256 digests plus child\u002Fparent session ids).\nAll of these fail open.\n\n```text\n\u003Cinterpreter> \u003Cstore.py> evidence write \u003C payload.json\n\u003Cinterpreter> \u003Cstore.py> evidence write --kind delegation --session-id PARENT --lane hermes-provider --moment subagent --ref-path delegation:PARENT:CHILD \u003C payload.json  # issue #164; answers {\"id\": ..., \"ok\": true}\n\u003Cinterpreter> \u003Cstore.py> evidence list --namespace NS [--session-id SID] [--lane LANE] [--moment MOMENT] [--json]\n\u003Cinterpreter> \u003Cstore.py> evidence show --namespace NS --id UUID [--json]\n\u003Cinterpreter> \u003Cstore.py> evidence for MEMORY_ID --json\n\u003Cinterpreter> \u003Cstore.py> evidence for --memory-id MEMORY_ID --json  # legacy alias\n\u003Cinterpreter> \u003Cstore.py> evidence scoped-show --namespace NS --id UUID [--json]\n\u003Cinterpreter> \u003Cstore.py> evidence associations --id UUID --json  # hidden operator helper\n```\n\nThe required `--namespace` argument on legacy CLI `list`\u002F`show` is a\ncompatibility\u002Fcontext marker and is ignored: evidence rows have no namespace\ncolumn, so this unscoped list\u002Fshow path has no namespace filter or\nauthorization boundary. `evidence list` filters only by `--session-id`,\n`--lane`, and `--moment`; `evidence show` selects by `--id`. The hidden\nunscoped `evidence associations` helper is used by the operator MCP bridge to\nexpose association rows for an evidence ID. `evidence scoped-show` is the\nstore-owned scoped lookup:\nit requires both `--namespace` and `--id`, returns the row only when an\nassociation reaches that namespace, and emits `namespace_not_allowed` for a\nmissing, unassociated, or foreign ID.\n\nThe Hermes MCP `evidence_for(memory_id)` tool derives the memory namespace.\nFor a scoped token, a missing or foreign memory returns the same structured\n`namespace_not_allowed` response (`namespace: null`, with detail that the\nmemory is not associated with an allowed namespace); an allowed memory returns\nits hash-free evidence rows. The MCP `evidence_show(id, namespace)` tool uses\nthe scoped lookup for scoped tokens, so missing, unassociated, and foreign\nevidence IDs all return the same structured `namespace_not_allowed` response\nfor the requested namespace. Unscoped operators retain the exact missing-ID\nresponses (`memory id not found` and `evidence id not found`) and legacy\nassociation behavior.\n\nThe session-cadence maintenance transaction runs evidence retention after its\nnormal organize\u002Fbackup work. `ZMEM_EVIDENCE_DAYS` defaults to 30 and removes\nrows strictly older than the supplied cadence clock minus that many days;\n`ZMEM_EVIDENCE_CAP`\ndefaults to 50,000 and keeps the newest rows by stable `ts,id` order.\nAssociations are deleted with their evidence and stale associations are swept.\nInvalid retention settings disable the evidence sweep rather than selecting a\nsurprising limit.\n\n`add` and `update` accept `--evidence ID[,ID...]` with at most 256 evidence\nIDs per memory write. Capture-policy advisories are emitted before the\ntransaction opens; the writer then validates each supplied ID before memory\nmutation and attaches the ids to the resulting memory before commit; existing\npairs are ignored. Explicit JSON `recall`, `recent`, and `recall --explain`\nrows include sorted `evidence_ids`. Passive injection and human fenced output\ndo not gain that field. `evidence for MEMORY_ID --json` (or the legacy\n`evidence for --memory-id MEMORY_ID --json` form) returns the memory namespace\nwith its hash-free evidence rows; if both forms are supplied, conflicting IDs\nexit 2. Strict import prevalidates\nall `memory_evidence` endpoints and reports a missing endpoint using its\nphysical source line before association insertion.\n\n`export-jsonl` reads one consistent snapshot. An unscoped export includes all\nevidence rows and only associations whose parent rows are in that export. A\nnamespace-scoped export includes only evidence\nreachable through the memory\u002Fepisode rows selected for that namespace and\nomits unassociated evidence, since it cannot be attributed to a namespace.\nRecords with the new top-level `table` discriminator use one staged read,\nduplicate-key\u002Fhash\u002Freference validation, and one outer transaction. Use the\nexplicit all-or-nothing form with bounded staging for evidence transfers:\n\n```text\n\u003Cinterpreter> \u003Cstore.py> ingest-jsonl --in evidence.jsonl --strict\n```\n\nThe automatic discriminator path also chooses strict staging when it can parse\na table record. Legacy memory-only JSONL retains its best-effort per-row\nbehavior; a wholly malformed source cannot be classified automatically.\nStrict staging parses and applies the same captured bytes, never reopens a\nmutable source between validation and mutation.\n\n#### Deterministic passive-query rewrite (issue #183)\n\nThe 0.43 passive `user_prompt` path may append bounded local context before\ncalling the existing selector. A prompt is ambiguous only when it has fewer\nthan the positive configured minimum of case-folded content terms and no exact\nanchor. The default minimum is 4 and the positive override is\n`ZMEM_AMBIG_MIN_TERMS`. Slash\u002Fbackslash\u002Fdot\u002Funderscore, namespace, flag, `Error`, and\n`Exception` tokens are exact anchors and bypass the rewrite. Context is\ndeduplicated in source order from the first 12 safe operation tokens and the\nnewest three safe edit basenames; it is capped at 150 context characters and\nthe total query at 500 characters. Host consumers fail open silently on\nmissing\u002Finvalid sessions or evidence, read\u002Ftimeout failures, and malformed\noutput. The standalone query-rewrite CLI preserves the original query with\n`rewrite=0` and emits one sanitized warning when its store or context is\nunavailable. `ZMEM_QUERY_CONTEXT=0` is an exact kill switch at each host\u002Fstore\nboundary. Explicit search and non-`user_prompt` moments are unchanged, and\nthis bounded capability is not a claim to implement all of issue #163.\n\n#### Read-only replay evaluator (issue #155\u002F#183)\n\nReplay uses explicitly supplied offline inputs (the CI invocation uses the\ncommitted fixture) and is not a live efficacy test:\n\n```text\npython scripts\u002Feval_replay.py --store tests\u002Ffixtures\u002Freplay\u002Fstore.sqlite \\\n  --log tests\u002Ffixtures\u002Freplay\u002Fdecisions.log --days 30 \\\n  --compare-baseline eval\u002Fbaseline-replay.json --json-out replay-report.json\n```\n\nThe closed report is exactly two lanes (`claude`, `hermes-provider`) crossed\nwith `session_start`, `user_prompt`, `pretool`, and `precompact` (eight rows).\nRepeatable `--transcript PATH` inputs are explicit regular bounded JSONL files\n(at most 16 files, 4 MiB per file, 32 MiB total, and 100,000 physical lines per\nfile); there is no glob, rotation, failure-database, ring, or ambient\noperator-store discovery. The evaluator derives its clock from the latest valid decision-log\ntimestamp, clears ambient routing\u002Frecall `ZMEM_*` values before store imports,\nand verifies input digests before publication. If no eligible later same-session\nobservation exists, observation-dependent zero fields are empty-denominator\ncompatibility values and stderr says metrics are unavailable; they are not a\nmeasured success, failure, or efficacy claim. See\n`tests\u002Ffixtures\u002Freplay\u002FREADME.md` for bounds and reproducibility details.\n\nObservational action matching (issue #156) adds a report-only `--actions` mode\nto the same evaluator:\n\n```text\npython scripts\u002Feval_replay.py --store tests\u002Ffixtures\u002Freplay\u002Fstore.sqlite \\\n  --log tests\u002Ffixtures\u002Freplay\u002Fdecisions.log --days 30 \\\n  --actions --actions-input tests\u002Ffixtures\u002Freplay\u002Factions.json \\\n  --json-out actions-report.json\n```\n\n`--actions-input` is an explicit JSON file of recorded `delivered_rows` and\n`evidence_rows` (delivered rows carry `id`, `session_id`, `timestamp`, and `operation`;\nevidence rows carry `session_id`, `timestamp`, `event_kind`, and\n`operation`). For each delivered row the matcher selects the first\nlater same-session evidence event inside the fixed `ZMEM_MATCH_WINDOW_S =\n1800` second window whose `derive_ops_tokens` normalization shares at least\n`ZMEM_MATCH_MIN_OVERLAP = 2` tokens with the delivered trigger, then labels\nthe row `applied` (success), `violated` (failure), or `ignored` (no\nqualifying event). Both constants are fixed with no environment overrides and\nare recorded in the report next to the sorted `results`. The boundary is\nstrictly report-only: no action counter is written, no write transaction is\nopened, and the store SHA-256 is unchanged (durable counters belong to\nissue #124). The oracle pair `tests\u002Ffixtures\u002Freplay\u002Factions.json` \u002F\n`actions-expected.json` is written only by\n`tests\u002Ffixtures\u002Freplay\u002Fgenerate_actions.py`, which refuses to replace\ncommitted bytes when they drift; malformed rows or non-UTC timestamps exit 2\nbefore any output file is replaced.\n\nCounterfactual replay (issue #157) measures with\u002Fwithout-memory divergence:\n\n```text\npython scripts\u002Feval_counterfactual.py \\\n  --tasks tests\u002Ffixtures\u002Fcounterfactual\u002Ftasks.json \\\n  --store tests\u002Ffixtures\u002Fcounterfactual\u002Fstore.sqlite \\\n  --model-id recorded-stub-v1 \\\n  --json-out counterfactual-report.json\n```\n\n(`--allow-model-calls` exists to opt a non-stub model id into real adapter\nresolution; without it the run takes the skip path described below.)\n\nEach recorded session replays twice — `ZMEM_INJECT=1` through the real\npassive injection lane (delivery ledger redirected to a scratch directory;\nread-only store URI; store SHA-256 verified before and after) and\n`ZMEM_INJECT=0` with delivery disabled — through the pinned\n`recorded-stub-v1` model path, which returns the task's recorded successful\naction exactly when the delivered fence contains that task's `memory_row_id`\nand the recorded no-memory action otherwise. The report carries\n`repeated_failure_rate` and `first_action_agreement` per condition (0.0\u002F1.0\nwith memory, 1.0\u002F0.0 without on the committed fixture) against the Draft\n2020-12 contract `eval\u002Fcounterfactual-schema.json`. The evaluator refuses the\noperator home store before opening anything. A real model id is only resolved\nwith `--allow-model-calls`; without it the run prints exactly\n`SKIPPED: model calls disabled` and exits 0 with `skipped: true` (writing the\nskipped report when `--json-out` is given) — no adapter\nimport, no download, no network. The committed task\u002Fstore\u002Fexpected fixture\ntrio is written only by `tests\u002Ffixtures\u002Fcounterfactual\u002Fgenerate.py`\n(developer-side; CI compares against the committed bytes and never\nregenerates).\n\nThe predeclared private real-corpus measurement of record for issue #155 is\ncommitted at `eval\u002Freal-corpus-2026-09-19.json`: a cohort frozen at\ndeclaration time (standalone store snapshot, `ver=0.49.0` projection of the\nfrozen decision log, and the cohort's one Claude-shaped transcript), declared\nwith SHA-256 digests on the issue before the evaluation ran. The file carries\naggregates and digests only; `tests\u002Ftest_eval_real_corpus_record.py` pins its\ndigests, schema, and privacy boundary. Future real-corpus replays must freeze\nthe store snapshot and declare its SHA-256 before the measurement — replays\nover live, undeclared, or outcome-selected inputs are not baselines.\n\n### entity-list — inspect entity identity (v10, issue #60)\n```\n\u003Cinterpreter> \u003Cstore.py> entity-list [--kind person|project|tool|preference|host|service|model|repo|file|command|error_class|other] [--json]\n```\nLists the entities the deterministic extractor minted: id, kind, canonical\nname, all normalized aliases, and how many memories link to each. `--kind`\nfilters to one kind; `--json` emits `[{id, kind, name, aliases, links}]`.\nEntity identities are GLOBAL: this listing (and every other entity surface)\nspans ALL namespaces — the entity tables carry no namespace column, and\n`entity_alias.alias_norm` is globally unique — so an alias resolves to\nexactly one entity no matter which project mentioned it (review PRR-011,\nPR #454: uniqueness is per ALIAS; canonical names are not unique-keyed —\n`resolve_entity` refuses with exit 2 when a name matches several entities).\nThis is the inspection\nsurface for humans and doctor — use it to see what the entity lane actually\nmatches. Issue #455 display masking: credential-shaped canonical names and\naliases render as the whole-value marker `[REDACTED_SECRET]` (full contract\nunder the classifier decision record below).\n\n### entity-merge — reconcile duplicate entities (v10, issue #60)\n```\n\u003Cinterpreter> \u003Cstore.py> entity-merge --from \u003Centity-id> --to \u003Centity-id> [--confirm]\n```\nMerges two entities: the `--from` entity's aliases and memory links move to\n`--to`, then `--from` is deleted. DRY RUN BY DEFAULT — without `--confirm`\nnothing is written and the plan (moves, alias\u002Flink collisions with the\ntarget, the deletion) is printed. With `--confirm` the write happens in one\ntransaction; a target that already has an alias or link keeps its own\n(collisions are counted, never overwritten).\n\nRefusals (exit 2, nothing written): unknown id on either side, `--from` ==\n`--to`, or a kind mismatch — merging a `person` into a `tool` would silently\nre-classify history. No write path auto-merges entities as a side effect\n(the issue's \"ZMem will not auto-merge people\" rule): merges happen only\nthrough the two EXPLICIT operator commands — this one (person→person merges\nare allowed here, manual and `--confirm`-gated) and\n`reconcile-entity-aliases --apply` (whose dry-run previews every merge; a\nlegacy `other:person:Name` row can merge into a person entity through it).\n\nTypical flow: `entity:Name` tags or backticked mentions created two\nentities for the same thing (e.g. `rg` and `ripgrep`); find both ids with\n`entity-list`, dry-run the merge, then apply with `--confirm`. Afterwards\nthe alias `rg` resolves to the surviving entity and recall's third list\nreturns all its linked memories.\n\nIssue #455 display masking: the dry-run plan masks credential-shaped\nfrom\u002Fto canonical names and the alias-collision join (full contract\nunder the classifier decision record below).\n\n### entity-card — build or refresh one entity's card (issue #179)\n```\n\u003Cinterpreter> \u003Cstore.py> entity-card build ENTITY [--json]\n\u003Cinterpreter> \u003Cstore.py> entity-card refresh ENTITY [--json]\n```\nDirect card operations on one entity (`ENTITY` = entity id, or an exact\ncanonical name; zero\u002Fmultiple name matches exit 2). `build` creates a\nmissing qualifying card and REFUSES a live card (exit 2, no write);\n`refresh` returns `created`\u002F`updated`\u002F`unchanged`\u002F`no_card` and tombstones\na live card whose entity fell below `ZMEM_CARD_MIN_MENTIONS`. `--json`\nemits one object with the exact key order `result, id, entity_id,\nsource_ref, warnings`. Full contract: \"Entity cards (issue #179)\" above.\n\n### reclassify-entities — rekind stored `other` entities (issue #176)\n```\n\u003Cinterpreter> \u003Cstore.py> reclassify-entities --dry-run|--apply [--json]\n```\nRuns the DETERMINISTIC classifier (pure string rules — no LLM, no shell, no\nnetwork, no DNS, no filesystem; precedence host → service → model → repo →\nfile → command → error_class → other over the case-folded name) over every\nentity row currently kinded `other` and proposes or applies new kinds.\nExactly one of `--dry-run`\u002F`--apply` is required (both or neither exits 2).\nNon-`other` rows are never touched; entity ids, aliases, and memory links\nare preserved; `--apply` writes in ONE transaction under the writer lease\nand rolls back completely on error. Like every entity surface, the scan\nspans ALL namespaces (there is no namespace filter — entity identities are\nglobal). `--apply` also refuses an unmigrated store up front (a schema\nstamp below the supported version exits 2 with the migration hint instead\nof a mid-apply sqlite error). `--dry-run` runs before the lease on a\nread-only connection, changes zero database bytes (no `-wal`\u002F`-shm`\nsidecars), and prints the same canonical JSON as apply: `{\"scanned\",\n\"changed\", \"before_other_link_share\", \"after_other_link_share\",\n\"changes\":[{id,name,old_kind,new_kind}]}` with changes ordered by entity id;\nfor `--apply` the scanned\u002Fbefore numbers describe the locked pre-update\nsnapshot inside the write transaction.\n\nIssue #455 display masking: change-row `name` fields render as the\nwhole-value marker `[REDACTED_SECRET]` when credential-shaped (full\ncontract under the classifier decision record below).\n\n**Writer lease and concurrent applies.** The writer lease is advisory — it\nmarks a live writer so `restore` observes it; it is NOT mutual exclusion. A\nsecond apply run after the first commits harmlessly reports `changed=0`\n(idempotent re-run); an apply whose targets were re-kinded by a writer\nbetween its scan and its transaction fails closed with a ValueError and zero\nchanges.\n\n**Backup before `--apply`.** `--apply` re-kinds rows in one transaction but\ntakes no snapshot of its own (unlike `rekey-namespace`). Take a verified\nbackup first — `\u003Cinterpreter> \u003Cstore.py> backup` — and keep it until the new\nkinds are confirmed; undo is `restore` from that snapshot (rows re-kinded by\nmistake come back with their old kinds; there is no per-row undo).\n\nAt write time the same classifier assigns kinds to unqualified extracted\nvalues (plain tag tokens, untyped `entity:Name`, CamelCase identifiers);\nexplicit typed tags, the `project:` namespace rule, and backtick→`tool`\nstay authoritative.\n\n**Per-kind entity-lane weights.** Recall multiplies each memory's entity-lane\nRRF contribution by the max weight across the kinds of the entities the\nQUERY matched (`ZMEM_ENTITY_KIND_WEIGHTS`, one JSON object such as\n`{\"other\":0.6,\"host\":1.0}`; defaults 1.0 for every kind except\n`other=0.6`; an invalid value logs one warning and uses the complete\ndefaults — recall never crashes on the knob). BM25, vector, and graph\ncontributions are unchanged; `recall --explain` reports the effective\nweights under `entity_kind_weights`.\n\n**Operator snapshot block.** `tests\u002Ffixtures\u002Fentity\u002Foperator-snapshot.json`\nrecords the motivating measurement and the fixture apply's fixed outcome:\n\n```json\n{\"as_of\": \"2026-09-10\", \"memory_entity_links\": 26511, \"other_links\": 24826,\n \"fixture\": {\"memory_entity_rows\": 3, \"before_other_link_share\": 1.0,\n             \"after_other_link_share\": 0.0, \"changed\": 4}}\n```\n\nThe 24,826-of-26,511 `other` share (0.9364…, exactly 24826\u002F26511) is why\n`other` defaults below\n1.0; the `fixture` block is test-pinned\n(`tests\u002Ftest_entity_fixture_provenance.py` cross-checks it against\n`reclassify-expected.json`) — the committed proof\nthe reclassification moves the fixture store's other-link share from 1.0 to\n0.0 in one apply. Regenerate the three fixed fixtures with\n`python tests\u002Ffixtures\u002Fentity\u002Fmake_reclassify_fixture.py` (deterministic). The\nreviewed `reclassify-expected.json` regenerates OUTSIDE any test in two steps:\nbuild the seeded fixture store with\n`python tests\u002Ffixtures\u002Fentity\u002Fmake_reclassify_fixture.py --emit-store \u003Cdir>`,\nthen run the exact dry-run against it —\n`ZMEM_STORE=\u003Cdir>\u002Fstore.sqlite \u003Cinterpreter> \u003Cstore.py> reclassify-entities\n--dry-run --json > tests\u002Ffixtures\u002Fentity\u002Freclassify-expected.json` — the same\nseeding `ReclassifyTest` uses (one source, `emit_store`).\n\n### entity-kind classifier decision record (issue #366)\n\nOwner-confirmed 2026-11-11 (verbatim directive: \"confirmed on 366\",\ntranscribed with provenance in the issue comment; proposed record\nadjudicated by an independent plan critic). The five precision rows observed\nin the #365 review are all **kept spec-literal** — each output matches its\ndocumented rule in `classify_entity_kind`'s docstring, and no guard is added\nahead of host\u002Fservice\u002Fcommand (guards would complicate the precedence chain\nissue #176 owns):\n\n1. Hyphenated one-dot basenames (`my-script.py`, `build-helper.sh`,\n   `claude-code.md`, `app-config.json`) stay `host` — the one-dot host rule\n   runs before file, and persisted kinds are sticky under\n   first-seen-kind-wins.\n2. The slash\u002Fdoc-suffix asymmetry stands: `docs\u002Freadme.md` → `other` while\n   `README.md` → `file` (the doc-suffix rule is gated on `\u002F` not in the\n   value).\n3. Bare runner-head words (`make`, `go`, `node`, `python`) stay `command`\n   (first whitespace token in RUNNER_HEADS).\n4. Service nouns matching filenames stay `service`: `server.log` and\n   `server.py` are `service` (whole-word rule); `access.log` remains `file`.\n5. CamelCase service identifiers (`UserService`, `DatabaseServer`) stay\n   `other` — the whole-word rule runs on the casefolded token; CamelCase\n   splitting is an issue #176 spec change and stays a non-goal.\n\nThe IPv4 fixture sub-decision: classify.json keeps its frozen 30 rows — the\nASCII-strict behavior (Arabic-Indic\u002Ffullwidth quads classify `other`) stays\npinned by the direct test `test_ipv4_octets_are_ascii_strict`, and no\nfixture row is added. The hardening items that motivated #366 landed via\nPR #365 (squash `a3e75178`, which carries the review-round fixes): the\nlinear host one-dot rewrite and the ASCII-strict IPv4 octet class.\n\nCompanion record from the PR #454 feedback ledger (F-30 \u002F PRR-013): recall\n`--json`\u002F`--explain` carry no LF-only byte contract (PRR-013: an\nLF-normalized golden exists only for the `--for-injection` arm and is\nunaffected) — recorded here so the decision line lives with the other dated\nrecords.\n\n**Display masking (issue #455).** Each entity report surface masks\ncredential-shaped display values (its own names\u002Faliases; the merge\ndry-run also the alias-collision join; reclassify the change-row names;\nreconcile the orphan name\u002Fsuffix\u002Fowner_name and merged-row names) with\nthe whole-value marker `[REDACTED_SECRET]` at render time — display only;\nstored rows, the classifier, and entity-card construction are untouched, and\nlegal `label:port` names (`localhost:8080`) render unmasked. Detection uses\nNFKC-normalized exact secret-head tokens over whitespace-trimmed\n`[:_=\"'-]`-delimited segments (colon, underscore, hyphen, equals, quote —\nso attribute-assignment shapes like `DB_PASSWORD=12345` and\n`DB_PASSWORD = 12345` mask, as do quoted-key shapes like\n`{\"password\": \"value\"}`; bare whitespace without punctuation does not\nseparate; fullwidth `：`\u002F`＝` fold to ASCII and EVERY Unicode format\ncharacter (category Cf — ZWJ, soft hyphen, word joiner, BOM) is\nstripped, so glued forms like `pass\\u200dword:12345` and\n`pass\\u00adword:12345` mask) and, for EVERY assignment-separator\noccurrence, over the\nDOT-segments of its key prefix (`db.password=12345`,\n`spring.datasource.password:5432`, earlier-separator forms\n`redis:db.password=12345` \u002F `--config=db.password=12345`, the dot-joined\nhead spelling `api.key=12345`, whitespace around\u002Finside key material\n(`db. password=…`, `db password=12345`, `--config password=12345`), and\ntrailing separators with an empty value (`db.password=`); never a global\ndot split — `api.example.com` and `acme.corp:8080` stay unmasked;\n`-`\u002F`_` are hard joins, so `db.password-prod=1` stays unmasked like\n`tokenized-service`; bare whitespace WITHOUT any assignment separator\n(`password 12345`) is not a key\u002Fvalue boundary and stays unmasked) plus\nthe shared redaction registry. The local\nhead list is REGISTRY-PARITY: every key-head family of\nredaction.py's credential pattern — `password`, `passwd`, `pwd`,\n`secret`, `token`, `api-key`\u002F`api_key`\u002F`apikey`,\n`private-key`\u002F`private_key`, `credential`, `auth` — is an exact local\ntoken, so short values below the registry's 8-character value floor\n(`pwd:12345`, `private_key:9`) mask locally too. Still unmasked by the\nexact-token boundary: prefix-shaped words (`tokenized-service`,\n`author`, `secretariat`) and glued NON-registry spellings\n(`secretkey=…`, `passphrase=…`, `pgpassword=…`). Masking is fail-closed:\nthe reports never fall back to raw output — a registry that becomes\nunavailable after startup (or whose call fails at render time) makes the\nrenderer refuse (dry-run exits 2 with empty stdout; apply reports from\nthe committed state), and a registry missing at process start fails the\nCLI at import before any report runs. The masking predicate is LINEAR in\nthe input — pinned by DisplayMaskPerfTest's scaling-ratio assertion\n(the pre-feedback form was quadratic in the separator count).\n\n### reconcile-entity-aliases — merge pre-widening orphan identities (issue #358)\n```\n\u003Cinterpreter> \u003Cstore.py> reconcile-entity-aliases --dry-run|--apply [--json]\n```\nA vocabulary widening silently re-keys stored aliases on the next relink: a store\nwritten before v16 stored a tag like `host:acme-prod` as one `other`-kind\nentity whose alias is the WHOLE token; after the widening, re-deriving that\nmemory mints `{host, \"acme-prod\"}` and leaves the old `other` entity with\nzero links (the 0.101.0 CHANGELOG discloses the behavior — only the entity\nlane's grouping changes; content and FTS recall are unaffected). This\ncommand reports — and, behind `--apply`, merges — those orphans back onto\nthe entity that now owns the suffix alias, so BOTH alias forms resolve to\none identity. Issue #455 display masking: orphan `name`\u002F`suffix`\u002F`owner_name` and\nmerged-row `name` render as the whole-value marker `[REDACTED_SECRET]`\nwhen credential-shaped (full contract under the classifier decision\nrecord above). (Dry-run's `mergeable` counts every orphan that HAS an owner;\napply additionally refuses cyclic chains, so `merged` can be smaller than\n`mergeable` by exactly the `skipped_cycle` count.) Run it AFTER upgrading past a widening and BEFORE the next\n`purge` (a purge reaps linkless entities and would delete the old alias form\nwith the orphan).\n\n`--dry-run` is strictly read-only (zero database bytes, no writer lease) and\nreports every orphaned `\u003Ckind>:\u003Cname>` other-entity with its prefix, suffix,\nand owner (null when no entity owns the suffix yet — EXCEPT two cases:\na no-owner orphan that another orphan's chain terminates ON stays alive\nand IS that chain's merge target; and an orphan that OWNS ITS OWN SUFFIX\nis reported with owner null and left untouched — self-merge is not a\nreconciliation). The\n`orphans` array means pre-apply candidates in dry-run and the remaining\nunmerged rows after apply. `--apply` takes the writer lease, refuses an\nunmigrated store up front, and performs every merge in ONE transaction:\nterminal-owner resolution first (a chain like `host:service:foo` →\n`service:foo` → `foo` merges into the final live owner; any chain that\nreaches a cycle is refused WHOLE — the cycle's members and any tail feeding\ninto it — reported as `skipped_cycle`\u002F`cycle_ids`), then per orphan: aliases\nmove first-delete-then-reinsert onto the owner, links move with\n`UNIQUE(memory_id, entity_id)` dedup, and the orphan row is deleted. A merged\norphan's live entity_card is tombstoned in the same transaction (a live card\nnaming a deleted entity would render without its recall suppression; the\nowner's card rebuilds from the owner's links via the cadence sweep). The old\nfull-token alias survives on the owner. Non-`other` rows are never touched,\nand the same ALL-namespaces scope as every entity surface applies.\n\n### links — inspect (or curate) a memory's associative links (v11, issue #61)\n```\n\u003Cinterpreter> \u003Cstore.py> links --id \u003Cuuid> [--json]           # list every edge\n\u003Cinterpreter> \u003Cstore.py> links --add --id \u003Ca> --id \u003Cb> \\\n    --relation \u003Crelated|supports|contradicts|updates|extends|derives> \\\n    [--score S] [--reason \"...\"]\n```\nList mode prints every `memory_link` edge touching the memory, both\ndirections (`out` = the memory is the source; JSON rows carry\n`{src, dst, direction, other, relation, score, created_at}`). A missing id\nexits 1 with the same stable stderr line as `get`.\n\n`--add` is the operator-curated insertion path for the typed relations\n(and for the trust-carrying `supports`\u002F`contradicts`). Symmetric\nrelations (`related`\u002F`supports`\u002F`contradicts`) insert BOTH directions;\ntyped relations (`updates`\u002F`extends`\u002F`derives`) keep their one authored\ndirection. Self-links and cross-namespace pairs are refused; re-adding an\nexisting edge is an exact no-op (idempotent, no second trust delta).\n`--relation contradicts|supports` adjusts `trust_score`, so those inserts\nREQUIRE `--reason` — the same deliberate-use guard `contradict` enforces\n(validated and echoed, not persisted).\n\nLinks are generated automatically on every `add`\u002F`update` (see\n`ZMEM_LINK_THRESHOLD` below) — deterministic, no LLM (no `ZMEM_LINK_LLM`\nknob exists; link generation never calls a model).\n\n### contradict — record a contradiction (v11, issue #61)\n```\n\u003Cinterpreter> \u003Cstore.py> contradict --id \u003Ca> --id \u003Cb> --reason \"\u003Cwhy they conflict>\"\n```\nInserts a `contradicts` pair (both directions) and applies the trust event\nto BOTH rows: `trust_score` −0.10 each, clamped to [0.0, 1.0] (ten\ncontradictions land a row at exactly 0.0, never below). Neither row is\nmerged, deleted, or rewritten — content, confidence, and signal are\nuntouched. `--reason` is REQUIRED (the `invalidate` deliberateness\nconvention); the v11 schema has no reason column, so the reason is\nvalidated and echoed but deliberately not persisted. Re-running the same\ncontradict is an exact no-op. A polarity-disagreeing `add` that lands in\nthe dedup window does this automatically (see `add`).\n\n**trust_score semantics** (v11): every memory starts at 1.0. A\n`contradicts` event lowers BOTH rows by 0.10; a `supports` link or a\npolarity-agreeing duplicate re-add (corroborating add, CLI `add`\u002F`update`\nonly — sync ingest never re-applies deltas) raises the keeper by 0.05.\nAlways clamped to [0.0, 1.0]; visible in `get --json`, `export-jsonl`,\nand `doctor`. `confidence`\u002F`signal` are never changed by linking — they\nare provenance inputs; trust_score is the contradiction ledger.\n\n## Operation feedback counters (issue #124)\n\n`applied_count`\u002F`violated_count` have ONE writer\n(`storelib\u002Fwrite.py::feedback_memory`) with TWO callers: the explicit\n`store.py feedback --id --applied|--violated` CLI and the observational\n`store.py operation-feedback` command that the capture-failure and\nposttoolbatch hooks invoke after each operation outcome. The command matches\nthe event against the session's delivered rows (1,800 s window, >= 2 token\noverlap), requires the event's evidence id to be associated with the memory,\nand records every verdict in a per-session sidecar\n(`\u003Cdata>\u002Fops\u002F\u003Csha256(session)[:32]>.feedback.jsonl`, one compact sorted-key\nJSON line with exactly `event_id`, `evidence_id`, `memory_id`, `overlap`,\n`session_id`, `timestamp`, `verdict` — verdict is applied, violated, or\nunmatched). Ranking popularity is usefulness only:\n`0.15*sqrt(applied_count) - 0.25*sqrt(violated_count)` clamped to\n[0.0, 1.0]; retrieval\u002Fsurfaced telemetry never feed ranking. `stats`,\ndoctor's voyager-counters check, and the miss-rate report expose seven keys\nin this order: `total_applied`, `total_violated`, `nonzero_applied`,\n`nonzero_violated`, `matched_applied`, `matched_violated`,\n`unmatched_operations`; the miss-rate JSON adds `feedback_associations`\n(session_id, event_id, memory_id, overlap, evidence_id per row).\n\n## Live host canary lanes (issue #96)\n\n`scripts\u002Fhost_canary.py` also owns nine LIVE host-canary lanes. Each lane\nruns against an isolated store, writes a schema-valid artifact\n(`canary\u002F\u003Clane>.json`, contract in `scripts\u002Fcanary-schema.json`), and always\nexits 0 for a completed run — the verdict lives in the artifact, never in\nthe exit code. A negative observation is a **structured fail** with the\nmeasured values and the exact command outcome in `notes`; it is never a\ncrash and never a faked pass. Lanes: `hermes-gateway`,\n`hermes-provider-mode`, `hermes-compat-mode`, `claude-compact`,\n`codex-trust`, `zcode-duplicate`, `exec-form-claude`, `exec-form-codex`,\n`exec-form-zcode`. Validate any artifact with\n`python scripts\u002Fhost_canary.py --validate-result canary\u002F\u003Clane>.json`.\nMeasured SHA\u002Fversion values are recorded only when the host's documented\nsurface emits them (`version-unavailable` otherwise); hook identifiers are\nderived from the host's real manifest (`PreCompact:zmem-launch.js` shape),\nnever invented; four before\u002Fafter inventory maps prove the run only touched\nthe isolated canary store. Canonical fence bytes come from the store's own\n`rendered` envelope (#158) — Hermes wraps them in `\u003Cmemory-context>`, so no\nzmem fixture ever contains that wrapper.\n\n## Timeout budget\n\nIssue #121: the hook path runs inside the host's hook timeout (the host\nconfigs give SessionStart 15 s) with an internal budget. Canonical shared\ninteger values live in `hooks\u002Ftimeout-budget.json`; the private\ntraining-capture allocations below are implemented in `hooks\u002Fzmem-launch.js`.\nRuntime overrides read env vars and fall back to those defaults (an invalid\nvalue falls back with exactly one warning).\n\n| Stage | Value | Override env var | Notes |\n|---|---|---|---|\n| Launcher watchdog | 12000 ms | `ZMEM_LAUNCHER_WATCHDOG_MS` | Kills the child tree at the deadline, emits the retained Tier 0 sentinel, logs `outer_timeout=1 reason=omitted`, exits 0. |\n| Automatic capture start | 5000 ms | - | Synchronous only when the store-issued capture id is needed; observe\u002Fsnapshot helpers are detached and fail open. The translated launcher clamps this private allocation to the remaining 12,000 ms watchdog deadline minus a 500 ms output reserve, and never asserts acknowledgement or outcome. |\n| Hermes capture subprocess | 5000 ms | - | `sync_turn` and session-end capture use their own per-operation cap and may add up to 5 seconds to that callback or maintenance path. The launcher’s full 5-second start allocation can leave roughly 6.5–7 seconds inside the 12,000 ms watchdog, below the 8-second recall budget, so watchdog preemption is an expected degradation. No whole-callback wall-clock guarantee is made; capture remains fail open. |\n| Namespace resolution | 2000 ms | `ZMEM_NAMESPACE_RESOLVE_MS` | Per interpreter attempt; successful non-empty REMOTE namespaces are cached per process. |\n| Namespace cache TTL | 60000 ms | `ZMEM_NAMESPACE_CACHE_TTL_MS` | Entry expires at exactly TTL; path-key resolutions are never cached. |\n| Store recall | 8000 ms (8.0 s) | `ZMEM_STORE_RECALL_TIMEOUT_S` | SessionStart + the shared recall body. Finite positive float; values above 8.0 clamp to 8.0 (one warning); values below 8.0 are honored. ONE store attempt at SessionStart (no retry loop). |\n| SQLite busy timeout | 5000 ms | - | `storelib\u002Fschema.py` (`PRAGMA busy_timeout=5000`); unchanged. |\n| Hermes manager join | 8000 ms | - | The Hermes MemoryManager joins external-provider prefetch at 8.0 s (external runtime, not vendored in this repo). |\n| Hermes provider deadline | 6000 ms | `ZMEM_HERMES_DEADLINE_S` | Issue #160: every provider prefetch runs under this deadline (default 6.0; invalid or `>= 8.0` values resolve to 6.0 with exactly one warning). |\n\nSessionStart emits a complete Tier 0 sentinel BEFORE the first store\nsubprocess (the fast path), so a store stall inside the watchdog window\nstill delivers Tier 0 to the host. The launcher watchdog arms before\nstartup, so namespace resolution counts against the 12,000 ms budget. The\n`scripts\u002Fbench_hook_latency.py` benchmark reports deterministic\ninjected-clock p50\u002Fp95 schedules for the seven stages (launcher,\nnamespace, store, embed, fuse, render, time-last-capture) — a\ncontract-regression gate whose `--compare-baseline` pins stage keys,\nper-stage values, and a machine-independent `input_digest`; it does not\nmeasure wall-clock latency.\n\n## Store hygiene (read-only, issue #97)\n\n`store.py hygiene` audits a SNAPSHOT of the store — it never opens or writes\nthe live store, and it never mutates a memory, vector row, namespace, or\nsignal. Hermes-origin MUTATIONS remain owned by issue #168's explicit\noperator map; this command is strictly report-only.\n\nOperator workflow:\n\n1. **Snapshot copy first.** The live store runs in WAL journal mode, so a\n   bare `cp` of `store.sqlite` can miss commits still sitting in the `-wal`\n   file (or open malformed mid-checkpoint). Use a crash-consistent snapshot\n   instead — either the built-in verified backup or SQLite's own backup API:\n\n   ```bash\n   # Option A (preferred): the store's verified backup writes a\n   # crash-consistent store-\u003Cstamp>.sqlite snapshot.\n   \u003Cinterpreter> skills\u002Fmemory\u002Fscripts\u002Fstore.py backup\n   # Option B: SQLite's online backup API.\n   sqlite3 ~\u002F.zmem\u002Fstore.sqlite \".backup '\u002Ftmp\u002Fsnapshot.sqlite'\"\n   ```\n\n   Then run the report against that snapshot (the command opens it with\n   SQLite `mode=ro`, so even an accident cannot write):\n\n   ```bash\n   \u003Cinterpreter> skills\u002Fmemory\u002Fscripts\u002Fstore.py hygiene \\\n     --store \u002Ftmp\u002Fsnapshot.sqlite \\\n     --origin-map origin-map.json --evidence-map evidence-map.json \\\n     --out report.json --format json\n   ```\n\n2. **Review the report.** It contains the snapshot's SHA-256, row totals and\n   live counts, sorted namespaces\u002Fsignals, the reviewed Hermes-origin ids,\n   the six known junk namespaces (`ns1`, `ns2`, `project:`, `test`,\n   `unfoldtest`, `user:t`), duplicate logical keys (live rows sharing a\n   `content_norm`), and a sorted `signal=none` upgrade action plan.\n\n3. **Verified backup** (`store.py backup`) before any remediation, then a\n   **dry-run rekey** (`rekey-namespace --dry-run`) if namespaces must move.\n\n4. **Mutation boundary.** The action plan emits `store.py update ...`\n   commands as REVIEW ARTIFACTS — verbatim and unquoted by contract, so read\n   them before running anything; it executes nothing. Every proposed upgrade\n   requires a live later grounded row, a live `supports`\u002F`updates`\u002F\n   `extends`\u002F`derives` link, a non-empty proof source reference, and a\n   justification. A rerun omits a target whose current row is superseded.\n\nInvalid input (unreadable files, malformed JSON, duplicate or unknown mapped\nids, SQLite errors) exits 2 with `[zmem] hygiene: invalid input` and creates\nno output file.\n\nNamespace resolution (host.py `resolve_namespace`) now distinguishes git\n`remote` \u002F `absent` \u002F `error`: a successful remote key is cached to disk\n(`\u003Cdata-dir>\u002Fnamespace-cache\u002F`, TTL 3600 s), an absent checkout keeps its\npath key, and a git ERROR inside a checkout resolves to the cached remote\nkey or `user:global` — never a path-shaped key. Cache failures fail open\nsilently. (This is the host-layer disk cache, distinct from the hook's\nper-process namespace cache in the timeout budget above.)\n\n## Hard rules\n- **Never put secrets\u002Fcredentials\u002FPII in the store.** It is a local plaintext sqlite\n  file. The write-time filter is advisory only (regex heuristic), not a guarantee.\n- **`doctor.py` is read-only.** It should inspect, never repair. Do not let it\n  create the store, rewrite config, or \"fix\" hook trust for the operator.\n- **Legacy namespace cutover:** before first v5 use, set\n  `ZMEM_NS_MIGRATION_MAP` to a JSON object mapping each old namespace to a live\n  checkout path, for example `{\"project:oldname\":\"C:\u002Fsrc\u002Fowner\u002Frepo\"}`. The\n  migration re-derives canonical remote-based keys from those checkouts; it\n  retries the map on later opens, but an omitted entry remains under its old key\n  until the map is supplied.\n- **Signal honesty:** `signal=none` means no external grounding — the lesson is the\n  agent's self-opinion. Never set `signal=test` unless a test actually ran.\n- **Wrap, do not replace:** this skill never writes to `tasks\u002F\u003Cslug>\u002F*.md` or\n  `issue-traces\u002F\u003Cissue>\u002F*.md`. Those are durable-session-state's source of truth.\n- **High-precision-first:** if a recalled memory does not clearly apply, ignore it.\n  Retrieved-wrong memory hurts more than retrieved-nothing.\n- **Source refs:** prefer immutable sources (`db:`, `session:`). For mutable markdown\n  (`file:`), a content hash is stored and checked on recall — if the source changed\n  since extraction, the memory's confidence is halved and flagged `[STALE SOURCE]`.\n  `file:` paths may be Windows (`C:\\Users\\...`) or Cygwin (`\u002Fc\u002FUsers\u002F...`) — both are\n  auto-normalized. If a `file:` ref cannot be opened, a stderr warning is emitted.\n\n## How recall works\nQuery hygiene (#112): the lexical query is normalized before it becomes the\nFTS5 MATCH — tokens are casefolded, deduped, stripped of edge punctuation,\nclosed-class English stop words are dropped, tokens under 3 characters match\nexactly instead of prefix-wildcard, the term list is capped at 24 (the last\nterms — the ops-token tail — always survive), and the MATCH is column-filtered\nto `{content tags}` so namespace text alone can never make a row a candidate.\n`recall --explain` records the resulting shape in its `query_shape` field.\n\nRetrieval is a **four-signal pipeline**: FTS5\u002FBM25 keyword match (always),\nvector KNN over stored embeddings (when the optional embedding runtime is\navailable), entity matching (v10 — always, no model needed; the query's\nidentifiers and plain tokens are matched against stored entity aliases),\nand the graph-seed arm (issue #136 — always, no model needed): the matched\nentities seed one bounded hop over seed-safe links (`related`, `supports`,\n`updates`, `derives`) so a linked neighbor can enter the pool without\nsharing any query token. The\nlanes' rankings are fused with Reciprocal Rank Fusion (RRF, k=60,\nper-id additive: a memory appearing in several lanes accumulates each lane's\ncontribution), then re-ranked by a **composite score** that combines:\n\n- **Relevance** (55%) — issue #113 lane composition: the max of the lexical\n  lane (term coverage x pool-normalized rank ratio; >= 2 matched terms),\n  embedding cosine, and entity-match fraction (see the per-lane subsection\n  below)\n- **Confidence** (20%) — grounded by signal tier (test\u002Fcompile > reviewer\u002Fuser > none)\n- **Recency** (15%) — exponential decay with a 90-day half-life\n- **Popularity** (10%) — retrieval frequency with diminishing returns (sqrt dampening)\n\nCandidates are namespace-filtered (the tier's expanded alias set) and subject\nto the confidence floor (below 0.25 is dropped before scoring). Staleness\ndemotion halves confidence, which feeds into the confidence component.\n\n**Per-lane relevance composition and inject floors (issue #113)**: a\ncandidate's relevance term is the MAX of its measured lanes — lexical\n(matched-terms\u002Ftotal-terms x row-|bm25|\u002Fpool-best-|bm25|), vec cosine, or the\nentity fraction — replacing the saturated `|bm25|\u002F(1+|bm25|)` back-solve, so a\nlexically-matched row keeps the cosine it earned and the score carries rank\ninformation from every lane. The lexical leg counts only when the row matches\n>= 2 distinct query terms or covers all of them — one shared generic token is\nnot relevance (the measured \"git status\" failure shape); an ineligible-but-\nranked row carries a measured 0.0, a lane with no signal carries `null`\n(absent). The inject gate consumes these values with DISJUNCTIVE floors: a\ntrusted row is admitted when ANY measured lane clears its own floor — lex\n0.30 \u002F cos 0.50 \u002F entity 0.50 (env `ZMEM_INJECT_FLOOR_LEX` \u002F\n`ZMEM_INJECT_FLOOR_COS` \u002F `ZMEM_INJECT_FLOOR_ENT`, calibrated starting\nvalues); a measured-but-failing lane does not veto a row another lane\nadmits, and ABSENT lanes are exempt (link-expansion rows carry none). A\nsilent inject whose rows passed the trust gate but failed these floors names\nthe new closed-set reason `below-relevance` (\"nothing relevant\") instead of\n`below-bar` (\"nothing trusted\"). `recall --explain` shows the numbers:\n`found`\u002F`below_limit` verdicts carry `detail.lanes`, the envelope reports the\nresolved `lane_floors`, and a row that missed every pool but would ride the\n1-hop link walk reports a `link_expansion` verdict (parent, relation, score)\ninstead of `not_in_pool`. Recall rows also carry the underscore-prefixed\ndiagnostic keys `_rel_lex` \u002F `_rel_cos` \u002F `_rel_ent` \u002F `_rel_graph` — the\nmeasured lane values the inject gate floors judge (same family as the\nexisting `_score` key).\n\n**Graph-seed arm and per-arm caps (issue #136)**: recall runs FOUR candidate\narms — FTS, vector, entity, and graph-seed — each capped BEFORE fusion so one\nover-generous arm cannot fill the fused window (named defaults equal the\npre-#136 windows, so default behavior is unchanged; `ZMEM_ARM_CAP_FTS` \u002F\n`ZMEM_ARM_CAP_VEC` \u002F `ZMEM_ARM_CAP_ENTITY` \u002F `ZMEM_ARM_CAP_GRAPH` override,\n`ZMEM_GRAPH_SEED=0` disables the graph arm). The graph-seed arm resolves the\nquery's entities (and ops tokens, which ride the same alias path) to seed\nmemories, then walks ONE bounded hop over `related`\u002F`supports`\u002F`updates`\u002F\n`derives` edges — never `contradicts` as a seed — and emits the capped,\ndeterministically ranked neighbors as the fourth RRF list. Each\ngraph-arm row carries a MEASURED `_rel_graph` lane: the best entry-edge\nscore, judged against the graph floor (default 0.75 = the write path's own\n`LINK_THRESHOLD`; env `ZMEM_INJECT_FLOOR_GRAPH`) by the same disjunctive\ninject gate — a graph-rescued row injects only when its edge to the\nquery-anchored seed is strong, never by the absent-lane exemption. `--explain`\nand the `--for-injection` envelope report per-arm `pre`\u002F`post`\u002F`cap` counts\n(`arms`), and the hook's decision line carries `arms=fts:P\u002FQ,vec:P\u002FQ,...`\nso the miss-rate report can attribute a hit to the arm that carried it.\n\n**Entity matching (v10, issue #60 5.3)** is the third fused list (after\nFTS\u002FBM25 and vector; the graph-seed arm above is the fourth): the query runs\nthrough the same deterministic extractor used at write time, and plain query\ntokens are additionally matched against stored entity aliases. Memories\nlinked to matched entities join RRF ranked by (number of matched entities,\nrecency), with the same namespace filter and the same `--as-of` temporal\npredicate as the other lanes — like the vector lane, an entity match in a\nFOREIGN namespace never leaks into the querying tier (it is found by its own\ntier's run, e.g. the global tier under `--include-global`). Works\nmodel-absent by design; an unknown alias contributes nothing.\n\n**Link expansion (v11, issue #61 6.3)**: after MMR and the passive-surface\nfilters, each result's `related`\u002F`supports` edges are walked ONE hop and up\nto `--link-budget` (default 2) neighbors are appended to the result set\n(never duplicating an already-present row, never crossing the tier's\nnamespace set, and always respecting the `--as-of` predicate — a neighbor\ninvalid at the requested instant does not surface). `contradicts`\nneighbors must additionally survive the confidence floor and are tagged\n`[CONTESTED LINK: \u003Cid>]` naming the other end of the contradiction.\nExpansion is bounded (one indexed lookup per result\nrow, budget-capped) — no PageRank or graph propagation of any kind.\n\n**MMR diversity (v10, issue #60 5.5)**: after the composite sort, and before\n`--limit`, Maximal Marginal Relevance re-orders each tier's candidates —\npicking the best-scoring row first, then trading relevance against\nsimilarity-to-already-picked (lambda: 0.7 default, `ZMEM_MMR_LAMBDA` env,\n`--no-mmr` to disable, 1.0 == no diversity). Row-to-row similarity is\nembedding cosine when both rows have embeddings, else Jaccard on normalized\ncontent tokens — so near-paraphrase clusters stop crowding out distinct\nfacts even on model-absent stores.\n\n**Entity cards (v10, issue #60 5.4)**: every recall row carries its linked\nentities — `entities: [{id, kind, name}]` in `recall --json`, at most three\nnames per row in the fenced hook render. `entity-list` inspects the entity\ntables; `entity-merge` reconciles duplicates.\n\n**Hybrid recall is the default** when the embedding runtime is available\n(auto-enabled; `--no-hybrid` forces lexical-only): a vector\u002Fembedding lane\nruns on top of this keyword pipeline and its ranking fuses with the lexical\nand entity lanes via the RRF above, so a memory BM25 missed can still\nsurface via embedding similarity. It needs the optional\nonnxruntime\u002Ftokenizers runtime and embedding model; without them recall\nfails open to plain keyword + entity ranking (see the `recall` command\nsection above for the full contract).\n\n**Entity extraction at write time is deterministic** (no LLM): explicit\n`entity:Name` \u002F `entity:\u003Ckind>:Name` tags, the `project:\u003Csuffix>` namespace\nsuffix, `--tags` tokens (`kind:Name` or plain), backtick-quoted spans in\ncontent (kind `tool`), and CamelCase identifiers (2+ humps). Every\nsource that carries NO explicit kind — untyped `entity:Name`, plain\n`--tags` tokens, CamelCase identifiers — is resolved by the\ndeterministic classifier (`reclassify-entities` documents its\nprecedence); a typed `entity:\u003Ckind>:Name`\u002F`kind:Name` tag and the\nbacktick-`tool` rule stay authoritative.\nStopwords (`the`, `and`, `use`, …) and path\u002FURL-shaped tokens are never\nentities; `person` entities exist only via an explicit `entity:person:`\ntag. The first kind seen for an alias wins; reconcile with `entity-merge`.\n\nThe `rebuild-fts` subcommand rebuilds the FTS5 index from scratch (useful after\nbulk imports or if the index drifts):\n```\n\u003Cinterpreter> \u003Cstore.py> rebuild-fts\n```\n\n## Sole memory system on Claude Code (replace native)\nWhen running under Claude Code with native memory turned off (`autoMemoryEnabled:\nfalse` in `~\u002F.claude\u002Fsettings.json`, or `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1`), ZMem\nis the **only** memory system in play — it is not layered on top of CC's own\nmemory. In that mode:\n- `store.py add` \u002F `recall` (this skill) is the canonical, sole path for\n  capturing and retrieving durable knowledge. Treat it exactly as you would\n  native memory on any other host.\n- **Do not hand-write** into CC's native `~\u002F.claude\u002F...\u002Fmemory\u002F` files or\n  `MEMORY.md` — that mechanism is intentionally disabled, and writing to it\n  would silently resurrect the duplicate-memory problem \"replace native\" exists\n  to avoid.\n- Project-level Tier 0 is `CLAUDE.md` (CC's own always-on mechanism, untouched\n  by ZMem) — not `AGENTS.md`. The SessionStart hook does not inject `AGENTS.md`\n  on Claude Code for this reason; it still does on ZCode.\n- If a SessionStart nudge appears telling you native memory still looks\n  enabled, that is informational for the user (a plugin cannot flip the\n  setting itself) — no action needed from you beyond surfacing it once.\n\n## Codex shared-store mode\nFor Codex, the safe cutover shape is:\n- **Disable Codex native memories yourself** in `~\u002F.codex\u002Fconfig.toml`; do not\n  let zmem auto-edit Codex config.\n- **Use one canonical physical store path** shared with the plugin hosts.\n- **Treat `ZMEM_TIER0=native` as the target shape**: keep Codex's own project\n  instruction surface native, and use zmem for shared Tier 2 durable memory.\n- **Do not assume Codex can write `~\u002F.zmem`.** If the shared store path is\n  outside Codex's writable roots, add a writable root or use a local broker\n  that owns the store and mediates read\u002Fwrite operations.\n- **Reapprove hooks after hook-surface changes.** Codex trusts hooks by a\n  SHA-256 over each normalized hook entry (event name + matcher + handler\n  fields). Every new or changed hook entry — this plugin's hooks file\n  changed in 0.28.0 (#95: PreToolUse + PreCompact entries) — carries a new\n  trusted hash, and an untrusted entry is SILENTLY SKIPPED: the hook simply\n  never runs. After upgrading, re-approve the hook surface in the Codex TUI\n  `\u002Fhooks` review (or re-record the `hooks.state` trusted hashes); for\n  vetted automation only, `codex exec --dangerously-bypass-hook-trust`\n  runs hooks without persisted trust for that invocation.\n\n## The reflection loop (Loop 1)\nThe `zmem-reflect.sh` Stop hook checks the episodic db for failed tool calls\n(status=error or exit_code!=0 on non-read-only tools) in the current session.\nIf found and no lesson references this session, it injects an additionalContext\nprompt at stop time. It is **non-blocking** (exit 0) — it only reminds you.\n\nThree env vars control the Stop hook's ZCode-db reader (#194): the reader opens\nthe db read-only (`mode=ro` URI plus `PRAGMA query_only`);\n`ZMEM_FAILURES_DB_TIMEOUT_S` bounds how long it waits on a busy ZCode db before\nreporting a substrate error (default 1.0; invalid or unset falls back to the\ndefault; clamped to 0.1-5.0; `store.py failures --db-timeout` overrides);\n`ZMEM_ZCODE_DB` overrides the ZCode db path (tests and operators can point the\ndetector at a scratch copy without touching `~\u002F.zcode`; empty\u002Funset means the\ndefault `~\u002F.zcode\u002Fcli\u002Fdb\u002Fdb.sqlite`); `ZMEM_REFLECT=0` (exactly `0`) disables\nthe whole Stop hook — unset, empty, or any other value keeps it enabled.\n\n### The no-failure nudge fires only on a signal change (#258)\nWhen the session has no tool failures and no rejections, the success nudge\n(\"you may have learned something worth capturing\") is gated on a state\nTRANSITION, not the state itself: it fires on the first Stop of a session\nor when a recognized runner signal appears or flips (`test`\u002F`compile`\u002F`lint`\nvia `capture_quality.infer_signal` over the transcript's Bash commands,\nread through `store.py signals`), and otherwise stays silent. Rejections\nneed no separate trigger — they have their own always-fire branch above\nthis gate (review PRR-001). \"Recognized\" is prefix-exact: bare `pytest`,\n`python -m pytest|unittest`, `python -m compileall`, `ruff check`, and\n`biome check` classify; compound commands (`cd x && pytest`, `uv run\npytest`) do not, so sessions that only invoke runners that way still see\nthe once-per-session behavior. The last-seen signal state is persisted per\nsession at `\u003CZMEM_DATA>\u002Fops\u002F\u003Csha256(session)[:32]>.signals` (atomic\ntmp-file + `os.replace` write; the finished file is swept on the shared\n7-day ops mtime policy, crashed-write `*.signals.tmp.*` orphans are\nreaped by `purge`) and re-recorded on every quiet Stop until a lesson is\ncaptured for the session — the lesson-dedup exit precedes the gate, so\nthe record freezes from that point (silent still means no\n`additionalContext`, never suppression of the signal itself): the\ncloseout skill's Step 0.5 candidate-review pass\n(`skills\u002Fcloseout\u002FSKILL.md`) is the intended future reader of that record;\na later PR extends it to surface suppressed signals. Substrate notes: on\nthe ZCode db substrate `tool_usage` has no command text, so no runner\nsignals are tracked and the nudge fires once per session; a missing\ntranscript path reads as a checked-empty scan (the hook treats it as a\nfresh quiet session); if `signals` cannot read the substrate (nonzero\nexit), the hook emits nothing and leaves the persisted state untouched —\na broken substrate must neither nag every Stop nor blank the record.\n\n### Subagent reflection is parent-side (#204)\nThe `zmem-subagent-reflect.sh` SubagentStop hook NEVER prompts the finishing\nsubagent. Claude Code honors additionalContext on (Subagent)Stop by continuing\nthe conversation, so a stop-time prompt would re-run the subagent turn and its\nreply (\"Memory captured\", \"blocked by sandbox guard, skipping\", …) would\nreplace the deliverable the orchestrator receives as the subagent's `\u003Cresult>`.\nInstead, when the subagent's own transcript shows failures (or rendered user\nrejections) and no lesson exists for `session:\u003Csid>:agent:\u003Caid>`, the hook\nwrites a hand-off sidecar under `\u003CZMEM_DATA>\u002Fsubagent-reflections\u002F` and emits\nthe empty envelope. The PARENT's own Stop hook scans that directory for this\nsession's sidecars, renders one reflection prompt covering all pending\nsubagent failures (pending sidecars alone are sufficient — a clean parent\ntranscript still prompts), and deletes the sidecars it rendered. Sidecars are\nconsumed on read, refreshed per agent on re-fire, and pruned when older than\n14 days, so at most one parent prompt surfaces per dispatched agent batch.\n`ZMEM_REFLECT=0` disables the subagent hook too (no sidecar). The Stop hook\nalso no-ops on Stop payloads carrying Claude subagent markers\n(`agent_id`\u002F`agent_transcript_path`) — defense-in-depth for hosts where Stop\nfires bare inside subagent contexts. Retention note: a sidecar persists the\nsubagent's failure details and rendered rejection reasons as plaintext JSON\nin this directory until the parent consumes it or the 14-day prune removes\nit (the prune runs from both the parent's Stop scan and each sidecar write).\n\nCapture a lesson only if it generalizes to a future session facing a similar\nsituation. If the failure was a one-off (typo, transient), do nothing — the prompt\nexplicitly allows that. Do not capture in-trajectory refinement tweaks as durable\nlessons; only capture what would help next time.\n",{"name":5,"description":7},"main","93a8d30750703261fe2b0a39760124f14062b7be","skills\u002Fmemory",2,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem","https:\u002F\u002Fgithub.com\u002Fzaxbysauce","2026-11-15 01:44:13",{"source":8,"skillName":5,"commands":31,"prompt":41,"zipAvailable":42},[32,35,38],{"tool":33,"command":34},"npx","npx skills add zaxbysauce\u002Fzmem --skill memory",{"tool":36,"command":37},"bunx","bunx skills add zaxbysauce\u002Fzmem --skill memory",{"tool":39,"command":40},"pnpm","pnpm dlx skills add zaxbysauce\u002Fzmem --skill memory","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002FSKILL.md",true,[44,50,54,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,256,260,264,268,272,276,280,284,288,292,296,300,304,308,312,316,320,324,328,332,336,340],{"path":45,"resourceType":46,"resourceTypeDesc":47,"sizeBytes":48,"sourceHtmlUrl":49},"models\u002Fconfig.json","REFERENCE","参考文档",650,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fmodels\u002Fconfig.json",{"path":51,"resourceType":46,"resourceTypeDesc":47,"sizeBytes":52,"sourceHtmlUrl":53},"models\u002Ftokenizer.json",131,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fmodels\u002Ftokenizer.json",{"path":55,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":58,"sourceHtmlUrl":59},"scripts\u002Fcapture_quality.py","SCRIPT","脚本",8843,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fcapture_quality.py",{"path":61,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":62,"sourceHtmlUrl":63},"scripts\u002Fcorrection_queue.py",21482,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fcorrection_queue.py",{"path":65,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":66,"sourceHtmlUrl":67},"scripts\u002Fcorrections.py",29505,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fcorrections.py",{"path":69,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":70,"sourceHtmlUrl":71},"scripts\u002Fcross_encoder_profiles.py",4474,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fcross_encoder_profiles.py",{"path":73,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":74,"sourceHtmlUrl":75},"scripts\u002Fdoctor.py",294500,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fdoctor.py",{"path":77,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":78,"sourceHtmlUrl":79},"scripts\u002Fdrift.py",19960,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fdrift.py",{"path":81,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":82,"sourceHtmlUrl":83},"scripts\u002Fembed_profiles.py",8919,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fembed_profiles.py",{"path":85,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":86,"sourceHtmlUrl":87},"scripts\u002Fembeddings.py",39292,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fembeddings.py",{"path":89,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":90,"sourceHtmlUrl":91},"scripts\u002Ffleet\u002F__init__.py",316,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Ffleet\u002F__init__.py",{"path":93,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":94,"sourceHtmlUrl":95},"scripts\u002Ffleet\u002Fbroker_cli.py",2696,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Ffleet\u002Fbroker_cli.py",{"path":97,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":98,"sourceHtmlUrl":99},"scripts\u002Ffleet\u002Fbroker.py",35284,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Ffleet\u002Fbroker.py",{"path":101,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":102,"sourceHtmlUrl":103},"scripts\u002Ffleet\u002Fmap_perms.py",7652,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Ffleet\u002Fmap_perms.py",{"path":105,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":106,"sourceHtmlUrl":107},"scripts\u002Ffleet\u002Fopqueue.py",15322,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Ffleet\u002Fopqueue.py",{"path":109,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":110,"sourceHtmlUrl":111},"scripts\u002Ffleet\u002Fsnapshot.py",41165,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Ffleet\u002Fsnapshot.py",{"path":113,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":114,"sourceHtmlUrl":115},"scripts\u002Ffleet\u002Ftransport.py",40548,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Ffleet\u002Ftransport.py",{"path":117,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":118,"sourceHtmlUrl":119},"scripts\u002Fhistory_mining.py",22098,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fhistory_mining.py",{"path":121,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":122,"sourceHtmlUrl":123},"scripts\u002Fhost.py",70653,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fhost.py",{"path":125,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":126,"sourceHtmlUrl":127},"scripts\u002Fimport-store.py",52867,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fimport-store.py",{"path":129,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":130,"sourceHtmlUrl":131},"scripts\u002Fopencode_bridge.py",6728,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fopencode_bridge.py",{"path":133,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":134,"sourceHtmlUrl":135},"scripts\u002Fprotocol.py",4598,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fprotocol.py",{"path":137,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":138,"sourceHtmlUrl":139},"scripts\u002Fredaction.py",11371,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fredaction.py",{"path":141,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":142,"sourceHtmlUrl":143},"scripts\u002Frequirements-training.txt",170,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Frequirements-training.txt",{"path":145,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":146,"sourceHtmlUrl":147},"scripts\u002Fschema_meta.py",36879,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fschema_meta.py",{"path":149,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":150,"sourceHtmlUrl":151},"scripts\u002Fstore.py",3086,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstore.py",{"path":153,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":154,"sourceHtmlUrl":155},"scripts\u002Fstorelib\u002F__init__.py",18845,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002F__init__.py",{"path":157,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":158,"sourceHtmlUrl":159},"scripts\u002Fstorelib\u002Fbackup.py",55167,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fbackup.py",{"path":161,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":162,"sourceHtmlUrl":163},"scripts\u002Fstorelib\u002Fbeliefs.py",41588,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fbeliefs.py",{"path":165,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":166,"sourceHtmlUrl":167},"scripts\u002Fstorelib\u002Fcli.py",369942,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fcli.py",{"path":169,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":170,"sourceHtmlUrl":171},"scripts\u002Fstorelib\u002Fconfig.py",4153,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fconfig.py",{"path":173,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":174,"sourceHtmlUrl":175},"scripts\u002Fstorelib\u002Fconsolidate.py",94287,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fconsolidate.py",{"path":177,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":178,"sourceHtmlUrl":179},"scripts\u002Fstorelib\u002Fcross_encoder.py",22846,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fcross_encoder.py",{"path":181,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":182,"sourceHtmlUrl":183},"scripts\u002Fstorelib\u002Fdataset.py",61025,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fdataset.py",{"path":185,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":186,"sourceHtmlUrl":187},"scripts\u002Fstorelib\u002Fdecision_report.py",33708,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fdecision_report.py",{"path":189,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":190,"sourceHtmlUrl":191},"scripts\u002Fstorelib\u002Fdelivery_ledger.py",96317,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fdelivery_ledger.py",{"path":193,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":194,"sourceHtmlUrl":195},"scripts\u002Fstorelib\u002Fentity.py",107981,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fentity.py",{"path":197,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":198,"sourceHtmlUrl":199},"scripts\u002Fstorelib\u002Fepisodes.py",14828,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fepisodes.py",{"path":201,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":202,"sourceHtmlUrl":203},"scripts\u002Fstorelib\u002Feval_gold.py",45159,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Feval_gold.py",{"path":205,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":206,"sourceHtmlUrl":207},"scripts\u002Fstorelib\u002Fevidence.py",21403,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fevidence.py",{"path":209,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":210,"sourceHtmlUrl":211},"scripts\u002Fstorelib\u002Ffalse_inject.py",18303,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Ffalse_inject.py",{"path":213,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":214,"sourceHtmlUrl":215},"scripts\u002Fstorelib\u002Ffeedback.py",10368,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Ffeedback.py",{"path":217,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":218,"sourceHtmlUrl":219},"scripts\u002Fstorelib\u002Fhygiene.py",13843,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fhygiene.py",{"path":221,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":222,"sourceHtmlUrl":223},"scripts\u002Fstorelib\u002Finject.py",89608,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Finject.py",{"path":225,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":226,"sourceHtmlUrl":227},"scripts\u002Fstorelib\u002Flinks.py",36393,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Flinks.py",{"path":229,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":230,"sourceHtmlUrl":231},"scripts\u002Fstorelib\u002Flog_rotate.py",9758,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Flog_rotate.py",{"path":233,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":234,"sourceHtmlUrl":235},"scripts\u002Fstorelib\u002Fmine.py",111242,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fmine.py",{"path":237,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":238,"sourceHtmlUrl":239},"scripts\u002Fstorelib\u002Fmiss_rate.py",72920,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fmiss_rate.py",{"path":241,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":242,"sourceHtmlUrl":243},"scripts\u002Fstorelib\u002Fnamespace_cache.py",4214,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fnamespace_cache.py",{"path":245,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":246,"sourceHtmlUrl":247},"scripts\u002Fstorelib\u002Fopcmds.py",1210,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fopcmds.py",{"path":249,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":250,"sourceHtmlUrl":251},"scripts\u002Fstorelib\u002Fops_tokens.py",29164,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fops_tokens.py",{"path":253,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":254,"sourceHtmlUrl":255},"scripts\u002Fstorelib\u002Fops.py",31202,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fops.py",{"path":257,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":258,"sourceHtmlUrl":259},"scripts\u002Fstorelib\u002Forganize.py",58825,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Forganize.py",{"path":261,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":262,"sourceHtmlUrl":263},"scripts\u002Fstorelib\u002Fpages.py",66491,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fpages.py",{"path":265,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":266,"sourceHtmlUrl":267},"scripts\u002Fstorelib\u002Fpromote.py",11730,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fpromote.py",{"path":269,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":270,"sourceHtmlUrl":271},"scripts\u002Fstorelib\u002Fpurge.py",74019,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fpurge.py",{"path":273,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":274,"sourceHtmlUrl":275},"scripts\u002Fstorelib\u002Fquery_ambiguity.py",7679,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fquery_ambiguity.py",{"path":277,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":278,"sourceHtmlUrl":279},"scripts\u002Fstorelib\u002Freadonly.py",17225,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Freadonly.py",{"path":281,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":282,"sourceHtmlUrl":283},"scripts\u002Fstorelib\u002Frecall.py",308762,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Frecall.py",{"path":285,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":286,"sourceHtmlUrl":287},"scripts\u002Fstorelib\u002Frekey.py",16841,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Frekey.py",{"path":289,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":290,"sourceHtmlUrl":291},"scripts\u002Fstorelib\u002Fremote_gate.py",30537,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fremote_gate.py",{"path":293,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":294,"sourceHtmlUrl":295},"scripts\u002Fstorelib\u002Fretain.py",30550,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fretain.py",{"path":297,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":298,"sourceHtmlUrl":299},"scripts\u002Fstorelib\u002Fschema.py",123696,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fschema.py",{"path":301,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":302,"sourceHtmlUrl":303},"scripts\u002Fstorelib\u002Fsource.py",23547,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fsource.py",{"path":305,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":306,"sourceHtmlUrl":307},"scripts\u002Fstorelib\u002Fstate_expiry.py",5718,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fstate_expiry.py",{"path":309,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":310,"sourceHtmlUrl":311},"scripts\u002Fstorelib\u002Fstate.py",7605,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fstate.py",{"path":313,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":314,"sourceHtmlUrl":315},"scripts\u002Fstorelib\u002Fsubject.py",4027,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fsubject.py",{"path":317,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":318,"sourceHtmlUrl":319},"scripts\u002Fstorelib\u002Fsync.py",179101,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fsync.py",{"path":321,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":322,"sourceHtmlUrl":323},"scripts\u002Fstorelib\u002Ftemporal.py",7665,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Ftemporal.py",{"path":325,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":326,"sourceHtmlUrl":327},"scripts\u002Fstorelib\u002Ftraining_capture.py",56824,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Ftraining_capture.py",{"path":329,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":330,"sourceHtmlUrl":331},"scripts\u002Fstorelib\u002Ftraining_export.py",598,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Ftraining_export.py",{"path":333,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":334,"sourceHtmlUrl":335},"scripts\u002Fstorelib\u002Ftraining.py",71363,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Ftraining.py",{"path":337,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":338,"sourceHtmlUrl":339},"scripts\u002Fstorelib\u002Ftune.py",6244,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Ftune.py",{"path":341,"resourceType":56,"resourceTypeDesc":57,"sizeBytes":342,"sourceHtmlUrl":343},"scripts\u002Fstorelib\u002Fwrite.py",146079,"https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fmemory\u002Fscripts\u002Fstorelib\u002Fwrite.py",[345,350,355,360,365,372],{"skillNo":346,"name":5,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":347,"categories":348,"keywords":349,"publishedAt":18},"01M78CEQCEHS30WN0456HFVZZZ","https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fhosts\u002Fexpected\u002Fcache\u002Fzcode\u002Fskills\u002Fmemory\u002FSKILL.md",[],[],{"skillNo":351,"name":5,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":352,"categories":353,"keywords":354,"publishedAt":18},"01M78CEPF4DNY1QASCA42AJ0FF","https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fhosts\u002Fexpected\u002Fcache\u002Fcodex\u002Fskills\u002Fmemory\u002FSKILL.md",[],[],{"skillNo":356,"name":5,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":357,"categories":358,"keywords":359,"publishedAt":18},"01M78CENGZKX5GMJMJS0BRHJZX","https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fhosts\u002Fexpected\u002Fcache\u002Fclaude\u002Fskills\u002Fmemory\u002FSKILL.md",[],[],{"skillNo":361,"name":5,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":362,"categories":363,"keywords":364,"publishedAt":18},"01M78CEMHW23T1R9DACHBWX9PE","https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fhosts\u002Fcheckout\u002Fskills\u002Fmemory\u002FSKILL.md",[],[],{"skillNo":366,"name":367,"nameZh":6,"description":368,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":369,"categories":370,"keywords":371,"publishedAt":18},"01M78CEBF8MGZNEDFY2ABV7GF2","swarm-pr-review","Run a graph-guided, tool-augmented PR review using context packing, parallel exploration, mandatory repository-agnostic risk-family coverage with dispatch scaled to diff size and risk, independent reviewer validation, critic challenge, and metrics writeback. Use for deep pull request review with low false-positive tolerance and high recall in any repository, on any agent harness (structured lane controller, native parallel subagents, or single-context sequential passes).","https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fswarm-pr-review\u002FSKILL.md",[],[],{"skillNo":373,"name":374,"nameZh":6,"description":375,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":376,"categories":377,"keywords":378,"publishedAt":18},"01M78CE2AZC8SR86K5574A6W8P","swarm-pr-feedback","Ingest and resolve known pull request feedback with skeptical source verification. Use when addressing pasted PR feedback, GitHub review comments or threads, requested changes, CI\u002Fcheck failures, merge conflicts, stale PR branches, or PR follow-up work that must close all known issues without dropping findings. Supports multi-round bot reviews when the repo uses an auto-review bot that posts a new review after every push, via the iterative pattern documented in the body. Stage A (structural pre-checks) and Stage B (reviewer + test_engineer) gates and the reviewer + critic closeout gate are MANDATORY for any change made as part of this process.","https:\u002F\u002Fgithub.com\u002Fzaxbysauce\u002Fzmem\u002Fblob\u002Fmain\u002Fskills\u002Fswarm-pr-feedback\u002FSKILL.md",[],[]]