fix: prevent config overwrite, session history race, and streaming message loss (#663)
Co-authored-by: zuolingxuan <zuolingxuan@bytedance.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
8c3a6a5f7a
commit
83858fdf73
@@ -45,7 +45,16 @@ export function subscribeHostEvent<T = unknown>(
|
||||
const listener = (payload: unknown) => {
|
||||
handler(payload as T);
|
||||
};
|
||||
ipc.on(ipcChannel, listener);
|
||||
// preload's `on()` wraps the callback in an internal subscription function
|
||||
// and returns a cleanup function that removes that exact wrapper. We MUST
|
||||
// use the returned cleanup rather than calling `off(channel, listener)`,
|
||||
// because `listener` !== the internal wrapper and removeListener would be
|
||||
// a no-op, leaking the subscription.
|
||||
const unsubscribe = ipc.on(ipcChannel, listener);
|
||||
if (typeof unsubscribe === 'function') {
|
||||
return unsubscribe;
|
||||
}
|
||||
// Fallback for environments where on() doesn't return cleanup
|
||||
return () => {
|
||||
ipc.off(ipcChannel, listener);
|
||||
};
|
||||
|
||||
@@ -1141,6 +1141,10 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
|
||||
switchSession: (key: string) => {
|
||||
if (key === get().currentSessionKey) return;
|
||||
// Stop any background polling for the old session before switching.
|
||||
// This prevents the poll timer from firing after the switch and loading
|
||||
// the wrong session's history into the new session's view.
|
||||
clearHistoryPoll();
|
||||
set((s) => buildSessionSwitchPatch(s, key));
|
||||
get().loadHistory();
|
||||
},
|
||||
@@ -1303,6 +1307,11 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
|
||||
const loadPromise = (async () => {
|
||||
const applyLoadedMessages = (rawMessages: RawMessage[], thinkingLevel: string | null) => {
|
||||
// Guard: if the user switched sessions while this async load was in
|
||||
// flight, discard the result to prevent overwriting the new session's
|
||||
// messages with stale data from the old session.
|
||||
if (get().currentSessionKey !== currentSessionKey) return;
|
||||
|
||||
// Before filtering: attach images/files from tool_result messages to the next assistant message
|
||||
const messagesWithToolImages = enrichWithToolResultFiles(rawMessages);
|
||||
const filteredMessages = messagesWithToolImages.filter((msg) => !isToolResultRole(msg.role));
|
||||
|
||||
@@ -48,6 +48,22 @@ export function handleRuntimeEventState(
|
||||
if (event.message && typeof event.message === 'object') {
|
||||
const msgRole = (event.message as RawMessage).role;
|
||||
if (isToolResultRole(msgRole)) return s.streamingMessage;
|
||||
// During multi-model fallback the Gateway may emit a delta with an
|
||||
// empty or role-only message (e.g. `{}` or `{ role: 'assistant' }`)
|
||||
// to signal a model switch. Accepting such a value would silently
|
||||
// discard all content accumulated so far in streamingMessage.
|
||||
// Only replace when the incoming message carries actual payload.
|
||||
const msgObj = event.message as RawMessage;
|
||||
// During multi-model fallback the Gateway may emit an empty or
|
||||
// role-only delta (e.g. `{}` or `{ role: 'assistant' }`) to signal
|
||||
// a model switch. If we already have accumulated streaming content,
|
||||
// accepting such a message would silently discard it. Only guard
|
||||
// when there IS existing content to protect; when streamingMessage
|
||||
// is still null, let any delta through so the UI can start showing
|
||||
// the typing indicator immediately.
|
||||
if (s.streamingMessage && msgObj.content === undefined) {
|
||||
return s.streamingMessage;
|
||||
}
|
||||
}
|
||||
return event.message ?? s.streamingMessage;
|
||||
})(),
|
||||
|
||||
Reference in New Issue
Block a user