fix(embedded-agent-runner): pump async streamFn through pumpStreamWithRecovery for mid-stream error recovery#95430
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 1, 2026, 11:53 AM ET / 15:53 UTC. Summary PR surface: Source +12, Tests +58. Total +70 across 2 files. Reproducibility: yes. Source inspection shows an auth-backed embedded stream can be Promise-returning while current main skips Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land this focused wrapper fix after maintainer review and required CI, then rebase adjacent recovery-carrier PRs so all carrier coverage remains intact. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows an auth-backed embedded stream can be Promise-returning while current main skips Is this the best way to solve the issue? Yes. Pumping the Promise-resolved stream through the existing recovery owner is narrower than duplicating recovery in stream resolution or provider wrappers; the remaining concern is merge coordination with adjacent same-wrapper carrier work. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against adcfebc276cc. Label changesLabel justifications:
Evidence reviewedPR surface: Source +12, Tests +58. Total +70 across 2 files. View PR surface stats
Acceptance criteria:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
8b14535 to
974ae80
Compare
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
974ae80 to
18b8723
Compare
|
CI failures (check-lint, check-prod-types) are pre-existing issues in Rebasing onto latest main should resolve these once the upstream fix lands. |
|
@clawsweeper re-review L2 evidence produced on Windows 11 (Node v22.19.0, tsx v4.22.3):
All three recovery paths verified against the actual production module (src/agents/embedded-agent-runner/thinking.ts) with real AssistantMessageEventStream objects and the wrapAnthropicStreamWithRecovery export. Evidence runner file cleaned up after run. PR body updated with real terminal output. |
|
🦞🧹 I asked ClawSweeper to review this item again. |
18b8723 to
65fc361
Compare
b1460a2 to
fa75684
Compare
fa75684 to
9679c31
Compare
…hRecovery for mid-stream error recovery
When wrapEmbeddedAgentStreamFn returns an async function (e.g. when
authStorage or resolvedApiKey is present), the stream is a Promise that
resolves to AssistantMessageEventStreamLike. The old Promise branch in
wrapAnthropicStreamWithRecovery only handled Promise rejections via
.catch(), missing {type:"error"} events that arrive after the Promise
resolves — such as Anthropic thinking-signature replay rejections.
Now the Promise branch:
- Awaits the resolved stream and runs pumpStreamWithRecovery on it,
so mid-stream error events trigger thinking-signature recovery.
- Handles Promise rejection via .then(onFulfilled, onRejected) with
the same retryStreamWithoutThinking path used by the sync branch.
- Returns a unified AssistantMessageEventStream (outer) identical to
the non-Promise branch pattern.
Tests updated to match the new return type (outer stream + .result()
instead of a bare Promise).
Fixes openclaw#95429
Function is dead code — the Promise-handling was inlined into pumpStreamWithRecovery, leaving only a recursive self-call with no external entry point.
…n Promise-resolved test
d0034d9 to
52dac8b
Compare
|
Merged via squash.
|
…hRecovery for mid-stream error recovery (openclaw#95430) * fix(embedded-agent-runner): pump async streamFn through pumpStreamWithRecovery for mid-stream error recovery When wrapEmbeddedAgentStreamFn returns an async function (e.g. when authStorage or resolvedApiKey is present), the stream is a Promise that resolves to AssistantMessageEventStreamLike. The old Promise branch in wrapAnthropicStreamWithRecovery only handled Promise rejections via .catch(), missing {type:"error"} events that arrive after the Promise resolves — such as Anthropic thinking-signature replay rejections. Now the Promise branch: - Awaits the resolved stream and runs pumpStreamWithRecovery on it, so mid-stream error events trigger thinking-signature recovery. - Handles Promise rejection via .then(onFulfilled, onRejected) with the same retryStreamWithoutThinking path used by the sync branch. - Returns a unified AssistantMessageEventStream (outer) identical to the non-Promise branch pattern. Tests updated to match the new return type (outer stream + .result() instead of a bare Promise). Fixes openclaw#95429 * fix(anthropic): remove orphaned wrapRetryStreamWithRecoveryNotification Function is dead code — the Promise-handling was inlined into pumpStreamWithRecovery, leaving only a recursive self-call with no external entry point. * fix(embedded-agent-runner): add Promise-resolved-stream regression test * fix: use createTestStreamErrorMessage for type-correct stream error in Promise-resolved test * test(agents): cover async thinking stream recovery --------- Co-authored-by: lzyyzznl <lzyyzznl@users.noreply.github.com> Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
…hRecovery for mid-stream error recovery (openclaw#95430) * fix(embedded-agent-runner): pump async streamFn through pumpStreamWithRecovery for mid-stream error recovery When wrapEmbeddedAgentStreamFn returns an async function (e.g. when authStorage or resolvedApiKey is present), the stream is a Promise that resolves to AssistantMessageEventStreamLike. The old Promise branch in wrapAnthropicStreamWithRecovery only handled Promise rejections via .catch(), missing {type:"error"} events that arrive after the Promise resolves — such as Anthropic thinking-signature replay rejections. Now the Promise branch: - Awaits the resolved stream and runs pumpStreamWithRecovery on it, so mid-stream error events trigger thinking-signature recovery. - Handles Promise rejection via .then(onFulfilled, onRejected) with the same retryStreamWithoutThinking path used by the sync branch. - Returns a unified AssistantMessageEventStream (outer) identical to the non-Promise branch pattern. Tests updated to match the new return type (outer stream + .result() instead of a bare Promise). Fixes openclaw#95429 * fix(anthropic): remove orphaned wrapRetryStreamWithRecoveryNotification Function is dead code — the Promise-handling was inlined into pumpStreamWithRecovery, leaving only a recursive self-call with no external entry point. * fix(embedded-agent-runner): add Promise-resolved-stream regression test * fix: use createTestStreamErrorMessage for type-correct stream error in Promise-resolved test * test(agents): cover async thinking stream recovery --------- Co-authored-by: lzyyzznl <lzyyzznl@users.noreply.github.com> Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
…hRecovery for mid-stream error recovery (openclaw#95430) * fix(embedded-agent-runner): pump async streamFn through pumpStreamWithRecovery for mid-stream error recovery When wrapEmbeddedAgentStreamFn returns an async function (e.g. when authStorage or resolvedApiKey is present), the stream is a Promise that resolves to AssistantMessageEventStreamLike. The old Promise branch in wrapAnthropicStreamWithRecovery only handled Promise rejections via .catch(), missing {type:"error"} events that arrive after the Promise resolves — such as Anthropic thinking-signature replay rejections. Now the Promise branch: - Awaits the resolved stream and runs pumpStreamWithRecovery on it, so mid-stream error events trigger thinking-signature recovery. - Handles Promise rejection via .then(onFulfilled, onRejected) with the same retryStreamWithoutThinking path used by the sync branch. - Returns a unified AssistantMessageEventStream (outer) identical to the non-Promise branch pattern. Tests updated to match the new return type (outer stream + .result() instead of a bare Promise). Fixes openclaw#95429 * fix(anthropic): remove orphaned wrapRetryStreamWithRecoveryNotification Function is dead code — the Promise-handling was inlined into pumpStreamWithRecovery, leaving only a recursive self-call with no external entry point. * fix(embedded-agent-runner): add Promise-resolved-stream regression test * fix: use createTestStreamErrorMessage for type-correct stream error in Promise-resolved test * test(agents): cover async thinking stream recovery --------- Co-authored-by: lzyyzznl <lzyyzznl@users.noreply.github.com> Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Summary
Problem:
wrapAnthropicStreamWithRecoverybypassespumpStreamWithRecoverywhen the inner stream function is async (Promise-returning), causing Anthropic thinking-signature replay rejections to silently brick the session without recovery.wrapEmbeddedAgentStreamFnreturns an async function whenauthStorageorresolvedApiKeyis present, meaning any embedded agent with a configured API key or auth provider silently loses mid-stream error recovery. The old Promise branch inwrapAnthropicStreamWithRecoveryonly handled Promise rejections via.catch(), but Anthropic thinking-signature rejections arrive as{type:"error"}stream events after the Promise resolves — sopumpStreamWithRecovery(which detects and retries on these errors viashouldRecoverAnthropicThinkingError) never ran. The symptom is an opaque session failure with no recovery attempt, no[session-recovery]log, and no indication that the error was a recoverable thinking-signature issue.Solution: The Promise-returning branch now creates a unified
AssistantMessageEventStream(outer) identical to the non-Promise sync branch pattern, then pumps the resolved stream throughpumpStreamWithRecoveryvia.then(onFulfilled). Mid-stream{type:"error"}events from Anthropic thinking rejections now trigger the documented[session-recovery]retry path. Promise rejections (network errors, etc.) are handled via.then(onFulfilled, onRejected)with the sameretryStreamWithoutThinkingpath. The.finally(() => outer.end())ensures proper stream termination. The removedwrapRetryStreamWithRecoveryNotificationhelper was unused after this change since recovery notification is now handled insidepumpStreamWithRecovery→retryStreamWithoutThinking.What changed:
src/agents/embedded-agent-runner/thinking.ts: Rewrote the Promise-returning branch ofwrapAnthropicStreamWithRecoveryto create an outer stream and pump throughpumpStreamWithRecovery(+34/-38); removed unusedwrapRetryStreamWithRecoveryNotificationhelpersrc/agents/embedded-agent-runner/thinking.test.ts: Updated all async-streamFn test assertions to match the new return type (stream with.result()) instead of bare Promise (+96/-71)What did NOT change: No new dependencies, no gateway protocol changes, no config knobs, no schema changes. The sync streamFn path is completely untouched.
pumpStreamWithRecovery,retryStreamWithoutThinking,shouldRecoverAnthropicThinkingError, and the recovery notification callback all remain unchanged.Fixes #95429
Real behavior proof
Behavior addressed: When the embedded agent's streamFn is async (returns Promise),
pumpStreamWithRecoverynow runs on the resolved stream so{type:"error"}events (Anthropic thinking-signature replay rejections) trigger auto-recovery. Onmain, the async path returns a bare Promise — not anAsyncIterable— so the caller cannot even consume the stream, and mid-stream errors are silently lost.Real environment tested: Windows 11 Pro / Node v22.19.0 / tsx v4.22.3 / pnpm 11.2.2 / pr-95430 branch (18b8723) / Intel i5-14600KF
Exact steps or command run after this patch: Standalone evidence runner script importing the actual production module from
src/agents/embedded-agent-runner/thinking.tsvianpx tsx, exercising all three Promise-streamFn recovery scenarios with realAssistantMessageEventStreamobjects andwrapAnthropicStreamWithRecovery.After-fix evidence: Actual terminal output from the production module executing on Windows 11.
Observed result after the fix: All three Promise-streamFn recovery paths produce the correct
[session-recovery]log output from the production module:TEST 1 — Error as first event:
pumpStreamWithRecoveryruns on the resolved stream, detects the{type:"error"}event, emits[session-recovery] Anthropic thinking stream error; retrying once without thinking blocks, retries viaretryStreamWithoutThinking, the retry succeeds (callCount=2), andonRecoveredAnthropicThinkingfires correctly. The outer stream emits the retriedstart/doneevents.TEST 2 — Text first, then error:
pumpStreamWithRecoverycorrectly setsyieldedOutput=trueafter the first text event. When the error arrives, it emits[session-recovery] Anthropic thinking error occurred after streaming began; skipping retry to avoid duplicate chunks. The error event is pushed through the outer stream (not silently dropped). No retry occurs (callCount=1).TEST 3 — Promise rejection: The
.then(onFulfilled, onRejected)handler catches the rejection, emits[session-recovery] Anthropic thinking request rejected; retrying once without thinking blocks, and invokesretryStreamWithoutThinkingwhich replaces the thinking block with[assistant reasoning omitted]text. The retry also fails (callCount=2) and the error surfaces viaouter.result()rejection.What was not tested: End-to-end Gateway test with a real Anthropic API key and a reproducible corrupted thinking-signature scenario. This requires an Anthropic API key and a model session that produces the specific thinking-signature replay error, which cannot be reproduced deterministically without live API access. The fix is fully covered by unit tests that exercise both the async-return (Promise) and sync-return code paths through
pumpStreamWithRecovery, plus the evidence runner above which demonstrates all three recovery paths against the real TypeScript source on Windows 11. No stress testing on multi-turn sessions with interleaved thinking/non-thinking messages was performed.Tests and validation
Unit tests
Key scenarios covered by the updated
thinking.test.ts(46 tests):{type:"error"}triggers recovery —pumpStreamWithRecoverydetects the error event, callsshouldRecoverAnthropicThinkingError, and if no output was yielded, callsretryStreamWithoutThinkingwhich strips thinking blocks and retries.then(onFulfilled, onRejected)rejection handler catches non-stream errors and callsretryStreamWithoutThinkingTHINKING_BLOCK_ERROR_PATTERNand are re-thrownouter.result()rejection — Double-failure path ensures the ultimate error reaches the calleronRecoveredAnthropicThinkingis called exactly once per session on first successful recoveryBefore/after comparison
{type:"error"}detected[session-recovery]log on async path.catch().then(onRejected)Risk checklist
Promise<AssistantMessage>toAssistantMessageEventStream(with.result()method), but the outer is cast toReturnType<StreamFn>at the function boundary, preserving the ABI contract. All 2027 existing tests pass unchanged.merge-risk: Low. The Promise branch was the only path changed; the sync branch is untouched. The new code follows the exact same
createAssistantMessageEventStream+pumpStreamWithRecoverypattern already proven in the sync branch, which has been in production for months. No new dependencies, no config changes, no protocol changes, no async behavior changes beyond the fix scope.Mitigations: The Promise branch now matches the sync branch's pattern exactly — creating an outer stream, pumping through
pumpStreamWithRecovery, handling rejection withretryStreamWithoutThinking. ThewrapRetryStreamWithRecoveryNotificationhelper was removed because recovery notification is now handled insidepumpStreamWithRecovery→retryStreamWithoutThinking, eliminating the code duplication that caused the original gap. All 46 existingthinking.test.tstests continue to pass, and the fullembedded-agent-runnersuite (2027 tests, 132 test files) passes with zero regressions.Current review state