fix(memory): explicit stores must never silently drop on anchor-guard rejection - #1587
Merged
Aaronontheweb merged 1 commit intoJul 6, 2026
Conversation
… instead of silently dropping (opsx: memory-core-redesign)
Eval run ad9a2312 (daemon log lines 856/903/943/1036) surfaced a silent-fallback
bug in MemoryCurationEvaluator.EvaluateAsync: when an anchor exact-match's
deterministic Update decision is downgraded by GuardDestructiveUpdate (the
proposal would not preserve the target's content), the resulting Skip was
returned as the FINAL decision - terminating evaluation before the embedding
kNN nominator or lexical content-term search ever ran. An LLM-emitted junk
anchor (the bare stopword "the") collided with an unrelated existing document
sharing that same junk anchor text; the guard correctly refused to clobber the
unrelated doc, but nothing then created the requested fact - an explicit
store_memory proposal ended as a no-op (operations=0) with only a debug-level
skip marker, in 4/5 repro runs.
Fix: when GuardDestructiveUpdate downgrades an anchor-matched Update to Skip,
demote every exact-anchor-matched candidate to an ordinary fuzzy candidate and
re-run the evaluation chain as if there had been no exact anchor match at all
- embedding nomination (when available), then lexical content search, then
the rules/LLM/auto-resolve tiers, with Create as the terminal default. The
demotion is what prevents re-looping into the same Update/guard-reject pair.
The guard's protective effect is unchanged: the mismatched target is never
overwritten. Emits a new curation_guard_fallthrough anchor={name}
rejectedTarget={id} marker so the path is observable; existing skip/degraded
markers are untouched.
Explicitly out of scope: anchor-name hygiene/stopword filtering (junk anchors
like "the" shouldn't be stored or fuzzy-matched at all) - that's a broader
change to anchor matching behavior, left as a follow-up with a code comment.
Tests (MemoryCurationEvaluatorParityTests, both Akka/ILogger construction
paths for parity):
- guard-rejected anchor Update, no other candidates -> Create (was Skip)
- guard-rejected anchor Update + scripted embedder/index with a real near-dupe
above the nominator threshold -> nominator runs, LLM tier invoked
- regression: guard-rejected Update whose content is close enough to clear
TryAutoResolveAmbiguous's thresholds once re-evaluated as fuzzy -> genuine
auto-resolved Skip (fall-through does not force Create over a real dupe)
- curation_guard_fallthrough marker fires with anchor + rejected target id
Suites green: Actors 2617, Daemon 832, Cli 1231, Configuration 461,
Embeddings 18. Release build clean (0 warnings/errors). Slopwatch 0 new
issues. Header verification clean.
Aaronontheweb
merged commit Jul 6, 2026
ec15629
into
netclaw-dev:feature/memory-embeddings
14 of 15 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug (eval run
ad9a2312, daemon log lines 856/903/943/1036)MemoryCurationEvaluator.EvaluateAsynchas an exact-anchor deterministic fastpath: when a proposal's anchor exact-matches an existing document, the rules
tier decides Skip/Update with no further evidence gathering. When that
decision is Update,
CurationRulesEvaluator.GuardDestructiveUpdatechecksthat the proposal actually preserves the target's existing content before
letting the raw overwrite through — if it doesn't, the guard downgrades the
decision to Skip.
That downgraded Skip was being returned as the final decision. In 4/5
repro runs, an LLM-emitted junk anchor name (the bare stopword
the)collided with an unrelated existing document that happened to carry the same
junk anchor text. The guard correctly refused to overwrite the unrelated
doc's content with the new proposal's — but nothing then created the
requested fact, and the embedding kNN nominator / lexical content-term search
never ran at all, because the exact-anchor fast path short-circuits ahead of
both. An explicit
store_memoryproposal silently became a no-op:operations=0on the checkpoint, with only a debug-level skip marker as atrace. That's a silent-fallback violation.
Fix
In the shared evaluator (both the inline per-session actor and the daemon
checkpoint-worker pipelines get this automatically post-Slice-1):
When
GuardDestructiveUpdatedowngrades an anchor-matched Update to Skip,the evaluator no longer terminates. It demotes every exact-anchor-matched
candidate to an ordinary fuzzy candidate and re-runs the entire remaining
evaluation chain exactly as if there had been no exact anchor match in the
first place: embedding nomination (when the embedder/vector index are
available), then the lexical content-term search, then the deterministic
rules tier / LLM tier / auto-resolve chain, with Create as the terminal
default. The demotion is what prevents the re-run from looping back into the
same Update → guard-reject pair.
The guard's protective effect is preserved — the mismatched target is
never overwritten by the raw anchor-path Update. Only the termination was
wrong; the fix corrects that without touching the guard's actual safety
check.
A new structured marker,
curation_guard_fallthrough anchor={name} rejectedTarget={id}, fires whenever this path triggers, so it's observablein daemon logs going forward. Existing skip/degraded markers are untouched.
Explicitly out of scope: anchor-name hygiene / stopword filtering. Junk
anchors like
thearguably shouldn't be stored or fuzzy-matched at all, butfixing that changes anchor-matching behavior broadly across the system. Left
as a follow-up with a code comment at the relevant site — this PR only fixes
the narrower "explicit store becomes a silent no-op" failure, regardless of
why the guard rejected the match.
Tests
MemoryCurationEvaluatorParityTests(both AkkaILoggingAdapterandMicrosoft.Extensions.Logging
ILoggerconstruction paths, proving parityholds for the new decision shapes):
Skip pre-fix; this is the fix, asserted directly).
real near-duplicate exists above the nominator similarity threshold → the
nominator runs for the first time (it never ran on the short-circuited
first pass) and forces the LLM tier.
ordinary fuzzy candidate, clears
TryAutoResolveAmbiguous's thresholds →genuine auto-resolved Skip. This proves the fall-through doesn't
over-correct into "never skip a real duplicate" — it defers to whatever the
rest of the flow legitimately produces.
curation_guard_fallthroughmarker fires with the anchor name andrejected target document id.
Gates
Netclaw.Actors.Tests: 2617 passedNetclaw.Daemon.Tests: 832 passedNetclaw.Cli.Tests: 1231 passedNetclaw.Configuration.Tests: 461 passedNetclaw.Embeddings.Tests: 18 passedNetclaw.slnx -c Release): 0 warnings, 0 errorsdotnet slopwatch analyze: 0 issuesAdd-FileHeaders.ps1 -Verify: all files have headers