From 9ba9e250bc89c7fc9e5dbbb99cdee3aa09391c8e Mon Sep 17 00:00:00 2001 From: Yang Liu Date: Sun, 31 May 2026 13:42:30 +1200 Subject: [PATCH] fix(parser): rename CollabSpawn.new_thread_id to new_session_id for Codex v0.131.0 Codex v0.131.0 (PR #22268) renamed the collab_agent_spawn_end hook event field new_thread_id to new_session_id. While the parser already reads both field names for backward compat, the output format still exposed CollabSpawn.new_thread_id to API consumers even for v0.131.0+ data where the value is actually a session ID. This commit completes the fix by: - Renaming CollabSpawn.new_thread_id to new_session_id in the Rust struct and TypeScript interface so the serialized JSON key matches the Codex API - Updating field read order: try new_session_id first, fall back to new_thread_id for pre-v0.131.0 compatibility - Updating all callers in session.rs (spawned_worker_ids collection and embed_worker_sessions subagent stitching) - Adding end-to-end session-level test: parse_session stitches a worker session using new_session_id from a v0.131.0 collab_agent_spawn_end event Fixes #83 --- shared/types.ts | 2 +- src-tauri/src/parser/session.rs | 57 +++++++++++++++++++++++++++++++-- src-tauri/src/parser/turn.rs | 18 +++++------ 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/shared/types.ts b/shared/types.ts index 9efae78..9b15482 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -23,7 +23,7 @@ export interface AgentMessage { export interface CollabSpawn { call_id: string; - new_thread_id: string; + new_session_id: string; agent_nickname: string; agent_role: string; model?: string | null; diff --git a/src-tauri/src/parser/session.rs b/src-tauri/src/parser/session.rs index 9b15c57..5389dad 100644 --- a/src-tauri/src/parser/session.rs +++ b/src-tauri/src/parser/session.rs @@ -111,7 +111,7 @@ fn parse_session_inner( // Collect spawned_worker_ids from all turns let spawned_worker_ids: Vec = turns .iter() - .flat_map(|t| t.collab_spawns.iter().map(|s| s.new_thread_id.clone())) + .flat_map(|t| t.collab_spawns.iter().map(|s| s.new_session_id.clone())) .collect(); // Determine total tokens from last turn's token info @@ -178,7 +178,7 @@ fn embed_worker_sessions( continue; }; - let Some(worker_path) = find_session_file_by_id(parent_path, &spawn.new_thread_id) + let Some(worker_path) = find_session_file_by_id(parent_path, &spawn.new_session_id) else { continue; }; @@ -470,7 +470,7 @@ mod tests { assert_eq!(session.spawned_worker_ids, vec!["worker-session"]); assert_eq!( - session.turns[0].collab_spawns[0].new_thread_id, + session.turns[0].collab_spawns[0].new_session_id, "worker-session" ); @@ -767,4 +767,55 @@ mod tests { assert_eq!(session.turns.len(), 1); assert!(!session.is_ongoing); } + + // Codex v0.131.0 (PR #22268): collab_agent_spawn_end uses new_session_id instead of + // new_thread_id. Verify end-to-end: parse_session reads new_session_id, populates + // spawned_worker_ids, and embed_worker_sessions correctly stitches the worker session. + #[test] + fn v0131_parse_session_stitches_worker_via_new_session_id() { + let tmp = tempdir().unwrap(); + let parent_path = tmp + .path() + .join("rollout-2026-05-18T10-03-00-parent-v131.jsonl"); + let worker_path = tmp + .path() + .join("rollout-2026-05-18T10-03-09-worker-v131.jsonl"); + std::fs::write( + &parent_path, + [ + r#"{"timestamp":"2026-05-18T10:03:00Z","type":"session_meta","payload":{"id":"parent-v131","timestamp":"2026-05-18T10:03:00Z","cli_version":"0.131.0"}}"#, + r#"{"timestamp":"2026-05-18T10:03:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}"#, + r#"{"timestamp":"2026-05-18T10:03:02Z","type":"response_item","payload":{"type":"function_call","name":"spawn_agent","arguments":"{\"agent_type\":\"worker\",\"message\":\"Gather data\"}","call_id":"call-spawn-v131"}}"#, + r#"{"timestamp":"2026-05-18T10:03:03Z","type":"event_msg","payload":{"type":"collab_agent_spawn_end","call_id":"call-spawn-v131","sender_session_id":"parent-v131","new_session_id":"worker-v131","new_agent_nickname":"Hypatia","new_agent_role":"worker","prompt":"Gather data","status":"pending_init"}}"#, + r#"{"timestamp":"2026-05-18T10:03:04Z","type":"response_item","payload":{"type":"function_call_output","call_id":"call-spawn-v131","output":"{\"agent_id\":\"worker-v131\",\"nickname\":\"Hypatia\"}"}}"#, + r#"{"timestamp":"2026-05-18T10:03:05Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1747562585.0}}"#, + ] + .join("\n"), + ) + .unwrap(); + std::fs::write( + &worker_path, + [ + r#"{"timestamp":"2026-05-18T10:03:09Z","type":"session_meta","payload":{"id":"worker-v131","timestamp":"2026-05-18T10:03:09Z","cli_version":"0.131.0","cwd":"/tmp/worker"}}"#, + r#"{"timestamp":"2026-05-18T10:03:10Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-worker"}}"#, + r#"{"timestamp":"2026-05-18T10:03:11Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-worker","completed_at":1747562591.0}}"#, + ] + .join("\n"), + ) + .unwrap(); + + let session = parse_session(&parent_path).unwrap(); + assert_eq!(session.id, "parent-v131"); + assert_eq!(session.spawned_worker_ids, vec!["worker-v131"]); + assert_eq!( + session.turns[0].collab_spawns[0].new_session_id, + "worker-v131" + ); + assert_eq!(session.turns[0].collab_spawns[0].agent_nickname, "Hypatia"); + let worker = session.turns[0].tool_calls[0] + .worker_session + .as_ref() + .expect("spawn_agent tool call should embed worker session"); + assert_eq!(worker.id, "worker-v131"); + } } diff --git a/src-tauri/src/parser/turn.rs b/src-tauri/src/parser/turn.rs index 18747e7..1d8e915 100644 --- a/src-tauri/src/parser/turn.rs +++ b/src-tauri/src/parser/turn.rs @@ -38,7 +38,7 @@ pub struct TokenInfo { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CollabSpawn { pub call_id: String, - pub new_thread_id: String, + pub new_session_id: String, pub agent_nickname: String, pub agent_role: String, pub model: Option, @@ -464,9 +464,9 @@ fn handle_event_msg( if let Some(turn) = turns.get_mut(tid) { let call_id = str_field(payload, "call_id"); // v0.131.0+ (PR #22268): field renamed new_thread_id → new_session_id - let new_thread_id = payload - .get("new_thread_id") - .or_else(|| payload.get("new_session_id")) + let new_session_id = payload + .get("new_session_id") + .or_else(|| payload.get("new_thread_id")) .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); @@ -485,7 +485,7 @@ fn handle_event_msg( turn.collab_spawns.push(CollabSpawn { call_id: call_id.clone(), - new_thread_id, + new_session_id, agent_nickname, agent_role, model, @@ -886,7 +886,7 @@ fn spawn_from_function_call_output( Some(CollabSpawn { call_id: call_id.to_string(), - new_thread_id: parsed.agent_id, + new_session_id: parsed.agent_id, agent_nickname: parsed.nickname, agent_role, model, @@ -922,7 +922,7 @@ mod tests { assert_eq!(turns.len(), 1); assert_eq!(turns[0].collab_spawns.len(), 1); assert_eq!(turns[0].collab_spawns[0].call_id, "call_spawn"); - assert_eq!(turns[0].collab_spawns[0].new_thread_id, "worker-session"); + assert_eq!(turns[0].collab_spawns[0].new_session_id, "worker-session"); assert_eq!(turns[0].collab_spawns[0].agent_nickname, "Parfit"); assert_eq!(turns[0].collab_spawns[0].agent_role, "worker"); assert_eq!( @@ -1168,7 +1168,7 @@ mod tests { assert_eq!(turns.len(), 1); assert_eq!(turns[0].collab_spawns.len(), 1); - assert_eq!(turns[0].collab_spawns[0].new_thread_id, "worker-session"); + assert_eq!(turns[0].collab_spawns[0].new_session_id, "worker-session"); assert_eq!(turns[0].collab_spawns[0].agent_nickname, "Noether"); assert_eq!(turns[0].tool_calls.len(), 1); assert_eq!(turns[0].tool_calls[0].kind, ToolKind::SpawnAgent); @@ -2003,7 +2003,7 @@ mod tests { assert_eq!(turns.len(), 1); assert_eq!(turns[0].collab_spawns.len(), 1); - assert_eq!(turns[0].collab_spawns[0].new_thread_id, "worker-sess-v131"); + assert_eq!(turns[0].collab_spawns[0].new_session_id, "worker-sess-v131"); assert_eq!(turns[0].collab_spawns[0].agent_nickname, "Turing"); assert_eq!(turns[0].tool_calls.len(), 1); assert_eq!(turns[0].tool_calls[0].kind, ToolKind::SpawnAgent);