mirror of
https://github.com/stablyai/orca.git
synced 2026-10-29 08:07:18 +00:00
fix(ai-vault): wait for Cursor read reservations
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user