5 Commits
Author SHA1 Message Date
james.maes 4b1dd6404b fix(voice): v0.1 CRIT cluster — wake / STT / TTS / boot probes (#28, #29, #30, #31, #32, #33, #34, #36, #158)
#28 (CRIT): setCancelInFlight calls stopFeed() (preserves stream); rebuild consumer re-arms DAG against new ring. Wake-word no longer dies on audio-graph rebuild.

#29 (CRIT): sttFactory reads PerTurnSnapshot.stt.whisperKitFallback; get_self_state returns canonical snake_case backend constants. Factory signature: () -> any STTProvider → () async -> any STTProvider (existing test sites compile unchanged under Swift 6 implicit-async-closure rules).

#30 (CRIT): TTSEngineActor.synthesize emits .ttsStopped after .finished on natural completion (two-emit-site model: barge-in + natural-completion).

#31 (CRIT): VoiceTTSAdapter.tierResolver wired to read snap.tts.tier from configStore.

#32 (HIGH): Rolling-window trim in OpenWakeWordSession.runORT — mel ≤ 84, embeddings ≤ 16, nextEmbStart slides on drop.

#33 (HIGH): three new boot probes — WakeWordBootHealthProbe, STTBootHealthProbe, TTSBootHealthProbe. New WakeWordDAG.isFeedArmed + VoiceTTSAdapter.hasEngine accessors.

#34 (HIGH): vad.reset() at top of startSTTSession clears Silero VAD LSTM hidden state + wasSpeech between sessions.

#36 (HIGH): new Broadcaster.Priority.agentHud (protects stateChange/turnEnd/error); installAgent adds subscriber pumping AgentHudIntent into dormantAgentContinuation.

Voice 90 + AgentCore 242 tests pass; all 18 boundary gates green.
2026-05-13 06:31:17 -05:00
james.maes a6f91e6b73 fix(bus,hud): v0.1 S1 cluster — webview didFail + menu-bar icon transition (#40, #41, #151)
#40: BridgeNavigationDelegate gains onDidFail closure (covers didFailProvisionalNavigation + didFail). WebviewBridge.failHandshake(reason:) transitions to new terminal .loadFailed state, cancels pending 2s timeout, fires existing alertPresenter, idempotent. AppDelegate.handleWebviewLoadFailure dual-surfaces: failHandshake + critical non-dismissible banner (defense-in-depth across test seams). WebviewBootHealthProbe gets .loadFailed case.

#41: HudStateCoordinator emit closure now ALSO calls menuBarController.transition(to:) in addition to bridge.send(.hudState(...)). Doc-comment correctly notes this is not a HUD-08 single-writer violation — transition writes currentState, not BusOutbound.hudState. scripts/check-single-writer-hudstate.sh still green.

Tests: 4 new Handshake cases (load-failure-from-sentHello with alert-not-duplicated, from-idle, idempotency, post-armed no-op), BridgeNavigationFailureTests end-to-end + per-arm, MenuBarIconStateTransitionTests drives dormantAgentContinuation and asserts .thinking → .speaking → .idle. 62/62 Bus pass; all 18 boundary gates green.
2026-05-13 06:29:34 -05:00
james.maesandClaude Opus 4.7 740bed4cbf fix(round-4): tag-agnostic ollama probe + mcp probe counts in-process tools
Two false-positive degraded reports surfaced during the second
Round 4 dogfood run:

1. Ollama probe was exact-string matching against required model
   names. `requiredModels` had `"nomic-embed-text"` (no tag), but
   `/api/tags` returns `"nomic-embed-text:latest"` (with default
   tag) — so the probe reported "missing models: nomic-embed-text"
   even after `ollama pull` succeeded. Fix: when a required name
   has no `:tag`, accept any tag (`name == required OR
   name.hasPrefix(required + ":")`). Required names that DO
   include a tag (e.g., `qwen3.6:latest`) keep their strict match.

2. MCP probe was reading ONLY `runtime.client.registeredToolNames()`
   — the stdio side (3 helpers: get_clipboard, get_time,
   run_applescript). The other 4+ in-process tools (self-knowledge
   + memory) live in `InProcessToolRegistry` and are routed
   through `InProcessAwareToolDispatcher` (commit 1b7fb81's B-01b
   fix). Probe now reads both registries, unions the names, and
   reports the combined count. Evidence line breaks out stdio vs
   in-process counts so the operator can see the split.

Verified: app build clean, app target compiles. Re-launch should
show ollama=ok (all 3 models present) and mcp=ok (8 tools after
self-knowledge tools register at install step 15).

The Toby fix verification mid-dogfood: the model received the
degradation summary in its system prompt and truthfully reported
"my memory subsystem is currently degraded (nomic-embed-text is
missing)" instead of yesterday's fake "Got it" promise. Round 4
slice 3 confirmed working end-to-end.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 10:29:58 -05:00
james.maesandClaude Opus 4.7 cfd469f72b feat(round-4): probe severity model — ok/soft/loud/critical
Round 4 of boot-health work. The Toby-the-dog incident — Jarvis said "Got
it" while the embedder model was missing and the fact never persisted —
proved that Round 2's probe banner was too quiet. Probes were already
reporting `.degraded` correctly; what was missing was a way for the App
layer to route a critical failure differently from a soft one (e.g.,
non-dismissible banner for memory off vs. silent Status row for camera
TCC not yet requested).

This slice introduces a severity axis without changing behavior:

- `FailureSeverity { critical, loud, soft }` on `ProbeStatus`'s non-ok
  cases (degraded/failed/unknown). `.ok` carries no severity.
- `ProbeStatus.severity` accessor (nil for `.ok`).
- `BootHealthSnapshot.overallHealth: OverallHealth { ok, soft, loud,
  critical }` — worst severity wins; `.ok` only if every subsystem ok.
- Each concrete probe annotates its failure modes:
  * Memory: failed → .critical (agent grounding; the Toby case)
  * Anthropic: missing/empty key → .critical (agent can't run)
  * Ollama: missing models → .loud (fact extraction broken, chat fine)
  * Voice: mic denied → .loud; dormant install → .soft
  * Vision: TCC denied → .soft (vision is optional)
  * MCP: 0 tools → .critical; <minimum → .loud
  * Replay: init failed → .critical; missing file → .loud
  * Webview: mismatched/timedOut → .critical; sentHello → .soft
- DormantSubsystemProbe gains `severity` parameter (default .soft);
  AppDelegate wires memory-dormant as .critical.

No behavioral change yet — banners still treat all failures equally;
the agent system prompt is unchanged. Slices 2 + 3 land those.

Tests: 13/13 in BootHealthOrchestratorTests pass, including 4 new tests
for overallHealth + severity accessor + JSON roundtrip with severity.
2 pre-existing failures in LLMEventTests/OllamaRequestBodyTests are
unrelated (qwen model-ID literal drift from commit 8d0a959, present
before this slice — confirmed via git stash).

Boundary gates 18/18 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 15:24:43 -05:00
james.maesandClaude Opus 4.7 687b80b3f0 feat(round-2): boot-phase health probes — NOT FAKED, structured logs
After all install Tasks join, runs one live probe per subsystem and
emits structured `BootHealth: subsystem=X state=Y latencyMs=Z evidence="..."`
log lines. Any subsystem reporting .failed enqueues a non-modal banner
via HUDBannerCoordinator (priority 3 — sits between hotkey-bind-failed
and ollama-url-rejected). The snapshot is stored on bootHealthOrchestrator
so Round 3's Status menu can read + re-probe.

Anti-fake-status invariant is type-level: ProbeStatus is a sum type with
.ok / .degraded(reason:) / .failed(reason:) / .unknown(reason:) — every
non-ok case carries a reason string, so a snapshot can never report
state=unknown without explaining why. A probe that can't execute MUST
return .unknown — never .ok with a fake evidence line.

New types in packages/AgentCore/Sources/AgentCore/BootHealth.swift:
  - ProbeStatus, ProbeOutcome, BootHealthProbe protocol
  - SubsystemHealth (Codable for Round 3 "Copy report"), BootHealthSnapshot
  - BootHealthOrchestrator actor — parallel TaskGroup fan-out,
    registration-order-preserved snapshot, injectable clock for tests

Eight concrete probes in App/Boot/BootHealthProbes.swift:
  - MemoryBootHealthProbe — MemoryStatsStoreAdapter.getMemoryStats round-trip
  - AnthropicBootHealthProbe — Keychain structural check (no billable
    network call at boot; Round 3 panel offers explicit network re-probe)
  - OllamaBootHealthProbe — GET /api/tags, 3s timeout, asserts qwen3.6 +
    qwen2.5-coder + nomic-embed-text all present
  - VoiceBootHealthProbe — AudioGraphOwner state + mic TCC
  - VisionBootHealthProbe — camera TCC + DiscoverySession (read-only,
    does NOT prompt TCC per RESEARCH §Example 2)
  - MCPBootHealthProbe — runtime.client.registeredToolNames count
  - ReplayBootHealthProbe — replay.sqlite presence + parent-dir writability
  - WebviewBootHealthProbe — WebviewBridge.handshakeState (.armed = live)
  - DormantSubsystemProbe — placeholder for subsystems whose actor was nil
    at probe-registration; reports .unknown honestly rather than skipping

AppDelegate wiring (step 16, runs after selfKnowledgeInstallTask which
transitively awaits every other install Task):
  - bootHealthOrchestrator: BootHealthOrchestrator stored property
  - bootHealthTask: Task<Void, Never>? cancelled in applicationWillTerminate
  - runBootHealth() async: registers probes (idempotent — re-probes from
    Round 3 reuse the existing registry), runs, logs, enqueues banners
  - emitBootHealthLog / enqueueFailedBootHealthBanners helpers

Tests (packages/AgentCore/Tests/AgentCoreTests/BootHealthOrchestratorTests.swift):
  - 8/8 passing (210ms wall — 3×200ms parallel probes prove TaskGroup
    fan-out works; serial would be 600ms+)
  - Coverage: runAll invokes every probe, snapshot preserves registration
    order, parallel execution timing, lastSnapshot persistence, .failed
    propagation, .unknown not coerced to .ok, deterministic clock
    injection for latencyMs, JSON round-trip for all 4 ProbeStatus variants

Verification before commit:
  - swift test --package-path packages/AgentCore --filter
    BootHealthOrchestratorTests: 8/8 PASS (0.210s)
  - swift test --package-path packages/Memory --filter
    MemoryExtractorTests: 11/11 PASS
  - bash scripts/check-app-builds.sh: PASS
  - 18/18 boundary gates: PASS

Dogfooding next: cold-launch + grep system.log for `BootHealth:
subsystem=` lines; force a failure (e.g. kill Ollama) and verify
banner appears. Status menu (Round 3) is next session task.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:03:24 -05:00