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.
| Intention | CLI | MCP | Rust |
|---|---|---|---|
| Describe this server | phux terminal describe | phux_api_describe | phux_client::terminal::describe |
| Create, place and start | phux terminal create --request-json JSON | phux_terminal_create | phux_client::terminal::create |
| Recover a receipt, without effects | phux terminal operation --session KEY --idempotency-key HEX32 | phux_terminal_operation | phux_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.arrangementandplacement.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;failedis 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, andschema_version(1). On the JSON receipt,execution.stateandexecution.outcomeaccept an unknown value, and wireErrorCodealready decodes an unrecognized code asUnknown, so naming a new JSON state, outcome, or error code does not bumpschema_version. That rule is JSON-only. A#[serde(other)]Unknowndoes not keep the original token: it re-serializes asunknown. The binarywire_enum!/enum_bytepath 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.