diff --git a/designs/protocol/agent-control-api-atem.md b/designs/protocol/agent-control-api-atem.md new file mode 100644 index 0000000..d97aa0e --- /dev/null +++ b/designs/protocol/agent-control-api-atem.md @@ -0,0 +1,471 @@ +# Agent control API: Atem's response to Astation's proposal (r2) + +Status: Atem response r2, 2026-10-17. r1 answered "Agent control API: +Astation and Atem joint proposal" r1 ([Astation #52](https://github.com/Agora-Build/Astation/issues/52)); +r2 answers Astation's review of r1 (four required points, accepted +corrections) and its proposal r2. Baseline: Atem `main` at `53577a7` (#75), +contract [atem-session v1 r7.2](atem-session-v1.md), [agent chat host](../agent-chat-host.md), +[chat developer guide](../agent-chat-api.md). **Today** means in the code at +`53577a7`; **r8** means the draft in the contract's appendix "r8 draft" +(exact wire shapes) with the `r8-*` fixtures in `fixtures/` (parsed, not run +until r8 ships). Nothing r8 is emitted before both sides pass its fixtures. +Addendum 2026-10-18 (§9): two user-approved r8 features, chat Send that +acquires the lease (`chat.send_acquire`, from Astation's "Chat Send and +session control" handoff) and lease-free approval answers +(`approval.lease_free`); contract appendix §F and §G, against `main` at +`976e154` (r7.3, #76 and #77). Revised 2026-10-18 for Astation's r3 +review of that addendum: submitter principals and a durable control fence +(contract §H), one admission persistence rule (§I), the client rule for a +late admitting reply, and the fixture corrections (§9, end). Revised again +for Astation's r4 review: a failed fence write can't lose a revocation +(clean-close record; recovered queued work after any other end is +rejected, contract §H; §9, end). + +## 0. Answers to the r1 review + +**1. Version admission (high).** Agreed. r8 `hello` carries `revision: 8`; +an r8 host refuses a `hello` without it, or with another value, with +`error{code:"unsupported_version", revision: 8}` before any attachment or +session work, and keeps refusing everything until a correct `hello` is +welcomed. `welcome` echoes `revision` and `features`; the client checks the +revision before anything else, refuses an older host outright and enables +only advertised features. Removed forms (`open.before`, `open.generation`, +the `status.page` marker) are refused explicitly with +`unsupported_version`, never read as a newest-page open. The envelope stays +`v: 1`: one gate for every family, at admission, with an explicit refusal; a +new `v` would need the same check in three parsers and still carry no +feature list. Appendix A. Fixtures: `r8-hello-without-revision-8-is-refused-before-any-attachment`, +`r8-client-refuses-a-host-without-revision-8`, +`r8-client-enables-only-advertised-features`, `r8-open-with-before-is-refused`. + +**2. Page/live ordering (medium).** Resolved by an explicit ordered-stream +guarantee, no new host field. r1's unsequenced `history_page` is dropped: +every page, lookups included, is snapshotted and enqueued in one step of the +broker's single-threaded pass, as sequenced envelopes in the conversation's +one stream, between live events. **Today** this already holds for the open +page and for #75's `open{before}` page (synchronous `ChatSession::page`, +each envelope numbered by `next_head`, ending with `status.page`). A +message's `sequence` is its position: newer versions always carry higher +sequences, so a client keeps the highest-sequence version per `messageId` +and a page never replaces a newer one. Astation puts its HTTP/SSE cursor +(the conversation sequence) on each page and applies the same rule; a reopen +starts a new stream. Appendix B. Fixture: +`r8-history-page-and-live-events-share-one-ordered-stream` (host side; the +delayed-HTTP-page merge fixture is Astation's). + +**3. Bounded recovery (medium).** Agreed. `history` takes exactly one of +`before`, `turn_id`, `request_id`, `next` (validated formats and lengths; +otherwise the new `invalid_request`). Every page is bounded by 50 messages, +256 KiB content and 1 MiB serialized (sum of its envelopes), lookups +included. A continuation is an opaque `next` bound to attachment, +conversation, target, generation, selector and snapshot; out of context or +stale it is `stale_target`. `end: true` means the selected result is +exhausted. `coverage` is `complete`|`partial`|`unavailable`. +`receipt.result.reply_message_ids` holds at most 32 ids plus `more: true`; +`history{request_id}` pages carry the rest; a `receipts` reply is bounded by +construction (≤ 64 × ≤ 6 KiB). Appendix B, D. Fixtures: +`r8-long-turn-is-recovered-in-bounded-pages`, +`r8-receipt-result-bounds-reply-message-ids`, +`r8-history-selectors-are-exclusive-and-validated`, +`r8-stale-or-foreign-next-token-is-stale-target`. + +**4. Truncation vs gaps (medium).** Agreed; r1's "any truncated flag means +reopen" is withdrawn. Truncation is persistent coverage information +(`oldest_retained`, `truncated: true`), shown and never a reset. Interior +holes (worker backlog drops a reload can't restore) are reported as +persistent `holes: [{after_message_id, before_message_id, count?}]`, in +`history_end` and in a sequenced `coverage` event; reopen can't repair them. +A client resyncs only on a sequence gap, a fenced conversation, another +generation or `close` with a `reason`. **r8 implementation change:** today +(#75) a backlog gap makes the broker reload the worker's history and then +send `error` + `close`, forcing a reopen; in r8 it reloads, sends the +recovered messages in sequence (`after_message_id`), and reports what stayed +missing as a hole, with no close. Appendix C. Fixtures: +`r8-truncated-history-reopens-once-then-stays-live`, +`r8-client-shows-truncation-and-resyncs-only-on-a-sequence-gap`, +`r8-backlog-hole-is-reported-not-reopened`. + +**Accepted corrections.** All of them, as Astation states them. Codex chat is +built (#73; Codex 0.162.1 with trusted hooks; first prompt typed in the +terminal; `opened.unavailable` adds `error_details`). Always allow is not +Claude-only (Codex: an Atem session rule, exact command and cwd, this +process). One host conversation per target and attachment, fanned out by +Astation. Pages are batches, not pending jobs; the end marker is now +`history_end`. A completed prompt receipt can be an interrupted turn (today +`detail: "done"|"interrupted"`, #75). `receipt.result` reply ids are a +projection of retained history. **Today after #75:** a broker restart +restores the worker's whole retained history (500 / 8 MiB) with approval +cards, and a card pending at adoption becomes `closed` (the host released +that approval), so recovered cards never revive a released approval; this +still needs the joint test. + +## 1. Agreement and ownership + +Agreed. Atem owns the original agent process, the provider adapters, agent +events, chat history and its retention, approvals (hooks, decisions, session +rules) and execution outcomes (receipts). Astation owns the protected API, +the event stream and its replay, the native UI and notifications. Astation +never invents a reply, approval resolution or successful receipt. Neither side +keeps backward compatibility; both update together. **Today**, the split already +holds in the code: +- Every chat message carries its real `origin` (`local` for prompts typed in + the terminal, `remote` for chat submits). The prompt's user message and the + replies to it carry its `requestId` (`chat/session.rs`, `ChatMessage`). +- Approvals reach every attachment with `control` whether or not anyone holds + the lease. An answer may take the lease with it (`acquire: true`). The + session inbox (`approval_request`/`approval_closed`) is authoritative (§6a). + In r8 answers no longer touch the lease (§9). +- Every remote mutation is a session-v1 submission with a durable receipt + (`FileJournal`, fsync per transition). A duplicate returns the recorded + receipt and is never run again. A prompt's text leaves the journal when its + receipt leaves `accepted`. + +## 2. Q1: providers and capabilities + +**Today.** + +| | Claude Code | Codex | +|---|---|---| +| Versions | 2.1.x; verified on 2.1.301 | built in #73; verified on codex-cli 0.162.1 | +| Sources | per-launch `--settings` hooks (SessionStart, UserPromptSubmit, Pre/PostToolUse, Notification, Stop) plus the session JSONL transcript | Atem entries in `$CODEX_HOME/hooks.json` (`chat/codex/mod.rs` `EVENTS`) plus the rollout JSONL | +| Descriptor `capabilities` | `["terminal-v1","chat-v1"]` once SessionStart arrives (15 s, else a notice; a late one still enables it) | `["terminal-v1","chat-v1"]` from launch when Atem's hooks are installed (`[codex] hooks` yes) | +| `opened.unavailable` | `["streaming","reasoning"]` | `["streaming","reasoning","error_details"]` | +| Always allow | the provider's own `permission_suggestions` (persistent rule) | an Atem session rule (`scope:"session"`, r7.2): Codex fails closed on `updatedPermissions` | + +- **Capability model.** A healthy adapter puts `chat-v1` in the descriptor's + `capabilities`. `opened.unavailable` lists what that adapter's chat can't + provide (`adapter.rs` `UNAVAILABLE_*`), so clients state it instead of waiting. + `open` without chat-v1 gets `error{code:"busy"}` with the text "chat is not + available for this agent[: reason]". Process state and `activity` + (`ready`/`working`/`waiting`/`unknown`) are separate descriptor fields. +- **Codex first-prompt rule.** Codex reports nothing before its first prompt + (SessionStart is lazy). Chat opens from launch, but status stays `unknown` and + a chat submit is refused with `FIRST_PROMPT` ("type a first prompt in the + terminal: …"). This follows the 2026-10-17 ruling: text typed into Codex's + startup dialogs could answer them. The first hook pins the session and names + the rollout. (`agent-chat-host.md` was corrected in #75.) +- **Correction to the proposal.** "Always allow uses an exact provider-offered + rule, Claude only" is wrong since r7.2. Codex cards offer allow once, deny, + and "Always allow `` in for this session". Atem's worker keeps + that rule for the agent process and answers a later byte-equal request itself + (card `auto_allowed` with `rule_id`, no approval, no push). +- **Which Codex qualifies.** A Codex build qualifies when (a) its `hooks.json` + runs these events with stdin JSON carrying `session_id`, `transcript_path` + and `turn_id`: SessionStart, UserPromptSubmit, Pre/PostToolUse, + PermissionRequest (synchronous), Stop, Interrupt and SessionEnd, and the user + has trusted Atem's entries in `/hooks`, and (b) its rollout + `rollout-*-.jsonl` writes `event_msg` records `task_started{turn_id}`, + `item_completed{turn_id, item}` (with items `UserMessage{id, content[{type:"text",text}]}`, + `AgentMessage{id, content[{type:"Text",text}], phase}` and + `CommandExecution{id, command[], …}`), `task_complete{turn_id}` and + `turn_aborted{turn_id}`. Atem verifies this on 0.162.1 only. Other versions are + untested. +- **Format change.** A known record that lacks the ids the reader relies on is a + fatal `Problem`. Chat is withdrawn: open conversations get `error` and `close`, + `chat-v1` leaves the descriptor, and the terminal and approvals keep working. + The reader skips record types and items it doesn't know. Claude behaves the + same way for a transcript format change. + +## 3. History + +**Today** (`53577a7`): `open` sends `opened` (0) and the newest page (50 +messages / 256 KiB) as sequenced `message{history:true}`, with no end +marker. `open{before}` on an open conversation pages without a reset: the +older page continues the live sequence and ends with a sequenced `status` +carrying `page{before, count, start}` (an empty page is the marker alone). +A `before` not retained, or of another `generation`, is `stale_target`. A +worker backlog `gap` triggers a reload of the worker's history, then `error` +"the chat history was incomplete…" and `close`. Retention: 500 messages / +8 MiB per `process_gen` in the broker and the worker (one message ≤ 128 KiB); +the worker's backlog while no broker reads it is 4096 items / 8 MiB, oldest +dropped first (`gap`); adoption restores the whole retained history with +cards. No chat history on disk. `/clear`, resume and Codex `/new` keep the +generation and close conversations ("switched"); a new process starts empty. + +**r8** (appendix B, C): `open` ends with `history_end`; `history` with one +selector; bounded pages with `next`, `end`, `coverage`, `oldest_retained`, +`truncated`, `holes`; the `coverage` event; `close.reason`. + +## 4. Request → reply recovery + +**Today:** a completed `prompt` receipt's `detail` is the turn outcome +(`done`|`interrupted`, journaled as that word only). Messages carry +`turnId`, and a remote prompt's messages carry its `requestId`. The broker's +turn id per receipt is in memory only. + +**r8** (appendix D): `result{turn_id, user_message_id, outcome, +reply_message_ids ≤ 32, more, replies}`; ids only in the journal, reply ids +computed from retained history when sent; the rest by +`history{request_id}`. An `executing` prompt across a broker restart stays +`outcome_unknown` without `result`, is never re-run, and its messages remain +findable by `history{request_id}` while retained. + +## 5. Q4: approval parts + +**Today** (`adapter.rs` `ApprovalCard`, `host_bridge.rs`, contract §6a). Each +approval of a session is one assistant `message` with `messageId` +`approval-` and one part. Every state change re-sends the whole +message, and the client replaces it: + +```json +{"messageId":"approval-88888888-…","role":"assistant","turnId":"…","createdAt":1760600000000, + "parts":[{"id":"approval","kind":"approval","approval_id":"88888888-…","tool":"Bash", + "summary":"npm test","state":"pending", + "always_rules":[{"rule_id":"s-0123456789abcdef","label":"Always allow `npm test` in Atem for this session","scope":"session"}]}]} +``` + +- `state` (`ApprovalCardState`): `pending`; then `allowed`, `always_allowed` or + `denied` (answered remotely, set at the hand-off); `answered_terminal`; + `expired` (`timed_out`); `closed` (`interrupted`, `released`, `process_ended`, + or a remote answer the agent didn't take, whose outcome is unknown, or that + passed the 630 s deadline); `auto_allowed` (a session rule allowed it; final + on arrival, with `rule_id` and `always_rules: []`). `note` is a short + explanation, e.g. that the session rule wasn't kept (`ApprovalRuleNotKept`). +- Correlation: `approval_id` equals the session `approval_request.approval_id`. + A card leaves `pending` when `approval_closed` is sent. The session inbox + decides whether an answer is possible. A card is display only and never + authority. Cards go only to conversations of attachments with `control`, + in history and live. `opened.approvalRules` and `rules` events carry the + active session rules. +- Errors for an approval answered from chat follow its receipt: + `rejected`/`outcome_unknown` with the receipt's `detail`, or the admission + code (`stale_target` for a closed or unknown approval and for a `rule_id` + it didn't offer, `stale_lease`, `forbidden`, `session_closed`). +- Withdrawal: when chat is withdrawn, switched or restarted, conversations + get `error` and `close`. The approval stays in the session inbox and can be + answered there (approvals are not a chat capability). A broker restart + releases every approval ("no decision", `approval_closed{released}`); since + #75 its cards survive in history as `closed`. + +**r8** (feature `approval.card_reason`): every final card except +`auto_allowed` carries `reason`, the `approval_closed` reason +(`answered_remote`, `answered_terminal`, `interrupted`, `timed_out`, +`released`, `process_ended`). Clients then never have to map `closed` back to a +cause. The state names above stay as they are. + +## 6. Versioning + +r8, gated by `hello.revision` (appendix A, answer 0.1). Features: +`chat.history_end`, `chat.history`, `chat.lookup`, `chat.coverage`, +`receipt.result`, `approval.session_rules` (r7.2, shipped), +`approval.card_reason`, `chat.send_acquire`, `approval.lease_free` (§9). A side advertises or uses a feature only after its +`r8-*` fixtures pass in Rust (Atem) and Swift (Astation). + +## 7. Work + +**Atem work** (S ≤ 1 day, M ≤ 3 days): + +| # | Item | Size | +|---|---|---| +| A1 | `hello.revision` admission, `welcome.revision`/`features`, refusal of removed forms | S | +| A2 | `history_end`; `history` with selectors, validation, three bounds, `next` tokens (MAC, context binding) | M | +| A3 | Coverage: `oldest_retained`, `truncated`, holes; backlog gap → reload + `coverage` event (replaces #75's error + close); `close.reason` | M | +| A4 | `receipt.result` (journal ids with `completed`; reply ids from history, ≤ 32 + `more`) | M | +| A5 | Card `reason`; contract r8 text replaces r7.2 parts; docs | S | +| A6 | Fixture runner actions (schema.md "r8 draft"), drop `requires`; live check with Claude 2.1.301 and Codex 0.162.1 | M | +| A7 | `chat.send_acquire`: submit-level `acquire`/`takeover`/`expected_controller`, admission and commit after the durable write, `lease` on the admitting reply, `controller` on `lease_held`, prompt static checks at admission, submitter principal and durable control fence (§H), the admission persistence rule with its refusal list (§I) | M | +| A8 | `approval.lease_free`: drop the lease check and `acquire` (refused), ignore `lease_epoch`; docs | S | + +**Joint fixtures** (in `fixtures/`, r8 draft, NOT RUN until r8 ships): +`r8-hello-without-revision-8-is-refused-before-any-attachment`, +`r8-client-refuses-a-host-without-revision-8`, +`r8-client-enables-only-advertised-features`, +`r8-open-with-before-is-refused`, +`r8-history-page-and-live-events-share-one-ordered-stream`, +`r8-long-turn-is-recovered-in-bounded-pages`, +`r8-truncated-history-reopens-once-then-stays-live`, +`r8-client-shows-truncation-and-resyncs-only-on-a-sequence-gap`, +`r8-backlog-hole-is-reported-not-reopened`, +`r8-receipt-result-bounds-reply-message-ids`, +`r8-history-selectors-are-exclusive-and-validated`, +`r8-stale-or-foreign-next-token-is-stale-target`, and for §9 the fourteen +§F fixtures (`r8-send-*`, `r8-two-simultaneous-sends-acquire-once`, +`r8-ownership-is-checked-at-admission-not-as-displayed`, +`r8-local-input-wins-over-a-send`, `r8-stale-takeover-confirmation-is-refused`, +`r8-duplicate-send-never-reacquires-or-replays`, +`r8-disconnect-right-after-acceptance`, `r8-failed-persistence-changes-no-lease`, +`r8-expired-lease-is-checked-at-admission`, +`r8-revoked-control-refuses-the-send-and-its-queued-prompt`, +`r8-process-replaced-before-dispatch`), the §H/§I and late-reply ones +listed in §9, and the five §G ones (`r8-approval-*`). +Still to write with the implementation: card `reason`, `receipt.result` +after a broker restart and for an interrupted turn. + +## 8. Notes on Astation's API layer (suggestions, Astation owns these) + +- **One host conversation per target.** Atem allows at most 8 chat + conversations per session (`MAX_CONVERSATIONS`). A ninth closes the oldest + with `close`. Astation's link is one attachment, so per-reader `open`s for + native UI, API clients and scripts would evict each other. Suggestion: keep + one host conversation per (`session_id`, `process_gen`) and fan it out to every + `chat_id` in Astation's read model. An attachment without `control` sees no + cards, so open the shared conversation from the controlling attachment and + filter per reader. +- **SSE replay is Astation's.** Atem provides `generation`, a per-conversation + `sequence` (also each message version's position), the session registry + `seq`/`runtime_epoch` and receipts. Resynchronize by reopening only on a + sequence gap, a fenced conversation, another generation or `close` with a + `reason`; truncation, holes and `coverage` events are shown, never a + reason to reopen. Atem keeps no replay window per client. +- **Notification triggers** from host events: + +| Notification | Host event | +|---|---| +| Approval needed / resolved | session `approval_request` / `approval_closed` (not the chat card) | +| Turn completed | chat `complete` (`outcome`), or `receipt` `completed` with `result.outcome` for API prompts | +| Request rejected / unknown | `receipt` `rejected` / `outcome_unknown` | +| Agent waiting | descriptor `activity:"waiting"` | +| Session ended | descriptor `state` `exited`/`interrupted`/`recovery_required`/`removed` | +| Control lost | lease `stale_lease` / registry `controller` change; attachment fenced (`stale_target`) | + +Backfilled `message{history:true}` and repeated `approval_request` after `list` +are catch-up and must not create new alerts. + +**Voice readiness.** `messageId`, `turnId`, `requestId`, `origin`, +`createdAt` and the receipt `result` ids are stable within a `process_gen`. A +later voice-brainstorm feature (`designs/voice-context.md`, branch +`docs/voice-context`) can cite turns and messages by these ids without new +host state. That feature is not designed here. + +## 9. Chat Send and lease-free approvals (addendum, 2026-10-18) + +**Chat Send (`chat.send_acquire`, appendix §F).** This answers the +handoff's open points: + +- **Shape.** A `prompt` submit (or chat-v1 `submit` with `text`) carries + `acquire: true`, and for a confirmed takeover also `takeover: true` and + `expected_controller: {controller_session, lease_epoch}`, the lease the + dialog showed. The binding then has no `lease_epoch`. Holding the lease, + a client may still send the r7 form with `lease_epoch`, checked as + today. +- **Transaction.** One step of the broker's single-threaded pass. It checks + binding, fields, duplicate, `control`, a running session with `chat-v1`, + then ownership at that instant (expiry applied first), the prompt's + static checks, limits and the epoch reservation. Then the durable + `accepted` write, and only then the lease grant or takeover, + `lease_changed` to the others, and the reply with + `lease{lease_epoch, ms_left}`. Any failure before the commit means no + lease change and no execution. A failed journal write is `busy`, a + definitive refusal (§I below). This is today's approval-`acquire` order + (`lease_to_acquire` → journal append → `grant`), with the takeover made + explicit. +- **Codes.** `lease_held` (now with `controller`) for another holder or a + stale confirmation; `stale_lease` for the same controller on another + attachment or a wrong `lease_epoch` in the r7 form; `forbidden` without + `control`, before any lease state; `invalid_request` for the field rules + and a prompt's static checks; `busy` for a failed durable write or a + session without `chat-v1`; `unsupported_version` when the feature isn't + advertised. +- **Identity.** The flags are admission parameters, outside the digest and + the immutable request, like `lease_epoch`. A duplicate returns the + recorded receipt and never acquires, takes over or reruns, so the client + keeps one `request_id` and text from the first Send through refusal, + confirmation and uncertain delivery. A retry can't become + `request_id_reused`, which would tempt a second run. +- **Scope.** `prompt` only. `interrupt` would act on another controller's + turn, the silent ownership change Send must never make. `stop` ends every + lease anyway. Both still need an explicit Take Control. +- **Renewal.** Astation renews: the native app while that session's chat or + terminal view is in the foreground, then `release`. For an API caller, + renewal happens only while the caller keeps renewing; an abandoned caller + loses control within 15 s. The host never renews implicitly: not on chat + reads, queries, duplicates, a Send under the own lease, or execution. +- **New host rule.** A principal that loses `control` is fenced durably + (below): its leases are revoked and its not-yet-executing accepted work + rejected (`control revoked`). Local input still revokes, and the + adapter's unsent-input check still refuses a queued prompt at dispatch. +- **Late admitting reply.** The client applies a receipt's `lease` only if + the submit is outstanding on the current attachment, no newer epoch for + that session arrived since it was sent, and the deadline (send time + + `ms_left`) is ahead; otherwise it stays a viewer (contract §F). + +**Lease-free approvals (`approval.lease_free`, appendix §G).** User decision +2026-10-18, replacing r7's answer-takes-the-lease. An answer needs a verified +attachment with `control` and the exact pending request. It never takes, +needs or moves the lease, and a `lease_epoch` in its binding is ignored. +`acquire` is refused `unsupported_version` (removed form). The first +admitted answer wins and later or closed ones are `stale_target`. Rules +still need the exact offered rule and scope (r7.2), and revoke stays r7.3. +Card states and push are unchanged. + +**r3 review answers (2026-10-18).** + +- **Finding 1, revoked control (§H).** Accepted work records its submitter + *principal* (the authenticated identity: today the one paired Astation; + with account trust each device grant) and the principal's `control_gen`. + Revoking `control` writes a durable fence (`control_gen` raised) before + anything else; queued work of every attachment of that principal, live + or gone, is rejected `control revoked`, and the dispatch check (also for + work recovered after a restart or adoption) compares the recorded + `control_gen` with the current one. Disconnect, expiry and a + same-permission reconnect keep work; regaining control doesn't revive + work from before the revocation. After a restart, recovered work runs + only once its principal is confirmed in that run (60 s, else `control + not confirmed`). (Superseded by the r4 answer below: confirmation alone + can't recover a fence lost to a failed write.) +- **Finding 2, persistence (§I).** Accepted means the `accepted` record is + durable. A failed admission write is `busy` and definitive: nothing + changes, the journal halts until restart, the refusal is recorded on a + separate refusal list first, and at restart the request is `rejected` + ("acceptance not confirmed") whether or not its record reached the disk. + §6's double-I/O exception and the same-id retry after such a `busy` are + removed. If even the refusal can't be recorded, the host sends no `busy` + (delivery is uncertain and the journal decides at restart). A resend uses + a new `request_id`: the old one is final, so it can't run twice. +- **Finding 3.** The two captures in + `r8-failed-persistence-changes-no-lease` are at the step level (the + scenario now follows §I); no other r8 fixture had one inside `match`. +- **Other obligations.** `chat_opens` is 2 in + `r8-long-turn-is-recovered-in-bounded-pages`; `wire_bytes` is 675 in + `r8-client-shows-truncation-and-resyncs-only-on-a-sequence-gap`; the + schema's `open_approval` row follows control-based delivery. New + fixtures: `r8-approval-answer-keeps-the-holders-deadline` (non-zero + clock, deadline unchanged, queued input kept, no holder), + `r8-delayed-send-reply-does-not-regrant-lost-control` and + `r8-client-delayed-send-reply-never-restores-control`, + `r8-revoked-control-fences-work-across-reconnect-and-restart`, + `r8-control-fence-holds-when-a-rejection-write-fails`, + `r8-recovered-work-needs-its-principal-confirmed`, + `r8-double-write-failure-takeover-never-runs`, + `r8-crash-after-durable-acceptance-grants-no-lease`. Astation owns its + API/native arbitration and renewal tests; approval persistence, the + terminal-answer race and catch-up are run against the r8 implementation. + +**r4 review answers (2026-10-18).** + +- **Finding 1, failed fence write (§H).** Queued work whose authorization + history can't be established never runs. In the run: if any write of a + revocation fails, the host revokes the lease and rejects the principal's + queued work in memory at once (`control revoked`), halts, and dispatches + nothing queued (of anyone) until the restart. Across a restart: the + journal gets a clean-close record, written only at an orderly shutdown + with a healthy journal and bound to that run's `runtime_epoch`; after any + run without one (crash, kill, halted journal, failed fence) every + recovered accepted request not yet executing is `rejected` + "authorization history unknown", for every principal, whatever reached + the disk, before anything is dispatched. The 60 s confirmation stays, + after a clean close only: it covers revocations made while the host was + down. Precedence at restart: "acceptance not confirmed" (refusal list) + first, then "authorization history unknown", then (clean close only) + `host trust changed`, `control revoked`, `control not confirmed`; all are + `rejected`, so the detail never changes an outcome. Cost: after a crash, + queued prompts that never started are resent with a new `request_id`; + executing work stays `outcome_unknown`. Fixtures: + `r8-failed-fence-write-rejects-queued-work-at-once`, + `r8-failed-fence-write-then-control-restored-never-runs` (both rollback + outcomes), `r8-crash-rejects-queued-work-whose-history-is-unknown`, + `r8-clean-close-runs-queued-work-once-its-principal-is-confirmed`; + `r8-recovered-work-needs-its-principal-confirmed` now starts from a clean + close; `r8-crash-after-durable-acceptance-grants-no-lease` and + `r8-control-fence-holds-when-a-rejection-write-fails` now end + "authorization history unknown". +- **Finding 2, fresh controller.** The second welcome in + `r8-client-delayed-send-reply-never-restores-control` names a new + `controller_session`. No other fixture reuses one across attachments + (`r8-client-refuses-a-host-without-revision-8` repeats a welcome on the + same attachment, which is not a replacement). +- **Implementation obligations.** Listed under §H's fixtures in the + contract: a halted journal blocks queued raw terminal writes at the PTY + write boundary; refusal-list write, rename and replay faults are + idempotent and never let a refused request run; clean-close write and + removal crash points. diff --git a/designs/protocol/atem-session-v1.md b/designs/protocol/atem-session-v1.md index ce39581..c742ab9 100644 --- a/designs/protocol/atem-session-v1.md +++ b/designs/protocol/atem-session-v1.md @@ -1,5 +1,13 @@ # atem-session v1: sessions, control leases, receipts and approvals (r7.3: r7.2 with lease-free rule revocation — for review) +Draft (not implemented): r8 — required `hello.revision`, `welcome.features`, +history pages in the sequenced stream with bounded selectors and coverage, +`receipt.result`, removal of `open.before`, chat Send that acquires the lease +with the prompt (`chat.send_acquire`) and lease-free approval answers +(`approval.lease_free`), with the submitter principal and its durable +control fence (§H) and one admission persistence rule (§I) — is in the +appendix "r8 draft" at the end; this +r7.3 text stays authoritative until r8 ships. Status: r7.3 (2026-10-17) lets any attachment with `control` revoke an Atem session rule (§6a) with or without the lease: `approval_rule_revoke` needs only `control`, a running session and an active rule of that exact process @@ -325,7 +333,8 @@ Normative rules: is persisted per session so it never repeats. - **Scope**: the lease governs terminal input/resize/stop, chat submission, interrupt, approval answers and future mutations. Disconnect stops renewal; - the lease then expires. + the lease then expires. (r8 draft: approval answers leave the lease's scope, + appendix §G; a chat Send may acquire the lease with its prompt, §F.) ## 6. Submissions and receipts @@ -365,7 +374,11 @@ Kinds and payloads (v1): the client may retry with the same `request_id`. After a double I/O failure (the write fails and so does undoing it) a request answered `busy` may still be dispatched once, under its original target and trust generation; a retry - with the same `request_id` returns its receipt. + with the same `request_id` returns its receipt. (r8 draft: this exception + and the same-id retry are **removed**; a `busy` for a failed admission + write is a definitive refusal, the journal halts, and the request is + resolved `rejected` "acceptance not confirmed" at restart and never + executed; appendix §I.) - A duplicate `submit` names the current target: one naming an earlier `process_gen` of the session is `stale_target`, while `receipt_query` for that earlier target still returns its receipts. @@ -419,6 +432,11 @@ Kinds and payloads (v1): `accepted` work has provably not started any side effect, so it may still be dispatched — once, under its original target, and only if the host trust generation is unchanged (else `rejected`, `detail:"host trust changed"`). + (r8 draft: only when the previous run ended with a clean close, and then + only once its submitter principal's control is confirmed in the new run + at the recorded `control_gen`; after any other end it is `rejected` + "authorization history unknown", appendix §H; a request on the refusal + list is `rejected` first, §I.) `executing` work may have started a side effect: it is never dispatched again; it is reconciled with the worker/provider, and if that can't establish the outcome it becomes `outcome_unknown`. @@ -441,6 +459,13 @@ agent's own prompt asks. Delivery is independent of control (user decision, and is pushed to the account's devices, whoever holds the lease; answering takes the lease with the answer (`acquire`). +**r8 draft (appendix §G, `approval.lease_free`, user decision 2026-10-18):** +an answer needs only a verified attachment with `control` and the exact +pending request. It never takes, needs or moves the lease, its binding's +`lease_epoch` is ignored, `acquire` is refused `unsupported_version`, and +the first admitted answer wins (later ones `stale_target`). This r7 text +stays authoritative until r8 ships. + | op | dir | fields | |---|---|---| | `approval_request` | H→C | `approval_id` (UUID), `session_id`, `process_gen`, `tool`, `summary`, `created_at`, `always_rules: [{rule_id, label, scope}]` | @@ -769,3 +794,781 @@ reconnection, an old signature or a relay timestamp; rollback protection and conservative restart/clock handling), channel key establishment with replay/downgrade protection binding endpoint keys, account, roles, permissions and generations, the `resume_proof` format, and QR grant binding. + +## Appendix: r8 draft (not implemented; r7.3 above stays authoritative) + +Status: **draft**, 2026-10-17, answering Astation's review of Atem's agent +control API response r1 (`agent-control-api-atem.md` r2). Nothing here is +emitted or accepted by any build until r8 ships on both sides together and +the `r8-*` fixtures (`fixtures/schema.md`, "r8 draft fixtures") pass in Rust +and Swift. Then this appendix replaces the affected r7.2 text. No backward +compatibility: an r8 host and an r8 client refuse every r7 peer. + +Revised 2026-10-18 for Astation's review of the §F/§G draft (r3 review): +control is a permission of an authenticated **principal**, recorded with +accepted work and revoked through a durable fence that outlives +attachments, reconnects and restarts (§H, finding 1); one persistence rule +for admission (§I, finding 2): a request is accepted only once its +`accepted` record is durable, a `busy` for a failed write is a definitive +refusal resolved `rejected` ("acceptance not confirmed") at restart, and +§6's double-I/O exception (a request answered `busy` "may still be +dispatched once") is **removed**, as is "the client may retry with the +same `request_id`" after such a `busy` (a resend uses a new id, §I.5). +Fixtures: the captures in `r8-failed-persistence-changes-no-lease` moved +to the step level and the scenario now follows §I; new +`r8-double-write-failure-takeover-never-runs`, +`r8-crash-after-durable-acceptance-grants-no-lease`, +`r8-revoked-control-fences-work-across-reconnect-and-restart`, +`r8-control-fence-holds-when-a-rejection-write-fails`, +`r8-recovered-work-needs-its-principal-confirmed`, +`r8-approval-answer-keeps-the-holders-deadline`, +`r8-delayed-send-reply-does-not-regrant-lost-control` and the client +scenario `r8-client-delayed-send-reply-never-restores-control`; the two +r2 history corrections (`chat_opens` 2, `wire_bytes` 675) are applied. + +Revised 2026-10-18 for Astation's r4 review (a failed fence write could +lose a revocation if control was restored before the next +authentication): queued work whose authorization history can't be +established never runs. A failure while writing a revocation rejects +that principal's queued work at once in memory and nothing queued is +dispatched after a journal halt (§H, "A failed revocation write"); the +journal gets a **clean close** record, and after any run that didn't end +with one every recovered accepted request not yet executing is `rejected` +"authorization history unknown", for every principal (§H, "Recovery"). +The 60 s confirmation stays, after a clean close only. Fixtures: new +`r8-failed-fence-write-rejects-queued-work-at-once`, +`r8-failed-fence-write-then-control-restored-never-runs` (rollback +succeeds, then rollback fails), `r8-crash-rejects-queued-work-whose-history-is-unknown`, +`r8-clean-close-runs-queued-work-once-its-principal-is-confirmed`; +`r8-recovered-work-needs-its-principal-confirmed` now starts from a clean +close, and `r8-crash-after-durable-acceptance-grants-no-lease` and +`r8-control-fence-holds-when-a-rejection-write-fails` now end +`rejected` "authorization history unknown"; the second welcome of +`r8-client-delayed-send-reply-never-restores-control` names a new +`controller_session` (§3). Schema: `crash_runtime`, and `restart_runtime` +is an orderly stop. + +### A. Version admission + +The envelope stays `v: 1`; the revision named in `hello` is the gate. One +mechanism covers every family (a new `v` would need the same check in +session, terminal and chat parsers and still say nothing about features), and +the refusal is explicit at admission rather than a silent misreading later. + +| op | dir | r8 fields | +|---|---|---| +| `hello` | C→H | **`revision`** (required, the JSON integer `8`), `resume?` | +| `welcome` | H→C | r7.2 fields plus **`revision: 8`** and **`features: [string]`** | + +- An r8 host answers a `hello` with no `revision`, another number or a + non-integer with session `error{re, code:"unsupported_version", revision: + 8, detail}` and does nothing else: no attachment, no `welcome`, no lease, + receipt, subscription or conversation. Every later request on that + transport is refused as before `welcome` (in its family, + `unsupported_version`) until a `hello{revision: 8}` is welcomed. +- The client checks `welcome.revision == 8` before anything else; without + it (an r7 host ignores the unknown `hello.revision` and welcomes without + one) the client refuses the attachment: it sends nothing further and + applies nothing it receives, and tells the user the host's Atem must be + updated. +- `features` lists what this host implements. The client enables only the + advertised features it knows (unknown names are ignored) and never sends + an op of a feature it didn't see; an operation that needs a missing + feature is shown unavailable. A host never advertises a feature before its + fixtures pass; an op of an unadvertised feature is `unsupported_version`. + Per-session abilities stay in the descriptor's `capabilities`. + +| feature | adds | +|---|---| +| `chat.history_end` | `open` ends with a sequenced `history_end` (§B) | +| `chat.history` | `history{before|next}` pages in the sequenced stream (§B) | +| `chat.lookup` | `history{turn_id|request_id}` (§B) | +| `chat.coverage` | `oldest_retained`, `truncated`, `holes`, `coverage`, the `coverage` event, `close.reason` (§C) | +| `receipt.result` | `result` on completed `prompt` receipts (§D) | +| `approval.session_rules` | r7.2 session rules (already shipped) | +| `approval.card_reason` | final cards carry `reason` (the `approval_closed` reason) | +| `chat.send_acquire` | a `prompt` submit may acquire the lease with the prompt (§F) | +| `approval.lease_free` | approval answers need `control`, never the lease (§G) | + +- **Removed in r8**, refused explicitly with `unsupported_version` and no + effect (never read as another form): chat `open.before`, `open.generation` + (the r7.2/#75 paging form; an open conversation is not reset and no new + one opens), the `status.page` marker (replaced by `history_end`), a + `hello` without `revision`, and `acquire` on an approval answer (r7 §6a; + in the payload or beside it, §G). + +### B. History pages in the ordered conversation stream + +**Ordering guarantee.** A conversation is one ordered stream with one +`sequence` counter. The host builds a page in a single step of its +single-threaded broker pass: it snapshots the retained history and enqueues +the page's envelopes with consecutive sequences, and no live event of that +conversation is enqueued between them. Every live event produced after the +snapshot has a higher sequence. (Today's code already does this: +`host_bridge.rs` `open_conversation` builds the open page and, since #75, the +`open{before}` page plus its `status.page` marker synchronously from +`ChatSession::page`, numbering every envelope with the conversation's +`next_head`; r8 makes it a contract.) + +Each message envelope's `sequence` is its position: a message version +carried at a higher sequence is newer. A client keeps, per `messageId`, the +version with the highest sequence and never lets a page replace a newer +version. No per-message revision is needed because the stream is the order. +Astation, which returns pages over HTTP and changes over SSE, attaches its +own replay cursor (the conversation sequence) to each page and applies the +same rule. A reopen starts a new stream (`opened` at 0), so positions are +compared within one `opened` only. + +`open` (no `before`, no `generation`) replies `opened` (sequence 0), the +newest page as `message{history: true}`, then `history_end`. Older pages +and lookups: + +```json +{"type":"agentChat","data":{"v":1,"op":"history","conversationId":"conv-1","agentId":"ses-aaaaaaaaaaaa", + "generation":"22222222-2222-4222-8222-222222222222","page_id":"p7","turn_id":"turn-9", + "binding":{"target":{…},"attachment_gen":"33333333-…"}}} +``` + +- **Selectors**: exactly one of `before` (a retained `messageId`: the page + ends just before it), `turn_id`, `request_id` (the turn whose user message + carries it) or `next` (a continuation token, which carries its selector). + None, two or more → chat `error{code:"invalid_request"}`. +- **Validation**: `before`, `turn_id`: 1–128 bytes of `[A-Za-z0-9._:-]`; + `request_id`: lowercase v4 UUID; `next`: 1–512 bytes of base64url; + `page_id`: 1–64 bytes of `[A-Za-z0-9._:-]`, chosen by the client; + `generation` must equal `binding.target.process_gen` and the conversation + must be open on this attachment (`stale_target`). At most 8 history + requests a second per conversation (`busy`). +- **Reply**: `message{history: true, page_id}` envelopes, then + `history_end`, all sequenced: + +```json +{"type":"agentChat","data":{"v":1,"op":"history_end","conversationId":"conv-1","agentId":"ses-aaaaaaaaaaaa", + "generation":"22222222-…","sequence":58,"page_id":"p7", + "page":{"selector":"turn_id","count":28,"content_bytes":168000,"wire_bytes":1017000, + "end":false,"next":"","coverage":"complete", + "oldest_retained":"m-0001","truncated":true,"holes":[]}}} +``` + + `page_id` is absent on the open's `history_end` (`selector: "newest"`). +- **Order**: `newest`/`before` pages go backwards (each page lists its + messages oldest first; `next` continues with older ones). `turn_id`/ + `request_id` pages go forwards from the turn's oldest retained message to + its newest message at the first page's snapshot; later messages of the + turn arrive live. +- **Bounds** (every page, lookups included): ≤ 50 messages, ≤ 256 KiB of + content (message bytes as counted by `ChatSession`), and ≤ 1 MiB + serialized (the sum of the page's envelopes, so Astation can return it as + one body); a page stops before the message that would pass any bound and + holds at least one message when one remains (a message is ≤ 128 KiB of + content, < 1 MiB however escaped). Cards filtered out for a view-only + attachment don't count. +- **`end`**: `true` when the selected result is exhausted (no older retained + message, or the turn's last message at the snapshot was sent); then there + is no `next`. +- **`next`**: opaque, made by the host (MAC under a key of this runtime), + binding the attachment, `conversationId`, full target (session and + `process_gen`), selector and value, the continuation point and the history + snapshot (the history's reload count). Used in another conversation, + attachment or target, altered, after its continuation point was evicted, + after a history reload, or after a runtime restart → chat + `error{code:"stale_target", page_id}`; the client starts a new selection. +- **`before` not retained** → `stale_target` (as #75). A `turn_id`/ + `request_id` with nothing retained is not an error: `count: 0`, `end: + true`, `coverage: "unavailable"`. +- Errors for a history request are sequenced in the conversation and carry + its `page_id` when that was valid; nothing else is sent and nothing resets. + +### C. Coverage: truncation and holes + +- **`coverage`** of the selected result: `complete` (every message of it is + retained), `partial` (some were evicted or fall in a hole; for a lookup, + the turn's oldest messages were evicted or a hole lies inside the turn), + `unavailable` (none is retained). +- **Truncation** is persistent: `oldest_retained` (the oldest retained + `messageId`, `null` when empty) and `truncated: true` once anything of this + process's history was evicted (retention 500 messages / 8 MiB, or an + adoption cut). It is shown, never a reset; clients keep consuming the + stream and never reopen because of it. +- **Holes** are gaps inside the retained timeline: items the worker's + backlog dropped (`gap`) that a reload of the worker's history could not + restore. `holes: [{after_message_id, before_message_id, count?}]` names the + retained neighbours (`before_message_id: null` at the live edge until the + next message); `count` when known. A `history_end` lists the holes whose + `before_message_id` is in its page (≤ 16, then `holes_more: true`); holes + older than `oldest_retained` become truncation. Holes are persistent and + no reopen repairs them. +- **Backlog gap handling (r8 implementation change from #75).** Today a + worker chunk with `gap > 0` makes the broker reload the worker's history + and then send every open conversation `error` "the chat history was + incomplete…" and `close`, forcing a reopen. In r8 the broker reloads, sends + each recovered message to open conversations as a sequenced + `message{history: true, after_message_id}` (timeline order; `after_message_id` + is the retained message it follows), and reports what stayed missing in a + sequenced event, with no `error` or `close`: + +```json +{"type":"agentChat","data":{"v":1,"op":"coverage","conversationId":"conv-1","agentId":"ses-aaaaaaaaaaaa", + "generation":"22222222-…","sequence":9,"oldest_retained":"m-0001","truncated":false, + "holes":[{"after_message_id":"m-0004","before_message_id":"m-0006","count":1}]}} +``` + + A reload that fails (worker gone, over the reload bound) reports the gap as + a hole with `count` when known. +- **Resync** (reopen) only on: a sequence gap in the conversation, a fenced + conversation (`stale_target` for its attachment, a new attachment), another + `generation`, or `close` with a `reason`. r8 `close` carries `reason`: + `switched` (`/clear`, resume, `/new`), `withdrawn` (chat withdrawn), + `restarted` (new process), `evicted` (a ninth conversation), `fenced`, + `reset` (any other host reset). Truncation, holes and `coverage` events + never ask for one. + +### D. `receipt.result` + +```json +{"type":"session","data":{"v":1,"op":"receipt","request_id":"44444444-…","target":{…},"kind":"prompt", + "state":"completed","detail":"done","updated_at":1760600000000, + "result":{"turn_id":"turn-7","user_message_id":"turn-7-u","outcome":"done", + "reply_message_ids":["turn-7-a-0001","…","turn-7-a-0032"],"more":true,"replies":"complete"}}} +``` + +- On `completed` `prompt` receipts only. `turn_id`, `user_message_id` and + `outcome` (`done`|`interrupted`, equal to `detail`) are journaled with the + `completed` transition (ids only, never text). `reply_message_ids` (the + turn's assistant messages, timeline order, **at most 32**), `more` (true + when more replies are retained) and `replies` (`complete`|`partial`| + `unavailable`, the turn's coverage as in §C) are computed from retained + history each time the receipt is sent, so they may change with late + content or eviction. The rest come from `history{request_id}`; nothing is + ever re-run to recover replies. An `outcome_unknown` prompt has no + `result`. +- Bounds: a receipt with `result` is ≤ 6 KiB (32 ids ≤ 128 bytes each, plus + r7.2 bounds); a `receipts` reply holds ≤ 64 receipts (r7.2's + `request_ids` bound), so ≤ 384 KiB, below the 1 MiB message bound without + continuation. + +### F. Chat Send acquires the lease with the prompt (`chat.send_acquire`) + +User-approved behaviour (2026-10-18, Astation handoff "Chat Send and session +control"). Chat and the remote terminal share one lease per session; viewing +either needs none. Remote terminal input still needs an explicit Take +Control (§5 `acquire`). A chat Send needs no separate step: + +| ownership at admission | Send | +|---|---| +| nobody | acquires and submits in one host operation | +| this attachment | submits under its current lease | +| another attachment | refused `lease_held` naming the holder; only an explicit, confirmed takeover takes it | + +**Wire.** A §6 `submit` of kind `prompt`, or a chat-v1 `submit` with +`text`, may carry `acquire: true`, and with it `takeover: true` plus +`expected_controller`, the lease the user confirmed taking over, in the +descriptor's `controller` shape (§4). Its binding then carries no +`lease_epoch` (as for §5 `acquire`): + +```json +{"type":"session","data":{"v":1,"op":"submit","id":"…", + "binding":{"target":{…},"attachment_gen":"33333333-…"}, + "request_id":"44444444-…","kind":"prompt","payload":{"text":"run the tests"}, + "acquire":true}} +{"type":"session","data":{"v":1,"op":"submit","id":"…", + "binding":{"target":{…},"attachment_gen":"66666666-…"}, + "request_id":"44444444-…","kind":"prompt","payload":{"text":"run the tests"}, + "acquire":true,"takeover":true, + "expected_controller":{"controller_session":"…","lease_epoch":7}}} +{"type":"agentChat","data":{"v":1,"op":"submit","conversationId":"…","agentId":"ses-aaaaaaaaaaaa", + "generation":"22222222-…","requestId":"44444444-…","text":"run the tests","acquire":true, + "binding":{"target":{…},"attachment_gen":"33333333-…"}}} +``` + +- **Field rules** (each `invalid_request`, nothing recorded, no lease + change): `acquire` and `takeover` appear only as `true`; `takeover` needs + `acquire` and `expected_controller`, and `expected_controller` needs + `takeover`; `acquire` excludes `binding.lease_epoch`. Only `prompt` (chat: + `text`) may acquire. `interrupt`, `stop` and `approval_rule_revoke` with + `acquire` are `invalid_request`; an approval answer with `acquire` is the + removed r7 form, `unsupported_version` (§G). Without the advertised + feature, `acquire` is `unsupported_version` (§A). +- **Why `prompt` only.** The user approved Send. `interrupt` acts on the + turn the current controller started: an acquiring interrupt from someone + else is exactly the silent ownership change Send must never make, and on + an unowned session an explicit Take Control first loses nothing (a raced + interrupt is refused `stale_lease`, never misapplied). `stop` ends the + process and with it every lease (`process_ended`), so acquiring for it + means nothing. Either can become its own feature later. +- **Without `acquire`** a `prompt` is the r7 submit: the current lease + (`lease_epoch`) is checked as in §6 and a wrong one is `stale_lease`. A + client that holds the lease sends this form; a client that doesn't (or no + longer knows) sends `acquire: true`. + +**Request identity.** `acquire`, `takeover` and `expected_controller` are +admission parameters, like `binding.lease_epoch` and `attachment_gen`: they +are in neither the payload digest nor the immutable request (§6 unchanged: +`payload` is `{"text"}`). A duplicate `request_id` returns the recorded +receipt whatever flags it carries; it never acquires, takes over, renews or +re-executes. Why: a retry must be safe whatever the client believed about +ownership when it resent. The client keeps the `request_id` and text from +the first Send through a `lease_held` refusal, the takeover confirmation +and any uncertain delivery. If the first attempt was in fact accepted (its +reply lost), the confirmed retry gets the original receipt instead of +`request_id_reused`, which would tempt a new id and a second run. Flags in +the digest would buy nothing, since a duplicate never acts on them. A +refused Send records nothing, so the retry is a new admission. Before +offering a new Send or a takeover after an uncertain delivery, the client +checks the receipt (`receipt_query`). Neither a query nor a duplicate +acquires. + +**Admission** (one step of the broker's single-threaded pass, so no other +admission, local input or lease change comes between its checks and its +commit). The order follows today's `SessionHost::submit`: + +1. Binding (attachment, full target, trust and process generation): + `unauthorized`/`stale_target`/`unknown_session` as §2. +2. Decode and field rules: `unsupported_version`/`invalid_request`. +3. Duplicate `request_id`: the recorded receipt (view only, no `lease` + field) or `request_id_reused`. +4. `control` → `forbidden`: the attachment's principal holds `control` + now (§H). Control authorizes the operation; the lease only coordinates + writers. +5. A session that accepts work → `session_closed`; one whose descriptor + lacks `chat-v1` → `busy` ("chat is not available for this agent"), so + nobody takes control for a prompt that can't run. +6. Ownership, on the host's monotonic clock at this instant (a lease with + `now >= deadline` has expired: the host applies the expiry first, with + its `lease_changed{expired}`, as on any clock step). Never as Astation + last displayed it: + - held live by this attachment: **own**, no lease change; + - held by this `controller_session` on another attachment: `stale_lease` + (as §5 `acquire`); + - held by another, no `takeover`: `lease_held`; + - held by another, `takeover` with an `expected_controller` other than + the current (`controller_session`, `lease_epoch`): `lease_held` (a + stale confirmation never authorizes a different takeover); + - held by another, `takeover` matching: a **takeover** is planned; + - nobody: a **grant** is planned (also with `takeover`: nobody's control + is taken, so the confirmation isn't needed). +7. The prompt's static checks: text ≤ 32 768 bytes after trimming, not + starting with `/` or `!` (`ChatSession`'s `clean_prompt`), now at + admission → `invalid_request`. Dynamic ones (status ready, no unsent + terminal input, the same generation) stay at dispatch (`rejected` with + the adapter's text), as in r7. +8. `receipt_limit`; the attachment's in-flight bound → `busy`. +9. A planned grant or takeover reserves its epochs durably (§5 epoch + blocks). Failure → `busy`. +10. `accepted` is written durably (fsync), with the submitter principal + and its `control_gen` (§H). A failure here or in step 9 is §I: `busy`, + no lease change, nothing executes, the journal halts, and after the + restart the request is `rejected` ("acceptance not confirmed"); a + resend uses a new `request_id` (§I.5). Epochs reserved in step 9 may + then be skipped, which §5 allows. +11. **Commit.** Only now the lease moves: a grant or takeover gives this + attachment a new epoch with the full 15 s (capped by its authorization); + a takeover also raises the input epoch, so the previous holder's queued + bytes are dropped at the write boundary. Every other attachment with + `view` gets `lease_changed{granted|takeover}` and subscribers get + `session_changed`. Then the reply, then scheduling as any prompt (§6). + +This is today's approval `acquire` transaction (`lease_to_acquire` reserves +before the journal append, `grant` runs only after it succeeds), with two +differences. The takeover must be explicit and must match +`expected_controller`. And the sender gets its lease in the reply, not in +`lease_changed`, per §5's recipient rule. + +**Reply.** The reply that admits the Send carries the sender's lease: +session `receipt{state:"accepted", …, lease:{lease_epoch, ms_left}}`, or +chat-v1 `accepted{requestId, lease:{lease_epoch, ms_left}}`. That covers a +grant, a takeover and own. For own the epoch is unchanged and `ms_left` is +what remains: a Send never extends a lease (§5 rule 4). The client applies +it as the reply to an `acquire` sent when the Send was sent (§5 rules 2–5: +deadline = send time + `ms_left`). No other receipt carries `lease`: not +`executing`/final ones, duplicates or `receipts`. A client whose admitting +reply was lost doesn't control. If the registry shows it as controller, an +explicit `acquire` (the repeat-acquire rule) returns the current lease +unchanged. + +**Refusal.** `lease_held` names the holder in the descriptor's shape so the +UI can offer "Take over and send": + +```json +{"type":"session","data":{"v":1,"op":"error","re":"…","code":"lease_held", + "controller":{"controller_session":"…","lease_epoch":7}}} +{"type":"agentChat","data":{"v":1,"op":"error","conversationId":"…","requestId":"44444444-…", + "code":"lease_held","text":"another controller holds this session", + "controller":{"controller_session":"…","lease_epoch":7}}} +``` + +r8 adds `controller` to every `lease_held`, the §5 `acquire` refusal +included. The host names only the opaque `controller_session`. Mapping it +to a person or device is Astation's job (its link is one attachment, and it +arbitrates its own clients before they reach the host). Nothing in a +refusal is recorded or executed, and no takeover happens. + +**Renewal and release.** The lease is the attachment's, as after §5 +`acquire`: it lasts 15 s and its holder renews it every 5 s (§5). Astation +owns renewal. The native app renews while that session's chat or terminal +view is in the foreground on the device whose user sent. It stops when the +view closes, the app goes to the background or the device sleeps, and then +sends `release` (best effort; otherwise the lease expires). For the +protected local API, the caller holds the lease explicitly: Astation renews +only while that caller keeps renewing through the API and releases when the +caller's API session ends. An abandoned caller loses control within 15 s. +The host renews nothing implicitly: not chat `open`, `history`, terminal +`subscribe`, `receipt_query`, a duplicate submit, a Send under the own +lease, nor execution of an accepted prompt. Expiry, release, local input or +a takeover don't cancel accepted work (§6). + +**Unchanged.** Local terminal input revokes any remote lease before it is +written (`local_input`, §5). A Send queued behind a running turn is then +refused at dispatch by the adapter's unsent-input check. A Send's prompt is +a §6 receipt with every r7 check and transition. Process replacement ends +the lease (`process_ended`) and rejects accepted prompts (`session closed`); +a trust change rejects them (`host trust changed`). New in r8: a +principal that loses `control` is fenced durably (§H): leases held by any +of its attachments are revoked (`lease_changed{revoked}`), and its +`accepted` work not yet `executing`, from any of its attachments, live or +gone, is rejected (`detail:"control revoked"`), now or at dispatch after a +restart; executing work isn't cancelled. In compatibility mode this +happens when the link re-authenticates with fewer permissions, before the +new attachment is welcomed. + +Fixtures: `r8-send-acquires-an-unowned-session`, +`r8-send-under-the-own-lease-checks-the-epoch`, +`r8-send-refused-while-another-holds-then-takeover`, +`r8-two-simultaneous-sends-acquire-once`, +`r8-ownership-is-checked-at-admission-not-as-displayed`, +`r8-local-input-wins-over-a-send`, +`r8-stale-takeover-confirmation-is-refused`, +`r8-duplicate-send-never-reacquires-or-replays`, +`r8-disconnect-right-after-acceptance`, +`r8-failed-persistence-changes-no-lease`, +`r8-expired-lease-is-checked-at-admission`, +`r8-revoked-control-refuses-the-send-and-its-queued-prompt`, +`r8-process-replaced-before-dispatch`, `r8-send-acquire-wire-rules`, +`r8-delayed-send-reply-does-not-regrant-lost-control` and, client side, +`r8-client-delayed-send-reply-never-restores-control`. + +**Client rule for the reply's `lease`** (normative; §5 rules 1–5 apply as +to an `acquire` reply). The `lease` on an admitting receipt is applied only +if (a) its `re` is a submit still outstanding on the current attachment (a +reply to a submit sent on a replaced or fenced attachment is dropped: the +receipt state may be shown, the lease never), (b) its `lease_epoch` is not +lower than the highest `lease_epoch` the client has seen for that session +since the submit was sent (from `lease_changed`, `session_changed`, +`sessions` or another `lease`): a lower one was superseded by local input, +takeover, expiry, release or revocation while the reply was in flight, and +(c) the deadline it gives (submit send time + `ms_left`) is still ahead +(`now < deadline`). Otherwise the client stays a viewer; only an explicit +`acquire` or a new Send regains control. The host side is unchanged: a +superseded grant is already gone there, so the old epoch is `stale_lease`. + +### G. Lease-free approval answers (`approval.lease_free`) + +User decision, 2026-10-18. It replaces r7's "answering takes the lease with +the answer (`acquire`)". Answering a permission prompt is a decision about +that prompt, not writer coordination: it doesn't need the lease and never +moves it. + +- **Payload** (strict): `{approval_id, decision, rule_id?, message?}`, as + r7 without `acquire`. `acquire` is refused `unsupported_version` as a + removed form, with no effect: in the payload (the r7 form) or beside it + (§F's submit field). The binding's `lease_epoch` is optional and ignored, + as for `approval_rule_revoke` (r7.3): a stale epoch or someone else's + isn't refused. chat-v1 answers (`submit` with `approval`) follow the same + rules. +- **Admission**, in order: a duplicate `request_id` returns its receipt + (view only); `control` → `forbidden` (a verified attachment with + `control`; view-only ones get no card and can't answer); a session that + accepts work → `session_closed`; the approval pending for exactly the + binding's `session_id` and `process_gen`, and an `allow_always` + `rule_id` it offered, else `stale_target`. No lease check of any kind. +- **Never moves the lease.** No grant, takeover, renewal or release, no + `lease_changed`, no `session_changed`. The holder keeps its epoch and + deadline, and its queued input isn't fenced. A non-holder's answer is + handed over like the holder's. +- **First answer wins.** The first answer the host admits while the request + is pending wins. In the same step it goes `executing` and the approval + closes (`approval_closed{answered_remote}`, push resolved). A later + answer, from the holder or anyone, is `stale_target` and nothing of it is + recorded; so is one to a request already closed (`answered_terminal`, + `timed_out`, `interrupted`, `released`, `process_ended`). Holding the + lease gives no priority. A duplicate of the winning answer returns its + receipt. The terminal's own prompt still races as in r7 + (`outcome_unknown` when both crossed). +- **Rules** are granted only with the exact rule and scope the card showed + (`allow_always` names an offered `rule_id`; r7.2 unchanged). Rule revoke + stays r7.3: `control`, no lease, `acquire` refused. +- Unchanged from r7: delivery to every attachment with `control` whoever + holds the lease, push, the receipt lifecycle, the 630 s hand-off deadline, + release on Codex input, restart behaviour, and lease changes never + closing an approval. + +r8 text for §6a then reads: the intro's "answering takes the lease with the +answer (`acquire`)" becomes "answering needs `control`, never the lease"; the +`submit` row drops `acquire?`; the payload, admission and "Acquire is +atomic" paragraphs are replaced by the rules above; §5 "Scope" drops +approval answers; §6's admission note on approval answers with `acquire` +becomes "approval answers skip the lease check (§G)". + +Fixtures: `r8-approval-answer-by-a-non-holder-leaves-the-lease`, +`r8-approval-answer-keeps-the-holders-deadline` (answers by the holder +and a non-holder at t = 6000/9000 leave the holder's deadline at 15 000, +its queued input is written, and an answer with no holder grants +nothing), `r8-approval-answer-with-acquire-is-refused`, +`r8-approval-answers-crossing-first-wins`, +`r8-approval-answer-to-a-closed-request-is-stale-target`. + +### H. Submitter principal and the control fence (r8) + +Answers the r3 review's finding 1. Control is a permission of an +authenticated principal, not of an attachment, and accepted work keeps the +authority it was accepted under only while that principal keeps `control`. + +- **Principal.** The authenticated identity an attachment speaks for, set + by the host from the link's authentication, never from message fields + and never from the relay (relay labels are display only). Today + (compatibility mode) there is exactly one: the paired Astation the link + captured and `cred_state.json` verified, under the current + `host_trust_gen`. With account trust (`../account-trust.md` §3.3) each + device with a signed grant on the account is its own principal (its + verified device id); Astation's native and API callers stay inside + Astation's principal, and Astation arbitrates them (§F). `attachment_gen` + and `controller_session` are not principals: every connect, + re-authentication or replacement (r7.1) makes new ones for the same + principal. +- **Control generation.** Per principal the host keeps, durably in the + journal, whether it holds `control` and a `control_gen`: an integer from + 1 that never decreases and is never reset (kept like the lease-epoch + floors). Every revocation of `control` raises it by one. Granting + `control` again keeps the raised value; it never restores an old one. +- **Recorded at acceptance.** The `accepted` record carries the submitter + `principal` and its `control_gen` at admission. Like `attachment_gen` + this is admission metadata: it is in neither the payload digest nor the + immutable request (§6), so duplicates and `request_id_reused` are + unchanged (a duplicate needs only view). +- **Revocation is a durable fence.** When the host learns that a principal + lost `control` (today: its link re-authenticates with fewer permissions; + with account trust also: its grant is removed or lapses, + `../account-trust.md` §3.4), it does in one step of the pass: + 1. write the fence `control_revoked{principal, control_gen}` (the raised + value) durably (fsync); + 2. fence the principal's live attachments that had `control`, and revoke + a lease held by any of its attachments (`lease_changed{revoked}`; the + input epoch rises, so queued bytes are dropped); + 3. reject each of its `accepted` requests not yet `executing`, whichever + of its attachments submitted it, live or gone (`rejected`, + `detail:"control revoked"`, each persisted); + 4. only then welcome the new attachment. + + Executing work isn't cancelled (§6). +- **A failed revocation write** (r4 review). If any write of that step + fails (the fence in step 1, or a rejection in step 3), whatever the + rollback did, the host goes on in memory, in the same step: it fences + the principal's live attachments that had `control` and revokes the + lease as in step 2 (the input epoch rises, so the worker drops queued + remote bytes at the write boundary; the epoch of that `lease_changed` is + not persisted, which is harmless: clients order by `runtime_epoch` + first, and the restart starts a new one), and marks every one of the + principal's `accepted` requests not yet `executing` `rejected`, + `detail:"control revoked"`, in memory, sent to any live attachment + entitled to the receipt (today none: the principal's attachments were + just fenced). The journal halts (§I): every later session request but + `hello` is `busy`, the new link is closed without a `welcome`, and + **nothing queued is dispatched after a journal halt**, of any principal + (nor any remote terminal input admitted), until the restart. These + in-memory rejections aren't persisted; the restart rejects the same + requests anyway, because a halted run never writes a clean close + (Recovery, below), so they can never run. +- **Clean close.** At an orderly shutdown (SIGTERM/SIGINT, idle exit, + replacement) whose journal is healthy (not halted, no failed write in + this run), after admission has stopped and the last other journal write + is durable, the host writes `/clean_close` + (`{"runtime_epoch": n}`, the run's own epoch; temp, fsync, rename, + directory fsync). A crash, a kill, a halted journal or a failed write + (including a failed fence) means no clean close. At startup the host + reads it before incrementing `runtime_epoch`: the previous run ended + cleanly only when the file exists, parses and names the stored epoch. + Then it removes the file (and the incremented epoch supersedes it in any + case, so a removal that fails can't vouch for a later crash). An + unreadable or stale file counts as no clean close. +- **Dispatch check.** Before an `accepted` request becomes `executing` + (queued in this run, or recovered after a broker restart or worker + adoption) the host checks its recorded principal: it must hold `control` + now, with `control_gen` equal to the recorded one. Otherwise `rejected`, + `detail:"control revoked"`, never executed. Kinds dispatched in the step + that admits them (`interrupt`, `stop`, approval answers, rule revokes) + pass trivially. +- **Same principal, new connection.** A disconnect, a reconnect, a + re-authentication with the same permissions or an attachment replacement + leaves `control_gen` unchanged, so accepted work goes on, as it does + after lease expiry or release (§6). A principal that regains `control` + after a revocation has no authority over work accepted before it: that + work carries the older `control_gen` and was rejected at the fence, or is + at dispatch. +- **Recovery** (broker restart, with worker adoption). Before anything is + dispatched, every recovered `accepted` request not yet `executing` is + resolved in this order, each resolution persisted: + 1. on the refusal list (§I.3) → `rejected`, `detail:"acceptance not + confirmed"`; + 2. the previous run did **not** end with a clean close → `rejected`, + `detail:"authorization history unknown"`, for every principal, + whatever the journal holds (a fence on disk or not: a failed write + may have left it, so what reached the disk never changes the + outcome); + 3. after a clean close: host trust changed → `host trust changed` (§6); + `control_gen` below its principal's durable one → `control revoked`; + the rest is dispatched only once its principal's control is + confirmed in this run: today when an attachment of that principal is + welcomed with `control` (welcomed without it, the fence is written + first and the work is `control revoked`); with account trust, when its + stored grant verifies. Not confirmed 60 s after the host's start + (`now >= start + 60 000`) → `rejected`, `detail:"control not + confirmed"`. + + `executing` work is unaffected: reconciled, else `outcome_unknown` + (§6). If a resolution can't be persisted, the host starts with remote + sessions disabled (`welcome.host_error`) and dispatches nothing. + Requests already `rejected` in the journal keep their detail; the + in-memory rejections of a failed revocation write (above) were never + persisted, so the same requests come back `rejected` with step 2's + detail. Only the state is final; a detail is the best the journal can + establish. + + Why rule 2: a clean close proves every revocation this host observed is + durably fenced, so the stored `control_gen` is a complete history of + them. Without one, a revocation may have been observed and lost (the + r4 trace: the fence write fails, the host restarts, control is restored + before the principal next authenticates, and a current `control` flag + can't show the missed revocation), so no recovered queued work runs. + The cost: after a crash, queued prompts that never started must be sent + again by the client, with a new `request_id` (the old one is final, + §I.5). + + Why the 60 s confirmation stays (after a clean close only): rule 2 + covers revocations the host saw; confirmation covers those that happen + while it is down, learned only from the principal's next + authentication (welcomed without `control`, the fence is written before + anything of it runs), and the bound keeps a prompt from running long + after its sender went away. A revocation and a re-grant that both happen + entirely while the host is down are invisible to it; the authority + (today Astation, which arbitrates its callers, §F) owns that case. The + restart disconnected every client anyway; they reconnect and query. +- **Today.** One principal, whose control changes only when it + re-authenticates with fewer permissions; unpair raises + `host_trust_gen`, which already rejects accepted work (`host trust + changed`, §6). The rule is per principal so account trust adds + principals without changing it. The relay supplies none of this. + +Fixtures: `r8-revoked-control-refuses-the-send-and-its-queued-prompt`, +`r8-revoked-control-fences-work-across-reconnect-and-restart`, +`r8-control-fence-holds-when-a-rejection-write-fails`, +`r8-failed-fence-write-rejects-queued-work-at-once`, +`r8-failed-fence-write-then-control-restored-never-runs`, +`r8-crash-rejects-queued-work-whose-history-is-unknown`, +`r8-clean-close-runs-queued-work-once-its-principal-is-confirmed`, +`r8-recovered-work-needs-its-principal-confirmed`. + +Conformance obligations (r4 review; implementation tests, not wire +fixtures): fault the fence write with both rollback outcomes and the +rejection writes; check that a halted journal also blocks queued raw +terminal writes at the PTY write boundary (they don't pass through the +`accepted` → `executing` transition); fault the refusal-list write and +its atomic replacement, with crashes before and after its rename and +before and after the `rejected` transition is durable: replaying or +clearing the list is idempotent and never lets a refused request run; +and crash before, during and after the clean-close write and its removal. + +### I. Admission persistence: one rule (r8) + +Answers the r3 review's finding 2. It replaces §6's "If the durable write +of `accepted` fails … the client may retry with the same `request_id`. +After a double I/O failure … a request answered `busy` may still be +dispatched once …": that exception is removed, and a request answered +`busy` never executes. It applies to every kind and to both forms of +submit (session `submit`, chat-v1 `submit`). It matches `FileJournal` +today: an fsync per transition, the log cut back after a failed write, +and every later append refused until the journal is reopened. + +1. **Accepted means durable.** A request is accepted only when its + `accepted` record is written and fsync'd. Only then does anything act + on it: the lease moves (§F step 11), the reply is sent, the work is + scheduled. +2. **A failed admission write is a definitive refusal.** If a journal + write of an admission fails (the epoch reservation, §F step 9, or the + `accepted` record, step 10): no lease change, no `lease_changed` or + `session_changed`, no receipt, nothing scheduled. The journal halts: + until the runtime restarts and reopens it, every session request but + `hello` is `busy` (`detail:"journal halted; restart Atem"`) and nothing + becomes `executing`, which needs a durable write too. Before replying, + the host records the refusal durably apart from the failed log: the + request's immutable values (§6; no payload) go on the journal's refusal + list (`/refused`, replaced atomically: temp, fsync, rename, + directory fsync). Then the reply: `error{code:"busy", detail:"acceptance + not recorded"}`. +3. **Resolved at restart, never executed.** When the journal reopens, + before recovery dispatches anything, each request on the refusal list + is made durably `rejected`, `detail:"acceptance not confirmed"`: a + `Transition` when its `accepted` record is on disk after all (the write + failed after a partial or complete write, or the rollback failed), else + a receipt recorded directly as `rejected` (— → `rejected`, nothing + accepted) with its immutable values. Then the list is cleared. If that + can't be done, the host starts with remote sessions disabled + (`welcome.host_error`, as for a damaged `runtime_epoch`) and dispatches + nothing. What reached the disk never changes the outcome, so every + implementation agrees on it. +4. **No durable refusal, no `busy`.** If the refusal can't be recorded + either, the host doesn't answer `busy`: it closes the link with no + reply and stays halted, and the client treats the request's delivery as + uncertain. After the restart the journal decides: nothing (the request + was never accepted), or, when the `accepted` record reached the disk, an + accepted request that §H's recovery rejects (`authorization history + unknown`: a halted run never ends with a clean close). The same holds + for a crash after the `accepted` record became durable and before the + lease moved or the reply was sent. Either way it never runs and no + lease is granted: + leases aren't persisted, and a lease is only ever granted in the step + that sends the admitting reply. The client follows §F: `receipt_query` + before any new Send or takeover. +5. **Resending.** Before the restart a retry is `busy` (halted). After it, + a duplicate of the refused request returns its recorded `rejected` + receipt (view only, no `lease`) whatever flags it carries, and is never + re-admitted. To send the same text again the client uses a **new** + `request_id`. Why: the old id is final and can only ever return + `rejected`, and a new id can't run the prompt twice because the old one + is durably refused. It is the one case where the client changes ids, + and only once the old id is known to be final; §F keeps the id through + `lease_held`, the takeover confirmation and uncertain delivery because + there the old id may still run. A client that doesn't know why it got + `busy` queries after reconnecting: `rejected` (whatever the detail: + "acceptance not confirmed", "authorization history unknown") → a new + id; nothing recorded → the same id. +7. **Which detail wins at restart.** "acceptance not confirmed" (the + refusal list) is resolved first and wins over §H's "authorization + history unknown": the client was told `busy`, and the detail confirms + that refusal. Both are `rejected`, final and never executed, so the + choice changes no outcome (§H, Recovery, gives the full order). +6. Unchanged from §6: a failed `executing` write rejects the request + before any effect, and every exit from `executing` is persisted before + it is reported; any failed write halts the journal as in 2. + +Fixtures: `r8-failed-persistence-changes-no-lease` (single failure, the +retry with the same id while halted, then the restart: `rejected`, the +same id returns that receipt and isn't re-admitted, a new id is), +`r8-double-write-failure-takeover-never-runs` (a confirmed takeover while +another holds the lease: the write and its rollback fail, so the record +may be on disk; no lease change, `rejected` after the restart, never +executed, the same id returns the receipt), +`r8-crash-after-durable-acceptance-grants-no-lease` (durable, then a +crash before the grant: no lease, and after the restart `rejected` +"authorization history unknown", never executed). + +### E. Errors (r8 additions) + +| code | when | +|---|---| +| `unsupported_version` | also: `hello` without `revision: 8`; anything before such a `welcome`; `open.before`/`open.generation`; an op of an unadvertised feature; `acquire` on an approval answer (§G) | +| `invalid_request` | new: a well-formed message of a known op whose fields break its rules (history selector count, field formats or lengths; §F's `acquire`/`takeover`/`expected_controller` rules and a prompt's static checks) | +| `stale_target` | also: a `next` token used out of its context or stale; a `before` no longer retained | +| `busy` | also: more than 8 history requests a second in a conversation; an acquiring Send to a session without `chat-v1` (§F); a failed admission write (`detail:"acceptance not recorded"`, a definitive refusal) and every request after it, or after any other failed journal write such as a revocation's (§H), until restart (`journal halted; restart Atem`) (§I) | +| `rejected` receipt details | new: `control revoked` (the submitter principal lost `control`, §H), `control not confirmed` (recovered work whose principal wasn't confirmed within 60 s after a clean close, §H), `authorization history unknown` (recovered work not yet executing after a run that didn't end with a clean close, §H), `acceptance not confirmed` (a request refused `busy` for a failed write, resolved at restart, §I) | +| `lease_held` | now carries `controller{controller_session, lease_epoch}` (also for §5 `acquire`); also: an acquiring Send while another holds the lease, or a takeover whose `expected_controller` is no longer the holder (§F) | +| `forbidden` | also: a Send or approval answer from an attachment without `control`, checked before any lease state (§F, §G) | diff --git a/designs/protocol/fixtures/r8-approval-answer-by-a-non-holder-leaves-the-lease.json b/designs/protocol/fixtures/r8-approval-answer-by-a-non-holder-leaves-the-lease.json new file mode 100644 index 0000000..97aefe1 --- /dev/null +++ b/designs/protocol/fixtures/r8-approval-answer-by-a-non-holder-leaves-the-lease.json @@ -0,0 +1,438 @@ +{ + "name": "r8-approval-answer-by-a-non-holder-leaves-the-lease", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native", + "holds_lease_epoch": 1 + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + }, + { + "name": "c3", + "attachment_gen": "77777777-7777-4777-8777-777777777777", + "permissions": [ + "view" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000001" + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "sessions", + "re": "11111111-1111-4111-8111-000000000001", + "sessions.#": 1 + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000002" + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "sessions", + "re": "11111111-1111-4111-8111-000000000002", + "sessions.#": 1 + } + }, + { + "open_approval": "ses-aaaaaaaaaaaa", + "tool": "Bash", + "summary": "npm test", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "tool": "Bash", + "summary": "npm test", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + "capture": { + "a1": "approval_id" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "tool": "Bash", + "summary": "npm test", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ], + "approval_id": "${a1}" + } + }, + { + "send": "c3", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "77777777-7777-4777-8777-777777777777" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "approval", + "payload": { + "approval_id": "${a1}", + "decision": "allow_once" + } + } + } + }, + { + "expect": "c3", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000003", + "code": "forbidden" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "approval", + "payload": { + "approval_id": "${a1}", + "decision": "allow_always", + "rule_id": "r-npm-test" + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000004", + "kind": "approval" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${a1}", + "reason": "answered_remote" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${a1}", + "reason": "answered_remote" + } + }, + { + "approval_applied": "${a1}", + "ok": true + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "completed" + } + }, + { + "open_approval": "ses-aaaaaaaaaaaa", + "tool": "Bash", + "summary": "cargo check", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "tool": "Bash", + "summary": "cargo check", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + "capture": { + "a2": "approval_id" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "tool": "Bash", + "summary": "cargo check", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ], + "approval_id": "${a2}" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000005", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "lease_epoch": 7 + }, + "request_id": "44444444-4444-4444-8444-000000000003", + "kind": "approval", + "payload": { + "approval_id": "${a2}", + "decision": "deny", + "message": "not now" + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000003", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000005", + "kind": "approval" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000003", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${a2}", + "reason": "answered_remote" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${a2}", + "reason": "answered_remote" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "expect_none": "c3", + "for_ms": 0 + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "renew", + "id": "11111111-1111-4111-8111-000000000006", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease", + "re": "11111111-1111-4111-8111-000000000006", + "binding.lease_epoch": 1 + } + }, + { + "assert": { + "lease_holder": "c1", + "approval_decisions": [ + { + "approval_id": "${a1}", + "decision": "allow_always", + "rule_id": "r-npm-test" + }, + { + "approval_id": "${a2}", + "decision": "deny", + "message": "not now" + } + ] + } + } + ], + "note": "r8 approval answers need only a verified attachment with control and the exact pending request. c2 does not hold the lease and answers without lease_epoch, then with an epoch it does not hold (ignored, never checked): both are accepted and handed over; the lease never moves (no lease_changed, no session_changed) and c1 renews at the same epoch. A view-only attachment gets no card and its answer is forbidden." +} diff --git a/designs/protocol/fixtures/r8-approval-answer-keeps-the-holders-deadline.json b/designs/protocol/fixtures/r8-approval-answer-keeps-the-holders-deadline.json new file mode 100644 index 0000000..dd097b8 --- /dev/null +++ b/designs/protocol/fixtures/r8-approval-answer-keeps-the-holders-deadline.json @@ -0,0 +1,630 @@ +{ + "name": "r8-approval-answer-keeps-the-holders-deadline", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native", + "holds_lease_epoch": 1 + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000001" + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "sessions", + "re": "11111111-1111-4111-8111-000000000001", + "sessions.#": 1 + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000002" + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "sessions", + "re": "11111111-1111-4111-8111-000000000002", + "sessions.#": 1 + } + }, + { + "send": "c1", + "json": { + "type": "terminal", + "data": { + "v": 1, + "op": "subscribe", + "terminal_id": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + } + } + } + }, + { + "expect": "c1", + "type": "terminal", + "match": { + "op": "attached", + "session.terminal_id": "ses-aaaaaaaaaaaa" + }, + "capture": { + "s": "stream_id" + } + }, + { + "hold_pty_writes": true + }, + { + "send": "c1", + "json": { + "type": "terminal", + "data": { + "v": 1, + "op": "input", + "terminal_id": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "stream_id": "${s}", + "sequence": 0, + "bytes": "aGkN", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + } + } + } + }, + { + "advance_ms": 6000 + }, + { + "open_approval": "ses-aaaaaaaaaaaa", + "tool": "Bash", + "summary": "npm test", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "tool": "Bash", + "summary": "npm test", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + "capture": { + "a1": "approval_id" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "tool": "Bash", + "summary": "npm test", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ], + "approval_id": "${a1}" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "approval", + "payload": { + "approval_id": "${a1}", + "decision": "allow_once" + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000003", + "kind": "approval" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${a1}", + "reason": "answered_remote" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${a1}", + "reason": "answered_remote" + } + }, + { + "approval_applied": "${a1}", + "ok": true + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "completed" + } + }, + { + "advance_ms": 3000 + }, + { + "open_approval": "ses-aaaaaaaaaaaa", + "tool": "Bash", + "summary": "cargo check", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "tool": "Bash", + "summary": "cargo check", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + "capture": { + "a2": "approval_id" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "tool": "Bash", + "summary": "cargo check", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ], + "approval_id": "${a2}" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "approval", + "payload": { + "approval_id": "${a2}", + "decision": "deny", + "message": "not now" + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000004", + "kind": "approval" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "executing" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${a2}", + "reason": "answered_remote" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${a2}", + "reason": "answered_remote" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "acquire", + "id": "11111111-1111-4111-8111-000000000005", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "takeover": false + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease", + "re": "11111111-1111-4111-8111-000000000005", + "binding.lease_epoch": 1, + "ms_left": 6000 + } + }, + { + "flush_pty_writes": true + }, + { + "assert": { + "lease_holder": "c1", + "pty_writes_remote": 1, + "pty_writes_dropped": 0 + } + }, + { + "advance_ms": 5999 + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "advance_ms": 1 + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "expired", + "lease_epoch": 2, + "controller": null + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "expired", + "lease_epoch": 2, + "controller": null + } + }, + { + "open_approval": "ses-aaaaaaaaaaaa", + "tool": "Bash", + "summary": "npm run build", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "tool": "Bash", + "summary": "npm run build", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + "capture": { + "a3": "approval_id" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "tool": "Bash", + "summary": "npm run build", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ], + "approval_id": "${a3}" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000006", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000003", + "kind": "approval", + "payload": { + "approval_id": "${a3}", + "decision": "allow_once" + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000003", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000006", + "kind": "approval" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000003", + "state": "executing" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${a3}", + "reason": "answered_remote" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${a3}", + "reason": "answered_remote" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "lease_holder": null, + "approval_decisions": [ + { + "approval_id": "${a1}", + "decision": "allow_once" + }, + { + "approval_id": "${a2}", + "decision": "deny", + "message": "not now" + }, + { + "approval_id": "${a3}", + "decision": "allow_once" + } + ] + } + } + ], + "note": "Appendix §G at a non-zero clock. c1 holds the lease from t=0 (deadline 15000) and has terminal input queued at the writer under epoch 1. At t=6000 c1 itself answers (its lease_epoch is ignored) and at t=9000 c2, a non-holder, answers: neither renews, grants or fences anything (no lease_changed, no session_changed). Without renewing, c1's repeat acquire at t=9000 returns the same epoch with ms_left 6000 (the deadline is still 15000, it was not reset by either answer), the queued input is written, not dropped, and the lease expires exactly at 15000. With nobody holding the lease, c2 answers a third prompt: accepted and handed over, and no lease is granted." +} diff --git a/designs/protocol/fixtures/r8-approval-answer-to-a-closed-request-is-stale-target.json b/designs/protocol/fixtures/r8-approval-answer-to-a-closed-request-is-stale-target.json new file mode 100644 index 0000000..a63b8b1 --- /dev/null +++ b/designs/protocol/fixtures/r8-approval-answer-to-a-closed-request-is-stale-target.json @@ -0,0 +1,529 @@ +{ + "name": "r8-approval-answer-to-a-closed-request-is-stale-target", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native", + "holds_lease_epoch": 1 + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000001" + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "sessions", + "re": "11111111-1111-4111-8111-000000000001", + "sessions.#": 1 + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000002" + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "sessions", + "re": "11111111-1111-4111-8111-000000000002", + "sessions.#": 1 + } + }, + { + "open_approval": "ses-aaaaaaaaaaaa", + "tool": "Bash", + "summary": "npm test", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "tool": "Bash", + "summary": "npm test", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + "capture": { + "a1": "approval_id" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "tool": "Bash", + "summary": "npm test", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ], + "approval_id": "${a1}" + } + }, + { + "close_approval": "${a1}", + "reason": "answered_terminal" + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${a1}", + "reason": "answered_terminal" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${a1}", + "reason": "answered_terminal" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "approval", + "payload": { + "approval_id": "${a1}", + "decision": "allow_once" + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000003", + "code": "stale_target" + } + }, + { + "open_approval": "ses-aaaaaaaaaaaa", + "tool": "Bash", + "summary": "cargo check", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "tool": "Bash", + "summary": "cargo check", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + "capture": { + "a2": "approval_id" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "tool": "Bash", + "summary": "cargo check", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ], + "approval_id": "${a2}" + } + }, + { + "close_approval": "${a2}", + "reason": "timed_out" + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${a2}", + "reason": "timed_out" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${a2}", + "reason": "timed_out" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "approval", + "payload": { + "approval_id": "${a2}", + "decision": "allow_once" + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000004", + "code": "stale_target" + } + }, + { + "open_approval": "ses-aaaaaaaaaaaa", + "tool": "Bash", + "summary": "npm run build", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "tool": "Bash", + "summary": "npm run build", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + "capture": { + "a3": "approval_id" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "tool": "Bash", + "summary": "npm run build", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ], + "approval_id": "${a3}" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000005", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000003", + "kind": "approval", + "payload": { + "approval_id": "${a3}", + "decision": "allow_once" + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000003", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000005", + "kind": "approval" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000003", + "state": "executing" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${a3}", + "reason": "answered_remote" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${a3}", + "reason": "answered_remote" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000006", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + }, + "request_id": "44444444-4444-4444-8444-000000000004", + "kind": "approval", + "payload": { + "approval_id": "${a3}", + "decision": "deny" + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000006", + "code": "stale_target" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000007", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000003", + "kind": "approval", + "payload": { + "approval_id": "${a3}", + "decision": "allow_once" + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000003", + "state": "executing", + "re": "11111111-1111-4111-8111-000000000007" + } + }, + { + "approval_applied": "${a3}", + "ok": true + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000003", + "state": "completed" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "lease_holder": "c1", + "dispatches": 1, + "approval_decisions": [ + { + "approval_id": "${a3}", + "decision": "allow_once" + } + ] + } + } + ], + "note": "An answer to an approval that is no longer pending (answered in the terminal, timed out, or already answered remotely) is stale_target for holder and non-holder alike: nothing recorded, nothing handed over, no lease effect. A duplicate of the answer that was handed over returns its receipt." +} diff --git a/designs/protocol/fixtures/r8-approval-answer-with-acquire-is-refused.json b/designs/protocol/fixtures/r8-approval-answer-with-acquire-is-refused.json new file mode 100644 index 0000000..9c9bb62 --- /dev/null +++ b/designs/protocol/fixtures/r8-approval-answer-with-acquire-is-refused.json @@ -0,0 +1,261 @@ +{ + "name": "r8-approval-answer-with-acquire-is-refused", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native", + "holds_lease_epoch": 1 + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000001" + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "sessions", + "re": "11111111-1111-4111-8111-000000000001", + "sessions.#": 1 + } + }, + { + "open_approval": "ses-aaaaaaaaaaaa", + "tool": "Bash", + "summary": "npm test", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "tool": "Bash", + "summary": "npm test", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + "capture": { + "a1": "approval_id" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "approval", + "payload": { + "approval_id": "${a1}", + "decision": "allow_once", + "acquire": true + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000002", + "code": "unsupported_version" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "approval", + "payload": { + "approval_id": "${a1}", + "decision": "allow_once" + }, + "acquire": true + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000003", + "code": "unsupported_version" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "lease_holder": "c1", + "approvals_pending": 1, + "dispatch_attempts": 0, + "receipts_visible": { + "c2": 0 + } + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "approval", + "payload": { + "approval_id": "${a1}", + "decision": "allow_once" + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000004", + "kind": "approval" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${a1}", + "reason": "answered_remote" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "assert": { + "lease_holder": "c1", + "approvals_pending": 0, + "approval_decisions": [ + { + "approval_id": "${a1}", + "decision": "allow_once" + } + ] + } + } + ], + "note": "acquire on an approval answer is a removed form in r8 (it took the lease in r7): in the payload or beside it, it is refused unsupported_version with no effect: the approval stays pending, nothing is recorded or handed over, the lease does not move. The plain answer then works without the lease." +} diff --git a/designs/protocol/fixtures/r8-approval-answers-crossing-first-wins.json b/designs/protocol/fixtures/r8-approval-answers-crossing-first-wins.json new file mode 100644 index 0000000..b23fe62 --- /dev/null +++ b/designs/protocol/fixtures/r8-approval-answers-crossing-first-wins.json @@ -0,0 +1,292 @@ +{ + "name": "r8-approval-answers-crossing-first-wins", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native", + "holds_lease_epoch": 1 + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000001" + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "sessions", + "re": "11111111-1111-4111-8111-000000000001", + "sessions.#": 1 + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000002" + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "sessions", + "re": "11111111-1111-4111-8111-000000000002", + "sessions.#": 1 + } + }, + { + "open_approval": "ses-aaaaaaaaaaaa", + "tool": "Bash", + "summary": "npm test", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "tool": "Bash", + "summary": "npm test", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ] + }, + "capture": { + "a1": "approval_id" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "tool": "Bash", + "summary": "npm test", + "always_rules": [ + { + "rule_id": "r-npm-test", + "label": "Bash(npm test:*)", + "scope": "this project" + } + ], + "approval_id": "${a1}" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "approval", + "payload": { + "approval_id": "${a1}", + "decision": "deny" + } + } + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "approval", + "payload": { + "approval_id": "${a1}", + "decision": "allow_once" + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000003", + "kind": "approval" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${a1}", + "reason": "answered_remote" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${a1}", + "reason": "answered_remote" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000004", + "code": "stale_target" + } + }, + { + "assert": { + "lease_holder": "c1", + "dispatches": 1, + "approval_decisions": [ + { + "approval_id": "${a1}", + "decision": "deny" + } + ] + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000005", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "approval", + "payload": { + "approval_id": "${a1}", + "decision": "deny" + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing", + "re": "11111111-1111-4111-8111-000000000005" + } + }, + { + "assert": { + "dispatches": 1 + } + } + ], + "note": "Two answers to one approval cross: the lease holder's allow and a non-holder's deny reach the host in the same instant. The first admitted while the request is pending wins and is handed over; the approval closes in that same step, so the second is stale_target and nothing of it is recorded. Holding the lease gives no priority." +} diff --git a/designs/protocol/fixtures/r8-backlog-hole-is-reported-not-reopened.json b/designs/protocol/fixtures/r8-backlog-hole-is-reported-not-reopened.json new file mode 100644 index 0000000..a1df8b1 --- /dev/null +++ b/designs/protocol/fixtures/r8-backlog-hole-is-reported-not-reopened.json @@ -0,0 +1,542 @@ +{ + "name": "r8-backlog-hole-is-reported-not-reopened", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "chat_history": "ses-aaaaaaaaaaaa", + "messages": [ + { + "messageId": "m-0001", + "role": "user", + "turnId": "turn-1", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 1" + } + ], + "origin": "local" + }, + { + "messageId": "m-0002", + "role": "assistant", + "turnId": "turn-1", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 2" + } + ] + }, + { + "messageId": "m-0003", + "role": "user", + "turnId": "turn-2", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 3" + } + ], + "origin": "local" + } + ] + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "open", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + } + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "opened", + "sequence": 0 + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 1, + "messageId": "m-0001", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 2, + "messageId": "m-0002", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 3, + "messageId": "m-0003", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "history_end", + "sequence": 4, + "page.end": true, + "page.coverage": "complete" + } + }, + { + "chat_message": "ses-aaaaaaaaaaaa", + "message": { + "messageId": "m-0004", + "role": "assistant", + "turnId": "turn-2", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 4" + } + ] + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 5, + "messageId": "m-0004" + } + }, + { + "worker_gap": "ses-aaaaaaaaaaaa", + "dropped": [ + "m-0005", + "m-0006", + "m-0007" + ], + "recoverable": [ + "m-0006", + "m-0007" + ], + "delivered_after": [ + { + "messageId": "m-0008", + "role": "assistant", + "turnId": "turn-4", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 8" + } + ] + } + ] + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 6, + "messageId": "m-0008" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 7, + "messageId": "m-0006", + "history": true, + "after_message_id": "m-0004" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 8, + "messageId": "m-0007", + "history": true, + "after_message_id": "m-0006" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "coverage", + "sequence": 9, + "holes": [ + { + "after_message_id": "m-0004", + "before_message_id": "m-0006", + "count": 1 + } + ], + "truncated": false + } + }, + { + "chat_message": "ses-aaaaaaaaaaaa", + "message": { + "messageId": "m-0009", + "role": "user", + "turnId": "turn-5", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 9" + } + ], + "origin": "local" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 10, + "messageId": "m-0009" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p1", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "before": "m-0009" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 11, + "messageId": "m-0001", + "history": true, + "page_id": "p1" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 12, + "messageId": "m-0002", + "history": true, + "page_id": "p1" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 13, + "messageId": "m-0003", + "history": true, + "page_id": "p1" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 14, + "messageId": "m-0004", + "history": true, + "page_id": "p1" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 15, + "messageId": "m-0006", + "history": true, + "page_id": "p1" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 16, + "messageId": "m-0007", + "history": true, + "page_id": "p1" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 17, + "messageId": "m-0008", + "history": true, + "page_id": "p1" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "history_end", + "sequence": 18, + "page_id": "p1", + "page.end": true, + "page.truncated": false, + "page.coverage": "partial", + "page.holes": [ + { + "after_message_id": "m-0004", + "before_message_id": "m-0006", + "count": 1 + } + ] + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "close", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa" + } + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "open", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + } + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "opened", + "sequence": 0 + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 1, + "messageId": "m-0001", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 2, + "messageId": "m-0002", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 3, + "messageId": "m-0003", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 4, + "messageId": "m-0004", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 5, + "messageId": "m-0006", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 6, + "messageId": "m-0007", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 7, + "messageId": "m-0008", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 8, + "messageId": "m-0009", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "history_end", + "sequence": 9, + "page.coverage": "partial", + "page.holes.#": 1 + } + }, + { + "expect_none": "c1", + "for_ms": 1000 + }, + { + "assert": { + "dispatch_attempts": 0, + "pty_writes": 0, + "lease_holder": null, + "chat_closes": 0, + "chat_opens": 2, + "dispatches": 0 + } + } + ], + "note": "r8 implementation change from #75 (which answers a worker backlog gap with a reload, then error and close): the worker's backlog dropped m-0005..m-0007 while the broker was busy. The host reloads the worker's history and sends what it recovered (m-0006, m-0007) as sequenced message{history: true} with after_message_id, in timeline order, then the live stream goes on. m-0005 is no longer in the worker's history either, so a coverage event reports a persistent hole between m-0004 and m-0006 inside the retained timeline (not truncation: oldest_retained is unchanged). No error, no close, no reopen loop; reopening shows the same hole, which nothing can repair." +} \ No newline at end of file diff --git a/designs/protocol/fixtures/r8-clean-close-runs-queued-work-once-its-principal-is-confirmed.json b/designs/protocol/fixtures/r8-clean-close-runs-queued-work-once-its-principal-is-confirmed.json new file mode 100644 index 0000000..9d58c81 --- /dev/null +++ b/designs/protocol/fixtures/r8-clean-close-runs-queued-work-once-its-principal-is-confirmed.json @@ -0,0 +1,266 @@ +{ + "name": "r8-clean-close-runs-queued-work-once-its-principal-is-confirmed", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000001", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000001", + "lease.lease_epoch": 1 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 1 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "then deploy" + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000002" + } + }, + { + "restart_runtime": { + "runtime_gen": "99999999-9999-4999-8999-999999999999", + "runtime_epoch": 8 + } + }, + { + "reconnect": "c2", + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "dispatch_attempts": 1, + "lease_holder": null + } + }, + { + "advance_ms": 30000 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "dispatch_attempts": 1 + } + }, + { + "reconnect": "c1", + "attachment_gen": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + }, + { + "assert": { + "dispatch_attempts": 2, + "dispatches": 2, + "lease_holder": null + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt_query", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + }, + "request_ids": [ + "44444444-4444-4444-8444-000000000001", + "44444444-4444-4444-8444-000000000002" + ] + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipts", + "re": "11111111-1111-4111-8111-000000000003", + "receipts.#": 2, + "receipts.0.state": "outcome_unknown", + "receipts.1.state": "executing" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "then deploy" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "executing", + "re": "11111111-1111-4111-8111-000000000004" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "dispatch_attempts": 2, + "lease_holder": null + } + } + ], + "note": "Appendix §H, Recovery rule 3. c1 executes P1 with P2 queued; the runtime stops in order with a healthy journal, so it writes a clean close. After the restart P2's control_gen equals its principal's durable one; it waits for that principal's confirmation: another principal (c2) reconnecting doesn't dispatch it, c1 welcomed with control 30 s after the start does, once, with no lease (leases aren't persisted). P1, executing at the stop, is outcome_unknown. The resend with the same id returns the receipt and neither acquires nor reruns." +} diff --git a/designs/protocol/fixtures/r8-client-delayed-send-reply-never-restores-control.json b/designs/protocol/fixtures/r8-client-delayed-send-reply-never-restores-control.json new file mode 100644 index 0000000..66591fa --- /dev/null +++ b/designs/protocol/fixtures/r8-client-delayed-send-reply-never-restores-control.json @@ -0,0 +1,583 @@ +{ + "name": "r8-client-delayed-send-reply-never-restores-control", + "requires": "r8", + "side": "client", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "hello", + "id": "11111111-1111-4111-8111-000000000001", + "revision": 8 + } + } + }, + { + "deliver_to_client": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "welcome", + "id": "11111111-1111-4111-8111-000000000901", + "re": "11111111-1111-4111-8111-000000000001", + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "controller_session": "12121212-1212-4212-8212-121212121212", + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "client_kind": "native", + "permissions": [ + "view", + "control" + ], + "authorization_ms_left": 86400000, + "server_time": 1760500000000, + "revision": 8, + "features": [ + "chat.history_end", + "chat.history", + "chat.lookup", + "chat.coverage", + "receipt.result", + "approval.session_rules", + "approval.card_reason", + "chat.send_acquire", + "approval.lease_free" + ] + } + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000002" + } + } + }, + { + "deliver_to_client": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "sessions", + "id": "11111111-1111-4111-8111-000000000902", + "re": "11111111-1111-4111-8111-000000000002", + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7, + "seq": 1, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "provider": "claude", + "cwd_label": "Atem", + "state": "running", + "activity": "ready", + "activity_since": 1760500000000, + "controller": null, + "capabilities": [ + "terminal-v1", + "chat-v1" + ], + "created_at": 1760490000000 + } + ] + } + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "advance_ms": 100, + "timers": false + }, + { + "deliver_to_client": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "lease_changed", + "id": "11111111-1111-4111-8111-000000000903", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "controller": null, + "lease_epoch": 2, + "reason": "local_input" + } + } + }, + { + "deliver_to_client": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt", + "id": "11111111-1111-4111-8111-000000000904", + "re": "11111111-1111-4111-8111-000000000003", + "request_id": "44444444-4444-4444-8444-000000000001", + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "kind": "prompt", + "state": "accepted", + "updated_at": 1760500000000, + "lease": { + "lease_epoch": 1, + "ms_left": 15000 + } + } + } + }, + { + "assert_client": { + "controls": false, + "input_enabled": false + } + }, + { + "advance_ms": 900, + "timers": false + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "then deploy" + }, + "acquire": true + } + } + }, + { + "deliver_to_client": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "lease_changed", + "id": "11111111-1111-4111-8111-000000000905", + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "controller": { + "controller_session": "13131313-1313-4313-8313-131313131313", + "lease_epoch": 4 + }, + "lease_epoch": 4, + "reason": "takeover" + } + } + }, + { + "deliver_to_client": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt", + "id": "11111111-1111-4111-8111-000000000906", + "re": "11111111-1111-4111-8111-000000000004", + "request_id": "44444444-4444-4444-8444-000000000002", + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "kind": "prompt", + "state": "accepted", + "updated_at": 1760500001000, + "lease": { + "lease_epoch": 3, + "ms_left": 15000 + } + } + } + }, + { + "assert_client": { + "controls": false, + "input_enabled": false + } + }, + { + "advance_ms": 1000, + "timers": false + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000005", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + }, + "request_id": "44444444-4444-4444-8444-000000000003", + "kind": "prompt", + "payload": { + "text": "and tag it" + }, + "acquire": true + } + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "hello", + "id": "11111111-1111-4111-8111-000000000006", + "revision": 8 + } + } + }, + { + "deliver_to_client": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "welcome", + "id": "11111111-1111-4111-8111-000000000907", + "re": "11111111-1111-4111-8111-000000000006", + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "controller_session": "14141414-1414-4414-8414-141414141414", + "attachment_gen": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + "client_kind": "native", + "permissions": [ + "view", + "control" + ], + "authorization_ms_left": 86400000, + "server_time": 1760500000000, + "revision": 8, + "features": [ + "chat.history_end", + "chat.history", + "chat.lookup", + "chat.coverage", + "receipt.result", + "approval.session_rules", + "approval.card_reason", + "chat.send_acquire", + "approval.lease_free" + ] + } + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000007" + } + } + }, + { + "deliver_to_client": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "sessions", + "id": "11111111-1111-4111-8111-000000000908", + "re": "11111111-1111-4111-8111-000000000007", + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7, + "seq": 1, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "provider": "claude", + "cwd_label": "Atem", + "state": "running", + "activity": "ready", + "activity_since": 1760500000000, + "controller": null, + "capabilities": [ + "terminal-v1", + "chat-v1" + ], + "created_at": 1760490000000 + } + ] + } + } + }, + { + "deliver_to_client": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt", + "id": "11111111-1111-4111-8111-000000000909", + "re": "11111111-1111-4111-8111-000000000005", + "request_id": "44444444-4444-4444-8444-000000000003", + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "kind": "prompt", + "state": "accepted", + "updated_at": 1760500002000, + "lease": { + "lease_epoch": 5, + "ms_left": 15000 + } + } + } + }, + { + "assert_client": { + "controls": false, + "input_enabled": false + } + }, + { + "advance_ms": 1000, + "timers": false + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000008", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + }, + "request_id": "44444444-4444-4444-8444-000000000004", + "kind": "prompt", + "payload": { + "text": "and publish it" + }, + "acquire": true + } + } + }, + { + "advance_ms": 15000, + "timers": false + }, + { + "deliver_to_client": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt", + "id": "11111111-1111-4111-8111-000000000910", + "re": "11111111-1111-4111-8111-000000000008", + "request_id": "44444444-4444-4444-8444-000000000004", + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "kind": "prompt", + "state": "accepted", + "updated_at": 1760500003000, + "lease": { + "lease_epoch": 6, + "ms_left": 15000 + } + } + } + }, + { + "assert_client": { + "controls": false, + "input_enabled": false + } + }, + { + "advance_ms": 2000, + "timers": false + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000009", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + }, + "request_id": "44444444-4444-4444-8444-000000000005", + "kind": "prompt", + "payload": { + "text": "and announce it" + }, + "acquire": true + } + } + }, + { + "advance_ms": 100, + "timers": false + }, + { + "deliver_to_client": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt", + "id": "11111111-1111-4111-8111-000000000911", + "re": "11111111-1111-4111-8111-000000000009", + "request_id": "44444444-4444-4444-8444-000000000005", + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "kind": "prompt", + "state": "accepted", + "updated_at": 1760500020000, + "lease": { + "lease_epoch": 7, + "ms_left": 15000 + } + } + } + }, + { + "assert_client": { + "controls": true, + "input_enabled": true, + "deadline_ms": 35000 + } + } + ], + "note": "Client side, appendix §F reply rule (t = fake monotonic clock, ms). The lease on an admitting receipt never restores control that was lost while the reply was in flight. (1) Local input revoked the lease (epoch 2) before the receipt naming epoch 1 arrived: lower than an epoch seen since the Send, dropped. (2) A takeover by another controller (epoch 4) beat the receipt naming epoch 3: dropped. (3) The attachment was replaced (a second hello, new welcome) before the receipt of a Send sent on the old one arrived: not outstanding on the current attachment, dropped. (4) The receipt of a Send sent at t=3000 arrives at t=18000: deadline = 3000 + 15000 = now, so control stays off (expiry is now >= deadline). A timely receipt (sent at 20000, applied at 20100) gives control until 20000 + 15000 = 35000. On one ordered link the host sends the receipt before such a lease_changed; these orders arise between Astation's own layers (native or API callers behind one attachment), and the client must hold either way." +} diff --git a/designs/protocol/fixtures/r8-client-enables-only-advertised-features.json b/designs/protocol/fixtures/r8-client-enables-only-advertised-features.json new file mode 100644 index 0000000..bac4c01 --- /dev/null +++ b/designs/protocol/fixtures/r8-client-enables-only-advertised-features.json @@ -0,0 +1,125 @@ +{ + "name": "r8-client-enables-only-advertised-features", + "side": "client", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "hello", + "id": "11111111-1111-4111-8111-000000000001", + "revision": 8 + } + } + }, + { + "deliver_to_client": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "welcome", + "id": "11111111-1111-4111-8111-000000000002", + "re": "11111111-1111-4111-8111-000000000900", + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "controller_session": "12121212-1212-4212-8212-121212121212", + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "client_kind": "native", + "permissions": [ + "view", + "control" + ], + "authorization_ms_left": 86400000, + "server_time": 1760500000000, + "revision": 8, + "features": [ + "chat.history_end", + "chat.history", + "receipt.result", + "approval.session_rules", + "chat.future-thing" + ] + } + } + }, + { + "assert_client": { + "admitted": true, + "revision": 8, + "features_enabled": [ + "chat.history_end", + "chat.history", + "receipt.result", + "approval.session_rules" + ], + "lookup_enabled": false, + "coverage_enabled": false + } + }, + { + "client_try": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p1", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "turn_id": "turn-1" + } + }, + "refused": "feature chat.lookup not advertised" + }, + { + "assert_client": { + "sent": [] + } + } + ], + "note": "Client side: welcome.revision is 8 but features lacks chat.lookup and chat.coverage. The client enables exactly the advertised features it knows (an unknown feature name is ignored), refuses locally to send a history lookup (turn_id or request_id) and sends nothing. A client that requires a missing feature for an operation shows that operation as unavailable; it never guesses the host supports it." +} \ No newline at end of file diff --git a/designs/protocol/fixtures/r8-client-refuses-a-host-without-revision-8.json b/designs/protocol/fixtures/r8-client-refuses-a-host-without-revision-8.json new file mode 100644 index 0000000..62ce497 --- /dev/null +++ b/designs/protocol/fixtures/r8-client-refuses-a-host-without-revision-8.json @@ -0,0 +1,146 @@ +{ + "name": "r8-client-refuses-a-host-without-revision-8", + "side": "client", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": false + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "hello", + "id": "11111111-1111-4111-8111-000000000001", + "revision": 8 + } + } + }, + { + "deliver_to_client": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "welcome", + "id": "11111111-1111-4111-8111-000000000002", + "re": "11111111-1111-4111-8111-000000000900", + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "controller_session": "12121212-1212-4212-8212-121212121212", + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "client_kind": "native", + "permissions": [ + "view", + "control" + ], + "authorization_ms_left": 86400000, + "server_time": 1760500000000 + } + } + }, + { + "assert_client": { + "admitted": false, + "revision": null, + "features_enabled": [], + "sent_list": false, + "input_enabled": false, + "chat_enabled": false + } + }, + { + "deliver_to_client": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "sessions", + "id": "11111111-1111-4111-8111-000000000003", + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7, + "seq": 1, + "sessions": [] + } + } + }, + { + "assert_client": { + "admitted": false, + "registry": [], + "sent_list": false + } + }, + { + "deliver_to_client": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "welcome", + "id": "11111111-1111-4111-8111-000000000004", + "re": "11111111-1111-4111-8111-000000000900", + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "controller_session": "12121212-1212-4212-8212-121212121212", + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "client_kind": "native", + "permissions": [ + "view", + "control" + ], + "authorization_ms_left": 86400000, + "server_time": 1760500000000, + "revision": 7, + "features": [ + "chat.history_end", + "chat.history", + "chat.lookup", + "chat.coverage", + "receipt.result", + "approval.session_rules", + "approval.card_reason" + ] + } + } + }, + { + "assert_client": { + "admitted": false, + "features_enabled": [], + "sent_list": false + } + } + ], + "note": "Client side: an r7 host ignores the unknown hello.revision and welcomes without revision (or names another one). The r8 client refuses the attachment: it enables no feature, sends no list, acquire, chat open or terminal request, and applies nothing the host sends afterwards. It reports that the host's Atem must be updated. No fallback to r7 behaviour." +} \ No newline at end of file diff --git a/designs/protocol/fixtures/r8-client-shows-truncation-and-resyncs-only-on-a-sequence-gap.json b/designs/protocol/fixtures/r8-client-shows-truncation-and-resyncs-only-on-a-sequence-gap.json new file mode 100644 index 0000000..ff6f612 --- /dev/null +++ b/designs/protocol/fixtures/r8-client-shows-truncation-and-resyncs-only-on-a-sequence-gap.json @@ -0,0 +1,262 @@ +{ + "name": "r8-client-shows-truncation-and-resyncs-only-on-a-sequence-gap", + "side": "client", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "hello", + "id": "11111111-1111-4111-8111-000000000001", + "revision": 8 + } + } + }, + { + "deliver_to_client": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "welcome", + "id": "11111111-1111-4111-8111-000000000002", + "re": "11111111-1111-4111-8111-000000000900", + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "controller_session": "12121212-1212-4212-8212-121212121212", + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "client_kind": "native", + "permissions": [ + "view", + "control" + ], + "authorization_ms_left": 86400000, + "server_time": 1760500000000, + "revision": 8, + "features": [ + "chat.history_end", + "chat.history", + "chat.lookup", + "chat.coverage", + "receipt.result", + "approval.session_rules", + "approval.card_reason" + ] + } + } + }, + { + "client_open_chat": "c1", + "conversation": "conv-1", + "session_id": "ses-aaaaaaaaaaaa" + }, + { + "deliver_to_client": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "opened", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "status": { + "state": "ready", + "since": 1760600000000 + }, + "unavailable": [ + "streaming", + "reasoning" + ], + "approvalRules": [], + "generation": "22222222-2222-4222-8222-222222222222", + "sequence": 0 + } + } + }, + { + "deliver_to_client": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "message", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "history": true, + "messageId": "m-0005", + "role": "user", + "turnId": "turn-3", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 5" + } + ], + "origin": "local", + "generation": "22222222-2222-4222-8222-222222222222", + "sequence": 1 + } + } + }, + { + "deliver_to_client": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history_end", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "page": { + "selector": "newest", + "count": 1, + "content_bytes": 9, + "wire_bytes": 675, + "end": true, + "coverage": "partial", + "oldest_retained": "m-0005", + "truncated": true, + "holes": [] + }, + "generation": "22222222-2222-4222-8222-222222222222", + "sequence": 2 + } + } + }, + { + "assert_client": { + "chat_truncated": true, + "chat_reopens": 0, + "chat_loaded": true + } + }, + { + "deliver_to_client": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "message", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "messageId": "m-0006", + "role": "assistant", + "turnId": "turn-3", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 6" + } + ], + "generation": "22222222-2222-4222-8222-222222222222", + "sequence": 3 + } + } + }, + { + "deliver_to_client": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "message", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "messageId": "m-0007", + "role": "user", + "turnId": "turn-4", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 7" + } + ], + "origin": "local", + "generation": "22222222-2222-4222-8222-222222222222", + "sequence": 4 + } + } + }, + { + "assert_client": { + "chat_messages": [ + "m-0005", + "m-0006", + "m-0007" + ], + "chat_reopens": 0, + "sent": [] + } + }, + { + "deliver_to_client": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "message", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "messageId": "m-0009", + "role": "user", + "turnId": "turn-5", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 9" + } + ], + "origin": "local", + "generation": "22222222-2222-4222-8222-222222222222", + "sequence": 6 + } + } + }, + { + "assert_client": { + "chat_reopens": 1 + } + } + ], + "note": "Client side: truncated: true and coverage partial are shown and the client keeps consuming the live stream; it does not reopen for them. It reopens once on an actual sequence gap (5 missing before 6). Resync triggers are only a sequence gap, a fenced conversation, another generation, or close with a reason." +} \ No newline at end of file diff --git a/designs/protocol/fixtures/r8-control-fence-holds-when-a-rejection-write-fails.json b/designs/protocol/fixtures/r8-control-fence-holds-when-a-rejection-write-fails.json new file mode 100644 index 0000000..bec0ac1 --- /dev/null +++ b/designs/protocol/fixtures/r8-control-fence-holds-when-a-rejection-write-fails.json @@ -0,0 +1,311 @@ +{ + "name": "r8-control-fence-holds-when-a-rejection-write-fails", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000001", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000001", + "lease.lease_epoch": 1 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 1 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "then deploy" + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000002" + } + }, + { + "fail_fsync_of": 2 + }, + { + "set_permissions": "c1", + "permissions": [ + "view" + ], + "attachment_gen": "77777777-7777-4777-8777-777777777777" + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "revoked", + "lease_epoch": 2, + "controller": null + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "journal_halted": true, + "lease_holder": null, + "dispatch_attempts": 1 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt_query", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "77777777-7777-4777-8777-777777777777" + }, + "request_ids": [ + "44444444-4444-4444-8444-000000000002" + ] + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000003", + "code": "busy", + "detail": "journal halted; restart Atem" + } + }, + { + "restart_runtime": { + "runtime_gen": "99999999-9999-4999-8999-999999999999", + "runtime_epoch": 8 + } + }, + { + "reconnect": "c1", + "attachment_gen": "88888888-8888-4888-8888-888888888888" + }, + { + "reconnect": "c2", + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt_query", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "88888888-8888-4888-8888-888888888888" + }, + "request_ids": [ + "44444444-4444-4444-8444-000000000002" + ] + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipts", + "re": "11111111-1111-4111-8111-000000000004", + "receipts.#": 1, + "receipts.0.state": "rejected", + "receipts.0.detail": "authorization history unknown" + } + }, + { + "set_permissions": "c1", + "permissions": [ + "view", + "control" + ], + "attachment_gen": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt_query", + "id": "11111111-1111-4111-8111-000000000005", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + }, + "request_ids": [ + "44444444-4444-4444-8444-000000000002" + ] + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipts", + "re": "11111111-1111-4111-8111-000000000005", + "receipts.#": 1, + "receipts.0.state": "rejected", + "receipts.0.detail": "authorization history unknown" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "dispatch_attempts": 1, + "lease_holder": null + } + } + ], + "note": "Appendix §H. c1 loses control while P2 is queued. The fence (the first journal write of the revocation) is durable; the write rejecting P2 (the second) fails, so P2 is rejected in memory (\"A failed revocation write\"), the journal halts with P2 still accepted on disk, nothing queued is dispatched and every later request is busy. A halted run writes no clean close, so after the restart P2 is durably rejected at recovery before anything is dispatched, with the detail authorization history unknown (Recovery rule 2 applies whatever the journal holds, the durable fence included), whether or not c1 has reconnected; regaining control doesn't revive it." +} diff --git a/designs/protocol/fixtures/r8-crash-after-durable-acceptance-grants-no-lease.json b/designs/protocol/fixtures/r8-crash-after-durable-acceptance-grants-no-lease.json new file mode 100644 index 0000000..fa4def8 --- /dev/null +++ b/designs/protocol/fixtures/r8-crash-after-durable-acceptance-grants-no-lease.json @@ -0,0 +1,227 @@ +{ + "name": "r8-crash-after-durable-acceptance-grants-no-lease", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native", + "holds_lease_epoch": 1 + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000001" + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "sessions", + "re": "11111111-1111-4111-8111-000000000001", + "sessions.#": 1, + "sessions.0.controller.lease_epoch": 1 + }, + "capture": { + "c1ctl": "sessions.0.controller.controller_session" + } + }, + { + "crash_after_accepted": { + "runtime_gen": "99999999-9999-4999-8999-999999999999", + "runtime_epoch": 8 + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "deploy to staging" + }, + "acquire": true, + "takeover": true, + "expected_controller": { + "controller_session": "${c1ctl}", + "lease_epoch": 1 + } + } + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "reconnect": "c1", + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + }, + { + "assert": { + "lease_holder": null, + "dispatch_attempts": 0 + } + }, + { + "reconnect": "c2", + "attachment_gen": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + }, + { + "assert": { + "lease_holder": null, + "dispatch_attempts": 0 + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt_query", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + }, + "request_ids": [ + "44444444-4444-4444-8444-000000000001" + ] + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipts", + "re": "11111111-1111-4111-8111-000000000003", + "receipts.#": 1, + "receipts.0.state": "rejected", + "receipts.0.detail": "authorization history unknown" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "deploy to staging" + }, + "acquire": true, + "takeover": true, + "expected_controller": { + "controller_session": "${c1ctl}", + "lease_epoch": 1 + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "rejected", + "re": "11111111-1111-4111-8111-000000000004", + "detail": "authorization history unknown" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "lease_holder": null, + "dispatch_attempts": 0, + "pty_writes": 0 + } + } + ], + "note": "Appendix §I.4 and §H. The accepted record of c2's confirmed takeover Send becomes durable, then the runtime crashes before the lease moves or the reply is sent (no reply, no lease_changed to c1). After the restart nobody holds the lease (leases aren't persisted), and since the run didn't end with a clean close the request is durably rejected (authorization history unknown) before anything is dispatched: c1 and c2 reconnecting dispatch nothing. c2 queries first (uncertain delivery), then a resend with the same id and flags returns the rejected receipt and neither acquires nor takes over; sending it again needs a new request_id." +} diff --git a/designs/protocol/fixtures/r8-crash-rejects-queued-work-whose-history-is-unknown.json b/designs/protocol/fixtures/r8-crash-rejects-queued-work-whose-history-is-unknown.json new file mode 100644 index 0000000..4d803d9 --- /dev/null +++ b/designs/protocol/fixtures/r8-crash-rejects-queued-work-whose-history-is-unknown.json @@ -0,0 +1,327 @@ +{ + "name": "r8-crash-rejects-queued-work-whose-history-is-unknown", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000001", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000001", + "lease.lease_epoch": 1 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 1 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "then deploy" + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000002" + } + }, + { + "crash_runtime": { + "runtime_gen": "99999999-9999-4999-8999-999999999999", + "runtime_epoch": 8 + } + }, + { + "reconnect": "c1", + "attachment_gen": "77777777-7777-4777-8777-777777777777" + }, + { + "reconnect": "c2", + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "journal_halted": false, + "dispatch_attempts": 1, + "lease_holder": null + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt_query", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "77777777-7777-4777-8777-777777777777" + }, + "request_ids": [ + "44444444-4444-4444-8444-000000000001", + "44444444-4444-4444-8444-000000000002" + ] + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipts", + "re": "11111111-1111-4111-8111-000000000003", + "receipts.#": 2, + "receipts.0.state": "outcome_unknown", + "receipts.1.state": "rejected", + "receipts.1.detail": "authorization history unknown" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "77777777-7777-4777-8777-777777777777" + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "then deploy" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "rejected", + "re": "11111111-1111-4111-8111-000000000004", + "detail": "authorization history unknown" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "dispatch_attempts": 1, + "lease_holder": null + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000005", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "77777777-7777-4777-8777-777777777777" + }, + "request_id": "44444444-4444-4444-8444-000000000003", + "kind": "prompt", + "payload": { + "text": "then deploy" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000003", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000005" + }, + "capture": { + "e3": "lease.lease_epoch" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000003", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": "${e3}" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "dispatch_attempts": 2, + "dispatches": 2, + "lease_holder": "c1" + } + } + ], + "note": "Appendix §H, Recovery rule 2. c1 executes P1 with P2 queued; the runtime crashes (no clean close). At the restart P2 is durably rejected (authorization history unknown) before anything is dispatched, although its principal reconnects with control at once and no revocation happened: without a clean close the host can't establish that none did. P1, executing, is outcome_unknown as before. The cost: the same id returns the rejected receipt and is never re-admitted; the client sends the prompt again with a new request_id, which is admitted and acquires." +} diff --git a/designs/protocol/fixtures/r8-delayed-send-reply-does-not-regrant-lost-control.json b/designs/protocol/fixtures/r8-delayed-send-reply-does-not-regrant-lost-control.json new file mode 100644 index 0000000..ea169fd --- /dev/null +++ b/designs/protocol/fixtures/r8-delayed-send-reply-does-not-regrant-lost-control.json @@ -0,0 +1,350 @@ +{ + "name": "r8-delayed-send-reply-does-not-regrant-lost-control", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "hold": { + "to": "c1", + "op": "receipt" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000001", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 1 + } + }, + { + "local_input": "ses-aaaaaaaaaaaa", + "bytes": "eA0=" + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "local_input", + "lease_epoch": 2, + "controller": null + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "local_input", + "lease_epoch": 2, + "controller": null + } + }, + { + "deliver": "held" + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000001", + "lease.lease_epoch": 1 + } + }, + { + "assert": { + "lease_holder": null + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "renew", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000002", + "code": "stale_lease" + } + }, + { + "hold": { + "to": "c1", + "op": "receipt" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "then deploy" + }, + "acquire": true + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 3 + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "acquire", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "takeover": true + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease", + "re": "11111111-1111-4111-8111-000000000004", + "binding.lease_epoch": 4 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "takeover", + "lease_epoch": 4 + } + }, + { + "deliver": "held" + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000003", + "lease.lease_epoch": 3 + } + }, + { + "assert": { + "lease_holder": "c2" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "renew", + "id": "11111111-1111-4111-8111-000000000005", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 3 + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000005", + "code": "stale_lease" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000006", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 3 + }, + "request_id": "44444444-4444-4444-8444-000000000003", + "kind": "prompt", + "payload": { + "text": "and tag it" + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000006", + "code": "stale_lease" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "lease_holder": "c2", + "dispatch_attempts": 1 + } + } + ], + "note": "Appendix §F, reply rule. c1's acquiring Send is admitted (epoch 1) but its accepted receipt is delayed; meanwhile local input revokes the lease (epoch 2). The late receipt still names epoch 1, which grants nothing: the host has no holder and the old epoch is stale_lease. Then a second Send acquires the free lease (epoch 3), its receipt is delayed, and c2 takes over (epoch 4) before it arrives: the receipt's epoch 3 is superseded, c2 keeps the lease, and c1's renew or r7-form submit under epoch 3 is stale_lease. The second prompt stays queued behind the first (accepted work isn't cancelled by losing the lease). The client side is r8-client-delayed-send-reply-never-restores-control." +} diff --git a/designs/protocol/fixtures/r8-disconnect-right-after-acceptance.json b/designs/protocol/fixtures/r8-disconnect-right-after-acceptance.json new file mode 100644 index 0000000..9337ab2 --- /dev/null +++ b/designs/protocol/fixtures/r8-disconnect-right-after-acceptance.json @@ -0,0 +1,285 @@ +{ + "name": "r8-disconnect-right-after-acceptance", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "drop": { + "to": "c1", + "op": "receipt" + } + }, + { + "drop": { + "to": "c1", + "op": "receipt" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000001", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "disconnect": "c1" + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 1 + } + }, + { + "assert": { + "lease_holder": "c1", + "dispatches": 1 + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "deploy to staging" + }, + "acquire": true + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000002", + "code": "lease_held", + "controller.lease_epoch": 1 + } + }, + { + "reconnect": "c1", + "attachment_gen": "99999999-9999-4999-8999-999999999999" + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt_query", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "99999999-9999-4999-8999-999999999999" + }, + "request_ids": [ + "44444444-4444-4444-8444-000000000001" + ] + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipts", + "re": "11111111-1111-4111-8111-000000000003", + "receipts.#": 1, + "receipts.0.state": "executing" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "99999999-9999-4999-8999-999999999999" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing", + "re": "11111111-1111-4111-8111-000000000004" + } + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "advance_ms": 15000 + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "expired", + "lease_epoch": 2, + "controller": null + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "expired", + "lease_epoch": 2, + "controller": null + } + }, + { + "turn_complete": "ses-aaaaaaaaaaaa", + "turn_id": "turn-1" + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt_query", + "id": "11111111-1111-4111-8111-000000000005", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "99999999-9999-4999-8999-999999999999" + }, + "request_ids": [ + "44444444-4444-4444-8444-000000000001" + ] + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipts", + "re": "11111111-1111-4111-8111-000000000005", + "receipts.#": 1, + "receipts.0.state": "completed", + "receipts.0.detail": "done" + } + }, + { + "assert": { + "lease_holder": null, + "dispatch_attempts": 1, + "dispatches": 1 + } + } + ], + "note": "The sender's transport drops right after its Send was accepted, before either receipt arrives. The accepted prompt runs (disconnect never cancels accepted work); the lease, granted to the fenced attachment, is not handed on and expires at its deadline. On a new attachment the client queries the receipt first, then a resend of the same request returns it: no acquisition, no second run." +} diff --git a/designs/protocol/fixtures/r8-double-write-failure-takeover-never-runs.json b/designs/protocol/fixtures/r8-double-write-failure-takeover-never-runs.json new file mode 100644 index 0000000..741ab85 --- /dev/null +++ b/designs/protocol/fixtures/r8-double-write-failure-takeover-never-runs.json @@ -0,0 +1,242 @@ +{ + "name": "r8-double-write-failure-takeover-never-runs", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native", + "holds_lease_epoch": 1 + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000001" + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "sessions", + "re": "11111111-1111-4111-8111-000000000001", + "sessions.#": 1, + "sessions.0.controller.lease_epoch": 1 + }, + "capture": { + "c1ctl": "sessions.0.controller.controller_session" + } + }, + { + "fail_next_fsync": true, + "undo_fails": true + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "deploy to staging" + }, + "acquire": true, + "takeover": true, + "expected_controller": { + "controller_session": "${c1ctl}", + "lease_epoch": 1 + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000002", + "code": "busy", + "detail": "acceptance not recorded" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "lease_holder": "c1", + "dispatch_attempts": 0, + "journal_halted": true + } + }, + { + "restart_runtime": { + "runtime_gen": "99999999-9999-4999-8999-999999999999", + "runtime_epoch": 8 + } + }, + { + "reconnect": "c1", + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + }, + { + "reconnect": "c2", + "attachment_gen": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + }, + { + "assert": { + "lease_holder": null, + "dispatch_attempts": 0 + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "deploy to staging" + }, + "acquire": true, + "takeover": true, + "expected_controller": { + "controller_session": "${c1ctl}", + "lease_epoch": 1 + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "rejected", + "re": "11111111-1111-4111-8111-000000000003", + "detail": "acceptance not confirmed" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt_query", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + }, + "request_ids": [ + "44444444-4444-4444-8444-000000000001" + ] + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipts", + "re": "11111111-1111-4111-8111-000000000004", + "receipts.#": 1, + "receipts.0.state": "rejected", + "receipts.0.detail": "acceptance not confirmed" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "lease_holder": null, + "dispatch_attempts": 0, + "pty_writes": 0 + } + } + ], + "note": "Appendix §I. c2 confirms a takeover of c1's lease and sends; the accepted write fails and so does its rollback, so the record may be on disk. The reply is busy (the refusal is on the refusal list), c1 keeps the lease and hears nothing, and nothing runs. At the restart (leases aren't persisted) the request is resolved rejected (acceptance not confirmed) before recovery dispatches anything, even if its accepted record is there: it was refused, so it never executes and never takes over. The same request_id with the same takeover flags returns the rejected receipt; c1 sees the same receipt." +} diff --git a/designs/protocol/fixtures/r8-duplicate-send-never-reacquires-or-replays.json b/designs/protocol/fixtures/r8-duplicate-send-never-reacquires-or-replays.json new file mode 100644 index 0000000..b6f7cb8 --- /dev/null +++ b/designs/protocol/fixtures/r8-duplicate-send-never-reacquires-or-replays.json @@ -0,0 +1,382 @@ +{ + "name": "r8-duplicate-send-never-reacquires-or-replays", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000001", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000001", + "lease.lease_epoch": 1 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 1 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "release", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease_released", + "re": "11111111-1111-4111-8111-000000000002" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "released", + "lease_epoch": 2 + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "acquire", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "takeover": false + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease", + "re": "11111111-1111-4111-8111-000000000003", + "binding.lease_epoch": 3 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 3 + }, + "capture": { + "c2ctl": "controller.controller_session" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing", + "re": "11111111-1111-4111-8111-000000000004" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000005", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true, + "takeover": true, + "expected_controller": { + "controller_session": "${c2ctl}", + "lease_epoch": 3 + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing", + "re": "11111111-1111-4111-8111-000000000005" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000006", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "something else" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000006", + "code": "request_id_reused" + } + }, + { + "assert": { + "lease_holder": "c2", + "dispatches": 1 + } + }, + { + "hold": { + "to": "c1", + "op": "receipt" + } + }, + { + "turn_complete": "ses-aaaaaaaaaaaa", + "turn_id": "turn-1" + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000007", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "completed", + "re": "11111111-1111-4111-8111-000000000007", + "detail": "done" + } + }, + { + "deliver": "held" + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "completed", + "detail": "done" + } + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "lease_holder": "c2", + "dispatch_attempts": 1, + "dispatches": 1 + } + } + ], + "note": "acquire, takeover and expected_controller are admission parameters, not request identity: the digest covers the payload only and the immutable tuple is r7's. A duplicate request_id returns the recorded receipt (no lease field) whatever flags it carries, never acquires, takes over, renews or re-executes; other text under the same id is request_id_reused. A delayed receipt changes nothing either." +} diff --git a/designs/protocol/fixtures/r8-expired-lease-is-checked-at-admission.json b/designs/protocol/fixtures/r8-expired-lease-is-checked-at-admission.json new file mode 100644 index 0000000..657ecea --- /dev/null +++ b/designs/protocol/fixtures/r8-expired-lease-is-checked-at-admission.json @@ -0,0 +1,235 @@ +{ + "name": "r8-expired-lease-is-checked-at-admission", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native", + "holds_lease_epoch": 1 + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "advance_ms": 15000, + "timers": false + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000001", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "expired", + "lease_epoch": 2, + "controller": null + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000001", + "lease.lease_epoch": 3 + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "expired", + "lease_epoch": 2, + "controller": null + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 3 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "deploy to staging" + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000002", + "code": "stale_lease" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "renew", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000003", + "code": "stale_lease" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "deploy to staging" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000004", + "code": "lease_held", + "controller.lease_epoch": 3 + } + }, + { + "assert": { + "lease_holder": "c2", + "dispatch_attempts": 1, + "dispatches": 1 + } + } + ], + "note": "Expiry is now >= deadline on the host's clock at admission, with or without a timer tick: an ordinary Send reaching the host after the holder's lease expired acquires (the host applies the expiry first, then grants), and the old holder's epoch is stale for submit and renew alike. The old holder's own ordinary Send is then refused lease_held." +} diff --git a/designs/protocol/fixtures/r8-failed-fence-write-rejects-queued-work-at-once.json b/designs/protocol/fixtures/r8-failed-fence-write-rejects-queued-work-at-once.json new file mode 100644 index 0000000..87b598d --- /dev/null +++ b/designs/protocol/fixtures/r8-failed-fence-write-rejects-queued-work-at-once.json @@ -0,0 +1,351 @@ +{ + "name": "r8-failed-fence-write-rejects-queued-work-at-once", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000001", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000001", + "lease.lease_epoch": 1 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 1 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "then deploy" + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000002" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + }, + "request_id": "44444444-4444-4444-8444-000000000003", + "kind": "prompt", + "payload": { + "text": "then tag the release" + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000003", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000003" + } + }, + { + "fail_next_fsync": true + }, + { + "set_permissions": "c1", + "permissions": [ + "view" + ], + "attachment_gen": "77777777-7777-4777-8777-777777777777" + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "revoked", + "controller": null + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "journal_halted": true, + "lease_holder": null, + "dispatch_attempts": 1, + "attachments": 1 + } + }, + { + "turn_complete": "ses-aaaaaaaaaaaa", + "turn_id": "turn-1" + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "dispatch_attempts": 1 + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000004", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000004", + "code": "busy", + "detail": "journal halted; restart Atem" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "dispatch_attempts": 1, + "lease_holder": null + } + }, + { + "restart_runtime": { + "runtime_gen": "99999999-9999-4999-8999-999999999999", + "runtime_epoch": 8 + } + }, + { + "reconnect": "c1", + "attachment_gen": "88888888-8888-4888-8888-888888888888" + }, + { + "reconnect": "c2", + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + }, + { + "assert": { + "journal_halted": false, + "dispatch_attempts": 1, + "lease_holder": null + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt_query", + "id": "11111111-1111-4111-8111-000000000005", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "88888888-8888-4888-8888-888888888888" + }, + "request_ids": [ + "44444444-4444-4444-8444-000000000001", + "44444444-4444-4444-8444-000000000002", + "44444444-4444-4444-8444-000000000003" + ] + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipts", + "re": "11111111-1111-4111-8111-000000000005", + "receipts.#": 3, + "receipts.0.state": "outcome_unknown", + "receipts.1.state": "rejected", + "receipts.1.detail": "authorization history unknown", + "receipts.2.state": "rejected", + "receipts.2.detail": "authorization history unknown" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "dispatch_attempts": 1, + "lease_holder": null + } + } + ], + "note": "Appendix §H, \"A failed revocation write\". c1 executes P1 and queues P2 and P3. It loses control and the fence write itself fails (the rollback succeeds). In the same step the host revokes the lease in memory (revoked to c2; its epoch isn't persisted, so the fixture doesn't name it), rejects P2 and P3 in memory (control revoked; no live attachment of c1 is entitled to the receipts, so nothing is sent), halts the journal and closes c1's new link without a welcome (only c2 stays attached). P1's turn ending dispatches nothing: nothing queued is dispatched after a halt, and c2's Send is busy. A halted run writes no clean close, so after the restart P2 and P3 are durably rejected (authorization history unknown) before anything is dispatched, P1 is outcome_unknown, and nothing ever runs again." +} diff --git a/designs/protocol/fixtures/r8-failed-fence-write-then-control-restored-never-runs.json b/designs/protocol/fixtures/r8-failed-fence-write-then-control-restored-never-runs.json new file mode 100644 index 0000000..95c605b --- /dev/null +++ b/designs/protocol/fixtures/r8-failed-fence-write-then-control-restored-never-runs.json @@ -0,0 +1,522 @@ +{ + "name": "r8-failed-fence-write-then-control-restored-never-runs", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000001", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000001", + "lease.lease_epoch": 1 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 1 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "then deploy" + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000002" + } + }, + { + "fail_next_fsync": true + }, + { + "set_permissions": "c1", + "permissions": [ + "view" + ], + "attachment_gen": "77777777-7777-4777-8777-777777777777" + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "revoked", + "controller": null + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "journal_halted": true, + "lease_holder": null, + "dispatch_attempts": 1 + } + }, + { + "restart_runtime": { + "runtime_gen": "99999999-9999-4999-8999-999999999999", + "runtime_epoch": 8 + } + }, + { + "set_permissions": "c1", + "permissions": [ + "view", + "control" + ], + "attachment_gen": "88888888-8888-4888-8888-888888888888" + }, + { + "reconnect": "c2", + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "journal_halted": false, + "dispatch_attempts": 1, + "lease_holder": null + } + }, + { + "advance_ms": 60000 + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "dispatch_attempts": 1 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt_query", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "88888888-8888-4888-8888-888888888888" + }, + "request_ids": [ + "44444444-4444-4444-8444-000000000001", + "44444444-4444-4444-8444-000000000002" + ] + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipts", + "re": "11111111-1111-4111-8111-000000000003", + "receipts.#": 2, + "receipts.0.state": "outcome_unknown", + "receipts.1.state": "rejected", + "receipts.1.detail": "authorization history unknown" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "88888888-8888-4888-8888-888888888888" + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "then deploy" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "rejected", + "re": "11111111-1111-4111-8111-000000000004", + "detail": "authorization history unknown" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "dispatch_attempts": 1, + "lease_holder": null + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000005", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "88888888-8888-4888-8888-888888888888" + }, + "request_id": "44444444-4444-4444-8444-000000000003", + "kind": "prompt", + "payload": { + "text": "run the tests again" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000003", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000005" + }, + "capture": { + "e3": "lease.lease_epoch" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000003", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": "${e3}" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000006", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "88888888-8888-4888-8888-888888888888" + }, + "request_id": "44444444-4444-4444-8444-000000000004", + "kind": "prompt", + "payload": { + "text": "then deploy again" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000004", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000006", + "lease.lease_epoch": "${e3}" + } + }, + { + "fail_next_fsync": true, + "undo_fails": true + }, + { + "set_permissions": "c1", + "permissions": [ + "view" + ], + "attachment_gen": "cccccccc-cccc-4ccc-8ccc-cccccccccccc" + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "revoked", + "controller": null + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "journal_halted": true, + "lease_holder": null, + "dispatch_attempts": 2 + } + }, + { + "restart_runtime": { + "runtime_gen": "dddddddd-dddd-4ddd-8ddd-dddddddddddd", + "runtime_epoch": 9 + } + }, + { + "set_permissions": "c1", + "permissions": [ + "view", + "control" + ], + "attachment_gen": "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee" + }, + { + "reconnect": "c2", + "attachment_gen": "ffffffff-ffff-4fff-8fff-ffffffffffff" + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "dispatch_attempts": 2, + "lease_holder": null + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt_query", + "id": "11111111-1111-4111-8111-000000000007", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee" + }, + "request_ids": [ + "44444444-4444-4444-8444-000000000003", + "44444444-4444-4444-8444-000000000004" + ] + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipts", + "re": "11111111-1111-4111-8111-000000000007", + "receipts.#": 2, + "receipts.0.state": "outcome_unknown", + "receipts.1.state": "rejected", + "receipts.1.detail": "authorization history unknown" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "dispatch_attempts": 2, + "lease_holder": null + } + } + ], + "note": "Appendix §H, Recovery, Astation's r4 trace. c1 executes P1 with P2 queued under control_gen 1. It loses control and the fence write fails with a successful rollback: no durable fence, the journal halts. After the restart (no clean close: the journal was halted) the principal is welcomed with control again at once, so the stored control_gen still equals P2's and a control flag can't show the missed revocation; P2 was nevertheless rejected (authorization history unknown) at startup, before c1 authenticated, and never runs, even 60 s later; the resend with the same id returns that receipt. Second cycle: P3 executes and P4 is queued (an acquiring Send by the holder), then the fence write and its rollback both fail, so the fence record may be on disk. The outcome is the same whatever reached the disk: P4 rejected (authorization history unknown), never dispatched; P1 and P3 outcome_unknown." +} diff --git a/designs/protocol/fixtures/r8-failed-persistence-changes-no-lease.json b/designs/protocol/fixtures/r8-failed-persistence-changes-no-lease.json new file mode 100644 index 0000000..6f60713 --- /dev/null +++ b/designs/protocol/fixtures/r8-failed-persistence-changes-no-lease.json @@ -0,0 +1,354 @@ +{ + "name": "r8-failed-persistence-changes-no-lease", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "fail_next_fsync": true + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000001", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000001", + "code": "busy", + "detail": "acceptance not recorded" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "lease_holder": null, + "dispatch_attempts": 0, + "journal_halted": true + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000002", + "code": "busy", + "detail": "journal halted; restart Atem" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "acquire", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "takeover": false + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000003", + "code": "busy", + "detail": "journal halted; restart Atem" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "lease_holder": null, + "dispatch_attempts": 0 + } + }, + { + "restart_runtime": { + "runtime_gen": "99999999-9999-4999-8999-999999999999", + "runtime_epoch": 8 + } + }, + { + "reconnect": "c1", + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + }, + { + "reconnect": "c2", + "attachment_gen": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + }, + { + "assert": { + "journal_halted": false, + "dispatch_attempts": 0 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "rejected", + "re": "11111111-1111-4111-8111-000000000004", + "detail": "acceptance not confirmed" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "lease_holder": null, + "dispatch_attempts": 0 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt_query", + "id": "11111111-1111-4111-8111-000000000005", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + }, + "request_ids": [ + "44444444-4444-4444-8444-000000000001" + ] + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipts", + "re": "11111111-1111-4111-8111-000000000005", + "receipts.#": 1, + "receipts.0.request_id": "44444444-4444-4444-8444-000000000001", + "receipts.0.state": "rejected", + "receipts.0.detail": "acceptance not confirmed" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000006", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000006" + }, + "capture": { + "e1": "lease.lease_epoch" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": "${e1}" + } + }, + { + "assert": { + "lease_holder": "c1", + "dispatch_attempts": 1, + "dispatches": 1 + } + } + ], + "note": "Appendix §I. The admission's journal write fails (the epoch reservation or the accepted record; the rollback succeeds, so nothing of the request is on disk): the Send is answered busy (acceptance not recorded), no lease is granted, c2 hears nothing and nothing runs. The journal halts: the retry with the same request_id and c2's acquire are busy until the runtime restarts. The refusal was recorded on the refusal list before the reply, so at the restart the request is resolved rejected (acceptance not confirmed) whatever reached the disk: the same request_id now returns that receipt and is never re-admitted (no lease). The resend uses a new request_id, which is admitted normally and acquires (its epoch is captured: reservations may skip epochs)." +} diff --git a/designs/protocol/fixtures/r8-hello-without-revision-8-is-refused-before-any-attachment.json b/designs/protocol/fixtures/r8-hello-without-revision-8-is-refused-before-any-attachment.json new file mode 100644 index 0000000..1a2e354 --- /dev/null +++ b/designs/protocol/fixtures/r8-hello-without-revision-8-is-refused-before-any-attachment.json @@ -0,0 +1,237 @@ +{ + "name": "r8-hello-without-revision-8-is-refused-before-any-attachment", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native", + "welcome": false + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "hello", + "id": "11111111-1111-4111-8111-000000000001" + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000001", + "code": "unsupported_version", + "revision": 8 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "hello", + "id": "11111111-1111-4111-8111-000000000002", + "revision": 7 + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000002", + "code": "unsupported_version", + "revision": 8 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "hello", + "id": "11111111-1111-4111-8111-000000000003", + "revision": "r8" + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000003", + "code": "unsupported_version" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000004" + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000004", + "code": "unsupported_version" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "acquire", + "id": "11111111-1111-4111-8111-000000000005", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "takeover": false + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000005", + "code": "unsupported_version" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "open", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + } + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "error", + "code": "unsupported_version", + "conversationId": "conv-1" + } + }, + { + "expect_none": "c1", + "for_ms": 1000 + }, + { + "assert": { + "dispatch_attempts": 0, + "pty_writes": 0, + "lease_holder": null, + "attachments": 0, + "receipts_visible": { + "c1": 0 + } + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "hello", + "id": "11111111-1111-4111-8111-000000000006", + "revision": 8 + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "welcome", + "re": "11111111-1111-4111-8111-000000000006", + "revision": 8, + "features": [ + "chat.history_end", + "chat.history", + "chat.lookup", + "chat.coverage", + "receipt.result", + "approval.session_rules", + "approval.card_reason" + ] + }, + "capture": { + "ag": "attachment_gen" + } + }, + { + "assert": { + "dispatch_attempts": 0, + "pty_writes": 0, + "lease_holder": null, + "attachments": 1 + } + } + ], + "note": "An r8 host admits only hello{revision: 8}. A hello without revision (an r7 client), with another number, or with a non-integer is refused with session error unsupported_version (carrying the host's revision) before any attachment exists; session, terminal and chat requests stay refused as before welcome. No attachment, lease, receipt, dispatch or chat conversation results. A corrected hello is then welcomed with revision 8 and the host's features." +} \ No newline at end of file diff --git a/designs/protocol/fixtures/r8-history-page-and-live-events-share-one-ordered-stream.json b/designs/protocol/fixtures/r8-history-page-and-live-events-share-one-ordered-stream.json new file mode 100644 index 0000000..c0d3063 --- /dev/null +++ b/designs/protocol/fixtures/r8-history-page-and-live-events-share-one-ordered-stream.json @@ -0,0 +1,381 @@ +{ + "name": "r8-history-page-and-live-events-share-one-ordered-stream", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000001" + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "sessions", + "re": "11111111-1111-4111-8111-000000000001", + "sessions.#": 1 + } + }, + { + "chat_history": "ses-aaaaaaaaaaaa", + "messages": [ + { + "messageId": "m-0001", + "role": "user", + "turnId": "turn-1", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "run the tests" + } + ], + "origin": "local" + }, + { + "messageId": "m-0002", + "role": "assistant", + "turnId": "turn-1", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "Running npm test" + } + ] + } + ] + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "open", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + } + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "opened", + "sequence": 0 + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 1, + "messageId": "m-0001", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 2, + "messageId": "m-0002", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "history_end", + "sequence": 3, + "page.end": true, + "page.count": 2 + } + }, + { + "open_approval": "ses-aaaaaaaaaaaa", + "tool": "Bash", + "summary": "npm test", + "turn_id": "turn-1" + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_request", + "session_id": "ses-aaaaaaaaaaaa", + "tool": "Bash" + }, + "capture": { + "aid": "approval_id" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 4, + "messageId": "approval-${aid}", + "parts.0.state": "pending" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p1", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "turn_id": "turn-1" + } + } + }, + { + "close_approval": "${aid}", + "reason": "answered_terminal" + }, + { + "chat_message": "ses-aaaaaaaaaaaa", + "message": { + "messageId": "m-0002", + "role": "assistant", + "turnId": "turn-1", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "Running npm test" + }, + { + "id": "p2", + "kind": "text", + "markdown": "All 12 tests passed." + } + ] + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 5, + "messageId": "m-0001", + "history": true, + "page_id": "p1" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 6, + "messageId": "m-0002", + "history": true, + "page_id": "p1", + "parts.#": 1 + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 7, + "messageId": "approval-${aid}", + "history": true, + "page_id": "p1", + "parts.0.state": "pending" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "history_end", + "sequence": 8, + "page_id": "p1", + "page.end": true, + "page.count": 3, + "page.selector": "turn_id", + "page.coverage": "complete" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "approval_closed", + "approval_id": "${aid}", + "reason": "answered_terminal" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 9, + "messageId": "approval-${aid}", + "parts.0.state": "answered_terminal", + "parts.0.reason": "answered_terminal" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 10, + "messageId": "m-0002", + "parts.#": 2 + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p2", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "turn_id": "turn-1" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 11, + "messageId": "m-0001", + "history": true, + "page_id": "p2" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 12, + "messageId": "m-0002", + "history": true, + "page_id": "p2", + "parts.#": 2 + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 13, + "messageId": "approval-${aid}", + "history": true, + "page_id": "p2", + "parts.0.state": "answered_terminal" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "history_end", + "sequence": 14, + "page_id": "p2", + "page.end": true, + "page.count": 3 + } + }, + { + "assert": { + "dispatch_attempts": 0, + "pty_writes": 0, + "lease_holder": null, + "approvals_pending": 0 + } + } + ], + "note": "A history page is snapshotted and enqueued in one host step, in the conversation's sequenced stream, between live events. The history request p1 arrives before the approval closes and m-0002 gains a part, so its page (sequences 5-8) holds the older versions and every newer version follows at a higher sequence (9, 10). A client keeps, per messageId, the version with the highest sequence: the delayed page never replaces the closed card or drop the appended part, and a page never revives an approval (the session inbox decides; approvals_pending is 0). A later page (p2) holds the new versions." +} \ No newline at end of file diff --git a/designs/protocol/fixtures/r8-history-selectors-are-exclusive-and-validated.json b/designs/protocol/fixtures/r8-history-selectors-are-exclusive-and-validated.json new file mode 100644 index 0000000..a125046 --- /dev/null +++ b/designs/protocol/fixtures/r8-history-selectors-are-exclusive-and-validated.json @@ -0,0 +1,571 @@ +{ + "name": "r8-history-selectors-are-exclusive-and-validated", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "chat_history": "ses-aaaaaaaaaaaa", + "messages": [ + { + "messageId": "m-0001", + "role": "user", + "turnId": "turn-1", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "hi" + } + ], + "origin": "local" + } + ] + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "open", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + } + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "opened", + "sequence": 0 + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 1, + "messageId": "m-0001", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "history_end", + "sequence": 2, + "page.end": true + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p0", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "before": "m-0001", + "turn_id": "turn-1" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "error", + "sequence": 3, + "code": "invalid_request", + "page_id": "p0" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p1", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "turn_id": "turn-1", + "request_id": "44444444-4444-4444-8444-000000000001" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "error", + "sequence": 4, + "code": "invalid_request", + "page_id": "p1" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p2", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "before": "m-0001", + "next": "dG9rZW4" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "error", + "sequence": 5, + "code": "invalid_request", + "page_id": "p2" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p3", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + } + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "error", + "sequence": 6, + "code": "invalid_request", + "page_id": "p3" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p4", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "not-a-uuid" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "error", + "sequence": 7, + "code": "invalid_request", + "page_id": "p4" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p5", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-00000000000A" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "error", + "sequence": 8, + "code": "invalid_request", + "page_id": "p5" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p6", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "turn_id": "ttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "error", + "sequence": 9, + "code": "invalid_request", + "page_id": "p6" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p7", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "turn_id": "" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "error", + "sequence": 10, + "code": "invalid_request", + "page_id": "p7" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p8", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "before": "m 0001" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "error", + "sequence": 11, + "code": "invalid_request", + "page_id": "p8" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p9", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "next": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "error", + "sequence": 12, + "code": "invalid_request", + "page_id": "p9" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p10", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "next": "not base64url!" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "error", + "sequence": 13, + "code": "invalid_request", + "page_id": "p10" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "this page id is too long and has spaces in it", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + } + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "error", + "sequence": 14, + "code": "invalid_request" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p-ok", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "before": "m-0001" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "history_end", + "sequence": 15, + "page_id": "p-ok", + "page.count": 0, + "page.end": true + } + }, + { + "expect_none": "c1", + "for_ms": 1000 + }, + { + "assert": { + "dispatch_attempts": 0, + "pty_writes": 0, + "lease_holder": null, + "chat_opens": 1, + "chat_closes": 0 + } + } + ], + "note": "history takes exactly one of before, turn_id, request_id or next. None, two, or a malformed value (before/turn_id not 1-128 bytes of [A-Za-z0-9._:-], request_id not a lowercase v4 UUID, next not 1-512 bytes of base64url, page_id not 1-64 bytes of [A-Za-z0-9._:-]) is chat error invalid_request, sequenced in the conversation with the request's page_id when it was valid. No page is sent and nothing resets. A valid request still works afterwards." +} \ No newline at end of file diff --git a/designs/protocol/fixtures/r8-local-input-wins-over-a-send.json b/designs/protocol/fixtures/r8-local-input-wins-over-a-send.json new file mode 100644 index 0000000..f09ba3f --- /dev/null +++ b/designs/protocol/fixtures/r8-local-input-wins-over-a-send.json @@ -0,0 +1,311 @@ +{ + "name": "r8-local-input-wins-over-a-send", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000001", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000001", + "lease.lease_epoch": 1 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 1 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "then run the linter" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000002", + "lease.lease_epoch": 1 + } + }, + { + "local_input": "ses-aaaaaaaaaaaa", + "bytes": "eA==" + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "local_input", + "lease_epoch": 2, + "controller": null + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "local_input", + "lease_epoch": 2, + "controller": null + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + }, + "request_id": "44444444-4444-4444-8444-000000000003", + "kind": "prompt", + "payload": { + "text": "and commit" + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000003", + "code": "stale_lease" + } + }, + { + "turn_complete": "ses-aaaaaaaaaaaa", + "turn_id": "turn-1" + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "completed", + "detail": "done" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "executing" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "rejected", + "detail": "the terminal has unsent input; finish or clear it there" + } + }, + { + "assert": { + "lease_holder": null, + "pty_writes_local": 1, + "pty_writes_remote": 0, + "dispatch_attempts": 2, + "dispatches": 1 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000003", + "kind": "prompt", + "payload": { + "text": "and commit" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000003", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000004", + "lease.lease_epoch": 3 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000003", + "state": "executing" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000003", + "state": "rejected", + "detail": "the terminal has unsent input; finish or clear it there" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 3 + } + }, + { + "assert": { + "lease_holder": "c1", + "dispatch_attempts": 3, + "dispatches": 1 + } + } + ], + "note": "Local terminal input revokes the lease a Send acquired (reason local_input, new epoch) before it is written, exactly as for terminal input. A queued accepted prompt is not cancelled by the revocation, but the chat adapter's draft check refuses it at dispatch because the terminal now has unsent input (rejected, no paste). The old epoch is stale; a later acquiring Send may take the free lease, and its prompt is refused the same way while the local draft is there." +} diff --git a/designs/protocol/fixtures/r8-long-turn-is-recovered-in-bounded-pages.json b/designs/protocol/fixtures/r8-long-turn-is-recovered-in-bounded-pages.json new file mode 100644 index 0000000..aeb4363 --- /dev/null +++ b/designs/protocol/fixtures/r8-long-turn-is-recovered-in-bounded-pages.json @@ -0,0 +1,397 @@ +{ + "name": "r8-long-turn-is-recovered-in-bounded-pages", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "open", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + } + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "opened", + "sequence": 0 + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "history_end", + "sequence": 1, + "page.end": true, + "page.count": 0, + "page.coverage": "complete" + } + }, + { + "chat_turn": "ses-aaaaaaaaaaaa", + "turn_id": "turn-9", + "origin": "local", + "assistant_messages": 70, + "text_bytes": 6000, + "escape": "control" + }, + { + "expect_live": "c1", + "message_ids": [ + "turn-9-u", + "turn-9-a-0001", + "turn-9-a-0002", + "turn-9-a-0003", + "turn-9-a-0004", + "turn-9-a-0005", + "turn-9-a-0006", + "turn-9-a-0007", + "turn-9-a-0008", + "turn-9-a-0009", + "turn-9-a-0010", + "turn-9-a-0011", + "turn-9-a-0012", + "turn-9-a-0013", + "turn-9-a-0014", + "turn-9-a-0015", + "turn-9-a-0016", + "turn-9-a-0017", + "turn-9-a-0018", + "turn-9-a-0019", + "turn-9-a-0020", + "turn-9-a-0021", + "turn-9-a-0022", + "turn-9-a-0023", + "turn-9-a-0024", + "turn-9-a-0025", + "turn-9-a-0026", + "turn-9-a-0027", + "turn-9-a-0028", + "turn-9-a-0029", + "turn-9-a-0030", + "turn-9-a-0031", + "turn-9-a-0032", + "turn-9-a-0033", + "turn-9-a-0034", + "turn-9-a-0035", + "turn-9-a-0036", + "turn-9-a-0037", + "turn-9-a-0038", + "turn-9-a-0039", + "turn-9-a-0040", + "turn-9-a-0041", + "turn-9-a-0042", + "turn-9-a-0043", + "turn-9-a-0044", + "turn-9-a-0045", + "turn-9-a-0046", + "turn-9-a-0047", + "turn-9-a-0048", + "turn-9-a-0049", + "turn-9-a-0050", + "turn-9-a-0051", + "turn-9-a-0052", + "turn-9-a-0053", + "turn-9-a-0054", + "turn-9-a-0055", + "turn-9-a-0056", + "turn-9-a-0057", + "turn-9-a-0058", + "turn-9-a-0059", + "turn-9-a-0060", + "turn-9-a-0061", + "turn-9-a-0062", + "turn-9-a-0063", + "turn-9-a-0064", + "turn-9-a-0065", + "turn-9-a-0066", + "turn-9-a-0067", + "turn-9-a-0068", + "turn-9-a-0069", + "turn-9-a-0070" + ], + "sequence_from": 2 + }, + { + "recover": "c1", + "request": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p1", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "turn_id": "turn-9" + } + }, + "message_ids": [ + "turn-9-u", + "turn-9-a-0001", + "turn-9-a-0002", + "turn-9-a-0003", + "turn-9-a-0004", + "turn-9-a-0005", + "turn-9-a-0006", + "turn-9-a-0007", + "turn-9-a-0008", + "turn-9-a-0009", + "turn-9-a-0010", + "turn-9-a-0011", + "turn-9-a-0012", + "turn-9-a-0013", + "turn-9-a-0014", + "turn-9-a-0015", + "turn-9-a-0016", + "turn-9-a-0017", + "turn-9-a-0018", + "turn-9-a-0019", + "turn-9-a-0020", + "turn-9-a-0021", + "turn-9-a-0022", + "turn-9-a-0023", + "turn-9-a-0024", + "turn-9-a-0025", + "turn-9-a-0026", + "turn-9-a-0027", + "turn-9-a-0028", + "turn-9-a-0029", + "turn-9-a-0030", + "turn-9-a-0031", + "turn-9-a-0032", + "turn-9-a-0033", + "turn-9-a-0034", + "turn-9-a-0035", + "turn-9-a-0036", + "turn-9-a-0037", + "turn-9-a-0038", + "turn-9-a-0039", + "turn-9-a-0040", + "turn-9-a-0041", + "turn-9-a-0042", + "turn-9-a-0043", + "turn-9-a-0044", + "turn-9-a-0045", + "turn-9-a-0046", + "turn-9-a-0047", + "turn-9-a-0048", + "turn-9-a-0049", + "turn-9-a-0050", + "turn-9-a-0051", + "turn-9-a-0052", + "turn-9-a-0053", + "turn-9-a-0054", + "turn-9-a-0055", + "turn-9-a-0056", + "turn-9-a-0057", + "turn-9-a-0058", + "turn-9-a-0059", + "turn-9-a-0060", + "turn-9-a-0061", + "turn-9-a-0062", + "turn-9-a-0063", + "turn-9-a-0064", + "turn-9-a-0065", + "turn-9-a-0066", + "turn-9-a-0067", + "turn-9-a-0068", + "turn-9-a-0069", + "turn-9-a-0070" + ], + "min_pages": 3, + "max_count": 50, + "max_content_bytes": 262144, + "max_wire_bytes": 1048576, + "end": { + "page.end": true, + "page.selector": "turn_id", + "page.coverage": "complete", + "page.holes.#": 0 + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "close", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa" + } + } + }, + { + "recover": "c1", + "request": { + "type": "agentChat", + "data": { + "v": 1, + "op": "open", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + } + } + }, + "message_ids": [ + "turn-9-u", + "turn-9-a-0001", + "turn-9-a-0002", + "turn-9-a-0003", + "turn-9-a-0004", + "turn-9-a-0005", + "turn-9-a-0006", + "turn-9-a-0007", + "turn-9-a-0008", + "turn-9-a-0009", + "turn-9-a-0010", + "turn-9-a-0011", + "turn-9-a-0012", + "turn-9-a-0013", + "turn-9-a-0014", + "turn-9-a-0015", + "turn-9-a-0016", + "turn-9-a-0017", + "turn-9-a-0018", + "turn-9-a-0019", + "turn-9-a-0020", + "turn-9-a-0021", + "turn-9-a-0022", + "turn-9-a-0023", + "turn-9-a-0024", + "turn-9-a-0025", + "turn-9-a-0026", + "turn-9-a-0027", + "turn-9-a-0028", + "turn-9-a-0029", + "turn-9-a-0030", + "turn-9-a-0031", + "turn-9-a-0032", + "turn-9-a-0033", + "turn-9-a-0034", + "turn-9-a-0035", + "turn-9-a-0036", + "turn-9-a-0037", + "turn-9-a-0038", + "turn-9-a-0039", + "turn-9-a-0040", + "turn-9-a-0041", + "turn-9-a-0042", + "turn-9-a-0043", + "turn-9-a-0044", + "turn-9-a-0045", + "turn-9-a-0046", + "turn-9-a-0047", + "turn-9-a-0048", + "turn-9-a-0049", + "turn-9-a-0050", + "turn-9-a-0051", + "turn-9-a-0052", + "turn-9-a-0053", + "turn-9-a-0054", + "turn-9-a-0055", + "turn-9-a-0056", + "turn-9-a-0057", + "turn-9-a-0058", + "turn-9-a-0059", + "turn-9-a-0060", + "turn-9-a-0061", + "turn-9-a-0062", + "turn-9-a-0063", + "turn-9-a-0064", + "turn-9-a-0065", + "turn-9-a-0066", + "turn-9-a-0067", + "turn-9-a-0068", + "turn-9-a-0069", + "turn-9-a-0070" + ], + "min_pages": 3, + "max_count": 50, + "max_content_bytes": 262144, + "max_wire_bytes": 1048576, + "end": { + "page.end": true, + "page.selector": "newest", + "page.truncated": false + } + }, + { + "expect_none": "c1", + "for_ms": 1000 + }, + { + "assert": { + "dispatch_attempts": 0, + "pty_writes": 0, + "lease_holder": null, + "chat_opens": 2, + "chat_closes": 0 + } + } + ], + "note": "One turn of 71 messages: a short user prompt and 70 assistant messages of 6000 bytes each made of U+0001, which JSON escapes to 6 bytes (about 36 KiB per serialized message, 420 KB of content). Recovering it by turn_id goes oldest first; reopening and following the open page's next goes newest first (pages of older messages, each listed oldest first). Each page stops before the message that would pass 50 messages, 256 KiB of content or 1 MiB serialized (the sum of its envelopes), so here the serialized bound cuts the pages. Each page continues with history{next} and the token from the previous history_end. The concatenated pages hold every message exactly once, in order, with no history_end saying end: true before the last one." +} \ No newline at end of file diff --git a/designs/protocol/fixtures/r8-open-with-before-is-refused.json b/designs/protocol/fixtures/r8-open-with-before-is-refused.json new file mode 100644 index 0000000..9e30bb4 --- /dev/null +++ b/designs/protocol/fixtures/r8-open-with-before-is-refused.json @@ -0,0 +1,311 @@ +{ + "name": "r8-open-with-before-is-refused", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "chat_history": "ses-aaaaaaaaaaaa", + "messages": [ + { + "messageId": "m-0001", + "role": "user", + "turnId": "turn-1", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 1" + } + ], + "origin": "local" + }, + { + "messageId": "m-0002", + "role": "assistant", + "turnId": "turn-1", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 2" + } + ] + }, + { + "messageId": "m-0003", + "role": "user", + "turnId": "turn-2", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 3" + } + ], + "origin": "local" + }, + { + "messageId": "m-0004", + "role": "assistant", + "turnId": "turn-2", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 4" + } + ] + } + ] + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "open", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + } + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "opened", + "sequence": 0 + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 1, + "messageId": "m-0001", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 2, + "messageId": "m-0002", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 3, + "messageId": "m-0003", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 4, + "messageId": "m-0004", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "history_end", + "sequence": 5, + "page.end": true, + "page.count": 4, + "page.coverage": "complete" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "open", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "before": "m-0003" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "error", + "code": "unsupported_version", + "conversationId": "conv-1" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "open", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "before": "m-0003", + "generation": "22222222-2222-4222-8222-222222222222" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "error", + "code": "unsupported_version", + "conversationId": "conv-1" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "open", + "conversationId": "conv-2", + "agentId": "ses-aaaaaaaaaaaa", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "before": "m-0003" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "error", + "code": "unsupported_version", + "conversationId": "conv-2" + } + }, + { + "chat_message": "ses-aaaaaaaaaaaa", + "message": { + "messageId": "m-0005", + "role": "user", + "turnId": "turn-3", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 5" + } + ], + "origin": "local" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 6, + "messageId": "m-0005" + } + }, + { + "expect_none": "c1", + "for_ms": 1000 + }, + { + "assert": { + "dispatch_attempts": 0, + "pty_writes": 0, + "lease_holder": null, + "chat_opens": 1, + "chat_conversations": 1, + "chat_closes": 0 + } + } + ], + "note": "r8 removes open.before (and its generation). An r8 host answers such an open with chat error unsupported_version and nothing else: no opened, no page, no reset of the open conversation (the next live message continues at sequence 6), and no new conversation for conv-2 (it gets no live messages). It is never read as an open of the newest page. Older pages use history." +} \ No newline at end of file diff --git a/designs/protocol/fixtures/r8-ownership-is-checked-at-admission-not-as-displayed.json b/designs/protocol/fixtures/r8-ownership-is-checked-at-admission-not-as-displayed.json new file mode 100644 index 0000000..52706e1 --- /dev/null +++ b/designs/protocol/fixtures/r8-ownership-is-checked-at-admission-not-as-displayed.json @@ -0,0 +1,282 @@ +{ + "name": "r8-ownership-is-checked-at-admission-not-as-displayed", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000001" + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "sessions", + "re": "11111111-1111-4111-8111-000000000001", + "sessions.#": 1, + "sessions.0.controller": null + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "acquire", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "takeover": false + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease", + "re": "11111111-1111-4111-8111-000000000002", + "binding.lease_epoch": 1 + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 1 + }, + "capture": { + "c1ctl": "controller.controller_session" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "session_changed", + "descriptor.controller.lease_epoch": 1 + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000003", + "code": "lease_held", + "controller.controller_session": "${c1ctl}", + "controller.lease_epoch": 1 + } + }, + { + "assert": { + "lease_holder": "c1", + "dispatch_attempts": 0 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "release", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease_released", + "re": "11111111-1111-4111-8111-000000000004" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "released", + "lease_epoch": 2, + "controller": null + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "session_changed", + "descriptor.controller": null + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000005", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true, + "takeover": true, + "expected_controller": { + "controller_session": "${c1ctl}", + "lease_epoch": 1 + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000005", + "lease.lease_epoch": 3 + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "session_changed", + "descriptor.controller.lease_epoch": 3 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 3 + } + }, + { + "assert": { + "lease_holder": "c2", + "dispatches": 1 + } + } + ], + "note": "Astation's display is not authority. c2 saw the session unowned, then c1 acquired before c2's ordinary Send arrived: the Send is refused lease_held (no execution, no takeover). Later c2's takeover confirmation names c1's lease, but c1 has released meanwhile: the session is unowned at admission, so the Send acquires as an ordinary grant (reason granted; nobody's control is taken)." +} diff --git a/designs/protocol/fixtures/r8-process-replaced-before-dispatch.json b/designs/protocol/fixtures/r8-process-replaced-before-dispatch.json new file mode 100644 index 0000000..c28ab74 --- /dev/null +++ b/designs/protocol/fixtures/r8-process-replaced-before-dispatch.json @@ -0,0 +1,286 @@ +{ + "name": "r8-process-replaced-before-dispatch", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000001", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000001", + "lease.lease_epoch": 1 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 1 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "then deploy" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000002", + "lease.lease_epoch": 1 + } + }, + { + "replace_process": "ses-aaaaaaaaaaaa", + "process_gen": "88888888-8888-4888-8888-888888888888" + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "process_ended", + "lease_epoch": 2, + "controller": null + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "process_ended", + "lease_epoch": 2, + "controller": null + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "rejected", + "detail": "session closed" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "outcome_unknown", + "detail": "agent exited" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000003", + "kind": "prompt", + "payload": { + "text": "then deploy" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000003", + "code": "stale_target" + } + }, + { + "assert": { + "lease_holder": null, + "dispatch_attempts": 1, + "dispatches": 1 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "88888888-8888-4888-8888-888888888888" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000004", + "kind": "prompt", + "payload": { + "text": "then deploy" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000004", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000004", + "lease.lease_epoch": 3 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000004", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 3 + } + }, + { + "assert": { + "lease_holder": "c1", + "dispatch_attempts": 2, + "dispatches": 2 + } + } + ], + "note": "The agent process is replaced while a Send is accepted but not dispatched: the lease ends (process_ended), the executing prompt becomes outcome_unknown (agent exited) and the queued one rejected (session closed), never sent to the new process. A Send naming the old process_gen is stale_target with no lease change; one naming the new process acquires as usual." +} diff --git a/designs/protocol/fixtures/r8-receipt-result-bounds-reply-message-ids.json b/designs/protocol/fixtures/r8-receipt-result-bounds-reply-message-ids.json new file mode 100644 index 0000000..e4b402e --- /dev/null +++ b/designs/protocol/fixtures/r8-receipt-result-bounds-reply-message-ids.json @@ -0,0 +1,325 @@ +{ + "name": "r8-receipt-result-bounds-reply-message-ids", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native", + "holds_lease_epoch": 1 + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "open", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + } + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "opened", + "sequence": 0 + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "history_end", + "sequence": 1, + "page.count": 0 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000001", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "summarize the repo" + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "chat_turn": "ses-aaaaaaaaaaaa", + "turn_id": "turn-7", + "request_id": "44444444-4444-4444-8444-000000000001", + "origin": "remote", + "assistant_messages": 40, + "text_bytes": 200 + }, + { + "expect_live": "c1", + "message_ids": [ + "turn-7-u", + "turn-7-a-0001", + "turn-7-a-0002", + "turn-7-a-0003", + "turn-7-a-0004", + "turn-7-a-0005", + "turn-7-a-0006", + "turn-7-a-0007", + "turn-7-a-0008", + "turn-7-a-0009", + "turn-7-a-0010", + "turn-7-a-0011", + "turn-7-a-0012", + "turn-7-a-0013", + "turn-7-a-0014", + "turn-7-a-0015", + "turn-7-a-0016", + "turn-7-a-0017", + "turn-7-a-0018", + "turn-7-a-0019", + "turn-7-a-0020", + "turn-7-a-0021", + "turn-7-a-0022", + "turn-7-a-0023", + "turn-7-a-0024", + "turn-7-a-0025", + "turn-7-a-0026", + "turn-7-a-0027", + "turn-7-a-0028", + "turn-7-a-0029", + "turn-7-a-0030", + "turn-7-a-0031", + "turn-7-a-0032", + "turn-7-a-0033", + "turn-7-a-0034", + "turn-7-a-0035", + "turn-7-a-0036", + "turn-7-a-0037", + "turn-7-a-0038", + "turn-7-a-0039", + "turn-7-a-0040" + ], + "sequence_from": 2 + }, + { + "turn_complete": "ses-aaaaaaaaaaaa", + "turn_id": "turn-7" + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "complete", + "sequence": 43, + "turnId": "turn-7", + "outcome": "done" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "completed", + "detail": "done", + "result.turn_id": "turn-7", + "result.user_message_id": "turn-7-u", + "result.outcome": "done", + "result.reply_message_ids.#": 32, + "result.reply_message_ids.0": "turn-7-a-0001", + "result.reply_message_ids.31": "turn-7-a-0032", + "result.more": true, + "result.replies": "complete" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt_query", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_ids": [ + "44444444-4444-4444-8444-000000000001" + ] + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipts", + "receipts.#": 1, + "receipts.0.result.reply_message_ids.#": 32, + "receipts.0.result.more": true + } + }, + { + "recover": "c1", + "request": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p1", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001" + } + }, + "message_ids": [ + "turn-7-u", + "turn-7-a-0001", + "turn-7-a-0002", + "turn-7-a-0003", + "turn-7-a-0004", + "turn-7-a-0005", + "turn-7-a-0006", + "turn-7-a-0007", + "turn-7-a-0008", + "turn-7-a-0009", + "turn-7-a-0010", + "turn-7-a-0011", + "turn-7-a-0012", + "turn-7-a-0013", + "turn-7-a-0014", + "turn-7-a-0015", + "turn-7-a-0016", + "turn-7-a-0017", + "turn-7-a-0018", + "turn-7-a-0019", + "turn-7-a-0020", + "turn-7-a-0021", + "turn-7-a-0022", + "turn-7-a-0023", + "turn-7-a-0024", + "turn-7-a-0025", + "turn-7-a-0026", + "turn-7-a-0027", + "turn-7-a-0028", + "turn-7-a-0029", + "turn-7-a-0030", + "turn-7-a-0031", + "turn-7-a-0032", + "turn-7-a-0033", + "turn-7-a-0034", + "turn-7-a-0035", + "turn-7-a-0036", + "turn-7-a-0037", + "turn-7-a-0038", + "turn-7-a-0039", + "turn-7-a-0040" + ], + "min_pages": 1, + "max_count": 50, + "max_content_bytes": 262144, + "max_wire_bytes": 1048576, + "end": { + "page.end": true, + "page.selector": "request_id", + "page.coverage": "complete" + } + }, + { + "assert": { + "dispatches": 1, + "receipts_visible": { + "c1": 1 + } + } + } + ], + "note": "A completed prompt receipt carries result {turn_id, user_message_id, outcome, reply_message_ids, more, replies}. reply_message_ids lists at most the first 32 replies in timeline order; more: true says history{request_id} pages hold the rest. Only turn_id, user_message_id and outcome are journaled; reply ids are computed from retained history when the receipt is sent. A receipts reply holds at most 64 receipts, each bounded, so it stays far below 1 MiB. Nothing is dispatched again to recover replies." +} \ No newline at end of file diff --git a/designs/protocol/fixtures/r8-recovered-work-needs-its-principal-confirmed.json b/designs/protocol/fixtures/r8-recovered-work-needs-its-principal-confirmed.json new file mode 100644 index 0000000..ac1a910 --- /dev/null +++ b/designs/protocol/fixtures/r8-recovered-work-needs-its-principal-confirmed.json @@ -0,0 +1,264 @@ +{ + "name": "r8-recovered-work-needs-its-principal-confirmed", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000001", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000001", + "lease.lease_epoch": 1 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 1 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "then deploy" + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000002" + } + }, + { + "restart_runtime": { + "runtime_gen": "99999999-9999-4999-8999-999999999999", + "runtime_epoch": 8 + } + }, + { + "reconnect": "c2", + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + }, + { + "advance_ms": 59999 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "dispatch_attempts": 1, + "lease_holder": null + } + }, + { + "advance_ms": 1 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "dispatch_attempts": 1 + } + }, + { + "reconnect": "c1", + "attachment_gen": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt_query", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + }, + "request_ids": [ + "44444444-4444-4444-8444-000000000001", + "44444444-4444-4444-8444-000000000002" + ] + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipts", + "re": "11111111-1111-4111-8111-000000000003", + "receipts.#": 2, + "receipts.0.state": "outcome_unknown", + "receipts.1.state": "rejected", + "receipts.1.detail": "control not confirmed" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "then deploy" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "rejected", + "re": "11111111-1111-4111-8111-000000000004", + "detail": "control not confirmed" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "dispatch_attempts": 1, + "lease_holder": null + } + } + ], + "note": "Appendix §H, Recovery rule 3. c1 executes P1 with P2 queued; the runtime stops in order with a healthy journal (clean close). After the restart P2 runs only once its principal's control is confirmed in this run; another principal (c2) reconnecting doesn't confirm it. 60 s after the host's start (now >= start + 60000) without c1 it is rejected (control not confirmed) and never runs; c1, reconnecting later, finds that receipt (P1, executing at the stop, is outcome_unknown) and its resend with the same id returns it." +} diff --git a/designs/protocol/fixtures/r8-revoked-control-fences-work-across-reconnect-and-restart.json b/designs/protocol/fixtures/r8-revoked-control-fences-work-across-reconnect-and-restart.json new file mode 100644 index 0000000..90296e0 --- /dev/null +++ b/designs/protocol/fixtures/r8-revoked-control-fences-work-across-reconnect-and-restart.json @@ -0,0 +1,440 @@ +{ + "name": "r8-revoked-control-fences-work-across-reconnect-and-restart", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000001", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000001", + "lease.lease_epoch": 1 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 1 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "then deploy" + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000002" + } + }, + { + "disconnect": "c1" + }, + { + "reconnect": "c1", + "attachment_gen": "77777777-7777-4777-8777-777777777777" + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "turn_complete": "ses-aaaaaaaaaaaa", + "turn_id": "turn-1" + }, + { + "assert": { + "dispatch_attempts": 2, + "dispatches": 2 + } + }, + { + "advance_ms": 15000 + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "expired", + "lease_epoch": 2, + "controller": null + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "expired", + "lease_epoch": 2, + "controller": null + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "77777777-7777-4777-8777-777777777777" + }, + "request_id": "44444444-4444-4444-8444-000000000003", + "kind": "prompt", + "payload": { + "text": "then tag the release" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000003", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000003", + "lease.lease_epoch": 3 + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 3 + } + }, + { + "disconnect": "c1" + }, + { + "set_permissions": "c1", + "permissions": [ + "view" + ], + "attachment_gen": "88888888-8888-4888-8888-888888888888" + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "revoked", + "lease_epoch": 4, + "controller": null + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "turn_complete": "ses-aaaaaaaaaaaa", + "turn_id": "turn-2" + }, + { + "assert": { + "dispatch_attempts": 2, + "lease_holder": null + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt_query", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "88888888-8888-4888-8888-888888888888" + }, + "request_ids": [ + "44444444-4444-4444-8444-000000000001", + "44444444-4444-4444-8444-000000000002", + "44444444-4444-4444-8444-000000000003" + ] + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipts", + "re": "11111111-1111-4111-8111-000000000004", + "receipts.#": 3, + "receipts.0.state": "completed", + "receipts.1.state": "completed", + "receipts.2.request_id": "44444444-4444-4444-8444-000000000003", + "receipts.2.state": "rejected", + "receipts.2.detail": "control revoked" + } + }, + { + "set_permissions": "c1", + "permissions": [ + "view", + "control" + ], + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000005", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + }, + "request_id": "44444444-4444-4444-8444-000000000003", + "kind": "prompt", + "payload": { + "text": "then tag the release" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000003", + "state": "rejected", + "re": "11111111-1111-4111-8111-000000000005", + "detail": "control revoked" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "dispatch_attempts": 2, + "lease_holder": null + } + }, + { + "restart_runtime": { + "runtime_gen": "99999999-9999-4999-8999-999999999999", + "runtime_epoch": 8 + } + }, + { + "reconnect": "c1", + "attachment_gen": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + }, + { + "reconnect": "c2", + "attachment_gen": "cccccccc-cccc-4ccc-8ccc-cccccccccccc" + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt_query", + "id": "11111111-1111-4111-8111-000000000006", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" + }, + "request_ids": [ + "44444444-4444-4444-8444-000000000003" + ] + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipts", + "re": "11111111-1111-4111-8111-000000000006", + "receipts.#": 1, + "receipts.0.state": "rejected", + "receipts.0.detail": "control revoked" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "dispatch_attempts": 2, + "lease_holder": null + } + } + ], + "note": "Appendix §H. c1's principal sends P1 (acquiring) and queues P2. It disconnects and reconnects with the same permissions: same principal, same control_gen, so P2 still runs after P1 (disconnect and reconnect never cancel accepted work). After its lease expires, the new attachment sends P3, queued behind P2, then disconnects. The principal re-authenticates with view only while the old attachment is already gone: the host writes the fence first, revokes the lease that attachment held (revoked to c2), and rejects P3 (control revoked) although its attachment is gone; P2, executing, completes. Regaining control later gives no authority over P3: the duplicate returns the rejected receipt and nothing runs, also after a broker restart." +} diff --git a/designs/protocol/fixtures/r8-revoked-control-refuses-the-send-and-its-queued-prompt.json b/designs/protocol/fixtures/r8-revoked-control-refuses-the-send-and-its-queued-prompt.json new file mode 100644 index 0000000..8997c05 --- /dev/null +++ b/designs/protocol/fixtures/r8-revoked-control-refuses-the-send-and-its-queued-prompt.json @@ -0,0 +1,241 @@ +{ + "name": "r8-revoked-control-refuses-the-send-and-its-queued-prompt", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000001", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000001", + "lease.lease_epoch": 1 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 1 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "then deploy" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000002", + "lease.lease_epoch": 1 + } + }, + { + "set_permissions": "c1", + "permissions": [ + "view" + ], + "attachment_gen": "99999999-9999-4999-8999-999999999999" + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "revoked", + "lease_epoch": 2, + "controller": null + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "99999999-9999-4999-8999-999999999999" + }, + "request_id": "44444444-4444-4444-8444-000000000003", + "kind": "prompt", + "payload": { + "text": "deploy anyway" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000003", + "code": "forbidden" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt_query", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "99999999-9999-4999-8999-999999999999" + }, + "request_ids": [ + "44444444-4444-4444-8444-000000000001", + "44444444-4444-4444-8444-000000000002", + "44444444-4444-4444-8444-000000000003" + ] + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipts", + "re": "11111111-1111-4111-8111-000000000004", + "receipts.#": 2, + "receipts.0.request_id": "44444444-4444-4444-8444-000000000001", + "receipts.0.state": "executing", + "receipts.1.request_id": "44444444-4444-4444-8444-000000000002", + "receipts.1.state": "rejected", + "receipts.1.detail": "control revoked" + } + }, + { + "turn_complete": "ses-aaaaaaaaaaaa", + "turn_id": "turn-1" + }, + { + "assert": { + "lease_holder": null, + "dispatch_attempts": 1, + "dispatches": 1 + } + } + ], + "note": "c1's device loses control (re-authenticated with view only: a new attachment, the old one fenced). The host revokes the lease that attachment held (revoked), rejects its accepted prompts not yet executing (detail control revoked), and lets executing work finish (§6). Its later Send is forbidden before any lease check: control authorizes, the lease only coordinates writers." +} diff --git a/designs/protocol/fixtures/r8-send-acquire-wire-rules.json b/designs/protocol/fixtures/r8-send-acquire-wire-rules.json new file mode 100644 index 0000000..312a53f --- /dev/null +++ b/designs/protocol/fixtures/r8-send-acquire-wire-rules.json @@ -0,0 +1,456 @@ +{ + "name": "r8-send-acquire-wire-rules", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native", + "holds_lease_epoch": 1 + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000001" + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "sessions", + "re": "11111111-1111-4111-8111-000000000001", + "sessions.#": 1 + }, + "capture": { + "c1ctl": "sessions.0.controller.controller_session" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": false + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000002", + "code": "invalid_request" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "takeover": true, + "expected_controller": { + "controller_session": "${c1ctl}", + "lease_epoch": 1 + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000003", + "code": "invalid_request" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true, + "takeover": true + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000004", + "code": "invalid_request" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000005", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true, + "expected_controller": { + "controller_session": "${c1ctl}", + "lease_epoch": 1 + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000005", + "code": "invalid_request" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000006", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true, + "takeover": false, + "expected_controller": { + "controller_session": "${c1ctl}", + "lease_epoch": 1 + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000006", + "code": "invalid_request" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000007", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "lease_epoch": 1 + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000007", + "code": "invalid_request" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000008", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "interrupt", + "payload": {}, + "acquire": true + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000008", + "code": "invalid_request" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000009", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "stop", + "payload": {}, + "acquire": true + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000009", + "code": "invalid_request" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000010", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000003", + "kind": "approval_rule_revoke", + "payload": { + "rule_id": "s-0123456789abcdef" + }, + "acquire": true + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000010", + "code": "invalid_request" + } + }, + { + "send": "c2", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "submit", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "requestId": "44444444-4444-4444-8444-000000000004", + "text": "run the tests", + "acquire": true, + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "lease_epoch": 1 + } + } + } + }, + { + "expect": "c2", + "type": "agentChat", + "match": { + "op": "error", + "requestId": "44444444-4444-4444-8444-000000000004", + "code": "invalid_request" + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "lease_holder": "c1", + "dispatch_attempts": 0, + "receipts_visible": { + "c1": 0, + "c2": 0 + } + } + } + ], + "note": "acquire and takeover are present only as true; takeover needs acquire and expected_controller (and expected_controller needs takeover); acquire excludes lease_epoch; only kind prompt (and a chat-v1 text submit) may acquire in r8: interrupt, stop and approval_rule_revoke with acquire are invalid_request. Each is refused before anything is recorded or the lease moves." +} diff --git a/designs/protocol/fixtures/r8-send-acquires-an-unowned-session.json b/designs/protocol/fixtures/r8-send-acquires-an-unowned-session.json new file mode 100644 index 0000000..fab85de --- /dev/null +++ b/designs/protocol/fixtures/r8-send-acquires-an-unowned-session.json @@ -0,0 +1,236 @@ +{ + "name": "r8-send-acquires-an-unowned-session", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000001" + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "sessions", + "re": "11111111-1111-4111-8111-000000000001", + "sessions.#": 1, + "sessions.0.controller": null + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000002", + "lease.lease_epoch": 1, + "lease.ms_left": 15000 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 1 + }, + "capture": { + "c1ctl": "controller.controller_session" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "session_changed", + "descriptor.controller.lease_epoch": 1, + "descriptor.controller.controller_session": "${c1ctl}" + } + }, + { + "assert": { + "lease_holder": "c1", + "dispatch_attempts": 1, + "dispatches": 1 + } + }, + { + "advance_ms": 5000 + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "renew", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease", + "re": "11111111-1111-4111-8111-000000000003", + "binding.lease_epoch": 1, + "ms_left": 15000 + } + }, + { + "turn_complete": "ses-aaaaaaaaaaaa", + "turn_id": "turn-1" + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "completed", + "detail": "done" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "submit", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "requestId": "44444444-4444-4444-8444-000000000002", + "text": "now fix the failing test", + "acquire": true, + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + } + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "accepted", + "requestId": "44444444-4444-4444-8444-000000000002", + "conversationId": "conv-1", + "lease.lease_epoch": 1, + "lease.ms_left": 15000 + } + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "assert": { + "lease_holder": "c1", + "dispatches": 2 + } + } + ], + "note": "Nobody holds control: an acquiring Send (session submit or chat-v1 submit with acquire: true, no lease_epoch) is one host operation. The prompt is durably accepted, then the lease is granted to the sender; the reply carries it (lease{lease_epoch, ms_left}) and every other viewer gets lease_changed{granted}. The sender renews as usual; a chat-v1 Send while it holds the lease uses it (no new grant, no lease_changed)." +} diff --git a/designs/protocol/fixtures/r8-send-refused-while-another-holds-then-takeover.json b/designs/protocol/fixtures/r8-send-refused-while-another-holds-then-takeover.json new file mode 100644 index 0000000..beca0fe --- /dev/null +++ b/designs/protocol/fixtures/r8-send-refused-while-another-holds-then-takeover.json @@ -0,0 +1,223 @@ +{ + "name": "r8-send-refused-while-another-holds-then-takeover", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native", + "holds_lease_epoch": 1 + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000001" + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "sessions", + "re": "11111111-1111-4111-8111-000000000001", + "sessions.#": 1, + "sessions.0.controller.lease_epoch": 1 + }, + "capture": { + "c1ctl": "sessions.0.controller.controller_session" + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000002", + "code": "lease_held", + "controller.controller_session": "${c1ctl}", + "controller.lease_epoch": 1 + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "assert": { + "lease_holder": "c1", + "dispatch_attempts": 0 + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true, + "takeover": true, + "expected_controller": { + "controller_session": "${c1ctl}", + "lease_epoch": 1 + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000003", + "lease.lease_epoch": 2, + "lease.ms_left": 15000 + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "session_changed", + "descriptor.controller.lease_epoch": 2 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "takeover", + "lease_epoch": 2 + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "renew", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000004", + "code": "stale_lease" + } + }, + { + "assert": { + "lease_holder": "c2", + "dispatches": 1 + } + } + ], + "note": "Another attachment holds the lease. An ordinary Send (acquire without takeover) is refused lease_held naming the holder (controller{controller_session, lease_epoch}, the descriptor's shape): nothing is recorded, nothing executes, no takeover. The confirmed retry keeps the same request_id and text and adds takeover: true with expected_controller exactly as the dialog showed; the host takes over and accepts in one operation, the old holder gets lease_changed{takeover} and its epoch is stale." +} diff --git a/designs/protocol/fixtures/r8-send-under-the-own-lease-checks-the-epoch.json b/designs/protocol/fixtures/r8-send-under-the-own-lease-checks-the-epoch.json new file mode 100644 index 0000000..7d3eb55 --- /dev/null +++ b/designs/protocol/fixtures/r8-send-under-the-own-lease-checks-the-epoch.json @@ -0,0 +1,261 @@ +{ + "name": "r8-send-under-the-own-lease-checks-the-epoch", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native", + "holds_lease_epoch": 1 + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000001", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000001" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 2 + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "run the tests" + } + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000002", + "code": "stale_lease" + } + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "lease_epoch": 1 + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000003", + "code": "invalid_request" + } + }, + { + "advance_ms": 10000 + }, + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000004", + "lease.lease_epoch": 1, + "lease.ms_left": 5000 + } + }, + { + "expect_none": "c2", + "for_ms": 0 + }, + { + "turn_complete": "ses-aaaaaaaaaaaa", + "turn_id": "turn-1" + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "completed", + "detail": "done" + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000002", + "state": "executing" + } + }, + { + "advance_ms": 5000 + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "expired", + "lease_epoch": 2, + "controller": null + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "expired", + "lease_epoch": 2, + "controller": null + } + }, + { + "assert": { + "lease_holder": null, + "dispatches": 2 + } + } + ], + "note": "The sender holds the lease. An ordinary submit carries lease_epoch and is checked as in r7 (a wrong epoch is stale_lease). An acquiring Send by the holder is admitted under its current lease: same epoch, no lease_changed, and the deadline is not extended (ms_left is what remains; a chat read or a Send never renews). acquire with a lease_epoch is invalid_request." +} diff --git a/designs/protocol/fixtures/r8-stale-or-foreign-next-token-is-stale-target.json b/designs/protocol/fixtures/r8-stale-or-foreign-next-token-is-stale-target.json new file mode 100644 index 0000000..cc0c9b9 --- /dev/null +++ b/designs/protocol/fixtures/r8-stale-or-foreign-next-token-is-stale-target.json @@ -0,0 +1,3055 @@ +{ + "name": "r8-stale-or-foreign-next-token-is-stale-target", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "chat_history": "ses-aaaaaaaaaaaa", + "messages": [ + { + "messageId": "m-0001", + "role": "user", + "turnId": "turn-1", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0002", + "role": "assistant", + "turnId": "turn-1", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0003", + "role": "user", + "turnId": "turn-2", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0004", + "role": "assistant", + "turnId": "turn-2", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0005", + "role": "user", + "turnId": "turn-3", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0006", + "role": "assistant", + "turnId": "turn-3", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0007", + "role": "user", + "turnId": "turn-4", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0008", + "role": "assistant", + "turnId": "turn-4", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0009", + "role": "user", + "turnId": "turn-5", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0010", + "role": "assistant", + "turnId": "turn-5", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0011", + "role": "user", + "turnId": "turn-6", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0012", + "role": "assistant", + "turnId": "turn-6", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0013", + "role": "user", + "turnId": "turn-7", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0014", + "role": "assistant", + "turnId": "turn-7", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0015", + "role": "user", + "turnId": "turn-8", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0016", + "role": "assistant", + "turnId": "turn-8", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0017", + "role": "user", + "turnId": "turn-9", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0018", + "role": "assistant", + "turnId": "turn-9", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0019", + "role": "user", + "turnId": "turn-10", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0020", + "role": "assistant", + "turnId": "turn-10", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0021", + "role": "user", + "turnId": "turn-11", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0022", + "role": "assistant", + "turnId": "turn-11", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0023", + "role": "user", + "turnId": "turn-12", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0024", + "role": "assistant", + "turnId": "turn-12", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0025", + "role": "user", + "turnId": "turn-13", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0026", + "role": "assistant", + "turnId": "turn-13", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0027", + "role": "user", + "turnId": "turn-14", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0028", + "role": "assistant", + "turnId": "turn-14", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0029", + "role": "user", + "turnId": "turn-15", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0030", + "role": "assistant", + "turnId": "turn-15", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0031", + "role": "user", + "turnId": "turn-16", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0032", + "role": "assistant", + "turnId": "turn-16", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0033", + "role": "user", + "turnId": "turn-17", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0034", + "role": "assistant", + "turnId": "turn-17", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0035", + "role": "user", + "turnId": "turn-18", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0036", + "role": "assistant", + "turnId": "turn-18", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0037", + "role": "user", + "turnId": "turn-19", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0038", + "role": "assistant", + "turnId": "turn-19", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0039", + "role": "user", + "turnId": "turn-20", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0040", + "role": "assistant", + "turnId": "turn-20", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0041", + "role": "user", + "turnId": "turn-21", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0042", + "role": "assistant", + "turnId": "turn-21", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0043", + "role": "user", + "turnId": "turn-22", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0044", + "role": "assistant", + "turnId": "turn-22", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0045", + "role": "user", + "turnId": "turn-23", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0046", + "role": "assistant", + "turnId": "turn-23", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0047", + "role": "user", + "turnId": "turn-24", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0048", + "role": "assistant", + "turnId": "turn-24", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0049", + "role": "user", + "turnId": "turn-25", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0050", + "role": "assistant", + "turnId": "turn-25", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0051", + "role": "user", + "turnId": "turn-26", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0052", + "role": "assistant", + "turnId": "turn-26", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0053", + "role": "user", + "turnId": "turn-27", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0054", + "role": "assistant", + "turnId": "turn-27", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0055", + "role": "user", + "turnId": "turn-28", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0056", + "role": "assistant", + "turnId": "turn-28", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0057", + "role": "user", + "turnId": "turn-29", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0058", + "role": "assistant", + "turnId": "turn-29", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0059", + "role": "user", + "turnId": "turn-30", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0060", + "role": "assistant", + "turnId": "turn-30", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0061", + "role": "user", + "turnId": "turn-31", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0062", + "role": "assistant", + "turnId": "turn-31", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0063", + "role": "user", + "turnId": "turn-32", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0064", + "role": "assistant", + "turnId": "turn-32", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0065", + "role": "user", + "turnId": "turn-33", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0066", + "role": "assistant", + "turnId": "turn-33", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0067", + "role": "user", + "turnId": "turn-34", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0068", + "role": "assistant", + "turnId": "turn-34", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0069", + "role": "user", + "turnId": "turn-35", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0070", + "role": "assistant", + "turnId": "turn-35", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0071", + "role": "user", + "turnId": "turn-36", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0072", + "role": "assistant", + "turnId": "turn-36", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0073", + "role": "user", + "turnId": "turn-37", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0074", + "role": "assistant", + "turnId": "turn-37", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0075", + "role": "user", + "turnId": "turn-38", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0076", + "role": "assistant", + "turnId": "turn-38", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0077", + "role": "user", + "turnId": "turn-39", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0078", + "role": "assistant", + "turnId": "turn-39", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0079", + "role": "user", + "turnId": "turn-40", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0080", + "role": "assistant", + "turnId": "turn-40", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0081", + "role": "user", + "turnId": "turn-41", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0082", + "role": "assistant", + "turnId": "turn-41", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0083", + "role": "user", + "turnId": "turn-42", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0084", + "role": "assistant", + "turnId": "turn-42", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0085", + "role": "user", + "turnId": "turn-43", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0086", + "role": "assistant", + "turnId": "turn-43", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0087", + "role": "user", + "turnId": "turn-44", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0088", + "role": "assistant", + "turnId": "turn-44", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0089", + "role": "user", + "turnId": "turn-45", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0090", + "role": "assistant", + "turnId": "turn-45", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0091", + "role": "user", + "turnId": "turn-46", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0092", + "role": "assistant", + "turnId": "turn-46", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0093", + "role": "user", + "turnId": "turn-47", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0094", + "role": "assistant", + "turnId": "turn-47", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0095", + "role": "user", + "turnId": "turn-48", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0096", + "role": "assistant", + "turnId": "turn-48", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0097", + "role": "user", + "turnId": "turn-49", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0098", + "role": "assistant", + "turnId": "turn-49", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0099", + "role": "user", + "turnId": "turn-50", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0100", + "role": "assistant", + "turnId": "turn-50", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0101", + "role": "user", + "turnId": "turn-51", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0102", + "role": "assistant", + "turnId": "turn-51", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0103", + "role": "user", + "turnId": "turn-52", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0104", + "role": "assistant", + "turnId": "turn-52", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0105", + "role": "user", + "turnId": "turn-53", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0106", + "role": "assistant", + "turnId": "turn-53", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0107", + "role": "user", + "turnId": "turn-54", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0108", + "role": "assistant", + "turnId": "turn-54", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0109", + "role": "user", + "turnId": "turn-55", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0110", + "role": "assistant", + "turnId": "turn-55", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0111", + "role": "user", + "turnId": "turn-56", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0112", + "role": "assistant", + "turnId": "turn-56", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0113", + "role": "user", + "turnId": "turn-57", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0114", + "role": "assistant", + "turnId": "turn-57", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0115", + "role": "user", + "turnId": "turn-58", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0116", + "role": "assistant", + "turnId": "turn-58", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0117", + "role": "user", + "turnId": "turn-59", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0118", + "role": "assistant", + "turnId": "turn-59", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + }, + { + "messageId": "m-0119", + "role": "user", + "turnId": "turn-60", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ], + "origin": "local" + }, + { + "messageId": "m-0120", + "role": "assistant", + "turnId": "turn-60", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + ] + } + ] + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "open", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + } + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "opened", + "sequence": 0 + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 1, + "messageId": "m-0071", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 2, + "messageId": "m-0072", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 3, + "messageId": "m-0073", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 4, + "messageId": "m-0074", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 5, + "messageId": "m-0075", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 6, + "messageId": "m-0076", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 7, + "messageId": "m-0077", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 8, + "messageId": "m-0078", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 9, + "messageId": "m-0079", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 10, + "messageId": "m-0080", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 11, + "messageId": "m-0081", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 12, + "messageId": "m-0082", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 13, + "messageId": "m-0083", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 14, + "messageId": "m-0084", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 15, + "messageId": "m-0085", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 16, + "messageId": "m-0086", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 17, + "messageId": "m-0087", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 18, + "messageId": "m-0088", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 19, + "messageId": "m-0089", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 20, + "messageId": "m-0090", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 21, + "messageId": "m-0091", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 22, + "messageId": "m-0092", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 23, + "messageId": "m-0093", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 24, + "messageId": "m-0094", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 25, + "messageId": "m-0095", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 26, + "messageId": "m-0096", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 27, + "messageId": "m-0097", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 28, + "messageId": "m-0098", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 29, + "messageId": "m-0099", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 30, + "messageId": "m-0100", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 31, + "messageId": "m-0101", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 32, + "messageId": "m-0102", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 33, + "messageId": "m-0103", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 34, + "messageId": "m-0104", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 35, + "messageId": "m-0105", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 36, + "messageId": "m-0106", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 37, + "messageId": "m-0107", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 38, + "messageId": "m-0108", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 39, + "messageId": "m-0109", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 40, + "messageId": "m-0110", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 41, + "messageId": "m-0111", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 42, + "messageId": "m-0112", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 43, + "messageId": "m-0113", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 44, + "messageId": "m-0114", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 45, + "messageId": "m-0115", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 46, + "messageId": "m-0116", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 47, + "messageId": "m-0117", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 48, + "messageId": "m-0118", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 49, + "messageId": "m-0119", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 50, + "messageId": "m-0120", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "history_end", + "sequence": 51, + "page.count": 50, + "page.end": false + }, + "capture": { + "tok": "page.next" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "open", + "conversationId": "conv-2", + "agentId": "ses-aaaaaaaaaaaa", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + } + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "opened", + "sequence": 0, + "conversationId": "conv-2" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 1, + "conversationId": "conv-2", + "messageId": "m-0071", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 2, + "conversationId": "conv-2", + "messageId": "m-0072", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 3, + "conversationId": "conv-2", + "messageId": "m-0073", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 4, + "conversationId": "conv-2", + "messageId": "m-0074", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 5, + "conversationId": "conv-2", + "messageId": "m-0075", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 6, + "conversationId": "conv-2", + "messageId": "m-0076", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 7, + "conversationId": "conv-2", + "messageId": "m-0077", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 8, + "conversationId": "conv-2", + "messageId": "m-0078", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 9, + "conversationId": "conv-2", + "messageId": "m-0079", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 10, + "conversationId": "conv-2", + "messageId": "m-0080", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 11, + "conversationId": "conv-2", + "messageId": "m-0081", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 12, + "conversationId": "conv-2", + "messageId": "m-0082", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 13, + "conversationId": "conv-2", + "messageId": "m-0083", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 14, + "conversationId": "conv-2", + "messageId": "m-0084", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 15, + "conversationId": "conv-2", + "messageId": "m-0085", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 16, + "conversationId": "conv-2", + "messageId": "m-0086", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 17, + "conversationId": "conv-2", + "messageId": "m-0087", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 18, + "conversationId": "conv-2", + "messageId": "m-0088", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 19, + "conversationId": "conv-2", + "messageId": "m-0089", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 20, + "conversationId": "conv-2", + "messageId": "m-0090", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 21, + "conversationId": "conv-2", + "messageId": "m-0091", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 22, + "conversationId": "conv-2", + "messageId": "m-0092", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 23, + "conversationId": "conv-2", + "messageId": "m-0093", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 24, + "conversationId": "conv-2", + "messageId": "m-0094", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 25, + "conversationId": "conv-2", + "messageId": "m-0095", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 26, + "conversationId": "conv-2", + "messageId": "m-0096", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 27, + "conversationId": "conv-2", + "messageId": "m-0097", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 28, + "conversationId": "conv-2", + "messageId": "m-0098", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 29, + "conversationId": "conv-2", + "messageId": "m-0099", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 30, + "conversationId": "conv-2", + "messageId": "m-0100", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 31, + "conversationId": "conv-2", + "messageId": "m-0101", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 32, + "conversationId": "conv-2", + "messageId": "m-0102", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 33, + "conversationId": "conv-2", + "messageId": "m-0103", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 34, + "conversationId": "conv-2", + "messageId": "m-0104", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 35, + "conversationId": "conv-2", + "messageId": "m-0105", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 36, + "conversationId": "conv-2", + "messageId": "m-0106", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 37, + "conversationId": "conv-2", + "messageId": "m-0107", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 38, + "conversationId": "conv-2", + "messageId": "m-0108", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 39, + "conversationId": "conv-2", + "messageId": "m-0109", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 40, + "conversationId": "conv-2", + "messageId": "m-0110", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 41, + "conversationId": "conv-2", + "messageId": "m-0111", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 42, + "conversationId": "conv-2", + "messageId": "m-0112", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 43, + "conversationId": "conv-2", + "messageId": "m-0113", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 44, + "conversationId": "conv-2", + "messageId": "m-0114", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 45, + "conversationId": "conv-2", + "messageId": "m-0115", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 46, + "conversationId": "conv-2", + "messageId": "m-0116", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 47, + "conversationId": "conv-2", + "messageId": "m-0117", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 48, + "conversationId": "conv-2", + "messageId": "m-0118", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 49, + "conversationId": "conv-2", + "messageId": "m-0119", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 50, + "conversationId": "conv-2", + "messageId": "m-0120", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "history_end", + "sequence": 51, + "conversationId": "conv-2", + "page.end": false + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-2", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p1", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "next": "${tok}" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "error", + "sequence": 52, + "conversationId": "conv-2", + "code": "stale_target", + "page_id": "p1" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p2", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "next": "${tok}x" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "error", + "sequence": 52, + "code": "stale_target", + "page_id": "p2" + } + }, + { + "chat_evict": "ses-aaaaaaaaaaaa", + "count": 75 + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p3", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "next": "${tok}" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "error", + "sequence": 53, + "code": "stale_target", + "page_id": "p3" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p4", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "turn_id": "turn-40" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 54, + "messageId": "m-0079", + "history": true, + "page_id": "p4" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 55, + "messageId": "m-0080", + "history": true, + "page_id": "p4" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "history_end", + "sequence": 56, + "page_id": "p4", + "page.end": true, + "page.selector": "turn_id", + "page.coverage": "complete" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p5", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "turn_id": "turn-1" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "history_end", + "sequence": 57, + "page_id": "p5", + "page.count": 0, + "page.end": true, + "page.coverage": "unavailable" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p6", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "turn_id": "turn-38" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 58, + "messageId": "m-0076", + "history": true, + "page_id": "p6" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "history_end", + "sequence": 59, + "page_id": "p6", + "page.count": 1, + "page.end": true, + "page.coverage": "partial" + } + }, + { + "assert": { + "dispatch_attempts": 0, + "pty_writes": 0, + "lease_holder": null, + "chat_closes": 0 + } + } + ], + "note": "next is opaque and bound to the attachment, conversation, target (session and process generation), selector and the history snapshot it continues. Used in another conversation, altered, or after the message it continues from was evicted (or after a history reload, or another runtime), it is chat error stale_target, sequenced, and no page is sent; the client starts a new selection. A lookup whose turn is no longer retained is not an error: an empty page with coverage unavailable and end: true. A turn whose oldest messages were evicted (turn-38 lost m-0075) is coverage partial." +} \ No newline at end of file diff --git a/designs/protocol/fixtures/r8-stale-takeover-confirmation-is-refused.json b/designs/protocol/fixtures/r8-stale-takeover-confirmation-is-refused.json new file mode 100644 index 0000000..f8d4e94 --- /dev/null +++ b/designs/protocol/fixtures/r8-stale-takeover-confirmation-is-refused.json @@ -0,0 +1,384 @@ +{ + "name": "r8-stale-takeover-confirmation-is-refused", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native", + "holds_lease_epoch": 1 + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + }, + { + "name": "c3", + "attachment_gen": "77777777-7777-4777-8777-777777777777", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000001" + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "sessions", + "re": "11111111-1111-4111-8111-000000000001", + "sessions.#": 1, + "sessions.0.controller.lease_epoch": 1 + }, + "capture": { + "c1ctl": "sessions.0.controller.controller_session" + } + }, + { + "send": "c3", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "list", + "id": "11111111-1111-4111-8111-000000000002" + } + } + }, + { + "expect": "c3", + "type": "session", + "match": { + "op": "sessions", + "re": "11111111-1111-4111-8111-000000000002", + "sessions.#": 1, + "sessions.0.controller.lease_epoch": 1 + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000003", + "code": "lease_held", + "controller.controller_session": "${c1ctl}", + "controller.lease_epoch": 1 + } + }, + { + "send": "c3", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "acquire", + "id": "11111111-1111-4111-8111-000000000004", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "77777777-7777-4777-8777-777777777777" + }, + "takeover": true + } + } + }, + { + "expect": "c3", + "type": "session", + "match": { + "op": "lease", + "re": "11111111-1111-4111-8111-000000000004", + "binding.lease_epoch": 2 + } + }, + { + "expect": "c3", + "type": "session", + "match": { + "op": "session_changed", + "descriptor.controller.lease_epoch": 2 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "takeover", + "lease_epoch": 2 + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "takeover", + "lease_epoch": 2 + }, + "capture": { + "c3ctl": "controller.controller_session" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "session_changed", + "descriptor.controller.lease_epoch": 2 + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000005", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true, + "takeover": true, + "expected_controller": { + "controller_session": "${c1ctl}", + "lease_epoch": 1 + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000005", + "code": "lease_held", + "controller.controller_session": "${c3ctl}", + "controller.lease_epoch": 2 + } + }, + { + "expect_none": "c1", + "for_ms": 0 + }, + { + "expect_none": "c3", + "for_ms": 0 + }, + { + "assert": { + "lease_holder": "c3", + "dispatch_attempts": 0 + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000006", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true, + "takeover": true, + "expected_controller": { + "controller_session": "${c3ctl}", + "lease_epoch": 2 + } + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000006", + "lease.lease_epoch": 3 + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "session_changed", + "descriptor.controller.lease_epoch": 3 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "takeover", + "lease_epoch": 3 + } + }, + { + "expect": "c3", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "takeover", + "lease_epoch": 3 + } + }, + { + "expect": "c3", + "type": "session", + "match": { + "op": "session_changed", + "descriptor.controller.lease_epoch": 3 + } + }, + { + "send": "c3", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "renew", + "id": "11111111-1111-4111-8111-000000000007", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "77777777-7777-4777-8777-777777777777", + "lease_epoch": 2 + } + } + } + }, + { + "expect": "c3", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000007", + "code": "stale_lease" + } + }, + { + "assert": { + "lease_holder": "c2", + "dispatches": 1 + } + } + ], + "note": "A takeover confirmation authorizes taking over exactly the lease it showed. c2's dialog named c1's lease (epoch 1); meanwhile c3 took over (epoch 2). c2's takeover Send names a lease that no longer exists: refused lease_held with the current holder, nothing recorded or executed, c3 keeps control. Re-confirmed against c3's lease it takes over and accepts atomically." +} diff --git a/designs/protocol/fixtures/r8-truncated-history-reopens-once-then-stays-live.json b/designs/protocol/fixtures/r8-truncated-history-reopens-once-then-stays-live.json new file mode 100644 index 0000000..1a3b139 --- /dev/null +++ b/designs/protocol/fixtures/r8-truncated-history-reopens-once-then-stays-live.json @@ -0,0 +1,575 @@ +{ + "name": "r8-truncated-history-reopens-once-then-stays-live", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "chat_history": "ses-aaaaaaaaaaaa", + "messages": [ + { + "messageId": "m-0001", + "role": "user", + "turnId": "turn-1", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 1" + } + ], + "origin": "local" + }, + { + "messageId": "m-0002", + "role": "assistant", + "turnId": "turn-1", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 2" + } + ] + }, + { + "messageId": "m-0003", + "role": "user", + "turnId": "turn-2", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 3" + } + ], + "origin": "local" + }, + { + "messageId": "m-0004", + "role": "assistant", + "turnId": "turn-2", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 4" + } + ] + }, + { + "messageId": "m-0005", + "role": "user", + "turnId": "turn-3", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 5" + } + ], + "origin": "local" + }, + { + "messageId": "m-0006", + "role": "assistant", + "turnId": "turn-3", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 6" + } + ] + }, + { + "messageId": "m-0007", + "role": "user", + "turnId": "turn-4", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 7" + } + ], + "origin": "local" + }, + { + "messageId": "m-0008", + "role": "assistant", + "turnId": "turn-4", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 8" + } + ] + }, + { + "messageId": "m-0009", + "role": "user", + "turnId": "turn-5", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 9" + } + ], + "origin": "local" + }, + { + "messageId": "m-0010", + "role": "assistant", + "turnId": "turn-5", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 10" + } + ] + } + ] + }, + { + "chat_evict": "ses-aaaaaaaaaaaa", + "count": 4 + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "open", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + } + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "opened", + "sequence": 0 + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 1, + "messageId": "m-0005", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 2, + "messageId": "m-0006", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 3, + "messageId": "m-0007", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 4, + "messageId": "m-0008", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 5, + "messageId": "m-0009", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 6, + "messageId": "m-0010", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "history_end", + "sequence": 7, + "page.end": true, + "page.count": 6, + "page.truncated": true, + "page.oldest_retained": "m-0005", + "page.coverage": "partial", + "page.holes.#": 0 + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "close", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa" + } + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "open", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + } + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "opened", + "sequence": 0 + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 1, + "messageId": "m-0005", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 2, + "messageId": "m-0006", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 3, + "messageId": "m-0007", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 4, + "messageId": "m-0008", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 5, + "messageId": "m-0009", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 6, + "messageId": "m-0010", + "history": true + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "history_end", + "sequence": 7, + "page.end": true, + "page.count": 6, + "page.truncated": true, + "page.oldest_retained": "m-0005", + "page.coverage": "partial", + "page.holes.#": 0 + } + }, + { + "chat_message": "ses-aaaaaaaaaaaa", + "message": { + "messageId": "m-0011", + "role": "user", + "turnId": "turn-6", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 11" + } + ], + "origin": "local" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 8, + "messageId": "m-0011" + } + }, + { + "chat_message": "ses-aaaaaaaaaaaa", + "message": { + "messageId": "m-0012", + "role": "assistant", + "turnId": "turn-6", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 12" + } + ] + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 9, + "messageId": "m-0012" + } + }, + { + "chat_evict": "ses-aaaaaaaaaaaa", + "count": 2 + }, + { + "chat_message": "ses-aaaaaaaaaaaa", + "message": { + "messageId": "m-0013", + "role": "user", + "turnId": "turn-7", + "createdAt": 1760600000000, + "parts": [ + { + "id": "p1", + "kind": "text", + "markdown": "message 13" + } + ], + "origin": "local" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 10, + "messageId": "m-0013" + } + }, + { + "send": "c1", + "json": { + "type": "agentChat", + "data": { + "v": 1, + "op": "history", + "conversationId": "conv-1", + "agentId": "ses-aaaaaaaaaaaa", + "generation": "22222222-2222-4222-8222-222222222222", + "page_id": "p1", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "before": "m-0011" + } + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 11, + "messageId": "m-0007", + "history": true, + "page_id": "p1" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 12, + "messageId": "m-0008", + "history": true, + "page_id": "p1" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 13, + "messageId": "m-0009", + "history": true, + "page_id": "p1" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "message", + "sequence": 14, + "messageId": "m-0010", + "history": true, + "page_id": "p1" + } + }, + { + "expect": "c1", + "type": "agentChat", + "match": { + "op": "history_end", + "sequence": 15, + "page_id": "p1", + "page.end": true, + "page.truncated": true, + "page.oldest_retained": "m-0007", + "page.coverage": "partial" + } + }, + { + "expect_none": "c1", + "for_ms": 1000 + }, + { + "assert": { + "dispatch_attempts": 0, + "pty_writes": 0, + "lease_holder": null, + "chat_opens": 2, + "chat_closes": 0, + "dispatches": 0 + } + } + ], + "note": "Retention evicted m-0001..m-0004. Every history_end says truncated: true with oldest_retained, coverage partial: persistent information to display, never a reset. The client reopens once (close, open), gets the same truncated page, and then consumes live messages at consecutive sequences; further eviction only moves oldest_retained in later pages. The host sends no close, error or new opened, and nothing is dispatched (no prompt replay)." +} \ No newline at end of file diff --git a/designs/protocol/fixtures/r8-two-simultaneous-sends-acquire-once.json b/designs/protocol/fixtures/r8-two-simultaneous-sends-acquire-once.json new file mode 100644 index 0000000..0497d7e --- /dev/null +++ b/designs/protocol/fixtures/r8-two-simultaneous-sends-acquire-once.json @@ -0,0 +1,180 @@ +{ + "name": "r8-two-simultaneous-sends-acquire-once", + "requires": "r8", + "setup": { + "host": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "runtime_gen": "55555555-5555-4555-8555-555555555555", + "runtime_epoch": 7 + }, + "sessions": [ + { + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222", + "chat": true + } + ], + "clients": [ + { + "name": "c1", + "attachment_gen": "33333333-3333-4333-8333-333333333333", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + }, + { + "name": "c2", + "attachment_gen": "66666666-6666-4666-8666-666666666666", + "permissions": [ + "view", + "control" + ], + "client_kind": "native" + } + ] + }, + "steps": [ + { + "send": "c1", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000001", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "33333333-3333-4333-8333-333333333333" + }, + "request_id": "44444444-4444-4444-8444-000000000001", + "kind": "prompt", + "payload": { + "text": "run the tests" + }, + "acquire": true + } + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "submit", + "id": "11111111-1111-4111-8111-000000000002", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_id": "44444444-4444-4444-8444-000000000002", + "kind": "prompt", + "payload": { + "text": "deploy to staging" + }, + "acquire": true + } + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "accepted", + "re": "11111111-1111-4111-8111-000000000001", + "lease.lease_epoch": 1 + } + }, + { + "expect": "c1", + "type": "session", + "match": { + "op": "receipt", + "request_id": "44444444-4444-4444-8444-000000000001", + "state": "executing" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "lease_changed", + "reason": "granted", + "lease_epoch": 1 + }, + "capture": { + "c1ctl": "controller.controller_session" + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "error", + "re": "11111111-1111-4111-8111-000000000002", + "code": "lease_held", + "controller.controller_session": "${c1ctl}", + "controller.lease_epoch": 1 + } + }, + { + "send": "c2", + "json": { + "type": "session", + "data": { + "v": 1, + "op": "receipt_query", + "id": "11111111-1111-4111-8111-000000000003", + "binding": { + "target": { + "account_id": "account-a", + "atem_device_id": "host-a", + "host_trust_gen": 1, + "session_id": "ses-aaaaaaaaaaaa", + "process_gen": "22222222-2222-4222-8222-222222222222" + }, + "attachment_gen": "66666666-6666-4666-8666-666666666666" + }, + "request_ids": [ + "44444444-4444-4444-8444-000000000002" + ] + } + } + }, + { + "expect": "c2", + "type": "session", + "match": { + "op": "receipts", + "re": "11111111-1111-4111-8111-000000000003", + "receipts.#": 0 + } + }, + { + "assert": { + "lease_holder": "c1", + "dispatch_attempts": 1, + "dispatches": 1 + } + } + ], + "note": "Two ordinary Sends from different attachments reach an unowned session at the same instant. Admission is one step of the broker's single-threaded pass: the first admitted acquires and is accepted; the second sees the new holder and is refused lease_held with nothing recorded or executed." +} diff --git a/designs/protocol/fixtures/schema.md b/designs/protocol/fixtures/schema.md index ef74631..c42c0f2 100644 --- a/designs/protocol/fixtures/schema.md +++ b/designs/protocol/fixtures/schema.md @@ -43,7 +43,7 @@ setup messages is visible to the steps. | `{"close_session": session_id}` | the runtime closes the session (it no longer accepts work; the process may still run) | | `{"turn_complete": session_id, "turn_id": t}` | the provider reports that the session's executing prompt's turn `t` ended | | `{"hold_pty_writes": true}` / `{"flush_pty_writes": true}` | remote terminal writes queue at the PTY writer / the writer writes them now, each re-checked at the write boundary at the current clock (dropped ones count as `pty_writes_dropped`) | -| `{"open_approval": session_id, "tool": t, "summary": s, "always_rules"?: [{rule_id, label, scope}]}` | the agent asks a permission prompt the runtime offers remotely (r7 §6a); the host opens an approval for the session's current process, or refuses silently (no live remote lease: the agent's own prompt asks). Capture its id from the `approval_request` | +| `{"open_approval": session_id, "tool": t, "summary": s, "always_rules"?: [{rule_id, label, scope}]}` | the agent asks a permission prompt the runtime offers remotely (r7 §6a); the host opens an approval for the session's current process whoever holds the lease, or nobody (r7 §6a; at most 16 pending), and sends `approval_request` to every attachment with `control` subscribed by `list` (view-only ones get nothing). Capture its id from the `approval_request` | | `{"close_approval": approval_id, "reason": r}` | the runtime saw the prompt end without the remote answer (`answered_terminal`, `interrupted`, `timed_out`, `process_ended`) | | `{"approval_applied": approval_id, "ok": bool}` | the agent's report on a handed-over decision: applied (`completed`) or not known to be (`outcome_unknown`) | | `{"approval_rules": session_id, "rules": [{rule_id, label, scope}]}` | the runtime reports the session's active Atem session rules (r7.2); a revoke the host dispatches is answered at once by the harness: the rule is dropped (the receipt completes) and the rules left are reported | @@ -67,3 +67,120 @@ from the agents' permission hooks. `approval-rule-revoke-*` needs session rules, which only a Codex session's `allow_always` creates: the real-runtime runner (whose agent is Claude) lists it as not run. + +## r8 draft fixtures (not run until r8 ships) + +`r8-*.json` scenarios carry `"requires": "r8"` and exercise the **r8 +draft** in `../atem-session-v1.md` (appendix "r8 draft"). Both runners +(`src/session_protocol/fixtures.rs`, `tests/runtime_fixtures_binary.rs`) +parse them and report them as NOT RUN (`requires r8`); nothing below is +implemented yet. When r8 ships, `requires` is removed and the runners gain +these actions. + +- In an r8 scenario the harness's setup `hello` carries `"revision": 8` and + the `welcome` must echo `revision: 8` and carry `features`. +- `clients[].welcome: false`: the harness sends no `hello` for that client + (the steps do). +- Each client `name` is one authenticated principal (§H): `reconnect`, + `resume`, `set_permissions` and a second `hello` keep it; its + `attachment_gen`s change. (Today's host has one principal, the paired + Astation; the harness gives each client its own so the per-principal + rules can be checked.) After `restart_runtime`, `crash_runtime` or + `crash_after_accepted` the host's start is the current clock (§H's 60 s + confirmation window). +- In an r8 scenario `restart_runtime` is an orderly stop and start: the + host writes its clean-close record (§H) when its journal is healthy, + and none when the journal is halted. `crash_runtime` and + `crash_after_accepted` never write one. The clean-close write is not a + journal append counted by `fail_next_fsync`/`fail_fsync_of`. + +| step (r8 draft) | meaning | +|---|---| +| `{"chat_history": session_id, "messages": [message]}` | the session's retained chat history before any step reads it (complete chat-v1 message bodies, oldest first) | +| `{"chat_message": session_id, "message": message}` | the agent produced a message, new or a whole replacement of one with the same `messageId` | +| `{"chat_evict": session_id, "count": n}` | retention evicted the oldest `n` messages (`oldest_retained` moves, `truncated` becomes true) | +| `{"chat_turn": session_id, "turn_id": t, "request_id"?: r, "origin": o, "assistant_messages": n, "text_bytes": b, "escape"?: "control"}` | one turn arrives live: a user message `-u` (origin `o`, `requestId` `r` when given) and `n` assistant messages `-a-0001`…, each one text part of `b` bytes; `escape: "control"` fills the text with U+0001 (6 bytes once JSON-escaped), else `x` | +| `{"worker_gap": session_id, "dropped": [id], "recoverable": [id], "delivered_after": [message]}` | the worker's backlog dropped `dropped`, then delivered `delivered_after` with `gap > 0`; the host's reload of the worker's history finds only `recoverable` (the rest is gone from the worker too) | +| `{"open_approval": …, "turn_id": t}` | as before; `turn_id` is the card's `turnId` | +| `{"expect_live": client, "message_ids": [id], "sequence_from": n}` | takes that many chat `message`s (no `history`), with those ids in order and consecutive sequences from `n` | +| `{"recover": client, "request": msg, "message_ids": [id], "min_pages": n, "max_count": c, "max_content_bytes": b, "max_wire_bytes": w, "end": {path: value}}` | sends `request` (a chat `history` or `open`), takes its page (`opened` first for an `open`; `message{history:true}`s then `history_end`), and while `page.end` is false sends `history{next}` with the same binding and conversation (page id `-2`, …) and takes the next page. Every page: consecutive sequences, ≤ `c` messages, ≤ `b` content bytes, ≤ `w` serialized bytes (the sum of its envelopes), and no `end: true` before the last. Newest-first selections are reassembled oldest first. The messages taken equal `message_ids` exactly (no gap, no duplicate); the last `history_end` matches `end`; at least `n` pages | +| `{"client_try": client, "json": msg, "refused": text}` | client side: the client is asked to send `msg` and must refuse locally with that reason (nothing is sent) | +| `{"client_open_chat": client, "conversation": id, "session_id": s}` | client side: the client opens a chat (its `open` is consumed by the fixture) | +| `{"drop": {"to": client, "op": op}}` | the next matching host→client message is lost in transit: never delivered and not expected (repeat the step to lose several) | +| `{"set_permissions": client, "permissions": [p], "attachment_gen": uuid}` | the client's principal re-authenticates with these permissions on a new attachment, whether or not its old one is still live (after `disconnect` too). Losing `control` follows §H: the fence is written, live attachments are fenced, a lease held by any of its attachments is revoked (`lease_changed{revoked}` to the other viewers) and its `accepted` work not yet executing, from any of its attachments, is rejected (`control revoked`); then the harness welcomes the new attachment. The permissions stay with the principal: later `reconnect`s use them | +| `{"fail_next_fsync": true, "undo_fails"?: true}` | r8 (§I; for a revocation's writes §H, "A failed revocation write"): the next journal append fails and the journal halts until `restart_runtime` (every session request but `hello` is `busy`); its rollback succeeds (nothing of it is on disk) unless `undo_fails`, which leaves the record on disk (the worst case). The refusal list write succeeds | +| `{"fail_fsync_of": n}` | as `fail_next_fsync` for the `n`-th next journal append (1 = the next one); the earlier ones succeed | +| `{"crash_after_accepted": {"runtime_gen": uuid, "runtime_epoch": n}}` | the next `accepted` record becomes durable, then the runtime crashes before acting on it (no lease change, no reply, no other message) and restarts with these values, as `crash_runtime` | +| `{"crash_runtime": {"runtime_gen": uuid, "runtime_epoch": n}}` | the runtime is killed (no clean close) and restarts with these values; workers survive and are adopted; every link drops and clients `reconnect` | +| `{"replace_process": session_id, "process_gen": uuid}` | the agent process is replaced (restart/resume) with a new `process_gen` (`SessionHost::replace_process`) | + +Send and approval scenarios (`r8-send-*`, `r8-approval-*`, and the other +§F fixtures listed below): the setup `welcome` must advertise +`chat.send_acquire` and `approval.lease_free`. A `local_input` whose bytes +end without CR/LF leaves unsent terminal input, so every prompt dispatched +afterwards is refused by the adapter's draft check (`rejected`, detail `the +terminal has unsent input; finish or clear it there`). A `receipts` reply +lists receipts in the order of `request_ids` (unrecorded ids are omitted). + +New `assert` keys (r8 draft): `journal_halted` (the journal refuses +appends until a restart, §I), `attachments` (live attachments on the host), +`chat_opens` / `chat_closes` (`opened` / `close` the host sent, all +conversations), `chat_conversations` (open conversations). + +New `assert_client` keys (r8 draft): `admitted` (the welcome passed the +revision check), `revision`, `features_enabled` (advertised features the +client knows and enabled), `lookup_enabled`, `coverage_enabled`, +`chat_enabled`, `chat_loaded` (a `history_end` arrived), `chat_truncated`, +`chat_messages` (ids in timeline order), `chat_reopens` (opens the client +sent after the first), `sent` (messages sent since the last check). + +The r8 draft scenarios: + +| fixture | side | checks | +|---|---|---| +| `r8-hello-without-revision-8-is-refused-before-any-attachment` | host | r7/missing/other revision refused `unsupported_version` before any attachment or session work | +| `r8-client-refuses-a-host-without-revision-8` | client | r8 client against an older host: no attachment, no feature | +| `r8-client-enables-only-advertised-features` | client | a missing required feature is never used | +| `r8-open-with-before-is-refused` | host | `open.before` refused explicitly, never read as a newest-page open | +| `r8-history-page-and-live-events-share-one-ordered-stream` | host | page snapshot and enqueue are one step in the sequenced stream | +| `r8-long-turn-is-recovered-in-bounded-pages` | host | > 50 messages / > 256 KiB / escaping overhead, bounded pages, `next` | +| `r8-truncated-history-reopens-once-then-stays-live` | host | truncation is persistent information, no reset | +| `r8-client-shows-truncation-and-resyncs-only-on-a-sequence-gap` | client | no reopen loop; resync on a real sequence gap | +| `r8-backlog-hole-is-reported-not-reopened` | host | worker backlog gap: reload, then a persistent hole (changes #75's error + close) | +| `r8-receipt-result-bounds-reply-message-ids` | host | `reply_message_ids` ≤ 32 + `more`, rest by `history{request_id}` | +| `r8-history-selectors-are-exclusive-and-validated` | host | exactly one selector; formats and lengths | +| `r8-stale-or-foreign-next-token-is-stale-target` | host | `next` bound to its context; evicted turn `unavailable`, partly evicted `partial` | +| `r8-send-acquires-an-unowned-session` | host | §F: unowned → acquire + accept in one operation; `lease` on the reply, `granted` to viewers; chat-v1 Send under the own lease | +| `r8-send-under-the-own-lease-checks-the-epoch` | host | ordinary submit checks `lease_epoch`; acquiring Send by the holder: same epoch, deadline not extended | +| `r8-send-refused-while-another-holds-then-takeover` | host | `lease_held{controller}`, nothing runs; confirmed `takeover` with the same `request_id` takes over atomically | +| `r8-two-simultaneous-sends-acquire-once` | host | first admitted acquires; second `lease_held`, nothing recorded | +| `r8-ownership-is-checked-at-admission-not-as-displayed` | host | ownership changed after display: refused; holder gone before the takeover: plain grant | +| `r8-local-input-wins-over-a-send` | host | local input revokes the Send's lease; queued prompt refused by the draft check | +| `r8-stale-takeover-confirmation-is-refused` | host | `expected_controller` no longer the holder → `lease_held`, no takeover | +| `r8-duplicate-send-never-reacquires-or-replays` | host | duplicates (any flags) return the receipt; no acquire, takeover or rerun; delayed receipt | +| `r8-disconnect-right-after-acceptance` | host | replies lost, work runs, lease expires; receipt query then duplicate on a new attachment | +| `r8-failed-persistence-changes-no-lease` | host | §I: failed admission write: `busy`, no grant, nothing runs, journal halted (same-id retry `busy`); after the restart the id is `rejected` "acceptance not confirmed" and not re-admitted; a new id is admitted | +| `r8-double-write-failure-takeover-never-runs` | host | §I: confirmed takeover while c1 holds; write and rollback fail: `busy`, c1 keeps the lease; after the restart `rejected`, never executed, same id returns it | +| `r8-crash-after-durable-acceptance-grants-no-lease` | host | §I.4: durable accepted, crash before the grant: no lease; `rejected` "authorization history unknown" at the restart, never dispatched; duplicate returns the receipt | +| `r8-expired-lease-is-checked-at-admission` | host | expiry at admission without a timer; old epoch stale | +| `r8-revoked-control-refuses-the-send-and-its-queued-prompt` | host | lost `control`: lease revoked, queued prompt rejected, Send `forbidden` | +| `r8-revoked-control-fences-work-across-reconnect-and-restart` | host | §H: same-permission reconnect keeps queued work; view-only re-authentication after disconnect rejects work of the gone attachment; regaining control or a restart doesn't revive it | +| `r8-control-fence-holds-when-a-rejection-write-fails` | host | §H: fence durable, rejection write fails: halted, nothing dispatched; no clean close, so recovery rejects the work (`authorization history unknown`) | +| `r8-failed-fence-write-rejects-queued-work-at-once` | host | §H: the fence write fails: lease revoked and the principal's queued work rejected in memory at once, halted, nothing dispatched after; restart: `authorization history unknown` | +| `r8-failed-fence-write-then-control-restored-never-runs` | host | §H (r4 trace): the fence write fails (rollback succeeds, then in a second cycle rollback fails), restart, control restored before confirmation: the queued request is `rejected` "authorization history unknown" and never dispatched | +| `r8-crash-rejects-queued-work-whose-history-is-unknown` | host | §H rule 2: crash with queued work, principal back with control at once: `rejected` "authorization history unknown"; same id returns it, a new id is admitted | +| `r8-clean-close-runs-queued-work-once-its-principal-is-confirmed` | host | §H rule 3: after a clean close queued work waits for its principal (not another one) and runs once, no lease | +| `r8-recovered-work-needs-its-principal-confirmed` | host | §H rule 3: after a clean close recovered work waits for its principal; not confirmed by 60 s → `control not confirmed` | +| `r8-delayed-send-reply-does-not-regrant-lost-control` | host | §F: the admitting receipt delayed past local input or a takeover grants nothing; its epoch is `stale_lease` | +| `r8-client-delayed-send-reply-never-restores-control` | client | §F reply rule: a late `lease` after local input, takeover, attachment replacement or local expiry is not applied; a timely one is | +| `r8-process-replaced-before-dispatch` | host | queued Send rejected on replacement; old `process_gen` `stale_target`; new one acquires | +| `r8-send-acquire-wire-rules` | host | field rules → `invalid_request`; `interrupt`/`stop`/`approval_rule_revoke` can't acquire | +| `r8-approval-answer-by-a-non-holder-leaves-the-lease` | host | §G: non-holder answers (no epoch, a foreign epoch), lease unchanged; view-only `forbidden` | +| `r8-approval-answer-keeps-the-holders-deadline` | host | §G at t = 6000/9000: holder and non-holder answers keep the deadline (repeat acquire: `ms_left` 6000), queued input written, expiry at 15000; answer with no holder grants nothing | +| `r8-approval-answer-with-acquire-is-refused` | host | `acquire` (payload or submit) → `unsupported_version`, no effect | +| `r8-approval-answers-crossing-first-wins` | host | holder and non-holder cross: first admitted wins, second `stale_target` | +| `r8-approval-answer-to-a-closed-request-is-stale-target` | host | terminal-answered, timed-out and already-answered requests → `stale_target` | + +Astation's API-level merge fixture (a delayed HTTP page against newer SSE +versions) is Astation's; the host side of it is +`r8-history-page-and-live-events-share-one-ordered-stream`. diff --git a/src/session_protocol/fixtures.rs b/src/session_protocol/fixtures.rs index 1366001..dac34a2 100644 --- a/src/session_protocol/fixtures.rs +++ b/src/session_protocol/fixtures.rs @@ -52,6 +52,9 @@ pub struct ClientSetup { pub client_kind: ClientKind, #[serde(default)] pub holds_lease_epoch: Option, + /// r8 draft: `false` leaves the client without the harness's `hello`. + #[serde(default)] + pub welcome: Option, } #[derive(Debug, Clone, Deserialize)] @@ -77,6 +80,10 @@ pub struct Scenario { pub name: String, #[serde(default)] pub side: Side, + /// A contract revision this runner doesn't implement yet (`"r8"`): the + /// scenario is parsed and reported as not run. + #[serde(default)] + pub requires: Option, pub setup: Setup, pub steps: Vec, /// Documentation for readers; not interpreted. @@ -176,12 +183,15 @@ pub type HostFactory<'a> = &'a dyn Fn(&Setup) -> Result, pub enum Outcome { Passed, Skipped, + /// Needs a contract revision not implemented yet (`requires`). + NotRun, } #[derive(Debug, Default)] pub struct Report { pub passed: Vec, pub skipped: Vec, + pub not_run: Vec, pub failed: Vec<(String, String)>, } @@ -227,6 +237,7 @@ pub fn run_all(dir: &Path, host: Option) -> Report { match result { Ok(Outcome::Passed) => report.passed.push(name), Ok(Outcome::Skipped) => report.skipped.push(name), + Ok(Outcome::NotRun) => report.not_run.push(name), Err(e) => report.failed.push((name, e)), } } @@ -237,6 +248,12 @@ pub fn run_all(dir: &Path, host: Option) -> Report { pub fn run_scenario(file: &Value, host: Option) -> Result { let scenario: Scenario = serde_json::from_value(file.clone()).map_err(|e| format!("scenario: {e}"))?; + if scenario.requires.is_some() { + return Ok(Outcome::NotRun); + } + if scenario.setup.clients.iter().any(|c| c.welcome.is_some()) { + return Err(format!("{}: welcome needs requires", scenario.name)); + } let mut runner = match scenario.side { Side::Host => match host { None => return Ok(Outcome::Skipped), @@ -942,6 +959,8 @@ mod tests { use super::*; use serde_json::json; + /// The scenarios this runner implements (r8-draft ones, with + /// `requires`, are left out; see `r8_draft_fixtures_parse_and_are_not_run`). fn scenario_files() -> Vec<(String, Value)> { scenario_paths(&fixtures_dir()) .into_iter() @@ -949,9 +968,27 @@ mod tests { let v: Value = serde_json::from_slice(&std::fs::read(&p).unwrap()).unwrap(); (p.file_name().unwrap().to_string_lossy().into_owned(), v) }) + .filter(|(_, v)| v.get("requires").is_none()) .collect() } + #[test] + fn r8_draft_fixtures_parse_and_are_not_run() { + let mut draft = Vec::new(); + for p in scenario_paths(&fixtures_dir()) { + let v: Value = serde_json::from_slice(&std::fs::read(&p).unwrap()).unwrap(); + if v.get("requires").is_some() { + let s: Scenario = serde_json::from_value(v.clone()).expect("r8 fixture parses"); + assert_eq!(s.requires.as_deref(), Some("r8"), "{}", s.name); + assert_eq!(run_scenario(&v, Some(&fake_runtime)), Ok(Outcome::NotRun)); + draft.push(s.name); + } + } + assert!(!draft.is_empty(), "r8 draft fixtures missing"); + let report = run_all(&fixtures_dir(), Some(&fake_runtime)); + assert_eq!(report.not_run, draft); + } + fn fake_runtime(setup: &Setup) -> Result, String> { Ok(Box::new(super::super::fake_runtime::FakeRuntime::new( setup, diff --git a/src/session_protocol/tests.rs b/src/session_protocol/tests.rs index d351653..c6120e1 100644 --- a/src/session_protocol/tests.rs +++ b/src/session_protocol/tests.rs @@ -224,6 +224,10 @@ fn every_fixture_wire_message_decodes_and_round_trips() { for path in files { let name = path.file_name().unwrap().to_string_lossy().to_string(); let file = read_fixture(&name); + // r8-draft fixtures use wire forms the r7.2 decoder doesn't know yet. + if file.get("requires").is_some() { + continue; + } let steps = file["steps"].as_array().unwrap(); for (i, step) in steps.iter().enumerate() { let mut msgs = Vec::new(); diff --git a/tests/runtime_fixtures_binary.rs b/tests/runtime_fixtures_binary.rs index 7c2608c..0be7ec9 100644 --- a/tests/runtime_fixtures_binary.rs +++ b/tests/runtime_fixtures_binary.rs @@ -45,7 +45,8 @@ //! mode), `fail_next_fsync`, `change_trust_gen`, `approval_rules` (session //! rules come only from a Codex session), and approval closes other //! than `answered_terminal`/`interrupted` or `approval_applied` with `ok: -//! false`. +//! false`. r8-draft scenarios (`requires: "r8"`) are reported as not run +//! (`requires r8`) until r8 ships. //! //! A scenario the real runtime fails would be listed in `KNOWN_DEVIATIONS` //! with the failure it must keep showing, and why (none today). @@ -1438,9 +1439,13 @@ fn run_named(scenarios: &[Value], only: Option<&str>) -> Vec<(String, Result<(), .filter(|s| only.is_none_or(|o| s["name"] == o)) .map(|s| { let name = s["name"].as_str().unwrap().to_string(); - let handle = scope.spawn(move || match unsupported(s) { - Some(why) => Err(format!("not run: {why}")), - None => run(s), + let handle = scope.spawn(move || match s.get("requires") { + // r8-draft fixtures: parsed, not run until r8 ships. + Some(r) => Err(format!("not run: requires {}", r.as_str().unwrap_or("?"))), + None => match unsupported(s) { + Some(why) => Err(format!("not run: {why}")), + None => run(s), + }, }); (name, handle) }) @@ -1458,10 +1463,15 @@ fn shared_host_fixtures_run_against_the_real_runtime() { assert!(scenarios.len() >= 13, "host fixtures missing"); let only = std::env::var("ATEM_FIXTURE").ok(); let results = run_named(&scenarios, only.as_deref()); + let draft: Vec<&str> = scenarios + .iter() + .filter(|s| s.get("requires").is_some()) + .map(|s| s["name"].as_str().unwrap()) + .collect(); let mut failed = Vec::new(); for (name, result) in &results { let known = KNOWN_DEVIATIONS.iter().find(|(n, _)| n == name); - let not_run = NOT_RUN.contains(&name.as_str()); + let not_run = NOT_RUN.contains(&name.as_str()) || draft.contains(&name.as_str()); match (result, known) { (Err(e), _) if e.starts_with("not run:") && not_run => { eprintln!("NOT RUN {name} ({})", &e["not run: ".len()..])