diff --git a/crates/tracedecay-sessions/src/runtime/hosts/codex/observation.rs b/crates/tracedecay-sessions/src/runtime/hosts/codex/observation.rs index cf3816de8df..7b3c317f1f7 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/codex/observation.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/codex/observation.rs @@ -45,6 +45,8 @@ use tracedecay_runtime_core::resident_memory::ProcessSharedMemoryReservationV1; #[cfg(test)] mod meta_cache_tests; #[cfg(test)] +mod reoffer_tests; +#[cfg(test)] mod retired_source_tests; #[cfg(test)] mod rollout_source_tests; @@ -403,6 +405,11 @@ impl CodexObservationAdmission<'_> { /// Prepare this bounded page's context and scope decisions on the blocking /// pool. The page owns its reservation throughout preparation; only these /// decisions, not filesystem work, are used by frame callbacks and retries. + #[tracing::instrument( + name = "sessions.observation.codex.resolve_scope", + level = "trace", + skip_all + )] async fn resolve_scope( &self, matchers: &ProjectRootMatcherCache, @@ -772,6 +779,7 @@ async fn admit_codex_jsonl_page( .with_max_new_bytes(max_new_bytes) .with_persisted_cursor_update(PersistedCursorUpdate::Replace) .with_lazy_shared_frame_preparation() + .with_skip_empty_initialization() .with_cancellation(cancellation.clone()); if let Some(max_frames) = max_frames { request = request.with_max_frames(max_frames); diff --git a/crates/tracedecay-sessions/src/runtime/hosts/codex/observation/reoffer_tests.rs b/crates/tracedecay-sessions/src/runtime/hosts/codex/observation/reoffer_tests.rs new file mode 100644 index 00000000000..d6272604004 --- /dev/null +++ b/crates/tracedecay-sessions/src/runtime/hosts/codex/observation/reoffer_tests.rs @@ -0,0 +1,357 @@ +//! Empty Codex re-offers must not pay repository resolution costs or cache a +//! scope decision that can leak into the next nonempty admission page. + +use std::io::Write as _; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; +use std::time::Instant; + +use serde_json::{Value, json}; +use tempfile::TempDir; +use tracedecay_domain::{ObservationScopeV1, ProjectId}; +use tracedecay_runtime_core::config::ProfileRoot; +use tracedecay_runtime_core::git_discovery::{ + GitRepositoryIdentityOutcome, discover_repository_identity_cli_first, +}; +use tracedecay_runtime_core::path_safety::plain_git_args; + +use super::{ + CodexJsonlAdmissionProgress, CodexObservationAdmission, codex_observation_source_v2, + try_admit_codex_jsonl_observations, +}; +use crate::admission::test_support::MemoryHostAdmission; +use crate::observation::ObservationCancellation; +use crate::runtime::observation::jsonl_observation_admission::install_test_shared_jsonl_preparation_authority; +use crate::runtime::shared::ProjectRootMatcherCache; +use crate::runtime::with_transcript_source_profile; + +static IDENTITY_RESOLUTIONS: Mutex> = Mutex::new(Vec::new()); + +fn recording_identity(path: &Path) -> GitRepositoryIdentityOutcome { + IDENTITY_RESOLUTIONS + .lock() + .unwrap() + .push(path.to_path_buf()); + discover_repository_identity_cli_first(path) +} + +fn resolution_count(root: &Path) -> usize { + IDENTITY_RESOLUTIONS + .lock() + .unwrap() + .iter() + .filter(|path| path.starts_with(root)) + .count() +} + +fn message(session: &str, item: &str) -> Value { + json!({ + "timestamp": "2026-09-04T12:00:01.004Z", + "type": "event_msg", + "payload": { + "type": "item_completed", + "thread_id": session, + "turn_id": item, + "item": { + "type": "UserMessage", + "id": item, + "content": [{"type": "text", "text": item}] + } + } + }) +} + +fn write_rollout(path: &Path, cwd: &Path, session: &str) -> u64 { + let mut file = std::fs::File::create(path).unwrap(); + writeln!( + file, + "{}", + json!({ + "timestamp": "2026-09-04T12:00:00.000Z", + "type": "session_meta", + "payload": {"id": session, "cwd": cwd} + }) + ) + .unwrap(); + writeln!(file, "{}", message(session, "initial-item")).unwrap(); + file.metadata().unwrap().len() +} + +fn append_in_scope(path: &Path, cwd: &Path, session: &str, item: &str) -> u64 { + let mut file = std::fs::OpenOptions::new().append(true).open(path).unwrap(); + writeln!( + file, + "{}", + json!({"type": "turn_context", "payload": {"cwd": cwd, "model": "scope-model"}}) + ) + .unwrap(); + writeln!(file, "{}", message(session, item)).unwrap(); + file.metadata().unwrap().len() +} + +struct Fixture { + _temp: TempDir, + root: PathBuf, + project: PathBuf, + outside: PathBuf, + profile: ProfileRoot, + project_id: ProjectId, + project_admission: MemoryHostAdmission, + profile_admission: MemoryHostAdmission, +} + +impl Fixture { + fn new() -> Self { + install_test_shared_jsonl_preparation_authority(); + let temp = TempDir::new().unwrap(); + let root = temp.path().canonicalize().unwrap(); + let project = root.join("project"); + let outside = root.join("outside"); + for repository in [&project, &outside] { + std::fs::create_dir_all(repository.join("src")).unwrap(); + let output = std::process::Command::new( + tracedecay_runtime_core::git::try_git_program().unwrap(), + ) + .args(plain_git_args(&[ + "init", + "--quiet", + "--initial-branch=main", + ])) + .current_dir(repository) + .output() + .unwrap(); + assert!( + output.status.success(), + "git init: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + Self { + profile: ProfileRoot::under_home(root.join("owner")), + _temp: temp, + root, + project, + outside, + project_id: ProjectId::new("project.codex-idle-reoffer").unwrap(), + project_admission: MemoryHostAdmission::default(), + profile_admission: MemoryHostAdmission::default(), + } + } + + async fn admit( + &self, + path: &Path, + cache: &ProjectRootMatcherCache, + cancellation: &ObservationCancellation, + ) -> [CodexJsonlAdmissionProgress; 2] { + let project = try_admit_codex_jsonl_observations( + path, + CodexObservationAdmission::Project { + root: &self.project, + project_id: self.project_id.clone(), + }, + &self.project_admission, + None, + None, + cancellation, + cache, + ) + .await + .unwrap(); + let profile = try_admit_codex_jsonl_observations( + path, + CodexObservationAdmission::Profile { + session_id: None, + registered_roots: std::slice::from_ref(&self.project), + }, + &self.profile_admission, + None, + None, + cancellation, + cache, + ) + .await + .unwrap(); + [project, profile] + } +} + +fn assert_unchanged(progress: CodexJsonlAdmissionProgress, len: u64) { + assert!(progress.resumed); + assert!(!progress.source_deferred); + assert_eq!(progress.covered_through, len); + assert_eq!(progress.bytes_consumed, 0); + assert_eq!(progress.frames_decoded, 0); + assert_eq!(progress.frames_persisted, 0); + assert_eq!(progress.prior_context_bytes, 0); +} + +#[test] +fn unchanged_corpus_reoffers_skip_cold_scope_resolution() { + let fixture = Fixture::new(); + let mut rollouts = Vec::new(); + for index in 0..16 { + let repository = if index % 2 == 0 { + &fixture.project + } else { + &fixture.outside + }; + let cwd = repository.join(format!("cwd-{index}")); + std::fs::create_dir_all(&cwd).unwrap(); + let path = fixture.root.join(format!("rollout-{index}.jsonl")); + let len = write_rollout(&path, &cwd, &format!("reoffer-session-{index}")); + rollouts.push((path, len)); + } + tracedecay_runtime_core::native_runtime::block_on(with_transcript_source_profile( + fixture.profile.clone(), + async { + let cancellation = ObservationCancellation::default(); + let initial = ProjectRootMatcherCache::with_identity_resolver(recording_identity); + for (path, len) in &rollouts { + for progress in fixture.admit(path, &initial, &cancellation).await { + assert!(!progress.source_deferred); + assert_eq!(progress.covered_through, *len); + assert_eq!(progress.bytes_consumed, *len); + } + } + let stores = [&fixture.project_admission, &fixture.profile_admission]; + assert_eq!(stores.map(|store| store.observations().len()), [16, 16]); + let captures = stores.map(MemoryHostAdmission::capture_call_counts); + let retained = stores.map(|store| { + store + .observations() + .iter() + .map(|record| { + ( + record.observation().observation_id().clone(), + record.committed_cursor().clone(), + ) + }) + .collect::>() + }); + let before = resolution_count(&fixture.root); + assert!(before >= rollouts.len(), "the resolver probe must be live"); + drop(initial); + + // A cold process cache cannot hide redundant resolution on the + // first empty re-offer. Later rounds also exercise the warm path. + let cache = ProjectRootMatcherCache::with_identity_resolver(recording_identity); + let started = Instant::now(); + let mut reoffers = 0; + for _ in 0..3 { + for (path, len) in &rollouts { + for progress in fixture.admit(path, &cache, &cancellation).await { + assert_unchanged(progress, *len); + reoffers += 1; + } + } + } + let resolutions = resolution_count(&fixture.root) - before; + tracing::info!( + reoffers, + git_identity_resolutions = resolutions, + wall_seconds = started.elapsed().as_secs_f64(), + "unchanged Codex corpus re-offer measurement" + ); + assert_eq!(reoffers, 96); + assert_eq!(resolutions, 0, "empty pages must not resolve Git scope"); + assert_eq!( + stores.map(MemoryHostAdmission::capture_call_counts), + captures + ); + for (store, retained) in stores.into_iter().zip(retained) { + let current = store + .observations() + .iter() + .map(|record| { + ( + record.observation().observation_id().clone(), + record.committed_cursor().clone(), + ) + }) + .collect::>(); + assert_eq!( + current, retained, + "accepted source identity must not change" + ); + } + }, + )) + .unwrap(); +} + +#[test] +fn append_after_empty_reoffer_rechecks_scope_and_source_identity() { + let fixture = Fixture::new(); + let path = fixture.root.join("rollout-live.jsonl"); + let session = "reoffer-live-session"; + let len = write_rollout(&path, &fixture.project.join("src"), session); + tracedecay_runtime_core::native_runtime::block_on(with_transcript_source_profile( + fixture.profile.clone(), + async { + let cancellation = ObservationCancellation::default(); + let initial = ProjectRootMatcherCache::default(); + let admitted = fixture.admit(&path, &initial, &cancellation).await; + assert_eq!(admitted[0].frames_persisted, 2); + assert_eq!(admitted[1].frames_persisted, 0); + drop(initial); + + let cache = ProjectRootMatcherCache::with_identity_resolver(recording_identity); + for progress in fixture.admit(&path, &cache, &cancellation).await { + assert_unchanged(progress, len); + } + assert_eq!(resolution_count(&fixture.root), 0); + + let new_len = + append_in_scope(&path, &fixture.outside.join("src"), session, "outside-item"); + let appended = fixture.admit(&path, &cache, &cancellation).await; + assert_eq!(appended[0].frames_persisted, 0); + assert!(appended[1].frames_persisted > 0); + for progress in appended { + assert!(!progress.source_deferred); + assert_eq!(progress.covered_through, new_len); + assert_eq!(progress.bytes_consumed, new_len - len); + } + assert!(resolution_count(&fixture.root) > 0); + let expected_source = codex_observation_source_v2(session).unwrap(); + let profile_records = fixture.profile_admission.observations(); + assert!(!profile_records.is_empty()); + for record in &profile_records { + assert_eq!(record.observation().source(), &expected_source); + assert_eq!(record.observation().scope(), &ObservationScopeV1::Profile); + } + assert_eq!(fixture.project_admission.observations().len(), 2); + + let final_len = append_in_scope( + &path, + &fixture.project.join("src"), + session, + "returned-item", + ); + let returned = fixture.admit(&path, &cache, &cancellation).await; + assert!(returned[0].frames_persisted > 0); + assert_eq!(returned[1].frames_persisted, 0); + for progress in returned { + assert!(!progress.source_deferred); + assert_eq!(progress.covered_through, final_len); + assert_eq!(progress.bytes_consumed, final_len - new_len); + } + assert_eq!( + fixture.profile_admission.observations().len(), + profile_records.len() + ); + for progress in fixture.admit(&path, &cache, &cancellation).await { + assert_unchanged(progress, final_len); + } + + cancellation.cancel(); + let captures = fixture.project_admission.capture_call_counts(); + for progress in fixture.admit(&path, &cache, &cancellation).await { + assert!(progress.source_deferred, "cancellation is not convergence"); + assert_eq!(progress.frames_persisted, 0); + } + assert_eq!(fixture.project_admission.capture_call_counts(), captures); + }, + )) + .unwrap(); +} diff --git a/crates/tracedecay-sessions/src/runtime/observation/jsonl_observation_admission.rs b/crates/tracedecay-sessions/src/runtime/observation/jsonl_observation_admission.rs index 5b76239710b..39b0e2afc8f 100644 --- a/crates/tracedecay-sessions/src/runtime/observation/jsonl_observation_admission.rs +++ b/crates/tracedecay-sessions/src/runtime/observation/jsonl_observation_admission.rs @@ -89,6 +89,7 @@ pub(in crate::runtime) struct JsonlObservationAdmissionRequest<'request> { persisted_cursor_update: PersistedCursorUpdate, cancellation: ObservationCancellation, shared_frame_preparation: SharedJsonlFramePreparation, + skip_empty_initialization: bool, } impl<'request> JsonlObservationAdmissionRequest<'request> { @@ -112,6 +113,7 @@ impl<'request> JsonlObservationAdmissionRequest<'request> { persisted_cursor_update: PersistedCursorUpdate::Monotonic, cancellation: ObservationCancellation::default(), shared_frame_preparation: SharedJsonlFramePreparation::None, + skip_empty_initialization: false, } } @@ -145,6 +147,13 @@ impl<'request> JsonlObservationAdmissionRequest<'request> { self.shared_frame_preparation = SharedJsonlFramePreparation::Lazy; self } + + /// Opt in only when an empty page needs no provider-specific validation. + /// Pi, for example, must verify its header even on a no-frame rewrite. + pub(in crate::runtime) fn with_skip_empty_initialization(mut self) -> Self { + self.skip_empty_initialization = true; + self + } } pub(in crate::runtime) enum JsonlFrameAdmission { @@ -2345,6 +2354,7 @@ pub(in crate::runtime) async fn admit_jsonl_observations( persisted_cursor_update, cancellation, shared_frame_preparation, + skip_empty_initialization, } = request; if cancellation.is_cancelled() { return Err(TranscriptIngestError::Cancelled { provider }); @@ -2459,6 +2469,48 @@ pub(in crate::runtime) async fn admit_jsonl_observations( return Err(TranscriptIngestError::Cancelled { provider }); } let generation = ObservationSourceGenerationV1::new(raw.new_cursor.file_id)?; + // An unchanged, fully covered source has no context or frames to resolve. + // Keep the durable completion check: a prior interrupted rewrite may still + // have unretired records even though this pass reads zero new bytes. + if skip_empty_initialization + && had_expected_cursor + && previous_generation == Some(generation) + && expected_cursor + .as_ref() + .is_some_and(|cursor| cursor.position() == raw.start_offset) + && !raw.replacement_generation + && raw.start_offset == raw.read_through + && raw.frames.is_empty() + && raw.skipped.is_empty() + && !progress.source_deferred + { + let records_retired = admission + .complete_source_rewrite(&source, &scope, generation) + .await + .map_err(|outcome| { + if is_admission_cancellation(&outcome, &cancellation) { + TranscriptIngestError::Cancelled { provider } + } else { + host_admission_error(provider, outcome) + } + })?; + tracing::debug!( + event = "transcript_admission_batch", + phase = "complete", + provider, + transcript = %transcript_log_identity(path), + total_frames, + retained_frame_bytes, + bytes_consumed = progress.bytes_consumed, + source_deferred = progress.source_deferred, + records_retired, + "transcript admission batch finished" + ); + progress.covered_through = expected_cursor + .as_ref() + .map_or(0, ObservationSourceCursorV1::position); + return Ok(progress); + } let mut state = initialize(JsonlObservationScan { resumed: had_expected_cursor && raw.start_offset > 0, // Derived from the scanned generation rather than this batch's first diff --git a/crates/tracedecay-sessions/src/runtime/observation/jsonl_observation_admission/tests.rs b/crates/tracedecay-sessions/src/runtime/observation/jsonl_observation_admission/tests.rs index 4f3c2bd5885..a50adb2af82 100644 --- a/crates/tracedecay-sessions/src/runtime/observation/jsonl_observation_admission/tests.rs +++ b/crates/tracedecay-sessions/src/runtime/observation/jsonl_observation_admission/tests.rs @@ -81,6 +81,7 @@ struct SeamSpyAdmission { /// strictly inside its range. peer_covers_advance_through: Mutex>, rewrites: Mutex>, + rewrite_completion_failure: Mutex>, } #[derive(Debug, PartialEq, Eq)] @@ -1053,6 +1054,9 @@ impl HostAdmission for SeamSpyAdmission { .lock() .unwrap() .push(RewriteCall::Complete(generation)); + if let Some(failure) = self.rewrite_completion_failure.lock().unwrap().take() { + return Box::pin(async move { Err(failure) }); + } self.inner .complete_source_rewrite(source, scope, generation) } @@ -1971,6 +1975,218 @@ async fn a_refusal_memo_cannot_authorize_uncovered_bytes_or_hide_an_in_place_rew } } +/// Other providers may use their initializer to verify proof even with zero +/// frames. Default behavior therefore continues invoking it and preserves a +/// refusal before the pending-rewrite completion check. +#[tokio::test] +async fn unchanged_noop_does_not_skip_initialization_without_provider_opt_in() { + let (_temp, path, len) = rollout_fixture(); + let spy = SeamSpyAdmission::default(); + let first = + try_admit_codex_jsonl_observations_for_profile_with_admission(&path, None, &[], &spy, None) + .await + .expect("initial admission"); + assert_eq!(first.covered_through, len); + spy.rewrites.lock().unwrap().clear(); + let original_cursor = stored_cursor(&spy).await; + + let request = super::JsonlObservationAdmissionRequest::new( + "codex", + &path, + &spy, + crate::runtime::hosts::codex::codex_observation_source_v2(SESSION_ID).unwrap(), + ObservationScopeV1::Profile, + RetentionClass::new("test").unwrap(), + ); + let mut initialized = false; + let error = super::admit_jsonl_observations( + request, + async |_| { + initialized = true; + Err::<(), TranscriptIngestError>(TranscriptIngestError::InvalidFrameState { + provider: "codex", + }) + }, + |(), _, _, _, _, _| panic!("empty page cannot normalize a frame"), + ) + .await + .expect_err("without opt-in the initializer must still validate an empty page"); + assert!(initialized); + assert!(matches!( + error, + TranscriptIngestError::InvalidFrameState { provider: "codex" } + )); + assert_eq!(stored_cursor(&spy).await, original_cursor); + assert!(spy.rewrites.lock().unwrap().is_empty()); +} + +/// An empty re-offer still requires a successful durable rewrite-completion +/// check. A transient read failure must remain a typed block, not an empty +/// success that incorrectly declares source convergence. +#[tokio::test] +async fn unchanged_codex_reoffer_propagates_rewrite_completion_failure() { + let (_temp, path, len) = rollout_fixture(); + let spy = SeamSpyAdmission::default(); + let initial = + try_admit_codex_jsonl_observations_for_profile_with_admission(&path, None, &[], &spy, None) + .await + .expect("initial admission"); + assert_eq!(initial.covered_through, len); + let cursor = stored_cursor(&spy).await.expect("committed source cursor"); + let rows = spy.inner.observations().len(); + let captures = spy.capture_count(); + assert!(rows > 0, "the source must have real committed evidence"); + spy.rewrites.lock().unwrap().clear(); + *spy.rewrite_completion_failure.lock().unwrap() = Some( + HostAdmissionOutcome::retained_unavailable("projection_storage_retry_scheduled"), + ); + + let request = super::JsonlObservationAdmissionRequest::new( + "codex", + &path, + &spy, + crate::runtime::hosts::codex::codex_observation_source_v2(SESSION_ID).unwrap(), + ObservationScopeV1::Profile, + RetentionClass::new("test").unwrap(), + ) + .with_persisted_cursor_update(super::PersistedCursorUpdate::Replace) + .with_skip_empty_initialization(); + let error = super::admit_jsonl_observations( + request, + async |_| { + Err::<(), TranscriptIngestError>(TranscriptIngestError::InvalidFrameState { + provider: "codex", + }) + }, + |(), _, _, _, _, _| panic!("unchanged source cannot normalize frames"), + ) + .await + .expect_err("failed durable rewrite check must not report convergence"); + assert!(matches!( + error, + TranscriptIngestError::HostAdmission { + provider: "codex", + reason: "projection_storage_retry_scheduled", + retryable: true, + .. + } + )); + assert_eq!(stored_cursor(&spy).await, Some(cursor.clone())); + assert_eq!(spy.inner.observations().len(), rows); + assert_eq!(spy.capture_count(), captures); + assert_eq!( + spy.rewrites.lock().unwrap().as_slice(), + &[RewriteCall::Complete(cursor.generation())] + ); + + let retry = + try_admit_codex_jsonl_observations_for_profile_with_admission(&path, None, &[], &spy, None) + .await + .expect("retry must recheck durable rewrite completion"); + assert_eq!(retry.covered_through, len); + assert_eq!(retry.bytes_consumed, 0); + assert_eq!(retry.frames_persisted, 0); + assert_eq!(stored_cursor(&spy).await, Some(cursor.clone())); + assert_eq!(spy.inner.observations().len(), rows); + assert_eq!( + spy.rewrites.lock().unwrap().as_slice(), + &[ + RewriteCall::Complete(cursor.generation()), + RewriteCall::Complete(cursor.generation()), + ] + ); +} + +/// A durable unchanged cursor must not pay for a second provider-scope +/// initialization on every discovery re-offer. Rewrite completion remains +/// authoritative even when no frames were read, and the next append must +/// still admit through normal scope/provenance checks. +#[tokio::test] +async fn unchanged_codex_reoffers_skip_empty_initializer_but_complete_rewrites() { + let (_temp, path, len) = rollout_fixture(); + let spy = SeamSpyAdmission::default(); + let first = + try_admit_codex_jsonl_observations_for_profile_with_admission(&path, None, &[], &spy, None) + .await + .expect("initial admission"); + assert_eq!(first.covered_through, len); + let previous = stored_cursor(&spy).await.expect("durable cursor"); + let original_rows = spy.inner.observations().len(); + assert!( + original_rows > 0, + "the cold pass must admit actual evidence" + ); + let original_captures = spy.capture_count(); + spy.rewrites.lock().unwrap().clear(); + + for _ in 0..16 { + let request = super::JsonlObservationAdmissionRequest::new( + "codex", + &path, + &spy, + crate::runtime::hosts::codex::codex_observation_source_v2(SESSION_ID).unwrap(), + ObservationScopeV1::Profile, + RetentionClass::new("test").unwrap(), + ) + .with_persisted_cursor_update(super::PersistedCursorUpdate::Replace) + .with_lazy_shared_frame_preparation() + .with_skip_empty_initialization(); + let mut initialized = false; + let idle = super::admit_jsonl_observations( + request, + async |_| { + initialized = true; + Err::<(), TranscriptIngestError>(TranscriptIngestError::InvalidFrameState { + provider: "codex", + }) + }, + |(), _, _, _, _, _| panic!("unchanged source must not normalize frames"), + ) + .await + .expect("unchanged source must avoid the empty initializer"); + assert!(!initialized, "zero-frame re-offer started scope resolution"); + assert_eq!(idle.bytes_consumed, 0); + assert_eq!(idle.frames_persisted, 0); + assert_eq!(idle.covered_through, len); + } + { + let completions = spy.rewrites.lock().unwrap(); + assert_eq!( + completions.len(), + 16, + "no-op re-offers must still check durable rewrites" + ); + assert!(completions.iter().all(|call| { + matches!(call, RewriteCall::Complete(generation) if *generation == previous.generation()) + })); + } + assert_eq!(spy.inner.observations().len(), original_rows); + assert_eq!(spy.capture_count(), original_captures); + assert_eq!(stored_cursor(&spy).await, Some(previous)); + + let appended = json!({ + "timestamp": "2026-01-01T00:00:02.000Z", + "type": "event_msg", + "payload": crate::runtime::hosts::codex::user_message_payload("newly appended evidence") + }) + .to_string() + + "\n"; + std::fs::OpenOptions::new() + .append(true) + .open(&path) + .unwrap() + .write_all(appended.as_bytes()) + .unwrap(); + spin_until_jsonl_change_settled(&path); + let resumed = + try_admit_codex_jsonl_observations_for_profile_with_admission(&path, None, &[], &spy, None) + .await + .expect("append must still take the real admission path"); + assert_eq!(resumed.bytes_consumed, appended.len() as u64); + assert_eq!(resumed.frames_persisted, 1); + assert_eq!(spy.inner.observations().len(), original_rows + 1); +} + #[tokio::test] async fn an_unchanged_resumed_rollout_does_not_rebuild_its_prior_context() { let (_temp, path, len) = rollout_fixture();