feat(memory): operator alert when embedding/reranker model provisioning fails#1611
Merged
Aaronontheweb merged 1 commit intoJul 9, 2026
Conversation
…ng fails 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.
Aaronontheweb
merged commit Jul 9, 2026
fb76fa5
into
netclaw-dev:feature/memory-embeddings
15 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 1 -- investigation findings
Gap confirmed.
EmbeddingWarmupHostedService.WarmUpAsync/WarmUpRelevanceGateAsynccatch every provisioning/load failure for both ONNX models (embedder:snowflake-arctic-embed-m-int8or configuredModelId; relevance:ms-marco-minilm-l-6-v2), set the holder to anUnavailable*stub, andlogger.LogError(...). Nothing operator-facing fired -- discovery requirednetclaw doctor,netclaw status, or readingdaemon.log, all pull-based.Mechanism found and reused:
IOperationalNotificationSink/OperationalAlert(src/Netclaw.Configuration/OperationalAlert.cs,IOperationalNotificationSink.cs). This is the generic push-to-operator seam already used by:McpReconnectionService(mcp.server.reconnected)McpClientManager(mcp.server.disconnected,mcp.auth.expired)ReminderManagerActor(reminder.execution.failed,reminder.auto_disabled) -- the PR feat(reminders): operator visibility into reminder failures and skips (#1494) #1503 "reminder failure visibility" mechanism turned out to be built on this same generic sink, not a bespoke reminder-only path (the reminder-specificIReminderChannelNotifieris a separate, narrower mechanism for posting into a specific reminder's destination channel -- not reusable here since embedding warmup runs at startup with no session/channel context)RoutingChatClient(provider.failover,provider.unreachable)ProviderOAuthTokenRefreshService,McpOAuthService,BinaryUpdateCheckService,DaemonLifecycleNotifierWebhookNotificationServiceimplements the sink and delivers to configured Slack/generic webhook targets (orNullNotificationSinkwhen none configured -- alerts are always logged at the emission site regardless). This is a required, non-nullable dependency at every existing call site, consistent with the constitution's "no silent fallbacks" / required-dependency rules.The 0.24.3 "Degraded startup mode" banner (PR #1540, No-Op
IChatClientfallback) is a reactive mechanism (a chat-turn response), not a push -- not applicable here.Given a strong existing mechanism, proceeded to Phase 2 (no STOP).
What was implemented
AlertTypevalues:MemoryEmbeddingModelUnavailable,MemoryRelevanceModelUnavailable(OperationalAlert.cs).EmbeddingWarmupHostedServicetakes a new requiredIOperationalNotificationSinkdependency. On provisioning/load failure for either model (whileMemory.Embeddings.Enabled=true), fires an alert carrying:modelId, alertSource)reason= exception message)netclaw doctor, and (embedder only)netclaw memory backfill-embeddingsre-provisions on next start -- wording mirrors the existingMemoryEmbeddingDoctorCheck/MemoryRelevanceGateDoctorCheckremediation text so operators see consistent guidance whether pullingnetclaw doctoror reacting to a push.Interlocked.CompareExchangeso each model alerts at most once per daemon run, never per retry.Memory.Embeddings.Enabled=false-- intentional, not degraded, state.KeepWarmTickAsync's own doc comment already states a single tick miss is not user-visible degradation (transient ONNX hiccup, may self-heal next tick). Alerting on the first miss would false-positive on exactly that documented case. Doing this right needs a consecutive-failure threshold (mirroringReminderManagerActor's auto-disable pattern) before promoting a keep-warm miss to alert-worthy -- a real design decision, not just plumbing. Left as a follow-up, documented inLogKeepWarmFailed's remarks.Bonus fix (surfaced by testing the both-models-fail case):
WarmUpAsynchad a pre-existingreturn;in the embedder's catch block that madeWarmUpRelevanceGateAsyncunreachable whenever the embedder itself failed -- directly contradicting the method's own documented "runs regardless of whether the embedder itself just degraded" contract, and silently suppressing the relevance-model alert in the worst-case (both models down) scenario. Restructured so the embedder try/catch no longer early-returns; gap-repair only runs if the embedder loaded, and the relevance gate always gets its independent attempt.Sample alert content
Tests
EmbeddingWarmupHostedServiceTests(25 tests total, new coverage added):backfill-embeddingsmention)WarmUpAsynccalls -> latch holds, still exactly one alert per model total (not per call)Gates run locally:
dotnet build Netclaw.slnx-- cleandotnet test--Netclaw.Daemon.Tests(854),Netclaw.Actors.Tests(2671),Netclaw.Embeddings.Tests(48),Netclaw.Configuration.Tests(467) -- all greendotnet slopwatch analyze-- 0 issuesAdd-FileHeaders.ps1 -Verify-- all headers present*Configproperties added/changed, so no schema (netclaw-config.v1.schema.json) update needed./evals/run-evals.sh) not run here -- requires a live model endpoint/Docker not available in this environment (same caveat noted in PR feat(reminders): operator visibility into reminder failures and skips (#1494) #1503); the skill edits are additive documentation lines describing an already-implemented, tested code path, low riskSkill sync
netclaw-operationsSKILL.md 2.25.0 -> 2.26.0;references/diagnostics.mddocuments the new alert types and a new "Memory embedding/relevance model unavailable" symptom rownetclaw-memorySKILL.md 1.13.0 -> 1.14.0; Embeddings section now mentions the pushed operator alert alongside the existing log/status pull-based signals