feat(agents): project Codex's notifications into the transcript
P22 of the native-harness rollout. This extends P09's events route and projection so a codex conversation's app-server notifications write into the same store writer. It only writes; rendering is P23, routing P24, and statuses and labels P25.
What changed
-
Reader:
native-events/codex-moments.tsturns Codex's notifications into P09's harness-neutral moments. The route picks the reader byconversations.runtime. The runner's own events and tool-answer settling moved to a sharedrunner-moments.ts, so Claude Code's reader is unchanged in behavior. A reader now gets a smallNativeHeldview: the held calls, the recorded session, and the event's sequence. -
What's mapped:
- Words:
item/agentMessage/deltais drafted by sentence. AnagentMessageitem is a step, told whole when it completes. - Tool calls:
item/startedanditem/completedstart and settlecommandExecution,fileChange,mcpToolCall,webSearchandcollabAgentToolCall(subagents). - Todo list:
turn/plan/updatedbecomes atodoListcall. App-server has no todo item. - Reasoning: a
reasoningsummary becomes a reasoning part. - Turn end:
turn/completedends a model turn, and the runner's END ends the Luke turn, as on Claude Code. - Meter:
thread/tokenUsage/updatedfeeds it. - Compaction:
contextCompactionandthread/compactedsetcompacted_at. - Session: a main
thread/startedrecords the thread id intonative_session.
- Words:
-
Subagents: notifications on any thread other than the session's are skipped, and their subagent call stands for them.
-
Meter by growth: Codex reports the thread's running total, so the projection keeps the last metered total and gives the turn only the growth. A resumed thread repeats its total, and that repeat meters nothing.
-
Plan failures: a failed turn's
failure_detailcarries Codex'scodexErrorInfoword plus any OpenAIsubscription_sharing_*/chatpass_v2_*code in the error, following P09's rate-limit rule. That coverssubscription_sharing_usage_limit_exceeded,subscription_sharing_user_not_eligible, and auth failures (unauthorized,subscription_sharing_invalid_user). Statuses stay with P25. -
Stored names:
CODEX_TOOLin@luke/features/agent-toolsis registered inCODER_TOOL_SET. AnmcpToolCallto thelukeserver is stored under the Luke tool's own name (read_inbox), its answer is read as that tool's JSON, and picture blocks are left out of it. -
Runner change (agreed with the orchestrator): P20's Codex runner minted a fresh
clientUserMessageIdand ignored the line'suuid, so nothing tied a turn to Luke's line. The runner now:- uses the line's
uuidasclientUserMessageIdwhen a message goes alone; - posts
luke_runner_accepted { uuid }per line onceturn/startorturn/steeris answered; - posts one per line for a merged next turn, each line once.
The Codex runner's acceptance now matches Claude Code's, and P09's projection takes awaiting lines on ACCEPTED, unchanged.
- uses the line's
-
Unchanged from P09: a replayed batch is a no-op, a retired bearer writes nothing, and END calls
sandboxAtRunnerEnd.
Tests
runner-events-app.test.tsgains a Codex block:- P16's hermetic recording (
tools/codex-probe/fixtures/hermetic/app-server.jsonl), cut into the three launches P20's runner makes of it, produces three turns. The workflow turn is settled, with its steered line joined to it. Thesleep 600turn is cancelled. The resumed turn is settled and its replayed total meters nothing. Each turn has its expected parts, usage, and recorded thread id. - A replayed or overlapping batch changes nothing, and END is told once.
- A retired bearer writes nothing.
- A synthetic turn covers a Luke MCP tool's name with its picture left out, reasoning, the todo list, a subagent thread folding under its call, compaction, and a usage-limit failure's
failure_detail.
- P16's hermetic recording (
- The existing Claude Code tests pass unchanged.
codex.test.ts(runner) checks that ACCEPTED is posted per line under Luke's id, thatclientUserMessageIdequals it, and that a refused steer's line is accepted only when the next turn takes it.
Checks
The affected tests and typechecks pass locally. The local pnpm check:affected run was starved for memory in the sandbox: the service typecheck was OOM-killed, and one unrelated suite, automations-procedures, hit a hook timeout. Both pass when run alone, and CI is the gate. The developer waived Preview runs for this rollout.
Made with Luke