feat(memory): graph-seed candidate arm and per-arm caps before fusion - release 0.26.0 (issue #136) - #150
Conversation
… release 0.26.0 (issue #136) Recall now runs FOUR candidate arms: FTS, vector, entity, and graph-seed. The graph arm resolves the query's entities/ops tokens to seed memories and walks ONE bounded hop over related/supports/updates/derives edges (never contradicts), emitting a capped ranked list as the fourth RRF input - so a prompt sharing only an entity with a hit can rescue the linked lesson it shares no content token with. Per-arm caps (named defaults = previous windows, ZMEM_ARM_CAP_* overrides) truncate each arm BEFORE fusion; --explain and the --for-injection envelope report per-arm pre/post/cap counts (arms) and the hook decision line gains arms=fts:P/Q,... so the miss-rate report can attribute a hit to its arm. Graph-arm rows carry a MEASURED _rel_graph lane (best entry-edge score) judged by a fourth inject floor (ZMEM_INJECT_FLOOR_GRAPH, default 0.75 = LINK_THRESHOLD): a graph rescue injects only when relevant; a curated sub-threshold neighbor is judged and dropped (curing the pre-#136 floor-exempt false injection via link expansion). No-links stores stay byte-identical (arm on == off); the injection-gold baseline is unchanged (all 5 metrics equal). Version bump 0.25.0 -> 0.26.0 across the 7 manifests + CHANGELOG; release-manifest.json re-emitted (72 files, digest 8b08370f).
There was a problem hiding this comment.
2 issues found across 23 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="skills/memory/scripts/storelib/miss_rate.py">
<violation number="1" location="skills/memory/scripts/storelib/miss_rate.py:215">
P2: When `doctor --miss-rate` processes new lines, `parse_bg_log` stores `arms` but `run_miss_report` never aggregates or returns it, so the report cannot attribute hits to an arm. Add an arm-attribution aggregate to the returned report and render it.</violation>
</file>
<file name="skills/memory/scripts/storelib/links.py">
<violation number="1" location="skills/memory/scripts/storelib/links.py:445">
P2: When a capped-out graph neighbor also matches another arm, `graph_rel_map` still marks it as a measured graph candidate. This lets the graph arm affect its injection decision despite the pre-fusion graph cap; restrict `rel_map` to the same capped rows as `capped_ids`.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| "moment": moment, | ||
| # Issue #136: the additive arms= attribution field, verbatim | ||
| # (the B-1 report surfaces which arm carried a hit). | ||
| "arms": arms, |
There was a problem hiding this comment.
P2: When doctor --miss-rate processes new lines, parse_bg_log stores arms but run_miss_report never aggregates or returns it, so the report cannot attribute hits to an arm. Add an arm-attribution aggregate to the returned report and render it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At skills/memory/scripts/storelib/miss_rate.py, line 215:
<comment>When `doctor --miss-rate` processes new lines, `parse_bg_log` stores `arms` but `run_miss_report` never aggregates or returns it, so the report cannot attribute hits to an arm. Add an arm-attribution aggregate to the returned report and render it.</comment>
<file context>
@@ -203,6 +210,9 @@ def parse_bg_log(path) -> list:
"moment": moment,
+ # Issue #136: the additive arms= attribution field, verbatim
+ # (the B-1 report surfaces which arm carried a hit).
+ "arms": arms,
})
return out
</file context>
| return [], {} | ||
|
|
||
| ranked = sorted(fetched, key=lambda r: (-best[r["id"]][0], r["id"])) | ||
| rel_map = {r["id"]: max(0.0, min(1.0, best[r["id"]][0])) for r in ranked} |
There was a problem hiding this comment.
P2: When a capped-out graph neighbor also matches another arm, graph_rel_map still marks it as a measured graph candidate. This lets the graph arm affect its injection decision despite the pre-fusion graph cap; restrict rel_map to the same capped rows as capped_ids.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At skills/memory/scripts/storelib/links.py, line 445:
<comment>When a capped-out graph neighbor also matches another arm, `graph_rel_map` still marks it as a measured graph candidate. This lets the graph arm affect its injection decision despite the pre-fusion graph cap; restrict `rel_map` to the same capped rows as `capped_ids`.</comment>
<file context>
@@ -361,6 +371,81 @@ def generate_links_on_write(
+ return [], {}
+
+ ranked = sorted(fetched, key=lambda r: (-best[r["id"]][0], r["id"]))
+ rel_map = {r["id"]: max(0.0, min(1.0, best[r["id"]][0])) for r in ranked}
+ return [r["id"] for r in ranked[:cap]], rel_map
+
</file context>
| rel_map = {r["id"]: max(0.0, min(1.0, best[r["id"]][0])) for r in ranked} | |
| rel_map = {r["id"]: max(0.0, min(1.0, best[r["id"]][0])) for r in ranked[:cap]} |
There was a problem hiding this comment.
🟡 Changes recommended
The hook decision line iterates the label "ent" against the envelope's arms dict keyed "entity", so the entity arm is silently dropped from the arms= attribution field.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR implements issue #136 (Workstream C, PR 6/6), adding a fourth graph-seed candidate arm to zmem's recall pipeline and introducing per-arm caps applied before RRF fusion. Previously, links could only explain a hit (via post-result expansion); now the query's entity-resolved seed memories walk one bounded hop over related/supports/updates/derives edges (never contradicts) to rescue linked memories that share no content token with the prompt. Graph-arm rows carry a measured _rel_graph lane judged by a fourth disjunctive inject floor (ZMEM_INJECT_FLOOR_GRAPH, default 0.75 = LINK_THRESHOLD), so a graph rescue injects only when relevant. The change is guarded to be byte-identical for link-free stores whether the arm is on or off.
Changes:
- New
graph_seed_idsarm +_arm_cap/4th_rrf_fuselist, with per-arm pre/post/cap accounting surfaced in--explain, the--for-injectionenvelope, and the hook decision line (arms=...). - New disjunctive graph inject floor (
_rel_graphlane), single-sourced throughschema_meta.py. - Version bump 0.25.0 → 0.26.0 across all manifests + CHANGELOG + release manifest, plus a new 21-test suite and additive-tolerance updates to existing pins.
File summaries
| File | Description |
|---|---|
| skills/memory/scripts/storelib/recall.py | Adds _arm_cap, 4th RRF list, per-arm caps + arm_stats, _rel_graph stamping, and envelope arms. |
| skills/memory/scripts/storelib/links.py | New graph_seed_ids one-hop bounded graph-seed arm with deterministic ranking. |
| skills/memory/scripts/storelib/inject.py | Extends lane floors to a 4-tuple and joins _rel_graph to the relevance disjunction. |
| skills/memory/scripts/storelib/eval_gold.py | Threads the graph lane through the eval gate twin. |
| skills/memory/scripts/schema_meta.py | Single-sources INJECT_FLOOR_GRAPH_DEFAULT/_ENV. |
| skills/memory/scripts/storelib/miss_rate.py | Parses the additive arms= decision-line field. |
| hooks/lib/zmem-recall-body.py | Renders arms= on the decision line (entity arm key mismatch — see comment). |
| skills/memory/SKILL.md / docs/CLAIMS-AUDIT.md / CHANGELOG.md | Documents the four-signal pipeline and per-arm caps. |
| tests/test_graph_seed_arm.py (+4 pin updates) | New acceptance suite plus additive trailing-field tolerance in existing tests. |
| release-manifest.json, marketplace.json, .claude/.codex/.zcode/.agents plugin manifests, hermes-plugin/plugin.yaml | Version bump 0.25.0 → 0.26.0 and regenerated digests. |
Review details
- Files reviewed: 23/23 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| armf = " arms=" + ",".join( | ||
| "{0}:{1}/{2}".format( | ||
| name, int(arms[name].get("post", 0)), | ||
| int(arms[name].get("cap", 0))) | ||
| for name in ("fts", "vec", "ent", "graph") | ||
| if name in arms | ||
| ) |
…bel, lane composition (issue #136)
|
All 8 findings from this review are dispositioned in commit 29cc9bc (7 fixed, 1 by-design with a compensating control). The fix round also resolved the 13 findings from our own 12-lane review; full validation: 93-file test loop green, the six frozen issue-#136 acceptance checks reproduce byte-identically, and the committed injection-gold baseline matches on all five metrics. Fixed:
By design (with the compensating control from #2):
One CI failure this review surfaced indirectly (the |
Closes #136.
What
Recall now runs four candidate arms — FTS, vector, entity, and a new graph-seed arm — each capped BEFORE fusion, with per-arm accounting end to end:
related/supports/updates/derivesedges (nevercontradictsas a seed) emits a capped, deterministically ranked neighbor list as the fourth RRF input. A prompt that shares only an entity with a hit can now rescue the linked lesson it shares no content token with — links create candidates, not only explain them. Namespace-contained (shared_fetch_by_idsfilter), one batched indexed query (placeholders exactly4 + 2·len(seeds), the PR feat(memory): score composition — keep the fused rank, keep the cosine, add a normalized relevance floor (issue #113) #147 bounding rule), deterministic tie-breaks, no LLM.ZMEM_ARM_CAP_FTS/VEC/ENTITY/GRAPHenv overrides;ZMEM_GRAPH_SEED=0disables the graph arm.--explainand the--for-injectionenvelope report per-armpre/post/capcounts (arms), and the hook decision line gainsarms=fts:P/Q,vec:P/Q,ent:P/Q,graph:P/Q(with the matching_BG_LINE_REparser group so miss-rate lines never silently drop)._rel_graph(best entry-edge score), judged by a fourth disjunctive inject floor —ZMEM_INJECT_FLOOR_GRAPH, default 0.75 = LINK_THRESHOLD — so a graph rescue injects only when relevant, and a curated sub-threshold neighbor is judged and dropped (curing the pre-[Workstream C] PR 6 of 6: Graph-seed candidate arm and per-arm caps before fusion (adopted from Hindsight TEMPR) #136 floor-exempt false injection via post-result expansion). Unrelated rows keep the key absent and stay exempt exactly as before.Guarantees
eval/baseline-injection.json(no false-injection regression).[CONTESTED LINK]expansion untouched (frozen check C4)._vec_knn_in_namespace's overfetch escalation + in-SQL namespace filter — verified and dispositioned in writing.Evidence
.agents/issue-traces/136-graph-seed-arm-per-arm-caps/repro/.tests/test_graph_seed_arm.py(21 tests) + updated pins (test_explain_per_lane,test_explain_recall,test_bg_log_sid,test_decision_log— additive trailing-field tolerance only).Version
0.25.0 → 0.26.0 (all seven plugin manifests, CHANGELOG, release manifest re-emitted).
Summary by cubic
Recall now runs a fourth graph-seed candidate arm so a prompt sharing only an entity with a hit can rescue the linked memory it shares no content tokens with, and caps each arm before fusion with defaults equal to the previous windows. Link-free stores stay byte-identical and the injection-gold baseline is unchanged.
New Features
related/supports/updates/derivesedges (nevercontradicts), and emits a capped, deterministic ranked list as a fourth RRF input.ZMEM_ARM_CAP_FTS/VEC/ENTITY/GRAPH;ZMEM_GRAPH_SEED=0disables the graph arm._rel_graphlane judged byZMEM_INJECT_FLOOR_GRAPH(default 0.75 =LINK_THRESHOLD), so a curated sub-threshold neighbor is dropped instead of riding the absent-lane exemption.--explainand--for-injectionenvelopes include per-arm pre/post/cap counts (arms), and the hook decision line gainsarms=fts:P/Q,...with matching parser support.Written for commit 2195fdb. Summary will update on new commits.
Review-round follow-up (29cc9bc)
The 12-lane review + cubic round produced 13 + 8 findings; all are dispositioned in 29cc9bc (reply to the cubic comment has the per-finding table; one links.py item is by-design with a compensating cap-governance control).
Re-validation at 29cc9bc:
scripts/eval_inject_runner.py --compare-baseline eval/baseline-injection.json: all 5 metrics equal (exit 0).python scripts/release_gate.py --verify-manifest: fresh (72 files, digest f5fc5e0f, version 0.26.0).