Archived
15 KiB
15 KiB
Review Report
- Task: 20260831-105824-plan-code-ui-audit
- Agent: reviewer
- Model: openai-codex/gpt-5.6-sol
- Effort: high
- Reviewed: 2026-08-31T13:02:29Z
- Verdict: changes-requested
Findings
1. High — 12 newly claimed tests are not in the generated Xcode project; the reported count is impossible
- Evidence:
Tests/PinnedMediaResourceLoaderTests.swiftdefines 7 tests andTests/ConversationStateTests.swiftdefines 5, but neither filename appears anywhere inVoice.xcodeproj/project.pbxproj. The current test log starts onlyMediaAPITests,MediaReconcileTests, andTranscriptDedupeTests. XcodeBuildMCP therefore reports 16 tests: 1 API + 7 media reconcile + 8 transcript tests. It does not run either new file. - Contradicted claim:
iterations/04-implementation.md:88-94says 16 total while separately claiming 7 transcript + 7 resource-loader + 5 conversation-state tests plus existing media/API tests. Those components total 28, not 16.iterations/03-implementation.md:49-54likewise claims the undiscovered resource tests ran. - Additional failures hidden by non-discovery:
PinnedMediaResourceLoaderTests.swift:67expectshttps://192.168.86.2:9178/stream/track-123, butMediaAPIcorrectly builds under its/media/apibase (Sources/Media/MediaAPI.swift:125-133,185-190), so the expected URL must behttps://192.168.86.2:9178/media/api/stream/track-123.ConversationStateTests.swift:18-26expects listening after two zero ticks. EMA remains0.02304after those ticks, above the0.02agent threshold, so the test needs a third zero tick or a deterministic threshold-crossing loop.
- Impact: the parser, stream URL, and new state-machine tests have never compiled or executed. Current “16 passed” does not verify the claimed fixes.
- Correction: regenerate
Voice.xcodeprojfromproject.yml, correct the two invalid expectations, rerun all expected 28 tests, and report suites/counts from the actual result bundle.
2. High — visible history duplicates every live final, while legitimate repeated finals are still lost
- Evidence: every accepted final is added both to
eventsandhistory.entries(Sources/LiveKit/TranscriptHandler.swift:45-61,64-89).TranscriptListView.visibleRowsthen concatenates all history entries with all live events (Sources/Features/Conversation/OrbView.swift:78-92). A live final therefore renders once as a “restored” history row and once as its live event. - Repeat semantics: dedupe state records only role/text/time, not source (
TranscriptHandler.swift:31-35,45-60,82-88). Two distinct primary utterances with identical text inside five seconds are suppressed even though cross-source dedupe should suppress only the opposite-source copy. After five seconds,TranscriptHistory.addstill permanently drops an adjacent same-role/same-text repeat (Sources/LiveKit/TranscriptHistory.swift:25-31), so it will not survive relaunch. - Test gap:
TranscriptDedupeTests.swift:100-120is named “legitimate repeat after dedupe window,” but does not advance time and explicitly asserts that the repeat is suppressed. There are no restoration/max-50/live-visible-row/clear integration tests requested by iteration 04. - Impact: current messages appear twice; valid repetitions such as “yes” twice can disappear from events within five seconds or from persisted history indefinitely.
- Correction: snapshot restored entries separately from live-session history, or render a single canonical transcript collection; do not concatenate two stores containing the same finals. Track source in recent dedupe records and consume only an opposite-source duplicate. Remove permanent dedupe from persistence. Inject a clock and test before/after-window same-source repeats, cross-source pairs, restoration, live append, max-50, and clear.
3. High — server URL changes still write/apply per keystroke, active-session changes are dropped rather than deferred, and invalid values still fall back silently for media
- Evidence: Settings continues assigning
settings.serverURLfrom everyurlTextchange (Sources/Features/Settings/SettingsView.swift:18-24), andSettingsStorepersists each assignment. Conversation replacement fires for every intermediate value that parses as a valid URL (Sources/Features/Conversation/ConversationView.swift:71-83), so typing a valid hostname can launch repeated unstructured disconnect/replacement tasks. - The active-session guard simply returns (
ConversationView.swift:75); there is no state observer or pending configuration, so the change is never applied when the session later becomes idle. makeSessionmutates an invalid value to the default beforeonAppearvalidation (ConversationView.swift:22-35,58-61), so the promised original-value error cannot appear. Invalid values entered later get no visible validation error. Media still silently uses the default host for invalid visible settings (Sources/Features/Media/MediaPanelView.swift:448-459).- Impact: the iteration-04 configuration contract is not met; conversation/media can use stale or fallback configuration, and valid typing can race multiple session replacements.
- Correction: keep draft URL state local, validate on explicit Apply/Submit, show validation in Settings, persist only accepted values, serialize one replacement, and queue a pending accepted config until the current session reaches idle. Media must use the last accepted config, never derive a fallback from invalid draft text.
4. High — terminal cleanup can recursively react to its own disconnect, agent-timeout retry can be mistaken for terminal loss, and non-timeout connect failures still bypass cleanup
- Evidence:
teardownForFailure()callsteardownRoom()while session state remains.ready/.waitingAgent(Sources/LiveKit/VoiceSession.swift:182-212).teardownRoom()callsroom.disconnect()before replacingroom(:177-180). The resulting.disconnectedcallback sees the same room and active state and can invoketeardownForFailure()again (:515-533). - The intended first agent-timeout retry also calls
teardownRoom()while state is.waitingAgent(:192-196), so its intentional disconnect can enter terminal-failure cleanup instead of retrying. - A non-timeout
room.connectfailure still only assigns.failed(:134-166) after media scope may already have started; it does not recreate the room or clean media state. A user retry can therefore reuse that room. - Impact: retries and terminal teardown are race-dependent and can recurse, abort the single retry, or leave stale media/room state.
- Correction: introduce an idempotent teardown flag/state before awaiting disconnect; distinguish intentional disconnects from remote terminal disconnects; set the externally visible failure/transitional state before disconnect callbacks can run; and route every post-token/connect failure through fresh-room cleanup. Add a fake-room lifecycle seam or deterministic integration tests.
5. High — no-resume media interruption can still restart before the pause control completes
- Evidence: on
.ended,deferclearsisInterruptionActivesynchronously (Sources/Media/MediaPlayerController.swift:77-98), while the server pause is sent in an unstructured asynchronous task (:91-96). Until that request completes, polling may still receivedesired == "playing"; reconciliation sees the interruption flag already false and callsplayer.play()(:188-200,237-252). If there is no room or the request fails, server desired remains playing indefinitely. - Impact: media can restart immediately after an interruption that explicitly did not authorize resume.
- Correction: retain a separate resume-suppression state through successful server reconciliation (or synchronous local desired override plus acknowledged pause), and do not permit reconcile-driven play until a later explicit resume/play action. Add deterministic begin/end-without-resume/poll-before-ack/control-failure tests.
6. Medium — resource-loader cleanup is unused and task bookkeeping is data-racy
- Evidence:
removeLoader(_:)has no caller; repository search finds only its declaration (Sources/Media/PinnedMediaResourceLoader.swift:70-75).MediaPlayerController.wireSourceandclearMedianever remove the previous asset mapping (Sources/Media/MediaPlayerController.swift:274-283,477-486), soloaderURLsremains unbounded. activeTasksis written on the main resource-loader delegate queue (PinnedMediaResourceLoader.swift:134-135,139-149) and also from URLSession completion callbacks on another queue (:98-101) with no actor, lock, or serial queue. It uses ahashValueinteger instead ofObjectIdentifierdirectly (:98,146), allowing collisions.- For 206 responses with missing/malformed/unknown total, code falls back to
expectedContentLength, which is the returned range length, and incorrectly labels it as the full resource (:117-127,153-165). The authored tests endorse this fallback and, in any event, are not discovered. Status/cancellation behavior requested in iteration 04 has no test. - Impact: loader mappings leak across track changes, cancellation/completion can race dictionary mutation, and AVPlayer can receive an incorrect total length for partial content.
- Correction: invoke removal when replacing/clearing an item, serialize all dictionaries on one queue/lock, key by
ObjectIdentifier, and assignexpectedContentLengthonly for 200; for 206 set content length only from a known Content-Range total. Add discovered status/cancellation/bookkeeping tests.
7. Medium — microphone interruption ownership is still set before the mute succeeds and mute/unmute tasks can cross
- Evidence:
.begansetsmicMutedByInterruption = truebefore launching asynchronoussetMuted(true)(Sources/LiveKit/VoiceSession.swift:435-448)..endedcan then launchsetMuted(false)without awaiting/cancelling the begin task (:449-461). The flag therefore means “mute requested,” not “this interruption successfully muted the mic.” - Impact: rapid or malformed begin/end sequences can leave SDK mute operations completing out of order, including ending muted after a should-resume event. A failed begin-mute still causes an interruption-owned unmute attempt.
- Correction: serialize interruption microphone operations under a generation/token, mark ownership only after successful mute, and cancel/ignore superseded completions. Device qualification remains required.
Acceptance Criteria
Prior finding disposition
| Prior finding | Iteration-04 disposition |
|---|---|
| 1. Compile failure | Fixed; current build succeeds |
| 2. Failure/disconnect lifecycle | Not fixed; Finding 4 |
| 3. Media interruption | Not fixed; Finding 5 |
| 4. Text-mode mic privacy | Fixed by API guards + disabled toggle; device-unverified |
| 5. Server configuration | Not fixed; Finding 3 |
| 6. Transcript dedupe/repeats | Partial; symmetry improved, legitimate repeats still wrong; Finding 2 |
| 7. Visible history/Clear | Partial; Clear is truthful, rendering duplicates live finals; Finding 2 |
| 8. Mic interruption ownership | Partial; lifecycle resets added, async ownership race remains; Finding 7 |
| 9. Conversation state | Source fix is correct; claimed tests are undiscovered and currently contain an invalid EMA expectation |
| 10. Stale media publish | Fixed by room/session/poll generation capture; device-unverified |
| 11. Resource loader | Partial; Finding 6 |
| 12. Generic-host TLS | Fixed by host-scoped pin/default handling; network/device-unverified |
| 13. UI/accessibility parity | Correctly documented as pending separate redesign |
| 14. Model picker copy | Fixed by readiness check |
Iteration 04 request disposition
| Request item | Verdict |
|---|---|
| 1. Complete iteration 03 | Partial: stream helper and parser compile; dedupe/interruption/resource tests and semantics remain incomplete |
| 2. Terminal failure cleanup | Changes required (Finding 4) |
| 3. Text-mode mic invariant | Implemented; device-unverified |
| 4. Server config semantics | Changes required (Finding 3) |
| 5. Visible history/Clear | Changes required (Finding 2) |
| 6. Current-reference state inference | Source implemented; test target/expectation correction required |
| 7. Stale-safe media publish | Implemented by inspection; device-unverified |
| 8. Resource-loader hardening | Changes required (Findings 1 and 6) |
| 9. Configurable HTTPS trust | Implemented by inspection; external-host/device test absent |
| 10. Model picker copy | Implemented |
| 11. PLAN/README qualification | Implemented; docs now classify redesign/device work as pending |
| 12. Idle-timer wording | Implemented correctly |
Verification Evidence
- Loaded the repository XcodeBuildMCP skill and confirmed defaults: project
/Users/shane/dev/voice-ios/Voice.xcodeproj, schemeVoice_iOS, iPhone 17 Pro simulatorAFDA137E-1657-428E-BF01-E5CD7989772C. - XcodeBuildMCP
test_sim→ 16 passed, 0 failed, 0 skipped in 9.6 s. Log:~/Library/Developer/XcodeBuildMCP/workspaces/voice-ios-9eb036fdea81/logs/test_sim_2026-08-31T12-58-36-052Z_pid94423_12510633.log. The log lists onlyMediaAPITests,MediaReconcileTests, andTranscriptDedupeTests; it proves the two new test files were omitted. - XcodeBuildMCP
build_sim→ Build succeeded in 2.8 s. Log:~/Library/Developer/XcodeBuildMCP/workspaces/voice-ios-9eb036fdea81/logs/build_sim_2026-08-31T12-58-53-892Z_pid94423_5f92c809.log. git diff --check→ clean.- Inspected the complete current diff, all five test files, generated project membership, iteration 03/04 reports, product sources, PLAN/README/device docs, and prior review findings.
- Reviewer changed only artifact reports/manifest, not product implementation.
Residual Risks
- Real-device microphone, interruption, LiveKit terminal failure, wake-lock, TLS alternate-host, and supported-media behavior remain unqualified.
- Current YouTube library webm remains server-blocked for AVPlayer; a supported MP3/AAC stream is still needed for client qualification.
- UI redesign/accessibility/app icon remain intentionally pending and out of this correction scope.
- Plain LAN WebSocket signaling and rotating self-signed pin remain documented deployment risks.
Required Actions
- Regenerate the Xcode project, fix undiscovered test expectations, and prove the full expected 28-test suite runs.
- Correct Findings 2–7 and add deterministic tests or explicit integration seams for the high-risk lifecycle paths.
- Update implementation reports/test counts to match actual result-bundle evidence.
- Request another independent review before approval; retain device-only qualifications.