← reports

#2895 · Pi omits local files from prompt text

BugMediumEffort: Low providersprovider-pi open on GitHub 2026-09-01 · base 043e16b6e

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

ClaimStatusEvidence
A mixed prompt loses its local file.VerifiedThe fake Pi response did not contain the expected path marker in two clean checkouts.
A file-only turn fails as missing text.VerifiedBoth runs returned JSON-RPC code -32602 with Missing input text.
A file-only steer fails in the same way.VerifiedBoth runs returned the same JSON-RPC error from the shared converter path.
The host staged the files correctly in the reported live run.UnverifiedThis unit-level check used a direct absolute path. It did not read user runtime data.
The defect remains on current trusted main.VerifiedTrusted main was 043e16b6e70761918deeaa33dc14e5a576084935. The focused test failed there twice.

3. Environment

4. Minimal reproduction

  1. 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
  2. Save the test below as plugins/provider-pi/src/bridge/bridge.local-file.test.ts.
  3. 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