Skip to content

refactor(memory): shared curation evaluator for both write pipelines (memory-core-redesign slice 1)#1575

Merged
Aaronontheweb merged 4 commits into
netclaw-dev:devfrom
Aaronontheweb:redesign/slice-1-shared-evaluator
Jul 4, 2026
Merged

refactor(memory): shared curation evaluator for both write pipelines (memory-core-redesign slice 1)#1575
Aaronontheweb merged 4 commits into
netclaw-dev:devfrom
Aaronontheweb:redesign/slice-1-shared-evaluator

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

Summary

Implements Slice 1 (tasks 1.1–1.4) of the memory-core-redesign OpenSpec change (#1570): extracts a shared MemoryCurationEvaluator used by both memory write pipelines, so the inline per-session actor path and the daemon checkpoint-worker path can never diverge again.

What unified

MemoryCurationActor.EvaluateSingleAsync (record bypass → fuzzy anchor candidates → content-term candidate search → CurationRulesEvaluator.Evaluate → LLM tier + GuardDestructiveUpdateTryAutoResolveAmbiguous → Create fallback) and the actor's decision→write-operation mapping (including Consolidate's re-anchor/tombstone side effects) now live in one MemoryCurationEvaluator (src/Netclaw.Actors/Memory/MemoryCurationEvaluator.cs). The actor delegates to it; MemoryCurationEngine.CurateAsync (daemon) routes every extracted candidate through the same EvaluateAsyncApplyDecisionAsync sequence before ApplyCurationBatchAsync persists the result.

Structural finding: the daemon path previously performed no per-candidate relationship evaluation at all — only MemoryRulesFirstExtractor extraction (with its fingerprint literal-duplicate check) followed by a direct write. It never called CurationRulesEvaluator and never guarded updates. Unification here means routing its candidates through the evaluator for the first time, exactly as anticipated by the change's design doc (D4).

The ONE intended behavioral delta

The daemon path gains GuardDestructiveUpdate — closing audit finding D14 (the lossless-update guard existed only on the inline actor path). An Update decision whose proposal does not preserve the existing body is downgraded to Skip on both paths now. The daemon engine constructs the evaluator without an LLM client (its worker has none), preserving today's deterministic-only behavior there; Ambiguous decisions resolve via TryAutoResolveAmbiguous.

Marker-stability guarantee

The evaluator logs through a minimal internal ICurationLog seam with two adapters (AkkaCurationLog for the actor's ILoggingAdapter, MicrosoftCurationLog for the engine's MEL ILogger). Message templates and argument order are byte-identical to the pre-refactor actor code, so the July 2026 audit tooling's grep targets keep firing unchanged from both paths: curation_dual_search, curation_llm_decision, curation_llm_no_decision, curation_llm_timeout, curation_llm_error, curation_ambiguous_auto_resolved, curation_ambiguous_create_fallback, curation_skip/_update/_consolidate/_create, curation_reanchor, curation_tombstone_anchor.

Characterization matrix (MemoryCurationEvaluatorParityTests, 9 tests, all green)

Both constructions (actor-style ILoggingAdapter / engine-style ILogger, no LLM) driven against one seeded store, asserting decision equality:

Case Decision Parity
Record bypass Create identical
Exact anchor + high overlap Skip identical
Exact anchor + newer superset Update identical
Fuzzy anchor > 0.8 overlap Consolidate identical
Gray zone 0.4–0.8, no LLM auto-resolve → Skip identical
Fuzzy anchor < 0.4 overlap Create identical
Guard downgrade (proposal drops existing body) Update → Skip identical — now fires on BOTH paths (D14)
Scripted LLM, parseable SKIP LLM decision honored + guarded
Scripted LLM, empty response falls back to deterministic auto-resolve

Validation

  • dotnet test src/Netclaw.Actors.Tests2540/2540 passed
  • dotnet test src/Netclaw.Daemon.Tests825/825 passed
  • dotnet slopwatch analyze — 0 issues
  • Add-FileHeaders.ps1 -Verify — all files have headers
  • Memory eval gate (openai-compatible @ spark-acad, nvidia/Qwen3.6-27B-NVFP4, 5 runs/case):
Category: Memory Pipeline
  [PASS] memory_recall_active           5/5 (1.00)  — recall active, not degraded
  [FAIL] memory_identity_preference_routing 3/5 (0.60)  — durable user preference routed to memory, not SOUL.md
  [PASS] memory_explicit_store          5/5 (1.00)  — explicit remember request uses store_memory
  [PASS] memory_checkpoint_enqueue      5/5 (1.00)  — checkpoint enqueued for non-identity fact
  [PASS] memory_recall_filters          5/5 (1.00)  — candidate selection with score filtering
  Category: 4/5 passed (YELLOW)
Overall: 4/5 cases passed (80.0%)

The single failing case is an instrumentation false negative, not a regression from this PR: in both "failing" runs the model behaved exactly as the eval's own comment defines correct ("already saved — I can see 'Favorite Color — Chartreuse' in my durable memories", no SOUL.md edit), but the assertion greps for the literal substring memory, which does not match the word "memories". A first eval attempt scored 2/5 because the vLLM endpoint restarted mid-run (15/25 prompts got Connection refused); the retry above is the clean run. This case is untouched by this refactor (which only affects the post-proposal curation write pipeline, not model tool routing or response text) and the same code scored 5/5 on it in the earlier partial run.

OpenSpec

opsx: memory-core-redesign slice 1 (task 1.1)

Extracts the full curation decision flow (record bypass -> fuzzy anchor
candidates -> content-term search -> CurationRulesEvaluator.Evaluate ->
LLM tier + GuardDestructiveUpdate -> TryAutoResolveAmbiguous -> Create
fallback) out of MemoryCurationActor.EvaluateSingleAsync into a new
MemoryCurationEvaluator, and wires the actor through it. The evaluator
also owns ApplyDecisionAsync, the decision -> write-operation mapping
(including Consolidate's re-anchor/tombstone side effects) that used to
live inline in the actor's write phase.

Constructor dependencies are SQLiteMemoryStore (required) and IChatClient?
(genuinely optional: absent LLM is a real permanent operating mode for a
session with no compaction-role model, not a compat shim).

Logging seam: the actor logs via Akka ILoggingAdapter, the daemon engine
(next commit) via Microsoft.Extensions.Logging ILogger. A minimal internal
ICurationLog delegate (AkkaCurationLog / MicrosoftCurationLog) lets one
evaluator body emit identical markers (curation_llm_decision,
curation_llm_no_decision, curation_llm_timeout, curation_llm_error,
curation_dual_search, curation_ambiguous_auto_resolved,
curation_ambiguous_create_fallback, curation_skip/update/consolidate/
create, curation_reanchor, curation_tombstone_anchor) through either
logging stack, since the July 2026 audit tooling greps daemon logs for
these exact marker strings.

SidecarSessionCorrelationTests updated to call
MemoryCurationEvaluator.TryLlmEvaluationAsync (moved from the actor).
…ator

opsx: memory-core-redesign slice 1 (task 1.2)

Structural finding: MemoryCurationEngine.CurateAsync (the daemon
checkpoint-worker path) performed NO per-candidate relationship
evaluation before this change — only MemoryRulesFirstExtractor
extraction (with its own fingerprint-based literal-duplicate check) plus
a direct write via ApplyCurationBatchAsync. It never called
CurationRulesEvaluator, never ran the LLM tier, and never guarded
against destructive updates. Unification therefore means routing
extracted candidates through MemoryCurationEvaluator for the first time,
not deleting a parallel copy of the actor's logic (none existed).

MemoryCurationEngine now constructs a MemoryCurationEvaluator with the
store and its own ILogger, and WITHOUT an LLM client (the daemon worker
has none to give it) — every Ambiguous decision on this path resolves
via CurationRulesEvaluator.TryAutoResolveAmbiguous. CurateAsync's
extraction step (MemoryRulesFirstExtractor, fingerprint pre-check) is
unchanged; after building operations from extracted candidates, each is
evaluated then applied via the shared EvaluateAsync/ApplyDecisionAsync
before being returned to the checkpoint worker's ApplyCurationBatchAsync
call.

THE ONE INTENDED BEHAVIORAL DELTA (audit finding D14): the daemon path
now applies CurationRulesEvaluator.GuardDestructiveUpdate, closing the
gap where only the inline actor path guarded against an Update decision
overwriting existing content it does not preserve.
opsx: memory-core-redesign slice 1 (task 1.3, 1.4)

MemoryCurationEvaluatorParityTests seeds one SQLiteMemoryStore and
constructs two evaluators against it — one via the ILoggingAdapter
constructor (as MemoryCurationActor does), one via the ILogger
constructor (as MemoryCurationEngine does), both without an LLM client —
and asserts EvaluateAsync returns identical CurationDecision values for
the same operation across the full decision matrix:

- record bypass -> Create
- exact anchor, high content overlap -> Skip
- exact anchor, newer + guard-clean superset -> Update
- fuzzy anchor, high overlap -> Consolidate
- gray zone (0.4-0.8), no LLM -> deterministic auto-resolve (Skip)
- fuzzy anchor, low overlap -> Create
- guard downgrade: an Update whose proposal drops the existing body's
  content -> Skip on BOTH paths (audit finding D14 closed)

Plus two LLM-tier cases via a scripted IChatClient: a parseable decision
(SKIP) and an empty response, which must fall back to the same
deterministic auto-resolve decision as the no-LLM case.

Gates run clean: dotnet test Netclaw.Actors.Tests (2540/2540) and
Netclaw.Daemon.Tests (825/825), dotnet slopwatch analyze (0 issues),
Add-FileHeaders.ps1 -Verify. Marks tasks 1.1-1.4 done in
openspec/changes/memory-core-redesign/tasks.md.
@Aaronontheweb Aaronontheweb added the memory Memory formation, recall, curation pipeline label Jul 4, 2026

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

LGTM - basically standardizes the daemon's memory curation system + the session one to use the same standards and decision tree

@Aaronontheweb
Aaronontheweb merged commit 79534dc into netclaw-dev:dev Jul 4, 2026
15 checks passed
@Aaronontheweb
Aaronontheweb deleted the redesign/slice-1-shared-evaluator branch July 4, 2026 15:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

memory Memory formation, recall, curation pipeline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant