Loading workspace insights... Statistics interval
7 days30 daysLatest CI Pipeline Executions
bcb1b939 chore: migrate toolchain to oxc + TypeScript 7 + vite 8 (#949)
* chore: migrate toolchain to oxc + TypeScript 7 + vite 8
Replace ESLint/Prettier with the oxc toolchain, upgrade the type
checker to TypeScript 7 (native Go compiler), and move the build stack
to vite 8 (Rolldown). Framework tooling that can't run TS7/vite8 yet is
pinned back via the @typescript/typescript6 shim and per-package vite
pins.
Formatter
- Prettier -> oxfmt (.oxfmtrc.json migrated from prettier config).
oxfmt formats .svelte natively, so prettier + prettier-plugin-svelte
are removed. `format` -> `oxfmt .`.
Linter
- ESLint -> oxlint, untyped + type-aware (oxlint-tsgolint). The custom
`as unknown as` ban is ported via oxlint-plugin-eslint; unused-imports
and @stylistic run through oxlint's JS-plugin layer. ESLint and
@tanstack/eslint-config removed. `test:eslint` -> `test:oxlint`.
TypeScript 7
- Root typescript -> 7.0.2 (tsc is the native Go compiler). types now
defaults to [], so tsconfig.base sets `"types": ["node"]`.
- JS-Compiler-API consumers use the @typescript/typescript6 (6.0.2)
shim: vite-plugin-dts (built-in fallback), @sveltejs/package,
svelte-check, vue-tsc, kiira, knip (packageExtensions);
@analogjs/vite-plugin-angular pinned to 5.9.3. Framework packages
(angular/vue/solid/svelte) stay on 5.9.3.
- Fixes: ai-acp Uint8Array->BufferSource (TS 5.7 typed-array generics);
ai-react/ai-preact rootDir "../.." (TS7 rootDir default + cross-package
test fixtures); one ai-client @ts-expect-error reposition.
Build (vite 8 / Rolldown)
- vite -> 8.1.4, vitest -> 4.1.10. @tanstack/vite-config bumped to 0.5.2
and patched: outDir->outDirs (vite-plugin-dts 5.x) and an added
rollupOptions.preserveModules mirror so multi-entry output holds on
both vite 7 and 8.
- ai-solid-ui stays on vite 7: Solid JSX has no vite 8/Rolldown support
yet (solidjs/solid-start#2075).
Also: nx 22->23, knip 5->6. sherif ignores typescript+vite (intentional
multi-version). CLAUDE.md updated for the new commands/tooling.
* ci: apply automated fixes
* fix(examples): pin example apps to vite 7
The TanStack Start example apps (Start + nitro + framework plugins)
don't build under vite 8 / Rolldown yet — ts-solid-chat and
ts-react-chat fail in the Start server-fn compiler / nitro server
build. Keep the library packages on vite 8 and hold the dev-only
example apps on vite 7 until Start/nitro support vite 8.
* fix: unblock example builds + ai-mcp CI timeout under the new toolchain
- ai/realtime: drop the literal `createServerFn()` from the JSDoc example.
The vite 8/Rolldown build preserves JSDoc in the emitted .js (vite 7
stripped it), and TanStack Start's server-fn plugin string-matches
`createServerFn`, so it tried to compile @tanstack/ai's realtime dist and
resolve the framework runtime (@tanstack/solid-start) from ai's package,
failing ts-solid-chat's build. The example still shows realtimeToken usage.
- ai-mcp: raise vitest testTimeout to 30s. The client/server handshake
tests absorb ~20s+ cold module-import cost on CI, tipping the first test
over the 5s default (flaky timeout).
* chore(examples): upgrade nitro to latest
- nitro (v3): sandbox-web + ts-react-media 3.0.1-alpha.2 -> 3.0.260610-beta;
ts-react-search pinned off the floating `latest` tag to the same version.
Consolidates to a single nitro v3 in the lockfile (drops the stale alpha).
- @tanstack/nitro-v2-vite-plugin: ^1.154.7 -> ^1.155.0 across the Start
examples + testing/panel + testing/e2e.
Verified: all six examples build and testing/panel + testing/e2e typecheck.
Examples stay on vite 7 for now (Start/nitro vite 8 support is a separate
follow-up).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(examples): move React Start examples to nitro 3 + vite 8
Un-pins the React Start examples from vite 7 and standardizes them on
nitro 3 (`nitro/vite`), dropping @tanstack/nitro-v2-vite-plugin.
- sandbox-web, ts-react-media, ts-react-search: already nitro 3 — flipped
vite ^7.3.3 -> ^8.1.4.
- ts-react-chat, ts-code-mode-web: migrated nitroV2Plugin() -> nitro(),
vite -> ^8.1.4.
nitro 3's server build (rolldown) externalizes node_modules but must
resolve each external at build time. Under pnpm, server-only deps reached
only via a workspace adapter's nested store dir don't resolve from the
example, so the native/wasm/binary ones are declared as direct deps:
- ts-react-chat: @anthropic-ai/sdk, @elevenlabs/elevenlabs-js, dockerode
- ts-code-mode-web: isolated-vm, quickjs-emscripten(+core) and the four
@jitl/quickjs-wasmfile-* variants
The pure-JS server deps the old nitro-v2 externals list named
(google-auth-library, gaxios, jws, gcp-metadata, google-logging-utils, ws,
node-fetch, openai) are now bundled normally and dropped from ssr.external.
ts-solid-chat stays on vite 7 (Solid JSX has no vite 8/Rolldown support
yet); its nitroV2Plugin was already disabled.
Verified: all five migrated examples build and test:types clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(examples): drop stray route-dir test in ts-react-chat
`src/routes/api.sandbox-triage.test.ts` sat under the router's route
directory, so TanStack Router picked it up as a route candidate and
warned it exports no Route. Delete it; equivalent coverage already lives
at `src/sandbox-triage.test.ts`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(examples): use vite 8 native tsconfig paths, drop vite-tsconfig-paths
Vite 8 resolves tsconfig `paths` natively via `resolve.tsconfigPaths`, so
the `vite-tsconfig-paths` plugin is redundant (and now emits a deprecation
warning). Replace `viteTsConfigPaths({ projects: ['./tsconfig.json'] })`
with `resolve: { tsconfigPaths: true }` and remove the dep in the six
vite-8 projects (ts-react-chat, ts-code-mode-web, ts-react-media,
ts-react-search, testing/panel, testing/e2e).
ts-solid-chat and ts-group-chat stay on vite 7, so they keep the plugin.
Verified: all six build and test:types clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: apply automated fixes
* feat(examples): move ts-solid-chat to vite 8
Both blockers the vite-7 pin cited are resolved upstream: vitejs/vite#22057
(Rolldown JSX scanner) and solidjs/solid-start#2075 (vite 8 compat) are now
closed. The example builds on vite 8 because it applies `vite-plugin-solid`
itself, which handles Solid's JSX transform under Rolldown.
- vite ^7.3.3 -> ^8.1.4; also switch to vite 8 native `resolve.tsconfigPaths`
and drop the vite-tsconfig-paths plugin (merged into the existing resolve).
- @tanstack/ai-solid-ui stays on vite 7: its @tanstack/vite-config library
build runs no Solid JSX transform, and Rolldown's parser rejects raw JSX
("JSX syntax is disabled") where vite 7's esbuild parsed it implicitly.
That needs a build-pipeline fix (or vite-plugin-solid@3 / Solid 2), not a
version bump.
Verified: ts-solid-chat builds and test:types clean on vite 8.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-solid-ui): build on vite 8 via vite-plugin-solid
ai-solid-ui was the last Solid holdout on vite 7. Its @tanstack/vite-config
build had no Solid JSX transform — vite 7's esbuild parsed the `.tsx`
(jsxImportSource: solid-js) implicitly, but vite 8's Rolldown parser rejects
raw JSX ("JSX syntax is disabled").
Fix it in the package's own config (not the shared vite-config patch, which
is framework-agnostic): add vite-plugin-solid, mirroring what
solid-ai-devtools already does on vite 8.
- vite ^7.3.3 -> ^8.1.4; add vite-plugin-solid + `plugins: [solid()]`.
- Test env node -> happy-dom: once solid() transforms the components into the
test graph they import solid-js/web (references `document`), so the suite
needs a DOM. Adds happy-dom devDep.
Verified: build, test:types, test:lib (7/7), and publint all pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: align dep ranges to satisfy sherif (happy-dom, dockerode, isolated-vm)
The vite-8/nitro-3 work pinned three newly-direct deps to their resolved
versions, which mismatched the ranges the workspace already declares and
tripped sherif's multiple-dependency-versions check:
- happy-dom ^20.0.11 -> ^20.0.10 (matches root)
- dockerode ^4.0.12 -> ^4.0.2 (matches @tanstack/ai-sandbox-docker)
- isolated-vm ^6.1.2 -> ^6.0.2 (matches @tanstack/ai-isolate-node)
Each existing range still resolves to the same installed version, so this
is a declared-range alignment only. sherif passes; affected builds + tests
unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: apply automated fixes
* fix: align ts-group-chat nitro version to fix sherif
The ts-group-chat example (added in #962) kept nitro 3.0.1-alpha.2 while all
other examples use 3.0.260610-beta, causing sherif's multiple-dependency-versions
check to fail in CI. Bump it to match.
* fix: resolve oxlint/knip failures from main merge under new toolchain
The merge brought main's resumable-streams work into the oxc/oxlint/knip6 toolchain, surfacing three checks that were green on main's eslint/knip5 setup:
- ai/stream-to-response.ts: oxlint no-control-regex flagged /[\0\r\n]/; replace
with String.includes checks (same behavior, no control-char regex).
- ai-durable-stream: still had eslint lint scripts (added on main); migrate to
oxlint (test:oxlint + lint:fix) and vitest run, matching every other package.
- ai-groq/schema-converter.ts: knip6 flagged a dead transformNullsToUndefined
re-export (imported only to re-export, consumed nowhere); remove it.
* chore: consume @tanstack/vite-config via pkg.pr.new, drop patch + dts override
TanStack/config#403 fixes vite-config for vite-plugin-dts 5 / Vite 8 upstream.
Point @tanstack/vite-config at that PR's pkg.pr.new build (SHA-pinned,
integrity-locked) and remove the local workarounds it obviates:
- delete patches/@tanstack__vite-config@0.5.2.patch + its patchedDependencies entry
- drop the vite-plugin-dts: 5.0.3 override (the fixed config pins dts 5 itself,
and it's ai's only path to vite-plugin-dts)
- type durableStream's read(signal?: AbortSignal) — the fixed config's stricter
dts pass surfaced a latent implicit-any
- CLAUDE.md: correct the TS6-shim tool list (dts is native-TS7 now)
Revert to the released @tanstack/vite-config once #403 lands (tracked in #975).
* chore: use @tanstack/vite-config@0.6.0 (released dts-5/Vite-8 fix)
TanStack/config#403 shipped in 0.6.0. Swap the pkg.pr.new preview for the real
release. Added to minimumReleaseAgeExclude since 0.6.0 is fresh (<24h) and the
build-time devDep is needed now.
* chore: use kiira@0.6.0 (native TS7 engine), drop kiira TS6 shim
kiira 0.6.0 type-checks doc snippets with the project's native TypeScript 7
compiler (engine: auto) and fixes the pnpm external-package install
(ERR_PNPM_IGNORED_BUILDS false failure). It no longer needs the
@typescript/typescript6 shim, so remove the kiira packageExtension. Refresh the
minimumReleaseAge excludes to 0.6.0 (freshly published).
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> bcb1b939 chore: migrate toolchain to oxc + TypeScript 7 + vite 8 (#949)
* chore: migrate toolchain to oxc + TypeScript 7 + vite 8
Replace ESLint/Prettier with the oxc toolchain, upgrade the type
checker to TypeScript 7 (native Go compiler), and move the build stack
to vite 8 (Rolldown). Framework tooling that can't run TS7/vite8 yet is
pinned back via the @typescript/typescript6 shim and per-package vite
pins.
Formatter
- Prettier -> oxfmt (.oxfmtrc.json migrated from prettier config).
oxfmt formats .svelte natively, so prettier + prettier-plugin-svelte
are removed. `format` -> `oxfmt .`.
Linter
- ESLint -> oxlint, untyped + type-aware (oxlint-tsgolint). The custom
`as unknown as` ban is ported via oxlint-plugin-eslint; unused-imports
and @stylistic run through oxlint's JS-plugin layer. ESLint and
@tanstack/eslint-config removed. `test:eslint` -> `test:oxlint`.
TypeScript 7
- Root typescript -> 7.0.2 (tsc is the native Go compiler). types now
defaults to [], so tsconfig.base sets `"types": ["node"]`.
- JS-Compiler-API consumers use the @typescript/typescript6 (6.0.2)
shim: vite-plugin-dts (built-in fallback), @sveltejs/package,
svelte-check, vue-tsc, kiira, knip (packageExtensions);
@analogjs/vite-plugin-angular pinned to 5.9.3. Framework packages
(angular/vue/solid/svelte) stay on 5.9.3.
- Fixes: ai-acp Uint8Array->BufferSource (TS 5.7 typed-array generics);
ai-react/ai-preact rootDir "../.." (TS7 rootDir default + cross-package
test fixtures); one ai-client @ts-expect-error reposition.
Build (vite 8 / Rolldown)
- vite -> 8.1.4, vitest -> 4.1.10. @tanstack/vite-config bumped to 0.5.2
and patched: outDir->outDirs (vite-plugin-dts 5.x) and an added
rollupOptions.preserveModules mirror so multi-entry output holds on
both vite 7 and 8.
- ai-solid-ui stays on vite 7: Solid JSX has no vite 8/Rolldown support
yet (solidjs/solid-start#2075).
Also: nx 22->23, knip 5->6. sherif ignores typescript+vite (intentional
multi-version). CLAUDE.md updated for the new commands/tooling.
* ci: apply automated fixes
* fix(examples): pin example apps to vite 7
The TanStack Start example apps (Start + nitro + framework plugins)
don't build under vite 8 / Rolldown yet — ts-solid-chat and
ts-react-chat fail in the Start server-fn compiler / nitro server
build. Keep the library packages on vite 8 and hold the dev-only
example apps on vite 7 until Start/nitro support vite 8.
* fix: unblock example builds + ai-mcp CI timeout under the new toolchain
- ai/realtime: drop the literal `createServerFn()` from the JSDoc example.
The vite 8/Rolldown build preserves JSDoc in the emitted .js (vite 7
stripped it), and TanStack Start's server-fn plugin string-matches
`createServerFn`, so it tried to compile @tanstack/ai's realtime dist and
resolve the framework runtime (@tanstack/solid-start) from ai's package,
failing ts-solid-chat's build. The example still shows realtimeToken usage.
- ai-mcp: raise vitest testTimeout to 30s. The client/server handshake
tests absorb ~20s+ cold module-import cost on CI, tipping the first test
over the 5s default (flaky timeout).
* chore(examples): upgrade nitro to latest
- nitro (v3): sandbox-web + ts-react-media 3.0.1-alpha.2 -> 3.0.260610-beta;
ts-react-search pinned off the floating `latest` tag to the same version.
Consolidates to a single nitro v3 in the lockfile (drops the stale alpha).
- @tanstack/nitro-v2-vite-plugin: ^1.154.7 -> ^1.155.0 across the Start
examples + testing/panel + testing/e2e.
Verified: all six examples build and testing/panel + testing/e2e typecheck.
Examples stay on vite 7 for now (Start/nitro vite 8 support is a separate
follow-up).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(examples): move React Start examples to nitro 3 + vite 8
Un-pins the React Start examples from vite 7 and standardizes them on
nitro 3 (`nitro/vite`), dropping @tanstack/nitro-v2-vite-plugin.
- sandbox-web, ts-react-media, ts-react-search: already nitro 3 — flipped
vite ^7.3.3 -> ^8.1.4.
- ts-react-chat, ts-code-mode-web: migrated nitroV2Plugin() -> nitro(),
vite -> ^8.1.4.
nitro 3's server build (rolldown) externalizes node_modules but must
resolve each external at build time. Under pnpm, server-only deps reached
only via a workspace adapter's nested store dir don't resolve from the
example, so the native/wasm/binary ones are declared as direct deps:
- ts-react-chat: @anthropic-ai/sdk, @elevenlabs/elevenlabs-js, dockerode
- ts-code-mode-web: isolated-vm, quickjs-emscripten(+core) and the four
@jitl/quickjs-wasmfile-* variants
The pure-JS server deps the old nitro-v2 externals list named
(google-auth-library, gaxios, jws, gcp-metadata, google-logging-utils, ws,
node-fetch, openai) are now bundled normally and dropped from ssr.external.
ts-solid-chat stays on vite 7 (Solid JSX has no vite 8/Rolldown support
yet); its nitroV2Plugin was already disabled.
Verified: all five migrated examples build and test:types clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(examples): drop stray route-dir test in ts-react-chat
`src/routes/api.sandbox-triage.test.ts` sat under the router's route
directory, so TanStack Router picked it up as a route candidate and
warned it exports no Route. Delete it; equivalent coverage already lives
at `src/sandbox-triage.test.ts`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(examples): use vite 8 native tsconfig paths, drop vite-tsconfig-paths
Vite 8 resolves tsconfig `paths` natively via `resolve.tsconfigPaths`, so
the `vite-tsconfig-paths` plugin is redundant (and now emits a deprecation
warning). Replace `viteTsConfigPaths({ projects: ['./tsconfig.json'] })`
with `resolve: { tsconfigPaths: true }` and remove the dep in the six
vite-8 projects (ts-react-chat, ts-code-mode-web, ts-react-media,
ts-react-search, testing/panel, testing/e2e).
ts-solid-chat and ts-group-chat stay on vite 7, so they keep the plugin.
Verified: all six build and test:types clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: apply automated fixes
* feat(examples): move ts-solid-chat to vite 8
Both blockers the vite-7 pin cited are resolved upstream: vitejs/vite#22057
(Rolldown JSX scanner) and solidjs/solid-start#2075 (vite 8 compat) are now
closed. The example builds on vite 8 because it applies `vite-plugin-solid`
itself, which handles Solid's JSX transform under Rolldown.
- vite ^7.3.3 -> ^8.1.4; also switch to vite 8 native `resolve.tsconfigPaths`
and drop the vite-tsconfig-paths plugin (merged into the existing resolve).
- @tanstack/ai-solid-ui stays on vite 7: its @tanstack/vite-config library
build runs no Solid JSX transform, and Rolldown's parser rejects raw JSX
("JSX syntax is disabled") where vite 7's esbuild parsed it implicitly.
That needs a build-pipeline fix (or vite-plugin-solid@3 / Solid 2), not a
version bump.
Verified: ts-solid-chat builds and test:types clean on vite 8.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai-solid-ui): build on vite 8 via vite-plugin-solid
ai-solid-ui was the last Solid holdout on vite 7. Its @tanstack/vite-config
build had no Solid JSX transform — vite 7's esbuild parsed the `.tsx`
(jsxImportSource: solid-js) implicitly, but vite 8's Rolldown parser rejects
raw JSX ("JSX syntax is disabled").
Fix it in the package's own config (not the shared vite-config patch, which
is framework-agnostic): add vite-plugin-solid, mirroring what
solid-ai-devtools already does on vite 8.
- vite ^7.3.3 -> ^8.1.4; add vite-plugin-solid + `plugins: [solid()]`.
- Test env node -> happy-dom: once solid() transforms the components into the
test graph they import solid-js/web (references `document`), so the suite
needs a DOM. Adds happy-dom devDep.
Verified: build, test:types, test:lib (7/7), and publint all pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: align dep ranges to satisfy sherif (happy-dom, dockerode, isolated-vm)
The vite-8/nitro-3 work pinned three newly-direct deps to their resolved
versions, which mismatched the ranges the workspace already declares and
tripped sherif's multiple-dependency-versions check:
- happy-dom ^20.0.11 -> ^20.0.10 (matches root)
- dockerode ^4.0.12 -> ^4.0.2 (matches @tanstack/ai-sandbox-docker)
- isolated-vm ^6.1.2 -> ^6.0.2 (matches @tanstack/ai-isolate-node)
Each existing range still resolves to the same installed version, so this
is a declared-range alignment only. sherif passes; affected builds + tests
unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: apply automated fixes
* fix: align ts-group-chat nitro version to fix sherif
The ts-group-chat example (added in #962) kept nitro 3.0.1-alpha.2 while all
other examples use 3.0.260610-beta, causing sherif's multiple-dependency-versions
check to fail in CI. Bump it to match.
* fix: resolve oxlint/knip failures from main merge under new toolchain
The merge brought main's resumable-streams work into the oxc/oxlint/knip6 toolchain, surfacing three checks that were green on main's eslint/knip5 setup:
- ai/stream-to-response.ts: oxlint no-control-regex flagged /[\0\r\n]/; replace
with String.includes checks (same behavior, no control-char regex).
- ai-durable-stream: still had eslint lint scripts (added on main); migrate to
oxlint (test:oxlint + lint:fix) and vitest run, matching every other package.
- ai-groq/schema-converter.ts: knip6 flagged a dead transformNullsToUndefined
re-export (imported only to re-export, consumed nowhere); remove it.
* chore: consume @tanstack/vite-config via pkg.pr.new, drop patch + dts override
TanStack/config#403 fixes vite-config for vite-plugin-dts 5 / Vite 8 upstream.
Point @tanstack/vite-config at that PR's pkg.pr.new build (SHA-pinned,
integrity-locked) and remove the local workarounds it obviates:
- delete patches/@tanstack__vite-config@0.5.2.patch + its patchedDependencies entry
- drop the vite-plugin-dts: 5.0.3 override (the fixed config pins dts 5 itself,
and it's ai's only path to vite-plugin-dts)
- type durableStream's read(signal?: AbortSignal) — the fixed config's stricter
dts pass surfaced a latent implicit-any
- CLAUDE.md: correct the TS6-shim tool list (dts is native-TS7 now)
Revert to the released @tanstack/vite-config once #403 lands (tracked in #975).
* chore: use @tanstack/vite-config@0.6.0 (released dts-5/Vite-8 fix)
TanStack/config#403 shipped in 0.6.0. Swap the pkg.pr.new preview for the real
release. Added to minimumReleaseAgeExclude since 0.6.0 is fresh (<24h) and the
build-time devDep is needed now.
* chore: use kiira@0.6.0 (native TS7 engine), drop kiira TS6 shim
kiira 0.6.0 type-checks doc snippets with the project's native TypeScript 7
compiler (engine: auto) and fixes the pnpm external-package install
(ERR_PNPM_IGNORED_BUILDS false failure). It no longer needs the
@typescript/typescript6 shim, so remove the kiira packageExtension. Refresh the
minimumReleaseAge excludes to 0.6.0 (freshly published).
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> 7c7aa09a feat: resumable streams — reconnect to in-flight SSE responses via pluggable delivery durability (#955)
* feat: resumable SSE streams via pluggable delivery durability
Add a transport-level StreamDurability seam to toServerSentEventsResponse:
chunks are appended to an ordered log before delivery and each SSE event is
tagged with an opaque adapter-owned id: offset. Reconnects (Last-Event-ID)
and joins (?offset=-1&runId) replay from the log without re-running the
provider. Ships memoryStream (in-core, dev/test) and the new
@tanstack/ai-durable-stream package (Durable Streams protocol adapter).
Client: fetchServerSentEvents now auto-resumes id-tagged streams, de-dupes
replayed prefixes, exposes joinRun(runId), and throws
DurableStreamIncompleteError when a durable run ends with no terminal event
and no forward progress.
Split out of #785 so state persistence and delivery durability land
independently.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6Arc9bWdLq1aRRnDRCLMt
* docs(resumable-streams): Cloudflare Durable Streams backend via service binding
Document running against the Durable Streams Cloudflare Workers + DO
backend: same protocol, no new adapter — inject the service binding's
fetch via the adapter's injectable fetch option, or point server at the
deployed Worker URL. Note the DO alarm satisfies the lease/reaper needed
for producer-death terminalization.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6Arc9bWdLq1aRRnDRCLMt
* docs(durable-stream): Cloudflare service-binding example; make server optional when fetch is provided
The durableStream adapter is a protocol client, so a Cloudflare Workers +
Durable Objects backend that speaks the same protocol needs no new adapter —
just the injected `fetch` seam. Over a service binding the host is irrelevant
(dispatch routes to the bound Worker by path), so `server` is now optional
whenever `fetch` is supplied and defaults to a reserved `.internal` base.
Passing neither `server` nor `fetch` throws loudly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(resumable-streams): bound eviction, reconnection, and surface silent failures
Addresses review findings on the resumable-streams PR:
#1 memoryStream never evicted its process-global log Map (unbounded growth).
Completed logs are now swept after a grace window with a hard LRU cap;
active runs are never evicted. Adds MemoryStreamOptions.
#3 memoryStream join/resume of an unknown or evicted run parked forever. A
concrete resume of a missing log now throws; a from-start join bounds the
wait for the first chunk (firstChunkDeadlineMs) instead of hanging.
#2 The client resumable-SSE reconnect loop and the durableStream read loop
were unbounded and backoff-free. The client now throttles between attempts
and caps the total (StreamReconnectLimitError); durableStream caps
consecutive body-read-failure retries. Normal long-poll advancement is
never throttled. Adds reconnect options to both.
#4 Durability terminal-append / close failures are rethrown to the live
consumer but invisible to a replaying joiner. toServerSentEventsResponse
now accepts `debug` to record the real cause server-side via the library's
logger.
Also: durableStream `server` is optional when `fetch` is provided (service
bindings). Docs + changeset updated; unit tests added for each path (timing-
and eviction-based behavior is covered by unit tests rather than the aimock
e2e harness, which can't exercise it deterministically).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: apply automated fixes
* docs(example): add resumable-streams demo to ts-react-chat
New /resumable route pair: api.resumable.ts (memoryStream-backed POST that
appends+tags each SSE event, plus a GET joinRun replay endpoint) and
resumable.tsx (start a run, then join it by run ID — in a second tab or after a
reload — replaying from the durability log without re-running the model). Nav
link added to Header. Kept the shared api.tanchat route untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(resumable-streams): note other durability backends (Electric, etc.)
Clarify that durableStream works with any Durable Streams protocol server, and
that other systems (a Postgres-backed log via Electric, Redis streams, a queue)
can back durability by implementing the four-method StreamDurability interface.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai, ai-client): resumable NDJSON + XHR delivery durability
Extend resumable streams (delivery durability) beyond SSE to NDJSON and
the XHR transports.
Server (@tanstack/ai):
- toHttpStream gains an optional getId; when present each NDJSON line is
emitted as an { id, chunk } envelope (NDJSON has no native event id).
Untagged streams stay bare lines, byte-identical to before.
- toHttpResponse gains durability/batch/debug, reusing the same
durableStreamSource as toServerSentEventsResponse.
Client (@tanstack/ai-client):
- Generalize the SSE-only reconnect loop into transport-agnostic
resumableStream(openEventSource, signal, reconnect). Shared line parsers
(linesToSSEEvents/linesToNdjsonEvents) feed fetch (fetchEventSource) and
XHR (xhrEventSource) thunks.
- fetchHttpStream, xhrServerSentEvents, xhrHttpStream are now resumable and
expose joinRun. XHR onerror surfaces StreamReadError so a durable XHR run
can reconnect; StreamReadError message is now transport-neutral.
Tests: NDJSON server durability suite, NDJSON+XHR resumable-transport client
suite, NDJSON arm on the delivery-durability e2e harness + spec.
Docs/skill/changeset updated for NDJSON + XHR.
* fix(ai, ai-client): CR round 1 — durability logging, memory-log leak, CRLF, [DONE] id parity
Round 1 review fixes (7-agent CR):
- HIGH: toServerSentEventsResponse / toHttpResponse constructed the durability
logger only when debug was passed, so terminal-append/close failures were
silently swallowed by default (fully lost on the client-disconnect path).
Now instantiate resolveDebugOption(debug) unconditionally, matching every
other activity (errors category is on by default).
- HIGH: memoryStream.read() called getOrCreateLog before the unknown-run
check, leaving a permanent empty log per unknown/evicted resume — unbounded
growth defeating the eviction logic. Peek with memoryLogs.get; a concrete
offset for an absent run throws without inserting; a from-start join creates
the log for the produce race but deletes it on the first-chunk deadline.
- readStreamLines (fetch) now strips a trailing CR, matching readXhrLines, so
CRLF SSE servers do not miss the [DONE] sentinel.
- Fetch SSE [DONE] synthesis now threads the run's ids (parity with the XHR
xhrSSEParser), so a [DONE]-terminating server that omits ids still yields a
correlated terminal.
- Clarified the ReconnectOptions.maxAttempts comment (counts total lifetime
reconnects, not consecutive no-progress ones).
- Softened the changeset claim: completion terminalizes when the source emits
its own terminal event.
- SKILL.md anti-pattern examples: gpt-4o -> gpt-5.5.
- Tests: fetch NDJSON reconnect test uses reconnect delayMs:0; parseNdjsonEvents
helper mirrors the production !('type' in value) envelope guard.
Call sites cleared:
- responseToSSEEvents: added optional 3rd param fallbackIds; all existing
callers (responseToSSEChunks, fetch connect/joinRun) pass <=3 args, backward
compatible.
- readStreamLines / memoryStream.read: signatures unchanged; behavior-preserving
except the removed phantom insertion (observable throw/reject paths unchanged,
already covered by stream-durability.test.ts).
* fix(ai-client): CR round 2 — reconnect resilience + joinRun id parity
Round 2 confirmation-round fixes:
- resumableStream: a transport drop (StreamTruncatedError/StreamReadError) now
retries whenever an offset is held, even if THAT attempt made no new progress
(a caught-up run whose parked long-poll socket drops, or a proxy that drops
right after replaying the de-duped overlap). The total-attempts ceiling still
bounds a genuine flapper; the per-attempt progress requirement only converted
recoverable drops into hard failures on flaky networks. The clean-end path
stays strict and now documents the invariant it relies on (a durable transport
must not surface an empty long-poll window as a clean end; both shipped
backends honor it).
- xhrServerSentEvents.joinRun now threads { runId } into the [DONE] fallback,
matching fetchServerSentEvents.joinRun (correlation parity).
- e2e parseNdjson + toHttpResponse @param prose aligned (envelope guard;
batch is nested under durability, debug documented).
- docs: durable sources must emit their own terminal; memoryStream is for
replaying completed runs (live mid-stream resume needs a backend whose
producer outlives the delivery socket); qualified producer-death headline as
backend-driven.
Covering test: a reconnect that replays only the de-duped overlap then drops is
retried, not surfaced as an error (connection-adapters-resumable.test.ts).
Call sites cleared: resumableStream catch condition — only relaxed the retry
guard (dropped '&& progressed'), kept StreamReadError/StreamTruncatedError type
gate + lastEventId gate, so a first-attempt failure with no offset still
rethrows (asserted by existing 'does not retry HTTP setup failures' test).
* fix(ai, ai-client): CR round 3 — producer robustness, fetch retry, NDJSON headers, docs
Round 3 confirmation-round fixes (scope widened per request to cover the
pre-existing durability-producer bugs).
Producer (durableStreamSource):
- Flush buffered-but-unflushed chunks to the log before terminalizing on the
abort/disconnect path (previously up to batchSize-1 already-produced chunks
were dropped, so a joiner replayed a truncated prefix). Matches the error path.
- Prefer the real provider error over a generic AbortError when a run both
fails and is aborted, so a joiner sees the true cause.
- Do not rethrow a post-terminal close()/append failure to the live consumer
once a terminal was already forwarded — rethrowing appended a contradictory
RUN_ERROR after RUN_FINISHED on the wire. Late cleanup failures are recorded
server-side via logger.errors instead.
- validateOffset now also rejects offsets with surrounding whitespace (the SSE
client .trim()s the id, so such an offset would not round-trip on reconnect).
Client:
- normalizeConnectionAdapter.send: guard terminal synthesis in the catch so a
missing-id throw can't mask the original error.
- fetchEventSource: wrap a fetch() rejection (offline/DNS/refused) as
StreamReadError so a reconnect retries from the offset, matching XHR; a
first-attempt failure with no offset still surfaces.
- readStreamLines: final decoder.decode() flush so a cut mid-multibyte-char is
reported as truncation.
Server transport:
- toHttpResponse defaults Content-Type to application/x-ndjson + no-cache
(overridable), matching the SSE helper, so intermediaries don't buffer it.
Docs/skill/harness:
- JSDoc @examples use openaiText('gpt-5.5') (chat has no model field) and wrap
durability examples in a POST handler; model ids normalized to gpt-5.5 across
connection-adapters.md + SKILL.md; SKILL sources += resumable-streams; doc
reconnection wording scoped to the clean-end path; e2e X-Run-Id no longer
advertised on reconnect; harness + seen-set comments.
Tests: flush-on-abort, double-terminal-suppression, whitespace-offset rejection,
NDJSON Content-Type, and fetch-rejection retry (+ first-attempt surfacing).
Deferred (low, out of delta subject): fetch body not cancelled on early terminal
return (reverted — the reader-cancel broke mock-reader teardown in ~26
pre-existing tests; XHR-parity nit, durable backends close on terminal anyway).
* fix(ai-client, docs): CR round 4 — doc-accuracy, comment precision, coverage
Round 4 confirmation-round fixes (docs/comments/tests + one defensive guard;
no new production logic bugs were found this round).
Docs (correcting inaccuracies introduced in earlier CR rounds):
- Reconnection-bounding section now states the durable-vs-non-durable
distinction accurately: a transport error retries while an offset is held; a
durable clean end with no progress fails with DurableStreamIncompleteError;
only a non-durable clean end is a completed run. Documents why the asymmetry
is deliberate.
- connection-adapters.md no longer groups xhrServerSentEvents (SSE) under the
NDJSON/toHttpResponse sentence.
- Added a reconnect-safety warning: the client auto-reconnects by re-POSTing, so
non-idempotent POST-handler work must be guarded behind a resume check.
- config.json: dropped the redundant updatedAt on the newly-added overview page.
- changeset: reconnect option applies to all four HTTP adapters, not just
fetchServerSentEvents.
Code:
- readXhrLines.finish() now guards status===0 like enqueueDelta (avoids a bogus
'status: 0' error if loadend fires before load/error/abort).
- Comments: linesToSSEEvents one-id-per-data-event assumption; clarified the
fetch-rejection wrap note.
Tests:
- XHR onerror→reconnect (proves StreamReadError from onerror drives a retry with
Last-Event-ID) and NDJSON provider-throw terminal persistence — closing the
highest-value coverage gaps on the XHR/NDJSON surface.
- delayMs:0 on the reconnecting fetch-SSE tests (speed/consistency).
Deferred (pre-existing / by-design / out-of-delta-subject): reconnect clean-end
asymmetry (correct + documented), abortableIterable listener cleanup, fetch body
cancel on early exit (reverted — broke mock-reader teardown), pump finally-throw
surfacing, SSE persistent-id interop.
* fix(ai, ai-client, docs): CR round 5 — self-inflicted doc/comment staleness + one silent-swallow
Round 5 confirmation-round fixes. No genuine code-logic defects surfaced this
round; the items below are (a) one silent failure introduced by the R3
double-terminal guard and (b) doc/comment staleness introduced by earlier
rounds, plus a pre-existing doc-example hang bug.
Code:
- durableStreamSource: a producer error thrown AFTER a terminal was forwarded
was suppressed by the !terminalForwarded rethrow guard (correct — avoids a
contradictory second terminal) but never logged, so it vanished. Now logged
via logger.errors like the close/terminal-append failures. Covering test added.
Docs/comments (correcting staleness from earlier rounds):
- debug JSDoc (both response helpers) + overview.md prose no longer imply
server-side logging requires ; the errors category is on by default
(R1 change), and debug only routes/raises verbosity.
- toServerSentEventsStream JSDoc documents its getId param (parity w/ toHttpStream).
- overview.md GET join example guards a missing offset and its comment no longer
over-claims 'never iterates the provider' for a bodyless produce path.
- Removed review-artifact comments ('Finding 6', 'the R1 comment claims').
Docs (pre-existing example bug, flagged twice):
- WebSocket subscribe() example drains the queue before honoring (a
burst + close in one macrotask previously dropped queued chunks, incl. a
trailing RUN_FINISHED → client hang) and registers the abort listener once.
Deferred to a follow-up (pre-existing / off NDJSON-XHR subject / documented
design): SSE heartbeat/empty-data frame tolerance; abortableIterable orphan-
promise .catch; joinRun offset=-1 + Last-Event-ID precedence; reconnect
lifetime-ceiling on healthy socket-per-event runs; fetch body-cancel on early
terminal; assorted test-hygiene (shared FakeXhr, delayMs).
* feat(ai-client, docs): reconnect ceiling = consecutive-no-progress (default 5); custom-adapter guide
Addressing review feedback:
- Reconnect ceiling: maxAttempts now bounds CONSECUTIVE reconnects that deliver
no new events (default lowered 1000 -> 5); forward progress resets the counter.
A healthy long run (even a socket-per-event proxy) never approaches it; it
fires only when the run is genuinely stuck. This also resolves the CR finding
that the old total-lifetime ceiling could fail a healthy progressing run.
Ceiling test split into a no-progress-flapper (hits it) + a progress-resets
test (does not).
- stream-to-response: terminalForwarded lint fix (scoped no-unnecessary-condition
disable; the flag is only assigned inside the flush() closure that TS CFA
cannot observe).
Docs:
- New guide docs/resumable-streams/custom-adapter.md: implement the four-method
StreamDurability contract over your own store, the offset/park/terminalize
rules, wiring, and offset branding. Registered in config.json, cross-linked
from the overview.
- chat/connection-adapters: show the GET handler (joinRun) alongside POST.
- overview reconnection-bounding section updated to the new semantics + default.
NOTE: did NOT make memoryStream a silent default (explored per request, then
reverted on review) - durability stays opt-in to avoid shipping an in-process,
single-process-only, per-run-buffering backend to production by default.
advertiseRunId is a local var in the e2e harness route, not public API.
* fix(ai-client, ai-durable-stream): address CodeRabbit review feedback
- SSE id parsing: preserve the opaque offset verbatim (strip only a single
leading space per the SSE spec, no trim, which would mangle a valid offset),
and treat an empty id: as a resume-cursor reset (drop lastEventId + clear the
de-dupe set) rather than a durable empty offset.
- resolveReconnectOptions: reject non-finite / negative maxAttempts and delayMs
up front so a NaN/Infinity ceiling cannot cause unbounded reconnects.
- durable-stream read: throw on non-strictly-increasing record sequences within
a response instead of silently dropping later records.
- durable-stream: new operationTimeoutMs (default 30000) bounds create/append/
close via an AbortSignal so a stalled backend cannot hang delivery or
terminalization; long-poll reads are intentionally excluded.
- e2e delivery-durability spec: document the aimock-policy exemption.
Tests added: empty-id reset + invalid-reconnect-bounds (ai-client), non-monotonic
seq rejection + operation timeout (ai-durable-stream).
Not applied (verified against the code): peer-dep workspace:^ is consistent with
all sibling packages (changing to * would break sherif); memory retention is
already bounded (sweepMemoryLogs + TTL + cap); the throw-in-finally is
intentional aggregation and ESLint-suppressed (repo does not use Biome); batch
is already documented as nested; tests already follow the package tests/ dir
convention.
* ci: apply automated fixes
* docs(resumable-streams): simplify overview to the happy path, split advanced out
- overview.md: rewritten as the 3-step common case (pick an adapter, wrap the
response with POST+GET, client is automatic). Removed em dashes. No longer
makes it look harder than it is.
- advanced.md (new): moved the deep material here — durableStream options,
joinRun (attach-by-id), completion/stop/errors, memoryStream-in-production,
reconnection bounding, offset ownership, Cloudflare, process death, and
delivery-is-not-state.
- joinRun is now documented under Advanced (it is a manual, opt-in API; the
common reconnect-on-drop path needs no client code).
- Scrubbed em dashes from custom-adapter.md and the resumable sections I added
to connection-adapters.md; fixed the custom-adapter process-death link to
point at the advanced page.
- config.json: registered the Advanced page.
* docs(resumable-streams): drop dead chat() placeholder from GET replay handler
The resume path serves entirely from the durability log and never iterates
the source stream, so the chat() call and its replay: threadId were dead
code. Replace with an empty stream and guard that offset is present so a bare
GET does not fall through to the produce path.
* feat(ai): add resumeServerSentEventsResponse / resumeHttpResponse helpers
A resume GET is served entirely from the durability log and never iterates a
producer stream, so the response helpers previously forced callers to fabricate
an empty stream in every GET handler. These helpers take just the durability
adapter, do the replay, and return a 400 when the request carries no resume
offset. Dogfood them in the e2e harness and the example app, and simplify the
docs GET handler to a one-liner.
* ci: apply automated fixes
* docs(chat): use resumeServerSentEventsResponse in the connection-adapters GET example
The resumable-SSE server example still constructed a dead chat() with a
replay: threadId in its GET handler. Replace it with the resume helper.
* refactor(example): resumable demo uses useChat; docs show the reconnect side-effect guard
Rewrite the ts-react-chat resumable route from hand-rolled connection driving
(useState/useRef/drainInto + manual connect/joinRun) to a plain useChat page,
matching the overview doc: the durable route makes reconnect automatic with no
client code. Expand the overview reconnect gotcha into a runnable example that
guards one-time side effects behind durability.resumeFrom().
* ci: apply automated fixes
* fix(ai-client): don't truncate agentic runs at the first RUN_FINISHED
The resumable stream engine returned on the first RUN_FINISHED/RUN_ERROR.
An agent loop emits one RUN_STARTED/RUN_FINISHED pair per turn, so a
tool-calling run carries several terminals in a single response — returning
on the first dropped the tool result and the final answer. This engine drives
every stream (durable and non-durable alike), so it regressed existing
non-durable clients: all tool/agentic/custom-event/structured E2E tests hung
after the first turn while plain single-turn streams passed.
Drain the event source to its natural end (the server closes the response only
when the run is truly complete) and use the terminal flag post-loop to decide
done-vs-reconnect. Restores the pre-durability read-to-close behavior for
non-durable streams; durable behavior is unchanged (single-terminal responses
end right after the terminal, so every resumable unit test still holds).
* fix(ai-client): send durability run id as X-Run-Id header, not a query param
The resumable adapters appended `?runId=<id>` to every POST (useChat always
supplies a runId), rewriting the request URL for all existing clients — not
just durable ones. That broke callers/tests that match the bare endpoint URL
and violated the invariant that a non-durable request is byte-identical to a
plain fetch.
Send the client-chosen run id in an `X-Run-Id` request header instead. The
POST URL is now untouched, so existing clients are unaffected, while a
durability sink still keys its log by the client's run id (memoryStream's
readRunId reads the header first, then falls back to the `?runId` query the
GET join path still uses). Durability remains a transparent add-on.
* ci: apply automated fixes
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Alem Tuzlak <t.zlak@hotmail.com> 7c7aa09a feat: resumable streams — reconnect to in-flight SSE responses via pluggable delivery durability (#955)
* feat: resumable SSE streams via pluggable delivery durability
Add a transport-level StreamDurability seam to toServerSentEventsResponse:
chunks are appended to an ordered log before delivery and each SSE event is
tagged with an opaque adapter-owned id: offset. Reconnects (Last-Event-ID)
and joins (?offset=-1&runId) replay from the log without re-running the
provider. Ships memoryStream (in-core, dev/test) and the new
@tanstack/ai-durable-stream package (Durable Streams protocol adapter).
Client: fetchServerSentEvents now auto-resumes id-tagged streams, de-dupes
replayed prefixes, exposes joinRun(runId), and throws
DurableStreamIncompleteError when a durable run ends with no terminal event
and no forward progress.
Split out of #785 so state persistence and delivery durability land
independently.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6Arc9bWdLq1aRRnDRCLMt
* docs(resumable-streams): Cloudflare Durable Streams backend via service binding
Document running against the Durable Streams Cloudflare Workers + DO
backend: same protocol, no new adapter — inject the service binding's
fetch via the adapter's injectable fetch option, or point server at the
deployed Worker URL. Note the DO alarm satisfies the lease/reaper needed
for producer-death terminalization.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A6Arc9bWdLq1aRRnDRCLMt
* docs(durable-stream): Cloudflare service-binding example; make server optional when fetch is provided
The durableStream adapter is a protocol client, so a Cloudflare Workers +
Durable Objects backend that speaks the same protocol needs no new adapter —
just the injected `fetch` seam. Over a service binding the host is irrelevant
(dispatch routes to the bound Worker by path), so `server` is now optional
whenever `fetch` is supplied and defaults to a reserved `.internal` base.
Passing neither `server` nor `fetch` throws loudly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(resumable-streams): bound eviction, reconnection, and surface silent failures
Addresses review findings on the resumable-streams PR:
#1 memoryStream never evicted its process-global log Map (unbounded growth).
Completed logs are now swept after a grace window with a hard LRU cap;
active runs are never evicted. Adds MemoryStreamOptions.
#3 memoryStream join/resume of an unknown or evicted run parked forever. A
concrete resume of a missing log now throws; a from-start join bounds the
wait for the first chunk (firstChunkDeadlineMs) instead of hanging.
#2 The client resumable-SSE reconnect loop and the durableStream read loop
were unbounded and backoff-free. The client now throttles between attempts
and caps the total (StreamReconnectLimitError); durableStream caps
consecutive body-read-failure retries. Normal long-poll advancement is
never throttled. Adds reconnect options to both.
#4 Durability terminal-append / close failures are rethrown to the live
consumer but invisible to a replaying joiner. toServerSentEventsResponse
now accepts `debug` to record the real cause server-side via the library's
logger.
Also: durableStream `server` is optional when `fetch` is provided (service
bindings). Docs + changeset updated; unit tests added for each path (timing-
and eviction-based behavior is covered by unit tests rather than the aimock
e2e harness, which can't exercise it deterministically).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: apply automated fixes
* docs(example): add resumable-streams demo to ts-react-chat
New /resumable route pair: api.resumable.ts (memoryStream-backed POST that
appends+tags each SSE event, plus a GET joinRun replay endpoint) and
resumable.tsx (start a run, then join it by run ID — in a second tab or after a
reload — replaying from the durability log without re-running the model). Nav
link added to Header. Kept the shared api.tanchat route untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(resumable-streams): note other durability backends (Electric, etc.)
Clarify that durableStream works with any Durable Streams protocol server, and
that other systems (a Postgres-backed log via Electric, Redis streams, a queue)
can back durability by implementing the four-method StreamDurability interface.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ai, ai-client): resumable NDJSON + XHR delivery durability
Extend resumable streams (delivery durability) beyond SSE to NDJSON and
the XHR transports.
Server (@tanstack/ai):
- toHttpStream gains an optional getId; when present each NDJSON line is
emitted as an { id, chunk } envelope (NDJSON has no native event id).
Untagged streams stay bare lines, byte-identical to before.
- toHttpResponse gains durability/batch/debug, reusing the same
durableStreamSource as toServerSentEventsResponse.
Client (@tanstack/ai-client):
- Generalize the SSE-only reconnect loop into transport-agnostic
resumableStream(openEventSource, signal, reconnect). Shared line parsers
(linesToSSEEvents/linesToNdjsonEvents) feed fetch (fetchEventSource) and
XHR (xhrEventSource) thunks.
- fetchHttpStream, xhrServerSentEvents, xhrHttpStream are now resumable and
expose joinRun. XHR onerror surfaces StreamReadError so a durable XHR run
can reconnect; StreamReadError message is now transport-neutral.
Tests: NDJSON server durability suite, NDJSON+XHR resumable-transport client
suite, NDJSON arm on the delivery-durability e2e harness + spec.
Docs/skill/changeset updated for NDJSON + XHR.
* fix(ai, ai-client): CR round 1 — durability logging, memory-log leak, CRLF, [DONE] id parity
Round 1 review fixes (7-agent CR):
- HIGH: toServerSentEventsResponse / toHttpResponse constructed the durability
logger only when debug was passed, so terminal-append/close failures were
silently swallowed by default (fully lost on the client-disconnect path).
Now instantiate resolveDebugOption(debug) unconditionally, matching every
other activity (errors category is on by default).
- HIGH: memoryStream.read() called getOrCreateLog before the unknown-run
check, leaving a permanent empty log per unknown/evicted resume — unbounded
growth defeating the eviction logic. Peek with memoryLogs.get; a concrete
offset for an absent run throws without inserting; a from-start join creates
the log for the produce race but deletes it on the first-chunk deadline.
- readStreamLines (fetch) now strips a trailing CR, matching readXhrLines, so
CRLF SSE servers do not miss the [DONE] sentinel.
- Fetch SSE [DONE] synthesis now threads the run's ids (parity with the XHR
xhrSSEParser), so a [DONE]-terminating server that omits ids still yields a
correlated terminal.
- Clarified the ReconnectOptions.maxAttempts comment (counts total lifetime
reconnects, not consecutive no-progress ones).
- Softened the changeset claim: completion terminalizes when the source emits
its own terminal event.
- SKILL.md anti-pattern examples: gpt-4o -> gpt-5.5.
- Tests: fetch NDJSON reconnect test uses reconnect delayMs:0; parseNdjsonEvents
helper mirrors the production !('type' in value) envelope guard.
Call sites cleared:
- responseToSSEEvents: added optional 3rd param fallbackIds; all existing
callers (responseToSSEChunks, fetch connect/joinRun) pass <=3 args, backward
compatible.
- readStreamLines / memoryStream.read: signatures unchanged; behavior-preserving
except the removed phantom insertion (observable throw/reject paths unchanged,
already covered by stream-durability.test.ts).
* fix(ai-client): CR round 2 — reconnect resilience + joinRun id parity
Round 2 confirmation-round fixes:
- resumableStream: a transport drop (StreamTruncatedError/StreamReadError) now
retries whenever an offset is held, even if THAT attempt made no new progress
(a caught-up run whose parked long-poll socket drops, or a proxy that drops
right after replaying the de-duped overlap). The total-attempts ceiling still
bounds a genuine flapper; the per-attempt progress requirement only converted
recoverable drops into hard failures on flaky networks. The clean-end path
stays strict and now documents the invariant it relies on (a durable transport
must not surface an empty long-poll window as a clean end; both shipped
backends honor it).
- xhrServerSentEvents.joinRun now threads { runId } into the [DONE] fallback,
matching fetchServerSentEvents.joinRun (correlation parity).
- e2e parseNdjson + toHttpResponse @param prose aligned (envelope guard;
batch is nested under durability, debug documented).
- docs: durable sources must emit their own terminal; memoryStream is for
replaying completed runs (live mid-stream resume needs a backend whose
producer outlives the delivery socket); qualified producer-death headline as
backend-driven.
Covering test: a reconnect that replays only the de-duped overlap then drops is
retried, not surfaced as an error (connection-adapters-resumable.test.ts).
Call sites cleared: resumableStream catch condition — only relaxed the retry
guard (dropped '&& progressed'), kept StreamReadError/StreamTruncatedError type
gate + lastEventId gate, so a first-attempt failure with no offset still
rethrows (asserted by existing 'does not retry HTTP setup failures' test).
* fix(ai, ai-client): CR round 3 — producer robustness, fetch retry, NDJSON headers, docs
Round 3 confirmation-round fixes (scope widened per request to cover the
pre-existing durability-producer bugs).
Producer (durableStreamSource):
- Flush buffered-but-unflushed chunks to the log before terminalizing on the
abort/disconnect path (previously up to batchSize-1 already-produced chunks
were dropped, so a joiner replayed a truncated prefix). Matches the error path.
- Prefer the real provider error over a generic AbortError when a run both
fails and is aborted, so a joiner sees the true cause.
- Do not rethrow a post-terminal close()/append failure to the live consumer
once a terminal was already forwarded — rethrowing appended a contradictory
RUN_ERROR after RUN_FINISHED on the wire. Late cleanup failures are recorded
server-side via logger.errors instead.
- validateOffset now also rejects offsets with surrounding whitespace (the SSE
client .trim()s the id, so such an offset would not round-trip on reconnect).
Client:
- normalizeConnectionAdapter.send: guard terminal synthesis in the catch so a
missing-id throw can't mask the original error.
- fetchEventSource: wrap a fetch() rejection (offline/DNS/refused) as
StreamReadError so a reconnect retries from the offset, matching XHR; a
first-attempt failure with no offset still surfaces.
- readStreamLines: final decoder.decode() flush so a cut mid-multibyte-char is
reported as truncation.
Server transport:
- toHttpResponse defaults Content-Type to application/x-ndjson + no-cache
(overridable), matching the SSE helper, so intermediaries don't buffer it.
Docs/skill/harness:
- JSDoc @examples use openaiText('gpt-5.5') (chat has no model field) and wrap
durability examples in a POST handler; model ids normalized to gpt-5.5 across
connection-adapters.md + SKILL.md; SKILL sources += resumable-streams; doc
reconnection wording scoped to the clean-end path; e2e X-Run-Id no longer
advertised on reconnect; harness + seen-set comments.
Tests: flush-on-abort, double-terminal-suppression, whitespace-offset rejection,
NDJSON Content-Type, and fetch-rejection retry (+ first-attempt surfacing).
Deferred (low, out of delta subject): fetch body not cancelled on early terminal
return (reverted — the reader-cancel broke mock-reader teardown in ~26
pre-existing tests; XHR-parity nit, durable backends close on terminal anyway).
* fix(ai-client, docs): CR round 4 — doc-accuracy, comment precision, coverage
Round 4 confirmation-round fixes (docs/comments/tests + one defensive guard;
no new production logic bugs were found this round).
Docs (correcting inaccuracies introduced in earlier CR rounds):
- Reconnection-bounding section now states the durable-vs-non-durable
distinction accurately: a transport error retries while an offset is held; a
durable clean end with no progress fails with DurableStreamIncompleteError;
only a non-durable clean end is a completed run. Documents why the asymmetry
is deliberate.
- connection-adapters.md no longer groups xhrServerSentEvents (SSE) under the
NDJSON/toHttpResponse sentence.
- Added a reconnect-safety warning: the client auto-reconnects by re-POSTing, so
non-idempotent POST-handler work must be guarded behind a resume check.
- config.json: dropped the redundant updatedAt on the newly-added overview page.
- changeset: reconnect option applies to all four HTTP adapters, not just
fetchServerSentEvents.
Code:
- readXhrLines.finish() now guards status===0 like enqueueDelta (avoids a bogus
'status: 0' error if loadend fires before load/error/abort).
- Comments: linesToSSEEvents one-id-per-data-event assumption; clarified the
fetch-rejection wrap note.
Tests:
- XHR onerror→reconnect (proves StreamReadError from onerror drives a retry with
Last-Event-ID) and NDJSON provider-throw terminal persistence — closing the
highest-value coverage gaps on the XHR/NDJSON surface.
- delayMs:0 on the reconnecting fetch-SSE tests (speed/consistency).
Deferred (pre-existing / by-design / out-of-delta-subject): reconnect clean-end
asymmetry (correct + documented), abortableIterable listener cleanup, fetch body
cancel on early exit (reverted — broke mock-reader teardown), pump finally-throw
surfacing, SSE persistent-id interop.
* fix(ai, ai-client, docs): CR round 5 — self-inflicted doc/comment staleness + one silent-swallow
Round 5 confirmation-round fixes. No genuine code-logic defects surfaced this
round; the items below are (a) one silent failure introduced by the R3
double-terminal guard and (b) doc/comment staleness introduced by earlier
rounds, plus a pre-existing doc-example hang bug.
Code:
- durableStreamSource: a producer error thrown AFTER a terminal was forwarded
was suppressed by the !terminalForwarded rethrow guard (correct — avoids a
contradictory second terminal) but never logged, so it vanished. Now logged
via logger.errors like the close/terminal-append failures. Covering test added.
Docs/comments (correcting staleness from earlier rounds):
- debug JSDoc (both response helpers) + overview.md prose no longer imply
server-side logging requires ; the errors category is on by default
(R1 change), and debug only routes/raises verbosity.
- toServerSentEventsStream JSDoc documents its getId param (parity w/ toHttpStream).
- overview.md GET join example guards a missing offset and its comment no longer
over-claims 'never iterates the provider' for a bodyless produce path.
- Removed review-artifact comments ('Finding 6', 'the R1 comment claims').
Docs (pre-existing example bug, flagged twice):
- WebSocket subscribe() example drains the queue before honoring (a
burst + close in one macrotask previously dropped queued chunks, incl. a
trailing RUN_FINISHED → client hang) and registers the abort listener once.
Deferred to a follow-up (pre-existing / off NDJSON-XHR subject / documented
design): SSE heartbeat/empty-data frame tolerance; abortableIterable orphan-
promise .catch; joinRun offset=-1 + Last-Event-ID precedence; reconnect
lifetime-ceiling on healthy socket-per-event runs; fetch body-cancel on early
terminal; assorted test-hygiene (shared FakeXhr, delayMs).
* feat(ai-client, docs): reconnect ceiling = consecutive-no-progress (default 5); custom-adapter guide
Addressing review feedback:
- Reconnect ceiling: maxAttempts now bounds CONSECUTIVE reconnects that deliver
no new events (default lowered 1000 -> 5); forward progress resets the counter.
A healthy long run (even a socket-per-event proxy) never approaches it; it
fires only when the run is genuinely stuck. This also resolves the CR finding
that the old total-lifetime ceiling could fail a healthy progressing run.
Ceiling test split into a no-progress-flapper (hits it) + a progress-resets
test (does not).
- stream-to-response: terminalForwarded lint fix (scoped no-unnecessary-condition
disable; the flag is only assigned inside the flush() closure that TS CFA
cannot observe).
Docs:
- New guide docs/resumable-streams/custom-adapter.md: implement the four-method
StreamDurability contract over your own store, the offset/park/terminalize
rules, wiring, and offset branding. Registered in config.json, cross-linked
from the overview.
- chat/connection-adapters: show the GET handler (joinRun) alongside POST.
- overview reconnection-bounding section updated to the new semantics + default.
NOTE: did NOT make memoryStream a silent default (explored per request, then
reverted on review) - durability stays opt-in to avoid shipping an in-process,
single-process-only, per-run-buffering backend to production by default.
advertiseRunId is a local var in the e2e harness route, not public API.
* fix(ai-client, ai-durable-stream): address CodeRabbit review feedback
- SSE id parsing: preserve the opaque offset verbatim (strip only a single
leading space per the SSE spec, no trim, which would mangle a valid offset),
and treat an empty id: as a resume-cursor reset (drop lastEventId + clear the
de-dupe set) rather than a durable empty offset.
- resolveReconnectOptions: reject non-finite / negative maxAttempts and delayMs
up front so a NaN/Infinity ceiling cannot cause unbounded reconnects.
- durable-stream read: throw on non-strictly-increasing record sequences within
a response instead of silently dropping later records.
- durable-stream: new operationTimeoutMs (default 30000) bounds create/append/
close via an AbortSignal so a stalled backend cannot hang delivery or
terminalization; long-poll reads are intentionally excluded.
- e2e delivery-durability spec: document the aimock-policy exemption.
Tests added: empty-id reset + invalid-reconnect-bounds (ai-client), non-monotonic
seq rejection + operation timeout (ai-durable-stream).
Not applied (verified against the code): peer-dep workspace:^ is consistent with
all sibling packages (changing to * would break sherif); memory retention is
already bounded (sweepMemoryLogs + TTL + cap); the throw-in-finally is
intentional aggregation and ESLint-suppressed (repo does not use Biome); batch
is already documented as nested; tests already follow the package tests/ dir
convention.
* ci: apply automated fixes
* docs(resumable-streams): simplify overview to the happy path, split advanced out
- overview.md: rewritten as the 3-step common case (pick an adapter, wrap the
response with POST+GET, client is automatic). Removed em dashes. No longer
makes it look harder than it is.
- advanced.md (new): moved the deep material here — durableStream options,
joinRun (attach-by-id), completion/stop/errors, memoryStream-in-production,
reconnection bounding, offset ownership, Cloudflare, process death, and
delivery-is-not-state.
- joinRun is now documented under Advanced (it is a manual, opt-in API; the
common reconnect-on-drop path needs no client code).
- Scrubbed em dashes from custom-adapter.md and the resumable sections I added
to connection-adapters.md; fixed the custom-adapter process-death link to
point at the advanced page.
- config.json: registered the Advanced page.
* docs(resumable-streams): drop dead chat() placeholder from GET replay handler
The resume path serves entirely from the durability log and never iterates
the source stream, so the chat() call and its replay: threadId were dead
code. Replace with an empty stream and guard that offset is present so a bare
GET does not fall through to the produce path.
* feat(ai): add resumeServerSentEventsResponse / resumeHttpResponse helpers
A resume GET is served entirely from the durability log and never iterates a
producer stream, so the response helpers previously forced callers to fabricate
an empty stream in every GET handler. These helpers take just the durability
adapter, do the replay, and return a 400 when the request carries no resume
offset. Dogfood them in the e2e harness and the example app, and simplify the
docs GET handler to a one-liner.
* ci: apply automated fixes
* docs(chat): use resumeServerSentEventsResponse in the connection-adapters GET example
The resumable-SSE server example still constructed a dead chat() with a
replay: threadId in its GET handler. Replace it with the resume helper.
* refactor(example): resumable demo uses useChat; docs show the reconnect side-effect guard
Rewrite the ts-react-chat resumable route from hand-rolled connection driving
(useState/useRef/drainInto + manual connect/joinRun) to a plain useChat page,
matching the overview doc: the durable route makes reconnect automatic with no
client code. Expand the overview reconnect gotcha into a runnable example that
guards one-time side effects behind durability.resumeFrom().
* ci: apply automated fixes
* fix(ai-client): don't truncate agentic runs at the first RUN_FINISHED
The resumable stream engine returned on the first RUN_FINISHED/RUN_ERROR.
An agent loop emits one RUN_STARTED/RUN_FINISHED pair per turn, so a
tool-calling run carries several terminals in a single response — returning
on the first dropped the tool result and the final answer. This engine drives
every stream (durable and non-durable alike), so it regressed existing
non-durable clients: all tool/agentic/custom-event/structured E2E tests hung
after the first turn while plain single-turn streams passed.
Drain the event source to its natural end (the server closes the response only
when the run is truly complete) and use the terminal flag post-loop to decide
done-vs-reconnect. Restores the pre-durability read-to-close behavior for
non-durable streams; durable behavior is unchanged (single-terminal responses
end right after the terminal, so every resumable unit test still holds).
* fix(ai-client): send durability run id as X-Run-Id header, not a query param
The resumable adapters appended `?runId=<id>` to every POST (useChat always
supplies a runId), rewriting the request URL for all existing clients — not
just durable ones. That broke callers/tests that match the bare endpoint URL
and violated the invariant that a non-durable request is byte-identical to a
plain fetch.
Send the client-chosen run id in an `X-Run-Id` request header instead. The
POST URL is now untouched, so existing clients are unaffected, while a
durability sink still keys its log by the client's run id (memoryStream's
readRunId reads the header first, then falls back to the `?runId` query the
GET join path still uses). Durability remains a transparent add-on.
* ci: apply automated fixes
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Alem Tuzlak <t.zlak@hotmail.com> 35946e3c feat: configurable client-side message queue for useChat (#900)
* feat(ai-client): add message-queue config types
* feat(ai-client): export message-queue public types
* feat(ai-client): add message-queue config normalization + tests
* feat(ai-client): enqueue/cancel/flush + whenBusy policy in ChatClient
* feat(ai-client): auto-drain queue (fifo/batch) + flush on stop/clear/unsubscribe
Wires the queue drain into streamResponse's settle path so queued sends
actually go out (fifo one-at-a-time, or batch merged with newlines), and
flushes any pending queue on stop()/clear()/unsubscribe() so callers don't
get a surprise send after tearing down. Also surfaces the queue on the
devtools snapshot.
* fix(ai-client): flush message queue when a stream errors
A RUN_ERROR or thrown non-abort error left streamCompletedSuccessfully
false, so the finally block's queue handling (guarded by that flag) was
skipped entirely. Queued messages were stranded until a later direct
sendMessage sent first and drained the stale queued item afterward,
inverting message order. Flush (not drain) the queue on the non-success
settle path, matching stop()'s existing behavior.
* feat(ai-svelte): expose queue + cancelQueued + per-send whenBusy
* feat(ai-vue): expose queue + cancelQueued + per-send whenBusy
* feat(ai-react): expose queue + cancelQueued + per-send whenBusy
* feat(ai-solid): expose queue + cancelQueued + per-send whenBusy
* test: update framework hook tests for queue-by-default behavior
* docs: add changeset for client message queue
* docs(skills): document message queue in chat-experience skill
* fix(ai-vue,ai-solid): reflow re-export + correct queue jsdoc
* docs: document client message queue (queue option, cancelQueued)
* test(e2e): queue-while-streaming, cancel, and drain ordering
* feat(ai-preact): expose queue + cancelQueued + per-send whenBusy
Mirrors the ai-react implementation (17bb417d): ChatClient's queue-by-default
behavior is now wired through the Preact useChat hook via onQueueChange,
with cancelQueued and per-send whenBusy passthrough. Updates the two tests
that previously asserted a mid-stream second sendMessage was dropped to
instead assert both messages land in order.
* feat: re-export queue config types from framework packages + add ai-preact to changeset
* test(ai-client): cover multimodal batch-drain merge; docs: clarify whenBusy shorthand
* fix: sort queue-type imports alphabetically in framework types
* ci: apply automated fixes
* fix: export queue types from framework package entrypoints
The framework packages re-exported QueuedMessage/WhenBusy/QueueConfig/
QueueStrategy/QueueOption from their internal types module but never
forwarded them through the public index entry, so importing them from
e.g. @tanstack/ai-react failed at the package boundary.
* docs(example): add queueing-strategies route to ts-react-chat
Side-by-side panels (queue/fifo, interrupt, queue/batch) driven by a
shared composer, showcasing how each queue strategy handles a message
sent while a stream is in flight, with live queue rendering + cancel.
* fix: make message queue drain reliably and close review gaps
FIFO was stranding user messages when concurrent sendMessage calls both
passed the isLoading check (or when drain re-entered sendMessage and
re-enqueued items). Claim the client with sendInFlight, walk the queue in
a drain loop via deliverMessage, and stop the loop cleanly on interrupt.
Also: stable strategy pending ids, maxSize validation, Angular injectChat
parity, reload flushes queue, continuation errors still drain, queue
option live-updates, docs/skill accuracy, and unit coverage for the new
policy branches.
* fix: resolve eslint errors in message-queue branch code
Clear @typescript-eslint/no-unnecessary-condition findings in drainQueue
and continuation handling, drop an unnecessary type assertion in queue
tests, and fix import order / redundant narrowing in the queueing example.
* fix: close message-queue review gaps (batch drain, strategy API, docs)
Loop batch drain so mid-stream enqueues are not stranded, harden
deliverMessage with a claim that hands off to isLoading, unify
QueueStrategy on WhenBusy + busyReason, and re-export SendMessageOptions.
Docs now say drain only on successful settle; tests cover body, mid-drain
error/interrupt, and batch re-enqueue.
* ci: apply automated fixes
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> 35946e3c feat: configurable client-side message queue for useChat (#900)
* feat(ai-client): add message-queue config types
* feat(ai-client): export message-queue public types
* feat(ai-client): add message-queue config normalization + tests
* feat(ai-client): enqueue/cancel/flush + whenBusy policy in ChatClient
* feat(ai-client): auto-drain queue (fifo/batch) + flush on stop/clear/unsubscribe
Wires the queue drain into streamResponse's settle path so queued sends
actually go out (fifo one-at-a-time, or batch merged with newlines), and
flushes any pending queue on stop()/clear()/unsubscribe() so callers don't
get a surprise send after tearing down. Also surfaces the queue on the
devtools snapshot.
* fix(ai-client): flush message queue when a stream errors
A RUN_ERROR or thrown non-abort error left streamCompletedSuccessfully
false, so the finally block's queue handling (guarded by that flag) was
skipped entirely. Queued messages were stranded until a later direct
sendMessage sent first and drained the stale queued item afterward,
inverting message order. Flush (not drain) the queue on the non-success
settle path, matching stop()'s existing behavior.
* feat(ai-svelte): expose queue + cancelQueued + per-send whenBusy
* feat(ai-vue): expose queue + cancelQueued + per-send whenBusy
* feat(ai-react): expose queue + cancelQueued + per-send whenBusy
* feat(ai-solid): expose queue + cancelQueued + per-send whenBusy
* test: update framework hook tests for queue-by-default behavior
* docs: add changeset for client message queue
* docs(skills): document message queue in chat-experience skill
* fix(ai-vue,ai-solid): reflow re-export + correct queue jsdoc
* docs: document client message queue (queue option, cancelQueued)
* test(e2e): queue-while-streaming, cancel, and drain ordering
* feat(ai-preact): expose queue + cancelQueued + per-send whenBusy
Mirrors the ai-react implementation (17bb417d): ChatClient's queue-by-default
behavior is now wired through the Preact useChat hook via onQueueChange,
with cancelQueued and per-send whenBusy passthrough. Updates the two tests
that previously asserted a mid-stream second sendMessage was dropped to
instead assert both messages land in order.
* feat: re-export queue config types from framework packages + add ai-preact to changeset
* test(ai-client): cover multimodal batch-drain merge; docs: clarify whenBusy shorthand
* fix: sort queue-type imports alphabetically in framework types
* ci: apply automated fixes
* fix: export queue types from framework package entrypoints
The framework packages re-exported QueuedMessage/WhenBusy/QueueConfig/
QueueStrategy/QueueOption from their internal types module but never
forwarded them through the public index entry, so importing them from
e.g. @tanstack/ai-react failed at the package boundary.
* docs(example): add queueing-strategies route to ts-react-chat
Side-by-side panels (queue/fifo, interrupt, queue/batch) driven by a
shared composer, showcasing how each queue strategy handles a message
sent while a stream is in flight, with live queue rendering + cancel.
* fix: make message queue drain reliably and close review gaps
FIFO was stranding user messages when concurrent sendMessage calls both
passed the isLoading check (or when drain re-entered sendMessage and
re-enqueued items). Claim the client with sendInFlight, walk the queue in
a drain loop via deliverMessage, and stop the loop cleanly on interrupt.
Also: stable strategy pending ids, maxSize validation, Angular injectChat
parity, reload flushes queue, continuation errors still drain, queue
option live-updates, docs/skill accuracy, and unit coverage for the new
policy branches.
* fix: resolve eslint errors in message-queue branch code
Clear @typescript-eslint/no-unnecessary-condition findings in drainQueue
and continuation handling, drop an unnecessary type assertion in queue
tests, and fix import order / redundant narrowing in the queueing example.
* fix: close message-queue review gaps (batch drain, strategy API, docs)
Loop batch drain so mid-stream enqueues are not stranded, harden
deliverMessage with a claim that hands off to isLoading, unify
QueueStrategy on WhenBusy + busyReason, and re-export SendMessageOptions.
Docs now say drain only on successful settle; tests cover body, mid-drain
error/interrupt, and batch re-enqueue.
* ci: apply automated fixes
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com>