diff --git a/src/main/ai-vault/session-scanner-cursor-local-bounds.test.ts b/src/main/ai-vault/session-scanner-cursor-local-bounds.test.ts index 17529e1e790..21bbe468adf 100644 --- a/src/main/ai-vault/session-scanner-cursor-local-bounds.test.ts +++ b/src/main/ai-vault/session-scanner-cursor-local-bounds.test.ts @@ -179,6 +179,52 @@ describe('local Cursor sidecar discovery bounds', () => { expect(result.truncated.buckets).toBe(true) }) + it('keeps lowercase Windows scope buckets exact through the 64-path cap', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-cursor-win-scope-cap-')) + tempRoots.push(root) + const roots = isolatedScanRoots(root) + const scopePaths = Array.from( + { length: CURSOR_REMOTE_MAX_SCOPE_PATHS - 1 }, + (_, index) => `c:\\workspaces\\repo-${String(index).padStart(3, '0')}` + ) + let targetScope = '' + for (let index = 0; !targetScope; index += 1) { + const candidate = `c:\\workspaces\\target-${index}` + if (cursorBucketForCwd(candidate, 'win32').startsWith('f')) { + targetScope = candidate + } + } + scopePaths.push(targetScope) + const targetBucket = cursorBucketForCwd(targetScope, 'win32') + const targetMeta = await addSession( + roots.cursorChatsDir, + targetBucket, + 'lowercase-drive-session' + ) + + await Promise.all( + Array.from({ length: CURSOR_REMOTE_MAX_BUCKETS }, (_, index) => + mkdir(join(roots.cursorChatsDir, index.toString(16).padStart(32, '0')), { + recursive: true + }) + ) + ) + + const result = await scanAiVaultSessions({ + ...roots, + platform: 'win32', + executionHostId: 'local', + scopePaths, + limit: 5 + }) + + expect(result.sessions.some((session) => session.filePath === targetMeta)).toBe(true) + expect(result.sessions.find((session) => session.filePath === targetMeta)?.cwd).toBe( + targetScope + ) + expect(result.issues.some((issue) => issue.message.includes('scope paths limit'))).toBe(false) + }) + it('detects scope-path truncation before applying the cap', async () => { const root = await mkdtemp(join(tmpdir(), 'orca-cursor-scope-trunc-')) tempRoots.push(root) diff --git a/src/main/ai-vault/session-scanner-cursor-sources.ts b/src/main/ai-vault/session-scanner-cursor-sources.ts index 65ef609d72a..d3d42eef6b0 100644 --- a/src/main/ai-vault/session-scanner-cursor-sources.ts +++ b/src/main/ai-vault/session-scanner-cursor-sources.ts @@ -3,6 +3,7 @@ import { join } from 'node:path' import type { AiVaultScanIssue } from '../../shared/ai-vault-types' import { cursorBucketForCwd, + cursorContextPathForHash, cursorLegacySlug, cursorSessionActivityMtimeMs, cursorScopeCwdCandidates, @@ -71,16 +72,7 @@ async function discoverCursorSidecars(args: { issues: AiVaultScanIssue[] }): Promise { const startedAt = Date.now() - const rawScopePaths = args.options.scopePaths ?? [] - // Detect truncation on the raw caller list before any conversion work/cap. - const scopePathsTruncated = rawScopePaths.length > CURSOR_SCOPE_PATH_LIMIT - const scopePaths = await localScopeCandidates({ - ...args, - options: { - ...args.options, - scopePaths: rawScopePaths.slice(0, CURSOR_SCOPE_PATH_LIMIT) - } - }) + const scopePaths = localSidecarScopeCandidates(args) let discovery try { discovery = await discoverLocalCursorSidecarsBounded({ @@ -112,17 +104,6 @@ async function discoverCursorSidecars(args: { ...discovery.counters, elapsedMs: Math.max(0, Date.now() - startedAt) } - const truncated = { - ...discovery.truncated, - scopePaths: discovery.truncated.scopePaths || scopePathsTruncated - } - if (scopePathsTruncated && !discovery.truncated.scopePaths) { - args.issues.push({ - agent: 'cursor', - path: 'cursor sidecar scan', - message: 'Cursor sidecar scan truncated by the scope paths limit.' - }) - } if (!discovery.rootRealPath) { return { agent: 'cursor', @@ -131,7 +112,7 @@ async function discoverCursorSidecars(args: { cursorLayout: 'sidecar', cursorStorageContextKey: args.roots.storageContextKey, cursorDiscoveryCounters: counters, - cursorDiscoveryTruncated: truncated + cursorDiscoveryTruncated: discovery.truncated } } @@ -152,7 +133,7 @@ async function discoverCursorSidecars(args: { cursorCwdEvidenceByPath: discovery.evidenceByPath, cursorExpectedRootRealPath: discovery.rootRealPath, cursorDiscoveryCounters: counters, - cursorDiscoveryTruncated: truncated + cursorDiscoveryTruncated: discovery.truncated } } @@ -208,7 +189,7 @@ async function localScopeCandidates(args: { options: AiVaultScanOptions }): Promise { const candidates = new Set() - for (const scopePath of args.options.scopePaths ?? []) { + for (const scopePath of (args.options.scopePaths ?? []).slice(0, CURSOR_SCOPE_PATH_LIMIT)) { for (const cwd of cursorScopeCwdCandidates({ scopePath, platform: args.roots.targetPlatform, @@ -232,6 +213,24 @@ async function localScopeCandidates(args: { return [...candidates] } +function localSidecarScopeCandidates(args: { + roots: CursorRootPair + options: AiVaultScanOptions +}): string[] { + const candidates = new Set() + for (const scopePath of args.options.scopePaths ?? []) { + const candidate = cursorContextPathForHash( + scopePath, + args.roots.storageContextKey, + args.roots.targetPlatform + ) + if (candidate) { + candidates.add(candidate) + } + } + return [...candidates] +} + function dedupeFiles(files: readonly FileWithMtime[]): FileWithMtime[] { const byPath = new Map() for (const file of files) {