Skip to content

feat(memory): graph-seed candidate arm and per-arm caps before fusion - release 0.26.0 (issue #136) - #150

Merged
zaxbysauce merged 2 commits into
mainfrom
feat/136-graph-seed-arm
Sep 9, 2026
Merged

feat(memory): graph-seed candidate arm and per-arm caps before fusion - release 0.26.0 (issue #136)#150
zaxbysauce merged 2 commits into
mainfrom
feat/136-graph-seed-arm

Conversation

@zaxbysauce

@zaxbysauce zaxbysauce commented Sep 9, 2026

Copy link
Copy Markdown
Owner

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:

  1. Graph-seed arm (issue [Workstream C] PR 6 of 6: Graph-seed candidate arm and per-arm caps before fusion (adopted from Hindsight TEMPR) #136): the query's entities (and ops tokens, via the same alias path) resolve to seed memories; ONE bounded hop over related/supports/updates/derives edges (never contradicts as 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_ids filter), one batched indexed query (placeholders exactly 4 + 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.
  2. Per-arm caps: named defaults equal the previous windows (default behavior unchanged); ZMEM_ARM_CAP_FTS/VEC/ENTITY/GRAPH env overrides; ZMEM_GRAPH_SEED=0 disables the graph arm. --explain and the --for-injection envelope report per-arm pre/post/cap counts (arms), and the hook decision line gains arms=fts:P/Q,vec:P/Q,ent:P/Q,graph:P/Q (with the matching _BG_LINE_RE parser group so miss-rate lines never silently drop).
  3. Measured graph lane + floor: graph-arm rows carry _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

  • Byte-identical for link-free stores with the arm on or off (frozen check C5, GREEN-to-GREEN).
  • Injection-gold baseline unchanged — all 5 metrics equal eval/baseline-injection.json (no false-injection regression).
  • Post-result [CONTESTED LINK] expansion untouched (frozen check C4).
  • Vector-KNN namespace-slot starvation (issue scope item 4) already handled by _vec_knn_in_namespace's overfetch escalation + in-SQL namespace filter — verified and dispositioned in writing.

Evidence

  • Frozen red checkpoint (sha256-bound manifest): C1–C4/C6 FAIL at base → PASS at HEAD; C5 GREEN both sides; full logs in .agents/issue-traces/136-graph-seed-arm-per-arm-caps/repro/.
  • New suite 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).
  • Independent implementation review: APPROVE (2 rounds; round-2 delta review at the shipped SHA). Final critic: APPROVE (all 8 ACs independently re-verified).

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

  • The graph arm resolves the query's entities (and ops tokens) to seed memories, walks one bounded hop over related/supports/updates/derives edges (never contradicts), and emits a capped, deterministic ranked list as a fourth RRF input.
  • Caps are overridable via ZMEM_ARM_CAP_FTS/VEC/ENTITY/GRAPH; ZMEM_GRAPH_SEED=0 disables the graph arm.
  • Graph-arm rows carry a measured _rel_graph lane judged by ZMEM_INJECT_FLOOR_GRAPH (default 0.75 = LINK_THRESHOLD), so a curated sub-threshold neighbor is dropped instead of riding the absent-lane exemption.
  • --explain and --for-injection envelopes include per-arm pre/post/cap counts (arms), and the hook decision line gains arms=fts:P/Q,... with matching parser support.
  • Version bumped to 0.26.0 across all manifests, the CHANGELOG, and the release manifest.

Written for commit 2195fdb. Summary will update on new commits.

Review in cubic

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:

  • Full 93-file test loop: green.
  • Frozen issue-[Workstream C] PR 6 of 6: Graph-seed candidate arm and per-arm caps before fusion (adopted from Hindsight TEMPR) #136 acceptance checks C1-C6: byte-identical to their frozen head logs.
  • 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).
  • Feedback-phase gates: fresh swarm-reviewer on the fix diff -> APPROVE (13/13 FIXED-VERIFIED, 2 LOW nits fixed); separate final critic -> CLOSURE-APPROVED (six targeted hunts clean; characterization re-capture independently reproduced via dual-tree git-archive build).

… 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).

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread skills/memory/scripts/storelib/recall.py Outdated
"moment": moment,
# Issue #136: the additive arms= attribution field, verbatim
# (the B-1 report surfaces which arm carried a hit).
"arms": arms,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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]}

Comment thread skills/memory/scripts/storelib/inject.py Outdated
Comment thread skills/memory/scripts/storelib/recall.py Outdated
Comment thread skills/memory/scripts/storelib/recall.py Outdated
Comment thread hooks/lib/zmem-recall-body.py Outdated
Comment thread tests/test_explain_per_lane.py
Comment thread skills/memory/SKILL.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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_ids arm + _arm_cap/4th _rrf_fuse list, with per-arm pre/post/cap accounting surfaced in --explain, the --for-injection envelope, and the hook decision line (arms=...).
  • New disjunctive graph inject floor (_rel_graph lane), single-sourced through schema_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.

Comment on lines +415 to +421
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
)
@zaxbysauce

Copy link
Copy Markdown
Owner Author

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:

  1. recall.py _rrf_fuse positional break (P2)k is restored to its original fourth positional slot; graph_ids is keyword-only after *, so no existing caller can silently rebind the smoothing constant. The unit test now passes graph_ids=["d"] explicitly.
  2. recall.py uncapped _rel_graph stamping (P2) — the graph lane is now stamped only for rows in the capped graph_ids set; the uncapped relevance map feeds only the envelope's arms.graph.pre count, which is its documented purpose.
  3. recall.py graph-only candidates never composite-scored (P2) — the measured graph edge score now joins the lane-max relevance computation, so final ranking reflects the graph arm; gate and ranking agree (the injection-gold baseline is unchanged: no links in that fixture).
  4. test_explain_per_lane env pin (P3)ZMEM_INJECT_FLOOR_GRAPH added to the popped tuple.
  5. miss_rate arms aggregation (P2)run_miss_report now aggregates the decision log's arms= field into report["arms"] (per-arm carried injected/silent counts, lines with/without the field) and doctor --miss-rate renders it; two new report tests.
  6. inject.py legacy 3-tuple lane_floors IndexError (P2) — both gate implementations (inject.py and the eval_gold mirror) now zip lane names with the floors tuple, so a legacy 3-value override keeps working and the graph floor applies only when a fourth value is supplied; pinned by a new compat test.
  7. hooks serializer ent/entity mismatch (P2) — the wire label ent now reads the envelope key entity; a functional importlib test renders the real hook body's decision line and asserts ent:P/Q appears.
  8. SKILL.md colon-clause collision (P3) — the entity-matching description is reattached to the entity lane and the graph-seed arm describes its own seeding (one bounded hop over seed-safe links).

By design (with the compensating control from #2):

  • links.py rel_map covering capped-out neighbors (P2) — intentional: the map must cover the pre-cap neighbor set because the envelope's arms.graph.pre accounting is defined as the pre-cap eligible pool. It cannot affect gate or explain attribution: every consumer that stamps a measured lane (_rel_graph, lane-max, candidate_lanes) is gated on membership in the capped graph_ids set, so a capped-out neighbor never carries a measured graph lane anywhere a decision is made.

One CI failure this review surfaced indirectly (the test_link_neighbor_rendered_but_never_counted pin) is also fixed: graph-only rows now render but never feed the surfaced/retrieval counters, extending the issue #114 telemetry law, and the pin accepts either link-expansion or graph-arm arrival while requiring no lexical match.

@zaxbysauce
zaxbysauce merged commit cfd2ead into main Sep 9, 2026
5 checks passed
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.

[Workstream C] PR 6 of 6: Graph-seed candidate arm and per-arm caps before fusion (adopted from Hindsight TEMPR)

2 participants