One process per user and node. It owns identity, order, replay and fencing for every session it carries, and nothing about how those sessions think.
Map
Three doors, one sequence
Fig. IV.1Clients, probes and runtimes enter through separate listeners. Every client action passes the one action log; every backend frame resolves to one session cell.
The AHP surface uses the official Rust ahp-types, ahp and ahp-ws crates, pinned together at 0.8.0, so state and reducers are the protocol’s own rather than a parallel model. Session cells are sharded: a slow file sync or a busy session delays only itself, and the global lock holds nothing longer than a route lookup.
Identity
Who a session is, which process speaks for it, and who asked
Fig. IV.2A copied session file cannot collide with its original: the URI depends on the source the backend reports and the host it reports to.
Opaque by design. Paths, runtime UUIDs and secrets never appear in AHP summaries; clients see only ahp-session:/ and ahp-chat:/ URIs.
Aliases. A backend may declare earlier source identities while it migrates; the URI does not change, and conflicting aliases are refused.
Logical clients. A clientId outlives any one WebSocket. A replacement transport fences the older one, and its clientSeq continues.
Order
Every action gets one place in line
Fig. IV.3Rejected actions are still ordered. Repeated ones are not re-ordered at all.
Reduce, then publish. The official reducer runs before an accepted action is broadcast, so every client applies exactly what the host applied.
Echo with origin. A client’s action comes back with its clientId and clientSeq; a rejected one carries a rejectionReason and changes nothing.
Backend actions have no origin. Text deltas, tool calls and settlements come from the runtime and are ordered the same way.
Coalesced, never dropped. Adjacent text and reasoning deltas merge within 8 ms or 64 KiB; lifecycle, tool, input and settlement events keep their order.
Outcomes
Accepted is not done, and unknown is not refused
Fig. IV.4Three different answers: the host accepted it, the runtime accepted it, the turn settled. Indeterminate is a fourth, and it is never retried for you.
Before a turn reaches a runtime, Paratenic records and reduces the client’s action durably and only then releases a reserved command slot. If the connection drops after delivery but before the runtime answers, nobody can know whether the turn ran, so the command is marked indeterminate: the client sees exactly that, and replaying it is a human decision. Answers to questions follow a stricter rule still — a completion is published only after the runtime resolves it.
Replay
Reconnect without losing your place
Fig. IV.5The window is process memory, not transcript storage. Transcripts live in per-session checkpoints.
Fencing
A stale process cannot take a command
Fig. IV.6Commands name the incarnation they are for. A replaced one cannot accept them, and its loose ends become indeterminate rather than retried.
The same rule applies to a whole provider connection: a newer connector registration for the same provider id fences the previous one and every session route it carried, which is why independent connectors need distinct ids.
Bounds
Every queue has a size
Limit
Value
When it is reached
AHP message, backend frame
1 MiB
refused; the frame is not truncated
WebSocket connections
1,024
new connections refused
logical clients
4,096
new clients refused
backend connections
1,024
new runtimes refused
sessions per host
10,000
registration refused
chats per session
256
createChat refused
pending questions
64
16 questions and 64 options a request; 64 KiB of text
replay window
4,096
actions; also 32 MiB root and 1 MiB a session — older clients snapshot
feed index
1,024
rows; oldest leave first
initial history
100
turns per registration; older pages load on demand
A client whose outbound queue fills is disconnected on its own. It reconnects, replays or snapshots, and nobody else waits for it.
Persistence
Small index, independent checkpoints
State lives in an owner-only directory, ~/.local/state/paratenic by default: a compact global index plus one checkpoint per session, written in the background and coalesced. A durable control change — a new chat, a provider, a terminal claim — waits for its own sequence-fenced save receipt before it is acknowledged, without holding the host lock while the disk catches up.
The host identity in that directory is what keeps URIs stable across restarts. Treat it as sensitive: it holds synchronised conversation content.
Admission
Who may listen where
Fig. IV.7Paratenic never claims that a bearer token over open plaintext is secure. Prefer loopback and an authenticated tunnel.
Token files, not tokens. The token is read from a file whose resolved target must be a regular, owner-only file — managed-secret symlinks are fine. It never appears in arguments, URLs, logs or generated configuration.
What stays open./healthz carries no content and /status only aggregates, so probes work without the token; session titles and backend identities need it.
Feed
One bounded stream of what happened
The host publishes a semantic feed as the virtual resource virtual://paratenic/feed. A client reads it once and holds one watch per host; the watch never scans history, subscribes to a chat or wakes a dormant runtime. An incremental, session-sharded index reduces actions as they arrive and keeps at most 1,024 rows. Rows can mark a turn’s final reply, which is how Culture’s Agent replies filter works.