From 5e93f06343bd5efdcae8395d670e183c553f356e Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 9 Sep 2026 15:49:21 -0400 Subject: [PATCH] Fix mobile legacy inventory reset and stale completion regression Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --- ...ost-session-native-chat-operations.test.ts | 40 +++++++++++++--- ...ive-host-session-native-chat-operations.ts | 47 +++++++++++-------- 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/mobile/src/session/native-host-session-native-chat-operations.test.ts b/mobile/src/session/native-host-session-native-chat-operations.test.ts index 7b4522b74cf..974e9f18e62 100644 --- a/mobile/src/session/native-host-session-native-chat-operations.test.ts +++ b/mobile/src/session/native-host-session-native-chat-operations.test.ts @@ -22,6 +22,8 @@ function client(sendRequest: RpcClient['sendRequest']): RpcClient { describe('native host session native chat operations', () => { it('stops the agent with a bare Escape that carries no enter at all', async () => { const sendRequest = vi.fn().mockResolvedValue({ + id: 'test', + _meta: { runtimeId: 'host' }, ok: true, result: { delivered: true } }) @@ -40,6 +42,8 @@ describe('native host session native chat operations', () => { // The shared chat write refuses to start under a 2s residual budget. Stop does not: the // call it replaced tried on whatever was left and could be accepted. const sendRequest = vi.fn().mockResolvedValue({ + id: 'test', + _meta: { runtimeId: 'host' }, ok: true, result: { send: { accepted: true } } }) @@ -61,33 +65,57 @@ describe('native host session native chat operations', () => { }) it('ignores an older inventory completion after cache reset', async () => { - const deferred: Array<(value: any) => void> = [] + const deferred: Array<(value: Awaited>) => void> = [] const sendRequest = vi.fn((method) => { - if (method === 'files.searchPaths') + if (method === 'files.searchPaths') { return Promise.resolve({ + id: 'test', + _meta: { runtimeId: 'host' }, ok: false, error: { code: 'method_not_found', message: 'unsupported' } }) + } return new Promise((resolve) => deferred.push(resolve)) }) const operations = nativeHostSessionNativeChatOperations(client(sendRequest)) const old = operations.searchFiles(target(), 'fresh') + await vi.waitFor(() => expect(deferred).toHaveLength(1)) + const overlapping = operations.searchFiles(target(), 'old') operations.resetFileSearchCache('wt-1') const fresh = operations.searchFiles(target(), 'fresh') - deferred[1]?.({ ok: true, result: { files: [{ relativePath: 'fresh.ts' }] } }) + await vi.waitFor(() => expect(deferred).toHaveLength(2)) + deferred[1]!({ + id: 'test', + _meta: { runtimeId: 'host' }, + ok: true, + result: { files: [{ relativePath: 'fresh.ts' }] } + }) await expect(fresh).resolves.toEqual(['fresh.ts']) - deferred[0]?.({ ok: true, result: { files: [{ relativePath: 'old.ts' }] } }) - await old + deferred[0]!({ + id: 'test', + _meta: { runtimeId: 'host' }, + ok: true, + result: { files: [{ relativePath: 'old.ts' }] } + }) + await expect(old).resolves.toBeNull() + await expect(overlapping).resolves.toBeNull() await expect(operations.searchFiles(target(), 'fresh')).resolves.toEqual(['fresh.ts']) }) it('keeps the legacy file inventory scoped to the workspace that produced it', async () => { const sendRequest = vi.fn(async (method, params) => { if (method === 'files.searchPaths') { - return { ok: false, error: { code: 'method_not_found', message: 'unsupported' } } + return { + id: 'test', + _meta: { runtimeId: 'host' }, + ok: false, + error: { code: 'method_not_found', message: 'unsupported' } + } } const worktree = (params as { worktree: string }).worktree return { + id: 'test', + _meta: { runtimeId: 'host' }, ok: true, result: { files: diff --git a/mobile/src/session/native-host-session-native-chat-operations.ts b/mobile/src/session/native-host-session-native-chat-operations.ts index 15d9b257b27..b3ced59cbbd 100644 --- a/mobile/src/session/native-host-session-native-chat-operations.ts +++ b/mobile/src/session/native-host-session-native-chat-operations.ts @@ -28,7 +28,6 @@ export function nativeHostSessionNativeChatOperations( const legacyPathsByWorkspace = new Map() const legacyLoadByWorkspace = new Map>() const legacyGenerationByWorkspace = new Map() - const legacyGenerationByWorkspace = new Map() return { async readability(workspaceId) { if (isFloatingWorkspaceWorktreeId(workspaceId)) { @@ -74,6 +73,7 @@ export function nativeHostSessionNativeChatOperations( resetFileSearchCache(workspaceId) { searchSupported = null legacyPathsByWorkspace.delete(workspaceId) + legacyLoadByWorkspace.delete(workspaceId) legacyGenerationByWorkspace.set( workspaceId, (legacyGenerationByWorkspace.get(workspaceId) ?? 0) + 1 @@ -108,26 +108,33 @@ export function nativeHostSessionNativeChatOperations( .sendRequest('files.list', { worktree: `id:${target.workspaceId}` }) - .then((response) => (response.ok ? extractPaths(response.result) : null)) - legacyLoad = request.finally(() => { - if (legacyLoadByWorkspace.get(target.workspaceId) === legacyLoad) { - legacyLoadByWorkspace.delete(target.workspaceId) - } - }) - legacyLoadByWorkspace.set(target.workspaceId, legacyLoad) - const paths = await legacyLoad - if ((legacyGenerationByWorkspace.get(target.workspaceId) ?? 0) !== generation) { - return null - } - if (!paths) return null - legacyPaths = paths - legacyPathsByWorkspace.set(target.workspaceId, paths) - } else { - const paths = await legacyLoad - if (!paths) return null - legacyPaths = paths - legacyPathsByWorkspace.set(target.workspaceId, paths) + .then((response) => { + if ( + !response.ok || + (legacyGenerationByWorkspace.get(target.workspaceId) ?? 0) !== generation + ) { + return null + } + const paths = extractPaths(response.result) + legacyPathsByWorkspace.set(target.workspaceId, paths) + return paths + }) + .finally(() => { + if ( + legacyLoadByWorkspace.get(target.workspaceId) === request && + !legacyPathsByWorkspace.has(target.workspaceId) + ) { + legacyLoadByWorkspace.delete(target.workspaceId) + } + }) + legacyLoad = request + legacyLoadByWorkspace.set(target.workspaceId, request) } + const paths = await legacyLoad + if (!paths) { + return null + } + legacyPaths = paths } return rankSuggestions(legacyPaths, query, FILE_RESULT_LIMIT) }