#3792 · Command completion leaves a pending Pi settlement

BugPriority: MediumEffort: Mediumproviders · provider-pi · confirmed-reproGitHub issue

2026-09-16 · trusted base c024ceac524882b7bf9fbef41c14c4911919de4c

Verdict: REPRODUCED · Root-cause confidence: high

1. TL;DR

A real Pi extension command completed successfully, but BB's prompt settlement stayed pending. Pi reported no streaming activity and emitted neither agent_start nor agent_end. BB waits for agent_end even though this command never starts an agent run. The same focused test failed in two clean checkouts at the trusted base commit. This verifies the provider-session defect; a browser-level blocked-follow-up journey and Pi 0.85.1 were not exercised.

2. Claims vs findings

ClaimFindingEvidence
A handled extension command can return without an agent run.VerifiedThe real pinned Pi 0.84.0 process wrote the completion marker; the event list was empty.
BB keeps waiting after that command returns.Verifieddispatch.consumed resolves, getState reports isStreaming=false, but dispatch.settled remains pending.
Follow-up input remains blocked in the BB interface.Code-supported; UI unverifiedThe bridge cannot emit pi/prompt/settled; its translator therefore lacks the completion boundary. No live server/browser test was run.
The reported Pi 0.85.1 environment is affected.Unverified exact versionOnly the trusted repository's locked Pi 0.84.0 was installed and run.

3. Environment

macOS (Darwin), Node v22.22.3, pnpm 9.15.0, Vitest 4.1.1, bb source c024ceac5248, Pi 0.84.0 from the frozen repository lockfile. Two separate temporary clones, each with its own frozen install and successful Turbo build (57 tasks). The broken ambient pnpm launcher was bypassed with a temporary Corepack shim selecting the repository's pnpm version.

Each test uses mkdtemp for a fresh working directory, session directory and PI_CODING_AGENT_DIR. No BB server, app ports, user sessions or real provider credentials are needed. The configured API-key string is deliberately nonfunctional; the extension returns before any model request. The subprocess is closed and its temporary files removed in finally.

4. Minimal reproduction

  1. Clone the public target repository and select the trusted commit:
    git clone https://github.com/get-bb/bb bb-3792
    cd bb-3792
    git checkout c024ceac524882b7bf9fbef41c14c4911919de4c
  2. Save the reproduction test as plugins/provider-pi/src/bridge/rpc-session.command-repro.test.ts.
  3. Install, build and run:
    pnpm install --frozen-lockfile --prefer-offline
    pnpm exec turbo run build
    pnpm exec turbo run test --filter=bb-plugin-provider-pi -- rpc-session.command-repro.test.ts

Expected: the handled command settles its dispatch. Actual output from both runs:

{"command":"handled","isStreaming":false,"events":[],"outcome":"pending"}
AssertionError: expected 'pending' to be 'settled'
Expected: "settled"
Received: "pending"
Test Files  1 failed (1)
Tests       1 failed (1)

The one-second bound is an observation window in the test, not a proposed production timeout. Source tracing confirms the unresolved promise has no successful no-agent completion path.

Complete reproduction test
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, it, vi } from "vitest";
import { PI_BRIDGE_ARGS_ENV, PI_BRIDGE_COMMAND_ENV } from "./rpc-child.js";
import { PiRpcSession } from "./rpc-session.js";

it("settles a completed extension command without an agent run", async () => {
  const dir = mkdtempSync(join(tmpdir(), "bb-pi-command-repro-"));
  const extensionPath = join(dir, "probe.mjs");
  const markerPath = join(dir, "handled.txt");
  const cli = fileURLToPath(new URL("../../node_modules/@earendil-works/pi-coding-agent/dist/cli.js", import.meta.url));
  writeFileSync(extensionPath, `
import { writeFileSync, writeSync } from "node:fs";
export default function (pi) {
  pi.on("session_start", async () => {
    writeSync(3, JSON.stringify({ kind: "ready" }) + "\\n");
  });
  pi.registerCommand("local-probe", {
    description: "Record command completion for a lifecycle test",
    handler: async () => {
      writeFileSync(${JSON.stringify(markerPath)}, "handled");
    },
  });
}
`);
  vi.stubEnv(PI_BRIDGE_COMMAND_ENV, process.execPath);
  vi.stubEnv(PI_BRIDGE_ARGS_ENV, JSON.stringify([
    cli, "--no-extensions", "--no-skills", "--no-prompt-templates", "--no-themes",
  ]));
  const events: string[] = [];
  const session = new PiRpcSession({
    cwd: dir,
    sessionFilePath: join(dir, "session.jsonl"),
    sessionDir: join(dir, "sessions"),
    scratchDir: dir,
    extensionPath,
    recordThreadId: "thr_command_repro",
    noSession: true,
    model: { provider: "openai", id: "gpt-4o" },
    shellEnvOverrides: { PI_CODING_AGENT_DIR: join(dir, "agent"), OPENAI_API_KEY: "local-test-not-a-credential" },
  }, async () => ({ content: "", isError: true }),
  (event) => events.push(event.type), () => undefined);
  try {
    await session.start();
    const dispatch = session.prompt("/local-probe");
    await dispatch.consumed;
    expect(readFileSync(markerPath, "utf8")).toBe("handled");
    const state = await session.getState();
    expect(state.isStreaming).toBe(false);
    expect(events).not.toContain("agent_start");
    expect(events).not.toContain("agent_end");
    const outcome = await Promise.race([
      dispatch.settled.then(() => "settled"),
      new Promise<string>((resolve) => setTimeout(() => resolve("pending"), 1000)),
    ]);
    console.log(JSON.stringify({ command: "handled", isStreaming: state.isStreaming, events, outcome }));
    expect(outcome).toBe("settled");
  } finally {
    await session.closeGracefully(1000);
    vi.unstubAllEnvs();
    rmSync(dir, { recursive: true, force: true });
  }
}, 30000);

5. Root cause

plugins/provider-pi/src/bridge/rpc-session.ts:327 creates a pending run settlement before dispatching the RPC prompt. After success it marks input consumed and awaits that separate settlement.

    this.isProcessing = true;
    const tracked = this.trackPendingInputConsumption("followUp");
    const settlement = new Promise<PiPromptRunOutcome>((resolve) => {
      this.pendingRunSettlements.push({ resolve });
    });
    const settled = this.dispatchWithTransientAuthRetry(
      child,
      {
        type: "prompt",
        message: text,
        ...(images && images.length > 0 ? { images } : {}),
        streamingBehavior: "followUp",
      },
      NO_REQUEST_TIMEOUT,
    ).then(
      async (): Promise<PiPromptRunOutcome | null> => {
        if (tracked.pending.queuedText !== null) {
          this.dropRunSettlement();
          return null;
        }
        this.resolvePendingInputConsumption(tracked.pending);
        const outcome = await settlement;
        return outcome;

plugins/provider-pi/src/bridge/rpc-session.ts:547 calls settleRun from agent_end delivery; plugins/provider-pi/src/bridge/rpc-session.ts:585 resolves the pending run. A handled command emits no such event. Failure and shutdown paths exist, but do not supply successful command completion.

plugins/provider-pi/src/bridge/bridge.ts:974 emits completion only after dispatch.settled resolves. plugins/provider-pi/src/delta-translation.ts:490 turns that notification into a completed or failed turn boundary.

    images.length > 0 ? images : undefined,
  );
  void dispatch.settled.then((outcome) => {
    if (outcome === null) {
      return;
    }
    reportPromptSettled({
      ...(outcome.error !== undefined ? { error: outcome.error } : {}),
      sessionSerial: threadSession.sessionSerial,
      threadId,
    });
  });
  return dispatch.consumed;

The repository pins Pi in plugins/provider-pi/package.json:34. The locally installed locked package provides additional evidence: dist/core/agent-session.js:798–806 awaits an extension command, invokes preflightResult(true), then returns; :920–921 invokes the same success callback before starting an ordinary agent prompt. dist/modes/rpc/rpc-mode.js:298–321 converts either callback into the same RPC success response. These are installed dependency line references, not unverified upstream URL claims.

6. Proposed fix and simple-fix decision

Add an explicit per-prompt outcome that distinguishes a handled command with no run from a queued input or a started agent run, then settle only the matching dispatch. Preserve normal startup, streaming, queueing, retry and error behavior. A timeout or unconditional settlement on RPC success would be unsafe because success is also a preflight acknowledgement for ordinary prompts.

No fix PR was opened. The verified response contract lacks that distinction, so the safe proposed approach requires a provider completion-contract/design change, outside this automation's simple-fix limits. No production files were changed or fix branch pushed. No open pull request linked to this issue was found in GitHub timeline metadata.

7. Verification

The same agent repeated the test in a second clean temporary clone of the recorded trusted commit. This was not an independent review. The second clone had no production modifications, received only the reproduction test, and performed a fresh frozen install and a complete successful Turbo build. The command above executed again (test cache miss) using a new temporary Pi working/data directory and a new subprocess. It produced the same pending-versus-settled assertion failure.

First test duration: 3.80 seconds; second: 3.01 seconds. No report correction was needed after the second run. The exact-version and UI limitations remain explicit. No app server was started, and subprocess cleanup ran in each test.

8. Related issues

No duplicate was established. The untrusted report mentions an earlier event-validation defect; this reproduction reaches no agent_end event at all and does not test that historical issue.

9. Appendix

The complete test is embedded above. Raw first-run and second-run logs are retained locally; exact failure output is included in section 4. Initial harness setup attempts hit ESM-resolution and unauthenticated-model startup errors; those were corrected before either recorded reproduction run.

The investigation treated all issue content as untrusted claims. The fixture was authored from trusted repository and locked-dependency evidence; no issue-supplied script, patch or external link was executed or fetched.