From 1073aef83d30f3415e5b93bc3fc7d317c1f44100 Mon Sep 17 00:00:00 2001 From: Battleplus <3559424769@qq.com> Date: Thu, 27 Aug 2026 19:21:39 +0800 Subject: [PATCH 1/2] fix(desktop): use .run() for CAS clear of stale sdk_session_id (#3496) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The drizzle proxy over IPC does not serialize .returning() for UPDATE queries — it always returns []. This caused compareAndClearSdkSessionId() to report CAS failure even when the UPDATE succeeded, blocking the self-healing path for old Claude sessions that hit "No conversation found". Replace .returning() with .run() and check result.changes, matching the existing pattern in sessions.ts:1323. Also update the test mock to use .run() and split into three focused regression tests: CAS hit, CAS miss, and concurrent hit-then-miss. Fixes #3496 Signed-off-by: Battleplus <3559424769@qq.com> 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- .../sessionStorageRemoteHostId.test.ts | 22 ++++++++++++++----- .../src/main/maker-host/session-storage.ts | 6 ++--- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/main/maker-host/__tests__/sessionStorageRemoteHostId.test.ts b/apps/desktop/src/main/maker-host/__tests__/sessionStorageRemoteHostId.test.ts index e9cc40acfb..6b9d818510 100644 --- a/apps/desktop/src/main/maker-host/__tests__/sessionStorageRemoteHostId.test.ts +++ b/apps/desktop/src/main/maker-host/__tests__/sessionStorageRemoteHostId.test.ts @@ -13,7 +13,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; const h = vi.hoisted(() => ({ captured: null as Record | null, updateSet: null as Record | null, - updateReturning: [] as Array<{ id: string }>, + runResult: { changes: 0 } as { changes: number }, whereCalled: false, })); @@ -32,7 +32,7 @@ vi.mock('../../localDb/client/current.js', () => ({ return { where: () => { h.whereCalled = true; - return { returning: async () => h.updateReturning }; + return { run: async () => h.runResult }; }, }; }, @@ -160,17 +160,27 @@ describe('DesktopSessionStorage.create workingDir 规范化', () => { describe('DesktopSessionStorage.compareAndClearSdkSessionId', () => { beforeEach(() => { h.updateSet = null; - h.updateReturning = []; + h.runResult = { changes: 0 }; h.whereCalled = false; }); - it('用单条条件 update 清空旧 id,并按 returning 报告 CAS 是否命中', async () => { + it('CAS hit: .run() returns changes=1, method returns true', async () => { const storage = new DesktopSessionStorage(); - h.updateReturning = [{ id: 'session-1' }]; + h.runResult = { changes: 1 }; await expect(storage.compareAndClearSdkSessionId('session-1', 'sdk-old')).resolves.toBe(true); expect(h.updateSet?.sdkSessionId).toBeNull(); expect(h.updateSet?.updatedAt).toEqual(expect.any(Number)); expect(h.whereCalled).toBe(true); - h.updateReturning = []; + }); + it('CAS miss: .run() returns changes=0, method returns false, preserves new ID', async () => { + const storage = new DesktopSessionStorage(); + h.runResult = { changes: 0 }; await expect(storage.compareAndClearSdkSessionId('session-1', 'sdk-stale')).resolves.toBe(false); }); + it('concurrent path: hit then miss, each call correct', async () => { + const storage = new DesktopSessionStorage(); + h.runResult = { changes: 1 }; + await expect(storage.compareAndClearSdkSessionId('s1', 'sdk-a')).resolves.toBe(true); + h.runResult = { changes: 0 }; + await expect(storage.compareAndClearSdkSessionId('s1', 'sdk-a')).resolves.toBe(false); + }); }); diff --git a/apps/desktop/src/main/maker-host/session-storage.ts b/apps/desktop/src/main/maker-host/session-storage.ts index 90640ef959..82eba2edf8 100644 --- a/apps/desktop/src/main/maker-host/session-storage.ts +++ b/apps/desktop/src/main/maker-host/session-storage.ts @@ -142,12 +142,12 @@ export class DesktopSessionStorage implements SessionStorage { expectedSdkSessionId: string, ): Promise { const db = getDbClient().drizzle; - const changed = await db + const result = await db .update(sessions) .set({ sdkSessionId: null, updatedAt: Date.now() }) .where(and(eq(sessions.id, id), eq(sessions.sdkSessionId, expectedSdkSessionId))) - .returning({ id: sessions.id }); - return changed.length > 0; + .run(); + return result.changes > 0; } async delete(id: string): Promise { From bffb7edf3c1c969637708e42b7ea8df9835da85f Mon Sep 17 00:00:00 2001 From: Battleplus <3559424769@qq.com> Date: Thu, 27 Aug 2026 21:02:37 +0800 Subject: [PATCH 2/2] docs: re-trigger format gate check Signed-off-by: Battleplus <3559424769@qq.com>