fix(ai-vault): bound raced Cursor sidecar reads

This commit is contained in:
Brennan Benson
2026-08-10 13:57:15 -07:00
parent 7a9b388bce
commit d386fa8262
4 changed files with 139 additions and 14 deletions
@@ -103,7 +103,7 @@ type VerifiedReadTracker = {
budgetsByStorageKey: Map<string, VerifiedReadBudget>
}
type VerifiedReadBudget = { returnedBytes: number; gate: Promise<void> }
type VerifiedReadBudget = { chargedBytes: number; gate: Promise<void> }
function createVerifiedReadTracker(
discoveries: readonly SessionFileDiscovery[] | undefined
@@ -154,7 +154,7 @@ function verifiedReadBudget(tracker: VerifiedReadTracker, storageKey: string): V
if (existing) {
return existing
}
const created = { returnedBytes: 0, gate: Promise.resolve() }
const created = { chargedBytes: 0, gate: Promise.resolve() }
tracker.budgetsByStorageKey.set(storageKey, created)
return created
}
@@ -243,26 +243,40 @@ async function parseSidecarWithAggregateCap(
// Gate covers reserve → verified read → commit of actual returned bytes.
return withVerifiedReadGate(budget, async () => {
throwIfAiVaultScanCancelled(args.signal)
if (budget.returnedBytes >= CURSOR_REMOTE_MAX_AGGREGATE_BYTES) {
if (budget.chargedBytes >= CURSOR_REMOTE_MAX_AGGREGATE_BYTES) {
if (storage) {
storage.truncated.sidecarBytes = true
}
return null
}
if (budget.returnedBytes + estimatedBytes > CURSOR_REMOTE_MAX_AGGREGATE_BYTES) {
if (budget.chargedBytes + estimatedBytes > CURSOR_REMOTE_MAX_AGGREGATE_BYTES) {
if (storage) {
storage.truncated.sidecarBytes = true
}
return null
}
const result = await parseCursorSidecarFileCached({
file: candidate.file,
platform: args.platform,
targetPlatform: candidate.cursorTargetPlatform,
executionHostId: args.executionHostId,
expectedRootRealPath: candidate.cursorExpectedRootRealPath
})
let result
try {
result = await parseCursorSidecarFileCached({
file: candidate.file,
platform: args.platform,
targetPlatform: candidate.cursorTargetPlatform,
executionHostId: args.executionHostId,
expectedRootRealPath: candidate.cursorExpectedRootRealPath
})
} catch (error) {
if (storage) {
storage.counters.boundedReads += 1
}
if (isVerifiedReadTooLargeError(error)) {
budget.chargedBytes = CURSOR_REMOTE_MAX_AGGREGATE_BYTES
if (storage) {
storage.truncated.sidecarBytes = true
}
}
throw error
}
if (!result.cacheHit) {
const readBytes = result.returnedBytes ?? 0
@@ -271,8 +285,8 @@ async function parseSidecarWithAggregateCap(
storage.counters.boundedReads += 1
storage.counters.returnedBytes += readBytes
}
budget.returnedBytes += readBytes
if (budget.returnedBytes > CURSOR_REMOTE_MAX_AGGREGATE_BYTES) {
budget.chargedBytes += readBytes
if (budget.chargedBytes > CURSOR_REMOTE_MAX_AGGREGATE_BYTES) {
if (storage) {
storage.truncated.sidecarBytes = true
}
@@ -296,3 +310,7 @@ async function parseSidecarWithAggregateCap(
: null
})
}
function isVerifiedReadTooLargeError(error: unknown): boolean {
return error instanceof Error && error.message === 'file_too_large'
}
@@ -3,6 +3,7 @@ import { lstat, mkdir, mkdtemp, realpath, rm, utimes, writeFile } from 'node:fs/
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import type { AiVaultScanIssue } from '../../shared/ai-vault-types'
import {
CURSOR_REMOTE_MAX_AGGREGATE_BYTES,
CURSOR_SIDECAR_MAX_BYTES
@@ -115,6 +116,45 @@ describe('Cursor verified-read budgets by storage context', () => {
}
}, 30_000)
it('stops after a raced sidecar grows past the verified-read limit', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-cursor-storage-oversized-race-'))
roots.push(root)
const chatsRoot = join(root, '.cursor', 'chats')
const files = await Promise.all([
addSession(chatsRoot, 'race-first', sidecarPayload(1_000), 2_000),
addSession(chatsRoot, 'race-second', sidecarPayload(1_000), 1_000)
])
await Promise.all(
files.map((file) => writeFile(file.path, sidecarPayload(CURSOR_SIDECAR_MAX_BYTES + 1)))
)
const discovery = sidecarDiscovery('native', chatsRoot, await realpath(chatsRoot), files)
const span = startSpan('cursor-storage-oversized-race-test')
const issues: AiVaultScanIssue[] = []
try {
const result = await processLocalCursorCandidates({
candidates: discoveryCandidates(discovery),
discoveries: [discovery],
executionHostId: 'local',
issues,
limit: 20,
parseStats: createSessionParseStats(),
platform: 'linux',
scopeLimit: 20,
span
})
expect(result.sessions).toEqual([])
expect(discovery.cursorDiscoveryCounters?.boundedReads).toBe(1)
expect(discovery.cursorDiscoveryCounters?.returnedBytes).toBe(0)
expect(discovery.cursorDiscoveryTruncated?.sidecarBytes).toBe(true)
expect(issues).toHaveLength(1)
expect(issues[0]?.message).toContain('file_too_large')
} finally {
span.end()
}
})
it('does not let native reads consume the WSL ingress budget', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-cursor-storage-budget-'))
roots.push(root)
+59 -1
View File
@@ -1,13 +1,18 @@
import { writeFileSync } from 'node:fs'
import { chmod, mkdtemp, mkdir, rm, symlink, utimes, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { afterEach, describe, expect, it } from 'vitest'
import { scanCursorSidecars } from './cursor-sidecar-scan'
import { defaultCursorSidecarScanRequest } from '../shared/cursor-sidecar-scan'
import {
CURSOR_SIDECAR_MAX_BYTES,
defaultCursorSidecarScanRequest
} from '../shared/cursor-sidecar-scan'
import { cursorBucketForCwd } from '../main/ai-vault/session-scanner-cursor-paths'
const roots: string[] = []
const context = { clientId: 1, isStale: () => false }
const CHECKS_BEFORE_FIRST_VERIFIED_READ = 8
async function createRoot(): Promise<string> {
const root = await mkdtemp(join(tmpdir(), 'orca-cursor-scan-'))
@@ -287,6 +292,59 @@ describe('scanCursorSidecars', () => {
).rejects.toThrow('cursor_sidecar_scan_cancelled')
})
it('rejects when cancellation lands during the final verified sidecar read', async () => {
const root = await createRoot()
const chatsRoot = join(root, 'chats')
await addSession(chatsRoot, 'cccccccccccccccccccccccccccccccc', 'alive')
let checks = 0
await expect(
scanCursorSidecars(defaultCursorSidecarScanRequest(chatsRoot, [], process.platform), {
clientId: 1,
isStale: () => {
checks += 1
return checks > CHECKS_BEFORE_FIRST_VERIFIED_READ
}
})
).rejects.toThrow('cursor_sidecar_scan_cancelled')
})
it('stops after a raced sidecar grows past the verified-read limit', async () => {
const root = await createRoot()
const chatsRoot = join(root, 'chats')
const bucket = 'cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd'
await Promise.all([
addSession(chatsRoot, bucket, 'first'),
addSession(chatsRoot, bucket, 'second')
])
let checks = 0
const result = await scanCursorSidecars(
defaultCursorSidecarScanRequest(chatsRoot, [], process.platform),
{
clientId: 1,
isStale: () => {
checks += 1
if (checks === CHECKS_BEFORE_FIRST_VERIFIED_READ) {
for (const sessionId of ['first', 'second']) {
writeFileSync(
join(chatsRoot, bucket, sessionId, 'meta.json'),
'x'.repeat(CURSOR_SIDECAR_MAX_BYTES + 1)
)
}
}
return false
}
}
)
expect(result.sidecars).toEqual([])
expect(result.counters.boundedReads).toBe(1)
expect(result.counters.returnedBytes).toBe(0)
expect(result.truncated.sidecarBytes).toBe(true)
expect(result.issues).toContainEqual(expect.objectContaining({ message: 'file_too_large' }))
})
it('retains the newer equal-size session under a tight aggregate byte cap', async () => {
const root = await createRoot()
const chatsRoot = join(root, 'chats')
+9
View File
@@ -80,6 +80,7 @@ async function readCandidates(
expectedRootRealPath: rootRealPath,
maxBytes: caps.sidecarBytes
})
throwIfCancelled(context)
const bytes = Buffer.byteLength(content, 'utf8')
if (response.counters.returnedBytes + bytes > caps.aggregateBytes) {
response.truncated.sidecarBytes = true
@@ -107,6 +108,10 @@ async function readCandidates(
if (!isMissing(error)) {
addIssue(response, candidate.metaPath, error)
}
if (isVerifiedReadTooLargeError(error)) {
response.truncated.sidecarBytes = true
break
}
}
}
}
@@ -166,6 +171,10 @@ function isMissing(error: unknown): boolean {
return code === 'ENOENT' || code === 'ENOTDIR'
}
function isVerifiedReadTooLargeError(error: unknown): boolean {
return error instanceof Error && error.message === 'file_too_large'
}
function cancellationFromContext(context: RequestContext): CursorSidecarScanCancellation {
return {
throwIfCancelled: () => {