Python: Fix DevUI streaming memory growth regression (#6038)

* Fix DevUI streaming memory growth regression

Bounds retained streaming/debug state in DevUI and strengthens browser regression coverage for long streamed responses.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address DevUI memory review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix DevUI bundle trailing whitespace

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-05-27 09:48:29 +02:00
committed by GitHub
Unverified
parent e1e6e3d35e
commit 3242d8a4c4
8 changed files with 259 additions and 512 deletions
@@ -519,6 +519,7 @@ class ApiClient {
lastMessageId,
lastSequenceNumber,
accumulatedText: storedState?.accumulatedText,
accumulatedTextIsPreview: storedState?.accumulatedTextIsPreview,
}),
event,
currentResponseId,
@@ -15,11 +15,13 @@ export interface StreamingState {
lastSequenceNumber: number;
timestamp: number; // When this state was last updated
completed: boolean; // Whether the stream completed successfully
accumulatedText?: string; // Accumulated text content for quick restoration
accumulatedText?: string; // Bounded tail preview for refresh restoration
accumulatedTextIsPreview?: boolean;
}
const STORAGE_KEY_PREFIX = "devui_streaming_state_";
const STATE_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
const MAX_ACCUMULATED_TEXT_PREVIEW_CHARS = 16 * 1024;
interface CreateStreamingStateOptions {
conversationId: string;
@@ -27,6 +29,7 @@ interface CreateStreamingStateOptions {
lastMessageId?: string;
lastSequenceNumber?: number;
accumulatedText?: string;
accumulatedTextIsPreview?: boolean;
}
/**
@@ -36,6 +39,21 @@ function getStorageKey(conversationId: string): string {
return `${STORAGE_KEY_PREFIX}${conversationId}`;
}
function normalizeAccumulatedTextPreview(state: StreamingState): StreamingState {
if (
state.accumulatedText === undefined ||
state.accumulatedText.length <= MAX_ACCUMULATED_TEXT_PREVIEW_CHARS
) {
return state;
}
return {
...state,
accumulatedText: state.accumulatedText.slice(-MAX_ACCUMULATED_TEXT_PREVIEW_CHARS),
accumulatedTextIsPreview: true,
};
}
/**
* Read raw streaming state from storage, including completed entries.
*/
@@ -56,7 +74,7 @@ function readStreamingState(conversationId: string): StreamingState | null {
return null;
}
return state;
return normalizeAccumulatedTextPreview(state);
}
/**
@@ -68,8 +86,9 @@ export function createStreamingState({
lastMessageId,
lastSequenceNumber = -1,
accumulatedText,
accumulatedTextIsPreview = false,
}: CreateStreamingStateOptions): StreamingState {
return {
return normalizeAccumulatedTextPreview({
conversationId,
responseId,
lastMessageId,
@@ -77,7 +96,8 @@ export function createStreamingState({
timestamp: Date.now(),
completed: false,
accumulatedText,
};
accumulatedTextIsPreview,
});
}
/**
@@ -108,7 +128,15 @@ export function applyStreamingEventToState(
typeof event.delta === "string" &&
event.delta.length > 0
) {
nextState.accumulatedText = `${state.accumulatedText ?? ""}${event.delta}`;
const accumulatedText = `${state.accumulatedText ?? ""}${event.delta}`;
const isPreview =
state.accumulatedTextIsPreview ||
accumulatedText.length > MAX_ACCUMULATED_TEXT_PREVIEW_CHARS;
nextState.accumulatedText = isPreview
? accumulatedText.slice(-MAX_ACCUMULATED_TEXT_PREVIEW_CHARS)
: accumulatedText;
nextState.accumulatedTextIsPreview = isPreview;
}
return nextState;
@@ -120,7 +148,7 @@ export function applyStreamingEventToState(
export function saveStreamingState(state: StreamingState): void {
try {
const key = getStorageKey(state.conversationId);
const data = JSON.stringify(state);
const data = JSON.stringify(normalizeAccumulatedTextPreview(state));
localStorage.setItem(key, data);
} catch (error) {
console.error("Failed to save streaming state:", error);
@@ -129,7 +157,7 @@ export function saveStreamingState(state: StreamingState): void {
clearExpiredStreamingStates();
// Try again
const key = getStorageKey(state.conversationId);
const data = JSON.stringify(state);
const data = JSON.stringify(normalizeAccumulatedTextPreview(state));
localStorage.setItem(key, data);
} catch {
console.error("Failed to save streaming state even after cleanup");