Skip to content

fix(graph): scope edge reads to one layer, and make the migration inventory precise - #177

Merged
Cipher208 merged 2 commits into
masterfrom
fix/graph-edge-layer-scope
Nov 1, 2026
Merged

Cipher208 merged 2 commits into
masterfrom
fix/graph-edge-layer-scope

Conversation

@Cipher208

Copy link
Copy Markdown
Owner

Two packages, both found by measuring the real bases

1. memory_graph_edges leaked the other layer's content

It filtered the requested layer on the source node only, while returning both endpoints' content:

WHERE {where} AND s.layer = ?          -- t.layer never checked
... t.content as tgt_content

Reproduced on 7fae1f6: layer="user" returned an agent node's content through a user → agent edge, and the agent side leaked user text symmetrically. The user_id half of this asymmetry was closed by F54 (both endpoints); the layer half was missed.

Both branches now require s.layer and t.layer. Same-layer edges are untouched; only cross-layer ones are hidden.

Latent today — the live bases have zero cross-layer edges — and live the moment graph nodes are re-layered: the pending historical migration would create ~36 000 of them in hermes alone. This is why the fix has to land before any node is moved.

Verifier confirmed the reproduction on base and killed all three guards by mutation (target predicate, source predicate, F54 user predicate). Over-filtering probed: same-layer edges survive for out/in/both, both layers symmetric, node_id=0 unscoped branch, limit clamping intact.

Also corrected count_edges' docstring, which claimed its source-only convention was the one memory_graph_edges used. After this change it is not, and on a cross-layer edge the counter will exceed what the tool lists — stated in the docstring rather than silently diverging.

2. The inventory flagged things it should not

Measured on the three live bases with a marker list of the address forms agents use for the owner:

base candidates before after removed
hermes 649 642 7 wiki_page
mimocode 35 20 15 wiki_page
cowagent 27 26 1 wiki_page

Two precision defects:

  • The identity/profile node was a candidate in every base. Its content enumerates the address forms, so it matched several markers at once. It is legitimate user-layer knowledge — moving it would delete the owner's own profile from user memory. Exclusions now mark it decision="exclude" and keep it in the manifest so the decision stays reviewable. The policy block carries only version/source/count/sha256, never the patterns.
  • wiki_page is a pointer, not prose. Its content is a filesystem path, so it produced candidates with no text evidence. Removed from the structural set.

summary gains reviewable = candidates - excluded, because the confidence buckets count every emitted candidate and a consumer should not have to know to subtract excluded itself.

Result: 642 / 20 / 26 candidates, exactly one excluded per base — the same #5 profile node in each.

Verification

Two blind verifier passes, both FIX_CONFIRMED, plus corrections for what they found:

  • the first pass caught that summary.excluded overlapped the confidence buckets — fixed with reviewable;
  • it also caught that one exclusion test used a pattern matching no node, so "the word is not in the manifest" was vacuously true. Rewritten to use a matching pattern and to assert the honest contract: the policy is metadata-only, while a matched candidate's reasons legitimately echo excluded:<pattern>.

Regression probes: read-only snapshot + WAL (committed-but-uncheckpointed rows visible, source bytes unchanged), fingerprint, output guards for memory.db/-wal/-shm/-journal/hardlink/symlink, atomic replace, exact PK validation, duplicate and target-collision fields, user-scoped edge evidence, apply still rejected, byte-identical output on two runs.

Still not done

apply is not implemented. It is a separate package with snapshot, fingerprint gate, postconditions and an explicit write approval. No live base has been modified.

Known divergence left open on purpose: after a re-layering, count_edges will count cross-layer edges that memory_graph_edges will not list. Documented, not fixed — it needs a decision about what the statistic should mean.

Gates

pytest -q            2737 passed
mypy . --strict      63 errors          (identical to 7fae1f6)
ruff check / format  clean / 750 files

memory_graph_edges filtered the requested layer on the source node only while
returning BOTH endpoints' content, so an edge from the requested layer to
another layer answered with the foreign layer's text. Reproduced on 7fae1f6:
layer=user returned an agent node's content through a user->agent edge, and the
agent side leaked user text symmetrically.

The user_id half of this asymmetry was closed by F54, which added the predicate
to both endpoints; the layer half was missed. Both branches now require s.layer
and t.layer, which keeps every same-layer edge and hides only the cross-layer
ones.

Latent today — the live bases have no cross-layer edges — and live the moment
graph nodes are re-layered: the pending historical migration would create tens
of thousands of them in hermes alone.

Also corrects count_edges' docstring, which claimed its source-only convention
was the one memory_graph_edges used. After this change it is not, and on a
cross-layer edge the counter will exceed what the tool lists.
Two precision defects measured on the three live bases.

An identity/profile node that enumerates the address forms matched several
markers at once and became a migration candidate in every base. It is
legitimate user-layer knowledge — moving it would delete the owner's own
profile from user memory. Exclusions now mark such a node decision="exclude"
and keep it in the manifest so the decision stays reviewable; the policy block
carries only version/source/count/sha256, never the patterns.

wiki_page is a structural signal whose content is a filesystem path rather than
prose, so it produced candidates with no text evidence (7/15/1 across the live
bases). The type is out of the structural set; such nodes carry no agent text.

summary gains reviewable = candidates - excluded, because the confidence
buckets count every emitted candidate and a consumer should not have to know to
subtract excluded itself.
@github-actions github-actions Bot added the fix label Nov 1, 2026
@coderabbitai

coderabbitai Bot commented Nov 1, 2026

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@Cipher208
Cipher208 merged commit 1d4ce83 into master Nov 1, 2026
19 checks passed
@Cipher208
Cipher208 deleted the fix/graph-edge-layer-scope branch November 1, 2026 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant