# Paratenic live-backend protocol

Paratenic accepts live runtime adapters over an owner-private Unix stream socket.
The protocol is neutral: it describes hosted agent sessions and launch-capable providers and contains no Cacophony or pi-daemon types.

A dedicated session connection owns exactly one logical session.
A version 8 provider connection can instead register and route many logical sessions over that one physical connection.
A provider connection also advertises one or more providers and admits creation of new sessions.
Frames are UTF-8 JSON objects delimited by LF and are limited to 16 MiB including the delimiter. An oversized frame is refused loudly (handshake error or a logged connection close), never silently skipped. The host imposes no session, connection, chat, input-request or logical-client count limits beyond those AHP defines (none).
Backend protocol versions `1` through `14` are accepted for session connections.
Version 2 adds explicitly advertised elicitation support.
Version 3 adds provider registration, authenticated claims for client-created AHP sessions, multi-chat routing, and paged history.
Version 4 adds explicit tool-confirmation, result-confirmation, and client-tool arbitration.
Version 5 adds constrained MCP side-channel registration and verbatim JSON-RPC forwarding.
Version 6 adds official-state auxiliary resources, complete AHP action projection, provider authentication, progress, and OTLP telemetry.
Version 7 adds stable source aliases and explicit full/read-only chat interactivity for policy-backed inventory sessions.
Version 8 adds multiplexed `register_session` / `unregister_session` routes on a provider connection so large dormant catalogues consume metadata rather than one socket and task per session.
Version 9 adds explicit indeterminate command outcomes and content-free upstream source identity/readiness receipts; older backends retain their negotiated version and existing frame semantics.
Version 10 adds the managed invocation command/event union, including source observation cuts, original-scope recovery and scoped Stop. Managed invocation advertisement requires version 10; a multiplexed session cannot upgrade beyond its provider connection's accepted version. This host also supports the bounded retained-content transport and official rich transcript records introduced by the portable-content v10 adapter work; older v10 hosts must not be assumed to support rich source replay.
Version 11 adds source-identity-checked turn reconciliation and independent source-history cursors; portable-image producers require this version rather than inferring support from an older v10 host. See [the portable-content contract](portable-images-implementation.md#native-source-turn-reconciliation-backend-version-11).
Version 12 adds ordered source snapshot batches with durable accepted-prefix acknowledgement.
Version 13 adds `source_turn_resources`: a complete source snapshot serialized into an ordered list of immutable `text/plain` content resources. Its envelope carries `sourceId`, `active`, `historyOnly`, and `resources`. The host validates every resource's session ownership and digest, checks the envelope against the decoded snapshot, and applies the original single-snapshot identity/placement/managed-input and durable-commit rules. No partial turn is published and no source entry or response part is dropped. Decoding is bounded only by an operator-configured session content quota, if one is set; the wire frame limit is 16 MiB. The 512 KiB producer page target is only a packing target, never a whole-turn validity limit. Older hosts retain the ordinary small-snapshot path; a large snapshot requiring resource transport explicitly requires an upgraded host and remains in the native replay outbox until then.
Version 14 adds the owning backend's sequenced `profile_image_changed` set/clear event for the bounded `paratenic.identity` session metadata convention. See [agent identity images](agent-identity-images.md) for ownership, byte/pixel/type limits, durable acknowledgement, clear/reconnect semantics and client projection. Older hosts never receive this event.
A version 1 backend remains fully supported but cannot send input events or receive input-completion commands.
The registration response echoes the accepted version.

## Sequenced publication and logical failure isolation

`sequenced_event` carries `session`, optional `chat`, `sequence` (starting at 1),
`event`, and optional `backendConnectionId`. The ID is the **logical** ID returned
in that session's `registered` / `session_registered.registration` receipt, not
the physical provider's ID. A gap returns `event_resync_required` with the
expected sequence; an already accepted sequence can be acknowledged again only
while that exact logical registration remains current.

For version-10+ multiplexed providers, semantic rejection returns
`event_rejected` and retires **only that logical backend route**. It does not
close the provider or retire any other session. The failed epoch remains fenced:
repeating either the rejected sequence or an older sequence is rejected, never
ACKed. The host does not advance past or silently drop the failed event.
Unsequenced `event` / `chat_event` cannot bypass this fence; their refusal has
`sequence:0`, which is not a valid sequenced publication ID. Dedicated session
connections may still close after a semantic rejection.

Recovery is explicit: pause only the failed session's uploader, fail its receipt
waiters, stop/rotate its old observer, preserve its authoritative native cut,
then `register_session` again on the same provider. Only a successful registration
resets that session to sequence 1. Re-registration requires `backendConnectionId`
on every sequenced frame, even if the first epoch used legacy omission. Discard
old queued envelopes rather than tagging them with the new ID. An old ID returns
`event_epoch_mismatch`; an omitted replacement ID returns `event_epoch_required`.
These validation refusals do not retire the new route. Replaying actual native
state is still necessary; registration is not proof that rejected content was
published. A per-provider 10,000-epoch registry bounds transport metadata.

ACK, resync and rejection receipts echo optional `backendConnectionId`.
Successful receipts and semantic failures identify the actual logical generation;
a stale-generation validation refusal echoes the supplied old ID. An omitted ID
after replacement is unattributable and its refusal carries no generation. Key
receipt waiters by logical generation/session/sequence and ignore old-generation
receipts. Neither an unattributable validation error nor an old epoch's reply is
authority to retire a newer epoch. Legacy peers may omit the echo entirely.

## Registration

A session connection's first frame must be `hello`:

```json
{
  "type": "hello",
  "protocolVersion": 8,
  "instanceId": "opaque-process-incarnation",
  "sourceId": "opaque-stable-conversation-identity",
  "sourceAliases": ["optional-prior-stable-identity"],
  "provider": "pi",
  "displayName": "Pi",
  "title": "Session title",
  "cwd": "/absolute/local/path",
  "sessionId": "optional-local-runtime-id",
  "model": { "provider": "provider-id", "id": "model-id" },
  "thinkingLevel": "medium",
  "runtimeIdle": true,
  "interactivity": "full",
  "inputCapabilities": {
    "questionKinds": ["single_select"],
    "freeform": true,
    "drafts": false,
    "maxQuestions": 1,
    "maxOptions": 9
  },
  "chatCapabilities": {
    "create": true,
    "dispose": true,
    "fork": true,
    "sideChat": true,
    "pagedHistory": true
  },
  "toolCapabilities": {
    "confirmations": true,
    "resultConfirmations": true,
    "clientTools": true
  },
  "mcpServers": [
    {
      "id": "workspace-tools",
      "uri": "https://example.invalid/mcp",
      "name": "Workspace Tools",
      "capabilities": {
        "serverTools": true,
        "toolsListChanged": true,
        "serverResources": false,
        "resourcesListChanged": false,
        "logging": false,
        "sampling": false
      }
    }
  ],
  "historyNextCursor": "opaque-older-page-cursor",
  "history": [
    {
      "id": "stable-turn-id",
      "startedAt": "2026-08-28T10:00:00.000Z",
      "userText": "Earlier question",
      "responseText": "Earlier answer",
      "reasoningText": "",
      "cancelled": false
    }
  ]
}
```

`sourceId` is stable for one conversation and must not be a copied runtime UUID by itself.
`instanceId` changes whenever the adapter process/session incarnation changes.
A direct native Pi adapter sets `runtimeIdle` only when Pi's public `ctx.isIdle()` is true and `ctx.hasPendingMessages()` is false. This excludes active runs and queued continuations; Pi currently exposes no separate in-flight input-preflight receipt, so operators must not treat this field as proof that a particular input was handled. When a different native-Pi `instanceId` replaces a prior generation while `runtimeIdle` is true, Paratenic terminally reconciles only an exact empty active turn as `backend_admission_indeterminate`. It preserves queued/uncertain inputs without replay, records both backend instance identities, and leaves same-instance reconnects, nonempty turns, non-Pi providers, and replacements reporting active work untouched.
Paratenic combines its persisted host identity with `sourceId` to derive opaque AHP session and chat URIs.
Local paths and runtime IDs are not projected into AHP summaries.
An adapter may include up to 100 bounded completed history turns so a newly registered conversation starts with useful transcript state instead of an empty chat.

A successful registration returns:

```json
{
  "type": "registered",
  "protocolVersion": 8,
  "hostId": "019...",
  "session": "ahp-session:/...",
  "chat": "ahp-chat:/...",
  "backendConnectionId": "019..."
}
```

A newer connection for the same `sourceId` fences the previous incarnation.
The old connection receives a terminal `backend_replaced` error when delivery is still possible.

`registered` may also carry two optional source-resume fields:

- `sourceContinuity`: unchanged for as long as this host keeps the session's durable source history; it changes if host state or the session is recreated. A backend may keep its own acknowledged-source ledger across reconnects only while this value is unchanged.
- `sourceCursor`: the ID of the last closed turn the host holds for the session's primary chat. Source turns are committed in order, so every earlier turn is held too. A backend with no ledger for the current host state, after its own restart or a continuity change, skips its turns up to and including this one and exports only the rest. A cursor the backend's history does not contain means the whole history is exported.

### Provider registration and session creation

A launch-capable adapter uses backend protocol version 3 and opens a separate provider connection:

```json
{
  "type": "provider_hello",
  "protocolVersion": 8,
  "instanceId": "opaque-provider-manager-incarnation",
  "providers": [
    {
      "id": "pi-managed",
      "displayName": "Managed Pi",
      "description": "Creates managed Pi sessions",
      "multipleChats": {
        "create": true,
        "dispose": true,
        "fork": true,
        "sideChat": true,
        "pagedHistory": true
      }
    }
  ]
}
```

Paratenic returns `provider_registered` with the host ID, backend connection ID, and accepted provider IDs.
A newer registration fences the previous provider-manager connection and every logical session route carried by it.
Only provider IDs with a live provider route accept AHP `createSession`.
Independent connectors must use distinct stable provider IDs; a different PID, service name, API endpoint, or disjoint session list does not create a different provider namespace.

A version 9 `provider_hello` may also include a bounded, nonsecret upstream identity:

```json
{ "source": { "serviceId": "stable-upstream-service", "instanceId": "current-upstream-incarnation" } }
```

This identity is separate from the adapter's own `instanceId` and never grants additional authority.
A version 9 provider reports current readiness with `{"type":"provider_status","ready":true}` or `false`.
Paratenic timestamps that receipt on receipt using a monotonic clock and never restores readiness from disk.
The owner-authenticated `/providers?provider=<id>` HTTP surface returns bounded current connection/source IDs, protocol version, readiness and receipt age, plus a completeness flag and observation timestamp.
Missing, stale or disconnected receipts are not proof of readiness; `/status` remains aggregate health without identities or session content.

A version 8 provider registers a logical session on the same connection with a correlated frame:

```json
{
  "type": "register_session",
  "requestId": "019...",
  "hello": {
    "protocolVersion": 8,
    "instanceId": "opaque-session-incarnation",
    "sourceId": "stable-source",
    "provider": "pi-managed",
    "displayName": "Managed Pi",
    "title": "Dormant session",
    "cwd": "/workspace"
  }
}
```

Paratenic replies with `session_registered`, preserving the ordinary `registered` payload plus the request ID.
Commands continue to use the ordinary `command` envelope, including the target session and backend incarnation, on the shared connection.
Direct Pi backends may advertise a bounded `completionItems` catalogue with opaque `/` command and `!` executable values, labels, and descriptions. Paratenic exposes those through ordinary AHP `completionTriggerCharacters`, `terminalCommandPrefix`, and `completions`. A `chat/turnStarted` beginning with `!` is routed as a `shell` backend command to the owning Pi runtime; Paratenic never executes that command on a different host.

Events keep their ordinary session/chat-qualified shape.
`command_result` is correlated exactly as for a dedicated connection.

### Ingress concurrency boundary

Backend socket readers admit a session event through a separately locked, bounded URI-to-route index. Each immutable route cell binds the exact backend connection and transport generation to the hosted session reducer gate. Re-registration atomically replaces that cell; queued work carries the old cell identity and is rejected as `backend_replaced` before reduction instead of mutating the replacement session. Route lookup and multiplexed transport validation do not acquire the delivery/root registry, and waiting on one session reducer never holds the route index or another session reducer.

The global AHP action sequencer and bounded reconnect log remain one independently locked `SharedActionLog`; there is no second server-sequence authority. Concurrent session commits pass the PR 128 ordered fanout gate around Feed fact projection, sequence/replay commit, subscriber resolution, and cancellation-safe outbound publication. A later session therefore cannot publish sequence N+1 before sequence N, and a publication failure forces subscriber snapshot recovery. Reconnect and persistence take coherent action-log snapshots with one monotonic global order.

Each hosted session cell now owns its backend command correlation, auxiliary changeset and annotation reducers, bounded local replay floor/window, and monotonic checkpoint revision. A narrow command-ID route index locates the owning cell for transport settlement; settlement takes the same reducer gate, so it cannot race input or tool-resolution events while correlation state is checked out. Reducers publish bounded deltas into the ordered delivery/root path, then network sends and checkpoint filesystem writes occur after session and root locks are released. Logical clients/subscriptions, Feed/root summaries, terminal, resource-watch, MCP, and provider control state remain root-scoped; they do not serialize reducer preparation for unrelated sessions.

Durable provider, client, terminal, chat, and session control mutations capture their exact persistence snapshot while holding the root registry, then release `HostState` before awaiting the sequence-fenced full-save receipt. A slow session fence, file sync, atomic rename, or directory sync may delay that command's accepted response, but cannot pin `/status`, `/sessions`, an unrelated backend registration, or another session reducer. Duplicate client dispatches re-establish durability before replaying an accepted envelope after an earlier indeterminate save. If a newer session checkpoint or tombstone is requested after a full save captures its state, the full save releases that session fence and waits for the covering local revision to become durable; a failed covering write fails the full barrier rather than acknowledging requested-but-unwritten bytes. `/status` exposes only bounded persistence diagnostics—active full-save stage/age/root bytes and aggregate pending/active checkpoint counts—without session URIs or transcript content.

#### Bounded HTTP inspection

`GET /status` and authenticated `GET /sessions` return their existing complete JSON schema on success. `/sessions` retains ID sorting and bearer authentication before inspection; `/status` retains its existing access policy. Counts keep their existing definitions: connected sessions are hosted cells with a backend connection, not a count of backend transports, and pending backend commands count the existing root map. `persistence: null` means persistence is disabled.

These HTTP projections do not wait for the registry, hosted cells, action-log metadata, or enabled persistence-diagnostic locks. A shared, nonqueued one-request permit covers collection, sorting and response encoding. Contention yields HTTP 503 with `{"error":"inspection_busy","retryable":true}`. Poisoned/unavailable sampling yields `inspection_unavailable` with `retryable:false`; exceeded snapshot bounds yield `inspection_limit` with `retryable:false`. The latter is not a suggestion to retry immediately. No partial list, fabricated zeros, or partial healthy status is returned. No background waiter or retry task is started.

The projection has no row-count limit. Copied session metadata UTF-8 and the encoded JSON (including escaping) are each bounded by the 16 MiB wire-message limit. Exactly-at-limit data is accepted when its complete encoding fits; above-limit state fails explicitly with `inspection_limit`. Sorting/encoding occur after releasing the registry; the permit is released only after bounded response construction, not after slow client consumption. Thus each response body is bounded, rather than accumulating unbounded inspection jobs.

Successful inspection is observational, not a transaction across atomics, storage and session reducers. Registry membership is held stable during its short scan, but individual cells and persistence diagnostics are sampled separately. `observedAt` is not a persistence or queue-drain acknowledgement. `/healthz` remains basic liveness. AHP `session_summaries()` and pagination contracts are unchanged and are not claimed to have these nonblocking inspection guarantees.

A provider removes a route with correlated `unregister_session`; this detaches the runtime route without interpreting AHP lifecycle ownership.
Registration and unregistration failures are correlated and do not tear down unrelated multiplexed sessions.


Paratenic sends a correlated provider command after validating and durably reserving the client-chosen session URI:

```json
{
  "type": "provider_command",
  "commandId": "019...",
  "backendInstanceId": "opaque-provider-manager-incarnation",
  "provider": "pi-managed",
  "command": {
    "type": "create_session",
    "session": "ahp-session:/client-chosen-id",
    "creationToken": "one-time-secret",
    "workingDirectories": ["file:///workspace"],
    "config": {}
  }
}
```

The provider manager must durably admit or reject the command with `command_result` before launching the runtime.
After acceptance, the new runtime opens its ordinary session connection and includes the exact `requestedSession` and `creationToken` in `hello`.
Paratenic compares the token in constant time, validates the provider, consumes the pending creation, preserves the client-chosen session URI, and emits `session/ready`.
The token is never exposed on AHP.
Unsolicited, mismatched, or stale claims cannot assign a new resource.
If the original registration committed but its acknowledgement was lost, the exact already-bound source/provider may reconnect to that same requested URI; this is ordinary source re-registration, not a second use of the token to claim another resource.
If an admitted launch fails before registration, the provider sends `session_creation_failed` with the session URI, one-time token, and bounded error message; Paratenic publishes the official `session/creationFailed` action.

For a session created through that provider connection, AHP `disposeSession` sends a correlated provider `dispose_session` command containing the AHP session URI and stable backend `sourceId`.
Paratenic removes the session, its subscriptions, and its live session-backend connection only after the provider accepts disposal; rejection leaves the session intact.

Provider registration deliberately does not grant a session connection permission to spawn another runtime.
Ordinary attached Pi sessions remain registration-only and retain their single live `AgentSession` ownership.

## Commands

Paratenic sends one of these command payloads inside a correlated envelope:

```json
{
  "type": "command",
  "commandId": "019...",
  "backendInstanceId": "opaque-process-incarnation",
  "session": "ahp-session:/...",
  "chat": "ahp-chat:/...",
  "command": { "type": "prompt", "text": "..." }
}
```

Command types are:

- version 3 `create_chat` with the client-chosen chat URI, optional correlated initial turn/text, optional fork or side-chat source, and working-directory subset;
- version 3 `dispose_chat` with the chat URI;
- version 3 `fetch_turns` with the chat URI and opaque cursor;
- version 4 `confirm_tool` and `confirm_tool_result` with stable turn/tool IDs and the complete decision;
- version 4 `client_tool_content` and `complete_client_tool`, carrying official AHP content/result values as neutral JSON after ownership validation;
- version 6 `apply_resource_action`, carrying an official action for a session or chat after host-side capability and reducer validation;
- version 6 `invoke_changeset_operation`, carrying a validated operation ID, resource, and optional target;
- version 6 `authenticate`, carrying a bounded token only to the backend that owns the advertised or dynamically discovered protected resource;
- `prompt` with `text` and, additively, the originating AHP `turnId`;
- `steer` with `text` and, additively, the AHP pending-message `id`;
- `follow_up` with `text` and, additively, the AHP pending-message `id`;
- `configure` with optional `model: { provider, id }` and `thinkingLevel`;
- version 2 `complete_input` with `requestId`, `response`, and stable-ID-keyed `answers`; and
- `abort`.

The adapter validates `backendInstanceId`, `session`, and the optional version-3 `chat` target before acting.
Protocol 1 and 2 session commands omit `chat` and always target the deterministic primary chat.
It executes a given `commandId` at most once for the process incarnation and returns the same result if that ID is repeated.

```json
{
  "type": "command_result",
  "commandId": "019...",
  "accepted": true
}
```

A negative result may include a bounded `error` string.
Acceptance says the runtime admitted the command; later events establish execution and terminal settlement.
Pi 0.84.4's public extension API exposes `sendUserMessage()` as a void operation and does not report whether the input pipeline continued, transformed, asynchronously delayed, or returned `handled`. The direct native adapter therefore cannot truthfully derive a source-consumption receipt from a timer or FIFO lifecycle event: `before_agent_start` has no source command identity and may occur after unrelated interactive input. Native prompt `command_result` remains an enqueue acknowledgement rather than proof that the model started.

A versioned, opt-in cooperative input owner can acquire a one-shot reporter through the shared `paratenic:acquire-prompt-outcome:v1` extension event synchronously at its `input` handler entry. The reporter capability closes over the producer's exact command, backend instance, registration and async context; it is not derived from text, order or elapsed time. If that same owner actually returns `handled`, it invokes the reporter as its final fallible action. The adapter then sends:

```json
{
  "type": "command_handled",
  "commandId": "019...",
  "backendInstanceId": "opaque-process-incarnation",
  "session": "ahp-session:/...",
  "reason": "recipient-verification-filter"
}
```

Paratenic accepts this frame only for protocol version 2+, the current exact backend instance/session and a retained command-to-turn identity. It terminally marks only the matching empty active turn as `backend_input_handled`. `agent_start` retires the reporter identity before any late callback can settle live work. The capability is one-shot and dies with registration/shutdown; unowned or noncooperating inputs provide no handled receipt and remain subject to replacement reconciliation. This opt-in does not advertise queue/Stop support or authorize replay.

Replacement reconciliation below terminally marks a stranded prior admission as indeterminate; it never relabels that uncertainty as rejection or authorizes replay.
Version 9 has a separate outcome for a delivered command whose admission or execution cannot be proved:

```json
{ "type": "command_indeterminate", "commandId": "019...", "error": "reconcile the original request" }
```

There is deliberately no `accepted` field on this frame.
It must not be encoded as `accepted:false` or described as rejection before execution.
Paratenic emits a correlated `backend_indeterminate` chat error or an RPC error with `data.outcome: indeterminate`, without replaying the originating client action.
Uncertain creation keeps its reserved URI and one-time claim; uncertain disposal keeps the session.
Input/tool arbitration still waits for the runtime's authoritative resolution rather than treating uncertainty as an answer.

For ordinary turns, Paratenic durably records and reduces the originating AHP action before releasing a reserved command-queue slot to the backend. Reviewed session configuration and MCP lifecycle requests instead retain a durable unresolved sequence marker before dispatch: configuration changes reduce only after definitive acceptance against their reviewed baseline, while MCP lifecycle acceptance is state-neutral.
An elicitation completion is instead reserved and delivered first; it is reduced as completed only after the runtime accepts arbitration and emits its authoritative `input_resolved` event.
A definitively rejected start-turn command is completed with an AHP chat error.
Loss after command delivery but before the result is indeterminate and is never treated as safe implicit replay.

## Events

Legacy/default-chat events use the registered AHP session URI:

```json
{
  "type": "event",
  "session": "ahp-session:/...",
  "event": { "type": "text_delta", "text": "..." }
}
```

Protocol version 3 uses `chat_event` with both `session` and `chat` for events belonging to an explicitly addressed chat.
`create_chat` is acknowledged before the runtime emits events for that chat; `dispose_chat` is acknowledged only after the runtime has stopped producing them.
When `create_chat` carries `initialTurnId` and `initialText`, Paratenic has already reduced that initial turn and the runtime must not echo a second `user_message`.
For `fetch_turns`, the runtime sends `turns_loaded` before its accepting `command_result`, ensuring the AHP `fetchTurns` response never overtakes the required `chat/turnsLoaded` action.
Unqualified `event` frames always address the deterministic primary chat and are not redirected by the session's mutable `defaultChat` UI hint.

Supported events are:

- `user_message` with `text` and optional `turnId`;
- `agent_started`;
- `text_delta` or `thinking_delta` with `text`;
- `tool_started` with `toolCallId`, `toolName`, and serialized `input`;
- version 4 `tool_confirmation_requested` with final input, presentation text, editability, and stable confirmation options;
- `tool_completed` with `toolCallId`, `toolName`, `output`, and `isError`;
- version 4 `tool_result_confirmation_requested` with the completed result;
- version 4 `tool_confirmation_resolved` and `tool_result_confirmation_resolved` for local/runtime arbitration, optionally correlated to the winning backend command ID;
- version 4 `client_tool_requested`, which selects a matching active AHP client by stable client/tool identity and creates the standard `toolClientExecution` aggregate;
- `config_changed` with optional `model: { provider, id }` and `thinkingLevel`;
- `usage` with optional `inputTokens`, `outputTokens`, `cacheReadTokens`, and `model`;
- `settled` with `cancelled` and optional `error`; and
- `title_changed` with `title`;
- version 2 `input_requested` with one complete bounded neutral request;
- version 2 `input_updated` with a complete replacement for a still-pending request;
- version 2 `input_snapshot` with the complete bounded pending-request set after registration/reconnect. Optional `available` defaults to `true`; `false` marks the session's source input authority unavailable and does **not** replace or resolve its retained requests. The host publishes `paratenic.inputAvailable` through summary metadata and `session/metaChanged`, and rejects answers until a fresh available snapshot. This is a source-session availability fence, not a backend cancellation acknowledgement; and
- version 2 `input_resolved` with the runtime-authoritative resolution, answers, source, and optional winning backend `commandId`; and
- version 3 `turns_loaded` with an oldest-first bounded history page and the next opaque cursor;
- version 6 `resource_snapshot` with a confined changeset or annotations resource, kind, and official state value; and
- version 6 `resource_action` with an official AHP action for the owning session, chat, changeset, or annotations channel.

Protocol-v6 provider connections may send `root_action` containing only official `root/configChanged` values.
They may send typed `notification` frames for `root/progress` and advertised `otlp/exportLogs`, `otlp/exportTraces`, or `otlp/exportMetrics` channels.
Session or provider connections may send `auth/required` only for channels and providers they own.
Paratenic checks the method, official payload shape, duplicated channel field, ownership, capability advertisement, and subscription before relay.

Ordinary directly attached Pi sessions retain the neutral `pi` provider identity; a provider connection's sessions carry that provider's own identity, so clients never relabel one as the other.

`sourceAliases` allows a backend to adopt a prior source identity during migration without changing the AHP session URI.
Aliases are bounded per declaration, unique, and rejected when they resolve to conflicting sessions.
Accepted aliases and prior canonical sources are retained in the owning session's checkpoint, so a fast managed-source registration after restart resolves to the same AHP session before its inventory identity arrives.
Restoring a legacy whole-host snapshot carries its alias index into those session-local checkpoints; removing a session also removes its alias routes.

Input capability is explicit.
A backend may send input events only after registering protocol version 2 with `inputCapabilities`, and Paratenic sends `complete_input` only to that backend incarnation.
Question and option IDs are stable opaque identities; labels and array indices are never response identity.
Paratenic projects these neutral frames into AHP 0.8 `chat/input*` actions and `SessionState.inputNeeded` using the official reducers.

A request with an explicit `turnId` must target the active turn; its response part and input-needed entry are retired when that turn settles (including through a native source snapshot).
An omitted `turnId` means the request is runtime-owned, not implicitly bound to whichever turn is streaming.
These requests use the standard session input-needed queue without creating a synthetic user/assistant turn or attaching a form to unrelated transcript history.
They can be posted or replayed while idle, coexist with later native turns, and remain pending until `input_resolved` or an available `input_snapshot` removes them.
A completion routes by the exact session/chat/request identity in that queue, regardless of the active turn.
Its correlated `chat/inputCompleted` receipt is published only after runtime resolution, followed by removal from the session queue; a client that has no matching active-turn form may reduce the chat receipt as a no-op.

Session and root catalogue status reflect the independent input-needed queue as well as chat activity.
Native source history reconstruction must not clear `InputNeeded` for pending requests, and draining the last request restores the actual chat activity (including idle).
Unavailable input snapshots, backend replacement/disconnection and exact command/client-sequence fences still prevent unsafe completion; no Culture stale-input guard is bypassed.

The backend may send `ping`; Paratenic answers `pong`.
Protocol, authority, frame, session, and validation failures return an `error` frame or close the connection.

## MCP lifecycle request outcomes

Official `session/mcpServerStartRequested` and `session/mcpServerStopRequested` actions target an advertised, unambiguous MCP customization in a ready session. Backends need protocol6+ resource-action support. Disabled start is rejected; stopping an already-running disabled service remains available for cleanup. The host forwards the original action via `apply_resource_action` but does not apply its optimistic lifecycle reducer.

A definitive backend acceptance emits a correlated state-neutral action:

```json
{"type":"paratenic/mcpLifecycleAccepted","id":"server-id","operation":"start"}
```

The envelope retains the original AHP client identity/sequence and is queryable through `paratenic/dispatchOutcome`. `operation` is `start` or `stop`. This extension acknowledges the request only; it is not an observed process transition. Official clients preserve unknown actions without reducing their state. Only an explicit backend `session/mcpServerStateChanged` changes the reported lifecycle, including when that event arrives before the acknowledgement. Authentication/input arbitration is not automatically settled by this acceptance.

Definitive refusal emits the original rejected action and leaves observed server state unchanged. Indeterminate/lost responses retain uncertainty and block duplicate original-sequence dispatch; no deadline or reconnection invents a safe retry. The pending same-server guard and existing128global/32per-client unresolved-session-action limits bound admission. For disk compatibility the generalized non-executable marker set keeps its previous serialized `unresolvedConfigurations` key: older hosts already suppress reuse of those sequences regardless of action family.

This host boundary is qualified with model-free AHP/backend framing and checkpoint reconstruction, not live Pi MCP execution or OS-process restart. Culture Web lifecycle controls, protected-resource authentication and native side-channel UI remain separate frontend work.

## Protected-resource authentication review

`authenticate` remains an official **root-channel** command. Static provider protected resources, a currently auth-required MCP customization (including nested plugin/directory entries), and an active auth-required MCP tool call can identify a credential destination. A legacy request without a review is accepted only when there is one unambiguous advertised target; it never chooses a first matching provider/session or distributes the token to multiple targets. Legacy lookup is bounded to8192visited entries; explicit target lookup avoids unrelated session scans.

New native clients first call the read-only `paratenic/authContext` extension with `channel: "ahp-root://"`, the exact protected `resource`, and a typed `target`:

- `{"type":"provider","provider":"…"}`
- `{"type":"mcp_server","session":"ahp-session:/…","serverId":"…"}`
- `{"type":"tool_call","session":"ahp-session:/…","chat":"ahp-chat:/…","turnId":"…","toolCallId":"…"}`

The response contains `review` and `required_scopes` (null, empty and nonempty are distinct). Put that exact non-secret review under `_meta.paratenicAuth` in the later official `authenticate` request. Its proof binds host identity, logical backend registration, target and complete challenge metadata within a64KiBencoded bound. It is a change-detection value, **not a credential, signature or authorization grant**. The host recomputes it before nonblocking queue admission, retaining the target session lock through that admission. Changed issuer, scopes, metadata, registration, turn or target is refused before delivery. Older hosts that do not implement the context extension must not receive a guessed scoped credential request.

The backend `authenticate` command additively preserves optional granted `scopes` and the reviewed `target`. Required scopes are never invented as granted scopes. Token limit is64KiB; resource/target identities and scope count/length are bounded. The owned credential wrapper redacts Debug and zeroizes its owned allocation on drop, but this does not promise erasure of SDK/serde transport copies or remote backend storage. No token is included in the context, receipt or host checkpoint.

Queue refusal means not sent. After admission, a backend deadline or disconnect/interruption is structured `outcome: "indeterminate"` with the original command ID, never permission to retry automatically. Explicit backend refusal uses fixed safe text rather than provider error content. Acceptance does not synthesize MCP Ready or resolve a tool challenge; subsequent authoritative state remains required.

This host boundary and the shared native/Web reviewed bearer handoff are qualified with owned synthetic credentials and real AHP/backend paths. Provider, MCP and active-tool controls use explicit review and the official root command; uncertainty is inspected by original native receipt rather than replay. OAuth discovery/PKCE/state/redirect/acquisition/refresh/storage and live provider/platform qualification remain separate work. No live authorization service is contacted by these tests.

## MCP side channels

A protocol-version-5 session backend may register bounded MCP server descriptors in `hello.mcpServers`.
Paratenic maps each descriptor to an official ready `McpServerCustomization` and derives a stable opaque `mcp://` channel.
The advertised capability union is the sole method allowlist: tools, resources, logging, and sampling methods are rejected unless their corresponding capability is present, and MCP initialize traffic is always rejected.

AHP-side MCP requests and notifications retain verbatim MCP JSON-RPC bodies plus the required top-level AHP `channel`.
Paratenic removes that routing field, assigns an internal UUID, and sends `mcp_request` to the owning session backend. New hosts include the exact logical `backendConnectionId` so a multiplexed provider can distinguish sessions that reuse the same `serverId`; older messages may omit it.
The backend answers with `mcp_response`; Paratenic restores the caller's original JSON-RPC ID and channel. For a multiplexed provider, the pending request retains the logical session route, and the reply is accepted only from that route's current physical transport owner. A copied request UUID from another provider or a retired logical route cannot consume the pending response.
A provider must observe `provider_registered.scopedMcp: true` before using scoped MCP notifications. Absence means unsupported, not an inferred capability from the protocol version. Send the current logical `backendConnectionId` from `session_registered` on `mcp_notification`; the host checks that it still belongs to that physical provider and derives the canonical channel from that logical route plus `serverId`. Bare provider, foreign and retired scopes are refused with a content-free backend error without disconnecting healthy sibling routes. Dedicated session backends may still omit the field. A supplied channel cannot override the host's routing identity, and unadvertised notification methods remain refused.

MCP work uses a bounded per-connection task set so a held side-channel reply does not block later MCP requests or the ordinary ordered AHP reader. Limits are16inflight MCP tasks per frontend connection and128pending request entries globally. Backend queue wait and response wait share the existing absolute60second command deadline. Closing the frontend aborts/reaps its owned waiters and removes its pending correlations; it does not replay or claim cancellation of a backend operation already admitted.
MCP request IDs, backend command IDs, AHP client sequence numbers, and tool-call IDs remain separate namespaces.

## Local security and lifecycle

The socket parent and socket are forced to owner-only permissions.
The service takes an exclusive lock on its state directory so only one Paratenic instance owns the canonical socket and state.
A locked instance may remove a refused stale socket left by an unclean prior exit; it never replaces a live listener.

Adapters reconnect with exponential backoff and bounded in-memory event buffering.
Ordinary interactive runtimes remain locally usable while Paratenic is unavailable.
An adapter must stop registration and reconnect activity when its owning runtime session shuts down.

## Other AHP hosts

Paratenic does not republish another AHP host's sessions. An AHP host owns its
sessions; pi-daemon 1.x is reached directly by clients as its own host, and
aggregation is client-side. The former `connect ahp` mirror is removed.

## Managed invocation extension (explicitly negotiated)

No first-party connector currently negotiates this extension; it remains part of the host protocol.

A protocol-10 backend may opt into `chatCapabilities.managedInvocations`. The
managed connector requires an exact version-10 ProviderRegistered acknowledgement
before any session registration, observer event or input admission. An older host
must reject its version-10 hello; even an incorrectly downgraded version-9
acknowledgement is refused. There is no retry using protocol 9 or raw controls.
The new host continues accepting legacy versions, including protocol-9 provider
readiness and indeterminate outcomes. Managed capabilities/events on a legacy
connection are refused. SDK capability alone is not receiver support.
This is separate
from raw `follow_up`/`steer` support. The Pi connector advertises it only after
explicit invocation-context v1 negotiation and a validated observer snapshot
cut. Observer loss revokes readiness; SDK version/provider name never grants
it. Resident-session qualification uses observer attachment without hydration.
Scoped cancellation never falls back to raw `abort`.

The neutral extension adds `managed_prompt` (text, original turnId),
`managed_follow_up` (text, immutable pending version), `observe_invocation`
(original target; read-only), and `abort_invocation` (original turnId and source
binding). Pending versions contain the original command UUID, pending ID, chat,
and client origin/sequence. Equal text or a reused pending ID is not a version.
The existing chat checkpoint retains command correlation after backend ACK and
reconnection. Unresolved records are admission-bounded, not silently evicted.

`invocation_outcome` carries the original commandId, target, and outcome;
`invocation_user_message` carries an actual source user boundary and its original
invocation binding. Ordered `invocation_boundary` metadata carries native
entry/retirement without inventing user/model events. If an input hook handles
a prompt without emitting any user/model event, the original precreated turn
settles only after both original terminal completion and ordered native
retirement. It gains no invented reply or extra turn, and an early HTTP ticket
cannot overtake the source stream. Only identity-validated `succeeded` plus explicit ticket
`delivery.state=delivered` projects completed. That retires only the unchanged
pending version, not an edited/replaced item. This means successful producer SDK
invocation completion, not live/model consumption. Ticket results never supply
assistant text or synthesize a new turn. Unknown outcomes remain visible without
input replay. Existing dispatch receipts expose terminal removal or rejection;
receipt lookups may enqueue a coalesced original-ticket GET, never a wake POST.
The original command task continues bounded-rate observation while connected;
there is no 30-second native-duration deadline. Reads have individual deadlines,
with backoff capped at five seconds, and end on terminal evidence or owner
cancellation. A lost wake ACK and initially absent ticket preserve the original
service/session/generation/request/key scope in the chat checkpoint. Recovery
uses only the existing WAKE-scope ticket GET until the original ticket is known.
No empty/fabricated ticket ID or new POST is used.

The connector reserves a FIFO admission position in each existing command task
before spawning it. The dedicated admission guard covers observer readiness,
preflight and the wake-admission attempt, and is released before execution-ticket
polling. It is not the session-state mutex or an execution scheduler. Cancellation
removes a waiter without letting successors pass an unfinished predecessor.
Stop and original-ticket reads bypass that gate; other workers have separate
gates. Concurrent commands share the actual observer attach-readiness result—
`Some(observer)` alone never authorizes a later POST.

Transient pending-command callbacks also retain the original managed version.
Pruning a terminal durable correlation cannot turn a delayed NACK into a legacy
permission to remove a newer same-ID item. Completed no-user/mixed spans can be
pruned after original native retirement and terminal proof without weakening
that separate callback fence.

Pi context negotiation uses `GET /v1/capabilities`'s `data.invocationContext`,
string contractVersion `1.0`, orderedEvents/snapshotCut and identityKind
`sdk-invocation`. The actual context comes from
`attach_ready.snapshot.invocationContext` at the existing highWaterCursor, then
`invocation_context` events in that same ordered stream. Epoch/revision/gap
failures invalidate live bindings. Queue admission does not change active scope.
Managed mixed-input attribution retains its original tuple: attribution and
`cancellation.state` are independent. Unattributed+owned can cancel original A
under its service/epoch/span fences; raw/children cannot acquire that tuple.

A scoped Stop request produces a non-reducing
`paratenic/invocationStopRequested` receipt, not an optimistic turn cancellation.
The native settled event owns turn settlement; queued B remains governed by the
producer's existing queue policy. The approved producer HTTP route is
`POST /v1/session/{sessionRef}/wake/cancel` with expectedGeneration and required
requestId/idempotencyKey target, plus requestId identifying THIS cancel command.
The optional X-Request-Id matches that requestId; no Idempotency-Key is sent.
The matching normal v1 response contains original session/generation/target,
requested|settled|not_found observation, and an optional original wake ticket.
None proves interruption or non-delivery. A missing/mismatched response leads
only to an original-ticket GET and an indeterminate command outcome—never a
repeated POST. The frozen producer contract and fixtures are from
Pi-Daemon ef07cbd002d7c4c082696256ff7cb53bb896194c (PR188).
No broad abort fallback is allowed.


### Managed transcript continuity across host/backend reconnection

A restored active managed turn is not proof that its producer output stream was
reattached without loss. The connector emits `invocation_observation_cut` from
the validated framed snapshot (service/session/generation/sourceEpoch, active
managed span, and `sourceIdle`) before post-cut events. `sourceIdle` is true only
when validated `context.active` is null; raw-native or unattributed-children
activity is not idle. Missing `sourceIdle` defaults to false. It contains no output inferred from ticket
results or latest-message text.

On a new backend incarnation or restored disconnected host, an existing active
managed turn is marked output-discontinuous. This includes a precreated turn
whose user boundary might have arrived during the unobserved interval.
A matching original terminal ticket plus a validated cut where that span is no
longer active ends that exact turn with `source_observation_gap`, preserving all
received response parts. If the span is still active at the cut, the live suffix
continues until native settlement/retirement; an early HTTP terminal result must
not overtake it. Native settlement after a discontinuity also explicitly reports
incomplete output instead of silently calling a partial transcript successful.
A completed prior turn and newer replacement turns are not changed.

This is an honest incomplete-output recovery, not lossless transcript replay.
The current connector has no durable producer-cursor-to-host-revision checkpoint;
its dormant history importer lacks an exact resident SDK-entry/AHP-turn binding.
No ticket success, text equality or last-assistant value is used to fill missing
bytes. Actual public history retrieval for this interrupted original turn has
not been qualified; this change does not advertise it as available recovery.
Unknown/incomplete output is
not non-delivery and never permits automatic input replay or scoped cancellation
against a successor.

For a precreated turn with no observed native binding, a matching persisted
original service/session/generation/request/key plus original terminal ticket
and a fresh cut with explicit `sourceIdle: true` and no active managed span may
report the same gap. Absence of a managed span alone is insufficient. An arbitrary active span cannot
be joined to that turn by timing, and no epoch/span binding is manufactured.
Both this path and the live-tail path use `source_observation_gap`. Native
`agent_settled` alone is not reported as settlement of the entire invocation or
its children; the error describes unverifiable transcript continuity only.
Visible copy: “Reply continuity could not be verified after reconnection.
Received output was kept; your message was not resent.”
