From 51c7467b2e074e326253943d510460cebd1f38d9 Mon Sep 17 00:00:00 2001 From: Junha Park <0xjunha@gmail.com> Date: Fri, 3 Jul 2026 12:45:33 +0900 Subject: [PATCH 1/3] feat(rollout): mark claude 2.1.199 exact-supported --- CHANGELOG.md | 2 + crates/rollout-audit/src/claude.rs | 63 +++++------ crates/rollout/src/claude/tests.rs | 65 ++++++++++- crates/rollout/src/claude/version.rs | 162 +++++++++++++++++++++++---- docs/claude-support.md | 4 +- docs/schema-changelog.md | 17 ++- 6 files changed, 254 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24b0b99..910607d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable Darc release changes should be summarized here. ## Unreleased +- Mark directly audited Claude Code rollout versions through 2.1.199 as exact-supported. + ## [0.2.2] - 2026-05-20 - Show live refresh progress with spinner steps and session/project progress bars. diff --git a/crates/rollout-audit/src/claude.rs b/crates/rollout-audit/src/claude.rs index 70edf2f..2f07645 100644 --- a/crates/rollout-audit/src/claude.rs +++ b/crates/rollout-audit/src/claude.rs @@ -3367,10 +3367,10 @@ mod tests { #[test] fn selects_audited_range_from_latest_published_down_to_exact_cutoff() { let versions = collect_stable_release_versions(vec![ - "2.1.130".to_owned(), - "2.1.128".to_owned(), - "2.1.126".to_owned(), - "2.1.124".to_owned(), + "2.1.200".to_owned(), + "2.1.199".to_owned(), + "2.1.198".to_owned(), + "2.1.197".to_owned(), ]); let selected = select_audited_release_versions(&versions, None).unwrap(); @@ -3379,7 +3379,7 @@ mod tests { .iter() .map(|version| version.raw.clone()) .collect::>(), - vec!["2.1.130".to_owned(), "2.1.128".to_owned()] + vec!["2.1.200".to_owned(), "2.1.199".to_owned()] ); } @@ -3410,12 +3410,11 @@ mod tests { #[test] fn sampling_plan_picks_stride_anchors_and_assumed_gaps() { let versions = collect_stable_release_versions(vec![ - "2.1.131".to_owned(), - "2.1.130".to_owned(), - "2.1.129".to_owned(), - "2.1.128".to_owned(), - "2.1.127".to_owned(), - "2.1.126".to_owned(), + "2.1.202".to_owned(), + "2.1.201".to_owned(), + "2.1.200".to_owned(), + "2.1.199".to_owned(), + "2.1.198".to_owned(), ]); let audited = select_audited_release_versions(&versions, None).unwrap(); let plan = build_sampling_plan(&audited, 2); @@ -3426,50 +3425,50 @@ mod tests { .map(|version| version.raw.clone()) .collect::>(), vec![ - "2.1.131".to_owned(), - "2.1.130".to_owned(), - "2.1.128".to_owned(), + "2.1.202".to_owned(), + "2.1.201".to_owned(), + "2.1.199".to_owned(), ] ); assert_eq!( plan.assumed_compatible_intervals, - vec!["2.1.129".to_owned()] + vec!["2.1.200".to_owned()] ); } #[test] fn reports_compatibility_when_transcript_manifests_match() { let provider = FakeClaudeSchemaAuditProvider::new( - &["2.1.130", "2.1.128", "2.1.126"], + &["2.1.200", "2.1.199", "2.1.198"], &[ ( - "2.1.130", + "2.1.200", ClaudeAuditSnapshot { transcript_manifest: manifest( &["assistant", "progress", "system", "user"], &["Bash", "Read", "Task"], ), - sdk_manifest: sdk_manifest(Some("0.2.130")), + sdk_manifest: sdk_manifest(Some("0.3.200")), }, ), ( - "2.1.128", + "2.1.199", ClaudeAuditSnapshot { transcript_manifest: manifest( &["assistant", "progress", "system", "user"], &["Bash", "Read", "Task"], ), - sdk_manifest: sdk_manifest(Some("0.2.128")), + sdk_manifest: sdk_manifest(Some("0.3.199")), }, ), ( - "2.1.126", + "2.1.198", ClaudeAuditSnapshot { transcript_manifest: manifest( &["assistant", "progress", "system", "user"], &["Bash", "Read", "Task"], ), - sdk_manifest: sdk_manifest(Some("0.2.126")), + sdk_manifest: sdk_manifest(Some("0.3.198")), }, ), ], @@ -3489,7 +3488,7 @@ mod tests { report.outcome, ClaudeSchemaAuditOutcome::Compatible )); - assert_eq!(report.latest_published_version, "2.1.130"); + assert_eq!(report.latest_published_version, "2.1.200"); assert_eq!(report.audited_versions.len(), 2); assert!(report.supplementary_sdk_drift.is_some()); } @@ -3497,26 +3496,26 @@ mod tests { #[test] fn detects_first_transcript_drift_and_preserves_sdk_signal_separately() { let provider = FakeClaudeSchemaAuditProvider::new( - &["2.1.130", "2.1.128", "2.1.126"], + &["2.1.200", "2.1.199", "2.1.198"], &[ ( - "2.1.130", + "2.1.200", ClaudeAuditSnapshot { transcript_manifest: manifest( &["assistant", "mystery-event", "progress", "system", "user"], &["Bash", "Read", "Task"], ), - sdk_manifest: sdk_manifest(Some("0.2.130")), + sdk_manifest: sdk_manifest(Some("0.3.200")), }, ), ( - "2.1.128", + "2.1.199", ClaudeAuditSnapshot { transcript_manifest: manifest( &["assistant", "progress", "system", "user"], &["Bash", "Read", "Task"], ), - sdk_manifest: sdk_manifest(Some("0.2.128")), + sdk_manifest: sdk_manifest(Some("0.3.199")), }, ), ], @@ -3535,7 +3534,7 @@ mod tests { let ClaudeSchemaAuditOutcome::Drift(drift) = report.outcome else { panic!("expected transcript drift"); }; - assert_eq!(drift.first_drift_version, "2.1.130"); + assert_eq!(drift.first_drift_version, "2.1.200"); assert_eq!( drift.boundary_precision, ClaudeSchemaDriftBoundaryPrecision::Exact @@ -3547,16 +3546,16 @@ mod tests { .any(|line| line.contains("mystery-event")) ); assert!(report.supplementary_sdk_drift.is_some()); - assert!(report.inspected_versions.contains(&"2.1.128".to_owned())); + assert!(report.inspected_versions.contains(&"2.1.199".to_owned())); assert!( !report .compatible_inspected_versions - .contains(&"2.1.130".to_owned()) + .contains(&"2.1.200".to_owned()) ); assert!( !report .assumed_compatible_intervals - .contains(&"2.1.128".to_owned()) + .contains(&"2.1.199".to_owned()) ); } diff --git a/crates/rollout/src/claude/tests.rs b/crates/rollout/src/claude/tests.rs index 5dd918a..11d352e 100644 --- a/crates/rollout/src/claude/tests.rs +++ b/crates/rollout/src/claude/tests.rs @@ -148,13 +148,25 @@ fn extracts_final_answers_for_representative_schema_epochs() -> Result<()> { ), ( "2.1.90", - "claude.primary_transcript.2_1_90_to_latest", + "claude.primary_transcript.2_1_90_to_2_1_160", ParseDeterminism::BestEffortForward, Some("end_turn"), ), ( "2.1.126", - "claude.primary_transcript.2_1_90_to_latest", + "claude.primary_transcript.2_1_90_to_2_1_160", + ParseDeterminism::Exact, + Some("end_turn"), + ), + ( + "2.1.161", + "claude.primary_transcript.2_1_161_to_2_1_197", + ParseDeterminism::Exact, + Some("end_turn"), + ), + ( + "2.1.198", + "claude.primary_transcript.2_1_198_to_latest", ParseDeterminism::Exact, Some("end_turn"), ), @@ -344,7 +356,7 @@ fn preserves_modern_agent_attachments_and_system_events() -> Result<()> { assert_eq!( rollout.schema_id, - "claude.primary_transcript.2_1_90_to_latest" + "claude.primary_transcript.2_1_90_to_2_1_160" ); assert_eq!(rollout.determinism, ParseDeterminism::BestEffortForward); assert_eq!(rollout.turns.len(), 1); @@ -456,7 +468,7 @@ fn parses_checked_in_real_fixture_after_attachment_drift() -> Result<()> { assert_eq!( rollout.schema_id, - "claude.primary_transcript.2_1_90_to_latest" + "claude.primary_transcript.2_1_90_to_2_1_160" ); assert_eq!(rollout.determinism, ParseDeterminism::BestEffortForward); assert_eq!(rollout.turns.len(), 1); @@ -743,6 +755,51 @@ fn normalizes_agent_progress_and_hook_summary_steps() -> Result<()> { Ok(()) } +#[test] +fn parses_exact_late_modern_prompt_source_and_hook_summary() -> Result<()> { + let rollout = parse_fixture( + r##"{"parentUuid":null,"isSidechain":false,"promptId":"prompt-1","promptSource":"user","type":"user","message":{"role":"user","content":"Read README.md"},"uuid":"user-1","timestamp":"2026-04-01T00:00:01Z","userType":"external","entrypoint":"sdk-cli","cwd":"/tmp/repo","sessionId":"parent-session","version":"2.1.198","gitBranch":"HEAD"} +{"parentUuid":"user-1","isSidechain":false,"message":{"model":"claude-sonnet-4-6","id":"assistant-1","type":"message","role":"assistant","content":[{"type":"tool_use","id":"tool-read","name":"Read","input":{"file_path":"README.md"}}],"stop_reason":"tool_use","stop_sequence":null},"requestId":"req-1","type":"assistant","uuid":"assistant-1","timestamp":"2026-04-01T00:00:02Z","userType":"external","entrypoint":"sdk-cli","cwd":"/tmp/repo","sessionId":"parent-session","version":"2.1.198","gitBranch":"HEAD"} +{"parentUuid":"assistant-1","isSidechain":false,"promptId":"prompt-1","promptSource":"tool_result","origin":"tool","type":"user","message":{"role":"user","content":[{"tool_use_id":"tool-read","type":"tool_result","content":"# Audit Fixture"}]},"uuid":"user-2","timestamp":"2026-04-01T00:00:03Z","toolUseResult":{"type":"text","file":{"filePath":"README.md","content":"# Audit Fixture\n","numLines":1,"startLine":1,"totalLines":1}},"sourceToolAssistantUUID":"assistant-1","userType":"external","entrypoint":"sdk-cli","cwd":"/tmp/repo","sessionId":"parent-session","version":"2.1.198","gitBranch":"HEAD"} +{"parentUuid":"user-2","isSidechain":false,"type":"system","subtype":"stop_hook_summary","hookCount":1,"hookInfos":[],"hookErrors":[],"preventedContinuation":false,"stopReason":"","hasOutput":false,"level":"info","timestamp":"2026-04-01T00:00:04Z","uuid":"system-1","toolUseID":"tool-read","userType":"external","entrypoint":"sdk-cli","cwd":"/tmp/repo","sessionId":"parent-session","version":"2.1.198","gitBranch":"HEAD"} +{"parentUuid":"user-2","isSidechain":false,"message":{"model":"claude-sonnet-4-6","id":"assistant-2","type":"message","role":"assistant","content":[{"type":"text","text":"# Audit Fixture"}],"stop_reason":"end_turn","stop_sequence":null},"requestId":"req-2","type":"assistant","uuid":"assistant-2","timestamp":"2026-04-01T00:00:05Z","userType":"external","entrypoint":"sdk-cli","cwd":"/tmp/repo","sessionId":"parent-session","version":"2.1.198","gitBranch":"HEAD"} +"##, + &primary_context(), + )?; + + assert_eq!( + rollout.schema_id, + "claude.primary_transcript.2_1_198_to_latest" + ); + assert_eq!(rollout.determinism, ParseDeterminism::Exact); + let turn = &rollout.turns[0]; + assert_eq!(turn.user_message, "Read README.md"); + assert_eq!(turn.status, CodexTurnStatus::Completed); + assert_eq!(turn.steps.len(), 3); + let CodexTurnStep::ToolCall { name, .. } = &turn.steps[0] else { + panic!("expected read tool call"); + }; + assert_eq!(name, "Read"); + let CodexTurnStep::ToolCallOutput { output, .. } = &turn.steps[1] else { + panic!("expected read tool output"); + }; + assert!(output.contains("# Audit Fixture")); + let CodexTurnStep::HookSummary { + call_id, + hook_count, + has_output, + .. + } = &turn.steps[2] + else { + panic!("expected hook summary"); + }; + assert_eq!(call_id.as_deref(), Some("tool-read")); + assert_eq!(*hook_count, 1); + assert!(!*has_output); + + Ok(()) +} + #[test] fn parses_mixed_tool_result_and_prompt_user_lines_as_two_turns() -> Result<()> { let rollout = parse_fixture( diff --git a/crates/rollout/src/claude/version.rs b/crates/rollout/src/claude/version.rs index 9392a5b..d47adeb 100644 --- a/crates/rollout/src/claude/version.rs +++ b/crates/rollout/src/claude/version.rs @@ -101,13 +101,21 @@ pub(crate) enum ClaudeSchemaEpoch { /// /// Observed Claude CLI versions: `2.1.84 ..= 2.1.89`. V2_1_84To2_1_89, - /// Current late-modern Claude transcript family beginning at the refined `attachment` drift. + /// Late-modern Claude transcript family beginning at the refined `attachment` drift. /// - /// Observed Claude CLI versions: `>= 2.1.90`. + /// Observed Claude CLI versions: `2.1.90 ..= 2.1.160`. + V2_1_90To2_1_160, + /// Claude transcript family observed after user lines gained `promptSource`. + /// + /// Observed Claude CLI versions: `2.1.161 ..= 2.1.197`. + V2_1_161To2_1_197, + /// Current Claude transcript family observed with modern hook summary and origin metadata. + /// + /// Observed Claude CLI versions: `>= 2.1.198`. /// /// Versions newer than the latest exact-supported release currently map here in /// `BestEffortForward` mode until a narrower modern family is carved out. - V2_1_90ToLatest, + V2_1_198ToLatest, } impl ClaudeSchemaEpoch { @@ -124,7 +132,9 @@ impl ClaudeSchemaEpoch { Self::V2_1_38To2_1_61 => "claude.primary_transcript.2_1_38_to_2_1_61", Self::V2_1_62To2_1_83 => "claude.primary_transcript.2_1_62_to_2_1_83", Self::V2_1_84To2_1_89 => "claude.primary_transcript.2_1_84_to_2_1_89", - Self::V2_1_90ToLatest => "claude.primary_transcript.2_1_90_to_latest", + Self::V2_1_90To2_1_160 => "claude.primary_transcript.2_1_90_to_2_1_160", + Self::V2_1_161To2_1_197 => "claude.primary_transcript.2_1_161_to_2_1_197", + Self::V2_1_198ToLatest => "claude.primary_transcript.2_1_198_to_latest", }, ClaudeSessionKind::Subagent => match self { Self::V1_0_88To2_0_5 => "claude.subagent_transcript.1_0_88_to_2_0_5", @@ -136,19 +146,30 @@ impl ClaudeSchemaEpoch { Self::V2_1_38To2_1_61 => "claude.subagent_transcript.2_1_38_to_2_1_61", Self::V2_1_62To2_1_83 => "claude.subagent_transcript.2_1_62_to_2_1_83", Self::V2_1_84To2_1_89 => "claude.subagent_transcript.2_1_84_to_2_1_89", - Self::V2_1_90ToLatest => "claude.subagent_transcript.2_1_90_to_latest", + Self::V2_1_90To2_1_160 => "claude.subagent_transcript.2_1_90_to_2_1_160", + Self::V2_1_161To2_1_197 => "claude.subagent_transcript.2_1_161_to_2_1_197", + Self::V2_1_198ToLatest => "claude.subagent_transcript.2_1_198_to_latest", }, } } /// Returns whether this epoch relies on historical text-only completion fallback. pub(crate) const fn uses_text_completion_fallback(self) -> bool { - !matches!(self, Self::V2_1_84To2_1_89 | Self::V2_1_90ToLatest) + !matches!( + self, + Self::V2_1_84To2_1_89 + | Self::V2_1_90To2_1_160 + | Self::V2_1_161To2_1_197 + | Self::V2_1_198ToLatest + ) } /// Returns whether this epoch recognizes top-level `attachment` lines natively. pub(crate) const fn supports_attachment_line(self) -> bool { - matches!(self, Self::V2_1_90ToLatest) + matches!( + self, + Self::V2_1_90To2_1_160 | Self::V2_1_161To2_1_197 | Self::V2_1_198ToLatest + ) } } @@ -178,14 +199,14 @@ pub fn resolve_claude_parse_determinism(cli_version: Option<&str>) -> ParseDeter pub(crate) fn resolve_claude_schema(cli_version: Option<&str>) -> ClaudeSchemaResolution { let Some(cli_version) = cli_version else { return ClaudeSchemaResolution { - epoch: ClaudeSchemaEpoch::V2_1_90ToLatest, + epoch: ClaudeSchemaEpoch::V2_1_198ToLatest, determinism: ParseDeterminism::BestEffortForward, }; }; let Ok(version) = ClaudeCliVersion::parse(cli_version) else { return ClaudeSchemaResolution { - epoch: ClaudeSchemaEpoch::V2_1_90ToLatest, + epoch: ClaudeSchemaEpoch::V2_1_198ToLatest, determinism: ParseDeterminism::BestEffortForward, }; }; @@ -222,8 +243,12 @@ fn resolve_claude_epoch(version: &ClaudeCliVersion) -> ClaudeSchemaEpoch { ClaudeSchemaEpoch::V2_1_62To2_1_83 } else if version < &ClaudeCliVersion::stable(2, 1, 90) { ClaudeSchemaEpoch::V2_1_84To2_1_89 + } else if version < &ClaudeCliVersion::stable(2, 1, 161) { + ClaudeSchemaEpoch::V2_1_90To2_1_160 + } else if version < &ClaudeCliVersion::stable(2, 1, 198) { + ClaudeSchemaEpoch::V2_1_161To2_1_197 } else { - ClaudeSchemaEpoch::V2_1_90ToLatest + ClaudeSchemaEpoch::V2_1_198ToLatest } } @@ -255,6 +280,64 @@ const EXACT_SUPPORTED_CLAUDE_STABLE_VERSIONS: &[(u32, u32, u32)] = &[ (2, 1, 124), (2, 1, 126), (2, 1, 128), + (2, 1, 129), + (2, 1, 131), + (2, 1, 132), + (2, 1, 133), + (2, 1, 136), + (2, 1, 137), + (2, 1, 138), + (2, 1, 139), + (2, 1, 140), + (2, 1, 141), + (2, 1, 142), + (2, 1, 143), + (2, 1, 144), + (2, 1, 145), + (2, 1, 146), + (2, 1, 147), + (2, 1, 148), + (2, 1, 149), + (2, 1, 150), + (2, 1, 152), + (2, 1, 153), + (2, 1, 154), + (2, 1, 156), + (2, 1, 157), + (2, 1, 158), + (2, 1, 159), + (2, 1, 160), + (2, 1, 161), + (2, 1, 162), + (2, 1, 163), + (2, 1, 165), + (2, 1, 166), + (2, 1, 167), + (2, 1, 168), + (2, 1, 169), + (2, 1, 170), + (2, 1, 172), + (2, 1, 173), + (2, 1, 174), + (2, 1, 175), + (2, 1, 176), + (2, 1, 177), + (2, 1, 178), + (2, 1, 179), + (2, 1, 181), + (2, 1, 182), + (2, 1, 183), + (2, 1, 185), + (2, 1, 186), + (2, 1, 187), + (2, 1, 190), + (2, 1, 191), + (2, 1, 193), + (2, 1, 195), + (2, 1, 196), + (2, 1, 197), + (2, 1, 198), + (2, 1, 199), ]; /// Returns one exact-supported Claude version from the canonical table. @@ -434,35 +517,70 @@ mod tests { assert_eq!( resolve_claude_schema(Some("2.1.92")), super::ClaudeSchemaResolution { - epoch: ClaudeSchemaEpoch::V2_1_90ToLatest, + epoch: ClaudeSchemaEpoch::V2_1_90To2_1_160, determinism: ParseDeterminism::BestEffortForward, } ); assert_eq!( resolve_claude_schema(Some("2.1.100")), super::ClaudeSchemaResolution { - epoch: ClaudeSchemaEpoch::V2_1_90ToLatest, + epoch: ClaudeSchemaEpoch::V2_1_90To2_1_160, determinism: ParseDeterminism::Exact, } ); assert_eq!( resolve_claude_schema(Some("2.1.126")), super::ClaudeSchemaResolution { - epoch: ClaudeSchemaEpoch::V2_1_90ToLatest, + epoch: ClaudeSchemaEpoch::V2_1_90To2_1_160, determinism: ParseDeterminism::Exact, } ); assert_eq!( resolve_claude_schema(Some("2.1.128")), super::ClaudeSchemaResolution { - epoch: ClaudeSchemaEpoch::V2_1_90ToLatest, + epoch: ClaudeSchemaEpoch::V2_1_90To2_1_160, + determinism: ParseDeterminism::Exact, + } + ); + assert_eq!( + resolve_claude_schema(Some("2.1.160")), + super::ClaudeSchemaResolution { + epoch: ClaudeSchemaEpoch::V2_1_90To2_1_160, + determinism: ParseDeterminism::Exact, + } + ); + assert_eq!( + resolve_claude_schema(Some("2.1.161")), + super::ClaudeSchemaResolution { + epoch: ClaudeSchemaEpoch::V2_1_161To2_1_197, + determinism: ParseDeterminism::Exact, + } + ); + assert_eq!( + resolve_claude_schema(Some("2.1.178")), + super::ClaudeSchemaResolution { + epoch: ClaudeSchemaEpoch::V2_1_161To2_1_197, + determinism: ParseDeterminism::Exact, + } + ); + assert_eq!( + resolve_claude_schema(Some("2.1.198")), + super::ClaudeSchemaResolution { + epoch: ClaudeSchemaEpoch::V2_1_198ToLatest, + determinism: ParseDeterminism::Exact, + } + ); + assert_eq!( + resolve_claude_schema(Some("2.1.199")), + super::ClaudeSchemaResolution { + epoch: ClaudeSchemaEpoch::V2_1_198ToLatest, determinism: ParseDeterminism::Exact, } ); assert_eq!( resolve_claude_schema(Some("bad-version")), super::ClaudeSchemaResolution { - epoch: ClaudeSchemaEpoch::V2_1_90ToLatest, + epoch: ClaudeSchemaEpoch::V2_1_198ToLatest, determinism: ParseDeterminism::BestEffortForward, } ); @@ -472,10 +590,14 @@ mod tests { fn exposes_epoch_capabilities() { assert!(ClaudeSchemaEpoch::V1_0_88To2_0_5.uses_text_completion_fallback()); assert!(!ClaudeSchemaEpoch::V2_1_84To2_1_89.uses_text_completion_fallback()); - assert!(!ClaudeSchemaEpoch::V2_1_90ToLatest.uses_text_completion_fallback()); + assert!(!ClaudeSchemaEpoch::V2_1_90To2_1_160.uses_text_completion_fallback()); + assert!(!ClaudeSchemaEpoch::V2_1_161To2_1_197.uses_text_completion_fallback()); + assert!(!ClaudeSchemaEpoch::V2_1_198ToLatest.uses_text_completion_fallback()); assert!(!ClaudeSchemaEpoch::V2_1_62To2_1_83.supports_attachment_line()); assert!(!ClaudeSchemaEpoch::V2_1_84To2_1_89.supports_attachment_line()); - assert!(ClaudeSchemaEpoch::V2_1_90ToLatest.supports_attachment_line()); + assert!(ClaudeSchemaEpoch::V2_1_90To2_1_160.supports_attachment_line()); + assert!(ClaudeSchemaEpoch::V2_1_161To2_1_197.supports_attachment_line()); + assert!(ClaudeSchemaEpoch::V2_1_198ToLatest.supports_attachment_line()); } #[test] @@ -491,18 +613,18 @@ mod tests { } assert_eq!( latest_exact_supported_claude_cli_version().to_string(), - "2.1.128" + "2.1.199" ); } #[test] fn exposes_expected_parse_determinism() { assert_eq!( - resolve_claude_parse_determinism(Some("2.1.128")), + resolve_claude_parse_determinism(Some("2.1.199")), ParseDeterminism::Exact ); assert_eq!( - resolve_claude_parse_determinism(Some("2.1.127")), + resolve_claude_parse_determinism(Some("2.1.194")), ParseDeterminism::BestEffortForward ); } diff --git a/docs/claude-support.md b/docs/claude-support.md index 62193eb..1a313ed 100644 --- a/docs/claude-support.md +++ b/docs/claude-support.md @@ -4,10 +4,10 @@ Darc tracks Claude rollout support at three levels: -- `exact`: versions backed by checked fixtures and explicit parser coverage. Darc stores this as an explicit audited version set in `crates/rollout/src/claude/version.rs`, with the support rationale recorded in [Schema changelog](schema-changelog.md). Today the exact set includes audited anchors from `1.0.91` through `2.1.128`, but only for the individual versions listed in the Rust table. +- `exact`: versions backed by checked fixtures and explicit parser coverage. Darc stores this as an explicit audited version set in `crates/rollout/src/claude/version.rs`, with the support rationale recorded in [Schema changelog](schema-changelog.md). Today the exact set includes audited anchors from `1.0.91` through `2.1.199`, but only for the individual versions listed in the Rust table. - `best_effort_forward`: versions that map onto a known Claude schema epoch but are not fixture-backed exact matches. Darc preserves unknown payloads instead of dropping them, so parsing continues with degraded certainty rather than failing fast. - `unsupported`: versions earlier than the practical Claude audit floor (`1.0.88`) or malformed rollouts that cannot be parsed safely. Individual unsupported rollout files are skipped during `darc index`; they do not abort the entire index run. The current parser epochs are broader than the exact set. Exactness is intentionally narrower than epoch membership, and `latest_exact_supported_claude_cli_version()` means the highest exact audited release, not every lower release. -Known adjacent audit drift boundaries are currently `2.0.22`, `2.1.85`, and `2.1.90`. Only `2.1.90` is currently parser-relevant: checked fixtures and parser behavior treat it as the top-level `attachment` boundary. `2.0.22` changed the audit's stream-json subtype manifest, and `2.1.85` is a non-breaking manifest change where audited fixtures stopped emitting optional `progress` lines. Other epoch boundaries are coarse compatibility windows inferred from sampled audits and fixtures, not proven adjacent breaking versions. +Known adjacent audit drift boundaries are currently `2.0.22`, `2.1.85`, `2.1.90`, `2.1.160`, `2.1.161`, `2.1.178`, `2.1.198`, and `2.1.199`. Parser-relevant boundaries are `2.1.90` for top-level `attachment` lines, `2.1.161` for persisted `promptSource` user-line metadata, and `2.1.198` for observed `stop_hook_summary` and `origin` metadata. The `2.0.22`, `2.1.85`, `2.1.160`, `2.1.178`, and `2.1.199` audit drifts are recorded in the schema changelog but did not require additional persisted JSONL parser epoch splits. diff --git a/docs/schema-changelog.md b/docs/schema-changelog.md index 013d020..40b69a8 100644 --- a/docs/schema-changelog.md +++ b/docs/schema-changelog.md @@ -23,6 +23,11 @@ Parser epochs are broader compatibility buckets. When sampled endpoints on both - `2.0.22`: live refinement found `2.0.21` compatible with the prior manifest and `2.0.22` drifted. The visible audit-manifest change was `stream_event_subtypes` gaining `hook_response` in stream-json output; Darc does not currently treat this as a persisted rollout parser epoch boundary. - `2.1.85`: live refinement found `2.1.84` before the change and `2.1.85` after it. The audited fixtures no longer emitted `progress` lines. Darc treats progress as optional preserved metadata, so this was not parser-breaking. - `2.1.90`: checked fixtures and parser behavior treat this as the top-level `attachment` boundary. `2.1.89` is before attachment-line support; `2.1.90` and later map to the attachment-aware parser epoch. +- `2.1.160`: live stride-1 audit found the first audit-manifest drift after `2.1.128`; the reported change was `stream_event_subtypes` length changing from 7 to 8 in stream-json output. Persisted JSONL fixtures did not show a parser-relevant shape change at this boundary. +- `2.1.161`: persisted JSONL fixtures gained `promptSource` on user lines. Darc treats this as the start of `claude.*_transcript.2_1_161_to_2_1_197`. +- `2.1.178`: live stride-1 audit reported a sampled adjacent drift window. Local persisted JSONL fixture comparison did not show a parser-relevant shape change, so no parser epoch split was added. +- `2.1.198`: persisted JSONL fixtures observed `system` lines with `subtype: "stop_hook_summary"` and user-line `origin` metadata. Darc treats this as the start of `claude.*_transcript.2_1_198_to_latest`. +- `2.1.199`: live stride-1 audit reported a sampled adjacent drift window. The `2.1.198_to_latest` parser epoch treats the observed hook-summary and origin surfaces as optional, so no additional split was added. The other parser epoch starts in `crates/rollout/src/claude/version.rs` are provisional coarse windows, not proven adjacent breaking versions. @@ -44,9 +49,19 @@ The other parser epoch starts in `crates/rollout/src/claude/version.rs` are prov - Evidence: live `darc claude-schema-audit --use-host-auth --from-version 2.1.84 --sample-stride 2 --survey-mode refine`. - Notes: The transcript manifest changed at `2.1.85`: the audited fixtures no longer emitted `progress` lines. Darc already treats progress lines as optional preserved metadata, so no parser epoch split was required. +### 2.1.128 ..= 2.1.199 stride-1 latest sweep + +- Upstream versions: published stable npm releases from `2.1.128` through `2.1.199`; npm reported `451` stable Claude Code versions overall, with `latest` and `next` at `2.1.199` and `stable` at `2.1.191`. +- Darc support: `exact` for every directly inspected published version in the range: `2.1.128`, `2.1.129`, `2.1.131`, `2.1.132`, `2.1.133`, `2.1.136`, `2.1.137`, `2.1.138`, `2.1.139`, `2.1.140`, `2.1.141`, `2.1.142`, `2.1.143`, `2.1.144`, `2.1.145`, `2.1.146`, `2.1.147`, `2.1.148`, `2.1.149`, `2.1.150`, `2.1.152`, `2.1.153`, `2.1.154`, `2.1.156`, `2.1.157`, `2.1.158`, `2.1.159`, `2.1.160`, `2.1.161`, `2.1.162`, `2.1.163`, `2.1.165`, `2.1.166`, `2.1.167`, `2.1.168`, `2.1.169`, `2.1.170`, `2.1.172`, `2.1.173`, `2.1.174`, `2.1.175`, `2.1.176`, `2.1.177`, `2.1.178`, `2.1.179`, `2.1.181`, `2.1.182`, `2.1.183`, `2.1.185`, `2.1.186`, `2.1.187`, `2.1.190`, `2.1.191`, `2.1.193`, `2.1.195`, `2.1.196`, `2.1.197`, `2.1.198`, and `2.1.199`. +- Schema ids: `claude.*_transcript.2_1_90_to_2_1_160`, `claude.*_transcript.2_1_161_to_2_1_197`, and `claude.*_transcript.2_1_198_to_latest`. +- Evidence: live `darc claude-schema-audit --use-host-auth --from-version 2.1.128 --sample-stride 1 --survey-mode refine`. +- Low-cost profile: every inspected version accepted Haiku with `--effort low`; Sonnet was probed as the fallback profile but was not needed for fixture coverage. +- Notes: The first audit-manifest drift was `2.1.160`, where stream-json event subtypes changed. Persisted JSONL comparison found the parser-relevant `promptSource` user-line boundary at `2.1.161` and the hook-summary/origin boundary at `2.1.198`. Additional live drift windows at `2.1.178` and `2.1.199` did not require parser epoch splits for the persisted JSONL shapes exercised by the audit fixtures. +- Supplementary SDK drift: the first Agent SDK metadata drift was at Claude Code `2.1.129` (`0.2.128` to `0.2.129`); SDK drift does not determine rollout transcript compatibility. + ### 2.1.126 ..= 2.1.128 stride-1 follow-up - Darc support: `exact` for `2.1.126` and `2.1.128`. -- Schema id: `claude.*_transcript.2_1_90_to_latest`. +- Schema id: `claude.*_transcript.2_1_90_to_2_1_160`. - Evidence: live `darc claude-schema-audit --use-host-auth --from-version 2.1.126 --sample-stride 1 --survey-mode refine`. - Notes: Transcript manifests matched. Supplementary Agent SDK metadata changed from `0.2.126` to `0.2.128`, but SDK drift does not determine rollout transcript compatibility. From 0d43dde4c0acaef92d5863cd48009df49981de5e Mon Sep 17 00:00:00 2001 From: Junha Park <0xjunha@gmail.com> Date: Fri, 3 Jul 2026 12:45:55 +0900 Subject: [PATCH 2/3] fix(rollout-audit): drain codex schema export output --- crates/rollout-audit/src/codex.rs | 123 ++++++++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 6 deletions(-) diff --git a/crates/rollout-audit/src/codex.rs b/crates/rollout-audit/src/codex.rs index 0f86d73..99bf8e2 100644 --- a/crates/rollout-audit/src/codex.rs +++ b/crates/rollout-audit/src/codex.rs @@ -4,8 +4,9 @@ use std::{ fs::{self, File}, io::{self, Read}, path::{Path, PathBuf}, - process::Command, - time::{SystemTime, UNIX_EPOCH}, + process::{Command, Output, Stdio}, + thread, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; use anyhow::{Context, Result, bail, ensure}; @@ -31,6 +32,8 @@ const GITHUB_RELEASE_SOURCE: &str = "GitHub Releases (openai/codex)"; const RELEASE_TAG_PREFIX: &str = "rust-v"; /// Stores the rollout schema file name exported from released Codex binaries. const ROLLOUT_SCHEMA_FILE_NAME: &str = "RolloutLine.json"; +/// Stores the timeout used for released Codex schema export commands. +const CODEX_SCHEMA_EXPORT_TIMEOUT: Duration = Duration::from_secs(180); /// Lists the Darc files most likely to need updates after Codex schema drift. const LIKELY_UPDATE_PATHS: &[&str] = &[ "crates/rollout/src/codex/version.rs", @@ -415,12 +418,13 @@ impl CodexSchemaAuditProvider for GitHubCodexSchemaAuditProvider { sanitize_for_path(tag_name), unique_suffix() )); - let output = build_released_binary_command(&binary_path, working_dir, &runtime_root)? + let mut command = build_released_binary_command(&binary_path, working_dir, &runtime_root)?; + command .arg("app-server") .arg("generate-internal-json-schema") .arg("-o") - .arg(&schema_dir) - .output() + .arg(&schema_dir); + let output = run_command_with_timeout(&mut command, CODEX_SCHEMA_EXPORT_TIMEOUT) .with_context(|| { format!( "failed to run released Codex binary for `{tag_name}` at {}", @@ -450,6 +454,82 @@ impl CodexSchemaAuditProvider for GitHubCodexSchemaAuditProvider { } } +/// Runs one process with a hard timeout and returns its captured output. +fn run_command_with_timeout(command: &mut Command, timeout: Duration) -> Result { + let mut child = command + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context("failed to spawn command")?; + let stdout_reader = spawn_output_reader( + child + .stdout + .take() + .context("failed to capture child stdout")?, + ); + let stderr_reader = spawn_output_reader( + child + .stderr + .take() + .context("failed to capture child stderr")?, + ); + let started = Instant::now(); + loop { + if let Some(status) = child.try_wait().context("failed to poll child process")? { + return collect_command_output(status, stdout_reader, stderr_reader); + } + if started.elapsed() >= timeout { + let _ = child.kill(); + let status = child + .wait() + .context("failed to wait for timed out child process")?; + let output = collect_command_output(status, stdout_reader, stderr_reader)?; + bail!( + "command timed out after {}s: {}", + timeout.as_secs(), + command_output_summary(&output.stderr) + ); + } + thread::sleep(Duration::from_millis(100)); + } +} + +/// Spawns one background reader that drains one child output pipe into memory. +fn spawn_output_reader( + mut reader: impl Read + Send + 'static, +) -> thread::JoinHandle>> { + thread::spawn(move || { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes)?; + Ok(bytes) + }) +} + +/// Collects the completed child status plus drained stdout/stderr bytes into one `Output`. +fn collect_command_output( + status: std::process::ExitStatus, + stdout_reader: thread::JoinHandle>>, + stderr_reader: thread::JoinHandle>>, +) -> Result { + Ok(Output { + status, + stdout: join_output_reader(stdout_reader, "stdout")?, + stderr: join_output_reader(stderr_reader, "stderr")?, + }) +} + +/// Joins one background pipe reader and returns the captured bytes. +fn join_output_reader( + handle: thread::JoinHandle>>, + label: &str, +) -> Result> { + match handle.join() { + Ok(result) => result.with_context(|| format!("failed to read child {label}")), + Err(_panic) => anyhow::bail!("child {label} reader panicked"), + } +} + /// Builds one released Codex command with a scrubbed runtime environment. fn build_released_binary_command( binary_path: &Path, @@ -1019,6 +1099,8 @@ mod tests { ffi::OsString, fs::File, path::{Path, PathBuf}, + process::Command, + time::Duration, }; use anyhow::anyhow; @@ -1033,7 +1115,7 @@ mod tests { extract_verified_binary_package, host_platform_from_parts, latest_exact_supported_codex_cli_version, parse_stable_release_tag, run_codex_schema_audit_with_provider, run_codex_schema_audit_with_provider_and_progress, - select_audited_release_tags, + run_command_with_timeout, select_audited_release_tags, }; struct FakeSchemaAuditProvider { @@ -1163,6 +1245,35 @@ mod tests { ); } + #[cfg(unix)] + #[test] + fn timeout_helper_drains_large_child_output() { + let mut command = Command::new("sh"); + command.arg("-c").arg( + "i=0; while [ \"$i\" -lt 4096 ]; do printf '0123456789abcdef0123456789abcdef'; i=$((i + 1)); done; printf stderr-ok >&2", + ); + + let output = run_command_with_timeout(&mut command, Duration::from_secs(5)).unwrap(); + + assert!(output.status.success()); + assert!(output.stdout.len() > 64 * 1024); + assert_eq!(String::from_utf8_lossy(&output.stderr), "stderr-ok"); + } + + #[cfg(unix)] + #[test] + fn timeout_helper_closes_child_stdin() { + let mut command = Command::new("sh"); + command + .arg("-c") + .arg("if read _line; then printf open; else printf closed; fi"); + + let output = run_command_with_timeout(&mut command, Duration::from_secs(5)).unwrap(); + + assert!(output.status.success()); + assert_eq!(String::from_utf8_lossy(&output.stdout), "closed"); + } + #[test] fn reports_compatibility_when_normalized_schemas_match() { let provider = FakeSchemaAuditProvider::new( From 1990b60c2fb8dd70d900b658f4bf25fe27759403 Mon Sep 17 00:00:00 2001 From: Junha Park <0xjunha@gmail.com> Date: Fri, 3 Jul 2026 12:46:09 +0900 Subject: [PATCH 3/3] feat(rollout): mark codex 0.142.5 exact-supported --- CHANGELOG.md | 1 + crates/rollout-audit/src/codex.rs | 98 ++++++++++++++--------------- crates/rollout/src/codex/tests.rs | 46 ++++++++++++++ crates/rollout/src/codex/version.rs | 42 ++++++++++--- docs/schema-audits.md | 7 ++- docs/schema-changelog.md | 7 ++- 6 files changed, 139 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 910607d..f8d1488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable Darc release changes should be summarized here. ## Unreleased - Mark directly audited Claude Code rollout versions through 2.1.199 as exact-supported. +- Mark source-audited Codex rollout versions through 0.142.5 as exact-supported. ## [0.2.2] - 2026-05-20 diff --git a/crates/rollout-audit/src/codex.rs b/crates/rollout-audit/src/codex.rs index 99bf8e2..8841108 100644 --- a/crates/rollout-audit/src/codex.rs +++ b/crates/rollout-audit/src/codex.rs @@ -1198,8 +1198,8 @@ mod tests { #[test] fn parses_and_filters_stable_codex_release_tags() { let tags = collect_stable_release_tags(vec![ - "rust-v0.128.0".to_owned(), - "rust-v0.129.0-alpha.1".to_owned(), + "rust-v0.142.5".to_owned(), + "rust-v0.142.6-alpha.1".to_owned(), "rust-v0.127.0".to_owned(), "rust-vrust-v0.99.0-alpha.16".to_owned(), "not-a-codex-tag".to_owned(), @@ -1207,29 +1207,29 @@ mod tests { assert_eq!( raw_tags(&tags), - vec!["rust-v0.128.0".to_owned(), "rust-v0.127.0".to_owned()] + vec!["rust-v0.142.5".to_owned(), "rust-v0.127.0".to_owned()] ); assert_eq!( - parse_stable_release_tag("rust-v0.128.0") + parse_stable_release_tag("rust-v0.142.5") .unwrap() .version .to_string(), - "0.128.0" + "0.142.5" ); - assert!(parse_stable_release_tag("rust-v0.128.0-alpha.1").is_none()); + assert!(parse_stable_release_tag("rust-v0.142.5-alpha.1").is_none()); } #[test] fn selects_audited_range_from_latest_stable_down_to_exact_cutoff() { assert_eq!( latest_exact_supported_codex_cli_version().to_string(), - "0.128.0" + "0.142.5" ); let tags = collect_stable_release_tags(vec![ - "rust-v0.130.0".to_owned(), - "rust-v0.129.0".to_owned(), - "rust-v0.128.0".to_owned(), + "rust-v0.143.0".to_owned(), + "rust-v0.142.6".to_owned(), + "rust-v0.142.5".to_owned(), "rust-v0.127.0".to_owned(), ]); @@ -1238,9 +1238,9 @@ mod tests { assert_eq!( raw_tags(&selected), vec![ - "rust-v0.130.0".to_owned(), - "rust-v0.129.0".to_owned(), - "rust-v0.128.0".to_owned(), + "rust-v0.143.0".to_owned(), + "rust-v0.142.6".to_owned(), + "rust-v0.142.5".to_owned(), ] ); } @@ -1277,10 +1277,10 @@ mod tests { #[test] fn reports_compatibility_when_normalized_schemas_match() { let provider = FakeSchemaAuditProvider::new( - &["rust-v0.130.0", "rust-v0.129.0", "rust-v0.128.0"], + &["rust-v0.143.0", "rust-v0.142.6", "rust-v0.142.5"], &[ ( - "rust-v0.130.0", + "rust-v0.143.0", json!({ "type": ["null", "object"], "definitions": { @@ -1292,7 +1292,7 @@ mod tests { }), ), ( - "rust-v0.129.0", + "rust-v0.142.6", json!({ "definitions": { "RolloutItem": { @@ -1304,7 +1304,7 @@ mod tests { }), ), ( - "rust-v0.128.0", + "rust-v0.142.5", json!({ "type": ["object", "null"], "definitions": { @@ -1329,13 +1329,13 @@ mod tests { report.outcome, CodexSchemaAuditOutcome::Compatible )); - assert_eq!(report.latest_stable_release_tag, "rust-v0.130.0"); + assert_eq!(report.latest_stable_release_tag, "rust-v0.143.0"); assert_eq!( report.audited_tags, vec![ - "rust-v0.130.0".to_owned(), - "rust-v0.129.0".to_owned(), - "rust-v0.128.0".to_owned(), + "rust-v0.143.0".to_owned(), + "rust-v0.142.6".to_owned(), + "rust-v0.142.5".to_owned(), ] ); } @@ -1343,24 +1343,24 @@ mod tests { #[test] fn detects_drift_at_the_first_newer_stable_tag() { let provider = FakeSchemaAuditProvider::new( - &["rust-v0.130.0", "rust-v0.129.0", "rust-v0.128.0"], + &["rust-v0.143.0", "rust-v0.142.6", "rust-v0.142.5"], &[ ( - "rust-v0.130.0", + "rust-v0.143.0", json!({ "type": "object", "required": ["timestamp", "item", "trace_id"], }), ), ( - "rust-v0.129.0", + "rust-v0.142.6", json!({ "type": "object", "required": ["timestamp", "item", "trace_id"], }), ), ( - "rust-v0.128.0", + "rust-v0.142.5", json!({ "type": "object", "required": ["timestamp", "item"], @@ -1379,7 +1379,7 @@ mod tests { let CodexSchemaAuditOutcome::Drift(drift) = report.outcome else { panic!("expected schema drift"); }; - assert_eq!(drift.first_drift_tag, "rust-v0.129.0"); + assert_eq!(drift.first_drift_tag, "rust-v0.142.6"); assert!( drift .difference_summary @@ -1391,24 +1391,24 @@ mod tests { #[test] fn reports_progress_for_compatible_audits() { let provider = FakeSchemaAuditProvider::new( - &["rust-v0.130.0", "rust-v0.129.0", "rust-v0.128.0"], + &["rust-v0.143.0", "rust-v0.142.6", "rust-v0.142.5"], &[ ( - "rust-v0.130.0", + "rust-v0.143.0", json!({ "type": "object", "required": ["timestamp", "item"], }), ), ( - "rust-v0.129.0", + "rust-v0.142.6", json!({ "type": "object", "required": ["item", "timestamp"], }), ), ( - "rust-v0.128.0", + "rust-v0.142.5", json!({ "required": ["timestamp", "item"], "type": "object", @@ -1438,12 +1438,12 @@ mod tests { progress .iter() .any(|line| line - .contains("Exporting baseline RolloutLine schema from rust-v0.128.0")) + .contains("Exporting baseline RolloutLine schema from rust-v0.142.5")) ); assert!( progress .iter() - .any(|line| line.contains("Comparing rust-v0.129.0 against baseline (1/2)")) + .any(|line| line.contains("Comparing rust-v0.142.6 against baseline (1/2)")) ); assert!(progress.last().is_some_and(|line| { line.contains("No schema drift detected across 3 audited stable tag(s).") @@ -1453,10 +1453,10 @@ mod tests { #[test] fn detects_order_sensitive_prefix_items_drift() { let provider = FakeSchemaAuditProvider::new( - &["rust-v0.130.0", "rust-v0.129.0", "rust-v0.128.0"], + &["rust-v0.143.0", "rust-v0.142.6", "rust-v0.142.5"], &[ ( - "rust-v0.130.0", + "rust-v0.143.0", json!({ "type": "array", "prefixItems": [ @@ -1466,7 +1466,7 @@ mod tests { }), ), ( - "rust-v0.129.0", + "rust-v0.142.6", json!({ "type": "array", "prefixItems": [ @@ -1476,7 +1476,7 @@ mod tests { }), ), ( - "rust-v0.128.0", + "rust-v0.142.5", json!({ "type": "array", "prefixItems": [ @@ -1498,7 +1498,7 @@ mod tests { let CodexSchemaAuditOutcome::Drift(drift) = report.outcome else { panic!("expected schema drift"); }; - assert_eq!(drift.first_drift_tag, "rust-v0.129.0"); + assert_eq!(drift.first_drift_tag, "rust-v0.142.6"); assert!( drift .difference_summary @@ -1510,10 +1510,10 @@ mod tests { #[test] fn ignores_order_only_one_of_reorders() { let provider = FakeSchemaAuditProvider::new( - &["rust-v0.130.0", "rust-v0.129.0", "rust-v0.128.0"], + &["rust-v0.143.0", "rust-v0.142.6", "rust-v0.142.5"], &[ ( - "rust-v0.130.0", + "rust-v0.143.0", json!({ "oneOf": [ { @@ -1536,7 +1536,7 @@ mod tests { }), ), ( - "rust-v0.129.0", + "rust-v0.142.6", json!({ "oneOf": [ { @@ -1559,7 +1559,7 @@ mod tests { }), ), ( - "rust-v0.128.0", + "rust-v0.142.5", json!({ "oneOf": [ { @@ -1600,10 +1600,10 @@ mod tests { #[test] fn ignores_order_only_all_of_reorders() { let provider = FakeSchemaAuditProvider::new( - &["rust-v0.130.0", "rust-v0.129.0", "rust-v0.128.0"], + &["rust-v0.143.0", "rust-v0.142.6", "rust-v0.142.5"], &[ ( - "rust-v0.130.0", + "rust-v0.143.0", json!({ "allOf": [ { "properties": { "kind": { "const": "x" } }, "type": "object" }, @@ -1612,7 +1612,7 @@ mod tests { }), ), ( - "rust-v0.129.0", + "rust-v0.142.6", json!({ "allOf": [ { "properties": { "kind": { "const": "x" } }, "type": "object" }, @@ -1621,7 +1621,7 @@ mod tests { }), ), ( - "rust-v0.128.0", + "rust-v0.142.5", json!({ "allOf": [ { "properties": { "value": { "type": "string" } }, "type": "object" }, @@ -1649,8 +1649,8 @@ mod tests { fn resolves_expected_release_asset_names_for_supported_platforms() { let mac = host_platform_from_parts("macos", "aarch64").unwrap(); assert_eq!( - mac.release_asset_name("0.128.0"), - "codex-npm-darwin-arm64-0.128.0.tgz" + mac.release_asset_name("0.142.5"), + "codex-npm-darwin-arm64-0.142.5.tgz" ); assert_eq!( mac.package_binary_path(), @@ -1659,8 +1659,8 @@ mod tests { let linux = host_platform_from_parts("linux", "x86_64").unwrap(); assert_eq!( - linux.release_asset_name("0.128.0"), - "codex-npm-linux-x64-0.128.0.tgz" + linux.release_asset_name("0.142.5"), + "codex-npm-linux-x64-0.142.5.tgz" ); assert_eq!( linux.package_binary_path(), diff --git a/crates/rollout/src/codex/tests.rs b/crates/rollout/src/codex/tests.rs index d32daad..be46d8c 100644 --- a/crates/rollout/src/codex/tests.rs +++ b/crates/rollout/src/codex/tests.rs @@ -59,6 +59,52 @@ fn parses_turn_lifecycle_rollout_and_records_schema_metadata() -> Result<()> { Ok(()) } +#[test] +fn parses_latest_exact_turn_lifecycle_response_item_variants() -> Result<()> { + let rollout = parse_rollout_reader( + Cursor::new( + r#"{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{"id":"fixture","cwd":"/tmp/repo","cli_version":"0.142.5"}} +{"timestamp":"2026-01-01T00:00:01Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1","started_at":1767225601}} +{"timestamp":"2026-01-01T00:00:02Z","type":"event_msg","payload":{"type":"user_message","message":"Inspect repo"}} +{"timestamp":"2026-01-01T00:00:03Z","type":"event_msg","payload":{"type":"safety_buffering","enabled":false}} +{"timestamp":"2026-01-01T00:00:04Z","type":"response_item","payload":{"type":"context_compaction","encrypted_content":"ctx"}} +{"timestamp":"2026-01-01T00:00:05Z","type":"response_item","payload":{"type":"compaction_trigger"}} +{"timestamp":"2026-01-01T00:00:06Z","type":"response_item","payload":{"type":"agent_message","author":"assistant","recipient":"user","content":[{"type":"text","text":"intermediate"}]}} +{"timestamp":"2026-01-01T00:00:07Z","type":"response_item","payload":{"type":"message","role":"assistant","phase":"final_answer","content":[{"type":"output_text","text":"Done"}]}} +{"timestamp":"2026-01-01T00:00:08Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1","completed_at":1767225608}} +"#, + ), + Path::new("fixture.jsonl"), + )?; + + assert_eq!(rollout.cli_version, "0.142.5"); + assert_eq!(rollout.schema_id, "codex.turn_lifecycle"); + assert_eq!(rollout.determinism, ParseDeterminism::Exact); + assert_eq!(rollout.turns.len(), 1); + assert_eq!(rollout.turns[0].status, CodexTurnStatus::Completed); + assert_eq!( + rollout.turns[0].final_answer, + Some(CodexTurnMessage { + timestamp: "2026-01-01T00:00:07Z".to_owned(), + text: "Done".to_owned(), + }) + ); + assert!(matches!( + &rollout.turns[0].steps[0], + CodexTurnStep::ProviderResponseItem { item_type, .. } if item_type == "context_compaction" + )); + assert!(matches!( + &rollout.turns[0].steps[1], + CodexTurnStep::ProviderResponseItem { item_type, .. } if item_type == "compaction_trigger" + )); + assert!(matches!( + &rollout.turns[0].steps[2], + CodexTurnStep::ProviderResponseItem { item_type, .. } if item_type == "agent_message" + )); + + Ok(()) +} + #[test] fn rejects_structured_tool_output_in_pre_097_epoch() { let error = parse_rollout_reader( diff --git a/crates/rollout/src/codex/version.rs b/crates/rollout/src/codex/version.rs index 1014e73..53d3405 100644 --- a/crates/rollout/src/codex/version.rs +++ b/crates/rollout/src/codex/version.rs @@ -111,7 +111,7 @@ pub fn resolve_codex_parse_determinism(cli_version: &str) -> SchemaResult CodexCliVersion { - CodexCliVersion::stable(0, 128, 0) + CodexCliVersion::stable(0, 142, 5) } /// Returns whether one `response_item.type` variant is expected for the given Codex CLI version. @@ -123,6 +123,10 @@ pub const fn latest_exact_supported_codex_cli_version() -> CodexCliVersion { /// - `compaction` appears in `>=0.59.0` /// - `image_generation_call` appears in `>=0.108.0` /// - `tool_search_call` and `tool_search_output` appear in `>=0.115.0` +/// - `other` is present by the `0.128.0` exact schema boundary +/// - `context_compaction` appears in `>=0.129.0` +/// - `compaction_trigger` appears in `>=0.131.0` +/// - `agent_message` appears in `>=0.138.0` pub(crate) fn supports_response_item(version: &CodexCliVersion, kind: &str) -> bool { match kind { "message" @@ -137,6 +141,10 @@ pub(crate) fn supports_response_item(version: &CodexCliVersion, kind: &str) -> b "compaction" => version >= &CodexCliVersion::stable(0, 59, 0), "image_generation_call" => version >= &CodexCliVersion::stable(0, 108, 0), "tool_search_call" | "tool_search_output" => version >= &CodexCliVersion::stable(0, 115, 0), + "other" => version >= &CodexCliVersion::stable(0, 128, 0), + "context_compaction" => version >= &CodexCliVersion::stable(0, 129, 0), + "compaction_trigger" => version >= &CodexCliVersion::stable(0, 131, 0), + "agent_message" => version >= &CodexCliVersion::stable(0, 138, 0), _ => false, } } @@ -247,10 +255,7 @@ pub enum CodexSchemaId { StructuredToolOutput, /// Current rollout family used for modern Codex sessions. /// - /// Supported Codex CLI versions: `>=0.104.0-alpha.1, <=0.128.0`. - /// - /// Versions newer than `0.128.0` currently map here in `BestEffortForward` mode until a newer - /// exact family is added. + /// Supported Codex CLI versions: `>=0.104.0-alpha.1, <=0.142.5`. TurnLifecycle, } @@ -376,7 +381,7 @@ mod tests { } ); assert_eq!( - resolve_codex_schema("0.128.0").unwrap(), + resolve_codex_schema("0.142.5").unwrap(), super::CodexSchemaResolution { schema_id: CodexSchemaId::TurnLifecycle, determinism: ParseDeterminism::Exact, @@ -387,7 +392,7 @@ mod tests { #[test] fn resolves_newer_versions_as_best_effort_forward() { assert_eq!( - resolve_codex_schema("0.129.0").unwrap(), + resolve_codex_schema("0.143.0").unwrap(), super::CodexSchemaResolution { schema_id: CodexSchemaId::TurnLifecycle, determinism: ParseDeterminism::BestEffortForward, @@ -399,7 +404,7 @@ mod tests { fn exposes_latest_exact_supported_codex_cli_version() { assert_eq!( latest_exact_supported_codex_cli_version().to_string(), - "0.128.0" + "0.142.5" ); } @@ -410,7 +415,11 @@ mod tests { ParseDeterminism::Exact ); assert_eq!( - resolve_codex_parse_determinism("0.129.0").unwrap(), + resolve_codex_parse_determinism("0.142.5").unwrap(), + ParseDeterminism::Exact + ); + assert_eq!( + resolve_codex_parse_determinism("0.143.0").unwrap(), ParseDeterminism::BestEffortForward ); } @@ -422,6 +431,13 @@ mod tests { let v108 = CodexCliVersion::parse("0.108.0").unwrap(); let v114 = CodexCliVersion::parse("0.114.0").unwrap(); let v115 = CodexCliVersion::parse("0.115.0").unwrap(); + let v127 = CodexCliVersion::parse("0.127.0").unwrap(); + let v128 = CodexCliVersion::parse("0.128.0").unwrap(); + let v129 = CodexCliVersion::parse("0.129.0").unwrap(); + let v130 = CodexCliVersion::parse("0.130.0").unwrap(); + let v131 = CodexCliVersion::parse("0.131.0").unwrap(); + let v137 = CodexCliVersion::parse("0.137.0").unwrap(); + let v138 = CodexCliVersion::parse("0.138.0").unwrap(); assert!(supports_response_item(&v094, "web_search_call")); assert!(!supports_response_item(&v094, "tool_search_call")); @@ -430,6 +446,14 @@ mod tests { assert!(supports_response_item(&v108, "image_generation_call")); assert!(!supports_response_item(&v114, "tool_search_output")); assert!(supports_response_item(&v115, "tool_search_output")); + assert!(!supports_response_item(&v127, "other")); + assert!(supports_response_item(&v128, "other")); + assert!(!supports_response_item(&v128, "context_compaction")); + assert!(supports_response_item(&v129, "context_compaction")); + assert!(!supports_response_item(&v130, "compaction_trigger")); + assert!(supports_response_item(&v131, "compaction_trigger")); + assert!(!supports_response_item(&v137, "agent_message")); + assert!(supports_response_item(&v138, "agent_message")); } #[test] diff --git a/docs/schema-audits.md b/docs/schema-audits.md index a19c848..05bd0dd 100644 --- a/docs/schema-audits.md +++ b/docs/schema-audits.md @@ -31,6 +31,11 @@ What the audit does not do: - It only audits stable releases that are currently published on Codex GitHub Releases. - It does not bump Darc's exact-support boundary automatically. +macOS-safe fallback: + +- If macOS blocks or warns on downloaded release binaries, do not bypass Gatekeeper for the audit cache. Use official `openai/codex` source tags instead: check out each stable `rust-v...` tag in the audit range, add a temporary binary in `codex-rs/app-server-protocol` that calls `codex_app_server_protocol::generate_internal_json_schema(out_dir)`, and compare the generated `RolloutLine.json` files with the same normalization rules as `crates/rollout-audit/src/schema_diff.rs`. +- This source-only fallback avoids executing downloaded Codex binaries while still deriving the schema from the tagged upstream source. + What the audit caches locally: ```bash @@ -42,7 +47,7 @@ On Linux and Windows, the default cache root follows the platform cache director If you see an error like: ```text -GitHub Releases are missing the stable release tag `rust-v0.128.0` +GitHub Releases are missing the stable release tag `rust-v0.142.5` ``` the published release catalog no longer contains the exact-support boundary tag that Darc needs as the audit baseline. Darc cannot advance the audit until that release remains available or the exact-support boundary is updated. diff --git a/docs/schema-changelog.md b/docs/schema-changelog.md index 40b69a8..08d50ee 100644 --- a/docs/schema-changelog.md +++ b/docs/schema-changelog.md @@ -4,13 +4,14 @@ This maintainer ledger records why Darc treats upstream rollout versions as exac ## Codex -### Exact through 0.128.0 +### Exact through 0.142.5 -- Upstream versions: stable Codex CLI releases `0.33.0 ..= 0.128.0`. +- Upstream versions: stable Codex CLI releases `0.33.0 ..= 0.142.5`. - Darc support: `exact` when the rollout version is within the supported parser families and the parser sees no unsupported exact-mode fields. - Schema owner: `crates/rollout/src/codex/version.rs`. - Audit evidence: `darc codex-schema-audit` compares published GitHub release schemas against `latest_exact_supported_codex_cli_version()`. -- Notes: `0.128.0` is the current exact boundary. Newer stable releases stay `best_effort_forward` until a schema audit confirms compatibility and the boundary is advanced. +- Latest sweep evidence: source-only export from official `openai/codex` tags to avoid executing downloaded release binaries on macOS. The sweep generated `RolloutLine.json` from `codex-app-server-protocol::generate_internal_json_schema()` for `rust-v0.128.0`, `rust-v0.129.0`, `rust-v0.130.0`, `rust-v0.131.0`, `rust-v0.132.0`, `rust-v0.133.0`, `rust-v0.134.0`, `rust-v0.135.0`, `rust-v0.136.0`, `rust-v0.137.0`, `rust-v0.138.0`, `rust-v0.139.0`, `rust-v0.140.0`, `rust-v0.141.0`, `rust-v0.142.0`, `rust-v0.142.1`, `rust-v0.142.2`, `rust-v0.142.3`, `rust-v0.142.4`, and `rust-v0.142.5`. +- Notes: The source sweep found upstream schema drift after `0.128.0`. Parser-relevant response item variants were version-gated in Darc: `other` by `0.128.0`, `context_compaction` at `0.129.0`, `compaction_trigger` at `0.131.0`, and `agent_message` at `0.138.0`. Other observed drift in app-server event and metadata definitions is ignored or preserved by the Codex parser. ## Claude Code