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
2026-08-25 01:03:20 +00:00
committed by copyberry
parent 80e871309a
commit 41ca88e95c
80 changed files with 5140 additions and 47 deletions
@@ -870,6 +870,35 @@ impl ThreadRequestProcessor {
.map(|response| Some(response.into()))
}
pub(crate) async fn thread_timeline_list(
&self,
params: ThreadTimelineListParams,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
let thread_id = ThreadId::from_string(&params.thread_id)
.map_err(|error| invalid_request(format!("invalid thread id: {error}")))?;
let page = self
.thread_store
.list_timeline(StoreListTimelineParams {
thread_id,
cursor: params.cursor,
page_size: params
.limit
.map(|limit| limit as usize)
.unwrap_or(THREAD_ITEMS_DEFAULT_LIMIT)
.clamp(1, THREAD_ITEMS_MAX_LIMIT),
})
.await
.map_err(paginated_history_list_error)?;
Ok(Some(
ThreadTimelineListResponse {
data: page.items,
next_cursor: page.next_cursor,
active_realtime_session_at_page_start: page.active_realtime_session_at_page_start,
}
.into(),
))
}
pub(crate) async fn thread_shell_command(
&self,
request_id: &ConnectionRequestId,