sessionpipe

HTTP binding

This document is part of the sessionpipe protocol and is licensed under CC BY 4.0. The words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are used as in RFC 2119. Status: Draft (see VERSIONING.md).

The protocol's events (PROTOCOL.md) travel to a receiver over HTTPS. This document is the binding: discovery, the batch request, responses and errors, retry, idempotency, the control long-poll and the native lane. The header names and signature scheme are those of Standard Webhooks, verbatim.

1. Discovery: GET /.well-known/sessionpipe

At the receiver's origin root. A sender reads it when a sink is added and re-reads it at most daily (24 h cache); a receiver MAY set Cache-Control.

{
  "protocol": [1],
  "max_tier": 2,
  "capabilities": ["events", "backfill", "forget", "control", "native"],
  "auth": ["bearer"],
  "endpoints": {
    "events":  "/api/sessionpipe/v1/events",
    "control": "/api/sessionpipe/v1/control",
    "native":  "/api/sessionpipe/v1/native"
  },
  "batch":   { "max_events": 50, "max_bytes": 262144 },
  "control": { "wait_max_s": 25 }
}
Field Rule
protocol The protocol integers the receiver accepts.
max_tier The highest tier it will store. A sender MUST cap its sink's tier at this value.
capabilities events is required. backfill: accepts session.backfill. forget: honours session.forgotten. control: serves the control endpoint. native: serves the native lane. Additions to the protocol are announced here (VERSIONING.md).
auth bearer only in v1.
endpoints Paths relative to the origin, or absolute URLs.
batch The largest request the receiver takes. A sender MUST NOT exceed either bound. Defaults when absent: 50 / 262 144.
control wait_max_s: the longest long-poll it will hold.

Schema: well-known.json.

2. Events: POST {events}

Body {"events":[…]} — 1 to batch.max_events events, at most batch.max_bytes bytes (batch.json).

Request headers:

Header Rule
content-type application/json
authorization Bearer <token> — the token the person configured for the sink.
sessionpipe-protocol 1 — the batch's protocol integer, so a receiver can answer 400 without parsing.
webhook-id OPTIONAL. A unique id for this delivery attempt's message (retries reuse it).
webhook-timestamp OPTIONAL. Unix seconds.
webhook-signature OPTIONAL. v1,<base64> — HMAC-SHA256 over <webhook-id>.<webhook-timestamp>.<body> with the secret behind the whsec_ prefix (base64-decoded). Several space-separated signatures MAY be present; one match is enough. A receiver that verifies MUST reject a timestamp more than 5 minutes off its clock.

Response 202 with batch-response.json:

{ "accepted": 47, "duplicates": 3, "rejected": [{ "id": "01K6…", "reason": "tier_type" }], "max_tier": 2 }

accepted + duplicates + rejected.length MUST equal the number of events sent. A rejected event is one the receiver will never store (its reason says why); the batch as a whole still succeeded and the sender's cursor moves past it. max_tier repeats the well-known value so a sender learns a lowered cap without a discovery round trip.

Errors

Status Meaning Sender behaviour
400 Malformed body, or {"reason":"unsupported_protocol","supported":[1]} Drop the whole batch; log it.
401 Bad or missing token Pause the sink; doctor says so. Never retry in a loop.
403 {"reason":"tier_above_max","max_tier":N}: an event's tier exceeds the receiver's maximum Lower the sink's tier to max_tier, re-filter, retry.
413 Batch too large Halve the batch and retry; a single event over the limit is dropped and logged.
429 Too many requests, with retry-after Wait as told, then retry.
5xx, network error, timeout Receiver unavailable Retry with backoff 2 s · 10 s · 60 s · 5 min · 30 min, then every 30 min, forever.

Bodies of 4xx/5xx responses SHOULD be error.json. The sender's cursor moves only on 202.

Idempotency

A receiver MUST deduplicate on (harness.name, session.id, session.seq) and count a repeat in duplicates. Order of arrival is not guaranteed; seq is the order. A receiver SHOULD keep at least the highest seq per session plus a window of recent values, so a replay after an outage costs nothing.

Time

A receiver MUST record its own arrival time beside time. The difference is the delivery latency the project measures (target: p50 under 2 s on a laptop).

3. Control: GET {control}?session=<harness>:<id>&wait=<s>

Only when the well-known file lists control, the sink was added with --control, and a session is live. Bearer auth. The receiver holds the request up to wait seconds (≤ wait_max_s) and answers 200 with control-poll.json when a message is waiting, or 204 when none is. Messages are defined in CONTROL.md.

POST {control}/ack

control-ack.json: {"acks":[{"id","outcome":"delivered"|"expired"|"unsupported"|"failed","at","detail?"}]}. A receiver MUST keep re-delivering a message on every poll until it is acked or its expires_at passes. 202 on success.

4. Native lane: POST {native}/<harness>

Raw hook JSON exactly as a harness's own HTTP hook handler sends it (Claude Code type: "http", Copilot CLI). The receiver normalises it with core's adapter, stores it at the tier it declared for the lane (≤ 1), and records privacy.rulesets: [] — no redaction ran at source. This lane exists for a receiver you own on a machine you own; a public receiver SHOULD NOT advertise native.

5. Forget

A session.forgotten event (any tier) asks the receiver to delete everything it holds for that session: events, transcripts, derived data. A receiver that lists forget MUST do so before answering 202 (or within a stated retention window, documented on its /.well-known origin), and MUST answer later reads of the session as though it never existed.

6. Transport requirements

7. Delivery scenarios (conformance)

The runner sends and expects, in this order; each is a fixture under conformance/delivery/:

Scenario Send Expect
accept a valid tier-0 batch 202, accepted = n
dedup the same batch again 202, duplicates = n
oversize a batch over max_events or max_bytes 413
tier an event with tier > max_tier 403 with max_tier
protocol sessionpipe-protocol: 99 400 unsupported_protocol
auth a wrong bearer (auth: "wrong" in the fixture) 401
unknown-type type: "x.y" 202, stored, retrievable
forget session.forgotten, then a read 202, then nothing
control a queued message, a poll, an ack 200 with the message; 204 after the ack