Use native Codex steering and transcript deletion

This commit is contained in:
iris committed 2026-09-09 12:19:11 -04:00
1 parent 00538cc19b
commit 8c88a7e991
12 files changed
+1004 -390

No files matched your search

+24 -18
View File
@@ -29,7 +29,7 @@ Android app (Compose)
backend (Rust/Axum, desktop)
├─ SessionManager ── Session ── Driver (trait)
│ ├─ ClaudeDriver (claude stream-json over stdio)
│ ├─ CodexDriver (codex exec --json, one process per turn)
│ ├─ CodexDriver (persistent codex app-server JSONL)
│ ├─ LlamaDriver (llama-server over HTTP)
│ └─ EchoDriver (the test rig)
│ each driver's process is spawned through a Transport,
@@ -193,25 +193,24 @@ is the rule behind the import refusal, the single `ClaudeDriver::launch`
entry point, and the `Exited` correction below; two CLIs on one session file
duplicate the conversation into it and bill the second for re-reading it all.
### Codex driver specifics (2026-09-07)
### Codex driver specifics (2026-09-09)
Codex uses `codex exec --json`, whose stdout is JSONL: `thread.started`, turn
boundaries, item start/completion records, and the final token usage. The
driver translates those records into the same events as every other session
and persists the reported thread id. Later turns run `codex exec resume <id>
--json`; model, effort and attachments remain launch arguments owned by the
driver rather than branches in routes or screens.
Codex uses one persistent `codex app-server --stdio` per session. The original
2026-09-07 implementation used one `codex exec --json` process per turn, but
that surface cannot steer: a message typed during work was held until the turn
ended, and Pause killed the whole process before starting another resume. The
app-server protocol provides the operations the interface actually promises:
`turn/steer` injects a message into the active turn and `turn/interrupt` stops
that turn while leaving the conversation process alive.
One `exec` process is one turn and exits normally at its end. The session
therefore owns a sequence of child processes rather than one permanently idle
child: a clean exit after `turn.completed` means `idle`, while an exit before a
turn boundary is an error. A process in flight still uses `process.json` plus
detached stdout/stderr logs, so it survives and is adopted across a backend
restart exactly like the long-lived CLI. Messages received during a turn are
persisted and start later turns in order. The JSON exec surface has no stdin
steering or interactive approval protocol, so it cannot inject a message at a
tool boundary or answer a question inside the same process; those limits are
reported rather than guessed around.
The process's stdin is a fifo and its output is a detached log, with protocol
state persisted beside the thread id. It therefore survives and is adopted
across a backend restart like the Claude CLI. A steer is sent immediately and
is announced where Codex emits its user-message item; if Codex says the active
turn is not steerable, the message remains queued and starts the next turn
instead of being lost. An interrupt requested while a turn is still starting
is applied once Codex supplies that turn's id, so it cannot leak forward and
hide a later failure.
Codex subscription limits come from the CLI's `account/rateLimits/read`
app-server request on the machine whose setup runs Codex. This keeps login and
@@ -810,6 +809,13 @@ directory under `$XDG_DATA_HOME/ai-app/sessions/` (transcript, attachments,
produced images, process record), owner-only. Deleting a session is the
complete path out of everything spawning one created.
Claude Code and Codex also keep their own durable transcript. The delete
dialog names that owner and can remove its copy too: Claude files are resolved
under `~/.claude/projects`, while a Codex thread id resolves only the matching
rollout under `~/.codex/sessions`. The provider-owned copy is deleted first, so
a remote-machine failure leaves the app session intact rather than reporting a
half-delete as success.
**Every request body refuses fields it does not know**
(`serde(deny_unknown_fields)`). A caller that misspells `permissionMode` got
a 200 and a session in the default mode, which is indistinguishable from