From d95709833b33cba5258ef41b8ab01c13032e00a0 Mon Sep 17 00:00:00 2001 From: Charan Rathore <180254320+charan-rathore@users.noreply.github.com> Date: Thu, 8 Oct 2026 12:11:23 +0530 Subject: [PATCH] fix(pages): do not bind a conversation when its page is deleted during thread creation PageService.conversation awaits remote Intelligence thread creation. If the page is deleted in that interval, the reservation is gone but the response still binds a visible orphan local conversation and returns success. Recheck the page after the await and before the local bind and ready step so a deleted page rejects with Page not found instead. The existing catch still releases the pending reservation and the map cleanup still runs; a remotely created thread cannot be undone, this prevents the local orphan success. Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com> --- src/server/page-service.ts | 3 ++ tests/page-conversation-deletion.test.ts | 37 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 tests/page-conversation-deletion.test.ts diff --git a/src/server/page-service.ts b/src/server/page-service.ts index 4b55f817..b103ad8a 100644 --- a/src/server/page-service.ts +++ b/src/server/page-service.ts @@ -75,6 +75,9 @@ export class PageService { name: page.title, }), ); + // The page may have been deleted while Intelligence created its thread. + // Recheck before binding a local conversation or marking it ready. + this.workspace.pages.get(spaceId, pageId); if (!this.workspace.canAccessSpace(dotId, spaceId)) throw new PageError('Space access has been revoked.'); const thread = diff --git a/tests/page-conversation-deletion.test.ts b/tests/page-conversation-deletion.test.ts new file mode 100644 index 00000000..1689168f --- /dev/null +++ b/tests/page-conversation-deletion.test.ts @@ -0,0 +1,37 @@ +import { expect, it, vi } from 'vitest'; +import { WorkspaceStore } from '../src/server/workspace.js'; +import { PageService } from '../src/server/page-service.js'; + +it('does not bind a conversation when its page is deleted during thread creation', async () => { + const ws = new WorkspaceStore(':memory:', 'owner'); + try { + const dot = ws.dots()[0]; + const page = ws.pages.create(dot.spaceId, { title: 'Temporary' }); + let resolve!: () => void; + const remote = new Promise((done) => { + resolve = done; + }); + const getOrCreateThread = vi.fn(() => remote); + const service = new PageService(ws, () => ({ + getOrCreateThread, + getThreadMessages: async () => ({ messages: [] }), + })); + const pending = service.conversation(dot.spaceId, page.id, dot.id); + const outcome = expect(pending).rejects.toThrow(/Page not found/); + expect(getOrCreateThread).toHaveBeenCalledTimes(1); + ws.pages.delete(dot.spaceId, page.id); + resolve(); + await outcome; + expect(ws.conversations()).toHaveLength(0); + expect(ws.pages.thread(page.id, dot.id)).toBeUndefined(); + const replacement = ws.pages.create(dot.spaceId, { title: 'Replacement' }); + const thread = await service.conversation( + dot.spaceId, + replacement.id, + dot.id, + ); + expect(ws.pages.forThread(thread.id)?.id).toBe(replacement.id); + } finally { + ws.close(); + } +});