Skip to content

fix: make merge_evidence attach evidence instead of replacing content - #303

Merged
Barsoomx merged 2 commits into
masterfrom
fix/merge-evidence-only
Jul 29, 2026
Merged

fix: make merge_evidence attach evidence instead of replacing content#303
Barsoomx merged 2 commits into
masterfrom
fix/merge-evidence-only

Conversation

@Barsoomx

Copy link
Copy Markdown
Owner

What

merge_evidence now merges evidence. It keeps the target memory's title and body and
attaches the candidate's sources to a new version. revise_memory is unchanged.

Why

MergeMemoryCandidate delegated to _execute_candidate_revision — the same function that
implements REVISE — which sets memory.title and memory.body from the candidate. A
merge was a full content replacement wearing the name "merge".

That is wrong for this outcome specifically:

  • the judge reaches it only via relation equivalent, with reason code equivalent_claim.
    By definition the candidate carries no new information, so overwriting can only lose
    specificity — a target saying "audit logs 90 days, debug 30 days" gets replaced by an
    "equivalent" candidate saying "audit logs retained 90 days".
  • ReviseMemoryFromCandidate already exists for the "update the text" case. If MERGE also
    rewrote text, REVISE differed from it only by an evidence-tier gate.

The concrete symptom is now pinned as a test on the deterministic EXACT_IDENTITY path:

target:    'Cache eviction policy'   + body
candidate: 'cache   Eviction Policy' + body.upper()

These normalize to the same claim, so the gate merges them — and the old code replaced the
memory's title with the candidate's mangled casing.

Urgency

Merge fires 69 times all-time today because almost every attempt is denied. The companion
change (#A, deterministic outcome derivation) puts merge_evidence at rung 4 and makes it
reachable for every supported candidate — against a backlog of 5,824 mostly-duplicate
proposed candidates. Shipping #A alone would knowingly amplify a destructive path. Both
must reach the stand together.

How

A single branch in _candidate_revision_content: for MERGE the written text comes from the
current target state (memory.title, prior_version.body); for REVISE nothing changes.

_revalidated_sanitized_view still runs for MERGE and still raises on mismatch — it is
a security fence re-checking that the candidate's sanitized content has not changed since
the deterministic gate. Only what gets written changed.

Idempotency is untouched: _candidate_revision_fingerprint keys on
subject_id=candidate_id plus the candidate fence and the candidate's own title/body, none
of which this change affects. Two different candidates merging into the same memory still
commit two distinct transitions — pinned by a test.

Tests

No pre-existing test had to be modified, which is itself evidence that nothing relied on the
overwrite. Added coverage:

  • target title/body unchanged, new version created, candidate sources attached to it
  • the exact projection / full_text carries the retained text and does not contain the
    candidate's
  • REVISE still writes the candidate's content
  • the sanitized-view fence still rejects a candidate whose content changed
  • two candidates merging into one memory produce distinct transitions
  • the deterministic EXACT_IDENTITY path keeps the target's wording

Barsoomx added 2 commits July 29, 2026 18:49
MERGE currently rewrites the target memory with the candidate text, which
loses specificity for a relation the judge already called equivalent. Pin
the intended contract first: the target keeps its own title and body, the
new version carries the candidate sources, the exact projection reflects
the retained text, REVISE still writes the candidate text, the sanitized
view revalidation still rejects drifted content, and two candidates
merging into one memory still produce two distinct transitions.
MergeMemoryCandidate delegated to the REVISE executor, so a MERGE wrote the
candidate's title and body over the target memory. The judge only reaches
merge_evidence through relation equivalent, so by construction the candidate
carries no new information and overwriting can only lose specificity; the
outcome exists to attach the candidate's sources to a new version. It also
made ReviseMemoryFromCandidate redundant, since the two transitions then
differed only in their evidence-tier gate.

MERGE now writes the target's current title and body (memory.title plus
prior_version.body) into the new version. REVISE is unchanged.

Kept as one executor with the varying decision extracted into
_candidate_revision_content rather than forking a second function: the two
transitions share the whole work-claim, fence, lock, projection and commit
pipeline and differ in exactly one value, so duplicating it would let the
security fences drift apart. _revalidated_sanitized_view still runs for MERGE
and still raises on drift - only what is written changed.

Idempotency is unaffected: _candidate_revision_fingerprint keys on the
candidate id as subject plus the candidate fence, so two candidates merging
into the same memory stay two transitions even though neither now decides the
written text. The new version's _content_hash and the exact projection are
computed from the retained text, so retrieval reflects what the memory kept.

On the deterministic EXACT_IDENTITY path the candidate and target already
match after NFKC/casefold/whitespace normalization, so the only visible change
is that the target keeps its own casing and spacing.
@Barsoomx

Copy link
Copy Markdown
Owner Author

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@codecov-commenter

codecov-commenter commented Jul 29, 2026

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

Affected Assets, Files, and Routes:

view changes for bundle: engram-frontend-client-array-push

Assets Changed:

Asset Name Size Change Total Size Change (%)
server/app/(admin)/model-*.js 105 bytes 19.05kB 0.55%
server/app/(admin)/model-*.js -105 bytes 18.94kB -0.55%
static/w3t1W-*.js (New) 77 bytes 77 bytes 100.0% 🚀
static/w3t1W-*.js (New) 1.51kB 1.51kB 100.0% 🚀
static/9GnfQ6TJAyKnM4QAaEwlf/_buildManifest.js (Deleted) -1.51kB 0 bytes -100.0% 🗑️
static/9GnfQ6TJAyKnM4QAaEwlf/_ssgManifest.js (Deleted) -77 bytes 0 bytes -100.0% 🗑️

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.05%. Comparing base (775a6d5) to head (129999f).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #303      +/-   ##
==========================================
+ Coverage   91.03%   91.05%   +0.02%     
==========================================
  Files         222      222              
  Lines       23196    23201       +5     
==========================================
+ Hits        21116    21126      +10     
+ Misses       2080     2075       -5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@Barsoomx
Barsoomx merged commit ee7c594 into master Jul 29, 2026
14 checks passed
@Barsoomx
Barsoomx deleted the fix/merge-evidence-only branch July 29, 2026 16:57
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.

2 participants