--- title: "plan: Replace the embedded PI agent harness with Codex app-server" type: plan status: active date: 2026-04-08 --- # plan: Replace the embedded PI agent harness with Codex app-server ## Summary Replace the embedded PI execution engine incrementally, not the whole OpenClaw agent stack in one move. The safe cut point is below `runEmbeddedPiAgent` and above PI's `createAgentSession` call: - keep `src/agents/pi-embedded.ts` exports stable - keep `src/agents/pi-embedded-runner/run.ts` orchestration first - add a Codex app-server attempt backend beside `src/agents/pi-embedded-runner/run/attempt.ts` - preserve `RunEmbeddedPiAgentParams` and `EmbeddedPiRunResult` - keep the current PI backend as fallback until parity is proven This keeps the broad caller surface intact while replacing the highest-churn agent harness internals. Implementation status on `app-server`: - `runEmbeddedPiAgent` now calls an internal backend selector. - `OPENCLAW_AGENT_RUNTIME=codex-app-server` forces the Codex app-server backend. - `OPENCLAW_AGENT_RUNTIME=auto` tries app-server for `openai-codex` and falls back to PI if app-server fails. - default is `auto`; set `OPENCLAW_AGENT_RUNTIME=pi` for the legacy PI kill switch. - the app-server backend has a long-lived stdio JSON-RPC client, thread sidecar binding, turn start/interrupt/steer wiring, event projection, and dynamic OpenClaw tool bridging. - transcript mirroring is idempotent per app-server thread turn. - app-server compaction items now drive active-run compaction state and compaction events. - guardian review notifications are projected as diagnostic agent events only. - native app-server command/file/permission approval callbacks route through OpenClaw plugin approval requests, then fail closed if no approval route is available. ## Goal Use `codex app-server` for the core agent execution path without losing the OpenClaw behaviors that users notice: - session resume - in-flight queueing and steering - abort and restart behavior - channel-aware replies - reasoning and plan streaming - message/subagent/cron/plugin tools - compaction and context pressure recovery - session metadata and token/cost reporting - command/file approval policy - eventual guardian-backed approval review ## Non-Goals - Do not replace CLI backends in the first slice. - Do not remove the PI runner until app-server parity is measured. - Do not force all model providers through app-server on day one. - Do not move channel, plugin, or session-store code just to make the first backend work. - Do not expose app-server protocol details to channel/plugin code. - Do not depend on the experimental websocket transport for production. ## Current Shape `src/agents/pi-embedded.ts` is a narrow barrel. It exports the public functions and types used by the rest of OpenClaw: - `runEmbeddedPiAgent` - `queueEmbeddedPiMessage` - `abortEmbeddedPiRun` - `compactEmbeddedPiSession` - active-run checks and wait helpers - `EmbeddedPiRunResult` and related metadata types Most callers should not know whether PI or Codex app-server executes a turn. `src/agents/command/attempt-execution.ts` is the main user-run entrypoint. It selects the CLI backend for CLI providers, otherwise calls `runEmbeddedPiAgent` with channel routing, session identity, model/profile settings, workspace, skills, images, callbacks, and abort state. `src/agents/pi-embedded-runner/run.ts` does important OpenClaw orchestration: - session key backfill - global and per-session lanes - workspace resolution - runtime plugin loading - model/provider resolution - auth profile order and cooldown handling - fallback and retry policy - context engine setup and maintenance - compaction side effects - timeout and overflow recovery - result payload building - token usage accumulation - model switch handling This is too much to throw away in a first pass. `src/agents/pi-embedded-runner/run/attempt.ts` is the heavy PI-specific layer. It creates PI sessions, builds PI tools, opens the PI session manager, submits the prompt, subscribes to PI events, handles active-run steering, and deals with PI transcript/compaction details. That is the best first replacement target. ## Why Not Replace `pi-embedded.ts` Directly Replacing the barrel would force every downstream caller to absorb app-server semantics at once. That risks breaking behavior spread across: - auto reply command execution - gateway session control - subagent spawning and follow-up - queue modes - cron and heartbeat runs - tests that assert PI-shaped metadata Keeping the public barrel stable lets the migration behave like a backend swap, not a public agent API migration. ## Proposed Architecture Add an internal execution backend seam: ```ts type EmbeddedAgentAttemptBackend = { id: "pi" | "codex-app-server"; runAttempt(params: EmbeddedAgentAttemptParams): Promise; }; ``` The exact type names can be local to `src/agents/pi-embedded-runner/run.ts`. The contract should use the existing attempt/result data already returned by `runEmbeddedAttempt` rather than leaking Codex protocol types upward. Initial module layout: - `src/agents/codex-app-server-runner/client.ts` JSON-RPC stdio client, initialize/initialized handshake, request id routing, notification dispatch, server-initiated request handling. - `src/agents/codex-app-server-runner/protocol.ts` Hand-curated local subset of the generated app-server protocol. Refresh against `../codex/codex-rs/app-server-protocol/schema/typescript` when app-server changes. Run `pnpm codex-app-server:protocol:check` to catch drift in the generated event/request shapes this bridge depends on. - `src/agents/codex-app-server-runner/session-binding.ts` Map OpenClaw session files to Codex thread ids through a sidecar file. - `src/agents/codex-app-server-runner/event-projector.ts` Convert app-server events into OpenClaw callbacks and final result state. - `src/agents/codex-app-server-runner/dynamic-tools.ts` Convert OpenClaw tool definitions into app-server dynamic tools. - `src/agents/codex-app-server-runner/run-attempt.ts` Codex app-server backend implementation. Keep app-server-specific imports out of channel/plugin code. ## Runtime Process Model Prefer one long-lived app-server child process per OpenClaw gateway process. Reasons: - app-server already multiplexes threads - one process keeps model/config discovery warm - session resume maps naturally to app-server threads - process restart can be handled by reinitializing and resuming known threads MVP fallback: one app-server process per run or per session is acceptable only for early smoke tests. It should not become the long-term design because it wastes startup time and makes thread lifecycle harder to reason about. Transport: - use stdio JSONL: `codex app-server --listen stdio://` - do not use websocket in production yet; app-server marks it experimental and unsupported - initialize with `clientInfo.name = "openclaw"` - set `capabilities.experimentalApi = true` only when dynamic tools or guardian telemetry are enabled ## Session Binding First implementation uses a sidecar binding next to the PI session file: ```txt .codex-app-server.json ``` This avoids changing `SessionEntry` and the session-store contract while the backend is still feature-flagged. Longer term, move the binding into `SessionEntry`. Recommended first shape: ```ts export type SessionCodexAppServerBinding = { threadId: string; cwd?: string; model?: string; modelProvider?: string; codexHome?: string; createdAt: number; updatedAt: number; schemaVersion: 1; }; ``` Then: ```ts export type SessionEntry = { // existing fields... codexAppServer?: SessionCodexAppServerBinding; }; ``` Do not overload `cliSessionBindings`. CLI session ids, ACP runtime metadata, and Codex app-server thread ids have different lifecycle semantics. OpenClaw mirrors the minimal user/assistant turn back into the existing PI JSONL transcript so session views, cost readers, and transcript-indexing hooks still see the Codex app-server path. ## App-Server Protocol Mapping Thread lifecycle: - no binding: call `thread/start` - existing binding: call `thread/resume` - `/new` or reset: clear the binding and create a new thread - branch/fork support: defer until core resume is stable Turn lifecycle: - user prompt and images: `turn/start` - queued in-flight user text: `turn/steer` - abort: `turn/interrupt` - manual compaction: `thread/compact/start` - shell command outside agent turn: do not use initially Turn input: - text prompt -> `{ type: "text", text }` - remote image URL -> `{ type: "image", url }` - local screenshot/file image -> `{ type: "localImage", path }` - skills can later use app-server skill input items, but MVP should preserve OpenClaw's existing skills prompt snapshot to avoid a second migration. Config mapping: - `workspaceDir` -> `cwd` - `model` -> app-server `model` - OpenClaw provider -> app-server `modelProvider` only for providers app-server actually supports - `thinkLevel` / `reasoningLevel` -> `effort` where possible - approval policy -> app-server `approvalPolicy` - sandbox config -> app-server `sandbox` on thread start and `sandboxPolicy` on turn start - channel/runtime instructions -> `developerInstructions` or turn input wrapper, not a replacement for Codex base instructions ## Event Projection Create an event projector that owns all app-server to OpenClaw conversion. Minimum mappings: | App-server event | OpenClaw behavior | | -------------------------------------- | --------------------------------- | | `turn/started` | lifecycle event, active run state | | `item/agentMessage/delta` | partial reply/block reply stream | | `item/completed` for `agentMessage` | final assistant text | | `item/reasoning/summaryTextDelta` | `onReasoningStream` | | `item/reasoning/textDelta` | optional raw reasoning stream | | `item/plan/delta` | plan event stream | | `item/started` tool/command/file/MCP | tool/status event | | `item/completed` tool/command/file/MCP | tool result/status event | | `item/commandExecution/outputDelta` | command output event | | `item/fileChange/outputDelta` | file edit output event | | `turn/completed` | final `EmbeddedPiRunResult` | | `thread/status/changed` | diagnostic/lifecycle event | | `contextCompaction` item | compaction count/status | | guardian review notifications | best-effort approval telemetry | The projector should return the same data the PI subscription layer currently extracts: - assistant texts - reasoning texts - tool summaries - usage totals when available - compaction count - message-tool side effects - cron additions - final stop reason - abort/error state Do not reuse `subscribeEmbeddedPiSession` directly. It expects PI `AgentSession` events. Reuse smaller helpers from the PI subscription layer only when they are protocol-independent. ## Active Run Mapping Keep `src/agents/pi-embedded-runner/runs.ts`. The app-server backend registers an `EmbeddedPiQueueHandle`: - `queueMessage(text)` -> `turn/steer` - `abort()` -> `turn/interrupt` - `isStreaming()` -> current app-server turn state - `isCompacting()` -> app-server compaction state - `cancel(reason)` -> local cancellation plus app-server interrupt This preserves `queueEmbeddedPiMessage`, `abortEmbeddedPiRun`, `isEmbeddedPiRunActive`, and `waitForEmbeddedPiRunEnd` for existing callers. ## Tool Strategy Use two phases. ### Phase 1: Native Codex Tools Use Codex app-server's native tool stack first: - shell - file reads/writes - apply patch - plan/reasoning - MCP, if already configured in Codex This gets the runner alive with low glue code. Missing in this phase: - OpenClaw channel message tools - `sessions_send` / subagent tools - cron tools - OpenClaw plugin tools - channel-specific action/reaction tools - OpenClaw tool policy accounting This phase must stay feature-flagged and limited to manual testing. ### Phase 2: Dynamic OpenClaw Tools Bridge OpenClaw tools through app-server dynamic tools. Use `createOpenClawCodingTools` as the source of truth. That keeps existing tool policy, group/owner restrictions, plugin tools, abort signals, and message-tool side effects in one place. Bridge flow: 1. Build OpenClaw tools for the current run context. 2. Convert each safe schema to app-server `DynamicToolSpec`. 3. Register dynamic tools on `thread/start`. 4. Handle app-server `item/tool/call` requests. 5. Invoke the OpenClaw tool implementation. 6. Return app-server content items: - `{ type: "inputText", text }` - `{ type: "inputImage", imageUrl }` 7. Record OpenClaw side effects for final `EmbeddedPiRunResult`. Important caveat: app-server dynamic tool responses are text/image content items. Some OpenClaw tools have richer side effects. The bridge must keep those side effects in OpenClaw state, then return a concise text summary to Codex. Start with a narrow dynamic-tool allowlist: - channel message send tools - `sessions_send` - subagent spawn/control tools - cron add/list/remove tools Expand after the event projector and side-effect accounting are stable. ## Guardian and Approvals Codex app-server has two relevant approval modes: - `approvalsReviewer: "user"` - `approvalsReviewer: "guardian_subagent"` Use guardian only on the Codex-native approval path at first. App-server marks guardian review notifications as unstable, so OpenClaw should treat them as diagnostic/status data, not a stable persisted contract. Approval handling rules: - if `approvalPolicy` is never/no-approval, no client approval UI needed - if app-server asks OpenClaw for command/file/network approval and guardian is disabled, register an OpenClaw plugin approval request and translate the result back into the app-server approval response family - if guardian is enabled, let app-server make the review decision and project guardian status to `onAgentEvent` - never auto-accept app-server approval requests just because OpenClaw allowed a similar PI tool before - if the OpenClaw approval route is missing or errors, fail closed OpenClaw currently has no equivalent core agent guardian implementation. The repo's `guardian` references are unrelated platform/UI code or prose examples. ## Feature Flags Runtime selector: - `OPENCLAW_AGENT_RUNTIME=pi` - `OPENCLAW_AGENT_RUNTIME=codex-app-server` - `OPENCLAW_AGENT_RUNTIME=auto` Default: `auto`. `auto` should only choose app-server when all of these are true: - selected provider is `openai-codex` - app-server initializes successfully - dynamic tool bridge is enabled through the thread-start dynamic tool list Add a config key later only after the env flag proves useful. ## Rollout Slices ### Slice 0: Protocol Client and Tests - Add local protocol subset. - Add stdio JSON-RPC client. - Cover selector, event projection, and session binding. Done in the first implementation. ### Slice 1: Text-Only App-Server Attempt - Add backend seam under `runEmbeddedPiAgent`. - Implement `codex-app-server` backend with text prompt, no dynamic tools. - Use `thread/start`, `thread/resume`, `turn/start`, `turn/interrupt`. - Project assistant text, reasoning summary, plan, and turn completion. - Return `EmbeddedPiRunResult`. - Keep PI fallback and default. Manual smoke: one workspace, one prompt, no channel send tools. Done with dynamic tools included. ### Slice 2: Session Binding and Transcript Mirror - Add `SessionCodexAppServerBinding`. - Persist thread ids in a sidecar file first. - Mirror minimal user/assistant transcript to existing `sessionFile`. - Clear binding on reset. - Verify resume and `/new`. Done: sidecar thread persistence, minimal transcript mirror, and reset-aware binding clearing. ### Slice 3: Active Run Controls - Register app-server active run handle. - Map queueing to `turn/steer`. - Map abort/restart to `turn/interrupt`. - Preserve `waitForEmbeddedPiRunEnd`. - Add tests for queue/abort state. Done with targeted queue and abort coverage. ### Slice 4: Event Projector Parity - Add status projection for command/file/MCP/dynamic tools. - Add reasoning end handling. - Add compaction count/status. - Add usage extraction when app-server provides token usage. - Mark `totalTokensFresh=false` when usage is unavailable or not comparable. Done for command/file/MCP/dynamic item status, reasoning end, plan updates, tool summaries, token usage, and app-server compaction item state. Freshness metadata remains limited by the app-server usage payload. ### Slice 5: Message and Subagent Tools - Enable dynamic tools experimentally. - Bridge message tools and sessions/subagent tools first. - Preserve `didSendViaMessagingTool` suppression. - Preserve sent text/media/target metadata. - Add cron count support. Done through the OpenClaw dynamic tool bridge. Broader plugin-owned tools still depend on schema compatibility and per-tool validation. ### Slice 6: Compaction and Overflow - Support `compactEmbeddedPiSession` via `thread/compact/start`. - Map context overflow errors to existing failover/compaction flow where possible. - Decide which PI-specific tool-result truncation paths still apply. Manual compaction is wired through `thread/compact/start` when a Codex app-server thread binding exists. Overflow-specific recovery remains partial because app-server owns the native Codex history and exposes fewer PI-style transcript repair hooks. ### Slice 7: Default-On for Codex Provider - Use app-server automatically for the Codex provider. - Keep PI for non-Codex providers. - Keep env/config kill switch. - Add docs and release note only when user-facing behavior changes. Done: default runtime resolution is `auto`, which routes `openai-codex` attempts through app-server and falls back to PI on startup/runtime failure. Set `OPENCLAW_AGENT_RUNTIME=pi` to force the legacy PI backend. ### Slice 8: Retire PI from Codex Path - Remove PI session manager usage from the Codex provider path. - Keep PI dependency only for providers/features still using it. - Later decide whether to replace non-Codex provider execution or keep PI as a compatibility backend. ## Feature Parity Matrix | Behavior | MVP | Dynamic bridge | Notes | | ------------------------- | ---------- | -------------- | ----------------------------------------- | | text replies | yes | yes | app-server native | | reasoning stream | yes | yes | map summary deltas first | | plan stream | yes | yes | app-server plan item | | command/file tools | yes | yes | Codex native first | | channel message tools | no | yes | OpenClaw dynamic bridge | | subagents/session tools | no | yes | OpenClaw dynamic bridge | | cron tools | no | yes | OpenClaw dynamic bridge | | plugin tools | no | later | depends on schema compatibility | | queue steer | yes | yes | `turn/steer` | | abort | yes | yes | `turn/interrupt` | | compaction | yes | yes | `thread/compact/start` | | model fallback | partial | partial | keep PI orchestration; Codex auth differs | | auth profile rotation | no | no | app-server owns Codex auth | | prompt cache metrics | partial | partial | use app-server usage if exposed | | session transcript repair | not needed | not needed | do not port PI repair blindly | | guardian | partial | partial | unstable telemetry | ## Risks ### Tool Semantics Drift Codex native tools and OpenClaw PI tools are not the same contract. Message, subagent, cron, channel, and plugin tools must be bridged deliberately rather than assumed. Mitigation: keep `OPENCLAW_AGENT_RUNTIME=pi` as the kill switch and keep non-Codex providers on PI while dynamic tool side effects continue to be measured against `EmbeddedPiRunResult`. ### Auth and Provider Drift OpenClaw auth profiles and provider fallback were built around PI/provider adapters. App-server uses Codex config/auth. Mitigation: first support only the Codex provider. Keep PI for non-Codex providers and for auth-profile rotation. ### Session History Split Brain App-server has threads; OpenClaw has session files and session store metadata. Mitigation: persist explicit Codex thread bindings and mirror minimal transcript data until UI/history code can consume app-server thread history directly. ### Experimental APIs Dynamic tools and guardian telemetry require app-server experimental API opt-in. Mitigation: isolate protocol handling in one module, parse unstable guardian events permissively, and make dynamic tools separately flaggable. ### Compaction Mismatch PI compaction and app-server compaction are different systems. Mitigation: do not reuse PI transcript mutation logic on app-server threads. Map only user-facing compaction state and token metadata first. ## Test Plan Unit tests: - JSON-RPC client initializes and rejects pre-initialize calls. - fake app-server emits text/reasoning/plan/tool/turn completion events. - event projector builds `EmbeddedPiRunResult`. - session binding persists and clears thread ids. - active run handle queues via `turn/steer`. - active run handle aborts via `turn/interrupt`. - dynamic tool bridge maps a message tool side effect. - approval requests route through plugin approvals and fail closed when no approval route is available. - guardian telemetry is accepted but non-fatal. - transcript mirroring dedupes repeated turn mirrors. - protocol drift check matches generated Codex app-server request/item shapes. Targeted test commands: ```sh pnpm codex-app-server:protocol:check pnpm test src/agents/pi-embedded-runner/runs.test.ts pnpm test src/agents/command/attempt-execution.test.ts pnpm test src/agents/codex-app-server-runner ``` Build gate: ```sh pnpm build ``` Build is required because this touches lazy/module boundaries and generated protocol imports. Manual smoke: ```sh OPENCLAW_AGENT_RUNTIME=codex-app-server pnpm openclaw ... ``` Smoke cases: - text-only direct session - resumed session - queued follow-up while streaming - abort while command is running - manual compaction - channel send through dynamic message tool - subagent spawn and follow-up ## Implementation Order 1. Add protocol client and fake server tests. 2. Add backend selector, defaulting to PI. 3. Implement text-only app-server backend. 4. Add event projector for assistant/reasoning/plan/turn completion. 5. Add session binding and transcript mirror. 6. Add active run steer/abort. 7. Add dynamic message/subagent/cron tools. 8. Add compaction mapping. 9. Enable `auto` for Codex provider behind kill switch. 10. Measure parity, then remove PI from Codex-only execution path. ## Decision Proceed with a backend seam under `runEmbeddedPiAgent`, not a direct replacement of `src/agents/pi-embedded.ts`. The first production-safe milestone is: - app-server backend exists - default `auto` routes Codex provider through app-server with PI fallback - `OPENCLAW_AGENT_RUNTIME=pi` remains the kill switch - text/reasoning/plan streaming works - resume, abort, and steer work - transcript mirror is written idempotently - PI fallback remains available The Codex provider is now default-on through `auto`; PI remains available for fallback, non-Codex providers, and explicit rollback.