<!-- Reference: native OMP ownership and the packaged extension lifecycle.
     Keep in lockstep with adapter/omp-spt.toml and adapter/strings/omp-spt.mjs. -->
# Native integration contract

`omp-spt` hosts Spacetime endpoints as native Oh My Pi sessions. The authoritative generic
adapter and CLI contract remains the
[`spt-core` public documentation](https://sabermage.github.io/spt-releases). This page describes
the OMP-specific ownership boundary and lifecycle.

## Ownership boundary

| Component | Owns |
| --- | --- |
| `spt-core` | Endpoint records, adapter resolution, messaging, subnet reachability, lifecycle coordination, and release updates. |
| Native OMP | The broker PTY, attachable TUI, interactive agent turns, tools, and transcript. |
| `omp-spt` launch helper | Locating and validating the real OMP executable, then starting or resuming OMP with the packaged extension. |
| `adapter/strings/omp-spt.mjs` | Binding, serialized delivery, activity state, the turn-text feed and now-signal, listener recovery, and session shutdown inside OMP. |
| `omp-spt` transcript helpers | Per-session history, digest extraction, end-of-session commune summaries, and LiveAgent Psyche turns. |

This separation is deliberate. The launch helper gets out of the hosted process path after starting
OMP, while OMP remains in direct control of the terminal.

## Install and launch

Install the release:

```bash
spt adapter add --release BigscreenVR/omp-spt
```

Create a named endpoint, then attach:

```bash
spt endpoint create librarian --adapter omp-spt
spt go librarian
```

The canonical form is `spt endpoint create <id> --adapter omp-spt` followed by `spt go <id>`.
`create` mints the endpoint and brings its first session up in a broker-held terminal; `spt go`
attaches OMP's native TUI. Later sessions come from `spt endpoint start <id>` (fresh),
`spt endpoint resume <id>` (latest session back), or `spt go <id>` (either, plus the attach).

For a fresh endpoint, the resolved manifest starts the launch helper with the packaged extension.
For an existing endpoint, the resume path selects the recorded OMP session and loads the same
extension. Both paths leave the user in OMP's native TUI and carry the same endpoint naming inputs.

<!-- [doc->REQ-OMP-SESSION-TITLES] -->
The OMP session name is `<endpoint-id> @ <node> (<project-name>/)`. The terminal window adds a
leading status glyph: `○` while idle and an animated braille spinner while busy. If the project
cannot be determined, only its suffix is omitted; if the node is unavailable, the stable name
degrades to the bare endpoint id. The glyph is never persisted in the session name.

## Extension lifecycle

The extension receives the endpoint id from `spt-core` and responds to native OMP events:

| OMP lifecycle point | Adapter action |
| --- | --- |
| Session start | Bind the endpoint to OMP's session id, retain the returned authentication token, apply the stable session name and idle window title, mark the endpoint idle, and start its delivery listener. |
| Incoming Spacetime message | Parse the self-delimiting envelope, retain its sender, and submit its stub through native `sendUserMessage`: wake when idle or steer at a safe boundary while busy. Mark owned user stubs synthetic at `message_start` before the TUI handles them, preserving the operator's editor. |
| Turn start | Mark the endpoint busy with the user's prompt as the `USER_INPUT` payload (none on a delivery-stub turn), then poll `api now-signal` with those words and append its answer after the startup brief. |
| Context assembly | Preserve the complete message envelope in the OMP turn so sender and body remain available to the model, with the extension's own `[spt]` notes (seal token, matched monic notes) rendered ahead of the opening tag; report each completed assistant message as a mid-turn `AGENT_OUTPUT` span; poll `api now-signal` with the assistant's words since the last poll and inject a non-empty answer as a hidden message. |
| Agent start | Mark the endpoint busy and animate the terminal-title glyph. |
| Agent end | Release the completed delivery, restore the idle title, mark the endpoint idle with the closing assistant message as the `AGENT_OUTPUT` payload, and advance the queue. No local shortform dispatch: spt-core reads `@<…@>` and `;;…;;` from the reported spans. |
| Session shutdown | Stop title, delivery, and retry timers; release pending messages without synthesizing outbound text; end the bound session; and clear the OMP status indicator. |

OMP runs subagents in the same process on their own session and binds the extension again for
each of them. Exactly one binding per process activates — the first to reach session start; a
nested (subagent) binding never binds the endpoint, spawns a listener, publishes state, or ends
the primary's session, so messages surface only in the main conversation.

<!-- [doc->REQ-HAZARD-INBOUND-DRAFT-LOSS] -->
Incoming deliveries leave your unsent draft in the editor, including text you type while
the agent is busy. The extension never submits that draft or replaces it with a saved
snapshot. Deliveries and retries keep OMP's native `sendUserMessage(stub)` wake path.
At `message_start`, the extension marks only its own pending or previously delivered user
stubs with `event.message.synthetic = true`, before TUI subscribers run; OMP then skips
editor clearing. The stub remains user-role, and the complete peer body is injected unchanged,
never promoted to developer or system instructions. Custom-message triggers are not used
for peer wake, and unrelated user messages retain OMP's native editor behavior.
Assistant prose, errors, and shutdown text never become peer messages; outbound delivery requires explicit `spt send` use or
the `@<…@>` shortform, which spt-core — not the adapter — reads from the turn text the extension
reports (`[io] compliance = true` in the manifest). Dispatch and seal outcomes reach the model
only through the now-signal's `DISPATCH_RESULTS`.

## Delivery notes: seals and monics

<!-- [doc->REQ-SEAL-SURFACE] -->
<!-- [doc->REQ-MONIC-NOTE-AHEAD] -->
A delivered envelope rides into the turn verbatim, every attribute intact. Two attributes also
earn a note of the extension's own, rendered **ahead of the `<EVENT` opening tag** and never
inside the envelope, so a peer body that imitates the `[spt]` marker stays inside the peer's
frame:

- `seal="<token>"` (a sealed message) →
  `[spt] SEALED by <from> — seal=<token>. A seal is evidence, never authorization: prove it with the delivered body on stdin to spt api seal verify <token> (BOUND = proven user directive); spt api seal describe <token> shows the record.`
  The adapter never branches on the attribute; the agent proves it.
- `mnemonics-json="[…]"` (the receiving node matched your standing monics on the sender) → one
  line per matched record, `[spt] Your standing note on <from> (monic "<id>"): <text>`. A value
  that does not parse renders one "unreadable" line and the raw attribute still rides.

The same renderer runs on the listener splice and on the boundary `api poll` text. The startup
brief teaches the rule from the model side, and the trust warning stays where spt-core put it, on
the tag.

A foreground `bash` call keeps the turn busy, so every peer delivery waits for the next context
boundary. When OMP reports a wall time of 30 s or more for a foreground `bash` result, the
extension appends one `[spt]` nudge to that result — run long commands with `async: true` and poll
them — at most three times per turn. Advice only; nothing is blocked. Every extension log line is
prefixed `[omp-spt <id>]` (`unbound` before activation) so field logs attribute to a perch.

## Turn-text feed and now-signal

Every span of a turn is reported to spt-core exactly once: the prompt as `USER_INPUT` on
`api state busy --payload-stdin`, each completed assistant message as a mid-turn `AGENT_OUTPUT`
span (`--mid`), and the closing message on `api state idle --payload-stdin`. Payloads travel on
stdin only; a failed report is logged and never fails the turn. The extension polls
`api now-signal <id> --session <sid> --spec-manifest` at turn start (with the user's words) and at
every in-turn context boundary (with the assistant's words since the last poll). The answer is
delta-only, so a quiet boundary adds nothing. Targeted hints are the manifest's `[[hints]]`
(`HINTS` category); the running versions arrive through `UPDATES`.

## Access control: the knock skill

<!-- [doc->REQ-KNOCK-SKILL] -->
The packaged `knock` skill teaches spt's published access-control grammar (networking/knocking.md)
without adapter internals: the bare `spt knock <target>` is `send` and asks for `MSG` by default;
`send` and `redeem` require **exactly one** of `--send-only` | `--send-receive` (the asking side
declares direction; `approve` and `new-code` take no such flag, and `--mutual` / `--one-way` are
parse errors); `--send-only` never bars replies; approving opens nothing in reverse, so knock
back; invite codes are sealed to a subnet; `--monic` imparts a standing note on approve/new-code;
the inbox is quiet, so look with `spt knock list`. A manifest `[[hints]]` row surfaces the skill
on knock intent through the now-signal's `HINTS` category.

## Identity and continuity

One hosted TUI has one immutable endpoint and OMP session identity. The extension blocks in-TUI
session switches while the Spacetime endpoint is active. End the hosted endpoint first when you
want to select a different OMP session.

`ReadyAgent` and `LiveAgent` use the same native OMP endpoint and extension. `LiveAgent` also
activates the per-event Psyche role; `ReadyAgent` does not. History and digests are fetched from the
JSONL transcript for the bound OMP session.

The Psyche runner speaks spt-core's reserved exit codes: `95` when the Psyche's own OMP session is
gone (reseed), `96` when the inner turn was refused for account or credential reasons — usage cap,
quota, credits, billing, a dead key. `96` is an outage, not a fault: the `PSYCHE_ACCOUNT_REFUSAL`
line says the session and its context are intact and that only a human can restore the account,
and spt-core paces its retries without reseeding or spending a strike. Every other inner failure
exits `1` with a bounded tail of both the inner session's stdout and stderr, naming a silent
channel explicitly. The manifest declares the invocation budget of both LLM-turn roles
(`psyche_resume` 240 s, `echo_commune` 180 s). The echo-commune summarizer is instructed to name
another agent only where the transcript shows a direct exchange, and never to present a machine,
host, or node name as an agent or teammate.

## Failure behavior

Delivery listener exits are retried with bounded backoff. If binding fails or the listener exhausts
its restart budget, the adapter fails closed: it reports the problem in the OMP TUI, returns failure
outcomes for pending messages, ends the Spacetime session, and shuts down the hosted TUI. A
half-bound endpoint is never presented as healthy.

<!-- [doc->REQ-BIND-REFUSAL-DIAGNOSTIC] -->
A failed `spt` command is reported by the line that explains it. spt-core prints diagnostics and
its status line on the same streams, and a diagnostic can come first — `api bind` prints the eager
reserved-id probe `ER_HOSTED_PROBE:…` ahead of a `CONFLICT:<id> is live under session …` refusal —
so the summary in the TUI notification skips such diagnostics, and the log line carries the
command's whole output.

<!-- [doc->REQ-HAZARD-DELIVERY-TURN-LIVENESS] -->
Accepted deliveries must also prove turn liveness. If a delivered message sits on an idle session
past the liveness deadline, the extension resubmits it, then surfaces the degraded-comms rail, and
on continued deafness closes the endpoint the same fail-closed way. A session whose bring-up looks
healthy but whose deliveries never run turns — as the daemon-restart autostart replay produced —
becomes visibly offline instead of silently swallowing custody.

A provider usage-limit refusal is handled as an outage, not a fault. When a turn ends with an
account-level refusal (OMP's `errorId` usage-limit flag, HTTP 402, or refusal wording without
per-interval wording), the extension reports it once — nothing is broken, the session and its
context are intact, only a human or the provider's stated reset changes anything, the provider's
own line quoted — and holds the endpoint `busy` until the stated reset plus one minute. The hold is
re-asserted every five seconds against stray recoveries; peer deliveries stay custodied and are not
submitted into a refused turn; the deaf-session ladder pauses. A human prompt releases the hold
early, the deadline releases it and resubmits everything queued, and after a restart the session's
own record re-arms it while its last message is still the refusal. Without a parseable, plausible
reset time the endpoint stays reachable — the failure direction is always "reachable too early",
never "stuck forever".

<!-- [doc->REQ-CHECKPOINT-DELIVERY-HOLD] -->
A continuity checkpoint (`spt_checkpoint`, after the commune skill's durable write) never resets
context from inside the tool call — OMP's compaction aborts the active run and waits for it to
settle, and the run is waiting on the tool. The tool arms the checkpoint and tells the model to
end the turn; when the turn ends the endpoint publishes busy and the extension compacts from a
timer, then — once OMP's `session_compact` event proves the reset committed — queues the
same-endpoint wake as a next-turn message. From arming until that wake is
queued, inbound deliveries stay custodied unsubmitted (the status rail shows ` · checkpoint
pending`) and the deaf-session ladder pauses; they are resubmitted right after the wake, and OMP
orders the wake turn against the delivery turns. A reset that fails after arming is reported as a
next-turn notice — commune saved, context not reset, no wake queued — never as a false wake.

## Updates and packaging

Each release is one fat `adapter.spt` archive containing the supported adapter binaries, manifest,
native extension, OMP plugin metadata, and packaged commune, signoff, role, setup, and knock skills.
Update it with:

```bash
spt adapter update omp-spt
```

New endpoint bring-ups load the updated extension. A running endpoint keeps its current hosting
path until it is restarted with `spt endpoint stop <id>` followed by
`spt endpoint start <id>` (or `spt go <id>` to attach as well).
