Skip to content

feat(think,ai-chat,agents): expose incident identity + enrich onExhausted payload (#1631) - #1636

Merged
threepointone merged 1 commit into
mainfrom
feat/chat-recovery-exhausted-context
Jun 1, 2026
Merged

threepointone merged 1 commit into
mainfrom
feat/chat-recovery-exhausted-context

Conversation

@threepointone

Copy link
Copy Markdown
Contributor

Stack

This is PR 4 of 4 (top) for #1631. Stacked; base is feat/think-repair-interrupted-tool-part-hook (PR #1635). Retargets to main as the stack merges. Diff shown is only this PR's commit.

  1. fix(think,ai-chat): make chat-recovery progress signal compaction-immune (#1628) #1633 — progress signal compaction-immune (bug: chat-recovery progress marker is count-based, so compaction can cause premature budget exhaustion #1628) · patch
  2. fix(think,ai-chat): preserve settled work when a recovery turn is given up (#1631) #1634 — preserve settled work when a turn is given up · patch
  3. feat(think): add repairInterruptedToolPart hook for client-resolved tools (#1631) #1635 — repairInterruptedToolPart hook · minor
  4. ➡️ incident identity + onExhausted payload (this PR) · minor

Public-API addition to shared types — additive only. Touches agents (shared lifecycle.ts types), @cloudflare/think, and @cloudflare/ai-chat.


Problem (#1631, item 3)

A production agent needs a persisted, user-visible terminal signal when recovery gives up, and a stable way to key per-incident budget state — but the framework made products re-derive both:

  • onExhausted payload is too thin. ChatRecoveryExhaustedContext carried only incidentId / requestId / attempt / maxAttempts / recoveryKind / reason. To render or persist a "this turn failed, send a new message" banner (especially when the client was disconnected at the moment recovery exhausted — exactly when this happens under deploy churn), products re-implemented it in a full onChatRecovery override.
  • No stable incident identity exposed. onChatRecovery got incidentId (<root>:<latestUserMessageId>) but not the recoveryRootRequestId on its own. Since requestId changes on every chained continuation, products keyed budget/fresh-incident logic off message IDs — error-prone, and the source of premature exhaustion when a custom budget lacked the framework's progress-aware reset.

Fix (additive)

In the shared packages/agents/src/chat/lifecycle.ts:

  • ChatRecoveryContext gains recoveryRootRequestId: string — the stable request ID for the whole continuation chain (constant across chained continuations). The right key for per-incident budget tracking / fresh-incident detection without touching message IDs.
  • ChatRecoveryExhaustedContext gains recoveryRootRequestId, terminalMessage (the exact user-facing text from the chatRecovery config), and partialText / partialParts (what the turn produced before being given up on) — enough to render/persist a contextual terminal banner with zero re-derivation.

Wired through both packages: onChatRecovery ctx now includes recoveryRootRequestId; _exhaustChatRecovery takes the reconstructed partial and builds the enriched ctx (ai-chat's local ChatRecoveryIncident gained recoveryRootRequestId, mirroring think).

Safety of the required field

recoveryRootRequestId is required on ChatRecoveryContext, but that type is only ever constructed by the framework (think.ts, ai-chat/index.ts, and the test workers — all updated here). Consumers only override onChatRecovery(ctx) and read the context, so adding a field is backward-compatible for them. Verified there are no other ChatRecoveryContext constructors in the repo.

Heads-up for reviewers: this edits packages/agents/src/chat/lifecycle.ts, whose types ship via the built dist consumed by think/ai-chat. Local typecheck required rebuilding agents first (nx run agents:build); CI builds in dependency order so it's a non-issue there.

Files

  • packages/agents/src/chat/lifecycle.ts — recoveryRootRequestId on ChatRecoveryContext; enriched ChatRecoveryExhaustedContext.
  • packages/think/src/think.ts — pass recoveryRootRequestId into the onChatRecovery ctx; _exhaustChatRecovery(incident, config, partial) builds the enriched ctx.
  • packages/ai-chat/src/index.ts — same; local ChatRecoveryIncident gains recoveryRootRequestId (set in _beginChatRecoveryIncident).
  • Test agents capture the exhausted ctx (enableExhaustedCaptureForTest / getExhaustedContextsForTest) and the recovery ctx's recoveryRootRequestId.
  • .changeset/chat-recovery-exhausted-context.md (minor · agents + think + ai-chat).

Tests (both packages)

  • exposes recoveryRootRequestId on the onChatRecovery context.
  • onExhausted context carries terminalMessage, recoveryRootRequestId, and the partial — drive an incident to exhaustion and assert the captured exhausted ctx.

Verification

  • tsc --noEmit clean (think + ai-chat, after rebuilding agents); oxlint clean; oxfmt applied.
  • Full suites green: think 460, ai-chat 481.

Test plan

  • CI green
  • API-shape sign-off on the new ChatRecoveryContext / ChatRecoveryExhaustedContext fields
  • Confirm no external constructors of ChatRecoveryContext break (none in-repo)

Made with Cursor

@changeset-bot

changeset-bot Bot commented May 31, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 827e0d5

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

This PR includes changesets to release 3 packages
Name Type
agents Minor
@cloudflare/think Minor
@cloudflare/ai-chat Minor

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

@threepointone
threepointone force-pushed the feat/think-repair-interrupted-tool-part-hook branch from 60cf123 to fd8a39d Compare June 1, 2026 14:49
Base automatically changed from feat/think-repair-interrupted-tool-part-hook to main June 1, 2026 14:56
…sted payload (#1631)

Lets products build a terminal-state policy without re-deriving anything:

- ChatRecoveryContext (onChatRecovery) gains recoveryRootRequestId — the stable
  request id for the whole continuation chain, the right key for per-incident
  budget tracking / fresh-incident detection (no re-deriving from message IDs).
- ChatRecoveryExhaustedContext (onExhausted) gains recoveryRootRequestId,
  terminalMessage (exact user-facing text), partialText/partialParts (what the
  turn produced before it was given up on), and streamId/createdAt — enough to
  render/persist a terminal banner AND emit correlated terminal telemetry
  (msSinceTurnStart, stream correlation) directly.

streamId + createdAt were added after verifying the payload against the actual
consumer (g3's _emitExhaustedRecovery): it reads both from the recovery context
for telemetry, and they already exist on ChatRecoveryContext (the Pick source),
so adding them to the exhausted context is additive and unblocks re-homing the
exhaustion handler onto onExhausted with zero re-derivation (D4).

Shared types in `agents`; wired through think + ai-chat (_exhaustChatRecovery
now receives streamId + createdAt). Test agents capture the exhausted context;
tests assert both contexts (incl. streamId + createdAt) in both packages.

Rebased onto main (dropping the merged #1633/#1634/#1635 commits); adapted the
exhausted-ctx test to N1/#1638's alarm-debounce and gave the think harness an
explicit return shape (the context's MessagePart[] over-instantiates the RPC
stub type).

Co-authored-by: Cursor <cursoragent@cursor.com>
@threepointone
threepointone force-pushed the feat/chat-recovery-exhausted-context branch from 4e5d0cb to 827e0d5 Compare June 1, 2026 15:29
@threepointone

Copy link
Copy Markdown
Contributor Author

Rebased onto main + D4 extension (added streamId + createdAt)

Rebased onto current main (dropped the now-merged #1633/#1634/#1635 commits; the diff is just this PR's change). Adapted the exhausted-ctx test to N1/#1638's alarm-debounce (seed lastAttemptAt ~60s in the past so the wake actually exhausts), and gave the think test harness an explicit return shape for getExhaustedContextsForTest (the context's partialParts: MessagePart[] over-instantiates the RPC stub type → TS2589, which also poisoned a sibling stub method).

D4 extension: verified the payload against the actual consumer — g3's _emitExhaustedRecovery (the exhaustion handler being re-homed onto onExhausted). It reads streamId and createdAt from the recovery context (for msSinceTurnStart/durationMs and Sentry/PostHog correlation), but ChatRecoveryExhaustedContext Picked neither — even though both already exist on ChatRecoveryContext. Added streamId + createdAt to the Pick and set them in _exhaustChatRecovery (think + ai-chat). Additive (framework constructs the type; consumers only read). Now the handler can move onto onExhausted with zero re-derivation; the banner copy (g3's OOM-aware buildCrashRecoveryCopy) and org/project IDs remain app-side state, and classification rides reason.

Tests now assert streamId + createdAt on the exhausted context in both packages. Green: think 468, ai-chat 487, npm run check clean (91 projects).

@pkg-pr-new

pkg-pr-new Bot commented Jun 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

agents

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

@cloudflare/ai-chat

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

@cloudflare/codemode

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

hono-agents

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

@cloudflare/shell

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

@cloudflare/think

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

@cloudflare/voice

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

@cloudflare/worker-bundler

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

commit: 827e0d5

@threepointone
threepointone merged commit f5a0d00 into main Jun 1, 2026
4 checks passed
@threepointone
threepointone deleted the feat/chat-recovery-exhausted-context branch June 1, 2026 15:41
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