Skip to content
Closed
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
146 changes: 143 additions & 3 deletions docs/vrs/01-ding/02-codex/spec.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,153 @@
# 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 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 |
| --- | --- | --- |
| 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.

### 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,
Expand Down
Loading