Skip to content

feat(providers): add Pi coding agent - #6319

Draft
quasarea wants to merge 223 commits into
pingdotgg:t3code/codex-turn-mappingfrom
quasarea:codex/pi-provider-v2
Draft

feat(providers): add Pi coding agent#6319
quasarea wants to merge 223 commits into
pingdotgg:t3code/codex-turn-mappingfrom
quasarea:codex/pi-provider-v2

Conversation

@quasarea

@quasarea quasarea commented Aug 12, 2026

Copy link
Copy Markdown

Summary

Adds Pi as a first-class, disabled-by-default coding-agent provider for the Orchestrator V2 architecture.

This is a stacked draft targeting the head branch of #2829 (t3code/codex-turn-mapping); it can be retargeted to main after that PR lands.

  • adds a scoped Pi JSONL RPC transport with correlated requests, strict LF framing, process cleanup, and Windows command resolution
  • projects Pi streaming text, reasoning, tools, retries, compaction, terminal state, approvals, and user-input requests into Orchestrator V2 events
  • persists Pi session identity and resumes existing sessions with switch_session
  • discovers account-specific provider/model slugs and exposes Pi's seven thinking levels
  • forwards image attachments for prompts and steering
  • adds isolated RPC-backed text generation for commit messages, PR content, branch names, and task titles
  • registers Pi across provider settings, model selection, web icons, and mobile icons

Why

T3 currently has first-party Codex, Claude, Cursor, Grok, OpenCode, and ACP integrations, but no Orchestrator V2 driver for Pi. Pi's official RPC mode provides the session, streaming, model-selection, tool, and extension UI primitives needed for a native adapter without scraping terminal output.

The implementation intentionally stays inside T3 because the provider, orchestration, settings, and presentation contracts are currently internal. Repository coordination and Forgejo authentication remain outside this driver; Pi can invoke the shared forgejo-bridge CLI through its normal shell tools without duplicating API or credential logic.

User impact

After enabling Pi in provider settings, users can select discovered Pi models, choose supported thinking levels, run persistent coding sessions, resume them after T3 runtime restarts, steer or interrupt active work, answer extension prompts, attach images, and use Pi for T3's generated Git metadata.

Pi remains disabled by default and is marked Early Access.

Intentional limitations

  • provider-native conversation rollback and fork are disabled until Pi session entry IDs are projected safely; T3 filesystem checkpoints remain available
  • Pi usage is decoded and validated, but Orchestrator V2 does not currently expose a provider usage-accounting event surface
  • this branch was validated with deterministic RPC process fixtures because the Pi CLI was not installed on the development host

Validation

  • pnpm exec vp test run src/orchestration-v2/Adapters/PiRpcTransport.test.ts src/orchestration-v2/Adapters/PiRpcProtocol.test.ts src/provider/Layers/PiProvider.test.ts src/provider/Drivers/PiDriver.test.ts src/orchestration-v2/Adapters/PiAdapterV2.test.ts src/textGeneration/PiTextGeneration.test.ts — 20 tests passed
  • pnpm exec vp test run --project unit src/components/settings/ProviderSettingsForm.test.ts — 13 tests passed
  • pnpm exec vp test run src/settings.test.ts — 34 tests passed
  • server, web, contracts, and mobile typechecks passed
  • focused lint, formatting, and whitespace checks passed

References

Note

Add Pi coding agent as a new provider with orchestration, text generation, and settings UI

  • Introduces the pi provider driver backed by the @earendil-works/pi-coding-agent npm package, disabled by default with binaryPath: 'pi'.
  • Adds PiAdapterV2 implementing the full orchestration V2 interface: streaming text/tool events, interrupt/steer, extension UI approvals, thread snapshots, and image attachments via base64 encoding.
  • Adds PiTextGeneration for structured RPC-backed generation of thread titles, commit messages, PR content, and branch names.
  • Adds PiProvider with CLI health probing, RPC-based model discovery, and thinking-level capability descriptors for reasoning models.
  • Registers the provider across contracts (settings schema, display name, model slug aliases), server driver registry, orchestration V2 adapter registry, and web/mobile UI (icon, picker badge, settings form).
📊 Macroscope summarized 5c2ce2b. 16 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

juliusmarminge and others added 30 commits August 11, 2026 18:40
Co-authored-by: codex <codex@users.noreply.github.com>
- Initialize provider as unchecked in a pending state
- Update initial probe message to reflect session-local status
- Type the runtime effect with `Scope`
- Build the ACP session runtime without wrapping it in `Effect.scoped`
- Use strict TurnId and ProviderItemId parsing in Codex session routing
- Decode in-memory stdio chunks in streaming mode to avoid split UTF-8 corruption
- Transfer session-owned scopes into adapter state
- Ensure runtime scopes close on stop and startup failure
- Add regression coverage for scoped lifecycle cleanup
- Close the managed native event logger when the adapter layer tears down
- Make session runtime close idempotent with an atomic closed flag
- Add coverage for flushing thread native logs on shutdown
- Use codex app-server snapshots for auth, models, and skills
- Remove legacy CLI/config discovery paths and related helpers
- Update tests for the new provider status flow
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
- Document the target orchestration graph, IDs, lifecycles, and capability model
- Add Codex app-server probe fixtures and update the probe test harness
- Introduce orchestration v2 service interfaces and error types
- Add replay runtime, fixtures, and integration coverage
- Update shared contracts and probe transcripts

Co-authored-by: codex <codex@users.noreply.github.com>
- Add Codex adapter and replay harness wiring
- Introduce in-memory orchestration projections and provider registry
- Expand orchestration contracts for turn and runtime events
Co-authored-by: codex <codex@users.noreply.github.com>
- Add context transfer IDs, schemas, and projections
- Support cheap fork creation and Codex native fork rollback
- Cover fork idempotency and replay behavior in tests
- Track remaining projection, context transfer, rollback, capability, and subagent work
- Clarify current V2 baseline and debugger-only follow-ups
- Map fork and merge-back turns into stored handoffs and transfer resolutions
- Add shell snapshot projection support plus coverage tests
- Update replay fixtures and web contracts for the new turn flow
Co-authored-by: codex <codex@users.noreply.github.com>
- Move Codex replay recording into `apps/server`
- Add Claude Agent SDK replay fixtures and test harness
- Update orchestration-v2 fixture scenarios and docs
- Move Claude provider runtime logic into its own module
- Share the SDK query runner between live and replay paths
- Add replay driver error wrapping for unexpected failures
- persist the selected model on run records
- surface run model selection in the debug UI
- update replay fixtures and contracts for the new field
- Record Claude SDK transcripts across multiple prompts and restart/query modes
- Add approval and tool-call replay coverage for new orchestration fixtures
- Update Claude adapter testkit to model open/prompt/permission frames
- Derive Claude SDK query options from runtime policy
- Add read-only replay fixture and policy mapping tests
- Reuse shared approval-policy fixtures across orchestrator tests

Co-authored-by: codex <codex@users.noreply.github.com>
- add active steering and interrupt-restart replay fixtures
- update Claude adapter/orchestrator turn handling for steering
- refresh replay and integration test coverage
- add interrupt and mid-tool replay fixtures for Claude and Codex
- log Claude Agent SDK protocol frames to native event traces
- project Codex commandExecution start events into orchestration updates
- Map Cursor SDK agents and runs to V2 thread and turn lifecycles
- Update MCP capability, tool, and testing guidance for SDK-based injection
Co-authored-by: codex <codex@users.noreply.github.com>
mwolson and others added 18 commits August 11, 2026 18:46
- Match progress button spacing and single-line height to static git actions
rerere replayed stale resolutions during the rebase and committed nested
conflict markers in several files. Restore the branch-intended v2 shapes
and re-graft main's compatible additions (pending-card opacity comments,
theme-editor keybinding test, mobile scroll re-arm effects from pingdotgg#5566).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eysetIndex

Main owns migration numbering: 037_ProjectionTurnsKeysetIndex landed on
main (pingdotgg#5493), so the v2 migrations shift from 037-045 to 038-046.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Native subagent observability (pingdotgg#5219), wired per its spec's v2 merge plan:
- getWorkflowScript RPC re-homed onto the v2 WS surface (contracts, rpc
  group, ws handler, auth scope, client atom).
- AgentsPanel fed by the spec's mapper swap: projectedSubagentsToRuntime
  maps orchestration-v2 subagent entities into the panel model;
  deriveAgentPanelModel's v2Projection leg is now live and the v1 fold
  never runs. Agents surface wired into ChatView + RightPanelTabs.
Other ports and reconciliations:
- Shell reconnect-loop fix (pingdotgg#5561) ported into the v2 shell sync
  (same-session resubscribes resume from the in-memory cursor), with the
  cursor-resume regression test adapted to v2 fixtures.
- Mobile end-follow latch (pingdotgg#5566) ported onto the v2 ThreadFeed.
- Claude ede_diagnostic interrupt classification (pingdotgg#5557) ported into
  ClaudeAdapterV2 (aborted_tools/aborted_streaming => interrupted; CLI
  telemetry never becomes the failure banner). pingdotgg#5559 needs no v2 port
  (unknown system subtypes are already ignored).
- Plan sidebar removed from the v2 ChatView/ChatComposer per main's
  plans-fold-into-chat rework (pingdotgg#5558); rightPanelStore stays at main's
  surface set.
- SettingsPanels rebuilt as main's refactored version plus the branch's
  composer-context setting; sidebar snooze respects the time format
  (pingdotgg#4438 follow-through).
- v1-only leftovers deleted: zombie v1 adapters/ingestion/tests the v2
  rewrite removes, the v1-bound transfer-budget CI harness (pingdotgg#5350, needs
  a v2 rebuild), and main's v1 client pagination machinery (pingdotgg#5493 client
  side; the 037 keyset migration is kept — server-side v2 windowing is a
  follow-up).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rebase kept the LegendList 3.3.3 upgrade and patch from pingdotgg#5449 and the
mobile end-follow latch from pingdotgg#5566, but the v2 MessagesTimeline/ChatView
still carried the branch's blunt any-gesture-breaks-follow listeners.
Port main's pingdotgg#5566 web mechanics onto the v2 follow architecture:

- resolveTimelineIsAtEnd measures the 40px follow re-arm band from real
  geometry (contentLength/scroll/scrollLength minus the composer inset),
  keeping the isNearEnd fallback for older state shapes.
- Follow now breaks only on gestures that can actually leave the live
  edge: upward wheel with overflowing content, touch drags that exited
  the end band, scrollbar drags vs content clicks, and keyboard
  navigation (PageUp/Home/ArrowUp) — previously keyboard scrolling never
  broke follow and the next stream chunk yanked the view back down.
- Listener attach retries across frames so a thread switch cannot mount
  the list without its opt-out listeners.

Deliberately not ported: pingdotgg#5449's shouldRestorePosition disclosure
anchoring and follow-gated maintainScrollAtEnd — the v2 timeline keeps
maintainScrollAtEnd={false} with its own follow scrolls and anchor
system; flipping that core is a separate change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gdotgg#5449)

Complete the pingdotgg#5449 architecture on the v2 timeline, following the
LegendList author's direction to lean on the list's native mechanisms
instead of app-side scroll layers:

- maintainScrollAtEnd is enabled and owned by LegendList, gated off only
  while the user reads history (liveFollowEnabled), while a sent turn
  anchors near the top (anchoredEndSpace), or during the two-frame settle
  of a fold toggle.
- maintainVisibleContentPosition compensates size changes natively
  ({data, size, shouldRestorePosition}); fold toggles anchor compensation
  to the toggled row via a disclosure anchor key, so the trigger stays
  under the pointer instead of the viewport chasing the end.
- ChatView's hand-rolled streaming follow (double-rAF scrollToEnd on
  every data change) is gone; the app now only owns streaming
  adjustments during anchored-end-space mode, mirroring main.
- timelineLiveFollowEnabled state mirrors the follow refs so the
  render-visible gate switches native follow off when a gesture breaks
  follow and back on when the viewport returns to the end band.

Timeline tests updated to assert the native-ownership invariants.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Keep success feedback visible in the Git action control for 10 seconds
- Move the running elapsed timer into the panel menu slot
…s with v2

Post-rebase reconciliation sweep:
- Sidebar: main's folded Sidebar.tsx/Sidebar.logic.ts adapted to v2 shells
  (latestRun/runtime naming, waiting status instead of monitoring), with
  subagent-thread filtering and main's pinned-reorder helpers re-exported
- Pinned drag reorder (pingdotgg#5581) ported into v2: thread.pin orderKey +
  thread.pin.reorder command, thread.pin-reordered event, Orchestrator fold,
  ProjectionStore/Maintenance, client-runtime commands and shell mapping
- Project favicon (pingdotgg#4849-era) and defaultThreadEnvMode flowed through v2
  contracts (OrchestrationProjectShell, application event payloads)
- ChatView: main's pingdotgg#5592 header props, pull-request right-panel surfaces,
  liveAgentCount badge (pingdotgg#5745) wired into the v2 panel layout
- enableAssistantStreaming -> enableLegacyTokenStreaming rename applied to
  v2 RunExecutionService and replay testkit
- Removed v1 zombies resurrected by the rebase (provider service/reaper/
  ingestion + v1 layer tests, server.test.ts, integration harness)
- routeTree: main's tree + branch's /settings/scheduled-tasks route
- Misc marker-sweep syntax repairs (rpc.ts, entities.ts, localApi.test.ts,
  rightPanelStore.test.ts, GitManager.test.ts, mobile model menu helpers)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… 038-040

Main released ProjectionThreadsPinOrderKey (038),
ProjectionProjectsDefaultThreadEnvMode (039) and
ProjectionProjectFaviconPath (040), so the branch-private v2 stack shifts
up by three. Registry ids were already 41-49; this renames the files and
identifiers to match and updates the ledger expectations and through-id
boundaries in the migration tests (released boundary 37 -> 40).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- a6c9b41 (agents open pasted images): ClaudeAdapterV2 now grants the
  attachments dir alongside cwd via additionalDirectories and appends
  '[Attached ... is saved at: path]' lines to the turn text so tools can
  dereference pasted images (pixels alone are not tool-readable).
- 5bb8c03 (settle leaves monitors running): thread.settle now joins
  archive/delete in the provider-session detach set, so PR monitors, dev
  servers and subagent fleets stop when the user parks the thread. The
  settle guard already rejects active runs, and serialized dispatch closes
  the re-engage race the v1 fix handled with onlyIfSettled.
- e70cdb4 (Claude resume handshakes) and 2c7267a (reaper vs live
  background subagents) are already covered structurally in v2: results
  are turn-scoped with explicit zero-turn handshake drops, and idle
  release is pinned while background work is pending.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e panel-visibility merge

The keep-both merge nested main's plan-surface migration test inside a
branch popover test and dropped the threadPanelVisibilityByThreadKey key
from the migration results. Restore main's test body and include the
branch's (empty) visibility map in the expected persisted shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n text

Follow-up to the pingdotgg#5757 port: start and steer turns now append the
'[Attached ... is saved at: path]' line, so the adapter fixtures assert it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The round-3 reconciliation took main's ChatHeader wholesale and wired its
full prop set, resurrecting the scripts/open-in/git-actions cluster the
branch had deliberately relocated into the thread panel. Restore the
79-line slim header (project favicon + name + thread title) and its
minimal ChatView call. pingdotgg#5592's header actions stay a documented v2
follow-up, as decided in round 2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add ready, merge, and conflict-resolution actions to the PR row
- Share pull request action and handoff logic with the detail panel
- Fix thread details scrolling and row alignment
…reens

Round-4 reconciliation of pingdotgg#5986/pingdotgg#5988 with the v2 cutover files:
- PendingUserInputCard adopts main's collapsible overlay redesign with the
  v2 RuntimeRequestId/responseCapability plumbing (dead provider processes
  still read-only the card)
- ThreadFeed adopts the thread-feed-live-follow transition model, the
  user-scroll settle window, momentum handoff, and env-scoped feed keys
  while keeping the v2 nearListEnd layout gating
- ThreadDetailScreen hides (not unmounts) the composer while a user-input
  request owns the slot; multi-select answers flow as arrays end to end
  (threadActivity toggle/build helpers + tests, ThreadUserInputQuestion)
- ThreadComposer keeps the v2 canStopThread stop gate under main's
  onEditorFocusChange rename; standalone stop reuses the shared renderer
- Restored the branch's steer stop/send tests alongside main's composer
  test suite

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 51996517-1b64-46fd-80a9-fc343c40c304

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Aug 12, 2026
status: "active",
firstRunOrdinal: state.providerThread.firstRunOrdinal ?? turnInput.runOrdinal,
});
yield* updateSession("running");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Adapters/PiAdapterV2.ts:1019

After startTurn calls set_model, providerSession.model is never updated, so updateSession("running") keeps the model from openSession and providerSession.model misreports the model actually running the turn. Consider updating providerSession.model to turnInput.modelSelection.model before or during updateSession("running") so session snapshots reflect the switch.

              yield* updateThread(state, {
                status: "active",
                firstRunOrdinal: state.providerThread.firstRunOrdinal ?? turnInput.runOrdinal,
              });
-             yield* updateSession("running");
+             yield* updateSession("running", null, turnInput.modelSelection.model);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/Adapters/PiAdapterV2.ts around line 1019:

After `startTurn` calls `set_model`, `providerSession.model` is never updated, so `updateSession("running")` keeps the model from `openSession` and `providerSession.model` misreports the model actually running the turn. Consider updating `providerSession.model` to `turnInput.modelSelection.model` before or during `updateSession("running")` so session snapshots reflect the switch.

});
}

const models = modelsFromSettings(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/PiProvider.ts:243

authenticated is derived from the merged models list, which includes settings.customModels via providerModelsFromSettings. When RPC discovery fails or returns no models but the user has configured custom models, the provider is incorrectly reported as ready and authenticated, suppressing the warning that no models are available. Authentication should be determined from the discovered RPC models before merging custom settings.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/PiProvider.ts around line 243:

`authenticated` is derived from the merged `models` list, which includes `settings.customModels` via `providerModelsFromSettings`. When RPC discovery fails or returns no models but the user has configured custom models, the provider is incorrectly reported as `ready` and `authenticated`, suppressing the warning that no models are available. Authentication should be determined from the discovered RPC models before merging custom settings.

Effect.forkScoped,
);

const request = Effect.fn("PiRpcTransport.request")(function* (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Adapters/PiRpcTransport.ts:225

If a request fiber is interrupted after it registers its deferred in pendingRequests but before it completes, the map entry is never removed. A later response for that orphaned id is silently consumed by the abandoned deferred instead of being exposed on messages, and repeated cancelled requests leak entries until transport shutdown. The cleanup at the end of request only runs on normal completion — interruption skips it. Wrap the body in Effect.ensuring(() => Effect.sync(() => pendingRequests.delete(id))) so the entry is removed regardless of how the fiber terminates.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/Adapters/PiRpcTransport.ts around line 225:

If a `request` fiber is interrupted after it registers its deferred in `pendingRequests` but before it completes, the map entry is never removed. A later response for that orphaned `id` is silently consumed by the abandoned deferred instead of being exposed on `messages`, and repeated cancelled requests leak entries until transport shutdown. The cleanup at the end of `request` only runs on normal completion — interruption skips it. Wrap the body in `Effect.ensuring(() => Effect.sync(() => pendingRequests.delete(id)))` so the entry is removed regardless of how the fiber terminates.

>,
) {
const now = yield* DateTime.now;
const nativeItemId = projected.toolCallId;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Adapters/PiAdapterV2.ts:572

On tool.completed, emitTool overwrites the tool call's input with {}, so the final turn_item.updated event drops the arguments that were emitted on tool.started/tool.updated. Persisted completed tool calls end up with empty input. This is because projected.args is absent in the completion projection, and the code uses projected.type === "tool.completed" ? {} : projected.args. Track the last emitted args per toolCallId and reuse it on completion instead of resetting to {}.

Suggested change
const nativeItemId = projected.toolCallId;
const nativeItemId = projected.toolCallId;
const lastArgs = turn.toolArgs.get(nativeItemId) ?? projected.args ?? {};
if (projected.type !== "tool.completed") turn.toolArgs.set(nativeItemId, projected.args ?? {});
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/Adapters/PiAdapterV2.ts around line 572:

On `tool.completed`, `emitTool` overwrites the tool call's `input` with `{}`, so the final `turn_item.updated` event drops the arguments that were emitted on `tool.started`/`tool.updated`. Persisted completed tool calls end up with empty input. This is because `projected.args` is absent in the completion projection, and the code uses `projected.type === "tool.completed" ? {} : projected.args`. Track the last emitted `args` per `toolCallId` and reuse it on completion instead of resetting to `{}`.

threadDisposition: "reusable",
},
);
delete state.activeTurn;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Adapters/PiAdapterV2.ts:810

When a Pi turn reaches terminal status, any pending runtime requests (extension UI prompts) for that turn are left in pendingPrompts and state.runtimeRequests with status "pending" forever. The corresponding nodes and turn items also remain "waiting". This means an interrupted or failed turn leaves stale approval/user-input requests that never resolve, and respondToRuntimeRequest will still accept and send responses for them afterward.

In handleProjected for run.terminal, the code finalizes content and deletes state.activeTurn but never iterates pendingPrompts to cancel or resolve entries belonging to the terminal turn. Consider resolving all pending prompts for that turn (emitting terminal node/turn-item/runtime-request updates and sending extension_ui_response with cancelled: true to the transport) before clearing state.activeTurn.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/Adapters/PiAdapterV2.ts around line 810:

When a Pi turn reaches terminal status, any pending runtime requests (extension UI prompts) for that turn are left in `pendingPrompts` and `state.runtimeRequests` with status `"pending"` forever. The corresponding nodes and turn items also remain `"waiting"`. This means an interrupted or failed turn leaves stale approval/user-input requests that never resolve, and `respondToRuntimeRequest` will still accept and send responses for them afterward.

In `handleProjected` for `run.terminal`, the code finalizes content and deletes `state.activeTurn` but never iterates `pendingPrompts` to cancel or resolve entries belonging to the terminal turn. Consider resolving all pending prompts for that turn (emitting terminal node/turn-item/runtime-request updates and sending `extension_ui_response` with `cancelled: true` to the transport) before clearing `state.activeTurn`.

Comment on lines +209 to +214
yield* Stream.fromQueue(outgoing).pipe(
Stream.run(child.stdin),
Effect.mapError((cause) => new PiRpcTransportError({ operation: "write", cause })),
Effect.catch((error) => Queue.fail(messages, error)),
Effect.forkScoped,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High Adapters/PiRpcTransport.ts:209

When Stream.run(child.stdin) fails, only messages is failed — closed is never completed and outgoing is never ended. As a result, subsequent send calls still enqueue data and report success even though nothing is writing to the child, and request calls block until their full timeout instead of detecting the broken transport. The write-failure path should call markClosed (or at least complete closed and end outgoing) so callers see the transport is dead.

  yield* Stream.fromQueue(outgoing).pipe(
    Stream.run(child.stdin),
    Effect.mapError((cause) => new PiRpcTransportError({ operation: "write", cause })),
-    Effect.catch((error) => Queue.fail(messages, error)),
+    Effect.catch((error) =>
+      Effect.gen(function* () {
        yield* Queue.fail(messages, error);
        yield* markClosed();
      }),
    ),
    Effect.forkScoped,
  );
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/Adapters/PiRpcTransport.ts around lines 209-214:

When `Stream.run(child.stdin)` fails, only `messages` is failed — `closed` is never completed and `outgoing` is never ended. As a result, subsequent `send` calls still enqueue data and report success even though nothing is writing to the child, and `request` calls block until their full timeout instead of detecting the broken transport. The write-failure path should call `markClosed` (or at least complete `closed` and end `outgoing`) so callers see the transport is dead.

Comment on lines +103 to +107
Schema.Struct({
type: Schema.Literal("message_update"),
message: PiMessage,
assistantMessageEvent: PiAssistantMessageEvent,
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High Adapters/PiRpcProtocol.ts:103

The message_update schema requires a message field, but Pi's RPC protocol omits message from message_update events and sends only assistantMessageEvent. Every normal streaming update therefore fails decodeKnownEvent and is converted to PiRpcProtocolError, so text_delta and thinking_delta deltas are never projected. Making message optional (or removing it) aligns the schema with the actual protocol.

Suggested change
Schema.Struct({
type: Schema.Literal("message_update"),
message: PiMessage,
assistantMessageEvent: PiAssistantMessageEvent,
}),
Schema.Struct({
type: Schema.Literal("message_update"),
message: Schema.optional(PiMessage),
assistantMessageEvent: PiAssistantMessageEvent,
}),
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/Adapters/PiRpcProtocol.ts around lines 103-107:

The `message_update` schema requires a `message` field, but Pi's RPC protocol omits `message` from `message_update` events and sends only `assistantMessageEvent`. Every normal streaming update therefore fails `decodeKnownEvent` and is converted to `PiRpcProtocolError`, so `text_delta` and `thinking_delta` deltas are never projected. Making `message` optional (or removing it) aligns the schema with the actual protocol.

...(timeoutMs === undefined ? {} : { timeoutMs }),
};
}
if (method === "editor") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Adapters/PiRpcProtocol.ts:467

projectPiExtensionUiRequest drops the validated timeout for editor requests, even though the user-input prompt type supports timeoutMs and the select and input branches preserve it. An editor request carrying a timeout is returned without its deadline, so the runtime can keep accepting input after Pi has already timed out the request. The editor branch is missing the conditional timeoutMs property that the other branches include.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/Adapters/PiRpcProtocol.ts around line 467:

`projectPiExtensionUiRequest` drops the validated `timeout` for `editor` requests, even though the `user-input` prompt type supports `timeoutMs` and the `select` and `input` branches preserve it. An editor request carrying a timeout is returned without its deadline, so the runtime can keep accepting input after Pi has already timed out the request. The `editor` branch is missing the conditional `timeoutMs` property that the other branches include.

completedAt: null,
};
state.providerTurns.set(String(providerTurn.id), providerTurn);
state.activeTurn = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High Adapters/PiAdapterV2.ts:1006

startTurn emits active thread, running session, and running provider-turn updates and installs state.activeTurn before sending the prompt RPC. When requestData for the prompt rejects or times out, the function fails with ProviderAdapterTurnStartError, but the already-emitted active/running state and state.activeTurn are never rolled back and no terminal event is emitted — leaving the session and thread stuck running indefinitely. Move the state mutations and emissions to after the prompt RPC succeeds, or roll back state.activeTurn, reset the thread/session status, and emit a terminal event in the error path.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/Adapters/PiAdapterV2.ts around line 1006:

`startTurn` emits `active` thread, `running` session, and `running` provider-turn updates and installs `state.activeTurn` *before* sending the `prompt` RPC. When `requestData` for the `prompt` rejects or times out, the function fails with `ProviderAdapterTurnStartError`, but the already-emitted `active`/`running` state and `state.activeTurn` are never rolled back and no terminal event is emitted — leaving the session and thread stuck running indefinitely. Move the state mutations and emissions to after the `prompt` RPC succeeds, or roll back `state.activeTurn`, reset the thread/session status, and emit a terminal event in the error path.

isError: event.isError,
},
];
case "agent_end": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High Adapters/PiRpcProtocol.ts:346

projectPiEvent emits run.terminal on agent_end when willRetry is false, but agent_end only signals the end of one low-level run — compaction retries or queued continuations can still follow. Because consumers finalize and delete the active turn on run.terminal, any subsequent compaction or follow-up continuation output is silently dropped and the turn is reported complete prematurely. The terminal projection should be deferred to agent_settled (which means no continuation remains), while still carrying the final stop/error state from the last assistant message.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/Adapters/PiRpcProtocol.ts around line 346:

`projectPiEvent` emits `run.terminal` on `agent_end` when `willRetry` is false, but `agent_end` only signals the end of one low-level run — compaction retries or queued continuations can still follow. Because consumers finalize and delete the active turn on `run.terminal`, any subsequent compaction or follow-up continuation output is silently dropped and the turn is reported complete prematurely. The terminal projection should be deferred to `agent_settled` (which means no continuation remains), while still carrying the final stop/error state from the last assistant message.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effect service conventions review of the new Pi provider modules. Three mechanical convention issues found in PiAdapterV2.ts / PiAdapterV2.test.ts; the transport, protocol, driver, provider-snapshot, and text-generation modules otherwise follow the repo's Effect conventions (subpath namespace imports, Schema.TaggedErrorClass failures with structural attributes and preserved cause, dependencies acquired with yield* Service in PiAdapterV2Driver.create / PiDriver.create / makePiTextGeneration, no ManagedRuntime/runPromise outside test boundaries).

Posted via Macroscope — Effect Service Conventions

};
const events = yield* Queue.unbounded<
ProviderAdapterV2Event,
import("../ProviderAdapter.ts").ProviderAdapterV2Error | Cause.Done<void>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline import("...") type expressions bypass the module's import list. ../ProviderAdapter.ts and @t3tools/contracts are already imported at the top of this file, so it would be more consistent to add ProviderAdapterV2Error (here), ProviderAdapterV2EnsureThreadInput (line 846), ProviderAdapterV2RuntimePolicy, ThreadId, and ModelSelection (lines 928-930) to those existing type imports and reference them directly.

Posted via Macroscope — Effect Service Conventions

readonly instanceId: ProviderInstanceId;
readonly settings: PiSettings;
readonly environment: NodeJS.ProcessEnv;
readonly idAllocator: IdAllocatorV2Shape;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IdAllocatorV2Shape is the legacy standalone shape type. Consider referring to the inferred service interface instead — readonly idAllocator: IdAllocatorV2["Service"]; (as GrokAdapterV2Options already does) — and dropping the then-unused type IdAllocatorV2Shape from the import on line 33.

Posted via Macroscope — Effect Service Conventions

import * as Stream from "effect/Stream";
import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";

import { IdAllocatorV2, layer as idAllocatorLayer } from "../IdAllocator.ts";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

layer as idAllocatorLayer erases the service module namespace. Consider import * as IdAllocator from "../IdAllocator.ts"; and using IdAllocator.IdAllocatorV2 / IdAllocator.layer at the two use sites instead of the renamed layer export.

Posted via Macroscope — Effect Service Conventions

@juliusmarminge
juliusmarminge force-pushed the t3code/codex-turn-mapping branch 4 times, most recently from 96891a3 to 993407d Compare August 15, 2026 15:26
@juliusmarminge
juliusmarminge force-pushed the t3code/codex-turn-mapping branch from 519c42a to 4c55679 Compare August 17, 2026 10:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants