Stalled provider streams sending SSE keepalive comments are never detected — silent mid-turn hang after a tool round (no error, no retry)
- Dominant language
- Rust
- Stars
- 54.2k
- Forks
- 6.2k
- Avg merge
- 3d 2h
- Merged PRs (30d)
- 262
Description
**Describe the bug**
Mid-turn, right after a completed tool round, the next completion request can hang forever when the provider stalls the stream while still sending SSE keepalive comment frames (`: ping`). Goose surfaces no reply, no error, and no retry — the session stays "working" until the process is killed and the prompt is resent.
Keepalive comments are legal SSE that every event parser must ignore, while still being bytes on the wire. This makes a stalled stream indistinguishable from a slow model at the byte layer, and goose's current timeouts are all byte/line-based, so they are reset indefinitely:
- `ApiClient` (`crates/goose-providers/src/api_client.rs`, `client_builder`) configures reqwest's `.read_timeout()`. A read timeout only fires when *no bytes at all* arrive for the configured window; periodic `: ping` comments reset it forever.
- OpenAI/Anthropic engines have no event-layer inactivity timeout — only that read timeout (default 600s, `crates/goose-providers/src/openai.rs` `DEFAULT_TIMEOUT_SECONDS`), which keepalives perpetually reset.
- Ollama's stall detection (`chunk_timeout_secs` / `with_line_timeout`, `GOOSE_STREAM_TIMEOUT` in `crates/goose/src/providers/ollama_def.rs`) is also line/byte-based, so keepalive comment lines reset it as well.
Verified against `aaif-goose/goose` main @ `8ae4e4ba0`.
---
**To Reproduce**
A deterministic reproducer exists in llm-mockingbird, which adds a "stall" fault mode in [SynthLuvr/llm-mockingbird#26](https://github.com/SynthLuvr/llm-mockingbird/pull/26): the stream emits opening frames and deltas, then never terminates — no closing frames, no error frame, no `[DONE]` — optionally sending `: ping` comment frames every 500ms (or going fully silent with `streamStallKeepaliveMs: 0`).
1. Run mockingbird with `streamStallAfterMs` set for the OpenAI or Anthropic engine
2. Point goose at it and ask something that triggers a tool call
3. After the tool round completes, the next completion request stalls with keepalives
4. Goose never notices on its own: no error surfaced, no retry (exactly 1 main request observed)
5. In the PR's integration test (`goose-midstream-sse-stall.integration.test.ts`), `goose run` is killed at a 20s deadline
---
**Expected behavior**
Goose should detect a stalled stream even when keepalive/SSE-comment bytes keep arriving — e.g. an event-layer inactivity timeout that ignores comment frames, or a maximum stream wall-clock duration — then surface an error and retry per the existing retry policy.
---
**Screenshots**
N/A (reproducer is a mock server; see the linked PR for its test suite).
---
**Please provide the following information**
- **OS & Arch:** any (root cause verified in code; reproducer runs on Linux/macOS)
- **Interface:** CLI (affects any engine streaming through `ApiClient`, so UI/goosed too)
- **Version:** main @ `8ae4e4ba0` (2026-08-28)
- **Extensions enabled:** none required
- **Provider & Model:** any streaming provider; reproducer covers OpenAI-compatible and Anthropic engines
---
**Additional context**
Related but distinct issues (none cover this failure mode):
- #8437 — stream stall timeout fires too aggressively on slow-but-live streams (confirms detection is byte-based; inverse side of this problem)
- #8485 — panic in the Ollama stall-timeout path
- #10110 — multi-turn hang with high CPU (internal spin, not a silent wait on the provider)
- #9082 — silent hang where no LLM call is made at all (chat-mode shim)
- #10917 — retry coverage for provider errors (adjacent to the "no retry" aspect)
The mockingbird PR's goose test currently asserts the buggy behavior (no error, no retry, killed at deadline) and is meant to be inverted once goose detects or times out stalled streams.
Contributor guide
Assessment
This issue has not been assessed yet.