mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
1. Lazy agent loading - fix race condition where prompts sent before initialization completes:
- Add ensureAgentLoaded() helper that deduplicates initialization requests
- Update handleSendAgentMessage/Audio to await agent initialization before streaming
- Fix status updates being sent to client after initialization
2. Type safety - remove unsafe 'as any' casts for agent status:
- Create AGENT_LIFECYCLE_STATUSES constant as single source of truth
- Export AgentStatusSchema from messages.ts for reuse
- Update registry schema to validate lastStatus against AgentStatusSchema
- Add .default("closed") to handle missing status values from legacy files
- Remove (record.lastStatus as any) cast in buildStoredAgentPayload
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
4.8 KiB
4.8 KiB
Android Voice Recording Fix – Code Review
Correctness
- Using Expo's recorder instance as the single source of truth is the right direction, but the new guard in
stop()(src/hooks/use-audio-recorder.native.ts:247-254) now throws as soon as Expo'sisRecordingflag isfalse, even if we initiated a session (recordingStartTimeis still set) and the OS stopped it for us (focus loss, phone call, permission revocation, etc.). In that situation we still have to callstop()to fetch the URI/work around the SDK bug, but the hook now exits early with “Not recording”, so the audio file is lost and UI state machines (e.g. dictation) never advance. Previously we relied on our ownisRecordingstate, so callers could still finalize the recording even if Expo toggled its flag behind our back. - The dictation modal relies on the recorder object changing identity to re-run its cleanup effects. After memoizing the hook's return value,
dictationRecorderno longer changes when recording starts or stops. When the user closes the modal whiledictationRecorder.start()is still awaiting (permission prompt, slow prepare), the effect atsrc/components/create-agent-modal.tsx:1164-1170runs once, seesisRecording?.()asfalse, and returns. When the native recorder finally transitions to recording there is no dependency change, so we never auto-stop; the microphone keeps running in the background until unmount. The previous implementation re-rendered whenisRecordingstate flipped, so the cleanup path executed. This is a regression introduced by the memoization/state removal.
Edge Cases
- Similar races exist anywhere we call
dictationRecorder.stop()conditionally (src/components/create-agent-modal.tsx:923-925,1172-1178). Those guards now consult the asynchronous native flag; if the OS toggles it before we read it, we skip cleanup entirely (andstop()would throw even if we tried). We need a fallback based on our own intent (e.g.,recordingStartTime !== null) to ensure we always clean up sessions we started.
Performance
- The metering effect (
src/hooks/use-audio-recorder.native.ts:175-193) now depends onrecorderState.meteringand spins up asetInterval.useAudioRecorderStatealready polls every 100 ms and triggers a render. BecauserecorderState.meteringis a dependency, this effect is torn down and recreated at that same cadence, so the interval almost never fires and we constantly allocate/clear timers. At best this starves the audio-level callback, at worst it's extra work on every render. We should either remove the interval and invoke the callback directly whenrecorderState.meteringchanges, or keep the interval but depend only onrecorderState.isRecording/configRef.
Code Quality
recordingOptionsstill depends onconfig?.onAudioLevelby reference (src/hooks/use-audio-recorder.native.ts:141-164). That means any caller that passes an inline callback will recreate the entire recorder on every render, reintroducing the race this fix aims to solve. The hook already captures the callback in a ref; the dependency only needs the boolean!!config?.onAudioLevelto flip metering on/off. Requiring every consumer to remember to wrap their callback inuseCallbackmakes this API fragile and hard to use correctly.
Completeness
- Only two call sites were updated to stabilize the callback, but
useAudioRecorderis exported for general use (src/app/audio-test.tsx, future flows). Unless we address the dependency noted above, any new screen that passesonAudioLevel={level => …}will regress immediately. Consider hardening the hook so callers cannot accidentally reintroduce the bug.
Testing Recommendations
- Start dictation, dismiss the modal (or navigate away) while the permission prompt is still showing, and verify that recording stops automatically once the modal is gone.
- Force Android to interrupt recording (receive a phone call / unplug the mic) and ensure
stop()still returns a blob instead of throwing “Not recording”. - Verify the audio level meter updates smoothly for a prolonged recording session (the current interval churn may cause it to freeze).
- Confirm the new error-handling path actually resets
isDictating,isDictationProcessing, and UI affordances after simulated failures (permission denied,sendAgentAudiorejection).
Potential Improvements
- Treat
!!config?.onAudioLevelas the dependency inrecordingOptions(and/or derive metering enablement internally) so callers no longer need to memoize callbacks. - Rework metering to emit directly from
useAudioRecorderStateupdates (or keep a ref torecorderState), avoiding the redundant timer and ensuring the callback fires consistently. - Augment
stop()to fall back torecordingStartTime/recorder.urieven if Expo'sisRecordingflag is already false so we can always deliver/clean up recordings we initiated.