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
This commit is contained in:
Charlie Marsh
2026-10-26 18:13:00 +00:00
committed by copyberry
parent cf29971d93
commit a797dd7c6f
206 changed files with 4691 additions and 3089 deletions
+13 -13
View File
@@ -17,7 +17,7 @@ use crate::history_cell::split_reasoning_summary_parts;
use crate::inline_visualization::InlineVisualizationContext;
use crate::legacy_core::config::Config;
use codex_app_server_protocol::Thread;
use codex_app_server_protocol::ThreadItem;
use codex_app_server_protocol::ThreadItemPayload;
use codex_app_server_protocol::UserInput;
use codex_protocol::ThreadId;
use codex_protocol::items::UserMessageItem;
@@ -107,7 +107,7 @@ pub(crate) fn thread_to_transcript_cells(
thread_items_to_transcript_cells(
thread_id,
&cwd,
turn.items,
turn.items.into_iter().map(|entry| entry.payload),
raw_reasoning_visibility,
config,
)
@@ -124,7 +124,7 @@ pub(crate) fn thread_to_transcript_cells(
pub(crate) fn thread_items_to_transcript_cells(
thread_id: Option<ThreadId>,
cwd: &AbsolutePathBuf,
items: impl IntoIterator<Item = ThreadItem>,
items: impl IntoIterator<Item = ThreadItemPayload>,
raw_reasoning_visibility: RawReasoningVisibility,
config: Option<&Config>,
) -> TranscriptCells {
@@ -134,7 +134,7 @@ pub(crate) fn thread_items_to_transcript_cells(
let mut cells: TranscriptCells = Vec::new();
let mut pending = None;
for item in items {
if matches!(item, ThreadItem::Reasoning { .. })
if matches!(item, ThreadItemPayload::Reasoning { .. })
&& let Some(group) = &mut pending
{
for cell in item_to_cells(
@@ -156,7 +156,7 @@ pub(crate) fn thread_items_to_transcript_cells(
continue;
}
match item {
item @ ThreadItem::McpToolCall { .. } => {
item @ ThreadItemPayload::McpToolCall { .. } => {
let Some(call) = tools::McpHistory::from_item(item) else {
PendingActivity::flush(&mut pending, &mut cells);
continue;
@@ -179,7 +179,7 @@ pub(crate) fn thread_items_to_transcript_cells(
cells.push(Arc::new(cell));
}
}
item @ ThreadItem::CommandExecution { .. } => {
item @ ThreadItemPayload::CommandExecution { .. } => {
if matches!(pending, Some(PendingActivity::Computer(_))) {
PendingActivity::flush(&mut pending, &mut cells);
}
@@ -220,14 +220,14 @@ pub(crate) fn thread_items_to_transcript_cells(
/// Project one item without changing the active widget or its turn lifecycle.
fn item_to_cells(
item: ThreadItem,
item: ThreadItemPayload,
cwd: &AbsolutePathBuf,
raw_reasoning_visibility: RawReasoningVisibility,
inline_visualization_context: Option<InlineVisualizationContext>,
) -> TranscriptCells {
let mut cells: TranscriptCells = Vec::new();
match item {
ThreadItem::UserMessage {
ThreadItemPayload::UserMessage {
id,
client_id,
content,
@@ -266,7 +266,7 @@ fn item_to_cells(
remote_image_urls: item.image_urls(),
}));
}
ThreadItem::AgentMessage { text, .. } => {
ThreadItemPayload::AgentMessage { text, .. } => {
let parsed = parse_assistant_markdown(&text, cwd.as_path());
if !parsed.visible_markdown.trim().is_empty() {
cells.push(Arc::new(AgentMarkdownCell::new_with_inline_visualizations(
@@ -276,7 +276,7 @@ fn item_to_cells(
)));
}
}
ThreadItem::FunctionCallOutput {
ThreadItemPayload::FunctionCallOutput {
name,
namespace,
output,
@@ -296,7 +296,7 @@ fn item_to_cells(
)));
}
}
ThreadItem::Plan { text, .. } => {
ThreadItemPayload::Plan { text, .. } => {
if !text.trim().is_empty() {
cells.push(Arc::new(crate::history_cell::new_proposed_plan(
text,
@@ -304,7 +304,7 @@ fn item_to_cells(
)));
}
}
ThreadItem::Reasoning {
ThreadItemPayload::Reasoning {
id,
summary,
content,
@@ -329,7 +329,7 @@ fn item_to_cells(
cells.push(Arc::new(cell));
}
}
item @ ThreadItem::CommandExecution { .. } => {
item @ ThreadItemPayload::CommandExecution { .. } => {
if let Some(command) = tools::CommandHistory::from_item(item) {
cells.push(Arc::new(command.into_cell()));
}