diff --git a/crates/maestro-daemon/src/projection.rs b/crates/maestro-daemon/src/projection.rs index 349a1bc..fb0a47f 100644 --- a/crates/maestro-daemon/src/projection.rs +++ b/crates/maestro-daemon/src/projection.rs @@ -438,6 +438,16 @@ mod tests { .unwrap() } + #[test] + fn a_session_update_tag_the_acp_dependency_does_not_know_fails_to_deserialize() { + // `SessionUpdate` has no unknown-variant fallback, so this never reaches `Projection::apply`. + let result: Result = serde_json::from_value(serde_json::json!({ + "sessionId": "acp-1", + "update": {"sessionUpdate": "some_future_update_variant"}, + })); + assert!(result.is_err()); + } + #[test] fn folds_content_tool_diff_plan_and_notice_rows() { let mut projection = Projection::default(); diff --git a/docs/STRUCTURED-VIEW.md b/docs/STRUCTURED-VIEW.md index 34b4a4c..92400cf 100644 --- a/docs/STRUCTURED-VIEW.md +++ b/docs/STRUCTURED-VIEW.md @@ -113,13 +113,24 @@ crate. | `ToolDiff` | The same tool-call machinery, when the tool's content includes a `Diff` block. | Same `tool:` identity as `ToolCall`; the *kind* flips between the two as the tool's own content changes, not the row. | | `Approval` | A daemon-originated permission request (`RequestPermissionRequest`), serialized without its nonce. | `approval:` — minted fresh per request, unrelated to the ACP request's own id. | | `Plan` | A `plan` update — the agent's current step list. | The fixed id `"plan"`. Each new plan **replaces** the row wholesale; entries are not diffed against the previous plan. | -| `Notice` | The raw JSON of any `session/update` variant the projection does not special-case. | `notice:`. | +| `Notice` | The raw JSON of any `session/update` variant the ACP dependency already decodes but the projection does not special-case. | `notice:`. | -`Notice` is the projection's forward-compatibility seam: ACP adds -notification variants over time, and an unrecognised one becomes a generic -row instead of being silently dropped or making the daemon reject the whole -stream. A client that does not know what to do with a `Notice` can render it -as an opaque, collapsed line — that is a reasonable floor, not a client bug. +`Notice` covers known-but-unspecialized `SessionUpdate` variants: ones the +`agent-client-protocol` dependency's schema already deserializes (it currently +also folds `available_commands_update` and `session_info_update` there, +alongside anything gated behind an unstable ACP feature this build doesn't +enable) but the projection's `match` doesn't fold into one of the other seven +kinds. It is not a wire-level forward-compatibility seam: `SessionUpdate` is +an internally-tagged enum with no unknown-variant fallback, so a +`session/update` whose `sessionUpdate` tag this build's dependency has never +heard of fails to deserialize before the projection ever runs, rather than +becoming a `Notice`. A genuinely new ACP variant needs the +`agent-client-protocol` dependency upgraded (and, typically, the projection's +`match` taught about it) before it can appear on this transcript at all — see +`projection.rs`'s test asserting that an unrecognised `sessionUpdate` tag is a +deserialization error, not a `Notice`. A client that does not know what to do +with a `Notice` can still render it as an opaque, collapsed line — that +remains a reasonable floor, not a client bug. Rendering is entirely a client concern. The projection's job stops at folding ACP into these eight shapes; markdown formatting, diff rendering, @@ -139,8 +150,11 @@ choices, not daemon ones. - Approval nonce minting, secrecy, and the single-active-approval rule (see below) — a client never sees an ACP permission request before the daemon has already turned it into an `Approval` row and a nonce. -- What happens to an ACP update variant nobody has taught the daemon about - yet (falls back to `Notice` rather than erroring). +- What happens to an ACP update variant the dependency decodes but nobody has + taught the projection to special-case (falls back to `Notice` rather than + erroring). A variant the dependency itself can't decode is a deserialization + error, not a `Notice` — see [The eight transcript row + kinds](#the-eight-transcript-row-kinds). - Persistence and replay: what is written to the event log, in what order, and how a restart resumes it.