Skip to content
Open
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
08a7bb5
feat(memory): embedding foundation — ONNX runtime, provisioning, embe…
Aaronontheweb Jul 5, 2026
e00a23b
Merge dev into feature/memory-embeddings (CI feature/* triggers #1586…
Aaronontheweb Jul 5, 2026
88cb85a
feat(memory): kNN-nominate/LLM-decide dedup + lossless merges (memory…
Aaronontheweb Jul 5, 2026
ec15629
fix(memory): guard-rejected anchor updates fall through to nomination…
Aaronontheweb Jul 6, 2026
12e4476
fix(smoke): update help screenshot baseline for netclaw memory comman…
Aaronontheweb Jul 8, 2026
acac139
opsx: memory-relevance-gate change artifacts (proposal, design, specs…
Aaronontheweb Jul 6, 2026
f507fdf
test: add failing tests for BOM frontmatter and missing SkillName (#1…
Aaronontheweb Jul 6, 2026
d2d07aa
chore: bump SkillServer to 0.4.0-beta.3 (#1593)
Aaronontheweb Jul 7, 2026
26b88be
Bump Grpc.Tools from 2.81.1 to 2.82.0 (#1591)
dependabot[bot] Jul 7, 2026
dec40d8
Bump version to 0.25.0-beta.2 and update release notes (#1594)
Aaronontheweb Jul 7, 2026
e1dea9d
Bump Netclaw.SkillClient from 0.4.0-beta.3 to 0.4.0-beta.4 (#1596)
dependabot[bot] Jul 8, 2026
4d6f365
fix(subagents): record sub-agent LLM token usage to daily stats (#159…
Aaronontheweb Jul 8, 2026
03723de
fix(release): extract only the latest RELEASE_NOTES section for the G…
Aaronontheweb Jul 8, 2026
b492a4b
Merge remote-tracking branch 'origin/dev' into feature/memory-embeddings
Aaronontheweb Jul 8, 2026
8c27daa
chore(release): prepare 0.25.0-alpha.onnx.1 experimental prerelease
Aaronontheweb Jul 8, 2026
95c9644
Merge upstream/feature/memory-embeddings into alpha.onnx.1 release prep
Aaronontheweb Jul 8, 2026
a6d0a2f
feat(memory): read-side hybrid recall with absolute cosine floor (mem…
Aaronontheweb Jul 8, 2026
b797f4b
feat(memory): calibrated MinCosineSimilarity default 0.68 from gold-p…
Aaronontheweb Jul 8, 2026
99a58e7
test(memory): gold-set recall regression suite, P09 paraphrase-gap fl…
Aaronontheweb Jul 8, 2026
7564384
fix(memory): coverage-gap candidates degrade to lexical instead of bl…
Aaronontheweb Jul 8, 2026
22121d1
docs(memory): hybrid recall skill guidance + eval suite run (memory-c…
Aaronontheweb Jul 8, 2026
76c3265
feat(memory): cross-encoder relevance gate on recall (memory-relevanc…
Aaronontheweb Jul 8, 2026
1776175
docs(memory): relevance gate skill, runbook, and calibration procedur…
Aaronontheweb Jul 8, 2026
9b9d78b
opsx: memory-query-prefix change artifacts (proposal, design, specs, …
Aaronontheweb Jul 8, 2026
a646aab
feat(memory): model-documented query prefix + manifest-carried floor …
Aaronontheweb Jul 8, 2026
e1bab29
feat(memory): int8 arctic embedder as default model with prefixed flo…
Aaronontheweb Jul 8, 2026
4c8cf68
Merge remote-tracking branch 'upstream/dev' into feature/memory-embed…
Aaronontheweb Jul 8, 2026
c0a5447
chore(release): prepare 0.25.0-alpha.onnx.2 experimental prerelease
Aaronontheweb Jul 8, 2026
4e6747a
fix(cli): dispatch netclaw memory command; netclawd --version no long…
Aaronontheweb Jul 9, 2026
53fa8e9
fix(memory): relevance-gate cold-start — keep-warm ticks, envelope-de…
Aaronontheweb Jul 9, 2026
944aadb
chore(release): prepare 0.25.0-alpha.onnx.3 experimental prerelease
Aaronontheweb Jul 9, 2026
fb76fa5
feat(memory): operator alert when embedding/reranker model provisioni…
Aaronontheweb Jul 9, 2026
f822b7d
fix(daemon,cli): graceful systemd stop; memory subcommand --help hand…
Aaronontheweb Jul 9, 2026
7143170
chore(release): prepare 0.25.0-alpha.onnx.4 experimental prerelease
Aaronontheweb Jul 10, 2026
3ee94a9
onnx.5: sync dev (fail-closed sub-agent approvals #1616 + fixes), pre…
Aaronontheweb Jul 12, 2026
57fd0a1
onnx.6: sync dev @ 0.25.0-beta.4 (curation dedup fix #1637), prepare …
Aaronontheweb Jul 14, 2026
70ba9b0
merge dev (post-0.25.0-beta.4) into feature/memory-embeddings for 0.2…
Aaronontheweb Jul 16, 2026
2077e4b
fix: bump System.Numerics.Tensors pin to 10.0.10 to match Microsoft.E…
Aaronontheweb Jul 16, 2026
5c7278f
Merge pull request #1662 from Aaronontheweb/release/0.25.0-alpha.onnx.7
Aaronontheweb Jul 16, 2026
3aa5617
fix(memory): raise curation LLM timeout default from 10s to 60s (#1679)
Aaronontheweb Jul 16, 2026
1a88f0e
Merge remote-tracking branch 'upstream/dev' into release/0.25.0-alpha…
Aaronontheweb Jul 18, 2026
83ad906
onnx.8: sync dev @ 0.25.0 stable (shutdown fixes #1673), prepare 0.25…
Aaronontheweb Jul 18, 2026
6cd1cce
merge: sync feature/memory-embeddings with dev @ 0.25.2
Aaronontheweb Aug 4, 2026
8e71acd
feat(memory): default ONNX embeddings to enabled
Aaronontheweb Aug 4, 2026
3ed49d5
test: flip Embeddings disabled-by-default bear trap to enabled-by-def…
Aaronontheweb Aug 4, 2026
5c45914
fix(doctor): downgrade missing-model from Error to Warning when AutoD…
Aaronontheweb Aug 4, 2026
cc2f0c6
Merge branch 'dev' into stage/onnx-memory-merge
Aaronontheweb Aug 5, 2026
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
29 changes: 29 additions & 0 deletions .github/workflows/publish_release_binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,35 @@ jobs:
--output-dir ./publish
--version ${{ github.ref_name }}

# ARM64 cross-compile verification: since ARM64 binaries are built on x64 runners,
# we cannot execute them. Instead, verify the build actually produced ARM64 ELF
# files (not x64) using the `file` command to detect architecture mismatch. This
# catches silent cross-compile failures. See CONTRIBUTING.md § Cross-Platform
# Publishing for context.
- name: Verify ARM64 binaries are actually ARM64 (not x64)
if: matrix.rid == 'linux-arm64'
shell: bash
run: |
set -euo pipefail
CLI="./publish/cli/netclaw"
DAEMON="./publish/daemon/netclawd"

for binary in "$CLI" "$DAEMON"; do
if [ ! -f "$binary" ]; then
echo "ERROR: Expected binary not found: $binary" >&2
exit 1
fi

# 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
Comment on lines +165 to +171
echo "✓ $binary is ARM64"
done

- name: Package archives (Unix)
if: runner.os != 'Windows'
run: |
Expand Down
65 changes: 46 additions & 19 deletions .slopwatch/baseline.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": 1,
"createdAt": "2026-05-12T17:20:55.7365203+00:00",
"updatedAt": "2026-06-10T19:44:08.3092383+00:00",
"updatedAt": "2026-07-08T22:05:54.9570245+00:00",
"description": "Initial baseline created by 'slopwatch init' on 2026-05-12 17:20:55 UTC",
"entries": [
{
Expand Down Expand Up @@ -31,24 +31,6 @@
"message": "Adding warnings to NoWarn: OPENAI001",
"baselinedAt": "2026-05-12T17:20:55.7420301+00:00"
},
{
"hash": "1a29ed65e4ed3efb",
"ruleId": "SW004",
"filePath": "src/Netclaw.Daemon.Tests/Services/ConfigWatcherServiceTests.cs",
"lineNumber": 139,
"codeSnippet": "Task.Delay(50, ct)",
"message": "Test uses Task.Delay(50) which may indicate a timing-dependent test",
"baselinedAt": "2026-05-12T17:20:55.7420338+00:00"
},
{
"hash": "fcb5e461d7f70a7c",
"ruleId": "SW004",
"filePath": "src/Netclaw.Daemon.Tests/Services/ConfigWatcherServiceTests.cs",
"lineNumber": 154,
"codeSnippet": "Task.Delay(100, ct)",
"message": "Test uses Task.Delay(100) which may indicate a timing-dependent test",
"baselinedAt": "2026-05-12T17:20:55.7420454+00:00"
},
{
"hash": "6ea5c8bbead4b59c",
"ruleId": "SW004",
Expand Down Expand Up @@ -174,6 +156,51 @@
"codeSnippet": "Fact(SkipUnless = nameof(IsPosix), Skip = \"POSIX-only — matcher routes through BashParser on POSIX\")",
"message": "Test method 'IsApproved_git_tag_grant_matches_both_version_forms' is disabled: POSIX-only — matcher routes through BashParser on POSIX",
"baselinedAt": "2026-06-10T19:44:08.3092375+00:00"
},
{
"hash": "687db840a8ff6f35",
"ruleId": "SW004",
"filePath": "src/Netclaw.Embeddings.Tests/BoundedConcurrencyGateTests.cs",
"lineNumber": 32,
"codeSnippet": "Task.Delay(20, ct)",
"message": "Test uses Task.Delay(20) which may indicate a timing-dependent test",
"baselinedAt": "2026-07-08T22:05:54.9567742+00:00"
},
{
"hash": "06c8b73e9ee96bc1",
"ruleId": "SW004",
"filePath": "src/Netclaw.Embeddings.Tests/BoundedConcurrencyGateTests.cs",
"lineNumber": 54,
"codeSnippet": "Enumerable.Range(0, 10)\n .Select(_ => gate.RunAsync(async ct =>\n {\n await Task.Delay(5, ct);\n return Interlocked.Increment(ref completed);\n }, TestContext.Current.CancellationToken))\n .ToArray()",
"message": "Test uses Task.Delay(?) which may indicate a timing-dependent test",
"baselinedAt": "2026-07-08T22:05:54.9568105+00:00"
},
{
"hash": "3d71bddf21a28fee",
"ruleId": "SW004",
"filePath": "src/Netclaw.Embeddings.Tests/BoundedConcurrencyGateTests.cs",
"lineNumber": 57,
"codeSnippet": "Task.Delay(5, ct)",
"message": "Test uses Task.Delay(5) which may indicate a timing-dependent test",
"baselinedAt": "2026-07-08T22:05:54.9568168+00:00"
},
{
"hash": "be802d7249cc2884",
"ruleId": "SW004",
"filePath": "src/Netclaw.Daemon.Tests/Services/EmbeddingWarmupHostedServiceTests.cs",
"lineNumber": 281,
"codeSnippet": "Task.Delay(25 * (i + 1))",
"message": "Test uses Task.Delay(25 * (i + 1)) which may indicate a timing-dependent test",
"baselinedAt": "2026-07-08T22:05:54.9568311+00:00"
},
{
"hash": "233d9d75b339f3f3",
"ruleId": "SW004",
"filePath": "src/Netclaw.Actors.Tests/Sessions/SQLiteMemoryRecallGateTests.cs",
"lineNumber": 456,
"codeSnippet": "Task.Delay(Timeout.InfiniteTimeSpan, ct)",
"message": "Test uses Task.Delay(Timeout.InfiniteTimeSpan) which may indicate a timing-dependent test",
"baselinedAt": "2026-07-08T22:05:54.957+00:00"
}
]
}
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ Prereleases ship to opt-in testers without touching any stable surface.
Use the **dotted** `beta.N` form (`beta.1`, `beta.2`, … `beta.10`) — never `beta1`.
A non-dotted identifier compares lexically (so `beta10` would rank below `beta2`), and
the release version gate rejects it.
2. Add a `RELEASE_NOTES.md` section for `0.23.0-beta.1`.
2. Add a `RELEASE_NOTES.md` section for `0.23.0-beta.1` (`## 0.23.0-beta.1 (YYYY-MM-DD)`).
3. Commit, then tag and push the full version (prefix `-` suffix):
```bash
git tag 0.23.0-beta.1 && git push origin 0.23.0-beta.1
Expand Down
13 changes: 13 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,19 @@
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
</ItemGroup>
<!-- Memory embeddings (Netclaw.Embeddings + Netclaw.Embeddings.Tests). CPU-only ONNX
inference + a pure-managed WordPiece tokenizer, per memory-core-redesign D1: no
sidecar process, no native dependency beyond the ONNX Runtime CPU EP that already
ships linux-x64 + linux-arm64 native assets. -->
<ItemGroup>
<PackageVersion Include="Microsoft.ML.OnnxRuntime" Version="1.27.0" />
<PackageVersion Include="FastBertTokenizer" Version="1.0.28" />
<!-- Pinned to 10.0.10 (matches $(MicrosoftAspNetCoreVersion)), not the lowest version that
compiles: Microsoft.Extensions.AI 10.8.0 already pulls System.Numerics.Tensors
>= 10.0.10 transitively, and CentralPackageTransitivePinningEnabled requires the
central pin to be at least as high as every transitive floor in the graph. -->
<PackageVersion Include="System.Numerics.Tensors" Version="10.0.10" />
</ItemGroup>
<!-- Source generators -->
<ItemGroup>
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" />
Expand Down
2 changes: 2 additions & 0 deletions Netclaw.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
</Folder>
<Folder Name="/src/">
<Project Path="src/Netclaw.Actors/Netclaw.Actors.csproj" />
<Project Path="src/Netclaw.Embeddings/Netclaw.Embeddings.csproj" />
<Project Path="src/Netclaw.Embeddings.Tests/Netclaw.Embeddings.Tests.csproj" />
<Project Path="src/Netclaw.Channels/Netclaw.Channels.csproj" />
<Project Path="src/Netclaw.Channels.Discord/Netclaw.Channels.Discord.csproj" />
<Project Path="src/Netclaw.Channels.Mattermost/Netclaw.Channels.Mattermost.csproj" />
Expand Down
84 changes: 84 additions & 0 deletions docs/runbooks/memory-health-and-evals.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,90 @@ PY

Then restart the daemon from local binaries before running evals.

## Relevance Gate Health

The relevance gate (`memory-relevance-gate`) is a post-floor cross-encoder
stage: for each of the (≤3) candidates that already cleared the cosine
floor, a small ONNX model (`ms-marco-minilm-l-6-v2`) scores `(query,
candidate)` jointly and drops anything below the calibrated threshold.
Activation follows `Memory.Embeddings.Enabled` unless
`Memory.Recall.RelevanceGate.Enabled`/`Threshold` explicitly override it.

1. Run offline diagnostics and review the `Memory Relevance Gate` check:

```bash
netclaw doctor
```

- `PASS` + "disabled (follows Memory.Embeddings.Enabled...)" or "disabled
(Memory.Recall.RelevanceGate.Enabled is explicitly false)" — expected,
healthy state for any deployment that hasn't opted into embeddings, or
that opted out of the gate specifically. Not an error.
- `PASS` + "Relevance gate healthy: model '...' provisioned (threshold
...)" — the model is present, hash-verified, and its manifest-carried
(or config-overridden) threshold is reported.
- `ERROR` + "missing or fails hash verification at `<path>`" — the model
was never provisioned or the on-disk artifact doesn't match the pinned
SHA-256. Restart the daemon to re-provision if `AutoDownload` is
enabled; otherwise provision manually and restart.

2. Check the degradation log line. When the gate is skipped for a turn
(model unavailable, its sub-budget exceeded — a 120 ms ceiling clamped to
whatever remains of the outer 300 ms `Memory.RecallTimeoutMs` envelope, so
a turn where earlier stages already ran long gets less than 120 ms; raised
from a fixed 60 ms by a 2026-07 production-canary finding of cold-start
timeouts — or recall running in lexical mode because there's no query
vector), the coordinator logs a rate-limited marker instead of silently
changing what gets injected:

```
memory_recall_gate_degraded session=<id> reason=<reason> elapsedMs=<ms>
```

`reason` is one of `gate_disabled_by_config`, `no_scorer_configured`,
`scorer_unavailable`, `sub_budget_exceeded`, or `score_failed:<ExceptionType>`.
`elapsedMs` is 0 for the first three (no scoring attempt ever started) and
the measured time spent before degrading for the latter two — useful for
telling a genuine cold-start/contention timeout apart from an instant
failure. Logged at `Warning` when the gate is enabled but a turn still
degraded (a genuine runtime condition worth noticing); logged at `Debug`
when the gate is off by config (the default, intentional state — not
spam). Rate-limited per-reason with the same cooldown as
`memory_recall_vector_degraded`, so expect at most one `Warning` line per
reason per cooldown window even under sustained degradation, not one per
turn.

3. Read `gateScores`/`droppedByGate`/`gateElapsedMs` on `memory_retrieval_final`
when diagnosing over- or under-injection or quantifying gate latency
margin against the 120 ms ceiling:

```bash
grep memory_retrieval_final "$HOME/.netclaw/logs/daemon-$(date +%F).log" | tail -20
```

- `droppedByGate` — how many of the floor's survivors the gate rejected
this turn. `0` on a turn that also injected nothing means the floor
itself already filtered everything (or the gate didn't run); a nonzero
`droppedByGate` with zero final `injectedCount` means the gate is the
reason nothing was injected, not the floor.
- `gateScores` — the cross-encoder score for every candidate the gate
scored (`id=score`, e.g. `doc-abc123=0.014`), regardless of whether it
survived. Compare against the active threshold (config override, or the
manifest's calibrated default reported by the doctor check) to see how
close a dropped candidate came, or how comfortably a survivor cleared
the bar. Absent `gateScores` (empty) on a hybrid-mode turn is itself a
signal the gate didn't run for that turn — check for a paired
`memory_recall_gate_degraded` line first before assuming a config
problem.
- Zero `gateScores` and zero `droppedByGate` on a turn is normal whenever
the floor itself already produced zero survivors — the gate never runs
against an empty candidate set. This is not a gate failure.

See `openspec/changes/memory-relevance-gate/design.md` for the calibration
procedure (threshold-sweep protocol, model shoot-out, and out-of-sample
validation numbers) if the operating point ever needs to be re-verified
against a different relevance model or corpus.

## Reproducible Memory Score (Non-LLM Judge)

Run the deterministic memory score script:
Expand Down
21 changes: 21 additions & 0 deletions evals/run-evals.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1137,6 +1137,24 @@ assert_memory_recall_filters() {
'
}

# memory-relevance-gate task 2.6: the automated analogue of the shoot-out's "zero-injection
# accuracy" metric. Off-topic query against the seeded corpus (travel/color/project-alpha/
# secret-token fixtures) must inject nothing.
#
# The eval container does not set Memory.Embeddings.Enabled (default false), so this case
# exercises the pre-existing lexical-only floor, not the cross-encoder gate itself (that
# requires an out-of-process download+provisioning step outside this harness's scope — see
# openspec/changes/memory-relevance-gate/tasks.md task 2.6: "authoring the case is [required];
# the eval RUN is not required here"). Asserting injectedCount=0 plus the unconditional
# droppedByGate= field (present on every memory_retrieval_final line regardless of mode --
# droppedByGate=0 accurately reports "nothing was dropped because nothing reached the gate")
# keeps this case correct and meaningful in both today's lexical-only default and a future run
# with embeddings enabled, without needing to touch the eval container's global config.
assert_memory_relevance_gate_zero_injection() {
daemon_log_contains 'memory_retrieval_final.*injectedCount=0' \
&& daemon_log_contains 'droppedByGate='
}

# Category 4: Tool Discovery & Use
assert_tool_discovery() {
stdout_contains '\[tool:call\] search_tools'
Expand Down Expand Up @@ -1761,6 +1779,9 @@ run_all() {
run_case memory_recall_filters "candidate selection with score filtering" \
"Tell me about my travel preferences"

run_case memory_relevance_gate_zero_injection "off-topic query injects nothing, gate marker logged" \
"What is the boiling point of tungsten in degrees Celsius?"

end_category

# ── Category 4: Tool Discovery & Use ──
Expand Down
Loading
Loading