mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
Merge branch 'main' into zuxin/summary_naming
This commit is contained in:
@@ -113,6 +113,13 @@
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"approvalId": {
|
||||
"description": "Unique identifier for this specific approval callback.\n\nFor regular shell/unified_exec approvals, this is null.\n\nFor zsh-exec-bridge subcommand approvals, multiple callbacks can belong to one parent `itemId`, so `approvalId` is a distinct opaque callback id (a UUID) used to disambiguate routing.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"command": {
|
||||
"description": "The command to be executed.",
|
||||
"type": [
|
||||
|
||||
@@ -1452,8 +1452,15 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"approval_id": {
|
||||
"description": "Identifier for this specific approval callback.\n\nWhen absent, the approval is for the command item itself (`call_id`). This is present for subcommand approvals (via execve intercept).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"call_id": {
|
||||
"description": "Identifier for the associated exec call, if available.",
|
||||
"description": "Identifier for the associated command execution item.",
|
||||
"type": "string"
|
||||
},
|
||||
"command": {
|
||||
@@ -6392,8 +6399,15 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"approval_id": {
|
||||
"description": "Identifier for this specific approval callback.\n\nWhen absent, the approval is for the command item itself (`call_id`). This is present for subcommand approvals (via execve intercept).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"call_id": {
|
||||
"description": "Identifier for the associated exec call, if available.",
|
||||
"description": "Identifier for the associated command execution item.",
|
||||
"type": "string"
|
||||
},
|
||||
"command": {
|
||||
|
||||
@@ -117,6 +117,13 @@
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"approvalId": {
|
||||
"description": "Identifier for this specific approval callback.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"callId": {
|
||||
"description": "Use to correlate this with [codex_core::protocol::ExecCommandBeginEvent] and [codex_core::protocol::ExecCommandEndEvent].",
|
||||
"type": "string"
|
||||
|
||||
@@ -2268,8 +2268,15 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"approval_id": {
|
||||
"description": "Identifier for this specific approval callback.\n\nWhen absent, the approval is for the command item itself (`call_id`). This is present for subcommand approvals (via execve intercept).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"call_id": {
|
||||
"description": "Identifier for the associated exec call, if available.",
|
||||
"description": "Identifier for the associated command execution item.",
|
||||
"type": "string"
|
||||
},
|
||||
"command": {
|
||||
|
||||
@@ -179,6 +179,13 @@
|
||||
},
|
||||
"CommandExecutionRequestApprovalParams": {
|
||||
"properties": {
|
||||
"approvalId": {
|
||||
"description": "Unique identifier for this specific approval callback.\n\nFor regular shell/unified_exec approvals, this is null.\n\nFor zsh-exec-bridge subcommand approvals, multiple callbacks can belong to one parent `itemId`, so `approvalId` is a distinct opaque callback id (a UUID) used to disambiguate routing.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"command": {
|
||||
"description": "The command to be executed.",
|
||||
"type": [
|
||||
@@ -264,6 +271,13 @@
|
||||
},
|
||||
"ExecCommandApprovalParams": {
|
||||
"properties": {
|
||||
"approvalId": {
|
||||
"description": "Identifier for this specific approval callback.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"callId": {
|
||||
"description": "Use to correlate this with [codex_core::protocol::ExecCommandBeginEvent] and [codex_core::protocol::ExecCommandEndEvent].",
|
||||
"type": "string"
|
||||
|
||||
@@ -2051,6 +2051,13 @@
|
||||
"CommandExecutionRequestApprovalParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"approvalId": {
|
||||
"description": "Unique identifier for this specific approval callback.\n\nFor regular shell/unified_exec approvals, this is null.\n\nFor zsh-exec-bridge subcommand approvals, multiple callbacks can belong to one parent `itemId`, so `approvalId` is a distinct opaque callback id (a UUID) used to disambiguate routing.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"command": {
|
||||
"description": "The command to be executed.",
|
||||
"type": [
|
||||
@@ -3465,8 +3472,15 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"approval_id": {
|
||||
"description": "Identifier for this specific approval callback.\n\nWhen absent, the approval is for the command item itself (`call_id`). This is present for subcommand approvals (via execve intercept).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"call_id": {
|
||||
"description": "Identifier for the associated exec call, if available.",
|
||||
"description": "Identifier for the associated command execution item.",
|
||||
"type": "string"
|
||||
},
|
||||
"command": {
|
||||
@@ -4939,6 +4953,13 @@
|
||||
"ExecCommandApprovalParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"approvalId": {
|
||||
"description": "Identifier for this specific approval callback.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"callId": {
|
||||
"description": "Use to correlate this with [codex_core::protocol::ExecCommandBeginEvent] and [codex_core::protocol::ExecCommandEndEvent].",
|
||||
"type": "string"
|
||||
|
||||
@@ -1452,8 +1452,15 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"approval_id": {
|
||||
"description": "Identifier for this specific approval callback.\n\nWhen absent, the approval is for the command item itself (`call_id`). This is present for subcommand approvals (via execve intercept).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"call_id": {
|
||||
"description": "Identifier for the associated exec call, if available.",
|
||||
"description": "Identifier for the associated command execution item.",
|
||||
"type": "string"
|
||||
},
|
||||
"command": {
|
||||
|
||||
@@ -1452,8 +1452,15 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"approval_id": {
|
||||
"description": "Identifier for this specific approval callback.\n\nWhen absent, the approval is for the command item itself (`call_id`). This is present for subcommand approvals (via execve intercept).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"call_id": {
|
||||
"description": "Identifier for the associated exec call, if available.",
|
||||
"description": "Identifier for the associated command execution item.",
|
||||
"type": "string"
|
||||
},
|
||||
"command": {
|
||||
|
||||
@@ -1452,8 +1452,15 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"approval_id": {
|
||||
"description": "Identifier for this specific approval callback.\n\nWhen absent, the approval is for the command item itself (`call_id`). This is present for subcommand approvals (via execve intercept).",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"call_id": {
|
||||
"description": "Identifier for the associated exec call, if available.",
|
||||
"description": "Identifier for the associated command execution item.",
|
||||
"type": "string"
|
||||
},
|
||||
"command": {
|
||||
|
||||
@@ -7,9 +7,16 @@ import type { ParsedCommand } from "./ParsedCommand";
|
||||
|
||||
export type ExecApprovalRequestEvent = {
|
||||
/**
|
||||
* Identifier for the associated exec call, if available.
|
||||
* Identifier for the associated command execution item.
|
||||
*/
|
||||
call_id: string,
|
||||
/**
|
||||
* Identifier for this specific approval callback.
|
||||
*
|
||||
* When absent, the approval is for the command item itself (`call_id`).
|
||||
* This is present for subcommand approvals (via execve intercept).
|
||||
*/
|
||||
approval_id?: string,
|
||||
/**
|
||||
* Turn ID that this command belongs to.
|
||||
* Uses `#[serde(default)]` for backwards compatibility.
|
||||
|
||||
@@ -9,4 +9,8 @@ export type ExecCommandApprovalParams = { conversationId: ThreadId,
|
||||
* Use to correlate this with [codex_core::protocol::ExecCommandBeginEvent]
|
||||
* and [codex_core::protocol::ExecCommandEndEvent].
|
||||
*/
|
||||
callId: string, command: Array<string>, cwd: string, reason: string | null, parsedCmd: Array<ParsedCommand>, };
|
||||
callId: string,
|
||||
/**
|
||||
* Identifier for this specific approval callback.
|
||||
*/
|
||||
approvalId: string | null, command: Array<string>, cwd: string, reason: string | null, parsedCmd: Array<ParsedCommand>, };
|
||||
|
||||
+10
@@ -5,6 +5,16 @@ import type { CommandAction } from "./CommandAction";
|
||||
import type { ExecPolicyAmendment } from "./ExecPolicyAmendment";
|
||||
|
||||
export type CommandExecutionRequestApprovalParams = { threadId: string, turnId: string, itemId: string,
|
||||
/**
|
||||
* Unique identifier for this specific approval callback.
|
||||
*
|
||||
* For regular shell/unified_exec approvals, this is null.
|
||||
*
|
||||
* For zsh-exec-bridge subcommand approvals, multiple callbacks can belong to
|
||||
* one parent `itemId`, so `approvalId` is a distinct opaque callback id
|
||||
* (a UUID) used to disambiguate routing.
|
||||
*/
|
||||
approvalId?: string | null,
|
||||
/**
|
||||
* Optional explanatory reason (e.g. request for network access).
|
||||
*/
|
||||
|
||||
@@ -1003,6 +1003,7 @@ mod tests {
|
||||
let params = v1::ExecCommandApprovalParams {
|
||||
conversation_id,
|
||||
call_id: "call-42".to_string(),
|
||||
approval_id: Some("approval-42".to_string()),
|
||||
command: vec!["echo".to_string(), "hello".to_string()],
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
reason: Some("because tests".to_string()),
|
||||
@@ -1022,6 +1023,7 @@ mod tests {
|
||||
"params": {
|
||||
"conversationId": "67e55044-10b1-426f-9247-bb680e5fe0c8",
|
||||
"callId": "call-42",
|
||||
"approvalId": "approval-42",
|
||||
"command": ["echo", "hello"],
|
||||
"cwd": "/tmp",
|
||||
"reason": "because tests",
|
||||
|
||||
@@ -258,6 +258,8 @@ pub struct ExecCommandApprovalParams {
|
||||
/// Use to correlate this with [codex_core::protocol::ExecCommandBeginEvent]
|
||||
/// and [codex_core::protocol::ExecCommandEndEvent].
|
||||
pub call_id: String,
|
||||
/// Identifier for this specific approval callback.
|
||||
pub approval_id: Option<String>,
|
||||
pub command: Vec<String>,
|
||||
pub cwd: PathBuf,
|
||||
pub reason: Option<String>,
|
||||
|
||||
@@ -3181,7 +3181,18 @@ pub struct CommandExecutionRequestApprovalParams {
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
pub item_id: String,
|
||||
/// Unique identifier for this specific approval callback.
|
||||
///
|
||||
/// For regular shell/unified_exec approvals, this is null.
|
||||
///
|
||||
/// For zsh-exec-bridge subcommand approvals, multiple callbacks can belong to
|
||||
/// one parent `itemId`, so `approvalId` is a distinct opaque callback id
|
||||
/// (a UUID) used to disambiguate routing.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub approval_id: Option<String>,
|
||||
/// Optional explanatory reason (e.g. request for network access).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub reason: Option<String>,
|
||||
/// The command to be executed.
|
||||
@@ -3197,6 +3208,7 @@ pub struct CommandExecutionRequestApprovalParams {
|
||||
#[ts(optional = nullable)]
|
||||
pub command_actions: Option<Vec<CommandAction>>,
|
||||
/// Optional proposed execpolicy amendment to allow similar commands without prompting.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
|
||||
}
|
||||
|
||||
@@ -1302,6 +1302,7 @@ impl CodexClient {
|
||||
thread_id,
|
||||
turn_id,
|
||||
item_id,
|
||||
approval_id,
|
||||
reason,
|
||||
command,
|
||||
cwd,
|
||||
@@ -1310,7 +1311,8 @@ impl CodexClient {
|
||||
} = params;
|
||||
|
||||
println!(
|
||||
"\n< commandExecution approval requested for thread {thread_id}, turn {turn_id}, item {item_id}"
|
||||
"\n< commandExecution approval requested for thread {thread_id}, turn {turn_id}, item {item_id}, approval {}",
|
||||
approval_id.as_deref().unwrap_or("<none>")
|
||||
);
|
||||
if let Some(reason) = reason.as_deref() {
|
||||
println!("< reason: {reason}");
|
||||
|
||||
@@ -637,7 +637,7 @@ Certain actions (shell commands or modifying files) may require explicit user ap
|
||||
Order of messages:
|
||||
|
||||
1. `item/started` — shows the pending `commandExecution` item with `command`, `cwd`, and other fields so you can render the proposed action.
|
||||
2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, optionally `reason`, plus `command`, `cwd`, and `commandActions` for friendly display.
|
||||
2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, optionally `approvalId` (for subcommand callbacks), `reason`, plus `command`, `cwd`, and `commandActions` for friendly display.
|
||||
3. Client response — `{ "decision": "accept", "acceptSettings": { "forSession": false } }` or `{ "decision": "decline" }`.
|
||||
4. `item/completed` — final `commandExecution` item with `status: "completed" | "failed" | "declined"` and execution output. Render this as the authoritative result.
|
||||
|
||||
|
||||
@@ -214,76 +214,88 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
});
|
||||
}
|
||||
},
|
||||
EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
|
||||
call_id,
|
||||
turn_id,
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
proposed_execpolicy_amendment,
|
||||
parsed_cmd,
|
||||
..
|
||||
}) => match api_version {
|
||||
ApiVersion::V1 => {
|
||||
let params = ExecCommandApprovalParams {
|
||||
conversation_id,
|
||||
call_id: call_id.clone(),
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
parsed_cmd,
|
||||
};
|
||||
let rx = outgoing
|
||||
.send_request(ServerRequestPayload::ExecCommandApproval(params))
|
||||
.await;
|
||||
tokio::spawn(async move {
|
||||
on_exec_approval_response(call_id, event_turn_id, rx, conversation).await;
|
||||
});
|
||||
}
|
||||
ApiVersion::V2 => {
|
||||
let item_id = call_id.clone();
|
||||
let command_actions = parsed_cmd
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(V2ParsedCommand::from)
|
||||
.collect::<Vec<_>>();
|
||||
let command_string = shlex_join(&command);
|
||||
let proposed_execpolicy_amendment_v2 =
|
||||
proposed_execpolicy_amendment.map(V2ExecPolicyAmendment::from);
|
||||
|
||||
let params = CommandExecutionRequestApprovalParams {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: turn_id.clone(),
|
||||
// Until we migrate the core to be aware of a first class CommandExecutionItem
|
||||
// and emit the corresponding EventMsg, we repurpose the call_id as the item_id.
|
||||
item_id: item_id.clone(),
|
||||
reason,
|
||||
command: Some(command_string.clone()),
|
||||
cwd: Some(cwd.clone()),
|
||||
command_actions: Some(command_actions.clone()),
|
||||
proposed_execpolicy_amendment: proposed_execpolicy_amendment_v2,
|
||||
};
|
||||
let rx = outgoing
|
||||
.send_request(ServerRequestPayload::CommandExecutionRequestApproval(
|
||||
params,
|
||||
))
|
||||
.await;
|
||||
tokio::spawn(async move {
|
||||
on_command_execution_request_approval_response(
|
||||
event_turn_id,
|
||||
EventMsg::ExecApprovalRequest(ev) => {
|
||||
let approval_id_for_op = ev.effective_approval_id();
|
||||
let ExecApprovalRequestEvent {
|
||||
call_id,
|
||||
approval_id,
|
||||
turn_id,
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
proposed_execpolicy_amendment,
|
||||
parsed_cmd,
|
||||
..
|
||||
} = ev;
|
||||
match api_version {
|
||||
ApiVersion::V1 => {
|
||||
let params = ExecCommandApprovalParams {
|
||||
conversation_id,
|
||||
item_id,
|
||||
command_string,
|
||||
call_id: call_id.clone(),
|
||||
approval_id,
|
||||
command,
|
||||
cwd,
|
||||
command_actions,
|
||||
rx,
|
||||
conversation,
|
||||
outgoing,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
reason,
|
||||
parsed_cmd,
|
||||
};
|
||||
let rx = outgoing
|
||||
.send_request(ServerRequestPayload::ExecCommandApproval(params))
|
||||
.await;
|
||||
tokio::spawn(async move {
|
||||
on_exec_approval_response(
|
||||
approval_id_for_op,
|
||||
event_turn_id,
|
||||
rx,
|
||||
conversation,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
ApiVersion::V2 => {
|
||||
let command_actions = parsed_cmd
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(V2ParsedCommand::from)
|
||||
.collect::<Vec<_>>();
|
||||
let command_string = shlex_join(&command);
|
||||
let proposed_execpolicy_amendment_v2 =
|
||||
proposed_execpolicy_amendment.map(V2ExecPolicyAmendment::from);
|
||||
|
||||
let params = CommandExecutionRequestApprovalParams {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: turn_id.clone(),
|
||||
item_id: call_id.clone(),
|
||||
approval_id: approval_id.clone(),
|
||||
reason,
|
||||
command: Some(command_string.clone()),
|
||||
cwd: Some(cwd.clone()),
|
||||
command_actions: Some(command_actions.clone()),
|
||||
proposed_execpolicy_amendment: proposed_execpolicy_amendment_v2,
|
||||
};
|
||||
let rx = outgoing
|
||||
.send_request(ServerRequestPayload::CommandExecutionRequestApproval(
|
||||
params,
|
||||
))
|
||||
.await;
|
||||
tokio::spawn(async move {
|
||||
on_command_execution_request_approval_response(
|
||||
event_turn_id,
|
||||
conversation_id,
|
||||
approval_id,
|
||||
call_id,
|
||||
command_string,
|
||||
cwd,
|
||||
command_actions,
|
||||
rx,
|
||||
conversation,
|
||||
outgoing,
|
||||
thread_state.clone(),
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
EventMsg::RequestUserInput(request) => {
|
||||
if matches!(api_version, ApiVersion::V2) {
|
||||
let questions = request
|
||||
@@ -933,6 +945,14 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
let cwd = exec_command_begin_event.cwd;
|
||||
let process_id = exec_command_begin_event.process_id;
|
||||
|
||||
{
|
||||
let mut state = thread_state.lock().await;
|
||||
state
|
||||
.turn_summary
|
||||
.command_execution_started
|
||||
.insert(item_id.clone());
|
||||
}
|
||||
|
||||
let item = ThreadItem::CommandExecution {
|
||||
id: item_id,
|
||||
command,
|
||||
@@ -1020,6 +1040,14 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
..
|
||||
} = exec_command_end_event;
|
||||
|
||||
{
|
||||
let mut state = thread_state.lock().await;
|
||||
state
|
||||
.turn_summary
|
||||
.command_execution_started
|
||||
.remove(&call_id);
|
||||
}
|
||||
|
||||
let status: CommandExecutionStatus = (&status).into();
|
||||
let command_actions = parsed_cmd
|
||||
.into_iter()
|
||||
@@ -1739,6 +1767,7 @@ async fn on_file_change_request_approval_response(
|
||||
async fn on_command_execution_request_approval_response(
|
||||
event_turn_id: String,
|
||||
conversation_id: ThreadId,
|
||||
approval_id: Option<String>,
|
||||
item_id: String,
|
||||
command: String,
|
||||
cwd: PathBuf,
|
||||
@@ -1746,6 +1775,7 @@ async fn on_command_execution_request_approval_response(
|
||||
receiver: oneshot::Receiver<ClientRequestResult>,
|
||||
conversation: Arc<CodexThread>,
|
||||
outgoing: ThreadScopedOutgoingMessageSender,
|
||||
thread_state: Arc<Mutex<ThreadState>>,
|
||||
) {
|
||||
let response = receiver.await;
|
||||
let (decision, completion_status) = match response {
|
||||
@@ -1794,7 +1824,24 @@ async fn on_command_execution_request_approval_response(
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(status) = completion_status {
|
||||
let suppress_subcommand_completion_item = {
|
||||
// For regular shell/unified_exec approvals, approval_id is null.
|
||||
// For zsh-fork subcommand approvals, approval_id is present and
|
||||
// item_id points to the parent command item.
|
||||
if approval_id.is_some() {
|
||||
let state = thread_state.lock().await;
|
||||
state
|
||||
.turn_summary
|
||||
.command_execution_started
|
||||
.contains(&item_id)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(status) = completion_status
|
||||
&& !suppress_subcommand_completion_item
|
||||
{
|
||||
complete_command_execution_item(
|
||||
conversation_id,
|
||||
event_turn_id.clone(),
|
||||
@@ -1811,7 +1858,7 @@ async fn on_command_execution_request_approval_response(
|
||||
|
||||
if let Err(err) = conversation
|
||||
.submit(Op::ExecApproval {
|
||||
id: item_id,
|
||||
id: approval_id.unwrap_or_else(|| item_id.clone()),
|
||||
turn_id: Some(event_turn_id),
|
||||
decision,
|
||||
})
|
||||
|
||||
@@ -20,6 +20,7 @@ type PendingInterruptQueue = Vec<(
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct TurnSummary {
|
||||
pub(crate) file_change_started: HashSet<String>,
|
||||
pub(crate) command_execution_started: HashSet<String>,
|
||||
pub(crate) last_error: Option<TurnError>,
|
||||
}
|
||||
|
||||
|
||||
@@ -298,6 +298,7 @@ async fn test_send_user_turn_changes_approval_policy_behavior() -> Result<()> {
|
||||
ExecCommandApprovalParams {
|
||||
conversation_id,
|
||||
call_id: "call1".to_string(),
|
||||
approval_id: None,
|
||||
command: format_with_current_shell("python3 -c 'print(42)'"),
|
||||
cwd: working_directory.clone(),
|
||||
reason: None,
|
||||
|
||||
@@ -99,7 +99,9 @@ async fn response_model_field_mismatch_emits_model_rerouted_notification_v2_when
|
||||
"type": "response.created",
|
||||
"response": {
|
||||
"id": "resp-1",
|
||||
"model": SERVER_MODEL,
|
||||
"headers": {
|
||||
"OpenAI-Model": SERVER_MODEL
|
||||
}
|
||||
}
|
||||
}),
|
||||
responses::ev_assistant_message("msg-1", "Done"),
|
||||
|
||||
@@ -167,6 +167,7 @@ struct ResponseCompletedOutputTokensDetails {
|
||||
pub struct ResponsesStreamEvent {
|
||||
#[serde(rename = "type")]
|
||||
pub(crate) kind: String,
|
||||
headers: Option<Value>,
|
||||
response: Option<Value>,
|
||||
item: Option<Value>,
|
||||
delta: Option<String>,
|
||||
@@ -179,20 +180,27 @@ impl ResponsesStreamEvent {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
/// Returns the effective model reported by the server, if present.
|
||||
///
|
||||
/// Precedence:
|
||||
/// 1. `response.headers` for standard Responses stream events.
|
||||
/// 2. top-level `headers` for websocket metadata events (for example
|
||||
/// `codex.response.metadata`).
|
||||
pub fn response_model(&self) -> Option<String> {
|
||||
self.response.as_ref().and_then(extract_server_model)
|
||||
}
|
||||
}
|
||||
let response_headers_model = self
|
||||
.response
|
||||
.as_ref()
|
||||
.and_then(|response| response.get("headers"))
|
||||
.and_then(header_openai_model_value_from_json);
|
||||
|
||||
fn extract_server_model(value: &Value) -> Option<String> {
|
||||
value
|
||||
.get("model")
|
||||
.and_then(json_value_as_string)
|
||||
.or_else(|| {
|
||||
value
|
||||
.get("headers")
|
||||
.and_then(header_openai_model_value_from_json)
|
||||
})
|
||||
match response_headers_model {
|
||||
Some(model) => Some(model),
|
||||
None => self
|
||||
.headers
|
||||
.as_ref()
|
||||
.and_then(header_openai_model_value_from_json),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn header_openai_model_value_from_json(value: &Value) -> Option<String> {
|
||||
@@ -963,7 +971,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn process_sse_emits_server_model_from_response_payload() {
|
||||
async fn process_sse_ignores_response_model_field_in_payload() {
|
||||
let events = run_sse(vec![
|
||||
json!({
|
||||
"type": "response.created",
|
||||
@@ -982,14 +990,10 @@ mod tests {
|
||||
])
|
||||
.await;
|
||||
|
||||
assert_eq!(events.len(), 3);
|
||||
assert_eq!(events.len(), 2);
|
||||
assert_matches!(&events[0], ResponseEvent::Created);
|
||||
assert_matches!(
|
||||
&events[0],
|
||||
ResponseEvent::ServerModel(model) if model == CYBER_RESTRICTED_MODEL_FOR_TESTS
|
||||
);
|
||||
assert_matches!(&events[1], ResponseEvent::Created);
|
||||
assert_matches!(
|
||||
&events[2],
|
||||
&events[1],
|
||||
ResponseEvent::Completed {
|
||||
response_id,
|
||||
token_usage: None,
|
||||
@@ -1035,43 +1039,41 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn process_sse_emits_server_model_again_when_response_model_changes() {
|
||||
let events = run_sse(vec![
|
||||
json!({
|
||||
"type": "response.created",
|
||||
"response": {
|
||||
"id": "resp-1",
|
||||
"model": "gpt-5.2-codex"
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"type": "response.completed",
|
||||
"response": {
|
||||
"id": "resp-1",
|
||||
"model": "gpt-5.3-codex"
|
||||
}
|
||||
}),
|
||||
])
|
||||
.await;
|
||||
#[test]
|
||||
fn responses_stream_event_response_model_reads_top_level_headers() {
|
||||
let ev: ResponsesStreamEvent = serde_json::from_value(json!({
|
||||
"type": "codex.response.metadata",
|
||||
"headers": {
|
||||
"openai-model": CYBER_RESTRICTED_MODEL_FOR_TESTS,
|
||||
}
|
||||
}))
|
||||
.expect("expected event to deserialize");
|
||||
|
||||
assert_eq!(events.len(), 4);
|
||||
assert_matches!(
|
||||
&events[0],
|
||||
ResponseEvent::ServerModel(model) if model == "gpt-5.2-codex"
|
||||
assert_eq!(
|
||||
ev.response_model().as_deref(),
|
||||
Some(CYBER_RESTRICTED_MODEL_FOR_TESTS)
|
||||
);
|
||||
assert_matches!(&events[1], ResponseEvent::Created);
|
||||
assert_matches!(
|
||||
&events[2],
|
||||
ResponseEvent::ServerModel(model) if model == "gpt-5.3-codex"
|
||||
);
|
||||
assert_matches!(
|
||||
&events[3],
|
||||
ResponseEvent::Completed {
|
||||
response_id,
|
||||
token_usage: None,
|
||||
can_append: false
|
||||
} if response_id == "resp-1"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_stream_event_response_model_prefers_response_headers() {
|
||||
let ev: ResponsesStreamEvent = serde_json::from_value(json!({
|
||||
"type": "response.created",
|
||||
"headers": {
|
||||
"openai-model": "top-level-model"
|
||||
},
|
||||
"response": {
|
||||
"id": "resp-1",
|
||||
"headers": {
|
||||
"openai-model": CYBER_RESTRICTED_MODEL_FOR_TESTS
|
||||
}
|
||||
}
|
||||
}))
|
||||
.expect("expected event to deserialize");
|
||||
|
||||
assert_eq!(
|
||||
ev.response_model().as_deref(),
|
||||
Some(CYBER_RESTRICTED_MODEL_FOR_TESTS)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2155,7 +2155,7 @@ impl Session {
|
||||
|
||||
/// Emit an exec approval request event and await the user's decision.
|
||||
///
|
||||
/// The request is keyed by `call_id` so matching responses are delivered
|
||||
/// The request is keyed by `call_id` + `approval_id` so matching responses are delivered
|
||||
/// to the correct in-flight turn. If the task is aborted, this returns the
|
||||
/// default `ReviewDecision` (`Denied`).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -2163,32 +2163,36 @@ impl Session {
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
call_id: String,
|
||||
approval_id: Option<String>,
|
||||
command: Vec<String>,
|
||||
cwd: PathBuf,
|
||||
reason: Option<String>,
|
||||
network_approval_context: Option<NetworkApprovalContext>,
|
||||
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
|
||||
) -> ReviewDecision {
|
||||
// command-level approvals use `call_id`.
|
||||
// `approval_id` is only present for subcommand callbacks (execve intercept)
|
||||
let effective_approval_id = approval_id.clone().unwrap_or_else(|| call_id.clone());
|
||||
// Add the tx_approve callback to the map before sending the request.
|
||||
let (tx_approve, rx_approve) = oneshot::channel();
|
||||
let approval_id = call_id.clone();
|
||||
let prev_entry = {
|
||||
let mut active = self.active_turn.lock().await;
|
||||
match active.as_mut() {
|
||||
Some(at) => {
|
||||
let mut ts = at.turn_state.lock().await;
|
||||
ts.insert_pending_approval(approval_id.clone(), tx_approve)
|
||||
ts.insert_pending_approval(effective_approval_id.clone(), tx_approve)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
};
|
||||
if prev_entry.is_some() {
|
||||
warn!("Overwriting existing pending approval for call_id: {approval_id}");
|
||||
warn!("Overwriting existing pending approval for call_id: {effective_approval_id}");
|
||||
}
|
||||
|
||||
let parsed_cmd = parse_command(&command);
|
||||
let event = EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
|
||||
call_id,
|
||||
approval_id,
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
command,
|
||||
cwd,
|
||||
@@ -2440,7 +2444,9 @@ impl Session {
|
||||
server_model: String,
|
||||
) -> bool {
|
||||
let requested_model = turn_context.model_info.slug.clone();
|
||||
if server_model == requested_model {
|
||||
let server_model_normalized = server_model.to_ascii_lowercase();
|
||||
let requested_model_normalized = requested_model.to_ascii_lowercase();
|
||||
if server_model_normalized == requested_model_normalized {
|
||||
info!("server reported model {server_model} (matches requested model)");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -311,8 +311,10 @@ async fn handle_exec_approval(
|
||||
event: ExecApprovalRequestEvent,
|
||||
cancel_token: &CancellationToken,
|
||||
) {
|
||||
let approval_id_for_op = event.effective_approval_id();
|
||||
let ExecApprovalRequestEvent {
|
||||
call_id,
|
||||
approval_id,
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
@@ -320,23 +322,28 @@ async fn handle_exec_approval(
|
||||
proposed_execpolicy_amendment,
|
||||
..
|
||||
} = event;
|
||||
let approval_id = call_id.clone();
|
||||
// Race approval with cancellation and timeout to avoid hangs.
|
||||
let approval_fut = parent_session.request_command_approval(
|
||||
parent_ctx,
|
||||
call_id,
|
||||
approval_id,
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
network_approval_context,
|
||||
proposed_execpolicy_amendment,
|
||||
);
|
||||
let decision =
|
||||
await_approval_with_cancel(approval_fut, parent_session, &approval_id, cancel_token).await;
|
||||
let decision = await_approval_with_cancel(
|
||||
approval_fut,
|
||||
parent_session,
|
||||
&approval_id_for_op,
|
||||
cancel_token,
|
||||
)
|
||||
.await;
|
||||
|
||||
let _ = codex
|
||||
.submit(Op::ExecApproval {
|
||||
id: approval_id,
|
||||
id: approval_id_for_op,
|
||||
turn_id: Some(turn_id),
|
||||
decision,
|
||||
})
|
||||
|
||||
@@ -247,6 +247,7 @@ impl NetworkApprovalService {
|
||||
.request_command_approval(
|
||||
turn_context.as_ref(),
|
||||
attempt.call_id.clone(),
|
||||
None,
|
||||
attempt.command.clone(),
|
||||
attempt.cwd.clone(),
|
||||
Some(format!(
|
||||
|
||||
@@ -109,6 +109,7 @@ impl Approvable<ShellRequest> for ShellRuntime {
|
||||
.request_command_approval(
|
||||
turn,
|
||||
call_id,
|
||||
None,
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
|
||||
@@ -110,6 +110,7 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
.request_command_approval(
|
||||
turn,
|
||||
call_id,
|
||||
None,
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
|
||||
@@ -865,6 +865,88 @@ pub async fn mount_compact_json_once(server: &MockServer, body: serde_json::Valu
|
||||
.await
|
||||
}
|
||||
|
||||
/// Mount a `/responses/compact` mock that mirrors the default remote compaction shape:
|
||||
/// keep user+developer messages from the request, drop assistant/tool artifacts, and append one
|
||||
/// summary user message.
|
||||
pub async fn mount_compact_user_history_with_summary_once(
|
||||
server: &MockServer,
|
||||
summary_text: &str,
|
||||
) -> ResponseMock {
|
||||
mount_compact_user_history_with_summary_sequence(server, vec![summary_text.to_string()]).await
|
||||
}
|
||||
|
||||
/// Same as [`mount_compact_user_history_with_summary_once`], but for multiple compact calls.
|
||||
/// Each incoming compact request receives the next summary text in order.
|
||||
pub async fn mount_compact_user_history_with_summary_sequence(
|
||||
server: &MockServer,
|
||||
summary_texts: Vec<String>,
|
||||
) -> ResponseMock {
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct UserHistorySummaryResponder {
|
||||
num_calls: AtomicUsize,
|
||||
summary_texts: Vec<String>,
|
||||
}
|
||||
|
||||
impl Respond for UserHistorySummaryResponder {
|
||||
fn respond(&self, request: &wiremock::Request) -> ResponseTemplate {
|
||||
let call_num = self.num_calls.fetch_add(1, Ordering::SeqCst);
|
||||
let Some(summary_text) = self.summary_texts.get(call_num) else {
|
||||
panic!("no summary text for compact request {call_num}");
|
||||
};
|
||||
let body_bytes = decode_body_bytes(
|
||||
&request.body,
|
||||
request
|
||||
.headers
|
||||
.get("content-encoding")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
);
|
||||
let body_json: Value = serde_json::from_slice(&body_bytes)
|
||||
.unwrap_or_else(|err| panic!("failed to parse compact request body: {err}"));
|
||||
let mut output = body_json
|
||||
.get("input")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
// Match current remote compaction behavior: keep user/developer messages and
|
||||
// omit assistant/tool history entries.
|
||||
.filter(|item| {
|
||||
item.get("type").and_then(Value::as_str) == Some("message")
|
||||
&& matches!(
|
||||
item.get("role").and_then(Value::as_str),
|
||||
Some("user") | Some("developer")
|
||||
)
|
||||
})
|
||||
.collect::<Vec<Value>>();
|
||||
// Append the synthetic summary message as the newest user item.
|
||||
output.push(serde_json::json!({
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [{"type": "input_text", "text": summary_text}],
|
||||
}));
|
||||
ResponseTemplate::new(200)
|
||||
.insert_header("content-type", "application/json")
|
||||
.set_body_json(serde_json::json!({ "output": output }))
|
||||
}
|
||||
}
|
||||
|
||||
let num_calls = summary_texts.len();
|
||||
let responder = UserHistorySummaryResponder {
|
||||
num_calls: AtomicUsize::new(0),
|
||||
summary_texts,
|
||||
};
|
||||
let (mock, response_mock) = compact_mock();
|
||||
mock.respond_with(responder)
|
||||
.up_to_n_times(num_calls as u64)
|
||||
.expect(num_calls as u64)
|
||||
.mount(server)
|
||||
.await;
|
||||
response_mock
|
||||
}
|
||||
|
||||
pub async fn mount_compact_response_once(
|
||||
server: &MockServer,
|
||||
response: ResponseTemplate,
|
||||
|
||||
@@ -1603,7 +1603,7 @@ async fn run_scenario(scenario: &ScenarioSpec) -> Result<()> {
|
||||
}
|
||||
test.codex
|
||||
.submit(Op::ExecApproval {
|
||||
id: approval.call_id,
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: decision.clone(),
|
||||
})
|
||||
@@ -1823,7 +1823,7 @@ async fn approving_execpolicy_amendment_persists_policy_and_skips_future_prompts
|
||||
|
||||
test.codex
|
||||
.submit(Op::ExecApproval {
|
||||
id: approval.call_id,
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::ApprovedExecpolicyAmendment {
|
||||
proposed_execpolicy_amendment: expected_execpolicy_amendment.clone(),
|
||||
@@ -1988,7 +1988,7 @@ async fn compound_command_with_one_safe_command_still_requires_approval() -> Res
|
||||
let approval = expect_exec_approval(&test, expected_command.as_str()).await;
|
||||
test.codex
|
||||
.submit(Op::ExecApproval {
|
||||
id: approval.call_id,
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Denied,
|
||||
})
|
||||
|
||||
@@ -100,7 +100,7 @@ async fn codex_delegate_forwards_exec_approval_and_proceeds_on_approval() {
|
||||
// Approve via parent using the emitted approval call ID.
|
||||
test.codex
|
||||
.submit(Op::ExecApproval {
|
||||
id: approval.call_id,
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
})
|
||||
|
||||
@@ -3038,6 +3038,131 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
// TODO(ccunningham): Update once pre-turn compaction context-overflow handling includes incoming
|
||||
// user input and emits richer oversized-input messaging.
|
||||
async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch() {
|
||||
skip_if_no_network!();
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let previous_model = "gpt-5.1-codex-max";
|
||||
let next_model = "gpt-5.2-codex";
|
||||
|
||||
let request_log = mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![
|
||||
ev_assistant_message("m1", "BEFORE_SWITCH_REPLY"),
|
||||
ev_completed_with_tokens("r1", 500),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_assistant_message("m2", "PRETURN_SWITCH_SUMMARY"),
|
||||
ev_completed_with_tokens("r2", 100),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_assistant_message("m3", "AFTER_SWITCH_REPLY"),
|
||||
ev_completed_with_tokens("r3", 100),
|
||||
]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
let model_provider = non_openai_model_provider(&server);
|
||||
let test = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_model(previous_model)
|
||||
.with_config(move |config| {
|
||||
config.model_provider = model_provider;
|
||||
set_test_compact_prompt(config);
|
||||
config
|
||||
.features
|
||||
.enable(codex_core::features::Feature::RemoteModels);
|
||||
config.model_auto_compact_token_limit = Some(200);
|
||||
})
|
||||
.build(&server)
|
||||
.await
|
||||
.expect("build codex");
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserTurn {
|
||||
items: vec![UserInput::Text {
|
||||
text: "BEFORE_SWITCH_USER".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: previous_model.to_string(),
|
||||
effort: None,
|
||||
summary: ReasoningSummary::Auto,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
})
|
||||
.await
|
||||
.expect("submit first user turn");
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserTurn {
|
||||
items: vec![UserInput::Text {
|
||||
text: "AFTER_SWITCH_USER".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: next_model.to_string(),
|
||||
effort: None,
|
||||
summary: ReasoningSummary::Auto,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
})
|
||||
.await
|
||||
.expect("submit second user turn");
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
|
||||
let requests = request_log.requests();
|
||||
assert_eq!(
|
||||
requests.len(),
|
||||
3,
|
||||
"expected first turn, pre-turn compact, and post-compact follow-up requests"
|
||||
);
|
||||
|
||||
let compact_body = requests[1].body_json().to_string();
|
||||
assert!(
|
||||
body_contains_text(&compact_body, SUMMARIZATION_PROMPT),
|
||||
"pre-turn compaction request should include summarization prompt"
|
||||
);
|
||||
assert!(
|
||||
!compact_body.contains("<model_switch>"),
|
||||
"pre-turn compaction request should strip incoming model-switch update item"
|
||||
);
|
||||
|
||||
let follow_up_body = requests[2].body_json().to_string();
|
||||
assert!(
|
||||
follow_up_body.contains("<model_switch>"),
|
||||
"post-compaction follow-up should include model-switch update item"
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
"pre_turn_compaction_strips_incoming_model_switch_shapes",
|
||||
format_labeled_requests_snapshot(
|
||||
"Pre-turn compaction during model switch (without pre-sampling model-switch compaction): current behavior strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.",
|
||||
&[
|
||||
("Initial Request (Previous Model)", &requests[0]),
|
||||
("Local Compaction Request", &requests[1]),
|
||||
("Local Post-Compaction History Layout", &requests[2]),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() {
|
||||
skip_if_no_network!();
|
||||
|
||||
|
||||
@@ -216,15 +216,9 @@ async fn remote_compact_runs_automatically() -> Result<()> {
|
||||
)
|
||||
.await;
|
||||
|
||||
let compacted_history = vec![
|
||||
responses::user_message_item("REMOTE_COMPACTED_SUMMARY"),
|
||||
ResponseItem::Compaction {
|
||||
encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
|
||||
},
|
||||
];
|
||||
let compact_mock = responses::mount_compact_json_once(
|
||||
let compact_mock = responses::mount_compact_user_history_with_summary_once(
|
||||
harness.server(),
|
||||
serde_json::json!({ "output": compacted_history.clone() }),
|
||||
"REMOTE_COMPACTED_SUMMARY",
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -249,7 +243,6 @@ async fn remote_compact_runs_automatically() -> Result<()> {
|
||||
let follow_up_request = responses_mock.single_request();
|
||||
let follow_up_body = follow_up_request.body_json().to_string();
|
||||
assert!(follow_up_body.contains("REMOTE_COMPACTED_SUMMARY"));
|
||||
assert!(follow_up_body.contains("ENCRYPTED_COMPACTION_SUMMARY"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -318,9 +311,11 @@ async fn remote_compact_trims_function_call_history_to_fit_context_window() -> R
|
||||
.await?;
|
||||
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let compact_mock =
|
||||
responses::mount_compact_json_once(harness.server(), serde_json::json!({ "output": [] }))
|
||||
.await;
|
||||
let compact_mock = responses::mount_compact_user_history_with_summary_once(
|
||||
harness.server(),
|
||||
"REMOTE_COMPACT_SUMMARY",
|
||||
)
|
||||
.await;
|
||||
|
||||
codex.submit(Op::Compact).await?;
|
||||
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
|
||||
@@ -436,9 +431,11 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window()
|
||||
.await?;
|
||||
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let compact_mock =
|
||||
responses::mount_compact_json_once(harness.server(), serde_json::json!({ "output": [] }))
|
||||
.await;
|
||||
let compact_mock = responses::mount_compact_user_history_with_summary_once(
|
||||
harness.server(),
|
||||
"REMOTE_AUTO_COMPACT_SUMMARY",
|
||||
)
|
||||
.await;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
@@ -667,9 +664,9 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result
|
||||
})
|
||||
.await;
|
||||
|
||||
let baseline_compact_mock = responses::mount_compact_json_once(
|
||||
let baseline_compact_mock = responses::mount_compact_user_history_with_summary_once(
|
||||
baseline_harness.server(),
|
||||
serde_json::json!({ "output": [] }),
|
||||
"REMOTE_BASELINE_SUMMARY",
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -766,9 +763,9 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result
|
||||
})
|
||||
.await;
|
||||
|
||||
let override_compact_mock = responses::mount_compact_json_once(
|
||||
let override_compact_mock = responses::mount_compact_user_history_with_summary_once(
|
||||
override_harness.server(),
|
||||
serde_json::json!({ "output": [] }),
|
||||
"REMOTE_OVERRIDE_SUMMARY",
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -814,15 +811,9 @@ async fn remote_manual_compact_emits_context_compaction_items() -> Result<()> {
|
||||
)
|
||||
.await;
|
||||
|
||||
let compacted_history = vec![
|
||||
responses::user_message_item("REMOTE_COMPACTED_SUMMARY"),
|
||||
ResponseItem::Compaction {
|
||||
encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
|
||||
},
|
||||
];
|
||||
let compact_mock = responses::mount_compact_json_once(
|
||||
let compact_mock = responses::mount_compact_user_history_with_summary_once(
|
||||
harness.server(),
|
||||
serde_json::json!({ "output": compacted_history.clone() }),
|
||||
"REMOTE_COMPACTED_SUMMARY",
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1353,14 +1344,9 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_us
|
||||
)
|
||||
.await;
|
||||
|
||||
let compacted_history = vec![
|
||||
responses::user_message_item("USER_ONE"),
|
||||
responses::user_message_item("USER_TWO"),
|
||||
responses::user_message_item(&summary_with_prefix("REMOTE_PRE_TURN_SUMMARY")),
|
||||
];
|
||||
let compact_mock = responses::mount_compact_json_once(
|
||||
let compact_mock = responses::mount_compact_user_history_with_summary_once(
|
||||
harness.server(),
|
||||
serde_json::json!({ "output": compacted_history }),
|
||||
&summary_with_prefix("REMOTE_PRE_TURN_SUMMARY"),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1424,6 +1410,142 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_us
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model_switch()
|
||||
-> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let previous_model = "gpt-5.1-codex-max";
|
||||
let next_model = "gpt-5.2-codex";
|
||||
let harness = TestCodexHarness::with_builder(
|
||||
test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_model(previous_model)
|
||||
.with_config(|config| {
|
||||
config.model_auto_compact_token_limit = Some(200);
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
let codex = harness.test().codex.clone();
|
||||
|
||||
let initial_turn_request_mock = responses::mount_sse_once(
|
||||
harness.server(),
|
||||
responses::sse(vec![
|
||||
responses::ev_assistant_message("m1", "BEFORE_SWITCH_REPLY"),
|
||||
responses::ev_completed_with_tokens("r1", 500),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let post_compact_turn_request_mock = responses::mount_sse_once(
|
||||
harness.server(),
|
||||
responses::sse(vec![
|
||||
responses::ev_assistant_message("m2", "AFTER_SWITCH_REPLY"),
|
||||
responses::ev_completed_with_tokens("r2", 80),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let compact_mock = responses::mount_compact_user_history_with_summary_once(
|
||||
harness.server(),
|
||||
&summary_with_prefix("REMOTE_SWITCH_SUMMARY"),
|
||||
)
|
||||
.await;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "BEFORE_SWITCH_USER".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
codex
|
||||
.submit(Op::OverrideTurnContext {
|
||||
cwd: None,
|
||||
approval_policy: None,
|
||||
sandbox_policy: None,
|
||||
windows_sandbox_level: None,
|
||||
model: Some(next_model.to_string()),
|
||||
effort: None,
|
||||
summary: None,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
})
|
||||
.await?;
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "AFTER_SWITCH_USER".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
assert_eq!(
|
||||
compact_mock.requests().len(),
|
||||
1,
|
||||
"expected a single remote pre-turn compaction request"
|
||||
);
|
||||
assert_eq!(
|
||||
initial_turn_request_mock.requests().len(),
|
||||
1,
|
||||
"expected initial turn request"
|
||||
);
|
||||
assert_eq!(
|
||||
post_compact_turn_request_mock.requests().len(),
|
||||
1,
|
||||
"expected post-compaction follow-up request"
|
||||
);
|
||||
|
||||
let initial_turn_request = initial_turn_request_mock.single_request();
|
||||
let compact_request = compact_mock.single_request();
|
||||
let post_compact_turn_request = post_compact_turn_request_mock.single_request();
|
||||
let compact_body = compact_request.body_json().to_string();
|
||||
assert!(
|
||||
!compact_body.contains("AFTER_SWITCH_USER"),
|
||||
"current behavior excludes incoming user from the pre-turn remote compaction request"
|
||||
);
|
||||
assert!(
|
||||
!compact_body.contains("<model_switch>"),
|
||||
"pre-turn remote compaction request should strip incoming model-switch update item"
|
||||
);
|
||||
|
||||
let follow_up_body = post_compact_turn_request.body_json().to_string();
|
||||
assert!(
|
||||
follow_up_body.contains("BEFORE_SWITCH_USER"),
|
||||
"post-compaction follow-up should preserve older user messages when they fit"
|
||||
);
|
||||
assert!(
|
||||
follow_up_body.contains("AFTER_SWITCH_USER"),
|
||||
"post-compaction follow-up should preserve incoming user message via runtime append"
|
||||
);
|
||||
assert!(
|
||||
follow_up_body.contains("<model_switch>"),
|
||||
"post-compaction follow-up should include the model-switch update item"
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
"remote_pre_turn_compaction_strips_incoming_model_switch_shapes",
|
||||
format_labeled_requests_snapshot(
|
||||
"Remote pre-turn compaction during model switch currently excludes incoming user input, strips incoming <model_switch> from the compact request payload, and restores it in the post-compaction follow-up request.",
|
||||
&[
|
||||
("Initial Request (Previous Model)", &initial_turn_request),
|
||||
("Remote Compaction Request", &compact_request),
|
||||
(
|
||||
"Remote Post-Compaction History Layout",
|
||||
&post_compact_turn_request
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
// TODO(ccunningham): Update once remote pre-turn compaction context-overflow handling includes
|
||||
// incoming user input and emits richer oversized-input messaging.
|
||||
@@ -1555,13 +1677,9 @@ async fn snapshot_request_shape_remote_mid_turn_continuation_compaction() -> Res
|
||||
)
|
||||
.await;
|
||||
|
||||
let compacted_history = vec![
|
||||
responses::user_message_item("USER_ONE"),
|
||||
responses::user_message_item(&summary_with_prefix("REMOTE_MID_TURN_SUMMARY")),
|
||||
];
|
||||
let compact_mock = responses::mount_compact_json_once(
|
||||
let compact_mock = responses::mount_compact_user_history_with_summary_once(
|
||||
harness.server(),
|
||||
serde_json::json!({ "output": compacted_history }),
|
||||
&summary_with_prefix("REMOTE_MID_TURN_SUMMARY"),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1599,6 +1717,196 @@ async fn snapshot_request_shape_remote_mid_turn_continuation_compaction() -> Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_reinjects_context()
|
||||
-> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let harness = TestCodexHarness::with_builder(
|
||||
test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(|config| {
|
||||
config.model_auto_compact_token_limit = Some(200);
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
let codex = harness.test().codex.clone();
|
||||
|
||||
let initial_turn_request_mock = responses::mount_sse_once(
|
||||
harness.server(),
|
||||
responses::sse(vec![
|
||||
responses::ev_function_call("call-remote-summary-only", DUMMY_FUNCTION_NAME, "{}"),
|
||||
responses::ev_completed_with_tokens("r1", 500),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let post_compact_turn_request_mock = responses::mount_sse_once(
|
||||
harness.server(),
|
||||
responses::sse(vec![
|
||||
responses::ev_assistant_message("m2", "REMOTE_SUMMARY_ONLY_FINAL_REPLY"),
|
||||
responses::ev_completed_with_tokens("r2", 80),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let compacted_history = vec![responses::user_message_item(&summary_with_prefix(
|
||||
"REMOTE_SUMMARY_ONLY",
|
||||
))];
|
||||
let compact_mock = responses::mount_compact_json_once(
|
||||
harness.server(),
|
||||
serde_json::json!({ "output": compacted_history }),
|
||||
)
|
||||
.await;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "USER_ONE".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
assert_eq!(compact_mock.requests().len(), 1);
|
||||
assert_eq!(
|
||||
initial_turn_request_mock.requests().len(),
|
||||
1,
|
||||
"expected initial turn request"
|
||||
);
|
||||
assert_eq!(
|
||||
post_compact_turn_request_mock.requests().len(),
|
||||
1,
|
||||
"expected post-compaction request"
|
||||
);
|
||||
|
||||
let compact_request = compact_mock.single_request();
|
||||
let post_compact_turn_request = post_compact_turn_request_mock.single_request();
|
||||
insta::assert_snapshot!(
|
||||
"remote_mid_turn_compaction_summary_only_reinjects_context_shapes",
|
||||
format_labeled_requests_snapshot(
|
||||
"Remote mid-turn compaction where compact output has only summary user content: continuation layout reinjects canonical context before that summary.",
|
||||
&[
|
||||
("Remote Compaction Request", &compact_request),
|
||||
(
|
||||
"Remote Post-Compaction History Layout",
|
||||
&post_compact_turn_request
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary()
|
||||
-> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let harness = TestCodexHarness::with_builder(
|
||||
test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(|config| {
|
||||
config.model_auto_compact_token_limit = Some(200);
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
let codex = harness.test().codex.clone();
|
||||
|
||||
let setup_turn_request_mock = responses::mount_sse_once(
|
||||
harness.server(),
|
||||
responses::sse(vec![
|
||||
responses::ev_assistant_message("setup", "REMOTE_SETUP_REPLY"),
|
||||
responses::ev_completed_with_tokens("setup-response", 60),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let second_turn_request_mock = responses::mount_sse_once(
|
||||
harness.server(),
|
||||
responses::sse(vec![
|
||||
responses::ev_shell_command_call("call-remote-multi-summary", "echo multi-summary"),
|
||||
responses::ev_completed_with_tokens("r1", 1_000),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let compact_mock = responses::mount_compact_user_history_with_summary_sequence(
|
||||
harness.server(),
|
||||
vec![
|
||||
summary_with_prefix("REMOTE_OLDER_SUMMARY"),
|
||||
summary_with_prefix("REMOTE_LATEST_SUMMARY"),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "USER_ONE".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
codex.submit(Op::Compact).await?;
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![UserInput::Text {
|
||||
text: "USER_TWO".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
assert_eq!(compact_mock.requests().len(), 2);
|
||||
assert_eq!(
|
||||
setup_turn_request_mock.requests().len(),
|
||||
1,
|
||||
"expected setup turn request"
|
||||
);
|
||||
assert_eq!(
|
||||
second_turn_request_mock.requests().len(),
|
||||
1,
|
||||
"expected second-turn pre-compaction request"
|
||||
);
|
||||
|
||||
let compact_requests = compact_mock.requests();
|
||||
assert_eq!(
|
||||
compact_requests.len(),
|
||||
2,
|
||||
"expected one setup compact and one mid-turn compact request"
|
||||
);
|
||||
let compact_request = compact_requests[1].clone();
|
||||
let second_turn_request = second_turn_request_mock.single_request();
|
||||
assert!(
|
||||
compact_request.body_contains_text("REMOTE_OLDER_SUMMARY"),
|
||||
"older summary should round-trip from conversation history into the next compact request"
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
"remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes",
|
||||
format_labeled_requests_snapshot(
|
||||
"Remote mid-turn compaction after an earlier summary compaction: the older summary remains in model-visible history and round-trips into the next compact request.",
|
||||
&[
|
||||
(
|
||||
"Second Turn Request (Before Mid-Turn Compaction)",
|
||||
&second_turn_request
|
||||
),
|
||||
("Remote Compaction Request", &compact_request),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
// TODO(ccunningham): Update once manual remote /compact with no prior user turn becomes a no-op.
|
||||
async fn snapshot_request_shape_remote_manual_compact_without_previous_user_messages() -> Result<()>
|
||||
|
||||
@@ -1042,7 +1042,7 @@ async fn handle_container_exec_user_approved_records_tool_decision() {
|
||||
|
||||
codex
|
||||
.submit(Op::ExecApproval {
|
||||
id: approval.call_id,
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
})
|
||||
@@ -1108,7 +1108,7 @@ async fn handle_container_exec_user_approved_for_session_records_tool_decision()
|
||||
|
||||
codex
|
||||
.submit(Op::ExecApproval {
|
||||
id: approval.call_id,
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::ApprovedForSession,
|
||||
})
|
||||
@@ -1174,7 +1174,7 @@ async fn handle_sandbox_error_user_approves_retry_records_tool_decision() {
|
||||
|
||||
codex
|
||||
.submit(Op::ExecApproval {
|
||||
id: approval.call_id,
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Approved,
|
||||
})
|
||||
@@ -1240,7 +1240,7 @@ async fn handle_container_exec_user_denies_records_tool_decision() {
|
||||
|
||||
codex
|
||||
.submit(Op::ExecApproval {
|
||||
id: approval.call_id,
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Denied,
|
||||
})
|
||||
@@ -1306,7 +1306,7 @@ async fn handle_sandbox_error_user_approves_for_session_records_tool_decision()
|
||||
|
||||
codex
|
||||
.submit(Op::ExecApproval {
|
||||
id: approval.call_id,
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::ApprovedForSession,
|
||||
})
|
||||
@@ -1373,7 +1373,7 @@ async fn handle_sandbox_error_user_denies_records_tool_decision() {
|
||||
|
||||
codex
|
||||
.submit(Op::ExecApproval {
|
||||
id: approval.call_id,
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: None,
|
||||
decision: ReviewDecision::Denied,
|
||||
})
|
||||
|
||||
@@ -121,7 +121,9 @@ async fn response_model_field_mismatch_emits_warning_when_header_matches_request
|
||||
"type": "response.created",
|
||||
"response": {
|
||||
"id": "resp-1",
|
||||
"model": SERVER_MODEL,
|
||||
"headers": {
|
||||
"OpenAI-Model": SERVER_MODEL
|
||||
}
|
||||
}
|
||||
}),
|
||||
core_test_support::responses::ev_completed("resp-1"),
|
||||
@@ -250,3 +252,58 @@ async fn openai_model_header_mismatch_only_emits_one_warning_per_turn() -> Resul
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn openai_model_header_casing_only_mismatch_does_not_warn() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let requested_header = REQUESTED_MODEL.to_ascii_uppercase();
|
||||
let response = sse_response(sse_completed("resp-1"))
|
||||
.insert_header("OpenAI-Model", requested_header.as_str());
|
||||
let _mock = mount_response_once(&server, response).await;
|
||||
|
||||
let mut builder = test_codex().with_model(REQUESTED_MODEL);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.codex
|
||||
.submit(Op::UserTurn {
|
||||
items: vec![UserInput::Text {
|
||||
text: "trigger casing check".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: test.cwd_path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: REQUESTED_MODEL.to_string(),
|
||||
effort: test.config.model_reasoning_effort,
|
||||
summary: ReasoningSummary::Auto,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut reroute_count = 0;
|
||||
let mut warning_count = 0;
|
||||
loop {
|
||||
let event = wait_for_event(&test.codex, |_| true).await;
|
||||
match event {
|
||||
EventMsg::ModelReroute(_) => reroute_count += 1,
|
||||
EventMsg::Warning(warning)
|
||||
if warning
|
||||
.message
|
||||
.contains("flagged for potentially high-risk cyber activity") =>
|
||||
{
|
||||
warning_count += 1;
|
||||
}
|
||||
EventMsg::TurnComplete(_) => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(reroute_count, 0);
|
||||
assert_eq!(warning_count, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
---
|
||||
source: core/tests/suite/compact.rs
|
||||
assertion_line: 3152
|
||||
expression: "format_labeled_requests_snapshot(\"Pre-turn compaction during model switch (without pre-sampling model-switch compaction): current behavior strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.\",\n&[(\"Initial Request (Previous Model)\", &requests[0]),\n(\"Local Compaction Request\", &requests[1]),\n(\"Local Post-Compaction History Layout\", &requests[2]),])"
|
||||
---
|
||||
Scenario: Pre-turn compaction during model switch (without pre-sampling model-switch compaction): current behavior strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.
|
||||
|
||||
## Initial Request (Previous Model)
|
||||
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
|
||||
01:message/user:<AGENTS_MD>
|
||||
02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
|
||||
03:message/developer:<PERMISSIONS_INSTRUCTIONS>
|
||||
04:message/user:BEFORE_SWITCH_USER
|
||||
|
||||
## Local Compaction Request
|
||||
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
|
||||
01:message/user:<AGENTS_MD>
|
||||
02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
|
||||
03:message/developer:<PERMISSIONS_INSTRUCTIONS>
|
||||
04:message/user:BEFORE_SWITCH_USER
|
||||
05:message/assistant:BEFORE_SWITCH_REPLY
|
||||
06:message/user:<SUMMARIZATION_PROMPT>
|
||||
|
||||
## Local Post-Compaction History Layout
|
||||
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
|
||||
01:message/developer:<personality_spec> The user has requested a new communication st...
|
||||
02:message/user:<AGENTS_MD>
|
||||
03:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
|
||||
04:message/user:BEFORE_SWITCH_USER
|
||||
05:message/user:<COMPACTION_SUMMARY>\nPRETURN_SWITCH_SUMMARY
|
||||
06:message/developer:<model_switch>\nThe user was previously using a different model....
|
||||
07:message/user:AFTER_SWITCH_USER
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: core/tests/suite/compact_remote.rs
|
||||
expression: "format_labeled_requests_snapshot(\"Remote mid-turn compaction after an earlier summary compaction: the older summary remains in model-visible history and round-trips into the next compact request.\",\n&[(\"Second Turn Request (Before Mid-Turn Compaction)\", &requests[1]),\n(\"Remote Compaction Request\", &compact_request),])"
|
||||
---
|
||||
Scenario: Remote mid-turn compaction after an earlier summary compaction: the older summary remains in model-visible history and round-trips into the next compact request.
|
||||
|
||||
## Second Turn Request (Before Mid-Turn Compaction)
|
||||
00:message/user:USER_ONE
|
||||
01:message/user:<COMPACTION_SUMMARY>\nREMOTE_OLDER_SUMMARY
|
||||
02:message/developer:<PERMISSIONS_INSTRUCTIONS>
|
||||
03:message/user:<AGENTS_MD>
|
||||
04:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
|
||||
05:message/user:<COMPACTION_SUMMARY>\nREMOTE_LATEST_SUMMARY
|
||||
06:message/user:USER_TWO
|
||||
|
||||
## Remote Compaction Request
|
||||
00:message/user:USER_ONE
|
||||
01:message/developer:<PERMISSIONS_INSTRUCTIONS>
|
||||
02:message/user:<AGENTS_MD>
|
||||
03:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
|
||||
04:message/user:<COMPACTION_SUMMARY>\nREMOTE_OLDER_SUMMARY
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
---
|
||||
source: core/tests/suite/compact_remote.rs
|
||||
expression: "format_labeled_requests_snapshot(\"Remote mid-turn compaction where compact output has only summary user content: continuation layout reinjects canonical context before that summary.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[1]),])"
|
||||
---
|
||||
Scenario: Remote mid-turn compaction where compact output has only summary user content: continuation layout reinjects canonical context before that summary.
|
||||
|
||||
## Remote Compaction Request
|
||||
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
|
||||
01:message/user:<AGENTS_MD>
|
||||
02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
|
||||
03:message/user:USER_ONE
|
||||
04:function_call/test_tool
|
||||
05:function_call_output:unsupported call: test_tool
|
||||
|
||||
## Remote Post-Compaction History Layout
|
||||
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
|
||||
01:message/user:<AGENTS_MD>
|
||||
02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
|
||||
03:message/user:<COMPACTION_SUMMARY>\nREMOTE_SUMMARY_ONLY
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: core/tests/suite/compact_remote.rs
|
||||
expression: "format_labeled_requests_snapshot(\"Remote pre-turn compaction during model switch currently excludes incoming user input, strips incoming <model_switch> from the compact request payload, and restores it in the post-compaction follow-up request.\",\n&[(\"Initial Request (Previous Model)\", &requests[0]),\n(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[1]),])"
|
||||
---
|
||||
Scenario: Remote pre-turn compaction during model switch currently excludes incoming user input, strips incoming <model_switch> from the compact request payload, and restores it in the post-compaction follow-up request.
|
||||
|
||||
## Initial Request (Previous Model)
|
||||
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
|
||||
01:message/user:<AGENTS_MD>
|
||||
02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
|
||||
03:message/user:BEFORE_SWITCH_USER
|
||||
|
||||
## Remote Compaction Request
|
||||
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
|
||||
01:message/user:<AGENTS_MD>
|
||||
02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
|
||||
03:message/user:BEFORE_SWITCH_USER
|
||||
04:message/assistant:BEFORE_SWITCH_REPLY
|
||||
|
||||
## Remote Post-Compaction History Layout
|
||||
00:message/user:BEFORE_SWITCH_USER
|
||||
01:message/developer:<PERMISSIONS_INSTRUCTIONS>
|
||||
02:message/developer:<personality_spec> The user has requested a new communication st...
|
||||
03:message/user:<AGENTS_MD>
|
||||
04:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
|
||||
05:message/user:<COMPACTION_SUMMARY>\nREMOTE_SWITCH_SUMMARY
|
||||
06:message/developer:<model_switch>\nThe user was previously using a different model....
|
||||
07:message/user:AFTER_SWITCH_USER
|
||||
@@ -146,7 +146,7 @@ Example:
|
||||
When Codex needs approval to apply changes or run commands, the server issues JSON‑RPC requests to the client:
|
||||
|
||||
- `applyPatchApproval { conversationId, callId, fileChanges, reason?, grantRoot? }`
|
||||
- `execCommandApproval { conversationId, callId, command, cwd, reason? }`
|
||||
- `execCommandApproval { conversationId, callId, approvalId?, command, cwd, reason? }`
|
||||
|
||||
The client must reply with `{ decision: "allow" | "deny" }` for each request.
|
||||
|
||||
|
||||
@@ -213,16 +213,19 @@ async fn run_codex_tool_session_inner(
|
||||
.await;
|
||||
|
||||
match event.msg {
|
||||
EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
|
||||
turn_id: _,
|
||||
command,
|
||||
cwd,
|
||||
call_id,
|
||||
reason: _,
|
||||
proposed_execpolicy_amendment: _,
|
||||
parsed_cmd,
|
||||
network_approval_context: _,
|
||||
}) => {
|
||||
EventMsg::ExecApprovalRequest(ev) => {
|
||||
let approval_id = ev.effective_approval_id();
|
||||
let ExecApprovalRequestEvent {
|
||||
turn_id: _,
|
||||
command,
|
||||
cwd,
|
||||
call_id,
|
||||
approval_id: _,
|
||||
reason: _,
|
||||
proposed_execpolicy_amendment: _,
|
||||
parsed_cmd,
|
||||
network_approval_context: _,
|
||||
} = ev;
|
||||
handle_exec_approval_request(
|
||||
command,
|
||||
cwd,
|
||||
@@ -232,6 +235,7 @@ async fn run_codex_tool_session_inner(
|
||||
request_id_str.clone(),
|
||||
event.id.clone(),
|
||||
call_id,
|
||||
approval_id,
|
||||
parsed_cmd,
|
||||
thread_id,
|
||||
)
|
||||
@@ -254,9 +258,6 @@ async fn run_codex_tool_session_inner(
|
||||
EventMsg::Warning(_) => {
|
||||
continue;
|
||||
}
|
||||
EventMsg::ModelReroute(_) => {
|
||||
continue;
|
||||
}
|
||||
EventMsg::ElicitationRequest(_) => {
|
||||
// TODO: forward elicitation requests to the client?
|
||||
continue;
|
||||
@@ -361,6 +362,7 @@ async fn run_codex_tool_session_inner(
|
||||
| EventMsg::RequestUserInput(_)
|
||||
| EventMsg::DynamicToolCallRequest(_)
|
||||
| EventMsg::ContextCompacted(_)
|
||||
| EventMsg::ModelReroute(_)
|
||||
| EventMsg::ThreadRolledBack(_)
|
||||
| EventMsg::CollabAgentSpawnBegin(_)
|
||||
| EventMsg::CollabAgentSpawnEnd(_)
|
||||
|
||||
@@ -57,10 +57,10 @@ pub(crate) async fn handle_exec_approval_request(
|
||||
tool_call_id: String,
|
||||
event_id: String,
|
||||
call_id: String,
|
||||
approval_id: String,
|
||||
codex_parsed_cmd: Vec<ParsedCommand>,
|
||||
thread_id: ThreadId,
|
||||
) {
|
||||
let approval_id = call_id.clone();
|
||||
let escaped_command =
|
||||
shlex::try_join(command.iter().map(String::as_str)).unwrap_or_else(|_| command.join(" "));
|
||||
let message = format!(
|
||||
|
||||
@@ -57,8 +57,15 @@ pub struct NetworkApprovalContext {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct ExecApprovalRequestEvent {
|
||||
/// Identifier for the associated exec call, if available.
|
||||
/// Identifier for the associated command execution item.
|
||||
pub call_id: String,
|
||||
/// Identifier for this specific approval callback.
|
||||
///
|
||||
/// When absent, the approval is for the command item itself (`call_id`).
|
||||
/// This is present for subcommand approvals (via execve intercept).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub approval_id: Option<String>,
|
||||
/// Turn ID that this command belongs to.
|
||||
/// Uses `#[serde(default)]` for backwards compatibility.
|
||||
#[serde(default)]
|
||||
@@ -81,6 +88,14 @@ pub struct ExecApprovalRequestEvent {
|
||||
pub parsed_cmd: Vec<ParsedCommand>,
|
||||
}
|
||||
|
||||
impl ExecApprovalRequestEvent {
|
||||
pub fn effective_approval_id(&self) -> String {
|
||||
self.approval_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.call_id.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct ElicitationRequestEvent {
|
||||
pub server_name: String,
|
||||
|
||||
@@ -2472,6 +2472,7 @@ async fn exec_approval_emits_proposed_command_and_decision_history() {
|
||||
// Trigger an exec approval request with a short, single-line command
|
||||
let ev = ExecApprovalRequestEvent {
|
||||
call_id: "call-short".into(),
|
||||
approval_id: Some("call-short".into()),
|
||||
turn_id: "turn-short".into(),
|
||||
command: vec!["bash".into(), "-lc".into(), "echo hello world".into()],
|
||||
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
@@ -2517,6 +2518,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() {
|
||||
// Multiline command: modal should show full command, history records decision only
|
||||
let ev_multi = ExecApprovalRequestEvent {
|
||||
call_id: "call-multi".into(),
|
||||
approval_id: Some("call-multi".into()),
|
||||
turn_id: "turn-multi".into(),
|
||||
command: vec!["bash".into(), "-lc".into(), "echo line1\necho line2".into()],
|
||||
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
@@ -2570,6 +2572,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() {
|
||||
let long = format!("echo {}", "a".repeat(200));
|
||||
let ev_long = ExecApprovalRequestEvent {
|
||||
call_id: "call-long".into(),
|
||||
approval_id: Some("call-long".into()),
|
||||
turn_id: "turn-long".into(),
|
||||
command: vec!["bash".into(), "-lc".into(), long],
|
||||
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
@@ -5684,6 +5687,7 @@ async fn approval_modal_exec_snapshot() -> anyhow::Result<()> {
|
||||
// Inject an exec approval request to display the approval modal.
|
||||
let ev = ExecApprovalRequestEvent {
|
||||
call_id: "call-approve-cmd".into(),
|
||||
approval_id: Some("call-approve-cmd".into()),
|
||||
turn_id: "turn-approve-cmd".into(),
|
||||
command: vec!["bash".into(), "-lc".into(), "echo hello world".into()],
|
||||
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
@@ -5743,6 +5747,7 @@ async fn approval_modal_exec_without_reason_snapshot() -> anyhow::Result<()> {
|
||||
|
||||
let ev = ExecApprovalRequestEvent {
|
||||
call_id: "call-approve-cmd-noreason".into(),
|
||||
approval_id: Some("call-approve-cmd-noreason".into()),
|
||||
turn_id: "turn-approve-cmd-noreason".into(),
|
||||
command: vec!["bash".into(), "-lc".into(), "echo hello world".into()],
|
||||
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
@@ -5791,6 +5796,7 @@ async fn approval_modal_exec_multiline_prefix_hides_execpolicy_option_snapshot()
|
||||
let command = vec!["bash".into(), "-lc".into(), script];
|
||||
let ev = ExecApprovalRequestEvent {
|
||||
call_id: "call-approve-cmd-multiline-trunc".into(),
|
||||
approval_id: Some("call-approve-cmd-multiline-trunc".into()),
|
||||
turn_id: "turn-approve-cmd-multiline-trunc".into(),
|
||||
command: command.clone(),
|
||||
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
@@ -6149,6 +6155,7 @@ async fn status_widget_and_approval_modal_snapshot() {
|
||||
// Now show an approval modal (e.g. exec approval).
|
||||
let ev = ExecApprovalRequestEvent {
|
||||
call_id: "call-approve-exec".into(),
|
||||
approval_id: Some("call-approve-exec".into()),
|
||||
turn_id: "turn-approve-exec".into(),
|
||||
command: vec!["echo".into(), "hello world".into()],
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
|
||||
Reference in New Issue
Block a user