mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
app-server: improve thread resume rejoin flow (#11776)
thread/resume response includes latest turn with all items, in band so no events are stale or lost Testing - e2e tested using app-server-test-client using flow described in "Testing Thread Rejoin Behavior" in codex-rs/app-server-test-client/README.md - e2e tested in codex desktop by reconnecting to a running turn
This commit is contained in:
Generated
+1
@@ -1354,6 +1354,7 @@ dependencies = [
|
|||||||
"strum_macros 0.27.2",
|
"strum_macros 0.27.2",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
|
"tracing",
|
||||||
"ts-rs",
|
"ts-rs",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ strum_macros = { workspace = true }
|
|||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
ts-rs = { workspace = true }
|
ts-rs = { workspace = true }
|
||||||
inventory = { workspace = true }
|
inventory = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
uuid = { workspace = true, features = ["serde", "v7"] }
|
uuid = { workspace = true, features = ["serde", "v7"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -24,9 +24,13 @@ use codex_protocol::protocol::CompactedItem;
|
|||||||
use codex_protocol::protocol::ContextCompactedEvent;
|
use codex_protocol::protocol::ContextCompactedEvent;
|
||||||
use codex_protocol::protocol::ErrorEvent;
|
use codex_protocol::protocol::ErrorEvent;
|
||||||
use codex_protocol::protocol::EventMsg;
|
use codex_protocol::protocol::EventMsg;
|
||||||
|
use codex_protocol::protocol::ExecCommandBeginEvent;
|
||||||
use codex_protocol::protocol::ExecCommandEndEvent;
|
use codex_protocol::protocol::ExecCommandEndEvent;
|
||||||
use codex_protocol::protocol::ItemCompletedEvent;
|
use codex_protocol::protocol::ItemCompletedEvent;
|
||||||
|
use codex_protocol::protocol::ItemStartedEvent;
|
||||||
|
use codex_protocol::protocol::McpToolCallBeginEvent;
|
||||||
use codex_protocol::protocol::McpToolCallEndEvent;
|
use codex_protocol::protocol::McpToolCallEndEvent;
|
||||||
|
use codex_protocol::protocol::PatchApplyBeginEvent;
|
||||||
use codex_protocol::protocol::PatchApplyEndEvent;
|
use codex_protocol::protocol::PatchApplyEndEvent;
|
||||||
use codex_protocol::protocol::ReviewOutputEvent;
|
use codex_protocol::protocol::ReviewOutputEvent;
|
||||||
use codex_protocol::protocol::RolloutItem;
|
use codex_protocol::protocol::RolloutItem;
|
||||||
@@ -36,8 +40,10 @@ use codex_protocol::protocol::TurnCompleteEvent;
|
|||||||
use codex_protocol::protocol::TurnStartedEvent;
|
use codex_protocol::protocol::TurnStartedEvent;
|
||||||
use codex_protocol::protocol::UserMessageEvent;
|
use codex_protocol::protocol::UserMessageEvent;
|
||||||
use codex_protocol::protocol::ViewImageToolCallEvent;
|
use codex_protocol::protocol::ViewImageToolCallEvent;
|
||||||
|
use codex_protocol::protocol::WebSearchBeginEvent;
|
||||||
use codex_protocol::protocol::WebSearchEndEvent;
|
use codex_protocol::protocol::WebSearchEndEvent;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use tracing::warn;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -57,14 +63,20 @@ pub fn build_turns_from_rollout_items(items: &[RolloutItem]) -> Vec<Turn> {
|
|||||||
builder.finish()
|
builder.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ThreadHistoryBuilder {
|
pub struct ThreadHistoryBuilder {
|
||||||
turns: Vec<Turn>,
|
turns: Vec<Turn>,
|
||||||
current_turn: Option<PendingTurn>,
|
current_turn: Option<PendingTurn>,
|
||||||
next_item_index: i64,
|
next_item_index: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for ThreadHistoryBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ThreadHistoryBuilder {
|
impl ThreadHistoryBuilder {
|
||||||
fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
turns: Vec::new(),
|
turns: Vec::new(),
|
||||||
current_turn: None,
|
current_turn: None,
|
||||||
@@ -72,14 +84,32 @@ impl ThreadHistoryBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(mut self) -> Vec<Turn> {
|
pub fn reset(&mut self) {
|
||||||
|
*self = Self::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish(mut self) -> Vec<Turn> {
|
||||||
self.finish_current_turn();
|
self.finish_current_turn();
|
||||||
self.turns
|
self.turns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn active_turn_snapshot(&self) -> Option<Turn> {
|
||||||
|
self.current_turn
|
||||||
|
.as_ref()
|
||||||
|
.map(Turn::from)
|
||||||
|
.or_else(|| self.turns.last().cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_active_turn(&self) -> bool {
|
||||||
|
self.current_turn.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared reducer for persisted rollout replay and in-memory current-turn
|
||||||
|
/// tracking used by running thread resume/rejoin.
|
||||||
|
///
|
||||||
/// This function should handle all EventMsg variants that can be persisted in a rollout file.
|
/// This function should handle all EventMsg variants that can be persisted in a rollout file.
|
||||||
/// See `should_persist_event_msg` in `codex-rs/core/rollout/policy.rs`.
|
/// See `should_persist_event_msg` in `codex-rs/core/rollout/policy.rs`.
|
||||||
fn handle_event(&mut self, event: &EventMsg) {
|
pub fn handle_event(&mut self, event: &EventMsg) {
|
||||||
match event {
|
match event {
|
||||||
EventMsg::UserMessage(payload) => self.handle_user_message(payload),
|
EventMsg::UserMessage(payload) => self.handle_user_message(payload),
|
||||||
EventMsg::AgentMessage(payload) => {
|
EventMsg::AgentMessage(payload) => {
|
||||||
@@ -89,21 +119,35 @@ impl ThreadHistoryBuilder {
|
|||||||
EventMsg::AgentReasoningRawContent(payload) => {
|
EventMsg::AgentReasoningRawContent(payload) => {
|
||||||
self.handle_agent_reasoning_raw_content(payload)
|
self.handle_agent_reasoning_raw_content(payload)
|
||||||
}
|
}
|
||||||
|
EventMsg::WebSearchBegin(payload) => self.handle_web_search_begin(payload),
|
||||||
EventMsg::WebSearchEnd(payload) => self.handle_web_search_end(payload),
|
EventMsg::WebSearchEnd(payload) => self.handle_web_search_end(payload),
|
||||||
|
EventMsg::ExecCommandBegin(payload) => self.handle_exec_command_begin(payload),
|
||||||
EventMsg::ExecCommandEnd(payload) => self.handle_exec_command_end(payload),
|
EventMsg::ExecCommandEnd(payload) => self.handle_exec_command_end(payload),
|
||||||
|
EventMsg::PatchApplyBegin(payload) => self.handle_patch_apply_begin(payload),
|
||||||
EventMsg::PatchApplyEnd(payload) => self.handle_patch_apply_end(payload),
|
EventMsg::PatchApplyEnd(payload) => self.handle_patch_apply_end(payload),
|
||||||
|
EventMsg::McpToolCallBegin(payload) => self.handle_mcp_tool_call_begin(payload),
|
||||||
EventMsg::McpToolCallEnd(payload) => self.handle_mcp_tool_call_end(payload),
|
EventMsg::McpToolCallEnd(payload) => self.handle_mcp_tool_call_end(payload),
|
||||||
EventMsg::ViewImageToolCall(payload) => self.handle_view_image_tool_call(payload),
|
EventMsg::ViewImageToolCall(payload) => self.handle_view_image_tool_call(payload),
|
||||||
|
EventMsg::CollabAgentSpawnBegin(payload) => {
|
||||||
|
self.handle_collab_agent_spawn_begin(payload)
|
||||||
|
}
|
||||||
EventMsg::CollabAgentSpawnEnd(payload) => self.handle_collab_agent_spawn_end(payload),
|
EventMsg::CollabAgentSpawnEnd(payload) => self.handle_collab_agent_spawn_end(payload),
|
||||||
|
EventMsg::CollabAgentInteractionBegin(payload) => {
|
||||||
|
self.handle_collab_agent_interaction_begin(payload)
|
||||||
|
}
|
||||||
EventMsg::CollabAgentInteractionEnd(payload) => {
|
EventMsg::CollabAgentInteractionEnd(payload) => {
|
||||||
self.handle_collab_agent_interaction_end(payload)
|
self.handle_collab_agent_interaction_end(payload)
|
||||||
}
|
}
|
||||||
|
EventMsg::CollabWaitingBegin(payload) => self.handle_collab_waiting_begin(payload),
|
||||||
EventMsg::CollabWaitingEnd(payload) => self.handle_collab_waiting_end(payload),
|
EventMsg::CollabWaitingEnd(payload) => self.handle_collab_waiting_end(payload),
|
||||||
|
EventMsg::CollabCloseBegin(payload) => self.handle_collab_close_begin(payload),
|
||||||
EventMsg::CollabCloseEnd(payload) => self.handle_collab_close_end(payload),
|
EventMsg::CollabCloseEnd(payload) => self.handle_collab_close_end(payload),
|
||||||
|
EventMsg::CollabResumeBegin(payload) => self.handle_collab_resume_begin(payload),
|
||||||
EventMsg::CollabResumeEnd(payload) => self.handle_collab_resume_end(payload),
|
EventMsg::CollabResumeEnd(payload) => self.handle_collab_resume_end(payload),
|
||||||
EventMsg::ContextCompacted(payload) => self.handle_context_compacted(payload),
|
EventMsg::ContextCompacted(payload) => self.handle_context_compacted(payload),
|
||||||
EventMsg::EnteredReviewMode(payload) => self.handle_entered_review_mode(payload),
|
EventMsg::EnteredReviewMode(payload) => self.handle_entered_review_mode(payload),
|
||||||
EventMsg::ExitedReviewMode(payload) => self.handle_exited_review_mode(payload),
|
EventMsg::ExitedReviewMode(payload) => self.handle_exited_review_mode(payload),
|
||||||
|
EventMsg::ItemStarted(payload) => self.handle_item_started(payload),
|
||||||
EventMsg::ItemCompleted(payload) => self.handle_item_completed(payload),
|
EventMsg::ItemCompleted(payload) => self.handle_item_completed(payload),
|
||||||
EventMsg::Error(payload) => self.handle_error(payload),
|
EventMsg::Error(payload) => self.handle_error(payload),
|
||||||
EventMsg::TokenCount(_) => {}
|
EventMsg::TokenCount(_) => {}
|
||||||
@@ -116,7 +160,7 @@ impl ThreadHistoryBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_rollout_item(&mut self, item: &RolloutItem) {
|
pub fn handle_rollout_item(&mut self, item: &RolloutItem) {
|
||||||
match item {
|
match item {
|
||||||
RolloutItem::EventMsg(event) => self.handle_event(event),
|
RolloutItem::EventMsg(event) => self.handle_event(event),
|
||||||
RolloutItem::Compacted(payload) => self.handle_compacted(payload),
|
RolloutItem::Compacted(payload) => self.handle_compacted(payload),
|
||||||
@@ -199,15 +243,51 @@ impl ThreadHistoryBuilder {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_item_completed(&mut self, payload: &ItemCompletedEvent) {
|
fn handle_item_started(&mut self, payload: &ItemStartedEvent) {
|
||||||
if let codex_protocol::items::TurnItem::Plan(plan) = &payload.item
|
match &payload.item {
|
||||||
&& plan.text.is_empty()
|
codex_protocol::items::TurnItem::Plan(plan) => {
|
||||||
{
|
if plan.text.is_empty() {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
self.upsert_item_in_turn_id(
|
||||||
|
&payload.turn_id,
|
||||||
|
ThreadItem::from(payload.item.clone()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
codex_protocol::items::TurnItem::UserMessage(_)
|
||||||
|
| codex_protocol::items::TurnItem::AgentMessage(_)
|
||||||
|
| codex_protocol::items::TurnItem::Reasoning(_)
|
||||||
|
| codex_protocol::items::TurnItem::WebSearch(_)
|
||||||
|
| codex_protocol::items::TurnItem::ContextCompaction(_) => {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let item = ThreadItem::from(payload.item.clone());
|
fn handle_item_completed(&mut self, payload: &ItemCompletedEvent) {
|
||||||
self.ensure_turn().items.push(item);
|
match &payload.item {
|
||||||
|
codex_protocol::items::TurnItem::Plan(plan) => {
|
||||||
|
if plan.text.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.upsert_item_in_turn_id(
|
||||||
|
&payload.turn_id,
|
||||||
|
ThreadItem::from(payload.item.clone()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
codex_protocol::items::TurnItem::UserMessage(_)
|
||||||
|
| codex_protocol::items::TurnItem::AgentMessage(_)
|
||||||
|
| codex_protocol::items::TurnItem::Reasoning(_)
|
||||||
|
| codex_protocol::items::TurnItem::WebSearch(_)
|
||||||
|
| codex_protocol::items::TurnItem::ContextCompaction(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_web_search_begin(&mut self, payload: &WebSearchBeginEvent) {
|
||||||
|
let item = ThreadItem::WebSearch {
|
||||||
|
id: payload.call_id.clone(),
|
||||||
|
query: String::new(),
|
||||||
|
action: None,
|
||||||
|
};
|
||||||
|
self.upsert_item_in_current_turn(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_web_search_end(&mut self, payload: &WebSearchEndEvent) {
|
fn handle_web_search_end(&mut self, payload: &WebSearchEndEvent) {
|
||||||
@@ -216,7 +296,30 @@ impl ThreadHistoryBuilder {
|
|||||||
query: payload.query.clone(),
|
query: payload.query.clone(),
|
||||||
action: Some(WebSearchAction::from(payload.action.clone())),
|
action: Some(WebSearchAction::from(payload.action.clone())),
|
||||||
};
|
};
|
||||||
self.ensure_turn().items.push(item);
|
self.upsert_item_in_current_turn(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_exec_command_begin(&mut self, payload: &ExecCommandBeginEvent) {
|
||||||
|
let command = shlex::try_join(payload.command.iter().map(String::as_str))
|
||||||
|
.unwrap_or_else(|_| payload.command.join(" "));
|
||||||
|
let command_actions = payload
|
||||||
|
.parsed_cmd
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(CommandAction::from)
|
||||||
|
.collect();
|
||||||
|
let item = ThreadItem::CommandExecution {
|
||||||
|
id: payload.call_id.clone(),
|
||||||
|
command,
|
||||||
|
cwd: payload.cwd.clone(),
|
||||||
|
process_id: payload.process_id.clone(),
|
||||||
|
status: CommandExecutionStatus::InProgress,
|
||||||
|
command_actions,
|
||||||
|
aggregated_output: None,
|
||||||
|
exit_code: None,
|
||||||
|
duration_ms: None,
|
||||||
|
};
|
||||||
|
self.upsert_item_in_turn_id(&payload.turn_id, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_exec_command_end(&mut self, payload: &ExecCommandEndEvent) {
|
fn handle_exec_command_end(&mut self, payload: &ExecCommandEndEvent) {
|
||||||
@@ -246,33 +349,25 @@ impl ThreadHistoryBuilder {
|
|||||||
exit_code: Some(payload.exit_code),
|
exit_code: Some(payload.exit_code),
|
||||||
duration_ms: Some(duration_ms),
|
duration_ms: Some(duration_ms),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Command completions can arrive out of order. Unified exec may return
|
// Command completions can arrive out of order. Unified exec may return
|
||||||
// while a PTY is still running, then emit ExecCommandEnd later from a
|
// while a PTY is still running, then emit ExecCommandEnd later from a
|
||||||
// background exit watcher when that process finally exits. By then, a
|
// background exit watcher when that process finally exits. By then, a
|
||||||
// newer user turn may already have started. Route by event turn_id so
|
// newer user turn may already have started. Route by event turn_id so
|
||||||
// replay preserves the original turn association.
|
// replay preserves the original turn association.
|
||||||
if let Some(turn) = self.current_turn.as_mut()
|
self.upsert_item_in_turn_id(&payload.turn_id, item);
|
||||||
&& turn.id == payload.turn_id
|
}
|
||||||
{
|
|
||||||
turn.items.push(item);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the originating turn is already finalized, append there instead
|
fn handle_patch_apply_begin(&mut self, payload: &PatchApplyBeginEvent) {
|
||||||
// of attaching to whichever turn is currently active during replay.
|
let item = ThreadItem::FileChange {
|
||||||
if let Some(turn) = self
|
id: payload.call_id.clone(),
|
||||||
.turns
|
changes: convert_patch_changes(&payload.changes),
|
||||||
.iter_mut()
|
status: PatchApplyStatus::InProgress,
|
||||||
.find(|turn| turn.id == payload.turn_id)
|
};
|
||||||
{
|
if payload.turn_id.is_empty() {
|
||||||
turn.items.push(item);
|
self.upsert_item_in_current_turn(item);
|
||||||
return;
|
} else {
|
||||||
|
self.upsert_item_in_turn_id(&payload.turn_id, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backward-compatibility fallback for partial/legacy streams where the
|
|
||||||
// event turn_id does not match any known replay turn.
|
|
||||||
self.ensure_turn().items.push(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_patch_apply_end(&mut self, payload: &PatchApplyEndEvent) {
|
fn handle_patch_apply_end(&mut self, payload: &PatchApplyEndEvent) {
|
||||||
@@ -282,7 +377,29 @@ impl ThreadHistoryBuilder {
|
|||||||
changes: convert_patch_changes(&payload.changes),
|
changes: convert_patch_changes(&payload.changes),
|
||||||
status,
|
status,
|
||||||
};
|
};
|
||||||
self.ensure_turn().items.push(item);
|
if payload.turn_id.is_empty() {
|
||||||
|
self.upsert_item_in_current_turn(item);
|
||||||
|
} else {
|
||||||
|
self.upsert_item_in_turn_id(&payload.turn_id, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_mcp_tool_call_begin(&mut self, payload: &McpToolCallBeginEvent) {
|
||||||
|
let item = ThreadItem::McpToolCall {
|
||||||
|
id: payload.call_id.clone(),
|
||||||
|
server: payload.invocation.server.clone(),
|
||||||
|
tool: payload.invocation.tool.clone(),
|
||||||
|
status: McpToolCallStatus::InProgress,
|
||||||
|
arguments: payload
|
||||||
|
.invocation
|
||||||
|
.arguments
|
||||||
|
.clone()
|
||||||
|
.unwrap_or(serde_json::Value::Null),
|
||||||
|
result: None,
|
||||||
|
error: None,
|
||||||
|
duration_ms: None,
|
||||||
|
};
|
||||||
|
self.upsert_item_in_current_turn(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_mcp_tool_call_end(&mut self, payload: &McpToolCallEndEvent) {
|
fn handle_mcp_tool_call_end(&mut self, payload: &McpToolCallEndEvent) {
|
||||||
@@ -321,7 +438,7 @@ impl ThreadHistoryBuilder {
|
|||||||
error,
|
error,
|
||||||
duration_ms,
|
duration_ms,
|
||||||
};
|
};
|
||||||
self.ensure_turn().items.push(item);
|
self.upsert_item_in_current_turn(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_view_image_tool_call(&mut self, payload: &ViewImageToolCallEvent) {
|
fn handle_view_image_tool_call(&mut self, payload: &ViewImageToolCallEvent) {
|
||||||
@@ -329,7 +446,23 @@ impl ThreadHistoryBuilder {
|
|||||||
id: payload.call_id.clone(),
|
id: payload.call_id.clone(),
|
||||||
path: payload.path.to_string_lossy().into_owned(),
|
path: payload.path.to_string_lossy().into_owned(),
|
||||||
};
|
};
|
||||||
self.ensure_turn().items.push(item);
|
self.upsert_item_in_current_turn(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_collab_agent_spawn_begin(
|
||||||
|
&mut self,
|
||||||
|
payload: &codex_protocol::protocol::CollabAgentSpawnBeginEvent,
|
||||||
|
) {
|
||||||
|
let item = ThreadItem::CollabAgentToolCall {
|
||||||
|
id: payload.call_id.clone(),
|
||||||
|
tool: CollabAgentTool::SpawnAgent,
|
||||||
|
status: CollabAgentToolCallStatus::InProgress,
|
||||||
|
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||||
|
receiver_thread_ids: Vec::new(),
|
||||||
|
prompt: Some(payload.prompt.clone()),
|
||||||
|
agents_states: HashMap::new(),
|
||||||
|
};
|
||||||
|
self.upsert_item_in_current_turn(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_collab_agent_spawn_end(
|
fn handle_collab_agent_spawn_end(
|
||||||
@@ -353,17 +486,31 @@ impl ThreadHistoryBuilder {
|
|||||||
}
|
}
|
||||||
None => (Vec::new(), HashMap::new()),
|
None => (Vec::new(), HashMap::new()),
|
||||||
};
|
};
|
||||||
self.ensure_turn()
|
self.upsert_item_in_current_turn(ThreadItem::CollabAgentToolCall {
|
||||||
.items
|
id: payload.call_id.clone(),
|
||||||
.push(ThreadItem::CollabAgentToolCall {
|
tool: CollabAgentTool::SpawnAgent,
|
||||||
id: payload.call_id.clone(),
|
status,
|
||||||
tool: CollabAgentTool::SpawnAgent,
|
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||||
status,
|
receiver_thread_ids,
|
||||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
prompt: Some(payload.prompt.clone()),
|
||||||
receiver_thread_ids,
|
agents_states,
|
||||||
prompt: Some(payload.prompt.clone()),
|
});
|
||||||
agents_states,
|
}
|
||||||
});
|
|
||||||
|
fn handle_collab_agent_interaction_begin(
|
||||||
|
&mut self,
|
||||||
|
payload: &codex_protocol::protocol::CollabAgentInteractionBeginEvent,
|
||||||
|
) {
|
||||||
|
let item = ThreadItem::CollabAgentToolCall {
|
||||||
|
id: payload.call_id.clone(),
|
||||||
|
tool: CollabAgentTool::SendInput,
|
||||||
|
status: CollabAgentToolCallStatus::InProgress,
|
||||||
|
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||||
|
receiver_thread_ids: vec![payload.receiver_thread_id.to_string()],
|
||||||
|
prompt: Some(payload.prompt.clone()),
|
||||||
|
agents_states: HashMap::new(),
|
||||||
|
};
|
||||||
|
self.upsert_item_in_current_turn(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_collab_agent_interaction_end(
|
fn handle_collab_agent_interaction_end(
|
||||||
@@ -376,17 +523,35 @@ impl ThreadHistoryBuilder {
|
|||||||
};
|
};
|
||||||
let receiver_id = payload.receiver_thread_id.to_string();
|
let receiver_id = payload.receiver_thread_id.to_string();
|
||||||
let received_status = CollabAgentState::from(payload.status.clone());
|
let received_status = CollabAgentState::from(payload.status.clone());
|
||||||
self.ensure_turn()
|
self.upsert_item_in_current_turn(ThreadItem::CollabAgentToolCall {
|
||||||
.items
|
id: payload.call_id.clone(),
|
||||||
.push(ThreadItem::CollabAgentToolCall {
|
tool: CollabAgentTool::SendInput,
|
||||||
id: payload.call_id.clone(),
|
status,
|
||||||
tool: CollabAgentTool::SendInput,
|
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||||
status,
|
receiver_thread_ids: vec![receiver_id.clone()],
|
||||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
prompt: Some(payload.prompt.clone()),
|
||||||
receiver_thread_ids: vec![receiver_id.clone()],
|
agents_states: [(receiver_id, received_status)].into_iter().collect(),
|
||||||
prompt: Some(payload.prompt.clone()),
|
});
|
||||||
agents_states: [(receiver_id, received_status)].into_iter().collect(),
|
}
|
||||||
});
|
|
||||||
|
fn handle_collab_waiting_begin(
|
||||||
|
&mut self,
|
||||||
|
payload: &codex_protocol::protocol::CollabWaitingBeginEvent,
|
||||||
|
) {
|
||||||
|
let item = ThreadItem::CollabAgentToolCall {
|
||||||
|
id: payload.call_id.clone(),
|
||||||
|
tool: CollabAgentTool::Wait,
|
||||||
|
status: CollabAgentToolCallStatus::InProgress,
|
||||||
|
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||||
|
receiver_thread_ids: payload
|
||||||
|
.receiver_thread_ids
|
||||||
|
.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect(),
|
||||||
|
prompt: None,
|
||||||
|
agents_states: HashMap::new(),
|
||||||
|
};
|
||||||
|
self.upsert_item_in_current_turn(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_collab_waiting_end(
|
fn handle_collab_waiting_end(
|
||||||
@@ -410,17 +575,31 @@ impl ThreadHistoryBuilder {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|(id, status)| (id.to_string(), CollabAgentState::from(status.clone())))
|
.map(|(id, status)| (id.to_string(), CollabAgentState::from(status.clone())))
|
||||||
.collect();
|
.collect();
|
||||||
self.ensure_turn()
|
self.upsert_item_in_current_turn(ThreadItem::CollabAgentToolCall {
|
||||||
.items
|
id: payload.call_id.clone(),
|
||||||
.push(ThreadItem::CollabAgentToolCall {
|
tool: CollabAgentTool::Wait,
|
||||||
id: payload.call_id.clone(),
|
status,
|
||||||
tool: CollabAgentTool::Wait,
|
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||||
status,
|
receiver_thread_ids,
|
||||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
prompt: None,
|
||||||
receiver_thread_ids,
|
agents_states,
|
||||||
prompt: None,
|
});
|
||||||
agents_states,
|
}
|
||||||
});
|
|
||||||
|
fn handle_collab_close_begin(
|
||||||
|
&mut self,
|
||||||
|
payload: &codex_protocol::protocol::CollabCloseBeginEvent,
|
||||||
|
) {
|
||||||
|
let item = ThreadItem::CollabAgentToolCall {
|
||||||
|
id: payload.call_id.clone(),
|
||||||
|
tool: CollabAgentTool::CloseAgent,
|
||||||
|
status: CollabAgentToolCallStatus::InProgress,
|
||||||
|
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||||
|
receiver_thread_ids: vec![payload.receiver_thread_id.to_string()],
|
||||||
|
prompt: None,
|
||||||
|
agents_states: HashMap::new(),
|
||||||
|
};
|
||||||
|
self.upsert_item_in_current_turn(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_collab_close_end(&mut self, payload: &codex_protocol::protocol::CollabCloseEndEvent) {
|
fn handle_collab_close_end(&mut self, payload: &codex_protocol::protocol::CollabCloseEndEvent) {
|
||||||
@@ -435,17 +614,31 @@ impl ThreadHistoryBuilder {
|
|||||||
)]
|
)]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect();
|
.collect();
|
||||||
self.ensure_turn()
|
self.upsert_item_in_current_turn(ThreadItem::CollabAgentToolCall {
|
||||||
.items
|
id: payload.call_id.clone(),
|
||||||
.push(ThreadItem::CollabAgentToolCall {
|
tool: CollabAgentTool::CloseAgent,
|
||||||
id: payload.call_id.clone(),
|
status,
|
||||||
tool: CollabAgentTool::CloseAgent,
|
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||||
status,
|
receiver_thread_ids: vec![receiver_id],
|
||||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
prompt: None,
|
||||||
receiver_thread_ids: vec![receiver_id],
|
agents_states,
|
||||||
prompt: None,
|
});
|
||||||
agents_states,
|
}
|
||||||
});
|
|
||||||
|
fn handle_collab_resume_begin(
|
||||||
|
&mut self,
|
||||||
|
payload: &codex_protocol::protocol::CollabResumeBeginEvent,
|
||||||
|
) {
|
||||||
|
let item = ThreadItem::CollabAgentToolCall {
|
||||||
|
id: payload.call_id.clone(),
|
||||||
|
tool: CollabAgentTool::ResumeAgent,
|
||||||
|
status: CollabAgentToolCallStatus::InProgress,
|
||||||
|
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||||
|
receiver_thread_ids: vec![payload.receiver_thread_id.to_string()],
|
||||||
|
prompt: None,
|
||||||
|
agents_states: HashMap::new(),
|
||||||
|
};
|
||||||
|
self.upsert_item_in_current_turn(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_collab_resume_end(
|
fn handle_collab_resume_end(
|
||||||
@@ -463,17 +656,15 @@ impl ThreadHistoryBuilder {
|
|||||||
)]
|
)]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect();
|
.collect();
|
||||||
self.ensure_turn()
|
self.upsert_item_in_current_turn(ThreadItem::CollabAgentToolCall {
|
||||||
.items
|
id: payload.call_id.clone(),
|
||||||
.push(ThreadItem::CollabAgentToolCall {
|
tool: CollabAgentTool::ResumeAgent,
|
||||||
id: payload.call_id.clone(),
|
status,
|
||||||
tool: CollabAgentTool::ResumeAgent,
|
sender_thread_id: payload.sender_thread_id.to_string(),
|
||||||
status,
|
receiver_thread_ids: vec![receiver_id],
|
||||||
sender_thread_id: payload.sender_thread_id.to_string(),
|
prompt: None,
|
||||||
receiver_thread_ids: vec![receiver_id],
|
agents_states,
|
||||||
prompt: None,
|
});
|
||||||
agents_states,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_context_compacted(&mut self, _payload: &ContextCompactedEvent) {
|
fn handle_context_compacted(&mut self, _payload: &ContextCompactedEvent) {
|
||||||
@@ -548,6 +739,7 @@ impl ThreadHistoryBuilder {
|
|||||||
self.finish_current_turn();
|
self.finish_current_turn();
|
||||||
self.current_turn = Some(
|
self.current_turn = Some(
|
||||||
self.new_turn(Some(payload.turn_id.clone()))
|
self.new_turn(Some(payload.turn_id.clone()))
|
||||||
|
.with_status(TurnStatus::InProgress)
|
||||||
.opened_explicitly(),
|
.opened_explicitly(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -642,6 +834,30 @@ impl ThreadHistoryBuilder {
|
|||||||
unreachable!("current turn must exist after initialization");
|
unreachable!("current turn must exist after initialization");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn upsert_item_in_turn_id(&mut self, turn_id: &str, item: ThreadItem) {
|
||||||
|
if let Some(turn) = self.current_turn.as_mut()
|
||||||
|
&& turn.id == turn_id
|
||||||
|
{
|
||||||
|
upsert_turn_item(&mut turn.items, item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(turn) = self.turns.iter_mut().find(|turn| turn.id == turn_id) {
|
||||||
|
upsert_turn_item(&mut turn.items, item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
warn!(
|
||||||
|
item_id = item.id(),
|
||||||
|
"dropping turn-scoped item for unknown turn id `{turn_id}`"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upsert_item_in_current_turn(&mut self, item: ThreadItem) {
|
||||||
|
let turn = self.ensure_turn();
|
||||||
|
upsert_turn_item(&mut turn.items, item);
|
||||||
|
}
|
||||||
|
|
||||||
fn next_item_id(&mut self) -> String {
|
fn next_item_id(&mut self) -> String {
|
||||||
let id = format!("item-{}", self.next_item_index);
|
let id = format!("item-{}", self.next_item_index);
|
||||||
self.next_item_index += 1;
|
self.next_item_index += 1;
|
||||||
@@ -684,7 +900,7 @@ fn render_review_output_text(output: &ReviewOutputEvent) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_patch_changes(
|
pub fn convert_patch_changes(
|
||||||
changes: &HashMap<std::path::PathBuf, codex_protocol::protocol::FileChange>,
|
changes: &HashMap<std::path::PathBuf, codex_protocol::protocol::FileChange>,
|
||||||
) -> Vec<FileUpdateChange> {
|
) -> Vec<FileUpdateChange> {
|
||||||
let mut converted: Vec<FileUpdateChange> = changes
|
let mut converted: Vec<FileUpdateChange> = changes
|
||||||
@@ -726,6 +942,17 @@ fn format_file_change_diff(change: &codex_protocol::protocol::FileChange) -> Str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn upsert_turn_item(items: &mut Vec<ThreadItem>, item: ThreadItem) {
|
||||||
|
if let Some(existing_item) = items
|
||||||
|
.iter_mut()
|
||||||
|
.find(|existing_item| existing_item.id() == item.id())
|
||||||
|
{
|
||||||
|
*existing_item = item;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
struct PendingTurn {
|
struct PendingTurn {
|
||||||
id: String,
|
id: String,
|
||||||
items: Vec<ThreadItem>,
|
items: Vec<ThreadItem>,
|
||||||
@@ -744,6 +971,11 @@ impl PendingTurn {
|
|||||||
self.opened_explicitly = true;
|
self.opened_explicitly = true;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn with_status(mut self, status: TurnStatus) -> Self {
|
||||||
|
self.status = status;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<PendingTurn> for Turn {
|
impl From<PendingTurn> for Turn {
|
||||||
@@ -757,10 +989,23 @@ impl From<PendingTurn> for Turn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&PendingTurn> for Turn {
|
||||||
|
fn from(value: &PendingTurn) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id.clone(),
|
||||||
|
items: value.items.clone(),
|
||||||
|
error: value.error.clone(),
|
||||||
|
status: value.status.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
|
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||||
|
use codex_protocol::items::UserMessageItem as CoreUserMessageItem;
|
||||||
use codex_protocol::models::MessagePhase as CoreMessagePhase;
|
use codex_protocol::models::MessagePhase as CoreMessagePhase;
|
||||||
use codex_protocol::models::WebSearchAction as CoreWebSearchAction;
|
use codex_protocol::models::WebSearchAction as CoreWebSearchAction;
|
||||||
use codex_protocol::parse_command::ParsedCommand;
|
use codex_protocol::parse_command::ParsedCommand;
|
||||||
@@ -771,6 +1016,7 @@ mod tests {
|
|||||||
use codex_protocol::protocol::CompactedItem;
|
use codex_protocol::protocol::CompactedItem;
|
||||||
use codex_protocol::protocol::ExecCommandEndEvent;
|
use codex_protocol::protocol::ExecCommandEndEvent;
|
||||||
use codex_protocol::protocol::ExecCommandSource;
|
use codex_protocol::protocol::ExecCommandSource;
|
||||||
|
use codex_protocol::protocol::ItemStartedEvent;
|
||||||
use codex_protocol::protocol::McpInvocation;
|
use codex_protocol::protocol::McpInvocation;
|
||||||
use codex_protocol::protocol::McpToolCallEndEvent;
|
use codex_protocol::protocol::McpToolCallEndEvent;
|
||||||
use codex_protocol::protocol::ThreadRolledBackEvent;
|
use codex_protocol::protocol::ThreadRolledBackEvent;
|
||||||
@@ -816,11 +1062,11 @@ mod tests {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
let items = events
|
let mut builder = ThreadHistoryBuilder::new();
|
||||||
.into_iter()
|
for event in &events {
|
||||||
.map(RolloutItem::EventMsg)
|
builder.handle_event(event);
|
||||||
.collect::<Vec<_>>();
|
}
|
||||||
let turns = build_turns_from_rollout_items(&items);
|
let turns = builder.finish();
|
||||||
assert_eq!(turns.len(), 2);
|
assert_eq!(turns.len(), 2);
|
||||||
|
|
||||||
let first = &turns[0];
|
let first = &turns[0];
|
||||||
@@ -883,6 +1129,55 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignores_non_plan_item_lifecycle_events() {
|
||||||
|
let turn_id = "turn-1";
|
||||||
|
let thread_id = ThreadId::new();
|
||||||
|
let events = vec![
|
||||||
|
EventMsg::TurnStarted(TurnStartedEvent {
|
||||||
|
turn_id: turn_id.to_string(),
|
||||||
|
model_context_window: None,
|
||||||
|
collaboration_mode_kind: Default::default(),
|
||||||
|
}),
|
||||||
|
EventMsg::UserMessage(UserMessageEvent {
|
||||||
|
message: "hello".into(),
|
||||||
|
images: None,
|
||||||
|
text_elements: Vec::new(),
|
||||||
|
local_images: Vec::new(),
|
||||||
|
}),
|
||||||
|
EventMsg::ItemStarted(ItemStartedEvent {
|
||||||
|
thread_id,
|
||||||
|
turn_id: turn_id.to_string(),
|
||||||
|
item: CoreTurnItem::UserMessage(CoreUserMessageItem {
|
||||||
|
id: "user-item-id".to_string(),
|
||||||
|
content: Vec::new(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||||
|
turn_id: turn_id.to_string(),
|
||||||
|
last_agent_message: None,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
let items = events
|
||||||
|
.into_iter()
|
||||||
|
.map(RolloutItem::EventMsg)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let turns = build_turns_from_rollout_items(&items);
|
||||||
|
assert_eq!(turns.len(), 1);
|
||||||
|
assert_eq!(turns[0].items.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
turns[0].items[0],
|
||||||
|
ThreadItem::UserMessage {
|
||||||
|
id: "item-1".into(),
|
||||||
|
content: vec![UserInput::Text {
|
||||||
|
text: "hello".into(),
|
||||||
|
text_elements: Vec::new(),
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn preserves_agent_message_phase_in_history() {
|
fn preserves_agent_message_phase_in_history() {
|
||||||
let events = vec![EventMsg::AgentMessage(AgentMessageEvent {
|
let events = vec![EventMsg::AgentMessage(AgentMessageEvent {
|
||||||
@@ -1212,6 +1507,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn reconstructs_tool_items_from_persisted_completion_events() {
|
fn reconstructs_tool_items_from_persisted_completion_events() {
|
||||||
let events = vec![
|
let events = vec![
|
||||||
|
EventMsg::TurnStarted(TurnStartedEvent {
|
||||||
|
turn_id: "turn-1".into(),
|
||||||
|
model_context_window: None,
|
||||||
|
collaboration_mode_kind: Default::default(),
|
||||||
|
}),
|
||||||
EventMsg::UserMessage(UserMessageEvent {
|
EventMsg::UserMessage(UserMessageEvent {
|
||||||
message: "run tools".into(),
|
message: "run tools".into(),
|
||||||
images: None,
|
images: None,
|
||||||
@@ -1311,6 +1611,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn reconstructs_declined_exec_and_patch_items() {
|
fn reconstructs_declined_exec_and_patch_items() {
|
||||||
let events = vec![
|
let events = vec![
|
||||||
|
EventMsg::TurnStarted(TurnStartedEvent {
|
||||||
|
turn_id: "turn-1".into(),
|
||||||
|
model_context_window: None,
|
||||||
|
collaboration_mode_kind: Default::default(),
|
||||||
|
}),
|
||||||
EventMsg::UserMessage(UserMessageEvent {
|
EventMsg::UserMessage(UserMessageEvent {
|
||||||
message: "run tools".into(),
|
message: "run tools".into(),
|
||||||
images: None,
|
images: None,
|
||||||
@@ -1471,6 +1776,82 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn drops_late_turn_scoped_item_for_unknown_turn_id() {
|
||||||
|
let events = vec![
|
||||||
|
EventMsg::TurnStarted(TurnStartedEvent {
|
||||||
|
turn_id: "turn-a".into(),
|
||||||
|
model_context_window: None,
|
||||||
|
collaboration_mode_kind: Default::default(),
|
||||||
|
}),
|
||||||
|
EventMsg::UserMessage(UserMessageEvent {
|
||||||
|
message: "first".into(),
|
||||||
|
images: None,
|
||||||
|
text_elements: Vec::new(),
|
||||||
|
local_images: Vec::new(),
|
||||||
|
}),
|
||||||
|
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||||
|
turn_id: "turn-a".into(),
|
||||||
|
last_agent_message: None,
|
||||||
|
}),
|
||||||
|
EventMsg::TurnStarted(TurnStartedEvent {
|
||||||
|
turn_id: "turn-b".into(),
|
||||||
|
model_context_window: None,
|
||||||
|
collaboration_mode_kind: Default::default(),
|
||||||
|
}),
|
||||||
|
EventMsg::UserMessage(UserMessageEvent {
|
||||||
|
message: "second".into(),
|
||||||
|
images: None,
|
||||||
|
text_elements: Vec::new(),
|
||||||
|
local_images: Vec::new(),
|
||||||
|
}),
|
||||||
|
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
|
||||||
|
call_id: "exec-unknown-turn".into(),
|
||||||
|
process_id: Some("pid-42".into()),
|
||||||
|
turn_id: "turn-missing".into(),
|
||||||
|
command: vec!["echo".into(), "done".into()],
|
||||||
|
cwd: PathBuf::from("/tmp"),
|
||||||
|
parsed_cmd: vec![ParsedCommand::Unknown {
|
||||||
|
cmd: "echo done".into(),
|
||||||
|
}],
|
||||||
|
source: ExecCommandSource::Agent,
|
||||||
|
interaction_input: None,
|
||||||
|
stdout: "done\n".into(),
|
||||||
|
stderr: String::new(),
|
||||||
|
aggregated_output: "done\n".into(),
|
||||||
|
exit_code: 0,
|
||||||
|
duration: Duration::from_millis(5),
|
||||||
|
formatted_output: "done\n".into(),
|
||||||
|
status: CoreExecCommandStatus::Completed,
|
||||||
|
}),
|
||||||
|
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||||
|
turn_id: "turn-b".into(),
|
||||||
|
last_agent_message: None,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut builder = ThreadHistoryBuilder::new();
|
||||||
|
for event in &events {
|
||||||
|
builder.handle_event(event);
|
||||||
|
}
|
||||||
|
let turns = builder.finish();
|
||||||
|
assert_eq!(turns.len(), 2);
|
||||||
|
assert_eq!(turns[0].id, "turn-a");
|
||||||
|
assert_eq!(turns[1].id, "turn-b");
|
||||||
|
assert_eq!(turns[0].items.len(), 1);
|
||||||
|
assert_eq!(turns[1].items.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
turns[1].items[0],
|
||||||
|
ThreadItem::UserMessage {
|
||||||
|
id: "item-2".into(),
|
||||||
|
content: vec![UserInput::Text {
|
||||||
|
text: "second".into(),
|
||||||
|
text_elements: Vec::new(),
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn late_turn_complete_does_not_close_active_turn() {
|
fn late_turn_complete_does_not_close_active_turn() {
|
||||||
let events = vec![
|
let events = vec![
|
||||||
@@ -1572,7 +1953,7 @@ mod tests {
|
|||||||
assert_eq!(turns.len(), 2);
|
assert_eq!(turns.len(), 2);
|
||||||
assert_eq!(turns[0].id, "turn-a");
|
assert_eq!(turns[0].id, "turn-a");
|
||||||
assert_eq!(turns[1].id, "turn-b");
|
assert_eq!(turns[1].id, "turn-b");
|
||||||
assert_eq!(turns[1].status, TurnStatus::Completed);
|
assert_eq!(turns[1].status, TurnStatus::InProgress);
|
||||||
assert_eq!(turns[1].items.len(), 2);
|
assert_eq!(turns[1].items.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2727,6 +2727,26 @@ pub enum ThreadItem {
|
|||||||
ContextCompaction { id: String },
|
ContextCompaction { id: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ThreadItem {
|
||||||
|
pub fn id(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
ThreadItem::UserMessage { id, .. }
|
||||||
|
| ThreadItem::AgentMessage { id, .. }
|
||||||
|
| ThreadItem::Plan { id, .. }
|
||||||
|
| ThreadItem::Reasoning { id, .. }
|
||||||
|
| ThreadItem::CommandExecution { id, .. }
|
||||||
|
| ThreadItem::FileChange { id, .. }
|
||||||
|
| ThreadItem::McpToolCall { id, .. }
|
||||||
|
| ThreadItem::CollabAgentToolCall { id, .. }
|
||||||
|
| ThreadItem::WebSearch { id, .. }
|
||||||
|
| ThreadItem::ImageView { id, .. }
|
||||||
|
| ThreadItem::EnteredReviewMode { id, .. }
|
||||||
|
| ThreadItem::ExitedReviewMode { id, .. }
|
||||||
|
| ThreadItem::ContextCompaction { id, .. } => id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||||
#[serde(tag = "type", rename_all = "camelCase")]
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
#[ts(tag = "type", rename_all = "camelCase")]
|
#[ts(tag = "type", rename_all = "camelCase")]
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ use codex_app_server_protocol::McpToolCallResult;
|
|||||||
use codex_app_server_protocol::McpToolCallStatus;
|
use codex_app_server_protocol::McpToolCallStatus;
|
||||||
use codex_app_server_protocol::ModelReroutedNotification;
|
use codex_app_server_protocol::ModelReroutedNotification;
|
||||||
use codex_app_server_protocol::PatchApplyStatus;
|
use codex_app_server_protocol::PatchApplyStatus;
|
||||||
use codex_app_server_protocol::PatchChangeKind as V2PatchChangeKind;
|
|
||||||
use codex_app_server_protocol::PlanDeltaNotification;
|
use codex_app_server_protocol::PlanDeltaNotification;
|
||||||
use codex_app_server_protocol::RawResponseItemCompletedNotification;
|
use codex_app_server_protocol::RawResponseItemCompletedNotification;
|
||||||
use codex_app_server_protocol::ReasoningSummaryPartAddedNotification;
|
use codex_app_server_protocol::ReasoningSummaryPartAddedNotification;
|
||||||
@@ -72,6 +71,7 @@ use codex_app_server_protocol::TurnPlanStep;
|
|||||||
use codex_app_server_protocol::TurnPlanUpdatedNotification;
|
use codex_app_server_protocol::TurnPlanUpdatedNotification;
|
||||||
use codex_app_server_protocol::TurnStatus;
|
use codex_app_server_protocol::TurnStatus;
|
||||||
use codex_app_server_protocol::build_turns_from_rollout_items;
|
use codex_app_server_protocol::build_turns_from_rollout_items;
|
||||||
|
use codex_app_server_protocol::convert_patch_changes;
|
||||||
use codex_core::CodexThread;
|
use codex_core::CodexThread;
|
||||||
use codex_core::ThreadManager;
|
use codex_core::ThreadManager;
|
||||||
use codex_core::parse_command::shlex_join;
|
use codex_core::parse_command::shlex_join;
|
||||||
@@ -81,7 +81,6 @@ use codex_core::protocol::Event;
|
|||||||
use codex_core::protocol::EventMsg;
|
use codex_core::protocol::EventMsg;
|
||||||
use codex_core::protocol::ExecApprovalRequestEvent;
|
use codex_core::protocol::ExecApprovalRequestEvent;
|
||||||
use codex_core::protocol::ExecCommandEndEvent;
|
use codex_core::protocol::ExecCommandEndEvent;
|
||||||
use codex_core::protocol::FileChange as CoreFileChange;
|
|
||||||
use codex_core::protocol::McpToolCallBeginEvent;
|
use codex_core::protocol::McpToolCallBeginEvent;
|
||||||
use codex_core::protocol::McpToolCallEndEvent;
|
use codex_core::protocol::McpToolCallEndEvent;
|
||||||
use codex_core::protocol::Op;
|
use codex_core::protocol::Op;
|
||||||
@@ -1706,46 +1705,6 @@ fn render_review_output_text(output: &ReviewOutputEvent) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_patch_changes(changes: &HashMap<PathBuf, CoreFileChange>) -> Vec<FileUpdateChange> {
|
|
||||||
let mut converted: Vec<FileUpdateChange> = changes
|
|
||||||
.iter()
|
|
||||||
.map(|(path, change)| FileUpdateChange {
|
|
||||||
path: path.to_string_lossy().into_owned(),
|
|
||||||
kind: map_patch_change_kind(change),
|
|
||||||
diff: format_file_change_diff(change),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
converted.sort_by(|a, b| a.path.cmp(&b.path));
|
|
||||||
converted
|
|
||||||
}
|
|
||||||
|
|
||||||
fn map_patch_change_kind(change: &CoreFileChange) -> V2PatchChangeKind {
|
|
||||||
match change {
|
|
||||||
CoreFileChange::Add { .. } => V2PatchChangeKind::Add,
|
|
||||||
CoreFileChange::Delete { .. } => V2PatchChangeKind::Delete,
|
|
||||||
CoreFileChange::Update { move_path, .. } => V2PatchChangeKind::Update {
|
|
||||||
move_path: move_path.clone(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_file_change_diff(change: &CoreFileChange) -> String {
|
|
||||||
match change {
|
|
||||||
CoreFileChange::Add { content } => content.clone(),
|
|
||||||
CoreFileChange::Delete { content } => content.clone(),
|
|
||||||
CoreFileChange::Update {
|
|
||||||
unified_diff,
|
|
||||||
move_path,
|
|
||||||
} => {
|
|
||||||
if let Some(path) = move_path {
|
|
||||||
format!("{unified_diff}\n\nMoved to: {}", path.display())
|
|
||||||
} else {
|
|
||||||
unified_diff.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn map_file_change_approval_decision(
|
fn map_file_change_approval_decision(
|
||||||
decision: FileChangeApprovalDecision,
|
decision: FileChangeApprovalDecision,
|
||||||
) -> (ReviewDecision, Option<PatchApplyStatus>) {
|
) -> (ReviewDecision, Option<PatchApplyStatus>) {
|
||||||
|
|||||||
@@ -336,6 +336,7 @@ pub(crate) struct CodexMessageProcessor {
|
|||||||
outgoing: Arc<OutgoingMessageSender>,
|
outgoing: Arc<OutgoingMessageSender>,
|
||||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||||
config: Arc<Config>,
|
config: Arc<Config>,
|
||||||
|
single_client_mode: bool,
|
||||||
cli_overrides: Vec<(String, TomlValue)>,
|
cli_overrides: Vec<(String, TomlValue)>,
|
||||||
cloud_requirements: Arc<RwLock<CloudRequirementsLoader>>,
|
cloud_requirements: Arc<RwLock<CloudRequirementsLoader>>,
|
||||||
active_login: Arc<Mutex<Option<ActiveLogin>>>,
|
active_login: Arc<Mutex<Option<ActiveLogin>>>,
|
||||||
@@ -361,6 +362,7 @@ pub(crate) struct CodexMessageProcessorArgs {
|
|||||||
pub(crate) config: Arc<Config>,
|
pub(crate) config: Arc<Config>,
|
||||||
pub(crate) cli_overrides: Vec<(String, TomlValue)>,
|
pub(crate) cli_overrides: Vec<(String, TomlValue)>,
|
||||||
pub(crate) cloud_requirements: Arc<RwLock<CloudRequirementsLoader>>,
|
pub(crate) cloud_requirements: Arc<RwLock<CloudRequirementsLoader>>,
|
||||||
|
pub(crate) single_client_mode: bool,
|
||||||
pub(crate) feedback: CodexFeedback,
|
pub(crate) feedback: CodexFeedback,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,6 +399,7 @@ impl CodexMessageProcessor {
|
|||||||
config,
|
config,
|
||||||
cli_overrides,
|
cli_overrides,
|
||||||
cloud_requirements,
|
cloud_requirements,
|
||||||
|
single_client_mode,
|
||||||
feedback,
|
feedback,
|
||||||
} = args;
|
} = args;
|
||||||
Self {
|
Self {
|
||||||
@@ -405,6 +408,7 @@ impl CodexMessageProcessor {
|
|||||||
outgoing: outgoing.clone(),
|
outgoing: outgoing.clone(),
|
||||||
codex_linux_sandbox_exe,
|
codex_linux_sandbox_exe,
|
||||||
config,
|
config,
|
||||||
|
single_client_mode,
|
||||||
cli_overrides,
|
cli_overrides,
|
||||||
cloud_requirements,
|
cloud_requirements,
|
||||||
active_login: Arc::new(Mutex::new(None)),
|
active_login: Arc::new(Mutex::new(None)),
|
||||||
@@ -3042,21 +3046,14 @@ impl CodexMessageProcessor {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(err) = self
|
let thread_state = self.thread_state_manager.thread_state(existing_thread_id);
|
||||||
.ensure_conversation_listener(
|
self.ensure_listener_task_running(
|
||||||
existing_thread_id,
|
existing_thread_id,
|
||||||
request_id.connection_id,
|
existing_thread.clone(),
|
||||||
false,
|
thread_state.clone(),
|
||||||
ApiVersion::V2,
|
ApiVersion::V2,
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
{
|
|
||||||
tracing::warn!(
|
|
||||||
"failed to attach listener for thread {}: {}",
|
|
||||||
existing_thread_id,
|
|
||||||
err.message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let config_snapshot = existing_thread.config_snapshot().await;
|
let config_snapshot = existing_thread.config_snapshot().await;
|
||||||
let mismatch_details = collect_resume_override_mismatches(params, &config_snapshot);
|
let mismatch_details = collect_resume_override_mismatches(params, &config_snapshot);
|
||||||
@@ -3068,41 +3065,39 @@ impl CodexMessageProcessor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(mut thread) = self
|
let listener_command_tx = {
|
||||||
.load_thread_from_rollout_or_send_internal(
|
let thread_state = thread_state.lock().await;
|
||||||
request_id.clone(),
|
thread_state.listener_command_tx()
|
||||||
existing_thread_id,
|
};
|
||||||
rollout_path.as_path(),
|
let Some(listener_command_tx) = listener_command_tx else {
|
||||||
config_snapshot.model_provider_id.as_str(),
|
let err = JSONRPCErrorError {
|
||||||
)
|
code: INTERNAL_ERROR_CODE,
|
||||||
.await
|
message: format!(
|
||||||
else {
|
"failed to enqueue running thread resume for thread {existing_thread_id}: thread listener is not running"
|
||||||
|
),
|
||||||
|
data: None,
|
||||||
|
};
|
||||||
|
self.outgoing.send_error(request_id, err).await;
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
let ThreadConfigSnapshot {
|
let command = crate::thread_state::ThreadListenerCommand::SendThreadResumeResponse(
|
||||||
model,
|
crate::thread_state::PendingThreadResumeRequest {
|
||||||
model_provider_id,
|
request_id: request_id.clone(),
|
||||||
approval_policy,
|
rollout_path,
|
||||||
sandbox_policy,
|
config_snapshot,
|
||||||
cwd,
|
},
|
||||||
reasoning_effort,
|
);
|
||||||
..
|
if listener_command_tx.send(command).is_err() {
|
||||||
} = config_snapshot;
|
let err = JSONRPCErrorError {
|
||||||
thread.status = self
|
code: INTERNAL_ERROR_CODE,
|
||||||
.thread_watch_manager
|
message: format!(
|
||||||
.loaded_status_for_thread(&thread.id)
|
"failed to enqueue running thread resume for thread {existing_thread_id}: thread listener command channel is closed"
|
||||||
.await;
|
),
|
||||||
let response = ThreadResumeResponse {
|
data: None,
|
||||||
thread,
|
};
|
||||||
model,
|
self.outgoing.send_error(request_id, err).await;
|
||||||
model_provider: model_provider_id,
|
}
|
||||||
cwd,
|
|
||||||
approval_policy: approval_policy.into(),
|
|
||||||
sandbox: sandbox_policy.into(),
|
|
||||||
reasoning_effort,
|
|
||||||
};
|
|
||||||
self.outgoing.send_response(request_id, response).await;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
@@ -5817,17 +5812,18 @@ impl CodexMessageProcessor {
|
|||||||
api_version: ApiVersion,
|
api_version: ApiVersion,
|
||||||
) {
|
) {
|
||||||
let (cancel_tx, mut cancel_rx) = oneshot::channel();
|
let (cancel_tx, mut cancel_rx) = oneshot::channel();
|
||||||
{
|
let mut listener_command_rx = {
|
||||||
let mut thread_state = thread_state.lock().await;
|
let mut thread_state = thread_state.lock().await;
|
||||||
if thread_state.listener_matches(&conversation) {
|
if thread_state.listener_matches(&conversation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
thread_state.set_listener(cancel_tx, &conversation);
|
thread_state.set_listener(cancel_tx, &conversation)
|
||||||
}
|
};
|
||||||
let outgoing_for_task = self.outgoing.clone();
|
let outgoing_for_task = self.outgoing.clone();
|
||||||
let thread_manager = self.thread_manager.clone();
|
let thread_manager = self.thread_manager.clone();
|
||||||
let thread_watch_manager = self.thread_watch_manager.clone();
|
let thread_watch_manager = self.thread_watch_manager.clone();
|
||||||
let fallback_model_provider = self.config.model_provider_id.clone();
|
let fallback_model_provider = self.config.model_provider_id.clone();
|
||||||
|
let single_client_mode = self.single_client_mode;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
@@ -5869,7 +5865,10 @@ impl CodexMessageProcessor {
|
|||||||
conversation_id.to_string().into(),
|
conversation_id.to_string().into(),
|
||||||
);
|
);
|
||||||
let (subscribed_connection_ids, raw_events_enabled) = {
|
let (subscribed_connection_ids, raw_events_enabled) = {
|
||||||
let thread_state = thread_state.lock().await;
|
let mut thread_state = thread_state.lock().await;
|
||||||
|
if !single_client_mode {
|
||||||
|
thread_state.track_current_turn_event(&event.msg);
|
||||||
|
}
|
||||||
(
|
(
|
||||||
thread_state.subscribed_connection_ids(),
|
thread_state.subscribed_connection_ids(),
|
||||||
thread_state.experimental_raw_events,
|
thread_state.experimental_raw_events,
|
||||||
@@ -5908,6 +5907,25 @@ impl CodexMessageProcessor {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
listener_command = listener_command_rx.recv() => {
|
||||||
|
let Some(listener_command) = listener_command else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
match listener_command {
|
||||||
|
crate::thread_state::ThreadListenerCommand::SendThreadResumeResponse(
|
||||||
|
resume_request,
|
||||||
|
) => {
|
||||||
|
handle_pending_thread_resume_request(
|
||||||
|
conversation_id,
|
||||||
|
&thread_state,
|
||||||
|
&thread_watch_manager,
|
||||||
|
&outgoing_for_task,
|
||||||
|
resume_request,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -6206,6 +6224,106 @@ impl CodexMessageProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_pending_thread_resume_request(
|
||||||
|
conversation_id: ThreadId,
|
||||||
|
thread_state: &Arc<Mutex<ThreadState>>,
|
||||||
|
thread_watch_manager: &ThreadWatchManager,
|
||||||
|
outgoing: &Arc<OutgoingMessageSender>,
|
||||||
|
pending: crate::thread_state::PendingThreadResumeRequest,
|
||||||
|
) {
|
||||||
|
let active_turn = {
|
||||||
|
let state = thread_state.lock().await;
|
||||||
|
state.active_turn_snapshot()
|
||||||
|
};
|
||||||
|
|
||||||
|
let request_id = pending.request_id;
|
||||||
|
let connection_id = request_id.connection_id;
|
||||||
|
let mut thread = match load_thread_for_running_resume_response(
|
||||||
|
conversation_id,
|
||||||
|
pending.rollout_path.as_path(),
|
||||||
|
pending.config_snapshot.model_provider_id.as_str(),
|
||||||
|
active_turn.as_ref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(thread) => thread,
|
||||||
|
Err(message) => {
|
||||||
|
outgoing
|
||||||
|
.send_error(
|
||||||
|
request_id,
|
||||||
|
JSONRPCErrorError {
|
||||||
|
code: INTERNAL_ERROR_CODE,
|
||||||
|
message,
|
||||||
|
data: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
thread.status = thread_watch_manager
|
||||||
|
.loaded_status_for_thread(&thread.id)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let ThreadConfigSnapshot {
|
||||||
|
model,
|
||||||
|
model_provider_id,
|
||||||
|
approval_policy,
|
||||||
|
sandbox_policy,
|
||||||
|
cwd,
|
||||||
|
reasoning_effort,
|
||||||
|
..
|
||||||
|
} = pending.config_snapshot;
|
||||||
|
let response = ThreadResumeResponse {
|
||||||
|
thread,
|
||||||
|
model,
|
||||||
|
model_provider: model_provider_id,
|
||||||
|
cwd,
|
||||||
|
approval_policy: approval_policy.into(),
|
||||||
|
sandbox: sandbox_policy.into(),
|
||||||
|
reasoning_effort,
|
||||||
|
};
|
||||||
|
outgoing.send_response(request_id, response).await;
|
||||||
|
thread_state.lock().await.add_connection(connection_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_thread_for_running_resume_response(
|
||||||
|
conversation_id: ThreadId,
|
||||||
|
rollout_path: &Path,
|
||||||
|
fallback_provider: &str,
|
||||||
|
active_turn: Option<&Turn>,
|
||||||
|
) -> std::result::Result<Thread, String> {
|
||||||
|
let mut thread = read_summary_from_rollout(rollout_path, fallback_provider)
|
||||||
|
.await
|
||||||
|
.map(summary_to_thread)
|
||||||
|
.map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"failed to load rollout `{}` for thread {conversation_id}: {err}",
|
||||||
|
rollout_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut turns = read_rollout_items_from_rollout(rollout_path)
|
||||||
|
.await
|
||||||
|
.map(|items| build_turns_from_rollout_items(&items))
|
||||||
|
.map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"failed to load rollout `{}` for thread {conversation_id}: {err}",
|
||||||
|
rollout_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if let Some(active_turn) = active_turn {
|
||||||
|
merge_turn_history_with_active_turn(&mut turns, active_turn.clone());
|
||||||
|
}
|
||||||
|
thread.turns = turns;
|
||||||
|
Ok(thread)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_turn_history_with_active_turn(turns: &mut Vec<Turn>, active_turn: Turn) {
|
||||||
|
turns.retain(|turn| turn.id != active_turn.id);
|
||||||
|
turns.push(active_turn);
|
||||||
|
}
|
||||||
|
|
||||||
fn collect_resume_override_mismatches(
|
fn collect_resume_override_mismatches(
|
||||||
request: &ThreadResumeParams,
|
request: &ThreadResumeParams,
|
||||||
config_snapshot: &ThreadConfigSnapshot,
|
config_snapshot: &ThreadConfigSnapshot,
|
||||||
|
|||||||
@@ -238,7 +238,8 @@ pub async fn run_main_with_transport(
|
|||||||
Some(start_websocket_acceptor(bind_address, transport_event_tx.clone()).await?);
|
Some(start_websocket_acceptor(bind_address, transport_event_tx.clone()).await?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let shutdown_when_no_connections = matches!(transport, AppServerTransport::Stdio);
|
let single_client_mode = matches!(transport, AppServerTransport::Stdio);
|
||||||
|
let shutdown_when_no_connections = single_client_mode;
|
||||||
|
|
||||||
// Parse CLI overrides once and derive the base Config eagerly so later
|
// Parse CLI overrides once and derive the base Config eagerly so later
|
||||||
// components do not need to work with raw TOML values.
|
// components do not need to work with raw TOML values.
|
||||||
@@ -439,6 +440,7 @@ pub async fn run_main_with_transport(
|
|||||||
outgoing: outgoing_message_sender,
|
outgoing: outgoing_message_sender,
|
||||||
codex_linux_sandbox_exe,
|
codex_linux_sandbox_exe,
|
||||||
config: Arc::new(config),
|
config: Arc::new(config),
|
||||||
|
single_client_mode,
|
||||||
cli_overrides,
|
cli_overrides,
|
||||||
loader_overrides,
|
loader_overrides,
|
||||||
cloud_requirements: cloud_requirements.clone(),
|
cloud_requirements: cloud_requirements.clone(),
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ pub(crate) struct MessageProcessorArgs {
|
|||||||
pub(crate) outgoing: Arc<OutgoingMessageSender>,
|
pub(crate) outgoing: Arc<OutgoingMessageSender>,
|
||||||
pub(crate) codex_linux_sandbox_exe: Option<PathBuf>,
|
pub(crate) codex_linux_sandbox_exe: Option<PathBuf>,
|
||||||
pub(crate) config: Arc<Config>,
|
pub(crate) config: Arc<Config>,
|
||||||
|
pub(crate) single_client_mode: bool,
|
||||||
pub(crate) cli_overrides: Vec<(String, TomlValue)>,
|
pub(crate) cli_overrides: Vec<(String, TomlValue)>,
|
||||||
pub(crate) loader_overrides: LoaderOverrides,
|
pub(crate) loader_overrides: LoaderOverrides,
|
||||||
pub(crate) cloud_requirements: CloudRequirementsLoader,
|
pub(crate) cloud_requirements: CloudRequirementsLoader,
|
||||||
@@ -154,6 +155,7 @@ impl MessageProcessor {
|
|||||||
outgoing,
|
outgoing,
|
||||||
codex_linux_sandbox_exe,
|
codex_linux_sandbox_exe,
|
||||||
config,
|
config,
|
||||||
|
single_client_mode,
|
||||||
cli_overrides,
|
cli_overrides,
|
||||||
loader_overrides,
|
loader_overrides,
|
||||||
cloud_requirements,
|
cloud_requirements,
|
||||||
@@ -184,6 +186,7 @@ impl MessageProcessor {
|
|||||||
config: Arc::clone(&config),
|
config: Arc::clone(&config),
|
||||||
cli_overrides: cli_overrides.clone(),
|
cli_overrides: cli_overrides.clone(),
|
||||||
cloud_requirements: cloud_requirements.clone(),
|
cloud_requirements: cloud_requirements.clone(),
|
||||||
|
single_client_mode,
|
||||||
feedback,
|
feedback,
|
||||||
});
|
});
|
||||||
let config_api = ConfigApi::new(
|
let config_api = ConfigApi::new(
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
use crate::outgoing_message::ConnectionId;
|
use crate::outgoing_message::ConnectionId;
|
||||||
use crate::outgoing_message::ConnectionRequestId;
|
use crate::outgoing_message::ConnectionRequestId;
|
||||||
|
use codex_app_server_protocol::ThreadHistoryBuilder;
|
||||||
|
use codex_app_server_protocol::Turn;
|
||||||
use codex_app_server_protocol::TurnError;
|
use codex_app_server_protocol::TurnError;
|
||||||
use codex_core::CodexThread;
|
use codex_core::CodexThread;
|
||||||
|
use codex_core::ThreadConfigSnapshot;
|
||||||
use codex_protocol::ThreadId;
|
use codex_protocol::ThreadId;
|
||||||
|
use codex_protocol::protocol::EventMsg;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::Weak;
|
use std::sync::Weak;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -16,6 +22,16 @@ type PendingInterruptQueue = Vec<(
|
|||||||
crate::codex_message_processor::ApiVersion,
|
crate::codex_message_processor::ApiVersion,
|
||||||
)>;
|
)>;
|
||||||
|
|
||||||
|
pub(crate) struct PendingThreadResumeRequest {
|
||||||
|
pub(crate) request_id: ConnectionRequestId,
|
||||||
|
pub(crate) rollout_path: PathBuf,
|
||||||
|
pub(crate) config_snapshot: ThreadConfigSnapshot,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) enum ThreadListenerCommand {
|
||||||
|
SendThreadResumeResponse(PendingThreadResumeRequest),
|
||||||
|
}
|
||||||
|
|
||||||
/// Per-conversation accumulation of the latest states e.g. error message while a turn runs.
|
/// Per-conversation accumulation of the latest states e.g. error message while a turn runs.
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
pub(crate) struct TurnSummary {
|
pub(crate) struct TurnSummary {
|
||||||
@@ -31,6 +47,8 @@ pub(crate) struct ThreadState {
|
|||||||
pub(crate) turn_summary: TurnSummary,
|
pub(crate) turn_summary: TurnSummary,
|
||||||
pub(crate) cancel_tx: Option<oneshot::Sender<()>>,
|
pub(crate) cancel_tx: Option<oneshot::Sender<()>>,
|
||||||
pub(crate) experimental_raw_events: bool,
|
pub(crate) experimental_raw_events: bool,
|
||||||
|
listener_command_tx: Option<mpsc::UnboundedSender<ThreadListenerCommand>>,
|
||||||
|
current_turn_history: ThreadHistoryBuilder,
|
||||||
listener_thread: Option<Weak<CodexThread>>,
|
listener_thread: Option<Weak<CodexThread>>,
|
||||||
subscribed_connections: HashSet<ConnectionId>,
|
subscribed_connections: HashSet<ConnectionId>,
|
||||||
}
|
}
|
||||||
@@ -47,17 +65,22 @@ impl ThreadState {
|
|||||||
&mut self,
|
&mut self,
|
||||||
cancel_tx: oneshot::Sender<()>,
|
cancel_tx: oneshot::Sender<()>,
|
||||||
conversation: &Arc<CodexThread>,
|
conversation: &Arc<CodexThread>,
|
||||||
) {
|
) -> mpsc::UnboundedReceiver<ThreadListenerCommand> {
|
||||||
if let Some(previous) = self.cancel_tx.replace(cancel_tx) {
|
if let Some(previous) = self.cancel_tx.replace(cancel_tx) {
|
||||||
let _ = previous.send(());
|
let _ = previous.send(());
|
||||||
}
|
}
|
||||||
|
let (listener_command_tx, listener_command_rx) = mpsc::unbounded_channel();
|
||||||
|
self.listener_command_tx = Some(listener_command_tx);
|
||||||
self.listener_thread = Some(Arc::downgrade(conversation));
|
self.listener_thread = Some(Arc::downgrade(conversation));
|
||||||
|
listener_command_rx
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn clear_listener(&mut self) {
|
pub(crate) fn clear_listener(&mut self) {
|
||||||
if let Some(cancel_tx) = self.cancel_tx.take() {
|
if let Some(cancel_tx) = self.cancel_tx.take() {
|
||||||
let _ = cancel_tx.send(());
|
let _ = cancel_tx.send(());
|
||||||
}
|
}
|
||||||
|
self.listener_command_tx = None;
|
||||||
|
self.current_turn_history.reset();
|
||||||
self.listener_thread = None;
|
self.listener_thread = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +99,23 @@ impl ThreadState {
|
|||||||
pub(crate) fn set_experimental_raw_events(&mut self, enabled: bool) {
|
pub(crate) fn set_experimental_raw_events(&mut self, enabled: bool) {
|
||||||
self.experimental_raw_events = enabled;
|
self.experimental_raw_events = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn listener_command_tx(
|
||||||
|
&self,
|
||||||
|
) -> Option<mpsc::UnboundedSender<ThreadListenerCommand>> {
|
||||||
|
self.listener_command_tx.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn active_turn_snapshot(&self) -> Option<Turn> {
|
||||||
|
self.current_turn_history.active_turn_snapshot()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn track_current_turn_event(&mut self, event: &EventMsg) {
|
||||||
|
self.current_turn_history.handle_event(event);
|
||||||
|
if !self.current_turn_history.has_active_turn() {
|
||||||
|
self.current_turn_history.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -200,12 +240,24 @@ impl ThreadStateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn remove_connection(&mut self, connection_id: ConnectionId) {
|
pub(crate) async fn remove_connection(&mut self, connection_id: ConnectionId) {
|
||||||
let Some(thread_ids) = self.thread_ids_by_connection.remove(&connection_id) else {
|
let thread_ids = self
|
||||||
return;
|
.thread_ids_by_connection
|
||||||
};
|
.remove(&connection_id)
|
||||||
|
.unwrap_or_default();
|
||||||
self.subscription_state_by_id
|
self.subscription_state_by_id
|
||||||
.retain(|_, state| state.connection_id != connection_id);
|
.retain(|_, state| state.connection_id != connection_id);
|
||||||
|
|
||||||
|
if thread_ids.is_empty() {
|
||||||
|
for thread_state in self.thread_states.values() {
|
||||||
|
let mut thread_state = thread_state.lock().await;
|
||||||
|
thread_state.remove_connection(connection_id);
|
||||||
|
if thread_state.subscribed_connection_ids().is_empty() {
|
||||||
|
thread_state.clear_listener();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for thread_id in thread_ids {
|
for thread_id in thread_ids {
|
||||||
if let Some(thread_state) = self.thread_states.get(&thread_id) {
|
if let Some(thread_state) = self.thread_states.get(&thread_id) {
|
||||||
let mut thread_state = thread_state.lock().await;
|
let mut thread_state = thread_state.lock().await;
|
||||||
|
|||||||
Reference in New Issue
Block a user