@cloudflare/think@0.2.5
cloudflare/agents@cloudflare/think@0.2.5Apr 18, 2026by github-actions[bot]
AI Summary
This patch release fixes critical cross-DO I/O errors when spawning sub-agents in production and removes the requirement for the experimental compatibility flag. It also exposes a `createdAt` timestamp on recovery contexts to help applications guard against stale, interrupted conversation turns.
Key Highlights
- Fixed `subAgent()` cross-DO I/O errors by replacing fetch-based handshake with new `_cf_initAsFacet(name)` RPC
- Removed requirement for experimental compatibility flag - `ctx.facets`, `ctx.exports`, and `env.LOADER` have graduated
- Added `_isFacet` guards to broadcast methods preventing parent DO WebSocket registry access from child isolates
- Exposed `createdAt` on `FiberRecoveryContext` and `ChatRecoveryContext` for stale recovery detection
- Removed internal `_cf_markAsFacet()` method - `_cf_initAsFacet(name)` is now the correct entry point
Breaking Changes
- Removed internal `_cf_markAsFacet()` method - use `_cf_initAsFacet(name)` instead
New Features
- New `createdAt: number` field on `FiberRecoveryContext` (epoch milliseconds when `runFiber` started)
- New `createdAt: number` field on `ChatRecoveryContext` (threaded through from underlying fiber)
- Stale-recovery guard pattern support via `Date.now() - ctx.createdAt` comparison
Full Release Notes
### Patch Changes
- [#1330](https://github.com/cloudflare/agents/pull/1330) [`b4d3fcf`](https://github.com/cloudflare/agents/commit/b4d3fcfcce7363b137ad47c31d40aebcb34d9a28) Thanks [@threepointone](https://github.com/threepointone)! - Fix `subAgent()` cross-DO I/O errors on first use and drop the `"experimental"` compatibility flag requirement.
### `subAgent()` cross-DO I/O fix
Three issues in the facet initialization path caused `"Cannot perform I/O on behalf of a different Durable Object"` errors when spawning sub-agents in production:
- `subAgent()` constructed a `Request` in the parent DO and passed it to the child via `stub.fetch()`. The `Request` carried native I/O tied to the parent isolate, which the child rejected.
- The facet flag was set _after_ the first `onStart()` ran, so `broadcastMcpServers()` fired with `_isFacet === false` on the initial boot.
- `_broadcastProtocol()`, the inherited `broadcast()`, and `_workflow_broadcast()` iterated the connection registry without an `_isFacet` guard, letting broadcasts reach into the parent DO's WebSocket registry from a child isolate.
Replaces the fetch-based handshake with a new `_cf_initAsFacet(name)` RPC that runs entirely in the child isolate, sets `_isFacet` before init, and seeds partyserver's `__ps_name` key directly. Adds `_isFacet` guards to `_broadcastProtocol()` and overrides `broadcast()` to no-op on facets so downstream callers (chat-streaming paths, workflow broadcasts, user `this.broadcast(...)`) are covered. Removes the previous internal `_cf_markAsFacet()` method — `_cf_initAsFacet(name)` is the correct entry point (it sets the flag before running the first `onStart()`, which `_cf_markAsFacet` did not).
### `"experimental"` compatibility flag no longer required
`ctx.facets`, `ctx.exports`, and `env.LOADER` (Worker Loader) have graduated out of the `"experimental"` compatibility flag in workerd. `agents` and `@cloudflare/think` no longer require it:
- `subAgent()` / `abortSubAgent()` / `deleteSubAgent()` — the `@experimental` JSDoc tag and runtime error messages no longer reference the flag. The runtime guards on `ctx.facets` / `ctx.exports` stay in place and now nudge users toward updating `compatibility_date` instead.
- `Think` — the `@experimental` JSDoc tag no longer references the flag.
No code change is required; remove `"experimental"` from your `compatibility_flags` in `wrangler.jsonc` if it was only there for these features.
- [#1332](https://github.com/cloudflare/agents/pull/1332) [`7cb8acf`](https://github.com/cloudflare/agents/commit/7cb8acff8281a30bc17980e506ab5582f3cb1c72) Thanks [@threepointone](https://github.com/threepointone)! - Expose `createdAt` on fiber and chat recovery contexts so apps can suppress continuations for stale, interrupted turns.
- `FiberRecoveryContext` (from `agents`) gains `createdAt: number` — epoch milliseconds when `runFiber` started, read from the `cf_agents_runs` row that was already tracked internally.
- `ChatRecoveryContext` (from `@cloudflare/ai-chat` and `@cloudflare/think`) gains the same `createdAt` field, threaded through from the underlying fiber.
With this, the stale-recovery guard pattern described in [#1324](https://github.com/cloudflare/agents/issues/1324) is a short override:
```typescript
override async onChatRecovery(ctx: ChatRecoveryContext): Promise<ChatRecoveryOptions> {
if (Date.now() - ctx.createdAt > 2 * 60 * 1000) return { continue: false };
return {};
}
```
No behavior change for existing callers. See `docs/chat-agents.md` (new "Guarding against stale recoveries" section) for the full recipe, including a loop-protection pattern using `onChatResponse`.