Skip to content

Assistant response lost on mid-stream timeout #79

Description

@FernandoCelmer

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:

  1. The accumulated text (text) and partial tool_calls (pending) are local variables inside _stream() — they're discarded, the exception propagates without returning anything.
  2. 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).
  3. After exhausting retries, the exception propagates with no try/except through Agent.run() / CodeLoop.run().
  4. 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

  1. 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).
  2. Increase the default timeout (60s is short for models with long reasoning).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions