Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions acp_adapter/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1383,6 +1383,13 @@ async def prompt(
return PromptResponse(stop_reason="end_turn")
state.is_running = True
state.current_prompt_text = user_text or "[Image attachment]"
# A real turn is starting, so any prompt salvaged for a later
# /steer after a prior client cancel is now stale — the user moved
# on to this turn. Drop it so a /steer on a future idle session
# can't resurrect an abandoned prompt. The /steer salvage path
# above already consumed and cleared it before reaching here, so
# this only discards a value left by an intervening completed turn.
state.interrupted_prompt_text = ""

logger.info("Prompt on session %s: %s", session_id, user_text[:100])

Expand Down
33 changes: 33 additions & 0 deletions tests/acp_adapter/test_acp_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,39 @@ async def test_acp_steer_after_zed_interrupt_replays_interrupted_prompt_with_gui
assert state.interrupted_prompt_text == ""


@pytest.mark.asyncio
async def test_acp_normal_turn_clears_stale_interrupted_prompt():
"""A completed normal turn must not leave a salvageable prompt behind.

Regression: interrupted_prompt_text is set only on a running-cancel and
cleared only inside the /steer salvage path. If the user cancels a
running turn, then sends an ordinary prompt (which runs to completion),
the interrupted prompt was never cleared — so a later /steer on the idle
session would resurrect and re-run the task the user cancelled and moved
on from. Starting any real turn must drop the stale salvage buffer.
"""
acp_agent, state, fake, _conn = make_agent_and_state()
# Left over from a prior running-cancel of "refactor the auth module".
state.interrupted_prompt_text = "refactor the auth module"

# An ordinary prompt runs to completion in between.
await acp_agent.prompt(
session_id=state.session_id,
prompt=[TextContentBlock(type="text", text="what's the weather")],
)
assert state.interrupted_prompt_text == ""
assert fake.runs == ["what's the weather"]

# Now /steer on the idle session must run ONLY the steer text, not the
# abandoned prompt.
fake.runs.clear()
await acp_agent.prompt(
session_id=state.session_id,
prompt=[TextContentBlock(type="text", text="/steer be concise")],
)
assert fake.runs == ["be concise"]


@pytest.mark.asyncio
async def test_acp_steer_on_idle_session_runs_as_regular_prompt():
# /steer on an idle session (no running turn, nothing to salvage) should
Expand Down
Loading