Skip to content

[Bug]: Pi turn-start prompt lacks streamingBehavior — pi 1.0.x rejects prompts while an agent run is active #15221

Description

@DerAndereAndi

Area: apps/server

Steps to reproduce

  1. Install pi ≥ 1.0.0 (observed on 1.0.0 and 1.0.1; npm i -g @earendil-works/pi-coding-agent or bun global) and enable the Pi provider.
  2. Have a pi session whose extension state triggers a run on resume — e.g. a pi-loop extension iteration waiting on a monitor event, or any pending extension continuation. (Observed: session last ended mid-run, so the resumed session re-fires work immediately after switch_session.)
  3. Open a T3 thread bound to that session and send a message.

T3's turn-start prompt races the extension-driven run:

→ {"type":"switch_session","sessionPath":"…/session.jsonl","id":"t3-0"}
→ {"type":"get_commands","id":"t3-1"}
→ {"type":"get_state","id":"t3-2"}
→ {"type":"get_available_models","id":"t3-3"}
→ {"type":"get_entries","id":"t3-4"}
→ {"type":"set_model",…,"id":"t3-5"}
→ {"type":"set_session_name",…,"id":"t3-6"}
→ {"type":"prompt","message":"Context handoff (delta_since_target_last_seen):…"}
← {"type":"response","command":"prompt","success":false,"error":"Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message."}

Expected behavior

The turn starts. Since pi 1.0.x, AgentSession.prompt() rejects a bare prompt while a run is active — agent-session.ts:1965-1970 at tag v1.0.1:

// If streaming, queue via steer() or followUp() based on option
if (this.isStreaming) {
	if (!options?.streamingBehavior) {
		throw new Error(
			"Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.",
		);
	}

So the turn-start prompt must either pass streamingBehavior (pi's steer semantics queue it atomically during an active run) or T3 should check get_state and abort the active run before prompting.

Actual behavior

The prompt is rejected and the user sees:

Provider error: Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.

The turn never starts; the session is left in an error state. Every retry while an extension-driven run is in flight bounces the same way, so the thread is effectively unusable.

Impact

Blocks work completely (for the affected thread). Workaround exists but is costly — see below.

Version or commit

0.0.46-nightly.20261003.2632 (f391794). Defect present at current main (ce90eec): turn-start sends a bare prompt at apps/server/src/orchestration-v2/Adapters/PiAdapterV2.ts:2358-2362 while the steer path (line 2444) already passes streamingBehavior: "steer" — its own comment (line 2421) documents pi's queue-or-run semantics:

Prompt with streamingBehavior steer is atomic on Pi's side: it queues during an active run and starts a new run if settlement won the race.

Environment

macOS 27.0.1 (arm64), T3 Code desktop (Nightly), pi @earendil-works/pi-coding-agent 1.0.1 (bun global install; behavior identical on 1.0.0), Node v26.9.0.

Related

#13507 / #13839 cover extension-driven session rewinds and their ref/rollback reconciliation — a different mechanism. This report is about the prompt contract itself: pi 1.0.x made bare prompt throw while a run is active, and T3's turn-start path (tested against pi 0.8x, cf. #13839's verification against pi 0.87.1) hasn't caught up. Both are part of the same theme: extensions drive pi sessions in ways the adapter's request paths don't fully anticipate.

Logs or stack traces

ProviderAdapterEventStreamError: Failed while streaming pi provider session provider-session:provider-instance:pi:thread:…:… events.
  at orchestrationV2.providerTurnStart.start (apps/server/dist/binCli-D9mHsJrM.mjs:228797)
  [cause]: Error: Cause([Fail(PiRpcError: Pi RPC read failed: pi process exited with code 0.)])

RPC capture (tee on pi stdin/stdout) of the rejected exchange and the extension-driven runs that follow — full transcript available on request. Redacted: session paths truncated; no secrets in the capture.

Workaround

Setting the Pi provider's Launch arguments to --no-extensions stops extension-discovery extensions (pi-lens, pi-loop, …) from loading in T3 sessions — T3's own --extension still loads — which removes the busy-at-load races. Cost: pi-lens features are unavailable in T3 sessions. Alternatively wait for extension-driven runs to settle before prompting.

Minor side note

A bare 1.0.1 (version string) line appears on pi's stdout in RPC mode — most likely pi's own version/update-check output landing on the protocol channel. T3 drops non-JSON lines so it's harmless, but raw prints on the RPC channel would be worth avoiding.

Activity

  1. juliusmarminge commented on Oct 3, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Triage

    Thanks @DerAndereAndi for the RPC capture and for pointing straight at the steer path. I confirmed this on current main (ce90eec1) and on the nightly in your report (f391794a35c6). The turn fails because the Pi turn-start prompt doesn't set streamingBehavior while Pi is already streaming. This is separate from issue #13507 and open PR #13839, which cover extension rewinds and ref reconciliation.

    What I found

    startTurn sends a bare prompt:

    yield* connection.send({
    type: "prompt",
    message: payload.message,
    ...(payload.images.length === 0 ? {} : { images: payload.images }),
    });

    steerTurn already passes streamingBehavior: "steer". In Pi, that option queues the message while isStreaming is set and is ignored when Pi is idle, in which case a new run starts.

    // Prompt with streamingBehavior steer is atomic on Pi's side: it
    // queues during an active run and starts a new run if settlement
    // won the race. A direct `steer` sent after Pi became idle would
    // remain queued forever. Send fire-and-forget under the session
    // permit so a slash-command dialog cannot block the turn, and so
    // settlement cannot overtake the active-turn check.
    // /compact is not a prompt: Pi's compact RPC aborts the agent first.
    yield* sessionEventPermit.withPermits(1)(
    Effect.gen(function* () {
    if (threadState?.activeTurn !== turn) {
    return yield* protocolError(`Pi turn ${steerInput.providerTurnId} is not active`);
    }
    if (compactCommand !== null) {
    turn.manualCompactInFlight = true;
    yield* connection.send(compactRpcRecord(compactCommand));
    pendingCompactResponses.push({
    providerTurnId: turn.providerTurn.id,
    kind: "steer",
    });
    } else if (payload !== null) {
    yield* connection.send({
    type: "prompt",
    message: payload.message,
    streamingBehavior: "steer",
    ...(payload.images.length === 0 ? {} : { images: payload.images }),
    });

    One small correction to the version framing: the throw itself predates Pi 1.0. AgentSession.prompt() has rejected a prompt without streamingBehavior during streaming since at least Pi 0.80.5, T3's minimum (source). Pi 1.0.x just makes it easy to hit, because after switch_session an extension continuation can already be streaming by the time T3 prompts. registerThread does call get_state but never reads isStreaming.

    A rejected turn-start prompt is stored as a provider failure and the turn is finalized, which is the banner you saw. That path leaves the Pi process running. The ProviderAdapterEventStreamError / pi process exited with code 0 trace comes from the stdout-close path, used when Pi exits on its own. It's worth keeping on the report, but the bare prompt is what blocks the thread.

    Likely fix area

    A maintainer will decide on the fix direction. The option that matches the existing steer path is to send the turn-start prompt with streamingBehavior: "steer": an idle session still starts a run, and a streaming one gets the user message injected instead of a failed turn. followUp would wait behind the extension run, and abort-then-prompt can race extensions that resume themselves (abort also doesn't cancel compaction). The message-steering replay fixture currently expects the initial prompt to omit streamingBehavior, so that expectation would need to change too.

    Workaround and side note

    --no-extensions in the Pi provider's launch arguments is a valid workaround. T3 appends its own --extension after those arguments, so the T3 bridge stays loaded while discovery extensions don't.

    The bare 1.0.1 line on stdout is separate and harmless: non-JSON stdout lines are dropped and don't break the RPC stream.

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 3, 2026
  3. added 6 commits that reference this issue on Oct 6, 2026
    cbab58f
    e962049
    1a1ed30
    e278bd2
    3b31b08
    4bc2474
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions