Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<VersionPrefix>0.25.0</VersionPrefix>
<VersionSuffix>beta.1</VersionSuffix>
<VersionSuffix>beta.2</VersionSuffix>
<PackageReleaseNotes>Netclaw v0.25.0-beta.1 — SkillServer native sub-agent sync, memory curation unification, systemd PATH fix

**Features**
Expand Down
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,14 @@
<PackageVersion Include="SlackNet" Version="$(SlackNetVersion)" />
<PackageVersion Include="SlackNet.Extensions.DependencyInjection" Version="$(SlackNetVersion)" />
<PackageVersion Include="Cronos" Version="0.13.0" />
<PackageVersion Include="Netclaw.SkillClient" Version="0.4.0-beta.1" />
<PackageVersion Include="Netclaw.SkillClient" Version="0.4.0-beta.4" />
<PackageVersion Include="ShellSyntaxTree" Version="0.1.5" />
<PackageVersion Include="Termina" Version="0.15.0" />
</ItemGroup>
<!-- Serialization -->
<ItemGroup>
<PackageVersion Include="Google.Protobuf" Version="3.35.1" />
<PackageVersion Include="Grpc.Tools" Version="2.81.1" />
<PackageVersion Include="Grpc.Tools" Version="2.82.0" />
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
</ItemGroup>
<!-- Test dependencies -->
Expand Down
9 changes: 9 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# NetClaw Release Notes

## 0.25.0-beta.2 (2026-07-07)

### Bug Fixes
- **UTF-8 BOM in skill frontmatter** — Fixed: skill scanner now strips UTF-8 BOM (`\uFEFF`) before parsing YAML frontmatter, and populates `SkillName` on all `SkillScanIssue` records so degenerate frontmatter no longer crashes the scan ([#1583](https://github.com/netclaw-dev/netclaw/pull/1583))
- **Model capability provenance logging** — Fixed: daemon now logs effective model capabilities with their provenance source, improving diagnostic visibility for model configuration issues ([#1584](https://github.com/netclaw-dev/netclaw/pull/1584))

### Dependency Updates
- **Bump SkillServer** — `Netclaw.SkillClient` 0.4.0-beta.1 → 0.4.0-beta.3 and adapt to API changes ([#1593](https://github.com/netclaw-dev/netclaw/pull/1593))

## 0.25.0-beta.1 (2026-07-05)

### Features
Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/memory-relevance-gate/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-06
387 changes: 387 additions & 0 deletions openspec/changes/memory-relevance-gate/design.md

Large diffs are not rendered by default.

167 changes: 167 additions & 0 deletions openspec/changes/memory-relevance-gate/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Proposal: memory-relevance-gate

Source PRD: PRD-007 (agent personality and local memory), continuing
`memory-core-redesign` (`openspec/changes/memory-core-redesign/`, PR #1570;
Slice 4 shipped the hybrid recall + calibrated cosine floor this change builds
on). Evidence base: `~/recall-research-local/2026-07/gate-shootout/` (4-design
gate shoot-out, 2026-07-06) and `~/recall-research-local/2026-07/gold-expansion/`
(450-query out-of-sample gold expansion + gate re-validation, 2026-07-06) —
operator-local research stores holding real (PII) traffic data, never
committed, per the same convention documented in
`docs/research/memory-audit-2026-07.md`.

## Why

Even with hybrid recall and the calibrated per-model cosine floor
(memory-core-redesign Slice 4), most nothing-relevant queries still cause an
injection: floor-only zero-injection accuracy measured **16.7%** on the July
gold set (`gold-prod-2026-07`, 93 queries) and **7.3%** on the 450-query
out-of-sample expanded gold set. Cosine similarity measures topical
"aboutness," not usefulness-for-answering — a candidate can clear the floor
and still be the wrong thing to inject. This is the dominant remaining
recall-quality defect because **60–65% of real queries have nothing relevant**
to recall at all (replicated across 543 labeled real-traffic queries: 93 July
+ 450 expansion), so the floor's residual miss rate lands on the majority
case, not the tail.

## What Changes

- **New relevance-gate stage after the cosine floor.** A tiny cross-encoder
scores `(query, candidate)` jointly for each of the (≤`AutoRecallMaxItems`
= 3) floor-surviving candidates; anything below a calibrated threshold S* is
dropped. Zero survivors after the gate ⇒ inject nothing, same as zero
survivors at the floor today.
- **Winner of a 4-design measured shoot-out, out-of-sample validated**:
`Xenova/ms-marco-MiniLM-L-6-v2`, `model_quantized.onnx` (int8, 22.07 MB,
SHA-256 `e9d8ebf845c413e981c175bfe49a3bfa9b3dcce2a3ba54875ee5df5a58639fbe`).
Out-of-sample (450-query expanded gold set, disjoint from the calibration
set) at S*=0.02: zero-injection accuracy **86.8%** (95% CI 82.3–90.3) vs
7.3% floor-only, recall retention **98.3%**, F0.5 **0.130** vs 0.100
floor-only, mean injected **0.251** vs 2.538 floor-only.
- **Reuses memory-core-redesign's infrastructure wholesale** — this is the
change's selling point, not an afterthought: the same consumer-defined-seam
pattern (`IMemoryEmbedder` → `IRelevanceScorer`), the same
allowlist-manifest provisioning pattern (`EmbeddingModelProvisioner` gains a
relevance-model manifest entry kind carrying pinned URL/SHA-256/size *and*
the calibrated operating threshold), the same warmup hosted service, and the
same loud-degradation contract (rate-limited log marker + doctor
visibility) — no new machinery class, only a new manifest entry and a new
scoring step in an existing pipeline.
- **One mental switch.** Gate activation is tied to
`Memory.Embeddings.Enabled` — there is no separate "turn semantic recall
quality on" knob. `Memory.Recall.RelevanceGate { Enabled (nullable, follows
Embeddings), Threshold (nullable, follows the manifest's calibrated S*) }`
exists only for an explicit operator override.
- **Logging.** `memory_retrieval_final` gains `gateScores` and `droppedByGate`
fields. A new eval case asserts the zero-injection behavior end-to-end:
seeded corpus, off-topic question, assert no `[memory-recall]` block and a
gate marker in the logs.
- **Rejected alternatives** (recorded for provenance; not shipped):
- *Distribution-shape statistical gate* (`z_top50 ≥ 2.80`): looked viable
in-sample (70% zero-injection) but failed out-of-sample — 65.2%
zero-injection accuracy, **86.5% recall retention (below the ≥90%
constraint)**, F0.5 0.089, *worse* than the 0.100 floor-only baseline.
- *Learned feature gate* (candidate-/query-level logistic regression and
GBM over cosine/margin/z-score/length/age features): query-level variant
measured out-of-fold AUC 0.545 (chance = 0.500, i.e. no signal);
candidate-level variant's positive-class support grew only 8→39 across
the gold expansion — still not enough to certify signal over
small-sample luck, and it showed an 80%-relative recall collapse on a
differently-composed transfer set.
- *Per-memory offender priors* (`pollution_count`/`injection_count` per
`docId`): structurally cold-start-bound — only 1.1% of top-3 recall
candidates carry 3+ injection observations to build a prior from, 5.3%
even at a relaxed 2+ threshold; 80.6% of top-3 candidates are cold-start
with no addressable history at all.

## Capabilities

### New Capabilities

- `memory-relevance-gate`: the `IRelevanceScorer` seam and
`OnnxCrossEncoderScorer` implementation, the relevance-model provisioning
manifest kind (pinned URL/SHA-256/size + calibrated threshold), and the
post-floor gate stage wired into automatic recall.

### Modified Capabilities

- `netclaw-agent-memory`: the automatic pre-turn recall requirement gains a
post-floor relevance-gate stage — floor-surviving candidates are scored and
filtered before injection; zero survivors after the gate is a "nothing
injected" outcome exactly like zero survivors at the floor; gate
unavailability or sub-budget timeout degrades to floor-only behavior with a
loud marker.
- `memory-embeddings`: the pinned-allowlist provisioning requirement is
generalized to a manifest entry *kind* so it can provision relevance
(cross-encoder) models alongside embedding models, and the warmup hosted
service provisions/warms both.

## Impact

- **Code**: new `IRelevanceScorer` seam (`Netclaw.Actors/Memory`), new
`OnnxCrossEncoderScorer` (`Netclaw.Embeddings`, pair encoding `[CLS] q [SEP]
d [SEP]` with `token_type_ids`, sigmoid over the single-logit head, dynamic
sequence length bucket-of-8 matching the embedder's convention);
`EmbeddingModelProvisioner`'s allowlist gains a relevance-model manifest
kind; `SQLiteMemoryRecallCoordinator` gains the post-floor gate stage under
a CE sub-budget; `Netclaw.Configuration` gains
`Memory.Recall.RelevanceGate`; doctor/status surfaces extend to cover the
relevance model; `netclaw-memory` skill update.
- **Dependencies**: none new — reuses the `Microsoft.ML.OnnxRuntime` +
managed-tokenizer stack memory-core-redesign Slice 2 already adopted. One
new pinned model artifact (~22 MB int8), never embedded in the binary,
downloaded and hash-verified at provisioning time exactly like the
embedding model is today.
- **Data/config**: `netclaw-config.v1.schema.json` gains the new nodes, all
nullable with manifest-derived defaults — additive, non-breaking.
- **Evals**: new zero-injection gate eval case; `memory_retrieval_final`'s
log schema gains two additive fields (`gateScores`, `droppedByGate`).
- **Target branch**: implementation lands on `feature/memory-embeddings` (the
in-flight branch carrying memory-core-redesign's embedding and recall
slices), not directly on `dev` — this change's tasks assume that branch's
`IMemoryEmbedder`/`MemoryEmbedderHolder`/`SQLiteMemoryRecallCoordinator`
hybrid-recall code as their starting point.

### In scope (MVP)

- The cross-encoder scorer, its provisioning manifest entry, the coordinator
wiring (score → threshold → drop), the config surface, degradation
semantics, logging fields, and the zero-injection eval case.
- Recording the shoot-out's rejected alternatives and residual failure modes
in `design.md` for provenance.

### Out of scope

- Domain-calibrated or class-conditional thresholds for the measured MS
MARCO under-scoring of procedural/command-style memories (residual, ~1.7%
of retained recall at S*=0.02) — future work, not this change.
- Consolidating `MemoryEmbedderHolder` and a prospective relevance-scorer
holder into one combined embedding-runtime holder — noted as an optional
simplification in `design.md`, not required for this change to ship.
- Any change to the cosine floor itself, the embedding model, or the fusion
weights (memory-core-redesign Slice 4 territory; this change only adds a
stage after that pipeline's existing output).
- Re-running or expanding the judged gold sets further; this change consumes
the existing gate-shootout and gold-expansion results as already-ratified
inputs.

## Security and Operational Impact

- **Model supply chain**: the relevance model is provisioned through the
same pinned-allowlist mechanism as the embedding model — id → URL + byte
size + SHA-256, arbitrary URLs rejected, atomic download (temp + rename),
hash-verified before load. No new supply-chain surface, only a new
manifest entry kind on the existing one.
- **Resource envelope**: measured on the reference CPU — ~11 ms p50 / ~35 ms
p95 to score 3 pairs (quantized int8), ~103 MB incremental RSS. Combined
with int8 embeddings (263 MB) and daemon peak (397 MB), the operator's
measured total is ≈763 MB — inside the 1 GB K8s pod limit, with headroom
noted rather than assumed.
- **Degradation**: gate unavailability (model not provisioned) or exceeding
its CE sub-budget (~60 ms, linked CTS) degrades to floor-only behavior — the
pre-existing, already-shipped recall path — plus a rate-limited
`memory_recall_gate_degraded` log marker and doctor visibility. Never a
silent fallback, matching memory-core-redesign's degradation contract.
- **Operations**: no new operator action required — gate activation follows
`Memory.Embeddings.Enabled`; the existing warmup hosted service and doctor
checks extend to cover the new model without a new CLI verb.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Delta: memory-embeddings (memory-relevance-gate)

## MODIFIED Requirements

### Requirement: Pinned model provisioning

Memory-subsystem models SHALL be selected by id from a pinned in-code
allowlist mapping model id to download URL, byte size, and SHA-256, covering
more than one kind of model artifact (embedding models and relevance-scoring
models share the same allowlist mechanism). A relevance-model manifest entry
SHALL additionally carry a calibrated similarity threshold alongside its
download and verification fields, so a model's operating point travels with
its id rather than living as a disconnected configuration default. Arbitrary
model URLs SHALL be rejected for every manifest kind. Provisioning SHALL
download atomically (temporary file then rename), verify the hash before
load, and run at daemon initialization when auto-download is enabled or on
explicit operator command. No model artifact SHALL be embedded in the
application binary.

#### Scenario: Hash mismatch refuses the model

- **GIVEN** a downloaded model artifact whose SHA-256 does not match the
allowlist entry
- **WHEN** provisioning verifies the artifact
- **THEN** the artifact is discarded and not loaded
- **AND** the failure is surfaced as a doctor-visible error

#### Scenario: Unknown model id is rejected

- **GIVEN** configuration naming a model id absent from the allowlist
- **WHEN** the daemon initializes embeddings
- **THEN** provisioning refuses with a configuration error identifying the
allowlisted ids

#### Scenario: Relevance manifest entry's threshold travels with its model id

- **GIVEN** an allowlisted relevance-model manifest entry carrying a
calibrated threshold
- **WHEN** that model id is provisioned and becomes active
- **THEN** the calibrated threshold from that same manifest entry is what
governs gating, not a threshold associated with any other model id
- **AND** switching to a different allowlisted relevance-model id switches
the effective threshold to that id's own calibrated value
Loading
Loading