OpenClaw Adds a Stream Stall Guard Because Agent Providers Sometimes Finish the Work and Forget to Say Done
OpenClaw PR #93640 is a reminder that “OpenAI-compatible” is not the same thing as operationally compatible. A provider can send every token the agent needs, include a finish_reason, record the request as successful on its dashboard, and still leave the client hanging forever because the stream never sends [DONE] or closes cleanly. Humans call that finished. Async iterators do not.
The fix adds a 15-second stream stall guard for OpenAI-compatible completions streams. It targets a specific ugly failure mode observed through the opencode-go provider on deepseek-v4-flash: provider-side calls completed successfully, but OpenClaw stayed stuck in model_call:stream_progress until the broader stuckSessionAbortMs watchdog killed the turn after roughly 600 seconds. The user saw failed cron jobs. The provider dashboard saw successful calls. Everyone was technically telling the truth, which is usually how the worst debugging sessions start.
The missing sentinel became a ten-minute outage
PR #93640 was created on June 16 at 13:05 UTC with 99 additions and one deletion across two files. The root cause, as described in the PR, is that the opencode-go DeepSeek V4 endpoint at https://opencode.ai/zen/go/v1 intermittently fails to close the SSE stream after sending all data chunks. Issue #93610 reports cron jobs using opencode-go/{model} failing with LLM request failed, even though manual runs worked and the provider showed the API call completing normally with full token counts.
The failure trajectory is the important detail: prompt.submitted, then more than 600 seconds of silence, then model.completed with aborted=true and externalAbort=true. The PR body says one provider-side call generated 226 output tokens successfully, but final stream termination never reached the gateway. In other words, the useful work was done. The orchestration semantics were not.
The implementation wraps the OpenAI completions response stream with withStreamCompletionGuard(). Once a chunk with finish_reason appears, OpenClaw races the next chunk against a 15-second timeout. If nothing arrives, it calls return() on the async iterator, exits the for await loop, and pushes a normal { type: 'done' } event. The validation is appropriately targeted: 272 tests pass, including two new guard tests covering finish-reason detection and normal-stream pass-through.
This is not OpenClaw pretending a paused stream is complete. The guard only activates after finish_reason, when the provider has already said the model reached the end of its answer. That is the difference between resilience and hallucinated completion.
Cheap model routing needs boring stream semantics
The provider angle matters because OpenCode Go positions itself as low-cost access to coding models such as DeepSeek V4 Flash/Pro, Qwen, Kimi, GLM, MiMo, and MiniMax, with limits expressed in dollar value rather than fixed request counts. That is attractive for agent workloads. Agents are expensive because they do not just answer once; they plan, inspect, edit, test, retry, summarize, and sometimes run on schedules when nobody is watching. A cheaper provider can materially change the economics.
But provider arbitrage comes with a reliability bill. OpenAI-compatible APIs may share request shapes, but streaming behavior is where compatibility often gets fuzzy. Does the provider emit finish_reason? Does it send [DONE]? Does it close the SSE stream? Does it include trailing usage metadata? What happens if the model finishes but the TCP stream stays open? These details sound beneath the dignity of an AI strategy deck. They are exactly what decides whether a cron job completes or hangs for ten minutes.
The cost implication is larger than one stuck run. A hung stream blocks queues, consumes wall-clock budget, prevents configured fallback from firing, and can cause scheduled jobs to miss their window. It also creates diagnostic split-brain: the provider says success, the orchestrator says abort, and the operator has to reconcile two truthful but incomplete observability views. That human time is part of the cost model too.
This is why the 15-second grace period is a reasonable compromise. Exiting immediately on finish_reason could cut off trailing metadata from providers that send it slowly. Waiting for the global stuck-session timeout is absurd once the provider has already signaled completion. Fifteen seconds is not a law of physics; it is a product judgment that says “enough room for normal stream cleanup, not enough room to turn a missing sentinel into an outage.”
What engineers should test before moving cron jobs
If you are routing production agent work through low-cost or OpenAI-compatible providers, update your test plan. A single interactive request is not enough. Test whether streamed responses terminate cleanly. Test whether the orchestrator emits its own done event. Test whether fallback triggers on genuine failures but not on completed streams with missing sentinels. Test scheduled jobs, because cron paths often expose timeout behavior that manual testing misses.
Also compare provider dashboards against orchestrator events. If the provider records success while the agent records abort, you need correlation IDs, run IDs, or request metadata to tie the two together. Without that, every incident becomes timestamp archaeology. This connects directly to the broader agent-cost conversation: tokens are only one meter. Runtime behavior, retries, hung sessions, blocked queues, and failed automation windows all belong in the operating cost.
The right abstraction for agent platforms is not “trust every compatible provider equally.” It is “normalize messy provider behavior into predictable agent semantics, while preserving enough detail for operators to debug.” OpenClaw’s stream guard is a small piece of that normalization. It does not fix the provider. It protects the agent runtime from one provider’s missing goodbye.
That distinction matters. As agent stacks increasingly mix premium models, cheap coding models, local inference, hosted search, and specialized tool providers, the orchestration layer becomes responsible for smoothing the edges without hiding the risks. Provider choice is now architecture. Stream termination is part of that architecture. If that sounds boring, congratulations: you found the part that breaks production.
Sources: OpenClaw PR #93640, OpenClaw issue #93610, OpenCode Go docs, OpenCode Go model and limits page