mirror of
https://github.com/open-webui/open-webui.git
synced 2026-09-16 23:43:03 +00:00
fix(stream): harden done-detection and isolate resume seq from persisted state
Two review findings:
1. Backend: the `done:True` detection in get_event_emitter called
`.get('done')` on whatever `event_data['data']` happened to be. For
most event types that's a dict, but some custom/pipeline events can
legitimately emit non-dict inner payloads (list/string/None), which
would raise AttributeError and break emission for that event. Now
narrowed properly:
inner = event_data.get('data') if isinstance(event_data, dict) else None
if isinstance(inner, dict) and inner.get('done') is True:
...
2. Frontend: previously stored `message.lastSeq = incomingSeq` directly
on the message object, which is part of `history` and gets serialized
by saveChatHandler. That leaked transport-level resume metadata into
persisted chat state. Moved bookkeeping into a component-local
`resumeSeqByMessageId: Map<string, number>` so it stays in memory
only and never touches the persisted schema. All reads/writes of
lastSeq now go through the map.
This commit is contained in:
parent
dec91764ed
commit
7dc4d843fa
2 changed files with 22 additions and 8 deletions
|
|
@ -1022,7 +1022,11 @@ async def get_event_emitter(request_info, update_db=True):
|
|||
# reconnecting clients a short grace window to pick up the final
|
||||
# frames before we delete the log (for anything beyond the grace
|
||||
# window, the DB is already up to date and resume isn't needed).
|
||||
if isinstance(event_data, dict) and event_data.get('data', {}).get('done') is True:
|
||||
# Narrow carefully: `event_data['data']` is a dict for most event
|
||||
# types but can legitimately be a list/str/None for some custom
|
||||
# pipeline-emitted events. Calling `.get` on those would raise.
|
||||
inner = event_data.get('data') if isinstance(event_data, dict) else None
|
||||
if isinstance(inner, dict) and inner.get('done') is True:
|
||||
async def _delayed_truncate(mid):
|
||||
try:
|
||||
await asyncio.sleep(30)
|
||||
|
|
|
|||
|
|
@ -169,6 +169,13 @@
|
|||
|
||||
let taskIds = null;
|
||||
|
||||
// Per-message transport seq for the stream-resume protocol. Kept as a
|
||||
// local Map (not a field on the message object) so this ephemeral
|
||||
// bookkeeping never leaks into `history`, which is serialized and
|
||||
// persisted via saveChatHandler. Values are refreshed in the WS event
|
||||
// handler and consulted when requesting resume after a reconnect.
|
||||
const resumeSeqByMessageId = new Map();
|
||||
|
||||
// Chat Input
|
||||
let prompt = '';
|
||||
let chatFiles = [];
|
||||
|
|
@ -453,14 +460,16 @@
|
|||
// mid-stream we can request a replay of only what we missed.
|
||||
// Also drop out-of-order replays (seq <= lastSeq) to keep
|
||||
// delta appends idempotent when live frames race replayed
|
||||
// frames after a reconnect.
|
||||
// frames after a reconnect. Bookkeeping lives in the local
|
||||
// `resumeSeqByMessageId` Map (not on the message object)
|
||||
// to keep this transport metadata out of persisted state.
|
||||
const incomingSeq = typeof event?.seq === 'number' ? event.seq : null;
|
||||
if (incomingSeq !== null) {
|
||||
const lastSeq = message.lastSeq ?? 0;
|
||||
const lastSeq = resumeSeqByMessageId.get(event.message_id) ?? 0;
|
||||
if (incomingSeq <= lastSeq) {
|
||||
return;
|
||||
}
|
||||
message.lastSeq = incomingSeq;
|
||||
resumeSeqByMessageId.set(event.message_id, incomingSeq);
|
||||
}
|
||||
|
||||
const type = event?.data?.type ?? null;
|
||||
|
|
@ -627,16 +636,17 @@
|
|||
// Ask the server to replay any WS events we missed for the given message.
|
||||
// Used when loading a chat with a message still in progress (page refresh
|
||||
// mid-stream) and when the socket reconnects after a drop. The server
|
||||
// re-emits any events we haven't seen (seq > message.lastSeq) as normal
|
||||
// `events` frames; no separate ack. Safe to call redundantly — the seq
|
||||
// idempotency guard in chatEventHandler drops anything already applied.
|
||||
// re-emits any events we haven't seen (seq > the client's tracked seq)
|
||||
// as normal `events` frames; no separate ack. Safe to call redundantly
|
||||
// — the seq idempotency guard in chatEventHandler drops anything
|
||||
// already applied.
|
||||
const requestResumeForMessage = (message) => {
|
||||
if (!message || !message.id || message.done) return;
|
||||
if (!$socket || !$socket.connected) return;
|
||||
$socket.emit('resume-stream', {
|
||||
chat_id: $chatId,
|
||||
message_id: message.id,
|
||||
last_seq: message.lastSeq ?? 0
|
||||
last_seq: resumeSeqByMessageId.get(message.id) ?? 0
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue