Skip to content

feat(v2.6): write-gate + scarcity quota + optional auto-recall hooks - #1

Merged
MXAntian merged 4 commits into
mainfrom
feature/v2.6-write-gate-and-hooks
Jul 9, 2026
Merged

MXAntian merged 4 commits into
mainfrom
feature/v2.6-write-gate-and-hooks

Conversation

@MXAntian

@MXAntian MXAntian commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Write-gate for meta_knowledge (meta-gate.mjs): content with concrete bindings (ISO date, mem-rowid ref, commit hash, absolute path, version, project/multi-person names) auto-downgrades to semi_abstract. Signal words like cross-project / heuristic exempt soft bindings but never hard ones.
  • Scarcity layer (migration 008): is_anchor (cap 40) + is_pinned (cap 30) boolean columns with partial indices. Hard quotas force callers to trade off instead of inflating importance to 9-10 for everything.
  • Optional Claude Code auto-recall hooks (new hooks/ dir): prompt-recall.mjs (UserPromptSubmit) + tool-recall-pre.mjs (PreToolUse for Bash/Grep/Read/Glob). Fail-soft, env-var configured, session-scoped dedup.
  • Store response now surfaces metaDowngrade + quotaRejected alongside nearDuplicates.

Why

A real snapshot of an active mneme DB measured:

  • 71% memory_level = `meta_knowledge`
  • 61% importance >= 8
  • 16.4% importance >= 9

The v2.4 "default `semi_abstract`" knob only affected the tool parameter default — self-discipline on wording didn't hold. This PR moves enforcement from documentation to code: gate at write, quota at write, both detection-only (never reject the store — same philosophy as the v2.4 near-dup gate).

Inspired by Ombre-Brain's scarcity-as-structure design. We kept the write-gate mechanical instead of borrowing Ombre's Russell emotional coordinate (cost-heavy for private single-user use, cross-purposes with our query-driven RRF recall).

Migration path

  • Migration 008 is inlined into `initMemory` alongside 003/004/006/007, so `ALTER TABLE ADD COLUMN` runs transparently on live DBs. Fresh installs get the columns from `schema.sql` after the tokenizer probe path.
  • Existing stores keep working: `is_anchor` / `is_pinned` default to 0. No caller changes required unless you want to opt into the flags.
  • No changes to recall ranking or scoring in this PR (that's for a follow-up once the write-side gates are validated in the wild).

Test plan

  • `node meta-gate.test.mjs` — 11/11 unit cases (hard bindings, signal-word exemptions, pass-through)
  • `node meta-gate.integration.test.mjs` — 3/3 e2e (downgrade, no-downgrade, non-meta pass-through)
  • `node anchor-pinned.integration.test.mjs` — 4/4 e2e (flag set, neutral, boundary quota rejection)
  • `node hooks/hooks.test.mjs` — 9/9 hook e2e (trigger, no-trigger, garbage stdin, glob-pure-pattern skip, unknown tool skip)
  • External review welcome on: 40/30 quota choices, generic trigger word set, docs §5 wiring examples

Predictions

  • fix: writing-side enforcement + hard quotas should compress the current 71% meta / 61% imp>=8 skew starting from the point of ingest.
  • risk: (1) meta-gate may over-downgrade domain-specific jargon — signal-word exemption exists but user has to know about it; (2) hook trigger set is generic EN+ZH — domain jargon will see fewer injections and need `TRIGGERS` fork; (3) 40/30 quotas are opinionated — future data may show they're too tight, one-line change to bump.
  • verify: rerun the same stats query a month from now — new `memory_level=meta` should be < 40% (currently 71%), new `importance>=9` should be < 10% (currently 16.4%); hook e2e 9/9 passes locally.

MXAntian and others added 2 commits July 9, 2026 21:51
Motivated by Ombre-Brain competitive research plus a real snapshot of an
active mneme DB: 71% of memories were memory_level=meta_knowledge and 61%
had importance>=8 — the "default semi_abstract" knob wasn't enough on its
own; we needed write-time enforcement and hard quotas.

Anti-inflation (v2.5 -> v2.6, migration 008)
* meta-gate.mjs (new): write-time gate. store_memory with meta_knowledge
  whose content has concrete bindings — ISO date, mem-rowid ref, commit
  hash, absolute path, version string, project or multi-person names —
  auto-downgrades to semi_abstract. Signal words like "cross-project" or
  "heuristic" exempt soft bindings (project/person) but never hard ones.
* migrations/008-add-anchor-pinned.sql: is_anchor (cap 40) / is_pinned
  (cap 30) boolean columns + partial indices. Scarcity forces callers to
  trade off instead of inflating importance to 9-10 for everything.
* index.mjs storeMemory: adds an opts parameter that carries downgrade
  and quota signals back to the caller via opts.out. Quota check on
  is_anchor/is_pinned; over-quota resets the flag to 0 and pushes
  opts.out.quotaRejected up for the MCP layer to surface. Migration 008
  is inlined into initMemory alongside the earlier migrations, so
  upgrading a live DB is transparent.
* mcp-server.mjs store_memory: tool description now explains the gate,
  clarifies importance is a display-only weak prior (not fed into decay
  or auto-forget), exposes is_anchor / is_pinned params, and surfaces
  both metaDowngrade and quotaRejected in the response body.
* scripts/anchor-pinned-candidates.mjs: dry-run helper to pick migration
  candidates from an existing DB by (access_count DESC, importance DESC).

Optional Claude Code auto-recall hooks (new hooks/ directory)
* hooks/prompt-recall.mjs: UserPromptSubmit hook. Fires on generic
  "how do I / where is / config / daemon" triggers (EN + ZH), injects top
  hits with importance >= 6 via hookSpecificOutput.additionalContext.
  Fully env-var configurable (MNEME_DB_PATH / MNEME_MIN_IMPORTANCE / ...).
* hooks/tool-recall-pre.mjs: PreToolUse hook for Bash | Grep | Read | Glob.
  Extracts a short query from the tool args (command head, grep pattern,
  file basename, glob stem) and surfaces relevant memories before the
  tool runs. Fail-soft: any error or timeout exits 0 silently.
* hooks/hooks.test.mjs: 9/9 end-to-end tests covering trigger, no-trigger,
  garbage stdin, glob-pure-pattern skip, unknown tool skip.
* docs/configuring-your-agent.{md,zh-CN.md} section 5: wiring examples,
  PreToolUse matcher reference, env-var table, opt-in debug patterns
  (stderr echo, jsonl trace), troubleshooting.

Bump package version to 2.6.0.

[prediction] fix: writing-side enforcement + hard quotas should compress
   the current 71% meta / 61% imp>=8 skew starting from the point of ingest.
[prediction] risk: (1) meta-gate may over-downgrade domain-specific
   jargon — signal-word exemption exists but user has to know about it;
   (2) hook trigger set is generic EN+ZH — if the user's prompts use
   domain jargon, they'll see fewer injections and need to fork TRIGGERS;
   (3) 40/30 quotas are opinionated — future data may show they're too
   tight, in which case bumping them is a one-line change.
[prediction] verify: rerun the same stats query a month from now — new
   memory_level=meta should be < 40% (currently 71%), new importance>=9
   should be < 10% (currently 16.4%); hook e2e 9/9 passes locally.

Co-Authored-By: 千夏 <chinatsu@clawgamers.com>
… scope

Address 6 findings from the independent review on #1:

P0
* meta-gate.mjs: PROJECT_NAMES and PERSON_NAMES no longer ship any hardcoded
  team roster or product names. Both default to empty and load from
  MNEME_META_GATE_PROJECT_NAMES / MNEME_META_GATE_PERSON_NAMES env vars
  (comma-separated). Signal words also load from
  MNEME_META_GATE_SIGNAL_WORDS with a small EN+ZH default set.
* meta-gate.mjs commit_hash regex: /\b[a-f0-9]{7,40}\b/ over-matched pure
  digits (port 8080000, seven-digit counters). Now requires at least one
  [a-f] via a lookahead: /\b(?=[a-f0-9]*[a-f])[a-f0-9]{7,40}\b/.
* index.mjs storeMemory: is_anchor / is_pinned COUNT check moved INSIDE the
  INSERT transaction so two concurrent writers can't both pass a full cap
  and produce a 41st anchor. quotaRejected is populated in the tx and
  surfaced on opts.out after commit.
* index.mjs migration 008: bare `catch {}` around ALTER TABLE was
  swallowing disk-full, permission, and WAL-lock errors. Narrowed to only
  swallow "duplicate column name" (the second-run replay case). The
  CREATE INDEX IF NOT EXISTS calls are naturally idempotent and no longer
  need a catch at all.

P1
* docs/configuring-your-agent.{md,zh-CN.md}: env-var table added
  MNEME_TOOL_QUERY_LEN (was exposed in tool-recall-pre.mjs but missing
  from docs).
* hooks/hooks.test.mjs: swapped E:/Project/chinatsu-workspace/... test
  fixture paths for relative ones (mcp-server.mjs, scripts/...).

Regenerated meta-gate.test.mjs to use dynamic import + env-configured
vocabs so the multi-person case still exercises soft bindings. 13/13
unit cases pass; 4/4 anchor-pinned integration cases pass (including the
41-row over-quota rejection); 9/9 hook e2e cases pass.

[prediction] fix: closes the leak vector (soft-binding vocabs) that let
   team names into a public MIT repo AND closes the false-positive
   commit-hash match on pure-digit content.
[prediction] risk: (1) callers who relied on the old defaults will see
   fewer downgrades until they set the env vars — I'll add those to
   chinatsu-workspace/.env.local separately; (2) the tx-internal COUNT
   adds one query per is_anchor/is_pinned store, but only when the flag
   is requested.
[prediction] verify: rerun meta-gate.test.mjs (13/13) and
   anchor-pinned.integration.test.mjs (4/4) — see the case4 log showing
   `41/40` rejection from inside the transaction.

Co-Authored-By: 千夏 <chinatsu@clawgamers.com>
@MXAntian
MXAntian marked this pull request as ready for review July 9, 2026 14:09
MXAntian and others added 2 commits July 9, 2026 22:44
Second-round independent review (DeepSeek + Codex, focus complementary to
the first round). 8 findings addressed:

P1
* scripts/anchor-pinned-candidates.mjs: DB path was hardcoded to the
  repo-relative engram.db, ignoring TOKENMEM_DB_PATH. Now uses the same
  resolution order as index.mjs — env override → existing engram.db →
  fresh-install tokenmem.db default — and also accepts a --db=<path> flag.
* meta-gate.mjs: abs_path hard binding was Windows-only. `/etc/mneme/config`
  and `/home/user/foo.js` never matched, so POSIX users silently kept
  meta_knowledge on path-bound content. Added a POSIX branch (leading /,
  ≥ 2 path segments); Windows drive-letter branch preserved.

P2
* hooks/{prompt-recall,tool-recall-pre}.mjs: parseInt was unchecked. A bad
  env like MNEME_TIMEOUT_MS=abc yielded NaN, which Node's setTimeout maps
  to `1ms` and prints TimeoutNaNWarning to stderr — violating the "silent
  fail-soft" contract. Introduced intEnv() using Number.isFinite; bad
  values silently fall back to defaults.
* hooks/tool-recall-pre.mjs extractQuery: the Read case only stripped the
  last extension, so foo.test.mjs → foo.test kept noise tokens in the
  recall query. Now strips all trailing extensions.
* hooks/tool-recall-pre.mjs extractQuery: the Glob stem length floor was
  ≥ 5, which rejected useful short patterns like src/**/*.ts. Lowered to
  ≥ 3 with a letter requirement so short dir stems (src, lib, api) pass
  while pure short ext stems (js, ts) don't.
* hooks/{prompt-recall,tool-recall-pre}.mjs saveInjected: the dedup state
  file was written with a single writeFileSync — a kill mid-write left
  truncated JSON, and the next load lost all dedup for the session. Now
  writes to a per-pid `.tmp-<pid>` then rename() — atomic, previous state
  survives crashes.
* Public-repo hygiene: cleaned residual `千夏` / `chinatsu-workspace` /
  `[2026-05-29 千夏]` attributions in comments across index.mjs,
  mcp-server.mjs, and hooks.test.mjs. Comments now describe the mechanism
  neutrally.
* docs/configuring-your-agent.{md,zh-CN.md}: added § 4b documenting
  MNEME_META_GATE_PROJECT_NAMES / PERSON_NAMES / SIGNAL_WORDS. Previously
  only mentioned in the meta-gate.mjs source header — public users would
  never discover soft matching without reading the code.

Tests
* meta-gate 14/14 (added POSIX abs_path case)
* anchor-pinned integration 4/4
* hooks e2e 10/10 (split glob-pure-pattern into short-stem-skip and
  dir-stem-fire cases)
* Manual probe: MNEME_TIMEOUT_MS=abc + prompt trigger → exit 0, no
  TimeoutNaNWarning on stderr, hook injects normally.

[prediction] fix: v2.6 write-gate now behaves consistently across POSIX
   and Windows for absolute paths, the migration helper obeys env-set
   DB paths, hooks are truly silent on bad env, and the public repo no
   longer carries private-workspace attribution.
[prediction] risk: (1) POSIX abs_path regex may match phrases where a
   noun happens to be `/foo/bar` inside prose — worst case is unnecessary
   downgrade, not data loss; (2) stem length ≥ 3 accepts short ext-like
   patterns (mjs, mid, cpp) as queries — cheap recall, low harm.
[prediction] verify: meta-gate.test.mjs 14/14, hooks.test.mjs 10/10, and
   `MNEME_TIMEOUT_MS=abc echo '...' | node hooks/prompt-recall.mjs 2>&1`
   shows no NaN warning.

Co-Authored-By: 千夏 <chinatsu@clawgamers.com>
Runs on push to main and every PR. Matrix: Node 20 and 22 on ubuntu-latest.

Stages
* npm install — verifies package.json deps resolve + native better-sqlite3
  builds on the runner (no lockfile in-repo, so `install` not `ci`).
* Syntax check — `node --check` on every .mjs (excluding node_modules).
  Catches typos and broken imports before test binaries even start.
* Unit — meta-gate.test.mjs (pure JS, no DB, 14 cases including the POSIX
  abs_path case added in the round-2 fix).
* Integration — meta-gate + anchor-pinned quota + hooks e2e, each pointed
  at a per-run temp DB via TOKENMEM_DB_PATH (${runner.temp}).

Test surface adjustments
* anchor-pinned.integration.test.mjs now resolves DB_PATH the same way
  index.mjs does (env → repo engram.db → tokenmem.db fallback) so
  TOKENMEM_DB_PATH lets CI point it at a fresh temp DB.
* Same file now pads the memories table with 40 raw-INSERT rows when the
  fresh DB doesn't have enough to trip the quota — the case-4 assertion
  needed 40 existing rows to flip to anchor=1, and a fresh DB only has
  the 3 rows from cases 1-3.
* Both write connections optionally loadExtension('simple') so a machine
  that DID load libsimple during initMemory (which then migrates
  memories_fts to tokenize='simple 0') can still INSERT via a fresh
  connection without hitting "no such tokenizer: simple". CI on ubuntu
  never has the .so and stays on unicode61 — the loadExtension is
  gated on existsSync.

Verified locally on both configurations
* With lib/libsimple-windows-x64/ present → 4/4 anchor-pinned pass
* With lib/ renamed away → 4/4 anchor-pinned pass (unicode61 fallback)

[prediction] fix: mneme now has automated regression protection instead
   of relying on reviewers to remember to run the tests.
[prediction] risk: (1) better-sqlite3 needs a native build on the runner
   — expected ~30s on ubuntu with cached apt tools; (2) node 20/22 matrix
   catches ABI drift up front but doubles CI minutes.
[prediction] verify: check the Actions tab on
   https://github.com/AgentGameLab/mneme/actions after this push — expect
   both matrix cells green within ~2 minutes.

Co-Authored-By: 千夏 <chinatsu@clawgamers.com>
@MXAntian
MXAntian merged commit dc9b024 into main Jul 9, 2026
2 checks passed
DottytheHomeless added a commit that referenced this pull request Aug 3, 2026
The conversations table and the recall conversation-fallback shipped a while
back, but nothing fed them — the table sat at 0 rows, so the verbatim fallback
layer effectively did not exist. What kept it broken was structural, not a bug:
the only call site of indexSessionTranscripts() lived in one contributor's
private harness repo, so fixing the engine reached nobody else.

Shipping the sweep next to the engine closes that gap — clone mneme and the
ingest is already there, no per-machine path wiring. Log path is overridable
via MNEME_SWEEP_LOG (use "-" for stderr only); logs/ is gitignored.

Verified on merged main: files=1494 rescanned=4 skipped=669 newMessages=22 in 0.4s.

Follow-up to #18 review caveat #1.

Co-authored-by: 小梦 <xiaomeng@clawgamers.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@MXAntian
MXAntian deleted the feature/v2.6-write-gate-and-hooks branch October 1, 2026 07:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant