From 1a774592ff06f55c2da27ab7ba3582949d206340 Mon Sep 17 00:00:00 2001 From: digua Date: Thu, 6 Aug 2026 14:49:15 +0800 Subject: [PATCH] fix(electron): close worker database before deleting sessions --- apps/desktop/main/internal-api/server.ts | 5 +++++ packages/http-routes/src/context/runtime.ts | 3 +++ .../http-routes/src/routes/shared-routes.test.ts | 14 ++++++++++++-- packages/http-routes/src/routes/web/sessions.ts | 3 ++- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/desktop/main/internal-api/server.ts b/apps/desktop/main/internal-api/server.ts index c1df5089..ff494c5b 100644 --- a/apps/desktop/main/internal-api/server.ts +++ b/apps/desktop/main/internal-api/server.ts @@ -44,6 +44,7 @@ import { assertDesktopDataDirCompatible, getDesktopAppVersion } from '../runtime import { resolveDesktopNativeBinding } from '../runtime/native-sqlite' import { resolveModelDownloadProxyUrl } from '../network/proxy' import { getDefaultUserDataDir, getDownloadsDir, getUserDataDir } from '../paths/locations' +import { closeDatabase as closeWorkerDatabase } from '../worker/workerManager' import { configureInternalHttpServer } from './http' export interface InternalEndpoint { @@ -148,6 +149,10 @@ export async function startInternalServer( dbManager: newDbManager, sessionAdapter, pathProvider, + beforeDeleteSession: async (sessionId) => { + // The Desktop worker caches readonly SQLite handles; Windows cannot unlink an open database file. + await closeWorkerDatabase(sessionId) + }, runtimeIdentity: runtime, nativeBinding, getVersion: () => getDesktopAppVersion(app.getVersion()), diff --git a/packages/http-routes/src/context/runtime.ts b/packages/http-routes/src/context/runtime.ts index 02882ee1..5c4b0e3d 100644 --- a/packages/http-routes/src/context/runtime.ts +++ b/packages/http-routes/src/context/runtime.ts @@ -8,6 +8,9 @@ export interface RuntimeRouteContext { pathProvider: PathProvider getVersion: () => string + /** Close platform-owned database handles before deleting a session. */ + beforeDeleteSession?: (sessionId: string) => void | Promise + runtimeIdentity?: RuntimeIdentity /** Native binding path for better-sqlite3 (CLI native copy / Electron-ABI desktop copy). */ nativeBinding?: string diff --git a/packages/http-routes/src/routes/shared-routes.test.ts b/packages/http-routes/src/routes/shared-routes.test.ts index ac32253e..0c208cb5 100644 --- a/packages/http-routes/src/routes/shared-routes.test.ts +++ b/packages/http-routes/src/routes/shared-routes.test.ts @@ -569,11 +569,20 @@ describe('registerSharedRoutes smoke tests', () => { assert.equal(meta.name, 'Renamed Chat') }) - it('DELETE /_web/sessions/:id delegates to the session adapter', async () => { + it('DELETE /_web/sessions/:id closes platform handles before deleting the session', async () => { const db = createSessionDb() const dbs = new Map([['chat-1', db]]) + const lifecycle: string[] = [] const routeApp = Fastify() - registerSessionRoutes(routeApp, createTestContext(dbs)) + const ctx = createTestContext(dbs) + ctx.beforeDeleteSession = async (sessionId) => { + lifecycle.push(`close:${sessionId}`) + } + ctx.sessionAdapter.deleteSessionFile = (sessionId) => { + lifecycle.push(`delete:${sessionId}`) + return lifecycle[0] === `close:${sessionId}` && dbs.delete(sessionId) + } + registerSessionRoutes(routeApp, ctx) await routeApp.ready() const resp = await routeApp.inject({ method: 'DELETE', url: '/_web/sessions/chat-1' }) @@ -584,6 +593,7 @@ describe('registerSharedRoutes smoke tests', () => { assert.equal(resp.statusCode, 200) assert.deepEqual(resp.json(), { success: true }) assert.equal(dbs.has('chat-1'), false) + assert.deepEqual(lifecycle, ['close:chat-1', 'delete:chat-1']) }) it('owner profile routes select, apply and dismiss across sessions', async () => { diff --git a/packages/http-routes/src/routes/web/sessions.ts b/packages/http-routes/src/routes/web/sessions.ts index 5409f857..21069e78 100644 --- a/packages/http-routes/src/routes/web/sessions.ts +++ b/packages/http-routes/src/routes/web/sessions.ts @@ -4,7 +4,7 @@ import type { RuntimeRouteContext } from '../../context/runtime' import type { ServiceRouteContext } from '../../context/services' import { sessionService, ownerProfileService, PreferencesManager } from '@openchatlab/node-runtime' -type SessionRouteContext = Pick & +type SessionRouteContext = Pick & Pick & Pick @@ -41,6 +41,7 @@ export function registerSessionRoutes(server: FastifyInstance, ctx: SessionRoute server.delete<{ Params: { id: string } }>('/_web/sessions/:id', async (request, reply) => { const { id } = request.params try { + await ctx.beforeDeleteSession?.(id) const deleted = sessionService.deleteSession(adapter, id) if (!deleted) { return reply.code(404).send({ success: false, error: 'File not found' })