bug(runtime): output-free streams closed before response.completed bypass bounded recovery
- Dominant language
- TypeScript
- Stars
- 5.4k
- Forks
- 502
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 715
Description
## Environment
- Maka commit: `b714a39` (from `origin/main` at `b714a392192961fef3d89d4358fb0a0858252804`)
- CLI version: `0.2.0`
- OS: macOS
- Model / Provider: Custom relay OpenAI Chat-compatible → `gpt-5.6-sol`
- Node.js: `24.19.0`
## Steps to reproduce
1. Start Maka TUI from source:
```sh
HOME= node packages/cli/dist/dev-cli.js
```
2. Select `gpt-5.6-sol` with Full access.
3. Send a long tool-heavy task (about 45 minutes, ~60 provider steps) through an OpenAI Chat-compatible relay whose upstream closes the stream around 900 s.
4. Wait for the upstream connection to close before protocol completion (in our run this hit step `58`, attempt `0`).
Live triggering depends on upstream behavior; two independent runs (2026-08-30 and 2026-09-03) reproduced this exact pattern at approximately 902 s. The accompanying PR uses a fake stream for deterministic unit reproduction.
## Actual
The TUI displays:
```text
Error: stream error: stream disconnected before completion: stream closed before response.completed (code=invalid_request_error)
```
The persisted error object in `runtime_events` records:
```json
{
"code": "invalid_request_error",
"kind": "error",
"message": "stream error: stream disconnected before completion: stream closed before response.completed"
}
```
The database records for this attempt (`usage_model_call_attempts`) show:
- Step / Attempt: `58 / 0`
- Latency: `902245ms`
- Time to first token (TTFT): `3026ms`
- `errorClass`: `Other`
- `providerCode`: `invalid_request_error`
- `retryable`: `0` (`false`)
- `status`: `interrupted`
- Number of `provider_retry` events for this turn: `0`
An isolated classifier probe confirms the diagnostic categorization:
```json
{
"input": {
"type": "invalid_request_error",
"code": "invalid_request_error",
"message": "stream disconnected before completion: stream closed before response.completed"
},
"classifyError": "Other",
"providerRetryMetadata": {
"retryable": false
},
"providerFailureDiagnostic": {
"errorClass": "Other",
"providerCode": "invalid_request_error",
"retryable": false
}
}
```
## Expected
When the physical provider attempt sits at a durable step boundary with no observable text, tool activity, completed steps, or continuation signature, Maka automatically recovers once via bounded recovery.
Any observable output triggers fail-closed termination to preserve durable tool side effects.
If recovery attempts exhaust the budget, Maka presents clear user guidance indicating that sending a message resumes execution from the persisted state.
## Why this is Maka's gap
The error code and premature stream termination originate from the upstream relay/provider, while Maka's error classification places this failure into terminal `Other`, leaving an output-free attempt without bounded recovery.
## Proposed invariant
When the current physical provider attempt sits at a durable step boundary with no text, tool activity, completed steps, or continuation signature, a premature stream close automatically recovers at most once; any observable output remains fail-closed, and durable tool side effects never replay.
Specific threshold criteria to enforce:
- `attemptSawText === false`
- `attemptSawToolActivity === false`
- `attemptSawThinking === false` (or following existing idle-recovery contracts for partial thinking)
- `stepStartedWithProviderContinuation === false`
- `attemptSawCompletedStep === false`
- Recovery bounded to the current provider step with a maximum of 1 recovery attempt
- Turn-level cancellation (Stop) continues to take precedence
## Proposed fix
1. Preserve a structured "stream closed before protocol completion" signal between `model-adapter.ts` and `ai-sdk-backend.ts`, prioritizing AI SDK typed causes.
2. Route this signal to the existing incomplete-stream recovery counter, reusing `incompleteStreamHasNoObservableOutput` and step-budget guards.
3. Preserve original provider diagnostic codes while recording bounded recovery under a dedicated telemetry reason.
4. For relays exposing error text only, apply narrow string matching for `stream disconnected before completion` and `stream closed before response.completed` with a single bounded retry.
## Related
- #2119 / #2124: Resolved stream idle watchdog aborts during tool argument streaming where chunks were filtered semantically; here raw chunks maintained watchdog activity until closure at 902s.
- #2487 / #2535: Added bounded recovery for 120s stream-idle aborts; here the watchdog did not abort and the error followed the terminal `Other` path.
- #3756 / #3758: Added retry handling for AI SDK retryable transport failures; here `retryable=false` occurred after TTFT.
- #1223: Established durable safe-boundary resume foundations that safeguard replay safety.
A draft PR with the narrow fix follows.
Contributor guide
Assessment
This issue has not been assessed yet.