08. Error Codes
Source of truth:
packages/shared/src/errors.ts(ErrorCodes). Codes in §3.7 are reserved (documented ahead of emission); everything else is live.
1. Goal
Provide one stable error vocabulary across:
- Renderer UI
- Electron IPC
- Rust host RPC
- Node pi sidecar bridge
2. Error object
type AppError = {
code: string // stable machine code, e.g. TOOL_DENIED
message: string // English UI/default message
details?: unknown
retriable?: boolean
source?: "renderer" | "electron" | "host" | "agent" | "plugin"
causeCode?: string // nested/transport code if mapped
traceId?: string
}Rules:
codeis immutable once publishedmessageis English source text (i18n key may map separately)- UI should prefer i18n key derived from
codewhen available
The desktop test suite (apps/desktop/test/error-code-registry.test.mjs) verifies that every ErrorCodes entry appears in this document and that every errorCode host-core emits from its RPC dispatcher and native tools is registered; reserved codes in §3.7 remain intentionally absent from ErrorCodes until an implementation emits them.
3. Code registry
3.1 App / protocol
| code | retriable | meaning |
|---|---|---|
PROTOCOL_MISMATCH | no | handshake/protocol version mismatch |
HOST_UNAVAILABLE | yes | Rust host not running/reachable |
HOST_OVERLOADED | yes | bounded host RPC/tool capacity is full; retry after backpressure |
AGENT_UNAVAILABLE | yes | pi sidecar not running/reachable |
APP_DEGRADED | yes | app running with limited capabilities |
INTERNAL | maybe | unexpected internal failure |
INVALID_ARGUMENT | no | request schema/args invalid, including a native-tool path of the wrong file/directory kind |
INVALID_PARAMS | no | host-core RPC parameter validation failed (numeric 1002); the sidecar and renderer surface it unchanged |
UNAUTHORIZED | no | capability/auth boundary rejected call |
NOT_FOUND | no | entity not found |
SESSION_NOT_FOUND | no | a session-scoped RPC (including tools.execute) named a session the host does not have; an unknown id never inherits the global workspace |
CONFLICT | maybe | state conflict / busy resource |
UNSUPPORTED | no | the operation has no implementation on this surface, e.g. a trusted-extension prompt while no desktop window can show it (spec 16 §9) |
FORBIDDEN | no | RACP: the principal's roles do not admit the method (spec 19 §13) |
METHOD_NOT_FOUND | no | RACP: unknown method on this Host, e.g. host/list without a Gateway |
IDEMPOTENCY_CONFLICT | no | a queue or turn idempotency key was reused with different input (D386) |
CURSOR_EXPIRED | no | RACP: the replay cursor is older than the retained window; resubscribe from a snapshot |
CLIENT_TOO_SLOW | yes | RACP: the client fell behind the event stream and was disconnected |
APPROVAL_EXPIRED | no | RACP: the approval deadline passed before an answer arrived |
APPROVAL_STALE | no | RACP: the approval was already settled or belongs to an older turn |
PAYLOAD_TOO_LARGE | no | RACP: a frame exceeded the negotiated size bound |
TIMEOUT | yes | generic timeout |
NETWORK_POLICY_BLOCKED | no | the main-process public-network guard refused a fetch because it judged the target: the URL failed the syntactic public-HTTPS check, or the local DNS lookup returned an address the policy classifies as non-public — including a fake-IP placeholder a local proxy invented (ADR 0243). A desktop-only code; a refusal is a verdict, so retrying cannot succeed until the address changes. A resolver that returned no answer at all is NETWORK_RESOLVE_FAILED instead (issue #419). Since ADR 0304 an endpoint the user typed themselves may resolve to their own loopback or LAN, so this code now reports a first hop only for the classes that name no service at all (cloud metadata, unspecified, multicast, reserved) or for a third-party hop — a redirect target, a catalog body, a registry record. |
NETWORK_RESOLVE_FAILED | yes | the main-process public-network guard could not classify the target host: the local DNS lookup returned no answer, or threw before returning one. The request is refused exactly as a policy refusal is, but no address was judged, so no page or log may report it as an address-check decision. Distinct from NETWORK_ERROR, which is a failure of the request itself. Retriable: a resolver or proxy that starts answering the same host makes the same request succeed (ADR 0243, issue #419). |
HOST_SHUTTING_DOWN | yes | the host received EOF and is draining; the call was refused rather than started |
RATE_LIMITED | yes | a per-caller host budget (plugin session import, batch operations) was exceeded inside its window |
LIMIT_EXCEEDED | no | a payload exceeded a fixed host bound (item count, byte size, or a 64 MiB NDJSON request line) and was refused |
CONFIG_SYNC_INVALID | no | invalid sync configuration, password, path, request, or approval input |
CONFIG_SYNC_LOCKED | no | the local encrypted sync vault is not unlocked |
CONFIG_SYNC_UNSUPPORTED | no | the vault format or S3 conditional-write capability is unsupported |
CONFIG_SYNC_REMOTE | maybe | S3 object, authentication, quota, or availability failure |
CONFIG_SYNC_CONFLICT | maybe | remote head, vault identity, or approval digest conflict |
CONFIG_SYNC_CRYPTO | no | authenticated encryption, object identity, or ciphertext validation failed |
CONFIG_SYNC_MAPPING_REQUIRED | no | imported project-scoped configuration needs an explicit local folder/group mapping |
CONFIG_SYNC_LIMIT_EXCEEDED | no | encrypted sync state exceeded an entity, object, resource, archive, or decompression bound |
HOST_UNAVAILABLE is reserved for a missing or broken host process/transport, not ordinary admission pressure. RPC capacity returns HOST_OVERLOADED, and an admitted shell that cannot start because the OS is temporarily out of process resources returns PROCESS_RESOURCE_EXHAUSTED. Host-core's control stdio is isolated from Tokio's dynamic blocking pool so the latter condition does not turn temporary thread pressure into a host process exit.
3.2 Agent / session
| code | retriable | meaning |
|---|---|---|
AGENT_BUSY | no | session already has active turn; leftover subagents after a terminal parent error do not keep the session busy (D352) |
AGENT_NOT_FOUND | no | session missing |
TURN_NOT_FOUND | no | turn id invalid |
TURN_ABORTED | no | turn aborted by user/system |
AGENT_SIDECAR_CRASHED | no | the Node agent sidecar process died mid-turn; the owning turn settles as aborted with this code instead of an unrelated plan-approval code (issue #1077) |
AGENT_SIDECAR_OOM | no | the sidecar died after its JavaScript heap hit the configured cap, diagnosed from the V8 fatal-error banner in its stderr tail; the same turn fails the same way until the input shrinks (issue #1077) |
MODEL_NOT_CONFIGURED | no | no usable model selected, or provider rejects the selected model as unknown |
PROVIDER_ERROR | yes | upstream provider failure; retryable 5xx gateway errors get up to ten same-turn retries, while malformed 400/422 requests, adapter-refused options, and explicit billing, credit, spend, usage, or disk exhaustion (including insufficient_quota) are terminal |
PROVIDER_UNAUTHORIZED | no | bad/missing provider credentials |
PROVIDER_RATE_LIMITED | yes | temporary provider throughput limit; runtime retries up to ten times across setup/stream. Explicit billing/account exhaustion remains terminal PROVIDER_ERROR, including when returned with HTTP 429 |
CONTEXT_TOO_LARGE | no | prompt/context still exceeds the safe model budget after recovery, the second provider overflow occurred, or automatic recovery is disabled |
CONTEXT_COMPACTION_FAILED | no | automatic retained-tail recovery could not prepare, persist, or fit a checkpoint, or manual checkpoint summary generation / durable append failed; the guarded next provider request does not start |
STREAM_FAILED | yes | provider stream was terminated, closed prematurely, or otherwise ended before a complete response; up to ten same-turn retries may precede the terminal event |
EMPTY_MODEL_RESPONSE | yes | the model ended its turn with no tool call and no visible text twice: once as streamed, once after the automatic re-run; the first reply to a Host-ledger completion notice is exempt (spec 02-agent-runtime §5e, D446) |
PROMPT_ENHANCEMENT_EMPTY | no | the one-shot enhancement model returned no text |
SPEECH_NOT_CONFIGURED | no | host speech ASR or TTS is not bound in settings |
SPEECH_PROTOCOL_UNSUPPORTED | no | the speech protocol is unknown or does not support this role |
SPEECH_INPUT_TOO_LARGE | no | speech input exceeds 25 MB |
SUBAGENT_IDLE_TIMEOUT | no | withdrawn (D328): idle watchdogs are not armed; the code remains for stored results |
SUBAGENT_DURATION_TIMEOUT | no | withdrawn (D328): duration watchdogs are not armed; the code remains for stored results |
SUBAGENT_CONTEXT_OVERFLOW | no | a delegate's own model context exceeded its safe budget and neither automatic turn-boundary compaction nor the degraded retry that keeps only the task brief and the most recent messages brought it back below the limit; the failure names the actionable recovery instead of the provider's overflow text |
SUBAGENT_OUTPUT_TRUNCATED | no | a delegate's report ended at the model output-token limit; the partial report is preserved for diagnosis, but the run is failed rather than presented as a completed delegation |
SUBAGENT_PARENT_FAILED | no | parent failure interrupted a running delegate; its chain remains manually resumable with Task, subject to the existing history and read-budget gates |
3.3 Workspace / tools / permissions
| code | retriable | meaning |
|---|---|---|
WORKSPACE_REQUIRED | no | no workspace bound |
PATH_OUTSIDE_WORKSPACE | no | path escapes sandbox before an explicit outside-path permission decision, or a prompt attachment is outside its session scratch/project/attachment roots |
VISUALIZATION_HTML_ONLY | no | a Write/Edit into the session visualization directory is not a direct-child .html fragment |
WORKSPACE_PATH_DENIED | no | an explicit Read/Write/Edit path hit the always-on security denylist (private keys, .env files, credential bundles, .git/objects); an outside-path grant does not lift it (spec 15 §3) |
READ_PATH_IS_DIRECTORY | no | Read was given a directory; the result carries a Glob suggestion |
TOOL_BINARY_CONTENT | no | Read refused to dump a binary file into the model context |
TOOL_NOT_FOUND | no | unknown tool |
TOOL_DENIED | no | permission denied / mode forbidden |
TOOL_TIMEOUT | yes | tool execution timeout |
TOOL_FAILED | maybe | tool executed but failed |
FILE_NOT_FOUND | no | Read/Write/Edit target path does not exist (distinct from TOOL_DENIED) |
TOOL_ABORTED | no | the tool was cancelled by a user stop or a turn abort before it finished |
MUTATION_RETRY_BUDGET_EXHAUSTED | yes | the repeat guard ended the turn after same-path Edit or shell patch failures; carries details.kind (edit or patch-command), the last tool error code, and a class-specific details.recovery hint |
PROCESS_RESOURCE_EXHAUSTED | yes | shell process could not start because the OS temporarily exhausted process resources |
SHELL_NOT_FOUND | no | no effective platform shell is available after catalog fallback; message carries guidance |
COMMAND_SHELL_CHANGED | no | pinned shell ID or dialect changed before execution |
COMMAND_SHELL_INVALID | no | settings supplied an unknown, unavailable, or wrong-platform shell ID |
PERMISSION_TIMEOUT | no | legacy compatibility code for an older permission prompt timeout; current local prompts remain pending instead |
PERMISSION_REQUIRED | no | waiting for user decision |
WRITE_DISABLED_IN_PLAN | no | contract-mode hard-deny for Write |
EDIT_DISABLED_IN_PLAN | no | contract-mode hard-deny for Edit |
PLUGIN_DISABLED_IN_PLAN | no | contract-mode hard-deny for every plugin tool |
TOOL_DISABLED_IN_PLAN | no | contract-mode hard-deny for an unknown/unlisted tool |
PLAN_NOT_ACTIVE | no | a submit tool ran while no contract was being negotiated |
PLAN_KIND_MISMATCH | no | SubmitPlan in Goal mode, or SubmitGoal in Plan mode |
PLAN_APPROVAL_REQUIRED | no | SubmitPlan/SubmitGoal is waiting for a separate approval |
PLAN_APPROVAL_TIMEOUT | no | absolute 30-minute plan approval deadline expired |
PLAN_APPROVAL_STALE | no | response does not match the live proposal/session/turn/tool-call/version |
PLAN_APPROVAL_INTERRUPTED | no | pending approval closed during abort, crash, or persistence failure |
PLAN_ARTIFACT_WRITE_FAILED | no | host could not write exact bytes to a new .pi/<kind>/*.md artifact |
PLAN_EXECUTION_INTERRUPTED | no | approved queued/running Plan or Goal execution stopped without replay |
PLAN_REQUIRES_INTERACTIVE_SESSION | no | unattended/scheduled Plan or Goal run cannot request approval |
PLAN_NOT_FOUND | no | no approval row matches the proposal id |
PLAN_SESSION_NOT_FOUND | no | the Plan/Goal RPC named a session the host does not have |
PLAN_WORKSPACE_REQUIRED | no | the session has no persisted project; temporary sessions cannot enter Plan or Goal |
PLAN_ALREADY_ACTIVE | no | the session already has a contract being negotiated |
PLAN_ALREADY_PENDING | no | a submit arrived while an approval for the same turn is still pending |
PLAN_ALREADY_RESOLVED | no | a second approve/reject reached an already-resolved approval |
PLAN_APPROVAL_CONFLICT | no | the approval row changed underneath a version-guarded update |
PLAN_INVALID_ACTION | no | the approval response is neither approve nor reject |
PLAN_INVALID_ARGUMENT | no | submit/resolve arguments failed validation |
PLAN_PERMISSION_MODE_REQUIRED | no | approve did not select ask, accept-edits, or auto |
PLAN_PERMISSION_MODE_INVALID | no | the selected permission mode is not one of the three |
PLAN_MARKDOWN_TOO_LARGE | no | the submitted Markdown exceeds the artifact size bound |
PLAN_REJECTED | no | the user rejected the proposal; the turn ends without execution |
PLAN_SUBMIT_FAILED | maybe | the host could not record the proposal |
PLAN_CONFIGURATION_BLOCKED | no | session.configure was refused while a proposal or execution is live |
PLAN_ARTIFACT_INVALID | no | the checkpoint artifact failed validation before execution |
PLAN_ARTIFACT_NOT_READY | no | execution was claimed before the artifact was durably written |
PLAN_ARTIFACT_PATH_UNSAFE | no | the artifact path escaped <workspaceRoot>/.pi/<kind>/ |
PLAN_ARTIFACT_COLLISION_LIMIT | no | the host ran out of unique artifact names |
PLAN_ARTIFACT_HASH_MISMATCH | no | artifact bytes no longer match the recorded hash at execution time |
PLAN_EXECUTION_ACTIVE | no | an approved execution is already running for the session |
PLAN_EXECUTION_NOT_FOUND | no | no queued execution matches the claim |
PLAN_EXECUTION_ALREADY_CLAIMED | no | another claimant took the queued execution first |
PLAN_EXECUTION_STALE | no | the execution epoch no longer matches the live session |
PLAN_EXECUTION_STATUS_INVALID | no | a status transition was not allowed from the current state |
PLAN_EXECUTION_CONFLICT | no | the execution row changed underneath a version-guarded update |
PLAN_EXECUTION_FAILED | maybe | the approved execution ended in an error |
PLAN_INTERNAL | maybe | a Plan/Goal host failure with no finer classification |
WRITE_DISABLED_IN_CHAT | no | historical (pre-D188 Chat profile); registered for stored transcripts, no longer emitted |
BASH_DISABLED_IN_CHAT | no | historical (pre-D188 Chat profile); registered for stored transcripts, no longer emitted |
The _IN_PLAN suffix and the PLAN_ prefix are historical: both contract modes (Plan and Goal) share these codes rather than duplicating a _IN_GOAL set (D198). The renderer picks its wording from the proposal's kind, so one code can surface as either "Plan" or "Goal" copy.
3.4 Edit contract (ADR 0087)
Emitted only by Edit. Version and provenance failures have their own codes because each names a different next action; reporting them as TOOL_FAILED loses that. See 18-line-anchored-edit-contract §11.
| code | retriable | meaning |
|---|---|---|
EDIT_TAG_REQUIRED | no | tag missing or not 4 hex digits |
EDIT_LEGACY_MATCH_FAILED | yes after a Read | legacy old_string was not found or matched multiple times; model must re-read or provide unique context |
EDIT_TAG_MISMATCH | yes after a Read | tag does not hash the live file and drift recovery declined; carries the live tag and current content at the anchors |
EDIT_TAG_UNKNOWN | yes after a Read | tag is well-formed but the session recorded no such content for the path |
EDIT_LINES_UNSEEN | yes | anchors reference lines the session never displayed; carries the revealed content |
EDIT_PARSE_FAILED | no | malformed op header, body row under a colonless header, missing body, or a -/context row; the host message identifies the required syntax when possible |
EDIT_RANGE_INVALID | no | reversed range, out-of-bounds line, overlapping ops, or duplicate anchor |
EDIT_BLOCK_UNRESOLVED | no | a N* locator did not resolve; message names the plain-range alternative |
EDIT_REGISTER_EMPTY | no | paste from an unset register |
EDIT_REGISTER_AMBIGUOUS | no | anonymous paste with more than one pending anonymous capture |
EDIT_REPAIR_AMBIGUOUS | no | boundary-repair candidates tied at minimum cost |
EDIT_NO_CHANGE | no | the apply produced text identical to the input, or MV resolved to the source path; the entire call leaves the source bytes untouched |
EDIT_AMPLIFICATION_LIMIT | no | lowering exceeded the expansion cap |
EDIT_LINES_UNSEEN is retriable without a further Read when its message reports a complete reveal: the revealed lines are merged into the session's provenance, so the same tag retried unchanged applies. A truncated reveal merges nothing and requires the re-read.
EDIT_TAG_MISMATCH, EDIT_TAG_UNKNOWN, and EDIT_LINES_UNSEEN each get one free attempt per path before the repeat guard counts them, because each already carries what the retry needs. The remaining codes count on first occurrence, and the failure that exhausts the budget surfaces as §3.3's MUTATION_RETRY_BUDGET_EXHAUSTED on the assistant row (18-line-anchored-edit-contract §9.3). Its details.recovery value distinguishes syntax correction from the fresh-read path, so a follow-up does not blindly re-read a file when the payload itself is malformed.
3.5 Secrets / settings
| code | retriable | meaning |
|---|---|---|
PROVIDER_SECRET_MISSING | no | enabled provider requires an API key |
MODEL_ALIAS_TOO_LONG | no | configured model alias exceeds 60 Unicode characters |
MODEL_BINDINGS_DEGRADED | no | stored model bindings are unreadable; explicit model-array replacement is blocked to prevent data loss |
SECRET_STORE_UNAVAILABLE | maybe | OS secure storage unavailable (reserved) |
SETTINGS_INVALID | no | settings payload invalid (reserved) |
3.6 Plugins
| code | retriable | meaning |
|---|---|---|
PLUGIN_NOT_FOUND | no | plugin id missing |
PLUGIN_INVALID | no | manifest/package invalid |
PLUGIN_LOAD_FAILED | maybe | enable/load failed |
PLUGIN_DISABLED | no | plugin disabled (reserved) |
PLUGIN_PERMISSION_DENIED | no | plugin lacks the declared and granted permission the call needs |
PLUGIN_INTEGRITY | no | package checksum or signature did not match the catalog entry |
PLUGIN_NETWORK | yes | marketplace download or catalog fetch failed |
PLUGIN_HOST_TOO_OLD | no | the package's engines.piDesktop range excludes this host |
PLUGIN_MARKET_INVALID | no | the marketplace catalog is malformed or missing required release fields |
PLUGIN_MARKET_UNTRUSTED_HOST | no | the catalog or package URL is outside the trusted marketplace hosts |
PLUGIN_MARKET_YANKED | no | the requested release was withdrawn from the catalog |
PLUGIN_MARKET_NOT_PUBLISHED | no | the platform has the version and is not offering it yet |
PLUGIN_MARKET_ARCHIVED | no | the plugin was withdrawn from the platform |
PLUGIN_MARKET_NOT_FOUND | no | the platform does not have that plugin or version |
PLUGIN_MARKET_RATE_LIMITED | yes | the download endpoint asked the client to wait |
PLUGIN_MARKET_NO_SOURCE | maybe | no distribution target can serve the package |
PLUGIN_CANCELLED | no | the user cancelled an install while it was downloading |
MCP_INVALID | no | a user MCP server definition failed validation |
SKILL_INVALID | no | a user skill document failed validation |
SUBAGENT_INVALID | no | a user subagent document failed validation |
CAPABILITY_INVALID | no | an agent capability root or scope setting failed validation |
PLUGIN_COMMAND_NOT_FOUND | no | command id missing (reserved) |
PLUGIN_CRASHED | yes | plugin runtime crashed (reserved) |
PLUGIN_CONTRACT_MISMATCH | no | unsupported manifest/api version (reserved) |
3.7 Reserved detail codes (not yet emitted)
Finer-grained provider/tool distinctions documented for future mapping. Until emitted, implementations use the canonical parent code shown.
| reserved code | canonical parent today | notes |
|---|---|---|
PROVIDER_BASE_URL_INVALID | PROVIDER_ERROR | endpoint invalid (400) |
PROVIDER_PROTOCOL_MISMATCH | PROVIDER_ERROR | wrong protocol profile |
PROVIDER_MODEL_NOT_FOUND | MODEL_NOT_CONFIGURED | unknown model id (404) |
PROVIDER_TIMEOUT | TIMEOUT | network/server timeout (retriable) |
PROVIDER_UNSUPPORTED_CAPABILITY | PROVIDER_ERROR | tools/vision unsupported |
PROVIDER_DISABLED | MODEL_NOT_CONFIGURED | provider disabled |
WORKSPACE_PATH_DENIED and TOOL_BINARY_CONTENT left this table when the host started emitting them (§3.3).
Historical aliases (never use in new code): PROVIDER_AUTH_FAILED → PROVIDER_UNAUTHORIZED; PROVIDER_STREAM_INTERRUPTED → STREAM_FAILED; WORKSPACE_OUTSIDE_ROOT → PATH_OUTSIDE_WORKSPACE; SECRET_MISSING → PROVIDER_SECRET_MISSING; SHELL_UNAVAILABLE → SHELL_NOT_FOUND; SHELL_IDENTITY_STALE → COMMAND_SHELL_CHANGED; PLAN_APPROVAL_EXPIRED → PLAN_APPROVAL_TIMEOUT. Truncation is not an error: a bounded tool result carries a marker naming which end survived and where the rest is, or reports the bounded window in sibling result fields (see 16-tool-result-limits).
3.8 Remote control (RACP-WS / SSH bootstrap)
Emitted by the desktop's remote-host client and the pi-host server when a session lives on a paired remote machine driven over RACP-WS (see 19-remote-agent-control-protocol, ../05-security/02-remote-control-security, ADR 0285). The renderer never sees the local/remote split beyond a badge; these codes surface through the same error object as any other call.
| code | retriable | meaning |
|---|---|---|
HOST_DISCONNECTED | yes | the remote host connection dropped; in-flight calls are rejected and the client reconnects and resubscribes by cursor |
HOST_BOOTSTRAP_FAILED | no | provisioning the remote pi-host over SSH failed (download, checksum mismatch, or install.sh); details.reason names the stage |
HOST_VERSION_MISMATCH | no | the remote pi-host version does not match the desktop; the desktop refuses to drive an incompatible host |
REMOTE_AUTH_FAILED | no | the device or pairing token was rejected on the RACP-WS upgrade |
REMOTE_CONNECTION_FAILED | yes | the RACP-WS transport could not connect (non-loopback URL, refused socket) |
REMOTE_FORWARD_FAILED | yes | the SSH loopback port forward could not be established |
REMOTE_PATH_NOT_FOUND | no | a remote project/workspace path does not exist on the host |
REMOTE_PATH_FORBIDDEN | no | a remote path is outside the host's permitted roots |
PAIRING_FAILED | no | connection/pair could not mint a device credential |
PAIRING_TOKEN_EXPIRED | no | the single-use pairing token expired before pairing completed |
CAPABILITY_UNAVAILABLE | no | an operation was requested for a capability the host advertised as unavailable (e.g. attachments, tool relay) |
3.9 Live Voice
Live Voice errors are returned through app-owned IPC and provider adapter events. They do not represent Agent turn failures. A retriable error means the user may retry the same call after the stated transient condition clears; it does not trigger automatic provider or billing fallback.
| code | retriable | meaning |
|---|---|---|
LIVE_DISABLED | no | Live Voice is disabled in settings |
LIVE_NOT_CONFIGURED | no | no valid Live Voice binding is selected |
LIVE_PROVIDER_NOT_FOUND | no | the selected Provider is missing, disabled, or changed while resolving credentials |
LIVE_AUTH_KIND_UNSUPPORTED | no | the selected Provider credential type is incompatible with the adapter |
LIVE_AUTH_REQUIRED | no | required OAuth or API-key credentials are absent or rejected |
LIVE_ACCOUNT_ID_MISSING | no | Codex OAuth account identity is absent or inconsistent |
LIVE_ACCESS_DENIED | no | the provider denied access or entitlement |
LIVE_RATE_LIMITED | yes | the provider returned a rate-limit response |
LIVE_PROTOCOL_UNSUPPORTED | no | endpoint, model, or requested protocol profile is unsupported |
LIVE_PROTOCOL_ERROR | no | a provider or IPC message is malformed or violates the selected protocol |
LIVE_ALREADY_ACTIVE | no | another Live Voice call or microphone-release quarantine owns the single-call slot |
LIVE_REQUEST_CONFLICT | no | an idempotency request ID was reused with different call parameters |
LIVE_SETTINGS_IN_USE | no | settings changed during preparation or the active binding cannot be rewritten |
LIVE_MEDIA_RELEASE_UNCONFIRMED | no | renderer media release was not acknowledged; Main quarantines the microphone lease |
LIVE_STALE_CALL | no | the call, request, or capture epoch is no longer current |
LIVE_INVALID_OWNER | no | IPC or media-port ownership does not match the trusted main frame |
LIVE_MICROPHONE_BUSY | no | Dictation, another Live call, or an unconfirmed prior release owns the shared capture lease |
LIVE_MICROPHONE_DENIED | no | the user or operating system denied microphone permission |
LIVE_MICROPHONE_UNAVAILABLE | no | no usable microphone device is available |
LIVE_MEDIA_UNSUPPORTED | no | required browser media or AudioWorklet support is unavailable |
LIVE_PLAYBACK_BLOCKED | maybe | browser audio playback needs a user gesture or could not resume |
LIVE_TIMEOUT | yes | a bounded startup, handshake, heartbeat, control, or cleanup stage timed out |
LIVE_NETWORK_ERROR | yes | a transient provider transport connection failed |
LIVE_NETWORK_POLICY_UNSUPPORTED | no | the desktop proxy route cannot be represented safely by the Live transport |
LIVE_AUDIO_BACKPRESSURE | no | bounded PCM or playback credits were exhausted |
LIVE_EXECUTION_NOT_CONNECTED | no | a provider requested an unsupported function/delegation execution path |
4. Mapping rules
Host RPC numeric → AppError.code
See 06-host-rpc-protocol.md numeric table.
Example: host 1004 → TOOL_DENIED.
Provider exceptions
Node sidecar maps provider SDK errors into:
PROVIDER_UNAUTHORIZEDPROVIDER_RATE_LIMITEDCONTEXT_TOO_LARGEMODEL_NOT_CONFIGURED(provider rejects the selected model with 404)PROVIDER_ERRORNETWORK_ERRORSTREAM_FAILED
A provider message is CONTEXT_TOO_LARGE when pi-ai's isContextOverflow recognizes it from the message text — the same check that starts provider overflow recovery — or when it carries the context-length wording the classifier already matched (HTTP 413 always). The terminal code therefore agrees with the recovery decision for every overflow wording pi-ai knows, for example DashScope/Qwen Range of input length should be, z.ai Prompt exceeds max length, xAI maximum prompt length is, Groq, llama.cpp, and Bedrock Input is too long for requested model. CONTEXT_TOO_LARGE is never replayed by either retry budget, and a delegate reports it as SUBAGENT_CONTEXT_OVERFLOW. Explicit billing/account exhaustion codes and messages (including insufficient_quota, credit_balance_exhausted, and organization/project spend-limit codes) map to terminal PROVIDER_ERROR even when the HTTP status is 429. Throughput rate-limit wording, including Bedrock's Throttling error: Too many tokens, please wait before trying again., stays PROVIDER_RATE_LIMITED.
An exact terminated provider message and equivalent premature stream-close messages map to STREAM_FAILED. A message is TURN_ABORTED only for a structured abort or the fixed phrases Request aborted, The operation was aborted, and Turn aborted; connection-abort wording such as software caused connection abort stays retriable NETWORK_ERROR, and stream aborted stays retriable STREAM_FAILED. A request-setup or post-response PROVIDER_RATE_LIMITED uses the shared runtime budget: ten retries after the initial attempt, with setup and stream failures counting together. Non-429 transient failures — STREAM_FAILED, NETWORK_ERROR, TIMEOUT, and retryable PROVIDER_ERROR such as an upstream gateway 502/503/504 — share their own bounded budget of ten retries after the initial attempt, also counted together across setup and stream, and separate from the 429 budget. Both budgets are abortable and reset after a complete successful model response, including a tool-call response, in both the main session and builtin subagents. Headers, partial output, and phase changes do not replenish them. Terminal exhaustion reports retryAttempt: 10 from the applicable budget even after retry activity cleanup. The 429 path honors retry-after-ms, retry-after seconds, and HTTP-date headers before client backoff and caps a wait at 30 seconds; the non-429 path applies the same precedence with an 8-second cap and otherwise waits 1, 2, 4, then remains at 8 seconds for later retries. Only the failed request is replayed; the session and its tool state are untouched. A non-retryable PROVIDER_ERROR from a malformed 400/422 request never enters either budget. The persisted infiniteProviderRetry setting is false by default; when true it removes only the retry-count ceiling for the admitted transient/network classes (including 429). Backoff, Retry-After, cancellation, and terminal classification remain unchanged, and the setting may continue API usage until the user stops the turn.
A NETWORK_ERROR carries the failing transport layer as bounded details: networkCategory (dns, tls, timeout, refused, unreachable, reset, proxy, or unknown when nothing survived), networkCode (the errno, e.g. ENOTFOUND, ECONNRESET, EPROTO, UND_ERR_SOCKET), and, when the transport reported them, networkSyscall and networkHost. Only the bare hostname is kept — never a URL, port, path, query, or credential — and providerCode is omitted when it would repeat networkCode. Per-layer codes (DNS_ERROR, TLS_ERROR, SOCKET_RESET, …) are deliberately not introduced: the category splits the layers without adding user-visible codes and locale strings for each of them.
The diagnosis is read from the live cause chain at the fetch boundary, not only from the provider message. pi-ai flattens a rejected request into errorMessage, so by the time classification runs the errno undici keeps in error.cause is already gone and a bare fetch failed can only be reported as networkCategory: unknown; the fetch wrapper still holds the original Error and supplies the same validated fields from it. A captured cause also settles the phase: the fault is reported as phase: request because no response ever arrived, which is what distinguishes it from a stream that ended mid-response. networkRoute (direct, environment-proxy, http-proxy, socks5-proxy) names the hop the request was taking, so a failure at the proxy is readable without guessing from an errno. A request bound for pi-ai's Google adapters carries no fetch wrapper and never reaches onResponse (issue #1072), so it reports neither field: it keeps the provider's own message, its Retry-After falls back to the bounded ladder, and the rebuild below does not fire for it.
When one origin fails this way repeatedly inside a turn — twice in a row, without any response — the provider transport is rebuilt before the next attempt instead of replaying into the same undici pool. The rebuild is process-wide and deliberately bounded: one rebuild per streak, at most one every 30 seconds, and never for a dns failure, which a fresh pool cannot change. The replacement is installed before the previous dispatcher is closed, and the previous one is closed gracefully, so a request another session already dispatched finishes on the pool it started on. The route in effect is reproduced, never downgraded to a direct connection.
Permission timeout
PERMISSION_TIMEOUT is a legacy compatibility code and is no longer emitted for local desktop permission requests. An unresolved local permission remains pending; explicit denial or cancellation is reported as TOOL_DENIED.
Shell and Plan/Goal checkpoint failures
SHELL_NOT_FOUND is returned only when catalog fallback finds no available platform shell. COMMAND_SHELL_CHANGED never retries with a different shell; the turn must obtain a fresh effective ID/dialect. PLAN_ARTIFACT_WRITE_FAILED never creates an approval row. PLAN_APPROVAL_TIMEOUT applies only to the absolute pending deadline; PLAN_EXECUTION_INTERRUPTED identifies an already-approved queued/running execution interrupted by abort or host recovery. PLAN_KIND_MISMATCH is a terminating tool error like PLAN_NOT_ACTIVE: the submit tool ran against the wrong contract, so no artifact is written and no approval row is created.
Local request preparation failures
A structured LOCAL_REQUEST_ERROR from context validation, context estimation, or request preparation maps to the existing INTERNAL code with retriable: false. Preserve its local origin and phase before adapter errors are flattened to text. Diagnostics may retain the cause type, but must not copy request content, search results, credentials or arbitrary cause messages into the UI. Do not identify these failures by matching an exception sentence or by treating all JavaScript TypeErrors alike: fetch transport failures retain the existing network/retry and cancellation behavior.
Restored-history validation may fail before a runtime stream exists. In that case the existing RPC error data carries errorCode, retriable: false, and safe details (origin, phase, optional cause type). No provider request is made, the sidecar stays available, and a stored record is never rewritten. A container that is not a stored block list still fails this way.
A single stored block that cannot be replayed is a different case: this app itself stores display-only blocks when a gateway drops ids, so the whole stored replay for that message degrades to "no replay" instead of failing every later turn. The turn continues, display rounds are unchanged, and the diagnostic records the block count and phases without copying search content, results or credentials.
5. UI handling guidelines
| class | UI behavior |
|---|---|
auth/config (PROVIDER_SECRET_MISSING, MODEL_NOT_CONFIGURED) | assistant error message with settings CTA |
| permission denials | inline tool card state |
| retriable provider/network | assistant error message with diagnostic details and Continue; the session-scoped failed-turn recovery card is a fallback only when no structured assistant error is present |
| internal/host unavailable | degraded banner + recovery tip |
Message-bound provider failures never use a toast or floating global banner. A PROVIDER_RATE_LIMITED failure remains invisible while its bounded retry budget is available; only exhaustion renders the assistant error and lifecycle error. The assistant error message shows a localized summary and stable code, with an accessible details disclosure containing the redacted provider response, provider ID, and model ID. Provider detail is capped at 600 characters and common credential/header values are redacted before event emission or persistence. When available, the details disclosure may also show bounded phase, providerStatus, providerCode, providerWaitMs, streamMs, retryAttempt, networkCategory, networkCode, networkSyscall, networkHost, networkRoute, requestMessages, requestBytes, and compactionGeneration fields. The request fields are counts and byte sizes only and the compaction field is the checkpoint generation counter; none of them carries message content. While a transient provider failure retries, the activity indicator's reason popover shows the localized summary, the stable code, and — for a network failure — the transport errno (NETWORK_ERROR · ENOTFOUND), so the failing layer is visible during the retry loop as well as in the log record. The assistant error card offers a localized Continue action that resends the continuation prompt (继续当前任务 / Continue the current task) in the same session without truncating the failed turn. The session-scoped failed-turn recovery card is used only when no structured assistant error is present; neither failure surface offers Regenerate.
6. i18n key convention
errors.<code>
errors.<code>.actionExamples:
errors.PROVIDER_SECRET_MISSINGerrors.PROVIDER_SECRET_MISSING.actionerrors.HOST_UNAVAILABLE
7. Acceptance
- Every IPC failure returns
AppError.code - No raw untyped string-only failures on main paths
- Plan/Goal hard-denies use explicit tool-specific codes; Bash is never denied by either contract mode solely because of the operating mode and instead follows permission policy
- Host numeric codes map to stable string codes
- Invalid shell settings, no-effective-shell/stale-pin, artifact-write, expiry, scheduled-rejection, and restart-interruption paths map to stable codes; only the documented pre-turn catalog fallback is allowed and no work is replayed
Certificate verification failures (issue #714)
NETWORK_ERROR is non-retriable when details.networkCode is a recognized certificate verification failure, including an untrusted/self-signed chain, an expired/not-yet-valid certificate, or ERR_TLS_CERT_ALTNAME_INVALID. A concrete certificate cause takes precedence over generic socket/proxy wrapper codes. Captured fetch causes apply this policy after adapter error flattening as well as during direct classification. Unknown and non-certificate TLS/protocol errors retain existing recovery behavior.
The transcript keeps the stable error code, transport errno and raw details, but uses localized certificate guidance instead of the generic connectivity summary. It asks the user to check the certificate, clock, and trusted roots used by security software/proxies, then restart after changing trust. It does not claim that interception is the only possible cause or offer a TLS bypass. Manual Continue remains available after the cause is corrected.
