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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant