This repository has no description
flarebot docs tanstack-query-migration-plan.md
33 kB

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.tsx mounts CustomerQueryProvider above the router. customer-query-session.ts owns one browser cache, origin/session keys and revocation; owner-client.ts notifies 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, useRuntimeInfoQuery and useDomainInfoQuery. 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 in useSettingsConnection; drafts stay in their editors.
  • Phase 2: useConversationsQuery supplies the shell, Home, command palette, task conversation choices and diagnostics. ShellSession retains 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, useTaskQuery and useTaskRunsQuery own task reads, cursor reconciliation and visible polling. TaskSession retains 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 and dispatch_error reconciliation 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 PublisherQueryProvider has an independent browser cache and seven reusable hooks under control-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 #

  1. Mount the customer Query provider above route content, where it survives navigation (src/App.tsx or 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.
  2. 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.
  3. 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.
  4. 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: false alone does not clear cached data. Preserve cached data during ordinary disconnection.
  5. Keep the publisher in a separate provider/cache. Its keys include the verified ownerSubject and 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 useMutation where 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: false and 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:

  1. Cached Settings return while a refresh is held, followed by fresh values; compare request counts with the baseline under the documented freshness policy.
  2. Background refetch with a dirty instruction/model/memory draft, including the original memory version and existing explicit conflict response.
  3. Shared conversation request deduplication and broadcast-driven updates in navigation and diagnostics, including a delayed stale list after creation.
  4. 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.
  5. Reads and writes during offline/reconnect transitions: no automatic write replay, no duplicate native reconnect loop, and refresh uses the new connection.
  6. 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.