Skip to content

feat(core,python): store-time write gate for every Hermes write path + DEP-3 Part B - #719

Draft
p-diogo wants to merge 9 commits into
feat/dep-5-pin-aware-resolutionfrom
feat/write-gate-dedup
Draft

p-diogo wants to merge 9 commits into
feat/dep-5-pin-aware-resolutionfrom
feat/write-gate-dedup

Conversation

@p-diogo

@p-diogo p-diogo commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

Depends on #702 (base branch) and #701 (merged in). Merge #701 and #702 first, then retarget this PR to main. Draft until the core audit clears.

What

Every Hermes write path now asks one store-time gate before it stores a fact. It also finishes DEP-3 Part B (topical dedup context). Refs internal #632, #501, #537.

The QA near-duplicates (#632, #501) were not caused by a weak dedup context. They came from write paths with no near-duplicate check (USER.md mirror, reconcile, explicit totalreclaw_remember) and from subgraph lag: back-to-back writes could not see each other.

Write gate

  • Core write_gate.rs: normalize_fact_text (NFC, case, typographic quotes and dashes, whitespace, edge punctuation) and decide_write(text, embedding, candidates, threshold, mode). PyO3 normalize_fact_text / decide_write, WASM normalizeFactText / decideWrite. Fixture write-gate-v1.json from an independent Python reference, checked in Rust, WASM and Python.
  • Candidates: topical recall on the new text (include_embeddings=True), plus an in-memory ledger of this process's writes from the last 15 minutes. The ledger covers subgraph lag. forget drops entries, and pin/unpin/retype/set_scope move them to the new id. It never touches disk.
  • Decision:
    • An exact match after normalization is skipped on every path. totalreclaw_remember returns already_stored.
    • A near match (cosine >= 0.85, the existing MemQ threshold) depends on the path:
      • auto-extraction skips it;
      • totalreclaw_remember returns possible_duplicate with the stored text, and the agent retries with force=true if the new text adds or changes information;
      • the mirror and reconcile store it.
  • Pins: the gate never replaces or tombstones anything, so a duplicate of a pin leaves the pin alone. Auto mode ignores pinned candidates for the cosine rule, so an ADD with new information about a pin is kept.

Why a near match is not a silent skip everywhere

Measured with Harrier on this machine: value changes score as high as paraphrases.

Pair Cosine
"User is vegetarian" / "User is vegetarian." 0.938
"drinks black coffee every morning" / "has black coffee each morning" 0.944
"lives in Lisbon" / "lives in Porto" 0.898
"Pin lock timeout is 30 minutes" / "... 60 minutes" 0.959
"1:1 with Inês is on Tuesdays" / "... moved to Thursdays" 0.924
"prefers dark mode" / "prefers light mode" 0.901

A silent cosine skip on the mirror or on explicit remember would drop corrections. The paths that have an LLM judge near matches, and the others skip only exact copies.

DEP-3 Part B

  • _fetch_recent_memories(state, messages, mode) keeps its name, because hermes/hooks.py imports it. It fetches:
    • topical top-20 via recall on the last 4 messages, capped at 500 chars, with include_embeddings=True;
    • ledger facts;
    • the 10 newest facts (new fetch_recent_facts, one subgraph read).
  • The window embedding only runs when the model is already local (embedding.is_model_available_locally()), so this fetch never starts the download.
  • Rendered by core build_dedup_context, with a pure-Python fallback checked against the same fixture. Pinned facts come first.
  • Read-only pins (changes feat(core): topical dedup context — build_dedup_context + WASM/PyO3 bindings (PRD-04 DEP-3, Part A) #701's formatter): pinned lines read [ID: x] [pinned, read-only] text, and the block ends with a note: never UPDATE/DELETE a pin, ADD new information instead. Without it the LLM aims UPDATEs at pins and the feat(core,python): pin-aware resolution + recall pin boost (PRD-04 DEP-5, Part A) #702 guard drops the new information. dedup-context-v1.json regenerated in place (17 cases, unreleased).
  • An UPDATE without existingFactId becomes an ADD.
  • No pinned-candidates scan. I dropped the plan's PINNED_CANDIDATES_QUERY (up to 5 × 1,000 rows, and its decayScore_gte filter is unverified on staging). feat(core,python): pin-aware resolution + recall pin boost (PRD-04 DEP-5, Part A) #702 puts pin state on every search result, so topical and recent pins are marked. A pin that is neither topical nor recent is not relevant to this window, and the feat(core,python): pin-aware resolution + recall pin boost (PRD-04 DEP-5, Part A) #702 guard protects it anyway.
  • Core floor not raised. pyproject still says >=2.6.1. The fallbacks make the Python package work on 2.6.1, verified by running the gate tests against the PyPI 2.6.1 wheel. Raise the floor when core ships decide_write / build_dedup_context.

Test evidence

  • cargo test (core): 758 + 4 passed. PyO3 python::tests: 54 passed. cargo clippy is clean on the new code.
  • Python suite (CI command): 2978 passed, 7 skipped, 1 xfailed.
  • WASM parity: write-gate 113/113, dedup-context 42/42. check-pyfunction-parity, check-docs and check-phrase-safety all OK.
  • The new behaviour tests in test_write_gate.py were written first. Red: 15 failed. Green after the change. They cover:
    • mirror + explicit remember of the same fact → one write;
    • two back-to-back extractions in the lag window → one write (exact and paraphrase);
    • a paraphrase is caught and a different fact is not (fake vectors, plus real Harrier when it is cached);
    • pin, forget-then-remember, and mirror-replace cases.
  • One existing test fake gained **kwargs (test_hermes_plugin.py).

Not done

  • No staging E2E. TS/MCP not ported (see Known Gaps in CLAUDE.md).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bd3G1BkVMFSXkcdAQ7nFax

p-diogo and others added 9 commits September 30, 2026 18:01
…ure (PRD-04 DEP-3)

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Resolve conflicts in ci.yml (keep both the DEP-6 and DEP-3 parity steps)
and the core CHANGELOG ([Unreleased] above 2.6.1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bd3G1BkVMFSXkcdAQ7nFax
Review fixes for PRD-04 DEP-3 Part A (refs internal #537):

- Collapse every line-break character, not just CR and LF. U+2028,
  U+2029, NEL, VT, FF and FS/GS/RS (the str.splitlines set) could still
  put a forged "[ID: ...]" on its own line for any model or renderer that
  honours them.
- WASM buildDedupContext: cap is now optional (default 30) and must be a
  non-negative integer. As a usize, -1 silently meant "no cap" and
  NaN/undefined silently gave an empty block.
- PyO3 build_dedup_context: cap defaults to 30.
- Fixture generator strips Unicode White_Space explicitly, matching Rust
  str::trim (Python str.strip also strips U+001C..U+001F). Two new
  fixture cases cover both; the 14 existing vectors are unchanged.
- Module doc states the caller contract (section sizes are the caller's
  job; pins crowd out the rest at cap; strip embeddings before
  serialising).
- New tests: non-array section ValueError (PyO3), default cap, cap
  validation (WASM), id collapse before dedupe, 5,000-pin section stops
  at cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bd3G1BkVMFSXkcdAQ7nFax
Stacks the write gate and DEP-3 Part B on #702 and #701. Only conflict:
core CHANGELOG Unreleased/Added, both entries kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bd3G1BkVMFSXkcdAQ7nFax
write_gate: normalize_fact_text (NFC, case, typographic folding,
whitespace, edge punctuation) and decide_write (exact match skips in
every mode; cosine >= 0.85 skips in auto, confirms in explicit, stores
in exact_only). PyO3 + WASM bindings, write-gate-v1 fixture from an
independent Python reference, TS parity test + CI step.

dedup_context (unreleased, #701): pinned lines carry
"[pinned, read-only]" and the block ends with a note telling the LLM to
ADD new information instead of updating a pin. Fixture regenerated in
place (17 cases).

Refs internal #632, #501, #537.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bd3G1BkVMFSXkcdAQ7nFax
Write gate (internal #632, #501): auto-extraction, totalreclaw_remember,
the USER.md mirror and reconcile all call core decide_write (pure-Python
fallback for older cores) against topical subgraph candidates plus a
15-minute in-memory ledger of this process's writes. The ledger covers
subgraph indexing lag; forget and pin/retype rewrites keep it in step.
Explicit remember of an exact copy returns already_stored; a near copy
returns possible_duplicate (retry with force=true). Mirror/reconcile
skip exact copies only.

DEP-3 Part B: extraction sees topical top-20 (include_embeddings=True),
just-written facts and the 10 newest, rendered by core
build_dedup_context with pinned facts first and read-only. UPDATE
without existingFactId becomes ADD. _fetch_recent_memories keeps its
name (hermes/hooks.py imports it).

Refs internal #632, #501, #537.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bd3G1BkVMFSXkcdAQ7nFax
@p-diogo

p-diogo commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator Author

Pedro approved the possible_duplicate / already_stored responses for explicit remember (retry with force=true) on 2026-11-03. Merge is still held for the core audit.

This branch has not been deployed

No deployments
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