Fix race conditions and error handling in async operations#95
Merged
Conversation
Correctness: - streaming/session.py: a non-CLIError exception in a parallel worker now fails the run with a clean error instead of dying with the daemon thread and letting the command exit 0 for a failed stream. - auth/loopback.py: the OAuth callback capture is now claim-once under a lock — a duplicate/late callback can no longer overwrite the captured token, and the timeout path can no longer race the handler mid-write. - tts/session.py: every protocol recv() is bounded (60s), so a server that goes silent mid-synthesis fails 'assembly speak' cleanly instead of hanging it forever. - auth/ams.py: a 2xx AMS response with an unparseable JSON body raises a clean APIError instead of escaping as a raw JSONDecodeError. - code_gen/agent.py: the generated voice-agent script's send_mic thread ends quietly when the socket closes instead of dumping a traceback on every normal exit. - commands/sessions.py: a 0-second audio duration renders as "0" instead of being coerced to a blank cell. - context.py: a TypeError while persisting browser-login credentials maps to the "could not save the credentials" message, not "Unexpected error". Cleanup: - ws.py/streaming/diagnostics.py: one shared handshake_status() classifier for 401/403 across stream/agent/speak — the two copies had already drifted on the SDK .code shape. - argscan.py/telemetry.py: quiet-flag forms (--quiet/-q) now live in argscan.requests_quiet alongside requests_json instead of being hardcoded in telemetry. https://claude.ai/code/session_01Cb2fCtBiA6LG667UnzWjvf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR fixes the 10 verified findings from a full-codebase review: it hardens error handling and fixes race conditions across concurrent operations, bounds a hang-prone receive loop, and lands two small dedup cleanups. The full
scripts/check.shgate passes (100% patch coverage; all diff-scoped mutants killed).Key Changes
Error Handling in Streaming Workers
aai_cli/streaming/session.py: Wrap non-CLIError exceptions in parallel worker threads withAPIErrorso they fail the run cleanly instead of silently dying with the daemon thread and exiting with code 0tests/test_stream_session.py: Add test verifying unexpected worker errors are caught and reported with clean output (no raw tracebacks)TTS WebSocket Timeout Bounds
aai_cli/tts/session.py: Add a 60-second timeout to allws.recv()calls via new_recv_raw()helper soassembly speakfails cleanly instead of hanging forever if the server goes silent mid-session;TimeoutErrormaps to a cleanAPIErrortests/test_tts_session.py: Add tests verifying the timeout is applied to every frame and a silent server surfaces as a clean errorOAuth Callback Capture Thread Safety
aai_cli/auth/loopback.py: Add athreading.LocktoCallbackCaptureand claim-once semantics: the first matching callback wins, and once the capture is claimed (by a callback or by the timeout inwait()), a late/duplicate callback can no longer mutate the resulttests/test_auth_loopback.py: Add tests for duplicate-callback handling and timeout claimingWebSocket Handshake Status Classification (dedup)
aai_cli/ws.py: Add a sharedhandshake_status()that reads both structured shapes (SDK.code, websockets.response.status_code);is_rejected_key()now vetoes 403 through it, so an SDK-shaped 403 with auth-worded text is no longer misclassified as a rejected keyaai_cli/streaming/diagnostics.py: Remove the duplicate_handshake_status()and use the shared classifier — the two copies had already driftedtests/test_ws.py: Add tests for both handshake exception shapes, the SDK-403 veto, and non-handshake exceptionsAMS JSON Response Validation
aai_cli/auth/ams.py: CatchValueErrorfromresp.json()and raise a cleanAPIErrorfor 2xx responses with unparseable bodies (proxy interference, truncation) instead of letting a rawJSONDecodeErrorsurface as "Unexpected error"tests/test_auth_ams.py: Add test for non-JSON 200 response handlingSessions Table Rendering
aai_cli/commands/sessions.py: DistinguishNone(missing value → blank cell) from0(legitimate zero duration → renders "0") in the duration columntests/test_sessions_command.py: Add test verifying zero duration renders as "0", not blankAuto-Login Error Handling
aai_cli/context.py: CatchTypeErrorfrom the TOML writer (non-serializable values) and emit the clean "could not save the credentials" message instead of the generic "Unexpected error"tests/test_context.py: Add test for TypeError during credential persistenceGenerated Voice-Agent Script
aai_cli/code_gen/agent.py: Guard the generatedsend_micthread'sws.send()with try/except so it ends quietly when the socket closes, instead of dumping a daemon-thread traceback on every normal exit of the sample script (the ready-gate was restructured toif not ready.is_set(): continue— equivalent logic, just reshaped for the guard)tests/test_code_gen_stream_agent.py: Add test verifying the generated code handles socket close gracefullyQuiet-Flag Dedup
aai_cli/argscan.py: Addrequests_quiet()so the--quiet/-qtoken forms live next torequests_json()aai_cli/telemetry.py: Use it in_notice_suppressed()instead of a hardcoded duplicate listhttps://claude.ai/code/session_01Cb2fCtBiA6LG667UnzWjvf