# Runtime and safety

[Back to README](../README.md) · [Usage](usage.md) · [Development](development.md) · [Roadmap](roadmap.md)

pi-delegate uses a fresh Pi RPC child for every task. This document explains what is inherited, who owns the execution, and which guarantees the extension does not provide.

## Process model

Each admitted invocation launches a separate Pi CLI process in RPC mode and creates a new in-memory session. Child sessions are not reused, and the parent's full conversation is not transferred. Only the explicit task and optional supplementary context are supplied as task input.

The child uses the CLI from the installed Pi host package:

- A Node.js host uses the current Node executable.
- A Bun host requires `node` on `PATH`.

RPC control commands have a 40-second response timeout. Inherited-tool initialization waits up to 30 seconds for missing tools to become available. The model task has no default fixed timeout; opt-in [execution budgets](usage.md#optional-execution-budget) can cancel on observed time/turn thresholds, while [soft pressure](usage.md#soft-pressure) is advisory rather than a hard execution budget.

The initialization request's deadline includes child cold startup, extension/session initialization, and the inherited-tool handshake; it is not an additional 40 seconds after the child is ready. Child registration loads only the child initialization branch, not parent task/panel modules. Each tool-wait poll enumerates the registry once and uses a name set; full schema/exposure/activation validation still runs before task submission.

## Concurrency scheduling

Synchronous and background tasks share a runtime-local FIFO pool, excluding the parent. The default maximum is **4**. Set `PI_DELEGATE_CONCURRENCY` to a positive safe integer before Pi starts; it is read when the extension registers and cannot be adjusted at runtime. Invalid values fail extension registration rather than silently removing the limit.

POSIX shell example:

```sh
PI_DELEGATE_CONCURRENCY=1 pi --extension ./packages/pi-delegate/src/index.ts
```

PowerShell example:

```powershell
$env:PI_DELEGATE_CONCURRENCY = "1"
pi --extension ./packages/pi-delegate/src/index.ts
```

Excess tasks remain `queued` with captured invocation/model/thinking inputs and no child process. Admitted tasks are `initializing` until observed original-task execution makes them `running`. Initialization and owned cleanup both occupy capacity; settlement alone does not release it. Queued cancellation settles without launching a child. Background acceptance remains immediate and independent of ordinary parent-turn cancellation; synchronous calls wait through admission, execution and cleanup.

Queue residence and initialization do not advance execution clocks, turns or soft pressure, and are not steer-ready. Scheduling adds no timeout, priority, preemption or automatic cost-budget cancellation.

## Execution budgets and cancellation observations

An invocation-local optional `budget: { maxSeconds?, maxTurns? }` is captured before admission and disclosed in the original child prompt. Neither omitted budgets nor soft pressure add hard limits. Budget observation starts on the same original-task `agent_start` boundary as progress/pressure; queueing and initialization are excluded, while provider/tool waits are included. Only completed `turn_end` observations count assistant-plus-associated-tools turns. Either enabled threshold at equality or above reserves one budget cancellation, closes pressure/steering and immediately submits RPC abort without extra reporting generation.

The shared runner owns abort, bounded best-effort final-entry readback and process cleanup, not the budget observer or task record. Cleanup readback uses a short control timeout (250 ms), rather than the ordinary 40-second command timeout, and is data collection, not a model wrap-up grace period. Timers are disposed on settlement, cancellation and teardown; late observations cannot reopen terminal work. Admission remains occupied until runner cleanup returns. Existing synchronous/manual and background/scope-invalidation ownership remains unchanged.

The terminal result retains effective limits and an explicit `budgetStop` cause/dimension/configured limit/observed seconds or count. Observed text deltas and message-end text are retained through shutdown and labelled partial, using existing preview/full-output artifacts. Reported streaming/tool usage is retained when final readback fails; readable entries replace corresponding observations rather than adding them twice, with additional available observations retained. Budget-cancelled usage may still be partial. Retained results/queries are observations, not new usage contributions; background usage stays separate from parent totals.

These are observed stop triggers, not instantaneous process-stop deadlines or strict provider-request ceilings. Provider cancellation, event delivery and bounded process cleanup can lag the threshold; no file/external effect is undone, and neither the selected workspace nor separately retained output artifacts is deleted. Upfront disclosure cannot guarantee completion/compliance, a final report, or task acceptance.

## Tool and configuration inheritance

The child follows normal initialization rather than executing tools through parent-process proxies.

Inheritance includes:

- The parent's environment and, by default, working directory; optional `cwd` selects another existing directory.
- Current model and thinking level. Same-directory execution retains parent project trust; a different directory follows native target trust, replaying explicit CLI trust overrides rather than transferring the parent's directory-specific decision.
- Extensions loaded through conventional configuration discovery, explicit extension paths, and observable tool/command source paths.
- Available extension CLI flags that can be replayed.
- The full inherited tool set, followed by restoration of the parent's active subset.
- Discoverable tools, including those reachable through `tool_search` and `codemode`.

MCP reconnects using the same configuration. Parent connections, caches, arbitrary in-memory configuration, and closure state are not shared.

After initialization, the extension checks inherited tool availability, schema, exposure, namespace, and activation state. Tools that cannot be reconstructed fail explicitly: they are not silently omitted, and execution does not fall back to parent proxies.

Model/thinking inheritance is a plain invocation snapshot with independent optional overrides. The runner uses exact RPC `set_model` (not fuzzy CLI model patterns), then `set_thinking_level` and `get_state` after tool initialization and before original-task submission. Pi adjusts thinking capabilities and checks child-local auth configuration. Results distinguish requested and confirmed effective startup values; shared scope-local records, query tools and Agents/panel views consume confirmed data. Model selection/readback failures never execute the task on a fallback model. No resolved credentials are copied, and no task overrides are persisted to user/project defaults.

Keep configuration files stable during child startup. Unobservable host-private flags and arbitrary runtime state are outside the inheritance contract.

## Parent-only delegation

The child loads this extension, but does not register `delegate`, `delegate_list`, `delegate_status`, `delegate_cancel`, or `delegate_steer`. These capabilities are unavailable through direct model declarations, tool search, and nested `codemode` calls.

The parent additionally registers `/delegates` for the floating task panel; it is not reconstructed as a child command. Numeric task labels are presentation correlations, not alternative control IDs.

This is a tool-registration boundary, not an operating-system sandbox. A child with shell access still has process-level capabilities and can launch other processes.

## Task ownership and cleanup

### Synchronous tasks

The initiating delegation call owns the child. Cancelling that call or shutting down the parent session terminates the child and cleans up owned initialization resources.

Synchronous tasks have scope-local read-only query IDs and retained results. The record does not own a second controller, extend the call's lifetime or send completion notifications. Task-addressed cancellation/steering against these IDs rejects without aborting the initiating call.

### Shared observations and records

The non-visual task record is accepted after validation and feeds list/query tools, the compact Agents view and the floating overlay from the same observation reducer. It correlates exact taskId with a session-local increasing label, stores selected versus confirmed startup information, and retains available results until existing scope invalidation. It does not launch children, control RPC or replace the usage ledger.

The five-second terminal-row timer removes only presentation rows. Paging limits each response, not the retained task set; there is no TTL/count eviction or cross-session persistence. Navigation clears records and closes the old panel while generation guards suppress late observations/results. Existing execution cleanup, cumulative/late usage reconciliation and external workspace/full-output-file lifetimes remain independent. Closing or viewing an overlay neither cancels work nor triggers parent inference.

### Background tasks

Accepted background work belongs to the originating session/runtime, not the initiating parent turn. Ordinary parent-turn completion or cancellation does not cancel it.

Explicit `delegate_cancel` cancels running work and waits for cleanup. Exit, session replacement/forking, extension reload, and branch-changing `/tree` navigation cancel background tasks and invalidate their IDs.

Results are not migrated into a replacement runtime or delivered to a destination branch. To avoid navigation races, tasks are cancelled before navigation; even if navigation is later cancelled, those tasks are not resurrected.

Background execution is asynchronous work inside the current Pi session, not a persistent service that survives Pi exit.

### Completed and failed tasks

Normal completion and failure also release the child process and temporary initialization snapshot.

Startup failures automatically include a **model-visible diagnostic tail** of child stderr, limited to the last **16 KiB**, alongside the original error. `details.startupDiagnostics` reports the startup step (`prepare`, `spawn`, `initialize`, `configure`, or `submit`), stderr, a truncation flag, and unexpected exit code/signal when observed. Intentional cleanup exit codes are not presented as the cause of failure. Synchronous results and `delegate_status` use the same diagnostics.

**Privacy:** child stderr may contain sensitive extension/configuration output. Failure diagnostics are enabled by default and are not guaranteed to be redacted. Do not log credentials in child startup code. No stderr log artifact is persisted by pi-delegate; the parent host may retain the returned failure text as part of its normal conversation history.

Startup failures also report the last operation, per-operation/total elapsed time observed before cleanup, RPC timeout command/id/limit and pipe-write state when applicable, and the last protocol record observed. A completed pipe write is not proof of host acceptance; timed-out commands are not automatically retried. Up to eight recent startup `extension_error` records are retained, with bounded path/event/error fields; these errors can contain sensitive configuration details just like stderr and are not guaranteed to be redacted.

A bounded, best-effort initialization checkpoint in the owned temporary directory distinguishes extension registration, waiting for tools, validation, readiness and failure. It includes available/expected tool counts and bounded missing-tool names when observed. The checkpoint is removed with initialization resources, is not a persistent log, and is read only for non-cancelled startup failures. A missing checkpoint or missing tool name does not prove which MCP server or extension caused the stall; unavailable causes are explicitly labelled unknown. Stderr is never parsed as protocol data.

Open `/delegates`, select the failed task and press Enter: the **Overview** includes the failure and startup diagnostics, with the same details also available under **Result** and through task queries. Long diagnostics can be scrolled; the panel strips terminal control sequences from untrusted failure text.

Collection ends and its buffer is discarded when original-task execution is observed or the task prompt is accepted as started. Successful tasks, subsequent runtime failures, and cancellations do not return startup stderr. Stderr alone does not make startup fail and is never parsed as RPC data. Diagnostics are collected after the existing bounded cleanup; pipes forcibly closed during cleanup may leave the tail incomplete. This is startup-error visibility, not a full execution record or a guarantee that reported task results are correct.

Usage is collected from final child entries when available. If cancellation or a transport failure prevents collection, results retain usage already observed in message events and the latest cumulative streaming update. Unreported provider usage cannot be recovered; these failure-path totals can be partial. Streaming updates, final messages, and final entries are not added together twice.

The authoritative readback includes assistant/tool-result messages, standalone usage entries and usage-bearing compaction/branch-summary entries. Nested tool usage already included in a tool result is not counted again; reasoning and cache-write subsets are not extra output/cache charges.

### Delegated usage visibility

While task rows are visible, the Agents area shows latest task usage and an explicitly labelled cumulative **Delegated total**, separate from Pi's parent-session totals. When the last task row expires after five seconds or rows are cleared, the whole widget disappears; totals/capacity alone do not keep or bring it back. Accounting is retained independently and is shown again with the next task in the same runtime. Contributions are replaced by new snapshots, not charged again on queries or final readback. Completed, failed and cancelled consumption survives row expiry. Branch navigation removes task handles/rows but retains consumed usage, including available late cleanup reconciliation without resurrecting an empty widget. Session replacement/reload creates a fresh total; no cross-runtime usage history is persisted.

The compact format is `↑8.2k ↓1.1k R20k W0 · $0.04`: ↑ is model input tokens, ↓ is output tokens, R is cache-read tokens and W is cache-write tokens. These are not network bytes, and cache tokens are separate from input. Display rounding does not change stored accounting precision.

The dollar value is Pi's provider-reported/calculated **estimate**, not the supplier invoice. Subscription and custom-provider pricing may differ or report zero. Updates depend on provider/host reporting; failure/cancellation totals may be partial and unreported consumption cannot be recovered. Do not add delegated totals to parent totals: synchronous usage is already represented through host tool-result accounting, whereas background usage is separately reported. The ledger and scheduling work without the TUI widget.

Full-output files are separate result artifacts and intentionally survive this cleanup so callers can retrieve truncated text. See [large output](usage.md#large-output).

## Runtime steering control

The background task owner resolves scope-local IDs and receives only a narrow `steer(message)` capability and read-only readiness/closure observation. The execution runner continues to own the process, transport, readiness observations and cleanup. Manual steering and pressure await RPC outcomes through the same task-local serial submission boundary; stdin write serialization alone would not order asynchronous Pi input handlers.

Readiness begins only after initialization and original-task start. Cancellation/invalidation disables controls synchronously, and `agent_settled` closes control before result collection/cleanup completes. Low-level `agent_end` may precede continuation and does not close it. Pending local submissions recheck closure before sending; an already-submitted host handler can finish after settlement without reopening the control or triggering a replacement run.

Manual text uses non-slash-prefixed raw RPC `steer`, preserving trusted handlers and host consumption mode. The prefix is not a sandbox against trusted handlers. The handling receipt is distinct from queue residence, transcript injection and eventual provider input. A control timeout has an uncertain submission outcome and is never automatically retried; it is not a new delivery/completion SLA. Manual-operation failures are separate from task outcomes; underlying transport/execution failures remain governed by normal delegation lifecycle. See [steering usage and receipt fields](usage.md#steering-an-active-background-task).

## Background result retrieval

Background outcomes remain queryable until the originating session/branch scope is invalidated. The extension does not inject automatic completion messages, queue follow-up work, or trigger a parent turn when a task ends, regardless of whether the parent is busy or idle.

Use `delegate_list` for discovery and `delegate_status` for retained results, including failed, cancelled and incomplete outcomes. Synchronous delegation still returns its result directly. Querying does not charge delegated usage again and does not establish model consumption or task acceptance.

## Child UI requests

For synchronous tasks:

- Supported RPC `select`, `confirm`, and `input` requests are forwarded to the parent's UI.
- Requests are cancelled when no UI is available.
- Pending forwarded dialogs are cancelled when their delegation returns, including child failure and normal completion; this does not change the task's outcome.
- Multiline `editor` requests are cancelled because the parent extension API cannot abort an open editor safely.
- Child notifications are forwarded with a subagent label.

For background tasks, `select`, `confirm`, `input`, and `editor` requests receive cancellation or refusal instead of opening blocking parent dialogs.

Refusing an individual request does not automatically cancel the whole task. Whether the child can continue determines its eventual outcome.

## Workspace and security boundaries

Parent and child agents share a startup workspace by default. Optional `cwd` selects a separate existing directory without enforcing an access boundary. There is no automatic worktree isolation, conflict warning, file locking, permission policy or operating-system sandbox provided by this extension.

The bundled `delegate-worktree` skill guides the parent in preparing worktrees from explicit commits without automatically stashing, committing or copying dirty main-workspace state. The parent reviews actual child artifacts, integrates and validates changes before cleanup. Failed or unmerged workspaces are retained. Runtime cleanup removes only owned process/initialization resources, never the selected workspace; background task IDs still expire with their existing ownership scope.

Coordinate modification scopes when multiple agents can write files. A prompt such as "analyze only" is an instruction, not an enforced filesystem permission.

Tools and environment inherited by a child may carry substantial authority. Excluding delegation tools prevents recursive tool access through this extension, not arbitrary process actions.

## Compatibility and verification

The implementation is validated with Pi 1.1.0. That evidence does not establish compatibility with every Pi version.

Tests exercise real Pi RPC processes, inheritance and MCP reconstruction, lifecycle boundaries, and deterministic local model providers. Task-center component and native custom-overlay/focus integration are covered, but real regular/fullscreen terminal rendering, keyboard/picker E2E and IME operation are not claimed. Tests requiring external model credentials are not part of the current verification.

See the [development guide](development.md#testing) for verification commands and test boundaries.
