Support paginated thread forks (#35220)

## Why

`thread/fork` rejected threads using paginated history, so they could not use the same fork workflow as legacy threads.

## What changed

- Create paginated forks by referencing a frozen source-history prefix while persisting only child-owned records in the new rollout.
- Support latest, `lastTurnId`, and `beforeTurnId` boundaries, including inherited lineage, model-context reconstruction, interruption markers, and approval-reviewer inheritance.
- Make paginated reads and occurrence search traverse referenced history while excluding source records added after the fork.
- Coordinate fork preparation with archive and delete operations, and materialize compressed rollouts before they become fork references.

## Testing

Added app-server and thread-store coverage for boundary selection, active turns, inherited reads and search, compressed lineage, and concurrent source lifecycle operations.

GitOrigin-RevId: 66052d69517d7fb97a3b7817ab77a39c5ad995fd
This commit is contained in:
Owen Lin authored and copyberry committed 2026-07-24 17:31:55 +00:00
1 parent 58b4277228
commit 05f000263b
44 files changed
+2810 -295

No files matched your search

@@ -26,17 +26,22 @@ pub(super) async fn materialize_to_sqlite(
thread_id: ThreadId,
rollout_path: &Path,
) -> ThreadStoreResult<()> {
let start_offset = super::thread_history::next_rollout_byte_offset(store, thread_id).await?;
let start_offset = super::thread_history::projection_state(store, thread_id)
.await?
.map_or(0, |state| state.next_byte_offset);
let (lines, next_offset) = read_complete_rollout_lines(rollout_path, start_offset).await?;
// Empty valid records can still consume bytes through blank or rejected complete lines.
if lines.is_empty() && start_offset == next_offset {
return Ok(());
}
let subagent_history_start_ordinal = codex_rollout::read_session_meta_line(rollout_path)
let session_meta = codex_rollout::read_session_meta_line(rollout_path)
.await
.map_err(thread_store_io_error)?
.meta
.subagent_history_start_ordinal;
.meta;
let initial_ordinal = session_meta
.history_base
.map_or(0, |base| base.end_ordinal_exclusive);
let subagent_history_start_ordinal = session_meta.subagent_history_start_ordinal;
let projections = lines
.iter()
@@ -67,6 +72,7 @@ pub(super) async fn materialize_to_sqlite(
thread_id,
start_offset,
next_offset,
initial_ordinal,
projections,
)
.await