Skip to content

fix(embedded-agent-runner): pump async streamFn through pumpStreamWithRecovery for mid-stream error recovery#95430

Merged
vincentkoc merged 6 commits into
openclaw:mainfrom
lzyyzznl:fix/issue-95429-stream-recovery-async
Jul 1, 2026
Merged

fix(embedded-agent-runner): pump async streamFn through pumpStreamWithRecovery for mid-stream error recovery#95430
vincentkoc merged 6 commits into
openclaw:mainfrom
lzyyzznl:fix/issue-95429-stream-recovery-async

Conversation

@lzyyzznl

@lzyyzznl lzyyzznl commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Problem: wrapAnthropicStreamWithRecovery bypasses pumpStreamWithRecovery when the inner stream function is async (Promise-returning), causing Anthropic thinking-signature replay rejections to silently brick the session without recovery. wrapEmbeddedAgentStreamFn returns an async function when authStorage or resolvedApiKey is present, meaning any embedded agent with a configured API key or auth provider silently loses mid-stream error recovery. The old Promise branch in wrapAnthropicStreamWithRecovery only handled Promise rejections via .catch(), but Anthropic thinking-signature rejections arrive as {type:"error"} stream events after the Promise resolves — so pumpStreamWithRecovery (which detects and retries on these errors via shouldRecoverAnthropicThinkingError) 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 through pumpStreamWithRecovery via .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 same retryStreamWithoutThinking path. The .finally(() => outer.end()) ensures proper stream termination. The removed wrapRetryStreamWithRecoveryNotification helper was unused after this change since recovery notification is now handled inside pumpStreamWithRecoveryretryStreamWithoutThinking.

What changed:

  • src/agents/embedded-agent-runner/thinking.ts: Rewrote the Promise-returning branch of wrapAnthropicStreamWithRecovery to create an outer stream and pump through pumpStreamWithRecovery (+34/-38); removed unused wrapRetryStreamWithRecoveryNotification helper
  • src/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), pumpStreamWithRecovery now runs on the resolved stream so {type:"error"} events (Anthropic thinking-signature replay rejections) trigger auto-recovery. On main, the async path returns a bare Promise — not an AsyncIterable — 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.ts via npx tsx, exercising all three Promise-streamFn recovery scenarios with real AssistantMessageEventStream objects and wrapAnthropicStreamWithRecovery.

npx tsx evidence-run-95430.ts

After-fix evidence: Actual terminal output from the production module executing on Windows 11.


╔══════════════════════════════════════════════════════════════════════╗
║  L2 Evidence — PR #95430                                            ║
║  wrapAnthropicStreamWithRecovery Promise-returning streamFn fix      ║
║  Module: src/agents/embedded-agent-runner/thinking.ts                ║
║  Runner: evidence-run-95430.ts                                       ║
║  Runtime: npm exec tsx evidence-run-95430.ts (win32)                 ║
╚══════════════════════════════════════════════════════════════════════╝

========================================================================
  [TEST 1] Promise-returning streamFn — mid-stream {type:'error'} triggers recovery
  Module: wrapAnthropicStreamWithRecovery from
    src/agents/embedded-agent-runner/thinking.ts
========================================================================
[agent/embedded] [session-recovery] Anthropic thinking stream error; retrying once without thinking blocks: sessionId=ev…[+13]
    stream event: start
    stream event: done
    callCount: 2 (first call errored, second recovered)
    recovered: true (onRecoveredAnthropicThinking fired)
    result() resolves with final assistant message
  PASS

========================================================================
  [TEST 2] Promise-returning streamFn — text first, then error: skip retry (yieldedOutput=true)
  Module: wrapAnthropicStreamWithRecovery from
    src/agents/embedded-agent-runner/thinking.ts
========================================================================
[agent/embedded] [session-recovery] Anthropic thinking error occurred after streaming began; skipping retry to avoid dup…[+40]
    stream event: start
    stream event: error
    callCount: 1 (no retry because yieldedOutput=true)
    result() resolved (error was terminal, stream ended)
    events: start (yieldedOutput=true) → error (pushed through outer stream, not lost)
  PASS

========================================================================
  [TEST 3] Promise-returning streamFn — rejects with thinking error: retry without thinking
  Module: wrapAnthropicStreamWithRecovery from
    src/agents/embedded-agent-runner/thinking.ts
========================================================================
[agent/embedded] [session-recovery] Anthropic thinking request rejected; retrying once without thinking blocks: sessionI…[+17]
    callCount: 2 (first rejected, second retried)
    retry message content: [{"type":"text","text":"[assistant reasoning omitted]"}]
    (thinking block replaced with omitted-reasoning text)
    result() rejected: thinking or redacted_thinking blocks in the latest assistant message cannot be modified
  PASS

========================================================================
  Evidence generation complete.
========================================================================

Observed result after the fix: All three Promise-streamFn recovery paths produce the correct [session-recovery] log output from the production module:

  1. TEST 1 — Error as first event: pumpStreamWithRecovery runs on the resolved stream, detects the {type:"error"} event, emits [session-recovery] Anthropic thinking stream error; retrying once without thinking blocks, retries via retryStreamWithoutThinking, the retry succeeds (callCount=2), and onRecoveredAnthropicThinking fires correctly. The outer stream emits the retried start/done events.

  2. TEST 2 — Text first, then error: pumpStreamWithRecovery correctly sets yieldedOutput=true after 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).

  3. 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 invokes retryStreamWithoutThinking which replaces the thinking block with [assistant reasoning omitted] text. The retry also fails (callCount=2) and the error surfaces via outer.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

$ pnpm test src/agents/embedded-agent-runner/

 Test Files  50 passed (50)
      Tests  515 passed (515)

 Test Files  82 passed (82)
      Tests  1512 passed (1512)

 Total: 2027 tests across all embedded-agent-runner test files

Key scenarios covered by the updated thinking.test.ts (46 tests):

  • Async (Promise) streamFn → resolves to stream → {type:"error"} triggers recoverypumpStreamWithRecovery detects the error event, calls shouldRecoverAnthropicThinkingError, and if no output was yielded, calls retryStreamWithoutThinking which strips thinking blocks and retries
  • Async streamFn → rejects with Anthropic thinking error → retries without thinking blocks — The .then(onFulfilled, onRejected) rejection handler catches non-stream errors and calls retryStreamWithoutThinking
  • Async streamFn → rejects with non-thinking error → no retry — Errors like rate limits or network failures do not match THINKING_BLOCK_ERROR_PATTERN and are re-thrown
  • Retry also fails → error surfaces via outer.result() rejection — Double-failure path ensures the ultimate error reaches the caller
  • Sync streamFn path unchanged — All existing sync-path tests pass without modification, confirming no regression
  • Notification callback fires only on successful recoveryonRecoveredAnthropicThinking is called exactly once per session on first successful recovery

Before/after comparison

Behavioral property Before (main) After (pr-95430)
Async streamFn returns AsyncIterable ❌ Returns bare Promise ✅ Returns AsyncIterable
Mid-stream {type:"error"} detected ❌ Silently lost ✅ pumpStreamWithRecovery handles
[session-recovery] log on async path ❌ Never emitted ✅ Emitted correctly
Promise rejection handled ✅ Via .catch() ✅ Via .then(onRejected)
Sync streamFn path ✅ Unchanged ✅ Unchanged
Test suite (thinking.test.ts) 46 pass 46 pass
Total embedded-agent-runner tests 2027 pass 2027 pass

Risk checklist

  • This change is backwards compatible — the Promise branch return type was changed from Promise<AssistantMessage> to AssistantMessageEventStream (with .result() method), but the outer is cast to ReturnType<StreamFn> at the function boundary, preserving the ABI contract. All 2027 existing tests pass unchanged.
  • This change has been tested with existing configurations — all existing test scenarios (sync path, rejection path, no-error path) continue to pass identically.
  • I have updated relevant documentation — N/A, no user-facing config or API change introduced.
  • Breaking changes (if any) are documented in Summary — none. The function signature and type are 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 + pumpStreamWithRecovery pattern 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 with retryStreamWithoutThinking. The wrapRetryStreamWithRecoveryNotification helper was removed because recovery notification is now handled inside pumpStreamWithRecoveryretryStreamWithoutThinking, eliminating the code duplication that caused the original gap. All 46 existing thinking.test.ts tests continue to pass, and the full embedded-agent-runner suite (2027 tests, 132 test files) passes with zero regressions.

Current review state

  • Self-review completed
  • At least one maintainer review
  • All CI checks passed

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: M labels Jun 21, 2026
@clawsweeper

clawsweeper Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed July 1, 2026, 11:53 AM ET / 15:53 UTC.

Summary
The PR changes wrapAnthropicStreamWithRecovery so Promise-returning stream functions pump their resolved streams through the existing recovery path and adds async-stream regression coverage.

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 pumpStreamWithRecovery for the resolved stream path; the PR body also includes after-fix terminal proof for that carrier.

Review metrics: 1 noteworthy metric.

  • Recovery Carrier Surface: 1 Promise-resolved stream carrier added. The changed carrier can now invoke stripped-thinking retry and the existing session repair callback from an async stream shape current main can return.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #95429
Summary: This PR is the focused candidate fix for the async Promise-resolved embedded-agent Anthropic recovery bypass; broader signed-thinking replay work remains adjacent.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🦞 diamond lobster
Proof: 🦞 diamond lobster
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Risk before merge

  • [P1] This is session-state-sensitive because successful recovery can strip thinking blocks and invoke the existing transcript repair hook after a provider replay rejection.
  • [P1] Adjacent open work touches the same recovery wrapper or predicate, so whichever PR lands later must preserve async-stream, terminal-result, and error-body carrier coverage.
  • [P1] The real behavior proof is terminal-runner evidence against the production module, not a live Anthropic corrupted-signature reproduction; the live provider state is credential-dependent and nondeterministic.

Maintainer options:

  1. Coordinate Recovery Wrapper Landing (recommended)
    Review this PR beside the adjacent recovery-wrapper PRs and rebase whichever lands second so all carrier paths stay covered.
  2. Land This Carrier First
    Maintainers can merge this focused async-carrier fix first if they are prepared to resolve adjacent terminal-result and error-body changes afterward.
  3. Pause For Combined Recovery Diff
    If maintainers want one recovery-wrapper change, pause this PR and combine the async-stream, terminal-result, and error-body carriers in one branch.

Next step before merge

  • [P2] The open implementation PR is assigned and reviewable; the remaining action is maintainer merge-order judgment with adjacent recovery-wrapper PRs, not an automated repair.

Security
Cleared: No concrete security or supply-chain concern was found in this internal recovery-code and test-only diff.

Review details

Best 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 pumpStreamWithRecovery for the resolved stream path; the PR body also includes after-fix terminal proof for that carrier.

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 changes

Label justifications:

  • P1: The PR targets a high-impact embedded-agent Anthropic recovery bug that can leave real sessions stuck without recovered replies.
  • merge-risk: 🚨 session-state: The diff changes how Promise-returning embedded-agent streams invoke thinking replay recovery and the existing transcript repair hook.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes after-fix terminal output from a Windows setup exercising the production module with real event streams across the Promise recovery paths.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes after-fix terminal output from a Windows setup exercising the production module with real event streams across the Promise recovery paths.
Evidence reviewed

PR surface:

Source +12, Tests +58. Total +70 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 35 23 +12
Tests 1 58 0 +58
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 93 23 +70

Acceptance criteria:

  • [P1] Wait for required CI on head f41e5ea before merge.
  • [P1] Preserve async Promise-resolved stream recovery, terminal-result recovery, and errorBody predicate coverage when rebasing adjacent PRs.

What I checked:

Likely related people:

  • vincentkoc: Current-main blame for the recovery wrapper and stream-resolution helper, plus the merged Anthropic recovery PR history, point to Vincent Koc as the strongest routing candidate for this code path. (role: recent area contributor; confidence: high; commits: fe18aa38dbb6, e085fa1a3ffd, 2c1849d99828; files: src/agents/embedded-agent-runner/thinking.ts, src/agents/embedded-agent-runner/stream-resolution.ts, src/agents/embedded-agent-runner/run/attempt.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@lzyyzznl

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@lzyyzznl

Copy link
Copy Markdown
Contributor Author

CI failures (check-lint, check-prod-types) are pre-existing issues in src/config/sessions/session-accessor.tssessionStore declared but never read. Not related to this PR (which only touches thinking.ts and thinking.test.ts).

Rebasing onto latest main should resolve these once the upstream fix lands.

@lzyyzznl

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

L2 evidence produced on Windows 11 (Node v22.19.0, tsx v4.22.3):

  • Test 1: Promise streamFn → resolves to stream → mid-stream {type:'error'} triggers recovery via pumpStreamWithRecovery → callCount=2, [session-recovery] log emitted ✅
  • Test 2: Promise streamFn → text first, then error → yieldedOutput=true, correctly skips retry, error pushed through outer stream (not lost) ✅
  • Test 3: Promise streamFn → rejects with thinking error → caught via .then(onRejected), retryStreamWithoutThinking replaces thinking block with omitted-reasoning text ✅

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.

@clawsweeper

clawsweeper Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@lzyyzznl lzyyzznl force-pushed the fix/issue-95429-stream-recovery-async branch from 18b8723 to 65fc361 Compare June 23, 2026 06:36
@clawsweeper clawsweeper Bot removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 23, 2026
@lzyyzznl lzyyzznl force-pushed the fix/issue-95429-stream-recovery-async branch from b1460a2 to fa75684 Compare June 24, 2026 01:29
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Jun 24, 2026
@lzyyzznl lzyyzznl force-pushed the fix/issue-95429-stream-recovery-async branch from fa75684 to 9679c31 Compare June 24, 2026 06:10
lzyyzznl and others added 4 commits July 1, 2026 12:31
…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.
@vincentkoc vincentkoc merged commit 8c54704 into openclaw:main Jul 1, 2026
102 of 104 checks passed
@vincentkoc

Copy link
Copy Markdown
Member

Merged via squash.

github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jul 2, 2026
…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>
chenyangjun-xy pushed a commit to chenyangjun-xy/openclaw that referenced this pull request Jul 3, 2026
…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>
sheyanmin pushed a commit to sheyanmin/openclaw that referenced this pull request Jul 8, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P1 High-priority user-facing bug, regression, or broken workflow. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: wrapAnthropicStreamWithRecovery bypassed when streamFn is async (embedded agent path)

2 participants