Problem
During long interactions (model with slow reasoning), the assistant's response can be lost entirely from the history — the user sends a message, waits, and only gets a generic error with no trace of the response.
Root cause
pycodeloop/providers/generic.py: a single timeout (default 60s, line ~194/336) covers the socket read for every chunk of the SSE stream. If the model takes longer than 60s "thinking" without emitting any bytes, the read in _stream() (lines ~403-410) raises TimeoutError mid-stream.
When that happens:
- The accumulated text (
text) and partial tool_calls (pending) are local variables inside _stream() — they're discarded, the exception propagates without returning anything.
agent.py (line ~44) treats TimeoutError as retryable and resends the request from scratch via _complete(), hitting the same 60s ceiling again, up to _MAX_RETRIES = 3 (line ~27).
- After exhausting retries, the exception propagates with no
try/except through Agent.run() / CodeLoop.run().
pycodeloop/cli/chat.py (line ~432) catches the exception generically, logs "✗ Error: ..." and stops — session.add_assistant() is never called.
Result: the user's message stays in the history with no corresponding response — it looks like the reply "disappeared".
Secondary finding
Session._repair_dangling_tool_calls() (pycodeloop/core/session.py, lines ~59-92) already handles orphaned tool_calls from a lost connection (inserts a synthetic "Cancelled — connection was lost before this tool ran." message), but there's no equivalent handling for when the entire text response never makes it into the session.
Tertiary finding
Agent._run_tool_calls() (line ~279): when a parallel tool call times out, executor.shutdown(wait=False) leaves the thread running in the background, but its real result is never used — another silent work-loss spot.
Proposed fix
- In
_stream(), catch timeout/network errors mid-read and return a partial ProviderResponse with stop_reason="connection_lost" instead of letting the raw exception propagate — reusing the pattern already in place for saw_terminal_marker=False, but only when something was actually accumulated (otherwise keep raising so the existing retry logic in agent.py still applies to a pure connection failure).
- Increase the default timeout (60s is short for models with long reasoning).
Problem
During long interactions (model with slow reasoning), the assistant's response can be lost entirely from the history — the user sends a message, waits, and only gets a generic error with no trace of the response.
Root cause
pycodeloop/providers/generic.py: a singletimeout(default 60s, line ~194/336) covers the socket read for every chunk of the SSE stream. If the model takes longer than 60s "thinking" without emitting any bytes, the read in_stream()(lines ~403-410) raisesTimeoutErrormid-stream.When that happens:
text) and partial tool_calls (pending) are local variables inside_stream()— they're discarded, the exception propagates without returning anything.agent.py(line ~44) treatsTimeoutErroras retryable and resends the request from scratch via_complete(), hitting the same 60s ceiling again, up to_MAX_RETRIES = 3(line ~27).try/exceptthroughAgent.run()/CodeLoop.run().pycodeloop/cli/chat.py(line ~432) catches the exception generically, logs "✗ Error: ..." and stops —session.add_assistant()is never called.Result: the user's message stays in the history with no corresponding response — it looks like the reply "disappeared".
Secondary finding
Session._repair_dangling_tool_calls()(pycodeloop/core/session.py, lines ~59-92) already handles orphaned tool_calls from a lost connection (inserts a synthetic "Cancelled — connection was lost before this tool ran." message), but there's no equivalent handling for when the entire text response never makes it into the session.Tertiary finding
Agent._run_tool_calls()(line ~279): when a parallel tool call times out,executor.shutdown(wait=False)leaves the thread running in the background, but its real result is never used — another silent work-loss spot.Proposed fix
_stream(), catch timeout/network errors mid-read and return a partialProviderResponsewithstop_reason="connection_lost"instead of letting the raw exception propagate — reusing the pattern already in place forsaw_terminal_marker=False, but only when something was actually accumulated (otherwise keep raising so the existing retry logic inagent.pystill applies to a pure connection failure).