#3604 · Editor storage requests lose the owning host
REPRODUCED · Root-cause confidence: high
1. TL;DR
The editor sends thread-storage file operations to a path derived from the server configuration and omits the thread's host ID. A remote thread therefore cannot reach its own files through these requests. Three regression tests reproduce this routing defect for read, listing, and save. This is an RPC-boundary reproduction using SDK test doubles, not a live two-machine browser test.
2. Claims vs findings
| Claim | Finding | Evidence |
|---|---|---|
| Thread storage resolves on the wrong host | Verified | Read, tree, and write arguments use /server-data and omit hostId in both base runs. |
| Editor displays 404 and an empty tree | Unverified live | Consistent with misrouted requests; no HTTP server or browser was started. |
| Save targets the wrong machine | Verified at request boundary | write receives the same wrong root; actual disk writes were not performed. |
| Other preview paths work | Partially verified by source | Existing server storage resolver uses host session data; live alternate previews were not tested. |
3. Environment
Trusted get-bb/bb origin/main at the commit above; macOS, Node 22.22.3, pnpm 9.15.0. Frozen installation and full Turbo build succeeded (56 tasks). A temporary pnpm launcher used Corepack because the host launcher pointed at a missing installation. No provider, server, ports, real runtime data, or credentials were used.
4. Minimal reproduction
- Check out the recorded trusted commit in a fresh checkout.
- Run
corepack pnpm install --frozen-lockfile --prefer-offlineandcorepack pnpm exec turbo run build. - Save server.test.ts as
plugins/monaco-editor/server.test.ts. - Run
corepack pnpm exec turbo run test --filter=bb-plugin-monaco-editor --force.
Expected: all requests use hostId remote-editor-host and root /remote-storage/thread-editor-test. Actual read argument diff (the listing and save tests fail equivalently):
- "hostId": "remote-editor-host", - "path": "/remote-storage/thread-editor-test/notes/document.txt", - "rootPath": "/remote-storage/thread-editor-test", + "path": "/server-data/thread-storage/thread-editor-test/notes/document.txt", + "rootPath": "/server-data/thread-storage/thread-editor-test", Tests 3 failed | 34 passed (37)
Complete regression test
import { describe, expect, it, vi } from "vitest";
import { createFakePluginHost } from "@get-bb/plugin-sdk/testing";
import plugin from "./server";
const source = {
kind: "thread-storage",
threadId: "thread-editor-test",
environmentId: "environment-editor-test",
projectId: null,
};
const storageRootPath = "/remote-storage/thread-editor-test";
const hostId = "remote-editor-host";
async function setup() {
const storageLocation = vi.fn(() => ({ hostId, storageRootPath }));
const read = vi.fn(() => ({
content: "saved text",
contentEncoding: "utf8",
sizeBytes: 10,
sha256: "original",
}));
const listPaths = vi.fn(() => ({
paths: [{ path: "notes/document.txt", kind: "file" }],
truncated: false,
}));
const write = vi.fn(() => ({ outcome: "written", sha256: "updated" }));
const { bb, harness } = createFakePluginHost({
pluginId: "monaco-editor",
sdk: {
system: { config: () => ({ dataDir: "/server-data" }) },
threads: { storageLocation },
files: { read, listPaths, write },
},
});
await plugin(bb);
return { harness, storageLocation, read, listPaths, write };
}
describe("thread storage host routing", () => {
it("reads from the thread's storage host and root", async () => {
const { harness, read, storageLocation } = await setup();
const result = await harness.callRpc("read", {
source,
path: "notes/document.txt",
});
expect(read).toHaveBeenCalledWith({
hostId,
rootPath: storageRootPath,
path: `${storageRootPath}/notes/document.txt`,
});
expect(storageLocation).toHaveBeenCalledWith({ threadId: source.threadId });
expect(result).toMatchObject({
kind: "text",
content: "saved text",
absolutePath: `${storageRootPath}/notes/document.txt`,
});
});
it("lists the thread's storage host and root", async () => {
const { harness, listPaths } = await setup();
const result = await harness.callRpc("tree", { source });
expect(listPaths).toHaveBeenCalledWith({
hostId,
path: storageRootPath,
includeFiles: true,
includeDirectories: true,
includeHidden: true,
limit: 10_000,
});
expect(result).toEqual({
root: storageRootPath,
entries: [{ path: "notes/document.txt", kind: "file" }],
truncated: false,
});
});
it("saves to the thread's storage host with the expected version", async () => {
const { harness, write } = await setup();
const result = await harness.callRpc("write", {
source,
path: "notes/document.txt",
content: "edited text",
expectedSha256: "original",
});
expect(write).toHaveBeenCalledWith({
hostId,
rootPath: storageRootPath,
path: `${storageRootPath}/notes/document.txt`,
content: "edited text",
contentEncoding: "utf8",
expectedSha256: "original",
});
expect(result).toEqual({ outcome: "written", sha256: "updated" });
});
});
5. Root cause
threadStorageRoot and resolveTarget build the root from server config or a process override. The thread-storage branch does not consult the owning thread's storage location and returns no hostId. All three RPC handlers consume this target. In contrast, the server storage resolver uses the host session's data directory. The process override also cannot represent storage for arbitrary threads.
6. Proposed fix
Resolve the thread through the existing SDK threads.storageLocation operation and pass its storageRootPath and hostId into the existing file handlers. Remove the obsolete local storage-root helper. This changes no SDK or wire contract. The local candidate changes 112 text lines across two files (103 additions, 9 deletions), including tests.
7. Verification
The same agent repeated the reproduction in a second clean detached worktree at the recorded base, under a distinct temporary directory. It received a separate frozen installation and the final regression test. Command: corepack pnpm exec turbo run test --filter=bb-plugin-monaco-editor --force. Result: 3 failed, 34 passed; Turbo reported zero cached tasks. Both runs show the same wrong root and missing host ID. No ports or application data directories were needed.
After the production fix, corepack pnpm exec turbo run test typecheck --filter=bb-plugin-monaco-editor passed: 37/37 tests and typecheck. An initial extra assertion about nested relative-path display exposed a separate existing separator behavior; it was removed to keep the regression about host routing. The final test was repeated against the unchanged production code in the second checkout. No root-cause correction was needed.
8. Related issues and PRs
No connected or cross-referenced pull requests were present in the issue timeline, and the open-PR search returned none. Existing editor issue #2519 concerns opener source input validation, a different failure path.
9. Appendix
Issue content was treated as untrusted claims. No supplied commands, patches, tests, or external links were executed or fetched. The test and fix were authored from repository code. No screenshots are included because this report verifies request routing rather than visual rendering.
> AGENT GENERATED