Commit Graph
9 Commits
Author SHA1 Message Date
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.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.maes 1b7fb81408 fix(10-02c): wire InProcessAwareToolDispatcher into runtime — closes B-01b
Prior B-01a fix (e0c0310) wired tool catalog ENUMERATION so the model gets
schemas for in-process tools. But ROUTING was still broken: MCPRuntimeWiring's
ToolRegistry only knew about the 3 stdio tools (get_time / get_clipboard /
run_applescript), so when the model emitted tool_use for get_active_audio_route
the inner MCPToolDispatcher returned "tool unknown" and the model surfaced the
failure as visible chat text ("the audio route tools are erroring out").

This commit lands the dispatch wire-up. With the types from 0961fef, 6503b23,
3cf0d39 already in place:

- App/AppDelegate.swift: eager-default-init inProcessToolRegistry at property
  declaration so the SAME actor reference is shared between installMCP (passes
  it to MCPRuntimeWiring.build for the composite dispatcher) and installMemory
  / installSelfKnowledgeTools (register tools into it). Refactor installMemory
  to populate INTO the existing registry via a new populateMemoryTools helper
  rather than constructing a fresh one. The pre-existing
  buildInProcessToolRegistry static delegates through the same helper for
  back-compat.

- App/MCP/MCPRuntimeWiring.swift: build() takes a new inProcessRegistry
  parameter (defaulted nil for tests). When supplied, the inner stdio
  MCPToolDispatcher gets wrapped by InProcessAwareToolDispatcher so name-routing
  picks in-process tools first, falls through to stdio. Confirmation gating,
  bus emission, and observer hooks fire identically on both paths because the
  composite sits INSIDE ConfirmingToolDispatcher's outer chain.

- .planning/phases/10-self-awareness-diagnostics/10-02c-PLAN.md: skeleton plan
  documenting the surgical fix.

Build: swift test --package-path packages/MCP PASS (91/91 XCTest + 6 swift-test).
bash scripts/check-app-builds.sh PASS.

Live verification deferred to user — relaunch + ask "what mic are you using?"
expects real device name in reply (no JSON blob), tool_use + tool_result for
get_active_audio_route in the log tail.
2026-05-07 13:12:23 -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.maesandClaude Opus 4.7 707c45b355 feat(track-d 2): register SearchMemoryTool + ForgetFactTool with MCP runtime
Closes the audit's "tools exist but unregistered" finding from
.planning/audit-2026-05-03/memory-audit.md §4-5: SearchMemoryTool +
ForgetFactTool source files have lived in `packages/MCP/Sources/MCP/
InProcess/` since Plan 07-03 with zero non-test instantiation. The agent
loop has had no way to call into memory.

Wiring:
  - New `App/MCP/InProcessMemoryAdapters.swift` bridges MCP's tool
    dispatch protocols to Memory's actors:
      * `HybridSearchAdapter: HybridSearchDispatching` wraps
        `Memory.HybridSearch` and translates `[FactRef]` → `[SearchMemoryHit]`
        (so MCP doesn't need to import Memory).
      * `ForgetFactStoreAdapter: ForgetFactDispatching` wraps
        `MemoryStore.forgetFact`.
  - `installMemory` builds an `InProcessToolRegistry` and registers tools
    based on preconditions:
      * `forget_fact` registers when `memoryStore != nil` (writes need DB)
      * `search_memory` registers when `memorySearchAvailable && embedder
        init succeeds` (reads need DB + vec0 + Ollama loopback)
  - Embedder construction (`OllamaEmbeddingClient` loopback assertion)
    failure is caught + logged; degrades to "writes ok, no search"
    rather than failing the whole subsystem.
  - New `inProcessToolRegistry: InProcessToolRegistry?` ivar, exposed
    @testable.

Test seam: `AppDelegate.buildInProcessToolRegistry(forgetDispatcher:
searchDispatcher:)` is the pure registry-construction helper — takes
already-built dispatchers (or nil) and applies the gating rules. Tests
inject stub dispatchers to assert positive registration without needing
a real `MemoryStore` (which requires vec0.dylib, env-gated in CI).

Tests added (compile-time gates per
.planning/debug/xctest-launch-runningboard-error-5.md):
  - `test_D2_noToolsRegisteredWhenStoreUnavailable` — production reality
    on every cold launch (vec0 placeholder)
  - `test_D2_bothToolsRegisterWhenBothDispatchersPresent` — store + search
    available
  - `test_D2_onlyForgetRegistersWhenSearchUnavailable` — store ok, vec
    failed (the future state once custom libsqlite3 lands but vec0.dylib
    is missing)
  - `test_D2_emptyRegistryWhenBothDispatchersNil` — degenerate case

Out of scope: exposing the registered tools to the LLM via
`AgentOrchestrator.availableTools` (currently `[]` — separate audit gap,
affects helper tools too). The "tools exist but unregistered" gap is
closed; the "tools exposed to LLM" gap is independent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 14:09:46 -05:00
james.maes dfc237db0c feat(07-06): installMemory + installVision + TS bus protocol 2.1.0 (Task 1)
- AppDelegate gains installMemory() and installVision() mirroring installVoice's
  six-step pattern. Memory installs BEFORE voice (memory ready before first
  turn); vision AFTER voice (camera lifecycle independent).
- MemoryStore + MemoryExtractionOrchestrator + MemoryExtractionCoordinator
  constructed; AppDelegateMemoryReplaySink bridges Memory.MemoryReplaySink ->
  Replay.ReplayLog.record(_:for:). Coordinator wiring deferred until
  AgentOrchestrator lands in AppDelegate (documented in SUMMARY).
- CameraCapture + PresenceMonitor instantiated; the SINGLE PresenceSignalBus
  reference (owned by PresenceMonitor) flows via AppDelegateContextBuilderAdapter
  to ContextBuilder.installPresence and via hudStateCoordinator?.attachPresence
  to the HUD coordinator. VISION-03 boundary preserved.
- VisionRouter constructed with T1=Ollama, T2=Ollama (sidecar deferred), T3=
  Anthropic (key from Keychain); the runTurn dispatcher branch is deferred
  until AgentOrchestrator wires up.
- DisablePresence menu-bar toggle wired alongside MuteWakeWord.
- BannerContent.cameraDenied + cameraRevoked presets; cameraDegradationTask
  watches CameraCapture.degradationStream and enqueues banners.
- HudStateCoordinator.attachPresence is a generic AsyncSequence subscriber
  preserving HUD-08 single-writer invariant (presence is record-only).
- ContextBuilder.installPresence(_:) is a static no-op drain (deferred wiring
  for the future per-turn system-prompt enricher).
- TS BUS_PROTOCOL_VERSION 2.0.0 -> 2.1.0; sessionHistory + TurnRow added to
  the BusOutbound discriminator union with full decode arm. Round-trip test
  bumped accordingly. scripts/check-bus-protocol-version.sh now passes.
2026-04-29 18:40:47 -05:00
james.maesandClaude Opus 4.7 094c067b94 fix(05-review): WR-02 hold broker and presenter strongly through holder
Previously ConfirmationPresenter held broker as `weak var` and
ConfirmationPresenterHolder held its inner presenter as `weak var` —
a doubly-weak chain that could silently break Approve/Deny button
responses if either side deinit'd during a pending request. The user
would see the panel close on Approve, then the orchestrator would
hang for 60s until the broker's timer fired .timeout.

Switched both to strong references. The cycle motivation
(broker → presenter → broker) is broken by the holder indirection:
broker holds the holder, holder holds presenter strongly, presenter
holds broker strongly. The chain is acyclic when read top-down because
the broker does not hold the presenter directly.

Documents the lifecycle invariant on MCPRuntime: AppDelegate must hold
runtime strongly for the lifetime of the process; orchestrator must
not outlive the runtime.

Adds ConfirmationLifecycleTests with three guards:
- canonical hold pattern always resolves Approve
- wedged presenter (never calls broker.response) still resolves via
  timeout — the timer is the load-bearing fallback
- source-level: ConfirmationPresenter declares `let broker:` (strong),
  not `weak var broker:`

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 16:28:54 -05:00
james.maesandClaude Opus 4.7 d95c4421fc fix(05-review): CR-02 wire ME-04 closure observer→channel→drain→ReplayLog
The orch→replay BoundedAsyncChannel<ReplayEvent> previously had no
producer (ReplayingToolResultObserver wrote directly to replayLog)
and the drain Task discarded everything it received. The channel +
drain were dead code; ME-04's contract was structurally unverifiable
in production.

Now:
- ReplayingToolResultObserver holds a BoundedAsyncChannel<ReplayEnvelope>
  (TurnID + ReplayEvent envelope so the consumer knows which turn to
  key against) and sends both pre- and post-sanitize bytes through it.
- AppDelegate opens a real ReplayLog at boot and the drain Task
  consumes envelopes off the channel and calls replayLog.record(...).
- AppDelegate calls MCPRuntimeWiring.build(...) (previously referenced
  only in a doc comment) so the dispatcher chain is actually
  instantiated. A NoopBusGateway placeholder fills the bus seam until
  Phase 6/7 orchestrator wiring lands.
- MCPRuntimeWiring.build's signature switches replayLog parameter to
  replayChannel — cleaner separation, since the channel is the
  load-bearing seam.

Adds three CR-02 regression tests in MCPRuntimeWiringTests:
observer→channel produces envelopes, observer→channel→drain→ReplayLog
end-to-end, and .dropOldest under burst saturation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 16:19:58 -05:00
james.maes daae27e720 feat(05-05): App-level MCPRuntimeWiring + ME-04 channel + SEC-07 observer
Closes ME-04 (Plan 04-05 deferred state). AppDelegate.applicationWill
FinishLaunching now instantiates BoundedAsyncChannel<ReplayEvent>(
capacity: 2048, policy: .dropOldest) for the orch→replay seam — the
production instance that Plan 04-05's ChannelTopologyTests CT2 only
verified as a primitive.

- App/MCP/MCPRuntimeWiring.swift: dependency-graph constructor for the
  full chain (MCPClient → ToolRegistry → MCPToolDispatcher →
  ConfirmingToolDispatcher) plus a compose() test seam. The
  ConfirmationPresenterHolder breaks the broker↔presenter cyclic init.
- App/MCP/ReplayingToolResultObserver.swift: SEC-07 observer that
  records pre-/post-sanitize bytes via two ReplayEvent.toolResultFull
  rows (one canonical, one ':raw'-suffixed toolUseId). Uses an
  injected turnIDResolver closure since orchestrator wiring lands in a
  later plan.
- AppDelegate stores the orchToReplayChannel + drain Task and tears
  them down on applicationWillTerminate.
- App/Tests/AppTests/MCPRuntimeWiringTests.swift: 3 structural tests
  (compose-returns-ConfirmingToolDispatcher; channel-capacity-+-policy;
  MCPRuntime-struct-addressable). No xcodebuild test launch — known
  Xcode 26 RunningBoard blocker per .planning/debug/.

Deviations:
- Replay / AgentCore / MCP / Bus Package.swifts had implicit transitive
  swift-log deps that SPM tolerated but Xcode framework-linker rejects
  once the App pulls them in as separate frameworks. Added explicit
  swift-log deps to all four (Rule 3, blocking).
- HudStateCoordinatorBusWiringTests qualified two HudState references
  to Jarvis.HudState — the new framework deps disturbed the App vs Bus
  symbol disambiguation (Rule 1, surgical).
- The plan's plain ReplayEvent.toolResultFull(toolUseId:rawBytes:
  sanitizedBytes:) variant doesn't exist (Phase 4 schema closed); the
  observer writes two rows with a ':raw' toolUseId suffix instead.

xcodebuild build (Debug) succeeds with the new MCP deps wired into the
App target. Test build (build-for-testing) also succeeds; runtime test
execution is the documented Xcode 26 RunningBoard blocker per
03-05-PLAN.
2026-04-25 14:57:01 -05:00