fix(acp): reset is_running when prompt() raises before the executor guard - #71028
fix(acp): reset is_running when prompt() raises before the executor guard#71028israellot wants to merge 1 commit into
Conversation
|
Thanks — the reported failure window is present on current Problems
Suggested changes
This is an automated hermes-sweeper review. |
551e898 to
92f53b6
Compare
|
Both points addressed in 1. Conflicting branch / risk of losing the newer session-cwd binding. Rather than resolving the old reindent mechanically, I reset the branch to current
2. Test moved into the current Verification on
|
|
CI failure on this PR is unrelated to the change — flagging it rather than working around it, since the flake is in The failure
This PR touches only Why it flakes (mechanism, not a guess) The failing line is the fixture precondition, before
A 1200-file checkout on a loaded 8-worker runner takes long enough to straddle a second boundary, so a subset of files falls outside the racy window — hence Suggested fix for the fixture: make the precondition independent of mtime granularity — either drop the One thing worth a second look beyond the test The same stat-cache behaviour reaches the product code, so this may be more than a flaky assertion. Case B is exactly the outcome the docstring says the coupling exists to prevent ("pinning alone would expose every text file as modified and hand the update an autostash of the whole tree"). If that's right, the repair wants a forced refresh ( (Environment note: the CI runner is git 2.54.0 on runner image 20260720.247.2; the numbers above are from git 2.43.0 locally, where 5 of the 9 tests in this file fail for the same stat-cache reason. The mechanism is version-independent; how often it bites is not.) Happy to open a separate issue or PR for the fixture fix if that's useful — just say which you'd prefer. Nothing on this branch needs to change for it, so this PR is ready as-is apart from the red check. |
…d don't resurrect stale pre-reset history fc05247 (NousResearch#80770) fixed tui_gateway/server.py replaying a turn's pre-turn history snapshot on the next prompt after a mid-turn crash: AIAgent persists its working transcript into agent._session_messages independently of the gateway's own in-memory history, via agent._persist_session() -- called per tool round/API call throughout a turn (agent/conversation_loop.py), not just at clean completion. If a turn raises, that working copy is ahead of whatever snapshot the caller was holding before the turn started. acp_adapter/server.py::_run_agent() has the identical shape: on the exception path, prefer agent._session_messages (the crash-time working copy) over state.history when it's a list; fall back to state.history unchanged if the crash happened before any persist ran. Review found the isinstance(agent_messages, list) guard never actually discriminates: agent/agent_init.py initializes _session_messages to [] (never None), so it's ALWAYS a list, on a real agent as much as mid-turn. Two consequences: - The intended fallback path only ever ran in the original test's mock agent, whose unconfigured _session_messages auto-attribute (a MagicMock, not a list) is what made isinstance() false there -- not anything a real agent produces. - More seriously: /reset (_cmd_reset) clears state.history but never touches agent._session_messages -- reset_session_state() (run_agent.py) only resets session-scoped token counters and the context-compressor engine, not this cache. A crash before the FIRST persist of a NEW turn right after /reset returned the _session_messages leftover from the PRE-reset conversation (a non-empty list, passes isinstance()), resurrecting the "cleared" conversation on the next prompt. Fix: snapshot _session_messages' content before the turn starts, and on the exception path only adopt it when it actually changed during this turn (persist ran) -- otherwise it's stale, and state.history (the correct pre-turn/post-reset value) is what belongs on the next prompt. Testing: - Updated the existing "no working history" test to set agent._session_messages = [] explicitly (what a real agent's pre-persist state actually is) instead of relying on the mock's unconfigured auto-attribute -- exercising the real invariant instead of an artifact of the mock. - Added a new regression reproducing the reported /reset bug directly: pre-reset _session_messages present, state.history cleared, turn crashes before any persist -- asserts state.history stays empty instead of adopting the stale pre-reset transcript. - Mutation-verified: reverting just the production fix reproduces the reported bug in the new /reset test (state.history resurrects the pre-reset conversation); the other two tests are unaffected either way, confirming they don't (by themselves) catch this specific bug. - Full tests/acp/ (129) + tests/acp_adapter/ (15) pass. - ruff check clean. Note: NousResearch#71028 (open, unrelated fix for a session-bricking bug in a different raise window earlier in prompt()) re-indents this same except-block as part of wrapping the whole method in an outer try -- its diff shows the block's content is otherwise unchanged. Textual (re-indentation) merge-conflict risk only, not a semantic one.
…uard prompt() flips state.is_running=True under the runtime lock, but its protective try blocks only cover the executor call and nothing before it. The SETUP region in between — callback-factory construction (make_tool_progress_cb / make_message_cb / ...), session-context and edit-approval requester wiring, agent callback assignment — is an unprotected raise window: any exception there escapes with is_running still True, permanently bricking the session. Every subsequent user message then hits the busy guard and gets "Queued for the next turn. (N queued)" with no turn ever draining the queue; the only recovery is restarting the editor/agent process. Fix at the root cause: wrap everything from immediately after the is_running=True flip through the end of the post-turn tail in one outer try whose except BaseException resets is_running/current_prompt_text under the runtime lock and re-raises. The existing inner executor except (which also resets and shapes the error response) is kept unchanged — the outer reset is idempotent with it. The busy-branch early returns happen BEFORE is_running is set and stay outside the guard. Re-applied on current main rather than merge-resolving the previous branch: its raw diff was ~570 lines of re-indentation over a stale base, and mechanically resolving that risked dropping the session binding main added since. The re-indent was done programmatically on asserted block boundaries; git diff -w shows +18/-0, and the newer set_session_vars() call is preserved inside the guard. Test drives the real prompt() path with make_message_cb (a setup-region callback factory) raising, asserts is_running is reset, then proves a follow-up prompt on the same session runs instead of queueing into a dead session.
92f53b6 to
2c36f0f
Compare
|
Rebuilt on current 1. "GitHub reports this branch as conflicting; resolving the reindent mechanically risks losing newer main behavior"I did not resolve the conflict. I reset the branch to current The re-indent was applied programmatically on asserted block boundaries (first line of the guarded region and the final 18 lines added, 0 removed, with whitespace ignored. That is the whole functional change: the outer The specific newer behavior you flagged survives, one indent level deeper inside the guard:
2. "Move the regression test into the current
|
What & why
prompt()inacp_adapter/server.pysetsstate.is_running = Trueunder the runtime lock, but its exception protection only begins at the executor call much later. Everything between the flip and the executortry— callback-factory construction (make_tool_progress_cb/make_message_cb/ …), the edit-approval requester wiring, agent callback assignment — is an unprotected raise window.If anything in that setup region raises, the exception escapes with
is_runningstillTrueand the session is permanently bricked: every subsequent user message hits the busy guard and is answered withQueued for the next turn. (N queued), and no turn ever drains the queue. The only recovery is restarting the editor / agent process.Fix at the root cause: open one outer
try:immediately after the runtime-lock block that setsis_running=True, enclosing the setup region, the executor call, and the existing post-turn tail. It closes withexcept BaseException:that resetsis_running/current_prompt_textunder the runtime lock and re-raises. The existing inner executorexcept(which also resets and shapes the error response) is kept unchanged — the outer reset is idempotent with it. The busy-branch early returns happen beforeis_runningis set and stay outside the guard.The diff is mostly re-indentation of the guarded block;
git diff -wshows the real change is ~18 added lines.How to test
New test:
TestPrompt::test_prompt_setup_exception_still_releases_session— patchesacp_adapter.server.make_message_cb(a setup-region callback factory) to raise, callsprompt(), and asserts:state.is_runningisFalseafterwards andcurrent_prompt_textis cleared,prompt()on the same session runs (run_conversationmocked) instead of queueing into a dead session, andqueued_promptsstays empty.Before the fix, step 2 fails:
AssertionError: setup exception left is_running=True — session bricked.Verification on this branch:
bash scripts/run_tests.sh tests/acp/test_server.py -q→ 87 passed, 0 failedbash scripts/run_tests.sh tests/acp/ -q→ 311 passed, 0 failed (13 files)scripts/check-windows-footguns.py acp_adapter/server.py tests/acp/test_server.py→ no footgunsReproduction sketch
Any transient failure in the setup region reproduces it, e.g. a raise inside a callback factory or the requester wiring:
Platforms tested
Linux fully verified; Windows/macOS not manually tested (change is pure Python control flow, no platform-specific I/O; footguns check clean).
Related — independent
final_response=Nonebricks the session) — same brick symptom, different raise window (post-turn tail vs. this PR's pre-executor setup region); independent, they compose.Prior art: this fix has been running in a public fork — YallaPlay/hermes-agent@6942586