fix(desktop): bot tile transcripts read from their owner backend
Salvages the client half of PR #99333. A workspace tile pinned to an exact owner (connection + target profile) was reconciled via a bare getLatestSessionMessages(storedId) — the foreground profile's backend — so a bot tile on another profile never saw its new turns (or saw the wrong session's). reconcileTileTranscripts now derives a ProfileScope from tile.ownerRoute and keys the per-tile signature by that route; route-less tiles keep the legacy local read. The tui_gateway/server.py `_sessions_sig` cross-profile scan from #99333 is intentionally not taken: the desktop runs one backend per profile, each with its own watcher, so scanning sibling profiles would misattribute ticks. Co-authored-by: stods21 <stods21@users.noreply.github.com>
This commit is contained in:
@@ -260,7 +260,7 @@ describe('active transcript refresh', () => {
|
||||
|
||||
// Behavior assertions:
|
||||
expect(updaterCallCount).toBeGreaterThan(0)
|
||||
expect(getLatestSessionMessages).toHaveBeenCalledWith(TILE_STORED_ID)
|
||||
expect(getLatestSessionMessages).toHaveBeenCalledWith(TILE_STORED_ID, undefined)
|
||||
})
|
||||
|
||||
it('reconciles an idle tile while the main pane is busy', async () => {
|
||||
@@ -285,7 +285,7 @@ describe('active transcript refresh', () => {
|
||||
updateSessionState
|
||||
})
|
||||
|
||||
expect(getLatestSessionMessages).toHaveBeenCalledWith(storedId)
|
||||
expect(getLatestSessionMessages).toHaveBeenCalledWith(storedId, undefined)
|
||||
expect(updateSessionState).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@@ -347,6 +347,43 @@ describe('active transcript refresh', () => {
|
||||
expect(updateSessionState).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('isolates tile transcript reads by connection and profile while preserving the legacy local path', async () => {
|
||||
vi.mocked(getLatestSessionMessages).mockImplementation(async storedId => transcript(storedId, storedId) as never)
|
||||
|
||||
const updateSessionState: Parameters<typeof reconcileTileTranscriptsForTest>[0]['updateSessionState'] = vi.fn(
|
||||
(_sessionId, updater) => updater({} as Parameters<typeof updater>[0])
|
||||
)
|
||||
|
||||
await reconcileTileTranscriptsForTest({
|
||||
tiles: [
|
||||
{
|
||||
ownerRoute: { connectionId: 'connection-a', mode: 'remote', profile: 'shared-profile', targetProfile: 'target-a' },
|
||||
runtimeId: 'runtime-a',
|
||||
storedSessionId: 'stored-a'
|
||||
},
|
||||
{
|
||||
ownerRoute: { connectionId: 'connection-b', mode: 'remote', profile: 'shared-profile' },
|
||||
runtimeId: 'runtime-b',
|
||||
storedSessionId: 'stored-b'
|
||||
},
|
||||
{ runtimeId: 'runtime-local', storedSessionId: 'stored-local' }
|
||||
],
|
||||
requestSequenceRef: { current: 0 },
|
||||
signatureRef: { current: new Map<string, string>() },
|
||||
updateSessionState
|
||||
})
|
||||
|
||||
expect(getLatestSessionMessages).toHaveBeenCalledWith('stored-a', { connectionId: 'connection-a', profile: 'target-a' })
|
||||
expect(getLatestSessionMessages).toHaveBeenCalledWith('stored-b', {
|
||||
connectionId: 'connection-b',
|
||||
profile: 'shared-profile'
|
||||
})
|
||||
expect(getLatestSessionMessages).toHaveBeenCalledWith('stored-local', undefined)
|
||||
expect(updateSessionState).toHaveBeenCalledWith('runtime-a', expect.any(Function), 'stored-a')
|
||||
expect(updateSessionState).toHaveBeenCalledWith('runtime-b', expect.any(Function), 'stored-b')
|
||||
expect(updateSessionState).toHaveBeenCalledWith('runtime-local', expect.any(Function), 'stored-local')
|
||||
})
|
||||
|
||||
it('skips the tile fetch entirely when nothing changed (signature-gated)', async () => {
|
||||
$changeEventsAvailable.set(true)
|
||||
|
||||
|
||||
@@ -72,6 +72,16 @@ function tileRuntimeOwnsLiveState(runtimeId: string): boolean {
|
||||
return Boolean(state && (state.busy || state.awaitingResponse || state.needsInput || state.turnLive))
|
||||
}
|
||||
|
||||
type TileTranscriptTarget = { ownerRoute?: SessionProfileRoute; storedSessionId: string; runtimeId?: string }
|
||||
|
||||
/** Signature key per tile — carries the owner route so two connections/profiles
|
||||
* sharing a stored id (or a tile re-homed to another owner) never alias. */
|
||||
function tileTranscriptSignatureKey(tile: TileTranscriptTarget): string {
|
||||
const route = tile.ownerRoute
|
||||
|
||||
return `tile:${route ? `${route.connectionId}:${route.targetProfile ?? route.profile}:` : ''}${tile.storedSessionId}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconcile the persisted transcripts of every open WORKSPACE TILE (#93942
|
||||
* slice 1). Bot canonical chats live here — never in $sessions /
|
||||
@@ -96,7 +106,7 @@ export async function reconcileTileTranscripts({
|
||||
}: {
|
||||
requestSequenceRef: MutableRefObject<number>
|
||||
signatureRef: MutableRefObject<Map<string, string>>
|
||||
tiles?: Array<{ storedSessionId: string; runtimeId?: string }>
|
||||
tiles?: TileTranscriptTarget[]
|
||||
updateSessionState: (
|
||||
sessionId: string,
|
||||
updater: (state: ClientSessionState) => ClientSessionState,
|
||||
@@ -104,7 +114,7 @@ export async function reconcileTileTranscripts({
|
||||
) => ClientSessionState
|
||||
}): Promise<void> {
|
||||
const tiles = tilesOverride ?? $sessionTiles.get()
|
||||
const openSignatureKeys = new Set(tiles.map(tile => `tile:${tile.storedSessionId}`))
|
||||
const openSignatureKeys = new Set(tiles.map(tileTranscriptSignatureKey))
|
||||
|
||||
for (const signatureKey of signatureRef.current.keys()) {
|
||||
if (!openSignatureKeys.has(signatureKey)) {
|
||||
@@ -139,19 +149,27 @@ export async function reconcileTileTranscripts({
|
||||
? tilesOverride.some(t => t.storedSessionId === storedSessionId && t.runtimeId === runtimeSessionId)
|
||||
: $sessionTiles.get().some(t => t.storedSessionId === storedSessionId && t.runtimeId === runtimeSessionId)
|
||||
|
||||
// Bot tiles are pinned to an exact owner (connection + target profile);
|
||||
// read from that backend, not whichever profile is foreground. Tiles
|
||||
// without a route keep the legacy local read.
|
||||
const profileScope: ProfileScope = tile.ownerRoute
|
||||
? { connectionId: tile.ownerRoute.connectionId, profile: tile.ownerRoute.targetProfile ?? tile.ownerRoute.profile }
|
||||
: undefined
|
||||
|
||||
const signatureKey = tileTranscriptSignatureKey(tile)
|
||||
|
||||
try {
|
||||
const latest = await getLatestSessionMessages(storedSessionId)
|
||||
const latest = await getLatestSessionMessages(storedSessionId, profileScope)
|
||||
|
||||
if (requestId !== requestSequenceRef.current || tileRuntimeOwnsLiveState(runtimeSessionId) || !tileStillPresent()) {
|
||||
// Tile closed or superseded mid-read — discard AND prune its
|
||||
// signature so the map doesn't grow one entry per ever-opened tile
|
||||
// for the app's lifetime (#94255 review point 3).
|
||||
signatureRef.current.delete(`tile:${storedSessionId}`)
|
||||
signatureRef.current.delete(signatureKey)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const signatureKey = `tile:${storedSessionId}`
|
||||
const signature = sessionMessagesSignature(latest.messages)
|
||||
|
||||
if (signatureRef.current.get(signatureKey) === signature) {
|
||||
|
||||
Reference in New Issue
Block a user