# Translator for Pi

English | [简体中文](README.zh-CN.md)

[![npm](https://img.shields.io/npm/v/@orbit-labs-ai/translator?logo=npm&label=npm)](https://www.npmjs.com/package/@orbit-labs-ai/translator)

This directory is the Pi adapter of [Translator](../README.md): a Pi extension whose entry point is **`index.ts`**. The [main guide](../README.md) covers installation, every setting and privacy. This page explains how the extension behaves inside Pi and how it is organized.

All Pi-specific code lives here; the shared translation rules live in `../shared/`. This directory handles only Pi registration, event handling, branch history, session journals and the optional Pi subprocess backend.

## Features

- **Shared preflight.** Before either direction calls a model, the shared scanner identifies certain literals, including bare Chinese filenames, paths, identifiers/settings and mentions, and checks the remaining prose. Literal-only input and output skip without a translation call or log; quoted Chinese prose remains translatable, and plain `OK` output still translates.
- **Input hook.** The `input` handler translates Chinese prompts and returns the translation together with your images. The skill and prompt-template lines Pi expands (`/skill:name …`, `/template …`, as `pi.getCommands()` lists them) go out as typed; a line that only starts with a slash (a path, a typo) and a `!` line Pi sends to the model are translated, while a `!` line Enter runs goes to the shell before this hook. Any pre-submission failure, including a storage or UI error, blocks the prompt instead of sending the original, and names the reason. Post-delivery display errors cannot undo submission.
- **Answer translation.** With Output on, the `turn_end` handler translates each complete final answer, including one cut at the output limit, and adds the Chinese as a separate entry after it. The answer itself is never replaced or resubmitted. Pi ends the turn only after its `turn_end` handlers, so this one holds the turn while the translation runs, but only until you interrupt or send another prompt: the translation then goes on, and its card follows (TUI/RPC abort releases the wait; print/JSON still waits before exiting). A long answer is translated in parts at the same time.
- **Subagent translation.** For Pi's documented `subagent` tool, the parent translates single, parallel and chain task arguments before dispatch. Completed public child answers get separate display-only cards, never translated tool results or chain handoffs. See [Subagents](#subagents) for the exact supported shapes and lifecycle.
- **Translation cards.** Each translation is a session entry drawn as a card with only what the transcript lacks: your original after the delivered translated user row, the Chinese translation after an answer. A successful original's card waits for its delivered row; while the prompt waits in Pi's steer or follow-up queue, which shows only its English, its original is shown above the editor. Failures show immediately. An answer's card that lands after later rows names the answer it belongs to. A card renders Markdown with Pi's own renderer and theme; expanding it (Ctrl+O) adds the translator model, history reuse and trace. A blocked prompt's card shows the reason and the original. The model never reads these entries.
- **Settings panel.** `/translator` opens a compact native panel with Input, Instruction, Output, Model (with search) and Thinking; `?` explains the selected row.
- **Toggle bar and shortcuts.** A bar under the editor shows Input, Instruction and Output; click them in fullscreen mode, or press Ctrl+Alt+T, L or Y. Ctrl+Alt+C cancels pending translations in both directions.
- **Branch settings.** Settings are saved per conversation branch and follow tree navigation, reload and resume.
- **History keeps your words.** Pi's editor history holds what you typed. When Pi refills it from the stored rows, which hold the English (at startup, `pi -c`, `/tree` and a session switch), each translated row is mapped back to its original, and only then: English you type yourself stays English. When `/tree` or `/fork` puts a translated prompt back in the editor, it puts back your original.
- **Isolated requests.** Each fresh input translation or output part uses an isolated `streamSimple` completion with at most one Translator-level retry for API/transport, empty/incomplete-response or timeout failures. Provider retries remain off; there is no history, tools, AI audit or repair call. The session model or pinned translator and its own thinking level are used. A reply the provider reports as a refusal is asked once of the newest model of another family from the same provider.
- **Journals.** A translation that proceeds past preflight is journaled beside the session file before any model call, so even a failed first prompt leaves a record. Preflight skips create no translation record.

## Installing and loading

From the `translator` directory:

```sh
./install.sh --agent pi
```

The installer adds the extension to the `extensions` list in your Pi `settings.json` and keeps everything else:

```json
{
  "extensions": ["/absolute/HOME/.agents/hooks/translator/pi/index.ts"]
}
```

This shows the entry it adds, not a file to copy over your settings. `--pi-dir` or `PI_CODING_AGENT_DIR` selects a non-default agent directory. The installer backs up the settings it changes, keeps your other extensions, and migrates known registrations of the old `pi.ts`. It does not edit project-level registrations.

After changing the source or the registration, run `/reload` or restart Pi; `/translator status` confirms that the extension loaded. Until then, a running Pi keeps the version it loaded, whole: an update on disk never mixes with it. Pi lets only commands reload extensions, so the extension cannot apply an update by itself. Changes to the configuration file alone are picked up at the next request.

To load only this entry during development, run `pi -e /path/to/orbit-tools/Hooks/translator/pi/index.ts`, and do not register it anywhere else at the same time. The package manifest likewise exposes only `./pi/index.ts`. Pi supplies the SDK and TUI dependencies, so there is nothing to install with npm.

The entry loads its local dependency graph through Pi's synchronous jiti loader,
so `/reload` refreshes nested `.mjs` code as well as the entry point instead of
reusing Node's native ESM cache. Host SDK/TUI imports keep their shared identity.

## Using it in Pi

**`/translator` is the only translation slash command.** With no arguments in the TUI, it opens a compact native panel: **Input**, **Instruction**, **Output**, **Model** and **Thinking**. The default view is nine lines: a short heading, five controls and one keyboard hint. ON/OFF badges distinguish the switches; the model resolves inline, and thinking shows an arrow only when the effective level differs (for example, `max → high` or `medium → off`). Locks and unavailable models remain visible without expanding help.

Use **↑/↓** to move, **Enter/Space** to change, and **Esc** to close; changes apply immediately. Press **?** to show or hide the selected setting's explanation, including the reason for a thinking difference. Full details also remain available through `/translator status`. The panel uses Pi's `SettingsList`, with a searchable `SelectList`/`Input` model picker offering **Follow session** and available registry models. Inside the picker, `?` is ordinary search text and **Esc** goes back. Closing the panel leaves the draft unchanged. On Pi 1.4.0 it restores the previous dialog, or the editor when no dialog remains; invalidating a hidden panel leaves the dialog above it focused. Pi's full `/reload` still resets all extension UI as usual.

```text
/translator
/translator status
/translator usage
/translator help
/translator input [on|off|toggle|status]
/translator instruction [on|off|toggle|status]
/translator output [on|off|toggle|status]
/translator model [follow|provider/model]
/translator thinking [low|medium|high|xhigh|max]
```

The three switch subcommands toggle when no value is given; `model` and `thinking` without a value report settings only. Invalid arguments and unknown model IDs do not change saved settings. Configuration makes no model call and never changes the main model or its thinking level. A model pin applies to both translation directions; `model follow` overrides a file pin and follows future main-model switches. Thinking is independent (default `medium`), not inherited from the main conversation. Changing model or thinking still permits reuse of valid cached input translations; disable the cache to force a fresh comparison.

- **Existing controls.** Click **Input**, **Instruction** or **Output** below the editor in fullscreen mode. **Ctrl+Alt+T/L/Y** toggle **Input**, **Instruction** and **Output**, including inside the settings panel. The panel, commands, shortcuts and bar share live state. The bar wraps without stealing your draft or focus; regular mode uses keyboard controls. RPC/JSON/print create no terminal widget or panel; the no-argument command shows status/help instead. Global defaults refresh at input/agent boundaries; model selection refreshes translator feedback. Forced-disabled input is visibly locked; unreadable config disables stale controls.
- **Scope.** All five settings are branch-relative overrides over file defaults, restored on tree navigation, reload and resume; a new session starts with file defaults. Pi resolves common configuration fields followed by `hosts.pi` defaults (`loadConfig(undefined, "pi")`), then its saved branch overrides and the input environment lock. The parent uses these same settings for subagent translation; child profiles do not select the translation model. `--no-session` keeps them in memory. Neither the shared config nor main-model settings are edited. Branch/session changes close the panel and invalidate its old callbacks. Changes apply to subsequent translations, not an in-flight request's snapshot.
- **Cancel input and output translation.** **Ctrl+Alt+C** cancels both directions, including simultaneous requests and while the panel has focus. Cancelled input submits neither original nor translation; cancelled output keeps the completed original answer. The shortcut does not change settings or disable later translations. There is no cancellation slash command or subcommand. TUI/RPC abort ends Pi's wait for completed-output translation, not the translation. Print/JSON waits for it even if the producing run aborts. When Esc or Alt+Up puts Pi's queue back in the editor, your originals take the place of their English. A prompt still being translated for Pi's queue when the run stops is cancelled, and in the TUI it goes back to the editor, after what Pi restores there.
- **Your prompts.** After a translated prompt is actually delivered as a user message, its original appears once in a display-only card after that row. The user row already shows the English, so the card does not repeat it. While steering and follow-up translations wait in the queue, even during long tool calls, they stay journal/cache-only, and the originals of the last three show above the editor. RPC notifications and headless stderr previews follow the same delivery timing. Failed input still shows its original and error immediately.
- **Answers.** With answer translation on, the status line shows `Translator · Output: translating to Chinese…` while it works, and the Chinese then appears as a separate entry after the answer. If you interrupt or send more first, the run goes on and the card follows; when later rows came between, its label names the answer it belongs to (`for “…”`). A warning that names a part (`part 2 of 4`) means that one part of a long answer failed; the answer stays as it is. Navigation, a session switch or `/reload` that stops a translation leaves a card that says so, and a late card, failed or not, names its answer.
- **Failures and other modes.** Failures appear as warnings. In RPC mode, records arrive as notifications; without a UI, previews go to standard error, never to the protocol's standard output.

The input handler combines its own cancellation signal with `ctx.signal` when
the host supplies one. Completed-output translation uses its own controller in
all modes; ordinary run abort never cancels it. Ordinary SDK/RPC `abort()` during idle input preprocessing is
not a reliable substitute for the shortcut; the regression suite retains this
limitation separately from the successful cancellation checks. Tree navigation,
reload and shutdown invalidate both input and output work and block late writes.
Bare SDK disposal blocks late results but may leave a request running until
completion or timeout. Checks on Pi 1.0.4 and 1.1.0 invoke the registered
shortcut against real SDK requests; physical Escape/key delivery is not claimed.

### Startup defaults

In your shell, `translator --host pi input on`, `translator --host pi output on`,
`translator --host pi model provider/model-id` and `translator --host pi thinking low`
set Pi-only defaults, also used after `/new`. `instruction` and `cache` work the
same way. `model follow` explicitly masks a common pin; `model inherit` or
`reset model` removes the Pi override. `reset all` removes all known Pi overrides,
including backend/timeout, while preserving unknown fields and Claude defaults.
Existing branch overrides still win. See [all default commands](../README.md#default-settings-for-new-sessions).

## Auxiliary usage accounting

`/translator usage` is read-only and makes no model call. It reports Translator's **whole-session auxiliary** tokens/cost by `input`, `output` and `subagent`, including abandoned branches, separately from Pi's native totals. It shows unreported attempts, pending requests (including cancellation/timeouts awaiting a reply), outcome counts and persistence warnings. Input/output skips, cache hits, and pre-call configuration/model lookup failures create no receipts. Each invoked isolated completion has a unique receipt, including API failures, refusals, retries, each concurrent answer part, both directions of task preparation and child-report display. Child execution usage remains the subagent tool's responsibility, not Translator's.

The public Metrics interoperability contract is:

```typescript
customType: "orbit.translator.usage.v1"
data: {
  version: 1,
  requestId: string, // fresh UUID for each actual completion attempt
  provider: string,
  model: string,
  usage: Usage | null, // Pi Usage: returned numeric fields only
  status: string,
  direction: "input" | "output" | "subagent",
  nativeAccounting: false
}
```

- Receipts use **only `pi.appendEntry()`**, a non-context custom entry. No native usage entry, tool-result rewrite, private API cast/patch or direct session JSONL write is involved. This release never emits `nativeAccounting: true`. The command does not change Pi's `/session` or footer totals. Claude Code and the standalone subprocess backend are outside this Pi extension contract.
- Capture happens immediately when the provider result returns, **before** response/literal/operator validation, fallback or aggregation. Provider/model use the returned identity when available, otherwise the requested identity. Refusal and fallback are separate receipts under their own models. Returned usage survives a rejected translation; legacy card/journal `usage` aggregates are not an accounting source.
- `usage` copies only finite nonnegative numeric Pi fields: `input`, `output`, `cacheRead`, `cacheWrite`, `totalTokens`, optional `reasoning` / `cacheWrite1h`, and `cost.{input,output,cacheRead,cacheWrite,total}`. Missing/malformed usage is `null`, never synthesized from text, token estimates, rates or earlier records. Reported zeros remain zeros. These are provider/SDK-reported values, not a billing guarantee; SDK defaults may themselves be zero. Reasoning and one-hour cache writes are subsets, not extra tokens to add.
- `status` describes the attempt, **not translation acceptance**: `completed` (provider stop, even if empty or literals later fail), `refused`, `incomplete`, `error`, `cancelled`, `timeout`, or `interrupted`. Only returned usage is numeric; thrown errors carry no inferred usage or copied error text. No prompts, outputs, paths, credentials or provider payload extras are in the receipt.
- Consumers read custom entries across `getEntries()` (**not only `getBranch()`**) and deduplicate `requestId` across the whole session before summing. Count `usage: null` as unreported, not zero. Do not sum legacy aggregates alongside these receipts, or add these auxiliary receipts into native totals. Reload/resume reads the same entries; forked/copied receipts retain their IDs and are counted once within the destination session. No historical backfill is attempted.

**Cancellation and ownership.** Ordinary cancellation/timeout does not invent a zero receipt while the provider is still pending: the command shows that pending attempt, and its eventual result can still carry numeric usage even though its text is discarded. Before tree navigation, reload, shutdown or session replacement, unresolved attempts are sealed on the original branch with `status: "interrupted"` and `usage: null`. Those immutable receipts cannot later be revised by a late reply, and an invalid context never writes into a new owner. This deliberately leaves unknown costs when a provider reports too late for the owning session. Bare SDK disposal without a shutdown hook, or process termination, can lose pending accounting altogether.

**Persistence and privacy.** No usage sidecar is added. Pi 1.4.0 buffers custom entries until its first user/assistant conversation message: a blocked first input has receipts in memory, not durable receipts on disk. In-process reload retains that buffer; exit or replacing that session can lose it. A later conversation message flushes buffered receipts. `--no-session` remains entirely in memory. A write failure warns and keeps best-effort in-memory totals for the current runtime; it never retries a model call and cannot promise durability after reload/exit. The older text-bearing translation journals remain separate, with their existing privacy rules; they are not recovered into this accounting stream. Durable receipts remain in the native session file after uninstall until that session is deleted. Do not treat these limits as complete provider billing coverage.

## Subagents

Subagent support uses the public tool contract verified on **Pi 1.3.0 and 1.4.0**. It recognizes the tool named **`subagent`** with the parameter/result contract of Pi's shipped subagent example, also used by the inspected workbench adapter. It does not install, replace or launch a subagent tool, discover agent profiles, or change its trust/approval policy. Load your existing subagent extension separately.

### Task input

With Input on, a public `tool_call` handler translates only these task strings:

| Mode | Parameters | Translated fields |
|---|---|---|
| Single | `{ agent, task, cwd? }` | `task` |
| Parallel | `{ tasks: [{ agent, task, cwd? }, …] }` | Every `tasks[i].task` |
| Chain | `{ chain: [{ agent, task, cwd? }, …] }` | Every `chain[i].task` template |

The example's optional top-level `cwd`, `agentScope` and `confirmProjectAgents` fields are preserved. Agent names, working directories, array order, approval flags and non-task fields are never translated. There must be exactly one mode. Parallel batches must contain **1–8 valid tasks**, matching the shipped tool's limit; the complete batch is validated before any preview model call (including malformed later items or sparse arrays). Other shapes or extra custom fields on a tool named `subagent` are **blocked with an unsupported-parameters warning when Input is on**, not guessed. Input off deliberately lets that tool receive its original arguments. Other tool names are untouched.

Each Chinese task uses the shared input preflight, literal masking, response safety, exact-input cache, translator model/thinking and optional language instruction. English tasks are submitted byte-for-byte without a suffix, but Input also prepares their **Chinese display-only preview**, independently of Output. Mixed tasks preserve already-Chinese sections and translate the English prose for display; the answer's brief-preface exemption does not apply. Literal-only tasks still need no call or record. The exact source, English submission and Chinese `displayText` remain separate; only Chinese-to-English results enter the input cache. Completed invocation/agent/index identities prevent repeat previews.

The whole batch is prepared sequentially and journaled **before any child can start**. A Chinese-to-English, storage or pre-dispatch UI failure, or cancellation, blocks the whole call without partially rewriting its tasks. A display-translation failure alone records a warning and retains the valid English task. Earlier prepared translations may remain in the cache/journal; they do not prove dispatch. Task cards likewise record preparation, not child execution or approval; they never replace your own prompt's history.

In chains, every `{previous}` operator is additionally protected, including repeated operators outside code. A missing, duplicated or added operator blocks dispatch; cached translations must preserve its count too. Templates are translated before the tool expands them. The tool then inserts the **original previous result**, never a Chinese display translation. Text the tool adds afterward, including that handoff, agent-profile/system prompts or other custom runtime content, is not retranslated. All templates are prepared even if a later execution failure stops the chain early.

Pi documents `event.input` as mutable and passes it to execution; later `tool_call` handlers see the new arguments. Translator changes no saved assistant tool-call row and cannot guarantee what another extension rewrites after its handler. The Chinese task preview is shown separately with its call ID, mode, agent and task index; the exact original remains recorded. Neither is injected as a message or prompt.

### Display-only answers

The notification-only `tool_execution_end` handler reads the documented `details.mode` and `details.results[]`. For each successful child (`exitCode: 0`), it takes **all public text blocks of its last assistant message**, only when its stop reason is `stop` or `length` and it contains no tool call. It translates completed siblings and earlier successful chain steps even when another child fails. Streaming progress, stderr, failed/aborted children, hidden reasoning, tool messages and unknown result shapes are not translated. There is no fallback that translates arbitrary tool `content`.

Translations are `output-translation` custom entries rendered by Pi's Markdown card renderer, labelled with call ID, mode, agent and index so identical agent names remain distinguishable. RPC gets display notifications and headless modes get stderr previews. **No `tool_result` replacement, result mutation, `sendMessage`, context edit or extra model turn is used.** Original `content`, `details`, structured results, images and error/usage metadata stay with the tool; chain handoffs and the parent's provider requests never receive these display translations. The parent translation request contains only the selected, masked task or public answer, not the child's profile, conversation, tools or reasoning.

A batch's answer translations run sequentially; an individual long answer still uses the shared concurrent-part translator. In TUI/RPC, interruption or queued input releases Pi's wait while the display batch and all completed siblings continue sequentially. Print/JSON waits for the batch before exiting. **Ctrl+Alt+C** cancels current subagent input/output translations and prevents remaining batch translations from starting; it does not stop already-dispatched agents or change settings. Use Pi's ordinary run abort to stop tool execution. A failed output translation leaves the original tool result intact and does not prevent another successful child's answer from being translated.

Tree navigation, session replacement, reload and shutdown invalidate pending batches. Live context/session checks reject late results; IDs plus task indices prevent duplicate completion cards on the active branch, including public nested `ctx.executeTool("subagent", …)` calls with `parentToolCallId`. Completed cards survive resume and reload without replaying translations. Bare SDK `dispose()` prevents dispatch/late writes, but Pi 1.3.0 can emit its own `turn_end could not resolve the persisted assistant entry ID` boundary diagnostic when disposed mid-tool; that diagnostic is not a successful orderly shutdown.

Subagent input traces share `prompt-translations/`, and output traces share `output-translations/`, beside the parent's session file. Originals and translations also live in the parent's non-context custom entries; the usual enabled shared input cache applies. `--no-session` uses memory only. These are plain-text local records with the same privacy and cleanup rules as [ordinary translations](../README.md#data-and-privacy).

## Gateway and unattended processes

A launcher can set `TRANSLATOR_PI_BYPASS=1` in its Pi child's environment to keep translation out of automated task execution. The entry returns before registering hooks, controls or renderers, without reading translation configuration, history or saved branch overrides. Both input and answer translation are skipped; the input and answer stay unchanged, with no language suffix, translation calls or new translation journals. Broken translation config and an unavailable translation model cannot block a task through Translator. Other extensions and Pi's tools/trust/authentication remain active.

The switch is process-local and requires an updated Translator as well as a launcher that sets it. Do not export it globally: normal Pi sessions without it still translate, including JSON/print/RPC input, with their existing fail-closed behavior. Saved settings remain intact for later interactive use. `TRANSLATOR_DISABLE=1` retains its separate, input-only meaning. This bypass disables `/translator` and its UI for the opted-out process; it is not a session setting or prompt-text heuristic.

For supported tasks, the parent publishes an explicit `TRANSLATOR_PI_HANDOFF` containing its PID and hashes/lengths of prepared task segments, never task text. Only a direct JSON child whose initial prompt matches that assignment (optionally with the official example's `Task: ` prefix) leaves input/final-report translation to the parent. Repeated `{previous}` substitutions must match exactly; original Chinese chain reports are not retranslated. With Input on, chain tasks need fixed non-whitespace task text outside `{previous}` (for example, `Review {previous}`); placeholder-only tasks fail closed with an explanation. Input off preserves them but does not publish ownership. Children reject unanchored tickets even from older parents, so those tickets cannot suppress unrelated input/output. The child still handles its own nested subagent calls. This avoids duplicate report calls without disabling arbitrary children merely because of a parent PID. Metadata is captured once across reload and cleared at completion/invalidation; no files are added, including in memory-only mode. Custom-launched sessions without a matching handoff translate normally when they load Translator, under their own defaults. Launchers that omit Translator, strip the handoff, insert an intermediate process or expose no supported results cannot guarantee parent-visible previews or cross-process deduplication.

## How it works

| Module | Responsibility |
|---|---|
| `index.ts` | Entry point: renderers, controls, input/output hooks and public message/delivery lifecycle handlers |
| `card.mjs` | The card for each record: only the Chinese, as Markdown, with details when expanded |
| `input.mjs` | Translates or reuses input, journals it before any model call, keeps images, and replaces or blocks the prompt |
| `output.mjs` | Translates complete public main/subagent answers; shows and records them, never replaces or resubmits them |
| `subagents.mjs` | Validates the supported subagent contract, atomically translates task arguments, protects chain operators and queues display-only final answers |
| `handoff.mjs` | Hash-only prepared-task ownership for direct JSON children; preserves original chain handoffs without blanket child suppression |
| `controls.mjs` | Unified command, button/shortcut actions, validated branch settings and panel/widget lifecycle |
| `settings.mjs` | Native five-option settings panel, searchable registry model picker and effective-thinking feedback |
| `widget.mjs` | Below-editor toggle layout, theme, wrapping and native mouse hit testing |
| `history.mjs` | Older records on the active branch, the shared, versioned exact-match cache, and the originals of refilled editor history and of a prompt `/tree` or `/fork` puts back |
| `trace.mjs` | Journals kept beside the session; never writes to Pi's native session file |
| `usage.mjs` | Per-attempt numeric auxiliary receipts, session ownership and whole-session usage summaries; never native accounting |
| `model.mjs` | Chooses the translator: the session model, at `thinkingLevel` clamped to what it supports, and the model asked after a refusal |
| `backend.mjs` | Isolated Pi subprocess used by the command-line tool and by Claude Code's optional Pi backend |

**Input.** Pre-submission failures, including UI, storage or runtime errors, return `action: "handled"`, blocking the prompt; Pi's ordinary extension-error handling could otherwise let the original through. Success writes the journal and enabled cache before returning `action: "transform"` with the English and original images, but appends no successful display entry yet. Public `message_start` / `message_end` identify delivery. Because Pi persists the row after `message_end`, the next ordered message/provider boundary confirms that exact user object on the active branch and appends a custom entry with `userMessageId` after it, before a later row. No timer, message injection, context mutation or extra turn is used. A post-delivery display error warns that the prompt was sent; it cannot fail submission retroactively. A blocked prompt names its reason (`Prompt NOT sent: translation failed (…)`).

**Delivery limits.** Pi exposes no input-to-queue ID or extension queue-removal event. Exact submitted text and image bytes are matched within the live session/generation; repeated identical originals consume separate occurrences, and repeat message events cannot duplicate cards. Different originals with indistinguishable pending translations, conflicting bypassed inputs (including `source: "extension"` or Input off), and normalized/rewritten images or text are deliberately not guessed. Their display may be omitted while persistent journals and enabled caches remain. Cleared queues are forgotten when no pending work is observed at input while Pi is idle, or at settlement; tree/session navigation, reload and shutdown discard pending display associations. Nothing rebuilds them from historical text. Existing delivered cards survive resume in their original order; a legacy card from an older version is not moved.

**Translation requests.** Each translation is a separate `ctx.modelRegistry.streamSimple` request with no conversation history, no tools, no cache session and no provider retries. It uses the effective branch/file model pin, or the session's current model read for each request when following. Translator `thinkingLevel` (default `medium`, overridden by `/translator thinking`) is independent of the main session's level: without an override, a session at `max` translates at requested `medium`. Pi clamps it to model support, and a model without reasoning gets no reasoning option. API/transport failures, empty/incomplete replies and request timeouts retry once on the same model. Each attempt has its own configured deadline. Explicit cancellation, local configuration/storage and literal-integrity faults do not retry. A missing pinned model fails rather than silently switching to the main model. A reply the provider reports as a refusal (`rawStopReason: "refusal"`, which pi-ai reports as an error) is asked once more of the newest available model of another family from the same provider (Sonnet, or Haiku when the translator already is a Sonnet model), on the same route and API (the same text before the family word, no variant such as `:batch`), ordered by the version after the family word; the record names it as `model` and the first one as `fallbackFrom`. Without such a model, the translation fails and says so. Neither the model pin nor thinking changes call Pi's main-model setters. The language instruction and answer translation remain independent switches.

**Preflight language decisions.** Both directions decide only before translation, before config/history/model work. The shared scanner identifies certain literal spans for both detection and request masking: code, math, links, filenames (including bare `报告.md`), paths and literal-like identifiers/settings/mentions. Literal-only text skips in both directions. Input translates remaining Chinese prose, including quoted Chinese; ambiguous prose stays visible to the model. Output additionally masks ASCII resources against Chinese edges, contextual technical identifiers/labels and single Titlecase names directly between Han prose (spaces allowed). Already-Chinese source units are protected/restored rather than sent again. A leading preface of at most 12 English words before an otherwise Chinese body of at least 80 unmasked Han characters skips; substantive English elsewhere and standalone `OK` still translate. English units stay batched, not one call per line. Skipping creates no translation request, record, status, preview or language suffix. See [local language detection](../README.md#local-language-detection) and [what is protected locally](../README.md#what-is-protected-locally).

**Response safety, not semantic validation.** After completion (at most one API retry per input or output part, shared with refusal fallback), only completed/nonempty response and literal-restoration safety checks run: each placeholder exactly once, and protected input code blocks in order. A failure blocks input or leaves the original answer unchanged; there is no AI audit or repair call. Neither a draft, restored text nor a joined long answer is rechecked for language. Literal content cannot trigger language rejection, and accepted results have no semantic quality guarantee.

**Cache integrity.** Reuse is keyed by exact source plus the scanner/prompt contract. The 0.7.0 `literal-whitelist-v9` contract bump isolates older versioned entries. Nonempty cache candidates, including unversioned legacy records, are checked only for source-literal bytes and occurrence counts, not order, language or semantics. Changing model/thinking does not bypass a valid entry; turn the cache off to force a fresh translation.

**Backend isolation.** The optional CLI subprocess explicitly passes `--append-system-prompt ""` to replace APPEND_SYSTEM discovery, in addition to disabling context files, tools, extensions, MCP and persistence.

**Answers.** Answer translation ignores tool calls, hidden thinking, unfinished or aborted answers, and anything that is not an assistant message; an answer cut at the output limit (`length`) is translated. In the TUI, the `turn_end` handler waits for the translation only until `ctx.signal` aborts or Pi holds queued messages (`hasPendingMessages`, checked every 100 ms); the translation keeps its own controller, so only Ctrl+Alt+C, navigation, reload or shutdown stop it. RPC also releases the wait without cancellation; without a UI the run waits through ordinary aborts before exiting. Navigation, a session switch, `/reload` or shutdown that ends a translation records a `cancelled` card first. A configuration that cannot be read warns once, instead of adding a failure card to every answer. Message IDs prevent duplicate translations on the active branch. A failure keeps the original answer, and switching sessions discards a pending translation. An answer longer than 2,000 characters once literals are masked is cut into parts of about equal length, between paragraphs or lines and never inside masked code, and up to four parts are translated at once, each with its own deadline when `timeoutMs` is set; see [Long answers](../README.md#long-answers).

**Storage.** With `--no-session`, everything stays in memory. Settings use non-context `translation-mode` entries as field patches, not snapshots of file defaults: old boolean entries remain valid, model changes save an atomic `provider`/`model` pair, and `follow` saves `null`/`null`. Malformed patches are ignored atomically. Persistent sessions get write-ahead translation journals even before Pi has written its first native conversation row. The extension never rewrites completed model-visible messages or tool results; its input handlers replace user input and supported subagent execution arguments before submission. It edits the editor only to give back a prompt the stopped run cancelled, and its history only by mapping refilled English rows to their originals.

## Verifying

From the `translator` directory:

```sh
node --test test/pi-*.test.mjs test/output-parts.test.mjs test/backend.test.mjs
```

The integration test uses the real Pi SDK, a settings entry generated by the installer, an isolated agent directory and a local HTTP model fixture. It checks the requests actually sent, the renderers, cache reuse, resuming from disk, restoring branch settings, translator-only model/thinking overrides in both directions, the isolation of answers, and the journal written when the first input fails. If the SDK is missing, the test is skipped and says so, which is not a pass. Set `PI_SDK_PATH` when Pi is not installed in its usual managed layout.

`test/pi-usage.test.mjs` covers failed validation, refusal/fallback, missing/malformed usage, cancellation, timeout, concurrent-part failures, whole-session deduplication, stale owners, cache/skips, subagent previews/limits and persistence errors. `test/pi-usage-host.test.mjs` uses Pi 1.4.0 and a localhost-only provider to verify actual custom entries, unchanged native totals/context, disk resume, reload, tree/session replacement, first-input buffering and no-session storage. Run both with `node --test --test-concurrency=1 test/pi-usage*.test.mjs`.

The focused subagent checks are:

```sh
node --test --test-concurrency=1 test/pi-subagents.test.mjs test/pi-subagents-host.test.mjs test/pi-subagents-process.test.mjs test/task-display.test.mjs
```

`pi-subagents.test.mjs` checks atomic batches, chain operators/cache integrity, literal protection, immutable results, unsupported shapes, errors, cancellation, output-wait release, session invalidation and card labels. `pi-subagents-host.test.mjs` uses the selected real Pi SDK (verified on 1.3.0 and 1.4.0) and a localhost provider with an **offline subagent-tool fixture**, not real child agents. It verifies argument mutation at execution, single/parallel/chain handoffs, nested `executeTool` IDs, original tool results on the next provider request, disk resume/reload, input/output cancellation and runtime replacement, and the automation bypass with broken config. It records the bare-disposal host diagnostic above. That SDK fixture does not spawn child agents. Separately, `pi-subagents-process.test.mjs` runs the actual Pi CLI and its **unmodified official subagent example**, launching real JSON child processes for single, parallel and chain modes. A localhost provider verifies English/Chinese task delivery, Chinese previews, unchanged parent/chain context, original human history and parent-coordinated suppression of duplicate child translation. Neither suite tests physical keys, reads personal agent profiles or makes paid calls. The shipped example and installed adapter's matching contract were inspected; other custom tools/launchers and older Pi subagent APIs are not claimed as verified.

`pi-lifecycle-e2e.test.mjs` tests actual RPC abort, a later turn, session replacement, and print/JSON exit waiting. `pi-retry-host.test.mjs` checks real SDK HTTP 400/503, empty/truncated responses, transport loss and deadlines in both directions, with exactly two HTTP requests on recovery. `pi-tui.test.mjs` also injects Escape through tmux while output is pending. These are real host executions with synthetic localhost responses, not live-model linguistic or physical GUI acceptance.

`pi-gateway.test.mjs` runs the real JSON/print CLI with the process-local bypass, a localhost provider that rejects translation, enabled resumed branch settings, an unavailable translation pin and malformed config. It checks unchanged task/cron/peer input, unchanged English answers, zero translation requests/journals, continued execution of another extension and fail-closed ordinary input when the bypass is absent. It does not use the live gateway, WeChat or personal configuration.

Additional regressions exercise the actual fullscreen renderer with raw mouse
sequences, draft focus, narrow layouts, branch changes and live source upgrades.
The native settings component is tested with the compact layout, on-demand help,
model search, keyboard/focus and mouse routing, all five thinking choices, inline
requested/effective feedback, shortcut synchronization, environment locks, Unicode,
theme changes, CSI-u question-mark input, search selection stability and stale
callbacks. The real TUI fixture mounts the panel through the host's actual
custom-dialog implementation and checks mouse routing after resize. Pi 1.4.0
checks also preserve another native dialog above/below the panel across dismissal,
tree navigation and SDK reload, without changing the draft or accepting stale callbacks.
These retained-UI lifecycle checks do not simulate the full TUI `/reload` reset. Real SDK tests inspect pinned/follow-model
requests and thinking clamping without changing the main session, and suspend
local translator responses to verify Ctrl+Alt+C cancellation of input, output and
both simultaneously. Reload/dispose/session replacement reject late results; a
separate regression documents the ordinary idle-input abort limitation. Real CLI
wire tests exclude synthetic global/ancestor instructions. These are not
physical-terminal tests. The [0.7.2 verification record](../docs/testing.md#verification-record-for-072)
records the full suite and auxiliary usage checks on **unmodified official Pi 1.4.0**.
No host-core patch or configuration migration is required; reload after updating
Translator to enable the new receipts. Pi 1.3.0 and older hosts were not rerun for
0.7.2, and auxiliary usage is verified only on 1.4.0.
The [0.7.0 verification record](../docs/testing.md#verification-record-for-070)
reports the complete passing suite on Pi 1.3.0, including task boundaries,
asymmetric filtering, literal safety, long parts, cache integrity and real
steer/follow-up delivery ordering. Older minimum hosts were not retested for this
release. SDK contract fixtures and actual spawned-child cases are distinguished above; neither uses live paid models.

See the [main guide](../README.md), the [architecture](../docs/architecture.md) and the [test matrix](../docs/testing.md) for configuration, privacy and limitations.

## License

[MIT](../../../LICENSE) © 2026 Orbit Labs contributors.
