develop
22
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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). |
||
|
|
1f8e03edeb |
fix(track-a): demo-blocker class + audit reports + chat-turn e2e tests
User feedback: 50+ hours of agent work, nothing demonstrably working.
Fanned out 6 deep auditors (voice, HUD, vision, memory, LLM, tests);
all reports landed at .planning/audit-2026-05-03/ with brutal-honesty
mandates. Synthesis (SYNTHESIS.md) confirmed: every modality has at
least one fatal break between code-that-passes-tests and
code-that-runs. Track A is the smallest credible path to one working
modality (text turn + animated rings).
Closed under TDD discipline:
**Item 4 — HUD idle ring static rendering (F-A1-01).**
- webview/packages/hud/src/hud/stateUniforms.ts:30 — was
`pulse: 0.0, rotate: 0.0` for idle, the all-static configuration
that rendered STANDBY as dots instead of animated rings. Now
pulse: 0.4, rotate: 0.05 — gentle ambient distinct from booting +
thinking.
- webview/packages/hud/tests/stateUniforms.test.ts — table updated;
new regression-guard `STATE_PARAMS[*] is not all-static` (7 cases,
one per HudState) prevents anyone from re-zeroing a state.
**Item 1 — CacheHints eligibility gate (likely streamTruncated cause).**
- packages/AgentCore/Sources/AgentCore/CacheHints.swift —
+ static func eligibleForSystemPrompt(_:ttl:). Anthropic's prompt
cache requires ≥1024 tokens at the breakpoint; below that, the
server returns 200 OK + immediate-EOF for `extended-cache-ttl`
hints. The orchestrator's default system prompt is ~10 tokens.
Helper returns nil for sub-threshold prompts so no cache_control
marker is emitted.
- packages/AgentCore/Sources/AgentOrchestrator/AgentOrchestrator.swift
— call sites now pass `CacheHints.eligibleForSystemPrompt(finalSystem)`
through runTurnLoop instead of unconditional `.extended1h`.
- Tests: CacheHintsEligibilityTests (6 cases), RequestBodyTests R5a/R5b
(2 cases verifying small-prompt → no cache_control in encoded JSON,
large-prompt → cache_control present).
**Item 2 — StreamOutcome diagnostic.**
- packages/AgentCore/Sources/AnthropicProvider/StreamOutcome.swift —
new pure classifier that distinguishes:
- non-2xx HTTP status
- 200 OK + 0 bytes (cache_control / auth-header rejection class)
- 200 OK + bytes + 0 frames (malformed body)
- 200 OK + frames + EOF before message_stop (mid-stream drop)
- 200 OK complete (healthy)
AnthropicProvider's read loop counts frames + bytes and logs the
summary once per stream. Production logs now distinguish failure
modes that previously all surfaced as `streamTruncatedFinal`.
- Tests: StreamOutcomeTests (7 cases, all branches).
**Item 3 — KeychainError surfacing.**
- packages/AgentCore/Sources/AnthropicProvider/AnthropicAPIKeyProvider.swift
— new builder `make(keychain:item:)` returns the
`@Sendable () async throws -> String` closure with errors
propagating verbatim. Closes the audit's hidden-401 finding:
`(try? keychainStoreLocal.get(.anthropic)) ?? ""` swallowed the
real KeychainError into an empty x-api-key, producing
`401 x-api-key header is required` indistinguishable from a
stale-key 401. Now the actual error reaches the chat panel via
AnthropicProvider → orchestrator.error → BusForwarder →
submitRejected.
- App/AppDelegate.swift — both Anthropic provider construction sites
(installAgent, installVision T3 wiring) now use the propagating
builder.
- Tests: AnthropicAPIKeyProviderTests (6 cases — itemNotFound,
unexpectedStatus, duplicateItem propagation; happy path; explicit
item; default item).
**Item 5 — F1 #2 chat-turn round-trip e2e tests.**
- packages/Bus/Tests/BusTests/RealWKWebViewIntegrationTests.swift —
+ 2 new tests using a real WKWebView:
- test_chatTurnRoundTrip_busOutboundReachesPageHandler — Swift
`bridge.send(.turnStarted/tokenDelta/turnEnded)` reaches the
page's onOutbound handler. This is the test the v0.12.0 audit
called out as catching BLOCKER-INT-1, INT-2, INT-3,
F-E-RACE-1, F-E-FK-1, F-E-WIRE-1 in one shot.
- test_chatTurnRoundTrip_busInboundFromJSReachesSwiftHandler —
JS-side `window.jarvisBus.send({type:'chatSubmit',...})` —
exactly what ChatInput.tsx does — reaches the bridge's
onInbound handler with the right body.
**Verification:**
- AgentCore: 215/215 (was 178; +37 across BusForwarder, CacheHints,
StreamOutcome, AnthropicAPIKeyProvider, RequestBody R5).
- Replay: 33/33.
- Bus: 57/58 (1 pre-existing failure: F-A2-01 stale `JarvisBusWorld`
assertion documented in AUDIT-FINDINGS, separate scope).
- webview/packages/hud: 94/94 (was 87; +7 stateUniforms regression-
guards + idle table updates).
- All 15 boundary grep gates: PASS.
- scripts/check-app-builds.sh: PASS.
**Audit reports retained at .planning/audit-2026-05-03/:**
- SYNTHESIS.md (consolidated, with triage plan)
- hud-audit.md, llm-audit.md, memory-audit.md, tests-audit.md,
vision-audit.md, voice-audit.md, voice.md (per-modality detail)
**Carry-forward (NOT closed this commit):**
- F-A2-01 stale WebviewBridgeOutboundTests assertion (pre-existing).
- Voice (F): models never bundled, audio graph never instantiated,
STT bridge body is `_ = chunk`, TTS engine nil. ~1 day.
- Vision (F): AVCapturePhotoOutput allocated but capturePhoto never
called; HUD camera button doesn't exist. ~1 day.
- Memory (F): Apple's stripped libsqlite3 prevents sqlite-vec from
loading; "future ops plan" has zero commits. ~3 days.
- LLM (D+): NoopBusGateway still drops tool-call events
(BLOCKER-INT-1).
- Tests (C-): 5 XCTAssertTrue(true) tautologies + multiple
rubber-stamp tests defending stubs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
048ab08ef6 |
fix(app,agent,replay): wire orchestrator install correctly + extract bus forwarder for testability
The 2026-05-03 INT-3 smoke test surfaced three latent runtime bugs the unit-level GSD verifications all missed because the audits were source- grep, not runtime-trace: 1. **F-E-RACE-1** — `agentInstallTask` awaited memory + vision installs but NOT the MCP runtime build task. MCPRuntime build is ~1s due to helper child-process spawn, so installAgent ALWAYS short-circuited on `mcpRuntime == nil` and the orchestrator + broadcaster + ALL six broadcaster subscribers stayed dormant for the entire process lifetime. Fix: capture `mcpInstallTask` Task handle and await it before calling installAgent. The v0.12.0 milestone audit's claim of "5 broadcaster subscribers wired" was accurate at source-grep level and false at runtime. 2. **F-E-FK-1** — installAgent constructed the orchestrator with a fresh SessionID but never called `replayLog.beginSession(...)`. Every `startTurn` violated the FK and returned `SubmitOutcome.rejected(reason: .configError)` — surfacing in the chat panel as "Config error — see ~/Library/Logs/Jarvis/system.log." Production code only ever called beginSession in tests (MCPRuntimeWiringTests). Fix: installAgent now reads CFBundleShortVersionString + CFBundleVersion from Info.plist, calls beginSession, and passes the returned SessionID to the orchestrator. 3. **F-E-WIRE-1** — `OutboundBatcher` was constructed only inside `installVoice`, AFTER OpenWakeWord/Silero models loaded. Voice DAG short-circuit on missing models in Debug builds left `self.outboundBatcher == nil`, and every `outboundBatcher?...` chained-optional in the .bus subscriber silently dropped tokenDelta / turnStarted / turnEnded / submitRejected. The chat panel got nothing back from the orchestrator. Fix: hoist batcher construction into installAgent (only depends on webviewBridge, not voice models); installVoice reuses self.outboundBatcher rather than overwriting. In addition, the .bus subscriber's translation logic — including StopReason → TurnTerminator mapping, turnStarted synthesis on first emission for a fresh TurnID, error → submitRejected forwarding for mid-stream failures — was inline in AppDelegate and therefore impossible to unit-test (App target xctest harness is upstream-broken on Xcode 26). Extracted to `BusForwarder` in AgentOrchestrator with a sink protocol; App provides `AppBusForwarderSink` (~50 LOC) as the glue. Every translation branch now has unit coverage. Files: - packages/AgentCore/Sources/AgentOrchestrator/BusForwarder.swift — Pure logic. `BusForwarderSink` protocol; `BusForwarder.terminator(for:)` StopReason mapping; `BusForwarder.drain(events:sink:)` async loop with per-turn lastEmittedTurnId state. No Bus package dep — keeps the layering clean. - packages/AgentCore/Tests/AgentOrchestratorTests/BusForwarderTests.swift — 16 cases covering every branch: terminator mapping (5, exhaustive on StopReason), tokenDelta forwarding (3 — synthesize start, dedupe start across same-turn deltas, two-turn handoff), turnEnd forwarding (4 — mapped terminator, synthesize-start-on-bare-turnEnd, streamTruncated→errored, state reset), error forwarding (2), ignored events (1), malformed-id guard (1). - packages/Replay/Tests/ReplayTests/SessionForeignKeyTests.swift — 3 cases (FK1: startTurn-without-beginSession-throws; FK2: post- beginSession-succeeds; FK3: multiple-startTurns-share-one-session). Closes the bug class regardless of any future AppDelegate refactor. - App/AppBusForwarderSink.swift — thin transport adapter. Translates `BusForwarder.Terminator` to wire-format `Bus.TurnTerminator` and dispatches across OutboundBatcher actor. - App/AppDelegate.swift — race fix, beginSession call, batcher hoist, inline subscriber replaced with `BusForwarder.drain(events: sink:)`. - Jarvis.xcodeproj/project.pbxproj — xcodegen regen picks up new App source file. - .planning/AUDIT-FINDINGS.md — three new findings (F-E-RACE-1, F-E-FK-1, F-E-WIRE-1) documented and marked closed; refactor rationale recorded; carry-forward items called out (streamTruncatedFinal cause unverified without Keychain access; F1 chat-turn end-to-end test is next). Verification: - AgentCore 194/194 (was 178; +16 BusForwarder). - Replay 33/33 (was 30; +3 SessionFK). - Bus 55/56 (F-A2-01 pre-existing). - All 15 boundary grep gates PASS. - scripts/check-app-builds.sh PASS. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
602a95b6c8 |
fix(app,broadcaster): add .bus subscriber forwarding tokenDelta to OutboundBatcher (closes BLOCKER-INT-2 / tokens half)
Phase E first fix from .planning/v0.12.0-MILESTONE-AUDIT.md. The orchestrator's events stream had FIVE subscribers (memory, transcript, devOverlay, frameAttach, voice) and ZERO that forwarded to the JS-side HUD chat panel. OutboundBatcher.postToken existed with zero production call sites cross-tree. As a result, the webview chatEvents store was guaranteed to render an empty array regardless of how many tokens the orchestrator emitted. This commit closes the tokenDelta half of BLOCKER-INT-2. turnStarted / turnEnded forwarding remains open as documented follow-up; the relevant translation (TurnID → UUID, StopReason → TurnTerminator) is non-trivial and warrants its own commit. Downgraded F-B2-INT-2 from 🔴 → 🟡 in AUDIT-FINDINGS.md to reflect partial closure. Changes: packages/AgentCore/Sources/AgentOrchestrator/OrchestratorEventBroadcaster.swift + Adds `case bus` to Priority enum with doc-comment explaining AppDelegate's bus-forwarding subscriber and citing the AUDIT plan. + Extends isProtectedForPriority() to include .bus alongside .memory and .transcript: protects everything except .tokenDelta / .thinkingDelta. Bus tokenDeltas are drop-eligible because OutboundBatcher coalesces at ~30 Hz internally; tail-dropping individual deltas under extreme overload is preferable to back-pressuring the orchestrator. Turn-bookkeeping events (.turnEnd, .toolCardUpdate, .usage, .error, .stateChange) are protected — those drive HUD chrome transitions that MUST reach the user. packages/AgentCore/Tests/AgentOrchestratorTests/OrchestratorEventBroadcasterTests.swift + Extends test_protectionMatrixIsCorrect_perPriority with a .bus assertion loop mirroring .memory / .transcript. Asserts tokenDelta + thinkingDelta drop-eligible, all 5 other event classes protected. AgentCore suite: 178/178 → 178/178 (added assertions to existing test, no new test method). App/AppDelegate.swift + busSubscriberTask: Task<Void, Never>? property storing the new sixth broadcaster subscriber drain task. Doc-comment cites the AUDIT-AND-FIX-PLAN BLOCKER-INT-2 origin. + Cancellation in applicationWillTerminate alongside the other 5 subscriber tasks. + In installAgent, after the .voice subscriber drain, subscribes to the broadcaster with priority .bus (capacity 256) and starts a Task that pattern-matches OrchestratorEvent.tokenDelta and forwards the chunk to `await self.outboundBatcher?.postToken(_:)`. `[weak self]` capture + nil-coalescing handles the construction- order skew where outboundBatcher is constructed in installVoice (which runs AFTER installAgent per the locked vision→agent→voice install order). `await` is required because OutboundBatcher is a public actor. + Updated final installAgent log line to list "bus" alongside the other 5 subscribers. Verification: • scripts/check-app-builds.sh: PASS — App target compiles cleanly. • AgentCore swift test: 178/178 pass. • Bus swift test: 56/56 except F-A2-01 (pre-existing stale-assertion test bug, unchanged). What this does NOT yet fix (deferred follow-up): • turnStarted / turnEnded forwarding. The Phase 4 SUMMARY's "synthesized client-side from first tokenDelta" assumption may cover turnStarted webview-side; turnEnded explicitly needs Swift translation from OrchestratorEvent.turnEnd → BusOutbound.turnEnded. • End-to-end visibility — a user can't actually see the new forwarding in action until BLOCKER-INT-3 (HUD chat input UI) is closed. Until then, the .bus subscriber correctly forwards an empty stream of zero tokenDeltas because no turn ever runs. • Tool-call event forwarding remains a separate problem (BLOCKER-INT-1 / NoopBusGateway, untouched here). Refs: .planning/v0.12.0-MILESTONE-AUDIT.md (BLOCKER-INT-2), .planning/AUDIT-AND-FIX-PLAN.md Phase E. |
||
|
|
75a4ed6991 |
feat(09-04): replace NullVoiceAdapters with real voice adapter triad + lock install order (D-09, D-10, BLOCKER-1, BLOCKER-2, WARNING-5)
- New App/Voice/{VoiceOrchestratorAdapter,VoiceTTSAdapter,VoiceBusEmitterAdapter,RejectReasonCopy}.swift
replaces NullOrchestratorAdapter / NullTTSAdapter / NullBusEmitterAdapter (D-09).
- VoiceOrchestratorAdapter forwards submit/cancelAndSubmit to AgentOrchestrator,
appends user-side text to TurnTranscriptStore (BLOCKER-1), enqueues a HUDBanner
on every SubmitOutcome.rejected case via shared RejectReasonCopy (D-10).
- VoiceTTSAdapter wraps an optional TTSEngineActor (engine construction lands
in a follow-on plan; nil-engine path no-ops gracefully).
- VoiceBusEmitterAdapter wraps an OutboundBatcher whose sink is the WebviewBridge
so audio-level RMS reaches the HUD's RingMesh pulse.
- AppDelegate.installVoice now uses the production adapters; install order
in applicationWillFinishLaunching reordered to vision -> agent -> voice
with voiceInstallTask awaiting agentInstallTask?.value (WARNING-5).
- New broadcaster .voice subscriber in installAgent: per-turn assistantText
accumulator + turnSourceWasVoice filter so text-originated turns NEVER drive
emitTurnEnded on the voice adapter (BLOCKER-2).
- SubmitOutcome.superseded extended with newTurnId so cancelAndSubmit callers
can append user text under the new turn id (BLOCKER-1 user-side closure on
the cancel path).
- VoiceSubscriberTextTurnFilterTests proves text turns never trigger
emitTurnEnded; VoiceOrchestratorAdapterTests covers RejectReasonCopy +
banner id mapping + VoiceBusEmitterAdapter forwarding.
- New scripts/check-install-order.sh + scripts/check-no-null-voice-adapters.sh
catch regressions structurally.
|
||
|
|
d4af611672 |
feat(09-03): thread PresenceStateSnapshot into AgentOrchestrator + system-prompt enrichment (D-13/D-14, INT-07-03)
- AgentOrchestrator constructor gains optional presenceSnapshot: PresenceStateSnapshot? (after visionRouter, defaulting to nil) - runTurn appends `await presenceSnapshot?.currentEnrichment()` to the system prompt OUTSIDE UntrustedWrapper.composeSystemPrompt (SEC-06 nonce-region preserved; presence text is trusted) - AppDelegate.installAgent passes PresenceStateSnapshot.shared so production turns carry ambient enrichment - 5 new AgentOrchestratorPresenceEnrichmentTests (no-snapshot unenriched / present sentence / 5s suppression / absent away sentence / no-mutation across turns) - 177 AgentCore tests pass; VISION-03 + orchestrator-events single-consumer gates green; App target builds |
||
|
|
13c8033028 |
feat(09-02): add VisionRouter dispatch + post-response escalation + voice-turn tracking (BLOCKER-2)
- ReplayEvent: new .escalationAttempt(turnId:kind:) case with .t1ToT2 EscalationKind (D-02 marker, distinct from AGENT-09 stream_truncated) - ReplayEventKind: new .escalationAttempt = "escalation_attempt" rawvalue - AgentOrchestrator: constructor gains optional visionRouter: VisionRouter? = nil - runTurn pre-stream: route image-bearing turns through VisionRouter, swap provider, track imageBearingTurns (D-01, D-16) - runTurnLoop: multimodal stream overload for image-bearing turns; assistantTextSoFar accumulator on .textDelta - runTurnLoop .endTurn arm: post-response evaluatePostResponse hook, .escalateToT2 → discard T1 message, swap to T2, continue outer (WARNING-3 LOCKED labeled-loop strategy); records .escalationAttempt under SAME turnId (D-02) - BLOCKER-2: voiceOriginatedTurns Set<TurnID> + recordVoiceTurn(_:) populator + turnSourceWasVoice(_:) public accessor (Plan 4 voice subscriber will gate emitTurnEnded on this) - turnHadImage(_:) public accessor (Plan 2 frame-attach release subscriber consumer) - AgentCore + Replay + MCP packages bumped from .v13 to .v14 (cascade — JarvisVision is .v14, project deployment target was already 14.0) - MockLLMProvider extended with multimodal stream overload + RecordedCall.images - 6 AgentOrchestratorVisionDispatchTests + 3 AgentOrchestratorVoiceTurnTrackingTests + 3 EscalationAttemptMarkerTests - All 63 existing AgentOrchestratorTests still pass; all 30 Replay tests pass; orchestrator-events single-consumer grep gate exits 0 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
23be18501c |
feat(09-01): TurnTranscriptStore actor + 7 isolation/concat tests
- Per-turn (userText, assistantText) accumulator keyed by TurnID. - BLOCKER-1 source of truth: feeds lookupTurnContent so MemoryExtractionCoordinator stops short-circuiting on nil pairs. - flushPair returns the pair only when BOTH halves populated; removes the entry on success (bounded memory). - Multiple assistant deltas concatenate; turn IDs are isolated. - Placed under packages/AgentCore/Sources/AgentOrchestrator/ rather than App/AgentRuntime/ because the App target lacks SPM test discovery (plan-recorded fallback). |
||
|
|
b055da68fa |
feat(09-01): OrchestratorEventBroadcaster fan-out actor + 6 D-invariant tests
- Single drainer of orchestrator.events fans out to N child AsyncStreams (D-05). - Per-consumer bounded mirror with drop-oldest policy (D-06). - Per-priority filter — memory/transcript protect everything except tokenDelta/thinkingDelta; voice protects turnEnd+error; frameAttach protects turnEnd; devOverlay treats all as lossy (D-07). - start()/stop() lifecycle; drain Task lives for app lifetime (D-08). - Tests: fan-out, drop-oldest, priority-protection on saturation, stuck-consumer isolation, stop-cancels-drain, full protection matrix. |
||
|
|
202b7e36a6 |
feat(07-05): LLMProvider multimodal protocol extension + ImageBlock + TurnInput.images (Task 1 GREEN)
- ImageBlock(mediaType:data:) Sendable+Equatable canonical frame value type - LLMProvider gains stream(messages:images:tools:...) with default extension forwarding to single-modal when images.isEmpty (D-18) - Non-overriding conformers precondition-fail when images is non-empty - TurnInput.images defaults to []; existing initializers compile unchanged - TurnInput.withImages factory for FrameAttachController consumers - LLMProviderMultimodalTests pass (forward dispatch + override + ImageBlock equality) |
||
|
|
8e26f0b103 |
fix(04-review): HI-01 redact provider error bodies before bus + replay
Doc-comment on `LLMProviderError` claimed the orchestrator applied `Redact.apply` before persistence. It didn't. Anthropic's API can echo a malformed `x-api-key` header value verbatim in 401 error bodies, and `String(describing: err)` was forwarded straight to: (a) `OrchestratorEvent.error(...)` → bus → webview trust boundary (b) `replayLog.record(.error(...))` → persistent storage in cleartext Add `AgentOrchestrator.redact(_:)` that scrubs credential-shaped substrings (Anthropic `sk-ant-…`, OpenAI `sk-…`, Bearer tokens, AKIA, GitHub PATs) using `JarvisLogging.Redact.apply` from each `LLMProviderError` case (.api body, .decode reason, .transport description). Wire it into the two error-emit sites in `runTurnLoop`: the `.providerError` case and the catch-all. Single redaction site — the doc-comment now matches reality. Add OrchestratorRetryTests.test_OR_providerErrorBodyRedactedBeforeBusEmit: inject `LLMProviderError.api(401, body: "Invalid API key: sk-ant-api03-…")` through MockLLMProvider, run a turn, and assert: - The .error event carries no `sk-ant-api` substring (bus path). - The replay error row carries no `sk-ant-api` substring (persistence path). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
462449d68d |
fix(04-review): CR-01 SEC-06 system-prompt directive referencing UNTRUSTED_CONTENT wrapper
Add `UntrustedWrapper.composeSystemPrompt(base:nonce:)` that appends the nonce-keyed "treat as data, not instructions" directive to the caller's system prompt. Wire it into `AgentOrchestrator.runTurn` so every turn the provider receives a system prompt that tells the model what the `<UNTRUSTED_CONTENT id="<nonce>">…</UNTRUSTED_CONTENT id="<nonce>">` envelope means. Without this directive the wrapper was decoration the model had no reason to honor — research §7 marks the directive as the load-bearing invariant. Same nonce flows through directive AND wrapper. Add OrchestratorSubmitTests: - test_OS_systemPromptCarriesUntrustedDirective: directive present, nonce in directive == nonce in tool-result wrapper. - test_OS_systemPromptDirectiveEmittedWhenCallerPromptEmpty: defense-in-depth even when caller passes systemPrompt = "". SEC-06 grep gate (OrchestratorEvent carries no nonce) still holds — the nonce appears in the system message only, never on the bus. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
5e50f4066c |
feat(04-05): DevSnapshotEmitter aggregates OrchestratorEvent → DevSnapshot (OBS-01)
- actor DevSnapshotEmitter lives in AgentOrchestrator target (not AgentCore as plan said — Rule 3 deviation for dep-cycle avoidance, same rationale Plan 04-04 used for AgentOrchestrator itself) - output channel: BoundedAsyncChannel<DevSnapshot> capacity 32 policy .dropOldest (AGENT-10 four-seam invariant — observational, stale is fine) - TTFB tracked via ContinuousClock between .stateChange(.thinking) and first .tokenDelta; totalMs between .thinking and .turnEnd - Last-5 tool call ring dedupes on toolUseId (T-04-05-03 DoS mitigation) - .turnEnd is idempotent (double-turnEnd does not crash; second is no-op) - BoundedAsyncChannel.capacity + .policy made `nonisolated` so topology tests (AGENT-10 verification) can introspect without actor hop - 9 DevSnapshotEmitter tests added (123 AgentCore tests total, was 114) - DevOverlay tests still pass (14/14) |
||
|
|
1aeec87682 |
feat(04-05): DevOverlay package + DevSnapshot/ToolCallRow in AgentOrchestrator (OBS-01)
- NSPanel-backed DevOverlayWindow (.floating + .nonactivating) hidden by default - SwiftUI DevOverlayView renders OBS-01 surface: state/turn, provider/model, token counts, cache creation/read + hit %, latency, last-5 tool calls - @Observable @MainActor DevOverlayViewModel — single write path via apply() - DevOverlayBridge pumps BoundedAsyncChannel<DevSnapshot> → ViewModel - DevSnapshot + ToolCallRow live in AgentOrchestrator target (Rule 3 deviation from plan's files_modified: DevSnapshotEmitter consumes both OrchestratorEvent and DevSnapshot, so parking both in AgentOrchestrator avoids the circular AgentOrchestrator→DevOverlay→AgentOrchestrator dep) - Additive OrchestratorEvent.usage(turnId:usage:) — surfaces TurnUsage to DevOverlay without reading replay; no nonce (SEC-06 preserved) - AgentOrchestrator now emits .usage on LLMEvent.usage alongside replay write - 14 DevOverlay tests pass; 114 AgentCore tests pass (no regression) |
||
|
|
219532f468 |
feat(04-04): AgentOrchestrator actor + submit/cancelAndSubmit (AGENT-06)
- AgentOrchestrator actor: turn-lifecycle state machine; LLMProvider protocol-driven; injection via providerFactory closure for testability - submit(_:) returns .ran or .rejected(.turnInFlight) - cancelAndSubmit(_:) returns .superseded(priorId, .bargedIn); critical AGENT-06 actor-reentrancy guard: priorTask.cancel() THEN `_ = await priorTask.value` BEFORE assigning new currentTurn (prevents late .messageStop from cancelled stream from clobbering currentTurn) - ReplayLog wired: turnNonce persisted via startTurn(turnNonce:); full blob via .toolResultFull (AGENT-08); SEC-06 invariant (nonce never appears in OrchestratorEvent emitted to bus) - ToolResultPacker integration: 8 KB cap on model-facing string (AGENT-08); full bytes preserved for replay via separate channel - Tool-use loop: append assistant tool_use msg, dispatch via ToolDispatcher, append wrapped tool_result msg, re-invoke provider - Architectural deviation (Rule 3 — blocking dep cycle): Replay already depends on AgentCore for TurnID; the plan's literal layout would have introduced AgentCore→Replay cycle. Resolved by carving a new target `AgentOrchestrator` in the AgentCore package, depending on both AgentCore and Replay. Plan files_modified paths shifted from `Sources/AgentCore/` to `Sources/AgentOrchestrator/` accordingly - 11 new tests (OrchestratorSubmitTests + OrchestratorCancelTests) plus MockLLMProvider scriptable test double; 105 total AgentCore tests green |