From bb25eb927d7225613043826f848d11ae473d0c46 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 16 Jun 2026 19:36:32 -0700 Subject: [PATCH] Revert "fix(chat): align message writer store resolution with reader" This reverts commit 802fecb245991bf7a2f405916d885e2196d1ddc1. --- ...at-message-persistence-store-divergence.md | 5 --- .../src/__tests__/chat-routes.test.ts | 36 +++++-------------- .../src/routes/register-chat-routes.ts | 24 +++---------- 3 files changed, 12 insertions(+), 53 deletions(-) delete mode 100644 .changeset/fix-chat-message-persistence-store-divergence.md diff --git a/.changeset/fix-chat-message-persistence-store-divergence.md b/.changeset/fix-chat-message-persistence-store-divergence.md deleted file mode 100644 index 1f3036f3ba..0000000000 --- a/.changeset/fix-chat-message-persistence-store-divergence.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -Fix regular chat (ChatView) messages disappearing after leaving and returning to a conversation. The chat message **writer** (`POST /api/chat/sessions/:id/messages`, plus cancel and `isGenerating` enrichment) resolved its per-project `ChatManager`/`ChatStore` through `getOrCreateProjectStore`, while the **reader** (`GET /api/chat/sessions/:id/messages`) resolves through the engine-aware `resolveProjectChatContext`. When those resolved to different store instances, a sent message persisted to one store but the reload read from another, so it vanished on return. The writer now resolves through the same `resolveProjectChatContext` path as the reader, guaranteeing writes and reads share one store. Quick Chat masked the bug by keeping its thread warm in memory (no server reload). diff --git a/packages/dashboard/src/__tests__/chat-routes.test.ts b/packages/dashboard/src/__tests__/chat-routes.test.ts index 38380eae06..b8268dc74a 100644 --- a/packages/dashboard/src/__tests__/chat-routes.test.ts +++ b/packages/dashboard/src/__tests__/chat-routes.test.ts @@ -1664,39 +1664,20 @@ describe("multi-project chat routing", () => { vi.restoreAllMocks(); }); - it("POST /cancel resolves the scoped manager via the engine-aware context (same store as the reader)", async () => { - // FNXC:ChatPersistence regression — the scoped writer (cancel/isGenerating/ - // messages) must resolve through resolveProjectChatContext, the SAME engine- - // aware path the reader uses, so writes and reads land in one store. The old - // writer diverged via getOrCreateProjectStore and dropped messages on reload. + it("POST /cancel uses scoped ChatManager when projectId is provided", async () => { mockCancelGeneration.mockReturnValue(false); - const engineChatStore = { ...mockChatStoreInstance }; - const getChatStore = vi.fn(() => engineChatStore); - const getTaskStore = vi.fn(() => store); - const mockEngine = { getChatStore, getTaskStore }; - const getEngine = vi.fn((id: string) => - id === secondarySession.projectId ? mockEngine : undefined, - ); - const { createServer } = await import("../server.js"); - const appWithEngine = createServer(store as any, { - chatStore: mockChatStore as any, - chatManager: mockChatManager as any, - engineManager: { getEngine, getAllEngines: vi.fn().mockReturnValue(new Map()) } as any, - }); const response = await request( - appWithEngine, + app, "POST", `/api/chat/sessions/${secondarySession.id}/cancel?projectId=${secondarySession.projectId}`, ); expect(response.status).toBe(200); - // Writer consulted the engine for this project (engine-aware resolution) - expect(getEngine).toHaveBeenCalledWith(secondarySession.projectId); - expect(getChatStore).toHaveBeenCalled(); - // The divergent getOrCreateProjectStore path is no longer used - expect(mockGetOrCreateProjectStore).not.toHaveBeenCalled(); - // cancelGeneration still runs on the scoped manager + expect((response.body as any).success).toBe(false); + // Scoped path: getOrCreateProjectStore is called with the secondary projectId + expect(mockGetOrCreateProjectStore).toHaveBeenCalledWith(secondarySession.projectId); + // cancelGeneration was called on the scoped manager expect(mockCancelGeneration).toHaveBeenCalledWith(secondarySession.id); }); @@ -1729,9 +1710,8 @@ describe("multi-project chat routing", () => { expect(response.status).toBe(200); expect((response.body as any).sessions).toHaveLength(1); - // FNXC:ChatPersistence — scoped isGenerating no longer resolves through the - // divergent getOrCreateProjectStore path; it shares the reader's store. - expect(mockGetOrCreateProjectStore).not.toHaveBeenCalled(); + // Scoped path: getOrCreateProjectStore is called for isGenerating resolution + expect(mockGetOrCreateProjectStore).toHaveBeenCalledWith(secondarySession.projectId); // isGenerating defaults to false (MockChatManager has no getGeneratingSessionIds) expect((response.body as any).sessions[0].isGenerating).toBe(false); }); diff --git a/packages/dashboard/src/routes/register-chat-routes.ts b/packages/dashboard/src/routes/register-chat-routes.ts index 414e573304..511680c856 100644 --- a/packages/dashboard/src/routes/register-chat-routes.ts +++ b/packages/dashboard/src/routes/register-chat-routes.ts @@ -9,7 +9,8 @@ import { CHAT_ALLOWED_MIME_TYPES, CHAT_MAX_ATTACHMENT_SIZE } from "./chat-attach import { rateLimit, RATE_LIMITS } from "../rate-limit.js"; import { writeSSEEvent, type SessionBufferedEvent } from "../sse-buffer.js"; import type { ApiRoutesContext } from "./types.js"; -import { getOrCreateScopedChatManager } from "../chat-project-services.js"; +import { getOrCreateScopedChatManager, getOrCreateScopedChatStore } from "../chat-project-services.js"; +import { getOrCreateProjectStore } from "../project-store-resolver.js"; interface ChatRouteDeps { parseLastEventId: (req: import("express").Request) => number | undefined; @@ -111,25 +112,8 @@ export function registerChatRoutes(ctx: ApiRoutesContext, deps: ChatRouteDeps): if (!options?.chatManager) throw new ApiError(503, "Chat manager not available"); return options.chatManager; } - /* - FNXC:ChatPersistence 2026-06-16-00:00: - The POST /chat/sessions/:id/messages writer MUST resolve the same per-project ChatStore the - GET /chat/sessions/:id/messages reader uses. Otherwise a sent message persists to one store while - the reload reads another, so the message vanishes when the user leaves and returns to the chat. - The reader (resolveScopedChatStore -> resolveProjectChatContext) prefers the engine's per-project - store/chatStore when an engine exists for the project. This writer previously diverged by going - through getOrCreateProjectStore + a fresh getOrCreateScopedChatStore, bypassing the engine, which - could bind the ChatManager to a different store instance than the reader. Resolve both writer and - reader through resolveProjectChatContext so they always share one store. Quick Chat masked this - bug by keeping its thread warm in memory (no server reload); ChatView reloads from the server on - return and surfaced the missing rows. - */ - const { store: projectStore, chatStore } = await resolveProjectChatContext({ - projectId, - defaultStore: store, - defaultChatStore: options?.chatStore, - engineManager: options?.engineManager, - }); + const projectStore = await getOrCreateProjectStore(projectId); + const chatStore = getOrCreateScopedChatStore(projectStore); return getOrCreateScopedChatManager(projectStore, chatStore, options?.pluginRunner); } // ── Chat Routes ────────────────────────────────────────────────────────────