#2895 · Pi omits local files from prompt text
Verdict: REPRODUCED · Root-cause confidence: high
1. TL;DR
The Pi bridge removes each non-image local file before it sends a prompt. A mixed prompt reaches Pi without the file path. A file-only turn or steer fails with Missing input text. The converter has no branch for localFile, although the prompt contract includes this input type.
2. Claims vs findings
| Claim | Status | Evidence |
|---|---|---|
| A mixed prompt loses its local file. | Verified | The fake Pi response did not contain the expected path marker in two clean checkouts. |
| A file-only turn fails as missing text. | Verified | Both runs returned JSON-RPC code -32602 with Missing input text. |
| A file-only steer fails in the same way. | Verified | Both runs returned the same JSON-RPC error from the shared converter path. |
| The host staged the files correctly in the reported live run. | Unverified | This unit-level check used a direct absolute path. It did not read user runtime data. |
| The defect remains on current trusted main. | Verified | Trusted main was 043e16b6e70761918deeaa33dc14e5a576084935. The focused test failed there twice. |
3. Environment
- Repository:
get-bb/bbat043e16b6e70761918deeaa33dc14e5a576084935. - System: macOS 26.6.1 on arm64.
- Node: 22.22.3. pnpm: 9.15.0.
- Pi package and fake RPC: 0.84.0.
- No server, port, provider account, or data directory was necessary.
4. Minimal reproduction
- Clone the trusted repository and select the base commit.
gh repo clone get-bb/bb bb-repro cd bb-repro git checkout 043e16b6e70761918deeaa33dc14e5a576084935 pnpm install --frozen-lockfile --prefer-offline
- Save the test below as
plugins/provider-pi/src/bridge/bridge.local-file.test.ts. - Run the focused test through Turbo.
pnpm exec turbo run test --filter=bb-plugin-provider-pi -- src/bridge/bridge.local-file.test.ts
Expected: three tests pass. Each Pi prompt contains the staged path marker.
Actual:
Test Files 1 failed (1)
Tests 3 failed (3)
mixed turn: expected the path marker, received no marker
file-only turn: { code: -32602, message: "Missing input text" }
file-only steer: { code: -32602, message: "Missing input text" }
Reproduction file: bridge.local-file.test.ts
import { join } from "node:path";
import { afterEach, beforeEach, expect, it } from "vitest";
import {
FULL_PERMISSION_OPTIONS,
type FakePiBridgeHarness,
startFakePiBridge,
} from "./test-support.js";
let harness: FakePiBridgeHarness;
beforeEach(async () => {
harness = await startFakePiBridge({
prefix: "bb-pi-local-file-",
initialize: true,
});
});
afterEach(async () => {
await harness.teardown();
});
function localFile(path: string) {
return {
type: "localFile" as const,
path,
name: "notes.md",
sizeBytes: 6,
mimeType: "text/markdown",
};
}
it("includes local file paths in turn prompts", async () => {
const threadId = "thr_local_file_turn";
const path = join(harness.workspaceDir, "notes.md");
const marker = `[Attached file: ${path}]`;
await harness.startThread(threadId);
const response = await harness.request(1, "turn/start", {
threadId,
providerThreadId: threadId,
clientRequestId: "creq_ab23456789",
input: [
{ type: "text", text: "Read this file.", mentions: [] },
localFile(path),
],
options: FULL_PERMISSION_OPTIONS,
});
expect(response.error).toBeUndefined();
expect(response.result).toEqual({ threadId });
await harness.waitForTurnBoundary(threadId);
expect(
harness.deltasOf(threadId).some(
(delta) =>
delta.kind === "item.textDelta" &&
String(delta.text).includes(`Read this file.\n${marker}`),
),
).toBe(true);
});
it("accepts a turn prompt that contains only a local file", async () => {
const threadId = "thr_local_file_only";
const path = join(harness.workspaceDir, "notes.md");
await harness.startThread(threadId);
const response = await harness.request(2, "turn/start", {
threadId,
providerThreadId: threadId,
clientRequestId: "creq_cd23456789",
input: [localFile(path)],
options: FULL_PERMISSION_OPTIONS,
});
expect(response.error).toBeUndefined();
expect(response.result).toEqual({ threadId });
});
it("includes local file paths in steer prompts", async () => {
const threadId = "thr_local_file_steer";
const path = join(harness.workspaceDir, "notes.md");
const marker = `[Attached file: ${path}]`;
await harness.startThread(threadId);
await harness.request(3, "turn/start", {
threadId,
providerThreadId: threadId,
clientRequestId: "creq_ef23456789",
input: [{ type: "text", text: "/hold", mentions: [] }],
options: FULL_PERMISSION_OPTIONS,
});
await harness.waitForDelta(threadId, (delta) => delta.kind === "turn.open");
const response = await harness.request(4, "turn/steer", {
threadId,
providerThreadId: threadId,
expectedTurnId: "turn-1",
clientRequestId: "creq_gh23456789",
input: [localFile(path)],
options: FULL_PERMISSION_OPTIONS,
});
expect(response.error).toBeUndefined();
expect(response.result).toEqual({ threadId });
await harness.waitForTurnBoundary(threadId);
expect(
harness.deltasOf(threadId).some(
(delta) =>
delta.kind === "item.textDelta" &&
String(delta.text).includes(marker),
),
).toBe(true);
});
Second clean verification
I created a second clone at the same commit. I installed the locked dependencies again. I ran the same Turbo command. The second run produced the same three failures. I made no report correction after this run.
5. Root cause
Both turn/start and turn/steer call extractInput. The converter adds text to chunks. It converts localImage values to Pi image content. It has no localFile branch. See the converter at the trusted base.
if (typed.type === "text" && typeof typed.text === "string") {
chunks.push(typed.text);
} else if (typed.type === "localImage" && typeof typed.path === "string") {
// image conversion
}
return { text: chunks.length > 0 ? chunks.join("\n") : undefined, images };
A mixed prompt retains text, so the request succeeds without the file. A file-only request leaves both outputs empty. The shared guards then return Missing input text. See the turn guard and the steer guard.
6. Proposed fix
Add a localFile branch in extractInput. Add [Attached file: PATH] to the text chunks. This format already exists in the Codex provider. Keep the focused tests for mixed turns, file-only turns, and file-only steers.
7. Related issues
PR #2727 added Pi image-only prompt support. Trusted repository history shows that change did not add local-file conversion.
8. Appendix
The issue data was untrusted. I used it only as a claim set. I did not run linked code or read user runtime data.
Commands
git fetch origin main git checkout --detach origin/main pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build pnpm exec turbo run test --filter=bb-plugin-provider-pi -- src/bridge/bridge.local-file.test.ts
Build result
Tasks: 18 successful, 18 total Time: 1m2.451s
Reproduction result in each checkout
Test Files 1 failed (1) Tests 3 failed (3) EXIT_CODE=1