Skip to content

fix(think): don't re-arm auto-continuation barrier on RPC stall recovery - #1671

Merged
threepointone merged 1 commit into
mainfrom
fix/think-rpc-stall-recovery-no-rearm
Jun 3, 2026
Merged

fix(think): don't re-arm auto-continuation barrier on RPC stall recovery#1671
threepointone merged 1 commit into
mainfrom
fix/think-rpc-stall-recovery-no-rearm

Conversation

@threepointone

@threepointone threepointone commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #1667 (event-driven auto-continuation barrier). Addresses two review findings.

Finding #1 — fix (real bug)

The RPC streaming path _streamResultToRpcCallback re-armed the auto-continuation coalesce timer in its finally even on the stream-stall recovery early-returns (scheduled / exhausted). A return inside the catch still runs the finally, so _onStreamingTurnFinalized() — which calls _rearmPendingAutoContinuationForBatch() — always fired.

This diverges from the WebSocket _streamResult recovery paths, which deliberately do a plain this._streamingAssistant = null without re-arming (see the comment at the WS scheduled branch: "recovery re-runs the turn and its own stream finalize re-triggers the held barrier").

Impact: when a parallel tool batch had a pending continuation at the moment the stall watchdog fired, the RPC re-arm scheduled a 50ms coalesce timer that could fire _fireAutoContinuation alongside the alarm-scheduled _chatRecoveryContinue continuation → a spurious double model invocation on the turn queue. idempotent: true only dedupes the scheduled task, not the auto-continuation.

Fix: a skipFinalizeRearm flag, set in the scheduled/exhausted branches, makes the finally do a plain _streamingAssistant = null clear instead of _onStreamingTurnFinalized() — mirroring the WS path. The held barrier is then re-triggered exactly once, by the recovery continuation's own stream finalize.

Finding #2 — document (not a regression, not safely fixable here)

The eviction self-healing path relies on the completing result carrying autoContinue: true — that re-creates _continuation.pending from the persisted transcript via _scheduleAutoContinuation. If the completing result is an errored autoContinue: false sibling after eviction, _rearmPendingAutoContinuationForBatch finds no pending (evicted) and no-ops, so the continuation is silently dropped.

The only signal that continuation was requested (the fast sibling's autoContinue: true) lives in the in-memory pending, not the persisted transcript — so it can't self-heal after eviction. This is not a regression (the old in-memory 60s timer was equally eviction-fragile) and a later user message / chat recovery repairs the transcript. A proper fix would require persisting the continuation-requested intent across eviction. Pinned the current behavior with an explicit test rather than changing it.

Tests

  • think-session.test.ts"does not re-arm a held auto-continuation when an RPC stall routes into bounded recovery": seeds a pending continuation, stalls into recovery, and asserts the coalesce timer is not re-armed (while _streamingAssistant is still cleared). Adds the testStallRecoveryDoesNotRearmPendingContinuation harness method.
  • client-tools.test.ts"documents the known gap: eviction + an errored autoContinue:false completing result drops the held continuation": asserts 0 continuations fire and both tool results are still applied to the transcript.

Verification

  • npm run check passes (sherif, export checks, oxfmt, oxlint, typecheck across 91 projects).
  • New + existing stall / self-heal tests green.
  • Changeset added: patch to @cloudflare/think.

Open in Devin Review
Follow-up to #1667. Two findings from review of the event-driven
auto-continuation barrier.

#1 (fix): The RPC streaming path (`_streamResultToRpcCallback`) re-armed
the auto-continuation coalesce timer in its `finally` even on the
stream-stall recovery early-returns (`scheduled`/`exhausted`). This is
unlike the WebSocket `_streamResult` recovery paths, which deliberately
do a plain `this._streamingAssistant = null` WITHOUT re-arming, because
the scheduled recovery continuation re-runs the turn and its own stream
finalize re-triggers the held barrier.

When a parallel tool batch had a pending continuation at the moment the
stall watchdog fired, the RPC re-arm scheduled a 50ms coalesce timer
that could fire `_fireAutoContinuation` alongside the alarm-scheduled
recovery continuation -> a spurious double model invocation on the turn
queue. The RPC recovery early-returns now mirror the WebSocket path: a
`skipFinalizeRearm` flag makes the `finally` do a plain clear instead of
`_onStreamingTurnFinalized()`, so the held barrier is re-triggered
exactly once by the recovery continuation.

#2 (document): The eviction self-healing path relies on the completing
result carrying `autoContinue: true` (it re-creates `_continuation.pending`
from the persisted transcript via `_scheduleAutoContinuation`). If the
completing result is an errored `autoContinue: false` sibling AFTER
eviction, `_rearmPendingAutoContinuationForBatch` finds no pending and
no-ops -- the continuation is silently dropped. This is NOT a regression
(the old in-memory 60s timer was equally eviction-fragile) and a later
user message / chat recovery repairs the transcript; fixing it properly
would require persisting the continuation-requested intent across
eviction. Pinned the current behavior with an explicit test rather than
changing it.

Tests:
- think-session.test.ts: asserts the coalesce timer is NOT re-armed
  after an RPC stall routes into recovery (while `_streamingAssistant`
  is still cleared). Adds `testStallRecoveryDoesNotRearmPendingContinuation`.
- client-tools.test.ts: documents the eviction + errored-completing-
  result gap (0 continuations, both results still applied to transcript).

npm run check passes; new + existing stall/self-heal tests green.
@changeset-bot

changeset-bot Bot commented Jun 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 37a44b7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cloudflare/think Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional findings.

Open in Devin Review
@pkg-pr-new

pkg-pr-new Bot commented Jun 3, 2026

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@1671

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@1671

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@1671

hono-agents

npm i https://pkg.pr.new/hono-agents@1671

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@1671

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@1671

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@1671

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@1671

commit: 37a44b7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant