# Protocol v2 Migration Notes

## Compatibility window

`PROTOCOL_VERSION` remains `1`. The daemon emits v1 frames by default and advertises `max_supported = 2` in `ProtocolInfo`. A v1 client that never reads `max_supported` is unaffected and continues to operate on v1 wire forms byte-for-byte. A v2-aware client checks `max_supported >= 2` and opts in to streaming responses on a per-verb basis by setting `prefer_stream: true` on the request.

## Breaking wire-shape changes

None. v1 fixture replay stays intact:

- `Request` enum gains an optional `prefer_stream` field on `RecentMemory`, `RecentAudit`, and `SubmitIntent`, with `#[serde(default)]` and `#[serde(skip_serializing_if = "Option::is_none")]`. A v1 client that omits the field decodes into `prefer_stream: None` and serializes back without the field, so existing fixtures round-trip unchanged.
- `Response` enum is unchanged. Streaming responses use a sibling `StreamEnvelope` enum on the same wire and never replace a `Response` variant.

## Affected surfaces

- `Request::RecentMemory { prefer_stream: Some(true) }` routes to `Server::stream_recent_memory`, which emits `StreamEnvelope::StreamBegin { response_kind: "memories" }` → N `StreamChunk` frames (one per `MemoryRecord`, sequence counting from `0`) → `StreamEnd { summary: None }`. Per-chunk schema: `covenant.ipc.v2.chunk.memory-record.v1`.
- `Request::RecentAudit { prefer_stream: Some(true) }` routes to `Server::stream_recent_audit`, emitting `response_kind: "audit_events"` with per-chunk schema `covenant.ipc.v2.chunk.audit-event.v1`.
- `Request::SubmitIntent { prefer_stream: Some(true) }` routes to `Server::stream_submit_intent`, emitting `response_kind: "intent_result"` with a single `StreamChunk` carrying the `AgentResult` (text + sources, runtime_events emptied since they were already folded into the audit chain) and a `StreamEnd { summary: Some({ intent_id, status, settlement }) }` carrying the IntentResult bookkeeping. Per-chunk schema: `covenant.ipc.v2.chunk.agent-result.v1`.
- Any non-IntentResult/non-Memories/non-AuditEvents response variant (e.g., `Response::Error` from capability or budget failure) is written as a v1-shape terminal frame on the same connection. ADR 0010 explicitly allows daemon-decides-not-to-stream fallback.

## Fixture files

- `agent-os/crates/covenant-ipc/tests/fixtures/protocol-info.v1.json` updated: `max_supported` flips from `1` to `2`.
- `agent-os/crates/covenant-ipc/tests/fixtures/v2/stream-envelope-begin.v2.json` added: pins the canonical `StreamBegin` wire form (`kind`, `stream_id`, `response_kind`).
- `agent-os/crates/covenant-ipc/tests/fixtures/v2/stream-envelope-end.v2.json` added: pins the canonical `StreamEnd` wire form for the success-terminal case with `summary = None`. Only `kind` and `stream_id` appear on the wire — `serde(skip_serializing_if = "Option::is_none")` drops the `summary` key when absent, matching the inline serde test in `covenant-ipc`.
- `agent-os/crates/covenant-ipc/tests/fixtures/v2/stream-envelope-error.v2.json` added: pins the canonical `StreamError` wire form (`kind`, `stream_id`, `message`). The stream is dead after this frame; no further `StreamChunk` or terminal envelopes are permitted for the same `stream_id`. Example `message` matches the inline serde test's canonical sample.
- `agent-os/crates/covenant-ipc/tests/fixtures/v2/stream-envelope-chunk.v2.json` added: pins the canonical `StreamChunk` envelope wrapper (`kind`, `stream_id`, `sequence`, `chunk`). The `chunk` body is a placeholder reused from the inline serde test; per-verb chunk-body fixtures (matching the `covenant.ipc.v2.chunk.memory-record.v1`, `audit-event.v1`, and `agent-result.v1` schemas) remain a future slice scoped to each streamable verb's payload.
- `agent-os/crates/covenant-ipc/tests/fixtures/v2/stream-envelope-end-with-summary.v2.json` added: pins the with-summary `StreamEnd` wire form used by `SubmitIntent` streams (three keys: `kind`, `stream_id`, `summary`). Placeholder `summary` contents (`intent_id`, `status`, `settlement: null`) pin the envelope key shape without coupling to SubmitIntent's IntentResult internals.

## Client expectations during rollout

- A pre-v2 client never sends `prefer_stream` and continues to receive v1 terminal frames. No client-side change required.
- A v2-aware client reads `ProtocolInfo.max_supported` on every connection and gates `prefer_stream: true` behind `max_supported >= 2`. Sending `prefer_stream: true` to a daemon that advertises `max_supported = 1` is undefined; the daemon may ignore the field and return a v1 frame, but the client should not assume it.
- The client MUST handle BOTH `Response` (v1 terminal) AND `StreamEnvelope` (v2 streaming) frames on the same connection after sending `prefer_stream: true` — the daemon decides per-verb whether to honor streaming. A capability failure or non-streamable variant returns a v1 `Response::Error` frame even when the client requested streaming. The `Frame` decoder in `covenant-ipc` handles both shapes.
- Stream IDs (`stream_id`) are scoped to the IPC connection. A client SHOULD NOT treat `stream_id` as globally unique across the daemon; concurrent streams on different connections may share IDs.
