From f4781e2b3b2086a50bf7693119e9c2104e38b017 Mon Sep 17 00:00:00 2001 From: qer Date: Tue, 23 Jun 2026 00:02:02 +0800 Subject: [PATCH 1/4] fix(web): clear all per-session state when archiving or removing a session --- .changeset/fix-web-forget-session.md | 5 +++ .../composables/client/useWorkspaceState.ts | 6 +-- .../src/composables/useKimiWebClient.ts | 39 ++++++++++++------- 3 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 .changeset/fix-web-forget-session.md diff --git a/.changeset/fix-web-forget-session.md b/.changeset/fix-web-forget-session.md new file mode 100644 index 0000000000..c175aed4ec --- /dev/null +++ b/.changeset/fix-web-forget-session.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Clear all per-session state when a session is archived or removed, so archived sessions no longer leave orphaned data behind. diff --git a/apps/kimi-web/src/composables/client/useWorkspaceState.ts b/apps/kimi-web/src/composables/client/useWorkspaceState.ts index 54232f260d..c590e5e4f2 100644 --- a/apps/kimi-web/src/composables/client/useWorkspaceState.ts +++ b/apps/kimi-web/src/composables/client/useWorkspaceState.ts @@ -64,7 +64,7 @@ export interface UseWorkspaceStateDeps { updateSession: (id: string, update: (session: AppSession) => AppSession) => void; upsertSessionFront: (session: AppSession) => void; appendSession: (session: AppSession) => void; - removeSession: (id: string) => void; + forgetSession: (id: string) => void; setActiveSessionId: (id: string | undefined) => void; /** Update one session's message list via a function of the current list. */ updateSessionMessages: ( @@ -110,7 +110,7 @@ export function useWorkspaceState(rawState: ExtendedState, deps: UseWorkspaceSta updateSession, upsertSessionFront, appendSession, - removeSession, + forgetSession, setActiveSessionId, updateSessionMessages, nextOptimisticMsgId, @@ -1322,7 +1322,7 @@ export function useWorkspaceState(rawState: ExtendedState, deps: UseWorkspaceSta try { const api = getKimiWebApi(); await api.archiveSession(id); - removeSession(id); + forgetSession(id); sideChat.clearSideChatForSession(id); const { [id]: _removedIds, ...restIds } = rawState.sideChatUserMessageIdsBySession; void _removedIds; diff --git a/apps/kimi-web/src/composables/useKimiWebClient.ts b/apps/kimi-web/src/composables/useKimiWebClient.ts index 33d58de50d..f55c38b6d1 100644 --- a/apps/kimi-web/src/composables/useKimiWebClient.ts +++ b/apps/kimi-web/src/composables/useKimiWebClient.ts @@ -442,6 +442,28 @@ function removeSessionMessages(sessionId: string): void { rawState.messagesBySession = rest; } +// --------------------------------------------------------------------------- +// Session teardown — single place that wipes a session and all its per-session +// sidecar state. Both removal entry points (not-found + archive) go through +// this, so adding a new per-session map only ever needs one new line here. +// --------------------------------------------------------------------------- +function forgetSession(sessionId: string): void { + removeSession(sessionId); + removeSessionMessages(sessionId); + delete rawState.approvalsBySession[sessionId]; + delete rawState.questionsBySession[sessionId]; + delete rawState.tasksBySession[sessionId]; + delete rawState.goalBySession[sessionId]; + delete rawState.gitStatusBySession[sessionId]; + delete rawState.lastSeqBySession[sessionId]; + delete rawState.compactionBySession[sessionId]; + delete rawState.messagesLoadingMoreBySession[sessionId]; + delete rawState.messagesHasMoreBySession[sessionId]; + delete rawState.messagesLoadMoreErrorBySession[sessionId]; + delete epochBySession[sessionId]; + sessionsKnownEmpty.delete(sessionId); +} + // Models + Providers reactive state and helpers live in // ./client/useModelProviderState. It is instantiated below (after the // `activity` computed it depends on) as `modelProvider`. @@ -894,20 +916,7 @@ function goalErrorMessage(err: unknown): string | undefined { } async function handleSessionNotFound(sessionId: string): Promise { - removeSession(sessionId); - removeSessionMessages(sessionId); - delete rawState.approvalsBySession[sessionId]; - delete rawState.questionsBySession[sessionId]; - delete rawState.tasksBySession[sessionId]; - delete rawState.goalBySession[sessionId]; - delete rawState.gitStatusBySession[sessionId]; - delete rawState.lastSeqBySession[sessionId]; - delete rawState.compactionBySession[sessionId]; - delete rawState.messagesLoadingMoreBySession[sessionId]; - delete rawState.messagesHasMoreBySession[sessionId]; - delete rawState.messagesLoadMoreErrorBySession[sessionId]; - delete epochBySession[sessionId]; - sessionsKnownEmpty.delete(sessionId); + forgetSession(sessionId); if (rawState.activeSessionId !== sessionId) return; @@ -1835,7 +1844,7 @@ const workspaceState = useWorkspaceState(rawState, { updateSession, upsertSessionFront, appendSession, - removeSession, + forgetSession, setActiveSessionId, updateSessionMessages, nextOptimisticMsgId, From 40defb6f91af0783811e46bdf3eb3feaf17d0e4d Mon Sep 17 00:00:00 2001 From: qer Date: Tue, 23 Jun 2026 00:09:49 +0800 Subject: [PATCH 2/4] fix(web): clear queued and in-flight prompt state in session teardown --- apps/kimi-web/src/composables/useKimiWebClient.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/kimi-web/src/composables/useKimiWebClient.ts b/apps/kimi-web/src/composables/useKimiWebClient.ts index f55c38b6d1..8d3e58d427 100644 --- a/apps/kimi-web/src/composables/useKimiWebClient.ts +++ b/apps/kimi-web/src/composables/useKimiWebClient.ts @@ -462,6 +462,14 @@ function forgetSession(sessionId: string): void { delete rawState.messagesLoadMoreErrorBySession[sessionId]; delete epochBySession[sessionId]; sessionsKnownEmpty.delete(sessionId); + // In-flight / queued prompt state: drop these too so a queued follow-up + // can't be submitted to a session that was just archived when its turn later + // goes idle (onSessionIdle drains queuedBySession[sid] without re-checking + // that the session still exists). + inFlightPromptSessions.delete(sessionId); + delete rawState.queuedBySession[sessionId]; + delete rawState.promptIdBySession[sessionId]; + delete rawState.sendingBySession[sessionId]; } // Models + Providers reactive state and helpers live in From 134e896f6837668a30c7c08c5b59f3c9b04cb220 Mon Sep 17 00:00:00 2001 From: qer Date: Tue, 23 Jun 2026 00:42:54 +0800 Subject: [PATCH 3/4] fix(web): unsubscribe session before clearing its state in teardown --- apps/kimi-web/src/composables/useKimiWebClient.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/kimi-web/src/composables/useKimiWebClient.ts b/apps/kimi-web/src/composables/useKimiWebClient.ts index 8d3e58d427..1f04a3f563 100644 --- a/apps/kimi-web/src/composables/useKimiWebClient.ts +++ b/apps/kimi-web/src/composables/useKimiWebClient.ts @@ -448,6 +448,10 @@ function removeSessionMessages(sessionId: string): void { // this, so adding a new per-session map only ever needs one new line here. // --------------------------------------------------------------------------- function forgetSession(sessionId: string): void { + // Stop receiving events for this session BEFORE clearing its state: a late or + // buffered event for this id would otherwise be reduced and recreate the very + // per-session maps we are about to delete. + eventConn?.unsubscribe(sessionId); removeSession(sessionId); removeSessionMessages(sessionId); delete rawState.approvalsBySession[sessionId]; From 02266ed31e007e30b9c15718a2fe7991506ea258 Mon Sep 17 00:00:00 2001 From: qer Date: Tue, 23 Jun 2026 01:18:39 +0800 Subject: [PATCH 4/4] fix(web): cancel pending WS subscriptions on unsubscribe --- apps/kimi-web/src/api/daemon/ws.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/kimi-web/src/api/daemon/ws.ts b/apps/kimi-web/src/api/daemon/ws.ts index b59e2f238e..ab4fd15eb5 100644 --- a/apps/kimi-web/src/api/daemon/ws.ts +++ b/apps/kimi-web/src/api/daemon/ws.ts @@ -160,6 +160,10 @@ export class DaemonEventSocket { /** Unsubscribe from a session's events. */ unsubscribe(sessionId: string): void { this.subscriptions.delete(sessionId); + // Also cancel a subscribe that was queued before server_hello; otherwise + // onServerHello would merge it back into the active subscription set. + const pendingIdx = this.pendingSubscriptions.findIndex((p) => p.sessionId === sessionId); + if (pendingIdx !== -1) this.pendingSubscriptions.splice(pendingIdx, 1); if (this.connected && this.ws) { this.send({ type: 'unsubscribe',