Repository navigation
fix(agent-api): serialize voice_desired_state mutations under a lock - #1928
Conversation
Fixes sonichi#1922. /voice/toggle does a read-modify-write on the module-global voice_desired_state; under a threaded server (sonichi#1921) two simultaneous toggles can interleave and land on the wrong state. /voice/set is wrapped in the same lock so a set can't slip inside a toggle. Responses are sent outside the lock (no I/O while holding it). Bare reads stay lock-free — a str assignment is atomic under the GIL. Test imports the real module over a real ThreadingHTTPServer and proves the handler acquires the lock: with voice_state_lock held by the test, toggle and set requests block until release. Goes red (AttributeError / non-blocking) when the fix is reverted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F9E832S1QZiwRdujtdKPs7
qingyun-wu
left a comment
There was a problem hiding this comment.
LGTM. The lock scope is right: RMW serialized, send_json outside the lock (no I/O while holding it), bare /voice/state reads left lock-free — a str read is atomic under the GIL, and a stale read there is indistinguishable from arriving a moment earlier. /voice/set sharing the same lock closes the set-inside-toggle interleave too.
Test is the strong kind: it importlib-loads the real src/agent-api.py and proves the handler acquires voice_state_lock (blocked-while-held, completes-after-release) rather than just asserting end states — reverting the handler hunk goes red. Confirmed hunks are disjoint from #1921 and the lock is inert until that lands. CI green.
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 47%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
| REPO = Path(__file__).resolve().parent.parent | ||
|
|
||
| # No token in env → check_auth() passes without Authorization headers. | ||
| os.environ.pop("AGENT_API_TOKEN", None) |
There was a problem hiding this comment.
Automated review: tests/agent-api-voice-state-lock.test.py:33 clears AGENT_API_TOKEN, but src/agent-api.py reads SUTANDO_API_TOKEN. If a developer or CI environment already exports the real token, the test server will still require auth and the /voice/toggle / /voice/set requests in this test will fail with 401 for the wrong reason. Please clear SUTANDO_API_TOKEN here as well (or send the auth header explicitly) so the regression test stays hermetic.
There was a problem hiding this comment.
Fixed in e5b2e7d — now clears both SUTANDO_API_TOKEN (the current name agent-api.py reads) and AGENT_API_TOKEN (legacy) so the test is hermetic regardless of which name a CI/dev env exports. Tests still pass.
|
Automated review:
|
qingyun-wu review: test cleared AGENT_API_TOKEN (an old name) but src/agent-api.py reads SUTANDO_API_TOKEN. A CI or dev environment that exports the real SUTANDO_API_TOKEN would silently put the test server into token-required mode, causing 401s for the wrong reason. Clear both names so the test is hermetic regardless of env. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EL2XBcV3RsWhevzTPtW4jw
|
Automated mergeability review (2026-07-09) Signal: NOT MERGEABLE Current blocker(s):
This is a conservative backlog triage signal. After the blockers are cleared, please rerun checks and request a focused review. |
liususan091219
left a comment
There was a problem hiding this comment.
Triage change-request (owner-approved batch, 2026-07-12 sweep). Conditions found on the current diff:
- live-test evidence required (voice/vision policy) — Voice runtime changed; body has no live repro, transcript, or concurrent request evidence.
Justification: kb triage standards (source-grep test standard, voice/vision live-test policy, necessity/coverage checks) — see sonichi/sutando-pr-triage kb/.
Address the asks and push; re-request review to clear this. If a finding is wrong, reply with the counter-evidence and it will be withdrawn.
cr-reason: live_test_required
liususan091219
left a comment
There was a problem hiding this comment.
Triage change-request (owner-approved batch, 2026-07-12 sweep). Conditions found on the current diff:
- live-test evidence required (voice/vision policy) — Voice runtime changed; body has no live repro, transcript, or concurrent request evidence.
Justification: kb triage standards (source-grep test standard, voice/vision live-test policy, necessity/coverage checks) — see sonichi/sutando-pr-triage kb/.
Address the asks and push; re-request review to clear this. If a finding is wrong, reply with the counter-evidence and it will be withdrawn.
cr-reason: live_test_required
Dismissing exact duplicate of the same sweep review (retry double-post); the original stands.
|
Before/after verification (origin/main 6388faf vs this head):
Verdict: fix is real, and this had the strongest evidence of the five (deterministic repro of the race both sides). BEHIND main, MERGEABLE. — Lucy (Mac Studio bot) |
|
@cla-assistant check |
Fixes #1922 (follow-up from the #1921 review).
What
/voice/toggledoes a read-modify-write on the module-globalvoice_desired_state. Under a threaded server (#1921, already running as a hotfix on qingyun-mac-mini) two simultaneous toggles can interleave the read and the write and land on the wrong state. This adds onethreading.Lock:/voice/toggle— RMW wrapped inwith voice_state_lock:/voice/set— same lock, so a set can't slip inside a toggle/voice/stateat agent-api.py:321) stay lock-free — a str read/assignment is atomic under the GILHunks are disjoint from #1921 (which touches lines ~284/~917) — the two merge cleanly in either order, and the lock is inert-but-harmless until #1921 lands.
Test (real module, revert-checked)
tests/agent-api-voice-state-lock.test.pyimportlib-loads the actualsrc/agent-api.pyand servesapi.Handleron a realThreadingHTTPServer, then proves the handler acquires the lock: withvoice_state_lockheld by the test, a/voice/toggleand a/voice/setrequest each block, and complete with the correct state only after release. 7/7 pass; run against unfixed code it exits 1. The three existing agent-api suites stay green.🤖 Generated with Claude Code
https://claude.ai/code/session_01F9E832S1QZiwRdujtdKPs7