feat(agents): the events route and projection into the transcript store
P09 of the plan "Bringing Codex or Claude Code Max plans into Luke". Builds on P06 (#4236, merged), whose bearer lookup this route reuses, and on P08 (#4255, merged).
What it does
Adds POST /api/agents/runner-events. A native harness's runner (@luke/agent-runner) posts its numbered event batches there, and the route writes them into the existing store writer the way eve's relay does.
- Auth: the runner's bearer, hashed and looked up against
conversations.runner_token_hashthrough P06'sadmitRunnerBearer. A wrong, missing or retired bearer gets a 401 before the body is read. - Pluggable input:
claude-code-moments.tsreads SDK messages into harness-neutral moments (native-moments.ts).native-projection.ts, a pure fold, turns moments intoBrainRunEvents plus a few other writes. P22 adds a Codex reader of the sameNativeReadershape; it does not need to touch the projection. - What it projects:
- Turns: one turn per launch. The turn opens on the runner's
luke_runner_accepted,system/init, or the first output. - Steps and text: each model call is a step (keyed by
message.idwhere present). Text is drafted by finished sentence fromstream_eventdeltas and told whole when the step closes. - Tool calls and reasoning: tool calls are started and settled, and
thinkingblocks become reasoning. - Turn end: a
luke_runner_endends the turn.turn_endedsettles it (failed if the lastresulterrored),stoppedcancels it, andfailed/input_closedfail it with a detail.
- Turns: one turn per launch. The turn opens on the runner's
- Special messages:
resultusage is summed onto the turn and written withTURN_ENDED, priced on the agent's model. That is today's meter:inputTokenscounts cache reads and writes, asmodel-catalog.tsreads it.compact_boundarygoes torecordCodingAgentCompaction(compacted_at).rate_limit_eventrejectedis kept, and a failed turn carries it infailure_detail(rate_limit_rejected five_hour resets …).limit_reachedis P15's.- The runner's WAITING event (P05's Stop hook) leaves the turn running, so the agent reads as waiting, not ended.
system/init'ssession_idgoes toconversations.native_session.
- Idempotence: new columns
conversations.native_sequenceandnative_projection(one additive migration). Both are readFOR UPDATEand saved in the same transaction as every write (native-projection-store.ts). Events at or below the saved sequence are skipped, so a duplicate or overlapping POST is a no-op.readNativeSequence(target)lets P11 fill the next launch'sfirstSequence. - Tool names:
CLAUDE_CODE_TOOLin@luke/features/agent-toolsholds Bash, Read, Edit, Write, NotebookEdit, Glob, Grep, WebFetch, WebSearch, TodoWrite, Agent and Skill, plusScheduleWakeupandMonitor(the plan's waiting tools, which P00's stream uses). These are registered inCODER_TOOL_SET.mcp__luke__xis stored asx, and its answer is parsed as the tool's JSON. Unregistered names are skipped with a log line. - Subagents: messages with
parent_tool_use_idfold under theirAgentcall. Only the call and its answer are stored. - Awaiting lines:
luke_runner_accepted { uuid }(P08's definition in@luke/agent-runner/protocol) takes the awaiting row whose client id is that uuid into the turn. The writer'stakeAwaitingLinegains this client-id form; the text form eve uses is unchanged. - Launch end:
runnerEventsApp({ ended })is a hook called once per newly applied END, after commit. The route wires it to P08'srunnerEndedthroughsandboxAtRunnerEnd, which providesFetchHttpClient, skips a conversation with no session, and logs a failed stop by its tag rather than failing the POST. The sandbox then stops at its timeout or the Terminal-lease sweep.runnerEndeditself is covered by P08's tests.
Out of scope: rendering (P10), launching (P08), the MCP server (P06), Codex (P22).
Tests
runner-events-app.test.ts(real dialect), driving P00's hermetic stream as the runner posts it:- the stored transcript, part by part
- the line taken into the turn
- WAITING keeping the turn running, with sentence drafting midway
- usage on the turn, and
native_sessionrecorded - a whole or overlapping replay changing nothing
- wrong, missing or retired bearers refused
- subagent folding, and stop → cancelled with
endedtold once - compaction, and the rate-limit failure detail
native-projection-store.test.ts: save and read back, rollback on failure, account isolation, cleared conversations.- Old transcripts read unchanged: the writer's text-form take and every existing transcript or tool-set test pass as before. The tool set only gains names.
Run locally: the new and P06 tests, tsc for service, database, web and agent-runner, oxlint, knip, discarded-effect, scripts/repository-checks.sh, drizzle-kit check, and the web rewrite tests. pnpm check:affected ran out of memory in the sandbox (parallel tsc), so I ran its pieces one by one; CI runs the full set.
The developer waived WORKFLOW.md's per-PR Preview/Mac runs for this rollout. No runtime edge or AGENTS.md change was needed.
Made with Luke