Skip to content

ai/state: finish the transport demotion — 7+ dual-write sites between the useChat array and the conversation cache #2145

Description

@2witstudios

What

Post-cutover, the architecture is store-first rendering: useConversationMessagesStore renders, and the AI SDK useChat array is demoted to "transport bookkeeping only" (chat-config.ts:1-14). But the transport still exists per surface (6 useChat instances: AiChatView.tsx:190, GlobalAssistantView.tsx:323-339 ×2, usePageAgentSidebarChat.ts:84-109 ×2) and must be manually re-seeded before every SDK re-invocation because regenerate()/addToolResult() POST its internal array. The result is a standing dual-write protocol the code itself flags as transitional and forbidden-by-default ("rail 11"; hydrateTransportBeforeReinvoke.ts:4-20 "deletion covenant... DELETE ME at the SDK 7 transport swap"; useOwnStreamMirror.ts:11-15 same covenant, deletion gate: grep -rn "TRANSITIONAL" apps/web/src must return zero — currently 13 hits).

Current dual-write inventory (each is a desync opportunity)

  1. AiChatView.tsx:288,307-309 — load path writes store (applyLoad) and transport (setMessages), each behind its own guard.
  2. AiChatView.tsx:860-878 — socket handlers dual-write append/edit/delete into both containers; the shared handlers (conversationCacheSocketHandlers.ts:43-104) are store-only, so AiChatView keeps a private fork (:27-29 explains why).
  3. useAnswerAskUser.ts:111,115,117 — store patch + transport hydrate + addToolResult (has its own stale-snapshot bugs, filed separately).
  4. useCacheMessageActions.ts:105-118,150-157 — edit/delete/retry write both.
  5. Send paths — optimistic user message to store while sendMessage appends to transport: SidebarChatTab.tsx:790-833, GlobalAssistantView.tsx:826-854, AiChatView.tsx:1176-1226.
  6. useOwnStreamMirror.ts:114-236 — transport→store direction, copied every tick, mounted 5×.
  7. useMessageActions.ts:134-154 — post-edit reconcile refetch setMessages(loaded) wholesale.

The scars are documented in-code: #2061 clobber guards grown then deleted per-surface (GlobalChatContext.tsx:36-49, SidebarChatTab.tsx:688-692), stale-busy-flag re-send fixed in one hook but not the other, chat-config.ts:4-10 blank-screen/stale-history history.

Ask

The endgame is already named in the covenants: swap to a transport that takes the message trail per-call (the store as the only container), then delete hydrateTransportBeforeReinvoke, useOwnStreamMirror, and the per-surface dual-writes wholesale. This issue is the tracking home for that cutover so the fixes stop being per-desync patches. Until then, every new message-mutating feature has to remember to write two containers in the right order behind the right guards.


Filed from the sources-of-truth audit.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions