Files
orca/src/shared/markdown-document-listing-limits.ts
T
Neil f59194d481 Bound file-search inventories and release abandoned Markdown data (#25369)
* Bound relay git-grep records and clean up capacity failures

Credits @OrcaWin for the original bounded-record proposal.

* Bound filesystem listing and transfer metadata at the execution host

Apply mobile limits before transport, list Markdown through its semantic producer, and retain complete directory results only within explicit capacity budgets. Stream SFTP directory packets and reject oversized transfer plans before reporting success; preserve narrow older-peer fallbacks and validate streamed response retention.

* Release inactive Markdown candidates and support folder scopes

Keep one current document snapshot per consumer and attach completion candidates to the actual editor model lifetime. Resolve folder workspace roots through existing workspace identities so their previews and completions receive the same authorized listing as worktrees.

* Preserve full runtime inventories under aggregate byte budgets

Leave unqualified inventories complete beyond 20,000 files. Charge retained paths and serialized bytes at local and relay producers, reject oversized full inventories explicitly, and bound old-peer response reassembly; keep caller-specific mobile and explicit result limits.

* Bound SFTP directory handle cleanup waits

Send CLOSE even after cancellation, stop waiting after five seconds without an acknowledgement, and ignore late callbacks. Preserve the original capacity or cancellation error and close handles returned after an aborted OPENDIR.

* Preserve registered root spelling in Markdown document paths

* Reject incomplete SFTP directory cleanup and final cancellation

* Bound pending response bytes before stream ownership arrives

* Preserve bounded directory reads on legacy SSH hosts

* Allow bounded per-chunk padding at response byte boundaries

* Bound response payload retention after stream metadata

* Bound complete legacy Quick Open inventories and cache retention by bytes

* Localize Markdown document listing fallback

* Exercise relay search decoding with real byte streams

* Preserve concrete relay stream fixture types

* test: compare distinct bundled ripgrep platforms on Windows

* Preserve Markdown discovery across Windows and legacy SSH hosts

* Make WSL process termination available to the bounds layer
2026-10-05 21:45:52 -07:00

120 lines
3.8 KiB
TypeScript

import type { MarkdownDocument } from './filesystem-entry-types'
import { measureUtf8ByteLength } from './utf8-byte-limits'
export const MARKDOWN_DOCUMENT_LISTING_MAX_DOCUMENTS = 20_000
export const MARKDOWN_DOCUMENT_LISTING_MAX_METADATA_BYTES = 8 * 1024 * 1024
export const MARKDOWN_DOCUMENT_LISTING_MAX_PATH_BYTES = 64 * 1024
export const MARKDOWN_DOCUMENT_LISTING_ERROR_CODE = 'markdown_document_listing_capacity'
const MARKDOWN_DOCUMENT_LISTING_ERROR_MESSAGE =
'Workspace is too large for Markdown link completion.'
const MARKDOWN_DOCUMENT_RETAINED_OVERHEAD_BYTES = 256
export type MarkdownDocumentListingLimits = {
maxDocuments: number
maxMetadataBytes: number
maxPathBytes: number
}
export type MarkdownDocumentListingBudget = {
documents: number
metadataBytes: number
limits: MarkdownDocumentListingLimits
}
export class MarkdownDocumentListingCapacityError extends Error {
readonly code = MARKDOWN_DOCUMENT_LISTING_ERROR_CODE
constructor() {
super(MARKDOWN_DOCUMENT_LISTING_ERROR_MESSAGE)
this.name = 'MarkdownDocumentListingCapacityError'
}
}
export function createMarkdownDocumentListingBudget(
requested: Partial<MarkdownDocumentListingLimits> = {}
): MarkdownDocumentListingBudget {
return {
documents: 0,
metadataBytes: 0,
limits: {
maxDocuments: clampLimit(requested.maxDocuments, MARKDOWN_DOCUMENT_LISTING_MAX_DOCUMENTS),
maxMetadataBytes: clampLimit(
requested.maxMetadataBytes,
MARKDOWN_DOCUMENT_LISTING_MAX_METADATA_BYTES
),
maxPathBytes: clampLimit(requested.maxPathBytes, MARKDOWN_DOCUMENT_LISTING_MAX_PATH_BYTES)
}
}
}
export function assertMarkdownDocumentPathWithinLimit(
path: string,
maxPathBytes = MARKDOWN_DOCUMENT_LISTING_MAX_PATH_BYTES
): void {
if (measureUtf8ByteLength(path, { stopAfterBytes: maxPathBytes }).exceededLimit) {
throw new MarkdownDocumentListingCapacityError()
}
}
export function estimateMarkdownDocumentRetainedBytes(document: MarkdownDocument): number {
return (
(document.filePath.length +
document.relativePath.length +
document.basename.length +
document.name.length) *
2 +
MARKDOWN_DOCUMENT_RETAINED_OVERHEAD_BYTES
)
}
export function retainMarkdownDocument(
budget: MarkdownDocumentListingBudget,
document: MarkdownDocument
): void {
if (
!document ||
typeof document.filePath !== 'string' ||
typeof document.relativePath !== 'string' ||
typeof document.basename !== 'string' ||
typeof document.name !== 'string'
) {
throw new MarkdownDocumentListingCapacityError()
}
assertMarkdownDocumentPathWithinLimit(document.filePath, budget.limits.maxPathBytes)
assertMarkdownDocumentPathWithinLimit(document.relativePath, budget.limits.maxPathBytes)
const retainedBytes = estimateMarkdownDocumentRetainedBytes(document)
if (
budget.documents >= budget.limits.maxDocuments ||
budget.metadataBytes + retainedBytes > budget.limits.maxMetadataBytes
) {
throw new MarkdownDocumentListingCapacityError()
}
budget.documents += 1
budget.metadataBytes += retainedBytes
}
export function assertMarkdownDocumentsWithinLimit(
documents: unknown,
requested: Partial<MarkdownDocumentListingLimits> = {}
): number {
const budget = createMarkdownDocumentListingBudget(requested)
if (!Array.isArray(documents)) {
throw new MarkdownDocumentListingCapacityError()
}
if (documents.length > budget.limits.maxDocuments) {
throw new MarkdownDocumentListingCapacityError()
}
for (const document of documents) {
retainMarkdownDocument(budget, document as MarkdownDocument)
}
return budget.metadataBytes
}
function clampLimit(value: number | undefined, maximum: number): number {
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {
return maximum
}
return Math.min(value, maximum)
}