fix(ai-vault): wait for Cursor read reservations

This commit is contained in:
Brennan Benson
2026-08-10 16:06:10 -07:00
parent 565429c8e1
commit 97a3b74978
3 changed files with 123 additions and 23 deletions
@@ -23,6 +23,12 @@ import {
selectCursorScopedGroups,
type CursorCandidateSelectionGroup
} from './session-scanner-cursor-selection'
import {
createCursorVerifiedReadBudget,
reserveCursorVerifiedReadBytes,
settleCursorVerifiedReadReservation,
type CursorVerifiedReadBudget
} from './session-scanner-cursor-read-budget'
const CURSOR_PARSE_CONCURRENCY = 8
@@ -100,11 +106,9 @@ type VerifiedReadTracker = {
truncated: NonNullable<SessionFileDiscovery['cursorDiscoveryTruncated']>
}
>
budgetsByStorageKey: Map<string, VerifiedReadBudget>
budgetsByStorageKey: Map<string, CursorVerifiedReadBudget>
}
type VerifiedReadBudget = { chargedBytes: number; reservedBytes: number }
function createVerifiedReadTracker(
discoveries: readonly SessionFileDiscovery[] | undefined
): VerifiedReadTracker {
@@ -132,12 +136,15 @@ function createVerifiedReadTracker(
return { byStorageKey, budgetsByStorageKey: new Map() }
}
function verifiedReadBudget(tracker: VerifiedReadTracker, storageKey: string): VerifiedReadBudget {
function verifiedReadBudget(
tracker: VerifiedReadTracker,
storageKey: string
): CursorVerifiedReadBudget {
const existing = tracker.budgetsByStorageKey.get(storageKey)
if (existing) {
return existing
}
const created = { chargedBytes: 0, reservedBytes: 0 }
const created = createCursorVerifiedReadBudget()
tracker.budgetsByStorageKey.set(storageKey, created)
return created
}
@@ -229,17 +236,13 @@ async function parseSidecarWithAggregateCap(
candidate.file.sizeBytes === undefined
? CURSOR_SIDECAR_MAX_BYTES
: Math.min(Math.max(0, candidate.file.sizeBytes), CURSOR_SIDECAR_MAX_BYTES)
throwIfAiVaultScanCancelled(args.signal)
const availableBytes =
CURSOR_REMOTE_MAX_AGGREGATE_BYTES - budget.chargedBytes - budget.reservedBytes
if (availableBytes <= 0 || estimatedBytes > availableBytes) {
const reservedBytes = await reserveCursorVerifiedReadBytes(budget, estimatedBytes, args.signal)
if (reservedBytes === null) {
if (storage) {
storage.truncated.sidecarBytes = true
}
return null
}
const reservedBytes = Math.min(CURSOR_SIDECAR_MAX_BYTES, availableBytes)
budget.reservedBytes += reservedBytes
let result
try {
@@ -252,7 +255,7 @@ async function parseSidecarWithAggregateCap(
maxBytes: reservedBytes
})
} catch (error) {
settleVerifiedReadReservation(
settleCursorVerifiedReadReservation(
budget,
reservedBytes,
isVerifiedReadTooLargeError(error)
@@ -270,7 +273,7 @@ async function parseSidecarWithAggregateCap(
}
const readBytes = result.cacheHit ? 0 : (result.returnedBytes ?? 0)
settleVerifiedReadReservation(budget, reservedBytes, readBytes)
settleCursorVerifiedReadReservation(budget, reservedBytes, readBytes)
if (!result.cacheHit && storage) {
storage.counters.boundedReads += 1
storage.counters.returnedBytes += readBytes
@@ -291,16 +294,6 @@ async function parseSidecarWithAggregateCap(
: null
}
function settleVerifiedReadReservation(
budget: VerifiedReadBudget,
reservedBytes: number,
chargedBytes: number
): void {
budget.reservedBytes -= reservedBytes
const available = CURSOR_REMOTE_MAX_AGGREGATE_BYTES - budget.chargedBytes - budget.reservedBytes
budget.chargedBytes += Math.min(chargedBytes, Math.max(0, available))
}
function isVerifiedReadTooLargeError(error: unknown): boolean {
return error instanceof Error && error.message === 'file_too_large'
}
@@ -0,0 +1,57 @@
import {
CURSOR_REMOTE_MAX_AGGREGATE_BYTES,
CURSOR_SIDECAR_MAX_BYTES
} from '../../shared/cursor-sidecar-scan'
import { throwIfAiVaultScanCancelled } from './ai-vault-scan-cancellation'
export type CursorVerifiedReadBudget = {
chargedBytes: number
reservedBytes: number
changed: Promise<void>
notifyChanged: () => void
}
export function createCursorVerifiedReadBudget(): CursorVerifiedReadBudget {
let notifyChanged = (): void => undefined
const changed = new Promise<void>((resolve) => {
notifyChanged = resolve
})
return { chargedBytes: 0, reservedBytes: 0, changed, notifyChanged }
}
export async function reserveCursorVerifiedReadBytes(
budget: CursorVerifiedReadBudget,
estimatedBytes: number,
signal?: AbortSignal
): Promise<number | null> {
while (true) {
throwIfAiVaultScanCancelled(signal)
const remainingBytes = CURSOR_REMOTE_MAX_AGGREGATE_BYTES - budget.chargedBytes
const availableBytes = remainingBytes - budget.reservedBytes
if (availableBytes > 0 && estimatedBytes <= availableBytes) {
const reservedBytes = Math.min(CURSOR_SIDECAR_MAX_BYTES, availableBytes)
budget.reservedBytes += reservedBytes
return reservedBytes
}
if (remainingBytes <= 0 || estimatedBytes > remainingBytes) {
return null
}
await budget.changed
}
}
export function settleCursorVerifiedReadReservation(
budget: CursorVerifiedReadBudget,
reservedBytes: number,
chargedBytes: number
): void {
budget.reservedBytes -= reservedBytes
const available = CURSOR_REMOTE_MAX_AGGREGATE_BYTES - budget.chargedBytes - budget.reservedBytes
budget.chargedBytes += Math.min(chargedBytes, Math.max(0, available))
budget.notifyChanged()
let notifyChanged = (): void => undefined
budget.changed = new Promise<void>((resolve) => {
notifyChanged = resolve
})
budget.notifyChanged = notifyChanged
}
@@ -78,6 +78,56 @@ describe('Cursor verified-read budgets by storage context', () => {
}
)
it.each(['native', 'wsl:ubuntu'])(
'waits for temporary %s reservations when the statted batch still fits',
async (storageKey) => {
const root = await mkdtemp(join(tmpdir(), 'orca-cursor-storage-reservations-'))
roots.push(root)
const chatsRoot = join(root, '.cursor', 'chats')
const fillerBytes = CURSOR_SIDECAR_MAX_BYTES - 100
const fillerFiles = await Promise.all(
Array.from({ length: 63 }, (_, index) =>
addSession(
chatsRoot,
`filler-${String(index).padStart(2, '0')}`,
sidecarPayload(fillerBytes),
10_000
)
)
)
const tailFiles = await Promise.all(
Array.from({ length: 8 }, (_, index) =>
addSession(chatsRoot, `tail-${index}`, sidecarPayload(1_000), 1_000)
)
)
const files = [...fillerFiles, ...tailFiles]
const discovery = sidecarDiscovery(storageKey, chatsRoot, await realpath(chatsRoot), files)
const span = startSpan('cursor-storage-reservation-wait-test')
try {
const result = await processLocalCursorCandidates({
candidates: discoveryCandidates(discovery),
discoveries: [discovery],
executionHostId: 'local',
issues: [],
limit: 100,
parseStats: createSessionParseStats(),
platform: 'linux',
scopeLimit: 100,
span
})
expect(result.sessions).toHaveLength(files.length)
expect(discovery.cursorDiscoveryCounters?.boundedReads).toBe(files.length)
expect(discovery.cursorDiscoveryCounters?.returnedBytes).toBe(63 * fillerBytes + 8_000)
expect(discovery.cursorDiscoveryTruncated?.sidecarBytes).toBe(false)
} finally {
span.end()
}
},
30_000
)
it('stops after the first verified read when cancellation lands during parsing', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-cursor-parse-cancel-'))
roots.push(root)