* Update usage and schedule consumer calculation after stream completion. * * CRITICAL ORDERING: This must be called AFTER the aggregator updates its messages. * If called before, the UI will re-render and read stale data from the aggregator, * causing a race condition where usage appear
(
workspaceId: string,
metadata?: { usage?: LanguageModelV2Usage }
)
| 2697 | * but skipped during history replay to avoid O(N) scheduling overhead |
| 2698 | */ |
| 2699 | private finalizeUsageStats( |
| 2700 | workspaceId: string, |
| 2701 | metadata?: { usage?: LanguageModelV2Usage } |
| 2702 | ): void { |
| 2703 | // During history replay: only bump usage, skip scheduling (caught-up schedules once at end) |
| 2704 | if (this.chatTransientState.get(workspaceId)?.replayingHistory) { |
| 2705 | if (metadata?.usage) { |
| 2706 | this.usageStore.bump(workspaceId); |
| 2707 | } |
| 2708 | return; |
| 2709 | } |
| 2710 | |
| 2711 | // Normal real-time path: always bump usage. |
| 2712 | // |
| 2713 | // Even if total usage is missing (e.g. provider doesn't return it or it timed out), |
| 2714 | // we still need to recompute usage snapshots to: |
| 2715 | // - Clear liveUsage once the active stream ends |
| 2716 | // - Pick up lastContextUsage changes from merged message metadata |
| 2717 | this.usageStore.bump(workspaceId); |
| 2718 | |
| 2719 | // Always schedule consumer calculation (tool calls, text, etc. need tokenization) |
| 2720 | // Even streams without usage metadata need token counts recalculated |
| 2721 | const aggregator = this.aggregators.get(workspaceId); |
| 2722 | if (aggregator) { |
| 2723 | this.consumerManager.scheduleCalculation(workspaceId, aggregator); |
| 2724 | } |
| 2725 | } |
| 2726 | |
| 2727 | private sleepWithAbort(timeoutMs: number, signal: AbortSignal): Promise<void> { |
| 2728 | return new Promise((resolve) => { |
no test coverage detected