Repository navigation
Harden message delivery in CLI and serve/plugin mode against transient failures #80
Copy link
Copy link
Closed
Labels
bugSomething isn't workingSomething isn't working
Description
Context
Follow-up to #79 (assistant response lost on mid-stream timeout, already fixed). Further investigation found more places where a message/turn can be silently lost in the terminal (pycodeloop/cli/chat.py) and in serve/plugin mode (pycodeloop/cli/serve.py), plus gaps in the original fix's exception coverage.
Findings
Same class as #79 (incomplete partial-preservation)
pycodeloop/providers/generic.py_stream()— the except clause only coversTimeoutError,ConnectionError,urllib.error.URLError. Other exceptions raised mid-read (ssl.SSLError,http.client.IncompleteRead, a malformed chunk causingKeyError/TypeError) still propagate raw and discard the accumulatedtext/pending.pycodeloop/cli/chat.py—_text_buffer(filled by_on_text_delta) is only flushed/shown on the success path (_on_usage, called afteragent.run()returns normally)._run_turn's genericexcept Exceptionlogs a bare error and never surfaces_text_buffer, so any exception not covered by fix Per-role model fallback chains in Agent #1 above still wipes out visibly-streamed text from the terminal.
Structural (serve.py / plugin protocol)
pycodeloop/cli/serve.py— malformed NDJSON input line is silentlycontinued with no log/response, leaving the client waiting forever on that request id.pycodeloop/cli/serve.pyprotocol has no heartbeat/keepalive. During long reasoning or a long-running tool with no incremental output, a client (e.g. a VSCode extension) with its own read timeout may conclude the process died and drop the connection.pycodeloop/cli/serve.py_send()has no try/except aroundsys.stdout.write/flush. If the client already disconnected,BrokenPipeErrorpropagates through anyon_*callback,_respond_errorthen tries to send again and raises a second unhandledBrokenPipeError, killing the daemon thread silently with no actionable log.pycodeloop/cli/serve.pyconfirm()blocks onanswer_queue.get()with no timeout (unlikechat.py'sCONFIRM_TIMEOUT = 3.0auto-approve). A disconnected/hung client deadlocks that chat thread forever.pycodeloop/core/agent.py(on_message,on_tool_call,on_tool_result,on_usage,on_context, etc.) andpycodeloop/core/codeloop.py'son_message = lambda: storage.post(...)— no callback is wrapped in try/except. A transient storage failure (disk error, DB down) aborts the entire in-flight turn instead of just failing that one persistence attempt.
Proposed fixes
- Widen the
_stream()except clause to catch any exception once something has already been accumulated (textorpending), always preserving partial output instead of an exception whitelist. - In
chat.py, flush_text_bufferinto the log (as an interrupted/partial response) in_run_turn's error handler before showing the generic error. - In
serve.py, log malformed input lines and send anerrorresponse referencing the request id when parseable, instead of silently dropping them. - Add a periodic heartbeat/keepalive notification in
serve.pywhile a turn is in flight (e.g. during long provider or tool calls) so the client knows the process is alive. - Wrap
_send()in try/except forBrokenPipeError/OSError, treating a broken pipe as "client gone" (stop the loop / mark disconnected) instead of raising again from the error path. - Add a timeout to
serve.py'sconfirm()with a safe default behavior (mirroringchat.py's auto-approve-on-timeout, or auto-decline if that's safer for the non-interactive plugin context). - Wrap each
Agentcallback invocation (_notify_messageand the otheron_*call sites) in try/except, logging/tracing the failure without aborting the rest of the turn.
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't working
Activity
FernandoCelmer commentedon Aug 24, 2026
Fixed in #82.
🪲 BUG-#80: Bump shipped provider templates timeout to match new 180s …
📝 PEP8-#80: Fix ruff lint/format violations flagged by CI
🪲 BUG-#79 #80: Fix message loss in long interactions (provider, chat,…