From bca84a60f4b1f1b6730ba0eb681c2c380b23ab62 Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Mon, 10 Aug 2026 17:52:09 +0200 Subject: [PATCH 01/15] Specify declared native DING delivery --- docs/vrs/01-ding/requirements.md | 57 +++++++++++++++++++++----------- docs/vrs/01-ding/spec.md | 55 +++++++++++++++++++++++------- docs/vrs/requirements.md | 5 ++- docs/vrs/spec.md | 29 +++++++++++----- 4 files changed, 105 insertions(+), 41 deletions(-) diff --git a/docs/vrs/01-ding/requirements.md b/docs/vrs/01-ding/requirements.md index 130f72fa..41437f0c 100644 --- a/docs/vrs/01-ding/requirements.md +++ b/docs/vrs/01-ding/requirements.md @@ -20,10 +20,10 @@ is in [`spec.md`](./spec.md). ## Assumptions -- **DING-A01 Rendered screens only:** The only available evidence about a - composer's state is a rendered terminal screen. No maintained harness exposes - an evented idle signal, so every precondition below is a measured heuristic - over text. `DQ2` in [`../spec.md`](../spec.md) tracks closing that gap. +`DING-A01 Rendered screens only` is retired. Rendered-screen evidence is a +limit of the legacy `ding` transport. It is not an assumption for a maintained +harness that declares a native `deliver` transport. + - **DING-A02 Cooperative human:** The human sharing a pane is not adversarial. A screen that deliberately imitates another harness's composer is a correctness concern, not a security boundary, consistent with `A02`. @@ -41,21 +41,40 @@ is in [`spec.md`](./spec.md). ## Requirements -### Must preserve initial transport and gate every retry - -- **DING-R01 Combined initial transport:** A fresh notice uses one bounded PTY - transaction containing the bracketed paste, the accepted 0.5 second delay, - and Return. Ownership is recorded before that command starts. Composer - heuristics do not split or suppress this initial transport. -- **DING-R02 Two adjacent retained-safe retry observations:** A later bare - Return is permitted only for a transport-owned payload whose exact notice is - still the complete composer and is classified `RetainedSafe` in two - immediately adjacent inspections. The final observation is adjacent to the - Return itself. Any change, block, or uncertainty prevents retry submission. -- **DING-R03 Fail-closed receipt and retry:** After the initial transport, a - changed composer, a human draft, an active turn, a modal, an unreadable - screen, an unrecognized harness, and a bounded observation timeout never - become `Delivered` and receive no retry input. Anything not positively +### Must select one explicit transport + +- **DING-R11 Declared transport:** An agent selects at most one delivery + transport. `ding` selects the legacy screen transport. `deliver "mcp"` + selects the Claude native transport. `deliver "app-server"` selects the + Codex native transport. A declaration with neither node has no DING delivery. + A declaration with both nodes, multiple `deliver` nodes, or an unsupported + `deliver` value is invalid. +- **DING-R12 No transport inference:** st2 does not infer a native transport + from an agent command or a screen. Native delivery uses the declared adapter. + A binary that does not know the `deliver` node rejects that declaration + instead of treating it as legacy delivery. +- **DING-R13 Durable native delivery:** The inbox file remains the source of + truth for native delivery. Only the adapter's declared success condition can + complete a delivery attempt. A closed, unavailable, stale, or unknown native + transport leaves the message unread and retryable. Archive precedence and + restart recovery remain unchanged. + +### Must preserve legacy transport and gate every legacy retry + +- **DING-R01 Combined initial transport:** A fresh legacy notice uses one + bounded PTY transaction containing the bracketed paste, the accepted 0.5 + second delay, and Return. Ownership is recorded before that command starts. + Composer heuristics do not split or suppress this initial transport. +- **DING-R02 Two adjacent retained-safe retry observations:** A later legacy + bare Return is permitted only for a transport-owned payload whose exact + notice is still the complete composer and is classified `RetainedSafe` in + two immediately adjacent inspections. The final observation is adjacent to + the Return itself. Any change, block, or uncertainty prevents retry + submission. +- **DING-R03 Fail-closed receipt and retry:** After the initial legacy + transport, a changed composer, a human draft, an active turn, a modal, an + unreadable screen, an unrecognized harness, and a bounded observation timeout + never become `Delivered` and receive no retry input. Anything not positively understood retains staged ownership. On an inspect-only staged retry, a maintained adapter may positively prove that the exact owned payload is no longer retained; that proof relinquishes ownership only when an archive diff --git a/docs/vrs/01-ding/spec.md b/docs/vrs/01-ding/spec.md index 3d432022..9a50b17c 100644 --- a/docs/vrs/01-ding/spec.md +++ b/docs/vrs/01-ding/spec.md @@ -10,6 +10,36 @@ specified in [`01-claude/spec.md`](./01-claude/spec.md) and Active. A map to the implementation and its evidence, not a replacement for the tests. +## Delivery selection + +Delivery is opt-in. An agent declaration selects one transport: + +| Declaration | Transport | +| --- | --- | +| `ding` | Legacy screen transport | +| `deliver "mcp"` | Claude native MCP transport | +| `deliver "app-server"` | Codex native app-server transport | +| Neither node | No delivery | + +`ding` and `deliver` are mutually exclusive. More than one `deliver` node is +invalid. Any other `deliver` value is invalid. st2 does not infer a transport +from the agent command because command arguments are opaque. + +The native selector is a new `deliver` node. A binary that does not implement +the node rejects the declaration. Native delivery must not be encoded as an +argument to `ding`, because an older parser would accept that form as legacy +`ding` and silently use the wrong transport. + +The durable inbox is the source of truth for every transport. An archive with +the same message name wins. A native adapter completes delivery only after its +declared provider-specific success condition. If the adapter is closed, +unavailable, stale, or in an unknown state, it sends no unsafe input and leaves +the inbox message unread for retry. Native adapters do not use the +rendered-screen classifier. + +The rest of this document defines the unchanged legacy screen transport. The +native wire contracts are in each maintained harness specification. + ## Composer states One inspection of a rendered screen, evaluated against one exact expected @@ -45,11 +75,11 @@ staged retry ─► receipt ─┬─ Accepted ─────────── └─ RetainedBlocked / Unproven ─────────────► Staged ``` -Fresh delivery preserves the production transport: one bounded PTY transaction -contains a bracketed paste, a 0.5 second delay, and Return (`DING-R01`). -Ownership is recorded immediately before that transaction. The production path -does not inspect the composer first and does not use the separate staging -helper. +Fresh legacy delivery preserves the production transport: one bounded PTY +transaction contains a bracketed paste, a 0.5 second delay, and Return +(`DING-R01`). Ownership is recorded immediately before that transaction. The +production path does not inspect the composer first and does not use the +separate staging helper. Every failure of that terminal command or of the following receipt observation resolves to `Staged` (`DING-R07`): the paste and Return may already have reached @@ -148,10 +178,11 @@ Declared `busy` never suppresses delivery; only fresh `dnd` defers it ## Known limits -- Idle proof depends on footer chrome that a harness may render differently - across permission or approval modes. A harness whose footer is not recognized - in a given mode defers indefinitely rather than delivering. This is an - explicit limit per `T01`, and each harness spec states which modes it proves. -- The classifier is a measured heuristic over rendered text, not an evented - signal, so a renderer change can defer delivery until the grammar is updated. - Tracked as `DQ2` in [`../spec.md`](../spec.md). +- Legacy idle proof depends on footer chrome that a harness may render + differently across permission or approval modes. A harness whose footer is + not recognized in a given mode defers indefinitely rather than delivering. + This is an explicit limit per `T01`, and each harness spec states which modes + it proves. +- The legacy classifier is a measured heuristic over rendered text, not an + evented signal, so a renderer change can defer legacy delivery until the + grammar is updated. Maintained native transports do not use this classifier. diff --git a/docs/vrs/requirements.md b/docs/vrs/requirements.md index dc519e72..18bd6ab9 100644 --- a/docs/vrs/requirements.md +++ b/docs/vrs/requirements.md @@ -84,7 +84,10 @@ accepted. - **R05 DING/archive semantics:** Inbox delivery, archive precedence, retries, suppression, and restart recovery are deterministic and tested. DING may interrupt agent work, but it must not alter or submit a human's active draft; - an unknown interaction state defers delivery. + an unknown interaction state defers delivery. A declaration selects either + the legacy screen transport or one supported native transport. st2 does not + infer a native transport from an opaque command. A failed or unavailable + transport leaves the inbox message unread and retryable. - **R06 Restartable launch definitions:** A restarted PTY or exec receives the complete effective launch definition, including environment and supported launch fields. diff --git a/docs/vrs/spec.md b/docs/vrs/spec.md index 561aa7ce..7e3aed5e 100644 --- a/docs/vrs/spec.md +++ b/docs/vrs/spec.md @@ -664,6 +664,26 @@ atomic inbox file → DING attempt → agent reads → archive receipt composer cannot create a short-lived PTY probe on every inbox poll. Inbox reads do not wake the sidecar; only mutations bypass its bounded poll cadence. +### Declared native DING delivery (DQ2 resolution) + +An agent selects at most one delivery transport. `ding` keeps the legacy +screen transport unchanged. `deliver "mcp"` selects native Claude delivery. +`deliver "app-server"` selects native Codex delivery. An agent with neither +node has no delivery. The two node forms are mutually exclusive, multiple +`deliver` nodes are invalid, and an unknown `deliver` value is invalid. + +The transport is explicit because an agent command is opaque. The native +selector is a new node so an older binary rejects the declaration. It is not an +argument to `ding`, which an older parser could accept as legacy delivery. + +Native delivery replaces rendered-screen inference for maintained Claude and +Codex agents that select it. Each native adapter uses its provider's evented +control channel and its provider-specific success condition. A closed, +unavailable, stale, or unknown channel sends no unsafe input. The durable inbox +message remains unread and retryable. Archive precedence, suppression, and +restart recovery remain the same for every transport. The legacy classifier +remains available only for agents that select `ding`. + ## State and scope - **R08:** Presence and activity status are separate signals. The catalog must @@ -785,15 +805,6 @@ the resident supervisor continues to reconcile the complete local catalog. boundary, and execution receipts are not yet specified. A successful executable eval and Nathan's approval should resolve this before adding scheduler requirements. -- **DQ2 Safe DING delivery:** Bounded observation now replaces the fixed - paste-to-Return delay: maintained Codex and Claude composers must be - positively empty before paste and show the exact staged notice twice before - a separate Return. Human, modal, active, changed, timed-out, and unknown - states fail closed, with staged-payload ownership preventing duplicate paste. - This measured screen heuristic is still not an evented proof and renderer - changes may defer delivery. Resolve the remaining gap with a stronger evented - signal or other measured classifier; a small on-device model is an optional - experiment, not a required architecture. - **DQ3 Catalog agent state:** Define the catalog paths, schemas, freshness rules, and atomic update semantics for presence, activity status, current plan, and current plan step. Prove that stale state is distinguishable and From a47962f82e0b25b2bd18c2040605440d4002d7b3 Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Mon, 10 Aug 2026 17:57:36 +0200 Subject: [PATCH 02/15] Correct native delivery compatibility boundary --- docs/vrs/01-ding/requirements.md | 7 +++++-- docs/vrs/01-ding/spec.md | 14 ++++++++++---- docs/vrs/spec.md | 14 +++++++++++--- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/docs/vrs/01-ding/requirements.md b/docs/vrs/01-ding/requirements.md index 41437f0c..3fe9a55d 100644 --- a/docs/vrs/01-ding/requirements.md +++ b/docs/vrs/01-ding/requirements.md @@ -51,13 +51,16 @@ harness that declares a native `deliver` transport. `deliver` value is invalid. - **DING-R12 No transport inference:** st2 does not infer a native transport from an agent command or a screen. Native delivery uses the declared adapter. - A binary that does not know the `deliver` node rejects that declaration - instead of treating it as legacy delivery. + A binary that supports `deliver` validates its value and its mutual exclusion + with `ding`. - **DING-R13 Durable native delivery:** The inbox file remains the source of truth for native delivery. Only the adapter's declared success condition can complete a delivery attempt. A closed, unavailable, stale, or unknown native transport leaves the message unread and retryable. Archive precedence and restart recovery remain unchanged. +- **DING-R14 Missing transport report:** Doctor reports an active agent that + declares neither `ding` nor `deliver`. The omission remains a valid opt-out + and does not block the agent. The report makes a no-delivery state visible. ### Must preserve legacy transport and gate every legacy retry diff --git a/docs/vrs/01-ding/spec.md b/docs/vrs/01-ding/spec.md index 9a50b17c..65b114d1 100644 --- a/docs/vrs/01-ding/spec.md +++ b/docs/vrs/01-ding/spec.md @@ -25,10 +25,16 @@ Delivery is opt-in. An agent declaration selects one transport: invalid. Any other `deliver` value is invalid. st2 does not infer a transport from the agent command because command arguments are opaque. -The native selector is a new `deliver` node. A binary that does not implement -the node rejects the declaration. Native delivery must not be encoded as an -argument to `ding`, because an older parser would accept that form as legacy -`ding` and silently use the wrong transport. +The native selector is a new `deliver` node. A binary released before this +contract ignores that unknown agent child. It lowers a valid `deliver`-only +agent with no delivery sidecar. The agent receives no DING. It does not silently +use the legacy screen transport. This is a visible delivery outage. + +A binary that supports `deliver` validates its value and its mutual exclusion +with `ding`. Native delivery must not be encoded as an argument to `ding`, +because a pre-change parser would accept that form as legacy `ding` and use the +wrong transport. Doctor reports an active agent that declares no transport. The +report does not make the valid no-delivery opt-out an error. The durable inbox is the source of truth for every transport. An archive with the same message name wins. A native adapter completes delivery only after its diff --git a/docs/vrs/spec.md b/docs/vrs/spec.md index 7e3aed5e..a894f073 100644 --- a/docs/vrs/spec.md +++ b/docs/vrs/spec.md @@ -672,9 +672,17 @@ screen transport unchanged. `deliver "mcp"` selects native Claude delivery. node has no delivery. The two node forms are mutually exclusive, multiple `deliver` nodes are invalid, and an unknown `deliver` value is invalid. -The transport is explicit because an agent command is opaque. The native -selector is a new node so an older binary rejects the declaration. It is not an -argument to `ding`, which an older parser could accept as legacy delivery. +The transport is explicit because an agent command is opaque. A binary released +before this contract ignores the unknown `deliver` child. It lowers a valid +`deliver`-only declaration with no delivery sidecar. The agent receives no DING +instead of receiving a DING through the legacy screen transport. This is a +visible delivery outage. A binary that supports `deliver` validates its value +and its mutual exclusion with `ding`. + +Native delivery is not an argument to `ding`, which a pre-change parser would +accept as legacy delivery and route through the wrong transport. Doctor reports +an active agent that declares neither transport. The no-delivery form remains a +valid opt-out and does not block the agent. Native delivery replaces rendered-screen inference for maintained Claude and Codex agents that select it. Each native adapter uses its provider's evented From ea5148393feebc68e70bef072c02ff6d1ccf014e Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Mon, 10 Aug 2026 18:07:32 +0200 Subject: [PATCH 03/15] Align DING ontology with native delivery --- docs/vrs/02-agent-spec/spec.md | 18 +++++++++++------- docs/vrs/ontology.md | 9 ++++++--- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/vrs/02-agent-spec/spec.md b/docs/vrs/02-agent-spec/spec.md index 5ef2aedb..e7b524d3 100644 --- a/docs/vrs/02-agent-spec/spec.md +++ b/docs/vrs/02-agent-spec/spec.md @@ -358,10 +358,13 @@ Authoring: [pinned complete declaration][evals-fields]. st2 source:

F14 Compact agent fields

-Compact `command`, `argv`, `env`, `lifecycle`, and `ding` convert to the -generated agent PTY and derived sidecar. The tasks use F09, F11, and F12; -`ding` carries the dependency on the generated agent task described there. -Compact syntax adds no other behavior. +Compact `command`, `argv`, `env`, and `lifecycle` define the generated agent +PTY. Bare `ding` selects the derived legacy screen sidecar. `deliver "mcp"` +selects native Claude delivery. `deliver "app-server"` selects native Codex +delivery. The two selector nodes are mutually exclusive. More than one +`deliver` node and any other value are invalid. Neither node means no delivery. +Each generated transport depends on the generated agent task. The tasks use +F09, F11, and F12. Compact syntax adds no other behavior. Authoring: [pinned compact tasks][evals-tasks]. That document and st2 `9887b28` predate compact `argv` and `lifecycle`. Current st2 source: @@ -370,9 +373,10 @@ predate compact `argv` and `lifecycle`. Current st2 source:

F15 Provider and ignored fields

-Core st2 ignores `harness`, `model`, `persona`, `permissions`, `transport`, -`strategy`, `meta`, and provider extensions. They do not change core equality, -wake behavior, or actions. Providers may convert them into F05 through F14; +Core st2 ignores `harness`, `model`, `persona`, `permissions`, the legacy +render-only `transport` field, `strategy`, `meta`, and provider extensions. +They do not change core equality, wake behavior, or actions. Providers may +convert them into F05 through F14; core acts only on that concrete output. Authoring: [pinned complete declaration][evals-fields]. st2 source: diff --git a/docs/vrs/ontology.md b/docs/vrs/ontology.md index 38606fb0..e57b46cb 100644 --- a/docs/vrs/ontology.md +++ b/docs/vrs/ontology.md @@ -154,9 +154,12 @@ Authority: [`message::Message`](../../src/message.rs#L26-L46) ### DING -The terminal notification that makes an agent aware of unread messages. +The delivery signal that makes an agent aware of unread messages. A declaration +selects either the legacy terminal transport or one provider-native control +transport. -Authority: [DING module contract](../../src/ding/mod.rs#L1-L14) +Authority: [DING specification](./01-ding/spec.md) and +[DING module contract](../../src/ding/mod.rs#L1-L14) ## Collision rules @@ -172,7 +175,7 @@ Authority: [DING module contract](../../src/ding/mod.rs#L1-L14) value and [bus ID](../../crates/agent-spec/src/spec.rs#L203-L211) for the host-qualified address. - Use [message](../../src/message.rs#L26-L46) for the durable record and - [DING](../../src/ding/mod.rs#L1-L14) for its terminal notification. + [DING](./01-ding/spec.md) for its delivery signal. ## Avoid From 28aa6303d90465a3329198f9a307759384bd4bca Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Mon, 10 Aug 2026 18:30:18 +0200 Subject: [PATCH 04/15] Specify Codex native DING transport --- docs/vrs/01-ding/02-codex/spec.md | 139 +++++++++++++++++++++++++++++- 1 file changed, 136 insertions(+), 3 deletions(-) diff --git a/docs/vrs/01-ding/02-codex/spec.md b/docs/vrs/01-ding/02-codex/spec.md index 71087902..8982f692 100644 --- a/docs/vrs/01-ding/02-codex/spec.md +++ b/docs/vrs/01-ding/02-codex/spec.md @@ -1,13 +1,146 @@ # Codex harness specification -The screen grammar by which DING recognizes a Codex composer. It realizes -[`../requirements.md`](../requirements.md) through the mechanism in -[`../spec.md`](../spec.md). +This document defines native app-server delivery and the legacy Codex screen +grammar. It realizes [`../requirements.md`](../requirements.md) through the +selection and durability rules in [`../spec.md`](../spec.md). ## Status Active. +## Native app-server transport + +`deliver "app-server"` selects this transport. st2 delivers through the Codex +app-server control protocol. It does not inspect the rendered screen, write to +the composer, or start the legacy `ding` sidecar. + +The native path uses typed user input only. It must never use +`thread/inject_items`. Raw injection does not start an idle turn, does not have +the typed user-message receipt below, and has different persistence behavior. + +### Controlled launch and thread identity + +st2 starts one app-server daemon for the declared agent on a host-local Unix +socket. It opens and initializes its control connection before it starts the +Codex TUI with `codex --remote unix://PATH`. The control client must be able to +observe `thread/started` before the TUI can create a new thread. + +For a new session, st2 records the thread ID from `thread/started`. For a +resumed session, st2 loads the recorded thread ID and calls `thread/resume` +before it permits delivery. It does not infer ownership from `thread/list`, a +working directory, a process, or a PTY. Those surfaces do not identify which +TUI owns a thread. + +The thread binding is persistent runtime state. It includes the exact agent +runtime incarnation that owns it. st2 rejects a binding from a prior +incarnation. A missing, conflicting, or stale binding makes the native +transport unavailable and leaves every message unread. + +The control client remains subscribed to these events for the bound thread: + +- `thread/status/changed`, +- `turn/started`, +- `turn/completed`, and +- `item/started` and `item/completed`. + +`ThreadStatus` distinguishes idle and active states. It does not carry the +active turn ID. Only the turn lifecycle supplies that ID. + +### Delivery state machine + +The adapter applies this rule to the bound thread and the FIFO inbox head: + +| Observed state | Request | Result | +| --- | --- | --- | +| Idle | `turn/start` with typed text | Start a turn and wake Codex | +| Active regular turn with exact current ID | `turn/steer` with typed text and `expectedTurnId` | Queue input on that turn | +| Review or manual compaction | None | Hold until a later idle state | +| No active turn ID, conflicting events, or stale ID | None | Reconcile state and hold | + +Every `turn/start` and `turn/steer` request includes a stable +`clientUserMessageId` derived from the recipient, thread binding, and message +filename. The adapter sends no turn-level overrides with `turn/steer`. + +`turn/steer` must use the exact ID from the latest unmatched `turn/started` +event. A `turn/completed` event clears that ID. A steering error for no active +turn, a stale `expectedTurnId`, review, or compaction is a hold result. The +adapter must not fall back to `turn/start` or `thread/inject_items` in the same +attempt. + +The adapter marks review and compaction as non-steerable when +`enteredReviewMode` or `contextCompaction` item events appear. The app server +can reject a request before those events arrive. The same hold rule applies to +that race. + +### Typed acceptance receipt + +A JSON-RPC success response is not a delivery receipt. A returned turn ID is +not a delivery receipt. st2 completes the DING attempt only after this event: + +```text +item/completed + item.type = "userMessage" + item.clientId = + threadId = +``` + +`item/completed` is the authoritative item state. An `item/started` event may +show progress, but it does not complete delivery. An item for another client +ID, thread, or runtime incarnation does not complete delivery. + +The adapter records submission state before it sends a request. If the control +connection closes after submission and before the typed receipt, the attempt is +ambiguous. On reconnect, st2 resumes the bound thread and reconciles its typed +user-message history before it sends that client ID again. + +The app server does not promise duplicate rejection for +`clientUserMessageId`. st2 therefore owns duplicate control. It persists one +accepted receipt for the message and runtime binding. Watcher events, poll +events, reconnects, and supervisor restarts consult that receipt before they +send. Archive precedence removes obsolete receipt state. + +### Durable inbox and shutdown + +The selected catalog inbox remains authoritative. Native delivery does not +archive or delete the message. The agent reads and archives it through normal +message commands. + +Before each attempt, the adapter runs the normal message sweep. An archive +record with the same filename wins. A held, rejected, disconnected, unknown, +or ambiguous attempt leaves the message unread and retryable. + +Control transport close is a normal adapter stop. The adapter sends nothing +after close. A restarted adapter must restore the exact thread binding and +duplicate-control state before it attempts delivery. + +### Remote TUI evidence + +The native transport is not accepted until a live `codex --remote` test proves +all of these results against the same app server and control client: + +- The normal TUI can start a new bound thread and resume that exact thread. +- An idle inbox message produces one typed `turn/start` user message and + observable agent work. +- A message during a regular active turn produces one typed `turn/steer` user + message without corrupting terminal input or the active turn. +- Review, compaction, stale-turn, and no-active-turn states hold the message. +- The inbox file remains until the agent reads and archives it. +- A deliberate protocol or receipt break makes the test fail. + +The evidence must also record every user-visible difference between a local +TUI and the remote TUI. Known protocol limits are not silently treated as +parity. + +Codex app-server and its remote transport are provider experimental surfaces. +The implementation pins its accepted protocol schema to a tested Codex +version. An incompatible schema or event change makes delivery unavailable and +leaves the inbox unread. + +## Legacy screen transport + +The remaining sections define the unchanged screen grammar selected by +`ding`. + ## Locating the composer This harness is located against the **raw** screen, including escape sequences, From 6be17760a86556dd1e0fc50c685fe3f879594ef5 Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Tue, 11 Aug 2026 16:31:29 +0200 Subject: [PATCH 05/15] Align Codex delivery contract with bounded inbox views --- docs/vrs/01-ding/02-codex/spec.md | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/vrs/01-ding/02-codex/spec.md b/docs/vrs/01-ding/02-codex/spec.md index 8982f692..35aab526 100644 --- a/docs/vrs/01-ding/02-codex/spec.md +++ b/docs/vrs/01-ding/02-codex/spec.md @@ -48,7 +48,11 @@ active turn ID. Only the turn lifecycle supplies that ID. ### Delivery state machine -The adapter applies this rule to the bound thread and the FIFO inbox head: +The adapter applies this rule to the bound thread and a bounded body-bearing +FIFO inbox view. The view contains the largest complete prefix that fits 16 +messages and 16 KiB. It never truncates a body. If the head does not fit, the +view identifies that message without its body, and all later messages remain +unread behind it. | Observed state | Request | Result | | --- | --- | --- | @@ -58,8 +62,10 @@ The adapter applies this rule to the bound thread and the FIFO inbox head: | No active turn ID, conflicting events, or stale ID | None | Reconcile state and hold | Every `turn/start` and `turn/steer` request includes a stable -`clientUserMessageId` derived from the recipient, thread binding, and message -filename. The adapter sends no turn-level overrides with `turn/steer`. +`clientUserMessageId` derived from the recipient, thread binding, and FIFO head +filename. The identifier controls duplicate transport for the delivered view; +it does not settle any included message. The adapter sends no turn-level +overrides with `turn/steer`. `turn/steer` must use the exact ID from the latest unmatched `turn/started` event. A `turn/completed` event clears that ID. A steering error for no active @@ -95,19 +101,20 @@ user-message history before it sends that client ID again. The app server does not promise duplicate rejection for `clientUserMessageId`. st2 therefore owns duplicate control. It persists one -accepted receipt for the message and runtime binding. Watcher events, poll -events, reconnects, and supervisor restarts consult that receipt before they -send. Archive precedence removes obsolete receipt state. +accepted receipt for the FIFO head that identifies the delivered view and its +runtime binding. Watcher events, poll events, reconnects, and supervisor +restarts consult that receipt before they send. Archive precedence removes +obsolete receipt state. ### Durable inbox and shutdown The selected catalog inbox remains authoritative. Native delivery does not -archive or delete the message. The agent reads and archives it through normal -message commands. +archive or delete any included message. The agent handles and archives each +message through normal message commands. Before each attempt, the adapter runs the normal message sweep. An archive -record with the same filename wins. A held, rejected, disconnected, unknown, -or ambiguous attempt leaves the message unread and retryable. +record for the identifying FIFO head wins. A held, rejected, disconnected, +unknown, or ambiguous attempt leaves every message unread and retryable. Control transport close is a normal adapter stop. The adapter sends nothing after close. A restarted adapter must restore the exact thread binding and From a4da875a45bc7cd4eef21e250a062e3d0fcd6145 Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Tue, 11 Aug 2026 22:07:32 +0200 Subject: [PATCH 06/15] Define measurable DING delivery efficiency --- docs/vrs/01-ding/02-codex/spec.md | 5 +++++ docs/vrs/01-ding/requirements.md | 21 +++++++++++++++++++++ docs/vrs/01-ding/spec.md | 29 +++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/docs/vrs/01-ding/02-codex/spec.md b/docs/vrs/01-ding/02-codex/spec.md index 35aab526..d1baae52 100644 --- a/docs/vrs/01-ding/02-codex/spec.md +++ b/docs/vrs/01-ding/02-codex/spec.md @@ -54,6 +54,11 @@ messages and 16 KiB. It never truncates a body. If the head does not fit, the view identifies that message without its body, and all later messages remain unread behind it. +The byte limit bounds inbox input handed to one model inference. The message +limit bounds the action set created by a burst of small messages. These values +are transport bounds, not efficiency thresholds. Their token and inference cost +remains unmeasured under `DING-R15` through `DING-R18`. + | Observed state | Request | Result | | --- | --- | --- | | Idle | `turn/start` with typed text | Start a turn and wake Codex | diff --git a/docs/vrs/01-ding/requirements.md b/docs/vrs/01-ding/requirements.md index 3fe9a55d..2020cefe 100644 --- a/docs/vrs/01-ding/requirements.md +++ b/docs/vrs/01-ding/requirements.md @@ -123,6 +123,27 @@ harness that declares a native `deliver` transport. archived staged head. Unread, unreadable, unrecognized, and ambiguous attempts retain staged ownership and retry by inspection without re-pasting. +### Must make delivery efficiency measurable + +- **DING-R15 Delivered-message cost unit:** Every efficiency result uses one + delivered inbox message as its cost unit. A positive legacy receipt counts its + exact FIFO head. A positive native receipt counts each complete inbox message + in its accepted view. Held attempts, retries, overflow, and an oversized-head + metadata fallback do not increase the delivered-message count. Results do not + use turns, sessions, requests, or attempts as the cost unit. +- **DING-R16 Inference and tool-crossing cost:** Evidence reports model + inferences per delivered message and model tool-boundary crossings per + delivered message as separate values. It does not infer either value from a + provider turn count. +- **DING-R17 Token cost classes:** Evidence reports input, output, cache-read + input, and cache-creation input tokens per delivered message as separate + values. An unavailable token class is `unknown`, not zero. +- **DING-R18 Comparable evidence:** A transport comparison is valid only when + the provider exposes the requested counts and the experiment attributes them + to the same isolated target messages. A transport that cannot be measured for + a requested metric is incomparable for that metric. No numeric pass threshold + exists until accepted evaluation evidence establishes it. + ## Evidence Each guarantee above is pinned by a named test in diff --git a/docs/vrs/01-ding/spec.md b/docs/vrs/01-ding/spec.md index 65b114d1..85b5118c 100644 --- a/docs/vrs/01-ding/spec.md +++ b/docs/vrs/01-ding/spec.md @@ -43,6 +43,35 @@ unavailable, stale, or in an unknown state, it sends no unsafe input and leaves the inbox message unread for retry. Native adapters do not use the rendered-screen classifier. +## Efficiency accounting + +Delivery efficiency is measured per delivered inbox message, not per provider +turn or session (`DING-R15`). The denominator is the exact number of messages +covered by a positive receipt: one FIFO head for legacy delivery, or the +complete messages in one accepted native view. A held or failed attempt, a +retry, overflow, and an oversized-head metadata fallback have a denominator of +zero. Correctness evidence reports those outcomes separately; it does not hide +them inside a cost average. + +Each experiment reports these values separately (`DING-R16`, `DING-R17`): + +| Metric | Current requirement status | +| --- | --- | +| Model inferences per delivered message | Unmeasured; no pass threshold | +| Model tool-boundary crossings per delivered message | Unmeasured; no pass threshold | +| Input tokens per delivered message | Unmeasured; no pass threshold | +| Output tokens per delivered message | Unmeasured; no pass threshold | +| Cache-read input tokens per delivered message | Unmeasured; no pass threshold | +| Cache-creation input tokens per delivered message | Unmeasured; no pass threshold | + +A provider turn is not evidence of one inference or a fixed number of tool +crossings. Counts must come from the provider's authoritative event or usage +surface. If a provider omits a requested count, the result is `unknown`. If the +experiment cannot isolate the target messages or expose comparable counts for +both transports, that metric is incomparable rather than zero or improved +(`DING-R18`). Accepted evaluation evidence can fill the baseline and threshold; +the specification does not invent either value. + The rest of this document defines the unchanged legacy screen transport. The native wire contracts are in each maintained harness specification. From b5debb736b5fd416eac7294849ed816242bd5625 Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Tue, 11 Aug 2026 22:20:38 +0200 Subject: [PATCH 07/15] Document missing Claude native adapter --- docs/vrs/01-ding/spec.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/vrs/01-ding/spec.md b/docs/vrs/01-ding/spec.md index 85b5118c..b5a0e886 100644 --- a/docs/vrs/01-ding/spec.md +++ b/docs/vrs/01-ding/spec.md @@ -21,6 +21,14 @@ Delivery is opt-in. An agent declaration selects one transport: | `deliver "app-server"` | Codex native app-server transport | | Neither node | No delivery | +The selector table defines the transport contract, not current implementation +parity. Current st2 implements the Codex app-server adapter. It does not +implement the production Claude MCP adapter. The parser accepts `deliver +"mcp"`, leaves the authored Claude launch unchanged, and derives no legacy +`ding` sidecar. A Claude agent that selects it therefore receives no DING. Do +not deploy that selector until the production adapter exists. The Claude +channel specification and standalone probe are not that adapter. + `ding` and `deliver` are mutually exclusive. More than one `deliver` node is invalid. Any other `deliver` value is invalid. st2 does not infer a transport from the agent command because command arguments are opaque. From 9c86a40d2f1326bcb5630d6cc91389fa884d5f61 Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Tue, 11 Aug 2026 22:42:06 +0200 Subject: [PATCH 08/15] Define Codex protocol version admission --- docs/vrs/01-ding/02-codex/spec.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/vrs/01-ding/02-codex/spec.md b/docs/vrs/01-ding/02-codex/spec.md index d1baae52..1ccfd14f 100644 --- a/docs/vrs/01-ding/02-codex/spec.md +++ b/docs/vrs/01-ding/02-codex/spec.md @@ -144,9 +144,12 @@ TUI and the remote TUI. Known protocol limits are not silently treated as parity. Codex app-server and its remote transport are provider experimental surfaces. -The implementation pins its accepted protocol schema to a tested Codex -version. An incompatible schema or event change makes delivery unavailable and -leaves the inbox unread. +The implementation pins its accepted protocol schema to a finite set of exact, +tested Codex versions. Adding a version requires a schema comparison for every +request, response, and event path used by this adapter plus the live remote TUI +acceptance above. The current set is Codex CLI 0.145.0 and 0.146.0. Any other +version, or an incompatible schema or event change, makes delivery unavailable +and leaves the inbox unread. ## Legacy screen transport From 7d33be555cbe026853ab0810c18b69f85777c56f Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Tue, 11 Aug 2026 23:27:51 +0200 Subject: [PATCH 09/15] State Codex post-settlement audit boundary --- docs/vrs/01-ding/02-codex/spec.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/vrs/01-ding/02-codex/spec.md b/docs/vrs/01-ding/02-codex/spec.md index 1ccfd14f..6b5472ee 100644 --- a/docs/vrs/01-ding/02-codex/spec.md +++ b/docs/vrs/01-ding/02-codex/spec.md @@ -125,6 +125,21 @@ Control transport close is a normal adapter stop. The adapter sends nothing after close. A restarted adapter must restore the exact thread binding and duplicate-control state before it attempts delivery. +### Post-settlement observability + +Version 1 is not a delivery audit log. Its accepted delivery state exists to +prevent duplicate transport while the identifying inbox head remains unread. +Archive precedence removes that state after the agent archives the head. The +archive proves agent settlement. After that removal, st2 cannot prove which +transport method, turn ID, client ID, or acceptance time delivered the message. +It must not make that historical claim. + +**Codex-DQ1 Post-settlement audit:** Decide whether a later contract should +retain a bounded transport audit after archive. That contract must define the +retained fields, privacy and redaction rules, retention and resource bounds, +and failure behavior before implementation. Version 1 deliberately defines no +duration, size, or pass threshold and retains no post-settlement audit record. + ### Remote TUI evidence The native transport is not accepted until a live `codex --remote` test proves From bb359f9b7a93ee72d92c06358d3b95587fa6fdba Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Tue, 11 Aug 2026 23:32:57 +0200 Subject: [PATCH 10/15] Require shared Codex server configuration --- docs/vrs/01-ding/02-codex/spec.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/vrs/01-ding/02-codex/spec.md b/docs/vrs/01-ding/02-codex/spec.md index 6b5472ee..818495b3 100644 --- a/docs/vrs/01-ding/02-codex/spec.md +++ b/docs/vrs/01-ding/02-codex/spec.md @@ -25,6 +25,14 @@ socket. It opens and initializes its control connection before it starts the Codex TUI with `codex --remote unix://PATH`. The control client must be able to observe `thread/started` before the TUI can create a new thread. +The app server and remote TUI must load one effective authored configuration. +st2 forwards the app-server-supported global `config`, `enable`, `disable`, and +`strict-config` arguments to the server. It keeps TUI-only model, policy, +workspace, authentication, and prompt arguments on the TUI command. In +particular, an authored project-trust override must reach the server that loads +project-local config and hooks; forwarding it only to the TUI is a delivery +configuration error. + For a new session, st2 records the thread ID from `thread/started`. For a resumed session, st2 loads the recorded thread ID and calls `thread/resume` before it permits delivery. It does not infer ownership from `thread/list`, a From 52dbccf84620900bb49bce4e10e40abaed37658f Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Tue, 11 Aug 2026 23:59:07 +0200 Subject: [PATCH 11/15] Require bounded Codex wrapper diagnostics --- docs/vrs/01-ding/02-codex/spec.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/vrs/01-ding/02-codex/spec.md b/docs/vrs/01-ding/02-codex/spec.md index 818495b3..cc063866 100644 --- a/docs/vrs/01-ding/02-codex/spec.md +++ b/docs/vrs/01-ding/02-codex/spec.md @@ -133,6 +133,14 @@ Control transport close is a normal adapter stop. The adapter sends nothing after close. A restarted adapter must restore the exact thread binding and duplicate-control state before it attempts delivery. +The wrapper must keep a diagnostic trace for its current run. It creates the +trace only after it holds the exclusive runtime-owner lock. The trace records +startup stages and the full terminal error chain. It must not record authored +argument values, prompts, or message bodies. The next exclusive owner +truncates the file before it starts, so the trace has a fixed one-run retention +bound. A failed launch must leave enough trace data to distinguish +server-socket setup from TUI thread binding. + ### Post-settlement observability Version 1 is not a delivery audit log. Its accepted delivery state exists to From 29f6c07077ae8a9ff527644aef4e98974df9a329 Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Wed, 12 Aug 2026 00:28:29 +0200 Subject: [PATCH 12/15] Specify response-driven Codex resume --- docs/vrs/01-ding/02-codex/spec.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/vrs/01-ding/02-codex/spec.md b/docs/vrs/01-ding/02-codex/spec.md index cc063866..7f70d154 100644 --- a/docs/vrs/01-ding/02-codex/spec.md +++ b/docs/vrs/01-ding/02-codex/spec.md @@ -34,10 +34,14 @@ project-local config and hooks; forwarding it only to the TUI is a delivery configuration error. For a new session, st2 records the thread ID from `thread/started`. For a -resumed session, st2 loads the recorded thread ID and calls `thread/resume` -before it permits delivery. It does not infer ownership from `thread/list`, a -working directory, a process, or a PTY. Those surfaces do not identify which -TUI owns a thread. +resumed session, st2 starts the TUI with the recorded thread ID. Its initialized +control client then calls `thread/resume` for that exact ID and binds the new +runtime incarnation only from the successful response. It must not wait for a +new `thread/started` notification or an unchanged status notification on the +control connection. A resume error or a different returned thread ID leaves +the prior binding non-current and every message unread. st2 does not infer +ownership from `thread/list`, a working directory, a process, or a PTY. Those +surfaces do not identify which TUI owns a thread. The thread binding is persistent runtime state. It includes the exact agent runtime incarnation that owns it. st2 rejects a binding from a prior From f853e20565d4fc2a3fa3a56908b89df16a513b3f Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Wed, 12 Aug 2026 00:59:28 +0200 Subject: [PATCH 13/15] Specify TUI-first Codex resume --- docs/vrs/01-ding/02-codex/spec.md | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/vrs/01-ding/02-codex/spec.md b/docs/vrs/01-ding/02-codex/spec.md index 7f70d154..e4779a17 100644 --- a/docs/vrs/01-ding/02-codex/spec.md +++ b/docs/vrs/01-ding/02-codex/spec.md @@ -35,13 +35,18 @@ configuration error. For a new session, st2 records the thread ID from `thread/started`. For a resumed session, st2 starts the TUI with the recorded thread ID. Its initialized -control client then calls `thread/resume` for that exact ID and binds the new -runtime incarnation only from the successful response. It must not wait for a -new `thread/started` notification or an unchanged status notification on the -control connection. A resume error or a different returned thread ID leaves -the prior binding non-current and every message unread. st2 does not infer -ownership from `thread/list`, a working directory, a process, or a PTY. Those -surfaces do not identify which TUI owns a thread. +control client calls `thread/loaded/list` until the result contains that exact +ID. This typed result must arrive before the control client calls +`thread/resume`. A started TUI process or a connected control socket does not +prove that the TUI loaded the thread. + +After this observation, the control client calls `thread/resume` for the exact +ID. st2 binds the new runtime incarnation only from the successful response. It +must not wait for a new `thread/started` notification or an unchanged status +notification on the control connection. A timeout, resume error, or different +returned thread ID leaves the prior binding non-current and every message +unread. st2 does not infer ownership from `thread/list`, a working directory, a +process, or a PTY. Those surfaces do not identify which TUI owns a thread. The thread binding is persistent runtime state. It includes the exact agent runtime incarnation that owns it. st2 rejects a binding from a prior @@ -166,6 +171,11 @@ The native transport is not accepted until a live `codex --remote` test proves all of these results against the same app server and control client: - The normal TUI can start a new bound thread and resume that exact thread. +- On resume, `thread/loaded/list` contains the preserved thread before the + control client calls `thread/resume`. +- The resumed TUI consumes its authored initial prompt and runs its + `SessionStart` resume hook. A started process or a successful control resume + is not sufficient evidence. - An idle inbox message produces one typed `turn/start` user message and observable agent work. - A message during a regular active turn produces one typed `turn/steer` user From e6ecc84363a0cce22f661da5bfc9c9e3ce9ec472 Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Wed, 12 Aug 2026 01:26:29 +0200 Subject: [PATCH 14/15] Require readable resume timeout ordering --- docs/vrs/01-ding/02-codex/spec.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/vrs/01-ding/02-codex/spec.md b/docs/vrs/01-ding/02-codex/spec.md index e4779a17..0c67766e 100644 --- a/docs/vrs/01-ding/02-codex/spec.md +++ b/docs/vrs/01-ding/02-codex/spec.md @@ -38,7 +38,9 @@ resumed session, st2 starts the TUI with the recorded thread ID. Its initialized control client calls `thread/loaded/list` until the result contains that exact ID. This typed result must arrive before the control client calls `thread/resume`. A started TUI process or a connected control socket does not -prove that the TUI loaded the thread. +prove that the TUI loaded the thread. The loaded-thread wait must expire before +the outer thread-binding wait, so its specific provider error reaches the +wrapper trace. After this observation, the control client calls `thread/resume` for the exact ID. st2 binds the new runtime incarnation only from the successful response. It From 490827adf3e13b269b1f8dd508c4debdd7ada1a7 Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Wed, 12 Aug 2026 02:01:57 +0200 Subject: [PATCH 15/15] Specify transient hook trust projection --- docs/vrs/01-ding/02-codex/spec.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/vrs/01-ding/02-codex/spec.md b/docs/vrs/01-ding/02-codex/spec.md index 0c67766e..43985872 100644 --- a/docs/vrs/01-ding/02-codex/spec.md +++ b/docs/vrs/01-ding/02-codex/spec.md @@ -33,6 +33,25 @@ particular, an authored project-trust override must reach the server that loads project-local config and hooks; forwarding it only to the TUI is a delivery configuration error. +Codex 0.145.0 and 0.146.0 do not apply +`--dangerously-bypass-hook-trust` to the startup review of a persistent remote +resume. When that flag is authored for a resumed session, st2 must use a +bounded hook-trust preflight before it starts the TUI. The preflight starts the +same exact app-server binary with the same effective authored configuration. It +calls typed `hooks/list` for the controlled workspace and reads the exact hook +key, current hash, managed state, and trust state. It then stops only that owned +preflight process. + +The final app server receives a session-only `hooks.state` projection for each +hook that the provider reports as `untrusted` or `modified`. The projection +contains only the provider key and its exact current hash. It does not persist +hook trust or change user configuration. Hooks that are already `trusted` or +`managed` are not projected. A rejected request, a different working +directory, an unknown trust state, a missing typed field, or conflicting hashes +must stop launch before the TUI starts. The wrapper trace records the preflight +stages and projected hook count, but it must not record hook keys, commands, or +hashes. Without the authored bypass flag, st2 must not create this projection. + For a new session, st2 records the thread ID from `thread/started`. For a resumed session, st2 starts the TUI with the recorded thread ID. Its initialized control client calls `thread/loaded/list` until the result contains that exact @@ -178,6 +197,10 @@ all of these results against the same app server and control client: - The resumed TUI consumes its authored initial prompt and runs its `SessionStart` resume hook. A started process or a successful control resume is not sufficient evidence. +- For a resumed session with authored hook-trust bypass, a typed preflight + projects the exact current hook hashes for that invocation. The TUI does not + stop at hook review, the hooks run, and the user configuration remains + byte-identical. - An idle inbox message produces one typed `turn/start` user message and observable agent work. - A message during a regular active turn produces one typed `turn/steer` user