fix(ai-vault): normalize Cursor scopes before shared caps

This commit is contained in:
Brennan Benson
2026-08-10 12:48:50 -07:00
parent ecb08771fd
commit 912cf9a0b6
2 changed files with 69 additions and 24 deletions
@@ -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)
@@ -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<SessionFileDiscovery> {
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<string[]> {
const candidates = new Set<string>()
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<string>()
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<string, FileWithMtime>()
for (const file of files) {