233 Commits
Author SHA1 Message Date
james.maesandClaude Opus 4.7 5ebe510627 docs: scrub Phase-7 + JarvisBusWorld doc drift (#19, #45) (#195)
* docs(memory): scrub Phase-7 hedge language from MemoryStore (#19)

Audit 2026-05-12 flagged five doc-comment references that mis-described
the implementation state. The cited plans (07-02, 07-03, 07-06) shipped
months ago — the bounded extraction channel, the hybrid FTS5 + vec0 read
path, the ReplayEvent emission, the regression corpus test seams, and
`installMemory`'s replay-sink wiring are all live, not "wires" or
"will replace" promises.

Five touchpoints rewritten to describe the code as it is:

- `MemoryReplaySink` doc: drop the "Plan 07-06" plan reference; the seam
  is just used at startup.
- `MemoryStore` actor doc: replace the "Plan 07-02 wires / Plan 07-03
  wires" sentence with the actual contract (single writer; hybrid search
  through `MemoryReadStore`; `recordRetrieval` emits `memoryRetrieval`).
- Read-path MARK: drop the "Plan 07-03" plan label; keep the
  requirement-ID list (MEM-07 / TEXT-03 / D-02 / D-05) since those are
  still useful for traceability to the audit corpus.
- `recordRetrieval` doc: replace "Mirrors recordMemoryMutation from 07-02"
  with "Mirrors recordMemoryMutation on the write path."
- Test-seams MARK: drop "Plan 07-06" prefix; describe by the test target
  (JARVIS_REAL_MODELS=1 corpus suite) instead.

The 2026-05-11 D-5/D-6 markers and the "Boot policy (changed 2026-05-11)"
doc are preserved unchanged — they document current behaviour, not
historical hedges.

`swift build --package-path packages/Memory` clean.

Closes #19

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(bus): scrub JarvisBusWorld drift + add grep gate (#45)

`fb41c5f` (2026-05-02) unified `Injection.js` and the HUD bundle into the
default `WKContentWorld.page` and retired the prior isolated
`JarvisBusWorld` named world. The doc-comments and test fixtures across
20+ lines kept describing the bug-era topology — pre-existing-bus attach
"because cross-world isolation", message-handler-only-visible-in-named-
world, etc. — for months afterward. Result: a new reader following the
docs would conclude the bus uses two worlds when it actually uses one.

Five rationale rewrites:

- `WebviewBridge.swift` class doc — handler is on the page world, not an
  isolated named world. Page scripts in the loaded HUD bundle address it
  via `window.webkit.messageHandlers.jarvisBus`.
- `Injection.js` header — names the page world explicitly; one-line nod to
  `fb41c5f` for the historical context.
- `bridge.ts` JSDoc + `isJarvisBusShape` rationale — recast the H-02
  attach-to-existing path as a "defensive double-install guard" inside
  ONE page world. Injection.js fires first at document-start; the bundle
  installer fires later as a top-level `main.tsx` side-effect; the guard
  prevents the bundle from clobbering Injection.js's queued early
  messages and captured handshake state. The duck-type `isJarvisBusShape`
  CODE is unchanged — only the comment narrative was misleading.
- `webview/packages/hud/src/main.tsx` — drop the cross-world-isolation
  framing; describe the single-world reality.
- `App/AppDelegate.swift` install comment — page world, not isolated.

Test-side updates (same scrub, plus one latent test bug surfaced):

- `WebviewBridgeTests.test_scriptMessageHandlerIsRegisteredUnderConfigured-
  Name`: was removing from `WKContentWorld.world(name: "JarvisBusWorld")`
  while the bridge registered against `.page` — silent no-op since
  `removeScriptMessageHandler` doesn't throw on absent. Now removes from
  `.page` (the world the bridge actually uses), making the
  "registration-is-evidence-of-success" assertion meaningful again.
- `WebviewBridgeOutboundTests`: comment-only.
- `RealWKWebViewIntegrationTests`: comment-only (forensic narrative kept,
  literal world name replaced with "prior isolated named content world").
- `webview/packages/bus/tests/bridge.test.ts`: H-02 describe-block + one
  it-name rewritten to match the new defensive-double-install rationale.

Infrastructure:

- `scripts/check-no-jarvis-bus-world-references.sh`: new boundary gate.
  Greps for the literal `JarvisBusWorld` in tracked files; excludes
  `.planning/`, `docs/`, `webview/packages/*/dist/`, and this script
  itself. Wired into `CLAUDE.md`'s gate listing; the CI
  `boundary-gates` job auto-discovers all `scripts/check-*.sh`, so no
  workflow edit is needed.
- `webview/packages/bus/package.json`: 0.0.0 → 0.0.1 (per spec —
  comment-only changes still bump patch so consumers can pin).
- `webview/packages/bus/dist/bridge.{js,d.ts}`: rebuilt via `pnpm --filter
  @jarvis/bus build`; NOT committed because `dist/` is not tracked (the
  build phase regenerates on each xcodebuild invocation).

Verified locally:
- `swift build --package-path packages/Bus` clean
- `pnpm --filter @jarvis/bus test` — 37/37 pass
- `bash scripts/check-no-jarvis-bus-world-references.sh` — PASS
- `bash scripts/check-bus-protocol-version.sh` — PASS (v2.3.0)
- `bash scripts/check-bus-harness-parity.sh` — PASS

Closes #45

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 09:51:03 -05:00
james.maesandClaude Opus 4.7 a121d79174 fix: STT backend constant casing + audioLevel NaN guard (#37, #47) (#196)
* docs(voice): pin STT backend constant identity contract (#37)

The runtime mismatch flagged by #37 was already fixed at the call sites
in `App/AppDelegate.swift:1631-1642` and `:2562-2575` — both routes now
reference `STTBackendSelector.backendSpeechAnalyzer` /
`backendWhisperKit` (snake_case) instead of the pre-2026-05-12 camelCase
string literals. The regression test
`STTBackendSwitchTests.testB5_canonicalConstantsRoundTripThroughMake`
already locks both halves of the contract:

  1. The snake_case constants round-trip through `make(backend:)` without
     hitting the unknown-backend default branch.
  2. The pre-fix camelCase shape (`"whisperKit"`) DOES land in the
     default branch and DOES emit an unknown-backend warning.

So the #37 close action is just to make that contract self-documenting
at the declaration site rather than relying on readers finding the test.
Add a doc-comment to the two `public static let backend*` constants that
explains the wire shape, names the regression test, references Issues
#29 + #37, and warns against renaming without updating `PerTurnSnapshot`
and the test in lockstep.

No code change; `swift test --package-path packages/Voice` continues to
pass (26 tests).

Closes #37

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(bus): clamp non-finite audioLevel rms to 0.0 at encode (#47)

`BusOutbound.audioLevel` carries a `Float rms` that `AudioLevelEmitter`
produces per audio frame (~100 Hz once the consumer is wired up).
Foundation's `JSONEncoder.NonConformingFloatEncodingStrategy` defaults
to `.throw`, so a single NaN — easy to land via a CoreAudio
over-/underflow or an all-zero buffer fed through certain RMS shapes —
would throw inside `WebviewBridge.sendRaw` and tear down the per-frame
bus path. The HUD-side consumer in `webview/packages/hud/src/bus/
client.ts:135` is currently a no-op (the ring shader is still on the
synthetic-sine animation from Plan 03-03), so the bug is LATENT, not
actively firing — but the clamp is cheap, defensive, and pins the
contract before the consumer activates.

Inline clamp at the encode site (`BusOutbound.encode(to:)` switch arm
for `.audioLevel`): replace non-finite floats with `0.0` via
`rms.isFinite ? rms : 0.0` immediately before `container.encode(_:)`.
No refactor of `AudioLevelEmitter` — the per-spec call here is
defensive at the serialization boundary, where every encoder-level
overflow path converges.

Two new unit tests in `CodableRoundTripTests`:
- `test_audioLevel_nanIsClampedToZero` — encodes `.audioLevel(rms: .nan)`,
  decodes, asserts `rms == 0.0` and `rms.isFinite`.
- `test_audioLevel_infinityIsClampedToZero` — companion for ±∞ so a
  future refactor cannot narrow the guard to NaN-only.

`swift test --package-path packages/Bus --filter CodableRoundTripTests`
passes (30 tests, +2 from this commit).

Closes #47

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 09:50:50 -05:00
james.maes 00f1075ea5 fix(agent): bundle assistant text with toolUse + document cache hint dormancy (#185)
Two related AgentCore tactical fixes from the 2026-05-12 audit.

#24 — Assistant text blocks alongside tool_use were dropped from message
history. Opus 4.7 commonly narrates ("Let me check the time for
you...") before calling a tool. On the next round-trip the appended
assistant message contained only the .toolUse block, so the model had
no record of the narration — a user follow-up like "what did you say
before checking the time?" would deny it.

Fix: introduce `assistantTextSinceLastFlush` parallel to
`assistantTextSoFar` (the latter stays cumulative for D-02 vision
routing). On every toolUseRequested, flush the per-flush buffer into a
.text(...) block bundled into the same assistant message as the
.toolUse, trim whitespace-only text to avoid Anthropic 400s on empty
text blocks, and reset the flush buffer so subsequent tool_uses in the
same round get only the text emitted between them. D-02 untouched.

#23 — CacheHints documentation flagged that the eligibility threshold
(4096 chars ≈ 1024 tokens) is structurally false on healthy boot
(preamble ~700 chars, empty initial context). Added a paragraph
explicitly documenting the dormancy, the intentional cost-acceptance
rationale, and the natural recovery once M-4 (v0.8) lands
searchFacts-driven hydration. No code change.

Tests: 3 new tests in AssistantTextWithToolUseTests cover text+toolUse
bundling, toolUse-alone (no spurious empty text block), and
whitespace-only-text rejection. Full AgentCore suite: 248 tests, 0
failures, 1 skipped.

Closes #23, #24
2026-05-14 06:53:35 -05:00
james.maes 5fc2d3fabf docs: reconcile Ollama model baseline to actual production names (#184)
The codebase moved to qwen3.6:latest (memory extractor) and
qwen2.5-coder:32b-instruct-q8_0 (agent fallback) in commit 8d0a959, but
CLAUDE.md and the MemoryRegressionCorpusTests doc-comments still cited
the vanilla qwen2.5-coder:32b string. Both updated to match what
ModelID.swift, MemoryExtractor.swift, and BootHealth.swift actually
use today.

No behavior change — documentation alignment only.

Closes #58
2026-05-13 16:25:00 -05:00
james.maes 53b8fb8f8b fix(voice): widen CI timing budgets for I3 + V4 (round 2) (#171)
Round 1 widening (#168) wasn't enough for macos-15. I3: stall 400→800ms, budget 250→500ms (CI took 263 vs 250). V4: window 500→1000ms, floor 4→3 (CI got 3 emissions in 500ms). Semantic preserved — I3 still asserts elapsed < stallDuration; V4 30Hz guarantee is enforced inside AudioLevelEmitter.start().
2026-05-13 09:36:15 -05:00
james.maes 928ce4bc61 fix(harness): move Corpora resource into Sources/Harness — fixes xcodebuild (#169)
Moves Corpora dir from packages/Harness/Corpora into packages/Harness/Sources/Harness/Corpora and updates the resource declaration from .copy("../../Corpora") to .copy("Corpora"). xcodebuild's strict package resolution was flagging the package-root location as unhandled despite the swift-test pattern working. Bundle.module layout unchanged; all call sites unaffected. 63/63 Harness tests pass locally.
2026-05-13 09:26:54 -05:00
james.maes e09d9090d0 fix(ci): Harness SQLite struct conflict + Voice cold-start timing budgets (#168)
Closes the remaining 3 CI failures after #167:

Harness + App target (cascade): two Harness files (ReplayMCPAdapter.swift, ReplayRunner.swift) still used 'import SQLite3' while their Replay dep had migrated to 'import CSQLiteVec' on 2026-05-11 (D-5/D-6). On Xcode 26 the SDK overlay's sqlite3_api_routines struct includes db_status64 + set_errmsg (SQLite 3.46+/3.51+) that CSQLiteVec's bundled sqlite3ext.h does not, producing 'struct sqlite3_api_routines is not present' errors. Aligning the imports matches the existing pattern; no brew SQLite install needed.

Voice 3 timing fixes for macos-15 cold-start: AVSpeechSmokeTests warms up AVSpeechSynthesizer in setUp() (cold init was 4.07s, budget 2s — assertion semantic preserved at 2s, just with a warm engine). TTSInterruptTests widens stall 100→400ms, budget 80→250ms (was 108ms in CI). VoiceControllerTests widens audio-level sleep 250→500ms (floor unchanged at ≥30Hz behavior).

All fixes verified locally on Xcode 26.5 / macOS 26.5 SDK. No continue-on-error shortcuts, no env manipulation.
2026-05-13 08:52:37 -05:00
james.maesandClaude Opus 4.7 bcc55c68be fix(ci): pin Xcode 26.x, drop phantom VoiceLog, harden Batcher timing (#167)
CI run 25799596445 failed on 6 SPM packages + the App target. Root cause
was a toolchain skew between local dev (Swift 6.3.2 / Xcode 26) and CI
(Swift 6.0 / Xcode 16):

- mlx-audio-swift v0.1.2 ships swift-tools-version:6.2 which Swift 6.0
  cannot parse, breaking Voice + Harness + the App target with
  "Dependencies could not be resolved because 'mlx-audio-swift' contains
  incompatible tools version (6.2.0)".
- MCP swift-sdk 0.12.0 uses the new `withThrowingTaskGroup` overload
  (no `of:` parameter) that only exists in Swift 6.2+.
- AgentCore tests use `@MainActor` XCTestCase with `await super.setUp()`,
  a pattern Swift 6.0 rejects but Swift 6.1+ accepts.

The macos-15 runner already ships Xcode 26.3 / 26.2 / 26.1 / 26.0
alongside 16.x, so the fix is to switch the Xcode select. Fallback to
Xcode_26.app preserves robustness against patch-level shuffles.

Also:
- Drop `VoiceLog` from the SPM matrix. No `packages/VoiceLog` directory
  exists; the job failed with `chdir error: No such file or directory`.
  Voice-log work is parked under v1.1.
- Bump BatcherTests `waitForDrain` from 80ms to 250ms. The batcher
  window is 33ms; 80ms was tight enough that macos-15 dropped
  test_tokenDeltaConcat with sendCount=0 because the scheduled drain
  hadn't completed its actor + main-actor hop before the test resumed.
  250ms (~7.5× the window) survives CI jitter; verified locally — all
  five Batcher tests pass in 1.27s.

Refs: CI run 25799596445.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 08:37:47 -05:00
james.maes 795c3bf5c9 feat(app): v0.1 UX surfaces — startup panels + InputMonitoring banner + PTT (#87, #88, #89, #156)
#89: new AppDelegate.startupSurfacesMode enum (.minimal/.diagnostic). Debug defaults .diagnostic; Release defaults .minimal. openStartupSurfacesIfDiagnostic() runs at tail of bootHealthTask (skipped under XCTest or when wizard is open). Status panel top-right + Dev Overlay bottom-right of NSScreen.main.visibleFrame. DevOverlay opens to Logs tab.

#88: BannerContent.inputMonitoringDenied is now nonDismissible: true. New HUDBannerCoordinator.dismiss(id:) clears a banner programmatically without writing dismissedThisLaunch. AppDelegate.reprobeInputMonitoring fires on NSApplication.didBecomeActiveNotification; rebinds the global hotkey on re-grant. Uses IOHIDCheckAccess (query-only, no prompt) per CLAUDE.md guidance. 3 new coordinator tests.

#87: WizardState.pttHotkey persisted under Jarvis.pttHotkey (independent of summon hotkey). Ships unbound per CLAUDE.md hotkey hygiene. AppDelegate.rebindPTTFromState() called from installVoice, foreground TCC re-grant, Settings Save/Clear. SettingsView extended with PTT hotkey section. Audio-graph isolation: NSEvent monitors only, no audio-graph surgery — safe alongside fix/voice-crit-v0.1. 3 new WizardState tests.

DevOverlay 14 + Voice 26 tests pass; all 17 grep boundary gates green (incl. single-writer-hudstate + no-modal-presentation).
2026-05-13 06:32:16 -05:00
james.maes bc74494ada fix(mcp): v0.1 CRIT cluster — turnID resolver wired + tool-result double-cap (#20, #21, #157)
#21 (CRIT): ToolResultPacker.pack(_:) is now a pass-through decoder; redundant inner 8 KB cap removed. MCP dispatcher's SanitizeForModel.prepareForBoundary(_, capBytes: 8192) is the sole cap site. Doc-comment rewritten honestly: replay row reflects boundary-prepared bytes (8 KB), not original helper output. Replay fidelity beyond 8 KB lifted to v0.5/M-3 once SanitizeForModel can move to a shared module without dep cycle.

#20 (CRIT): ReplayingToolResultObserver.turnIDResolver lock-protected via OSAllocatedUnfairLock<ResolverBox> (same Swift-6-async-safe pattern as InProcessConfirmationCache). New attach(turnIDResolver:) method. AgentOrchestrator.currentTurnID() actor-isolated accessor. installAgent attaches the resolver immediately after orchestrator assignment, with defensive systemLogger.warning if observer type mismatches.

Tests: ToolResultPackerTests TP2/TP5 updated to assert no [TRUNCATED] marker; OrchestratorSubmitTests OS6 reframed + new OS6b reads SQLite events table directly to assert 8 KB exact; MCPRuntimeWiringTests adds attach pre/post-attach + replay-log dual-write correlation.

AgentCore 243 + Memory 98 + Replay 34 + MCP all green; all 18 boundary gates green.
2026-05-13 06:31:46 -05:00
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 f59f5d12e3 fix(memory): v0.1 CRIT cluster — facts_vec / replay correlation / FTS5 sanitize / search_memory preamble (#12, #13, #14, #15, #16, #153)
#14 (CRIT): ContextBuilder.selfAwarePreamble names search_memory + forget_fact with explicit cue 'call search_memory FIRST. Never claim to remember without searching.'

#16 (HIGH): MemoryQueries.sanitizeFTS5Query strips non-alphanumeric, quotes tokens; HybridSearch.searchFacts calls it before binding to MATCH ?. Embedding still runs on raw query.

#12 (CRIT): MemoryStore.init accepts optional embedder; insertNewFact writes facts_vec after the facts commit. Vec failures log + leave FTS row (no rollback). installMemory shares one embedder between writer + reader.

#15 (HIGH): drop stableHash; pass sentinel 0 as sourceTurnId (option 1 per audit — smaller change surface than threading real rowid through 5 files). Schema comment updated.

#13 (CRIT): two-layer fix. Layer 1: AppDelegate owns a per-process sentinel TurnID; installAgent writes the matching turns row after beginSession; AppDelegateMemoryReplaySink attributes events to it. Layer 2: ReplayLog.flushPending retries failed batches event-by-event so one poisoned row only drops itself.

Memory 98 + Replay 34 + AgentCore 243 tests pass; all 18 boundary gates green.
2026-05-13 06:30:35 -05:00
james.maes 05411f82ff fix(vision): v0.1 cluster — T2 honesty + pendingFrame leak + TCC revoke observation (#1, #2, #3, #152)
#1 (HIGH): new hasRealT2Provider init param on AgentOrchestrator; t2AvailableForThisTurn now requires both decision.tier != .t3Cloud AND hasRealT2Provider. Production passes false (MissingT2Provider only). Pre-existing escalation test still passes true with mock T2.

#2 (MED): new FrameAttachController.releaseAfterRejectedSubmit() routes through the sole discardFrame() emission site (D-15 single-emission grep gate still green). Both handleChatSubmit and handleChatCancelAndSubmit call it on .rejected.

#3 (MED): AVCaptureSessionWasInterruptedNotification observer yields .midSessionRevoked. macOS limitation surfaced: AVCaptureSessionInterruptionReasonKey is API_UNAVAILABLE(macos), so we cannot discriminate by reason — any interruption surfaces the banner. NSApplication.didBecomeActive observer calls becameAuthorized() on non-authorized → .authorized flip.

Vision 75 + AgentCore 243 tests pass; all 18 boundary gates green.
2026-05-13 06:30:09 -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.maes 6ea2852af1 feat(devoverlay,logging): live log stream — every swift-log line into the overlay
Slice 3 of dev-overlay-end-to-end. Adds a fourth DevOverlay tab that
tails every log record from every Jarvis subsystem in real time —
agent, voice, vision, memory, MCP, bus, replay, system — without
polling files or relying on Console.app.

How it works:

1. `LogBroadcaster` (new) — process-wide actor with a 500-line ring
   buffer + subscriber set. Borrowed shape from
   OrchestratorEventBroadcaster so the project's three broadcasters
   share a mental model. New subscribers replay the buffer in order,
   then go live.

2. `BroadcastLogHandler` (new) — swift-log LogHandler conformance.
   Formats each log call into a `LogLine` and schedules an unstructured
   `Task { await LogBroadcaster.shared.publish(...) }` so the logging
   caller never blocks on actor isolation. Synchronous handler contract
   preserved; ordering is best-effort under heavy load.

3. `LoggingBootstrap.make(label:)` now returns
   `MultiplexLogHandler([file, os, broadcast])` — the new handler is
   purely additive. File rotation + os.Logger streaming unchanged.

4. `DevOverlayLogsView` (new) — fourth tab in the overlay. Subscribes
   to `LogBroadcaster.shared` on appear, caps at 1000 lines, renders
   monospaced. Per-channel filter (Menu w/ checkboxes), per-level
   filter (debug → critical), substring search, Pause/Resume button
   (Cmd+P), auto-scroll-to-bottom while not paused. Color-coded by
   level (warn=orange, error/critical=red).

Tests:
- 5 new LogBroadcasterTests covering publish/cap/replay/live/handler.
- swift test --package-path packages/Logging → 22/22 pass.
- swift test --package-path packages/DevOverlay → 14/14 pass.
- bash scripts/check-app-builds.sh → PASS.
- 18/18 boundary gates green.

Redaction note: file handler still routes through Redact.apply for
disk writes; the broadcaster sees raw messages because the DevOverlay
is a developer-only in-process surface. If this ever becomes
user-facing, route through Redact before publishing.
2026-05-12 14:27:57 -05:00
james.maes 50c2783a8c feat(devoverlay): wire DevSnapshotEmitter end-to-end + add tabbed view
The DevOverlay panel has been silently rendering DevSnapshot.initial since
Phase 4 because three independent breaks left the data path open-circuit
(see .planning/audit-2026-05-12/devoverlay-diagnosis.md for the full trace).

Slice 2 closes all three:

1. AppDelegate constructs `DevSnapshotEmitter` in `installAgent` and
   stores it on the delegate. Provider/model is set from configStore
   so the header row shows the active provider rather than a blank.

2. The `.devOverlay` broadcaster subscriber that previously drained to
   `/dev/null` now calls `emitter.apply(event)` for every event. This
   produces real DevSnapshots that flow through the bounded output
   channel (capacity 32, .dropOldest — AGENT-10 observational seam).

3. `DevOverlayWindow` now accepts the live emitter + BootHealthOrchestrator
   + a HudState reader closure. It instantiates its own DevOverlayBridge
   and attaches the bridge to `emitter.output` so viewModel.apply is
   actually called as snapshots arrive.

Also reshapes the SwiftUI surface as a TabView with three observational
panes (Agent, Tools, Health). The Health pane is read-only — the Status…
menu remains the place to trigger re-probes. The Logs pane lands in
Slice 3 (live log stream).

ViewModel extends with `bootHealth` + `hudStateLabel` slices, each with
its own single-writer mutator and `private(set)` enforcement.

Verification:
- swift test --package-path packages/DevOverlay → 14/14 pass
- swift test --package-path packages/AgentCore --filter DevSnapshotEmitterTests → 9/9 pass
- bash scripts/check-app-builds.sh → PASS
- 18/18 boundary gates green
2026-05-12 14:11:19 -05:00
james.maesandClaude Opus 4.7 a5999e2dde fix(memory): drop broken FK on facts.source_turn_id — closes Toby-fact persistence
Root cause of "facts never persist" surfaced during 2026-05-12 dogfood:

  ERROR [memory.orchestrator] memory applyOp failed: applyOpFailed(
    underlying: Replay.SQLiteError.stepFailed(
      code: 19, message: "FOREIGN KEY constraint failed"))

The schema declared:
  source_turn_id INTEGER REFERENCES turns(id)

But the writer at MemoryExtractionOrchestrator.swift:125 inserts:
  let turnIdInt: Int64 = stableHash(job.turnId.rawValue)

`TurnID` is a UUID-string wrapper; the writer FNV-1a-hashes it to an
Int64 for the integer column. The hash space and `turns.id`
(AUTOINCREMENT) are disjoint number systems by design, so every
INSERT into `facts` violated the FK and SQLite rolled back the row.

The BootHealth probe reports the symptom honestly ("26 turns, 0/0
facts") but couldn't flag this as a failure — 0 facts is the
legitimate empty-history case. The bug stayed invisible until the
user asked "is Toby a fact in the database?" and search returned
nothing despite asynchronous extraction running.

This bug was latent for the entire project history — the original
RESEARCH §1 schema specified the FK, but the writer was never wired
to use real `turns.id` rowids. Reintroducing the FK without first
fixing the writer (forward through MemoryStore.appendTurn returning
the rowid, then plumbed back to MemoryExtractionCoordinator) would
just re-break persistence.

Fix: drop the REFERENCES on `source_turn_id`. The column is now
documented as a "correlation tag", not a hard foreign key. The
`superseded_by REFERENCES facts(id)` constraint stays — that one
IS valid because supersede chains use `priorFact.id` (a real
AUTOINCREMENT rowid).

Existing DBs: `CREATE TABLE IF NOT EXISTS` won't ALTER an existing
table; the user must delete `~/Library/Application Support/Jarvis/
jarvis.db*` and relaunch. Their facts table has 0 rows so nothing
of value is lost. A proper migration framework (PRAGMA user_version)
is deferred to M-1+.

Follow-up (M-1+): plumb the real `turns.id` rowid through
`MemoryStore.appendTurn` → orchestrator state → coordinator →
applyOp so `source_turn_id` becomes a real FK again. Requires
changes across 4 files; not in scope for this hotfix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 12:07:44 -05:00
james.maesandClaude Opus 4.7 beb9d51aa5 test(agent-core): update qwen2.5-coder literal to q8_0 variant
Two test assertions still expected `"qwen2.5-coder:32b"` after commit
8d0a959 flipped ModelID.qwen25coder32b to
`"qwen2.5-coder:32b-instruct-q8_0"`. Same shape of fix as commit
5718bdd (qwen36 rename). The other "qwen2.5-coder:32b" occurrences
in NDJSONDecoderTests and OllamaImageBlockEncodingTests are
fixture JSON strings used as decoder input — not asserted against,
so they don't break.

After:
  swift test --package-path packages/AgentCore --filter LLMEventTests
  → 10/10 pass
  swift test --package-path packages/AgentCore --filter OllamaProviderTests
  → 38/38 pass

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 11:14:07 -05:00
james.maesandClaude Opus 4.7 23b4ca7712 feat(round-4): agent system prompt knows when subsystems are degraded
Slice 3 of Round 4 — the Toby-the-dog fix. With Slice 1 the probes
report severity and with Slice 2 banners route by severity, but the
critical behavioral change is here: the agent's system prompt itself
must carry the degradation truth so the model can't confidently lie
about features that don't work.

The Toby case in detail: user said "remember my dog Toby is a golden
retriever". The Ollama probe correctly reported missing nomic-embed-text
(degraded). The memory extractor tried to embed Toby, the embed call
failed, no row was inserted in `facts`. But the agent's system prompt
said "you are Jarvis with memory" and the model answered "Got it —
Toby." A lie, dressed up as competence.

Implementation:

- `AgentOrchestrator` gains a private `var degradationSummary: String?`
  + a public `setDegradationSummary(_:)` actor-isolated setter. Mutable
  because installAgent runs BEFORE runBootHealth in the boot order;
  the orchestrator is constructed with `nil` and AppDelegate pushes
  the summary in post-construction. Re-probes from the Status panel
  update the value mid-session.
- `runTurn` prepends the summary BEFORE `composedSystem` (which already
  includes the UntrustedWrapper nonce + locked self-aware preamble),
  so the warning lands at the very top of the system message — outranks
  the preamble, outranks presence/memory enrichment.
- `AppDelegate.runBootHealth` now calls
  `orchestrator.setDegradationSummary(buildDegradationSummary(snapshot:))`
  after every probe sweep. The builder produces text like:

    CRITICAL — SUBSYSTEMS DEGRADED:
    - memory: failed (vec0 missing) — DO NOT promise to use memory
    - ollama: degraded — missing models: nomic-embed-text
    Be honest. Do not claim to use tools that are unavailable. Do not
    pretend memory works if it doesn't.

  When every subsystem is `.ok`, the function returns `nil` and the
  orchestrator's behavior is identical to pre-Round-4.

- Cache-hints invariant preserved: the locked preamble is ~700 chars,
  the worst-case degradation summary is <500 chars; sum stays well
  under the 4096-char Anthropic cache-eligibility boundary so adding
  the summary doesn't unintentionally flip `cache_control` emission.
  CacheHintsEligibilityTests/preambleDoesNotEnableCacheUnintentionally
  remains green.

4 new tests in AgentOrchestratorDegradationPreambleTests (all pass):
  * DP1: initial summary threaded into first turn's system message
  * DP2: nil summary leaves prompt unchanged (no "CRITICAL" / "DO NOT")
  * DP3: setDegradationSummary updates the next turn's prompt
  * DP4: setting summary back to nil clears it from the next turn

Boundary gates 18/18 green. `swift test --package-path packages/AgentCore`
runs 242/240 (4 new minus 2 pre-existing unrelated failures from
qwen2.5-coder model-ID literal drift, present before Round 4).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 15:47: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
james.maesandClaude Opus 4.7 5718bdd820 test(memory): rename qwen25Coder32B test to qwen36
Matches commit 8d0a959 model flip — extractor default is now
ModelID.qwen36 ("qwen3.6:latest"). The previous test name + assertion
were stale and failed on `swift test --package-path packages/Memory`
with the "Optional(qwen3.6:latest)) is not equal to (qwen2.5-coder…)"
diff. No production code change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:02:57 -05:00
james.maesandClaude Opus 4.7 8d0a959a19 feat(model): qwen3.6 for memory extractor; q8_0 for agent-loop fallback
Round 1 of the post-D-5/D-6 model strategy split. The user has both
`qwen3.6:latest` (~22GB MoE) and `qwen2.5-coder:32b-instruct-q8_0` (~32GB)
pulled. CLAUDE.md previously pinned the entire local-model strategy to
the base `qwen2.5-coder:32b` because of an April 2026 Ollama tool-call
bug affecting Qwen 3 / 3.5 / Gemma 4. That ban was correct then; today
(2026-05-11) it isn't.

**Empirical retest 2026-05-11:** POST /api/chat (non-streaming) against
qwen3.6:latest with a representative `apply_memory_ops` tool definition
returned a clean tool_calls array, well-formed JSON args, no leaked
<think> tags. Upstream Ollama appears to have closed the relevant
issues (#14493 / #14601 / #14745 / #15315) in the interim. Documented
in ModelID.qwen36 doc comment.

Two-model strategy:

- **MemoryExtractor default → qwen3.6:latest** (`ModelID.qwen36`)
  Single-turn, single-tool, no cap recovery — the lowest-risk place to
  validate the newer model. If extraction stays stable for a few days
  of dogfooding, we revisit the agent-loop fallback.
- **Agent-loop Ollama fallback → qwen2.5-coder:32b-instruct-q8_0**
  (`ModelID.qwen25coder32b`'s rawValue is now the Q8_0 variant the user
  already has — higher fidelity than the default Q4_K_M, same model
  family, same tool-call wire format). The agent loop has multi-turn
  tool chains, cap recovery, tool_choice=none paths, and streaming all
  in scope — kept conservative until Qwen3.6 is verified there too.

Changes:
- packages/AgentCore/Sources/AgentCore/ModelID.swift
    + `qwen36` constant for qwen3.6:latest with empirical retest doc.
    ! `qwen25coder32b` rawValue is now `qwen2.5-coder:32b-instruct-q8_0`
      (same constant name, the Q8_0 raw value). All ModelID.qwen25coder32b
      consumers pick up the change automatically.
- packages/Memory/Sources/Memory/MemoryExtractor.swift
    - `model: ModelID = .qwen25coder32b` → `.qwen36`.
    Docstring updated to explain the empirical retest + why the
    extractor is the safest place for the newer model.
- packages/Harness/Sources/Harness/Runners/LiveOllamaRunner.swift
    `requiredModelID` constant now sources from
    `ModelID.qwen25coder32b.rawValue` so a future single-edit on the
    canonical model name propagates here automatically. Doc updated to
    point at the q8_0 variant + ~32GB pull size.

User-facing note: no model pulls required for this commit; both
models are already on the user's machine.

Round 2 (boot-phase health check) and Round 3 (Status menu dialog)
deferred to a fresh session for context budget — handoff doc to follow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 10:36:30 -05:00
james.maesandClaude Opus 4.7 cb584a2ac8 feat(d5d6): real memory database — vec0 statically linked, hard-fail boot, stats tool
Closes D-5 / D-6 (sqlite-vec dylib bundling) carry-forwards from the
2026-05-04 audit by adopting jkrukowski/SQLiteVec as a SPM dependency.
The package's CSQLiteVec target compiles sqlite3.c + sqlite-vec.c into
one statically-linked C target with `core_vec_init()` registering vec0
as an auto-extension. All sqlite3_* calls in Replay's SQLiteConnection
and Memory's MemoryStore resolve to this bundled SQLite (vec0 always
available) instead of Apple's stripped /usr/lib/libsqlite3.dylib.

The retired hand-rolled approach (dlsym pathway + JARVIS_VEC0_STUB_PATH
env-flag + Bundle.module placeholder text file) never worked on macOS
because Apple stripped sqlite3_load_extension from the system libsqlite3.
Memory init has been silently dormant since Phase 7 (2026-04 onward),
which is what made B-02 (history threading) appear permanently broken
to the user during dogfooding even after the 48ad8d6 tactical patch
landed.

Live verification 2026-05-11: fresh launch with empty jarvis.db creates
schema, persists turn rows across submits, B-02 multi-turn context
resolves correctly ("what breed is Toby?" → "Golden Retriever" after
prior turn established Toby is a Golden Retriever), and the new
get_memory_stats tool reports `turnsCount=6, vecVersion=v0.1.6,
sqliteVersion=3.51.0`.

Changes:

- packages/Replay/Package.swift
    + SQLiteVec SPM dependency (from: "0.0.14")
    + CSQLiteVec product wired into Replay target
- packages/Replay/Sources/Replay/SQLiteConnection.swift
    - import SQLite3
    + @_exported import CSQLiteVec
    All sqlite3_* call sites unchanged — linker now resolves to our
    bundled sqlite3 with vec0 statically linked in.
- packages/Memory/Package.swift
    + SQLiteVec SPM dependency (named product so MemoryStore can import
      CSQLiteVec directly for the core_vec_init() one-shot call)
    - resources: PLACEHOLDER.txt copy (defunct vec0.dylib stub slot)
- packages/Memory/Sources/Memory/MemoryStore.swift
    - import SQLite3 → import CSQLiteVec
    + _vecAutoExtensionRegistered static-let one-shot: calls
      CSQLiteVec.core_vec_init() which registers vec0 via
      sqlite3_auto_extension on the first MemoryStore construction.
      Idempotent (static-let initializer fires exactly once).
    - loadVecExtension(on:) dlsym/JARVIS_VEC0_STUB_PATH/Bundle.module
      pathway (retired; ~70 lines removed)
    Updated docstring to reflect the new hard-fail boot policy.
- packages/Memory/Sources/Memory/Resources/PLACEHOLDER.txt — deleted
- App/AppDelegate.swift installMemory()
    Reverses Track-D D-1 (75a10be, 2026-05-04): MemoryStore.init failures
    are now FATAL via Logger.critical + TCCAlertService.presentHardBlock
    + NSApp.terminate. The graceful-degradation path that kept the app
    booting with memoryStore=nil hid the entire B-02/B-04/B-05 silent-
    failure bug class. Aligns with the 2026-05-07 API-first pivot
    thesis: "no silent failures".
    `store` is now non-optional throughout installMemory; downstream
    callers no longer need `if let store = store`.
- packages/MCP/Sources/MCP/InProcess/GetMemoryStatsTool.swift (new)
    + MemoryStatsDispatching protocol
    + MemoryStats Codable struct (turnsCount, factsTotal, factsActive,
      sessionsCount, dbFileBytes, vecVersion, sqliteVersion,
      lastTurnAtUnixMs, lastFactAtUnixMs)
    + GetMemoryStatsTool conforming to InProcessTool
- App/MCP/InProcessMemoryAdapters.swift
    + MemoryStatsStoreAdapter — bridges the protocol to MemoryStore's
      public queryRowCount / querySingleString seams and FileManager
      for the on-disk file size. Read-only, no confirmation.
- App/AppDelegate.swift installSelfKnowledgeTools()
    + Registers GetMemoryStatsTool with MemoryStatsStoreAdapter.
- packages/Memory/Tests/MemoryTests/*
    Retired the JARVIS_VEC0_STUB_PATH XCTSkipUnless guards from 9 tests
    (vec0 is always available now — no need to env-gate). Rewrote
    MemoryStoreTests to drop the now-impossible "init throws when vec
    missing" case and assert facts_vec virtual table creation +
    vec_version() readback. Added seedTurnRow helper to
    MemoryStoreApplyOpTests so the now-enforced
    `facts.source_turn_id REFERENCES turns(id)` foreign key is honored
    in tests that previously passed when FK enforcement was dormant.

Test summary: Memory full suite 91 tests, 10 skipped (env-gated real-
Ollama tests), 0 failures. App target builds clean. Substrate verified
end-to-end via live launch (vec_version: v0.1.6 logged at boot;
get_memory_stats returns actual schema row counts).

Pending D-7 carry-forward: ollama pull nomic-embed-text (~270MB) for
embedder; user already has qwen2.5-coder:32b-instruct-q8_0 which is
a higher-quality variant of the project's hardcoded
qwen2.5-coder:32b — small ModelID config delta to follow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 10:25:56 -05:00
james.maesandClaude Opus 4.7 48ad8d631c fix(b-02): thread recent turns into AgentOrchestrator messages array
Carry-forward bug B-02 (live evidence 2026-05-07): conversation
continuity broken — "yes" / "why 4.5?" lose prior turn context because
AgentOrchestrator.process(_:) built [system, user(input.userText)]
with no history fetch.

Tactical patch outside the M-0..M-7 migration sequence, per
JARVIS-API-MIGRATION-PLAN.md §3 and UQ-2 (option B). Lands on develop
directly. M-4 (Memory surface) will reframe this path against the new
API contract but the user-visible bug closes today.

Substrate gap discovered + addressed in-scope. The `turns` table was
unwritten (schema present at MemorySchema.swift, FTS5 trigger ready,
read API at MemoryStore.recentTurnsForSession, but no INSERT site
anywhere in the codebase). The migration plan assumed turn writes
existed; they did not. Patch adds the missing writer plus the read
side wiring.

Changes:

- packages/Memory/Sources/Memory/MemoryQueries.swift
    + turnInsertSQL — single-row INSERT into turns
- packages/Memory/Sources/Memory/MemoryStore.swift
    + appendTurn(sessionId:role:content:source:createdAt:)
    Single-row INSERT; FTS5 trigger mirrors content into turns_fts
    automatically. T-06-05-03: function MUST NOT log content.
- packages/AgentCore/Sources/AgentOrchestrator/AgentOrchestrator.swift
    + PriorTurn struct + SessionHistoryLookup typealias
    + emptySessionHistoryLookup default (preserves pre-B-02 test setups)
    + sessionHistoryLookup ctor parameter
    Replaces the [system, user] messages-array build with prior-history
    hydration: system + chronological prior user/assistant rows + current
    user. Lookup throws -> warning log + empty history (matches D-3
    PriorFactsLookup resilience contract).
- App/AppDelegate.swift
    + sessionHistoryLookup closure: recentTurnsForSession(limit: 10),
    reverse DESC -> chronological, filter to user/assistant only, return
    [PriorTurn]
    + extends existing turnContent flush closure with appendTurn calls
    (rides on memory-extraction drain path; assistant row offset +1 ms
    so DESC + id-tiebreaker preserves user->assistant order)

Tests:

- packages/AgentCore/Tests/AgentOrchestratorTests/HistoryThreadingRegressionTests.swift
    HT-1: third turn includes prior user/assistant pairs in chronological
    order. 3/3 PASS.
    HT-2: empty-history default produces [system, user] (pre-B-02
    baseline preserved).
    HT-3: three-turn happy path emits zero LLMProviderError
    .streamTruncatedFinal events. Pins R-006 / Phase E gate behavior:
    patch grows messages array, not finalSystem, so
    CacheHints.eligibleForSystemPrompt(finalSystem) is unchanged.
- packages/Memory/Tests/MemoryTests/MemoryStoreAppendTurnTests.swift
    AT-1: appendTurn round-trips via recentTurnsForSession.
    AT-2: user/assistant pair reverses to chronological.
    AT-3: session_id scoping isolates rows.
    AT-4: turns_fts trigger fires on append.
    All env-gated on JARVIS_VEC0_STUB_PATH per existing MemoryStore-test
    convention; skip cleanly without vec0.dylib.

Verification:

- check-app-builds.sh PASS.
- Boundary gates sampled (check-no-leftover-stubs, applescript-confirm,
  corpus-secrets, bus-protocol-version, single-memory-mutated-emit,
  single-writer-hudstate) PASS.
- AgentCore affected-subset (Submit/Retry/TextEnd2End/CapRecovery/
  HistoryThreading) 26/26 PASS, 0 failures.
- Memory full suite 92 run, 23 skipped (env-gated), 0 failures.

Degradation contract: when MemoryStore is nil (dormant memory — vec0
or Ollama unavailable), turn persistence is also dormant; the
appendTurn call is guarded by memoryStore != nil. When D-5/D-6 close
and memory comes up live, persistence activates without further code
changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 08:50:40 -05:00
james.maes 0961fef40b feat(10-02c): add InProcessAwareToolDispatcher composite for B-01b dispatch routing
- Composite ToolDispatcher: name-routes between InProcessToolRegistry (memory
  + four self-knowledge tools) and inner stdio dispatcher (MCPToolDispatcher).
- Sits INSIDE ConfirmingToolDispatcher's chain so confirmation gating, bus
  emission, and observer hooks fire identically on both paths.
- In-process result bytes flow through the same SanitizeForModel.prepareForBoundary
  + ToolResultObserver pipeline as stdio results (audit-trail parity).
- Tools registered AFTER composite construction are still routable because
  the composite holds the registry actor reference and queries by name on
  every dispatch (resolves the installSelfKnowledgeTools-runs-after-installAgent
  install-order wrinkle).
- Closes 5/5 cases in InProcessAwareDispatchRoutingTests.
2026-05-07 11:19:13 -05:00
james.maes 6503b23301 feat(10-02c): add InProcessConfirmationCache for sync confirmation lookup
- Lock-protected Set<String> mirror of registered tools' requiresConfirmation flags.
- InProcessToolRegistry.register(_:) now writes through to the cache so the
  upcoming InProcessAwareToolDispatcher can answer requiresConfirmation(toolName:)
  synchronously from a nonisolated context (ToolDispatcher protocol contract).
- OSAllocatedUnfairLock: low-overhead, available since macOS 13.
2026-05-07 11:19:09 -05:00
james.maes 3cf0d396ea test(10-02c): add InProcessAwareDispatchRoutingTests for B-01b regression coverage
- 5 cases at the dispatch routing boundary: in-process route, stdio fallthrough,
  confirmation gate preservation, late-registration timing, observer parity.
- All FAIL until InProcessAwareToolDispatcher composite + InProcessConfirmationCache land.
- Bug: B-01 wired catalog enumeration (10-02b) but missed dispatch routing —
  MCPToolDispatcher only knows the 3 stdio tools, so when the model emitted
  tool_use for get_active_audio_route, dispatch failed and the failure
  surfaced as visible chat text.
2026-05-07 11:15:54 -05:00
james.maesandClaude Opus 4.7 3eeb3c178e feat(10-02b): add availableToolsResolver for late-bound tool catalogs
Plan 10-02b / B-01: AgentOrchestrator gains an optional
`availableToolsResolver: (@Sendable () async -> [ToolSchema])?` init
parameter that, when set, supersedes the static `availableTools:`
array on every outer-loop iteration of runTurnLoop.

The static array continues to work for tests (and is the default), but
production wires the resolver so the in-process registry's late-bound
self-knowledge tools (registered AFTER installAgent runs the
orchestrator constructor) reach the request body's tools[] payload.

Without this, the install-order dependency chain
  installMemory → installVision → installAgent → installVoice
                                              → installSelfKnowledgeTools
would force the orchestrator to capture an empty snapshot of the
in-process registry at construction time. By the time the user submits
their first turn, the four self-knowledge tools have registered — but
the orchestrator's frozen array would not see them.

The resolver is invoked once per outer turn iteration (not per stream
event), so the cost is one actor hop per LLM round-trip — negligible.

Test coverage:
- test_availableToolsResolver_supersedesStaticArrayPerTurn: seeds the
  resolver with empty on turn 1, populates 4 tools before turn 2, and
  asserts the provider sees the correct snapshot for each turn.
- All 222 AgentCore tests pass unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 07:37:01 -05:00
james.maesandClaude Opus 4.7 043402c670 feat(10-02b): retain stdio Tool defs in MCPClient + add toolCatalog()
Plan 10-02b / B-01: MCPClient previously discarded the Tool definitions
returned by each helper's tools/list response, retaining only the
toolToServer name→server lookup. The Anthropic / Ollama provider
request bodies need name + description + inputSchema for each stdio
tool, but with the definitions lost there was nothing to feed the
catalog wiring at App/AppDelegate.swift:1274.

Changes:
- MCPClient gains a private `toolDefinitions: [String: Tool]` cache
  populated from each helper's listTools() response alongside
  toolToServer.
- New `toolCatalog() -> [ToolSchema]` re-encodes Tool.inputSchema
  (Value, JSON-encodable) to bytes and projects to the AgentCore
  ToolSchema shape. Empty descriptions degrade to a placeholder
  ("MCP tool '...' (no description provided by helper).") so we
  never drop a tool from the catalog.
- shutdown() also clears toolDefinitions (lifecycle parity with
  toolToServer / registry).
- Imports AgentCore for ToolSchema (asymmetric dep already declared
  in Package.swift; mirrors MCPToolDispatcher.swift's usage).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 07:33:28 -05:00
james.maesandClaude Opus 4.7 c5c5072b5d feat(10-02b): add toolDescription to InProcessTool + toolSchemas() projection
Adds the substrate that the production fix at App/AppDelegate.swift:1274
needs to source the tool catalog from the in-process registry instead of
the hardcoded empty array.

Protocol change:
- InProcessTool gains `var toolDescription: String { get }` — a
  human-readable one-line description surfaced in the Anthropic / Ollama
  tools[] payload's `description` field. Named `toolDescription` (not
  `description`) to avoid colliding with CustomStringConvertible.

All 7 production InProcessTool implementations updated with descriptions
calibrated to drive correct tool selection by the model:
- list_audio_devices, get_active_audio_route, get_self_state,
  list_camera_devices (Phase 10-01 self-knowledge tools)
- search_memory, forget_fact, search_conversation (Phase 7 memory tools)

Registry change:
- InProcessToolRegistry.toolSchemas() projects every registered tool to
  a ToolSchema(name, description, inputSchema) suitable for the LLM
  request body. Returns name-sorted for prompt-cache determinism.
- Adds AgentCore import to InProcessToolRegistry for the ToolSchema type.

Test coverage:
- InProcessToolRegistrySchemasTests asserts all 7 tools project
  correctly with non-empty descriptions and inputSchema bytes.
- All 85 existing MCP tests pass unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 07:28:20 -05:00
james.maesandClaude Opus 4.7 260350b3dc test(10-02b): add AvailableToolsWiringTests regression for B-01
The 2026-05-07 live verification of Plan 10-02 surfaced that
App/AppDelegate.swift:1274 constructed AgentOrchestrator with a
hardcoded availableTools: [] empty array. The model saw the system
prompt naming tools but had no schemas in the API tools[] field, so
it hallucinated JSON tool definitions into chat instead of dispatching
real tool_use blocks.

These tests assert at the orchestrator → provider boundary: schemas
passed to AgentOrchestrator(availableTools:) MUST reach the
LLMProvider.stream(... tools:) call on the first turn. The previous
in-process registry test asserted at the wrong layer (registry was
correctly populated, but its catalog never reached the orchestrator).

Both tests pass at HEAD because the orchestrator's wiring of
availableToolsList → toolsForCall → provider.stream is sound. They
exist to catch any future regression at this layer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 07:18:45 -05:00
james.maes f22bf2ea3b feat(10-02): add self-aware preamble + ContextBuilder.systemPrompt(for:)
Replaces the hardcoded "You are Jarvis, a personal macOS assistant."
literal at App/AppDelegate.swift:1265 with a locked self-aware preamble
emitted by AgentCore's new ContextBuilder. The preamble names Jarvis,
lists capabilities (mic, camera, speakers), names the seven introspection
tools (list_audio_devices, get_active_audio_route, get_self_state,
list_camera_devices, get_time, get_clipboard, run_applescript) so the
model has handles, and ends with "You are not a text-only assistant."
Closes the 2026-05-05 / 2026-05-06 user-quoted failure modes.

Implements:
- ContextBuilder.selfAwarePreamble: locked Swift string constant (D-13).
  No JSON file, no localization, no runtime mutation. Length ~660 chars,
  well under the 4096-char cache-eligibility boundary (Pitfall #4 / A4).
- ContextBuilder.systemPrompt(for:): composes preamble FIRST, then
  presence enrichment, then memory hydration (D-12 — two distinct
  concatenation steps preserve cache_control breakpoints).
- TurnContext envelope with .empty static for the AppDelegate site
  (per-turn enrichment plumbing through orchestrator init is out of
  scope per D-12 fallback; AppDelegate calls .systemPrompt(for: .empty)).
- AppDelegate.swift orchestrator init now sources systemPrompt from
  ContextBuilder.

Rule 3 deviation (auto-fix blocking issue): the new
AgentCore.ContextBuilder name-collides with the pre-existing
JarvisVision.ContextBuilder (cloud-opt-in / frame-attach detector) at
two call sites that previously used bare `ContextBuilder()`. Both call
sites disambiguated to `JarvisVision.ContextBuilder()`:
- packages/AgentCore/Sources/AgentOrchestrator/AgentOrchestrator.swift:212
- App/AppDelegate.swift:1620
And the dependent test:
- packages/AgentCore/Tests/AgentOrchestratorTests/AgentOrchestratorVisionDispatchTests.swift:209

Wave-0 test preambleDoesNotEnableCacheUnintentionally flips RED→GREEN.
All 17 boundary gates green; check-app-builds.sh PASS.

Closes SELF-05, SELF-06 (Plan 10-02 implementation tasks).
2026-05-06 18:10:27 -05:00
james.maesandClaude Opus 4.7 4249bc27ab docs(mcp): list Phase 10 self-knowledge tools in package README
- Split InProcess registry rows: contract / registry / memory tools /
  unwired SearchConversationTool / four self-knowledge tools (74b9ac2)
- Note App-side dispatcher adapters in InProcessSelfStateAdapters.swift
- Notable files: list the four new InProcess tool sources

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 16:11:31 -05:00
james.maes 5bb9371e39 test(10-02): add Wave-0 RED preambleDoesNotEnableCacheUnintentionally
VALIDATION map row SELF-06 / T-10-CACHE-01. Asserts the soon-to-land
ContextBuilder.selfAwarePreamble does not flip cache-marker emission
relative to the prior literal "You are Jarvis, a personal macOS
assistant." (Pitfall #4 / Assumption A4 / PATTERNS AP-04), enforces the
D-13 lock anchor (hasPrefix "You are Jarvis"), the D-14 no-tool-catalog
constraint (no Markdown triple-backtick), and the D-12 composition
order (selfAwarePreamble FIRST in systemPrompt(for:)).

RED: ContextBuilder symbol does not exist yet — fails to compile.
2026-05-06 16:08:11 -05:00
james.maes 74b9ac2ff5 feat(10-01): wire four self-knowledge MCP tools into live runtime
App-side adapters bridge JarvisMCP's self-knowledge dispatcher protocols
to concrete runtime sources, mirroring the InProcessMemoryAdapters.swift
Track D-2 pattern (commit 707c45b). JarvisMCP stays unaware of
Voice/AVFoundation/CoreAudio.

- App/MCP/InProcessSelfStateAdapters.swift (NEW):
  - CoreAudioIntrospection.snapshotDevices() — full
    AudioObjectGetPropertyData enumeration, one entry per
    (device, direction) pair with name/uid/sampleRate/channels and
    isDefault/isActive flags resolved from
    kAudioHardwarePropertyDefaultInput/OutputDevice.
  - CoreAudioIntrospection.defaultDeviceUID(isInput:) — used by
    AudioGraphRouteAdapter to fill the route's input/output UID + name.
  - CoreAudioDeviceListAdapter — wraps snapshotDevices() (SELF-01).
  - AudioGraphRouteAdapter (actor) — hops into AudioGraphOwner for the
    probed sample rate + channels, combines with the CoreAudio default
    device UIDs for the active route (SELF-02).
  - SelfStateAdapter — closure-injected probes for provider, voice loop
    state, TTS tier, STT backend, wake-word mute. Uses captured launch
    Date for app uptime (NOT ProcessInfo.systemUptime which is host
    uptime per D-11). buildMode reads #if DEBUG. (SELF-03).
  - AVCaptureDeviceListAdapter — DiscoverySession enumeration; metadata-
    only, does NOT trigger camera TCC prompt (SELF-04).

- packages/Voice/Sources/Voice/AudioGraph/AudioGraph.swift: store the
  probedFormat on AudioGraph so AudioGraphOwner can expose it. Additive
  public let; no behavior change.

- packages/Voice/Sources/Voice/AudioGraph/AudioGraphOwner.swift:
  - new public ActiveRouteSnapshot Sendable struct (sampleRate, channels)
  - new public actor accessor activeRouteSnapshot() returns the probed
    input format. Hardware-identity (UID/name) intentionally lives in
    the App-side adapter so Voice doesn't take on CoreAudio-introspection
    concerns.

- App/AppDelegate.swift:
  - launchInstant: Date captured at AppDelegate construction time for
    SELF-03 app-uptime reporting (D-11).
  - selfKnowledgeInstallTask + installSelfKnowledgeTools() registers the
    four tools into the existing inProcessToolRegistry built by
    installMemory. Runs after voiceInstallTask so audioGraphOwner is
    available; degrades gracefully when audioGraphOwner is nil
    (registers the three tools that don't depend on it). All four tools
    declare requiresConfirmation:false (D-10 / AP-12); no caching
    (D-09 / AP-11). Provider identity reads from configStore.perTurn();
    voice loop state hops VoiceController.state; wake-word mute reads
    UserDefaults `features.voice.wakeWordMuted` (mirrors MuteWakeWord's
    persistence key).

Verification:
- swift test --package-path packages/MCP — all suites GREEN incl.
  registersFourSelfKnowledgeTools.
- swift test --package-path packages/Voice — 26 tests in 7 suites GREEN
  (AudioGraph probedFormat additive change, no regressions).
- bash scripts/check-app-builds.sh — PASS.
- All 18 boundary gates exit 0 (incl. check-applescript-confirmation,
  check-no-leftover-stubs, check-install-order, check-no-null-voice-
  adapters, check-no-evaluate-javascript).
2026-05-06 15:50:30 -05:00
james.maes b9ac76d07e feat(10-01): implement four self-knowledge MCP in-process tools
- ListAudioDevicesTool / list_audio_devices — wraps `AudioDeviceListing`
  dispatcher; encodes `{"devices": [...]}`. AudioDeviceEntry holds
  name/uid/isDefault/isActive/isInput/sampleRate/channels.
- GetActiveAudioRouteTool / get_active_audio_route — encodes
  `{"route": <route|null>}` with explicit nil-key emission so the model
  can disambiguate "graph closed" from "graph open with missing fields".
- GetSelfStateTool / get_self_state — SelfState carries all 11 D-11
  fields (appName, appVersion, buildMode, pid, uptimeSeconds,
  llmProvider, llmModel, ttsTier, sttBackend, wakeWordMuted,
  voiceLoopState).
- ListCameraDevicesTool / list_camera_devices — encodes
  `{"cameras": [...]}`. CameraDeviceEntry holds
  localizedName/uniqueID/isConnected/position.

All four declare `requiresConfirmation = false` explicitly per D-10
(read-only, no side effects; AP-12 — never default the flag).
JSONEncoder uses `.sortedKeys` for stable test fixtures. No caching
(D-09 / AP-11). All 6 Wave-0 tests GREEN; existing MCP suites unchanged.
2026-05-06 15:23:04 -05:00
james.maes 1952e74c8a test(10-01): add Wave-0 RED stubs for four self-knowledge MCP tools
- ListAudioDevicesToolTests, GetActiveAudioRouteToolTests,
  GetSelfStateToolTests, ListCameraDevicesToolTests — Stub-style
  dispatchers (D-31) drive each tool through its JSON encode path.
- MCPRuntimeWiringTests/registersFourSelfKnowledgeTools — asserts the
  four tool names register in InProcessToolRegistry AND each carries
  requiresConfirmation == false (D-10).
- All five fail to compile until Task 2 ships the production types
  (AudioDeviceListing/Entry, ActiveAudioRouteDispatching/Route,
  SelfStateDispatching/State, CameraDeviceListing/Entry, four tool
  structs). RED confirmed via `cannot find type ...` diagnostics.
2026-05-06 15:18:33 -05:00
james.maesandClaude Opus 4.7 cb7ee6fbb8 fix(voice): prime lastMicStatus to current auth at watcher startup — root cause of dead wake-word
`AudioGraphOwner.startMicRegrantWatcher` declared `lastMicStatus: Bool = false`
and entered the polling loop without seeding it. The first poll cycle (≈2s
after `open()`) always observed the transition `false → authorized` and
fired `rebuild(trigger: .micRegrant)`. The rebuild's `cancelInFlight` slot
ran `WakeWordDAG.cancel()` which finished the public `wakeWordStream`
permanently — VoiceController's consumer task exited and wake-word stayed
dead for the rest of the process lifetime.

Symptom on real hardware: ~2s after every cold launch, the log showed
`AudioGraphOwner: mic re-granted` followed by
`WakeWordDAG: cancelled — stream finished`, and "Hey Jarvis" never fired.

Fix: prime `lastMicStatus` from the current `AVCaptureDevice.authorizationStatus`
(or `authStatusProbe` test seam) BEFORE entering the polling loop, so the
first poll only fires on a real `denied → authorized` transition.

Also adds defense-in-depth so future legitimate rebuilds don't kill wake-word:
- `WakeWordDAG.stopFeed()` — cancels the feed Task without finishing the
  public stream. Reserved for transient teardown across a rebuild boundary.
- `WakeWordDAG.cancel()` — unchanged; finishes the stream. Reserved for
  permanent shutdown only.

Coverage: `MicRegrantWatcherStartupTests` (M1: no spurious rebuild when
authorized at startup; M2: real denied→authorized still fires; M3:
stopFeed preserves stream).

Live verified: app ran 1m 23s past `AudioGraph: opened`. Pre-fix the
spurious teardown fired at +2s on every cold launch; post-fix the
wake-word DAG stays alive and the stream remains open.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 11:07:41 -05:00
james.maesandClaude Opus 4.7 d8983af004 fix(anthropic): add stream:true to request body — root cause of streamTruncatedFinal
Discovered via live launch on 2026-05-05. Every Anthropic call returned
HTTP 200 + zero SSE frames; SSEDecoder emitted streamTruncatedFinal.

Root cause: `/v1/messages` opts into Server-Sent Events via the request
BODY field `"stream": true`, NOT via the `Accept: text/event-stream`
header alone. Our request body had every other field correct (model,
messages, system, tool_choice, max_tokens) but no stream field. Server
defaulted to non-streaming, returned a regular JSON response, our
SSELineReader read zero `data:` frames, decoder yielded
streamTruncatedFinal on EOF.

The 2026-05-03 audit-fix Track A added a StreamOutcome classifier and
the CacheHints.eligibleForSystemPrompt gate — both helpful diagnostic
+ defensive measures, but neither addressed the actual cause. The
audit hypothesis was "cache_control on small prompts triggers silent
rejection"; live testing disproved that and surfaced this simpler bug.

Fix:
- Add `stream: Bool` to AnthropicRequestBody encoding (mapped to JSON key `stream`)
- Pass `stream: true` from `RequestBody.encodeMultimodal`
- Reverted the speculative beta-header gating from this morning — beta
  header is unconditional per AGENT-02 and was never the cause

Tests:
- New `AnthropicProviderTests.LiveAnthropicStreamingTests`:
  - L3 (regression fence, no network): asserts encoded body contains
    `"stream": true`. FAILED before this commit. PASSES after.
  - L1 (live, env-gated on JARVIS_REAL_ANTHROPIC=1 + ANTHROPIC_API_KEY):
    real-API integration test that drives a small prompt and asserts at
    least one textDelta event arrives. Skipped without env.
  - L2 (no network): dumps the encoded body for diff against curl.
- All 49 existing AnthropicProvider tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 09:25:00 -05:00
james.maesandClaude Opus 4.7 85b14ceeea docs(docstrings): add header docstrings to key public types
- AppDelegate (App/): adds the missing "what is this" preamble + Lifecycle /
  Note on size / See also sections to the existing bootstrap-chain docstring.
- VoiceController (packages/Voice): converts the // MARK comment-block header
  to a proper /// docstring with Threading + Anti-patterns + See also sections.
- MCPClient (packages/MCP): adds a docstring covering the registry +
  per-server restart mutex + relationship to ChildSpawnGate / ConfirmingToolDispatcher.

Comment-only changes; verified via swift build (Voice, MCP) and
scripts/check-app-builds.sh.

The other types listed in the documentation brief (BufferBroadcaster,
SpeechAnalyzerSTT, CameraCapture, FrameAttachController, MemoryStore,
MemoryExtractionOrchestrator, HybridSearch, AgentOrchestrator, LLMProvider,
BusOutbound, BusInbound) already carry substantive header docstrings —
left alone per the audit-grade-improvement-only constraint.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 17:17:36 -05:00
james.maesandClaude Opus 4.7 508cf924d0 docs(packages): per-package README files
Adds README.md to each of the 13 SPM packages (AgentCore, Bus, Config,
DevOverlay, Harness, Keychain, Logging, MCP, Memory, Replay, Shell, Vision,
Voice). Each covers purpose, key public types, deps, consumers, invariants
(pulled from CLAUDE.md + grep-gates + audit findings), test stack, and
notable files. Memory README explicitly marks the package functionally OFF
until vec0.dylib + Ollama models land.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 17:13:33 -05:00
james.maesandClaude Opus 4.7 f9cd25d66e chore(test-naming): converge on Mock/Fake/Stub convention; document in CLAUDE.md; rename worst-mismatch stragglers
Per audit-2026-05-04 P2-15: 26 Mock*, 21 Fake*, 12 Stub* across the test
corpus, often used interchangeably for the same role. New CLAUDE.md
section "Test naming conventions" (line 127) sets the rule:

- Mock*: records calls AND returns scripted responses (assertion driver).
- Stub*: returns canned data without recording (deterministic input).
- Fake*: alternative implementation with realistic behavior (in-memory
  state-holder).

Renamed 15 worst-mismatch stragglers in the older corpus (per audit
constraint, did NOT touch types added in today's P1+P2 fix commits):

Stub* → Mock* (records + scripts):
- HybridSearchTests:    StubEmbedder → MockEmbedder
- HybridSearchTests:    StubStore → MockStore
- SessionHistoryTests:  StubStore → MockStore
- InProcessMemoryToolsTests: StubHybrid → MockHybrid
- InProcessMemoryToolsTests: StubHistory → MockHistory
- InProcessMemoryToolsTests: StubForget → MockForget

Mock* → Stub* (canned data, no recording):
- STTBackendSwitchTests: MockSpeechAnalyzerBridge → StubSpeechAnalyzerBridge
- STTBackendSwitchTests: MockWhisperKitBridge → StubWhisperKitBridge
- VoiceWiringTests:      MockVADEngineForW4 → StubVADEngineForW4
- VoiceWiringTests:      MockSTTProviderForW4 → StubSTTProviderForW4
- VoiceWiringTests:      MockTTSForW4 → StubTTSForW4
- VoiceWiringTests:      MockOrchestratorForW4 → StubOrchestratorForW4
- VoiceWiringTests:      MockBannerForW4 → StubBannerForW4
- VoiceWiringTests:      MockBusForW4 → StubBusForW4

Fake* → Mock* (records + scripts):
- WebviewBridgeOutboundTests:        FakeJSEvaluator → MockJSEvaluator
- HudStateCoordinatorBusWiringTests: FakeJSEvaluator → MockJSEvaluator
  (the comment on the type already said "Records each call for assertion")

Skipped per audit constraint:
- StubCapture/StubSink (Vision FrameAttachConfirmSendWiringTests, P1+P2)
- MockVADEngine + MockOrchestratorForController siblings (Voice
  VoiceControllerTests/VADGatedSessionTests, today's track-B churn)

Tests pass: Voice 87+23, MCP 85, Memory 88, Bus 58, AgentCore 19+72,
Vision 19, Shell 7+5. App target builds clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 16:42:16 -05:00
james.maesandClaude Opus 4.7 57847f2f58 chore(test-seams): move _test*/_force* seams from public to internal
Per audit-2026-05-04 P2-14: VoiceController._forceState,
_testFireSpeechEnd, _testCurrentSttSessionId were declared `public`
solely so XCTest could see them. Same for MCPClient._testHandle and
MCPServerHandle._testProcessIdentifier. These leaked test-only entry
points into each package's public API — anyone importing Voice could
call _forceState(.speaking) from production code.

Voice (4 seams): public → internal. Voice tests already use
@testable import Voice, so no test-side change required.

MCP (2 seams): public → @_spi(Testing) public. MCP tests opt in
via @testable @_spi(Testing) import JarvisMCP. The Harness package
(non-test) consumes _testHandle / _testProcessIdentifier for the
MCPCrashRunner SIGKILL recipe — opt in via @_spi(Testing) import.
@testable alone does NOT expose @_spi symbols — the SPI must be
explicitly imported, which is the desired discipline (each consumer
acknowledges they're reaching past the public ABI).

6 test seams total moved off the public ABI. Voice + MCP tests
pass (87 + 85). Harness builds clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 16:22:14 -05:00
james.maesandClaude Opus 4.7 263705d39d fix(voice p2-7): WhisperKitSTT replace silent try? with logged catch
Per audit 2026-05-04 code-style: WhisperKitSTT.transcribe used `try?`
to silently swallow bridge errors at three sites (window, tail, final).
The fallback STT path is allowed to fail back to assetMissing, but
silent swallows make failures undiagnosable.

Replace with do/catch + logger.warning. T-06-05-03 preserved: only the
error is logged, never transcript content.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 16:07:58 -05:00
james.maesandClaude Opus 4.7 a4f76661fc fix(memory p2-6): scrub raw search_memory query from HybridSearch debug log (T-06-05-03 analog)
Per audit 2026-05-04 security MEDIUM-1: HybridSearch.searchFacts logged
the raw user query string at debug level. Voice transcripts are guarded
by T-06-05-03; memory queries are equivalent user-private content.

Replace `query='\(query)'` with `queryLen=\(query.count)` — log
metadata only, never content. Add regression test that grep-asserts the
source file at runtime so a future log-content regression is caught at
test time.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 16:06:26 -05:00
james.maesandClaude Opus 4.7 11cb659e20 fix(voice p2-5): SpeechAnalyzerSTT direct await bridge.finish() — wrong-site anti-pattern application
Per audit 2026-05-04 concurrency HIGH-2: the prior code wrapped
bridge.finish() in a nested fire-and-forget Task with the comment
"DO NOT block on await analyzer.finish() synchronously" — but that
anti-pattern protects VoiceController's .speechEnd handler (where a
synchronous block would deadlock VAD events on the analyzer
completion path), NOT the feedTask here. feedTask IS the serial owner
of feed → finish ordering, so awaiting bridge.finish() directly is
correct AND lets finish errors propagate to storeError so finalize()
can throw them.

Update the class doc comment to clarify the anti-pattern's correct site.
Add S3 test: a mock bridge whose finish() throws yields a thrown error
out of finalize() (previously silently swallowed by the nested Task).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 16:04:19 -05:00
james.maesandClaude Opus 4.7 e8bbbcfe10 fix(voice p1-4): RingBuffer index atomics via swift-atomics for explicit acquire/release semantics
Per audit 2026-05-04 concurrency HIGH-3: RingBuffer.writeIdx/readIdx
were plain UInt64 vars accessed cross-thread (Core Audio tap thread
producer; Swift Task consumer). The doc claimed "atomic load/store" but
no fences existed. ARM64 makes aligned 64-bit loads/stores atomic at the
hardware level, but Swift's optimizer is free to reorder, hoist, or
coalesce — meaning the consumer could observe writeIdx advanced past a
slot whose storage write hadn't yet committed.

Adopt swift-atomics 1.3.0 (already transitively resolved by other
packages). Switch writeIdx/readIdx to ManagedAtomic<UInt64> with
explicit ordering:
  - Producer: load writeIdx .relaxed, write slot, store writeIdx .releasing.
  - Consumer: load writeIdx .acquiring (synchronizes with producer's
    releasing store), read slot, store readIdx .releasing.
  - Lag computation: load both with .acquiring for a consistent snapshot.

New test R5 exercises the happens-before invariant: producer publishes
fingerprinted (chunk, slot)-encoded samples; consumer asserts it never
observes a 0.0 sample (uninitialized slot) under concurrent run. R1..R4
are unchanged behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 16:01:14 -05:00
james.maesandClaude Opus 4.7 e606c63fea fix(voice p1-3): store + generation-guard the STT finalize Task to prevent stale-text reentry
Per audit 2026-05-04 concurrency HIGH-1: VoiceController.startSTTSession
spawned an unstored Task { [self] in ... await provider.finalize() ... }
that retained the actor strongly, had no cancellation hook, and could
re-enter handleSTTFinalized with stale text from a prior session if a
slow Whisper / SpeechAnalyzer finalize() returned after the next session
had already started.

Fix: store the Task on a new sttFinalizeTask property; cancel it in
shutdown; cancel + recreate in startSTTSession. Add a monotonic
sttSessionId; bump in startSTTSession; capture into the Task; check in
handleSTTFinalized via a generation-guarded overload that early-returns
on stale-id callbacks. Switch [self] → [weak self].

Test V5 reproduces the race and asserts the fix: capture session A's
id, finalize cleanly, start session B, simulate session A's late
finalize via the new _testFireSpeechEnd(text:sessionId:) seam, and
verify the orchestrator never received the stale text.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 15:54:00 -05:00
james.maesandClaude Opus 4.7 f19657a83e fix(voice p1-1): AudioLevelEmitter wires via BufferBroadcaster.Subscription, not raw RingBuffer
Per audit 2026-05-04 HIGH-4 + T1: AudioLevelEmitter was unwired in
production AND its API took a raw RingBuffer, inviting any future
production wire-up to re-introduce the sample-stealing bug Track B-7
just fixed (consumers racing the same SPSC read pointer).

Change init signature to require a BufferBroadcaster.Subscription so
the emitter has its own per-subscriber ring and never steals samples
from WakeWordDAG / chunkPump. Wire the emitter in installVoice via
audioGraphOwner.subscribe(); drive start()/stop() from VoiceController
state transitions on the listening boundary so the HUD ring pulses on
real mic RMS during listening sessions.

Existing testV4 updated to construct via BufferBroadcaster + subscribe
+ publish. New testV4b is the regression guard: two subscriptions on
one broadcaster receive distinct copies of every published buffer;
emitter draining sub A leaves sub B's ring untouched.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 15:40:32 -05:00