Files
2026-10-04 07:23:49 -04:00

81 lines
9.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.