fix(tui_gateway): keep the original prompt when a redirect corrects a turn

An accepted mid-turn redirect wrote its correction over inflight_turn["user"].
That field is the only user text session.resume can replay, so the prompt that
started the turn was gone the moment the user typed again while it ran. On the
next resume the client rebuilt the thread without it.

Record corrections in their own list instead, alongside the prompt. Renamed
_replace_inflight_user to _record_inflight_correction now that it appends.
_start_inflight_turn rebuilds the dict wholesale, so corrections cannot leak
into a later turn.
This commit is contained in:
Brooklyn Nicholson 2026-07-26 19:43:23 -05:00
parent 40dc36a842
commit 2dd4cbbe61
3 changed files with 70 additions and 9 deletions

View file

@ -66,7 +66,9 @@ def test_busy_interrupt_mode_redirects_active_turn(monkeypatch):
assert resp["result"]["status"] == "redirected"
assert seen == ["redirect"]
assert session["inflight_turn"]["user"] == "redirect"
# Appended, not overwritten: the original prompt must stay recoverable.
assert session["inflight_turn"]["user"] == "original request"
assert session["inflight_turn"]["corrections"] == ["redirect"]
assert session.get("queued_prompt") is None

View file

@ -7531,11 +7531,56 @@ def test_session_redirect_calls_capable_core_agent(monkeypatch):
"text": "use Postgres",
}
assert calls == ["use Postgres"]
assert session["inflight_turn"]["user"] == "use Postgres"
# The correction is recorded alongside the prompt that started the turn,
# never over it — resume must be able to rebuild both bubbles.
assert session["inflight_turn"]["user"] == "original request"
assert session["inflight_turn"]["corrections"] == ["use Postgres"]
assert session.get("last_active") is not None
assert before is None or session["last_active"] >= before
def test_session_redirect_records_correction_without_erasing_prompt():
"""A redirect must not overwrite the turn's original user text.
The inflight snapshot is the only thing session.resume can replay, so
overwriting ``user`` erased the prompt that started the turn and the
client repainted the thread with the user's message missing.
"""
session = {}
server._start_inflight_turn(session, "remove the session counts")
server._append_inflight_delta(session, "Moving.")
server._record_inflight_correction(session, "hurry up")
server._record_inflight_correction(session, "and the worktree ones")
snapshot = server._inflight_snapshot(session)
assert snapshot is not None
assert snapshot["user"] == "remove the session counts"
assert snapshot["corrections"] == ["hurry up", "and the worktree ones"]
def test_inflight_snapshot_omits_corrections_when_none_recorded():
session = {}
server._start_inflight_turn(session, "just the prompt")
snapshot = server._inflight_snapshot(session)
assert snapshot is not None
assert "corrections" not in snapshot
def test_new_turn_does_not_inherit_prior_turn_corrections():
session = {}
server._start_inflight_turn(session, "first prompt")
server._record_inflight_correction(session, "first correction")
server._start_inflight_turn(session, "second prompt")
snapshot = server._inflight_snapshot(session)
assert snapshot is not None
assert snapshot["user"] == "second prompt"
assert "corrections" not in snapshot
def test_session_redirect_queues_during_agent_build_window(monkeypatch):
# A fresh turn flips running=True and builds the agent asynchronously, so
# session["agent"] is briefly None. A correction landing here must queue