fix(chat-recovery): seal a turn after repeated interrupted attempts by mattzcarey · Pull Request #2615 · cloudflare/agents · GitHub
Skip to content

fix(chat-recovery): seal a turn after repeated interrupted attempts - #2615

Merged
mattzcarey merged 3 commits into
mainfrom
fix/2613-chat-recovery-interrupted-attempts
Oct 20, 2026
Merged

mattzcarey merged 3 commits into
mainfrom
fix/2613-chat-recovery-interrupted-attempts

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Chat recovery now seals a turn with interrupted_attempts_exceeded once chatRecovery.maxInterruptedAttempts of its attempts have died (default 5, the original turn included). Each attempt (the replay of an interrupted turn run, and each _chatRecoveryContinue / _chatRecoveryRetry callback) records itself on the incident as attemptInFlight before doing anything heavy, confirms it with storage.sync(), and clears it when it finishes. The next attempt counts a record left by another isolate, and nothing is written on the failure path any more. Fixes #2613.

A Durable Object's own memory-limit reset makes every later storage call in that invocation reject, and every #1825 counter was written after the reset:

  • recordOomAndDecide failed on its first read, so oomAttempts never moved
  • the alarm breaker's strike get/put threw and fell back to strikes = 1, so one DO logged "strike 1/3" 10,800 times a day for eight days
  • the attempt cap and no-progress window reset whenever the re-run streamed output
chatRecovery = {
  // Counts every interrupted attempt. The next isolate can't tell a
  // memory-limit reset from a deploy, so five deploys in a row on one turn
  // also seal it. `maxOomRetries: n` still works and reads as n + 1.
  maxInterruptedAttempts: 5
};

Tests ran: pnpm run check, the agents chat project, and the Think and ai-chat workers suites. I also deployed a Think agent whose model exceeds 128 MB on every run to a personal account. On released agents@0.28.1 it re-ran the model 14 times in 6 minutes and kept going (incident still scheduled, attempt: 1, oomAttempts never written). On this branch it ran the model 5 times and sealed with interrupted_attempts_exceeded.

…ed (#2613)

A Durable Object's own memory-limit reset makes every later storage call in
that invocation reject, so the OOM counters written on the failure path
(recordOomAndDecide, the alarm breaker's strike) never advanced and recovery
looped forever.

Count each interrupted attempt in beginIncident when the next isolate replays
the interrupted turn run, before anything heavy runs, and confirm the write
with storage.sync(). Seal at chatRecovery.maxInterruptedAttempts (default 5)
with interrupted_attempts_exceeded. Progress and debounce never reset the
count. Remove the failure-path OOM bookkeeping from Think and AIChatAgent.
maxOomRetries stays as a deprecated alias (maxOomRetries + 1).
@changeset-bot

changeset-bot Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

@agent-think

agent-think Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

🔴 agents import sizes: 2 entry points over threshold

Entry point Exports Largest gzip change Size now
🔴 agents/harness/pi 3 resized up to +46.4 KiB (+340.34%) 60.1 KiB
🔴 agents/chat 1 resized, 1 new, 1 removed +836 B (+17.15%) 5.6 KiB
🟡 agents/models/pi-ai 1 resized +2.5 KiB (+1.58%) 159 KiB
Changed exports (7)
Import Gzip change Size now
🔴 agents/harness/pi#PiHarness +46.4 KiB (+340.34%) 60.1 KiB
🔴 agents/harness/pi#PiSession +46 KiB (+962.03%) 50.7 KiB
🔴 agents/chat#ChatRecoveryEngine +836 B (+17.15%) 5.6 KiB
🟡 agents/models/pi-ai#createAI +2.5 KiB (+1.58%) 159 KiB
🟡 agents/harness/pi#openPiSessionStore +362 B (+4.6%) 8 KiB
🆕 agents/chat#DEFAULT_CHAT_RECOVERY_MAX_INTERRUPTED_ATTEMPTS — 2.5 KiB
➖ agents/chat#DEFAULT_CHAT_RECOVERY_MAX_OOM_RETRIES — —
How this works

Each runtime export is bundled on its own, minified, and gzipped. Changes smaller than 100 B, or smaller than 1% and 1 KiB, are ignored. Growth over 10% or 5 KiB is marked 🔴. This report is informational and does not fail CI. The workflow artifact contains every measurement.

Compared b905483d → eb6ec862 · workflow run · reported by agent-think[bot]

@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.

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 6 potential issues.

Devin Review

Comment thread packages/agents/src/chat/recovery-engine.ts Outdated
Comment on lines +461 to +468
// A recovered fiber is a run whose isolate died before it finished, so
// this opening counts one interrupted attempt for the turn (#2613).
const { incident, config, exhausted } = await this.beginIncident({
requestId,
recoveryRootRequestId,
latestUserMessageId: snapshot?.latestUserMessageId,
recoveryKind
recoveryKind,
trigger: { kind: "interrupted", runId: ctx.id }

@devin-ai-integration devin-ai-integration Bot Oct 19, 2026 •

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.

🔴 Oversized partials bypass the interruption cap

If getPartialStreamText exceeds the memory limit, beginIncident never counts the recovered run. Every replay reconstructs the same partial before the new budget runs, leaving the crash loop unbounded.

Learn more

On wake, the engine reconstructs the orphaned assistant response from its durable stream log before it opens the incident. The hosts' partial reconstruction reads every stored chunk and decodes them into parts; a sufficiently large log can itself exhaust isolate memory. Such a reset occurs before the added beginIncident call, so no count is written even though the turn is retried.

Example: An interrupted turn has enough buffered chunks that rebuilding its partial exceeds the isolate limit. Each new isolate attempts the same reconstruction, dies, and leaves interruptedAttempts unchanged.

Recommended fix: Open and sync an incident using the recovered run ID before loading or decoding the stream log. Defer reconstruction and classification until after the durable interruption count and exhaustion check, while preserving the final recovery kind needed for terminalization.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not fixed in this PR. A reset while rebuilding the partial happens before the replay opens the incident, so it is not counted. It does not re-run the model, and bounding it needs a terminal path that skips rebuilding the partial, since sealing through the normal path would hit the same reset. It is listed under what remains unbounded in design/rfc-chat-recovery-interrupted-attempts.md.

Comment thread .changeset/chat-recovery-interrupted-attempts.md Outdated
Comment thread design/rfc-chat-recovery-work-budget.md Outdated
Comment thread packages/agents/src/chat/recovery-incident.ts Outdated
Comment on lines +302 to +305
const legacyRetries = custom?.maxOomRetries;
if (typeof legacyRetries === "number" && legacyRetries >= 0) {
return Math.floor(legacyRetries) + 1;
}

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.

📝 Info: Legacy OOM setting also bounds deploys

Existing maxOomRetries configurations now limit all interrupted turns, including deploy resets. Operators relying on deploy tolerance need to revisit that setting.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Intended. The maxOomRetries JSDoc and the docs table say it now counts every interrupted attempt, deploys included, and the changeset marks it deprecated.

@pkg-pr-new

pkg-pr-new Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

agents

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

@cloudflare/ai-chat

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

@cloudflare/codemode

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

hono-agents

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

@cloudflare/shell

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

@cloudflare/think

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

@cloudflare/voice

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

@cloudflare/worker-bundler

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

commit: eb6ec86

…ehind

Every attempt that can run heavy work (the replay of an interrupted turn run,
and each recovery callback through ChatRecoveryEngine.runRecoveryAttempt)
records itself on the incident as attemptInFlight, confirmed with
storage.sync(), and clears it when it finishes. The next attempt counts a
record left by another isolate. This also catches a callback that dies
before starting its turn, which leaves no turn run to replay.

Move the decision record into its own RFC, update the living design docs,
and shorten the changeset to one line.

@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.

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

Devin Review found 4 new potential issues.

Devin Review

Comment thread packages/agents/src/chat/recovery-engine.ts Outdated
Comment thread packages/agents/src/chat/recovery-engine.ts
Comment on lines +979 to +985
const { attemptInFlight: _previous, ...rest } = existing;
return {
incident: {
...rest,
...(interruptedAttempts > 0 ? { interruptedAttempts } : {}),
...(exhausted ? {} : { attemptInFlight: attempt })
},

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.

🔴 Overlapping attempts lose interrupted replay counts

When a callback finishes while its replay still runs, attemptInFlight no longer records the replay. A reset of that replay then leaves no death to count, allowing repeated work past the limit.

Learn more

A recovered turn replay can enqueue a callback before it finishes. The callback's admission replaces the replay's sole persisted marker. If the callback finishes first, #finishAttempt removes that marker, although the replay is still active. If the replay then resets, the next isolate sees no in-flight marker and does not count that interrupted replay.

Example: Replay A opens an incident, callback B starts in the same object, B completes and clears its marker, then A hits a memory reset. Replay C finds no marker from A and starts without consuming another interruption.

Recommended fix: Track overlapping attempts independently, or preserve the older live replay marker when the callback finishes. Ensure the next admission counts each dead attempt without counting a still-running attempt from the same object.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in eb6ec86: attemptsInFlight is now a list. Entries from this isolate are attempts still running and are kept; each other isolate that left entries counts once, however many it left. Covered by tracks overlapping attempts apart, so a replay that dies after its callback finished still counts and counts one interruption for an isolate that died running two attempts.

Comment on lines +469 to +475
const { incident, exhausted } = admitChatRecoveryAttempt({
existing,
attempt,
config: adapter.resolveConfig(),
awaitingClientInteraction:
adapter.isAwaitingClientInteraction?.() ?? false
});

@devin-ai-integration devin-ai-integration Bot Oct 20, 2026 •

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.

🔍 Recovery callbacks also consume the budget

The PR description names turn replay as the counting point, but runRecoveryAttempt also counts dead callbacks before a turn starts. The description can mislead readers about which failures exhaust recovery.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Updated the PR description: it names both counting points, the replay of an interrupted turn run and each recovery callback.

@mattzcarey mattzcarey changed the title fix(chat-recovery): count interrupted attempts when the run is replayed fix(chat-recovery): seal a turn after repeated interrupted attempts Oct 20, 2026
…p sealed callbacks

attemptsInFlight is now a list, so a replay and the callback it schedules in
the same isolate are tracked apart; each dead isolate counts once. Removing a
finished attempt's entry is confirmed with storage.sync(), so a reset right
after a clean finish cannot look like an interruption. A callback still queued
for a sealed incident does nothing instead of re-running the model.

@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 found 1 new potential issue.

Devin Review

Comment on lines +44 to +53

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.

🟡 Accepted decision record loses its snapshot

The accepted RFC rewrites its proposal after the decision. Design rules keep accepted RFCs as snapshots; this erases the original decision history.

Learn more

An accepted RFC is a point-in-time decision record, while a design doc describes the current implementation. Design rules explicitly distinguish these roles and require RFCs not to be updated after the decision. This revision rewrites the accepted proposal and alternatives to reflect the new list-based implementation, so the earlier accepted proposal no longer survives in the record.

Example: A contributor investigating why the original single-record approach was adopted sees only the revised list-based proposal, not the original decision or its subsequent correction.

Recommended fix: Keep the accepted RFC as originally decided. Describe the list-based implementation in the living chat design document and, if its rationale merits a decision record, add a separate amendment RFC linked from that document.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This RFC is new in this PR and has not been merged, so there is no earlier accepted version to preserve. The single-record approach was never accepted on main; it is listed under the alternatives, with why it was replaced. The decision takes effect when this PR lands, and later changes will go in a new RFC.

@mattzcarey
mattzcarey merged commit 05843e4 into main Oct 20, 2026
36 of 42 checks passed
@mattzcarey
mattzcarey deleted the fix/2613-chat-recovery-interrupted-attempts branch October 20, 2026 15:02
@github-actions github-actions Bot mentioned this pull request Oct 20, 2026
brandhaug added a commit to brandhaug/b2b-saas-starter that referenced this pull request Oct 24, 2026
## pnpm-workspace.yaml (default)

## Dependency Updates

| Package | From | To | Type |
| --- | --- | --- | --- |
| `@better-auth/cimd` | 1.7.11 | 1.7.12 | patch |
| `@better-auth/mcp` | 1.7.11 | 1.7.12 | patch |
| `@better-auth/oauth-provider` | 1.7.11 | 1.7.12 | patch |
| `@better-auth/passkey` | 1.7.11 | 1.7.12 | patch |
| `@better-auth/sso` | 1.7.11 | 1.7.12 | patch |
| `@cloudflare/ai-chat` | 0.12.1 | 0.12.2 | patch |
| `@inlang/plugin-message-format` | 4.5.4 | 4.5.5 | patch |
| `ai` | 7.0.146 | 7.0.148 | patch |
| `better-auth` | 1.7.11 | 1.7.12 | patch |
| `effectful-better-auth` | 1.0.5 | 1.0.7 | patch |

## Release Notes

<details>
<summary><b>@<!---->better-auth/cimd</b> (1.7.11 → 1.7.12)</summary>

> [!IMPORTANT]
> **Action required**
>
> - If you use ID token sign-in with a provider that has no `clientId`,
set `clientId` to every client ID your apps sign in with.
([#11931](better-auth/better-auth#11931))

## `better-auth`

### Bug Fixes

- Fixed the JWT plugin to verify JWTs and session cookie-cache JWTs with
the stored key's algorithm instead of the token header's, and to warn
when signing or verifying a JWT without an issuer or audience
([#11925](better-auth/better-auth#11925))
- Fixed OAuth callbacks completed through the OAuth proxy to run the
same checks as the standard OAuth callback, including issuer and ID
token nonce validation
([#11930](better-auth/better-auth#11930))
- Fixed `auth.api.getSession()` to keep the original database error as
`error.cause` when the session lookup fails
([#11924](better-auth/better-auth#11924))

For detailed changes, see
[`CHANGELOG`](https://github.com/better-auth/better-auth/blob/29735514c737d2abeac2fb84f0691fd316231949/packages/better-auth/CHANGELOG.md)

## `@better-auth/core`

### Bug Fixes

- Fixed `mapProfileToUser` failing to typecheck under
`exactOptionalPropertyTypes` when it returns `undefined` for `name`,
`email`, or `image`
([#11918](better-auth/better-auth#11918))
- Fixed ID token sign-in to reject tokens when the provider has no
client ID, since their audience cannot be checked
([#11931](better-auth/better-auth#11931))
- Fixed a custom Facebook `getUserInfo` to no longer receive
client-submitted access tokens issued to another Facebook app, and ID
tokens missing `exp` or `iat` are now rejected
([#11919](better-auth/better-auth#11919))
- Restored the Twitch and Paybin ID token handling from before 1.7.11,
which made account info fail once the ID token stored at sign-in expired
([#11920](https://github.com/better-auth/bette

…[full
notes](https://github.com/better-auth/better-auth/releases/tag/v1.7.12)

</details>

<details>
<summary><b>@<!---->better-auth/mcp</b> (1.7.11 → 1.7.12)</summary>

> [!IMPORTANT]
> **Action required**
>
> - If you use ID token sign-in with a provider that has no `clientId`,
set `clientId` to every client ID your apps sign in with.
([#11931](better-auth/better-auth#11931))

## `better-auth`

### Bug Fixes

- Fixed the JWT plugin to verify JWTs and session cookie-cache JWTs with
the stored key's algorithm instead of the token header's, and to warn
when signing or verifying a JWT without an issuer or audience
([#11925](better-auth/better-auth#11925))
- Fixed OAuth callbacks completed through the OAuth proxy to run the
same checks as the standard OAuth callback, including issuer and ID
token nonce validation
([#11930](better-auth/better-auth#11930))
- Fixed `auth.api.getSession()` to keep the original database error as
`error.cause` when the session lookup fails
([#11924](better-auth/better-auth#11924))

For detailed changes, see
[`CHANGELOG`](https://github.com/better-auth/better-auth/blob/29735514c737d2abeac2fb84f0691fd316231949/packages/better-auth/CHANGELOG.md)

## `@better-auth/core`

### Bug Fixes

- Fixed `mapProfileToUser` failing to typecheck under
`exactOptionalPropertyTypes` when it returns `undefined` for `name`,
`email`, or `image`
([#11918](better-auth/better-auth#11918))
- Fixed ID token sign-in to reject tokens when the provider has no
client ID, since their audience cannot be checked
([#11931](better-auth/better-auth#11931))
- Fixed a custom Facebook `getUserInfo` to no longer receive
client-submitted access tokens issued to another Facebook app, and ID
tokens missing `exp` or `iat` are now rejected
([#11919](better-auth/better-auth#11919))
- Restored the Twitch and Paybin ID token handling from before 1.7.11,
which made account info fail once the ID token stored at sign-in expired
([#11920](https://github.com/better-auth/bette

…[full
notes](https://github.com/better-auth/better-auth/releases/tag/v1.7.12)

</details>

<details>
<summary><b>@<!---->better-auth/oauth-provider</b> (1.7.11 →
1.7.12)</summary>

> [!IMPORTANT]
> **Action required**
>
> - If you use ID token sign-in with a provider that has no `clientId`,
set `clientId` to every client ID your apps sign in with.
([#11931](better-auth/better-auth#11931))

## `better-auth`

### Bug Fixes

- Fixed the JWT plugin to verify JWTs and session cookie-cache JWTs with
the stored key's algorithm instead of the token header's, and to warn
when signing or verifying a JWT without an issuer or audience
([#11925](better-auth/better-auth#11925))
- Fixed OAuth callbacks completed through the OAuth proxy to run the
same checks as the standard OAuth callback, including issuer and ID
token nonce validation
([#11930](better-auth/better-auth#11930))
- Fixed `auth.api.getSession()` to keep the original database error as
`error.cause` when the session lookup fails
([#11924](better-auth/better-auth#11924))

For detailed changes, see
[`CHANGELOG`](https://github.com/better-auth/better-auth/blob/29735514c737d2abeac2fb84f0691fd316231949/packages/better-auth/CHANGELOG.md)

## `@better-auth/core`

### Bug Fixes

- Fixed `mapProfileToUser` failing to typecheck under
`exactOptionalPropertyTypes` when it returns `undefined` for `name`,
`email`, or `image`
([#11918](better-auth/better-auth#11918))
- Fixed ID token sign-in to reject tokens when the provider has no
client ID, since their audience cannot be checked
([#11931](better-auth/better-auth#11931))
- Fixed a custom Facebook `getUserInfo` to no longer receive
client-submitted access tokens issued to another Facebook app, and ID
tokens missing `exp` or `iat` are now rejected
([#11919](better-auth/better-auth#11919))
- Restored the Twitch and Paybin ID token handling from before 1.7.11,
which made account info fail once the ID token stored at sign-in expired
([#11920](https://github.com/better-auth/bette

…[full
notes](https://github.com/better-auth/better-auth/releases/tag/v1.7.12)

</details>

<details>
<summary><b>@<!---->better-auth/passkey</b> (1.7.11 → 1.7.12)</summary>

> [!IMPORTANT]
> **Action required**
>
> - If you use ID token sign-in with a provider that has no `clientId`,
set `clientId` to every client ID your apps sign in with.
([#11931](better-auth/better-auth#11931))

## `better-auth`

### Bug Fixes

- Fixed the JWT plugin to verify JWTs and session cookie-cache JWTs with
the stored key's algorithm instead of the token header's, and to warn
when signing or verifying a JWT without an issuer or audience
([#11925](better-auth/better-auth#11925))
- Fixed OAuth callbacks completed through the OAuth proxy to run the
same checks as the standard OAuth callback, including issuer and ID
token nonce validation
([#11930](better-auth/better-auth#11930))
- Fixed `auth.api.getSession()` to keep the original database error as
`error.cause` when the session lookup fails
([#11924](better-auth/better-auth#11924))

For detailed changes, see
[`CHANGELOG`](https://github.com/better-auth/better-auth/blob/29735514c737d2abeac2fb84f0691fd316231949/packages/better-auth/CHANGELOG.md)

## `@better-auth/core`

### Bug Fixes

- Fixed `mapProfileToUser` failing to typecheck under
`exactOptionalPropertyTypes` when it returns `undefined` for `name`,
`email`, or `image`
([#11918](better-auth/better-auth#11918))
- Fixed ID token sign-in to reject tokens when the provider has no
client ID, since their audience cannot be checked
([#11931](better-auth/better-auth#11931))
- Fixed a custom Facebook `getUserInfo` to no longer receive
client-submitted access tokens issued to another Facebook app, and ID
tokens missing `exp` or `iat` are now rejected
([#11919](better-auth/better-auth#11919))
- Restored the Twitch and Paybin ID token handling from before 1.7.11,
which made account info fail once the ID token stored at sign-in expired
([#11920](https://github.com/better-auth/bette

…[full
notes](https://github.com/better-auth/better-auth/releases/tag/v1.7.12)

</details>

<details>
<summary><b>@<!---->better-auth/sso</b> (1.7.11 → 1.7.12)</summary>

> [!IMPORTANT]
> **Action required**
>
> - If you use ID token sign-in with a provider that has no `clientId`,
set `clientId` to every client ID your apps sign in with.
([#11931](better-auth/better-auth#11931))

## `better-auth`

### Bug Fixes

- Fixed the JWT plugin to verify JWTs and session cookie-cache JWTs with
the stored key's algorithm instead of the token header's, and to warn
when signing or verifying a JWT without an issuer or audience
([#11925](better-auth/better-auth#11925))
- Fixed OAuth callbacks completed through the OAuth proxy to run the
same checks as the standard OAuth callback, including issuer and ID
token nonce validation
([#11930](better-auth/better-auth#11930))
- Fixed `auth.api.getSession()` to keep the original database error as
`error.cause` when the session lookup fails
([#11924](better-auth/better-auth#11924))

For detailed changes, see
[`CHANGELOG`](https://github.com/better-auth/better-auth/blob/29735514c737d2abeac2fb84f0691fd316231949/packages/better-auth/CHANGELOG.md)

## `@better-auth/core`

### Bug Fixes

- Fixed `mapProfileToUser` failing to typecheck under
`exactOptionalPropertyTypes` when it returns `undefined` for `name`,
`email`, or `image`
([#11918](better-auth/better-auth#11918))
- Fixed ID token sign-in to reject tokens when the provider has no
client ID, since their audience cannot be checked
([#11931](better-auth/better-auth#11931))
- Fixed a custom Facebook `getUserInfo` to no longer receive
client-submitted access tokens issued to another Facebook app, and ID
tokens missing `exp` or `iat` are now rejected
([#11919](better-auth/better-auth#11919))
- Restored the Twitch and Paybin ID token handling from before 1.7.11,
which made account info fail once the ID token stored at sign-in expired
([#11920](https://github.com/better-auth/bette

…[full
notes](https://github.com/better-auth/better-auth/releases/tag/v1.7.12)

</details>

<details>
<summary><b>@<!---->cloudflare/ai-chat</b> (0.12.1 → 0.12.2)</summary>

### Patch Changes

- [#2615](cloudflare/agents#2615)
[`05843e4`](cloudflare/agents@05843e4)
Thanks [@<!---->mattzcarey](https://github.com/mattzcarey)! - Chat
recovery now seals a turn after `chatRecovery.maxInterruptedAttempts`
interrupted attempts (default 5), so a turn that exceeds the Durable
Object memory limit on every run no longer retries forever;
`maxOomRetries` is deprecated. See [Stream
Recovery](https://github.com/cloudflare/agents/blob/main/docs/agents/chat-agents.md#stream-recovery).

</details>

<details>
<summary><b>better-auth</b> (1.7.11 → 1.7.12)</summary>

> [!IMPORTANT]
> **Action required**
>
> - If you use ID token sign-in with a provider that has no `clientId`,
set `clientId` to every client ID your apps sign in with.
([#11931](better-auth/better-auth#11931))

## `better-auth`

### Bug Fixes

- Fixed the JWT plugin to verify JWTs and session cookie-cache JWTs with
the stored key's algorithm instead of the token header's, and to warn
when signing or verifying a JWT without an issuer or audience
([#11925](better-auth/better-auth#11925))
- Fixed OAuth callbacks completed through the OAuth proxy to run the
same checks as the standard OAuth callback, including issuer and ID
token nonce validation
([#11930](better-auth/better-auth#11930))
- Fixed `auth.api.getSession()` to keep the original database error as
`error.cause` when the session lookup fails
([#11924](better-auth/better-auth#11924))

For detailed changes, see
[`CHANGELOG`](https://github.com/better-auth/better-auth/blob/29735514c737d2abeac2fb84f0691fd316231949/packages/better-auth/CHANGELOG.md)

## `@better-auth/core`

### Bug Fixes

- Fixed `mapProfileToUser` failing to typecheck under
`exactOptionalPropertyTypes` when it returns `undefined` for `name`,
`email`, or `image`
([#11918](better-auth/better-auth#11918))
- Fixed ID token sign-in to reject tokens when the provider has no
client ID, since their audience cannot be checked
([#11931](better-auth/better-auth#11931))
- Fixed a custom Facebook `getUserInfo` to no longer receive
client-submitted access tokens issued to another Facebook app, and ID
tokens missing `exp` or `iat` are now rejected
([#11919](better-auth/better-auth#11919))
- Restored the Twitch and Paybin ID token handling from before 1.7.11,
which made account info fail once the ID token stored at sign-in expired
([#11920](https://github.com/better-auth/bette

…[full
notes](https://github.com/better-auth/better-auth/releases/tag/v1.7.12)

</details>

---
*This PR was auto-generated by
[catalog-update-action](https://github.com/brandhaug/catalog-update-action).*

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
brandhaug added a commit to brandhaug/b2b-saas-starter that referenced this pull request Oct 24, 2026
## pnpm-workspace.yaml (default)

## Dependency Updates

| Package | From | To | Type |
| --- | --- | --- | --- |
| `agents` | 0.28.1 | 0.29.0 | minor |

## Release Notes

<details>
<summary><b>agents</b> (0.28.1 → 0.29.0)</summary>

### Minor Changes

- [#2560](cloudflare/agents#2560)
[`8c720d3`](cloudflare/agents@8c720d3)
Thanks [@<!---->mattzcarey](https://github.com/mattzcarey)! - Agent
Skills read and write a `@cloudflare/computer` Workspace, for both
projection and `skills.runner()`. See [Upgrading to a Computer
workspace](https://github.com/cloudflare/agents/blob/main/docs/think/index.md#upgrading-to-a-computer-workspace).

### Patch Changes

- [#2584](cloudflare/agents#2584)
[`3b3a88e`](cloudflare/agents@3b3a88e)
Thanks [@<!---->ben-reitz](https://github.com/ben-reitz)! - `Browser`
creates and closes its Browser Run sessions with the binding's typed
`acquire()` and `closeSession()`, falling back to the HTTP API in fully
local dev. See
[Browser](https://developers.cloudflare.com/agents/tools/browser/).

- [#2615](cloudflare/agents#2615)
[`05843e4`](cloudflare/agents@05843e4)
Thanks [@<!---->mattzcarey](https://github.com/mattzcarey)! - Chat
recovery now seals a turn after `chatRecovery.maxInterruptedAttempts`
interrupted attempts (default 5), so a turn that exceeds the Durable
Object memory limit on every run no longer retries forever;
`maxOomRetries` is deprecated. See [Stream
Recovery](https://github.com/cloudflare/agents/blob/main/docs/agents/chat-agents.md#stream-recovery).

- [#2604](cloudflare/agents#2604)
[`5d6f5d6`](cloudflare/agents@5d6f5d6)
Thanks [@<!---->mattzcarey](https://github.com/mattzcarey)! - `createAI`
takes `{ accountId, apiToken }` in place of `binding`, to use a gateway
in another account or to run outside a Worker. See [the models
docs](https://github.com/cloudflare/agents/blob/main/docs/agents/models.md#account-id-and-api-token).

-   [#2622](https://github.

…[full
notes](https://github.com/cloudflare/agents/releases/tag/agents%400.29.0)

</details>

---
*This PR was auto-generated by
[catalog-update-action](https://github.com/brandhaug/catalog-update-action).*

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
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.

Alarm memory-limit breaker (#1825) never trips after a DO memory-limit reset: strike counter can't be persisted, recovery loops indefinitely

1 participant