assistant-ui · Technical case study · October 2026

When a workspace switch kept the previous conversation list

An app switched its assistant from workspace A to B while the AI SDK component stayed mounted. The Cloud adapter changed, but the visible conversation list did not. A’s conversations remained on screen until the list was explicitly reloaded. We reproduced that handoff, tested a local repair, and followed the fix through merged upstream PR #8955.

@assistant-ui/ai-sdk 0.0.11Client thread-list lifecycleA/B causal controlsMerged PR #8955
28/28focused local repair

On the pinned upstream source, the focused baseline recorded 10 expected failures and 15 passes. After the local repair, the four-file run passed 28/28; an exact-input replay also passed 28/28. The public PR’s later verification is reported separately below.

The question

When the workspace changes, which conversation list is the user seeing?

A workspace selector is only correct if the list below it follows the newly selected Cloud client and scope. The mounted AI SDK wrapper changed its adapter without telling the retained list runtime to replace its visible state.

1. Load Aworkspace-a shows a-thread.
2. Select BCloud client or scopeId changes without unmounting.
3. List stays on AThe B list request is missing on pinned upstream.
4. Reload BA disappears and b-thread appears.

The app passes a Cloud client and scopeId to AISDKThreads. That wrapper constructs a Cloud thread-list adapter for RemoteThreadList. The list runtime holds the last visible records until threads.reload(). At the investigated revision, replacing the wrapper’s adapter did not call that lifecycle operation.

The source-pinned AI SDK wrapper and remote-list reload contract show the handoff. The pin is revision 5e64867 of @assistant-ui/ai-sdk 0.0.11; the baseline observation belongs to that revision.

What was observed

A stale client-visible list after a workspace change. The local test did not exercise hosted Assistant Cloud authorization or establish a server-side cross-account read.

How we worked

Pin the source. Switch the live component. Check the receiving side.

The controls measure visible threads, list calls, history requests, message writes, and run cancellation. Each answers a different part of the claim.

01 · Pin

Fix the starting point

Investigate the exact upstream wrapper and remote-list implementation at revision 5e64867.

02 · Reproduce

Keep it mounted

Render AISDKThreads on A, switch to B, and read the visible list and actual client calls.

03 · Control

Force one reload

On the same upstream component, call the public list reload and check whether B replaces A.

04 · Reattack

Stress the fix

Repeat client and scope changes, delayed responses, active runs, shared IDs, and new writes.

The causal control

Without a reload, A remained and no B list request appeared. A manual threads.reload() requested B and removed A. The local wrapper repair performed that same operation after the new adapter was committed.

Before and after

The list followed the adapter only after a reload.

The A/B fixture held the mounted component and workspace transition constant. The explicit-reload case isolates the missing step.

ObservationPinned upstreamLocal repair
Initial load on Aa-thread visiblea-thread visible
Switch to B while mountedA remains visible; no B list requestB requested; A removed; b-thread visible
Manual reload on upstreamB loads and A disappearsWrapper schedules the equivalent reload
Active A run during switchFailed B transition blocks later assertionsA run stops once; no A message written to B
New message after BFailed B transition blocks this branchB receives the write; A receives none
10 / 15Expected failures / passes on the pinned three-file upstream baseline (25 total)
28 / 28Passing tests in the repaired four-file run, including the Cloud adapter regression
28 / 28Passing tests on the same exact-input replay

The baseline’s failed B transition prevents its downstream cancellation and write assertions from running to completion. Those assertions are observations from the repaired variations.

Nine reattack variations

The fix had to survive more than A → B.

Eight cases used controlled in-memory Cloud clients. One used the real AssistantCloud SDK against a controlled local HTTP A/B service. All exercised the mounted AI SDK component path.

Changed conditionRepaired observationWhat it checks
Same client, new scopeIdB replaces A; unchanged rerender does not reloadScope alone is enough, without an ordinary-render loop
New client and scope, then back to AB loads; the return loads ABoth directions work
New client, same scopeReplacement client receives the list requestClient identity also triggers the handoff
Late A list responseB remains the visible listOld requests cannot restore A
Active A run; same remote thread ID in A and BB record wins; A run stops once; zero A writes land in BID reuse does not carry the old run into B in the fixture
Rapid A → B → CC is the settled listSuperseded reloads do not leave B visible
New conversation after BB receives the new write; A receives noneFresh work follows the selected client
Late A history responseB’s saved message remains; A’s stays outOld history does not overwrite B
Real Cloud SDK, local HTTP A/B fixtureB list and history GETs carry B owner/workspace headersClient request routing in a controlled service
Two kinds of evidence

Controlled clients count writes and cancellations. The HTTP fixture records list and history requests through the real SDK against local responses. Neither replaces a hosted service authorization test.

Repair and upstream review

Make the workspace handoff explicit.

When the Cloud client or scope changes, the AI SDK wrapper waits for React to commit the replacement adapter, then calls threads.reload(). Superseded scheduled work is canceled.

Local repair → merged PR

The list belongs to the selected workspace.

The local patch changed the wrapper, added a focused regression, and supplied a patch changeset. Upstream review expanded the final behavior to preserve controlled thread selection during the same transition.

Merged upstream

Local repair result

The wrapper reloads after the new adapter commits. The old list disappears, the B list arrives, and the reattack variations pass on the pinned source.

Merged PR #8955

The final change also handles a controlled threadId changed in the same render, avoids a duplicate initial Cloud load, and protects later selections and failed-load recovery.

Why the wrapper?
The remote-list contract leaves recreated adapter objects unchanged until explicit reload(). The AI SDK wrapper knows when the Cloud client or scope genuinely changes.
Final selection behavior
A newly requested controlled ID can be restored after replacement, including when outside the first list page or after a failed load. An unchanged ID from the old adapter resets at the scope boundary.
Public review

The workspace-scope issue #7203 and merged PR #7275 explain the earlier Cloud scope boundary. Merged PR #6080 defines list reset and explicit reload after an adapter replacement. PR #8955 connects that contract to the AI SDK wrapper’s actual workspace transition.

Verification

Separate the pinned research run from the final upstream checks.

The local report measured the pinned wrapper and its focused repair. The merged PR’s public description records additional fixes and its own suite results.

Evidence setResultWhere it applies
Pinned baseline10 expected failures, 15 passesThree focused files on upstream 5e64867
Local repair and replay28/28, then 28/28Four focused files on repaired pinned checkout
Wider local compatibilityAI SDK 473/473; core 3,082/3,082; Cloud 289/289 on each v7 and v6 peerResearch checkout; separate from the focused replay
Merged PR verificationAI SDK 478 tests; core 3,377 tests; builds, typechecks, focused formatting and lint checks passedPR #8955 verification record, with expanded controlled-selection tests

The pinned report also records 445 passing React 18 peer tests with 28 suite-design skips, React compiler 1/1, and standalone AI SDK 3/3. Counts from the later merged PR are intentionally kept separate.

Reproduction material

The research report describes an offline Linux x64 reviewer kit that runs the expected baseline failure, repair, and exact replay. That archive is not hosted by this page. The linked upstream PR, source pin, and test record are the public evidence readers can inspect here.

What this shows

The visible state must follow the active owner.

The failure was a missing client lifecycle step. The repair makes the selected workspace and visible list move together, including under delayed responses and interrupted work in the controlled fixtures.

A switch must carry its state.

Changing the adapter is not enough when the list keeps its previous records. The wrapper must explicitly reload after the replacement is committed, while the list runtime rejects work left behind by the old adapter.

The result here is a repeatable stale conversation list on the pinned client and a merged upstream correction. The controlled HTTP fixture does not prove that a hosted service returned an unauthorized conversation or accepted an unauthorized message.

Work with Kronowave

Which state crosses your agent’s next boundary?

We trace the selected owner, the runtime state, and the actual requests or effects, then work with maintainers to make the boundary hold.

48-hour Rapid Scan →