Persist realtime events in the thread timeline (#40508)

## Why

Clients need a bounded view of realtime conversations that preserves the order of speech, agent work, and turn lifecycle events without loading the full thread history.

## What changed

- Persist realtime session boundaries, transcript segments, and promoted agent items in paginated thread rollouts.
- Add the experimental `thread/timeline/list` API to page ordinary items, realtime items, and turn boundaries in canonical rollout order. Include `activeRealtimeSessionAtPageStart` so each page can be rendered independently.
- Emit typed `thread/realtime/item/started`, `thread/realtime/item/transcript/delta`, and `thread/realtime/item/completed` notifications around durable realtime items.
- Keep the existing `thread/items/list` response and legacy thread history behavior unchanged.

## Testing

- Cover mixed timeline pagination, realtime-session state at page boundaries, rollout replay, subagent history prefixes, transcript streaming, steering, and promoted agent artifacts.

GitOrigin-RevId: faa2c420192d63e060e8fb32446e3c797422656e
This commit is contained in:
guinness-oai authored and copyberry committed 2026-08-25 01:03:20 +00:00
1 parent 80e871309a
commit 41ca88e95c
80 files changed
+5140 -47

No files matched your search

@@ -648,6 +648,13 @@ mod tests {
.execute(&pool)
.await
.expect("insert item");
sqlx::query(
"INSERT INTO thread_realtime_items (thread_id, item_id, rollout_ordinal, created_at_ms, item_type, item_json) VALUES (?, 'realtime-1', 3, 1, 'realtime_session_started', '{}')",
)
.bind(thread_id_string.as_str())
.execute(&pool)
.await
.expect("insert realtime item");
sqlx::query(
"INSERT INTO thread_history_projection_state (thread_id, next_rollout_byte_offset, next_rollout_ordinal) VALUES (?, 3, 3)",
)
@@ -668,21 +675,23 @@ mod tests {
}
));
assert!(rollout_path.exists());
let counts = sqlx::query_as::<_, (i64, i64, i64)>(
let counts = sqlx::query_as::<_, (i64, i64, i64, i64)>(
r#"
SELECT
(SELECT COUNT(*) FROM thread_turns WHERE thread_id = ?),
(SELECT COUNT(*) FROM thread_items WHERE thread_id = ?),
(SELECT COUNT(*) FROM thread_realtime_items WHERE thread_id = ?),
(SELECT COUNT(*) FROM thread_history_projection_state WHERE thread_id = ?)
"#,
)
.bind(thread_id_string.as_str())
.bind(thread_id_string.as_str())
.bind(thread_id_string.as_str())
.bind(thread_id_string.as_str())
.fetch_one(&pool)
.await
.expect("read preserved history rows");
assert_eq!(counts, (1, 1, 1));
assert_eq!(counts, (1, 1, 1, 1));
}
#[tokio::test]
@@ -725,6 +734,13 @@ SELECT
.execute(&pool)
.await
.expect("insert item");
sqlx::query(
"INSERT INTO thread_realtime_items (thread_id, item_id, rollout_ordinal, created_at_ms, item_type, item_json) VALUES (?, 'realtime-1', 3, 1, 'realtime_session_started', '{}')",
)
.bind(thread_id_string.as_str())
.execute(&pool)
.await
.expect("insert realtime item");
sqlx::query(
"INSERT INTO thread_history_projection_state (thread_id, next_rollout_byte_offset, next_rollout_ordinal) VALUES (?, 3, 3)",
)
@@ -759,21 +775,23 @@ SELECT
.expect("delete thread");
assert!(!lock_path.exists());
let counts = sqlx::query_as::<_, (i64, i64, i64)>(
let counts = sqlx::query_as::<_, (i64, i64, i64, i64)>(
r#"
SELECT
(SELECT COUNT(*) FROM thread_turns WHERE thread_id = ?),
(SELECT COUNT(*) FROM thread_items WHERE thread_id = ?),
(SELECT COUNT(*) FROM thread_realtime_items WHERE thread_id = ?),
(SELECT COUNT(*) FROM thread_history_projection_state WHERE thread_id = ?)
"#,
)
.bind(thread_id_string.as_str())
.bind(thread_id_string.as_str())
.bind(thread_id_string.as_str())
.bind(thread_id_string.as_str())
.fetch_one(&pool)
.await
.expect("read remaining history rows");
assert_eq!(counts, (0, 0, 0));
assert_eq!(counts, (0, 0, 0, 0));
}
#[tokio::test]