Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 221 additions & 3 deletions docs/vrs/01-ding/02-codex/spec.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,231 @@
# 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.

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.

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
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. 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
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
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 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.

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 |
| 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 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
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 = <the exact clientUserMessageId>
threadId = <the bound thread ID>
```

`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 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 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 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
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
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
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.
- 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
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 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

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,
Expand Down
81 changes: 62 additions & 19 deletions docs/vrs/01-ding/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -41,21 +41,43 @@ 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 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

- **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
Expand Down Expand Up @@ -101,6 +123,27 @@ is in [`spec.md`](./spec.md).
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
Expand Down
Loading
Loading