Files
codex/codex-rs/tui/src/thread_transcript/exploration_groups.rs
T
Charlie Marsh a797dd7c6f Expose lifecycle timestamps on app-server thread items (#57172)
## What changed

- Add nullable `startedAtMs` and `completedAtMs` fields to `ThreadItem` in live notifications and turn history. Values are Unix milliseconds when recorded.
- Preserve recorded item times through history reconstruction, pagination, and resume, including command start times recorded during approval.
- Separate the Rust item payload into `ThreadItemPayload` while keeping the JSON payload flat, and update consumers, schemas, and generated TypeScript and Python types.

## Testing

Extend coverage for timestamp serialization, history reconstruction, paginated reads and resume, and persisted Python SDK items.

GitOrigin-RevId: ecbcee95af52db7c807b46a6e9deb30e5fba1f80
2026-10-26 18:13:00 +00:00

80 lines
2.5 KiB
Rust

//! Apply live exploration compatibility to replay and repair groups split by history pages.
use super::tools::CommandHistory;
use crate::exec_cell::ExecCell;
use crate::history_cell::HistoryCell;
use codex_app_server_protocol::ThreadItemPayload;
use codex_app_server_protocol::Turn;
use std::sync::Arc;
pub(crate) fn join_exploration_groups(
older: &Arc<dyn HistoryCell>,
newer: &Arc<dyn HistoryCell>,
turns: &[Turn],
) -> Option<Arc<dyn HistoryCell>> {
let older = older.as_any().downcast_ref::<ExecCell>()?;
let newer = newer.as_any().downcast_ref::<ExecCell>()?;
let items = adjacent_items(older, newer, turns)?;
let mut group = completed_group(items)?;
group.group.details = newer.group.details.clone();
group
.group
.details
.prepend(older.group.details.clone(), older.group.calls.len());
Some(Arc::new(group))
}
pub(crate) fn older_exploration_group(
older: &dyn HistoryCell,
newer: &ExecCell,
turns: &[Turn],
) -> Option<ExecCell> {
let older = older.as_any().downcast_ref::<ExecCell>()?;
let items = adjacent_items(older, newer, turns)?;
let older_items = items
.iter()
.filter(|entry| !matches!(&entry.payload, ThreadItemPayload::Reasoning { .. }))
.take(older.group.calls.len())
.cloned()
.collect::<Vec<_>>();
let mut group = completed_group(&older_items)?;
group.group.details = older.group.details.clone();
Some(group)
}
fn adjacent_items<'a>(
older: &ExecCell,
newer: &ExecCell,
turns: &'a [Turn],
) -> Option<&'a [codex_app_server_protocol::ThreadItem]> {
if !older.is_exploring_cell() || older.should_flush() || !newer.is_exploring_cell() {
return None;
}
let ids = older
.iter_calls()
.chain(newer.iter_calls())
.map(|call| call.call_id.as_str())
.collect::<Vec<_>>();
super::computer_groups::adjacent_activity_items(&ids, turns)
}
pub(super) fn completed_group(items: &[codex_app_server_protocol::ThreadItem]) -> Option<ExecCell> {
let mut group: Option<ExecCell> = None;
for entry in items {
let item = &entry.payload;
if matches!(item, ThreadItemPayload::Reasoning { .. }) {
continue;
}
let call = CommandHistory::from_item(item.clone())?.into_cell();
if !call.is_exploring_cell() {
return None;
}
if let Some(group) = &mut group {
group.append_completed(call).ok()?;
} else {
group = Some(call);
}
}
group
}