mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 00:02:29 +00:00
* feat(remote): add a positional file read to the filesystem provider Following a growing remote file means re-reading it from the top on every poll: the relay exposes only whole-file reads, so tailing an append-only log over SSH costs O(size) per tick. Adds fs.readFileRange plus a rangedReadVersion capability, and an optional readFileRange on IFilesystemProvider -- matching how lstat/ supportsQuickOpenSearch already declare degradable capabilities. Three deliberate choices: - The relay loops until the requested length is satisfied or the file truly ends, and REJECTS an over-cap request rather than clamping it. A clamped read is indistinguishable from EOF, so a caller advancing a cursor by bytesRead would silently skip data. - Bytes cross the wire base64-encoded. A range boundary can split a UTF-8 sequence at either edge, and a utf-8 round trip would substitute U+FFFD and shift every subsequent offset. - The provider throws a typed FileRangeReadUnsupportedError against an older relay instead of quietly falling back to a whole-file read. A tailing caller issues several reads per snapshot, so a per-call fallback is quadratic; callers probe supportsFileRangeRead once and snapshot instead. The response is validated before use -- a byte count disagreeing with the payload would shift every downstream offset while looking like success. Terminal-artifact reads/writes move to their own module, mirroring the relay's existing fs-handler-terminal-artifact split; the provider was at the max-lines ceiling and this was the cohesive piece to extract. * fix(remote): size the ranged read to what the relay writer can deliver The 4 MiB cap was justified against MAX_MESSAGE_SIZE (16 MiB), but that is the frame DECODER bound. Responses are gated by the writer's admission budget: a frame over DISPATCHER_CONTROL_QUEUE_MAX_BYTES (1 MiB) is demoted to the legacy-response lane, which is refused once the producer queue passes 2 MiB. A 4 MiB window is ~5.46 MiB of base64, so it was never admissible -- it came back as an opaque ResponseOverCapacity (-33008), which is neither of the PR's typed errors, and above ~1.4 MiB the outcome depended on unrelated queued traffic. Cap at STREAM_CHUNK_SIZE (256 KiB), the house per-frame budget for file bytes, which stays in the control lane unconditionally. Also: - Hoist the cap and offset validation into src/shared/file-range-read.ts so the client rejects an out-of-contract request locally instead of paying a round trip for an error that does not survive the wire as a type. - Validate filePath in the relay handler; a missing one threw a TypeError out of expandTilde despite the comment claiming hand-validated params. - Collapse the two fs.getCapabilities probes onto one cached fetch per multiplexer. They read one document, so probing per feature spent an extra round trip per connection and duplicated the eviction logic. - Reuse readFullStreamChunk instead of a second copy of the short-read fill. - allocUnsafe the window; only subarray(0, bytesRead) escapes, so a tailing poll no longer memsets the whole window per call. - Plain methods for readFileRange/supportsFileRangeRead rather than constructor-assigned arrows; both are unconditional, unlike downloadFolder. Tests: cover the dispatch path and fs.getCapabilities (neither was exercised), param validation at both boundaries, EOF at and past the end, and a full-cap read over a real RelayDispatcher. The transport guard fails at 4 MiB with the real -33008. * test(remote): pin the ranged-read cap to real control-queue headroom The cap comment claimed a full-cap window stays in the control lane "unconditionally" and the guard test only asserted one frame fits the lane, so a raise to 384-768 KiB stayed green while two concurrent full-cap responses would already overflow the shared control queue -- which for a response closes the client. Pin the two-deep headroom and state the real bound, including that widening the cap is a wire change against a host still advertising rangedReadVersion 1. Also cover the two behaviours the suite claimed but did not exercise: a regular file answers a full-cap read in one syscall, so the fill loop was untested (both mutations of readFullStreamChunk stayed green), and the merged capability document made the abort-does-not-evict guard load-bearing without any test reaching it. * fix(remote): harden ranged-read validation and retry
31 lines
1.4 KiB
TypeScript
31 lines
1.4 KiB
TypeScript
import { open } from 'node:fs/promises'
|
|
import { validateFileRangeRequest } from '../shared/file-range-read'
|
|
import { readFullStreamChunk } from './fs-handler-file-read'
|
|
|
|
/** Positional read for tailing an append-only file. Fills the window until
|
|
* `length` is satisfied or the file genuinely ends, so a short result always
|
|
* means EOF and never a partial syscall. Base64 because the payload is
|
|
* arbitrary bytes and may split a UTF-8 sequence at either edge.
|
|
*
|
|
* Validates here rather than at the dispatcher: this is the function that puts
|
|
* a caller-supplied offset into a read syscall, so it is the boundary that has
|
|
* to hold even if a future call site forgets. */
|
|
export async function readRelayFileRange(
|
|
filePath: string,
|
|
rawPosition: unknown,
|
|
rawLength: unknown
|
|
): Promise<{ base64: string; bytesRead: number }> {
|
|
const { position, length } = validateFileRangeRequest(rawPosition, rawLength)
|
|
const handle = await open(filePath, 'r')
|
|
try {
|
|
// allocUnsafe over alloc: only `subarray(0, bytesRead)` is ever read back, so
|
|
// uninitialised bytes cannot escape, and a tailing poll with a generous
|
|
// `length` should not pay a memset over the whole window per call.
|
|
const buffer = Buffer.allocUnsafe(length)
|
|
const bytesRead = await readFullStreamChunk(handle, buffer, length, position)
|
|
return { base64: buffer.subarray(0, bytesRead).toString('base64'), bytesRead }
|
|
} finally {
|
|
await handle.close()
|
|
}
|
|
}
|