apache / apache/maka

bug(runtime): output-free streams closed before response.completed bypass bounded recovery

Open
#4,599 1 comment 0 reactions 1 assignee Claimed by @jsiu93 View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.