Skip to content

feat(core,python): pin-aware resolution + recall pin boost (PRD-04 DEP-5, Part A) - #702

Open
p-diogo wants to merge 13 commits into
mainfrom
feat/dep-5-pin-aware-resolution
Open

p-diogo wants to merge 13 commits into
mainfrom
feat/dep-5-pin-aware-resolution

Conversation

@p-diogo

@p-diogo p-diogo commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator

Re-created from #671 on 2026-10-31. GitHub Support is removing the pull-request refs of every PR opened before the 2026-10-25 history rewrite (private-data purge), which would leave #671 without a diff. Branch and content are unchanged; the earlier discussion stays on #671.

Summary

PRD-04 F1 / DEP-5 (track kg, L2). Part A of 2 (Part B = MCP, after a core RC).

  • Core: RerankerConfig.pin_boost: Option<f64> (JSON pin_boost), Candidate.pinned (JSON pinned), DEFAULT_PIN_BOOST = 1.5; PyO3 rerank_with_config(..., pin_boost=None) + default_pin_boost(); WASM rerankWithConfig(..., pin_boost?) + defaultPinBoost(). Default is a no-op: every existing ranking is bit-for-bit identical (tests assert to_bits equality); no parity fixture changed.
  • Python: agent/pin_guard.py drops LLM UPDATE/DELETE whose target is pinned; unknown pin state fails closed (DELETE dropped, UPDATE → ADD). The contradiction resolver now receives entity refs + st:"p". It was dormant before: no entities were passed, so core never compared claims. It acts on existing_pinned only. Recall passes core's pin boost (feature-detected; no-op on core 2.6.x).
  • Docs: SKILL.md pin claim corrected; memory-taxonomy-v1.md §Pin semantics items 6–8; retrieval-v2.md §Pin boost; CLAUDE.md matrix + 3 Known Gaps; CHANGELOGs.

Decisions (see plan)

  • Pinned target → whole action dropped, matching core SkipNew{ExistingPinned} and the MCP/plugin contradiction path. Logs carry the category only; no decision-log row.
  • 1.5× = signed-off PinConfig::default().hard_boost. No boost without a query embedding: lexical-only ties would lift every pin to the top. Hermes auto-recall gets the boost with DEP-8.
  • Resolver outcomes other than existing_pinned stay log-only until DEP-12.
  • Candidate.pinned is a new public field: Rust struct-literal callers must add it. In-tree callers are fixed. Coordinator: confirm minor vs major for the core release.

Sequencing

Touches _auto_extract_inner in python/src/totalreclaw/agent/lifecycle.py: one import line plus one block after the contradiction try/except. DEP-3 edits the top of the same function. No fixed merge order with DEP-3: whichever merges second rebases.

Open items (New Feature Checklist)

  • Staging E2E python/tests/test_staging_pin_contract.py, run by the coordinator with a throwaway staging vault in the environment; it asserts the staging DataEdge.
  • Hermes real-user QA "pin, then contradict it three times in conversation" on the Python RC (PRD-04 G2).
  • Part B: MCP managed recall pin boost after the core RC.
  • Out of scope, flagged: python/src/totalreclaw/crystals/recrystallize.py rewrites and tombstones facts without preserving pin_status.

Test evidence

cargo test (core), cargo test --features python --lib python::tests, cargo check --tests (totalreclaw-memory), the CI Python suite, scripts/check-pyfunction-parity.py, scripts/check-docs.sh, scripts/check-phrase-safety.sh, and the WASM smoke (a,b,c / p,a,b / 1.5), all green locally.

🤖 Generated with Claude Code

p-diogo and others added 10 commits September 30, 2026 18:43
Co-Authored-By: Claude Code <noreply@anthropic.com>
… (PRD-04 DEP-5)

Co-Authored-By: Claude Code <noreply@anthropic.com>
…re pin boost (PRD-04 DEP-5)

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…04 DEP-5)

Co-Authored-By: Claude Code <noreply@anthropic.com>
…er (PRD-04 DEP-5)

Co-Authored-By: Claude Code <noreply@anthropic.com>
…RD-04 G2)

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
…s (DEP-5)

relay.query_subgraph returns the parsed body for any HTTP 2xx, so a
GraphQL error body ({"errors": [...]}) carries no "data" key; the lenient
.get chain collapsed it to None, get_fact_pin_status returned False, and
the pin guard let an LLM UPDATE/DELETE tombstone a pinned fact during a
subgraph hiccup.

_fetch_fact_by_id gains a keyword-only strict mode, used only by
get_fact_pin_status: a non-empty "errors" key, or a body whose "data" is
not a dict carrying "fact", raises RuntimeError (pin state unknown ->
guard fails closed). False is returned only for an explicit
data.fact is None. Other callers keep the lenient shape unchanged.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
p-diogo and others added 3 commits November 3, 2026 00:11
Conflicts: SKILL.md (kept main's Hermes-memory forget hint and the
DEP-5 pin wording), operations.py (pin-boost constant + ids-first query
comment), core CHANGELOG (Unreleased above 2.6.1). Moved the DEP-5
Python CHANGELOG entry out of the released 2.5.3 section into
[Unreleased].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bd3G1BkVMFSXkcdAQ7nFax
RRF scores sit on a 1/(60 + rank) floor, so 1.5x on the fused score
lifted any candidate ranked <= 31 on both signals above rank 1. In a
pool of 31 or fewer candidates every pinned fact ranked first, even
one with no lexical and no semantic match (new core test reproduces:
a 15-candidate pool put an unrelated pin at #1).

The boost now multiplies a pinned candidate's BM25 and cosine scores
before ranking. A pin moves up only past candidates whose relevance is
within 1.5x of its own, which is the signed-off "about 50% above
unpinned". Reported bm25/cosine scores and intent weights use the raw
values. No-boost path is still bit-identical; no parity fixture
changes. Spec, changelogs and binding docs updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bd3G1BkVMFSXkcdAQ7nFax
…th (DEP-5)

Runs auto_extract three times against a fake subgraph holding
AES-GCM-encrypted blobs. Pin state is read the production way:
search_facts (ids-first search, decrypt, is_pinned_claim) for the
contradiction resolver, get_fact_pin_status (strict fact-by-id) for the
pin guard. Each round asserts which mechanism stopped it. Controls show
the same rounds replace an unpinned fact; a GraphQL error body on the
pin read never tombstones. Mutation-checked: disabling the guard or the
search pin flag turns it red.

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

Review + refresh (2026-11-03)

Verdict: code-ready, merge held for the core audit. Main is merged in, one real bug in the boost is fixed, and a deeper end-to-end test is added.

1. Merged origin/main (37 commits: Python 2.5.3 on core 2.6.1, ids-first search #703, Harrier <eos> #708). Conflicts were docs-only: SKILL.md, the operations.py header comment, core CHANGELOG. The DEP-5 Python CHANGELOG entry had auto-merged into the released 2.5.3 section; it is now under [Unreleased].

2. Bug: the 1.5× pin boost put unrelated pins first in small candidate pools (e2d56ed). RRF scores sit on a 1/(60 + rank) floor. A 1.5× multiplier on the fused score therefore lifts any candidate ranked 31 or better on both signals above rank 1. In a pool of 31 or fewer candidates, every pinned fact ranked first, even with zero lexical and zero semantic match. A new core test reproduces it: in a 15-candidate pool, "grocery list" ranked #1 for "dark mode". The spec's own magnitude note said the same thing; the existing irrelevant-pin test passed only because it used 60 matches.
Fix: multiply the pinned candidate's BM25 and cosine scores before ranking. A pin now moves up only past candidates whose relevance is within 1.5× of its own. That matches the signed-off "about 50% above unpinned". The existing tests still pass: the no-boost path is bit-identical, a pin just outside the top k still enters it, and the WASM smoke still gives a,b,c / p,a,b / 1.5. No parity fixture changes. Spec §Pin boost and both CHANGELOGs are updated.

3. New test test_pin_survives_contradictions_e2e.py (1202409). auto_extract runs three times against a fake subgraph holding encrypted blobs. Pin state comes from the real search_facts (decrypt + is_pinned_claim) and the real get_fact_pin_status. The earlier integration test stubbed both. Each round asserts which mechanism stopped it: the guard for UPDATE, the core resolver for an entity-sharing ADD, the guard for DELETE. Controls cover the unpinned case and a GraphQL error body. I checked the test by breaking the code on purpose: disabling the guard, or dropping the search pin flag, makes it fail.

Pin-safety by path: auto-extract UPDATE/DELETE: guarded. Contradiction resolver: only existing_pinned is acted on (drop the new fact), so it never tombstones. Retype / set_scope / pin: these are user-invoked, and pin_status is preserved. Debrief/crystal: write-only. Explicit forget: user intent, allowed. recrystallize: user-invoked and confirm-gated, but it tombstones pinned session facts and doesn't carry pin_status (confirmed; noted on internal #558).

Tests: core cargo test 721+4 pass. PyO3 python::tests 54 pass. WASM build plus smoke pass. totalreclaw-memory cargo check --tests passes. Python suite (CI flags): 2813 passed, 7 skipped. Outer-protobuf parity, pyfunction parity, check-docs and phrase-safety are all clean.

Overlap with #701 (DEP-3): it only conflicts textually, in the core CHANGELOG [Unreleased] block. One semantic note for DEP-3 Part B: it lists pinned facts first in the extraction prompt. That will make the LLM aim more UPDATEs at pins, and the guard drops each one along with the new information. Part B should mark pins as read-only in the prompt. It can read RerankerResult.pinned from this PR.

Still open: the core release (Candidate.pinned is a new public struct field, so the coordinator decides minor vs major), the staging E2E, Hermes real-user QA (G2), and Part B (MCP).

refs totalreclaw-internal#538

🤖 Generated with Claude Code

@p-diogo

p-diogo commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator Author

Pedro approved the pin boost fix (e2d56ed: boost BM25 and cosine before fusion instead of multiplying the fused RRF score) 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