feat: consolidate iOS with API and web preserving history
Rebuild & Deploy / rebuild (push) Failing after 23s

This commit is contained in:
snowspeeder committed 2026-10-04 07:23:49 -04:00
commit d735ee72fa
102 files changed
+13077 -258

No files matched your search

@@ -0,0 +1,80 @@
# Review Report
- Task: 20260831-105824-plan-code-ui-audit
- Agent: reviewer
- Model: openai-codex/gpt-5.6-sol
- Effort: high
- Reviewed: 2026-08-31T14:25:00Z
- Verdict: changes-requested
## Findings
### 1. High — a newer microphone interruption can still finish with the microphone enabled
- **Verified source facts:** `.began` computes `wasActive` before awaiting the prior operation (`Sources/LiveKit/VoiceSession.swift:515-533`). In the required begin A → end A/unmute in flight → begin B sequence, A has left `isMuted == true` until its SDK unmute completes, so begin B captures `wasActive == false`. B then waits for A's end task; A completes, enables the microphone, and sets `isMuted = false` (`:580-585`). B resumes but exits at `guard wasActive` (`:531-535`) without muting.
- **Impact:** the retained task chain orders operations, but the final state can still be microphone-on during interruption B—the privacy failure iteration 07 was intended to remove.
- **Correction:** do not snapshot transient `isMuted` before the predecessor completes. Track the user's intended pre-interruption mute state separately, or decide/force the newer begin's mute after its predecessor finishes. Add a deterministic microphone-operation seam for begin A → end A suspended in unmute → begin B, asserting the final hardware/UI state is muted.
### 2. High — server replacement coalescing stops after one pending replacement
- **Verified source facts:** the first replacement consumes one `pendingServerURL` and launches a nested second task (`Sources/Features/Conversation/ConversationView.swift:122-147`). If URL C is accepted while that second replacement is awaiting `currentSession.disconnect()`, the top-level `.onChange` sees `isReplacingSession == true` and stores C at `:92-96`; however, the nested task only sets `isReplacingSession = false` at `:146` and never checks pending again.
- **Impact:** rapid A → B → C Applies can finish with a session and `activeServerURL` on B while the persisted/visible accepted setting is C. No further `onChange` occurs, so conversation and media can target different servers until another distinct edit or relaunch.
- **Correction:** use one retained replacement worker that loops until no pending latest URL remains. Each loop must capture/validate the URL, disconnect the exact current session, construct from that captured config, and atomically update session/active URL. Add an A → B → C coordinator test, including C arriving while B disconnects.
### 3. Medium — terminal failure cleanup leaves old session identity and error state visible into retry
- **Verified source facts:** explicit `disconnect()` clears `agent`, `isMuted`, `micError`, `models`, reconnect flags, and interruption ownership (`Sources/LiveKit/VoiceSession.swift:464-486`). `teardownForFailure(message:)` clears only wait/generation/interruption/state-model/wake-lock/media/room state (`:243-265`); it does not clear `agent`, `isMuted`, `micError`, `models`, or `isReconnecting`. `ConversationView` displays `agent` without requiring `.ready` (`Sources/Features/Conversation/ConversationView.swift:173-177`).
- **Impact:** after a terminal disconnect, the failed and subsequent connecting/waiting UI can show the old agent and microphone error. A retry can also retain the previous server's model list until replacement events arrive, allowing stale session identity/configuration data to bleed into a fresh lifecycle.
- **Correction:** make failure teardown reset the same per-room/session fields as explicit disconnect before publishing `.failed`, while preserving only deliberately persistent settings/history/tasks. Ignore control events outside active connecting/waiting/ready states. Add a small state-reset test seam if the Room cannot be faked.
## Acceptance Criteria
### Iteration 07 disposition
| Item | Result | Evidence / qualification |
|---|---|---|
| 1. Failure teardown blocks retry | Partial | `.tearingDown` correctly withholds `.failed`/Start until media and room cleanup finish, and `connect()` rejects that state. Per-session published fields are not fully reset (Finding 3). |
| 2. One retained mic operation chain | Changes required | Begin and end are retained and ordered, but begin snapshots transient mute state too early and can skip the final mute (Finding 1). |
| 3. Consume matched dedupe record | Implemented | Both suppress paths remove the record; P1/D1/P2/D2 and inverse tests pass with canonical and persistence counts of two. |
| 4. Coalesced captured-config replacement and failed Apply | Changes required | Captured configs, direct observation, and failed-state Apply are implemented; arbitrary latest-wins coalescing stops after the second replacement (Finding 2). |
| 5. Retained media interruption task/token | Implemented by inspection | A newer begin increments sequence, cancels prior work, and the resume path rechecks cancellation, sequence, active interruption, scope, and room immediately before play. Device qualification remains residual. |
| 6. Accurate suite evidence/title | Implemented | Current source/log breakdown is Transcript 24, MediaReconcile 7, ConversationState 5, MediaAPI 1, Pinned Loader 12; total 49. Unknown-total title is corrected. |
### Iteration 06 finding disposition
| Prior finding | Iteration 07 result |
|---|---|
| Failure/retry overlap | Core retry race fixed with `.tearingDown`; state reset remains incomplete (Finding 3). |
| Mic begin/end ordering | Not fully fixed (Finding 1). |
| Repeated-pair transcript loss | Fixed and tested in both orders. |
| Server replacement identity | Two-value case fixed; three-or-more coalescing remains incorrect (Finding 2). |
| Old media resume during newer interruption | Fixed by cancellation/sequence/active-state checks; device-unverified. |
| Failed server lockout/direct observation | Fixed: failed Apply is allowed and Settings directly observes the session. |
| Suite count accuracy | Fixed for iteration 07/current aggregate evidence. |
The current code cannot be approved while Findings 1–3 remain. Real-device, current-library media-format, and separate Hope UI-redesign work are qualifications below, not code findings in this review.
## Verification Evidence
- Read `iterations/07-request.md`, `iterations/07-implementation.md`, prior `iterations/06-review.md`, aggregate implementation/manifest, and the exact current lifecycle, interruption, server replacement, transcript, resource-loader, media, and test sources.
- `git status --short && git diff --check && git diff --stat` → 18 tracked product/config/doc files modified plus untracked `Tests/` and `.artifacts/`; diff check clean before reviewer artifact writes. Reviewer did not modify product code.
- Loaded the repository XcodeBuildMCP skill. `session_show_defaults` → `/Users/shane/dev/voice-ios/Voice.xcodeproj`, scheme `Voice_iOS`, iPhone 17 Pro simulator `AFDA137E-1657-428E-BF01-E5CD7989772C`.
- XcodeBuildMCP `test_sim` → **49 passed, 0 failed, 0 skipped** in 9.3 s. Log: `~/Library/Developer/XcodeBuildMCP/workspaces/voice-ios-9eb036fdea81/logs/test_sim_2026-08-31T14-20-50-489Z_pid94423_32855de5.log`; result bundle: `~/Library/Developer/XcodeBuildMCP/workspaces/voice-ios-9eb036fdea81/result-bundles/test_sim_2026-08-31T14-20-50-489Z_pid94423_fd18ee90.xcresult`. Log line 276 confirms 49 tests/5 suites and lines 198/258/270/271/275 confirm all suite executions.
- XcodeBuildMCP `build_sim` → **Build succeeded** in 3.2 s. Log: `~/Library/Developer/XcodeBuildMCP/workspaces/voice-ios-9eb036fdea81/logs/build_sim_2026-08-31T14-21-04-018Z_pid94423_8f26cb11.log`. Only AppIntents metadata-skip noise appeared; no Swift compiler warning.
- `rg -c '@Test\(' Tests/*.swift` → ConversationState 5, MediaAPI 1, MediaReconcile 7, PinnedMediaResourceLoader 12, TranscriptDedupe 24 (49 total).
- Generated `Voice.xcodeproj/project.pbxproj` contains all five test files in the test Sources phase.
- Findings 1–3 are deterministic source-order/state analyses. Runtime reproduction was not claimed because no fake microphone, Room, or server-replacement coordinator seams exist in the current test target.
## Residual Risks
- **Real device:** microphone capture/permission, actual AVAudioSession mic and media interruption timing, remote TTS audibility, wake lock, side-button behavior, and supported-format playback remain unqualified.
- **Server format:** the current YouTube library is webm and cannot qualify AVPlayer playback; use an MP3/AAC endpoint on device. Resource-loader status/cancel/range behavior still lacks a true network integration test.
- **Separate UI scope:** Hope LED-wall redesign, complete accessibility/Dynamic Type/reduced-motion work, visualizer/barge-in refinement, and app icon remain intentionally separate and unimplemented.
- Plain LAN `ws://` signaling and rotation of the self-signed pinned certificate remain documented deployment risks.
## Required Actions
1. Correct Findings 1–3 without broad UI redesign.
2. Add pure/deterministic tests for the mic chain's transient-state case and an arbitrary latest-wins replacement loop (at least A→B→C); cover failure field reset where practical.
3. Re-run all XcodeBuildMCP tests, separate simulator build, suite-count inspection, and `git diff --check`, then request another independent review.
4. Keep device, server-format, and Hope UI work explicitly qualified rather than treating them as blockers to this code-correctness loop.