TanStack Query migration plan #
Adopt @octanejs/tanstack-query for browser server state, beginning with Settings
and shared conversation metadata. The intended result is faster return visits,
fewer duplicate reads, and less repeated loading/error/refresh code.
Phases 0–4 have been implemented locally. The application regression suites pass; the full native bridge health check remains blocked by the local container runtime, as recorded below.
The original plan was prepared on 2026-09-09 against HEAD 945d0cd. The
customer runtime Effect plan is a separate
backend effort. This migration keeps its public RPC contracts.
Implementation record — phases 0–2 #
- Phase 0:
src/App.tsxmountsCustomerQueryProviderabove the router.customer-query-session.tsowns one browser cache, origin/session keys and revocation;owner-client.tsnotifies all participating owners of denied preflight or native expiry. Settings and task sockets remain independently owned. SSR clients are request-local, with no private fetch or dehydration. - Phase 1: Each of the six Settings reads has its own reusable hook under
src/runtime/queries/:useInstructionsQuery,useModelCatalogQuery,useModelSettingsQuery,useMemoriesQuery,useRuntimeInfoQueryanduseDomainInfoQuery. Their route components no longer mirror saved results or maintain manual read/loading/reload state. Safe writes use mutations; provider-key actions remain explicit native RPCs. Connection ownership lives inuseSettingsConnection; drafts stay in their editors. - Phase 2:
useConversationsQuerysupplies the shell, Home, command palette, task conversation choices and diagnostics.ShellSessionretains native readiness and creation intent but no conversation list copy. Broadcasts and confirmed creation retire stale list requests. Native creation and navigation guards are preserved.
The exact added binding is @octanejs/tanstack-query@0.1.52; the lockfile resolves
@tanstack/query-core@5.102.8. Octane remains 0.2.2, the router binding 0.1.52
and Agents 0.22.0. The resolved core's cancellation/retry implementation and
binding were inspected: reads consume Query's signal, while RPC completion is
also checked against the native connection because this SDK has no per-call
AbortSignal. Mutations use networkMode: "always", no retries and an execution-time
native readiness check. No compiler/package upgrade or package patch was needed;
resource hooks use .tsx for the pinned compiler's client hook-slot transform.
Baseline typecheck, release build, Settings and shell suites passed. The measured Settings → conversation → Settings sequence changed from one RPC each for instructions, catalog, model settings, memories and runtime info to zero each within 30 seconds. Its conversation-list calls fell from three to two, including route/creation refresh triggers. A dedicated fresh-page browser assertion verifies that simultaneous shell and diagnostics observers issue exactly one list RPC; a held native rename broadcast also produces one shared read.
The phase 2 client build added approximately 43.2 kB JavaScript / 12.5 kB gzip across its app, router and runtime chunks; CSS is unchanged. Before/after router chunks were 963.67/1,006.68 kB (310.12/322.61 kB gzip). There is no disk cache or private SSR payload to offset that cost.
tests/fixtures/settings-query-checks.mjs extends the existing packaged Settings
suite using real owner RPCs, response gates and controlled browser freshness.
It covers fresh return with held preflight, stale return with held read, dirty
instruction/model/memory drafts, original memory-version conflicts, transient
read failure/retry, stale read versus confirmed save, shared rename propagation,
offline write locks/no replay, and late read/mutation replies across a revoked
scope and new editor. Existing native signed-expiry, secret/SSR exclusions,
Kumo accessibility, mobile layout and explicit download checks remain.
tests/app-shell.test.mjs now triggers its stale-list race using an actual native
rename notification so it does not depend on unconditional fresh focus reads.
The task suite also verifies restored authentication reloads the URL-selected
run history while keeping the expired editor closed.
Phases 0–2 validation passed sequentially against the customer release:
pnpm typecheck, pnpm build:release, pnpm test:worker (2 tests),
pnpm test:settings, pnpm test:app-shell, pnpm test:chat-ui (23 tests),
pnpm test:tasks-ui (2 tests), and pnpm test:schedule-action (9 tests).
Formatting and git diff --check also pass. No native/browser checks were skipped.
Architecture changes are documented in agent-settings.md and
application-shell.md; the binding/compiler findings are recorded in
bug-lessons.md.
Implementation record — phases 3–4 #
- Phase 3:
useTaskListQuery,useTaskQueryanduseTaskRunsQueryown task reads, cursor reconciliation and visible polling.TaskSessionretains its native connection, drafts, task versions and frozen command identities, without a saved-result mirror or read timer. URL selection, 3/30-second polling, immutable older pages anddispatch_errorreconciliation remain intact. Each task has one history cache; page reads use native RPC directly and explicit pagination appends into that cache. Query cancellation and pagination lifetime guards reject abandoned replies. - Phase 4: The stable
PublisherQueryProviderhas an independent browser cache and seven reusable hooks undercontrol-plane/ui/queries/: identity, connection, installation list/detail, domain detail/zones and provider detail. Keys carry the verified owner and session generation, plus account, installation and cursor where relevant. Private responses cannot enter an unknown or retired owner's cache; deployment-grant expiry preserves installation metadata. Existing command controllers retain their frozen intents, explicit recovery and deadlines. Update-on-visit submits through a separate one-shot controller; query refreshes only perform GETs.
Native task browser assertions verify exactly one newest-page read for
settled history and two reads when one older page needs reconciliation,
independently of the number of settled pages loaded. Task UI (2), schedule action
(9), task service (1), and native execution (17) tests pass, including lost replies,
rapid selection, real UTC recurrence, lifecycle gates and worker restarts.
pnpm typecheck and pnpm build:release pass. The phase 3 router chunk is
1,007.60 kB (323.10 kB gzip), versus phase 2's 1,006.68 kB (322.61 kB gzip);
customer CSS remains 167.28 kB (27.56 kB gzip).
The publisher build uses pnpm build:control-plane:fixture. Its entry and
Onboarding chunks are 222.18/321.00 kB (70.24/100.81 kB gzip); CSS is 155.62 kB
(25.19 kB gzip). No pre-migration publisher bundle measurement was captured, so
these are final sizes, not a claimed delta.
pnpm test:installation-status (1), pnpm test:updates (4),
pnpm test:domains (16), pnpm test:ownership (17), and pnpm test:oauth (4) pass.
Typecheck, the publisher fixture build, formatting and git diff --check pass.
The installation suite adds a held native status response to check that 50 cached
rows and Open Flarebot remain visible, focus shares one in-flight GET, and refresh
submits no commands. It also requires native logout to return 303 and remove the
session cookie before another owner signs in. Logout clears saved intents while
keeping its submitting form mounted; native navigation retires the cache. Update
tests require focus, visibility and reconnect events to leave the upgrade POST
count at one. The provider browser flow adds failed
background-read recovery and verifies that read retries never enable a provider.
Domain UI authentication uses the native registry/vault and installation HTTP
route; its existing domain progression responses remain simulated, alongside the
separate native domain workflow, authorization and provisioning suites.
The full pnpm test:bridge health gate is not passing locally. Its provider
and login flows, including the new read-retry assertions, passed before reaching
the real Sandbox readiness probe. With the Podman socket started and DOCKER_HOST
set, the native container engine reports
crun: cannot set memory swappiness with cgroupv2; the probe consequently returns
403 instead of 200. Without a configured socket, the engine cannot connect to
/var/run/docker.sock. This is recorded as an environment limitation, with the
health assertion and production configuration unchanged. Run the complete bridge
suite on a supported Docker runtime before shipping.
Architecture details are updated in scheduled-tasks-ui.md,
installation-status.md and oauth-onboarding.md. No additional implementation
phase is defined by this plan; no deployment is included.
Intended user experience #
- Settings → conversation → Settings shows previously loaded settings immediately within the authenticated browser session. A refresh keeps the existing content visible; only the first load needs the full loading placeholder. Writes remain disabled until the native connection is ready.
- Navigation, the conversation command palette, and the diagnostics selector observe the same conversation list. A native change notification refreshes that shared query rather than leaving independently fetched copies out of date.
- Saving instructions, models or memories updates the visible saved data without a full-page reload. Background refresh never overwrites an unsaved draft.
- A transient read failure retains the last available data and offers a retry. Confirmed authentication loss removes private cached data and editors.
Phases 0–2 were the first deliverable. Task and publisher reads were implemented as follow-on phases with their own validation, recorded separately above.
Current state and proposed ownership #
| Area | Current implementation | Query responsibility | Retained responsibility |
|---|---|---|---|
| Instructions | src/routes/Settings.tsx |
getInstructionSettings, saved result, read and save status |
Connection lifecycle, draft, reset semantics |
| Models | src/routes/ModelSettings.tsx |
getModelCatalog, getModelSettings, saved model result |
Draft selection, provider availability rules, secret input handling |
| Memories | src/routes/MemorySettings.tsx |
listMemories, confirmed CRUD results, reload state |
Editor, original version, conflict messages, hash navigation |
| Installation/domain info | src/routes/InstallationSettings.tsx, DomainSettings.tsx |
getRuntimeInfo, /api/domain |
Existing DTO and URL validation, authenticated access |
| Conversation summaries | src/runtime/shell-session.tsx, src/routes/DiagnosticSettings.tsx |
One list cache, refresh and creation result | Native owner socket, connection status, broadcast listener, navigation intent |
| Tasks | src/runtime/task-session.ts |
List and run-history reads, eventual polling/pagination ownership | Drafts, versions, frozen create/run IDs, commands and reconciliation rules |
| Publisher | control-plane/ui/installation-status.tsx, Onboarding.tsx, UpdateInstallation.tsx, DomainSetup.tsx, ProviderSetup.tsx |
Authenticated connection/account, installation, domain and provider reads | Owner/account transitions, authorization, operation intent and command lifecycle |
| Live conversation | src/runtime/conversation-session.ts |
None in this plan | Native chat stream, history reconciliation, resume, stop, acknowledgements and activity state |
Use existing Cloudflare Agents RPC and HTTP endpoints as query functions. Do not
introduce REST replacements for RPC, another Agent SDK, a browser Effect runtime,
or a general-purpose request framework. Keep createOwnerClient and native
timeouts. Query does not own WebSocket authentication or reconnection.
Diagnostics export remains an explicit download, not a background query. The conversation's default-model read immediately before submission remains fresh and native; a cached Settings value must not replace that snapshot.
Dependency and implementation shape #
The package inspected for this plan was @octanejs/tanstack-query@0.1.52, with
@tanstack/query-core@^5.101.3 and peer octane@^0.1.51 || ^0.2.0. The app declares
octane@^0.2.2 and already uses @octanejs/tanstack-router. Verify the actual
resolved versions when implementing, then pin the chosen Query binding and update
pnpm-lock.yaml. Avoid unrelated Octane or router upgrades.
The binding reuses TanStack Query's core and supplies Octane hooks. See its README and binding status. Review the upstream defaults, cancellation, and mutation semantics against that version before relying on them.
Use a small provider module under src/runtime/ for QueryClient creation and
cache lifecycle, and cohesive query definitions for settings and conversations.
Share query keys/options where there are multiple consumers. Keep ordinary hooks
in components; do not recreate the current session stores inside a generic wrapper
around Query. Each migrated resource has one authoritative browser cache; avoid
mirroring its full result back into a second useState or session snapshot.
Cache lifetime and authentication #
- Mount the customer Query provider above route content, where it survives
navigation (
src/App.tsxor the stable root layout). Create one client per mounted browser app. Any server-rendered instance is request-local and empty; never use a Worker-global QueryClient containing owner data. - Start private reads only in the browser after the relevant native connection or HTTP authentication boundary is ready. Keep generic SSR and hydration output. This plan requires no private prefetch, dehydration, persisted query cache, service-worker cache, or router SSR Query integration.
- Scope customer keys to the runtime origin and a nonsecret, in-memory owner session scope. The fixed-owner customer app can establish that scope after authenticated preflight. Preserve it across route changes and ordinary socket reconnects; a socket generation is not the cache identity. Never use cookies, bearer tokens or provider keys as query keys.
- Centralize notification of confirmed session loss from the existing connection
owners. On denied owner preflight or native expiry (4001), disable readers,
invalidate the old session scope, cancel/remove its queries and clear private
mutation/editor state. Late reads or mutation callbacks must not repopulate
the old scope or change a newly mounted editor.
enabled: falsealone does not clear cached data. Preserve cached data during ordinary disconnection. - Keep the publisher in a separate provider/cache. Its keys include the verified
ownerSubjectand account/installation/cursor as applicable. An identity-only response establishes owner changes; deployment-grant expiry must not be mistaken for identity loss or erase ready installations. Keep the identity bootstrap separate from owner-scoped resource queries so an unknown owner never inherits previous private query data.
Reads, freshness and cancellation #
Use these initial settings explicitly, then adjust only for observed behavior:
| Resource | Freshness and refresh policy |
|---|---|
| Model catalog | staleTime: Infinity within the browser session; invalidate after reauthentication/reconnect or a known runtime update |
| Instructions, model settings, memories, runtime/domain info | staleTime: 30_000; manual refresh, stale focus refresh and forced refresh after native reconnect |
| Conversation list | staleTime: 30_000; invalidate on native conversations-changed, creation and existing route/reconnect refresh triggers |
| Inactive customer data | gcTime: 5 * 60_000; no disk persistence |
| Read failures | Start with retry: false; native connection retry remains separately owned, and users retain explicit read retry |
Use isPending together with connection state for initial loading, and
isFetching for non-destructive background refresh. A disabled query can be
pending without fetching. Do not leave a signed-out screen showing an indefinite
loading indicator or hide existing values during every refresh.
Map native broadcasts and connection readiness into Query invalidation. Browser network availability is not AgentClient readiness; Query's reconnect behavior does not replace native connection guards. Remove duplicated focus listeners or polling loops only once Query owns the corresponding refresh behavior.
Pass Query's AbortSignal to HTTP requests, combined with the existing bounded
deadline through response-body consumption. For Agent RPC, verify the pinned
SDK's cancellation support. Canceling a Query does not prove that an RPC stopped.
Retain current-connection/session checks at asynchronous boundaries and explicitly
cancel affected reads when a connection is retired. Do not remove every generation
guard merely because a read moved into a hook.
Mutations and drafts #
- Move ordinary instruction/model/memory writes to
useMutationwhere it removes repeated pending/error bookkeeping. Preserve existing write locks and safe public error messages. Recheck the current authenticated connection when the mutation executes, including after awaiting readiness. - Use
retry: falseand avoid automatic offline queuing/replay. For migrated writes,networkMode: "always"plus a live-connection check can fail immediately instead of pausing a command until connectivity returns. Verify this behavior against the pinned binding; a disabled button alone is insufficient. - Cancel conflicting reads before applying a confirmed mutation response. Update the affected cache from that response and invalidate dependent reads as needed. Preserve immediate confirmed memory/model/instruction updates; do not replace them with a blank loading screen or an unnecessary wait for a second read.
- Keep drafts separate from query data. Initialize from saved data and refresh a pristine draft only; retain dirty values and the original memory/task version through refetch and reconnect. A newly mounted editor may use cached data; navigation need not gain a new persistent unsaved-draft feature.
- Keep provider-key writes on a narrow explicit action path initially. Mutation caches can retain their variables: do not put raw API keys into generic mutation variables, query data/keys, metadata, logs or browser storage. Preserve clearing the key input on success, failure, disconnect and navigation. Only invalidate the safe model/provider-presence result after the action.
- Keep create/run/deploy/recover command identities and ambiguous-result handling in their current domain owners. Query mutation state is not a durable operation ledger. No automatic recovery, repeated creation or new request UUID on retry.
Ordered implementation phases #
0. Establish baseline and provider #
Inspect the current instructions, working tree, query binding metadata and native contracts. Read application shell, agent settings, scheduled tasks UI, and installation status. Before diagnosing any failure, symptom-match bug lessons.
Run pnpm typecheck, build the customer release, and run the existing Settings
and shell browser suites. Record client bundle sizes and the native RPC calls
during Settings → conversation → Settings. Establish current failures separately.
Add the dependency and stable Query provider, then implement session scope and authentication cleanup. Retain independently scoped Settings/task owner clients for the first deliverable; sharing data does not require a socket consolidation. Provide only the small integration needed for their auth notifications.
Exit: typecheck/build and existing browser behavior pass; the provider persists through route navigation and owner data cannot enter public SSR or another session.
1. Migrate Settings #
Migrate the seven distinct reads: instructions, model catalog, model settings,
memories, runtime info, domain info, and (in phase 2) diagnostics' conversation list.
Keep the existing safeSetupUrl/safeOrigin validation for /api/domain.
Replace per-resource result/loading/error/reload state with queries. Keep the Settings native connection owner and its reconnect/expiry transitions. Ensure the render tree actually displays cached settings while reconnecting, with writes locked; retaining a QueryClient without changing initial-loading rendering does not deliver the promised navigation improvement.
Migrate safe writes using the mutation policy above. Keep confirmation notices, draft retention, version conflicts, provider restrictions, memory hash navigation, installation descriptors and explicit diagnostics download behavior.
Exit: return navigation shows cached settings before a held refresh completes; background refresh and read failure preserve visible data and dirty drafts; confirmed writes update the cache; expiry clears all private panels and drafts.
2. Share conversation metadata #
Move the conversation list into one query owned through the shell's native
connection. DiagnosticSettings observes the same query using a shared hook or
query options, instead of issuing its own list RPC through the Settings socket.
All existing shell consumers must read that cache, including navigation, Home,
the command palette and task conversation selection.
Keep shell connection status separate from query fetch status. Preserve the initial contract that Connected follows native readiness and a successful first list read. Native broadcasts invalidate the list; creation inserts its confirmed result without a late list response removing it. Preserve the navigation guard that prevents a late creation response redirecting a user who already navigated. Keep uncertain creation outcomes explicit and never retry creation automatically.
Exit: simultaneous consumers share an in-flight list request; a native rename or creation updates all mounted consumers; stale reads cannot undo a newer result. Remove the obsolete list copy and its manual read-state bookkeeping.
Phases 0–2 form the first implementation deliverable. Update agent-settings.md
and application-shell.md to describe the actual cache/auth boundary. Record the
observed return-navigation and request-sharing improvements plus bundle delta.
3. Migrate task reads #
Extract task lists and run-history reads from TaskSession into queries keyed by
owner scope, task ID and cursor. Keep the editor/command state and native scheduling
service. The URL remains the selection authority.
Preserve visible polling at 30 seconds, or 3 seconds when loaded runs are active, and stop polling while hidden/disconnected. Query should own polling once migrated; remove the old read timers and overlapping request locks it replaces.
Do not assume a default infinite query preserves the existing history contract.
Refresh the newest page and at most one older page with unsettled runs; retain
immutable older pages/cursors when overlap proves continuity, and drop a
disconnected tail. Use explicit page queries or a small reconciliation helper if
necessary. Preserve the existing handling of dispatch_error rows as well as
active runs. A refetch of every accumulated page would be a behavior/performance
regression and needs a separate decision.
Keep original task versions, draft rebasing, immutable create payloads and run-now UUIDs. Cancel/invalidate affected queries around commands without interpreting client cancellation as durable task cancellation.
Exit: existing task UI/native execution tests pass with pagination continuity, older active-run settlement, rapid selection changes, lost replies and retry IDs preserved. No task-result mirror or second polling loop remains.
4. Migrate publisher reads #
Add the separate Query provider at the stable publisher app entry. Migrate
/api/connection, installation list/detail, domain detail/zones and provider
detail reads incrementally. Keep account selection, session restoration and
command programs explicit. Include all owner/account/installation/page inputs in
the relevant keys; never display one owner's previous page as another's placeholder.
Preserve the existing polling policies: installation status every 2 seconds while active and approximately 30 seconds otherwise, update detail every 1.5 seconds, and domain transitions every 3 seconds. Respect visibility and request overlap rules already applicable to each surface. Stop transition polling on terminal states. Retain read data through transient failures and deployment-grant expiry.
Preserve the 15-second installation read deadline versus the 120-second installation command acknowledgement deadline. Retain each other endpoint's existing timeout. Keep sessionStorage's exact nonsecret frozen intents, owner change cleanup, manual recovery, ambiguous upgrade handling and redirect timing. Refreshing a query must never call reserve, start, upgrade, recover or domain POSTs.
In particular, UpdateInstallation currently combines initial reads and a
guarded upgrade submission in its load flow. Split that flow before using Query:
the existing update-on-visit controller retains submission ownership, while a
query function only reads status. Preserve its submitted flag and frozen target;
focus, polling and query retries must not become additional submission triggers.
Exit: the native installation/domain/update flows pass lost-reply, reload, owner/account switching, grant-expiry, offline and recovery cases. Their state machines own commands while Query owns migrated reads.
Validation and handoff #
Extend existing native browser suites rather than testing that a hook was called. Keep successful results sourced from actual Agent RPC/HTTP fixtures; delay or drop real replies to exercise races. Use deterministic response gates with cleanup. Observe bug lessons on held responses exceeding RPC deadlines, browser offline events leaving sockets alive, and polling missing transient phases.
Add focused behavioral coverage for:
- Cached Settings return while a refresh is held, followed by fresh values; compare request counts with the baseline under the documented freshness policy.
- Background refetch with a dirty instruction/model/memory draft, including the original memory version and existing explicit conflict response.
- Shared conversation request deduplication and broadcast-driven updates in navigation and diagnostics, including a delayed stale list after creation.
- Expiry followed by late read/mutation completion and a new session: private cached data and old editor callbacks cannot reappear. Provider keys never enter query or mutation caches, public HTML or browser persistence.
- Reads and writes during offline/reconnect transitions: no automatic write replay, no duplicate native reconnect loop, and refresh uses the new connection.
- For later phases, task cursor/older-run behavior and publisher owner isolation, frozen intents, command deadlines and stable request IDs after ambiguous replies.
Preserve the pinned Kumo components, accessibility labels, focus behavior, mounted dialog lifecycle, mobile layouts and 14px content text. Apply Kumo design guidance when changing rendering or frontend tests. Use subtle existing status UI for background reads; technical cache details do not belong in the product interface.
Run checks sequentially when they share generated artifacts or native fixtures:
| Change | Required validation |
|---|---|
| Each implementation phase | pnpm typecheck, customer build, focused native/browser regressions |
| Settings and shell | pnpm build:release, pnpm test:worker, pnpm test:settings, pnpm test:app-shell, pnpm test:chat-ui |
| Task reads | Customer release build, pnpm test:tasks-ui, pnpm test:schedule-action; run pnpm test:tasks and pnpm test:execution if command integration changes |
| Publisher reads | Customer release, then pnpm build:control-plane:fixture for a dirty development tree; pnpm test:installation-status, pnpm test:updates, pnpm test:domains, pnpm test:bridge, pnpm test:ownership |
Use the production control-plane catalog build only when its release requirements are satisfied. Follow the actual current scripts and CI fixture ordering; do not weaken assertions or mistake unavailable Chromium/Docker/native checks for passes. Run additional existing suites where changed boundaries require them.
For each delivered phase, record files/resources migrated, request/bundle evidence, checks and limitations, and the next unimplemented phase. Update the relevant architecture docs. Completion requires the user-visible behavior and removal of the superseded read-state machinery; dependency installation alone is not enough. Implementation installs the pinned package locally; deployment remains separate.