fix(acp): tolerate a null final_response when a turn is cancelled - #74242
fix(acp): tolerate a null final_response when a turn is cancelled#74242lxman wants to merge 1 commit into
Conversation
a033589 to
a790617
Compare
|
Thanks for the focused ACP regression fix. Current main still has the reported failure: The proposed normalization handles the null at the ACP ingress before all downstream uses, and the added prompt-level regression test exercises the cancelled response shape. Automated hermes-sweeper review. |
a790617 to
856ba53
Compare
856ba53 to
9d8e494
Compare
9d8e494 to
f34be99
Compare
|
Rebased onto Re-verified the premise on current
So the crash is still reachable, and the one-line change (
Ran outside the canonical runner — |
`result.get("final_response", "")` assumed the key is either absent or a
string, but the conversation loop can return it present-and-None — the
truncation path returns `partial_response or None` outright. A `.get`
default only applies to a missing key, so `final_response` became None and
the next line raised:
AttributeError: 'NoneType' object has no attribute 'startswith'
The adapter reported a bare JSON-RPC "Internal error" to the client, which
JetBrains treats as a non-recoverable session failure — it tears down the
session and kills the agent process, so a cancel could destroy a session
that was otherwise fine.
The rest of the codebase already treats this value as optional (see
`turn_finalizer`'s `len(final_response) if final_response else 0`); the ACP
adapter was the outlier. Normalising at the single ingress point covers all
five downstream uses.
Reported in NousResearch#73693 as the secondary exception after cancelling a hung turn.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
f34be99 to
1e73086
Compare
|
Contributing a regression test that complements this fix (from the duplicate #86817 I closed). This PR's normalization stops the Test (fits alongside your existing ACP regression tests; agent's @pytest.mark.asyncio
async def test_queued_prompt_drains_after_interrupted_turn():
"""A prompt queued after an interrupted turn must still run."""
acp_agent, state, fake, conn = make_interrupt_agent()
await acp_agent.prompt(
session_id=state.session_id,
prompt=[TextContentBlock(type="text", text="first")],
)
await acp_agent.prompt(
session_id=state.session_id,
prompt=[TextContentBlock(type="text", text="second")],
)
# Both prompts were dispatched to the agent — the queued one drained.
assert fake.runs == ["first", "second"]On the un-fixed code this test fails with the exact |
fix(acp): tolerate a null final_response when a turn is cancelled
No blocking issues found. |
What & why
Cancelling a turn could kill the session with:
surfaced to the client as a bare JSON-RPC "Internal error". JetBrains treats that
as a non-recoverable session failure: it tears down the session and kills the
agent process, so a cancel could destroy a session that was otherwise healthy.
result.get("final_response", "")assumed the key is absent-or-string, but theconversation loop can return it present-and-
None— the truncation path returnspartial_response or Noneoutright (agent/conversation_loop.py). A.getdefault only applies to a missing key, so the value stayed
None.The rest of the codebase already treats it as optional —
turn_finalizeruseslen(final_response) if final_response else 0, which is why the turn log printsresponse_len=0instead of crashing. The ACP adapter was the outlier.Normalising at the single ingress point covers all five downstream uses.
How to test
tests/acp/test_server.py::TestPrompt::test_prompt_survives_null_final_responsedrives a cancelled turn returning
final_response: None. It fails withAttributeErroron an unmodified checkout and passes with the fix.Platforms
Platform-independent. Full
tests/acpsuite run on Windows 11: 326 passed;4 pre-existing failures (
test_approval_isolation,test_edit_approval,test_ping_suppression) reproduce on an unmodified checkout.Reported as the secondary exception in #73693.