You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 covers TimeoutError, ConnectionError, urllib.error.URLError. Other exceptions raised mid-read (ssl.SSLError, http.client.IncompleteRead, a malformed chunk causing KeyError/TypeError) still propagate raw and discard the accumulated text/pending.
pycodeloop/cli/chat.py — _text_buffer (filled by _on_text_delta) is only flushed/shown on the success path (_on_usage, called after agent.run() returns normally). _run_turn's generic except Exception logs 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 silently continued with no log/response, leaving the client waiting forever on that request id.
pycodeloop/cli/serve.py protocol 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 around sys.stdout.write/flush. If the client already disconnected, BrokenPipeError propagates through any on_* callback, _respond_error then tries to send again and raises a second unhandled BrokenPipeError, killing the daemon thread silently with no actionable log.
pycodeloop/cli/serve.pyconfirm() blocks on answer_queue.get() with no timeout (unlike chat.py's CONFIRM_TIMEOUT = 3.0 auto-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.) and pycodeloop/core/codeloop.py's on_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 (text or pending), always preserving partial output instead of an exception whitelist.
In chat.py, flush _text_buffer into 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 an error response referencing the request id when parseable, instead of silently dropping them.
Add a periodic heartbeat/keepalive notification in serve.py while 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 for BrokenPipeError/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's confirm() with a safe default behavior (mirroring chat.py's auto-approve-on-timeout, or auto-decline if that's safer for the non-interactive plugin context).
Wrap each Agent callback invocation (_notify_message and the other on_* call sites) in try/except, logging/tracing the failure without aborting the rest of the turn.
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
_stream()except clause to catch any exception once something has already been accumulated (textorpending), always preserving partial output instead of an exception whitelist.chat.py, flush_text_bufferinto the log (as an interrupted/partial response) in_run_turn's error handler before showing the generic error.serve.py, log malformed input lines and send anerrorresponse referencing the request id when parseable, instead of silently dropping them.serve.pywhile a turn is in flight (e.g. during long provider or tool calls) so the client knows the process is alive._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.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).Agentcallback invocation (_notify_messageand the otheron_*call sites) in try/except, logging/tracing the failure without aborting the rest of the turn.