Skip to content

fix(memory): explicit stores must never silently drop on anchor-guard rejection - #1587

Merged
Aaronontheweb merged 1 commit into
netclaw-dev:feature/memory-embeddingsfrom
Aaronontheweb:fix/curation-guard-fallthrough
Jul 6, 2026
Merged

fix(memory): explicit stores must never silently drop on anchor-guard rejection#1587
Aaronontheweb merged 1 commit into
netclaw-dev:feature/memory-embeddingsfrom
Aaronontheweb:fix/curation-guard-fallthrough

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

Bug (eval run ad9a2312, daemon log lines 856/903/943/1036)

MemoryCurationEvaluator.EvaluateAsync has an exact-anchor deterministic fast
path: 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.GuardDestructiveUpdate checks
that 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_memory proposal silently became a no-op:
operations=0 on the checkpoint, with only a debug-level skip marker as a
trace. 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 GuardDestructiveUpdate downgrades 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 observable
in daemon logs going forward. Existing skip/degraded markers are untouched.

Explicitly out of scope: anchor-name hygiene / stopword filtering. Junk
anchors like the arguably shouldn't be stored or fuzzy-matched at all, but
fixing 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 Akka ILoggingAdapter and
Microsoft.Extensions.Logging ILogger construction paths, proving parity
holds for the new decision shapes):

  • Guard-rejected anchor Update with no other candidates → Create (was
    Skip pre-fix; this is the fix, asserted directly).
  • Guard-rejected anchor Update + a scripted embedder/vector index where a
    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.
  • Regression: a guard rejection whose content, once re-evaluated as an
    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.
  • The curation_guard_fallthrough marker fires with the anchor name and
    rejected target document id.

Gates

  • Netclaw.Actors.Tests: 2617 passed
  • Netclaw.Daemon.Tests: 832 passed
  • Netclaw.Cli.Tests: 1231 passed
  • Netclaw.Configuration.Tests: 461 passed
  • Netclaw.Embeddings.Tests: 18 passed
  • Release build (Netclaw.slnx -c Release): 0 warnings, 0 errors
  • dotnet slopwatch analyze: 0 issues
  • Add-FileHeaders.ps1 -Verify: all files have headers

… 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
Aaronontheweb merged commit ec15629 into netclaw-dev:feature/memory-embeddings Jul 6, 2026
14 of 15 checks passed
@Aaronontheweb
Aaronontheweb deleted the fix/curation-guard-fallthrough branch July 6, 2026 01: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.

1 participant