From 3efe9da0ef6d25fe6a7fefe05373894c79f0e638 Mon Sep 17 00:00:00 2001 From: Aditya kumar singh <143548997+Adityakk9031@users.noreply.github.com> Date: Wed, 23 Sep 2026 20:17:04 +0530 Subject: [PATCH 1/2] fix(store): synthesize session ID when empty in claude_code, codex, and pi --- src/harness/claude_code.rs | 24 +++++++++++++++++++++++- src/harness/codex.rs | 25 ++++++++++++++++++++++++- src/harness/pi.rs | 22 +++++++++++++++++++--- src/local.rs | 15 +++++++++++++-- tests/integration/claude_code.rs | 18 ++++++++++++++++++ tests/integration/codex.rs | 22 ++++++++++++++++++++++ tests/integration/pi.rs | 23 +++++++++++++++++++++++ 7 files changed, 142 insertions(+), 7 deletions(-) diff --git a/src/harness/claude_code.rs b/src/harness/claude_code.rs index 15417e7..841714a 100644 --- a/src/harness/claude_code.rs +++ b/src/harness/claude_code.rs @@ -482,12 +482,34 @@ impl Store for ClaudeStore { } fn save(&self, transcript: &Transcript) -> Result> { - let id = transcript.meta.id.clone(); + let id = if transcript.meta.id.is_empty() { + Uuid::new_v4().to_string() + } else { + transcript.meta.id.clone() + }; super::checked_id_component(ClaudeCode::NAME, &id)?; let cwd = transcript.meta.cwd.as_deref().unwrap_or_default(); let dir = self.root.join(encode_project_dir(cwd)); fs::create_dir_all(&dir)?; let path = dir.join(format!("{id}.jsonl")); + // If we synthesised a fresh id, stamp it into the transcript so the + // `sessionId` on every JSONL line matches the filename. Without this + // the file and its content would carry two different UUIDs and Claude + // Code would fail to resume the session. + let stamped; + let transcript = if transcript.meta.id == id { + transcript + } else { + stamped = Transcript::new( + { + let mut m = transcript.meta.clone(); + m.id.clone_from(&id); + m + }, + transcript.body.clone(), + ); + &stamped + }; fs::write(&path, ClaudeCode::to_text(transcript)?)?; Ok(Saved { id, diff --git a/src/harness/codex.rs b/src/harness/codex.rs index d2c5cc3..89829f9 100644 --- a/src/harness/codex.rs +++ b/src/harness/codex.rs @@ -26,6 +26,7 @@ use crate::common::{Block, ImageSource, Message, Meta, Role, Tool, ToolOutput, U use crate::error::{Error, Result}; use crate::harness::jsonl; use crate::transcript::{Codec, Common, Discovered, Harness, Saved, Store, TextCodec, Transcript}; +use uuid::Uuid; /// The Codex harness marker. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -770,7 +771,11 @@ impl Store for CodexStore { } fn save(&self, transcript: &Transcript) -> Result> { - let id = transcript.meta.id.clone(); + let id = if transcript.meta.id.is_empty() { + Uuid::new_v4().to_string() + } else { + transcript.meta.id.clone() + }; super::checked_id_component(Codex::NAME, &id)?; let t = &transcript.meta.timestamp; let dir = self @@ -781,6 +786,24 @@ impl Store for CodexStore { fs::create_dir_all(&dir)?; let compact = t.format("%Y-%m-%dT%H-%M-%S").to_string(); let path = dir.join(format!("rollout-{compact}-{id}.jsonl")); + // Stamp the resolved id into the transcript so the session_meta line + // inside the rollout carries the same id as the filename. Without this + // a synthesised UUID in the filename and an empty string in the file + // would prevent Codex from resuming the session. + let stamped; + let transcript = if transcript.meta.id == id { + transcript + } else { + stamped = Transcript::new( + { + let mut m = transcript.meta.clone(); + m.id.clone_from(&id); + m + }, + transcript.body.clone(), + ); + &stamped + }; fs::write(&path, Codex::to_text(transcript)?)?; Ok(Saved { id, diff --git a/src/harness/pi.rs b/src/harness/pi.rs index 5bc0bfc..90555b2 100644 --- a/src/harness/pi.rs +++ b/src/harness/pi.rs @@ -576,9 +576,14 @@ pub(crate) fn write_session( meta: &Meta, records: &[Record], ) -> Result> { - let id = meta.id.clone(); // Callers are the pi and campfire stores; either way the id is the - // transcript's own. + // transcript's own. If the transcript has no id yet, mint a fresh UUID + // so the session header and the filename are consistent. + let id = if meta.id.is_empty() { + Uuid::new_v4().to_string() + } else { + meta.id.clone() + }; super::checked_id_component("pi", &id)?; let cwd = meta.cwd.as_deref().unwrap_or_default(); let dir = sessions_dir.join(encode_cwd(cwd)); @@ -588,7 +593,18 @@ pub(crate) fn write_session( .to_rfc3339_opts(SecondsFormat::Millis, true) .replace([':', '.'], "-"); let path = dir.join(format!("{file_ts}_{id}.jsonl")); - fs::write(&path, jsonl::render(records)?)?; + // If we minted a fresh id, patch the session header in the records so the + // id field inside the file matches the filename. A mismatch would confuse + // pi's `--continue` lookup which keys on the in-file session id. + if meta.id == id { + fs::write(&path, jsonl::render(records)?)?; + } else { + let mut patched: Vec = records.to_vec(); + if let Some(Record::Session(header)) = patched.first_mut() { + header.id.clone_from(&id); + } + fs::write(&path, jsonl::render(&patched)?)?; + } Ok(Saved { id, reference: path, diff --git a/src/local.rs b/src/local.rs index d4fb85f..3bbea04 100644 --- a/src/local.rs +++ b/src/local.rs @@ -862,14 +862,25 @@ fn materialize_artifacts_for_claude_code( ) -> Result> { const MAX_ARTIFACT_BYTES: usize = 64 * 1024 * 1024; - crate::harness::checked_id_component(claude_code::ClaudeCode::NAME, &common.meta.id)?; + // Resolve the session id up front: if the transcript carries no id yet, + // mint a fresh UUID here so the artifact directory and the eventual + // session file both land under the same name. + let session_id = if common.meta.id.is_empty() { + uuid::Uuid::new_v4().to_string() + } else { + common.meta.id.clone() + }; + crate::harness::checked_id_component(claude_code::ClaudeCode::NAME, &session_id)?; let artifact_root = projects_root .join(claude_code::encode_project_dir( common.meta.cwd.as_deref().unwrap_or_default(), )) - .join(&common.meta.id) + .join(&session_id) .join("artifacts"); let mut prepared = common.clone(); + // Stamp the resolved id so the caller (ClaudeStore::save) and every + // downstream codec see the same session identity. + prepared.meta.id = session_id; for (message_index, message) in prepared.body.iter_mut().enumerate() { for (block_index, block) in message.content.iter_mut().enumerate() { let Block::Artifact { artifact } = block else { diff --git a/tests/integration/claude_code.rs b/tests/integration/claude_code.rs index ceb835a..b2cc089 100644 --- a/tests/integration/claude_code.rs +++ b/tests/integration/claude_code.rs @@ -91,6 +91,24 @@ fn store_round_trip_is_lossless_on_disk() { assert!(saved.reference.ends_with("sess-1.jsonl")); } +#[test] +fn store_save_empty_id_synthesizes_uuid() { + let dir = tempfile::tempdir().unwrap(); + let store = claude_code::ClaudeStore::new(dir.path()); + + let src = dir.path().join("orig.jsonl"); + std::fs::write(&src, sample_jsonl()).unwrap(); + + let mut loaded = store.load(&src).unwrap(); + loaded.meta.id = String::new(); + + let saved = store.save(&loaded).unwrap(); + assert!(!saved.id.is_empty()); + assert!(saved.reference.exists()); + assert!(saved.reference.ends_with(format!("{}.jsonl", saved.id))); + uuid::Uuid::parse_str(&saved.id).unwrap(); +} + #[test] fn windows_cwd_encodes_the_project_dir() { let dir = tempfile::tempdir().unwrap(); diff --git a/tests/integration/codex.rs b/tests/integration/codex.rs index 9875507..3dcfa5b 100644 --- a/tests/integration/codex.rs +++ b/tests/integration/codex.rs @@ -276,3 +276,25 @@ fn codec_fixpoint_through_common_loses_nothing() { let back = codex::Codex::to_common(&native).unwrap(); assert_eq!(common, back); } + +#[test] +fn store_save_empty_id_synthesizes_uuid() { + let dir = tempfile::tempdir().unwrap(); + let store = codex::CodexStore::new(dir.path()); + let src = dir.path().join("rollout-x.jsonl"); + std::fs::write(&src, exercise_rollout()).unwrap(); + + let mut loaded = store.load(&src).unwrap(); + loaded.meta.id = String::new(); + + let saved = store.save(&loaded).unwrap(); + assert!(!saved.id.is_empty()); + assert!(saved.reference.exists()); + assert!( + saved + .reference + .to_string_lossy() + .ends_with(&format!("{}.jsonl", saved.id)) + ); + uuid::Uuid::parse_str(&saved.id).unwrap(); +} diff --git a/tests/integration/pi.rs b/tests/integration/pi.rs index 9108f02..c2a8c4f 100644 --- a/tests/integration/pi.rs +++ b/tests/integration/pi.rs @@ -67,6 +67,29 @@ fn store_round_trip_is_lossless_on_disk() { assert!(saved.reference.to_string_lossy().contains("--repo--")); } +#[test] +fn store_save_empty_id_synthesizes_uuid() { + let dir = tempfile::tempdir().unwrap(); + let store = pi::PiStore::new(dir.path()); + + let src = dir.path().join("orig.jsonl"); + std::fs::write(&src, sample_jsonl()).unwrap(); + + let mut loaded = store.load(&src).unwrap(); + loaded.meta.id = String::new(); + + let saved = store.save(&loaded).unwrap(); + assert!(!saved.id.is_empty()); + assert!(saved.reference.exists()); + assert!( + saved + .reference + .to_string_lossy() + .ends_with(&format!("{}.jsonl", saved.id)) + ); + uuid::Uuid::parse_str(&saved.id).unwrap(); +} + #[test] fn discover_extracts_metadata() { let dir = tempfile::tempdir().unwrap(); From 8eab06bc1dbbad47fac3cd105c7c347944f836bc Mon Sep 17 00:00:00 2001 From: Aditya kumar singh <143548997+Adityakk9031@users.noreply.github.com> Date: Sat, 26 Sep 2026 13:19:00 +0530 Subject: [PATCH 2/2] fix(store): stamp synthesized session ID into native records for Claude and Codex --- src/harness/claude_code.rs | 14 +++++++++++++- src/harness/codex.rs | 28 +++++++++++++++++++++++++++- tests/integration/claude_code.rs | 3 +++ tests/integration/codex.rs | 3 +++ tests/integration/pi.rs | 3 +++ 5 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/harness/claude_code.rs b/src/harness/claude_code.rs index 841714a..72cf8c5 100644 --- a/src/harness/claude_code.rs +++ b/src/harness/claude_code.rs @@ -500,13 +500,25 @@ impl Store for ClaudeStore { let transcript = if transcript.meta.id == id { transcript } else { + let mut body = transcript.body.clone(); + for record in &mut body { + match record { + Record::User(entry) | Record::Assistant(entry) => { + entry.session_id = Some(id.clone()); + } + Record::Other(Value::Object(map)) if map.contains_key("sessionId") => { + map.insert("sessionId".to_string(), Value::String(id.clone())); + } + _ => {} + } + } stamped = Transcript::new( { let mut m = transcript.meta.clone(); m.id.clone_from(&id); m }, - transcript.body.clone(), + body, ); &stamped }; diff --git a/src/harness/codex.rs b/src/harness/codex.rs index 89829f9..d31b862 100644 --- a/src/harness/codex.rs +++ b/src/harness/codex.rs @@ -794,13 +794,39 @@ impl Store for CodexStore { let transcript = if transcript.meta.id == id { transcript } else { + let mut body = transcript.body.clone(); + let mut found_session_meta = false; + for line in &mut body { + if line.kind == "session_meta" + && let Value::Object(map) = &mut line.payload + { + map.insert("id".to_string(), Value::String(id.clone())); + found_session_meta = true; + } + } + if !found_session_meta { + let payload = json!({ + "id": id, + "timestamp": transcript.meta.timestamp.to_rfc3339_opts(SecondsFormat::Millis, true), + "cwd": transcript.meta.cwd.clone().unwrap_or_default(), + "originator": "codex_cli_rs", + "cli_version": transcript.meta.cli_version.clone().unwrap_or_default(), + "source": "cli", + "model_provider": "openai", + "base_instructions": Value::Null, + }); + body.insert( + 0, + meta_line(&transcript.meta.timestamp, "session_meta", payload), + ); + } stamped = Transcript::new( { let mut m = transcript.meta.clone(); m.id.clone_from(&id); m }, - transcript.body.clone(), + body, ); &stamped }; diff --git a/tests/integration/claude_code.rs b/tests/integration/claude_code.rs index b2cc089..c26f68b 100644 --- a/tests/integration/claude_code.rs +++ b/tests/integration/claude_code.rs @@ -107,6 +107,9 @@ fn store_save_empty_id_synthesizes_uuid() { assert!(saved.reference.exists()); assert!(saved.reference.ends_with(format!("{}.jsonl", saved.id))); uuid::Uuid::parse_str(&saved.id).unwrap(); + + let reloaded = store.load(&saved.reference).unwrap(); + assert_eq!(reloaded.meta.id, saved.id); } #[test] diff --git a/tests/integration/codex.rs b/tests/integration/codex.rs index 3dcfa5b..40d39af 100644 --- a/tests/integration/codex.rs +++ b/tests/integration/codex.rs @@ -297,4 +297,7 @@ fn store_save_empty_id_synthesizes_uuid() { .ends_with(&format!("{}.jsonl", saved.id)) ); uuid::Uuid::parse_str(&saved.id).unwrap(); + + let reloaded = store.load(&saved.reference).unwrap(); + assert_eq!(reloaded.meta.id, saved.id); } diff --git a/tests/integration/pi.rs b/tests/integration/pi.rs index c2a8c4f..8b287ef 100644 --- a/tests/integration/pi.rs +++ b/tests/integration/pi.rs @@ -88,6 +88,9 @@ fn store_save_empty_id_synthesizes_uuid() { .ends_with(&format!("{}.jsonl", saved.id)) ); uuid::Uuid::parse_str(&saved.id).unwrap(); + + let reloaded = store.load(&saved.reference).unwrap(); + assert_eq!(reloaded.meta.id, saved.id); } #[test]