From 32ab48bc0c07989b8b94b40191dfc7f9f653c85f Mon Sep 17 00:00:00 2001 From: Alex Nahas Date: Sat, 19 Sep 2026 22:25:59 -0700 Subject: [PATCH] Read native workspace byte ranges without loading entire files Expose an optional range capability so consumers can request a PDF prefix or suffix without materializing the full file. Keep OPFS reads under existing locks and preserve old WorkspaceFsLike implementations through the adapter's bounded fallback. Real browser tests check actual Blob read sizes; the SQL workspace regression exercises compatibility. --- .../.changeset/bounded-workspace-ranges.md | 7 +++++ vendor/agents/docs/fork-diff.md | 1 + .../shell/src/browser-tests/opfs.test.ts | 13 +++++++++ .../shell/src/browser-tests/opfs.worker.ts | 29 +++++++++++++++++++ .../shell/src/browser/opfs-workspace.ts | 23 ++++++++++++++- .../agents/packages/shell/src/filesystem.ts | 12 +++++++- .../agents/packages/shell/src/fs/interface.ts | 6 ++++ .../packages/shell/src/fs/read-range.ts | 14 +++++++++ .../shell/src/tests/agents/workspace.ts | 15 ++++++++++ .../shell/src/tests/workspace.test.ts | 9 ++++++ vendor/agents/packages/shell/src/workspace.ts | 16 ++++++++++ 11 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 vendor/agents/.changeset/bounded-workspace-ranges.md create mode 100644 vendor/agents/packages/shell/src/fs/read-range.ts diff --git a/vendor/agents/.changeset/bounded-workspace-ranges.md b/vendor/agents/.changeset/bounded-workspace-ranges.md new file mode 100644 index 0000000..425261d --- /dev/null +++ b/vendor/agents/.changeset/bounded-workspace-ranges.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/shell": minor +--- + +Add optional bounded byte reads to WorkspaceFsLike and FileSystem. OPFS reads +native File slices under its existing locks; WorkspaceFileSystem validates ranges +and preserves compatibility with backends that only implement whole-file reads. diff --git a/vendor/agents/docs/fork-diff.md b/vendor/agents/docs/fork-diff.md index 86f5a0e..60b890a 100644 --- a/vendor/agents/docs/fork-diff.md +++ b/vendor/agents/docs/fork-diff.md @@ -104,6 +104,7 @@ upstreamable additions, not claims that Workers need browser polyfills. | Early model preparation and media: Think `prepareModel`, inference configuration and `tools/workspace.ts` materialize history/tool media for one selected model. | Rook model configuration; model-history-capability and read-tool wire/contract tests. | `beforeTurn` occurs too late to restore bytes excluded during history rendering. Keep the early owner seam until upstream supplies equivalent selection and per-render image/PDF capability controls. | | Inference callback context: `think/src/inference-context.ts` binds admitted-turn model callbacks and tool iterator operations. | Real AI SDK mock-model streams in Think Chromium tests overlap ordinary/streaming tools and cleanup. | Native stream callbacks can enter outside their creator's scope. Compiler-assisted awaits cannot restore missing callback-entry context. Retire only when upstream/native context covers those same entry points. | | Shell OPFS: `shell/src/browser/` implements host-selected directories, symlinks, Web Locks and staged writes; `filesystem.ts`, `extras.ts`, `helpers.ts`, `fs/mime-types.ts` share existing filesystem facts. | Rook shared workspace; real Worker OPFS tests cover failures, cancellation and concurrent writes. | Optional backend, not a second Rook filesystem. Runtime SQL/OPFS alone does not implement Shell's filesystem contract. Retire with a matching upstream backend. | +| Shell byte ranges: `WorkspaceFsLike`, `FileSystem`, `WorkspaceFileSystem` and `browser/opfs-workspace.ts` expose optional bounded reads. | Rook mounted-PDF shell performance; native Worker OPFS tests assert exact slices and legacy SQL adapter tests verify fallback. | Filesystem capability belongs in Shell. Existing implementations remain compatible; retire when upstream exposes equivalent range reads. | | Shell metadata export and Git scans: package/build export `state-methods.ts`; `shell/src/git/index.ts` uses three `refresh:false` scan options. | Rook workspace connector; real browser Git test checks index bytes and changed-content detection. | Pure metadata avoids loading Codemode. Browser handles lack full POSIX stat identity; scans must not rewrite the index. Retire with equivalent public metadata and configurable/native scan behavior. | | Voice flush/error/meter: `agents/src/voice/{types,audio-pipeline,voice-input}.ts` exposes optional `waitUntilClosed`, waits final audio before idle, propagates close failures and meters accepted PCM; background context keeps microtask ordering with `await Promise.resolve()` inside eager async entry. | Rook transcriber/root Agent; native Voice and browser dictation tests. | Final transcript must precede idle. `waitUntilReady` is already upstream. The implementation now lives in `agents/voice`; retire with equivalent upstream hooks; an unrelated response-settlement workaround cannot replace transcriber flush. | | Package identity and syntax: six build scripts use tsdown’s `deps.neverBundle`, preserve constructor names and ES2021 async syntax; manifests expose optional browser and metadata leaves. Think aligns Chat to 4.38 and declares optional Slack/Discord adapter peers for those leaves. | Actor/facet identities and runtime async transform; built-package integration and export checks. | A consumer cannot recover renamed constructors or lowered async code. Retire when upstream builds preserve these contracts; never replace with source aliases. | diff --git a/vendor/agents/packages/shell/src/browser-tests/opfs.test.ts b/vendor/agents/packages/shell/src/browser-tests/opfs.test.ts index a46ae4d..f37b3c6 100644 --- a/vendor/agents/packages/shell/src/browser-tests/opfs.test.ts +++ b/vendor/agents/packages/shell/src/browser-tests/opfs.test.ts @@ -152,3 +152,16 @@ it("serializes appends from separate Workers using the same OPFS root", async () .sort() ); }); + +it("reads bounded binary ranges through OPFS without reading a whole native file", async () => { + expect(await run(directory(), "byte-ranges")).toEqual({ + prefix: [0, 1, 2], + suffix: [254, 255], + empty: [], + pastEnd: [], + missing: null, + directory: "EISDIR", + invalid: "EINVAL", + reads: [3, 2, 0, 0] + }); +}); diff --git a/vendor/agents/packages/shell/src/browser-tests/opfs.worker.ts b/vendor/agents/packages/shell/src/browser-tests/opfs.worker.ts index ebcf486..cd7b0a1 100644 --- a/vendor/agents/packages/shell/src/browser-tests/opfs.worker.ts +++ b/vendor/agents/packages/shell/src/browser-tests/opfs.worker.ts @@ -39,6 +39,35 @@ async function run(name: string, operation: string, writer = "") { const root = await storage.getDirectoryHandle(name, { create: true }); const ws = new OpfsWorkspace({ root }); switch (operation) { + case "byte-ranges": { + await ws.writeFileBytes( + "/data", + Uint8Array.from({ length: 256 }, (_, i) => i) + ); + await ws.symlink("data", "/link"); + const fs = ws; + const reads: number[] = []; + const arrayBuffer = Blob.prototype.arrayBuffer; + Blob.prototype.arrayBuffer = function () { + reads.push(this.size); + if (this.size > 3) throw new Error("Full-file read forbidden"); + return arrayBuffer.call(this); + }; + try { + return { + prefix: Array.from((await fs.readFileRange("/link", 0, 3)) ?? []), + suffix: Array.from((await fs.readFileRange("/data", 254, 10)) ?? []), + empty: Array.from((await fs.readFileRange("/data", 0, 0)) ?? []), + pastEnd: Array.from((await fs.readFileRange("/data", 300, 1)) ?? []), + missing: await fs.readFileRange("/missing", 0, 0), + directory: await errorCode(fs.readFileRange("/", 0, 0)), + invalid: await errorCode(fs.readFileRange("/data", -1, 1)), + reads + }; + } finally { + Blob.prototype.arrayBuffer = arrayBuffer; + } + } case "contract": return { memory: await fileContract(new InMemoryFs()), diff --git a/vendor/agents/packages/shell/src/browser/opfs-workspace.ts b/vendor/agents/packages/shell/src/browser/opfs-workspace.ts index fc8129d..e52a9d9 100644 --- a/vendor/agents/packages/shell/src/browser/opfs-workspace.ts +++ b/vendor/agents/packages/shell/src/browser/opfs-workspace.ts @@ -1,3 +1,4 @@ +import { validateReadRange } from "../fs/read-range"; import type { FileInfo, Workspace, WorkspaceFsLike } from "../filesystem"; import { MAX_WORKSPACE_PATH_LENGTH, @@ -125,7 +126,23 @@ export class OpfsWorkspace implements WorkspaceFsLike { return bytes === null ? null : TEXT_DECODER.decode(bytes); } + async readFileRange( + path: string, + offset: number, + length: number + ): Promise { + validateReadRange(offset, length); + return this.readBytes(path, { offset, length }); + } + async readFileBytes(path: string): Promise { + return this.readBytes(path); + } + + private async readBytes( + path: string, + range?: { offset: number; length: number } + ): Promise { const normalized = normalizePath(path); return this.withSharedTree(async () => { if (normalized === "/") { @@ -139,7 +156,11 @@ export class OpfsWorkspace implements WorkspaceFsLike { if (!native) return null; if (native.kind !== "file") throw fsError("EISDIR", `${path} is a directory`); - return new Uint8Array(await (await native.getFile()).arrayBuffer()); + const file = await native.getFile(); + const blob = range + ? file.slice(range.offset, range.offset + range.length) + : file; + return new Uint8Array(await blob.arrayBuffer()); }); }); } diff --git a/vendor/agents/packages/shell/src/filesystem.ts b/vendor/agents/packages/shell/src/filesystem.ts index 6d299b4..d708fca 100644 --- a/vendor/agents/packages/shell/src/filesystem.ts +++ b/vendor/agents/packages/shell/src/filesystem.ts @@ -178,7 +178,17 @@ export type WorkspaceFsLike = Pick< | "symlink" | "readlink" | "glob" ->; +> & { + /** Optional bounded byte read; follows symlinks, returns null for missing files. + * Offset and length must be nonnegative safe integers with a safe sum. + * Reads past EOF return an empty buffer; short reads at EOF are allowed. + */ + readFileRange?( + path: string, + offset: number, + length: number + ): Promise; +}; // ── Constants ──────────────────────────────────────────────────────── diff --git a/vendor/agents/packages/shell/src/fs/interface.ts b/vendor/agents/packages/shell/src/fs/interface.ts index ade18d1..587fcbd 100644 --- a/vendor/agents/packages/shell/src/fs/interface.ts +++ b/vendor/agents/packages/shell/src/fs/interface.ts @@ -52,6 +52,12 @@ export interface CpOptions { export interface FileSystem { readFile(path: string): Promise; readFileBytes(path: string): Promise; + /** Optional bounded byte read. Nonnegative safe integer range; missing paths throw ENOENT. */ + readFileRange?( + path: string, + offset: number, + length: number + ): Promise; writeFile(path: string, content: string): Promise; writeFileBytes(path: string, content: Uint8Array): Promise; appendFile(path: string, content: string | Uint8Array): Promise; diff --git a/vendor/agents/packages/shell/src/fs/read-range.ts b/vendor/agents/packages/shell/src/fs/read-range.ts new file mode 100644 index 0000000..e4b180e --- /dev/null +++ b/vendor/agents/packages/shell/src/fs/read-range.ts @@ -0,0 +1,14 @@ +/** Validate byte ranges before calling APIs whose slicing rules accept negatives. */ +export function validateReadRange(offset: number, length: number): void { + if ( + !Number.isSafeInteger(offset) || + offset < 0 || + !Number.isSafeInteger(length) || + length < 0 || + !Number.isSafeInteger(offset + length) + ) { + throw Object.assign(new RangeError("EINVAL: invalid byte range"), { + code: "EINVAL" + }); + } +} diff --git a/vendor/agents/packages/shell/src/tests/agents/workspace.ts b/vendor/agents/packages/shell/src/tests/agents/workspace.ts index 5d58728..7a884dc 100644 --- a/vendor/agents/packages/shell/src/tests/agents/workspace.ts +++ b/vendor/agents/packages/shell/src/tests/agents/workspace.ts @@ -1,3 +1,4 @@ +import { WorkspaceFileSystem } from "../../workspace"; import { subscribe as dcSubscribe, unsubscribe as dcUnsubscribe @@ -31,6 +32,20 @@ export class TestWorkspaceAgent extends Agent { } }); + async byteRangeContract() { + const fs = new WorkspaceFileSystem(this.workspace); + await this.workspace.writeFileBytes( + "/ranges", + new Uint8Array([0, 128, 255, 4]) + ); + await this.workspace.symlink("ranges", "/range-link"); + return { + bytes: Array.from(await fs.readFileRange("/range-link", 1, 2)), + pastEnd: Array.from(await fs.readFileRange("/ranges", 8, 2)), + zero: Array.from(await fs.readFileRange("/ranges", 0, 0)) + }; + } + async stat(path: string): Promise { try { return await this.workspace.stat(path); diff --git a/vendor/agents/packages/shell/src/tests/workspace.test.ts b/vendor/agents/packages/shell/src/tests/workspace.test.ts index 40345af..97bb6c3 100644 --- a/vendor/agents/packages/shell/src/tests/workspace.test.ts +++ b/vendor/agents/packages/shell/src/tests/workspace.test.ts @@ -2161,3 +2161,12 @@ describe("workspace — observability", () => { expect(log).toHaveLength(0); }); }); + +it("reads byte ranges through legacy WorkspaceFsLike adapters", async () => { + const agent = await freshAgent("range-fallback"); + expect(await agent.byteRangeContract()).toEqual({ + bytes: [128, 255], + pastEnd: [], + zero: [] + }); +}); diff --git a/vendor/agents/packages/shell/src/workspace.ts b/vendor/agents/packages/shell/src/workspace.ts index 83f22b1..d4f83c7 100644 --- a/vendor/agents/packages/shell/src/workspace.ts +++ b/vendor/agents/packages/shell/src/workspace.ts @@ -1,3 +1,4 @@ +import { validateReadRange } from "./fs/read-range"; import type { WorkspaceFsLike } from "./filesystem"; import type { FileSystem, FileSystemDirent, FsStat } from "./fs/interface"; import { FileSystemStateBackend } from "./memory"; @@ -42,6 +43,21 @@ export class WorkspaceFileSystem implements FileSystem { return bytes; } + async readFileRange( + path: string, + offset: number, + length: number + ): Promise { + validateReadRange(offset, length); + const bytes = this.ws.readFileRange + ? await this.ws.readFileRange(path, offset, length) + : (await this.ws.readFileBytes(path))?.slice(offset, offset + length); + if (bytes == null) throw enoent(path); + if (bytes.byteLength > length) + throw new Error("EIO: byte range exceeded requested length"); + return bytes; + } + async writeFile(path: string, content: string): Promise { await this.ws.writeFile(path, content); }