fix(clarify): unblock the agent when a prose reply is rejected against a multi-choice clarify - #84642
fix(clarify): unblock the agent when a prose reply is rejected against a multi-choice clarify#84642andyst-dev wants to merge 1 commit into
Conversation
|
Verified on your branch: |
fix(clarify): unblock the blocked agent when a prose reply is rejected against a multi-choice clarify
|
…d against a multi-choice clarify When a user answers a native multi-choice clarify with prose (e.g. asking what the options mean), _coerce_text_response returns None and resolve_text_response_for_session returned False — but it never resolved or cancelled the clarify wait. The agent thread was already blocked inside wait_for_response(), so the 'continue as a normal turn' path could not run and the wait parked for the full clarify_timeout (default 3600s), deadlocking the session for up to an hour (NousResearch#84608). Now, when a prose reply is rejected against a non-awaiting-text clarify, the clarify is resolved with the empty sentinel (same as clear_session) so the blocked agent returns promptly, while resolve_text_response_for_session still returns False so the caller dispatches the message as a normal turn. Open-ended and 'Other'-text clarifies are unchanged.
15adf13 to
ee0cd02
Compare
|
AI-assisted state-machine review; reviewed before posting. One ordering invariant may be worth pinning around the chosen “resolve sentinel + return False” behavior. A rejected prose message now has two effects: it resolves the currently blocked clarify, waking the existing agent turn, while resolve_text_response_for_session() returns False so that same prose continues through normal message dispatch. Could there be a race where the prose begins normal processing before the clarify-owning turn has observed the cancellation and relinquished its execution slot? The safety property I’d want is: A rejected clarify reply may unblock the owning turn and subsequently become normal input, but it must not create a second concurrently executing turn for that session. A deterministic regression could hold the original clarify-owning turn immediately after the sentinel wakes it, allow the prose-dispatch path to proceed, and verify that the prose remains queued/serialized until the first turn has exited or yielded session ownership. Then assert the prose is delivered exactly once afterward. It may also be useful to pin that the awakened turn cannot interpret the cancellation sentinel as permission to continue consequential work before the user's actual prose is processed. This seems distinct from the existing return-contract/sentinel observations: those establish what the two branches mean; this would establish their causal ordering when both are triggered by one message. |
|
Good observation — this is a real invariant worth pinning, and let me be precise about where it holds in this PR versus where it lives upstream. What this PR deterministically guarantees (inside What is NOT pinned here: the turn-serialization property you're pointing at — that the re-dispatched prose doesn't enter a second concurrently-executing turn for the same session before the awakened turn has yielded session ownership. That ordering lives in the agent execution loop / message dispatch above this module, which this utility doesn't import or control. So I'd split it:
What would you prefer — (a) I open a follow-up issue capturing the turn-serialization invariant (with your suggested deterministic test sketch) so it's scoped to the executor and doesn't get lost, or (b) you'd like me to attempt an integration test in this PR against the run-agent loop (heavier setup, but covers it end-to-end)? I'm happy to add (a) now and do (b) if you think it belongs in this change. |
Fixes #84608
Problem
When an agent calls
clarifywith a native multi-choice prompt and the user replies with prose (e.g. asking what the options mean), the reply is rejected as a clarify response and demoted to a queued steer — but the clarify wait is never resolved or cancelled. Since the agent thread is already blocked insidewait_for_response(), the "continue as a normal turn" path can't run, so the session parks for the fullagent.clarify_timeout(default 3600 seconds) before the queued reply is delivered. Asking a clarifying question about a clarify prompt is precisely what deadlocks the session for up to an hour.Root cause
resolve_text_response_for_session()(tools/clarify_gateway.py) returnedFalseon a rejected prose reply (matching_coerce_text_response's docstring: "so the message continues as a normal turn") but never resolved or cancelled the clarify entry. The blocked agent thread had no way to unblock.Fix
When a prose reply is rejected against a non-
awaiting_textclarify (native multi-choice), resolve the clarify with the empty sentinel — the same sentinelclear_session()uses — so the blocked agent returns promptly.resolve_text_response_for_sessionstill returnsFalse, so the caller dispatches the message as a normal turn.Open-ended clarifies and the "Other" text-capture path are unchanged (they already accept arbitrary text and never hit this branch).
Verification
Falsefromresolve_text_response_for_session, resolves the wait with the empty sentinel (blocked thread unblocks), and consumes the entry from the pending index.tests/tools/test_clarify_gateway.py: 24/24 pass.ruff checkclean on both edited files.