Skip to content

ONNX Memory: Local embedding backend for cross-session memory - #1749

Open
Aaronontheweb wants to merge 46 commits into
netclaw-dev:devfrom
Aaronontheweb:stage/onnx-memory-merge
Open

ONNX Memory: Local embedding backend for cross-session memory#1749
Aaronontheweb wants to merge 46 commits into
netclaw-dev:devfrom
Aaronontheweb:stage/onnx-memory-merge

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

Summary

This merges feature/memory-embeddings into dev after a sync with current dev @ 0.25.2. 36 commits of ONNX embedding work, covering the full semantic memory pipeline: embedding at write time, kNN/L curation at nomination time, hybrid (vector + lexical) recall, and a cross-encoder relevance gate.

What ships

New assembly: Netclaw.Embeddings

  • OnnxMemoryEmbedder — ONNX runtime-based embedding (implements IMemoryEmbedder)
  • OnnxCrossEncoderScorer — post-floor relevance gate
  • EmbeddingModelProvisioner — allowlist-gated model download, verify, and load

Default models

  • Embedder: snowflake-arctic-embed-m-int8 (int8 quantized; ~57% less RSS, ~1.7x faster than fp32)
  • Relevance gate: ms-marco-minilm-l-6-v2 (cross-encoder)
  • Both are allowlisted — no arbitrary model loading (supply-chain boundary)

Configuration (MemoryConfig)

Knob Default What it does
Memory.Embeddings.Enabled false Master toggle — off by default for this merge
Memory.Embeddings.ModelId snowflake-arctic-embed-m-int8 Allowlisted model ID
Memory.Embeddings.AutoDownload true Download model at startup if not present
Memory.Curation.* various kNN nominator thresholds, LLM curation budget
Memory.Recall.* various Hybrid fusion weights, cosine floor, recency decay
Memory.Recall.RelevanceGate.Enabled null Follows Embeddings.Enabled; explicit override available
Memory.Recall.RelevanceGate.Threshold null Follows model manifest; explicit override available

Operational alerts

  • Model provisioning failures surface via the operational alert channel (same as reminder-failure alerts)
  • netclaw doctor reports embedding model health

CLI

  • netclaw memory backfill-embeddings — re-embed corpus
  • netclaw memory subcommand dispatched properly

What does NOT ship

  • Memory.Embeddings.Enabled defaults to false — no ONNX models load, no embeddings are computed, no relevance gate runs. This is a no-op for existing installs.

Before merging, file:

  • NetClaw website issue: Document the Memory.Embeddings.Enabled toggle, memory/RAM impact (~500MB RSS for int8 embedder + ~300MB for cross-encoder = ~800MB total when both loaded), auto-download behavior, and the netclaw memory backfill-embeddings command
  • Release notes entry: Document ONNX memory as an opt-in feature — mention both model sizes, RAM footprint, enablement toggle, and the netclaw memory subcommand surface
  • Upgrade guide: Existing installs upgrade with zero impact (toggle off). Operators who enable it need ~800MB additional RAM and internet access on first start (or pre-provision models)

Merge conflicts resolved

  • Directory.Build.props → dev version
  • RELEASE_NOTES.md → dev version (ONNX alpha notes preserved in branch history)
  • feeds/skills/.system/files/netclaw-operations/SKILL.md → dev version

Slices covered

Slice PR Description
2 #1577 Embedding foundation: ONNX runtime, provisioning, embed-on-write, backfill
3 #1585 kNN-nominate / LLM-decide dedup + lossless merges
4 multiple Read-side hybrid recall with absolute cosine floor
#1588 Relevance-gate: cross-encoder scoring
#1608 Relevance-gate cold-start fixes
#1611 Operational alerts for model provisioning failures
#1679 Curation LLM timeout raised 10s → 60s

Aaronontheweb and others added 30 commits July 5, 2026 12:39
…d-on-write, backfill (memory-core-redesign slice 2) (netclaw-dev#1577)

* feat(embeddings): standalone ONNX embedding runtime + model provisioner

Adds the src/Netclaw.Embeddings project (Microsoft.ML.OnnxRuntime CPU EP,
FastBertTokenizer, System.Numerics.Tensors), referenced by nothing yet —
daemon/CLI wiring is Stage B.

- OnnxMemoryEmbedder: single InferenceSession (IntraOpNumThreads=4),
  BoundedConcurrencyGate (default max 2 concurrent inferences, peak
  concurrency observable for tests), FastBertTokenizer WordPiece
  tokenization truncated to 512 tokens. Feeds only the input names the
  loaded ONNX graph declares rather than hardcoding the 3-input BERT
  signature. CLS-token pooling (last_hidden_state[:, 0, :]) + L2
  normalization — verified against both allowlisted models' model cards
  (snowflake-arctic-embed-m: "use the CLS token"; mxbai-embed-large-v1:
  "works really well with cls pooling (default)").
- EmbeddingModelProvisioner: pinned in-code allowlist (model id -> URL,
  SHA-256, byte size, dimensions) injected as a required dependency (not
  a hardcoded internal) so tests can supply a localhost-pointed allowlist
  instead of ever reaching the real HuggingFace URLs. Atomic
  temp-file-then-rename download, byte-size + SHA-256 verification before
  the destination file is ever created, unknown-id rejection listing the
  allowlist. Allowlist entries (URLs pinned to a specific upstream commit,
  not `main`): snowflake-arctic-embed-m (768 dims, plain fp32 model.onnx,
  ~416 MB) and mxbai-embed-large-v1 fallback (1024 dims, ~1.27 GB fp32).

Tests (Netclaw.Embeddings.Tests, no network):
- Tiny fixture ONNX graph + WordPiece vocab (generated by
  Fixtures/generate_fixture_model.py, committed with a header comment
  explaining the graph shape and regeneration steps) exercise
  OnnxMemoryEmbedder end-to-end: deterministic output, L2-normalized,
  content-sensitive (attention-masked mean pooling reported at the CLS
  position so a content-blind bug would be caught), batch order
  preservation.
- BoundedConcurrencyGate tested in isolation against a controlled fake
  delayed workload (Task.Delay lives in the fake, not in test
  orchestration) proving the concurrency bound is actually enforced.
- EmbeddingModelProvisioner tested against a local HttpListener fixture:
  hash-mismatch and byte-size-mismatch rejection with no leftover temp
  files, unknown-id rejection listing the allowlist, successful
  provision leaves exactly the two expected files.

opsx: memory-core-redesign slice 2

* feat(memory): IMemoryEmbedder seam, content hasher, vector index, embeddings schema

- IMemoryEmbedder (+ UnavailableMemoryEmbedder degraded stub) in
  Netclaw.Actors/Memory so actor code carries no OnnxRuntime dependency;
  Netclaw.Embeddings implements the interface, never the reverse.
  UnavailableMemoryEmbedder throws InvalidOperationException with
  remediation text on Embed*Async rather than returning a garbage vector.
- MemoryContentHasher: SHA-256 over normalized title+body, reusing
  CurationRulesEvaluator.NormalizeForContainment (promoted from private to
  internal) rather than a second hand-rolled normalizer, so curation's
  destructive-update guard and the embedding re-embed skip can't quietly
  disagree about what counts as changed content.
- MemoryVectorIndex: per-model flat float[] + parallel id/kind arrays
  bundled into an immutable snapshot (no torn reads), TopK via
  System.Numerics.Tensors.TensorPrimitives.CosineSimilarity with a
  minCosine floor, reload gated on SQLiteMemoryStore.EmbeddingDataVersion
  so unchanged turns pay no reload cost.
- SQLiteMemoryStore: memory_embeddings(item_id, item_kind, model_id,
  content_hash, dims, vector BLOB, created_at) DDL in the existing
  idempotent InitializeAsync; UpsertEmbeddingAsync (hash-skip: no write
  and no EmbeddingDataVersion bump when the content hash is unchanged,
  float32 LE blob); GetEmbeddingsForModelAsync (thin query for the vector
  index to consume — the design's FindNearestByEmbeddingAsync, renamed
  per plan); GetEmbeddingCoverageAsync (total recallable docs,
  embedded-current-hash count, other-model count) for the coverage
  diagnostics spec requirement; TombstoneDocumentAsync extended to delete
  the tombstoned document's embedding rows in the same transaction and
  bump the version counter, since vectors are derived data that must not
  keep surfacing a dead document as a kNN neighbor.

No production code path calls any of this yet (embed-on-write, the vector
index's runtime wiring, and the doctor/status degradation surfaces are
Stage B) — this slice writes vectors, nothing reads them, zero behavior
risk per the design's migration plan.

opsx: memory-core-redesign slice 2

* feat(memory): mark tasks 2.1-2.6 complete (opsx: memory-core-redesign slice 2)

Task 2.12 (tests) stays unchecked — its warmup/gap-repair/doctor-facing
scenarios land with Stage B daemon wiring; the store/index/hasher/
provisioner/embedder subset testable at this layer is covered.

* feat(memory): embed-on-write foundation — holder, coordinator, store seams, config (opsx: memory-core-redesign slice 2, tasks 2.7/2.8/2.11)

- MemoryEmbedderHolder: mutable holder the warmup hosted service populates
  (hosted-service startup order vs construction-time DI documented on the type)
- MemoryEmbedOnWriteCoordinator: single embed-on-write hook both curation
  pipelines call post-commit; embedding failures never fail the memory write
  (vectors are derived data, D3)
- SQLiteMemoryStore: ApplyInlineCurationBatchAsync/ApplyCurationBatchAsync now
  return the written document rows (the post-commit ids+content the coordinator
  needs); GetDocumentsNeedingEmbeddingAsync derives gap-repair/backfill state
  (never a progress table); UpsertEmbeddingAsync reports wrote-vs-skipped
- MemoryCurationActor + MemoryCurationWorkerService callers embed after commit
- Memory.Embeddings config { Enabled=false (deliberate staging, flipped in
  Slice 3/4), ModelId=snowflake-arctic-embed-m, AutoDownload=true } + schema
  sync with defaults; NetclawPaths.ModelsDirectory + EmbeddingModelDirectory
- DaemonRuntimeStatus.Embeddings wire type (ok/degraded/disabled)
- EmbeddingModelProvisioner: skip-if-valid local copy (no network on restart)
  + TryLoadVerifiedAsync for AutoDownload=false paths

* feat(daemon): embedding warmup service, gap repair, degraded status surface (opsx: memory-core-redesign slice 2, tasks 2.7/2.8/2.10)

- EmbeddingWarmupHostedService: provision-or-degrade at startup (AutoDownload
  gates the network path entirely — even to repair a corrupt local copy), one
  warm-up inference, then a batched (16, yielding) gap-repair sweep over
  documents missing a current-model/current-hash embedding
- ANY failure => UnavailableMemoryEmbedder + error-level
  memory_embedding_unavailable log; daemon NEVER fails startup on embeddings
- DI: holder starts as Unavailable stub; warmup populates it; SessionMemoryServices
  carries it to the inline curation actor
- MemoryCurationWorkerService embeds written docs post-commit (task 2.8's
  second pipeline call site)
- DaemonRuntimeStatusService reports embeddings: ok/degraded/disabled with
  modelId under the Memory status block

* feat(cli): netclaw memory backfill-embeddings + embedding doctor check (opsx: memory-core-redesign slice 2, tasks 2.9/2.10)

- New 'netclaw memory' command group (offline, direct SQLite/model-file
  access) with backfill-embeddings [--force]: provisions if needed (clear
  error when AutoDownload=false and model missing), embeds in batches of 16
  with progress output, final embedded/skipped-hash-unchanged/failed summary;
  safe against a live daemon (WAL + per-item upserts whose hash check
  re-queries at call time)
- MemoryEmbeddingDoctorCheck: Error when Enabled but model missing/hash-invalid;
  Warning on missing current-model embeddings (count) or mixed-model corpus
  (recommends --force backfill); Pass with coverage summary; Pass when disabled
- Allowlist is an injected dependency on both (same seam as the provisioner)
  so tests use the tiny fixture model — no network in tests
- Schema round-trip tests for the Memory.Embeddings config section

* docs(opsx): correct D2 model line to shipped reality; mark tasks 2.7-2.12 complete (memory-core-redesign slice 2)

design.md D2 said 'snowflake-arctic-embed 137M int8' — Stage A shipped the
~110M-param arctic-embed-m fp32 ONNX artifact pinned by hash; int8 is noted
as a future optimization, not what the allowlist points at.

* ci+skills: arm64 onnxruntime smoke leg, memory/operations skill sync (opsx: memory-core-redesign slice 2)

* feat(tools): ONNX embedding latency bench + measured numbers, docs(opsx) design.md

Add tools/embed-latency-bench (standalone console, kept out of Netclaw.slnx):
loads the production OnnxMemoryEmbedder path (hash-verified snowflake-arctic-embed-m,
same pooling/threading/concurrency-gate config the daemon uses) and times batch=1
EmbedAsync calls across short-query/medium/doc-length corpora (20 warmup + 200 timed
iterations each), plus cold-load and a concurrency=2 pass.

Measured on the i9-9900K reference box (8 logical cores, contended: load avg 2.0-3.6,
~11/15 GiB RAM in use, live daemon running): short-query p50 281ms / p95 315ms -
statistically indistinguishable from doc-length (p50 275ms / p95 294ms) because
OnnxMemoryEmbedder pads every input to a fixed 512 tokens regardless of actual
length, so the fixed-size fp32 forward pass dominates latency, not tokenization.

Resolves memory-core-redesign task 2.13 and its design.md open question: the
150ms query-embedding sub-budget does NOT hold on this hardware (p95 ~2.1x over
budget, margin ~-165ms). Updates D6, the Risks/Trade-offs entry, and the Open
Questions table with the measured numbers and verdict; corrects stale "ONNX int8"
wording to match the D2-shipped fp32 reality (int8 remains a deferred optimization).
Highest-leverage unexplored mitigation: a query-specific max-length well below 512,
not int8 quantization.

* chore(bench): dynamic sequence length experiment

Extends tools/embed-latency-bench with a bench-only parallel code path
(OnnxMemoryEmbedder production code untouched) that:

- inspects InferenceSession.InputMetadata to confirm the ONNX graph's
  sequence axis is symbolic (dynamic), not fixed
- runs the same short/medium/doc corpora padded to actual tokenized
  length (bucket-of-8 rounding) instead of fixed 512, same 20
  warmup / 200 timed / batch=1 / Release protocol
- cross-checks correctness: cosine similarity between fixed-512 and
  dynamic-length embeddings for 10 fixed sentences
- records load average before/after for honest contention context

Measured on the reference box: short-query p50 19.0ms / p95 20.9ms
(vs 281.9ms / 310.5ms fixed-512) with 1.000000 cosine parity across
all 10 sentences. Well under the 150ms Slice 4 sub-budget.

Updates openspec/changes/memory-core-redesign/design.md (D6, Risks,
Open Questions) with the measured numbers and the decision to adopt
dynamic sequence length as the Slice 4 mitigation.
…-core-redesign slice 3) (netclaw-dev#1585)

* feat(memory): config surface for write-side curation (opsx: memory-core-redesign slice 3)

MemoryConfig gains Curation { NominatorSimilarityThreshold, NominatorK,
LlmMaxOutputTokens, LlmTimeoutSeconds } with schema sync (defaults,
additionalProperties: false). Nominator threshold/K are defined now with doc
comments noting they are consumed by Slice 3 Stage B (task 3.1), not this
change. Task 3.5.

* feat(memory): merged-body response protocol for UPDATE/CONSOLIDATE (opsx: memory-core-redesign slice 3)

CurationPromptBuilder's system prompt now instructs the curator to emit a
'---'-delimited lossless merged body after UPDATE/CONSOLIDATE keyword lines;
SKIP/CREATE remain keyword-only. ParseResponse extracts the optional body into
the new CurationDecision.MergedBody (absent/malformed body treated as null,
keyword-only responses remain valid). CurationDecision also gains FromLlmTier,
distinguishing LLM-synthesized decisions from the deterministic rules tier for
write-routing purposes (wired in the next commit). BuildUserMessage gains a
useFullCandidateContent parameter (default false, current 700-char preview
behavior) for Stage B's full-content nominated candidates — not consumed yet.

Task 3.2.

* feat(memory): MergeGuard load-bearing token retention validator (opsx: memory-core-redesign slice 3)

New deterministic MergeGuard.Validate(sourceBodies, mergedBody) -> pure
function checking (1) retention: >=95% of the union of load-bearing tokens
(URLs, numbers/versions/quantities/dates, camelCase/snake_case/kebab-case/
dotted.path/ALL_CAPS identifiers, file paths) extracted from every source body
must survive case-insensitively in the merged body, and (2) collapse: merged
length must be >=60% of the longest single source. Converts an LLM merge error
from silent data loss into a recoverable append-fallback signal (design D5);
wired into the write path in the next commit.

Task 3.3.

* feat(memory): guard-validated write routing, close raw-overwrite paths (opsx: memory-core-redesign slice 3)

MemoryCurationEvaluator.ApplyDecisionAsync now routes every LLM-tier
UPDATE/CONSOLIDATE decision through MergeGuard-validated merge or a structural
append fallback (existing body + dated separator + proposal, AppendDocument
semantics) instead of a raw overwrite — this is what makes AppendDocument a
real, reachable write path for the first time. EvaluateAsync returns a new
CurationEvaluation (Decision + Candidates) so ApplyDecisionAsync can validate
against the same candidate bodies the decision was made against without
re-querying the store; both callers (MemoryCurationActor, MemoryCurationEngine)
and the parity tests are updated for the new signature.

GuardDestructiveUpdate is no longer applied to LLM-tier decisions in
EvaluateAsync: its raw-proposal containment check would reject a legitimate
reworded merge, and the new write-time guard supersedes it for that tier. The
deterministic tier's exact-anchor UPDATE keeps its pre-Slice-3 behavior
unchanged (GuardDestructiveUpdate's containment proof already makes that raw
overwrite non-lossy on its own terms) — this is the one decision shape
explicitly exempted per design D5. Deterministic-tier CONSOLIDATE (fuzzy match
>=80% overlap, no LLM call) previously reached the store with no guard at all;
it now flows through the same append-fallback path as an LLM decision with no
merged body, closing that gap too.

MemoryCurationConfig threads through MemoryCurationActor/MemoryCurationEngine
to TryLlmEvaluationAsync, replacing the hardcoded 10s timeout and 4096 max
output tokens. SQLiteMemoryStore exposes its TimeProvider so the append
fallback's date separator stays consistent with the store's own persisted
timestamps.

New MemoryCurationMergeRoutingTests exercise this end-to-end through the real
evaluator + store: guard-fail and body-absent LLM Update/Consolidate produce
append semantics with the target's original body intact as a prefix; a guard-
passing merge writes the merged body; the deterministic exact-anchor Update
and fuzzy-match Consolidate paths are covered as regression/closed-gap proof.

Task 3.4. Marks tasks 3.2-3.5 complete in tasks.md (NOT 3.1/3.6/3.7 — Stage B,
a later dispatch).

* feat(memory): embedding kNN nominator — cosine nominates, LLM decides (opsx: memory-core-redesign slice 3, task 3.1)

Adds the nominate→decide dedup step to the shared MemoryCurationEvaluator
(design D4): when the embedder is available and there is no exact anchor
match, the proposal is embedded (same title\ncontent concatenation as
embed-on-write) and MemoryVectorIndex.TopK shortlists up to
Memory.Curation.NominatorK existing documents at or above
Memory.Curation.NominatorSimilarityThreshold. Nominees are hydrated into
full-content candidates (SQLiteMemoryStore.GetCandidatesByIdsAsync) tagged
with their cosine (ExistingMemoryCandidate.CosineSimilarity; anchor/lexical
candidates carry null).

Invariants (May 2026 measurement: no cosine threshold separates duplicates
from siblings — siblings live at 0.905–0.941 inside the duplicate band):
- Any nominee FORCES the LLM tier with full-content candidate previews;
  cosine never auto-merges and never auto-skips.
- Nominee present + no LLM (daemon checkpoint worker today) or LLM failure
  → conservative Create, deliberately bypassing TryAutoResolveAmbiguous:
  semantic-near content is exactly the ambiguity Jaccard heuristics cannot
  adjudicate, and a duplicate is recoverable where a wrong merge is not.
- No nominee + no anchor match → Create with zero LLM calls (the cheap
  common case — median nominee count on a random write is 0).
- Embedder unavailable/absent → pre-slice lexical content-term search runs
  unchanged as the degraded path (curation_nominator_degraded marker).

Wiring: new MemoryVectorIndexHolder (defers index construction until the
warmed-up embedder's model id/dimensions are known, mirrors
MemoryEmbedderHolder's holder-not-singleton rationale) is registered in the
daemon DI and threaded into BOTH write pipelines — the inline actor via
MemoryCurationActor.CreateProps/SessionMemoryServices, and the daemon
worker via MemoryCurationEngine's constructor.

New log markers: curation_nominated count/topCosine,
curation_nominator_degraded, curation_nominee_no_llm_decision.

Known limitation (documented on NominateAsync): proposals evaluated in one
batch cannot nominate each other — neither is committed/indexed while the
other is evaluated. Cross-batch and steady-state dedup are unaffected.

* test(memory): nominator matrix — forced-LLM, sibling never-auto-merge, degraded path, parity, actor e2e (opsx: memory-core-redesign slice 3, task 3.6)

All fixtures synthetic; cosine geometry is hand-crafted (unit vectors at
exactly 0.93 — inside the measured sibling band) rather than model-derived,
so every scenario is deterministic.

- MemoryCurationNominatorTests (evaluator level):
  * paraphrase pair at cosine 0.93 with word-Jaccard <0.4 forces the LLM
    tier (recording IChatClient proves the call) even though the lexical
    tier finds zero candidates and would have said Create silently
  * nominee + scripted LLM CREATE → two separate documents persist
  * nominee + NO LLM → conservative Create (reason cites no-auto-merge on
    cosine alone); still two documents — never a merge without the LLM
  * novel proposal (no nominee, no anchor) → Create with zero LLM calls
  * embedder unavailable → lexical candidates still produced +
    curation_nominator_degraded fires; null holder behaves identically
- MemoryCurationEvaluatorParityTests: nominee-present construction parity —
  actor-style (ILoggingAdapter) and engine-style (ILogger) evaluators
  sharing one store/embedder/index reach the identical forced-LLM decision
- MemoryCurationActorNominatorTests (Akka.TestKit): proposal driven through
  the real MemoryCurationActor end-to-end to a committed store write;
  AwaitAssertAsync polls for the forced LLM call (no sleeps)

* docs(memory): netclaw-memory skill 1.9.0 — semantic dedup + lossless merges; check tasks 3.1/3.6/3.7 (opsx: memory-core-redesign slice 3, task 3.7)

Skill addition (How Memory Works): with Memory.Embeddings.Enabled, a
near-duplicate proposal is nominated by embedding similarity and
adjudicated by the curator LLM (skip/update/consolidate/create) —
similarity alone never merges or skips — and merges are lossless-or-append
(merged body keeps every source fact; a deterministic guard falls back to
appending instead of overwriting when that check fails).

Memory eval gate (category=Memory, Qwen3.6-27B-NVFP4 @ spark-acad,
5 runs/case): 5/5 cases passed (100.0%), GREEN. Embeddings default OFF, so
the nominator idles in the eval daemon — the gate proves the shared
curation paths did not regress.
… instead of silently dropping (opsx: memory-core-redesign) (netclaw-dev#1587)

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.
netclaw-dev#1603)

The memory-core-redesign slice 2 (netclaw-dev#1577) added the `netclaw memory`
CLI command but never updated the approved `help` screenshot baseline,
so Screenshot Regression (Linux) has been red on every push to
feature/memory-embeddings since (verified via gh run list against
feature/memory-embeddings: 3/3 recent pushes failed this check).

Reviewed the captured diff locally: the only change is the new
  memory              Manage cross-session memory (embeddings backfill, offline)
line shifting everything below it down one row. No other screenshot
frame changed.
…tclaw-dev#1583)

* test: add failing tests for BOM frontmatter parsing and missing SkillName in issues

* fix: handle UTF-8 BOM in ExtractFrontmatter and populate SkillName on all issues

- Strip UTF-8 BOM (\uFEFF) before checking for YAML frontmatter delimiter
- Populate SkillName on all SkillScanIssue records created in SkillScanner
- Fix BOM check in ParseSkillFile and ParseFlatSkillFile for issue kind determination

Fixes netclaw-dev#1582

* fix(skills): prevent scan-aborting crash on degenerate frontmatter; normalize issue SkillNames

Review follow-ups on the BOM/SkillName change:

- ExtractFrontmatter threw ArgumentOutOfRangeException on a degenerate
  "---\n---" block (empty YAML body): the opening line's newline is also
  the closing delimiter, so the slice computed a negative-length range.
  Because Scan's parse calls are unguarded, this propagated out and aborted
  the entire skill-discovery pass (no skills loaded at all). Since
  File.ReadAllText strips the BOM, a plain "---\n---" SKILL.md on disk hit
  this too. Now guarded to return null (reported as invalid frontmatter).

- ResourceEnumerationFailed derived SkillName from the parent directory
  (Path.GetDirectoryName(skillDirectory)), yielding the container name
  ("files") instead of the skill name. Fixed to use the leaf directory.

- Normalized all error-path SkillName derivations via NormalizeSkillName so
  errored rows render the canonical lowercased name, consistent with
  accepted skills.

- Removed dead caller-side content.TrimStart('') no-ops: content comes
  from File.ReadAllText which already strips the BOM, so BOM tolerance lives
  solely in ExtractFrontmatter.

Adds regression tests: degenerate-block returns null (does not throw), Scan
survives a degenerate SKILL.md and keeps discovering healthy siblings, and
issue SkillNames are normalized from mixed-case directories.
* chore: bump SkillServer to 0.4.0-beta.3 and adapt to API changes

* fix: update test mocks to use v1 API paths

* test: align sidecar mock with v1 API
---
updated-dependencies:
- dependency-name: Grpc.Tools
  dependency-version: 2.82.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…ev#1596)

---
updated-dependencies:
- dependency-name: Netclaw.SkillClient
  dependency-version: 0.4.0-beta.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…law-dev#1597) (netclaw-dev#1600)

Sub-agent LLM calls never recorded token usage, so every sub-agent's
input/output tokens were invisible to `netclaw stats`. This was a
pre-existing gap, not a regression: SubAgentActor never had an
ISessionMetrics dependency and discarded the ChatResponse.Usage it already
receives from StreamingResponseReader. The recent observability PRs
(netclaw-dev#1428, netclaw-dev#1468, netclaw-dev#1472/netclaw-dev#1499) only added logs and pruned dead OTel Activities;
none ever touched token tracking.

Fix records at the source, mirroring the main session:
- Inject ISessionMetrics into SubAgentActor (via SubAgentSpawner/CreateProps).
  DI already registers it as a process-wide singleton, so no Program.cs change.
- Record response.Usage on every LlmResponseReceived (one per LLM turn:
  tool-call turns, retries, the forced-no-tools final turn, repair turns).
- Add cumulative input/output token totals to the completion summary log.

Recorded in the child, not propagated to the parent: ISessionMetrics is the
SAME process-wide singleton both share, so re-recording in the parent would
double-count, and folding sub-agent tokens into the parent's UsageOutput
would corrupt its context-window percentage (the sub-agent has its own
context window).

Regression coverage (all four fail if the recording is removed):
- SubAgentActor bills usage per LLM call and sums across the turn loop.
- Completion summary log carries token totals.
- Full spawner->CreateProps->actor wiring bills tokens to the spawner's metrics.

Adds a UsageOverride hook to the sub-agent test FakeChatClient and extracts a
shared RecordingSessionMetrics test helper.
…itHub release body

The 'Extract latest release notes' step in publish_release_binaries.yml matched
on '####' headings, but RELEASE_NOTES.md actually uses '## X.Y.Z (YYYY-MM-DD)'
headings. The regex never matched, so the entire RELEASE_NOTES.md file was
dumped verbatim as the GitHub release body for every tagged release (including
the 0.25.0-beta.1/beta.2 prereleases).

Fix the regex to match '## <digit>...' sections instead, so only the section
for the tag being released is extracted. Also correct CONTRIBUTING.md, which
documented the stale '#### X.Y.Z YYYY-MM-DD ####' heading format in the
Releasing section.
This is the named experimental memory-embeddings prerelease, cut from feature/memory-embeddings. It is a superset of 0.25.0-beta.2 and is channel-neutral (alpha.* sorts below beta.* in SemVer precedence), so it will not advance the beta or stable release channels — install only by exact version pin.
Reconciles cherry-picked dev commits (version-bump-to-beta.2, Grpc.Tools
2.82.0, SkillServer 0.4.0-beta.3, BOM frontmatter tests,
memory-relevance-gate opsx artifacts) plus two commits we lacked:
sub-agent LLM token usage daily stats fix (netclaw-dev#1600) and Netclaw.SkillClient
0.4.0-beta.3 -> 0.4.0-beta.4 bump (netclaw-dev#1596).

Conflict resolutions:
- Directory.Build.props: kept VersionSuffix alpha.onnx.1 (ours); we remain
  the alpha.onnx.1 experimental prerelease, not beta.2.
- Directory.Packages.props: took Netclaw.SkillClient 0.4.0-beta.4 (theirs).
- RELEASE_NOTES.md: kept the alpha.onnx.1 section on top, followed by a
  single 0.25.0-beta.2 section (content was identical on both sides), then
  the older history unchanged.
…ory-core-redesign slice 4, tasks 4.1-4.5)

Read-side hybrid recall (design D6): SQLiteMemoryRecallCoordinator now embeds
the query once per turn under a fixed 150ms sub-budget (linked CTS nested
inside the caller's Memory.RecallTimeoutMs) via the same
MemoryEmbedderHolder/MemoryVectorIndexHolder Slice 3's curation evaluator
established, and unions FTS5 lexical candidates with the vector index's
top-50 cosine matches (MemoryVectorIndex.TopK(minCosine: MinCosineSimilarity)
applies the absolute floor at the source).

Vector-only hits hydrate through the new
SQLiteMemoryStore.GetRecallCandidatesByIdsAsync, sharing its WHERE predicate
(recall-mode allowlist, boundary, audience, sensitivity, memory-class,
expiry) with SearchByPlanAsync's document branch via a new
DocumentRecallPolicyPredicateSql helper, so the two queries cannot drift and
a vector hit can never bypass a gate a lexical hit would have to clear.
DeterministicCandidateSelector.Score is now public so a vector-only
candidate is scored against plan terms with the identical lexical weights
lexical hits use.

Fusion: fused = VectorWeight*cosine + LexicalWeight*squash(selectorScore) +
(RecallRank/100/10), squash(s) = s/(s+8) mapping the unbounded selector
score into [0,1). Recency decay (task 4.4) multiplies the fused score by
0.85 + 0.15*2^(-ageDays/RecencyHalfLifeDays), structurally floor-bounded at
0.85 so it only breaks ties. Absolute floor: only candidates with
cosine >= MinCosineSimilarity are injectable; zero survivors returns a
healthy empty result (not degraded) so the [memory-recall] block is omitted
- design D6's "nothing relevant means nothing injected."

Degraded path (embedder/index unavailable, sub-budget exceeded, or no
holders wired) runs the pre-Slice-4 lexical pipeline verbatim - same
selector+composite formula, same 14.0 floor - which is exactly what the
untouched MemoryRecallScenarioTests suite (including P09) still pins. A
rate-limited memory_recall_vector_degraded log fires on every fallback
reason: Debug when Memory.Embeddings.Enabled is false (the default,
intentional state - mirrors curation_nominator_degraded's level choice),
Warning when embeddings are enabled but the turn still degraded.

Dynamic-length embedding (part of task 4.1's latency budget): ported the
bucket-of-8 padding from tools/embed-latency-bench into
OnnxMemoryEmbedder.EmbedOne, extracting ComputeBucketedLength as a
directly-unit-tested helper. This is the measured mitigation design D6
requires to keep the 150ms sub-budget from being blown by the previous
fixed-512-token padding.

Config: Memory.Recall { VectorWeight=0.7, LexicalWeight=0.3,
MinCosineSimilarity=0.55, RecencyHalfLifeDays=30 }, schema-synced with
per-field defaults/bounds under Memory (additionalProperties: false).

Design note: the coordinator's ctor now takes MemoryConfig (not just
MemoryRecallConfig) so it can read Embeddings.Enabled for the Debug/Warning
log-level split without threading a second config dependency alongside it;
TimeProvider is a new required ctor parameter (DI already registers
TimeProvider.System as a singleton, so Program.cs needed no new
registrations for either).

Tests: fusion floor rejects a lexically-strong/low-cosine candidate,
zero-survivors is healthy-empty, recency decay bounds (ratio to the 0.85
floor), degraded-path parity (unavailable embedder == no holders),
Debug-vs-Warning log level split, gated-hydration exclusions for
recall_mode/boundary/audience/sensitivity/memory_class (extends
SQLiteMemoryStoreEmbeddingTests), dynamic-length padding determinism/
dimension/norm + ComputeBucketedLength unit tests. MemoryRecallScenarioTests
(the pre-Slice-4 lexical gold suite) passes unchanged.

Gates: dotnet build (Debug+Release) clean, 0 warnings/errors.
Netclaw.Actors.Tests 2641, Netclaw.Embeddings.Tests 29,
Netclaw.Configuration.Tests 465, Netclaw.Cli.Tests 1233, Netclaw.Daemon.Tests
832 - all green. slopwatch: 0 new violations (5 pre-existing SW004 warnings
in untouched files). Header verification clean.

Tasks 4.1-4.5 marked done in tasks.md. Tasks 4.6 (calibration), 4.7
(gold-set regression suite), 4.8 (flip P09), 4.9 (eval/skill sync) are
explicitly out of scope for this slice.
…rod-2026-07 sweep (memory-core-redesign task 4.6)

Local floor-calibration sweep (2026-07-05, floor_calibration.py, in
~/recall-research-local/2026-07/quant-eval/) swept MinCosineSimilarity
0.30-0.75 against the 1,216-doc production snapshot (2026-07-03) and the
93-query gold-prod-2026-07 gold set (33 positive / 60 zero-relevant),
using the fp32 ONNX production-faithful embedder replica.

Result: fp32 (snowflake-arctic-embed-m, the shipped model per D2) optimum
is 0.68 -- F0.5 0.141 vs 0.106 at the old 0.55 placeholder (+33%
relative), zero-injection accuracy 13.3% (8/60, up from 0%), mean injected
2.53 (down from 3.00). Moderate symmetric plateau: robust to +/-0.01
drift, ~29% relative F0.5 drop by +/-0.03. uint8 variant optimum is 0.67
(F0.5 0.153, zero-injection 16.7%) but remains informational only since
int8 is not shipped.

Caveat carried into design.md and the config doc comment: even at the
optimum, 83-87% of genuinely nothing-relevant queries still get something
injected. An absolute cosine floor alone cannot close the zero-injection
gap -- that residual is tracked under the separate memory-relevance-gate
change.

Changes:
- design.md D6: replace the 0.55-placeholder phrasing with the calibrated
  0.68 default, a compact per-variant calibration table, and the residual
  caveat with a pointer to memory-relevance-gate.
- design.md Open Questions: mark the MinCosineSimilarity default question
  resolved with the measured answer.
- MemoryConfig.cs: MinCosineSimilarity default 0.55 -> 0.68; doc comment
  now cites the calibration instead of calling it a placeholder.
- netclaw-config.v1.schema.json: default 0.55 -> 0.68, description updated.
- MemoryConfigDefaultsTests.cs: pinned default assertion updated to 0.68.
- SQLiteMemoryRecallHybridTests.cs: stale comment referencing the old
  0.55 default corrected to 0.68 (test behavior unaffected -- fixture
  uses cosine 0.0/1.0, both well clear of either threshold).
- tasks.md: task 4.6 ticked.

Gates: dotnet build Netclaw.slnx, dotnet test
src/Netclaw.Configuration.Tests, dotnet test src/Netclaw.Actors.Tests
--filter FullyQualifiedName~Recall, dotnet slopwatch analyze, and
Add-FileHeaders.ps1 -Verify all green.
…ip, policy parity + latency budget (memory-core-redesign tasks 4.7-4.8)

- Flip P09 back to expected-recall: wire a ScriptedEmbedder + real MemoryVectorIndex into the
  scenario coordinator for just this row (cosine 0.85 to M16's seeded embedding), keeping every
  other scenario on the pre-existing lexical-only coordinator.
- Gold-set MRR/precision@3 test over the labeled scenario table (positive scenarios only):
  measured MRR 1.000, precision@3 0.849; floors set at 0.90/0.75 (~10% headroom).
- Two zero-injection facts under a healthy embedder, including a lexically-strong-but-unembedded
  candidate (M07 vs the P01 query) to demonstrate the absolute cosine floor gates every
  candidate once a query vector exists, not just vector-sourced ones.
- Two policy-parity facts: a high-cosine (~0.9) secret-sensitivity document and a high-cosine
  wrong-audience document are both withheld end-to-end through RecallAsync.
- New latency budget test in Netclaw.Embeddings.Tests (D1 seam: Actors must not reference
  Embeddings) using the tiny fixture ONNX model: warms up once, asserts median of 10 short-query
  EmbedAsync calls is under the 150ms sub-budget.

Real finding: SQLiteMemoryRecallCoordinator.ScoreHybrid applies MinCosineSimilarity to EVERY
candidate once a query vector exists (missing embedding defaults to cosine 0.0), so a pure
lexical hit lacking an embedding row is unrecallable whenever the embedder is healthy -- this is
documented as deliberate in the coordinator's own docstring, not a bug, but it means wiring the
embedder across the whole gold-set table (rather than per-scenario) would have broken every
lexical-only scenario. Documented in the test file's class summary.
…acking out recall (memory-core-redesign slice 4)

Slice 4's ScoreHybrid applied the absolute MinCosineSimilarity floor to every
candidate once a query vector existed, defaulting an unembedded candidate's
cosine to 0.0 and then rejecting it via that same floor. That made any
unembedded document structurally unrecallable while the embedder was
healthy -- enabling embeddings on an un-backfilled corpus blacked out ALL
recall until gap repair completed, contradicting design.md's migration
plan ("both paths degrade loudly to lexical when coverage is incomplete
rather than misbehaving").

Fix: distinguish three cases per candidate.
  1. Embedded + cosine >= floor -> admitted (unchanged).
  2. Embedded + cosine < floor -> excluded (unchanged; the calibrated
     absolute floor still gates every candidate the index actually holds
     a vector for).
  3. No embedding row at all (a coverage gap) -> bypasses the floor,
     competes on fused/lexical score alone (cosine term 0). Emits a
     rate-limited memory_recall_coverage_gap warning (Debug when
     embeddings are disabled by config), following the same
     rate-limiting pattern as the existing memory_recall_vector_degraded
     log.

MemoryVectorIndex.TopK gains an out-parameter overload reporting every
item id the index holds an embedding for (regardless of cosine),
computed from the identical snapshot the returned matches were scored
against so case 2 vs. case 3 can never straddle a concurrent index
reload.

Test updates: inverted the MemoryRecallScenarioTests fact that pinned
the old (wrong) behavior, renaming it to reflect that an unembedded
strong lexical match IS now recalled; added SQLiteMemoryRecallHybridTests
coverage for case 2 exclusion, case 3 admission + log, and no-log on a
fully-covered corpus; fixed a zero-survivors test whose premise relied on
the old defaulting-to-0.0 behavior. P09 and the MRR/precision floors are
unaffected.

Docs: corrected the coordinator's docstring (previously claimed the
old behavior was deliberate) and added the three-case semantics to
design.md D6, plus a netclaw-memory skill update documenting the new
log event.
…ore-redesign task 4.9)

netclaw-memory skill 1.9.1 -> 1.10.0:
- new Hybrid Recall section: FTS union vector candidates, weighted fusion
  (VectorWeight 0.7 / LexicalWeight 0.3), absolute MinCosineSimilarity floor
  (0.68, calibrated on gold-prod-2026-07), RecencyHalfLifeDays 30
- zero-injection turns documented as normal and healthy (agent must not
  report absent [memory-recall] blocks as memory failure)
- explicit degradation guidance: memory_recall_vector_degraded (per-turn
  lexical fallback) and memory_recall_coverage_gap (per-candidate lexical
  fallback), both self-healing
- operator guidance: run 'netclaw memory backfill-embeddings' right after
  enabling embeddings on an existing corpus

Eval suite (Memory Pipeline category, qwen3:8b via old-gpu Ollama,
5 runs/case, threshold 0.80, image built from 7564384):
all 5 cases GREEN at 5/5 - memory_recall_active,
memory_identity_preference_routing, memory_explicit_store,
memory_checkpoint_enqueue, memory_recall_filters
(run baa87c10-4dc6-4de5-b117-87ad076e52e5, archived under evals/runs/).
No eval case changes needed: no case asserts an always-present
[memory-recall] block, and the new vector-degraded/coverage-gap event
names do not collide with the memory_recall_degraded assertion regex.
…e-gate sections 1-2)

Implements sections 1-2 of the memory-relevance-gate OpenSpec change: a
post-floor cross-encoder relevance gate on automatic recall, on top of
memory-core-redesign Slice 4's hybrid recall + cosine floor.

Section 1 (scorer, provisioning, config):
- IRelevanceScorer seam + UnavailableRelevanceScorer stub (Netclaw.Actors/Memory),
  mirroring IMemoryEmbedder's contract exactly.
- OnnxCrossEncoderScorer (Netclaw.Embeddings): manual [CLS] query [SEP] candidate
  [SEP] pair encoding (FastBertTokenizer has no native pair-encoding support),
  correct token_type_ids, only_second truncation with a proven never-overflow
  invariant, dynamic bucket-of-8 padding (reuses OnnxMemoryEmbedder.ComputeBucketedLength),
  host-side sigmoid.
- RelevanceModelManifestEntry + EmbeddingModelProvisioner.RelevanceAllowlist,
  pinned to Xenova/ms-marco-MiniLM-L-6-v2 model_quantized.onnx - verified
  byte-for-byte against the live HuggingFace artifact (sha256, byte size, and
  tokenizer_config.json max length all confirmed) before committing the pin.
- RelevanceScorerHolder (mirrors MemoryEmbedderHolder) that also carries the
  active model's manifest-calibrated threshold, since Netclaw.Actors cannot
  reference Netclaw.Embeddings' manifest type directly.
- Memory.Recall.RelevanceGate { Enabled, Threshold } (both nullable,
  follows-manifest/follows-embeddings semantics) + schema sync.

Section 2 (coordinator wiring, degradation, tests, eval):
- SQLiteMemoryRecallCoordinator: post-floor gate stage under a 60ms sub-budget,
  scores the top AutoRecallMaxItems floor survivors, drops below-threshold
  candidates, reuses the existing zero-injection path for an all-dropped turn.
- Loud degradation (gate disabled/no scorer/unavailable/sub-budget-exceeded) to
  floor-only unfiltered, rate-limited memory_recall_gate_degraded log matching
  memory_recall_vector_degraded's Debug/Warning split.
- MemoryRelevanceGateDoctorCheck (sibling to MemoryEmbeddingDoctorCheck).
- memory_retrieval_final gains droppedByGate + gateScores fields.
- Tiny fixture cross-encoder ONNX graph + generator script (multiplicative
  per-segment type scale, not additive, so the fixture actually proves
  token_type_ids assignment rather than merely their presence).
- Eval case (memory_relevance_gate_zero_injection): off-topic query against the
  seeded corpus asserts injectedCount=0 and the always-present droppedByGate
  marker (robust to whether the eval container has embeddings enabled).

Tests: 2666 (Actors) + 41 (Embeddings) + 467 (Configuration) + 835 (Daemon) +
1237 (Cli) all green. slopwatch: 0 new issues (baseline refreshed - 2 stale
pre-existing entries from unrelated prior file changes dropped, 1 new
justified entry added for a fake service's Task.Delay(InfiniteTimeSpan) used
to deterministically test the gate's sub-budget cancellation).
…e (memory-relevance-gate section 3)

- netclaw-memory skill (1.10.0 -> 1.11.0): relevance-gate guidance, gate log event
- runbook: relevance gate health section (doctor check, degradation log, gateScores/droppedByGate reading)
- design.md: calibration verification procedure (harness location, inputs, outputs)
- tasks.md: tick 3.1-3.3 (14/14 complete)
…tasks)

Arctic-embed-m's documented retrieval query prefix is unapplied in production,
forfeiting measured recall quality (F0.5 0.141 -> 0.239 achievable). Change
adds a query-vs-passage purpose seam, manifest-carried per-model retrieval
calibration, and the atomic floor recalibration (0.68 -> 0.24 prefixed).
Evidence: ~/recall-research-local/2026-07/arctic-prefix-eval/RESULTS.md
…calibration (memory-query-prefix)

Implements all tasks (sections 1-3) of the memory-query-prefix OpenSpec
change on top of memory-core-redesign Slices 2-4 and memory-relevance-gate.

- EmbeddingPurpose (Passage | RetrievalQuery) added to the IMemoryEmbedder
  seam; every call site updated explicitly (embed-on-write, backfill CLI,
  gap repair, dedup nominator -> Passage; recall coordinator ->
  RetrievalQuery). No optional parameter.
- EmbeddingModelManifestEntry gains QueryPrefix + CalibratedMinCosineSimilarity.
  Arctic fp32 entry pins the verified model-card prefix ("Represent this
  sentence for searching relevant passages: ") and CalibratedMinCosineSimilarity
  = 0.24. mxbai fallback entry's query prefix independently verified against
  its own HF model card (identical string, not copy-paste drift) with
  CalibratedMinCosineSimilarity left null (uncalibrated).
- OnnxMemoryEmbedder applies the manifest prefix only for RetrievalQuery,
  before tokenization; passage path is byte-identical, pinned by a
  regression test against the pre-change vector.
- MemoryEmbedderHolder now carries QueryPrefix/CalibratedMinCosineSimilarity
  alongside the embedder, mirroring RelevanceScorerHolder.CalibratedThreshold.
- MemoryRecallConfig.MinCosineSimilarity is now nullable; null follows the
  active model's manifest calibration, explicit value overrides it. Schema
  updated to type ["number","null"] with a description warning the value is
  model/encoding-specific.
- SQLiteMemoryRecallCoordinator resolves the effective floor per turn
  (override ?? manifest); missing calibration + no override degrades to
  lexical-only with a distinct "missing_calibration" reason via the existing
  rate-limited degraded-log mechanism. memory_retrieval_final now logs
  appliedFloor + floorSource (manifest|override|n/a).
- MemoryEmbeddingDoctorCheck reports active-model query-prefix presence and
  effective floor + source.
- memory-core-redesign design.md D6 gets a superseding note: 0.68 is the
  no-prefix historical record, 0.24 (prefixed) is the calibration of record.
- netclaw-memory skill bumped to 1.12.0: prefix is automatic per-model,
  floor follows the manifest by default, override semantics documented.

Gates: dotnet build clean; Actors (2669), Embeddings (47), Configuration
(467), Daemon (835), Cli (1240) test suites all green; slopwatch 0 issues;
file headers verified; eval suite (NETCLAW_EVAL_CATEGORY=Memory, qwen3:8b @
old-gpu:11434) 6/6 cases passed (100%).
…or calibration

Adds snowflake-arctic-embed-m-int8 (HF onnx/model_uint8.onnx, pinned at the
same commit as the fp32 entry, shared tokenizer) to
EmbeddingModelProvisioner.Allowlist and flips Memory.Embeddings.ModelId's
default to it.

Calibration (arctic-int8-prefix-eval, gold-prod-2026-07 + repooled-test,
same widened tau sweep methodology as the fp32 prefix calibration):
int8-with-prefix beats fp32-with-prefix on every axis -- F0.5 0.244 vs
0.239, recall@3 0.404 vs 0.318, zero-injection accuracy 28.3% vs 26.7% --
at ~1.7x inference speed and ~57% less steady-state RSS (already measured
in memory-core-redesign's quant-eval). CalibratedMinCosineSimilarity=0.24.

fp32 (snowflake-arctic-embed-m) and mxbai-embed-large-v1 remain allowlisted
as explicit operator choices.

Upgrade story: gap-repair (EmbeddingWarmupHostedService), the vector index,
and the curation nominator are all scoped by the active model id already,
so an existing install's fp32 vectors are left in place and the entire
corpus re-embeds under the new default automatically at next startup;
netclaw doctor's existing mixed-model-id warning covers the interim state
with its backfill --force recommendation. Added a dedicated test proving
a model-id switch retargets gap repair to the new id.

netclaw-memory skill bumped 1.12.0 -> 1.13.0 with an upgrade-behavior note.

Gates: build clean, Configuration/Embeddings/Daemon/Cli/Actors test suites
green, slopwatch 0 issues, copyright headers verified.
…dings

# Conflicts:
#	src/Netclaw.Actors.Tests/Memory/MemoryCurationEvaluatorParityTests.cs
#	src/Netclaw.Actors.Tests/SubAgents/SubAgentActorTests.cs
#	src/Netclaw.Actors.Tests/SubAgents/SubAgentSpawnerTests.cs
Second experimental prerelease in the memory-embeddings series. All
features gated behind Memory.Embeddings.Enabled (off by default).

Headline features:
- Hybrid semantic recall with an absolute relevance floor: FTS5 lexical
  + embedding-cosine candidate union, recency-decay score fusion, and a
  gold-set-calibrated minimum-similarity floor (zero-injection turns are
  normal and healthy)
- Cross-encoder relevance gate: 22 MB int8 ms-marco-MiniLM-L-6-v2
  reranker scores floor survivors and drops weak matches (86.8%
  zero-injection accuracy, 98.3% recall retention out-of-sample)
- Model-documented query prefix + manifest-carried calibration:
  arctic-embed retrieval-mode query prefix with per-model pinned floor
  calibration (F0.5 +73%, recall@3 2.8x on the production gold set)
- int8 arctic embedder as the new default model: pre-quantized
  model_uint8.onnx (105 MB vs 416 MB fp32, ~1.7x faster, better
  retrieval quality with the prefix)
…er boots the daemon

Both regressions were caught by the production canary of 0.25.0-alpha.onnx.2.

Bug 1: "memory" had a working mode handler (Program.cs `if (mode is "memory")`)
and was advertised in `netclaw --help`, but was missing from
CliArgsParser.KnownCommands. The parser classified `netclaw memory ...` as
Unknown before dispatch ever reached the handler, so
`netclaw memory backfill-embeddings` failed with "'memory' is not a netclaw
command." Fix: add "memory" to KnownCommands.

Closes the defect class: the KnownCommands completeness test previously just
duplicated the hardcoded set as its own "expected" value, so it could never
catch drift between KnownCommands and the real mode handlers/help text.
Replaced it with a test that derives the expected set directly from
Program.cs source (every `if (mode is "...")` dispatch token, union every
command listed in the `--help` "Commands:" section) and asserts KnownCommands
equals that union — both directions. Also added an explicit regression test
parsing `netclaw memory backfill-embeddings` / `netclaw memory --help` /
`netclaw memory -h` through CliArgsParser and asserting Known, not Unknown.

Bug 2: netclawd ignored `--version`/`-v` entirely and booted a full daemon
instance (acquiring the lock file, starting the host). Added
DaemonCliArgs.IsVersionRequest, checked before any directory creation, lock
acquisition, or host startup, so `netclawd --version` prints and exits
cleanly. Unit tested in isolation since Program.cs is top-level statements.

Version-banner finding: both netclawd's new handler and the CLI's existing
`netclaw --version` used BuildInfo.Version, which is the numeric
AssemblyVersion prefix and silently drops any prerelease suffix — a beta
build like "0.25.0-alpha.onnx.2" printed as plain "0.25.0", indistinguishable
from a stable release. Both now use BuildInfo.FullVersion.
…rived gate budget (canary finding) (netclaw-dev#1608)

Production canary caught two memory_recall_gate_degraded events
(score_failed:TaskCanceledException) in scheduled-reminder sessions waking
from an idle period. Root cause: cold ONNX sessions (paged-out weights) plus
host CPU contention at reminder-fire time pushed total turn latency (plan ->
candidate selection -> query embed -> hybrid fusion -> gate) past the entire
300ms RecallTimeoutMs envelope before the gate even started scoring, so its
own sub-budget timer never got a chance to fire on its own terms -- the
OUTER ct was already cancelled by the time ScoreAsync threw. Both events
show mode=hybrid (query embed succeeded, just slow), so this is a cold-start
problem across the whole pipeline, not a per-call latency regression against
design D5's reference-box measurement.

Re-verified the "partial candidate-scoring coverage gaps of 3-12%" prior
claim against the same canary log window: gapCandidates/totalCandidates in
the two events are 3/61 (4.9%) and 7/57 (12.3%) -- these are
memory_recall_coverage_gap counters (pre-existing candidate-pool coverage
gaps for un-backfilled documents, memory-core-redesign Slice 4 gap-repair
design D6), not gate failures. Confirmed: unrelated to this fix.

Three-part fix:

1. Keep-warm: EmbeddingWarmupHostedService now runs a periodic keep-warm
   loop (every 5 minutes, while embeddings are enabled) that re-exercises
   both ONNX sessions with a tiny embed + tiny 1-pair CE score, so an idle
   gap never lets either session's working set page out entirely. Built on
   PeriodicTimer over the injected TimeProvider (same virtualizable pattern
   McpReconnectionService already uses), never throws out of a tick
   (rate-limited Debug log on failure), skips whichever side is unavailable,
   no-ops when embeddings are disabled, and stops cleanly via the existing
   IHostedService.StopAsync/CancellationTokenSource pattern.

2. Budget: SQLiteMemoryRecallCoordinator's relevance-gate sub-budget is now
   min(RelevanceGateSubBudgetMs, time remaining in the outer RecallTimeoutMs
   envelope) instead of a fixed value; the ceiling itself is raised from
   60ms to 120ms. The outer linked CTS remains the hard cap, so a turn with
   headroom gets more slack than before, while a turn where earlier stages
   already consumed the envelope degrades immediately instead of assuming a
   fixed budget is always affordable. Same degraded-path semantics on
   timeout/failure.

3. Observability: memory_retrieval_final now logs gateElapsedMs (gate
   scoring latency regardless of outcome), and memory_recall_gate_degraded
   now logs elapsedMs too, so soak data can quantify margins against the new
   ceiling.

Updated openspec/changes/memory-relevance-gate/design.md's 60ms figures to
120ms (envelope-clamped) with a canary-finding note; the Open Questions
entry about combined sub-budget worst-case latency is marked resolved by
this same finding. netclaw-memory skill does not mention the 60ms figure, so
it is unchanged.

Tests: keep-warm tick fires on schedule / calls embedder+scorer exactly once
per tick / swallows scorer exceptions / skips unavailable sides / no ticks
when disabled / stops cleanly on cancellation (FakeTimeProvider-driven, no
sleeps). Budget: envelope-exhausted vs. envelope-with-headroom behavioral
tests using a real-delay fake scorer, proving the applied sub-budget is
smaller than the fixed ceiling when the outer envelope is nearly spent.
Updated the existing sub-budget-timeout test's stale "~60ms" comment.
Aaronontheweb and others added 13 commits July 9, 2026 14:58
…ng fails (netclaw-dev#1611)

When Memory.Embeddings.Enabled=true and either ONNX model (the embedder or
the ms-marco-minilm-l-6-v2 relevance/reranker model) fails to provision or
load, the daemon previously only logged memory_embedding_unavailable /
memory_relevance_gate_unavailable and left the failure to be discovered via
netclaw doctor or the health endpoint -- both pull-based. Operators had no
push notification that memory was running degraded.

Reuses the existing IOperationalNotificationSink/OperationalAlert seam
(Netclaw.Configuration) -- the same push-to-operator mechanism
McpReconnectionService, ReminderManagerActor, and RoutingChatClient already
use for MCP/reminder/provider degradation, wired to Slack/webhook targets by
WebhookNotificationService. Two new AlertType values
(MemoryEmbeddingModelUnavailable, MemoryRelevanceModelUnavailable). Each
alert carries the model id, the failure reason, the concrete consequence
(lexical-only recall/dedup, or an unfiltered relevance gate), and a
remediation hint (check network/disk, netclaw doctor, netclaw memory
backfill-embeddings where applicable) -- content mirrors the existing
MemoryEmbeddingDoctorCheck/MemoryRelevanceGateDoctorCheck wording.

Latched per model (Interlocked-guarded) so each model alerts at most once
per daemon run, not per retry. No alert when Embeddings.Enabled=false (an
intentional, not degraded, state). Deliberately did NOT wire the keep-warm
loop's mid-run failure (memory_embedding_keep_warm_failed) into the same
alert path -- a single keep-warm miss is a transient probe result the
method's own doc comment already calls out as not user-visible degradation,
and alerting on the first miss would false-positive on exactly that. Doing
it properly needs a consecutive-failure threshold (mirroring
ReminderManagerActor's auto-disable pattern), which is a design decision,
not just plumbing -- left as a follow-up.

Also fixes a pre-existing bug surfaced by testing the two-model-failure
case: WarmUpAsync returned early from the embedder's catch block, so
WarmUpRelevanceGateAsync was unreachable whenever the embedder itself
failed -- contradicting the method's own 'runs regardless' contract for the
relevance gate and silently suppressing the relevance-model alert in the
worst-case (both models down) scenario.

Tests: embedder-only failure, relevance-only failure, both-fail (two
distinct alerts), success path (no alerts), disabled config (no alerts),
and a latch test proving repeated WarmUpAsync calls don't refire. 25 tests
in EmbeddingWarmupHostedServiceTests, all green. Full Netclaw.Daemon.Tests
(854), Netclaw.Actors.Tests (2671), Netclaw.Embeddings.Tests (48), and
Netclaw.Configuration.Tests (467) green. Full solution build clean.

Updates netclaw-operations (2.25.0 -> 2.26.0, diagnostics reference) and
netclaw-memory (1.13.0 -> 1.14.0, Embeddings section) skills per the
constitution's skill-sync rule.
Operational alert on model provisioning failure (netclaw-dev#1611), embedder-failure
no longer blocks relevance model (netclaw-dev#1611), graceful daemon shutdown budget
(netclaw-dev#1612), --help no longer executes commands (netclaw-dev#1612). Memory eval 6/6
(run 6cc18657); code validated by full PR CI on netclaw-dev#1611/netclaw-dev#1612.
…fixes), prepare 0.25.0-alpha.onnx.5 (netclaw-dev#1619)

* fix: support Discord DM reminders (netclaw-dev#1609)

* Refactor ModelContextProtocol versioning in props file (netclaw-dev#1614)

Updated ModelContextProtocol package versions to use a variable for versioning.

Signed-off-by: Aaron Stannard <aaron@petabridge.com>

* fix: serialize Slack processing status updates (netclaw-dev#1556)

Co-authored-by: Aaron Stannard <aaron@petabridge.com>

* ci: run required checks for merge queue groups (netclaw-dev#1617)

* Bump MessagePack from 3.1.7 to 3.1.8 (netclaw-dev#1605)

---
updated-dependencies:
- dependency-name: MessagePack
  dependency-version: 3.1.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(cli): model set/picker preserve hand-set modalities on re-set (netclaw-dev#1127) (netclaw-dev#1610)

* fix(cli): model set/picker preserve hand-set modalities on re-set (netclaw-dev#1127)

Re-selecting a model that is already configured wiped operator-set attributes on it.
The write path rebuilt the Models[role] entry from scratch via ModelEntryWriter, which
only writes modalities it was handed (a probe result). Modalities have no CLI input and
can only be hand-edited, so a manual 'model set' (or a context-window tweak, or the TUI
picker re-selecting the same model) passed null and silently deleted a hand-set
InputModalities/OutputModalities — the concrete netclaw-dev#1127 loss.

Add ModelEntryWriter.WriteRole, a non-destructive persist: when the role already points
at the same (provider, modelId), preserve the existing modalities and context window the
caller did not supply; switching to a different model still starts clean (old attributes
belonged to the old model). Routed 'model set' and the TUI model manager through it.

Verified against the shipped binary: on stock beta 0.25.0, 'model set main <same-model>
--context-window N' wipes InputModalities; with this fix the same command preserves it.

No config-shape or schema change, so this is fully backwards compatible.

Tests: WriteRole_SameModelWithoutModalities_PreservesHandSetModalities,
WriteRole_DifferentModel_DropsPreviousModelModalities.

* fix(cli): make model-set metadata operator-owned; discovery never clobbers it

Hardens the non-destructive `model set`/picker rewrite (netclaw-dev#1127) against every issue
surfaced reviewing netclaw-dev#1610, and closes the loop on modality overrides.

ContextWindow and modalities are documented to "take precedence over provider-reported
capability detection", so they are now treated as operator-owned overrides with a single
precedence rule: explicit operator input > existing stored value > probe. A fresh probe
seeds a first-time set or a model switch but never overwrites a value already on disk.

Changes:

1. ContextWindow clamp preserved on same-model re-set. WriteRole takes the explicit
   --context-window and the probe default separately; the old callers collapsed them
   (`contextWindow ?? discovered`), so probe/picker paths always passed a non-null value
   and the operator's clamp was overwritten on every re-selection.

2. Modalities are no longer silently overwritten by discovery. Previously a probe that
   reported modalities replaced a stored override (the netclaw-dev#1127 loss's twin); now the stored
   value wins, matching the field's "manual override bypasses detection" contract.

3. Operators can change/remove those overrides. Since discovery no longer edits them, add
   `--input-modalities`, `--output-modalities`, and `--clear-modalities` to `model set`.
   Explicit set replaces the stored value; clear removes it (runtime detection resolves).
   Supplying any of them (like --context-window) skips the probe as manual configuration.

4. Corrupt/legacy existing entry no longer aborts the command. ReadSameModelEntry guards
   the deserialize (catch JsonException): an unreadable entry (e.g. an unrecognized modality
   enum string) degrades to "nothing to preserve" and the command overwrites/repairs it.

5. No false-match on ModelReference defaults. Provider/ModelId default to the stock
   local-ollama model, so an entry omitting either key deserialized to that default and
   would false-match a re-set of the stock model; preservation now requires both keys.

6. Provenance not downgraded. A same-model re-set that did not re-resolve the ID (no probe
   → Manual) keeps a previously discovered origin (Live/Defaults); only a fresh discovery
   updates it.

Tests: ModelEntryWriter unit coverage for each precedence path (clamp-over-probe, probe-
does-not-override-existing-modalities, explicit set, clear-over-probe, first-time seeding,
default-model false-match, corrupt-entry overwrite, provenance preserve/update) plus CLI
end-to-end coverage for the new flags. Full CLI suite green; slopwatch clean; model-manager
smoke tape passes.

* fix(cli): harden model-set overrides + add --clear-context-window (netclaw-dev#1610)

Addresses code-review findings on the non-destructive model-set change:

- probe gate: only --context-window short-circuits the probe; a modality flag
  no longer skips model-existence validation and context-window discovery
- preservation read: a corrupt modality enum string no longer discards a valid
  operator-owned ContextWindow (field-tolerant recovery)
- arg parsing: missing flag values and unknown args fail loudly instead of
  being silently dropped
- cleared modality is now sticky: discovery is hands-off once a same-model
  entry exists, so a later probe cannot resurrect a --clear-modalities removal
- TryParseModalities rejects raw numeric strings (named flags only)
- provenance: preserve a prior discovered origin on any non-Live re-set
  (was only guarding Manual)
- new --clear-context-window flag to force window re-detection (symmetry with
  --clear-modalities)

Also hardens LoadModelSelection: a corrupt/legacy config no longer crashes
`model set` (repairs it) or `model list` (reports it cleanly) or the TUI.

Generalizes ModalityOverride into a shared ValueOverride<T> tri-state.
Updates netclaw-operations skill (providers.md). Docs website tracked in
netclaw-dev/netclaw-website#83.

* feat(config): preserve model definitions across role switches

* fix(config): validate named model role references

* fix(cli): preserve models when editing providers

* chore(deps): bump dotnet-sdk from 10.0.300 to 10.0.301 (netclaw-dev#1381)

Bumps [dotnet-sdk](https://github.com/dotnet/sdk) from 10.0.300 to 10.0.301.
- [Release notes](https://github.com/dotnet/sdk/releases)
- [Commits](dotnet/sdk@v10.0.300...v10.0.301)

---
updated-dependencies:
- dependency-name: dotnet-sdk
  dependency-version: 10.0.301
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(subagents): fail closed for unattended approvals (netclaw-dev#1616)

* chore(release): prepare 0.25.0-alpha.onnx.5 experimental prerelease

---------

Signed-off-by: Aaron Stannard <aaron@petabridge.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: petabridge-netclaw[bot] <289234546+petabridge-netclaw[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…, prepare 0.25.0-alpha.onnx.6 (netclaw-dev#1642)

* fix: support Discord DM reminders (netclaw-dev#1609)

* Refactor ModelContextProtocol versioning in props file (netclaw-dev#1614)

Updated ModelContextProtocol package versions to use a variable for versioning.

Signed-off-by: Aaron Stannard <aaron@petabridge.com>

* fix: serialize Slack processing status updates (netclaw-dev#1556)

Co-authored-by: Aaron Stannard <aaron@petabridge.com>

* ci: run required checks for merge queue groups (netclaw-dev#1617)

* Bump MessagePack from 3.1.7 to 3.1.8 (netclaw-dev#1605)

---
updated-dependencies:
- dependency-name: MessagePack
  dependency-version: 3.1.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(cli): model set/picker preserve hand-set modalities on re-set (netclaw-dev#1127) (netclaw-dev#1610)

* fix(cli): model set/picker preserve hand-set modalities on re-set (netclaw-dev#1127)

Re-selecting a model that is already configured wiped operator-set attributes on it.
The write path rebuilt the Models[role] entry from scratch via ModelEntryWriter, which
only writes modalities it was handed (a probe result). Modalities have no CLI input and
can only be hand-edited, so a manual 'model set' (or a context-window tweak, or the TUI
picker re-selecting the same model) passed null and silently deleted a hand-set
InputModalities/OutputModalities — the concrete netclaw-dev#1127 loss.

Add ModelEntryWriter.WriteRole, a non-destructive persist: when the role already points
at the same (provider, modelId), preserve the existing modalities and context window the
caller did not supply; switching to a different model still starts clean (old attributes
belonged to the old model). Routed 'model set' and the TUI model manager through it.

Verified against the shipped binary: on stock beta 0.25.0, 'model set main <same-model>
--context-window N' wipes InputModalities; with this fix the same command preserves it.

No config-shape or schema change, so this is fully backwards compatible.

Tests: WriteRole_SameModelWithoutModalities_PreservesHandSetModalities,
WriteRole_DifferentModel_DropsPreviousModelModalities.

* fix(cli): make model-set metadata operator-owned; discovery never clobbers it

Hardens the non-destructive `model set`/picker rewrite (netclaw-dev#1127) against every issue
surfaced reviewing netclaw-dev#1610, and closes the loop on modality overrides.

ContextWindow and modalities are documented to "take precedence over provider-reported
capability detection", so they are now treated as operator-owned overrides with a single
precedence rule: explicit operator input > existing stored value > probe. A fresh probe
seeds a first-time set or a model switch but never overwrites a value already on disk.

Changes:

1. ContextWindow clamp preserved on same-model re-set. WriteRole takes the explicit
   --context-window and the probe default separately; the old callers collapsed them
   (`contextWindow ?? discovered`), so probe/picker paths always passed a non-null value
   and the operator's clamp was overwritten on every re-selection.

2. Modalities are no longer silently overwritten by discovery. Previously a probe that
   reported modalities replaced a stored override (the netclaw-dev#1127 loss's twin); now the stored
   value wins, matching the field's "manual override bypasses detection" contract.

3. Operators can change/remove those overrides. Since discovery no longer edits them, add
   `--input-modalities`, `--output-modalities`, and `--clear-modalities` to `model set`.
   Explicit set replaces the stored value; clear removes it (runtime detection resolves).
   Supplying any of them (like --context-window) skips the probe as manual configuration.

4. Corrupt/legacy existing entry no longer aborts the command. ReadSameModelEntry guards
   the deserialize (catch JsonException): an unreadable entry (e.g. an unrecognized modality
   enum string) degrades to "nothing to preserve" and the command overwrites/repairs it.

5. No false-match on ModelReference defaults. Provider/ModelId default to the stock
   local-ollama model, so an entry omitting either key deserialized to that default and
   would false-match a re-set of the stock model; preservation now requires both keys.

6. Provenance not downgraded. A same-model re-set that did not re-resolve the ID (no probe
   → Manual) keeps a previously discovered origin (Live/Defaults); only a fresh discovery
   updates it.

Tests: ModelEntryWriter unit coverage for each precedence path (clamp-over-probe, probe-
does-not-override-existing-modalities, explicit set, clear-over-probe, first-time seeding,
default-model false-match, corrupt-entry overwrite, provenance preserve/update) plus CLI
end-to-end coverage for the new flags. Full CLI suite green; slopwatch clean; model-manager
smoke tape passes.

* fix(cli): harden model-set overrides + add --clear-context-window (netclaw-dev#1610)

Addresses code-review findings on the non-destructive model-set change:

- probe gate: only --context-window short-circuits the probe; a modality flag
  no longer skips model-existence validation and context-window discovery
- preservation read: a corrupt modality enum string no longer discards a valid
  operator-owned ContextWindow (field-tolerant recovery)
- arg parsing: missing flag values and unknown args fail loudly instead of
  being silently dropped
- cleared modality is now sticky: discovery is hands-off once a same-model
  entry exists, so a later probe cannot resurrect a --clear-modalities removal
- TryParseModalities rejects raw numeric strings (named flags only)
- provenance: preserve a prior discovered origin on any non-Live re-set
  (was only guarding Manual)
- new --clear-context-window flag to force window re-detection (symmetry with
  --clear-modalities)

Also hardens LoadModelSelection: a corrupt/legacy config no longer crashes
`model set` (repairs it) or `model list` (reports it cleanly) or the TUI.

Generalizes ModalityOverride into a shared ValueOverride<T> tri-state.
Updates netclaw-operations skill (providers.md). Docs website tracked in
netclaw-dev/netclaw-website#83.

* feat(config): preserve model definitions across role switches

* fix(config): validate named model role references

* fix(cli): preserve models when editing providers

* chore(deps): bump dotnet-sdk from 10.0.300 to 10.0.301 (netclaw-dev#1381)

Bumps [dotnet-sdk](https://github.com/dotnet/sdk) from 10.0.300 to 10.0.301.
- [Release notes](https://github.com/dotnet/sdk/releases)
- [Commits](dotnet/sdk@v10.0.300...v10.0.301)

---
updated-dependencies:
- dependency-name: dotnet-sdk
  dependency-version: 10.0.301
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(subagents): fail closed for unattended approvals (netclaw-dev#1616)

* Release 0.25.0-beta.3: update release notes and version metadata (netclaw-dev#1618)

* Add user-written `AGENTS.md` for application-specific agent guidelines (netclaw-dev#1622)

* Add deployment agent mission playbook

* Keep identity routing in embedded guidance

* Evaluate embedded identity routing

* Prioritize specialized subagent guidance

* test: skip SearXNG container test on Windows (netclaw-dev#1625)

* Use logical skill access and authoritative inventory refresh (netclaw-dev#1634)

* feat(skills): use logical skill access

* docs(evals): restore README

* test(skills): use root-preserving path joins

* Preserve Git working context across sessions and subagents (netclaw-dev#1630)

* feat: preserve git context across subagents

* fix: make subagent git context deterministic

* test: make fixture path intent explicit

* Stabilize config search screenshots (netclaw-dev#1635)

* Simplify STDIO MCP process ownership (netclaw-dev#1636)

* chore: bump Netclaw.SkillClient from 0.4.0-beta.4 to 0.4.0 stable (netclaw-dev#1638)

* fix(memory): stop curation dedup from overwriting existing documents (netclaw-dev#1637)

When a curation Create decision landed on an anchor that already had a
document, both batch appliers reused the existing document_id, and the
ON CONFLICT DO UPDATE overwrote that document's title, body, and
classification with the new proposal. The old content was lost; there
is no history table to recover it from.

Now a Create collision appends the new content below a dated separator
and keeps the existing title, boundary, audience, and sensitivity. If
the incoming content is already present verbatim, the write is skipped.
Consolidate decisions carry an explicit target document id, so they
keep replacing near-duplicates as designed. Update decisions and
no-collision inserts are unchanged.

* Release 0.25.0-beta.4: update release notes and version metadata (netclaw-dev#1640)

---------

Signed-off-by: Aaron Stannard <aaron@petabridge.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: petabridge-netclaw[bot] <289234546+petabridge-netclaw[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…xtensions.AI 10.8.0 floor

Dependabot bumped Microsoft.Extensions.AI 10.6.0 -> 10.8.0 (upstream netclaw-dev#1650) without
updating the transitively-required System.Numerics.Tensors floor (now >= 10.0.10),
leaving upstream/dev's own build broken via NU1109 package downgrade. Re-pins to
10.0.10, matching $(MicrosoftAspNetCoreVersion) per the existing comment policy.
…-alpha.onnx.7

onnx.7: sync dev post-beta.4 (tool pipeline refactor, webhook HMAC), prepare 0.25.0-alpha.onnx.7
….onnx.8

# Conflicts:
#	Directory.Build.props
#	RELEASE_NOTES.md
#	feeds/skills/.system/files/netclaw-operations/SKILL.md
#	feeds/skills/.system/files/netclaw-operations/references/diagnostics.md
#	src/Netclaw.Cli/Daemon/DaemonManager.cs
#	src/Netclaw.Configuration.Tests/DaemonConfigTests.cs
#	src/Netclaw.Configuration/DaemonConfig.cs
#	src/Netclaw.Daemon.Tests/DaemonShutdownConfigurationTests.cs
#	src/Netclaw.Daemon/DaemonShutdownConfiguration.cs
#	src/Netclaw.Daemon/Program.cs
…repare 0.25.0-alpha.onnx.8 (netclaw-dev#1695)

* release: prepare 0.25.0-beta.5 (netclaw-dev#1666)

* fix(daemon): bound the daemon-stop session drain and give CLI stop kill headroom (netclaw-dev#1673)

Two related shutdown-race defects:

1. The SIGTERM/daemon-stop CoordinatedShutdown drain task called
   SessionDrainHelper.DrainAsync with CancellationToken.None for the
   operation token, so a session parked on interactive tool approval
   (which can never ack) hung the call for the full 200s Akka
   before-service-unbind phase timeout, leaking the abandoned drain
   task. Fixed by bounding the drain with a TimeProvider-driven CTS
   sized to DaemonConfig.BoundedDrainTimeout (GracefulShutdownBudget
   minus a 10s safety margin), so it always finishes before the phase
   timeout fires. The drain's existing timeout logging now also lists
   which sessions didn't drain.

2. `netclaw daemon stop` waited only 10s before force-killing, and
   even after fixing that to match the daemon's own 200s phase
   timeout, the CLI and daemon would race exactly at the boundary
   (production evidence: a daemon force-killed ~100ms from a clean
   exit). Fixed by giving the CLI a 15s grace window to poll for exit
   after its budget elapses before escalating to SIGKILL, and adding
   TimeoutStopSec= to the generated systemd unit so systemd itself
   can't SIGKILL the cgroup out from under a still-waiting
   `netclaw daemon stop` (ExecStop=).

All the shutdown-timing constants now live in one place
(DaemonConfig.GracefulShutdownBudget and friends) so the layering
(bounded drain < Akka phase timeout < CLI budget+grace <
systemd TimeoutStopSec) can't drift out of lockstep again.

Fixes netclaw-dev#1664, netclaw-dev#1665

* fix(cli): reject numeric model modalities (netclaw-dev#1677)

* fix(cli): handle model migration errors without crashing (netclaw-dev#1678)

* fix(cli): handle model migration validation errors

* test(configuration): use non-resetting path joins

* fix(cli): report unresolved model references cleanly (netclaw-dev#1680)

* chore(deps): bump actions/setup-dotnet from 5 to 6 (netclaw-dev#1684)

Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 5 to 6.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](actions/setup-dotnet@v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* test(tools): remove obsolete shell timeout race (netclaw-dev#1688)

* Fix authoritative attachment path guidance (netclaw-dev#1686)

* fix(channels): clarify authoritative attachment paths

* chore: remove unnecessary openspec artifacts

* test(evals): cover authoritative attachment paths

* test(evals): stage attachment path fixture

* test(sessions): register session pipeline in shared fixture (netclaw-dev#1690)

* test(sessions): make tool result assertion deterministic

* test(sessions): restore tool result regression coverage

* fix(approvals): show MCP arguments in prompts (netclaw-dev#1689)

* fix(approvals): show MCP arguments in prompts

* fix(approvals): harden MCP invocation previews

* feat(install): automate shell PATH integration (netclaw-dev#1687)

* feat(install): automate shell PATH integration for Unix installers

Replace manual PATH instructions with automatic shell profile modification.
The installer now detects the user's shell and writes a self-guarding env
script (~/.netclaw/env) that is sourced from the appropriate RC file.

Key changes:
- Add --skip-shell flag to opt out of automatic shell modification
- Detect shell via $SHELL (bash, zsh, fish supported; others get manual instructions)
- Write ~/.netclaw/env with colon-affixed case guard (rustup/fzf pattern)
- Append source line to correct RC: ~/.bashrc (Linux), ~/.profile (macOS bash),
  ~/.zshrc (zsh with ZDOTDIR support), ~/.config/fish/conf.d/netclaw.fish (fish)
- Duplicate prevention: grep -xF guard before appending to RC
- RC file validated with bash -n after modification

Smoke tests (section 9):
- Separate tests for bash-linux, zsh, and fish RC modification
- --skip-shell flag: verify no RC/env file created
- Unknown shell: verify graceful fallback message
- ZDOTDIR: verify zsh respects ZDOTDIR for RC location
- Duplicate prevention: second install adds no extra source lines

* feat(install): automate PATH modification for Windows installer

Replace manual PATH instructions with automatic User-scope PATH modification.
The installer now prepends the install dir to the User PATH and broadcasts
WM_SETTINGCHANGE to Explorer so new terminal windows pick up the change.

Key changes:
- Add -SkipShell switch to opt out of automatic PATH modification
- Read User-scope PATH (not merged $env:PATH) to avoid Machine entry corruption
- Prepend install dir to User PATH (highest priority)
- Update current session's $env:PATH
- Broadcast WM_SETTINGCHANGE via P/Invoke SendMessageTimeout
- Guard against duplicates: normalize trailing backslash before comparison
- Check 32K character limit before writing, warn if near limit

Smoke tests:
- PATH automation logic: duplicate detection with fake PATH strings
- Trailing backslash normalization
- Null User PATH handled as empty
- -SkipShell flag: verify install completes with skipped message

* fix(smoke): isolate real install test to temp HOME

The real install test now uses a temp HOME directory so shell integration
writes to the temp dir instead of the CI runner's real profile. This also
lets us verify the env script was created and the RC file was modified.

Adds 2 new assertions: env script exists, RC file sources env script.

* fix(smoke): cross-platform install smoke test fixes

- bash test: select .profile on macOS (login shell) vs .bashrc on Linux
- fish test: unset XDG_CONFIG_HOME so config path stays within temp HOME
- fish test: add duplicate prevention and re-install assertions
- Windows: remove false-positive regex check on success message

* fix(smoke): use @() array coercion for empty PATH entries count

$entries4 is a 0-element array which PowerShell can coerce to $null in
some contexts, causing '$entries4.Count' to throw ParentContainsErrorRecordException.
$(@entries4).Count forces array coercion so .Count always works.

* fix(install): validate PATH integration through real shells

* fix(install): make manifest fallback portable

* Harden installer PATH mutations

* fix(smoke): compare persisted physical paths

* fix(installer): harden shell path mutation

* test(installer): support Windows PowerShell path joins

* test(installer): normalize manifest fixture encoding

* test(installer): capture expected PowerShell failures

* release: prepare 0.25.0 stable (netclaw-dev#1692)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Resolve conflicts in Directory.Build.props, RELEASE_NOTES.md, and
netclaw-operations SKILL.md — all taking dev's current version.
Copilot AI review requested due to automatic review settings August 4, 2026 00:03
All four slices (embedding foundation, kNN/L curation, hybrid recall,
relevance gate) are now shipping together — the default-off stance from
Slice 2 was a staging guardrail, not a permanent posture. Gap-repair runs
on every daemon startup via EmbeddingWarmupHostedService, and the doctor
check already surfaces model health and backfill gaps.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a local ONNX-based semantic memory stack to Netclaw.
It adds model provisioning, embed-on-write, kNN nomination, hybrid recall, and a cross-encoder relevance gate.

Changes:

  • Add Netclaw.Embeddings and Netclaw.Embeddings.Tests, plus a local latency bench tool.
  • Wire embeddings, vector index, and relevance gate into the daemon, CLI, doctor checks, status, and eval harness.
  • Add config/schema updates, new runbook guidance, and new regression tests for CLI help and daemon flags.

Reviewed changes

Copilot reviewed 115 out of 118 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tools/embed-latency-bench/embed-latency-bench.csproj Add an opt-in latency bench project.
src/Netclaw.Embeddings/Netclaw.Embeddings.csproj Add the embeddings runtime assembly project.
src/Netclaw.Embeddings.Tests/Netclaw.Embeddings.Tests.csproj Add the embeddings test project and fixtures.
src/Netclaw.Embeddings.Tests/LocalArtifactServer.cs Add a local HTTP server for provisioner tests.
src/Netclaw.Embeddings.Tests/Fixtures/tiny-vocab.txt Add a tiny WordPiece vocab fixture.
src/Netclaw.Embeddings.Tests/Fixtures/tiny-cross-encoder-vocab.txt Add a tiny cross-encoder vocab fixture.
src/Netclaw.Embeddings.Tests/Fixtures/generate_fixture_model.py Add a script to regenerate fixture ONNX graphs.
src/Netclaw.Embeddings.Tests/EmbedQueryLatencyBudgetTests.cs Add a query embed latency regression guard test.
src/Netclaw.Embeddings.Tests/BoundedConcurrencyGateTests.cs Add tests for bounded embed concurrency gating.
src/Netclaw.Daemon/Services/MemoryCurationWorkerService.cs Add embed-on-write after checkpoint curation writes.
src/Netclaw.Daemon/Program.cs Register embedder, index, relevance scorer, and warmup services.
src/Netclaw.Daemon/Netclaw.Daemon.csproj Reference Netclaw.Embeddings from the daemon.
src/Netclaw.Daemon/Gateway/DaemonRuntimeStatusService.cs Add memory embeddings status to daemon status output.
src/Netclaw.Daemon/DaemonCliArgs.cs Add a testable netclawd --version predicate.
src/Netclaw.Daemon.Tests/Netclaw.Daemon.Tests.csproj Link embeddings fixtures into daemon tests.
src/Netclaw.Daemon.Tests/Gateway/DaemonRuntimeStatusServiceTests.cs Add tests for embeddings status reporting.
src/Netclaw.Daemon.Tests/DaemonCliArgsTests.cs Add regression tests for daemon version args.
src/Netclaw.Configuration/Schemas/netclaw-config.v1.schema.json Add memory embeddings, curation, recall, and gate schema.
src/Netclaw.Configuration/OperationalAlert.cs Add alert types for model provisioning failures.
src/Netclaw.Configuration/NetclawPaths.cs Add a models directory under the Netclaw home.
src/Netclaw.Configuration/DaemonRuntimeStatus.cs Add a wire type for embeddings status.
src/Netclaw.Configuration.Tests/MemoryConfigDefaultsTests.cs Add default “bear trap” tests for memory knobs.
src/Netclaw.Cli/Webhooks/WebhooksCommand.cs Fix trailing help token behavior for webhooks verbs.
src/Netclaw.Cli/Reminder/ReminderCommand.cs Fix trailing help token behavior for reminder verbs.
src/Netclaw.Cli/Program.cs Add netclaw memory command and daemon help gating.
src/Netclaw.Cli/Netclaw.Cli.csproj Reference Netclaw.Embeddings from the CLI.
src/Netclaw.Cli/Doctor/MemoryRelevanceGateDoctorCheck.cs Add a doctor check for the relevance gate model.
src/Netclaw.Cli/Doctor/MemoryEmbeddingDoctorCheck.cs Add a doctor check for embedding model and coverage.
src/Netclaw.Cli/Doctor/DoctorRegistrationExtensions.cs Register the new memory doctor checks and allowlists.
src/Netclaw.Cli/Daemon/DaemonManager.cs Make graceful stop wait testable via TimeProvider.
src/Netclaw.Cli/Daemon/DaemonCommandDispatch.cs Add unit-testable daemon help dispatch logic.
src/Netclaw.Cli/CliArgsParser.cs Add a shared “trailing help token” scan helper.
src/Netclaw.Cli.Tests/Webhooks/WebhooksCommandTests.cs Add tests for the webhooks trailing help fix.
src/Netclaw.Cli.Tests/Reminder/ReminderCommandTests.cs Add tests for the reminder trailing help fix.
src/Netclaw.Cli.Tests/Netclaw.Cli.Tests.csproj Link embeddings fixtures into CLI tests.
src/Netclaw.Cli.Tests/Doctor/MemoryRelevanceGateDoctorCheckTests.cs Add tests for relevance gate doctor check branches.
src/Netclaw.Cli.Tests/Doctor/ConfigSchemaDoctorCheckTests.cs Extend schema doctor tests for new memory config shapes.
src/Netclaw.Cli.Tests/Cli/DaemonManagerGracefulShutdownTests.cs Add tests for graceful stop and unit timeout sync.
src/Netclaw.Cli.Tests/Cli/DaemonCommandDispatchTests.cs Add tests for daemon trailing help gating.
src/Netclaw.Actors/Sessions/SessionDependencies.cs Add embedder and index holders to session memory services.
src/Netclaw.Actors/Sessions/LlmSessionActor.cs Wire curation actor with config and optional embed deps.
src/Netclaw.Actors/Sessions/DeterministicCandidateSelector.cs Expose candidate scoring for hybrid recall use.
src/Netclaw.Actors/Netclaw.Actors.csproj Add tensors dependency for cosine and vector indexing.
src/Netclaw.Actors/Memory/RelevanceScorerHolder.cs Add holder for relevance scorer plus calibrated threshold.
src/Netclaw.Actors/Memory/MemoryVectorIndexHolder.cs Add holder that builds and refreshes the vector index.
src/Netclaw.Actors/Memory/MemoryEmbedOnWriteCoordinator.cs Add shared embed-on-write coordinator logic.
src/Netclaw.Actors/Memory/MemoryEmbedderHolder.cs Add holder for embedder plus prefix and floor calibration.
src/Netclaw.Actors/Memory/MemoryCurationPipeline.cs Wire curation engine with config and optional embed deps.
src/Netclaw.Actors/Memory/MemoryContentHasher.cs Add normalized content hashing for embed skip logic.
src/Netclaw.Actors/Memory/IRelevanceScorer.cs Add relevance scoring seam and unavailable stub.
src/Netclaw.Actors/Memory/IMemoryEmbedder.cs Add embedding seam with purpose enum and unavailable stub.
src/Netclaw.Actors/Memory/CurationRulesEvaluator.cs Extend candidate and decision types and share normalization.
src/Netclaw.Actors.Tests/Sessions/SidecarSessionCorrelationTests.cs Update tests for new curation evaluator signature.
src/Netclaw.Actors.Tests/Sessions/SessionMemoryObserverStorageIntegrationTests.cs Update curation actor props usage in tests.
src/Netclaw.Actors.Tests/Sessions/LlmSessionIntegrationTests.cs Update recall coordinator construction in tests.
src/Netclaw.Actors.Tests/Sessions/DeterministicRetrievalPlanningTests.cs Update recall coordinator construction in tests.
src/Netclaw.Actors.Tests/Memory/UnavailableRelevanceScorerTests.cs Add tests for unavailable relevance scorer contract.
src/Netclaw.Actors.Tests/Memory/UnavailableMemoryEmbedderTests.cs Add tests for unavailable embedder contract.
src/Netclaw.Actors.Tests/Memory/MemoryVectorIndexTests.cs Add tests for index reload and cosine TopK behavior.
src/Netclaw.Actors.Tests/Memory/MemoryRedesignedEvalSuiteTests.cs Update eval suite tests for new recall coordinator ctor.
src/Netclaw.Actors.Tests/Memory/MemoryEvalSeedSuiteTests.cs Update seed suite tests for new recall coordinator ctor.
src/Netclaw.Actors.Tests/Memory/MemoryContentHasherTests.cs Add tests for hash normalization and stability.
src/Netclaw.Actors.Tests/Memory/CurationPromptBuilderTests.cs Add merged-body parsing and full-candidate rendering tests.
openspec/changes/memory-relevance-gate/tasks.md Mark relevance gate change tasks complete.
openspec/changes/memory-query-prefix/tasks.md Add tasks for query prefix and floor calibration workflow.
openspec/changes/memory-query-prefix/specs/netclaw-agent-memory/spec.md Add delta spec text for prefix and floor semantics.
openspec/changes/memory-query-prefix/specs/memory-embeddings/spec.md Add delta spec for purpose-based embedding and calibration.
openspec/changes/memory-query-prefix/proposal.md Add proposal for query prefix and manifest floor approach.
openspec/changes/memory-query-prefix/design.md Add design details and calibration record for prefix change.
openspec/changes/memory-query-prefix/.openspec.yaml Add OpenSpec change metadata.
Netclaw.slnx Add embeddings projects to the solution.
feeds/skills/.system/files/netclaw-operations/SKILL.md Add ops guidance for embeddings backfill and doctor checks.
feeds/skills/.system/files/netclaw-operations/references/diagnostics.md Add alert symptom row for memory model failures.
evals/run-evals.sh Add a zero-injection case for off-topic recall.
docs/runbooks/memory-health-and-evals.md Add runbook section for relevance gate health checks.
Directory.Packages.props Pin ONNX runtime, tokenizer, and tensor packages centrally.
CONTRIBUTING.md Clarify prerelease notes header format.
.slopwatch/baseline.json Update baseline and add new delay-related entries.
.github/workflows/publish_release_binaries.yml Add ARM64 binary architecture verification step.
Suppressed comments (1)

src/Netclaw.Configuration/Schemas/netclaw-config.v1.schema.json:433

  • Memory.Curation.NominatorSimilarityThreshold and NominatorK schema descriptions say "Not yet consumed". The current code consumes both values in MemoryCurationEvaluator. Update the descriptions to avoid misleading operators.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +41 to +57
private async Task ServeLoopAsync()
{
while (true)
{
HttpListenerContext ctx;
try
{
ctx = await _listener.GetContextAsync().ConfigureAwait(false);
}
catch
{
return; // listener stopped/disposed — end the loop
}

_ = HandleAsync(ctx);
}
}
Comment on lines 13 to 20
internal sealed class MemoryCurationWorkerService(
SQLiteMemoryStore store,
MemoryCurationEngine engine,
TimeProvider timeProvider,
ILogger<MemoryCurationWorkerService> logger,
ISessionMetrics? metrics = null) : IHostedService, IDisposable
ISessionMetrics? metrics = null,
MemoryEmbedderHolder? embedderHolder = null) : IHostedService, IDisposable
{
Comment on lines +398 to +402
"Enabled": {
"type": "boolean",
"default": false,
"description": "When true, the daemon provisions the embedding model at startup and computes embeddings on memory writes. Defaults to false: this slice only writes vectors, nothing consumes them yet."
},
Comment on lines +165 to +171
# Check architecture with `file` command
file_output=$(file "$binary")
if ! echo "$file_output" | grep -q "ARM aarch64"; then
echo "ERROR: Binary $binary is not ARM64:" >&2
echo " $file_output" >&2
exit 1
fi
Copilot AI review requested due to automatic review settings August 4, 2026 00:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 115 out of 118 changed files in this pull request and generated 1 comment.

Comment on lines +19 to +24
[Fact]
public void Embeddings_disabled_by_default()
{
var config = new MemoryConfig();
Assert.False(config.Embeddings.Enabled);
}
…ault

Match the MemoryEmbeddingsConfig.Enabled default change from false to true.
Copilot AI review requested due to automatic review settings August 4, 2026 00:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 115 out of 118 changed files in this pull request and generated no new comments.

Suppressed comments (5)

src/Netclaw.Configuration.Tests/MemoryConfigDefaultsTests.cs:22

  • Memory.Embeddings.Enabled must default to false for this merge. The PR description and the schema set the default to false, but this test asserts true. A true default can trigger model provisioning and a large RSS increase on upgrade.
    src/Netclaw.Configuration/Schemas/netclaw-config.v1.schema.json:425
  • The schema says this setting is "Not yet consumed", but MemoryCurationConfig and the curation evaluator use it for the kNN nominator in this PR.
    src/Netclaw.Configuration/Schemas/netclaw-config.v1.schema.json:432
  • The schema says this setting is "Not yet consumed", but the kNN nominator uses it to limit nominees in this PR.
    src/Netclaw.Configuration/Schemas/netclaw-config.v1.schema.json:401
  • This description says "this slice only writes vectors, nothing consumes them yet". This PR wires hybrid recall and a relevance gate, so the statement is no longer correct.

This issue also appears in the following locations of the same file:

  • line 425
  • line 432
    src/Netclaw.Cli/Doctor/MemoryRelevanceGateDoctorCheck.cs:55
  • This check treats Memory.Recall.RelevanceGate.Enabled=true as active even when Memory.Embeddings.Enabled=false. The recall coordinator only runs the relevance gate in hybrid mode, so this configuration can never make the gate run. The check should warn the operator instead of requiring a provisioned relevance model that the daemon will not load.

…ownload is true

When Memory.Embeddings.AutoDownload is true, the daemon provisions models
on next startup — this is a self-healing condition. The doctor should
report it as a Warning (daemon will fix it), not an Error (operator must
act). Error is now reserved for the AutoDownload=false case where the
operator must manually provision models.

Same pattern applied to both MemoryEmbeddingDoctorCheck and
MemoryRelevanceGateDoctorCheck. Tests split to cover both branches.
Copilot AI review requested due to automatic review settings August 4, 2026 01:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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