mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Protect fork history references during rollout cleanup (#34566)
## Why Paginated forks can retain a `history_base` pointer to another rollout. Deleting or compressing either side of that relationship can leave the fork without the rollout history it references. ## What changed - Add a rollout reference index that scans active, archived, and compressed rollout metadata. - Reject deletion when a rollout is still referenced by a fork outside the requested deletion set. - Delete spawned thread subtrees as one store operation so internal references do not block deleting the complete subtree, while external references preflight the request before any files are removed. - Skip referenced rollouts, fork-pointer rollouts, and rollouts with unreadable metadata during background compression. ## Testing - Cover reference indexing across active, archived, compressed, duplicate, and self-referencing rollouts. - Verify deletion rejects external references without partially deleting a subtree and permits batch deletion of internally referenced threads. - Verify compression preserves fork pointer chains and continues past unreadable metadata. GitOrigin-RevId: a8672727f8706e743d7fb3ca895a1d9ceac98a3c
This commit is contained in:
1 parent
7bb13ab846
commit
0b175e6439
13 files changed
+801
-21
No files matched your search
@@ -7,6 +7,7 @@ use std::time::SystemTime;
|
||||
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::HistoryPosition;
|
||||
use codex_protocol::protocol::InitialHistory;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::RolloutLine;
|
||||
@@ -306,6 +307,86 @@ async fn worker_compresses_old_active_and_archived_rollouts() -> anyhow::Result<
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn worker_skips_archived_paginated_fork_pointer_chain() -> anyhow::Result<()> {
|
||||
let home = TempDir::new()?;
|
||||
let source_uuid = Uuid::from_u128(16);
|
||||
let source_id = ThreadId::from_string(&source_uuid.to_string())?;
|
||||
let source_path = rollout_path(home.path(), "2025-01-03T12-00-00", source_uuid);
|
||||
write_rollout(&source_path, source_id, "referenced source")?;
|
||||
set_old_mtime(&source_path)?;
|
||||
|
||||
let child_uuid = Uuid::from_u128(17);
|
||||
let child_id = ThreadId::from_string(&child_uuid.to_string())?;
|
||||
let child_path = archived_rollout_path(home.path(), "2025-01-03T12-00-01", child_uuid);
|
||||
write_rollout(&child_path, child_id, "fork child")?;
|
||||
set_history_base(
|
||||
child_path.as_path(),
|
||||
HistoryPosition {
|
||||
thread_id: source_id,
|
||||
end_ordinal_exclusive: 2,
|
||||
end_byte_offset: std::fs::metadata(source_path.as_path())?.len(),
|
||||
},
|
||||
)?;
|
||||
set_old_mtime(&child_path)?;
|
||||
|
||||
worker::run(home.path().to_path_buf()).await?;
|
||||
|
||||
assert!(source_path.exists());
|
||||
assert!(child_path.exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn worker_skips_source_referenced_by_archived_compressed_rollout() -> anyhow::Result<()> {
|
||||
let home = TempDir::new()?;
|
||||
let source_uuid = Uuid::from_u128(18);
|
||||
let source_id = ThreadId::from_string(&source_uuid.to_string())?;
|
||||
let source_path = rollout_path(home.path(), "2025-01-03T12-00-00", source_uuid);
|
||||
write_rollout(&source_path, source_id, "referenced source")?;
|
||||
set_old_mtime(&source_path)?;
|
||||
|
||||
let child_uuid = Uuid::from_u128(19);
|
||||
let child_id = ThreadId::from_string(&child_uuid.to_string())?;
|
||||
let child_path = archived_rollout_path(home.path(), "2025-01-03T12-00-01", child_uuid);
|
||||
write_rollout(&child_path, child_id, "fork child")?;
|
||||
set_history_base(
|
||||
child_path.as_path(),
|
||||
HistoryPosition {
|
||||
thread_id: source_id,
|
||||
end_ordinal_exclusive: 2,
|
||||
end_byte_offset: std::fs::metadata(source_path.as_path())?.len(),
|
||||
},
|
||||
)?;
|
||||
compress_now(child_path.as_path())?;
|
||||
|
||||
worker::run(home.path().to_path_buf()).await?;
|
||||
|
||||
assert!(source_path.exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn worker_skips_unreadable_metadata_without_blocking_other_compression() -> anyhow::Result<()>
|
||||
{
|
||||
let home = TempDir::new()?;
|
||||
let source_uuid = Uuid::from_u128(20);
|
||||
let source_id = ThreadId::from_string(&source_uuid.to_string())?;
|
||||
let source_path = rollout_path(home.path(), "2025-01-03T12-00-00", source_uuid);
|
||||
write_rollout(&source_path, source_id, "candidate")?;
|
||||
set_old_mtime(&source_path)?;
|
||||
|
||||
let unreadable_path = rollout_path(home.path(), "2025-01-03T12-00-01", Uuid::from_u128(21));
|
||||
fs::write(unreadable_path.as_path(), "{not json}\n")?;
|
||||
|
||||
worker::run(home.path().to_path_buf()).await?;
|
||||
|
||||
assert!(!source_path.exists());
|
||||
assert!(compressed_rollout_path(&source_path).exists());
|
||||
assert!(unreadable_path.exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_materializes_compressed_rollout_path() -> anyhow::Result<()> {
|
||||
let home = TempDir::new()?;
|
||||
@@ -637,6 +718,21 @@ fn write_rollout(path: &std::path::Path, thread_id: ThreadId, message: &str) ->
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_history_base(path: &std::path::Path, history_base: HistoryPosition) -> anyhow::Result<()> {
|
||||
let contents = fs::read_to_string(path)?;
|
||||
let mut lines = contents.lines();
|
||||
let mut head: serde_json::Value = serde_json::from_str(lines.next().expect("session meta"))?;
|
||||
head["payload"]["history_base"] = serde_json::to_value(history_base)?;
|
||||
let mut updated = serde_json::to_string(&head)?;
|
||||
for line in lines {
|
||||
updated.push('\n');
|
||||
updated.push_str(line);
|
||||
}
|
||||
updated.push('\n');
|
||||
fs::write(path, updated)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compress_now(path: &std::path::Path) -> anyhow::Result<()> {
|
||||
let compressed_path = compressed_rollout_path(path);
|
||||
let input = fs::File::open(path)?;
|
||||
|
||||
Reference in new issue
Block a user