feat(agents): project Codex's notifications into the transcript

Merged
CH
5d ago
+1238-149

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.ts turns Codex's notifications into P09's harness-neutral moments. The route picks the reader by conversations.runtime. The runner's own events and tool-answer settling moved to a shared runner-moments.ts, so Claude Code's reader is unchanged in behavior. A reader now gets a small NativeHeld view: the held calls, the recorded session, and the event's sequence.

  • What's mapped:

    • Words: item/agentMessage/delta is drafted by sentence. An agentMessage item is a step, told whole when it completes.
    • Tool calls: item/started and item/completed start and settle commandExecution, fileChange, mcpToolCall, webSearch and collabAgentToolCall (subagents).
    • Todo list: turn/plan/updated becomes a todoList call. App-server has no todo item.
    • Reasoning: a reasoning summary becomes a reasoning part.
    • Turn end: turn/completed ends a model turn, and the runner's END ends the Luke turn, as on Claude Code.
    • Meter: thread/tokenUsage/updated feeds it.
    • Compaction: contextCompaction and thread/compacted set compacted_at.
    • Session: a main thread/started records the thread id into native_session.
  • 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_detail carries Codex's codexErrorInfo word plus any OpenAI subscription_sharing_* / chatpass_v2_* code in the error, following P09's rate-limit rule. That covers subscription_sharing_usage_limit_exceeded, subscription_sharing_user_not_eligible, and auth failures (unauthorized, subscription_sharing_invalid_user). Statuses stay with P25.

  • Stored names: CODEX_TOOL in @luke/features/agent-tools is registered in CODER_TOOL_SET. An mcpToolCall to the luke server 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 clientUserMessageId and ignored the line's uuid, so nothing tied a turn to Luke's line. The runner now:

    • uses the line's uuid as clientUserMessageId when a message goes alone;
    • posts luke_runner_accepted { uuid } per line once turn/start or turn/steer is 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.

  • Unchanged from P09: a replayed batch is a no-op, a retired bearer writes nothing, and END calls sandboxAtRunnerEnd.

Tests

  • runner-events-app.test.ts gains 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. The sleep 600 turn 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.
  • The existing Claude Code tests pass unchanged.
  • codex.test.ts (runner) checks that ACCEPTED is posted per line under Luke's id, that clientUserMessageId equals 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


Devin Review