Run tasks on Codex beside Claude; drop turn caps and mid-turn snapshots

This commit is contained in:
Ian Gulliver
2026-10-15 14:55:58 -07:00
parent 17898b0191
commit 74707f6398
53 changed files with 1904 additions and 1295 deletions
+13 -8
View File
@@ -21,15 +21,20 @@ byre runs `codex` from `PATH`. On the owner's machine that is the CLI inside the
codex app-server -c check_for_update_on_startup=false
```
Run in the session's working directory, with `GIT_AUTHOR_*`/`GIT_COMMITTER_*` set to the commit identity; shell commands inherit them. NDJSON both ways on stdio. Messages are JSON-RPC 2.0 without the `"jsonrpc"` member: requests `{"id","method","params"}`, responses `{"id","result"}` or `{"id","error":{"code","message"}}`, notifications `{"method","params"}`. The server adds `emittedAtMs` to its notifications. The server also reads the user's `~/.codex/config.toml`, so its MCP servers, plugins and `notify` program apply to byre's sessions too.
Run in the session's working directory, with `GIT_AUTHOR_*`/`GIT_COMMITTER_*` set to the commit identity and anything in `Options.Env` (byre's agent manager adds the task's ports and `CODEX_HOME`, see `docs/operations.md`); shell commands inherit them. NDJSON both ways on stdio. Messages are JSON-RPC 2.0 without the `"jsonrpc"` member: requests `{"id","method","params"}`, responses `{"id","result"}` or `{"id","error":{"code","message"}}`, notifications `{"method","params"}`. The server adds `emittedAtMs` to its notifications. The server reads `config.toml` from `CODEX_HOME` (default `~/.codex`), so the MCP servers, plugins and `notify` program configured there apply to the session; `-c mcp_servers={}` doesn't remove them, a separate `CODEX_HOME` does.
## Session lifecycle
1. `initialize` with `clientInfo`, then the `initialized` notification.
2. `thread/start` (`cwd`, `model`, `approvalPolicy` `never`, `sandbox` `danger-full-access`, `developerInstructions` for byre's guidance), or `thread/resume` with `threadId` and the same settings. The server picks the thread id, returned as `thread.id`; resuming keeps it. `Session.ID` is the thread id.
3. `turn/start` with `threadId` and `input`, a list of `UserInput`: `{"type":"text","text":...}`, and for each image `{"type":"image","url":"data:<type>;base64,..."}` before the text. `model` on it switches the model for that turn and every later one (`Session.SetModel` holds the model for the next `Send`); `thread/read` reports the thread's current `model`. The response carries the new turn; `turn/started` follows.
4. `turn/interrupt` with `threadId` and the active `turnId` ends the turn: `turn/completed` arrives with `status` `interrupted` within milliseconds, a running command is killed, and the thread stays usable. `Session` tracks the active turn from `turn/started` and `turn/completed`.
5. `thread/compact/start` runs compaction as a turn of its own: `turn/started`, an item of type `contextCompaction`, `turn/completed`. The thread keeps its id and remembers the conversation in summary.
1. `initialize` with `clientInfo` and `capabilities.experimentalApi` true (dynamic tools need it: without it `thread/start` with `dynamicTools` fails with `thread/start.dynamicTools requires experimentalApi capability`), then the `initialized` notification.
2. `thread/start` (`cwd`, `model`, `approvalPolicy` `never`, `sandbox` `danger-full-access`, `developerInstructions` for byre's guidance, `dynamicTools` for `Options.MCPServers`), or `thread/resume` with `threadId` and the same settings but no tools, or `thread/fork` (`Options.ForkFrom` as `threadId`, `Options.ForkAt` as `lastTurnId`, the last turn the fork keeps; the turn must not be in progress) with the same settings. The server picks the thread id, returned as `thread.id`; resuming keeps it, forking makes a new one. `Session.ID` is the thread id. A thread's dynamic tools are stored with it, so resumed and forked threads keep them.
3. `turn/start` with `threadId` and `input`, a list of `UserInput`: `{"type":"text","text":...}`, and for each image `{"type":"image","url":"data:<type>;base64,..."}` before the text. With `Options.Reminder` set, every `turn/start` carries it as `additionalContext` `{"byre":{"kind":"application","value":...}}`. `model` on it switches the model for that turn and every later one (`Session.SetModel` holds the model for the next `Send`); `thread/read` reports the thread's current `model`. The response carries the new turn; `turn/started` follows.
4. `turn/steer` with `threadId`, `expectedTurnId` (the active turn) and `input` adds input to the running turn (`Session.Steer`); the model reads it at its next step without the turn ending.
5. `turn/interrupt` with `threadId` and the active `turnId` ends the turn: `turn/completed` arrives with `status` `interrupted` within milliseconds, a running command is killed, and the thread stays usable. `Session` tracks the active turn from `turn/started` and `turn/completed`.
6. `thread/compact/start` runs compaction as a turn of its own: `turn/started`, an item of type `contextCompaction`, `turn/completed`. The thread keeps its id and remembers the conversation in summary.
## Dynamic tools
Each `mcp.Server` in `Options.MCPServers` becomes one `dynamicTools` entry of type `namespace`, named after the server, holding its tools as `function`s with their names, descriptions and input schemas. Names and namespaces starting with `mcp__` are reserved (`dynamic tool name is reserved`), so byre's tools are `ask_human` and so on in the namespace `byre`; the model refers to them as `byre__ask_human`. A call arrives as the server request `item/tool/call` (`namespace`, `tool`, `arguments`, `callId`, `turnId`); `Session` runs that tool's `Handler` and answers `{"success":true,"contentItems":[{"type":"inputText","text":...}]}`, or `success` false with the error text. The call also shows as a `dynamicToolCall` item.
Closing stdin ends the process.
@@ -41,13 +46,13 @@ Closing stdin ends the process.
- `thread/tokenUsage/updated` carries `last` and `total` token counts and `modelContextWindow`; `account/rateLimits/updated` the account's rate-limit windows; `thread/status/changed`, `mcpServer/startupStatus/updated`, `account/updated` and `remoteControl/status/changed` also arrive.
- Lines that aren't a JSON object, don't parse, or carry a notification method outside `notificationParams` go to `Options.OnBadLine`; parsed notifications are still delivered as events. `Event.Decode` returns the notification's generated params type.
Server requests (approvals, user input, dynamic tool calls) shouldn't arrive with approvals off; if one does, command and file-change approvals are accepted and anything else gets a JSON-RPC error.
Besides `item/tool/call`, server requests (approvals, user input) shouldn't arrive with approvals off; if one does, command and file-change approvals are accepted and anything else gets a JSON-RPC error.
## Transcripts and contract tests
`Options.Transcript` records every line in both directions as `{"dir":"send"|"recv","msg":...}`, deltas included, with any `"email"` value replaced by `[redacted]`.
The tests in `internal/codex/session_internal_test.go` are scenarios (commit identity, text deltas, interrupt, image input, resume, model switch, compaction), each run two ways:
The tests in `internal/codex/session_internal_test.go` are scenarios (commit identity, text deltas, interrupt, image input, resume, model switch, compaction, tools across a resume, fork, steer), each run two ways:
- `TestLive` runs it against the real CLI with the models named by `testModel` and `switchModel`, and writes transcripts to `internal/codex/testdata/transcripts/` (gitignored).
- `TestContract` runs the same scenario against the committed transcripts in `internal/codex/testdata/contract/`. A replay server stands in for the CLI: it expects each recorded `send` line in turn (method and params equal, `cwd` aside) and writes each recorded `recv` line, so the client must send exactly what the recording shows and handle exactly what the server sent.