Reference

Detail that the README deliberately leaves out. For the command and flag listing, see cli.md; for the behavioural rules an agent should follow, see the agent skill.

Input leases

Every HTTP input and signal identifies its actor. Non-human actors acquire a lease before acting.

LeaseWho it is forWhat it guarantees
sharedseveral agents each submitting complete chunksthe runtime serializes each request, so bytes from two requests never interleave
exclusiveone automated actor across a multi-step interactionno other automated actor may write until it is released, expires, or a human preempts it
nonea personinput is always accepted, and immediately revokes a non-human exclusive lease

A human may also take an exclusive lease for a longer manual takeover. Leases use monotonic TTL deadlines, can be renewed, and disappear on release, expiry or preemption. Browser input from the stock page is identified as human:local-ui, so opening the window and typing preempts an agent without any further ceremony.

Each chunk is authorised under the manager lock, then written under that session’s writer lock. A blocked PTY write does not hold the manager lock or prevent reading, closing, or coordinating other sessions. HTTP writes run on a blocking task rather than a Tokio worker. The stock UI also keeps processing events and actions for other sessions while an input write is pending. A later lease change does not cancel a chunk whose write was already authorised.

Secret input mode

While a session is in secret input mode it publishes nothing derived from the typed bytes:

Every non-human actor is refused input, signals and leases for the duration.

Any actor may enter secret mode — raising the shield is the safe direction. Only a human actor may leave it, and leaving waits for the session to fall quiet so the echo of what was just typed is not released as publication resumes. Entry and exit are both events, and the stock page marks the state unmistakably.

After a secret window closes, the screen starts blank and contains only new output. Application cursor, bracketed paste, and alternate screen modes are preserved. Ask the program to redraw (for example, Ctrl-L) for a full picture.

Secret input boundaries cannot move across a chunk that is still being written. Starting or ending a window in that interval returns retryable input_in_progress; retry after the write completes. This keeps already authorised input outside a newly opened window and keeps private input volume private even when a write fails.

Composing with an input method

The field an IME composes into sits on the cursor cell, and is shown while a composition is in progress: the terminal’s own font, underlined, sized to the text being composed with wide characters counted as two columns. It returns to being invisible when the composition commits.

Two things follow from that, and both are the reason it is done this way. The half-typed word appears where it is being typed rather than nowhere, because the browser keeps the composing text in that field’s value. And a candidate window follows the cursor, because an input method anchors it to the focused field’s caret — with the field parked in a corner, so was the candidate list.

A commit sends the composed text as one write. The first keystroke after a commit is ordinary input and is sent as itself; on a Chinese keyboard that is usually the space between two words.

HTTP API

api serve --mode local (the default) binds 127.0.0.1. --mode lan binds all interfaces for a trusted IPv4 network and publishes this machine’s LAN address. There is no TLS and no Internet exposure built in.

Public for discovery: /health, /openapi.json, and the standalone JSON Schemas under /schemas/. Everything under /v1 requires Authorization: Bearer ... and never accepts a credential in a query string.

Callers can:

On Windows, only kill is supported: it terminates the process started for the session, rather than the foreground process group. interrupt and terminate return signal_not_supported.

Domain responses are AFDATA envelopes and pass through AFDATA redaction on the way out.

The SSE stream retains the newest 512 terminal events. With no Last-Event-ID it sends that backlog and then live events; with a valid cursor still inside the replay window it sends only events after that cursor. An unsigned decimal cursor equal to the first retained sequence minus one is still replayable.

An invalid cursor, one older than that boundary, or one ahead of the latest global sequence first receives a named replay_gap event:

event: replay_gap
data: {"type":"replay_gap","reason":"last_event_id_too_old","last_event_id":20,"oldest_seq":100,"latest_seq":611}

The notice has no SSE id; ordinary terminal events keep their existing seq, session_id, and event payload and sequence-based ids. The other reasons are invalid_last_event_id and last_event_id_ahead. last_event_id is null for an invalid cursor, and oldest_seq is null when no events are retained. The bounds come from the same subscription snapshot. After the notice the server sends the entire retained backlog and then live events. Listen for replay_gap and reread the session list and screens to recover the current state; the retained events cannot reconstruct the missing history.

A session’s exit is announced by at most one process_exited event. Its exit_code is null when the code could not be collected in time; a later status query reports the code, and no second event corrects the first. On Windows, a session that exits on its own and is closed without having been queried in between produces no process_exited event.

The OpenAPI document is committed and drift-tested against the code:

afterminal api export --directory openapi --force

Delivery

Delivery is hosted by Agent-First UI, which owns the browser process and disposable profile for window, the outer page and attention policy for link, and the registry entry for session. The upstream UI credential stays private inside the CLI. --port always names the bearer-protected API, never the delivery.

For link, AFUI applies its global attention.idle_timeout_s and attention.grace_period_s policy, including the warning and the renew action. The terminal page’s once-per-second refresh only keeps its own session list current; it does not choose the URL’s lifetime.

In attach mode (ui --api-url), the running API issues a separate private UI capability. The CLI keeps that capability alive behind the delivery and revokes it when the delivery ends; the server-side idle timeout on it is crash cleanup, not the person-facing lifetime. ui --api-url resolves no frontend locally — the machine serving that runtime decides which page it serves.

Override contract

ui_api_version is 3. A frontend may supply templates/page.html.j2, style.css and assets/**, each independently; a file it does not supply is afterminal’s.

Two things are not the frontend’s:

A frontend afterminal cannot load ends the command with ui_frontend_unusable and opens no window; it is never a quietly substituted built-in page. AFUI_SAFE_MODE=1 ignores every override.

Scope

The runtime has no agent-first-ui dependency and does not know application or task-completion semantics. Higher-level controllers decide what terminal output means.