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>
61 lines
2.5 KiB
Swift
61 lines
2.5 KiB
Swift
import Foundation
|
|
|
|
/// One Anthropic streaming session's bookkeeping. Constructed by the
|
|
/// provider as bytes flow through the SSE reader; finalized when the
|
|
/// stream ends (cleanly or otherwise) and emitted via `Logger.info` so
|
|
/// production logs distinguish:
|
|
///
|
|
/// - Non-2xx status → standard HTTP error (caller already handles)
|
|
/// - 200 OK + 0 bytes → cache_control / auth-header rejection class
|
|
/// (the 2026-05-03 audit symptom — Anthropic accepts the request,
|
|
/// then immediately closes when an `extended-cache-ttl` marker is
|
|
/// attached to a sub-1024-token system prompt)
|
|
/// - 200 OK + bytes + 0 frames → malformed response body (parser
|
|
/// couldn't find an SSE frame)
|
|
/// - 200 OK + frames + no message_stop → genuine mid-stream
|
|
/// truncation (network drop, server timeout)
|
|
/// - 200 OK complete → healthy turn
|
|
///
|
|
/// The classifier is pure (no I/O, no logging) so tests can drive it
|
|
/// with arbitrary counter combinations. The provider does the actual
|
|
/// counting in its read loop and the logging in its finalizer.
|
|
public struct StreamOutcome: Sendable, Equatable {
|
|
public let httpStatus: Int
|
|
public let bytesRead: Int
|
|
public let framesParsed: Int
|
|
public let messageStopSeen: Bool
|
|
|
|
public init(
|
|
httpStatus: Int,
|
|
bytesRead: Int,
|
|
framesParsed: Int,
|
|
messageStopSeen: Bool
|
|
) {
|
|
self.httpStatus = httpStatus
|
|
self.bytesRead = bytesRead
|
|
self.framesParsed = framesParsed
|
|
self.messageStopSeen = messageStopSeen
|
|
}
|
|
|
|
/// Tagged human-readable diagnostic. Production callers log this once
|
|
/// per stream session at `info` level. Format is intentionally simple
|
|
/// (no JSON, no key=value soup) — the orchestrator's replay log + the
|
|
/// per-channel system.log are the structured store; this string is for
|
|
/// fast eyeballing during incident triage.
|
|
public var summary: String {
|
|
if !(200..<300).contains(httpStatus) {
|
|
return "HTTP \(httpStatus)"
|
|
}
|
|
if bytesRead == 0 {
|
|
return "200 OK + 0 bytes — probable cache_control rejection or auth/header issue"
|
|
}
|
|
if framesParsed == 0 {
|
|
return "200 OK + \(bytesRead) bytes but 0 frames parsed — malformed SSE body"
|
|
}
|
|
if !messageStopSeen {
|
|
return "200 OK + \(framesParsed) frame(s), EOF before message_stop — mid-stream truncation"
|
|
}
|
|
return "200 OK complete (\(framesParsed) frame(s), \(bytesRead) byte(s))"
|
|
}
|
|
}
|