mirror of
https://github.com/openai/codex.git
synced 2026-04-25 23:24:55 +00:00
app-server: Replay pending item requests on thread/resume (#12560)
Replay pending client requests after `thread/resume` and emit resolved notifications when those requests clear so approval/input UI state stays in sync after reconnects and across subscribed clients. Affected RPCs: - `item/commandExecution/requestApproval` - `item/fileChange/requestApproval` - `item/tool/requestUserInput` Motivation: - Resumed clients need to see pending approval/input requests that were already outstanding before the reconnect. - Clients also need an explicit signal when a pending request resolves or is cleared so stale UI can be removed on turn start, completion, or interruption. Implementation notes: - Use pending client requests from `OutgoingMessageSender` in order to replay them after `thread/resume` attaches the connection, using original request ids. - Emit `serverRequest/resolved` when pending requests are answered or cleared by lifecycle cleanup. - Update the app-server protocol schema, generated TypeScript bindings, and README docs for the replay/resolution flow. High-level test plan: - Added automated coverage for replaying pending command execution and file change approval requests on `thread/resume`. - Added automated coverage for resolved notifications in command approval, file change approval, request_user_input, turn start, and turn interrupt flows. - Verified schema/docs updates in the relevant protocol and app-server tests. Manual testing: - Tested reconnect/resume with multiple connections. - Confirmed state stayed in sync between connections.
This commit is contained in:
committed by
GitHub
parent
66b0adb34c
commit
69d7a456bb
@@ -22,6 +22,7 @@ use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::protocol::AgentReasoningEvent;
|
||||
use codex_protocol::protocol::AgentReasoningRawContentEvent;
|
||||
use codex_protocol::protocol::AgentStatus;
|
||||
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use codex_protocol::protocol::CompactedItem;
|
||||
use codex_protocol::protocol::ContextCompactedEvent;
|
||||
use codex_protocol::protocol::DynamicToolCallResponseEvent;
|
||||
@@ -126,6 +127,9 @@ impl ThreadHistoryBuilder {
|
||||
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::ApplyPatchApprovalRequest(payload) => {
|
||||
self.handle_apply_patch_approval_request(payload)
|
||||
}
|
||||
EventMsg::PatchApplyBegin(payload) => self.handle_patch_apply_begin(payload),
|
||||
EventMsg::PatchApplyEnd(payload) => self.handle_patch_apply_end(payload),
|
||||
EventMsg::DynamicToolCallRequest(payload) => {
|
||||
@@ -364,6 +368,19 @@ impl ThreadHistoryBuilder {
|
||||
self.upsert_item_in_turn_id(&payload.turn_id, item);
|
||||
}
|
||||
|
||||
fn handle_apply_patch_approval_request(&mut self, payload: &ApplyPatchApprovalRequestEvent) {
|
||||
let item = ThreadItem::FileChange {
|
||||
id: payload.call_id.clone(),
|
||||
changes: convert_patch_changes(&payload.changes),
|
||||
status: PatchApplyStatus::InProgress,
|
||||
};
|
||||
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_patch_apply_begin(&mut self, payload: &PatchApplyBeginEvent) {
|
||||
let item = ThreadItem::FileChange {
|
||||
id: payload.call_id.clone(),
|
||||
@@ -1080,6 +1097,7 @@ mod tests {
|
||||
use codex_protocol::protocol::AgentMessageEvent;
|
||||
use codex_protocol::protocol::AgentReasoningEvent;
|
||||
use codex_protocol::protocol::AgentReasoningRawContentEvent;
|
||||
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use codex_protocol::protocol::CodexErrorInfo;
|
||||
use codex_protocol::protocol::CompactedItem;
|
||||
use codex_protocol::protocol::DynamicToolCallResponseEvent;
|
||||
@@ -1088,6 +1106,7 @@ mod tests {
|
||||
use codex_protocol::protocol::ItemStartedEvent;
|
||||
use codex_protocol::protocol::McpInvocation;
|
||||
use codex_protocol::protocol::McpToolCallEndEvent;
|
||||
use codex_protocol::protocol::PatchApplyBeginEvent;
|
||||
use codex_protocol::protocol::ThreadRolledBackEvent;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::TurnAbortedEvent;
|
||||
@@ -1980,6 +1999,133 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_apply_begin_updates_active_turn_snapshot_with_file_change() {
|
||||
let turn_id = "turn-1";
|
||||
let mut builder = ThreadHistoryBuilder::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: "apply patch".into(),
|
||||
images: None,
|
||||
text_elements: Vec::new(),
|
||||
local_images: Vec::new(),
|
||||
}),
|
||||
EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
|
||||
call_id: "patch-call".into(),
|
||||
turn_id: turn_id.to_string(),
|
||||
auto_approved: false,
|
||||
changes: [(
|
||||
PathBuf::from("README.md"),
|
||||
codex_protocol::protocol::FileChange::Add {
|
||||
content: "hello\n".into(),
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
}),
|
||||
];
|
||||
|
||||
for event in &events {
|
||||
builder.handle_event(event);
|
||||
}
|
||||
|
||||
let snapshot = builder
|
||||
.active_turn_snapshot()
|
||||
.expect("active turn snapshot");
|
||||
assert_eq!(snapshot.id, turn_id);
|
||||
assert_eq!(snapshot.status, TurnStatus::InProgress);
|
||||
assert_eq!(
|
||||
snapshot.items,
|
||||
vec![
|
||||
ThreadItem::UserMessage {
|
||||
id: "item-1".into(),
|
||||
content: vec![UserInput::Text {
|
||||
text: "apply patch".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
},
|
||||
ThreadItem::FileChange {
|
||||
id: "patch-call".into(),
|
||||
changes: vec![FileUpdateChange {
|
||||
path: "README.md".into(),
|
||||
kind: PatchChangeKind::Add,
|
||||
diff: "hello\n".into(),
|
||||
}],
|
||||
status: PatchApplyStatus::InProgress,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_patch_approval_request_updates_active_turn_snapshot_with_file_change() {
|
||||
let turn_id = "turn-1";
|
||||
let mut builder = ThreadHistoryBuilder::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: "apply patch".into(),
|
||||
images: None,
|
||||
text_elements: Vec::new(),
|
||||
local_images: Vec::new(),
|
||||
}),
|
||||
EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
|
||||
call_id: "patch-call".into(),
|
||||
turn_id: turn_id.to_string(),
|
||||
changes: [(
|
||||
PathBuf::from("README.md"),
|
||||
codex_protocol::protocol::FileChange::Add {
|
||||
content: "hello\n".into(),
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
reason: None,
|
||||
grant_root: None,
|
||||
}),
|
||||
];
|
||||
|
||||
for event in &events {
|
||||
builder.handle_event(event);
|
||||
}
|
||||
|
||||
let snapshot = builder
|
||||
.active_turn_snapshot()
|
||||
.expect("active turn snapshot");
|
||||
assert_eq!(snapshot.id, turn_id);
|
||||
assert_eq!(snapshot.status, TurnStatus::InProgress);
|
||||
assert_eq!(
|
||||
snapshot.items,
|
||||
vec![
|
||||
ThreadItem::UserMessage {
|
||||
id: "item-1".into(),
|
||||
content: vec![UserInput::Text {
|
||||
text: "apply patch".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
},
|
||||
ThreadItem::FileChange {
|
||||
id: "patch-call".into(),
|
||||
changes: vec![FileUpdateChange {
|
||||
path: "README.md".into(),
|
||||
kind: PatchChangeKind::Add,
|
||||
diff: "hello\n".into(),
|
||||
}],
|
||||
status: PatchApplyStatus::InProgress,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn late_turn_complete_does_not_close_active_turn() {
|
||||
let events = vec![
|
||||
|
||||
Reference in New Issue
Block a user