# Culture

Native C/Swift/Kotlin and browser WASM integration is documented in [Culture bindings](culture-bindings.md).

The canonical platform-neutral frontend contract lives in
[`.agents/skills/culture-frontend/`](../.agents/skills/culture-frontend/SKILL.md).
Every Android, Apple, web, terminal, or future frontend should keep a checked
conformance manifest against that skill's machine-readable requirement inventory.

Culture is Paratenic's native, keyboard-first multi-host client. The name follows the biological metaphor: a culture is where many agents can remain observable and interactive, while each Paratenic host maintains the life cycle of its own sessions.

## Architecture

Culture is a separate binary from the `paratenic` host. It is assembled from UI-independent workspace crates:

- `paratenic-client-core` — host-qualified replicas, loss fencing, subscription leases, bounded caches, command receipts, the session index, and bounded Feed/input-needed projections;
- `paratenic-client-model` — browser-safe identity, catalogue/Feed/input/completion behavior and shared Unicode geometry;
- `paratenic-client-ws` — canonical configuration plus authenticated `ws://` / `wss://` transport factories;
- `paratenic-client-runtime` — shared asynchronous native observation/control runtime, with the FFI's blocking compatibility adapter;
- `paratenic-ui-model` — serializable tabs, recursive splits, pane types, validated transitions and typed web DTOs;
- `paratenic-client-wasm` — browser execution of shared pure behavior, not an AHP client;
- `paratenic-web` — hosted native application, bounded HTTP/WebSocket presentation and embedded production assets;
- `paratenic-ag-ui` — a disposable AHP-to-AG-UI projection;
- `paratenic-a2ui` — a validated A2UI v0.9.1 surface store using client-owned component catalogues; and
- `culture` — Ratatui rendering, VT terminal emulation, keyboard handling, and the headless command server.

AHP is always authoritative. AG-UI projections can be discarded and rebuilt from AHP snapshots. A2UI components can produce typed intents, but cannot settle input, tools, or sessions themselves.

The host exposes its bounded semantic Feed as the virtual resource `virtual://paratenic/feed`. Clients use standard `resourceRead` plus one shared `createResourceWatch` result (`ahp-resource-watch:/feed`) per configured host. A session-sharded incremental index reduces semantic actions as they arrive and hard-caps every index and persistence path at 1,024 rows; `resourceRead` clones only that cap under the host lock and serializes after releasing it. The watch never scans host history, subscribes to a session/chat, hydrates a catalogue row, or wakes a dormant runtime. `paratenic-client-core` validates and interleaves these immutable host snapshots, fences configured host identity, and uses paginated session summaries plus already-resident replicas only when an older host does not expose the resource. Watch loss is recovered by rereading the bounded snapshot, whose `from_seq` and `replay_floor` retain explicit provenance.

Feed rows optionally carry `is_final_agent_message`: the final substantive Markdown response of a successfully completed turn, not user input, streaming text or commentary followed by a tool/reasoning/input part. Host live events and completed-history projection use the same rule; completion updates the existing event identity. Missing metadata from older hosts defaults to false rather than guessing completion. Android's **Agent replies** filter selects those rows. Category selections are inclusive and persistent; **All** clears the selection. Filtering/search preserves explicit live-follow state, while manual scrolling or Pause controls it independently. Hidden events are not marked read merely because a filtered view is live.

On Android, the workspace **View options** button is available on phones and tablets. It exposes side-by-side or stacked splits, focused-pane presentation, and the same theme/text controls as Settings. Recursive splits preserve the core tab, pane, axis and ratio identities; divider commits target the exact split path rather than whichever pane is focused. Phone landscape keeps compact navigation. **UI scale** (90%, 100%, 115%) is separate from text size, preserves system font scaling, and compensates minimum touch targets; appearance can be reset without changing workspace or Feed state.

## Install and run

From a checkout:

```bash
nix run .#culture
# or build and atomically install into the updater-owned location:
nix develop .#web  # pinned native/WASM/Node build tools, not runtime dependencies
just install culture
just install paratenic
just install all
```

The Just installer defaults to `~/.local/bin` and accepts an `INSTALL_DIR`
override. This is the same default used by `updatable-cli`, so subsequent
On Apple Silicon macOS, `culture update` also selects the matching
`Culture-<version>-macos-aarch64.app.tar.gz`, verifies its published SHA-256,
rejects archive links/path traversal or an unexpected bundle ID/version, and
atomically replaces `~/Applications/Culture.app` with rollback. `--check` and
`--status` report separate `cli` and `macosApp` objects. Other platforms leave
the app outcome explicitly unsupported and retain the existing CLI behavior.

`culture update` and `paratenic update` calls continue updating the installed
binaries without a Nix system switch.

Release archives contain a separately updateable `culture` binary:

```bash
culture update --check
culture update
# On Apple Silicon macOS this updates both ~/.local/bin/culture and
# ~/Applications/Culture.app from the same checksum-verified release.
```

## Browser workspace

Run `culture web` for the bundled browser frontend; `culture web --bind 0.0.0.0:7789` deliberately exposes trusted-network HTTP without an application password. Existing `hosts:` and protected credentials stay in native Rust. Web state/control sockets are separate from the TUI, and duplicate browser views share native subscriptions rather than starting another client runtime. The distribution needs no Node, checkout or hidden TUI at runtime.

See [Culture Web](culture-web.md) for configuration, origin/perimeter security, independent user services, developer workflows and the current qualification status. Closing a view or stopping observation is not an agent Stop command.

## Native macOS app

`Culture.app` is a native SwiftUI/AppKit frontend over the Rust client core. The
left source rail is populated directly by the Rust FFI host/session catalogue;
the right conversation stage subscribes through the same host-qualified
replicas and command router used by the TUI.

```bash
just macos-test
just macos-install
open ~/Applications/Culture.app
```

The installed bundle includes a real `CultureWidgets.appex`. The installer
registers it with `pluginkit` and fails if discovery does not converge. Ad-hoc
signing remains the hermetic CI default; local/device or distribution proof may
select an available Apple identity without embedding credentials:

```bash
CULTURE_CODESIGN_IDENTITY="Apple Development: …" just macos-install
```

The containing app and extension are signed with the same identity and app-group
entitlement. The app alone publishes an owner-private, atomic, 128 KiB bounded
snapshot into `group.com.harryaskham.culture`; the widget owns no FFI library,
AHP connection, subscription, token, or wake authority.

On Apple Silicon Darwin, `nix build .#culture-app` produces a signed bundle at `result/Applications/Culture.app`. Adding that package to `home.packages` installs it through Home Manager's application bundle aggregator, so the app advances with the locked Paratenic flake input rather than a mutable checkout.

The app uses bundle identifier `com.harryaskham.culture.macos`; sibling Apple
clients should use the same `com.harryaskham.culture.*` family.

### iPhone and Apple Watch onboarding

`culture mobile-bundle` prints every configured host as a JSON onboarding
bundle; `--include-tokens` adds each host's bearer read from its `token_file`.
On a Mac, `culture mobile-bundle --include-tokens | pbcopy` and **Paste from
clipboard** in Culture for iPhone (Settings → Add or import nodes) connects the
whole fleet without typing a token. Pasting the plain desktop YAML also works:
hosts sharing one token file (for example every `pi-daemon/daemon-main.token`
host) ask for that token once. The bundle is the same shape the Android Wear
bootstrap carries.

The iPhone sends every host, bearer and relay to Apple Watch automatically
whenever the configuration changes (WatchConnectivity, stored in the Watch
Keychain).

### `culture relay`

watchOS forbids app-level sockets on a real Watch (Apple TN3135), so a Watch can
neither speak AHP WebSockets nor join a tailnet itself. `culture relay` runs the
native runtime on a host that already reaches every node and serves its bounded
JSON commands over plain HTTP: `POST /v1/command` (snapshot, subscribe,
channel snapshots and revisions, UI intents, receipts, completions, media and
text reads; authority-changing commands are refused) and a revision long-poll
`GET /v1/revision?after=N&timeoutMs=T`. Responses are gzip-compressed and carry
`x-culture-revision`. Tailscale Funnel strips its mount path, so the root routes
serve it; `--base-path /culture-relay` also mounts them for proxies that keep it.

```sh
culture relay                      # 127.0.0.1:7797, bearer ~/.config/culture/relay.token (created 0600)
tailscale funnel --bg --https=443 --set-path=/culture-relay http://127.0.0.1:7797
culture mobile-bundle --include-tokens \
  --relay-url https://host.tailnet.ts.net/culture-relay \
  --relay-token-file ~/.config/culture/relay.token | pbcopy
```

Requests need `Authorization: Bearer <token>` (compared in constant time). The
relay keeps its own state directory (`<state_dir>/relay`), and releases leases
whose channels no client touched for ten minutes, so a sleeping Watch never pins
subscriptions. Snapshot requests may carry a view
(`{"catalogue":false,"feedLimit":48,"feedDetail":false,"sessionsPerHost":16}`)
and channel snapshots `{"turns":40}`; the Watch's view of a 57-node fleet is
about 40 kB on the wire.

As a service, import `homeManagerModules.culture-relay` (or the NixOS /
nix-darwin `culture-relay` module) and set `services.culture.relay.enable`;
`bind`, `configFile`, `tokenFile` and `basePath` mirror the CLI. Publishing it
(Funnel, Serve or another TLS proxy) stays the operator's choice.

### Canonical Apple experience

The production macOS app is the closest behavioral reference for future iPhone,
iPad, and watchOS clients: all share the **Agents**, first-class interleaved
**Feed**, actionable **Inbox**, **Settings/Nodes**, and agent-workspace hierarchy,
plus one typed path from immutable Rust snapshots through native presentation and
back through intents, receipts, and authoritative echo. It is not a desktop
layout or Liquid Glass skin to shrink onto other devices. The normative
[Apple platform profile](../.agents/skills/culture-frontend/references/APPLE-PLATFORMS.md)
specifies:

- macOS recursive panes, multiwindow and complete keyboard opening; iPad
  `NavigationSplitView`, multiwindow, drag/drop, keyboard and pointer; iPhone
  compact destination tabs plus workspace navigation stacks; and watchOS
  Recent/Inbox/Feed glances, Digital Crown scrolling, bounded
  dictation/choices, and exact iPhone handoff;
- responsive size-class preservation, Dynamic Type and VoiceOver, semantic
  themes/materials, one-line newest-first Feed ordering and follow state, and a
  20-visual-line initial viewport for nested terminal/code/diff/JSON/tool output;
- exact host-qualified deep links, provider provenance, cached/offline/error
  behavior, scene restoration, stable automation identifiers, privacy/redaction,
  explicit memory/subscription caps, and the release screenshot matrix; and
- WidgetKit/App Intents/Shortcuts/Handoff surfaces with behavioral Android App
  Widget/Shortcut/Quick Settings counterparts, all using shared authority and no
  background Feed subscription fanout.

A visible Culture client shares at most one bounded semantic Feed resource/watch
per configured AHP host; it never fans out across session/chat channels or wakes
dormant Pi-Daemon routes. Paginated session activity is an explicitly
lower-fidelity fallback, while widgets and background glances read only bounded
redacted cache and own no AHP connection.

Shared Rust/client-core owns AHP reduction, identity, pagination, Feed and Inbox
projections, command receipts, reconnect fencing, caches and subscription leases.
Apple platform code owns native navigation, rendering, accessibility, secure
system integration, and scene lifecycle. It must not implement another reducer,
Feed index, or command ledger. This documentation defines future Apple clients;
it does not start their implementation.

Input requests are reduced once in `paratenic-client-core` into a forward-compatible presentation model for text, number, integer, boolean, single-select, multi-select, confirmation, authentication, client execution, and unknown requests. Native bridges carry that model alongside the untouched official AHP request. TUI, Android, and macOS render the same semantics, while every answer still returns through `build_input_action` and the authoritative AHP action identity.

The macOS source rail keeps locally persisted pinned agents in a collapsible top section. Grouped-by-node mode also retains pinned agents in their node, caps each expanded node at 20 agents after the selected recent/status/name sort until Show all is selected, and displays relative update age for idle/cached agents. Flat All Agents mode removes pinned duplicates and uses semantic status colors instead of node colors.

The macOS composer activates a typed command surface for `/` and a distinct shell surface for `!`. Suggestions come from the owning Pi runtime's bounded command/executable catalogue through standard AHP completions; shell submissions execute on that runtime, never on the Culture client or another host. Model, effort, and context occupy a separate compact strip below the balanced input surface. Recursive pane layout, split ratios, focused pane, per-pane selected agent/mode/inspector state, and sidebar visibility use per-window scene storage and restore independently across relaunches. `Command-B` toggles the sidebar; native hidden-titlebar content reserves titlebar clearance only for the traffic lights inside the rail, leaving the pane header usable across the rest of the window. Below 700 points the sidebar collapses before content can clip, and the app remains usable down to its 560×480 compact minimum. `Shift-Command-P` opens a searchable command-and-agent palette, `Command-O` opens an agent directly into the focused pane, and `Shift-Command-N` opens the separate creation sheet.

The app includes:

- native multi-host agent catalogue with persisted node-grouped or flat views,
  recent/status/name ordering, search, cached/offline state, and provider-aware
  agent creation;
- AHP-native queued and steering messages while a turn is active;
- inline chat-local AHP input forms with authoritative submit/echo state, while
  retaining the aggregate inbox for cross-agent triage;
- rich Markdown transcript that preserves authored operational line breaks,
  blank separation, lists, headings, links, and fenced code, plus reasoning,
  complete tool-state cards, send/abort, details, changes, and unknown protocol fallback;
- configurable AHP tool presentation (`raw`, `terminal`, `diff`, and `code`) with
  terminal-style shell commands/ANSI output and structured file-edit diffs;
- independent native windows with shared Rust transport/reducer authority and
  per-window agent selection, detail mode, composer, and search state;
- Rust VT-emulated terminal frames plus native keyboard input;
- compact AHP-advertised model/context/effort controls above a full-width
  composer, omitting unavailable or unknowable values;
- native compact sidebar search, toolbar, menus, Settings, inspector, window lifecycle,
  accessibility semantics, reduced motion, and macOS 26 Liquid Glass with an
  older-system material fallback;
- owner-private JSON command server for state inspection, semantic navigation,
  screenshots, fixture automation, bounded diagnostics, and cleanup;
- an owner-only rotating operational log at `~/Library/Logs/Culture/Culture.log`
  containing lifecycle, latency, size/count, memory-pressure, mode, and
  content-free error-category records for post-hoc hang diagnosis;
- bounded revision polling so full FFI snapshots are decoded only when Rust
  state changes, plus hard UI caps on transcript tails and JSON/text inspection
  so pathological state cannot create unbounded AppKit selection/layout work; and
- deterministic fixture screenshots and real `.app` build/sign/install tests.

## Configuration

Culture uses `configurable-cli`, including standard path, validation, schema, export, and atomic owner-only initialization commands:

```bash
culture config path
culture config init
culture config show
culture config validate
culture config schema
```

The default path is `~/.config/culture/config.yaml` (or `$XDG_CONFIG_HOME/culture/config.yaml`). A minimal multi-host configuration is:

```yaml
state_dir: ~/.local/state/culture
command_socket: ~/.local/state/culture/control.sock
channel_cache_bytes: 536870912 # 512 MiB, allocated as needed
chat_tail_turns: 200 # advisory recent context; 0 requests full retained state
default_host: local
tui:
  editor_mode: normal # or vim
  keybindings: {}     # optional per-action overrides
web:
  bind: 127.0.0.1:7789
  base_path: /
  theme: system
hosts:
  - id: local
    label: This Mac
    url: ws://127.0.0.1:7788/ahp

  - id: helsinki
    label: Helsinki
    url: wss://agents.example.net/ahp
    token_file: ~/.config/culture/helsinki.token
    subscription_buffer: 8192
    request_timeout_seconds: 60
    max_message_bytes: 134217728 # 128 MiB complete AHP messages/frames
    # Used by the native new-session form:
    default_working_directory: file:///home/me/project
    auxiliary_channels:
      - ahp-otlp:/logs/info
```

Host IDs are stable routing identities. The same AHP URI on two hosts names two different resources. Bearer files are reread on every connection, allowing rotation without restarting Culture, and must be owner-only on Unix. Plaintext WebSockets away from loopback are rejected unless explicitly acknowledged with `allow_plaintext_non_loopback: true`.

Explicit chat snapshot hints may be shaped by serialized bytes as well as turn count. Without a hint, AHP snapshots return all retained channel turns. Native receive frames/messages default to 128 MiB and can be configured larger; existing explicit cache and timeout settings still apply. Recent messages and the active turn remain intact, and archived history remains pageable. A missing snapshot is not an empty conversation. See [snapshot windowing and recovery](chat-snapshot-windowing.md) for the heavy-history failure diagnosis and guarantees.

Client IDs and unresolved action receipts persist beneath `state_dir`. A caller-generated request ID is allocated once per host. A repeated accepted intent returns the original receipt; pending or indeterminate delivery is never blindly replayed.

## Creating agents

Every Culture surface creates an agent the same way, against any AHP host that implements `resolveSessionConfig` and `createSession` (Paratenic providers and Pi-Daemon profiles alike):

1. **Host** — the selected catalogue host, the focused agent's host, or `default_host`.
2. **Agent** — one of the host's advertised `RootState.agents` (Pi-Daemon profiles, Paratenic providers).
3. **Directory** — a host path, not a URI. It defaults to `default_working_directory`, then the host's `defaultDirectory`, then the host's resolved schema default. Culture sends it as the `file://` `workingDirectory`; when the schema names the directory itself (`cwd`, `workingDirectory`), that property is edited by this row instead of appearing twice.
4. **Configuration** — the host's `resolveSessionConfig` schema rendered as fields: enums and booleans as pickers, numbers and text as inputs, defaults and resolved values pre-filled. Every change re-resolves, and the host's resolved values replace the form's.

Creation is enabled only for a resolved, valid form. Culture creates the session with a client-chosen URI, reports the host's acceptance or error in the form, and opens the new agent when it is ready.

In the TUI, `↑`/`↓`/`Tab` move between rows, `←`/`→` (or `h`/`l`, `Space`) choose on pickers, text rows edit in place, `Enter` creates and `Esc` cancels.
The TUI exposes the form as `new_session` in its control-socket snapshot. The macOS command server opens the sheet with `new_agent` and drives the same resolve-then-create path with `create_agent` (`hostId`, `provider`, `directory`, `config`).

## Interaction model

The terminal composition is **technical, tactile, and calm**: a dense collapsible culture rail beside one dominant working canvas, restrained acid/cyan status accents, square working surfaces, and live signal dots as the recurring motif. The catalogue starts collapsed so a host with thousands of retained sessions does not overwhelm the workspace; `/` searches all sessions and resources without requiring branches to be expanded first.

| Key | Action |
| --- | --- |
| `j` / `k`, `↓` / `↑` | Navigate the focused catalogue, inbox, or document |
| `h` / `l`, `←` / `→` | Collapse/select the parent culture or expand/open the selected row |
| `/` | Filter all host-qualified sessions; `Enter` keeps the filter and `Esc` clears it |
| `Enter` / `Space` | Expand a culture, open a rich agent/terminal, or activate the focused control |
| `n` | Open the new-agent form (see [Creating agents](#creating-agents)) |
| `Tab` / `Shift-Tab` | Move focus through the culture rail and panes |
| `|` | Duplicate the focused view in a side-by-side split |
| `_` | Duplicate the focused view in a top-bottom split |
| `=` | Balance split proportions |
| `q` | Close the focused pane; hide the culture rail when it owns focus |
| `Ctrl-T` | Open a new workspace tab |
| `t c` | Close the active tab |
| `t n` / `t p` | Select the next / previous tab |
| `i` | Compose a prompt in a chat/agent pane |
| `Ctrl-Y` | Send from the composer |
| `Esc` | Leave Vim insert mode, then cancel the active editor/layer |
| `g i` | Open the cross-host input-needed inbox |
| `g d` | Open live diagnostics |
| `g t` | Open the default host's first advertised terminal |
| `g r` | Open the default host's AHP root inspector |
| `\` | Toggle the culture rail |
| `Alt-T` | Move focus between chat and an embedded terminal |
| `Alt-P` | Cycle terminal placement: inline, docked, floating, tabbed |
| `?` / `Ctrl-P` | Toggle the command palette |
| `Alt-Enter` | Queue the composed message for after the active turn |
| `Ctrl-R` | Restore the newest host-rejected message into the composer to resend it |
| `Shift-Q` / `Ctrl-C` | Exit cleanly |

Mouse capture is enabled in interactive mode. A left click expands cultures, opens rich agent/terminal views, selects tabs, and focuses panes; the wheel scrolls whichever catalogue or pane is under the pointer; middle-click closes a tab. Hold the terminal emulator's mouse-bypass modifier (commonly Shift) for native text selection.

Every command-palette action and navigation shortcut is resolved from the same typed action registry. Override individual actions without replacing the defaults for unrelated actions:

```yaml
tui:
  editor_mode: vim
  keybindings:
    split_horizontal: ["ctrl+\\"]
    split_vertical: ["ctrl+-"]
    close_pane: ["q"]
    command_palette: ["ctrl+p"]
```

Bindings accept one stroke or a two-stroke chord such as `t c` or `g i`. Duplicate bindings, ambiguous prefix/action combinations, unknown actions, and malformed strokes fail configuration validation. An empty list explicitly unbinds an action. The command palette displays the effective configured bindings rather than maintaining a separate shortcut list.

Vim editor mode starts each search, form, and composer in Normal mode. `i`/`a` enter Insert mode; `h`, `l`, `w`, `b`, `0`, `$`, and `x` edit in Normal mode; `Esc` returns to Normal mode before closing the layer. `Ctrl-Y` submits a composer. Normal editor mode retains direct typing and Enter-to-submit behavior.

A message sent while the chat has a turn in progress, or while Culture's own `chat/turnStarted` still awaits its echo, goes as `chat/pendingMessageSet`: as steering by default, which the host hands to the agent at its next point between agent messages, or queued for after the turn with `Alt-Enter` (Culture for macOS: the send button's menu). Hosts such as pi-daemon reject `chat/turnStarted` on a busy chat. A message stays `DELIVERY UNCONFIRMED` until the host echoes it with Culture's own origin (`clientId` and `clientSeq`). An action the host rejects shows `REJECTED` with the host's reason, and `Ctrl-R` restores it into the composer; sending it again replaces the rejected original.

Bracketed paste is handled as one bounded input event in search, forms, composers, and terminals instead of being mistaken for rapid shortcuts. Terminal focus sends characters and standard cursor/edit sequences directly through `terminal/input`; global escape chords remain available. Terminal output is parsed by an independent VT emulator per terminal and published to panes as immutable frames.

## Headless command server

The TUI and automation share the same `UiIntent` type. Run without claiming a terminal:

```bash
culture run --headless
```

Use the built-in client to send one JSON-lines request:

```bash
culture control '{"command":"snapshot"}'
culture control '{"command":"action","action":"open_inbox"}'
culture control '{"command":"channel_snapshot","host_id":"local","channel":"ahp-chat:/…"}'
culture control '{"command":"intent","request_id":"open-inbox-1","intent":{"type":"open_pane","title":"Input needed","kind":{"type":"input_inbox"}}}'
```

The socket also exposes the complete pinned AHP command and action surfaces. For requests containing authentication material, pipe JSON with `--request-file -` or use an owner-only file; Culture refuses inline `authenticate` requests because argv is observable:

```json
{"command":"ahp_request","host_id":"local","method":"listSessions","params":{"channel":"ahp-root://"}}
```

```json
{"command":"ahp_dispatch","request_id":"terminal-resize-1","host_id":"local","channel":"ahp-terminal:/…","action":{"type":"terminal/resized","cols":120,"rows":40}}
```

The Unix socket and its parent are owner-private. Frames are bounded to 1 MiB. Every request receives exactly one response line. `snapshot` includes host/session state, workspace layout, aggregate input count, and render/reducer/control telemetry, allowing a test driver to observe an action before issuing the next one.

## Scriptable session inventory and text send

In shipped Culture distributions, `culture list` and `culture send` use the same native Culture runtime, AHP routes and durable command ledger as the frontends. Developer-only builds enable these commands explicitly with `--features session-cli`; default SDK/native embedding builds do not link the runtime or include these command variants. The bundled `web` distribution feature includes `session-cli`. Each short-lived CLI invocation owns a separate owner-private `cli/` state namespace and an exclusive ledger lock; it does not connect to or replace the Web/TUI command socket. Host IDs are exact configured `hosts[].id` values, not a machine-name lookup or an inferred session owner. Only selected hosts are passed to the short-lived runtime; filtering does not connect to unrelated configured hosts.

```sh
culture list --node helsinki,aurora --status idle,input-needed --jsonl
culture list --node helsinki --jsonl | jq -r '.target' |
  while IFS= read -r target; do
    culture send --target "$target" --text-file ./message.txt
  done
# Or address one session directly (default chat resolved from its fresh session state):
culture send --node helsinki --session 'ahp-session:/…' --text 'continue'
```

`list` pages the complete official session catalogue for each selected host, emits one JSON row per session (`target`, host, session, status, status bits and generation), and fails rather than showing a partial catalogue if a host is unavailable, changes generation, exceeds the bound or repeats a cursor. `waiting` is an alias for `input-needed`, **not** idle. The opaque target encodes exact host/session identity; the displayed connection generation is diagnostic, not a durable cross-process fence. Every send independently subscribes to fresh live session/chat state before dispatch. `send` resolves the session's current advertised default chat, or accepts an explicit `--chat` within that session, and refuses a stale or already-active chat instead of silently queueing. For private text, use `--text-file -` to read bounded UTF-8 from stdin. `--request-id` reuses a stable caller identity; omitted IDs are generated. Do not automatically retry a pending/indeterminate result.

`send` prints its command receipt as JSON. Exit success means an authoritative AHP acceptance, **not** that Pi started, finished or consumed the message. A rejected, pending or indeterminate receipt prints the original identity and exits nonzero. Text beginning with `/` is submitted literally. In particular, Pi's built-in `/compact` is a TUI/RPC operation, not an AHP chat command: `culture send --text '/compact'` does **not** certify compaction. This interface deliberately does not expose arbitrary remote Pi command execution; scripts requiring real compaction must use a separately supported, correlated backend operation and await its completion before sending `continue`.

## Performance and correctness model

- Every host has an independent transport, authentication, sequence, reconnect, and event task.
- Large catalogues publish each bounded `listSessions` page into the shared index immediately; only a completed traversal replaces the authoritative host catalogue and disk cache. One slow or failed later page therefore cannot hide earlier sessions on Android, macOS, or the TUI.
- A transport-level notification tap preserves progress, OTLP telemetry, and auxiliary channels the pinned SDK does not yet reduce; explicit tap lag marks the host stale and forces reconnect/snapshot recovery instead of silently skipping frames.
- Every channel lives in an independent RCU cell. Panes read immutable `Arc` snapshots without a global transcript lock.
- Render invalidations pass through a one-slot coalescing channel and are capped at one frame per 16 ms.
- Opening duplicate panes increments a local lease; it does not create another wire subscription.
- Input aggregation is recomputed only after session input actions or session snapshots, not for every streamed token.
- Terminal emulators are sharded per terminal. Parsing one busy PTY does not lock another terminal or the workspace.
- Inactive channel state is evicted by a byte-bounded LRU while active/visible channels remain pinned.
- Transport lag, replay-floor misses, and generation changes make replicas stale and non-actionable until an authoritative snapshot replaces them.

The test suite covers host-qualified URI collisions, stale fencing, duplicate-pane leases, persistent action idempotency, input routing, bounded memory, 100-host/1,000-session search, 20-pane frame latency, ANSI terminal fidelity, command-socket lifecycle, and a headless connection against a real Paratenic AHP server. A real pseudo-terminal test also drives SGR mouse clicks/wheel input, keyboard search and singleton navigation, resize, semantic snapshot inspection, Ctrl-C, alternate-screen/mouse cleanup, and socket teardown through the shipped binary.

On the managed aarch64 macOS development node, optimized synthetic checks measured:

- **20 visible panes:** 0.80 ms average render time (target: under 16 ms);
- **100 hosts / 1,000 sessions:** 0.48 ms average filtered search (target: under 16 ms).

These are regression gates rather than marketing benchmarks: live diagnostics also retain render/reducer latency histograms, reconnect counts, control failures, and unresolved actions so slowdowns and correctness failures are observable in the running client.
