Skip to content

feat(memory): operator alert when embedding/reranker model provisioning fails#1611

Merged
Aaronontheweb merged 1 commit into
netclaw-dev:feature/memory-embeddingsfrom
Aaronontheweb:feat/model-provisioning-alert
Jul 9, 2026
Merged

feat(memory): operator alert when embedding/reranker model provisioning fails#1611
Aaronontheweb merged 1 commit into
netclaw-dev:feature/memory-embeddingsfrom
Aaronontheweb:feat/model-provisioning-alert

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

Phase 1 -- investigation findings

Gap confirmed. EmbeddingWarmupHostedService.WarmUpAsync/WarmUpRelevanceGateAsync catch every provisioning/load failure for both ONNX models (embedder: snowflake-arctic-embed-m-int8 or configured ModelId; relevance: ms-marco-minilm-l-6-v2), set the holder to an Unavailable* stub, and logger.LogError(...). Nothing operator-facing fired -- discovery required netclaw doctor, netclaw status, or reading daemon.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-specific IReminderChannelNotifier is 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, DaemonLifecycleNotifier

WebhookNotificationService implements the sink and delivers to configured Slack/generic webhook targets (or NullNotificationSink when 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 IChatClient fallback) 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

  • Two new AlertType values: MemoryEmbeddingModelUnavailable, MemoryRelevanceModelUnavailable (OperationalAlert.cs).
  • EmbeddingWarmupHostedService takes a new required IOperationalNotificationSink dependency. On provisioning/load failure for either model (while Memory.Embeddings.Enabled=true), fires an alert carrying:
    • which model failed (modelId, alert Source)
    • why (reason = exception message)
    • consequence: "Memory recall/dedup is running lexical-only -- semantic features are degraded." (embedder) / "The relevance gate is disabled -- recall is unfiltered by the cross-encoder." (relevance model)
    • remediation: check network/disk, netclaw doctor, and (embedder only) netclaw memory backfill-embeddings re-provisions on next start -- wording mirrors the existing MemoryEmbeddingDoctorCheck/MemoryRelevanceGateDoctorCheck remediation text so operators see consistent guidance whether pulling netclaw doctor or reacting to a push.
  • Latched per model via Interlocked.CompareExchange so each model alerts at most once per daemon run, never per retry.
  • No alert when Memory.Embeddings.Enabled=false -- intentional, not degraded, state.
  • Keep-warm mid-run failures deliberately not wired in. 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 (mirroring ReminderManagerActor'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 in LogKeepWarmFailed's remarks.

Bonus fix (surfaced by testing the both-models-fail case): WarmUpAsync had a pre-existing return; in the embedder's catch block that made WarmUpRelevanceGateAsync unreachable 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

Category: MemoryEmbeddingModelUnavailable
Type: memory.embedding_model.unavailable
Severity: Warning
Source: snowflake-arctic-embed-m-int8
Summary: Memory embedding model 'snowflake-arctic-embed-m-int8' could not be
  provisioned or loaded: <exception message> Memory recall/dedup is running
  lexical-only -- semantic features are degraded.
Context:
  modelId: snowflake-arctic-embed-m-int8
  reason: <exception message>
  consequence: Memory recall/dedup is running lexical-only -- semantic
    features are degraded.
  remediation: Check network access and disk space, run `netclaw doctor`, or
    run `netclaw memory backfill-embeddings` -- the daemon re-provisions the
    model on its next start.

Tests

EmbeddingWarmupHostedServiceTests (25 tests total, new coverage added):

  • embedder-only failure -> exactly one alert, correct category/content
  • relevance-only failure -> exactly one alert, correct category/content (no backfill-embeddings mention)
  • both models fail -> two distinct alerts, distinct alert ids
  • success path -> zero alerts
  • disabled config -> zero alerts
  • repeated WarmUpAsync calls -> latch holds, still exactly one alert per model total (not per call)

Gates run locally:

  • dotnet build Netclaw.slnx -- clean
  • dotnet test -- Netclaw.Daemon.Tests (854), Netclaw.Actors.Tests (2671), Netclaw.Embeddings.Tests (48), Netclaw.Configuration.Tests (467) -- all green
  • dotnet slopwatch analyze -- 0 issues
  • Add-FileHeaders.ps1 -Verify -- all headers present
  • No *Config properties added/changed, so no schema (netclaw-config.v1.schema.json) update needed
  • Skill-content eval suite (./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 risk

Skill sync

  • netclaw-operations SKILL.md 2.25.0 -> 2.26.0; references/diagnostics.md documents the new alert types and a new "Memory embedding/relevance model unavailable" symptom row
  • netclaw-memory SKILL.md 1.13.0 -> 1.14.0; Embeddings section now mentions the pushed operator alert alongside the existing log/status pull-based signals

…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
Aaronontheweb merged commit fb76fa5 into netclaw-dev:feature/memory-embeddings Jul 9, 2026
15 checks passed
Aaronontheweb added a commit that referenced this pull request Jul 10, 2026
Operational alert on model provisioning failure (#1611), embedder-failure
no longer blocks relevance model (#1611), graceful daemon shutdown budget
(#1612), --help no longer executes commands (#1612). Memory eval 6/6
(run 6cc18657); code validated by full PR CI on #1611/#1612.
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.

1 participant