Revert "fix(chat): align message writer store resolution with reader"

This reverts commit 802fecb245.
This commit is contained in:
gsxdsm
2026-06-16 19:36:32 -07:00
parent 0db81349bb
commit bb25eb927d
3 changed files with 12 additions and 53 deletions

View File

@@ -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).

View File

@@ -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);
});

View File

@@ -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 ────────────────────────────────────────────────────────────