Handler Protocol
| Status | Draft, stable from v0.1.0 |
| Version | 1 |
This document specifies the inter-process protocol between a pugmark runtime (the process driving a session — sequencing handler invocations, committing snapshots) and a handler (the subprocess or HTTP service that produces new log entries given an existing session log).
The spec is the authoritative source. Any conformant implementation, in any language, must satisfy the rules below; behavioural differences between an implementation and this document are bugs in the implementation.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in RFC 2119.
The on-disk storage layout the runtime persists to (and from which it reconstructs the session log on every invocation) is defined in a companion document, storage.md.
Table of Contents
- Overview
- Transports
- Wire format
- Input records
- Output records
- Events
- Object data fetch
- Memory namespaces
- Control messages
- Pause and sleep
- Forking
- Cross-session writes
- Environment
- Distributed tracing
- Named handlers (routing)
- Termination
- Stability and versioning
1. Overview
A pugmark session is an append-only log of objects persisted in object storage. A handler is a process that, given the session log up to some snapshot version v, produces zero or more new objects to be appended at snapshot v+1.
On every invocation, the runtime:
- Materialises the session’s full log as an ordered list of objects (parent-chain objects walked through manifests, followed by the current session’s snapshots in version order).
- Starts two ephemeral HTTP servers, one serving object bodies and one serving memory-namespace bodies. The handler reaches both at URLs the runtime hands it on stdin (§7, §8).
- Sends one Input record per log entry on the handler’s input stream, in stream order, followed by exactly one lifecycle event record.
- Reads zero or more Output records from the handler’s output stream until the handler closes it (subprocess exits, HTTP response ends).
- Commits the outputs as a new snapshot of the session, observing the snapshot-type and conflict rules in
storage.md.
The handler operates against a specific snapshot version; the runtime never feeds the handler partial state. Handlers MUST be tolerant of being invoked on the same snapshot more than once — the runtime may retry on commit conflict, and at-least-once notification delivery in cloud transports may cause duplicate invocations. Handlers SHOULD be deterministic given identical input; any non-determinism (LLM responses, timestamps) is implicitly checkpointed by the very act of appending its output to the log.
2. Transports
A handler communicates with the runtime over one of two transports. Both share an identical wire format above the transport layer.
2.1 Subprocess
The runtime spawns the handler as a child process. Input records arrive on the handler’s standard input (file descriptor 0). Output records leave on the handler’s standard output (file descriptor 1). The handler signals completion by exiting. Standard error (file descriptor 2) is not part of the protocol; handlers SHOULD use it for human-readable logs and diagnostics. A non-zero exit status MUST be treated by the runtime as a failed invocation; the snapshot is not advanced.
2.2 HTTP RPC
The runtime sends a single POST request to a URL the handler exposes. The request body is the input stream; the response body is the output stream. The handler signals completion by ending the response body with HTTP status 200. Any other status is a failed invocation.
The request MUST include three query parameters:
| Parameter | Type | Meaning |
|---|---|---|
agent | string | The agent identity for this invocation (§13). |
session | 32-hex string | The session ID. |
snapshot | decimal integer | The snapshot version the handler is operating on. |
The request MUST carry session metadata as an X-Pugmark-Session-Metadata header whose value is a JSON object mapping string keys to string values. The header is omitted if the manifest has no metadata.
The Content-Type of the request body MUST be application/json. The Content-Length header MUST be set; chunked transfer encoding MUST NOT be used by the runtime (some HTTP servers — notably Python’s wsgiref — do not decode chunked encoding correctly, causing reads to block).
3. Wire format
The input and output streams are sequences of JSON objects encoded as UTF-8. Each object is one Input record or one Output record; objects appear back-to-back, separated by any amount of JSON whitespace (space, tab, LF, CR).
Implementations:
- MUST encode each record as a single, complete JSON object — an opening
{, all of the record’s fields, and a closing}— and MUST NOT split a record across multiple top-level JSON values. - MUST be able to parse a stream of records separated by any combination of LF, CR, tab, or space (the standard JSON whitespace set).
- MUST NOT emit a byte-order mark; the stream is plain UTF-8.
- MUST NOT emit any non-JSON content on the protocol stream — no comments, no log lines, no blank-document framing. Diagnostic logging belongs on stderr (subprocess transport, §2.1) or in surrounding HTTP response bodies (RPC transport, §2.2), never inline with records.
- MUST ignore unknown fields on incoming records. This is what allows additive evolution within version 1 (§17).
- SHOULD emit a single LF after each record. This is purely a readability convention for humans inspecting the stream — parsers do not depend on it.
A record where every recognised field is empty (or omitted) is a no-op and MUST be tolerated by parsers.
3.1 Binary data
Binary bodies carried inside the data field of Input or Output records are encoded as base64 strings using the standard alphabet (RFC 4648 §4). Encoders MUST NOT use the URL-safe alphabet and MUST NOT insert line breaks within the encoded payload. Decoders MUST accept both padded and unpadded encodings.
3.2 Empty fields
The schemas in §4 and §5 list every field a record MAY carry. Every field is optional at the wire level — encoders SHOULD omit fields that are empty (zero-length string, zero-length object, zero integer, false boolean), and decoders MUST treat a missing field as equivalent to its zero value.
4. Input records
An Input record describes one log entry the handler should consider, or one lifecycle event. Schema:
{
"event": "<event-type>",
"url": "<http URL>",
"size": <int64>,
"content-type": "<MIME>",
"content-encoding": "<encoding>",
"schema-url": "<URL>",
"name": "<descriptor name>",
"metadata": { "<k>": "<v>", ... },
"data": "<base64>"
}
4.1 Field semantics
| Field | Type | Semantics |
|---|---|---|
event | string | One of push, start, fork, wake, memory, or empty. See §6. |
url | string | HTTP URL where the body can be fetched (§7). Absent for records that have no body (lifecycle events). |
size | integer | Size of the body in bytes. Informational; clients SHOULD NOT enforce equality with the fetched body length. |
content-type | string | MIME type of the body. Empty for lifecycle events. |
content-encoding | string | If set, the body has been pre-encoded; one of gzip or zstd. The HTTP server returns the body with this same Content-Encoding set, and conforming HTTP clients will decompress automatically (§7). |
schema-url | string | Optional schema URL associated with the body (e.g. a fully-qualified protobuf message type). |
name | string | The descriptor name. Conventionally a filename or a typed-event name. MAY be empty. |
metadata | object<string,string> | Arbitrary string-keyed metadata carried by the descriptor. |
data | base64 string | Inline body, base64-encoded (§3.1). Present only for descriptors that carry inline data (see §4.3). If present, url MUST be ignored by the handler. |
4.2 Stream order
On every invocation, the runtime emits Input records in this order:
- Memory namespace announcement — exactly one Input with
event = "memory"andurlpointing at the root of the memory HTTP server (URL ends with/). This MUST be the first Input on the stream. See §8. - Log replay — zero or more Inputs, one per object in the session’s log, in stream order. The runtime MUST preserve the order in which objects were committed (parent-chain objects first, oldest first; then the current session’s snapshots in version order; within a snapshot, descriptor order). The runtime MUST filter out:
- Control messages (§9).
- Pending fork records from the current session’s most-recent snapshot (§11.2).
- Lifecycle event — exactly one Input with
eventset to one ofwake,fork,start, or empty, and no other fields populated (see §6). This MUST be the last Input on the stream.
The handler MUST be tolerant of zero log-replay records (an empty session) and MUST treat the lifecycle event as the signal to begin processing.
4.3 The event field on replayed log entries
When replaying log entries, the runtime tags some of them with event = "push" to identify which entries are newly arrived relative to a hypothetical previous handler invocation on this session. The exact rule:
- If the most recent prior control message in this session has
status = "stopped"(the session was paused), every replayed entry that appears after that control message in the stream getsevent = "push". Entries before or at the control message position getevent = ""(empty). - Otherwise, the last
nreplayed entries getevent = "push", wherenis the number of objects in the current snapshot minus the number of objects in the previous snapshot. Earlier entries getevent = "".
Handlers MAY ignore the event tag on replayed entries entirely and treat the log uniformly as ordered events. The tag is provided as a convenience for handlers that want to react only to deltas.
5. Output records
An Output record describes one entry the handler is appending to the log, or a terminal signal (pause/sleep). Schema:
{
"uri": "<storage URI>",
"data": "<base64>",
"content-type": "<MIME>",
"content-encoding": "<encoding>",
"schema-url": "<URL>",
"name": "<descriptor name>",
"metadata": { "<k>": "<v>", ... },
"fork": <bool>,
"session": "<32-hex>"
}
5.1 Field semantics
| Field | Type | Semantics |
|---|---|---|
uri | string | Reference to an existing object at a remote storage URI (for example, s3://bucket/path/to/object). When set, the runtime fetches the body from that URI, computes its SHA-256 digest, and stores it under the content-addressable layout in storage.md. Mutually exclusive with data. If both are set, uri is ignored. |
data | base64 string | Inline body (§3.1). Mutually exclusive with uri. |
content-type | string | MIME type of the body. MUST be set unless the entry is a pause signal (§10), which carries content-type = "application/vnd.pugmark.pause+text". |
content-encoding | string | If set, one of gzip or zstd. The runtime stores the body as-is, with this encoding in effect on subsequent reads. |
schema-url | string | Optional schema URL stored alongside the body. |
name | string | Descriptor name. If non-empty and prefixed memories/<namespace>/, the entry is routed to the memory namespace (§8). |
metadata | object<string,string> | Arbitrary string-keyed metadata stored on the descriptor. |
fork | bool | If true, this entry creates a child session (§11). |
session | string | If set, the 32-hex session ID this entry targets. When equal to the current session, the entry is committed locally. When different, the entry is a cross-session write (§12). When combined with fork = true, the new child session is created with this ID; otherwise the runtime generates a random session ID. |
5.2 Order
Output records MAY appear in any order. The runtime processes them in stream order and assigns descriptor sequence numbers in that order (sequence numbers are part of the storage layer; see storage.md).
5.3 Output processing
For each Output record, the runtime:
- Resolves the body — from
dataif present, otherwise by fetchinguri. - Stores the body under the content-addressable layout (one canonical storage key per unique body; see
storage.md). - Routes the resulting descriptor:
- If
content-typeisapplication/vnd.pugmark.pause+text→ treated as a pause signal (§10). - Else if
namestarts withmemories/<namespace>/→ routed to the memory namespace (§8). - Else if
fork = true→ recorded as a pending fork (§11). - Else if
sessionis set and differs from the current session → recorded as a cross-session write (§12). - Else → appended to the current snapshot as a normal log entry.
- If
6. Events
Lifecycle events are signalled by the event field on Input records. The runtime emits exactly one lifecycle-event Input per invocation (in addition to the leading memory Input and the log-replay Inputs).
| Event | Meaning |
|---|---|
start | Trailing Input on the first invocation of a root (parentless) session whose snapshot version is 0. |
fork | Trailing Input on the first invocation of a child session (one whose manifest declares a non-zero parent) whose snapshot version is 0. |
wake | Trailing Input on an invocation that resumes a previously paused session because new objects appeared after the pause control message. |
push | Annotation on replayed log Inputs that are newly arrived relative to the previous invocation (§4.3). |
memory | The leading Input of every invocation, announcing the memory namespace HTTP server (§8). |
| (empty) | Replayed log Inputs that predate the latest “newly arrived” cohort, and the trailing lifecycle Input when none of start/fork/wake apply (steady-state in-flight execution). |
Handlers MAY ignore lifecycle events entirely. The replay-decide-append pattern needs no lifecycle awareness — the log itself fully determines the next action.
7. Object data fetch
When an Input record carries url (and no inline data), the handler retrieves the body via HTTP GET against that URL. The runtime exposes two HTTP servers per invocation:
- An object server serving bodies for log entries. URLs look like
http://<host>:<port>/sha256:<hex>where<hex>is the 64-character lowercase SHA-256 digest of the body. - A memory server serving the union of memory-namespace bodies (§8). URLs look like
http://<host>:<port>/<namespace>/<object-name>.
Both servers:
- MUST be reachable from the handler for the duration of one invocation.
- MAY bind to ephemeral ports — URLs MAY change across invocations.
- MUST respond with HTTP
200and the body bytes on success, settingContent-Typeand (when applicable)Content-Encodingheaders on the response. - MUST respond with HTTP
404for unknown keys. - MUST be treated as read-only by the handler —
PUT/POST/DELETErequests against them have undefined behaviour.
Handlers:
- MUST treat any non-2xx response as a runtime error.
- MUST honour
Content-Encodingreturned by the server. The body is delivered already-compressed; handlers (or their HTTP clients) must decompress before interpreting the body. - MUST NOT retain URLs beyond the invocation in which they were received.
7.1 Inline data
When a log entry’s body is small enough that the runtime chose to inline it (the storage layer permits inlining bodies whose length is at most 1024 bytes), the Input record carries the body in data (base64-encoded). The Input MAY also carry url; handlers MUST prefer data when both are present. The two are guaranteed to refer to identical bytes.
8. Memory namespaces
Memory namespaces are versioned key-value stores that live outside any single session. They are intended for cross-session shared state — long-term agent memory, configuration that survives across runs, or shared knowledge bases.
8.1 Discovery
The first Input record sent to the handler on every invocation is a memory event whose url points at the root of the memory HTTP server:
{"event": "memory", "url": "http://<host>:<port>/memories/"}
The URL MUST end with a trailing /. Handlers SHOULD record this URL as the base for memory operations during the current invocation. Implementations MAY suppress this event from handler-visible event streams (the user-level event API exposes only start, fork, wake, and push).
8.2 Naming
A memory key is a path of the form <namespace>/<object-name>. The <namespace> segment MAY itself contain / characters (allowing hierarchical namespacing). The <object-name> segment MUST NOT contain /.
Implementations resolving a key to a namespace MUST use the longest-prefix match: given a set of mounted namespaces and a request key K, the resolved namespace is the longest mounted namespace N such that K starts with N + "/". The remainder K[len(N)+1:] is the object name.
8.3 Reading a memory object
The handler reads memory objects by performing HTTP GET against <memory-server-root> + <namespace> + "/" + <object-name>. The response is identical in shape to object-server responses (§7).
8.4 Writing a memory object
The handler writes a memory object by emitting an Output record whose name is memories/<namespace>/<object-name>. The runtime routes such Outputs to the memory namespace rather than to the session log.
Properties:
- Memory writes are content-deduplicated like all other objects.
- Multiple writes to the same
<namespace>/<object-name>replace earlier entries. - The runtime MUST serialise concurrent writes to the same namespace and retry on conflict (the namespace’s storage commit is optimistic; see
storage.md). - Memory writes do not advance the session’s snapshot version. A handler whose only outputs are memory writes is treated as having produced no session work, and the runtime returns without committing a new session snapshot.
9. Control messages
A control message is an object whose content-type is application/vnd.pugmark.control+json. Its body is a JSON object:
{
"status": "running" | "stopped" | "unknown",
"reason": "<freeform string>",
"start-at": "<RFC 3339 timestamp>"
}
All three fields are optional; missing fields take their zero values.
Control messages are produced by the runtime, not by handlers. They are appended to the log at commit time to record the run’s outcome:
status: "stopped"records that the handler signalled pause/sleep (§10).start-atis set only when the handler requested sleep with a duration; the runtime computes the wake time and records it.reasonmirrors the human-readable reason from the pause output.
Control messages are filtered out of the input stream presented to handlers — handlers never see them.
Handlers MUST NOT emit Output records with content-type = "application/vnd.pugmark.control+json".
10. Pause and sleep
A handler signals that it has no more work to do — or that it wishes to sleep for a bounded duration — by emitting an Output record with:
content-type:application/vnd.pugmark.pause+textdata: free-text reason (UTF-8, base64-encoded per §3.1)metadata.duration(optional): a duration string for sleep; format is a sequence of decimal numbers each with optional fraction and a unit suffix (e.g.300ms,1.5s,2h30m). Valid units arens,us(orµs),ms,s,m,h.
The runtime, on observing a pause output:
- Captures the reason and the optional duration; the duration is capped at 10 minutes — values exceeding 10 minutes are clamped down.
- Continues processing any further Output records the handler emits (multiple outputs after a pause are still committed).
- At commit time, appends a control message (§9) with
status = "stopped", the captured reason, and (for sleep)start-at = (commit time) + (capped duration).
The next invocation of the same session sees the control message during log replay (the runtime filters it from the handler-visible stream, but uses it internally to decide what to do):
- Pure pause — control message has no
start-atand is the last entry in the session: the runtime MUST return immediately without invoking the handler. The session is woken when new objects appear after the control message. - Sleep — control message has
start-atset, and is still the last entry: the runtime MUST wait untilstart-atand then invoke the handler withevent = "wake". - Wake — control message is followed by additional objects (the session was paused, then objects appeared via provisional/queueing snapshots — see
storage.md): the runtime MUST invoke the handler immediately withevent = "wake".
Handlers MAY emit multiple pause outputs in one stream, but SHOULD NOT — only the last is recorded.
A handler whose only outputs are fork outputs (§11) — no local writes, no pause, no cross-session writes — is treated as an implicit pause: the runtime MUST commit a control message with status = "stopped" automatically. This prevents an infinite re-invocation loop while the parent waits on its children. Cross-session writes (§12) do not trigger implicit pause; a fire-and-forget producer keeps running.
11. Forking
A handler creates a child session by emitting an Output record with fork = true. The body of the fork output becomes the first object in the child session’s snapshot 1.
The child session’s ID is:
sessionif provided on the Output (the handler chose the ID), or- a freshly generated random ID otherwise.
11.1 Two-phase commit
Fork creation is two-phase and crash-recoverable:
- Record the fork. When the parent’s invocation commits, the fork Output appears in the parent’s new snapshot as an entry with
fork = trueand the chosentarget-session = <child-id>(the pending fork). The body has already been stored content-addressably. The child manifest does not yet exist. - Materialise the child. On the next invocation of the parent session, before sending Inputs to the handler, the runtime processes any pending forks from the parent’s most-recent snapshot. For each:
- Create the child manifest with parent pointer set to the parent’s snapshot, and inherit the parent manifest’s
handlerandagentfields and the fork descriptor’s metadata. Manifest creation is atomic and conflict-tolerant: if the child manifest already exists, the runtime loads it rather than failing. - If the child’s snapshot version is already
> 0, skip (the fork was already materialised by an earlier invocation). - Otherwise, commit a snapshot 1 to the child containing the fork’s body as a normal entry (with
fork = false, notarget-session). Commit conflicts on this write are treated as success — they mean another concurrent run already did it.
- Create the child manifest with parent pointer set to the parent’s snapshot, and inherit the parent manifest’s
- Filter the pending forks out of the input stream sent to the parent’s handler for this invocation, so the handler does not re-see its own fork commands as live work.
The two-phase design is crash-recoverable: at any point between phase 1 and phase 2 the next invocation completes the materialisation; conflict-loss on either write is harmless because the surviving result is the desired outcome.
11.2 Handler-side observation
A child session’s first invocation sees event = "fork" on the trailing lifecycle Input and event = "push" on the single replayed Input that carries the fork body. The child’s parent-chain log walk traverses through its manifest’s parent pointer, so the child sees all objects accessible from its parent’s snapshot, up to and including the fork point.
Subsequent invocations of the child see event = "wake" or empty, just like any other session.
When the parent invocation runs after a fork, fork descriptors in the parent’s snapshot that target other sessions (siblings of any in-progress fork chain — i.e. forks inherited from the parent’s own ancestors, addressed to sibling sessions) MUST be filtered out of the parent handler’s input stream as well, so no session sees the fork records of unrelated siblings.
12. Cross-session writes
A handler MAY write to a different existing session by emitting an Output with session set to the target session ID (32-hex) and fork = false. The body is stored once (content-addressed); a descriptor referencing it is committed to the target session as a provisional snapshot (see storage.md for snapshot types).
The descriptor written into the target carries source-tracking fields:
- The writer’s session ID (the session that emitted the cross-session write).
- The writer’s snapshot version.
This (source-session, source-version) pair is the idempotency key: before writing to the target, the runtime checks whether the target’s snapshot already contains a descriptor with the same source pair, and if so skips the write. This makes retries safe.
Cross-session writes do not trigger implicit pause on the writer (§10). A handler may continue running and emit further outputs after a cross-session write.
If session is set to the writer’s own session ID, the runtime MUST treat the entry as a normal local write (the session field is dropped at commit time).
13. Environment
The runtime, in the subprocess transport (§2.1), sets the following variables in the child’s environment:
| Variable | Value |
|---|---|
PUGMARK_AGENT_ID | The session’s agent identity (from the manifest; default "main"). |
PUGMARK_SESSION_ID | The 32-hex session ID for this invocation. |
PUGMARK_SNAPSHOT_VERSION | The snapshot version (decimal integer) the handler is operating on. |
PUGMARK_SESSION_METADATA | A JSON object string of the session’s metadata. Omitted when the manifest has no metadata. |
TRACEPARENT, TRACESTATE | W3C trace context (§14). Set only when the runtime has an active trace. |
For RPC mode (the subprocess hosts an HTTP service that the runtime calls — see §15), the runtime also sets:
| Variable | Value |
|---|---|
PUGMARK_RPC_URL | A URL the subprocess MUST listen on. Two schemes are accepted: fd://<N> (use inherited file descriptor N as a pre-created listening socket; Unix only); http://<host>:<port>/ (bind a new TCP listener at that address). |
In the HTTP RPC transport (§2.2), the same identity and metadata travel as query parameters and HTTP headers respectively — see §2.2.
Implementations MUST make the values of PUGMARK_AGENT_ID, PUGMARK_SESSION_ID, and PUGMARK_SESSION_METADATA available to handler code as the agent identity, session ID, and session metadata for the current invocation, regardless of which transport is in use. The exact mechanism — environment variable, constructor argument, ambient context — is an implementation detail of the SDK in each language.
14. Distributed tracing
The runtime propagates a W3C trace context to the handler. The mechanism:
- For the subprocess transport (§2.1), the runtime sets
TRACEPARENTand (if present)TRACESTATEenvironment variables on the child process. Their values are formatted according to the W3C Trace Context specification. - For the HTTP RPC transport (§2.2), the runtime sets
traceparentandtracestateHTTP headers on the request.
Handlers SHOULD consume the propagated context and attach it to spans they emit. HTTP fetches the handler issues against the object server (§7) and memory server (§8) SHOULD also carry the trace context onward, so the runtime’s spans for storage reads become children of the handler’s invocation span.
14.1 Session ID as trace ID
The 16-byte session ID MAY be reused as the OpenTelemetry trace ID for spans associated with the session — both are 16-byte identifiers and the wire representations are compatible. A conformant runtime MUST use the session ID as the trace ID for the root span of the invocation. This guarantees that every span emitted in the lifecycle of a session shares one trace ID, making cross-component correlation straightforward.
15. Named handlers (routing)
A session’s manifest MAY carry a handler field (a free-form string). When set, this field selects among multiple handler commands registered at the runtime level. A typical CLI invocation registers named handlers like:
pugmark run --handler python='python agent.py' --handler bash='bash worker.sh' --start
When the session’s manifest declares handler = "python", the runtime invokes the python command; when "bash", the bash command. When the manifest has no handler field set, the runtime falls back to a single default handler command supplied at invocation time.
Child sessions created via fork (§11) MUST inherit the parent’s handler and agent fields at materialisation time, so a fork tree consistently routes to the same handler.
16. Termination
The handler MUST close its output stream when it has produced all its outputs:
- In the subprocess transport, this means exiting (closing stdout implicitly).
- In the HTTP RPC transport, this means ending the response body.
The runtime closes the input stream when it has finished sending Inputs. Handlers MAY ignore that signal and exit on their own schedule; they MUST NOT rely on input-stream-close as a hard “no more inputs” signal in the sense of blocking — read attempts may return EOF or 0 bytes either before or after all expected Inputs have arrived, depending on timing.
A handler that exits without producing any Outputs is a valid no-op invocation; the session is unchanged.
A non-zero exit status (subprocess) or a non-200 HTTP response (RPC) MUST be treated by the runtime as a failed invocation; the snapshot is not advanced.
17. Stability and versioning
This protocol is version 1. Compatibility rules:
- The set of recognised top-level fields on Input (§4) and Output (§5) records is closed for v1. Adding new required fields constitutes v2.
- New optional fields MAY be added in v1 minor revisions; parsers MUST ignore unknown fields.
- The set of
eventvalues in §6 is closed for v1. Adding new values constitutes v2. - The HTTP object-server URL shape (
<root>/sha256:<hex>for objects,<root>/<namespace>/<name>for memory) is closed for v1. - The set of environment variables in §13 is open — pugmark MAY add new
PUGMARK_*variables without breaking the protocol. Handlers MUST ignore unknown ones. - The pause and control message content types (
application/vnd.pugmark.pause+text,application/vnd.pugmark.control+json) are closed for v1.
If a future version of pugmark needs to break the protocol in a non-backward-compatible way, the wire-level signal will be a leading Input with event = "protocol" and a metadata.version field carrying the new version number. Handlers that see such an Input and don’t recognise the version MUST terminate without producing any Outputs.