5.0 KiB
REMOTE-1504: Save and Upload Codex Conversation Transcript
Context
Cloud agent runs using the Codex harness already upload block snapshots on each save, but the session transcript (the JSONL rollout Codex writes to disk) was not being captured. Claude Code already does this — claude_code.rs calls upload_transcript alongside upload_current_block_snapshot via futures::try_join!, reading the session JSONL from ~/.claude/projects/…/<uuid>.jsonl. Codex stores its rollouts differently: ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl, so transcript capture needs its own envelope format and file-discovery logic.
Relevant files
app/src/ai/agent_sdk/driver/harness/codex.rs—CodexHarnessRunnerimpl, owns the per-run state andsave_conversationapp/src/ai/agent_sdk/driver/harness/claude_code.rs (504-528)— Claude'supload_transcript, the pattern being mirroredapp/src/ai/agent_sdk/driver/harness/claude_transcript.rs—ClaudeTranscriptEnvelope,read_envelope,read_jsonlapp/src/terminal/cli_agent_sessions/mod.rs—CLIAgentSessionsModel, singleton that tracks CLI agent session context includingsession_idapp/src/ai/agent_sdk/driver/harness/mod.rs—HarnessRunnertrait,upload_current_block_snapshot,handle_session_update
Proposed changes
New module: codex_transcript.rs
Parallel to claude_transcript.rs. Contains:
CodexTranscriptEnvelope— on-wire JSON shape:{ cwd, session_id, codex_version?, entries }. Simpler than Claude's envelope (no subagents/todos).entriesis the parsed JSONL content.CodexSessionMetadata—{ cwd, codex_version }extracted from the first JSONL line (SessionMeta). Cached viaOnceLockon the runner so subsequent saves skip reparsing.codex_sessions_root()— resolves$CODEX_HOME/sessionsor~/.codex/sessions.find_session_file(sessions_root, session_id)— walksYYYY/MM/DD/dirs looking forrollout-*-<uuid>.jsonl. ReturnsOk(None)when root doesn't exist or no match found. The walk is unavoidable since Codex names files with timestamps we don't control; acceptable cost on cloud agents where the sessions dir is small. The path is cached on the runner after first discovery.parse_session_meta(first_entry)— pullscwdandcli_versionfrom the first JSONL entry'spayloadobject. Constant for session lifetime so callers cache the result.
Reuses read_jsonl from claude_transcript for the actual JSONL parsing.
Changes to CodexHarnessRunner (codex.rs)
Three OnceLock fields added for lazy, set-once caching:
session_id: OnceLock<Uuid>— captured fromCLIAgentSessionsModelwhen hooks emitSessionStarttranscript_path: OnceLock<PathBuf>— resolved byfind_session_fileon first save, cached thereafter
This caching pattern differs from Claude Code, which re-reads the config dir every save. Done here for consistency with the immutable-once-set nature of Codex's SessionMeta line, and because the YYYY/MM/DD dir walk is more expensive than Claude's direct path lookup.
handle_session_update — new override. Reads session ID from CLIAgentSessionsModel (the singleton that receives events from the Codex hooks plugin). Parses the string into a Uuid and stores it in the OnceLock. No-ops once set. The session ID is needed to find the rollout file.
save_conversation — now runs upload_current_block_snapshot and upload_transcript concurrently via futures::try_join!, matching Claude's pattern. upload_transcript is a standalone async fn that:
- Returns early if session ID or transcript path aren't available yet (early periodic saves before hooks fire)
- Reads + parses JSONL in
spawn_blocking - Uses cached metadata or parses it from the first entry
- Builds
CodexTranscriptEnvelope, serializes, uploads viaget_transcript_upload_target+upload_to_target - Returns newly-parsed metadata (if any) so the caller can cache it
Design note: dir walk vs timestamp-based path
Codex filenames embed rollout-<ts>-<uuid>.jsonl. An alternative to the walk would be computing the expected YYYY/MM/DD from the session start time. Rejected because timezone/midnight-boundary bugs make it fragile — a session starting at 23:59 local might land in tomorrow's dir depending on Codex's clock handling. The walk is safe and runs once per session.
Testing and validation
codex_transcript_tests.rs— unit tests covering:codex_sessions_roothonors$CODEX_HOMEenv var overridefind_session_filewalks a syntheticYYYY/MM/DDtree and matches the right UUIDfind_session_filereturnsNonefor non-matching UUIDfind_session_filereturnsNonewhen root is missingread_enveloperound-trip: writes a synthetic rollout withSessionMeta+ event lines, recoverscwd,codex_version, correct entry countread_envelopereturnsNonefor missing session
- Manual: run a cloud agent with
--harness codex, verify transcript appears in GCS after save