Skip to content

fix(acp): cancelled turn with final_response=None permanently bricks the session (every prompt queued forever) - #64471

Open
israellot wants to merge 1 commit into
NousResearch:mainfrom
israellot:fix-acp-cancel-none-response-bricks-session
Open

fix(acp): cancelled turn with final_response=None permanently bricks the session (every prompt queued forever)#64471
israellot wants to merge 1 commit into
NousResearch:mainfrom
israellot:fix-acp-cancel-none-response-bricks-session

Conversation

@israellot

Copy link
Copy Markdown
Contributor

Problem

Cancelling an ACP turn (session/cancel, e.g. hitting Stop in Zed / a VS Code ACP client) while a tool call is in flight can permanently brick the session: every subsequent user prompt is answered only with Queued for the next turn. (N queued) and never runs. The only recovery is restarting the hermes acp process.

Root cause

When a cancel lands mid tool call, run_conversation returns {"final_response": None, "interrupted": True, ...} — the key is present with value None, so in acp_adapter/server.py::prompt:

final_response = result.get("final_response", "")   # keeps None — default doesn't apply
...
suppress_interrupt_response = interrupted and final_response.startswith(  # AttributeError
    INTERRUPT_WAITING_FOR_MODEL_PREFIX
)

The AttributeError escapes prompt() before the state.is_running = False reset at the end of the post-turn tail. The session is then stuck marked running forever:

  • the client sees prompt failed ...: Internal error (acp.exceptions.RequestError via acp/task/supervisor.py);
  • every later prompt hits the if state.is_running: guard and is appended to state.queued_promptsQueued for the next turn. (N queued);
  • nothing ever drains the queue, because draining only happens at the end of a successful prompt() call that can never start.

Observed live (agent log excerpt):

acp_adapter.server: Cancelled session 145febe7-...
agent.tool_executor: Tool terminal returned error (89.92s): {"output": "[Command interrupted]", "exit_code": 130}
agent.conversation_loop: Turn ended: reason=interrupted_by_user ... response_len=0
prompt failed for 145febe7-...: Internal error
acp.exceptions.RequestError: Internal error

Trigger condition: the cancel must land while the turn has produced no partial response text yet (e.g. during a long-running terminal tool call), so final_response comes back None rather than "" or partial text.

Fix (two layers)

  1. Coerce the trigger: final_response = result.get("final_response") or ""None can no longer reach .startswith().
  2. Fix the class, not just the instance: wrap the whole post-turn tail (history persist, provenance update, auto-title, final-response delivery) in try/finally so the state.is_running / state.current_prompt_text reset is guaranteed. Any future exception in that tail can no longer leave the session stuck busy — it propagates to the client as a failed request, but the next prompt runs normally.

The diff is larger than the logic change because the tail block is re-indented under the try:; git diff -w shows the real delta.

Tests

Two regression tests in tests/acp/test_server.py:

  • test_prompt_cancelled_turn_with_none_response_does_not_brick_session — cancelled turn returning final_response=None completes with stop_reason="cancelled", leaves is_running=False and an empty queue, and a follow-up prompt runs as a normal turn instead of being queued.
  • test_prompt_tail_exception_still_releases_session — a forced exception inside the guarded tail (save_session raising) still resets is_running/current_prompt_text.
python -m pytest tests/acp/ -q
308 passed

@alt-glitch alt-glitch added type/bug Something isn't working comp/acp Agent Communication Protocol adapter sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages P3 Low — cosmetic, nice to have labels Jul 14, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: competing fix for the same None-final_response ACP crash as #61927 and #62776 (the latter combines #61927 + #50461). Flagging the cluster so a maintainer can pick the canonical fix.

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Approved

Fix for ACP cancelled turns with final_response=None causing permanent blocking. Substantial fix with 170 additions, 78 deletions. No security concerns.


Reviewed by Hermes Agent

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the targeted ACP regression fix. The current-main premise is verified: acp_adapter/server.py:1599 preserves an explicit None from result["final_response"], and :1606 calls .startswith() before the normal session release at :1646-1648. Because the busy guard at :1374-1383 queues subsequent prompts while the state remains running, the reported permanent queueing behavior follows directly.

The PR normalizes the value before that prefix check and places the post-turn release in finally; its two added tests cover both the None cancellation path and an exception during history persistence. Current origin/main has no commits after this PR's base touching acp_adapter/server.py or tests/acp/test_server.py, so this should be a clean salvage.

This is an automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform area/sessions Session lifecycle, resume, persistence, history labels Jul 16, 2026
@hopyrez366

Copy link
Copy Markdown

Confirmed on my end. Running Hermes ACP with local Ollama model (qwen3.6-27B) via Zed agent panel.

Reproduction:

  1. Start a conversation in Zed agent panel
  2. Agent starts tool calls
  3. Hit Stop/cancel mid-tool-call
  4. Session bricks permanently — every subsequent prompt returns Queued for the next turn. (N queued)
  5. Only recovery is restarting hermes acp process

This has been blocking my workflow consistently. The fix in this PR looks correct — wrapping post-turn tail in try/finally and normalizing None before .startswith() should prevent the permanent brick.

Environment: Hermes latest main, Zed stable, Linux, local Ollama at http://192.168.1.15:9090/v1

…ponse=None

A /stop (session/cancel) landing mid tool call makes run_conversation
return {final_response: None, interrupted: True} — the key is
PRESENT, so result.get("final_response", "") keeps the None and
None.startswith(INTERRUPT_WAITING_FOR_MODEL_PREFIX) raises
AttributeError. The exception escaped prompt() BEFORE
state.is_running was reset, permanently marking the ACP session busy:
every subsequent user message hit the is_running guard and was parked
as "Queued for the next turn. (N queued)" with no turn ever draining
the queue.

Two-layer fix:
- coerce final_response with "or empty-string" so None cannot reach .startswith
- wrap the whole post-turn tail (persist, provenance update,
  auto-title, final-response delivery) in try/finally that guarantees
  the is_running/current_prompt_text reset, so no future tail
  exception can brick the session again

Regression tests: cancelled turn with final_response=None releases the
session and the follow-up prompt runs; a forced tail exception still
resets is_running.
@israellot
israellot force-pushed the fix-acp-cancel-none-response-bricks-session branch from 650029f to 7daec52 Compare August 19, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history comp/acp Agent Communication Protocol adapter P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants