* Reset derived UI state for a workspace so a fresh onChat replay can rebuild it. * * This is used when an onChat subscription ends unexpectedly (MessagePort/WebSocket hiccup). * Without clearing, replayed history would be merged into stale state (loadHistoricalMessages * only adds/overw
(workspaceId: string)
| 3354 | * only adds/overwrites, it doesn't delete messages that disappeared due to compaction/truncation). |
| 3355 | */ |
| 3356 | private resetChatStateForReplay(workspaceId: string): void { |
| 3357 | const aggregator = this.aggregators.get(workspaceId); |
| 3358 | if (!aggregator) { |
| 3359 | return; |
| 3360 | } |
| 3361 | |
| 3362 | // Clear any pending UI bumps from deltas - we're about to rebuild the message list. |
| 3363 | this.cancelPendingIdleBump(workspaceId); |
| 3364 | this.cancelPendingStreamingBump(workspaceId); |
| 3365 | |
| 3366 | // Preserve last-known usage while replay rebuilds the aggregator. |
| 3367 | // Without this, getWorkspaceUsage() can briefly return an empty state and hide |
| 3368 | // context/cost indicators until replayed usage catches up. |
| 3369 | const currentUsage = this.getWorkspaceUsage(workspaceId); |
| 3370 | const hasUsageSnapshot = |
| 3371 | currentUsage.totalTokens > 0 || |
| 3372 | currentUsage.lastContextUsage !== undefined || |
| 3373 | currentUsage.liveUsage !== undefined || |
| 3374 | currentUsage.liveCostUsage !== undefined; |
| 3375 | if (hasUsageSnapshot) { |
| 3376 | this.preReplayUsageSnapshot.set(workspaceId, currentUsage); |
| 3377 | } else { |
| 3378 | this.preReplayUsageSnapshot.delete(workspaceId); |
| 3379 | } |
| 3380 | |
| 3381 | aggregator.resetForReplay(); |
| 3382 | |
| 3383 | // Reset per-workspace transient state so the next replay rebuilds from the backend source of truth. |
| 3384 | const previousTransient = this.chatTransientState.get(workspaceId); |
| 3385 | const nextTransient = createInitialChatTransientState(); |
| 3386 | |
| 3387 | // Preserve active hydration across full replay resets so workspace-switch catch-up |
| 3388 | // remains in loading state until we receive an authoritative caught-up marker. |
| 3389 | if (previousTransient?.isHydratingTranscript) { |
| 3390 | nextTransient.isHydratingTranscript = true; |
| 3391 | } |
| 3392 | |
| 3393 | this.chatTransientState.set(workspaceId, nextTransient); |
| 3394 | |
| 3395 | this.historyPagination.set(workspaceId, createInitialHistoryPaginationState()); |
| 3396 | |
| 3397 | this.states.bump(workspaceId); |
| 3398 | this.checkAndBumpRecencyIfChanged(); |
| 3399 | } |
| 3400 | |
| 3401 | private getStartupAutoCompactionThreshold( |
| 3402 | workspaceId: string, |
no test coverage detected