fix(tui): eliminate TOCTOU race between goal followup and concurrent turn submission

H1: When a turn finished and a goal followup existed, the finally
block set running=False (under history_lock), then the goal followup
code re-acquired the lock and set running=True.  Between these two
lock acquisitions, a notification poller or concurrent prompt.submit
could acquire the lock, see running=False, and start a second turn on
the same session — leading to interleaved history writes.

Fix: instead of releasing and re-acquiring the turn guard, keep
running=True across the finally→goal-followup boundary.  Only set
running=False in the finally when there is NO goal followup to chain.
This eliminates the TOCTOU window entirely.

All 183 TUI tests pass, including goal command tests.
This commit is contained in:
asill-livestream 2026-06-04 06:42:31 +09:00
parent 8077e7d2fb
commit 42d7054f65
1 changed files with 11 additions and 13 deletions

View File

@ -4574,24 +4574,22 @@ def _run_prompt_submit(rid, sid: str, session: dict, text: Any) -> None:
pass
_clear_session_context(session_tokens)
with session["history_lock"]:
session["running"] = False
if not goal_followup:
# Only release the turn guard when there is no followup
# to chain. When a goal continuation exists we keep
# running=True so no other thread (notification poller,
# concurrent prompt.submit) can steal the turn during the
# gap between this finally and the followup dispatch below.
session["running"] = False
session["last_active"] = time.time()
_clear_inflight_turn(session)
_emit("session.info", sid, _session_info(agent, session))
# Chain a goal-continuation turn if the judge said so. We do
# this AFTER the finally releases session["running"], so the
# nested _run_prompt_submit doesn't deadlock on the busy
# guard. A real user prompt that races us wins because
# prompt.submit sets running=True under the history_lock and
# we check that guard before re-firing.
# Chain a goal-continuation turn if the judge said so. Because the
# finally block above intentionally kept running=True when a followup
# exists, there is no TOCTOU window — no other thread can sneak in
# and start a concurrent turn on this session.
if goal_followup:
with session["history_lock"]:
if session.get("running"):
# User already sent something — their turn wins,
# the judge will re-run on the next turn anyway.
return
session["running"] = True
try:
_emit("message.start", sid)
_run_prompt_submit(rid, sid, session, goal_followup)