Transport recovery boundaries#
Developer audit and implementation scope for #4657. The CLI/TUI recovery boundary and its limits are described below. Verification uses offline provider faults and loopback SSE fixtures, not paid inference.
Entrypoints#
Paths below are relative to the repository root.
Entrypoint |
Before shared recovery |
Recovery behavior / boundary |
|---|---|---|
CLI interactive: |
Adapter retries only before output; tagged failures return control to the user and clear queued prompts. |
|
CLI headless: same generation path |
Provider failure propagates to |
Same bounded generation recovery; exhaustion still exits, rather than starting a new process. |
TUI: |
Failure ends the worker; app stays open and may dispatch the next queued turn. |
Shared |
Server: |
Custom |
No automatic transport replay: bypasses |
ACP-backed server/session paths |
ACP streams |
Automatic replay explicitly disabled with |
Thread/subprocess subagents |
Thread generation uses core chat; subprocess monitor reports nonzero exit as failure, without respawn. |
Generation through |
Scheduled agent: |
One noninteractive invocation per scheduled run. |
A later timer run is new scheduled work, not guaranteed resumption of the failed turn. |
Completion and side-effect safety#
llm._reply_stream() can break on a complete runnable tool and drain remaining
provider events for metadata before returning. A drain failure must not execute
the failed attempt’s tool. Server custom streaming has a separate early-break
path; it does not inherit this safety contract automatically.
ToolUse._iter_from_tool requires a complete JSON boundary before parsing;
Responses native calls are withheld until reconciliation. Neither syntactic
completeness nor natural iterator exhaustion proves provider completion.
Chat Completions requires a non-null finish_reason, Anthropic requires
message_stop, and Responses requires a terminal completed/done/incomplete
event. Subscription [DONE] remains an accepted terminal sentinel. Natural EOF
without a terminal raises a typed transport failure before native reconciliation;
explicit provider failure codes and incomplete-response semantics remain distinct.
Intentional consumer closure is not interpreted as premature EOF.
Committed earlier tool results must survive retries. CLI/TUI chat.step() still
buffers the current assistant/tool results until execution finishes: this work
is not a crash-safe, exactly-once tool journal or a guarantee against replay by
an external process after a crash.
External runner audit (not fixes in this change)#
These observations concern separate local checkouts, not core guarantees:
gptme-contrib/packages/gptme-runloops/.../utils/execution.py::execute_gptmelaunches once, copies its trajectory, then removes private staging logs. Itsshell=Truegptme … | tee …pipeline lacks pipefail: tee’s success can mask gptme’s nonzero exit. It does not automatically resume the conversation.Runloops
utils/run_once.pysupports explicitresume=<session_id>by reusing a named conversation; that capability is not automatic failure recovery.Bob’s
scripts/runs/autonomous/autonomous-run.shhas a bounded, named-session 429 resume path (two attempts, cooldown and lane-lock reacquisition). Its entry guard requires exit 1 plus a 429 stderr match, while current core classifies rate limits as exit 75. Chunked transport failures do not match this path. Resume timeout remains nonzero (124). A later unrelated run is not a substitute.
The regression suites test_llm_generation_recovery,
test_subscription_terminal_recovery, test_sdk_stream_terminal,
test_recovery_nested_budget, test_chat_transient_recovery and
test_tui_recovery cover terminal-less EOF, partial preview reset, truncated
JSON/no execution, retained earlier tool results, queue retention, cancellation,
bounded physical sends, permanent errors and accurate exhaustion.