phux docs

Public terminal workflow

Create a terminal in a session, place it, execute argv, and keep a receipt without implementing phux-specific layout publication or rollback. This is an implemented vertical slice, not a replacement for every existing verb.

IntentionCLIMCPRust
Describe this serverphux terminal describephux_api_describephux_client::terminal::describe
Create, place and startphux terminal create --request-json JSONphux_terminal_createphux_client::terminal::create
Recover a receipt, without effectsphux terminal operation --session KEY --idempotency-key HEX32phux_terminal_operationphux_client::terminal::operation

All three CLI commands emit versioned JSON. Errors are JSON on stderr with a stable code; creation errors preserve the retry key and distinguish not sent, unknown outcome and a committed but incomplete receipt. Success means intent committed, not process exit or successful task completion.

First use

The server must already be running. Start from its description and the durable session keys in phux ls --json, not mutable names or a remembered @N:

phux terminal describe
session=$(phux ls --json | jq -r '.sessions[] | select(.name == "work") | .key')
key=$(openssl rand -hex 16)
request=$(jq -n --arg session "$session" --arg key "$key" \
  '{session: $session, idempotency_key: $key, argv: ["/bin/sleep", "30"]}')
phux terminal create --request-json "$request"

The strict request has idempotency_key (nonzero 32 hex digits), session (full 26-digit durable key), and optional argv, cwd, env, name, and placement. Unknown fields are rejected. Empty argv selects the server’s default shell; a nonempty argv executes directly in a new process. It is never shell source and never typed into an existing terminal. Environment maps cannot override server-owned PHUX_ variables or TERM. An explicit cwd must be an absolute, enterable directory on the executing server; it never silently falls back to another directory. This does not sandbox programs running as the serving Unix user.

Placement defaults to {"type":"new_group"}. To split beside a terminal created by this workflow, save its receipt and use:

{
  "type": "beside",
  "entry": "<receipt.placement.entry>",
  "resource": "<receipt.resource>",
  "instance": "<receipt.execution.instance>",
  "activation": 1,
  "axis": "horizontal",
  "ratio": 0.5
}

Use the actual activation returned, not a guessed 1. Horizontal means side by side; vertical means stacked. Ratio is the fraction kept by the anchor, finite and strictly between zero and one. The stable entry, resource, incarnation and activation must still match in the destination session. Unrelated layout edits need not conflict: the server applies the placement intention to the current arrangement. Focus and selection remain client-local.

Identity and outcomes

A successful receipt contains:

  • authority, session, resource: durable keys, not current handles;
  • placement.arrangement and placement.entry: stable identities of the committed presentation, which later edits may move or remove;
  • execution: the original activation’s current state and incarnation, when still recorded; failed is not a successful process exit;
  • terminal_id: a current local handle only when the original execution still has its engine; never persist it as a durable address;
  • idempotency_key, replayed, and schema_version (1). On the JSON receipt, execution.state and execution.outcome accept an unknown value, and wire ErrorCode already decodes an unrecognized code as Unknown, so naming a new JSON state, outcome, or error code does not bump schema_version. That rule is JSON-only. A #[serde(other)] Unknown does not keep the original token: it re-serializes as unknown. The binary wire_enum! / enum_byte path hard-fails on an unknown byte (UnknownEnumValue), so adding a wire enum value needs a feature bit.

Relaunch opens another activation. A receipt of the old creation does not adopt that replacement execution: its execution/handle observation becomes null. A lookup from the original session after a resource moves does not expose the execution in its new session. A missing observation is not proof that an execution ended successfully.

The server commits membership + placement + launch intent in one organization-store transaction. A placement refusal, stale anchor or failed store write starts no process and publishes no partial layout. The OS process starts after commit. If that launch fails, the creation remains committed with a retained failed execution; no caller needs to undo a layout or issue a compensating kill. After a restart, failed together with outcome unobserved means the launch intent was committed, never launched. It is not a completed process whose exit went unseen (completed + unobserved). The public workflow retains the resource until explicit removal. Its failed/completed execution survives engine retirement, but an explicit relaunch replaces the current activation and its result. This is not legacy spawn --retain=SECS screen retention, and does not promise retained PTY output after its engine is gone.

Generated arrangements obey the wire size/depth limits. SQLite also rejects any record or receipt that its bounded loader cannot read back; this can impose a tighter nesting bound than the wire. A store refusal commits no partial records and starts no process. Retiring a carried screen during hot handoff does not purge a durable execution result.

Lost replies and retry scope

There is no automatic effect replay in this client module. Keep the original request and key. After timeout, transport failure or an unreadable mutation reply, the original outcome is unknown. Read its receipt first:

phux terminal operation --session "$session" --idempotency-key "$key"

This lookup never launches or edits anything. A retained identical creation can also be explicitly replayed with the same key and identical payload; it returns the original resource and entry without running effects. Reusing the key for another payload is refused. Prefer lookup when recovering a lost reply: it does not depend on remembering every original default.

Receipts belong to the authority’s organization store and survive reconnects and server restarts, not moving to another authority. They are bounded by 24 hours or the oldest-first 10,000-receipt cap, whichever removes them first. A short elapsed time alone cannot establish retention. After removal or expiry, lookup reports receipt_not_retained with outcome unknown. This does not distinguish never received from received then evicted. A later request that reuses the same idempotency key after the receipt has been pruned is executed again: it is not a replay and it is not refused as a duplicate. Reconcile records and external effects; never blindly replay with either the old key or a fresh one. There is no exactly-once guarantee beyond retained receipts. Receipt environment values are stored as unsalted SHA-256 hashes (sha256: plus hex), not plaintext. The hash is not a secret: a low-entropy value is guessable, and the same value hashes the same way in every receipt, so receipts can be compared. Receipts written before hashing keep their plaintext until they are pruned. Replacing or deleting a receipt does not scrub SQLite WAL or free pages. Replaying an old-format receipt (plaintext environment still stored) conflicts: the operation id is reused with a payload that no longer compares equal.

The MCP adapter also has its separate in-memory mutation ledger. It forwards the creation key to the server, but its pending operation receipt and phux_operation_status do not become durable. phux_terminal_operation queries the server, not that adapter ledger.

Discovery, authorization and events

terminal describe asks the connected server for supported public workflows, complete nested input/output schemas, completion meanings and retry retention. It does not enumerate new server methods from an old client’s compiled catalog. These schemas derive from the same public DTOs used by CLI, MCP and Rust callers. phux --capabilities --json continues to describe the installed binary; it is not connected-server discovery.

Supported is not authorized. Every invocation still crosses authenticated server dispatch. Placed creation requires CREATE and ORGANIZE on the destination session. Lookup requires INVENTORY there and does not disclose another session’s receipt. No new listener, generic invocation dispatcher, local-only bypass, input lease exception or approval bypass is introduced.

Organization lifecycle events follow committed state. Their journal is bounded; resume gaps require a fresh level-state read, not an assumption of lossless ordering across disconnects. A creation receipt is not an agent turn, a shell-command completion, or a replacement for process/lifecycle observation.

Compatibility and migration

CREATE_PLACEMENT gates the additive placement field and richer receipt. API_DESCRIPTION gates description and read-only receipt lookup. Clients refuse before sending when the corresponding server support is absent; new complete-intention calls never silently downgrade to spawn-plus-rollback. Old clients keep their existing wire requests and ignore new receipt fields. The durable-result policy is persisted on the resource, not inferred from an engine-local flag or a receipt that may expire, so it survives hot handoff and cold restart. Persisted organization command, record and receipt additions are optional serde fields, so old organization records remain readable; no new database table is needed. Recorded launch directories are now honored literally by the organization executor: if one disappears before a create/restore/relaunch, that activation fails instead of executing in an unrelated fallback directory. Legacy raw spawn retains its separate compatibility behavior.

Empty-session directory seeding uses atomic placement on capable servers. Its older-server path remains explicitly isolated. Existing spawn, launch, raw input, shell run, agent prompt and agent wait retain their separate contracts. In particular, satellite routing, named legacy projections, timed screen retention and integration provenance are not silently redefined as this local durable workflow. Use the new workflow for new integrations; those legacy adapters can migrate only when their distinct semantics are preserved and tested. OSC 7501 program status and conversation-delivered agent mail are unchanged.