fix(turn,backend): confirm submit on turn records only, resend lost paste, longer tui wait
This commit is contained in:
@@ -89,7 +89,7 @@ class BackendOptions:
|
||||
history_injection_mode: HistoryInjectionMode = "native_jsonl"
|
||||
wait_for_turn_duration: bool = False
|
||||
include_meta_user: bool = False
|
||||
startup_delay: float = 10.0
|
||||
startup_delay: float = 60.0
|
||||
file_wait_timeout: float = 30.0
|
||||
turn_duration_timeout: float = 5.0
|
||||
|
||||
|
||||
+57
-18
@@ -52,7 +52,7 @@ _TERMINAL_STOP_REASONS: frozenset[str] = frozenset(
|
||||
|
||||
_DEFAULT_FILE_WAIT_TIMEOUT = 30.0
|
||||
_DEFAULT_TURN_DURATION_TIMEOUT = 5.0
|
||||
_DEFAULT_STARTUP_DELAY = 10.0
|
||||
_DEFAULT_STARTUP_DELAY = 60.0
|
||||
|
||||
# DECSET 2004 — claude's TUI enables bracketed paste mode early in init.
|
||||
# This is necessary (without it our paste markers parse as raw ANSI and
|
||||
@@ -169,8 +169,7 @@ class TurnManager:
|
||||
"""
|
||||
if self._started:
|
||||
_log.info(
|
||||
"start: session_id=%s already started, skip",
|
||||
self._pty.session_id,
|
||||
"start: session_id=%s already started, skip", self._pty.session_id
|
||||
)
|
||||
return
|
||||
_log.info(
|
||||
@@ -197,10 +196,24 @@ class TurnManager:
|
||||
_TUI_READY_MARKER, timeout=self._startup_delay
|
||||
)
|
||||
t_marker = loop.time() - t0
|
||||
if marker_found:
|
||||
_log.info(
|
||||
"start: session_id=%s marker DECSET2004 %s after %.2fs",
|
||||
"start: session_id=%s marker DECSET2004 FOUND after %.2fs",
|
||||
self._pty.session_id,
|
||||
t_marker,
|
||||
)
|
||||
else:
|
||||
# Proceeding anyway is a coin flip: without bracketed
|
||||
# paste enabled our ESC[200~ framing is parsed as junk
|
||||
# and the prompt vanishes silently. We still continue
|
||||
# rather than fail the turn — `send_user_message`
|
||||
# notices the missing turn record and re-sends — but
|
||||
# this is the line to grep for when a prompt is lost.
|
||||
_log.warning(
|
||||
"start: session_id=%s marker DECSET2004 TIMED_OUT after "
|
||||
"%.2fs — TUI not ready, the first prompt may be dropped "
|
||||
"(raise startup_delay if this recurs)",
|
||||
self._pty.session_id,
|
||||
"FOUND" if marker_found else "TIMED_OUT",
|
||||
t_marker,
|
||||
)
|
||||
# Phase 2: wait for the TUI render burst to settle —
|
||||
@@ -226,10 +239,7 @@ class TurnManager:
|
||||
)
|
||||
await asyncio.sleep(self._startup_delay)
|
||||
self._started = True
|
||||
_log.info(
|
||||
"start: session_id=%s READY",
|
||||
self._pty.session_id,
|
||||
)
|
||||
_log.info("start: session_id=%s READY", self._pty.session_id)
|
||||
|
||||
async def send_user_message(self, text: str) -> AsyncIterator[Event]:
|
||||
"""Send `text` as a user prompt and stream events until turn-end.
|
||||
@@ -299,12 +309,12 @@ class TurnManager:
|
||||
|
||||
while True:
|
||||
records = await self._watcher.read_once()
|
||||
if records and not submit_confirmed:
|
||||
if records and not submit_confirmed and _has_turn_record(records):
|
||||
submit_confirmed = True
|
||||
if submit_attempts > 1:
|
||||
_log.info(
|
||||
"send_user_message: session_id=%s SUBMIT confirmed "
|
||||
"after %d Enter(s), %.1fs",
|
||||
"after %d attempt(s), %.1fs",
|
||||
sid,
|
||||
submit_attempts,
|
||||
loop.time() - t_loop_start,
|
||||
@@ -354,15 +364,32 @@ class TurnManager:
|
||||
f"{now - t_loop_start:.0f}s ({self._watcher.path})"
|
||||
),
|
||||
)
|
||||
# Escalate: a swallowed Enter and a swallowed paste
|
||||
# look identical from here, but need different
|
||||
# remedies. Try the cheap, duplicate-proof one
|
||||
# first (a bare Enter is a no-op on an empty input
|
||||
# box); only re-send the whole prompt once that has
|
||||
# failed, since the paste itself must then have
|
||||
# been eaten — e.g. written before claude's TUI
|
||||
# enabled bracketed paste, which turns our markers
|
||||
# into discarded junk.
|
||||
if submit_attempts == 1:
|
||||
_log.warning(
|
||||
"send_user_message: session_id=%s no JSONL record %.1fs "
|
||||
"after Enter #%d — paste likely stuck in the input box, "
|
||||
"re-pressing Enter",
|
||||
"send_user_message: session_id=%s no turn record "
|
||||
"%.1fs after Enter — re-pressing Enter",
|
||||
sid,
|
||||
_SUBMIT_CONFIRM_WINDOW,
|
||||
submit_attempts,
|
||||
)
|
||||
await self._send_submit_key()
|
||||
else:
|
||||
_log.warning(
|
||||
"send_user_message: session_id=%s still no turn "
|
||||
"record after attempt #%d — the paste itself was "
|
||||
"lost, re-sending the whole prompt",
|
||||
sid,
|
||||
submit_attempts,
|
||||
)
|
||||
await self._pty.write(text)
|
||||
submit_attempts += 1
|
||||
submit_deadline = now + _SUBMIT_CONFIRM_WINDOW
|
||||
# Heartbeat log every 10s so a hung wait is visible.
|
||||
@@ -412,9 +439,7 @@ class TurnManager:
|
||||
)
|
||||
except MessageParseError as exc:
|
||||
_log.warning(
|
||||
"send_user_message: session_id=%s parse error: %s",
|
||||
sid,
|
||||
exc,
|
||||
"send_user_message: session_id=%s parse error: %s", sid, exc
|
||||
)
|
||||
if self._on_parse_error is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
@@ -556,6 +581,20 @@ class TurnManager:
|
||||
await self.aclose()
|
||||
|
||||
|
||||
def _has_turn_record(records: Iterable[JsonlRecord]) -> bool:
|
||||
"""Does this batch prove claude actually accepted our prompt?
|
||||
|
||||
Only ``user`` / ``assistant`` records do. A session writes a burst of
|
||||
bookkeeping on startup — ``mode``, ``permission-mode``, and, under
|
||||
``--remote-control``, ``bridge-session`` plus a ``system`` /
|
||||
``bridge_status`` heartbeat — none of which depend on a prompt having
|
||||
been submitted. Treating "any record at all" as proof made the submit
|
||||
check pass vacuously on exactly the cold-start turns it exists to
|
||||
catch.
|
||||
"""
|
||||
return any(rec.get("type") in ("user", "assistant") for rec in records)
|
||||
|
||||
|
||||
def _extract_duration_ms(data: dict[str, Any]) -> int | None:
|
||||
"""Pull a turn-duration value out of a `system.subtype=turn_duration` record."""
|
||||
for key in ("durationMs", "duration_ms"):
|
||||
|
||||
Reference in New Issue
Block a user