paratenic

Plate IV · Architecture

Inside the host.

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

Inside one Paratenic process Three listeners face outward: the AHP WebSocket at /ahp for clients, plain HTTP for content-free /healthz and aggregate /status plus bearer-protected /sessions and /providers inventory, and the owner-private Unix backend socket for runtimes. Client actions enter the single action log, which assigns one host-wide serverSeq and keeps a bounded replay window. Backend frames resolve through a route index to the exact session cell, which owns its reducer gate, command correlation and replay floor. Sessions feed a bounded semantic Feed index and are checkpointed independently to the owner-only state directory. AHP clients probes, tools runtimes WebSocket /ahpbearer when configured HTTP/healthz · /status/sessions · /providers backend socketowner-private Unix action logone serverSeq4,096-action replay route indexURI → backend cell session cellsreducer gatecommands · replay floor feed index≤ 1,024 rows checkpointsper session, coalesced ONE PARATENIC PROCESS dispatch frames commit,then fan out
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

Three identities, three jobs Session identity: a backend's stable sourceId is hashed with the host's persisted host_id to give the session URI, and the session's primary chat URI follows from it; neither leaks a path or runtime id. Incarnation identity: each adapter start sends a fresh instanceId, which every command carries and which fences older incarnations. Client identity: a logical clientId survives transport replacement, and its clientSeq makes every dispatch idempotent. sourceIdfrom the backend host_idpersisted by the host hash ahp-session:/…stable, opaque ahp-chat:/…primary chat instanceIdone per adapter start current incarnationolder ones fenced clientId · clientSeqlogical client, per dispatch one envelopea repeat returns the original carried by every command dedupe a newer hello for the same source wins no backend runs a command twice
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

One sequence for every action A serverSeq ruler from 18201 to 18207 with three lanes: client A, client B and the runtime. Client A's dispatch is accepted at 18201; runtime deltas take 18202 and 18203; client B's invalid dispatch is rejected yet still ordered at 18204; the runtime's tool start takes 18205. Client A repeats its dispatch with the same clientSeq: it receives the original 18201 envelope and nothing runs again. The turn settles at 18206. client A client B runtime serverSeq dispatch (A, 7) 18201 text_delta 18202 text_delta 18203 invalid: rejected 18204 tool_started 18205 settled 18206 repeat (A, 7) returns the 18201 envelope · runs nothing
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

Every outcome a client command can have A dispatched command is validated. If invalid or stale it is rejected: echoed with a rejection reason at its own serverSeq, and nothing runs. Otherwise it is reduced, made durable and echoed as accepted, then routed to the one backend that owns the session. The runtime accepts it, refuses it — which becomes a chat error with the draft kept — or the outcome is indeterminate because a response was lost after delivery; indeterminate is never replayed. An accepted command later settles as completed, cancelled or failed. dispatched valid? acceptedserverSeq · durable routedto the owner runtime accepted settledcompleted, cancelledor failed rejectedecho + reason · nothing runs runtime refusedchat error · draft kept indeterminatereconcile · never replay no yes events response lost
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

Replay, or a fresh snapshot A serverSeq ruler. The host keeps a bounded window of recent actions — up to 4,096, and at most 32 MiB for the root and 1 MiB per session — ending at the current head. A reconnecting client whose last seen sequence is inside the window receives the missing actions. One whose last seen sequence is older than the window's floor receives a fresh snapshot at a new fromSeq. After a host restart the window is empty, so every client takes a snapshot. REPLAY WINDOW · ≤ 4,096 ACTIONS floor head client X replays the gap client Y takes a fresh snapshot After a host restart the window is empty: every client snapshots, and root catalogue changes are re-read with listSessions.
Fig. IV.5The window is process memory, not transcript storage. Transcripts live in per-session checkpoints.

Fencing

A stale process cannot take a command

A newer incarnation fences the old one Adapter A is registered for a source. Adapter B starts for the same source and sends hello with a new instanceId; Paratenic registers it at the same session URI and tells A it has been replaced. A client's next command carries B's instanceId and goes to B. A late result from A is ignored, and any admission A left unresolved is marked indeterminate rather than retried. A registered for source s hello · source s · instanceId a21 registered · the same URI2 backend_replaced3 dispatch4 command · backendInstanceId a25 late command_result6 ignored; A’s stranded admissionbecomes indeterminate7 adapter AinstanceId a1adapter AinstanceId a1 ParatenichostParatenichost adapter BinstanceId a2adapter BinstanceId a2 CultureclientCultureclient
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

LimitValueWhen it is reached
AHP message, backend frame1 MiBrefused; the frame is not truncated
WebSocket connections1,024new connections refused
logical clients4,096new clients refused
backend connections1,024new runtimes refused
sessions per host10,000registration refused
chats per session256createChat refused
pending questions6416 questions and 64 options a request; 64 KiB of text
replay window4,096actions; also 32 MiB root and 1 MiB a session — older clients snapshot
feed index1,024rows; oldest leave first
initial history100turns 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

Who may listen where Listener admission. A loopback bind is allowed, with a bearer token optional. A non-loopback bind requires --token-file to name an existing regular owner-only file holding one token line; otherwise it is refused. It then also requires --allow-plaintext-non-loopback, an explicit statement that an owner-controlled TLS proxy protects it; otherwise it is refused. When both are present the listener starts, and the bearer is required on the WebSocket upgrade and on /sessions and /providers. --bind loopback? allowedtoken optional token file?regular · owner-only · 1 line refused TLS proxy flag? refused allowedbearer on /ahp,/sessions, /providers yes no no yes no yes you supply TLS in front
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.