Compare commits

...

2 Commits

Author SHA1 Message Date
rhan-oai
89bba5d2c3 [codex-analytics] add tool item event schemas 2026-05-02 23:28:33 -07:00
rhan-oai
73ff402725 [codex-analytics] add core item timing production 2026-05-02 20:56:20 -07:00
26 changed files with 553 additions and 6 deletions

View File

@@ -3,12 +3,17 @@ use crate::events::AppServerRpcTransport;
use crate::events::CodexAppMentionedEventRequest;
use crate::events::CodexAppServerClientMetadata;
use crate::events::CodexAppUsedEventRequest;
use crate::events::CodexCommandExecutionEventParams;
use crate::events::CodexCommandExecutionEventRequest;
use crate::events::CodexCompactionEventRequest;
use crate::events::CodexHookRunEventRequest;
use crate::events::CodexPluginEventRequest;
use crate::events::CodexPluginUsedEventRequest;
use crate::events::CodexRuntimeMetadata;
use crate::events::CodexToolItemEventBase;
use crate::events::CodexTurnEventRequest;
use crate::events::CommandExecutionFamily;
use crate::events::CommandExecutionSource;
use crate::events::GuardianApprovalRequestSource;
use crate::events::GuardianReviewDecision;
use crate::events::GuardianReviewEventParams;
@@ -17,6 +22,8 @@ use crate::events::GuardianReviewTerminalStatus;
use crate::events::GuardianReviewedAction;
use crate::events::ThreadInitializedEvent;
use crate::events::ThreadInitializedEventParams;
use crate::events::ToolItemFinalApprovalOutcome;
use crate::events::ToolItemTerminalStatus;
use crate::events::TrackEventRequest;
use crate::events::codex_app_metadata;
use crate::events::codex_hook_run_metadata;
@@ -884,6 +891,103 @@ fn thread_initialized_event_serializes_expected_shape() {
);
}
#[test]
fn command_execution_event_serializes_expected_shape() {
let event = TrackEventRequest::CommandExecution(CodexCommandExecutionEventRequest {
event_type: "codex_command_execution_event",
event_params: CodexCommandExecutionEventParams {
base: CodexToolItemEventBase {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item_id: "item-1".to_string(),
app_server_client: CodexAppServerClientMetadata {
product_client_id: "codex_tui".to_string(),
client_name: Some("codex-tui".to_string()),
client_version: Some("1.2.3".to_string()),
rpc_transport: AppServerRpcTransport::Websocket,
experimental_api_enabled: Some(true),
},
runtime: CodexRuntimeMetadata {
codex_rs_version: "0.99.0".to_string(),
runtime_os: "macos".to_string(),
runtime_os_version: "15.3.1".to_string(),
runtime_arch: "aarch64".to_string(),
},
thread_source: Some("user"),
subagent_source: None,
parent_thread_id: None,
tool_name: "shell".to_string(),
started_at_ms: 123_000,
completed_at_ms: Some(125_000),
duration_ms: Some(2000),
execution_duration_ms: Some(1800),
execution_started: true,
review_count: 0,
guardian_review_count: 0,
user_review_count: 0,
final_approval_outcome: ToolItemFinalApprovalOutcome::NotNeeded,
terminal_status: ToolItemTerminalStatus::Completed,
failure_kind: None,
requested_additional_permissions: false,
requested_network_access: false,
retry_count: 0,
},
command_execution_source: CommandExecutionSource::Agent,
command_execution_family: CommandExecutionFamily::Shell,
exit_code: Some(0),
command_action_count: Some(1),
},
});
let payload = serde_json::to_value(&event).expect("serialize command execution event");
assert_eq!(
payload,
json!({
"event_type": "codex_command_execution_event",
"event_params": {
"thread_id": "thread-1",
"turn_id": "turn-1",
"item_id": "item-1",
"app_server_client": {
"product_client_id": "codex_tui",
"client_name": "codex-tui",
"client_version": "1.2.3",
"rpc_transport": "websocket",
"experimental_api_enabled": true
},
"runtime": {
"codex_rs_version": "0.99.0",
"runtime_os": "macos",
"runtime_os_version": "15.3.1",
"runtime_arch": "aarch64"
},
"thread_source": "user",
"subagent_source": null,
"parent_thread_id": null,
"tool_name": "shell",
"started_at_ms": 123000,
"completed_at_ms": 125000,
"duration_ms": 2000,
"execution_duration_ms": 1800,
"execution_started": true,
"review_count": 0,
"guardian_review_count": 0,
"user_review_count": 0,
"final_approval_outcome": "not_needed",
"terminal_status": "completed",
"failure_kind": null,
"requested_additional_permissions": false,
"requested_network_access": false,
"retry_count": 0,
"command_execution_source": "agent",
"command_execution_family": "shell",
"exit_code": 0,
"command_action_count": 1
}
})
);
}
#[tokio::test]
async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialized() {
let mut reducer = AnalyticsReducer::default();

View File

@@ -61,6 +61,20 @@ pub(crate) enum TrackEventRequest {
Compaction(Box<CodexCompactionEventRequest>),
TurnEvent(Box<CodexTurnEventRequest>),
TurnSteer(CodexTurnSteerEventRequest),
#[allow(dead_code)]
CommandExecution(CodexCommandExecutionEventRequest),
#[allow(dead_code)]
FileChange(CodexFileChangeEventRequest),
#[allow(dead_code)]
McpToolCall(CodexMcpToolCallEventRequest),
#[allow(dead_code)]
DynamicToolCall(CodexDynamicToolCallEventRequest),
#[allow(dead_code)]
CollabAgentToolCall(CodexCollabAgentToolCallEventRequest),
#[allow(dead_code)]
WebSearch(CodexWebSearchEventRequest),
#[allow(dead_code)]
ImageGeneration(CodexImageGenerationEventRequest),
PluginUsed(CodexPluginUsedEventRequest),
PluginInstalled(CodexPluginEventRequest),
PluginUninstalled(CodexPluginEventRequest),
@@ -384,6 +398,218 @@ pub(crate) struct GuardianReviewEventPayload {
pub(crate) guardian_review: GuardianReviewEventParams,
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ToolItemFinalApprovalOutcome {
Unknown,
NotNeeded,
ConfigAllowed,
PolicyForbidden,
GuardianApproved,
GuardianDenied,
GuardianAborted,
UserApproved,
UserApprovedForSession,
UserDenied,
UserAborted,
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ToolItemTerminalStatus {
Completed,
Failed,
Rejected,
Interrupted,
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ToolItemFailureKind {
ToolError,
ApprovalDenied,
ApprovalAborted,
SandboxDenied,
PolicyForbidden,
}
#[derive(Serialize)]
pub(crate) struct CodexToolItemEventBase {
pub(crate) thread_id: String,
pub(crate) turn_id: String,
/// App-server ThreadItem.id. For tool-originated items this generally
/// corresponds to the originating core call_id.
pub(crate) item_id: String,
pub(crate) app_server_client: CodexAppServerClientMetadata,
pub(crate) runtime: CodexRuntimeMetadata,
pub(crate) thread_source: Option<&'static str>,
pub(crate) subagent_source: Option<String>,
pub(crate) parent_thread_id: Option<String>,
pub(crate) tool_name: String,
pub(crate) started_at_ms: u64,
pub(crate) completed_at_ms: Option<u64>,
pub(crate) duration_ms: Option<u64>,
pub(crate) execution_duration_ms: Option<u64>,
pub(crate) execution_started: bool,
pub(crate) review_count: u64,
pub(crate) guardian_review_count: u64,
pub(crate) user_review_count: u64,
pub(crate) final_approval_outcome: ToolItemFinalApprovalOutcome,
pub(crate) terminal_status: ToolItemTerminalStatus,
pub(crate) failure_kind: Option<ToolItemFailureKind>,
pub(crate) requested_additional_permissions: bool,
pub(crate) requested_network_access: bool,
pub(crate) retry_count: u64,
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum CommandExecutionFamily {
Shell,
UserShell,
UnifiedExec,
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum CommandExecutionSource {
Agent,
UserShell,
UnifiedExecStartup,
UnifiedExecInteraction,
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum WebSearchActionKind {
Search,
OpenPage,
FindInPage,
Other,
}
#[derive(Serialize)]
pub(crate) struct CodexCommandExecutionEventParams {
#[serde(flatten)]
pub(crate) base: CodexToolItemEventBase,
pub(crate) command_execution_source: CommandExecutionSource,
pub(crate) command_execution_family: CommandExecutionFamily,
pub(crate) exit_code: Option<i32>,
pub(crate) command_action_count: Option<u64>,
}
#[derive(Serialize)]
pub(crate) struct CodexCommandExecutionEventRequest {
pub(crate) event_type: &'static str,
pub(crate) event_params: CodexCommandExecutionEventParams,
}
#[derive(Serialize)]
pub(crate) struct CodexFileChangeEventParams {
#[serde(flatten)]
pub(crate) base: CodexToolItemEventBase,
pub(crate) file_change_count: u64,
pub(crate) file_add_count: u64,
pub(crate) file_update_count: u64,
pub(crate) file_delete_count: u64,
pub(crate) file_move_count: u64,
}
#[derive(Serialize)]
pub(crate) struct CodexFileChangeEventRequest {
pub(crate) event_type: &'static str,
pub(crate) event_params: CodexFileChangeEventParams,
}
#[derive(Serialize)]
pub(crate) struct CodexMcpToolCallEventParams {
#[serde(flatten)]
pub(crate) base: CodexToolItemEventBase,
pub(crate) mcp_server_name: String,
pub(crate) mcp_tool_name: String,
pub(crate) mcp_error_present: bool,
pub(crate) mcp_error_code: Option<String>,
}
#[derive(Serialize)]
pub(crate) struct CodexMcpToolCallEventRequest {
pub(crate) event_type: &'static str,
pub(crate) event_params: CodexMcpToolCallEventParams,
}
#[derive(Serialize)]
pub(crate) struct CodexDynamicToolCallEventParams {
#[serde(flatten)]
pub(crate) base: CodexToolItemEventBase,
pub(crate) dynamic_tool_name: String,
pub(crate) success: Option<bool>,
pub(crate) output_content_item_count: Option<u64>,
pub(crate) output_text_item_count: Option<u64>,
pub(crate) output_image_item_count: Option<u64>,
}
#[derive(Serialize)]
pub(crate) struct CodexDynamicToolCallEventRequest {
pub(crate) event_type: &'static str,
pub(crate) event_params: CodexDynamicToolCallEventParams,
}
#[derive(Serialize)]
pub(crate) struct CodexCollabAgentToolCallEventParams {
#[serde(flatten)]
pub(crate) base: CodexToolItemEventBase,
pub(crate) sender_thread_id: String,
pub(crate) receiver_thread_count: u64,
pub(crate) receiver_thread_ids: Option<Vec<String>>,
pub(crate) requested_model: Option<String>,
pub(crate) requested_reasoning_effort: Option<String>,
pub(crate) agent_state_count: Option<u64>,
pub(crate) completed_agent_count: Option<u64>,
pub(crate) failed_agent_count: Option<u64>,
}
#[derive(Serialize)]
pub(crate) struct CodexCollabAgentToolCallEventRequest {
pub(crate) event_type: &'static str,
pub(crate) event_params: CodexCollabAgentToolCallEventParams,
}
#[derive(Serialize)]
pub(crate) struct CodexWebSearchEventParams {
#[serde(flatten)]
pub(crate) base: CodexToolItemEventBase,
pub(crate) web_search_action: Option<WebSearchActionKind>,
pub(crate) query_present: bool,
pub(crate) query_count: Option<u64>,
}
#[derive(Serialize)]
pub(crate) struct CodexWebSearchEventRequest {
pub(crate) event_type: &'static str,
pub(crate) event_params: CodexWebSearchEventParams,
}
#[derive(Serialize)]
pub(crate) struct CodexImageGenerationEventParams {
#[serde(flatten)]
pub(crate) base: CodexToolItemEventBase,
pub(crate) image_generation_status: String,
pub(crate) revised_prompt_present: bool,
pub(crate) saved_path_present: bool,
}
#[derive(Serialize)]
pub(crate) struct CodexImageGenerationEventRequest {
pub(crate) event_type: &'static str,
pub(crate) event_params: CodexImageGenerationEventParams,
}
#[derive(Serialize)]
pub(crate) struct CodexAppMetadata {
pub(crate) connector_id: Option<String>,

View File

@@ -1134,6 +1134,7 @@ mod tests {
ServerNotification::ItemCompleted(codex_app_server_protocol::ItemCompletedNotification {
thread_id: "thread".to_string(),
turn_id: "turn".to_string(),
completed_at_ms: None,
item: codex_app_server_protocol::ThreadItem::AgentMessage {
id: "item".to_string(),
text: text.to_string(),
@@ -2007,6 +2008,7 @@ mod tests {
codex_app_server_protocol::ItemCompletedNotification {
thread_id: "thread".to_string(),
turn_id: "turn".to_string(),
completed_at_ms: None,
item: codex_app_server_protocol::ThreadItem::AgentMessage {
id: "item".to_string(),
text: "hello".to_string(),

View File

@@ -1932,6 +1932,14 @@
},
"ItemCompletedNotification": {
"properties": {
"completedAtMs": {
"description": "Unix timestamp (in milliseconds) when this item lifecycle completed, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"item": {
"$ref": "#/definitions/ThreadItem"
},
@@ -2030,6 +2038,14 @@
"item": {
"$ref": "#/definitions/ThreadItem"
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this item lifecycle started, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"threadId": {
"type": "string"
},

View File

@@ -10109,6 +10109,14 @@
"ItemCompletedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"completedAtMs": {
"description": "Unix timestamp (in milliseconds) when this item lifecycle completed, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"item": {
"$ref": "#/definitions/v2/ThreadItem"
},
@@ -10213,6 +10221,14 @@
"item": {
"$ref": "#/definitions/v2/ThreadItem"
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this item lifecycle started, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"threadId": {
"type": "string"
},

View File

@@ -6762,6 +6762,14 @@
"ItemCompletedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"completedAtMs": {
"description": "Unix timestamp (in milliseconds) when this item lifecycle completed, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"item": {
"$ref": "#/definitions/ThreadItem"
},
@@ -6866,6 +6874,14 @@
"item": {
"$ref": "#/definitions/ThreadItem"
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this item lifecycle started, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"threadId": {
"type": "string"
},

View File

@@ -1370,6 +1370,14 @@
}
},
"properties": {
"completedAtMs": {
"description": "Unix timestamp (in milliseconds) when this item lifecycle completed, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"item": {
"$ref": "#/definitions/ThreadItem"
},

View File

@@ -1373,6 +1373,14 @@
"item": {
"$ref": "#/definitions/ThreadItem"
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this item lifecycle started, if known.",
"format": "int64",
"type": [
"integer",
"null"
]
},
"threadId": {
"type": "string"
},

View File

@@ -3,4 +3,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ThreadItem } from "./ThreadItem";
export type ItemCompletedNotification = { item: ThreadItem, threadId: string, turnId: string, };
export type ItemCompletedNotification = { item: ThreadItem, threadId: string, turnId: string,
/**
* Unix timestamp (in milliseconds) when this item lifecycle completed, if known.
*/
completedAtMs: number | null, };

View File

@@ -3,4 +3,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ThreadItem } from "./ThreadItem";
export type ItemStartedNotification = { item: ThreadItem, threadId: string, turnId: string, };
export type ItemStartedNotification = { item: ThreadItem, threadId: string, turnId: string,
/**
* Unix timestamp (in milliseconds) when this item lifecycle started, if known.
*/
startedAtMs: number | null, };

View File

@@ -73,6 +73,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id: response.turn_id,
item,
completed_at_ms: None,
})
}
EventMsg::McpToolCallBegin(begin_event) => {
@@ -91,6 +92,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
started_at_ms: None,
})
}
EventMsg::McpToolCallEnd(end_event) => {
@@ -131,6 +133,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
completed_at_ms: None,
})
}
EventMsg::CollabAgentSpawnBegin(begin_event) => {
@@ -149,6 +152,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
started_at_ms: None,
})
}
EventMsg::CollabAgentSpawnEnd(end_event) => {
@@ -187,6 +191,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
completed_at_ms: None,
})
}
EventMsg::CollabAgentInteractionBegin(begin_event) => {
@@ -206,6 +211,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
started_at_ms: None,
})
}
EventMsg::CollabAgentInteractionEnd(end_event) => {
@@ -233,6 +239,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
completed_at_ms: None,
})
}
EventMsg::CollabWaitingBegin(begin_event) => {
@@ -256,6 +263,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
started_at_ms: None,
})
}
EventMsg::CollabWaitingEnd(end_event) => {
@@ -291,6 +299,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
completed_at_ms: None,
})
}
EventMsg::CollabCloseBegin(begin_event) => {
@@ -309,6 +318,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
started_at_ms: None,
})
}
EventMsg::CollabCloseEnd(end_event) => {
@@ -341,6 +351,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
completed_at_ms: None,
})
}
EventMsg::CollabResumeBegin(begin_event) => {
@@ -359,6 +370,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
started_at_ms: None,
})
}
EventMsg::CollabResumeEnd(end_event) => {
@@ -391,6 +403,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item,
completed_at_ms: None,
})
}
EventMsg::AgentMessageContentDelta(event) => {
@@ -440,6 +453,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item: item_started_event.item.into(),
started_at_ms: item_started_event.started_at_ms,
})
}
EventMsg::ItemCompleted(item_completed_event) => {
@@ -447,6 +461,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item: item_completed_event.item.into(),
completed_at_ms: item_completed_event.completed_at_ms,
})
}
EventMsg::PatchApplyUpdated(event) => {
@@ -462,6 +477,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item: build_command_execution_begin_item(&exec_command_begin_event),
started_at_ms: None,
})
}
EventMsg::ExecCommandOutputDelta(exec_command_output_delta_event) => {
@@ -490,6 +506,7 @@ pub fn item_event_to_server_notification(
thread_id,
turn_id,
item: build_command_execution_end_item(&exec_command_end_event),
completed_at_ms: None,
})
}
_ => unreachable!("unsupported item event"),
@@ -564,6 +581,7 @@ mod tests {
ItemStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: None,
item: ThreadItem::CollabAgentToolCall {
id: event.call_id,
tool: CollabAgentTool::ResumeAgent,
@@ -601,6 +619,7 @@ mod tests {
ItemCompletedNotification {
thread_id: "thread-2".to_string(),
turn_id: "turn-2".to_string(),
completed_at_ms: None,
item: ThreadItem::CollabAgentToolCall {
id: event.call_id,
tool: CollabAgentTool::ResumeAgent,
@@ -643,6 +662,7 @@ mod tests {
ItemStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn_1".to_string(),
started_at_ms: None,
item: ThreadItem::McpToolCall {
id: begin_event.call_id,
server: begin_event.invocation.server,
@@ -680,6 +700,7 @@ mod tests {
ItemStartedNotification {
thread_id: "thread-2".to_string(),
turn_id: "turn_2".to_string(),
started_at_ms: None,
item: ThreadItem::McpToolCall {
id: begin_event.call_id,
server: begin_event.invocation.server,
@@ -732,6 +753,7 @@ mod tests {
ItemCompletedNotification {
thread_id: "thread-3".to_string(),
turn_id: "turn_3".to_string(),
completed_at_ms: None,
item: ThreadItem::McpToolCall {
id: end_event.call_id,
server: end_event.invocation.server,
@@ -777,6 +799,7 @@ mod tests {
ItemCompletedNotification {
thread_id: "thread-4".to_string(),
turn_id: "turn_4".to_string(),
completed_at_ms: None,
item: ThreadItem::McpToolCall {
id: end_event.call_id,
server: end_event.invocation.server,

View File

@@ -1354,6 +1354,7 @@ mod tests {
id: "user-item-id".to_string(),
content: Vec::new(),
}),
started_at_ms: None,
}),
EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: turn_id.to_string(),

View File

@@ -6878,6 +6878,9 @@ pub struct ItemStartedNotification {
pub item: ThreadItem,
pub thread_id: String,
pub turn_id: String,
/// Unix timestamp (in milliseconds) when this item lifecycle started, if known.
#[ts(type = "number | null")]
pub started_at_ms: Option<i64>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -6940,6 +6943,9 @@ pub struct ItemCompletedNotification {
pub item: ThreadItem,
pub thread_id: String,
pub turn_id: String,
/// Unix timestamp (in milliseconds) when this item lifecycle completed, if known.
#[ts(type = "number | null")]
pub completed_at_ms: Option<i64>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]

View File

@@ -817,6 +817,7 @@ pub(crate) async fn apply_bespoke_event_handling(
thread_id: conversation_id.to_string(),
turn_id: turn_id.clone(),
item,
started_at_ms: None,
};
outgoing
.send_server_notification(ServerNotification::ItemStarted(notification))
@@ -967,6 +968,7 @@ pub(crate) async fn apply_bespoke_event_handling(
thread_id: conversation_id.to_string(),
turn_id: event_turn_id.clone(),
item: item.clone(),
started_at_ms: None,
};
outgoing
.send_server_notification(ServerNotification::ItemStarted(started))
@@ -975,6 +977,7 @@ pub(crate) async fn apply_bespoke_event_handling(
thread_id: conversation_id.to_string(),
turn_id: event_turn_id.clone(),
item,
completed_at_ms: None,
};
outgoing
.send_server_notification(ServerNotification::ItemCompleted(completed))
@@ -1024,6 +1027,7 @@ pub(crate) async fn apply_bespoke_event_handling(
thread_id: conversation_id.to_string(),
turn_id: event_turn_id.clone(),
item: item.clone(),
started_at_ms: None,
};
outgoing
.send_server_notification(ServerNotification::ItemStarted(started))
@@ -1032,6 +1036,7 @@ pub(crate) async fn apply_bespoke_event_handling(
thread_id: conversation_id.to_string(),
turn_id: event_turn_id.clone(),
item,
completed_at_ms: None,
};
outgoing
.send_server_notification(ServerNotification::ItemCompleted(completed))
@@ -1378,6 +1383,7 @@ async fn start_command_execution_item(
exit_code: None,
duration_ms: None,
},
started_at_ms: None,
};
outgoing
.send_server_notification(ServerNotification::ItemStarted(notification))
@@ -1426,6 +1432,7 @@ async fn complete_command_execution_item(
thread_id: conversation_id.to_string(),
turn_id,
item,
completed_at_ms: None,
};
outgoing
.send_server_notification(ServerNotification::ItemCompleted(notification))
@@ -1480,6 +1487,7 @@ pub(crate) async fn maybe_emit_hook_prompt_item_completed(
.map(codex_app_server_protocol::HookPromptFragment::from)
.collect(),
},
completed_at_ms: None,
};
outgoing
.send_server_notification(ServerNotification::ItemCompleted(notification))

View File

@@ -41,6 +41,7 @@ use crate::session_prefix::format_subagent_notification_message;
use crate::skills::SkillRenderSideEffects;
use crate::skills_load_input_from_config;
use crate::turn_metadata::TurnMetadataState;
use crate::turn_timing::now_unix_timestamp_ms;
use async_channel::Receiver;
use async_channel::Sender;
use chrono::Local;
@@ -1641,6 +1642,7 @@ impl Session {
thread_id: self.conversation_id,
turn_id: turn_context.sub_id.clone(),
item: item.clone(),
started_at_ms: Some(now_unix_timestamp_ms()),
}),
)
.await;
@@ -1658,6 +1660,7 @@ impl Session {
thread_id: self.conversation_id,
turn_id: turn_context.sub_id.clone(),
item,
completed_at_ms: Some(now_unix_timestamp_ms()),
}),
)
.await;

View File

@@ -107,7 +107,7 @@ fn now_unix_timestamp_secs() -> i64 {
now_unix_timestamp_ms() / 1000
}
fn now_unix_timestamp_ms() -> i64 {
pub(crate) fn now_unix_timestamp_ms() -> i64 {
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();

View File

@@ -303,6 +303,15 @@ async fn web_search_item_is_emitted() -> anyhow::Result<()> {
})
.await?;
let started = wait_for_event_match(&codex, |ev| match ev {
EventMsg::ItemStarted(ItemStartedEvent {
item: TurnItem::WebSearch(item),
started_at_ms,
..
}) => Some((item.clone(), *started_at_ms)),
_ => None,
})
.await;
let begin = wait_for_event_match(&codex, |ev| match ev {
EventMsg::WebSearchBegin(event) => Some(event.clone()),
_ => None,
@@ -311,16 +320,20 @@ async fn web_search_item_is_emitted() -> anyhow::Result<()> {
let completed = wait_for_event_match(&codex, |ev| match ev {
EventMsg::ItemCompleted(ItemCompletedEvent {
item: TurnItem::WebSearch(item),
completed_at_ms,
..
}) => Some(item.clone()),
}) => Some((item.clone(), *completed_at_ms)),
_ => None,
})
.await;
assert_eq!(begin.call_id, "web-search-1");
assert_eq!(completed.id, begin.call_id);
assert_eq!(started.0.id, begin.call_id);
assert!(started.1.is_some());
assert_eq!(completed.0.id, begin.call_id);
assert!(completed.1.is_some());
assert_eq!(
completed.action,
completed.0.action,
WebSearchAction::Search {
query: Some("weather seattle".to_string()),
queries: None,
@@ -369,11 +382,29 @@ async fn image_generation_call_event_is_emitted() -> anyhow::Result<()> {
})
.await?;
let started = wait_for_event_match(&codex, |ev| match ev {
EventMsg::ItemStarted(ItemStartedEvent {
item: TurnItem::ImageGeneration(item),
started_at_ms,
..
}) => Some((item.clone(), *started_at_ms)),
_ => None,
})
.await;
let begin = wait_for_event_match(&codex, |ev| match ev {
EventMsg::ImageGenerationBegin(event) => Some(event.clone()),
_ => None,
})
.await;
let completed = wait_for_event_match(&codex, |ev| match ev {
EventMsg::ItemCompleted(ItemCompletedEvent {
item: TurnItem::ImageGeneration(item),
completed_at_ms,
..
}) => Some((item.clone(), *completed_at_ms)),
_ => None,
})
.await;
let end = wait_for_event_match(&codex, |ev| match ev {
EventMsg::ImageGenerationEnd(event) => Some(event.clone()),
_ => None,
@@ -381,6 +412,10 @@ async fn image_generation_call_event_is_emitted() -> anyhow::Result<()> {
.await;
assert_eq!(begin.call_id, call_id);
assert_eq!(started.0.id, call_id);
assert!(started.1.is_some());
assert_eq!(completed.0.id, call_id);
assert!(completed.1.is_some());
assert_eq!(end.call_id, call_id);
assert_eq!(end.status, "completed");
assert_eq!(end.revised_prompt, Some("A tiny blue square".to_string()));

View File

@@ -20,6 +20,7 @@ fn failed_turn_does_not_overwrite_output_last_message_file() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
},
));

View File

@@ -181,6 +181,7 @@ fn command_execution_started_and_completed_translate_to_thread_events() {
item: command_item,
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: None,
}));
assert_eq!(
started,
@@ -216,6 +217,7 @@ fn command_execution_started_and_completed_translate_to_thread_events() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
},
));
assert_eq!(
@@ -250,6 +252,7 @@ fn empty_reasoning_items_are_ignored() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
},
));
@@ -274,6 +277,7 @@ fn unsupported_items_do_not_consume_synthetic_ids() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
},
));
@@ -295,6 +299,7 @@ fn unsupported_items_do_not_consume_synthetic_ids() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
},
));
@@ -327,6 +332,7 @@ fn reasoning_items_emit_summary_not_raw_content() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
},
));
@@ -362,6 +368,7 @@ fn web_search_completion_preserves_query_and_action() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
},
));
@@ -399,6 +406,7 @@ fn web_search_start_and_completion_reuse_item_id() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: None,
}));
let completed = processor.collect_thread_events(ServerNotification::ItemCompleted(
@@ -413,6 +421,7 @@ fn web_search_start_and_completion_reuse_item_id() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
},
));
@@ -472,6 +481,7 @@ fn mcp_tool_call_begin_and_end_emit_item_events() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: None,
}));
let completed = processor.collect_thread_events(ServerNotification::ItemCompleted(
ItemCompletedNotification {
@@ -492,6 +502,7 @@ fn mcp_tool_call_begin_and_end_emit_item_events() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
},
));
@@ -559,6 +570,7 @@ fn mcp_tool_call_failure_sets_failed_status() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
},
));
@@ -604,6 +616,7 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: None,
}));
let completed = processor.collect_thread_events(ServerNotification::ItemCompleted(
ItemCompletedNotification {
@@ -627,6 +640,7 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
},
));
@@ -695,6 +709,7 @@ fn collab_spawn_begin_and_end_emit_item_events() {
},
thread_id: "thread-parent".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: None,
}));
let completed = processor.collect_thread_events(ServerNotification::ItemCompleted(
ItemCompletedNotification {
@@ -717,6 +732,7 @@ fn collab_spawn_begin_and_end_emit_item_events() {
},
thread_id: "thread-parent".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
},
));
@@ -795,6 +811,7 @@ fn file_change_completion_maps_change_kinds() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
},
));
@@ -845,6 +862,7 @@ fn file_change_declined_maps_to_failed_status() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
},
));
@@ -882,6 +900,7 @@ fn agent_message_item_updates_final_message() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
},
));
@@ -916,6 +935,7 @@ fn agent_message_item_started_is_ignored() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: None,
}));
assert_eq!(
@@ -940,6 +960,7 @@ fn reasoning_item_completed_uses_synthetic_id() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
},
));
@@ -1296,6 +1317,7 @@ fn turn_completion_reconciles_started_items_from_turn_items() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: None,
}));
assert_eq!(
started,
@@ -1378,6 +1400,7 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
},
));
@@ -1426,6 +1449,7 @@ fn turn_completion_preserves_streamed_final_message_when_turn_items_are_empty()
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
},
));
@@ -1470,6 +1494,7 @@ fn failed_turn_clears_stale_final_message() {
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
},
));

View File

@@ -1828,6 +1828,9 @@ pub struct ItemStartedEvent {
pub thread_id: ThreadId,
pub turn_id: String,
pub item: TurnItem,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub started_at_ms: Option<i64>,
}
impl HasLegacyEvent for ItemStartedEvent {
@@ -1853,6 +1856,9 @@ pub struct ItemCompletedEvent {
pub thread_id: ThreadId,
pub turn_id: String,
pub item: TurnItem,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub completed_at_ms: Option<i64>,
}
pub trait HasLegacyEvent {
@@ -4592,6 +4598,7 @@ mod tests {
queries: None,
},
}),
started_at_ms: None,
};
let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false);
@@ -4608,6 +4615,7 @@ mod tests {
thread_id: ThreadId::new(),
turn_id: "turn-1".into(),
item: TurnItem::UserMessage(UserMessageItem::new(&[])),
started_at_ms: None,
};
assert!(
@@ -4629,6 +4637,7 @@ mod tests {
result: String::new(),
saved_path: None,
}),
started_at_ms: None,
};
let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false);
@@ -4659,6 +4668,7 @@ mod tests {
stdout: None,
stderr: None,
}),
started_at_ms: None,
};
let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false);
@@ -4686,6 +4696,7 @@ mod tests {
result: "Zm9v".into(),
saved_path: Some(test_path_buf("/tmp/ig-1.png").abs()),
}),
completed_at_ms: None,
};
let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false);
@@ -4725,6 +4736,7 @@ mod tests {
stdout: Some("Done!".into()),
stderr: Some(String::new()),
}),
completed_at_ms: None,
};
let legacy_events = event.as_legacy_events(/*show_raw_agent_reasoning*/ false);

View File

@@ -2597,6 +2597,7 @@ async fn inactive_thread_file_change_approval_recovers_buffered_changes() {
ServerNotification::ItemStarted(ItemStartedNotification {
thread_id: thread_id.to_string(),
turn_id: "turn-approval".to_string(),
started_at_ms: None,
item: ThreadItem::FileChange {
id: "patch-approval".to_string(),
changes: vec![FileUpdateChange {
@@ -4697,6 +4698,7 @@ async fn replace_chat_widget_reseeds_collab_agent_metadata_for_replay() {
codex_app_server_protocol::ItemStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: None,
item: ThreadItem::CollabAgentToolCall {
id: "wait-1".to_string(),
tool: codex_app_server_protocol::CollabAgentTool::Wait,

View File

@@ -16,6 +16,7 @@ async fn collab_spawn_end_shows_requested_model_and_effort() {
ServerNotification::ItemStarted(ItemStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: None,
item: AppServerThreadItem::CollabAgentToolCall {
id: "call-spawn".to_string(),
tool: AppServerCollabAgentTool::SpawnAgent,
@@ -34,6 +35,7 @@ async fn collab_spawn_end_shows_requested_model_and_effort() {
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
item: AppServerThreadItem::CollabAgentToolCall {
id: "call-spawn".to_string(),
tool: AppServerCollabAgentTool::SpawnAgent,
@@ -90,6 +92,7 @@ async fn live_app_server_user_message_item_completed_does_not_duplicate_rendered
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
item: AppServerThreadItem::UserMessage {
id: "user-1".to_string(),
content: vec![AppServerUserInput::Text {
@@ -135,6 +138,7 @@ async fn live_app_server_turn_completed_clears_working_status_after_answer_item(
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
item: AppServerThreadItem::AgentMessage {
id: "msg-1".to_string(),
text: "Yes. What do you need?".to_string(),
@@ -287,6 +291,7 @@ async fn live_app_server_file_change_item_started_preserves_changes() {
ServerNotification::ItemStarted(ItemStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: None,
item: AppServerThreadItem::FileChange {
id: "patch-1".to_string(),
changes: vec![FileUpdateChange {
@@ -320,6 +325,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() {
ServerNotification::ItemStarted(ItemStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: None,
item: AppServerThreadItem::CommandExecution {
id: "cmd-1".to_string(),
command: command.clone(),
@@ -341,6 +347,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() {
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
item: AppServerThreadItem::CommandExecution {
id: "cmd-1".to_string(),
command,
@@ -396,6 +403,7 @@ async fn live_app_server_collab_wait_items_render_history() {
ServerNotification::ItemStarted(ItemStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: None,
item: AppServerThreadItem::CollabAgentToolCall {
id: "wait-1".to_string(),
tool: AppServerCollabAgentTool::Wait,
@@ -418,6 +426,7 @@ async fn live_app_server_collab_wait_items_render_history() {
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
item: AppServerThreadItem::CollabAgentToolCall {
id: "wait-1".to_string(),
tool: AppServerCollabAgentTool::Wait,
@@ -471,6 +480,7 @@ async fn live_app_server_collab_spawn_completed_renders_requested_model_and_effo
ServerNotification::ItemStarted(ItemStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: None,
item: AppServerThreadItem::CollabAgentToolCall {
id: "spawn-1".to_string(),
tool: AppServerCollabAgentTool::SpawnAgent,
@@ -490,6 +500,7 @@ async fn live_app_server_collab_spawn_completed_renders_requested_model_and_effo
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
item: AppServerThreadItem::CollabAgentToolCall {
id: "spawn-1".to_string(),
tool: AppServerCollabAgentTool::SpawnAgent,

View File

@@ -654,6 +654,7 @@ pub(super) fn handle_agent_reasoning_final(chat: &mut ChatWidget) {
.last_turn_id
.clone()
.unwrap_or_else(|| "turn-1".to_string()),
completed_at_ms: None,
item: AppServerThreadItem::Reasoning {
id: "reasoning-1".to_string(),
summary: Vec::new(),
@@ -672,6 +673,7 @@ pub(super) fn handle_entered_review_mode(chat: &mut ChatWidget, review: impl Int
.last_turn_id
.clone()
.unwrap_or_else(|| "turn-1".to_string()),
started_at_ms: None,
item: AppServerThreadItem::EnteredReviewMode {
id: "review-start".to_string(),
review: review.into(),
@@ -700,6 +702,7 @@ pub(super) fn handle_exited_review_mode(chat: &mut ChatWidget) {
.last_turn_id
.clone()
.unwrap_or_else(|| "turn-1".to_string()),
completed_at_ms: None,
item: AppServerThreadItem::ExitedReviewMode {
id: "review-end".to_string(),
review: String::new(),
@@ -756,6 +759,7 @@ pub(super) fn handle_patch_apply_begin(
ServerNotification::ItemStarted(ItemStartedNotification {
thread_id: thread_id(chat),
turn_id: turn_id.into(),
started_at_ms: None,
item: AppServerThreadItem::FileChange {
id: call_id.into(),
changes: file_update_changes_from_tui(changes),
@@ -777,6 +781,7 @@ pub(super) fn handle_patch_apply_end(
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: thread_id(chat),
turn_id: turn_id.into(),
completed_at_ms: None,
item: AppServerThreadItem::FileChange {
id: call_id.into(),
changes: file_update_changes_from_tui(changes),
@@ -796,6 +801,7 @@ pub(super) fn handle_view_image_tool_call(
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: thread_id(chat),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
item: AppServerThreadItem::ImageView {
id: call_id.into(),
path,
@@ -815,6 +821,7 @@ pub(super) fn handle_image_generation_end(
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: thread_id(chat),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
item: AppServerThreadItem::ImageGeneration {
id: call_id.into(),
status: "completed".to_string(),
@@ -972,6 +979,7 @@ pub(super) fn handle_exec_begin(chat: &mut ChatWidget, item: AppServerThreadItem
.last_turn_id
.clone()
.unwrap_or_else(|| "turn-1".to_string()),
started_at_ms: None,
item,
}),
/*replay_kind*/ None,
@@ -1011,6 +1019,7 @@ pub(super) fn complete_assistant_message(
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: chat.thread_id.map(|id| id.to_string()).unwrap_or_default(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
item: AppServerThreadItem::AgentMessage {
id: item_id.to_string(),
text: text.to_string(),
@@ -1053,6 +1062,7 @@ pub(super) fn complete_user_message_for_inputs(
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: chat.thread_id.map(|id| id.to_string()).unwrap_or_default(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
item: AppServerThreadItem::UserMessage {
id: item_id.to_string(),
content,
@@ -1194,6 +1204,7 @@ pub(super) fn handle_exec_end(chat: &mut ChatWidget, item: AppServerThreadItem)
.last_turn_id
.clone()
.unwrap_or_else(|| "turn-1".to_string()),
completed_at_ms: None,
item,
}),
/*replay_kind*/ None,

View File

@@ -801,6 +801,7 @@ async fn live_reasoning_summary_is_not_rendered_twice_when_item_completes() {
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
item: AppServerThreadItem::Reasoning {
id: "reasoning-1".to_string(),
summary: vec!["Summary only".to_string()],

View File

@@ -154,6 +154,7 @@ async fn live_app_server_review_prompt_item_is_not_rendered() {
ServerNotification::ItemStarted(ItemStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: None,
item: review_mode_item.clone(),
}),
/*replay_kind*/ None,
@@ -166,6 +167,7 @@ async fn live_app_server_review_prompt_item_is_not_rendered() {
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
item: review_mode_item,
}),
/*replay_kind*/ None,
@@ -176,6 +178,7 @@ async fn live_app_server_review_prompt_item_is_not_rendered() {
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
item: AppServerThreadItem::UserMessage {
id: "review-prompt".to_string(),
content: vec![AppServerUserInput::Text {

View File

@@ -1149,6 +1149,7 @@ async fn slash_copy_state_tracks_plan_item_completion() {
ServerNotification::ItemCompleted(ItemCompletedNotification {
thread_id: String::new(),
turn_id: "turn-1".to_string(),
completed_at_ms: None,
item: AppServerThreadItem::Plan {
id: "plan-1".to_string(),
text: plan_text.clone(),