This repository has been archived on 2026-10-04. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
2026-08-31 19:50:46 -04:00

9.3 KiB
Raw Permalink Blame History

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.