Merge remote-tracking branch 'origin/main' into codex/pr23585-merge-fix

# Conflicts:
#	codex-rs/core/src/session/input_queue.rs
#	codex-rs/core/src/session/mod.rs
#	codex-rs/core/src/session/turn.rs
This commit is contained in:
Roy Han
2026-05-26 15:04:53 -07:00
193 changed files with 6453 additions and 1125 deletions
+23 -1
View File
@@ -1859,6 +1859,7 @@ dependencies = [
"pretty_assertions",
"regex-lite",
"reqwest",
"schemars 0.8.22",
"serde",
"serde_json",
"tempfile",
@@ -1927,6 +1928,7 @@ dependencies = [
"codex-utils-cli",
"codex-utils-json-to-toml",
"codex-utils-pty",
"codex-web-search-extension",
"core_test_support",
"flate2",
"futures",
@@ -2968,6 +2970,7 @@ dependencies = [
"chrono",
"codex-core",
"codex-extension-api",
"codex-otel",
"codex-protocol",
"codex-state",
"codex-tools",
@@ -3787,7 +3790,6 @@ dependencies = [
"codex-app-server-client",
"codex-app-server-protocol",
"codex-arg0",
"codex-chatgpt",
"codex-cli",
"codex-cloud-requirements",
"codex-config",
@@ -4116,6 +4118,26 @@ dependencies = [
"v8",
]
[[package]]
name = "codex-web-search-extension"
version = "0.0.0"
dependencies = [
"async-trait",
"codex-api",
"codex-core",
"codex-extension-api",
"codex-features",
"codex-login",
"codex-model-provider",
"codex-model-provider-info",
"codex-protocol",
"codex-tools",
"http 1.4.0",
"pretty_assertions",
"schemars 0.8.22",
"serde_json",
]
[[package]]
name = "codex-windows-sandbox"
version = "0.0.0"
+2
View File
@@ -48,6 +48,7 @@ members = [
"ext/goal",
"ext/guardian",
"ext/memories",
"ext/web-search",
"external-agent-migration",
"external-agent-sessions",
"keyring-store",
@@ -180,6 +181,7 @@ codex-lmstudio = { path = "lmstudio" }
codex-login = { path = "login" }
codex-message-history = { path = "message-history" }
codex-memories-extension = { path = "ext/memories" }
codex-web-search-extension = { path = "ext/web-search" }
codex-memories-read = { path = "memories/read" }
codex-memories-write = { path = "memories/write" }
codex-mcp = { path = "codex-mcp" }
@@ -401,6 +401,7 @@ fn sample_turn_steer_request(
},
],
responsesapi_client_metadata: None,
additional_context: None,
},
}
}
+1
View File
@@ -103,6 +103,7 @@ fn sample_turn_steer_request() -> ClientRequest {
expected_turn_id: "turn-1".to_string(),
input: Vec::new(),
responsesapi_client_metadata: None,
additional_context: None,
},
}
}
@@ -12,6 +12,28 @@
],
"type": "string"
},
"AdditionalContextEntry": {
"properties": {
"kind": {
"$ref": "#/definitions/AdditionalContextKind"
},
"value": {
"type": "string"
}
},
"required": [
"kind",
"value"
],
"type": "object"
},
"AdditionalContextKind": {
"enum": [
"untrusted",
"application"
],
"type": "string"
},
"ApprovalsReviewer": {
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
"enum": [
@@ -5768,6 +5768,28 @@
],
"type": "string"
},
"AdditionalContextEntry": {
"properties": {
"kind": {
"$ref": "#/definitions/v2/AdditionalContextKind"
},
"value": {
"type": "string"
}
},
"required": [
"kind",
"value"
],
"type": "object"
},
"AdditionalContextKind": {
"enum": [
"untrusted",
"application"
],
"type": "string"
},
"AdditionalFileSystemPermissions": {
"properties": {
"entries": {
@@ -164,6 +164,28 @@
],
"type": "string"
},
"AdditionalContextEntry": {
"properties": {
"kind": {
"$ref": "#/definitions/AdditionalContextKind"
},
"value": {
"type": "string"
}
},
"required": [
"kind",
"value"
],
"type": "object"
},
"AdditionalContextKind": {
"enum": [
"untrusted",
"application"
],
"type": "string"
},
"AdditionalFileSystemPermissions": {
"properties": {
"entries": {
@@ -5,6 +5,28 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"AdditionalContextEntry": {
"properties": {
"kind": {
"$ref": "#/definitions/AdditionalContextKind"
},
"value": {
"type": "string"
}
},
"required": [
"kind",
"value"
],
"type": "object"
},
"AdditionalContextKind": {
"enum": [
"untrusted",
"application"
],
"type": "string"
},
"ApprovalsReviewer": {
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
"enum": [
@@ -1,6 +1,28 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AdditionalContextEntry": {
"properties": {
"kind": {
"$ref": "#/definitions/AdditionalContextKind"
},
"value": {
"type": "string"
}
},
"required": [
"kind",
"value"
],
"type": "object"
},
"AdditionalContextKind": {
"enum": [
"untrusted",
"application"
],
"type": "string"
},
"ByteRange": {
"properties": {
"end": {
@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AdditionalContextKind } from "./AdditionalContextKind";
export type AdditionalContextEntry = { value: string, kind: AdditionalContextKind, };
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AdditionalContextKind = "untrusted" | "application";
@@ -7,6 +7,8 @@ export type { AccountUpdatedNotification } from "./AccountUpdatedNotification";
export type { ActivePermissionProfile } from "./ActivePermissionProfile";
export type { AddCreditsNudgeCreditType } from "./AddCreditsNudgeCreditType";
export type { AddCreditsNudgeEmailStatus } from "./AddCreditsNudgeEmailStatus";
export type { AdditionalContextEntry } from "./AdditionalContextEntry";
export type { AdditionalContextKind } from "./AdditionalContextKind";
export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions";
export type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions";
export type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile";
@@ -3428,6 +3428,7 @@ fn turn_start_params_preserve_explicit_null_service_tier() {
thread_id: "thread_123".to_string(),
input: vec![],
responsesapi_client_metadata: None,
additional_context: None,
environments: None,
cwd: None,
runtime_workspace_roots: None,
@@ -41,6 +41,23 @@ pub struct TurnEnvironmentParams {
pub cwd: AbsolutePathBuf,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "lowercase")]
#[ts(rename_all = "lowercase")]
#[ts(export_to = "v2/")]
pub enum AdditionalContextKind {
Untrusted,
Application,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AdditionalContextEntry {
pub value: String,
pub kind: AdditionalContextKind,
}
#[derive(
Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi,
)]
@@ -53,6 +70,10 @@ pub struct TurnStartParams {
#[experimental("turn/start.responsesapiClientMetadata")]
#[ts(optional = nullable)]
pub responsesapi_client_metadata: Option<HashMap<String, String>>,
/// Optional client-provided context fragments keyed by an opaque source identifier.
#[experimental("turn/start.additionalContext")]
#[ts(optional = nullable)]
pub additional_context: Option<HashMap<String, AdditionalContextEntry>>,
/// Optional turn-scoped environments.
///
/// Omitted uses the thread sticky environments. Empty disables
@@ -141,6 +162,10 @@ pub struct TurnSteerParams {
#[experimental("turn/steer.responsesapiClientMetadata")]
#[ts(optional = nullable)]
pub responsesapi_client_metadata: Option<HashMap<String, String>>,
/// Optional client-provided context fragments keyed by an opaque source identifier.
#[experimental("turn/steer.additionalContext")]
#[ts(optional = nullable)]
pub additional_context: Option<HashMap<String, AdditionalContextEntry>>,
/// Required active turn id precondition. The request fails when it does not
/// match the currently active turn.
pub expected_turn_id: String,
@@ -15,13 +15,21 @@ use codex_app_server_protocol::JSONRPCMessage;
use std::collections::HashMap;
use tokio::sync::mpsc;
use tokio::sync::watch;
use tokio::task::JoinHandle;
use tokio::task::JoinSet;
use tokio::time::Duration;
use tokio::time::Instant;
use tokio::time::timeout;
use tokio_util::sync::CancellationToken;
use tracing::info;
use tracing::warn;
const REMOTE_CONTROL_CLIENT_IDLE_TIMEOUT: Duration = Duration::from_secs(10 * 60);
pub(crate) const REMOTE_CONTROL_IDLE_SWEEP_INTERVAL: Duration = Duration::from_secs(30);
#[cfg(not(test))]
const REMOTE_CONTROL_TRANSPORT_EVENT_SEND_TIMEOUT: Duration = Duration::from_secs(5);
#[cfg(test)]
const REMOTE_CONTROL_TRANSPORT_EVENT_SEND_TIMEOUT: Duration = Duration::from_millis(10);
#[derive(Debug)]
pub(crate) struct Stopped;
@@ -138,9 +146,6 @@ impl ClientTracker {
if let Some(connection_id) = self.clients.get_mut(&client_key).map(|client| {
client.last_activity_at = Instant::now();
if let Some(seq_id) = seq_id {
client.last_inbound_seq_id = Some(seq_id);
}
client.connection_id
}) {
self.send_transport_event(TransportEvent::IncomingMessage {
@@ -148,6 +153,7 @@ impl ClientTracker {
message,
})
.await?;
self.record_inbound_message_delivery(&client_key, seq_id);
return Ok(());
}
@@ -177,23 +183,37 @@ impl ClientTracker {
disconnect_token.clone(),
));
self.clients.insert(
client_key,
client_key.clone(),
ClientState {
connection_id,
disconnect_token,
last_activity_at: Instant::now(),
last_inbound_seq_id: if is_legacy_stream_id { None } else { seq_id },
last_inbound_seq_id: None,
status_tx,
},
);
if is_legacy_stream_id {
self.legacy_stream_ids.insert(client_id.clone(), stream_id);
}
self.send_transport_event(TransportEvent::IncomingMessage {
connection_id,
message,
})
.await
if let Err(err) = self
.send_transport_event(TransportEvent::IncomingMessage {
connection_id,
message,
})
.await
{
if let Some(client) = self.remove_client(&client_key) {
client.disconnect_token.cancel();
// The initialize send already timed out on this queue; preserve close
// delivery without blocking reconnect on the same backpressure.
drop(self.spawn_connection_closed(client.connection_id));
}
return Err(err);
}
if !is_legacy_stream_id {
self.record_inbound_message_delivery(&client_key, seq_id);
}
Ok(())
}
ClientEvent::ClientMessageChunk { .. } | ClientEvent::Ack { .. } => Ok(()),
ClientEvent::Ping => {
@@ -290,16 +310,9 @@ impl ClientTracker {
&mut self,
client_key: &(ClientId, StreamId),
) -> Result<(), Stopped> {
let Some(client) = self.clients.remove(client_key) else {
let Some(client) = self.remove_client(client_key) else {
return Ok(());
};
if self
.legacy_stream_ids
.get(&client_key.0)
.is_some_and(|stream_id| stream_id == &client_key.1)
{
self.legacy_stream_ids.remove(&client_key.0);
}
client.disconnect_token.cancel();
self.send_transport_event(TransportEvent::ConnectionClosed {
connection_id: client.connection_id,
@@ -307,11 +320,109 @@ impl ClientTracker {
.await
}
fn remove_client(&mut self, client_key: &(ClientId, StreamId)) -> Option<ClientState> {
let client = self.clients.remove(client_key)?;
if self
.legacy_stream_ids
.get(&client_key.0)
.is_some_and(|stream_id| stream_id == &client_key.1)
{
self.legacy_stream_ids.remove(&client_key.0);
}
Some(client)
}
async fn send_transport_event(&self, event: TransportEvent) -> Result<(), Stopped> {
self.transport_event_tx
.send(event)
.await
.map_err(|_| Stopped)
let event = match event {
TransportEvent::ConnectionClosed { connection_id } => {
return self.send_connection_closed(connection_id).await;
}
event => event,
};
let event_name = transport_event_name(&event);
match timeout(
REMOTE_CONTROL_TRANSPORT_EVENT_SEND_TIMEOUT,
self.transport_event_tx.send(event),
)
.await
{
Ok(Ok(())) => Ok(()),
Ok(Err(_)) => {
warn!(
transport_event = event_name,
"remote control transport event receiver dropped"
);
Err(Stopped)
}
Err(_) => {
warn!(
transport_event = event_name,
timeout = ?REMOTE_CONTROL_TRANSPORT_EVENT_SEND_TIMEOUT,
"timed out forwarding remote control transport event"
);
Err(Stopped)
}
}
}
fn record_inbound_message_delivery(
&mut self,
client_key: &(ClientId, StreamId),
seq_id: Option<u64>,
) {
// Timed forwarding can fail, so only dedupe retries after app-server receives it.
if let Some(seq_id) = seq_id
&& let Some(client) = self.clients.get_mut(client_key)
{
client.last_inbound_seq_id = Some(seq_id);
}
}
async fn send_connection_closed(&self, connection_id: ConnectionId) -> Result<(), Stopped> {
// Worker shutdown can abort the caller; detach the cleanup event before awaiting it.
match self.spawn_connection_closed(connection_id).await {
Ok(result) => result,
Err(err) => {
warn!(
transport_event = "connection_closed",
?err,
"remote control transport event forwarding task failed"
);
Err(Stopped)
}
}
}
fn spawn_connection_closed(
&self,
connection_id: ConnectionId,
) -> JoinHandle<Result<(), Stopped>> {
info!(
connection_id = ?connection_id,
"forwarding remote control connection closed transport event"
);
let transport_event_tx = self.transport_event_tx.clone();
tokio::spawn(async move {
transport_event_tx
.send(TransportEvent::ConnectionClosed { connection_id })
.await
.map_err(|_| {
warn!(
transport_event = "connection_closed",
"remote control transport event receiver dropped"
);
Stopped
})
})
}
}
fn transport_event_name(event: &TransportEvent) -> &'static str {
match event {
TransportEvent::ConnectionOpened { .. } => "connection_opened",
TransportEvent::ConnectionClosed { .. } => "connection_closed",
TransportEvent::IncomingMessage { .. } => "incoming_message",
}
}
@@ -370,6 +481,13 @@ mod tests {
}
}
fn initialized_notification() -> JSONRPCMessage {
JSONRPCMessage::Notification(codex_app_server_protocol::JSONRPCNotification {
method: "initialized".to_string(),
params: None,
})
}
#[tokio::test]
async fn cancelled_outbound_task_emits_connection_closed() {
let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY);
@@ -487,6 +605,256 @@ mod tests {
.expect("shutdown should not hang on blocked server forwarding");
}
#[tokio::test]
async fn non_close_transport_event_send_times_out_when_queue_stays_full() {
let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY);
let (transport_event_tx, _transport_event_rx) = mpsc::channel(1);
let shutdown_token = CancellationToken::new();
let client_tracker =
ClientTracker::new(server_event_tx, transport_event_tx.clone(), &shutdown_token);
transport_event_tx
.send(TransportEvent::ConnectionClosed {
connection_id: next_connection_id(),
})
.await
.expect("transport event queue should accept prefill");
let send_result = client_tracker
.send_transport_event(TransportEvent::IncomingMessage {
connection_id: next_connection_id(),
message: initialized_notification(),
})
.await;
assert!(send_result.is_err());
}
#[tokio::test]
async fn incoming_message_timeout_does_not_advance_seq_id() {
let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY);
let (transport_event_tx, mut transport_event_rx) = mpsc::channel(2);
let shutdown_token = CancellationToken::new();
let mut client_tracker =
ClientTracker::new(server_event_tx, transport_event_tx.clone(), &shutdown_token);
client_tracker
.handle_message(initialize_envelope_with_stream_id(
"client-1",
Some("stream-1"),
))
.await
.expect("initialize should open client");
let connection_id = match transport_event_rx.recv().await.expect("open event") {
TransportEvent::ConnectionOpened { connection_id, .. } => connection_id,
other => panic!("expected connection opened, got {other:?}"),
};
let _ = transport_event_rx.recv().await.expect("initialize event");
for _ in 0..2 {
transport_event_tx
.send(TransportEvent::ConnectionClosed {
connection_id: next_connection_id(),
})
.await
.expect("transport event queue should accept prefill");
}
let retry_envelope = ClientEnvelope {
event: ClientEvent::ClientMessage {
message: initialized_notification(),
},
client_id: ClientId("client-1".to_string()),
stream_id: Some(StreamId("stream-1".to_string())),
seq_id: Some(1),
cursor: None,
};
assert!(
client_tracker
.handle_message(retry_envelope.clone())
.await
.is_err()
);
for _ in 0..2 {
let _ = transport_event_rx.recv().await.expect("prefilled event");
}
client_tracker
.handle_message(retry_envelope)
.await
.expect("retry should forward after timeout");
match transport_event_rx.recv().await.expect("retried event") {
TransportEvent::IncomingMessage {
connection_id: queued_connection_id,
..
} => assert_eq!(queued_connection_id, connection_id),
other => panic!("expected incoming message, got {other:?}"),
}
}
#[tokio::test]
async fn initialize_timeout_closes_open_connection() {
let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY);
let (transport_event_tx, mut transport_event_rx) = mpsc::channel(1);
let shutdown_token = CancellationToken::new();
let client_tracker =
ClientTracker::new(server_event_tx, transport_event_tx, &shutdown_token);
let mut handle_message = tokio::spawn(async move {
let mut client_tracker = client_tracker;
client_tracker
.handle_message(initialize_envelope_with_stream_id(
"client-1",
Some("stream-1"),
))
.await
});
assert!(
timeout(Duration::from_millis(50), &mut handle_message)
.await
.expect("initialize timeout rollback should not wait for close delivery")
.expect("handle message task should not panic")
.is_err()
);
let connection_id = match transport_event_rx.recv().await.expect("open event") {
TransportEvent::ConnectionOpened { connection_id, .. } => connection_id,
other => panic!("expected connection opened, got {other:?}"),
};
match transport_event_rx.recv().await.expect("close event") {
TransportEvent::ConnectionClosed {
connection_id: closed_connection_id,
} => assert_eq!(closed_connection_id, connection_id),
other => panic!("expected connection closed, got {other:?}"),
}
}
#[tokio::test]
async fn close_client_waits_for_transport_event_queue_capacity() {
let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY);
let (transport_event_tx, mut transport_event_rx) = mpsc::channel(2);
let shutdown_token = CancellationToken::new();
let mut client_tracker =
ClientTracker::new(server_event_tx, transport_event_tx, &shutdown_token);
client_tracker
.handle_message(initialize_envelope_with_stream_id(
"client-1",
Some("stream-1"),
))
.await
.expect("initialize should open client");
let connection_id = match transport_event_rx.recv().await.expect("open event") {
TransportEvent::ConnectionOpened { connection_id, .. } => connection_id,
other => panic!("expected connection opened, got {other:?}"),
};
let _ = transport_event_rx.recv().await.expect("initialize event");
for _ in 0..2 {
client_tracker
.transport_event_tx
.send(TransportEvent::IncomingMessage {
connection_id,
message: initialized_notification(),
})
.await
.expect("transport event queue should accept prefill");
}
let client_key = (
ClientId("client-1".to_string()),
StreamId("stream-1".to_string()),
);
let close_client = client_tracker.close_client(&client_key);
tokio::pin!(close_client);
assert!(
timeout(Duration::from_millis(20), &mut close_client)
.await
.is_err()
);
for _ in 0..2 {
match transport_event_rx.recv().await.expect("prefilled event") {
TransportEvent::IncomingMessage {
connection_id: queued_connection_id,
..
} => assert_eq!(queued_connection_id, connection_id),
other => panic!("expected incoming message, got {other:?}"),
}
}
close_client
.await
.expect("close should forward after queue drains");
match transport_event_rx.recv().await.expect("close event") {
TransportEvent::ConnectionClosed {
connection_id: closed_connection_id,
} => assert_eq!(closed_connection_id, connection_id),
other => panic!("expected connection closed, got {other:?}"),
}
}
#[tokio::test]
async fn close_client_keeps_forwarding_after_caller_is_aborted() {
let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY);
let (transport_event_tx, mut transport_event_rx) = mpsc::channel(2);
let shutdown_token = CancellationToken::new();
let mut client_tracker =
ClientTracker::new(server_event_tx, transport_event_tx, &shutdown_token);
client_tracker
.handle_message(initialize_envelope_with_stream_id(
"client-1",
Some("stream-1"),
))
.await
.expect("initialize should open client");
let connection_id = match transport_event_rx.recv().await.expect("open event") {
TransportEvent::ConnectionOpened { connection_id, .. } => connection_id,
other => panic!("expected connection opened, got {other:?}"),
};
let _ = transport_event_rx.recv().await.expect("initialize event");
for _ in 0..2 {
client_tracker
.transport_event_tx
.send(TransportEvent::IncomingMessage {
connection_id,
message: initialized_notification(),
})
.await
.expect("transport event queue should accept prefill");
}
let client_key = (
ClientId("client-1".to_string()),
StreamId("stream-1".to_string()),
);
let mut close_client =
tokio::spawn(async move { client_tracker.close_client(&client_key).await });
assert!(
timeout(Duration::from_millis(20), &mut close_client)
.await
.is_err()
);
close_client.abort();
let _ = close_client.await;
for _ in 0..2 {
let _ = transport_event_rx.recv().await.expect("prefilled event");
}
match timeout(Duration::from_secs(1), transport_event_rx.recv())
.await
.expect("close should be delivered")
.expect("close event")
{
TransportEvent::ConnectionClosed {
connection_id: closed_connection_id,
} => assert_eq!(closed_connection_id, connection_id),
other => panic!("expected connection closed, got {other:?}"),
}
}
#[tokio::test]
async fn initialize_with_new_stream_id_opens_new_connection_for_same_client() {
let (server_event_tx, _server_event_rx) = mpsc::channel(CHANNEL_CAPACITY);
@@ -19,16 +19,20 @@ use codex_app_server_protocol::RemoteControlConnectionStatus;
use codex_app_server_protocol::RemoteControlStatusChangedNotification;
use codex_login::AuthManager;
use codex_state::StateRuntime;
use futures::FutureExt;
use gethostname::gethostname;
use std::error::Error;
use std::fmt;
use std::io;
use std::panic::AssertUnwindSafe;
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tokio::sync::watch;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use tracing::error;
use tracing::info;
use tracing::warn;
pub struct RemoteControlStartConfig {
@@ -73,13 +77,21 @@ impl RemoteControlHandle {
return Err(RemoteControlUnavailable);
}
self.enabled_tx.send_if_modified(|state| {
let enabled_changed = self.enabled_tx.send_if_modified(|state| {
let changed = !*state;
*state = true;
changed
});
let status = self.status();
info!(
enabled_changed,
current_status = ?status.status,
environment_id = ?status.environment_id,
installation_id = %status.installation_id,
server_name = %status.server_name,
"remote control enable requested"
);
if matches!(
status.status,
RemoteControlConnectionStatus::Connected | RemoteControlConnectionStatus::Connecting
@@ -91,12 +103,21 @@ impl RemoteControlHandle {
}
pub fn disable(&self) -> RemoteControlStatusChangedNotification {
self.enabled_tx.send_if_modified(|state| {
let enabled_changed = self.enabled_tx.send_if_modified(|state| {
let changed = *state;
*state = false;
changed
});
let status = self.status();
info!(
enabled_changed,
current_status = ?status.status,
environment_id = ?status.environment_id,
installation_id = %status.installation_id,
server_name = %status.server_name,
"remote control disable requested"
);
self.publish_status(RemoteControlConnectionStatus::Disabled)
}
@@ -112,6 +133,7 @@ impl RemoteControlHandle {
&self,
connection_status: RemoteControlConnectionStatus,
) -> RemoteControlStatusChangedNotification {
let mut status_change = None;
self.status_tx.send_if_modified(|status| {
let next_status =
remote_control_status_with_connection_status(status, connection_status);
@@ -119,9 +141,21 @@ impl RemoteControlHandle {
return false;
}
status_change = Some((status.clone(), next_status.clone()));
*status = next_status;
true
});
if let Some((previous_status, next_status)) = status_change {
info!(
previous_status = ?previous_status.status,
next_status = ?next_status.status,
previous_environment_id = ?previous_status.environment_id,
next_environment_id = ?next_status.environment_id,
installation_id = %next_status.installation_id,
server_name = %next_status.server_name,
"remote control handle status changed"
);
}
self.status()
}
}
@@ -165,6 +199,8 @@ pub async fn start_remote_control(
let (enabled_tx, enabled_rx) = watch::channel(initial_enabled);
let server_name = gethostname().to_string_lossy().trim().to_string();
let remote_control_url = config.remote_control_url;
let installation_id = config.installation_id;
let initial_status = RemoteControlStatusChangedNotification {
status: if initial_enabled {
RemoteControlConnectionStatus::Connecting
@@ -172,16 +208,35 @@ pub async fn start_remote_control(
RemoteControlConnectionStatus::Disabled
},
server_name: server_name.clone(),
installation_id: config.installation_id.clone(),
installation_id: installation_id.clone(),
environment_id: None,
};
let (status_tx, _status_rx) = watch::channel(initial_status);
let status_publisher = RemoteControlStatusPublisher::new(status_tx.clone());
info!(
remote_control_url = %remote_control_url,
installation_id = %installation_id,
server_name = %server_name,
state_db_available,
initial_enabled,
"starting app-server remote control websocket task"
);
let remote_control_url_for_log = remote_control_url.clone();
let installation_id_for_log = installation_id.clone();
let server_name_for_log = server_name.clone();
let shutdown_token_for_log = shutdown_token.clone();
let join_handle = tokio::spawn(async move {
RemoteControlWebsocket::new(
info!(
remote_control_url = %remote_control_url_for_log,
installation_id = %installation_id_for_log,
server_name = %server_name_for_log,
initial_enabled,
"app-server remote control websocket task started"
);
let websocket_task = RemoteControlWebsocket::new(
websocket::RemoteControlWebsocketConfig {
remote_control_url: config.remote_control_url,
installation_id: config.installation_id,
remote_control_url,
installation_id,
remote_control_target,
server_name,
},
@@ -194,8 +249,38 @@ pub async fn start_remote_control(
shutdown_token,
enabled_rx,
)
.run(app_server_client_name_rx)
.await;
.run(app_server_client_name_rx);
match AssertUnwindSafe(websocket_task).catch_unwind().await {
Ok(()) => {
let shutdown_requested = shutdown_token_for_log.is_cancelled();
if shutdown_requested {
info!(
remote_control_url = %remote_control_url_for_log,
installation_id = %installation_id_for_log,
server_name = %server_name_for_log,
shutdown_requested,
"app-server remote control websocket task exited"
);
} else {
warn!(
remote_control_url = %remote_control_url_for_log,
installation_id = %installation_id_for_log,
server_name = %server_name_for_log,
shutdown_requested,
"app-server remote control websocket task exited without shutdown"
);
}
}
Err(panic) => {
error!(
remote_control_url = %remote_control_url_for_log,
installation_id = %installation_id_for_log,
server_name = %server_name_for_log,
"app-server remote control websocket task panicked"
);
std::panic::resume_unwind(panic);
}
}
});
Ok((
@@ -66,6 +66,10 @@ const REMOTE_CONTROL_ACCOUNT_ID_RETRY_INTERVAL: std::time::Duration =
std::time::Duration::from_secs(1);
const REMOTE_CONTROL_RECONNECT_BACKOFF_CAP: std::time::Duration =
std::time::Duration::from_secs(30);
const REMOTE_CONTROL_WEBSOCKET_CONNECT_TIMEOUT: std::time::Duration =
std::time::Duration::from_secs(30);
const REMOTE_CONTROL_CONNECTION_SHUTDOWN_TIMEOUT: std::time::Duration =
std::time::Duration::from_secs(5);
struct BoundedOutboundBuffer {
buffer_by_stream: HashMap<(ClientId, StreamId), VecDeque<ServerEnvelope>>,
@@ -179,14 +183,32 @@ impl WebsocketState {
return ClientSegmentObservation::Dropped;
}
let observation = self.client_segment_reassembler.observe(client_envelope);
if matches!(observation, ClientSegmentObservation::Forward(_))
&& let Some((key, seq_id)) = client_message_key
{
self.client_segment_reassembler.observe(client_envelope)
}
fn record_client_message_delivery(
&mut self,
client_envelope: &ClientEnvelope,
client_message_key: Option<((ClientId, Option<StreamId>), u64)>,
) {
if let Some(cursor) = client_envelope.cursor.as_deref() {
self.subscribe_cursor = Some(cursor.to_string());
}
if let Some((key, seq_id)) = client_message_key {
self.last_completed_client_chunk_seq_id_by_stream
.insert(key, seq_id);
}
observation
if let ClientEvent::Ack { segment_id } = &client_envelope.event
&& let Some(acked_seq_id) = client_envelope.seq_id
&& let Some(stream_id) = client_envelope.stream_id.as_ref()
{
self.outbound_buffer.ack(
&client_envelope.client_id,
stream_id,
acked_seq_id,
*segment_id,
);
}
}
fn invalidate_client_message_stream(&mut self, client_id: &ClientId, stream_id: &StreamId) {
@@ -255,6 +277,14 @@ enum ConnectOutcome {
Shutdown,
}
#[derive(Debug, Clone, Copy)]
enum ConnectionEndReason {
Shutdown,
Disabled,
EnabledWatchClosed,
ConnectionWorkerStopped,
}
pub(super) struct RemoteControlChannels {
pub(super) transport_event_tx: mpsc::Sender<TransportEvent>,
pub(super) status_publisher: RemoteControlStatusPublisher,
@@ -270,7 +300,12 @@ impl RemoteControlStatusPublisher {
Self { tx }
}
fn status(&self) -> RemoteControlStatusChangedNotification {
self.tx.borrow().clone()
}
fn publish_status(&self, connection_status: RemoteControlConnectionStatus) {
let mut status_change = None;
self.tx.send_if_modified(|status| {
let next_status =
remote_control_status_with_connection_status(status, connection_status);
@@ -278,12 +313,25 @@ impl RemoteControlStatusPublisher {
return false;
}
status_change = Some((status.clone(), next_status.clone()));
*status = next_status;
true
});
if let Some((previous_status, next_status)) = status_change {
info!(
previous_status = ?previous_status.status,
next_status = ?next_status.status,
previous_environment_id = ?previous_status.environment_id,
next_environment_id = ?next_status.environment_id,
installation_id = %next_status.installation_id,
server_name = %next_status.server_name,
"remote control websocket status changed"
);
}
}
fn publish_environment_id(&self, environment_id: Option<String>) {
let mut status_change = None;
self.tx.send_if_modified(|status| {
if status.status == RemoteControlConnectionStatus::Disabled {
return false;
@@ -298,9 +346,20 @@ impl RemoteControlStatusPublisher {
return false;
}
status_change = Some((status.clone(), next_status.clone()));
*status = next_status;
true
});
if let Some((previous_status, next_status)) = status_change {
info!(
status = ?next_status.status,
previous_environment_id = ?previous_status.environment_id,
next_environment_id = ?next_status.environment_id,
installation_id = %next_status.installation_id,
server_name = %next_status.server_name,
"remote control websocket environment changed"
);
}
}
}
@@ -367,12 +426,26 @@ impl RemoteControlWebsocket {
mut self,
app_server_client_name_rx: Option<oneshot::Receiver<String>>,
) {
info!(
remote_control_url = %self.remote_control_url,
installation_id = %self.installation_id,
server_name = %self.server_name,
initial_enabled = *self.enabled_rx.borrow(),
"app-server remote control websocket loop started"
);
let app_server_client_name = match self
.wait_for_app_server_client_name(app_server_client_name_rx)
.await
{
Ok(app_server_client_name) => app_server_client_name,
Err(_) => {
warn!(
remote_control_url = %self.remote_control_url,
installation_id = %self.installation_id,
server_name = %self.server_name,
shutdown_requested = self.shutdown_token.is_cancelled(),
"app-server remote control websocket loop stopped before client name was ready"
);
self.client_tracker.lock().await.shutdown().await;
return;
}
@@ -380,9 +453,27 @@ impl RemoteControlWebsocket {
loop {
if !self.wait_until_enabled().await {
info!(
remote_control_url = %self.remote_control_url,
installation_id = %self.installation_id,
server_name = %self.server_name,
shutdown_requested = self.shutdown_token.is_cancelled(),
current_status = ?self.status_publisher.status().status,
"app-server remote control websocket loop exiting while waiting for enablement"
);
break;
}
let status = self.status_publisher.status();
info!(
remote_control_url = %self.remote_control_url,
installation_id = %self.installation_id,
server_name = %self.server_name,
reconnect_attempt = self.reconnect_attempt.saturating_add(1),
current_status = ?status.status,
environment_id = ?status.environment_id,
"starting app-server remote control websocket connection cycle"
);
let shutdown_token = self.shutdown_token.child_token();
let websocket_connection = match self
.connect(&shutdown_token, app_server_client_name.as_deref())
@@ -397,11 +488,30 @@ impl RemoteControlWebsocket {
ConnectOutcome::Shutdown => break,
};
self.run_connection(websocket_connection, shutdown_token)
let connection_end_reason = self
.run_connection(websocket_connection, shutdown_token)
.await;
let status = self.status_publisher.status();
info!(
remote_control_url = %self.remote_control_url,
installation_id = %self.installation_id,
server_name = %self.server_name,
connection_end_reason = ?connection_end_reason,
current_status = ?status.status,
environment_id = ?status.environment_id,
enabled = *self.enabled_rx.borrow(),
"app-server remote control websocket connection cycle ended"
);
}
self.client_tracker.lock().await.shutdown().await;
info!(
remote_control_url = %self.remote_control_url,
installation_id = %self.installation_id,
server_name = %self.server_name,
shutdown_requested = self.shutdown_token.is_cancelled(),
"app-server remote control websocket loop exited"
);
}
async fn wait_for_app_server_client_name(
@@ -462,6 +572,19 @@ impl RemoteControlWebsocket {
loop {
let subscribe_cursor = self.state.lock().await.subscribe_cursor.clone();
let enrollment = self.enrollment.as_ref();
info!(
websocket_url = %remote_control_target.websocket_url,
installation_id = %self.installation_id,
server_name = %self.server_name,
reconnect_attempt = self.reconnect_attempt.saturating_add(1),
has_enrollment = enrollment.is_some(),
server_id = ?enrollment.map(|enrollment| enrollment.server_id.as_str()),
environment_id = ?enrollment.map(|enrollment| enrollment.environment_id.as_str()),
subscribe_cursor_present = subscribe_cursor.is_some(),
app_server_client_name = ?app_server_client_name,
"connecting to app-server remote control websocket"
);
let connect_options = RemoteControlConnectOptions {
installation_id: &self.installation_id,
server_name: &self.server_name,
@@ -500,10 +623,16 @@ impl RemoteControlWebsocket {
self.auth_recovery = self.auth_manager.unauthorized_recovery();
self.status_publisher
.publish_status(RemoteControlConnectionStatus::Connected);
let enrollment = self.enrollment.as_ref();
info!(
"connected to app-server remote control websocket: {}, {}",
remote_control_target.websocket_url,
format_headers(response.headers())
websocket_url = %remote_control_target.websocket_url,
installation_id = %self.installation_id,
server_name = %self.server_name,
server_id = ?enrollment.map(|enrollment| enrollment.server_id.as_str()),
environment_id = ?enrollment.map(|enrollment| enrollment.environment_id.as_str()),
subscribe_cursor_present = subscribe_cursor.is_some(),
response_headers = %format_headers(response.headers()),
"connected to app-server remote control websocket"
);
return ConnectOutcome::Connected(Box::new(websocket_connection));
}
@@ -519,12 +648,20 @@ impl RemoteControlWebsocket {
let reconnect_attempt = self.reconnect_attempt.saturating_add(1);
let (reconnect_delay, reconnect_backoff_reset) =
next_reconnect_delay(&mut self.reconnect_attempt);
let enrollment = self.enrollment.as_ref();
warn!(
websocket_url = %remote_control_target.websocket_url,
installation_id = %self.installation_id,
server_name = %self.server_name,
error = %err,
error_kind = ?err.kind(),
reconnect_attempt,
reconnect_delay = ?reconnect_delay,
reconnect_backoff_reset,
has_enrollment = enrollment.is_some(),
server_id = ?enrollment.map(|enrollment| enrollment.server_id.as_str()),
environment_id = ?enrollment.map(|enrollment| enrollment.environment_id.as_str()),
subscribe_cursor_present = subscribe_cursor.is_some(),
"failed to connect to app-server remote control websocket"
);
if reconnect_backoff_reset {
@@ -562,7 +699,7 @@ impl RemoteControlWebsocket {
&self,
websocket_connection: WebSocketStream<MaybeTlsStream<TcpStream>>,
shutdown_token: CancellationToken,
) {
) -> ConnectionEndReason {
let (websocket_writer, websocket_reader) = websocket_connection.split();
let mut join_set = tokio::task::JoinSet::new();
@@ -583,19 +720,48 @@ impl RemoteControlWebsocket {
));
let mut enabled_rx = self.enabled_rx.clone();
tokio::select! {
_ = shutdown_token.cancelled() => {}
let connection_end_reason = tokio::select! {
_ = shutdown_token.cancelled() => ConnectionEndReason::Shutdown,
changed = enabled_rx.wait_for(|enabled| !*enabled) => {
if changed.is_ok() {
self.status_publisher
.publish_status(RemoteControlConnectionStatus::Disabled);
ConnectionEndReason::Disabled
} else {
ConnectionEndReason::EnabledWatchClosed
}
}
_ = join_set.join_next() => {}
_ = join_set.join_next() => ConnectionEndReason::ConnectionWorkerStopped,
};
shutdown_token.cancel();
join_set.join_all().await;
Self::join_connection_workers(&mut join_set, REMOTE_CONTROL_CONNECTION_SHUTDOWN_TIMEOUT)
.await;
connection_end_reason
}
async fn join_connection_workers(
join_set: &mut tokio::task::JoinSet<()>,
shutdown_timeout: std::time::Duration,
) {
if tokio::time::timeout(shutdown_timeout, Self::drain_join_set(join_set))
.await
.is_ok()
{
return;
}
warn!(
shutdown_timeout = ?shutdown_timeout,
remaining_workers = join_set.len(),
"timed out waiting for remote control connection workers to stop; aborting"
);
join_set.abort_all();
Self::drain_join_set(join_set).await;
}
async fn drain_join_set(join_set: &mut tokio::task::JoinSet<()>) {
while join_set.join_next().await.is_some() {}
}
async fn run_server_writer(
@@ -675,12 +841,14 @@ impl RemoteControlWebsocket {
let queued_server_envelope = tokio::select! {
_ = shutdown_token.cancelled() => return Ok(()),
_ = ping_interval.tick() => {
if let Err(err) = websocket_writer
.send(tungstenite::Message::Ping(Vec::new().into()))
.await
{
return Err(io::Error::other(err));
}
tokio::select! {
_ = shutdown_token.cancelled() => return Ok(()),
send_result = websocket_writer.send(tungstenite::Message::Ping(Vec::new().into())) => {
if let Err(err) = send_result {
return Err(io::Error::other(err));
}
}
};
continue;
}
wait_result = used_rx.changed(), if !outbound_has_capacity =>
@@ -886,6 +1054,7 @@ impl RemoteControlWebsocket {
}
};
let client_message_key = WebsocketState::client_message_key(&client_envelope);
let observation = {
let mut websocket_state = state.lock().await;
websocket_state.observe_client_message(client_envelope, wire_size_bytes)
@@ -895,24 +1064,6 @@ impl RemoteControlWebsocket {
ClientSegmentObservation::Pending | ClientSegmentObservation::Dropped => continue,
};
{
let mut websocket_state = state.lock().await;
if let Some(cursor) = client_envelope.cursor.as_deref() {
websocket_state.subscribe_cursor = Some(cursor.to_string());
}
if let ClientEvent::Ack { segment_id } = &client_envelope.event
&& let Some(acked_seq_id) = client_envelope.seq_id
&& let Some(stream_id) = client_envelope.stream_id.as_ref()
{
websocket_state.outbound_buffer.ack(
&client_envelope.client_id,
stream_id,
acked_seq_id,
*segment_id,
);
}
}
let closed_client =
matches!(&client_envelope.event, ClientEvent::ClientClosed).then(|| {
(
@@ -920,6 +1071,7 @@ impl RemoteControlWebsocket {
client_envelope.stream_id.clone(),
)
});
let delivered_client_envelope = client_envelope.clone();
if client_tracker
.handle_message(client_envelope)
.await
@@ -927,6 +1079,10 @@ impl RemoteControlWebsocket {
{
return Ok(());
}
state
.lock()
.await
.record_client_message_delivery(&delivered_client_envelope, client_message_key);
if let Some((client_id, stream_id)) = closed_client {
let mut websocket_state = state.lock().await;
if let Some(stream_id) = stream_id {
@@ -1190,7 +1346,22 @@ pub(super) async fn connect_remote_control_websocket(
connect_options.subscribe_cursor,
)?;
match connect_async(request).await {
let websocket_connect_result = tokio::time::timeout(
REMOTE_CONTROL_WEBSOCKET_CONNECT_TIMEOUT,
connect_async(request),
)
.await
.map_err(|_| {
io::Error::new(
ErrorKind::TimedOut,
format!(
"timed out connecting to remote control websocket at `{}` after {:?}",
remote_control_target.websocket_url, REMOTE_CONTROL_WEBSOCKET_CONNECT_TIMEOUT
),
)
})?;
match websocket_connect_result {
Ok((websocket_stream, response)) => Ok((websocket_stream, response.map(|_| ()))),
Err(err) => {
match &err {
@@ -2013,6 +2184,17 @@ mod tests {
.expect("writer should stop cleanly");
}
#[tokio::test]
async fn join_connection_workers_aborts_stuck_worker_after_timeout() {
let mut join_set = tokio::task::JoinSet::new();
join_set.spawn(futures::future::pending::<()>());
RemoteControlWebsocket::join_connection_workers(&mut join_set, Duration::from_millis(10))
.await;
assert!(join_set.is_empty());
}
#[tokio::test]
async fn run_server_writer_inner_assigns_contiguous_seq_ids_per_stream() {
let (client_stream, mut server_stream) = connected_websocket_pair().await;
@@ -2350,6 +2532,65 @@ mod tests {
&raw[split..],
);
assert!(matches!(
observe_client_message(&mut state, first_chunk.clone()),
ClientSegmentObservation::Pending
));
let completed_envelope = match observe_client_message(&mut state, second_chunk) {
ClientSegmentObservation::Forward(client_envelope) => *client_envelope,
_ => panic!("expected completed client message"),
};
state.record_client_message_delivery(
&completed_envelope,
Some((
(
ClientId("client-1".to_string()),
Some(StreamId("stream-1".to_string())),
),
4,
)),
);
assert!(matches!(
observe_client_message(&mut state, first_chunk),
ClientSegmentObservation::Dropped
));
}
#[test]
fn websocket_state_allows_replay_before_completed_chunk_delivery() {
let (outbound_buffer, _used_rx) = BoundedOutboundBuffer::new();
let mut state = WebsocketState {
outbound_buffer,
subscribe_cursor: None,
next_seq_id_by_stream: HashMap::new(),
last_completed_client_chunk_seq_id_by_stream: HashMap::new(),
client_segment_reassembler: ClientSegmentReassembler::default(),
};
let message = JSONRPCMessage::Notification(JSONRPCNotification {
method: "initialized".to_string(),
params: None,
});
let raw = serde_json::to_vec(&message).expect("message should serialize");
let split = raw.len() / 2;
let first_chunk = client_chunk_envelope(
"client-1",
"stream-1",
/*seq_id*/ 4,
/*segment_id*/ 0,
/*segment_count*/ 2,
raw.len(),
&raw[..split],
);
let second_chunk = client_chunk_envelope(
"client-1",
"stream-1",
/*seq_id*/ 4,
/*segment_id*/ 1,
/*segment_count*/ 2,
raw.len(),
&raw[split..],
);
assert!(matches!(
observe_client_message(&mut state, first_chunk.clone()),
ClientSegmentObservation::Pending
@@ -2360,7 +2601,7 @@ mod tests {
));
assert!(matches!(
observe_client_message(&mut state, first_chunk),
ClientSegmentObservation::Dropped
ClientSegmentObservation::Pending
));
}
+1
View File
@@ -55,6 +55,7 @@ codex-file-search = { workspace = true }
codex-chatgpt = { workspace = true }
codex-login = { workspace = true }
codex-memories-extension = { workspace = true }
codex-web-search-extension = { workspace = true }
codex-memories-write = { workspace = true }
codex-mcp = { workspace = true }
codex-model-provider = { workspace = true }
+3
View File
@@ -12,6 +12,7 @@ use codex_extension_api::AgentSpawner;
use codex_extension_api::ExtensionEventSink;
use codex_extension_api::ExtensionRegistry;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_login::AuthManager;
use codex_protocol::ThreadId;
use codex_protocol::error::CodexErr;
use codex_protocol::protocol::Event;
@@ -22,6 +23,7 @@ use crate::outgoing_message::OutgoingMessageSender;
pub(crate) fn thread_extensions<S>(
guardian_agent_spawner: S,
event_sink: Arc<dyn ExtensionEventSink>,
auth_manager: Arc<AuthManager>,
) -> Arc<ExtensionRegistry<Config>>
where
S: AgentSpawner<StartThreadOptions, Spawned = NewThread, Error = CodexErr> + 'static,
@@ -29,6 +31,7 @@ where
let mut builder = ExtensionRegistryBuilder::<Config>::with_event_sink(event_sink);
codex_guardian::install(&mut builder, guardian_agent_spawner);
codex_memories_extension::install(&mut builder, codex_otel::global());
codex_web_search_extension::install(&mut builder, auth_manager);
Arc::new(builder.build())
}
+3 -2
View File
@@ -184,15 +184,16 @@ mod tests {
let thread_manager = Arc::new_cyclic(|thread_manager| {
ThreadManager::new(
&good_config,
auth_manager,
auth_manager.clone(),
SessionSource::Exec,
Arc::new(EnvironmentManager::default_for_tests()),
thread_extensions(
guardian_agent_spawner(thread_manager.clone()),
Arc::new(NoopExtensionEventSink),
auth_manager.clone(),
),
/*analytics_events_client*/ None,
thread_store,
Arc::clone(&thread_store),
Some(state_db.clone()),
"11111111-1111-4111-8111-111111111111".to_string(),
/*attestation_provider*/ None,
@@ -314,6 +314,7 @@ impl MessageProcessor {
thread_extensions(
guardian_agent_spawner(thread_manager.clone()),
app_server_extension_event_sink(outgoing.clone()),
auth_manager.clone(),
),
Some(analytics_events_client.clone()),
Arc::clone(&thread_store),
@@ -658,6 +658,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
text_elements: Vec::new(),
}],
responsesapi_client_metadata: None,
additional_context: None,
cwd: None,
runtime_workspace_roots: None,
approval_policy: None,
@@ -25,6 +25,8 @@ use codex_app_server_protocol::AccountLoginCompletedNotification;
use codex_app_server_protocol::AccountUpdatedNotification;
use codex_app_server_protocol::AddCreditsNudgeCreditType;
use codex_app_server_protocol::AddCreditsNudgeEmailStatus;
use codex_app_server_protocol::AdditionalContextEntry;
use codex_app_server_protocol::AdditionalContextKind;
use codex_app_server_protocol::AppInfo;
use codex_app_server_protocol::AppListUpdatedNotification;
use codex_app_server_protocol::AppSummary;
@@ -421,6 +423,7 @@ use codex_thread_store::ThreadStore;
use codex_thread_store::ThreadStoreError;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::io::Error as IoError;
@@ -1513,9 +1513,13 @@ impl ThreadRequestProcessor {
.clone()
.ok_or_else(|| internal_error("sqlite state db unavailable for memory reset"))?;
state_db.clear_memory_data().await.map_err(|err| {
internal_error(format!("failed to clear memory rows in state db: {err}"))
})?;
state_db
.memories()
.clear_memory_data()
.await
.map_err(|err| {
internal_error(format!("failed to clear memory rows in memories db: {err}"))
})?;
clear_memory_roots_contents(&self.config.codex_home)
.await
@@ -2783,15 +2787,20 @@ impl ThreadRequestProcessor {
/*include_history*/ true,
)
.await?;
if source_thread.thread_id != existing_thread_id {
return Err(invalid_request(format!(
"cannot resume running thread {existing_thread_id} from source thread {}",
source_thread.thread_id
)));
}
Some((existing_thread_id, existing_thread, source_thread))
} else {
None
let source_thread = self
.read_stored_thread_for_resume(
&params.thread_id,
params.path.as_ref(),
/*include_history*/ true,
)
.await?;
let existing_thread_id = source_thread.thread_id;
match self.thread_manager.get_thread(existing_thread_id).await {
Ok(existing_thread) => Some((existing_thread_id, existing_thread, source_thread)),
Err(_) => None,
}
};
if let Some((existing_thread_id, existing_thread, source_thread)) = running_thread {
@@ -2808,6 +2817,48 @@ impl ThreadRequestProcessor {
active_path.display()
)));
}
let config_snapshot = existing_thread.config_snapshot().await;
let mismatch_details = collect_resume_override_mismatches(params, &config_snapshot);
if !mismatch_details.is_empty() {
let has_subscribers = !self
.thread_state_manager
.subscribed_connection_ids(existing_thread_id)
.await
.is_empty();
let loaded_status = self
.thread_watch_manager
.loaded_status_for_thread(&existing_thread_id.to_string())
.await;
let is_running =
matches!(existing_thread.agent_status().await, AgentStatus::Running);
if !has_subscribers && matches!(loaded_status, ThreadStatus::Idle) && !is_running {
// A loaded idle thread is only a cache entry. Shut it down
// before removing it so cold resume cannot duplicate a
// thread that timed out during shutdown.
match wait_for_thread_shutdown(&existing_thread).await {
ThreadShutdownResult::Complete => {
self.thread_manager.remove_thread(&existing_thread_id).await;
self.finalize_thread_teardown(existing_thread_id).await;
return Ok(false);
}
ThreadShutdownResult::SubmitFailed => {
warn!("failed to submit Shutdown to thread {existing_thread_id}");
}
ThreadShutdownResult::TimedOut => {
warn!("thread {existing_thread_id} shutdown timed out");
}
}
}
// Preserve rejoin semantics when another client can still observe
// the loaded thread or shutdown did not complete.
tracing::warn!(
"thread/resume overrides ignored for loaded thread {}: {}",
existing_thread_id,
mismatch_details.join("; ")
);
}
let redact_resume_payloads =
should_redact_thread_resume_payloads(app_server_client_name.as_deref());
let history_items = source_thread
@@ -2837,15 +2888,6 @@ impl ThreadRequestProcessor {
)
.await?;
let config_snapshot = existing_thread.config_snapshot().await;
let mismatch_details = collect_resume_override_mismatches(params, &config_snapshot);
if !mismatch_details.is_empty() {
tracing::warn!(
"thread/resume overrides ignored for running thread {}: {}",
existing_thread_id,
mismatch_details.join("; ")
);
}
let mut summary_source_thread = source_thread;
summary_source_thread.history = None;
let mut thread_summary = self.stored_thread_to_api_thread(
@@ -1,4 +1,6 @@
use super::*;
use codex_protocol::protocol::AdditionalContextEntry as CoreAdditionalContextEntry;
use codex_protocol::protocol::AdditionalContextKind as CoreAdditionalContextKind;
#[derive(Clone)]
pub(crate) struct TurnRequestProcessor {
@@ -30,6 +32,29 @@ fn resolve_runtime_workspace_roots(
resolved_roots
}
fn map_additional_context(
additional_context: Option<HashMap<String, AdditionalContextEntry>>,
) -> BTreeMap<String, CoreAdditionalContextEntry> {
additional_context
.unwrap_or_default()
.into_iter()
.map(|(key, entry)| {
(
key,
CoreAdditionalContextEntry {
value: entry.value,
kind: match entry.kind {
AdditionalContextKind::Untrusted => CoreAdditionalContextKind::Untrusted,
AdditionalContextKind::Application => {
CoreAdditionalContextKind::Application
}
},
},
)
})
.collect()
}
struct ThreadSettingsBuildParams {
method: &'static str,
cwd: Option<PathBuf>,
@@ -391,6 +416,7 @@ impl TurnRequestProcessor {
.into_iter()
.map(V2UserInput::into_core)
.collect();
let additional_context = map_additional_context(params.additional_context);
let turn_has_input = !mapped_items.is_empty();
let thread_settings = self
.build_thread_settings_overrides(
@@ -419,6 +445,7 @@ impl TurnRequestProcessor {
environments: environment_selections,
final_output_json_schema: params.output_schema,
responsesapi_client_metadata: params.responsesapi_client_metadata,
additional_context,
thread_settings,
};
let turn_id = self
@@ -746,10 +773,12 @@ impl TurnRequestProcessor {
.into_iter()
.map(V2UserInput::into_core)
.collect();
let additional_context = map_additional_context(params.additional_context);
let turn_id = thread
.steer_input(
mapped_items,
additional_context,
Some(&params.expected_turn_id),
params.responsesapi_client_metadata,
)
@@ -1,8 +1,16 @@
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::create_fake_rollout;
use app_test_support::to_response;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ReviewDelivery;
use codex_app_server_protocol::ReviewStartParams;
use codex_app_server_protocol::ReviewStartResponse;
use codex_app_server_protocol::ReviewTarget;
use codex_app_server_protocol::ThreadForkParams;
use codex_app_server_protocol::ThreadForkResponse;
use codex_app_server_protocol::ThreadSource;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::TurnStartParams;
@@ -60,6 +68,7 @@ async fn turn_start_forwards_client_metadata_to_responses_request_v2() -> Result
let client_metadata = HashMap::from([
("fiber_run_id".to_string(), "fiber-start-123".to_string()),
("origin".to_string(), "gaas".to_string()),
("thread_source".to_string(), "client-supplied".to_string()),
]);
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
@@ -93,12 +102,188 @@ async fn turn_start_forwards_client_metadata_to_responses_request_v2() -> Result
.unwrap_or_else(|| panic!("missing x-codex-turn-metadata header"));
assert_eq!(metadata["fiber_run_id"].as_str(), Some("fiber-start-123"));
assert_eq!(metadata["origin"].as_str(), Some("gaas"));
assert_eq!(metadata["thread_source"].as_str(), Some("client-supplied"));
assert_eq!(metadata["turn_id"].as_str(), Some(turn.id.as_str()));
assert!(metadata.get("session_id").is_some());
Ok(())
}
#[tokio::test]
async fn turn_start_sends_fork_lineage_in_turn_metadata_for_thread_fork_v2() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let response_mock = responses::mount_sse_once(
&server,
responses::sse(vec![
responses::ev_response_created("resp-1"),
responses::ev_assistant_message("msg-1", "Done"),
responses::ev_completed("resp-1"),
]),
)
.await;
let codex_home = TempDir::new()?;
create_config_toml(
codex_home.path(),
&server.uri(),
/*supports_websockets*/ false,
)?;
let source_thread_id = create_fake_rollout(
codex_home.path(),
"2025-01-05T12-00-00",
"2025-01-05T12:00:00Z",
"Saved user message",
Some("mock_provider"),
/*git_info*/ None,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let ThreadForkResponse { thread, .. } =
fork_fake_rollout_thread(&mut mcp, source_thread_id.clone()).await?;
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "Continue".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
})
.await?;
let turn_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
)
.await??;
let TurnStartResponse { turn } = to_response::<TurnStartResponse>(turn_resp)?;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
let request = response_mock.single_request();
let metadata = request
.header("x-codex-turn-metadata")
.as_deref()
.map(parse_json_header)
.unwrap_or_else(|| panic!("missing x-codex-turn-metadata header"));
assert_eq!(
metadata["forked_from_thread_id"].as_str(),
Some(source_thread_id.as_str())
);
assert_eq!(metadata["thread_id"].as_str(), Some(thread.id.as_str()));
assert_eq!(metadata["turn_id"].as_str(), Some(turn.id.as_str()));
Ok(())
}
#[tokio::test]
async fn review_start_sends_fork_lineage_in_turn_metadata_for_thread_fork_v2() -> Result<()> {
skip_if_no_network!(Ok(()));
let review_payload = serde_json::json!({
"findings": [],
"overall_correctness": "good",
"overall_explanation": "Done",
"overall_confidence_score": 0.5
})
.to_string();
let server = responses::start_mock_server().await;
let response_mock = responses::mount_sse_once(
&server,
responses::sse(vec![
responses::ev_response_created("resp-1"),
responses::ev_assistant_message("msg-1", &review_payload),
responses::ev_completed("resp-1"),
]),
)
.await;
let codex_home = TempDir::new()?;
create_config_toml(
codex_home.path(),
&server.uri(),
/*supports_websockets*/ false,
)?;
let source_thread_id = create_fake_rollout(
codex_home.path(),
"2025-01-05T12-00-00",
"2025-01-05T12:00:00Z",
"Saved user message",
Some("mock_provider"),
/*git_info*/ None,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let ThreadForkResponse { thread, .. } =
fork_fake_rollout_thread(&mut mcp, source_thread_id.clone()).await?;
let review_req = mcp
.send_review_start_request(ReviewStartParams {
thread_id: thread.id.clone(),
delivery: Some(ReviewDelivery::Inline),
target: ReviewTarget::Custom {
instructions: "Review the fork".to_string(),
},
})
.await?;
let review_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(review_req)),
)
.await??;
let ReviewStartResponse {
review_thread_id, ..
} = to_response::<ReviewStartResponse>(review_resp)?;
assert_eq!(review_thread_id, thread.id);
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
let request = response_mock.single_request();
let metadata = request
.header("x-codex-turn-metadata")
.as_deref()
.map(parse_json_header)
.unwrap_or_else(|| panic!("missing x-codex-turn-metadata header"));
assert_eq!(
request.header("x-openai-subagent").as_deref(),
Some("review")
);
assert_eq!(
metadata["forked_from_thread_id"].as_str(),
Some(review_thread_id.as_str())
);
let review_request_thread_id = metadata["thread_id"]
.as_str()
.unwrap_or_else(|| panic!("missing review request thread_id"));
assert!(review_request_thread_id != review_thread_id.as_str());
assert_eq!(
request
.header("x-codex-window-id")
.as_deref()
.and_then(|window_id| window_id.split_once(':').map(|(thread_id, _)| thread_id)),
Some(review_request_thread_id)
);
assert!(metadata["turn_id"].as_str().is_some());
Ok(())
}
#[tokio::test]
async fn turn_steer_updates_client_metadata_on_follow_up_responses_request_v2() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -179,6 +364,7 @@ async fn turn_steer_updates_client_metadata_on_follow_up_responses_request_v2()
text_elements: Vec::new(),
}],
responsesapi_client_metadata: Some(steer_metadata.clone()),
additional_context: None,
expected_turn_id: turn_id.clone(),
})
.await?;
@@ -344,6 +530,25 @@ supports_websockets = {supports_websockets}
)
}
async fn fork_fake_rollout_thread(
mcp: &mut McpProcess,
source_thread_id: String,
) -> Result<ThreadForkResponse> {
let fork_req = mcp
.send_thread_fork_request(ThreadForkParams {
thread_id: source_thread_id,
thread_source: Some(ThreadSource::User),
..Default::default()
})
.await?;
let fork_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(fork_req)),
)
.await??;
to_response::<ThreadForkResponse>(fork_resp)
}
fn parse_json_header(value: &str) -> serde_json::Value {
match serde_json::from_str(value) {
Ok(value) => value,
@@ -49,7 +49,10 @@ async fn memory_reset_clears_memory_files_and_rows_preserves_threads() -> Result
.await??;
let _: MemoryResetResponse = to_response::<MemoryResetResponse>(response)?;
let stage1_outputs = state_db.list_stage1_outputs_for_global(/*n*/ 10).await?;
let stage1_outputs = state_db
.memories()
.list_stage1_outputs_for_global(/*n*/ 10)
.await?;
assert_eq!(stage1_outputs, Vec::new());
assert_eq!(
state_db.get_thread_memory_mode(thread_id).await?.as_deref(),
@@ -81,6 +84,7 @@ async fn seed_stage1_output(state_db: &Arc<StateRuntime>, codex_home: &Path) ->
state_db.upsert_thread(&metadata).await?;
let claim = state_db
.memories()
.try_claim_stage1_job(
thread_id,
worker_id,
@@ -94,6 +98,7 @@ async fn seed_stage1_output(state_db: &Arc<StateRuntime>, codex_home: &Path) ->
};
assert!(
state_db
.memories()
.mark_stage1_job_succeeded(
thread_id,
ownership_token.as_str(),
@@ -106,6 +111,7 @@ async fn seed_stage1_output(state_db: &Arc<StateRuntime>, codex_home: &Path) ->
"stage1 success should be recorded"
);
state_db
.memories()
.enqueue_global_consolidation(now.timestamp())
.await?;
@@ -68,4 +68,5 @@ mod turn_interrupt;
mod turn_start;
mod turn_start_zsh_fork;
mod turn_steer;
mod web_search;
mod windows_sandbox_setup;
@@ -43,6 +43,7 @@ use codex_app_server_protocol::ThreadSource;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::ThreadStatus;
use codex_app_server_protocol::ThreadUnsubscribeParams;
use codex_app_server_protocol::TurnItemsView;
use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::TurnStartResponse;
@@ -1929,7 +1930,7 @@ async fn thread_resume_and_read_interrupt_incomplete_rollout_turn_when_thread_is
}
#[tokio::test]
async fn thread_resume_without_overrides_does_not_change_updated_at_or_mtime() -> Result<()> {
async fn thread_resume_defers_updated_at_until_turn_start() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
let rollout = setup_rollout_fixture(codex_home.path(), &server.uri())?;
@@ -1973,6 +1974,33 @@ async fn thread_resume_without_overrides_does_not_change_updated_at_or_mtime() -
let after_modified = std::fs::metadata(&rollout.rollout_file_path)?.modified()?;
assert_eq!(after_modified, rollout.before_modified);
let unsubscribe_id = mcp
.send_thread_unsubscribe_request(ThreadUnsubscribeParams {
thread_id: thread_id.clone(),
})
.await?;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(unsubscribe_id)),
)
.await??;
let resume_id = mcp
.send_thread_resume_request(ThreadResumeParams {
thread_id: "not-a-valid-thread-id".to_string(),
path: Some(normalized_existing_path(&rollout.rollout_file_path)?),
cwd: Some(codex_home.path().to_string_lossy().to_string()),
..Default::default()
})
.await?;
let resume_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(resume_id)),
)
.await??;
let ThreadResumeResponse { cwd, .. } = to_response::<ThreadResumeResponse>(resume_resp)?;
assert_eq!(cwd, AbsolutePathBuf::from_absolute_path(codex_home.path())?);
let turn_id = mcp
.send_turn_start_request(TurnStartParams {
thread_id,
@@ -2291,9 +2319,10 @@ async fn thread_resume_rejects_mismatched_path_for_running_thread_id() -> Result
)
.await??;
let stale_path = rollout_path(codex_home.path(), "2025-01-01T00-00-00", &thread_id);
let stale_thread_id = Uuid::new_v4().to_string();
let stale_path = rollout_path(codex_home.path(), "2025-01-01T00-00-00", &stale_thread_id);
std::fs::create_dir_all(stale_path.parent().expect("stale path parent"))?;
let thread_uuid = Uuid::parse_str(&thread_id)?;
let thread_uuid = Uuid::parse_str(&stale_thread_id)?;
let mut stale_file = std::fs::File::create(&stale_path)?;
let stale_meta = json!({
"timestamp": "2025-01-01T00:00:00Z",
@@ -317,6 +317,7 @@ async fn thread_unsubscribe_preserves_cached_status_before_idle_unload() -> Resu
let resume_id = mcp
.send_thread_resume_request(ThreadResumeParams {
thread_id,
cwd: Some(codex_home.path().to_string_lossy().to_string()),
..Default::default()
})
.await?;
@@ -16,6 +16,8 @@ use app_test_support::write_mock_responses_config_toml_with_chatgpt_base_url;
use app_test_support::write_models_cache;
use codex_app_server::INPUT_TOO_LARGE_ERROR_CODE;
use codex_app_server::INVALID_PARAMS_ERROR_CODE;
use codex_app_server_protocol::AdditionalContextEntry;
use codex_app_server_protocol::AdditionalContextKind;
use codex_app_server_protocol::ByteRange;
use codex_app_server_protocol::ClientInfo;
use codex_app_server_protocol::CollabAgentStatus;
@@ -301,6 +303,82 @@ async fn turn_start_with_empty_input_runs_model_request() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn turn_start_additional_context_flows_to_model_input() -> Result<()> {
let responses = vec![create_final_assistant_message_sse_response("Done")?];
let server = create_mock_responses_server_sequence_unchecked(responses).await;
let codex_home = TempDir::new()?;
create_config_toml(
codex_home.path(),
&server.uri(),
"never",
&BTreeMap::default(),
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let thread_req = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("mock-model".to_string()),
..Default::default()
})
.await?;
let thread_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id,
input: vec![V2UserInput::Text {
text: "inspect tab".to_string(),
text_elements: Vec::new(),
}],
additional_context: Some(HashMap::from([(
"custom_source".to_string(),
AdditionalContextEntry {
value: "source value".to_string(),
kind: AdditionalContextKind::Untrusted,
},
)])),
..Default::default()
})
.await?;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
)
.await??;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
let requests = server
.received_requests()
.await
.context("failed to fetch received requests")?;
let request = requests
.iter()
.find(|request| request.url.path().ends_with("/responses"))
.context("expected model request")?;
let body = request
.body_json::<Value>()
.context("request body should be JSON")?;
assert!(
body.to_string()
.contains("<external_custom_source>source value</external_custom_source>")
);
Ok(())
}
#[tokio::test]
async fn turn_start_sends_originator_header() -> Result<()> {
let responses = vec![create_final_assistant_message_sse_response("Done")?];
@@ -2069,6 +2147,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
text_elements: Vec::new(),
}],
responsesapi_client_metadata: None,
additional_context: None,
cwd: Some(first_cwd.clone()),
runtime_workspace_roots: None,
approval_policy: Some(codex_app_server_protocol::AskForApproval::Never),
@@ -2111,6 +2190,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
text_elements: Vec::new(),
}],
responsesapi_client_metadata: None,
additional_context: None,
cwd: Some(second_cwd.clone()),
runtime_workspace_roots: None,
approval_policy: Some(codex_app_server_protocol::AskForApproval::Never),
@@ -1,5 +1,6 @@
#![cfg(unix)]
use anyhow::Context;
use anyhow::Result;
use app_test_support::McpProcess;
use app_test_support::create_mock_responses_server_sequence;
@@ -9,6 +10,8 @@ use app_test_support::to_response;
use app_test_support::write_mock_responses_config_toml_with_chatgpt_base_url;
use codex_app_server::INPUT_TOO_LARGE_ERROR_CODE;
use codex_app_server::INVALID_PARAMS_ERROR_CODE;
use codex_app_server_protocol::AdditionalContextEntry;
use codex_app_server_protocol::AdditionalContextKind;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCResponse;
@@ -21,6 +24,8 @@ use codex_app_server_protocol::TurnSteerParams;
use codex_app_server_protocol::TurnSteerResponse;
use codex_app_server_protocol::UserInput as V2UserInput;
use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS;
use serde_json::Value;
use std::collections::HashMap;
use tempfile::TempDir;
use tokio::time::timeout;
@@ -67,6 +72,7 @@ async fn turn_steer_requires_active_turn() -> Result<()> {
text_elements: Vec::new(),
}],
responsesapi_client_metadata: None,
additional_context: None,
expected_turn_id: "turn-does-not-exist".to_string(),
})
.await?;
@@ -176,6 +182,7 @@ async fn turn_steer_rejects_oversized_text_input() -> Result<()> {
text_elements: Vec::new(),
}],
responsesapi_client_metadata: None,
additional_context: None,
expected_turn_id: turn.id.clone(),
})
.await?;
@@ -284,6 +291,7 @@ async fn turn_steer_returns_active_turn_id() -> Result<()> {
text_elements: Vec::new(),
}],
responsesapi_client_metadata: None,
additional_context: None,
expected_turn_id: turn.id.clone(),
})
.await?;
@@ -312,3 +320,118 @@ async fn turn_steer_returns_active_turn_id() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn turn_steer_rejects_context_only_input_without_merging_context() -> Result<()> {
let tmp = TempDir::new()?;
let codex_home = tmp.path().join("codex_home");
std::fs::create_dir(&codex_home)?;
let working_directory = tmp.path().join("workdir");
std::fs::create_dir(&working_directory)?;
let server = create_mock_responses_server_sequence_unchecked(vec![
create_shell_command_sse_response(
vec!["sleep".to_string(), "1".to_string()],
Some(&working_directory),
Some(10_000),
"call_sleep",
)?,
app_test_support::create_final_assistant_message_sse_response("Done")?,
])
.await;
write_mock_responses_config_toml_with_chatgpt_base_url(
&codex_home,
&server.uri(),
&server.uri(),
)?;
mount_analytics_capture(&server, &codex_home).await?;
let mut mcp = McpProcess::new_without_managed_config(&codex_home).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let thread_req = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("mock-model".to_string()),
..Default::default()
})
.await?;
let thread_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "run sleep".to_string(),
text_elements: Vec::new(),
}],
cwd: Some(working_directory),
..Default::default()
})
.await?;
let turn_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
)
.await??;
let TurnStartResponse { turn } = to_response::<TurnStartResponse>(turn_resp)?;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/started"),
)
.await??;
let additional_context = Some(HashMap::from([(
"browser_info".to_string(),
AdditionalContextEntry {
value: "tab one".to_string(),
kind: AdditionalContextKind::Untrusted,
},
)]));
let steer_req = mcp
.send_turn_steer_request(TurnSteerParams {
thread_id: thread.id.clone(),
input: Vec::new(),
responsesapi_client_metadata: None,
additional_context,
expected_turn_id: turn.id,
})
.await?;
let steer_error: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(steer_req)),
)
.await??;
assert_eq!(steer_error.error.code, -32600);
assert_eq!(steer_error.error.message, "input must not be empty");
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
let requests = server
.received_requests()
.await
.context("failed to fetch received requests")?;
let response_requests = requests
.iter()
.filter(|request| request.url.path().ends_with("/responses"))
.collect::<Vec<_>>();
assert_eq!(response_requests.len(), 2);
let body = response_requests[1]
.body_json::<Value>()
.context("request body should be JSON")?;
assert!(
!body
.to_string()
.contains("<external_browser_info>tab one</external_browser_info>")
);
Ok(())
}
@@ -0,0 +1,220 @@
use std::path::Path;
use std::time::Duration;
use anyhow::Context;
use anyhow::Result;
use app_test_support::ChatGptAuthFixture;
use app_test_support::McpProcess;
use app_test_support::to_response;
use app_test_support::write_chatgpt_auth;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::UserInput as V2UserInput;
use codex_config::types::AuthCredentialsStoreMode;
use core_test_support::responses;
use pretty_assertions::assert_eq;
use serde_json::Value;
use serde_json::json;
use tempfile::TempDir;
use tokio::time::timeout;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;
// macOS and Windows Bazel CI can spend tens of seconds starting app-server
// subprocesses or processing test RPCs under load.
#[cfg(any(target_os = "macos", windows))]
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(60);
#[cfg(not(any(target_os = "macos", windows)))]
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10);
#[tokio::test]
async fn standalone_web_search_round_trips_encrypted_output() -> Result<()> {
let call_id = "web-run-1";
let server = responses::start_mock_server().await;
mount_search_response(&server).await;
let response_mock = responses::mount_sse_sequence(
&server,
vec![
responses::sse(vec![
responses::ev_response_created("resp-1"),
responses::ev_function_call_with_namespace(
call_id,
"web",
"run",
&json!({
"search_query": [{"q": "standalone web search"}],
})
.to_string(),
),
responses::ev_completed("resp-1"),
]),
responses::sse(vec![
responses::ev_assistant_message("msg-1", "Done"),
responses::ev_completed("resp-2"),
]),
],
)
.await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("access-chatgpt"),
AuthCredentialsStoreMode::File,
)?;
let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let thread_req = mcp
.send_thread_start_request(ThreadStartParams::default())
.await?;
let thread_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id,
input: vec![V2UserInput::Text {
text: "Search the web".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
})
.await?;
let turn_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(turn_req)),
)
.await??;
let _turn: TurnStartResponse = to_response::<TurnStartResponse>(turn_resp)?;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
let requests = response_mock.requests();
assert_eq!(requests.len(), 2);
let first_response = requests[0].body_json();
assert!(
requests[0].tool_by_name("web", "run").is_some(),
"web.run should be sent to the model"
);
assert!(
!has_hosted_web_search(&first_response),
"standalone web search should replace hosted web search"
);
let search_body = search_request_body(&server).await?;
assert_eq!(
search_body["commands"],
json!({
"search_query": [{"q": "standalone web search"}],
})
);
assert_eq!(
search_body["settings"]["allowed_callers"],
json!(["direct"])
);
assert_eq!(
search_body["input"]
.as_array()
.context("search input should be an array")?
.last(),
Some(&json!({
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "Search the web"}],
}))
);
assert_eq!(
requests[1].function_call_output(call_id),
json!({
"type": "function_call_output",
"call_id": call_id,
"output": [{
"type": "encrypted_content",
"encrypted_content": "ciphertext",
}],
})
);
Ok(())
}
async fn mount_search_response(server: &MockServer) {
Mock::given(method("POST"))
.and(path("/api/codex/alpha/search"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"encrypted_output": "ciphertext",
})))
.expect(1)
.mount(server)
.await;
}
fn has_hosted_web_search(body: &Value) -> bool {
body.get("tools")
.and_then(Value::as_array)
.is_some_and(|tools| {
tools
.iter()
.any(|tool| tool.get("type").and_then(Value::as_str) == Some("web_search"))
})
}
async fn search_request_body(server: &MockServer) -> Result<Value> {
server
.received_requests()
.await
.context("failed to fetch received requests")?
.into_iter()
.find(|request| request.url.path() == "/api/codex/alpha/search")
.context("expected standalone search request")?
.body_json()
.context("search request body should be JSON")
}
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> {
std::fs::write(
codex_home.join("config.toml"),
format!(
r#"
model = "mock-model"
approval_policy = "never"
sandbox_mode = "read-only"
model_provider = "openai-custom"
chatgpt_base_url = "{server_uri}"
[features]
standalone_web_search = true
[model_providers.openai-custom]
name = "OpenAI"
base_url = "{server_uri}/api/codex"
wire_api = "responses"
request_max_retries = 0
stream_max_retries = 0
supports_websockets = false
requires_openai_auth = true
"#
),
)
}
+32
View File
@@ -702,6 +702,7 @@ fn state_summary(check: &DoctorCheck) -> String {
"state DB integrity",
"log DB integrity",
"goals DB integrity",
"memories DB integrity",
]
.into_iter()
.all(|label| detail::detail_value(check, label).is_some_and(|value| value == "ok"));
@@ -1363,6 +1364,37 @@ Run codex doctor without --summary for detailed diagnostics.
);
}
#[test]
fn render_human_report_includes_memories_db_in_state_health_summary() {
let report = DoctorReport {
schema_version: 1,
generated_at: "0s since unix epoch".to_string(),
overall_status: CheckStatus::Ok,
codex_version: "0.0.0".to_string(),
checks: vec![
DoctorCheck::new(
"state.paths",
"state",
CheckStatus::Ok,
"state paths inspectable",
)
.detail("state DB: /tmp/state.sqlite")
.detail("state DB integrity: ok")
.detail("log DB: /tmp/logs.sqlite")
.detail("log DB integrity: ok")
.detail("goals DB: /tmp/goals.sqlite")
.detail("goals DB integrity: ok")
.detail("memories DB: /tmp/memories.sqlite")
.detail("memories DB integrity: ok"),
],
};
let rendered = render_human_report(&report, detailed_no_color_unicode_options());
assert!(rendered.contains("✓ state databases healthy"));
assert!(rendered.contains("memories DB /tmp/memories.sqlite · integrity ok"));
}
#[test]
fn render_human_report_supports_ascii_output() {
let rendered = render_human_report(
+3
View File
@@ -413,6 +413,7 @@ fn state_details(parsed: &[ParsedDetail]) -> Vec<HumanDetail> {
push_database_row(&mut out, parsed, "state DB");
push_database_row(&mut out, parsed, "log DB");
push_database_row(&mut out, parsed, "goals DB");
push_database_row(&mut out, parsed, "memories DB");
for (source, label) in [
("active rollout files", "active rollouts"),
@@ -440,6 +441,8 @@ fn state_details(parsed: &[ParsedDetail]) -> Vec<HumanDetail> {
"state DB integrity",
"log DB integrity",
"goals DB integrity",
"memories DB",
"memories DB integrity",
"active rollout files",
"archived rollout files",
],
+7 -13
View File
@@ -27,7 +27,7 @@ use codex_responses_api_proxy::Args as ResponsesApiProxyArgs;
use codex_rollout_trace::REDUCED_STATE_FILE_NAME;
use codex_rollout_trace::replay_bundle;
use codex_state::StateRuntime;
use codex_state::state_db_path;
use codex_state::memories_db_path;
use codex_tui::AppExitInfo;
use codex_tui::Cli as TuiCli;
use codex_tui::ExitReason;
@@ -1751,22 +1751,16 @@ async fn run_debug_clear_memories_command(
.build()
.await?;
let state_path = state_db_path(config.sqlite_home.as_path());
let mut cleared_state_db = false;
if tokio::fs::try_exists(&state_path).await? {
let state_db =
StateRuntime::init(config.sqlite_home.clone(), config.model_provider_id.clone())
.await?;
state_db.clear_memory_data().await?;
cleared_state_db = true;
}
let memories_path = memories_db_path(config.sqlite_home.as_path());
let cleared_memories_db =
StateRuntime::clear_memory_data_in_sqlite_home(config.sqlite_home.as_path()).await?;
clear_memory_roots_contents(&config.codex_home).await?;
let mut message = if cleared_state_db {
format!("Cleared memory state from {}.", state_path.display())
let mut message = if cleared_memories_db {
format!("Cleared memory state from {}.", memories_path.display())
} else {
format!("No state db found at {}.", state_path.display())
format!("No memories db found at {}.", memories_path.display())
};
message.push_str(&format!(
" Cleared memory directories under {}.",
+59 -3
View File
@@ -2,6 +2,7 @@ use std::path::Path;
use anyhow::Result;
use codex_state::StateRuntime;
use codex_state::memories_db_path;
use codex_state::state_db_path;
use predicates::str::contains;
use sqlx::SqlitePool;
@@ -23,6 +24,9 @@ async fn debug_clear_memories_resets_state_and_removes_memory_dir() -> Result<()
let thread_id = "00000000-0000-0000-0000-000000000123";
let db_path = state_db_path(codex_home.path());
let pool = SqlitePool::connect(&format!("sqlite://{}", db_path.display())).await?;
let memories_db_path = memories_db_path(codex_home.path());
let memories_pool =
SqlitePool::connect(&format!("sqlite://{}", memories_db_path.display())).await?;
sqlx::query(
r#"
@@ -74,7 +78,7 @@ INSERT INTO stage1_outputs (
"#,
)
.bind(thread_id)
.execute(&pool)
.execute(&memories_pool)
.await?;
sqlx::query(
@@ -99,13 +103,14 @@ INSERT INTO jobs (
"#,
)
.bind(thread_id)
.execute(&pool)
.execute(&memories_pool)
.await?;
let memory_root = codex_home.path().join("memories");
std::fs::create_dir_all(&memory_root)?;
std::fs::write(memory_root.join("memory_summary.md"), "stale memory")?;
pool.close().await;
memories_pool.close().await;
let mut cmd = codex_command(codex_home.path())?;
cmd.args(["debug", "clear-memories"])
@@ -113,7 +118,7 @@ INSERT INTO jobs (
.success()
.stdout(contains("Cleared memory state"));
let pool = SqlitePool::connect(&format!("sqlite://{}", db_path.display())).await?;
let pool = SqlitePool::connect(&format!("sqlite://{}", memories_db_path.display())).await?;
let stage1_outputs_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM stage1_outputs")
.fetch_one(&pool)
.await?;
@@ -131,3 +136,54 @@ INSERT INTO jobs (
Ok(())
}
#[tokio::test]
async fn debug_clear_memories_resets_memories_db_without_state_db() -> Result<()> {
let codex_home = TempDir::new()?;
let runtime =
StateRuntime::init(codex_home.path().to_path_buf(), "test-provider".to_string()).await?;
drop(runtime);
let db_path = state_db_path(codex_home.path());
let memories_db_path = memories_db_path(codex_home.path());
let memories_pool =
SqlitePool::connect(&format!("sqlite://{}", memories_db_path.display())).await?;
sqlx::query(
r#"
INSERT INTO stage1_outputs (
thread_id,
source_updated_at,
raw_memory,
rollout_summary,
generated_at,
rollout_slug,
usage_count,
last_usage,
selected_for_phase2,
selected_for_phase2_source_updated_at
) VALUES ('00000000-0000-0000-0000-000000000123', 1, 'raw', 'summary', 1, NULL, 0, NULL, 0, NULL)
"#,
)
.execute(&memories_pool)
.await?;
memories_pool.close().await;
std::fs::remove_file(&db_path)?;
let mut cmd = codex_command(codex_home.path())?;
cmd.args(["debug", "clear-memories"])
.assert()
.success()
.stdout(contains("Cleared memory state"));
let pool = SqlitePool::connect(&format!("sqlite://{}", memories_db_path.display())).await?;
let stage1_outputs_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM stage1_outputs")
.fetch_one(&pool)
.await?;
assert_eq!(stage1_outputs_count, 0);
pool.close().await;
assert!(!db_path.exists());
Ok(())
}
+1
View File
@@ -16,6 +16,7 @@ codex-utils-rustls-provider = { workspace = true }
futures = { workspace = true }
http = { workspace = true }
reqwest = { workspace = true, features = ["json", "stream"] }
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
+54 -15
View File
@@ -1,5 +1,6 @@
use crate::common::Reasoning;
use codex_protocol::models::ResponseItem;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
@@ -27,75 +28,100 @@ pub enum SearchInput {
Items(Vec<ResponseItem>),
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, JsonSchema)]
pub struct SearchCommands {
/// Query the internet search engine for a given list of queries.
#[serde(skip_serializing_if = "Option::is_none")]
pub search_query: Option<Vec<SearchQuery>>,
/// Query the image search engine for a given list of queries.
#[serde(skip_serializing_if = "Option::is_none")]
pub image_query: Option<Vec<SearchQuery>>,
/// Open pages by reference id or URL.
#[serde(skip_serializing_if = "Option::is_none")]
pub open: Option<Vec<OpenOperation>>,
/// Open links from previously opened pages.
#[serde(skip_serializing_if = "Option::is_none")]
pub click: Option<Vec<ClickOperation>>,
/// Find text patterns in pages.
#[serde(skip_serializing_if = "Option::is_none")]
pub find: Option<Vec<FindOperation>>,
/// Take screenshots of PDF pages.
#[serde(skip_serializing_if = "Option::is_none")]
pub screenshot: Option<Vec<ScreenshotOperation>>,
/// Look up prices for the given stock symbols.
#[serde(skip_serializing_if = "Option::is_none")]
pub finance: Option<Vec<FinanceOperation>>,
/// Look up weather forecasts.
#[serde(skip_serializing_if = "Option::is_none")]
pub weather: Option<Vec<WeatherOperation>>,
/// Look up sports schedules and standings.
#[serde(skip_serializing_if = "Option::is_none")]
pub sports: Option<Vec<SportsOperation>>,
/// Get time for the given UTC offsets.
#[serde(skip_serializing_if = "Option::is_none")]
pub time: Option<Vec<TimeOperation>>,
/// Set the length of the response to be returned.
#[serde(skip_serializing_if = "Option::is_none")]
pub response_length: Option<SearchResponseLength>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct SearchQuery {
/// Search query.
pub q: String,
/// Whether to filter by recency, as a number of recent days.
#[serde(skip_serializing_if = "Option::is_none")]
pub recency: Option<u64>,
/// Whether to filter by a specific list of domains.
#[serde(skip_serializing_if = "Option::is_none")]
pub domains: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct OpenOperation {
/// Reference id or URL to open.
pub ref_id: String,
/// Line number to position the page at.
#[serde(skip_serializing_if = "Option::is_none")]
pub lineno: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct ClickOperation {
/// Reference id containing the numbered link.
pub ref_id: String,
/// Numbered link id to open.
pub id: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct FindOperation {
/// Reference id or URL to search within.
pub ref_id: String,
/// Text pattern to find.
pub pattern: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct ScreenshotOperation {
/// Reference id or URL to screenshot.
pub ref_id: String,
/// Zero-indexed PDF page number.
pub pageno: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct FinanceOperation {
/// Ticker symbol to look up.
pub ticker: String,
/// Asset type to look up.
pub r#type: FinanceAssetType,
/// ISO 3166-1 alpha-3 country code, "OTC", or "" for cryptocurrency.
#[serde(skip_serializing_if = "Option::is_none")]
pub market: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum FinanceAssetType {
Equity,
@@ -104,49 +130,61 @@ pub enum FinanceAssetType {
Index,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct WeatherOperation {
/// Location in "Country, Area, City" format.
pub location: String,
/// Start date in YYYY-MM-DD format. Defaults to today.
#[serde(skip_serializing_if = "Option::is_none")]
pub start: Option<String>,
/// Number of days to return. Defaults to 7.
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct SportsOperation {
/// Tool name for sports requests.
#[serde(skip_serializing_if = "Option::is_none")]
pub tool: Option<SportsToolName>,
/// Sports function to call.
pub r#fn: SportsFunction,
/// League to look up.
pub league: SportsLeague,
/// Team to look up, using the common 3 or 4 letter alias used in broadcasts.
#[serde(skip_serializing_if = "Option::is_none")]
pub team: Option<String>,
/// Opponent to use with `team` when narrowing the lookup.
#[serde(skip_serializing_if = "Option::is_none")]
pub opponent: Option<String>,
/// Start date in YYYY-MM-DD format.
#[serde(skip_serializing_if = "Option::is_none")]
pub date_from: Option<String>,
/// End date in YYYY-MM-DD format.
#[serde(skip_serializing_if = "Option::is_none")]
pub date_to: Option<String>,
/// Number of games to return.
#[serde(skip_serializing_if = "Option::is_none")]
pub num_games: Option<u64>,
/// Locale for the lookup.
#[serde(skip_serializing_if = "Option::is_none")]
pub locale: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum SportsToolName {
Sports,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum SportsFunction {
Schedule,
Standings,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum SportsLeague {
Nba,
@@ -160,12 +198,13 @@ pub enum SportsLeague {
Ipl,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct TimeOperation {
/// UTC offset formatted like "+03:00".
pub utc_offset: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum SearchResponseLength {
Short,
@@ -6,6 +6,7 @@ use self::activation::installed_marketplace_metadata_matches;
use self::activation::write_installed_marketplace_metadata;
use self::git::clone_git_source;
use self::git::git_remote_revision;
use crate::marketplace::find_marketplace_manifest_path;
use crate::marketplace::validate_marketplace_root;
use codex_config::CONFIG_TOML_FILE;
use codex_config::ConfigLayerStack;
@@ -176,9 +177,7 @@ fn upgrade_configured_git_marketplace(
MARKETPLACE_UPGRADE_GIT_TIMEOUT,
)?;
let destination = install_root.join(&marketplace.name);
if destination
.join(".agents/plugins/marketplace.json")
.is_file()
if find_marketplace_manifest_path(&destination).is_some()
&& marketplace.last_revision.as_deref() == Some(remote_revision.as_str())
&& installed_marketplace_metadata_matches(&destination, marketplace, &remote_revision)
{
+6
View File
@@ -575,6 +575,9 @@
"sqlite": {
"type": "boolean"
},
"standalone_web_search": {
"type": "boolean"
},
"steer": {
"type": "boolean"
},
@@ -4505,6 +4508,9 @@
"sqlite": {
"type": "boolean"
},
"standalone_web_search": {
"type": "boolean"
},
"steer": {
"type": "boolean"
},
+3
View File
@@ -262,10 +262,12 @@ impl AgentControl {
.await?
}
(Some(session_source), None) => {
let forked_from_thread_id = thread_spawn_parent_thread_id(&session_source);
Box::pin(state.spawn_new_thread_with_source(
config.clone(),
self.clone(),
session_source,
forked_from_thread_id,
/*thread_source*/ Some(ThreadSource::Subagent),
/*persist_extended_history*/ false,
/*metrics_service_name*/ None,
@@ -487,6 +489,7 @@ impl AgentControl {
self.clone(),
session_source,
/*thread_source*/ Some(ThreadSource::Subagent),
/*forked_from_thread_id*/ Some(parent_thread_id),
/*persist_extended_history*/ false,
inherited_shell_snapshot,
inherited_exec_policy,
+3
View File
@@ -444,6 +444,7 @@ async fn send_input_submits_user_message() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
},
);
@@ -598,6 +599,7 @@ async fn spawn_agent_creates_thread_and_sends_prompt() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
},
);
@@ -770,6 +772,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
},
);
+2
View File
@@ -86,6 +86,7 @@ pub(crate) async fn run_codex_thread_interactive(
extensions: Arc::clone(&parent_session.services.extensions),
conversation_history: initial_history.unwrap_or(InitialHistory::New),
session_source: SessionSource::SubAgent(subagent_source.clone()),
forked_from_thread_id: Some(parent_session.conversation_id),
thread_source: Some(ThreadSource::Subagent),
agent_control: parent_session.services.agent_control.clone(),
dynamic_tools: Vec::new(),
@@ -192,6 +193,7 @@ pub(crate) async fn run_codex_thread_one_shot(
items: input,
final_output_json_schema,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
+9 -1
View File
@@ -21,6 +21,7 @@ use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AdditionalContextEntry;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::Op;
@@ -41,6 +42,7 @@ use codex_thread_store::ThreadStoreError;
use codex_thread_store::ThreadStoreResult;
use codex_utils_absolute_path::AbsolutePathBuf;
use rmcp::model::ReadResourceRequestParams;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
@@ -236,11 +238,17 @@ impl CodexThread {
pub async fn steer_input(
&self,
input: Vec<UserInput>,
additional_context: BTreeMap<String, AdditionalContextEntry>,
expected_turn_id: Option<&str>,
responsesapi_client_metadata: Option<HashMap<String, String>>,
) -> Result<String, SteerInputError> {
self.codex
.steer_input(input, expected_turn_id, responsesapi_client_metadata)
.steer_input(
input,
additional_context,
expected_turn_id,
responsesapi_client_metadata,
)
.await
}
@@ -2,6 +2,7 @@ use codex_protocol::items::HookPromptItem;
use codex_protocol::items::parse_hook_prompt_fragment;
use codex_protocol::models::ContentItem;
use super::AdditionalContextUserFragment;
use super::EnvironmentContext;
use super::FragmentRegistration;
use super::FragmentRegistrationProxy;
@@ -19,6 +20,8 @@ static USER_INSTRUCTIONS_REGISTRATION: FragmentRegistrationProxy<UserInstruction
FragmentRegistrationProxy::new();
static ENVIRONMENT_CONTEXT_REGISTRATION: FragmentRegistrationProxy<EnvironmentContext> =
FragmentRegistrationProxy::new();
static ADDITIONAL_CONTEXT_REGISTRATION: FragmentRegistrationProxy<AdditionalContextUserFragment> =
FragmentRegistrationProxy::new();
static SKILL_INSTRUCTIONS_REGISTRATION: FragmentRegistrationProxy<SkillInstructions> =
FragmentRegistrationProxy::new();
static USER_SHELL_COMMAND_REGISTRATION: FragmentRegistrationProxy<UserShellCommand> =
@@ -42,6 +45,7 @@ static LEGACY_MODEL_MISMATCH_WARNING_REGISTRATION: FragmentRegistrationProxy<
static CONTEXTUAL_USER_FRAGMENTS: &[&dyn FragmentRegistration] = &[
&USER_INSTRUCTIONS_REGISTRATION,
&ENVIRONMENT_CONTEXT_REGISTRATION,
&ADDITIONAL_CONTEXT_REGISTRATION,
&SKILL_INSTRUCTIONS_REGISTRATION,
&USER_SHELL_COMMAND_REGISTRATION,
&TURN_ABORTED_REGISTRATION,
+14
View File
@@ -1,4 +1,5 @@
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use std::marker::PhantomData;
@@ -81,6 +82,19 @@ pub trait ContextualUserFragment {
phase: None,
}
}
fn into_response_input_item(self) -> ResponseInputItem
where
Self: Sized,
{
ResponseInputItem::Message {
role: Self::role().to_string(),
content: vec![ContentItem::InputText {
text: self.render(),
}],
phase: None,
}
}
}
fn matches_marked_text(start_marker: &str, end_marker: &str, text: &str) -> bool {
+92
View File
@@ -0,0 +1,92 @@
use super::ContextualUserFragment;
use codex_utils_string::truncate_middle_with_token_budget;
const MAX_ADDITIONAL_CONTEXT_VALUE_TOKENS: usize = 1_000;
const ADDITIONAL_CONTEXT_END_MARKER_SUFFIX: &str = ">";
const ADDITIONAL_CONTEXT_START_MARKER_PREFIX: &str = "<external_";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AdditionalContextUserFragment {
key: String,
value: String,
}
impl AdditionalContextUserFragment {
pub(crate) fn new(key: String, value: String) -> Self {
Self { key, value }
}
}
impl ContextualUserFragment for AdditionalContextUserFragment {
fn role() -> &'static str {
"user"
}
fn markers(&self) -> (&'static str, &'static str) {
Self::type_markers()
}
fn type_markers() -> (&'static str, &'static str) {
(
ADDITIONAL_CONTEXT_START_MARKER_PREFIX,
ADDITIONAL_CONTEXT_END_MARKER_SUFFIX,
)
}
fn matches_text(text: &str) -> bool {
let trimmed = text.trim();
let Some(rest) = trimmed.strip_prefix(ADDITIONAL_CONTEXT_START_MARKER_PREFIX) else {
return false;
};
let Some((key, value_and_close)) = rest.split_once(ADDITIONAL_CONTEXT_END_MARKER_SUFFIX)
else {
return false;
};
value_and_close.ends_with(&format!("</external_{key}>"))
}
fn body(&self) -> String {
additional_context_body(&self.key, &self.value)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AdditionalContextDeveloperFragment {
key: String,
value: String,
}
impl AdditionalContextDeveloperFragment {
pub(crate) fn new(key: String, value: String) -> Self {
Self { key, value }
}
}
impl ContextualUserFragment for AdditionalContextDeveloperFragment {
fn role() -> &'static str {
"developer"
}
fn markers(&self) -> (&'static str, &'static str) {
Self::type_markers()
}
fn type_markers() -> (&'static str, &'static str) {
("", "")
}
fn body(&self) -> String {
additional_context_developer_body(&self.key, &self.value)
}
}
fn additional_context_body(key: &str, value: &str) -> String {
let value = truncate_middle_with_token_budget(value, MAX_ADDITIONAL_CONTEXT_VALUE_TOKENS).0;
format!("{key}>{value}</external_{key}")
}
fn additional_context_developer_body(key: &str, value: &str) -> String {
let value = truncate_middle_with_token_budget(value, MAX_ADDITIONAL_CONTEXT_VALUE_TOKENS).0;
format!("<{key}>{value}</{key}>")
}
-13
View File
@@ -1,8 +1,6 @@
//! Hidden user-context fragment for runtime-owned goal steering prompts.
use super::ContextualUserFragment;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseInputItem;
/// Hidden runtime-owned goal steering context injected into model input.
#[derive(Debug, Clone, PartialEq)]
@@ -17,17 +15,6 @@ impl GoalContext {
prompt: prompt.into(),
}
}
/// Converts the registered fragment into an active-turn injectable item.
pub fn into_response_input_item(self) -> ResponseInputItem {
ResponseInputItem::Message {
role: <Self as ContextualUserFragment>::role().to_string(),
content: vec![ContentItem::InputText {
text: self.render(),
}],
phase: None,
}
}
}
impl ContextualUserFragment for GoalContext {
+3
View File
@@ -8,6 +8,7 @@ mod collaboration_mode_instructions;
mod contextual_user_message;
mod environment_context;
mod fragment;
mod fragments;
mod goal_context;
mod guardian_followup_review_reminder;
mod hook_additional_context;
@@ -40,6 +41,8 @@ pub(crate) use environment_context::EnvironmentContext;
pub use fragment::ContextualUserFragment;
pub(crate) use fragment::FragmentRegistration;
pub(crate) use fragment::FragmentRegistrationProxy;
pub(crate) use fragments::AdditionalContextDeveloperFragment;
pub(crate) use fragments::AdditionalContextUserFragment;
pub use goal_context::GoalContext;
pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder;
pub(crate) use hook_additional_context::HookAdditionalContext;
+1
View File
@@ -5,6 +5,7 @@
//! events, and owns helper hooks used by goal lifecycle behavior.
use crate::StateDbHandle;
use crate::context::ContextualUserFragment;
use crate::context::GoalContext;
use crate::session::TurnInput;
use crate::session::session::Session;
@@ -714,6 +714,7 @@ async fn run_review_on_session(
environments: None,
final_output_json_schema: Some(params.schema.clone()),
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
#[allow(deprecated)]
cwd: Some(params.parent_turn.cwd.to_path_buf()),
+15 -1
View File
@@ -10,6 +10,7 @@ use tracing::debug_span;
use tracing::info_span;
use crate::session::SteerInputError;
use crate::session::TurnInput;
use crate::session::session::Session;
use crate::session::session::SessionSettingsUpdate;
@@ -194,6 +195,7 @@ pub(super) async fn user_input_or_turn_inner(
environments,
final_output_json_schema,
responsesapi_client_metadata,
additional_context,
thread_settings,
} = op
else {
@@ -224,6 +226,7 @@ pub(super) async fn user_input_or_turn_inner(
let accepted_items = match sess
.steer_input(
items.clone(),
additional_context.clone(),
/*expected_turn_id*/ None,
responsesapi_client_metadata.clone(),
)
@@ -254,9 +257,20 @@ pub(super) async fn user_input_or_turn_inner(
)
.await;
let accepted_items = items.clone();
let additional_context_input = {
let mut state = sess.state.lock().await;
state.additional_context.merge(additional_context)
};
let mut task_input = additional_context_input
.into_iter()
.map(TurnInput::ResponseInputItem)
.collect::<Vec<_>>();
if !items.is_empty() {
task_input.push(TurnInput::UserInput(items));
}
sess.spawn_task(
Arc::clone(&current_context),
items,
task_input,
crate::tasks::RegularTask::new(),
)
.await;
+4 -4
View File
@@ -166,16 +166,16 @@ impl InputQueue {
.accept_mailbox_delivery_for_current_turn();
}
pub(super) async fn push_pending_input_and_accept_mailbox_delivery_for_turn_state(
pub(super) async fn extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
&self,
turn_state: &Mutex<TurnState>,
input: TurnInput,
) -> Result<(), TurnInput> {
input: Vec<TurnInput>,
) -> Result<(), Vec<TurnInput>> {
let mut turn_state = turn_state.lock().await;
if turn_state.is_terminal() {
return Err(input);
}
turn_state.pending_input.items.push(input);
turn_state.pending_input.items.extend(input);
turn_state.accept_mailbox_delivery_for_current_turn();
Ok(())
}
+31 -11
View File
@@ -1,3 +1,4 @@
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fmt::Debug;
@@ -97,6 +98,7 @@ use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AdditionalContextEntry;
use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::HasLegacyEvent;
use codex_protocol::protocol::InterAgentCommunication;
@@ -398,6 +400,7 @@ pub(crate) struct CodexSpawnArgs {
pub(crate) extensions: Arc<codex_extension_api::ExtensionRegistry<crate::config::Config>>,
pub(crate) conversation_history: InitialHistory,
pub(crate) session_source: SessionSource,
pub(crate) forked_from_thread_id: Option<ThreadId>,
pub(crate) thread_source: Option<ThreadSource>,
pub(crate) agent_control: AgentControl,
pub(crate) dynamic_tools: Vec<DynamicToolSpec>,
@@ -462,6 +465,7 @@ impl Codex {
extensions,
conversation_history,
session_source,
forked_from_thread_id,
thread_source,
agent_control,
dynamic_tools,
@@ -617,6 +621,7 @@ impl Codex {
app_server_client_name: None,
app_server_client_version: None,
session_source,
forked_from_thread_id,
thread_source,
dynamic_tools,
persist_extended_history,
@@ -742,11 +747,17 @@ impl Codex {
pub async fn steer_input(
&self,
input: Vec<UserInput>,
additional_context: BTreeMap<String, AdditionalContextEntry>,
expected_turn_id: Option<&str>,
responsesapi_client_metadata: Option<HashMap<String, String>>,
) -> Result<String, SteerInputError> {
self.session
.steer_input(input, expected_turn_id, responsesapi_client_metadata)
.steer_input(
input,
additional_context,
expected_turn_id,
responsesapi_client_metadata,
)
.await
}
@@ -1086,6 +1097,7 @@ impl Session {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
},
/*mirror_user_text_to_realtime*/ None,
@@ -3152,6 +3164,7 @@ impl Session {
pub async fn steer_input(
&self,
input: Vec<UserInput>,
additional_context: BTreeMap<String, AdditionalContextEntry>,
expected_turn_id: Option<&str>,
responsesapi_client_metadata: Option<HashMap<String, String>>,
) -> Result<String, SteerInputError> {
@@ -3192,16 +3205,10 @@ impl Session {
return Err(SteerInputError::EmptyInput);
}
self.input_queue
.push_pending_input_and_accept_mailbox_delivery_for_turn_state(
active_turn.turn_state.as_ref(),
TurnInput::UserInput(input),
)
.await
.map_err(|input| match input {
TurnInput::UserInput(input) => SteerInputError::NoActiveTurn(input),
TurnInput::ResponseInputItem(_) => unreachable!("steer input must be user input"),
})?;
let additional_context_input = {
let mut state = self.state.lock().await;
state.additional_context.merge(additional_context)
};
if let Some(responsesapi_client_metadata) = responsesapi_client_metadata {
active_task
@@ -3210,6 +3217,19 @@ impl Session {
.set_responsesapi_client_metadata(responsesapi_client_metadata);
}
let mut pending_input = additional_context_input
.into_iter()
.map(TurnInput::ResponseInputItem)
.collect::<Vec<_>>();
let rejected_input = input.clone();
pending_input.push(TurnInput::UserInput(input));
self.input_queue
.extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
active_turn.turn_state.as_ref(),
pending_input,
)
.await
.map_err(|_| SteerInputError::NoActiveTurn(rejected_input))?;
Ok(active_turn_id.clone())
}
+7 -2
View File
@@ -69,12 +69,17 @@ pub(super) async fn spawn_review_thread(
.model_reasoning_summary
.unwrap_or(model_info.default_reasoning_summary);
let session_source = parent_turn_context.session_source.clone();
let forked_from_thread_id = {
let state = sess.state.lock().await;
state.session_configuration.forked_from_thread_id
};
let per_turn_config = Arc::new(per_turn_config);
let review_turn_id = sub_id.to_string();
let turn_metadata_state = Arc::new(TurnMetadataState::new(
sess.session_id().to_string(),
sess.thread_id().to_string(),
forked_from_thread_id,
parent_turn_context.thread_source,
review_turn_id.clone(),
#[allow(deprecated)]
@@ -132,11 +137,11 @@ pub(super) async fn spawn_review_thread(
};
// Seed the child task with the review prompt as the initial user message.
let input: Vec<UserInput> = vec![UserInput::Text {
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
text: review_prompt,
// Review prompt is synthesized; no UI element ranges to preserve.
text_elements: Vec::new(),
}];
}])];
let tc = Arc::new(review_turn_context);
tc.turn_metadata_state.spawn_git_enrichment_task();
// TODO(ccunningham): Review turns currently rely on `spawn_task` for TurnComplete but do not
+6 -1
View File
@@ -95,6 +95,8 @@ pub(crate) struct SessionConfiguration {
pub(super) app_server_client_version: Option<String>,
/// Source of the session (cli, vscode, exec, mcp, ...)
pub(super) session_source: SessionSource,
/// Immediate history source copied into this thread, when this thread was forked.
pub(super) forked_from_thread_id: Option<ThreadId>,
/// Optional analytics source classification for this thread.
pub(super) thread_source: Option<ThreadSource>,
pub(super) dynamic_tools: Vec<DynamicToolSpec>,
@@ -505,7 +507,10 @@ impl Session {
session_configuration.collaboration_mode.model(),
session_configuration.provider
);
let forked_from_id = initial_history.forked_from_id();
let forked_from_id = session_configuration
.forked_from_thread_id
.or_else(|| initial_history.forked_from_id());
session_configuration.forked_from_thread_id = forked_from_id;
let event_persistence_mode = if session_configuration.persist_extended_history {
ThreadEventPersistenceMode::Extended
+51 -25
View File
@@ -2287,6 +2287,7 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2333,6 +2334,7 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::Never),
collaboration_mode: Some(collaboration_mode),
@@ -2999,6 +3001,7 @@ async fn set_rate_limits_retains_previous_credits() {
app_server_client_name: None,
app_server_client_version: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
thread_source: None,
dynamic_tools: Vec::new(),
persist_extended_history: false,
@@ -3103,6 +3106,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
app_server_client_name: None,
app_server_client_version: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
thread_source: None,
dynamic_tools: Vec::new(),
persist_extended_history: false,
@@ -3630,6 +3634,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
app_server_client_name: None,
app_server_client_version: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
thread_source: None,
dynamic_tools: Vec::new(),
persist_extended_history: false,
@@ -4373,6 +4378,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
app_server_client_name: None,
app_server_client_version: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
thread_source: None,
dynamic_tools: Vec::new(),
persist_extended_history: false,
@@ -4482,6 +4488,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
app_server_client_name: None,
app_server_client_version: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
thread_source: None,
dynamic_tools: Vec::new(),
persist_extended_history: false,
@@ -4716,6 +4723,7 @@ async fn make_session_with_config_and_rx(
app_server_client_name: None,
app_server_client_version: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
thread_source: None,
dynamic_tools: Vec::new(),
persist_extended_history: false,
@@ -4819,6 +4827,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
app_server_client_name: None,
app_server_client_version: None,
session_source: session_source.clone(),
forked_from_thread_id: None,
thread_source: None,
dynamic_tools: Vec::new(),
persist_extended_history: false,
@@ -5430,6 +5439,7 @@ fn op_kind_for_input_and_context_ops() {
items: vec![],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
}
.kind(),
@@ -5460,6 +5470,7 @@ async fn user_turn_updates_approvals_reviewer() {
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(config.cwd.to_path_buf()),
approval_policy: Some(config.permissions.approval_policy.value()),
@@ -5781,10 +5792,10 @@ async fn spawn_task_turn_span_inherits_dispatch_trace_context() {
async {
sess.spawn_task(
Arc::clone(&tc),
vec![UserInput::Text {
vec![TurnInput::UserInput(vec![UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
}],
}])],
TraceCaptureTask {
captured_trace: Arc::clone(&captured_trace),
},
@@ -6312,6 +6323,7 @@ where
app_server_client_name: None,
app_server_client_version: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
thread_source: None,
dynamic_tools,
persist_extended_history: false,
@@ -6592,10 +6604,10 @@ async fn spawn_task_does_not_update_previous_turn_settings_for_non_run_turn_task
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
sess.set_previous_turn_settings(/*previous_turn_settings*/ None)
.await;
let input = vec![UserInput::Text {
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
}];
}])];
sess.spawn_task(
Arc::clone(&tc),
@@ -7861,10 +7873,10 @@ impl SessionTask for GuardianDeniedApprovalTask {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn guardian_auto_review_interrupts_after_three_consecutive_denials() {
let (sess, tc, rx) = make_session_and_context_with_rx().await;
let input = vec![UserInput::Text {
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
text: "trigger guardian denials".to_string(),
text_elements: Vec::new(),
}];
}])];
sess.spawn_task(Arc::clone(&tc), input, GuardianDeniedApprovalTask)
.await;
@@ -7892,10 +7904,10 @@ async fn guardian_auto_review_interrupts_after_three_consecutive_denials() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn guardian_helper_review_interrupts_after_three_consecutive_denials() {
let (sess, tc, rx) = make_session_and_context_with_rx().await;
let input = vec![UserInput::Text {
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
text: "keep turn active for helper reviews".to_string(),
text_elements: Vec::new(),
}];
}])];
sess.spawn_task(
Arc::clone(&tc),
input,
@@ -7952,10 +7964,10 @@ async fn guardian_helper_review_interrupts_after_three_consecutive_denials() {
#[test_log::test]
async fn abort_regular_task_emits_turn_aborted_only() {
let (sess, tc, rx) = make_session_and_context_with_rx().await;
let input = vec![UserInput::Text {
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
}];
}])];
sess.spawn_task(
Arc::clone(&tc),
input,
@@ -7985,10 +7997,10 @@ async fn abort_regular_task_emits_turn_aborted_only() {
#[tokio::test]
async fn abort_gracefully_emits_turn_aborted_only() {
let (sess, tc, rx) = make_session_and_context_with_rx().await;
let input = vec![UserInput::Text {
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
}];
}])];
sess.spawn_task(
Arc::clone(&tc),
input,
@@ -8018,10 +8030,10 @@ async fn abort_gracefully_emits_turn_aborted_only() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input() {
let (sess, tc, rx) = make_session_and_context_with_rx().await;
let input = vec![UserInput::Text {
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
}];
}])];
sess.spawn_task(
Arc::clone(&tc),
input,
@@ -8044,6 +8056,7 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
}];
sess.steer_input(
pending_user_input.clone(),
/*additional_context*/ Default::default(),
Some(&tc.sub_id),
/*responsesapi_client_metadata*/ None,
)
@@ -8140,7 +8153,10 @@ async fn steer_input_requires_active_turn() {
let err = sess
.steer_input(
input, /*expected_turn_id*/ None, /*responsesapi_client_metadata*/ None,
input,
/*additional_context*/ Default::default(),
/*expected_turn_id*/ None,
/*responsesapi_client_metadata*/ None,
)
.await
.expect_err("steering without active turn should fail");
@@ -8151,10 +8167,10 @@ async fn steer_input_requires_active_turn() {
#[tokio::test]
async fn terminal_active_turn_rejects_new_pending_input() {
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
let input = vec![UserInput::Text {
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
}];
}])];
sess.spawn_task(
Arc::clone(&tc),
input,
@@ -8174,6 +8190,7 @@ async fn terminal_active_turn_rejects_new_pending_input() {
}];
sess.steer_input(
pending_user_input.clone(),
/*additional_context*/ Default::default(),
Some(&tc.sub_id),
/*responsesapi_client_metadata*/ None,
)
@@ -8209,6 +8226,7 @@ async fn terminal_active_turn_rejects_new_pending_input() {
let err = sess
.steer_input(
steer_input,
/*additional_context*/ Default::default(),
Some(&tc.sub_id),
/*responsesapi_client_metadata*/ None,
)
@@ -8237,10 +8255,10 @@ async fn terminal_active_turn_rejects_new_pending_input() {
#[tokio::test]
async fn steer_input_enforces_expected_turn_id() {
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
let input = vec![UserInput::Text {
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
}];
}])];
sess.spawn_task(
Arc::clone(&tc),
input,
@@ -8258,6 +8276,7 @@ async fn steer_input_enforces_expected_turn_id() {
let err = sess
.steer_input(
steer_input,
/*additional_context*/ Default::default(),
Some("different-turn-id"),
/*responsesapi_client_metadata*/ None,
)
@@ -8282,10 +8301,10 @@ async fn steer_input_rejects_non_regular_turns() {
(TaskKind::Compact, NonSteerableTurnKind::Compact),
] {
let (sess, _tc, _rx) = make_session_and_context_with_rx().await;
let input = vec![UserInput::Text {
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
}];
}])];
let turn_context = sess.new_default_turn_with_sub_id("turn".to_string()).await;
sess.spawn_task(
turn_context,
@@ -8304,6 +8323,7 @@ async fn steer_input_rejects_non_regular_turns() {
let err = sess
.steer_input(
steer_input,
/*additional_context*/ Default::default(),
/*expected_turn_id*/ None,
/*responsesapi_client_metadata*/ None,
)
@@ -8319,10 +8339,10 @@ async fn steer_input_rejects_non_regular_turns() {
#[tokio::test]
async fn steer_input_returns_active_turn_id() {
let (sess, tc, _rx) = make_session_and_context_with_rx().await;
let input = vec![UserInput::Text {
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
text: "hello".to_string(),
text_elements: Vec::new(),
}];
}])];
sess.spawn_task(
Arc::clone(&tc),
input,
@@ -8340,6 +8360,7 @@ async fn steer_input_returns_active_turn_id() {
let turn_id = sess
.steer_input(
steer_input,
/*additional_context*/ Default::default(),
Some(&tc.sub_id),
/*responsesapi_client_metadata*/ None,
)
@@ -8564,6 +8585,7 @@ async fn active_goal_continuation_runs_again_after_no_tool_turn() -> anyhow::Res
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -8669,6 +8691,7 @@ async fn pending_request_user_input_does_not_spawn_extra_goal_continuation() ->
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -9216,6 +9239,7 @@ async fn completed_goal_accounts_current_turn_tokens_before_tool_response() -> a
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -9382,6 +9406,7 @@ async fn steered_input_reopens_mailbox_delivery_for_current_turn() {
text: "follow up".to_string(),
text_elements: Vec::new(),
}],
/*additional_context*/ Default::default(),
Some(&tc.sub_id),
/*responsesapi_client_metadata*/ None,
)
@@ -9431,6 +9456,7 @@ async fn stale_defer_mailbox_delivery_does_not_override_steered_input() {
text: "follow up".to_string(),
text_elements: Vec::new(),
}],
/*additional_context*/ Default::default(),
Some(&tc.sub_id),
/*responsesapi_client_metadata*/ None,
)
@@ -9512,10 +9538,10 @@ async fn tool_calls_reopen_mailbox_delivery_for_current_turn() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn abort_review_task_emits_exited_then_aborted_and_records_history() {
let (sess, tc, rx) = make_session_and_context_with_rx().await;
let input = vec![UserInput::Text {
let input = vec![TurnInput::UserInput(vec![UserInput::Text {
text: "start review".to_string(),
text_elements: Vec::new(),
}];
}])];
sess.spawn_task(Arc::clone(&tc), input, ReviewTask::new())
.await;
@@ -692,6 +692,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
session_source: SessionSource::SubAgent(SubAgentSource::Other(
GUARDIAN_REVIEWER_NAME.to_string(),
)),
forked_from_thread_id: None,
thread_source: None,
agent_control: AgentControl::default(),
dynamic_tools: Vec::new(),
+1 -1
View File
@@ -466,7 +466,7 @@ async fn run_hooks_and_record_inputs(
record_additional_contexts(sess, turn_context, hook_outcome.additional_contexts).await;
} else {
accepted_input = true;
if matches!(input_item, TurnInput::UserInput(_)) {
if matches!(input_item, TurnInput::UserInput(items) if !items.is_empty()) {
accepted_user_input = true;
}
record_pending_input(
@@ -482,6 +482,7 @@ impl Session {
let turn_metadata_state = Arc::new(TurnMetadataState::new(
session_id.to_string(),
thread_id.to_string(),
session_configuration.forked_from_thread_id,
session_configuration.thread_source,
sub_id.clone(),
cwd.clone(),
@@ -0,0 +1,37 @@
use std::collections::BTreeMap;
use crate::context::AdditionalContextDeveloperFragment;
use crate::context::AdditionalContextUserFragment;
use crate::context::ContextualUserFragment;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::protocol::AdditionalContextEntry;
use codex_protocol::protocol::AdditionalContextKind;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct AdditionalContextStore {
values: BTreeMap<String, AdditionalContextEntry>,
}
impl AdditionalContextStore {
pub(crate) fn merge(
&mut self,
values: BTreeMap<String, AdditionalContextEntry>,
) -> Vec<ResponseInputItem> {
let fragments = values
.iter()
.filter(|(key, value)| self.values.get(*key) != Some(*value))
.map(|(key, entry)| match entry.kind {
AdditionalContextKind::Untrusted => {
AdditionalContextUserFragment::new(key.clone(), entry.value.clone())
.into_response_input_item()
}
AdditionalContextKind::Application => {
AdditionalContextDeveloperFragment::new(key.clone(), entry.value.clone())
.into_response_input_item()
}
})
.collect();
self.values = values;
fragments
}
}
+2
View File
@@ -1,8 +1,10 @@
mod additional_context;
mod auto_compact_window;
mod service;
mod session;
mod turn;
pub(crate) use additional_context::AdditionalContextStore;
pub(crate) use auto_compact_window::AutoCompactWindowSnapshot;
pub(crate) use service::SessionServices;
pub(crate) use session::SessionState;
+3
View File
@@ -6,6 +6,7 @@ use codex_sandboxing::policy_transforms::merge_permission_profiles;
use std::collections::HashSet;
use std::collections::VecDeque;
use super::AdditionalContextStore;
use super::auto_compact_window::AutoCompactWindow;
use super::auto_compact_window::AutoCompactWindowSnapshot;
use crate::context_manager::ContextManager;
@@ -25,6 +26,7 @@ pub(crate) struct SessionState {
pub(crate) latest_rate_limits: Option<RateLimitSnapshot>,
pub(crate) server_reasoning_included: bool,
pub(crate) mcp_dependency_prompted: HashSet<String>,
pub(crate) additional_context: AdditionalContextStore,
/// Settings used by the latest regular user turn, used for turn-to-turn
/// model/realtime handling on subsequent regular turns (including full-context
/// reinjection after resume or `/compact`).
@@ -49,6 +51,7 @@ impl SessionState {
latest_rate_limits: None,
server_reasoning_included: false,
mcp_dependency_prompted: HashSet::new(),
additional_context: AdditionalContextStore::default(),
previous_turn_settings: None,
auto_compact_window: AutoCompactWindow::new(),
startup_prewarm: None,
+1 -1
View File
@@ -233,7 +233,7 @@ async fn record_stage1_output_usage_for_memory_citation(
}
if let Some(db) = state_db_ctx {
let _ = db.record_stage1_output_usage(&thread_ids).await;
let _ = db.memories().record_stage1_output_usage(&thread_ids).await;
}
true
}
+3 -8
View File
@@ -50,7 +50,6 @@ use codex_protocol::protocol::TurnAbortReason;
use codex_protocol::protocol::TurnAbortedEvent;
use codex_protocol::protocol::TurnCompleteEvent;
use codex_protocol::protocol::WarningEvent;
use codex_protocol::user_input::UserInput;
use codex_features::Feature;
use codex_protocol::models::ContentItem;
@@ -303,7 +302,7 @@ impl Session {
pub async fn spawn_task<T: SessionTask>(
self: &Arc<Self>,
turn_context: Arc<TurnContext>,
input: Vec<UserInput>,
input: Vec<TurnInput>,
task: T,
) {
self.abort_all_tasks(TurnAbortReason::Replaced).await;
@@ -314,7 +313,7 @@ impl Session {
pub(crate) async fn start_task<T: SessionTask>(
self: &Arc<Self>,
turn_context: Arc<TurnContext>,
input: Vec<UserInput>,
input: Vec<TurnInput>,
task: T,
) {
let task: Arc<dyn AnySessionTask> = Arc::new(task);
@@ -382,11 +381,7 @@ impl Session {
));
let ctx = Arc::clone(&turn_context);
let task_for_run = Arc::clone(&task);
let task_input = if input.is_empty() {
Vec::new()
} else {
vec![TurnInput::UserInput(input)]
};
let task_input = input;
let task_cancellation_token = cancellation_token.child_token();
// Task-owned turn spans keep a core-owned span open for the
// full task lifecycle after the submission dispatch span ends.
+33 -1
View File
@@ -581,6 +581,15 @@ impl ThreadManager {
pub async fn start_thread_with_options(
&self,
options: StartThreadOptions,
) -> CodexResult<NewThread> {
self.start_thread_with_options_and_fork_source(options, /*forked_from_thread_id*/ None)
.await
}
async fn start_thread_with_options_and_fork_source(
&self,
options: StartThreadOptions,
forked_from_thread_id: Option<ThreadId>,
) -> CodexResult<NewThread> {
let session_source = options
.session_source
@@ -594,6 +603,7 @@ impl ThreadManager {
Arc::clone(&self.state.auth_manager),
self.agent_control(),
session_source,
forked_from_thread_id,
thread_source,
options.dynamic_tools,
options.persist_extended_history,
@@ -634,7 +644,8 @@ impl ThreadManager {
history,
InterruptedTurnHistoryMarker::from_config(&options.config),
);
self.start_thread_with_options(options).await
self.start_thread_with_options_and_fork_source(options, Some(forked_from_thread_id))
.await
}
pub async fn resume_thread_from_rollout(
@@ -673,6 +684,7 @@ impl ThreadManager {
initial_history,
auth_manager,
self.agent_control(),
/*forked_from_thread_id*/ None,
thread_source,
Vec::new(),
persist_extended_history,
@@ -698,6 +710,7 @@ impl ThreadManager {
InitialHistory::New,
Arc::clone(&self.state.auth_manager),
self.agent_control(),
/*forked_from_thread_id*/ None,
/*thread_source*/ None,
Vec::new(),
/*persist_extended_history*/ false,
@@ -727,6 +740,7 @@ impl ThreadManager {
initial_history,
auth_manager,
self.agent_control(),
/*forked_from_thread_id*/ None,
thread_source,
Vec::new(),
/*persist_extended_history*/ false,
@@ -876,6 +890,13 @@ impl ThreadManager {
persist_extended_history: bool,
parent_trace: Option<W3cTraceContext>,
) -> CodexResult<NewThread> {
// `forked_from_id()` describes this history's existing lineage. When
// forking a resumed thread, the child copies the resumed thread itself.
let forked_from_thread_id = match &history {
InitialHistory::Resumed(resumed) => Some(resumed.conversation_id),
InitialHistory::Forked(_) => history.forked_from_id(),
InitialHistory::New | InitialHistory::Cleared => None,
};
let interrupted_marker = InterruptedTurnHistoryMarker::from_config(&config);
let history = fork_history_from_snapshot(snapshot, history, interrupted_marker);
let environments = default_thread_environment_selections(
@@ -887,6 +908,7 @@ impl ThreadManager {
history,
Arc::clone(&self.state.auth_manager),
self.agent_control(),
forked_from_thread_id,
thread_source,
Vec::new(),
persist_extended_history,
@@ -1020,6 +1042,7 @@ impl ThreadManagerState {
config,
agent_control,
self.session_source.clone(),
/*forked_from_thread_id*/ None,
/*thread_source*/ None,
/*persist_extended_history*/ false,
/*metrics_service_name*/ None,
@@ -1036,6 +1059,7 @@ impl ThreadManagerState {
config: Config,
agent_control: AgentControl,
session_source: SessionSource,
forked_from_thread_id: Option<ThreadId>,
thread_source: Option<ThreadSource>,
persist_extended_history: bool,
metrics_service_name: Option<String>,
@@ -1052,6 +1076,7 @@ impl ThreadManagerState {
Arc::clone(&self.auth_manager),
agent_control,
session_source,
forked_from_thread_id,
thread_source,
Vec::new(),
persist_extended_history,
@@ -1086,6 +1111,7 @@ impl ThreadManagerState {
Arc::clone(&self.auth_manager),
agent_control,
session_source,
/*forked_from_thread_id*/ None,
thread_source,
Vec::new(),
/*persist_extended_history*/ false,
@@ -1107,6 +1133,7 @@ impl ThreadManagerState {
agent_control: AgentControl,
session_source: SessionSource,
thread_source: Option<ThreadSource>,
forked_from_thread_id: Option<ThreadId>,
persist_extended_history: bool,
inherited_shell_snapshot: Option<Arc<ShellSnapshot>>,
inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
@@ -1121,6 +1148,7 @@ impl ThreadManagerState {
Arc::clone(&self.auth_manager),
agent_control,
session_source,
forked_from_thread_id,
thread_source,
Vec::new(),
persist_extended_history,
@@ -1142,6 +1170,7 @@ impl ThreadManagerState {
initial_history: InitialHistory,
auth_manager: Arc<AuthManager>,
agent_control: AgentControl,
forked_from_thread_id: Option<ThreadId>,
thread_source: Option<ThreadSource>,
dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
persist_extended_history: bool,
@@ -1156,6 +1185,7 @@ impl ThreadManagerState {
auth_manager,
agent_control,
self.session_source.clone(),
forked_from_thread_id,
thread_source,
dynamic_tools,
persist_extended_history,
@@ -1177,6 +1207,7 @@ impl ThreadManagerState {
auth_manager: Arc<AuthManager>,
agent_control: AgentControl,
session_source: SessionSource,
forked_from_thread_id: Option<ThreadId>,
thread_source: Option<ThreadSource>,
dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
persist_extended_history: bool,
@@ -1229,6 +1260,7 @@ impl ThreadManagerState {
extensions: Arc::clone(&self.extensions),
conversation_history: initial_history,
session_source,
forked_from_thread_id,
thread_source,
agent_control,
dynamic_tools,
@@ -2592,6 +2592,7 @@ async fn send_input_accepts_structured_items() {
],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
};
let captured = manager
@@ -40,7 +40,7 @@ pub fn create_view_image_tool(options: ViewImageToolOptions) -> ToolSpec {
ToolSpec::Function(ResponsesApiTool {
name: VIEW_IMAGE_TOOL_NAME.to_string(),
description: "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within <image ...> tags)."
description: "View a local image file from the filesystem when visual inspection is needed. Use this for images already available on disk."
.to_string(),
strict: false,
defer_loading: None,
+14 -4
View File
@@ -238,11 +238,12 @@ fn spec_for_model_request(
}
}
pub(crate) fn hosted_model_tool_specs(turn_context: &TurnContext) -> Vec<ToolSpec> {
fn hosted_model_tool_specs(context: &CoreToolPlanContext<'_>) -> Vec<ToolSpec> {
let turn_context = context.turn_context;
let mut specs = Vec::new();
let provider_capabilities = turn_context.provider.capabilities();
let web_search_mode = provider_capabilities
.web_search
let web_search_mode = (!standalone_web_run_available(context.extension_tool_executors)
&& provider_capabilities.web_search)
.then_some(turn_context.config.web_search_mode.value());
let web_search_config = if provider_capabilities.web_search {
turn_context.config.web_search_config.as_ref()
@@ -504,11 +505,20 @@ fn add_tool_sources(context: &CoreToolPlanContext<'_>, planned_tools: &mut Plann
add_mcp_runtime_tools(context, planned_tools);
add_dynamic_tools(context, planned_tools);
add_extension_tools(context, planned_tools);
for spec in hosted_model_tool_specs(context.turn_context) {
for spec in hosted_model_tool_specs(context) {
planned_tools.add_hosted_spec(spec);
}
}
fn standalone_web_run_available(
extension_tools: &[Arc<dyn ToolExecutor<ExtensionToolCall>>],
) -> bool {
let web_run = ToolName::namespaced("web", "run");
extension_tools
.iter()
.any(|executor| executor.tool_name() == web_run)
}
fn add_shell_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools) {
let turn_context = context.turn_context;
let features = turn_context.features.get();
+56 -1
View File
@@ -20,8 +20,11 @@ use codex_tools::DiscoverablePluginInfo;
use codex_tools::DiscoverableTool;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ResponsesApiTool;
use codex_tools::ToolCall as ExtensionToolCall;
use codex_tools::ToolExecutor;
use codex_tools::ToolExposure;
use codex_tools::ToolName;
use codex_tools::ToolOutput;
use codex_tools::ToolSpec;
use pretty_assertions::assert_eq;
use serde_json::json;
@@ -37,6 +40,7 @@ struct ToolPlanInputs {
mcp_tools: Option<Vec<ToolInfo>>,
deferred_mcp_tools: Option<Vec<ToolInfo>>,
discoverable_tools: Option<Vec<DiscoverableTool>>,
extension_tool_executors: Vec<Arc<dyn ToolExecutor<ExtensionToolCall>>>,
dynamic_tools: Vec<DynamicToolSpec>,
}
@@ -176,7 +180,7 @@ async fn probe_with(
mcp_tools: inputs.mcp_tools,
deferred_mcp_tools: inputs.deferred_mcp_tools,
discoverable_tools: inputs.discoverable_tools,
extension_tool_executors: Vec::new(),
extension_tool_executors: inputs.extension_tool_executors,
dynamic_tools: inputs.dynamic_tools.as_slice(),
},
);
@@ -253,6 +257,37 @@ fn use_bedrock_provider(turn: &mut TurnContext) {
turn.provider = create_model_provider(provider_info, turn.auth_manager.clone());
}
struct WebRunExtensionTool;
#[async_trait::async_trait]
impl ToolExecutor<ExtensionToolCall> for WebRunExtensionTool {
fn tool_name(&self) -> ToolName {
ToolName::namespaced("web", "run")
}
fn spec(&self) -> ToolSpec {
ToolSpec::Namespace(codex_tools::ResponsesApiNamespace {
name: "web".to_string(),
description: "Test web namespace.".to_string(),
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
name: "run".to_string(),
description: "Test standalone web search tool.".to_string(),
strict: false,
defer_loading: None,
parameters: codex_tools::JsonSchema::default(),
output_schema: None,
})],
})
}
async fn handle(
&self,
_call: ExtensionToolCall,
) -> Result<Box<dyn ToolOutput>, codex_tools::FunctionCallError> {
Ok(Box::new(codex_tools::JsonToolOutput::new(json!({}))))
}
}
fn duplicate_primary_environment(turn: &mut TurnContext) {
let mut second_environment = turn.environments.turn_environments[0].clone();
second_environment.environment_id = "secondary".to_string();
@@ -947,6 +982,26 @@ async fn hosted_tools_follow_provider_auth_model_and_config_gates() {
}
);
let standalone_web_search_without_web_run = probe(|turn| {
set_feature(turn, Feature::StandaloneWebSearch, /*enabled*/ true);
set_web_search_mode(turn, WebSearchMode::Live);
})
.await;
standalone_web_search_without_web_run.assert_visible_contains(&["web_search"]);
let standalone_web_search = probe_with(
|turn| {
set_feature(turn, Feature::StandaloneWebSearch, /*enabled*/ true);
set_web_search_mode(turn, WebSearchMode::Live);
},
ToolPlanInputs {
extension_tool_executors: vec![Arc::new(WebRunExtensionTool)],
..Default::default()
},
)
.await;
standalone_web_search.assert_visible_lacks(&["web_search"]);
let unsupported_provider = probe(|turn| {
set_web_search_mode(turn, WebSearchMode::Live);
use_bedrock_provider(turn);
+42 -59
View File
@@ -16,6 +16,7 @@ use codex_git_utils::get_git_remote_urls_assume_git_repo;
use codex_git_utils::get_git_repo_root;
use codex_git_utils::get_has_changes;
use codex_git_utils::get_head_commit_hash;
use codex_protocol::ThreadId;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
@@ -74,6 +75,8 @@ pub(crate) struct TurnMetadataBag {
#[serde(default, skip_serializing_if = "Option::is_none")]
thread_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
forked_from_thread_id: Option<ThreadId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
thread_source: Option<ThreadSource>,
#[serde(default, skip_serializing_if = "Option::is_none")]
turn_id: Option<String>,
@@ -107,7 +110,14 @@ fn merge_turn_metadata(
}
if let Some(responsesapi_client_metadata) = responsesapi_client_metadata {
for (key, value) in responsesapi_client_metadata {
if key == TURN_STARTED_AT_UNIX_MS_KEY {
if matches!(
key.as_str(),
"session_id"
| "thread_id"
| "turn_id"
| TURN_STARTED_AT_UNIX_MS_KEY
| "forked_from_thread_id"
) {
continue;
}
metadata
@@ -118,32 +128,6 @@ fn merge_turn_metadata(
to_ascii_json_string(&metadata).ok()
}
fn build_turn_metadata_bag(
session_id: Option<String>,
thread_id: Option<String>,
thread_source: Option<ThreadSource>,
turn_id: Option<String>,
sandbox: Option<String>,
repo_root: Option<String>,
workspace_git_metadata: Option<WorkspaceGitMetadata>,
) -> TurnMetadataBag {
let mut workspaces = BTreeMap::new();
if let (Some(repo_root), Some(workspace_git_metadata)) = (repo_root, workspace_git_metadata)
&& !workspace_git_metadata.is_empty()
{
workspaces.insert(repo_root, workspace_git_metadata.into());
}
TurnMetadataBag {
session_id,
thread_id,
thread_source,
turn_id,
workspaces,
sandbox,
}
}
pub async fn build_turn_metadata_header(
cwd: &AbsolutePathBuf,
sandbox: Option<&str>,
@@ -164,20 +148,24 @@ pub async fn build_turn_metadata_header(
return None;
}
build_turn_metadata_bag(
/*session_id*/ None,
/*thread_id*/ None,
/*thread_source*/ None,
/*turn_id*/ None,
sandbox.map(ToString::to_string),
repo_root,
Some(WorkspaceGitMetadata {
associated_remote_urls,
latest_git_commit_hash,
has_changes,
}),
)
.to_header_value()
let workspace_git_metadata = WorkspaceGitMetadata {
associated_remote_urls,
latest_git_commit_hash,
has_changes,
};
let mut metadata = TurnMetadataBag {
sandbox: sandbox.map(ToString::to_string),
..Default::default()
};
if let Some(repo_root) = repo_root
&& !workspace_git_metadata.is_empty()
{
metadata
.workspaces
.insert(repo_root, workspace_git_metadata.into());
}
metadata.to_header_value()
}
#[derive(Clone, Debug)]
@@ -198,6 +186,7 @@ impl TurnMetadataState {
pub(crate) fn new(
session_id: String,
thread_id: String,
forked_from_thread_id: Option<ThreadId>,
thread_source: Option<ThreadSource>,
turn_id: String,
cwd: AbsolutePathBuf,
@@ -214,15 +203,15 @@ impl TurnMetadataState {
)
.to_string(),
);
let base_metadata = build_turn_metadata_bag(
Some(session_id),
Some(thread_id),
let base_metadata = TurnMetadataBag {
session_id: Some(session_id),
thread_id: Some(thread_id),
forked_from_thread_id,
thread_source,
Some(turn_id),
turn_id: Some(turn_id),
sandbox,
/*repo_root*/ None,
/*workspace_git_metadata*/ None,
);
..Default::default()
};
let base_header = base_metadata
.to_header_value()
.unwrap_or_else(|| "{}".to_string());
@@ -346,19 +335,13 @@ impl TurnMetadataState {
let Some(repo_root) = state.repo_root.clone() else {
return;
};
let enriched_metadata = build_turn_metadata_bag(
state.base_metadata.session_id.clone(),
state.base_metadata.thread_id.clone(),
state.base_metadata.thread_source,
state.base_metadata.turn_id.clone(),
state.base_metadata.sandbox.clone(),
Some(repo_root),
Some(workspace_git_metadata),
);
if enriched_metadata.workspaces.is_empty() {
if workspace_git_metadata.is_empty() {
return;
}
let mut enriched_metadata = state.base_metadata.clone();
enriched_metadata
.workspaces
.insert(repo_root, workspace_git_metadata.into());
if let Some(header_value) = enriched_metadata.to_header_value() {
*state
+58 -5
View File
@@ -93,6 +93,7 @@ fn turn_metadata_state_uses_platform_sandbox_tag() {
let state = TurnMetadataState::new(
"session-a".to_string(),
"thread-a".to_string(),
/*forked_from_thread_id*/ None,
Some(ThreadSource::User),
"turn-a".to_string(),
cwd,
@@ -128,6 +129,7 @@ fn turn_metadata_state_uses_explicit_subagent_thread_source() {
let state = TurnMetadataState::new(
"session-a".to_string(),
"thread-a".to_string(),
/*forked_from_thread_id*/ None,
Some(ThreadSource::Subagent),
"turn-a".to_string(),
cwd,
@@ -143,6 +145,35 @@ fn turn_metadata_state_uses_explicit_subagent_thread_source() {
assert!(json.get("session_source").is_none());
}
#[test]
fn turn_metadata_state_includes_root_fork_lineage() {
let temp_dir = TempDir::new().expect("temp dir");
let cwd = temp_dir.path().abs();
let permission_profile = PermissionProfile::read_only();
let source_thread_id =
ThreadId::from_string("11111111-1111-4111-8111-111111111111").expect("thread id");
let state = TurnMetadataState::new(
"session-a".to_string(),
"thread-a".to_string(),
Some(source_thread_id),
Some(ThreadSource::User),
"turn-a".to_string(),
cwd,
&permission_profile,
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
);
let header = state.current_header_value().expect("header");
let json: Value = serde_json::from_str(&header).expect("json");
assert_eq!(
json["forked_from_thread_id"].as_str(),
Some("11111111-1111-4111-8111-111111111111")
);
}
#[test]
fn turn_metadata_state_includes_turn_started_at_unix_ms_after_start() {
let temp_dir = TempDir::new().expect("temp dir");
@@ -152,6 +183,7 @@ fn turn_metadata_state_includes_turn_started_at_unix_ms_after_start() {
let state = TurnMetadataState::new(
"session-a".to_string(),
"thread-a".to_string(),
/*forked_from_thread_id*/ None,
Some(ThreadSource::User),
"turn-a".to_string(),
cwd,
@@ -179,6 +211,7 @@ fn turn_metadata_state_includes_model_and_reasoning_effort_only_in_request_meta(
let state = TurnMetadataState::new(
"session-a".to_string(),
"thread-a".to_string(),
/*forked_from_thread_id*/ None,
/*thread_source*/ None,
"turn-a".to_string(),
cwd,
@@ -224,6 +257,7 @@ fn turn_metadata_state_marks_user_input_requested_during_turn_only_for_mcp_reque
let state = TurnMetadataState::new(
"session-a".to_string(),
"thread-a".to_string(),
/*forked_from_thread_id*/ None,
/*thread_source*/ None,
"turn-a".to_string(),
cwd,
@@ -266,7 +300,7 @@ fn turn_metadata_state_marks_user_input_requested_during_turn_only_for_mcp_reque
}
#[test]
fn turn_metadata_state_ignores_client_turn_started_at_unix_ms_before_start() {
fn turn_metadata_state_ignores_client_reserved_metadata_before_start() {
let temp_dir = TempDir::new().expect("temp dir");
let cwd = temp_dir.path().abs();
let permission_profile = PermissionProfile::read_only();
@@ -274,6 +308,7 @@ fn turn_metadata_state_ignores_client_turn_started_at_unix_ms_before_start() {
let state = TurnMetadataState::new(
"session-a".to_string(),
"thread-a".to_string(),
/*forked_from_thread_id*/ None,
Some(ThreadSource::User),
"turn-a".to_string(),
cwd,
@@ -281,15 +316,22 @@ fn turn_metadata_state_ignores_client_turn_started_at_unix_ms_before_start() {
WindowsSandboxLevel::Disabled,
/*enforce_managed_network*/ false,
);
state.set_responsesapi_client_metadata(HashMap::from([(
"turn_started_at_unix_ms".to_string(),
"client-supplied".to_string(),
)]));
state.set_responsesapi_client_metadata(HashMap::from([
(
"turn_started_at_unix_ms".to_string(),
"client-supplied".to_string(),
),
(
"forked_from_thread_id".to_string(),
"client-supplied".to_string(),
),
]));
let header = state.current_header_value().expect("header");
let json: Value = serde_json::from_str(&header).expect("json");
assert!(json.get("turn_started_at_unix_ms").is_none());
assert!(json.get("forked_from_thread_id").is_none());
}
#[test]
@@ -297,10 +339,13 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
let temp_dir = TempDir::new().expect("temp dir");
let cwd = temp_dir.path().abs();
let permission_profile = PermissionProfile::read_only();
let source_thread_id =
ThreadId::from_string("44444444-4444-4444-8444-444444444444").expect("thread id");
let state = TurnMetadataState::new(
"session-a".to_string(),
"thread-a".to_string(),
Some(source_thread_id),
Some(ThreadSource::User),
"turn-a".to_string(),
cwd,
@@ -318,6 +363,10 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
),
("session_id".to_string(), "client-supplied".to_string()),
("thread_id".to_string(), "client-supplied".to_string()),
(
"forked_from_thread_id".to_string(),
"client-supplied".to_string(),
),
("thread_source".to_string(), "client-supplied".to_string()),
(
"turn_started_at_unix_ms".to_string(),
@@ -337,6 +386,10 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
assert_eq!(json["reasoning_effort"].as_str(), Some("client-supplied"));
assert_eq!(json["session_id"].as_str(), Some("session-a"));
assert_eq!(json["thread_id"].as_str(), Some("thread-a"));
assert_eq!(
json["forked_from_thread_id"].as_str(),
Some("44444444-4444-4444-8444-444444444444")
);
assert_eq!(json["thread_source"].as_str(), Some("user"));
assert_eq!(json["turn_id"].as_str(), Some("turn-a"));
assert_eq!(
+1
View File
@@ -767,6 +767,7 @@ impl TestCodex {
environments,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(self.config.cwd.to_path_buf()),
approval_policy: Some(approval_policy),
+5
View File
@@ -53,6 +53,7 @@ async fn interrupt_long_running_tool_emits_turn_aborted() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -110,6 +111,7 @@ async fn interrupt_tool_records_history_entries() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -131,6 +133,7 @@ async fn interrupt_tool_records_history_entries() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -214,6 +217,7 @@ async fn interrupt_persists_turn_aborted_marker_in_next_request() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -235,6 +239,7 @@ async fn interrupt_persists_turn_aborted_marker_in_next_request() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -0,0 +1,581 @@
use anyhow::Result;
use codex_protocol::items::TurnItem;
use codex_protocol::protocol::AdditionalContextEntry;
use codex_protocol::protocol::AdditionalContextKind;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ItemCompletedEvent;
use codex_protocol::protocol::Op;
use codex_protocol::user_input::UserInput;
use core_test_support::context_snapshot;
use core_test_support::context_snapshot::ContextSnapshotOptions;
use core_test_support::context_snapshot::ContextSnapshotRenderMode;
use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_once;
use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex::test_codex;
use core_test_support::wait_for_event_match;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn additional_context_is_model_visible_but_not_a_user_message_item() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let request = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let test = test_codex()
.with_config(|config| config.include_environment_context = false)
.build(&server)
.await?;
test.codex
.submit(Op::UserInput {
environments: None,
items: vec![UserInput::Text {
text: "inspect the active tab".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: BTreeMap::from([
(
"browser_info".to_string(),
AdditionalContextEntry {
value: "tab one".to_string(),
kind: AdditionalContextKind::Untrusted,
},
),
(
"automation_info".to_string(),
AdditionalContextEntry {
value: "run one".to_string(),
kind: AdditionalContextKind::Application,
},
),
]),
thread_settings: Default::default(),
})
.await?;
let user_item = wait_for_event_match(&test.codex, |event| match event {
EventMsg::ItemCompleted(ItemCompletedEvent {
item: TurnItem::UserMessage(item),
..
}) => Some(item.clone()),
_ => None,
})
.await;
assert_eq!(
user_item.content,
vec![UserInput::Text {
text: "inspect the active tab".to_string(),
text_elements: Vec::new(),
}]
);
wait_for_event_match(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)).then_some(())
})
.await;
let request = request.single_request();
insta::assert_snapshot!(
"additional_context_simple_input",
context_snapshot::format_labeled_requests_snapshot(
"additional context is inserted before the user turn input.",
&[("Request", &request)],
&ContextSnapshotOptions::default()
.strip_capability_instructions()
.render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 160 }),
)
);
let developer_context_texts = request
.message_input_texts("developer")
.into_iter()
.filter(|text| text.starts_with("<automation_info>"))
.collect::<Vec<_>>();
assert_eq!(
developer_context_texts,
vec!["<automation_info>run one</automation_info>"]
);
assert_eq!(
request.message_input_texts("user"),
vec![
"<external_browser_info>tab one</external_browser_info>",
"inspect the active tab",
]
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn external_context_like_user_text_remains_a_user_message_item() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let request = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let test = test_codex()
.with_config(|config| config.include_environment_context = false)
.build(&server)
.await?;
let user_input = UserInput::Text {
text: "<external_api>".to_string(),
text_elements: Vec::new(),
};
test.codex
.submit(Op::UserInput {
environments: None,
items: vec![user_input.clone()],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: BTreeMap::new(),
thread_settings: Default::default(),
})
.await?;
let user_item = wait_for_event_match(&test.codex, |event| match event {
EventMsg::ItemCompleted(ItemCompletedEvent {
item: TurnItem::UserMessage(item),
..
}) => Some(item.clone()),
_ => None,
})
.await;
assert_eq!(user_item.content, vec![user_input]);
wait_for_event_match(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)).then_some(())
})
.await;
let request = request.single_request();
assert_eq!(request.message_input_texts("user"), vec!["<external_api>"]);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn additional_context_trust_controls_message_role() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let request = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let test = test_codex()
.with_config(|config| config.include_environment_context = false)
.build(&server)
.await?;
test.codex
.submit(Op::UserInput {
environments: None,
items: vec![UserInput::Text {
text: "inspect context".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: BTreeMap::from([
(
"browser_info".to_string(),
AdditionalContextEntry {
value: "tab one".to_string(),
kind: AdditionalContextKind::Untrusted,
},
),
(
"automation_info".to_string(),
AdditionalContextEntry {
value: "run one".to_string(),
kind: AdditionalContextKind::Application,
},
),
]),
thread_settings: Default::default(),
})
.await?;
wait_for_event_match(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)).then_some(())
})
.await;
let request = request.single_request();
let developer_context_texts = request
.message_input_texts("developer")
.into_iter()
.filter(|text| text.starts_with("<automation_info>"))
.collect::<Vec<_>>();
assert_eq!(
developer_context_texts,
vec!["<automation_info>run one</automation_info>"]
);
assert_eq!(
request.message_input_texts("user"),
vec![
"<external_browser_info>tab one</external_browser_info>",
"inspect context",
]
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn additional_context_is_deduplicated_between_turns_while_retained() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let first_request = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let second_request = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
)
.await;
let test = test_codex()
.with_config(|config| config.include_environment_context = false)
.build(&server)
.await?;
let additional_context = BTreeMap::from([(
"browser_info".to_string(),
AdditionalContextEntry {
value: "same tab".to_string(),
kind: AdditionalContextKind::Untrusted,
},
)]);
test.codex
.submit(Op::UserInput {
environments: None,
items: vec![UserInput::Text {
text: "first turn".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: additional_context.clone(),
thread_settings: Default::default(),
})
.await?;
wait_for_event_match(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)).then_some(())
})
.await;
test.codex
.submit(Op::UserInput {
environments: None,
items: vec![UserInput::Text {
text: "second turn".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context,
thread_settings: Default::default(),
})
.await?;
wait_for_event_match(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)).then_some(())
})
.await;
assert_eq!(
first_request.single_request().message_input_texts("user"),
vec![
"<external_browser_info>same tab</external_browser_info>",
"first turn",
]
);
assert_eq!(
second_request.single_request().message_input_texts("user"),
vec![
"<external_browser_info>same tab</external_browser_info>",
"first turn",
"second turn",
]
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn additional_context_removes_one_value_while_adding_another() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let first_request = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let second_request = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]),
)
.await;
let third_request = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-3"), ev_completed("resp-3")]),
)
.await;
let test = test_codex()
.with_config(|config| config.include_environment_context = false)
.build(&server)
.await?;
test.codex
.submit(Op::UserInput {
environments: None,
items: vec![UserInput::Text {
text: "first turn".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: BTreeMap::from([
(
"automation_info".to_string(),
AdditionalContextEntry {
value: "run one".to_string(),
kind: AdditionalContextKind::Untrusted,
},
),
(
"browser_info".to_string(),
AdditionalContextEntry {
value: "tab one".to_string(),
kind: AdditionalContextKind::Untrusted,
},
),
]),
thread_settings: Default::default(),
})
.await?;
wait_for_event_match(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)).then_some(())
})
.await;
test.codex
.submit(Op::UserInput {
environments: None,
items: vec![UserInput::Text {
text: "second turn".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: BTreeMap::from([
(
"automation_info".to_string(),
AdditionalContextEntry {
value: "run one".to_string(),
kind: AdditionalContextKind::Untrusted,
},
),
(
"terminal_info".to_string(),
AdditionalContextEntry {
value: "pty one".to_string(),
kind: AdditionalContextKind::Untrusted,
},
),
]),
thread_settings: Default::default(),
})
.await?;
wait_for_event_match(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)).then_some(())
})
.await;
test.codex
.submit(Op::UserInput {
environments: None,
items: vec![UserInput::Text {
text: "third turn".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: BTreeMap::from([
(
"automation_info".to_string(),
AdditionalContextEntry {
value: "run one".to_string(),
kind: AdditionalContextKind::Untrusted,
},
),
(
"browser_info".to_string(),
AdditionalContextEntry {
value: "tab one".to_string(),
kind: AdditionalContextKind::Untrusted,
},
),
(
"terminal_info".to_string(),
AdditionalContextEntry {
value: "pty one".to_string(),
kind: AdditionalContextKind::Untrusted,
},
),
]),
thread_settings: Default::default(),
})
.await?;
wait_for_event_match(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)).then_some(())
})
.await;
assert_eq!(
first_request.single_request().message_input_texts("user"),
vec![
"<external_automation_info>run one</external_automation_info>",
"<external_browser_info>tab one</external_browser_info>",
"first turn",
]
);
assert_eq!(
second_request.single_request().message_input_texts("user"),
vec![
"<external_automation_info>run one</external_automation_info>",
"<external_browser_info>tab one</external_browser_info>",
"first turn",
"<external_terminal_info>pty one</external_terminal_info>",
"second turn",
]
);
assert_eq!(
third_request.single_request().message_input_texts("user"),
vec![
"<external_automation_info>run one</external_automation_info>",
"<external_browser_info>tab one</external_browser_info>",
"first turn",
"<external_terminal_info>pty one</external_terminal_info>",
"second turn",
"<external_browser_info>tab one</external_browser_info>",
"third turn",
]
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn additional_context_values_are_truncated_before_model_input() -> Result<()> {
skip_if_no_network!(Ok(()));
const MAX_EXPECTED_EXTERNAL_CONTEXT_TEXT_BYTES: usize = 5 * 1024;
let server = start_mock_server().await;
let request = mount_sse_once(
&server,
sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]),
)
.await;
let test = test_codex()
.with_config(|config| config.include_environment_context = false)
.build(&server)
.await?;
let long_browser_value = format!("browser-head-{}browser-tail", "b".repeat(40_000));
let long_automation_value = format!("automation-head-{}automation-tail", "a".repeat(40_000));
let untruncated_browser_fragment =
format!("<external_browser_info>{long_browser_value}</external_browser_info>");
let untruncated_automation_fragment =
format!("<automation_info>{long_automation_value}</automation_info>");
test.codex
.submit(Op::UserInput {
environments: None,
items: vec![UserInput::Text {
text: "summarize context".to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: BTreeMap::from([
(
"automation_info".to_string(),
AdditionalContextEntry {
value: long_automation_value.clone(),
kind: AdditionalContextKind::Application,
},
),
(
"browser_info".to_string(),
AdditionalContextEntry {
value: long_browser_value.clone(),
kind: AdditionalContextKind::Untrusted,
},
),
]),
thread_settings: Default::default(),
})
.await?;
wait_for_event_match(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_)).then_some(())
})
.await;
let request = request.single_request();
let developer_texts = request
.message_input_texts("developer")
.into_iter()
.filter(|text| text.starts_with("<automation_info>"))
.collect::<Vec<_>>();
let [automation_text] = developer_texts.as_slice() else {
panic!("expected application additional context, got {developer_texts:?}");
};
assert!(automation_text.starts_with(&format!(
"<automation_info>automation-head-{}",
"a".repeat(1024)
)));
assert!(automation_text.contains("tokens truncated"));
assert!(automation_text.ends_with("automation-tail</automation_info>"));
assert!(automation_text.len() < untruncated_automation_fragment.len());
assert!(
automation_text.len() <= MAX_EXPECTED_EXTERNAL_CONTEXT_TEXT_BYTES,
"application additional context was not capped before model input: {} bytes",
automation_text.len()
);
let user_texts = request.message_input_texts("user");
let [external_text, user_text] = user_texts.as_slice() else {
panic!("expected external context plus user input, got {user_texts:?}");
};
assert_eq!(user_text, "summarize context");
assert!(external_text.starts_with(&format!(
"<external_browser_info>browser-head-{}",
"b".repeat(1024)
)));
assert!(external_text.contains("tokens truncated"));
assert!(external_text.ends_with("browser-tail</external_browser_info>"));
assert!(external_text.len() < untruncated_browser_fragment.len());
assert!(
external_text.len() <= MAX_EXPECTED_EXTERNAL_CONTEXT_TEXT_BYTES,
"untrusted additional context was not capped before model input: {} bytes",
external_text.len()
);
Ok(())
}
@@ -91,6 +91,7 @@ async fn submit_without_wait_with_turn_permissions(
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(harness.cwd().to_path_buf()),
approval_policy: Some(AskForApproval::Never),
+2
View File
@@ -653,6 +653,7 @@ async fn submit_turn(
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(test.cwd.path().to_path_buf()),
approval_policy: Some(approval_policy),
@@ -2598,6 +2599,7 @@ async fn matched_prefix_rule_runs_unsandboxed_under_zsh_fork() -> Result<()> {
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(test.cwd.path().to_path_buf()),
approval_policy: Some(approval_policy),
+34
View File
@@ -392,6 +392,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -758,6 +759,7 @@ async fn includes_session_id_thread_id_and_model_headers_in_request() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -969,6 +971,7 @@ async fn includes_base_instructions_override_in_request() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1026,6 +1029,7 @@ async fn chatgpt_auth_sends_correct_request() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1150,6 +1154,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1189,6 +1194,7 @@ async fn includes_user_instructions_message_in_request() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1277,6 +1283,7 @@ async fn includes_apps_guidance_as_developer_message_for_chatgpt_auth() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1340,6 +1347,7 @@ async fn omits_apps_guidance_for_api_key_auth_even_when_feature_enabled() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1399,6 +1407,7 @@ async fn omits_apps_guidance_when_configured_off() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1441,6 +1450,7 @@ async fn omits_environment_context_when_configured_off() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1498,6 +1508,7 @@ async fn skills_append_to_developer_message() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1581,6 +1592,7 @@ async fn skills_use_aliases_in_developer_message_under_budget_pressure() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1642,6 +1654,7 @@ async fn includes_configured_effort_in_request() -> anyhow::Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1684,6 +1697,7 @@ async fn includes_no_effort_in_request() -> anyhow::Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1727,6 +1741,7 @@ async fn includes_default_reasoning_effort_in_request_when_defined_by_model_info
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1778,6 +1793,7 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(config.cwd.to_path_buf()),
approval_policy: Some(config.permissions.approval_policy.value()),
@@ -1834,6 +1850,7 @@ async fn configured_reasoning_summary_is_sent() -> anyhow::Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1899,6 +1916,7 @@ async fn user_turn_explicit_reasoning_summary_overrides_model_catalog_default()
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(config.cwd.to_path_buf()),
approval_policy: Some(config.permissions.approval_policy.value()),
@@ -1959,6 +1977,7 @@ async fn reasoning_summary_is_omitted_when_disabled() -> anyhow::Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -2018,6 +2037,7 @@ async fn reasoning_summary_none_overrides_model_catalog_default() -> anyhow::Res
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -2057,6 +2077,7 @@ async fn includes_default_verbosity_in_request() -> anyhow::Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -2105,6 +2126,7 @@ async fn configured_verbosity_not_sent_for_models_without_support() -> anyhow::R
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -2152,6 +2174,7 @@ async fn configured_verbosity_is_sent() -> anyhow::Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -2204,6 +2227,7 @@ async fn includes_developer_instructions_message_in_request() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -2500,6 +2524,7 @@ async fn token_count_includes_rate_limits_snapshot() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -2639,6 +2664,7 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -2716,6 +2742,7 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2731,6 +2758,7 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2816,6 +2844,7 @@ async fn incomplete_response_emits_content_filter_error_message() -> anyhow::Res
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2927,6 +2956,7 @@ async fn azure_overrides_assign_properties_used_for_responses_url() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -3016,6 +3046,7 @@ async fn env_var_overrides_loaded_auth() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -3073,6 +3104,7 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -3089,6 +3121,7 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -3105,6 +3138,7 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1319,6 +1319,7 @@ async fn responses_websocket_usage_limit_error_emits_rate_limit_event() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1408,6 +1409,7 @@ async fn responses_websocket_invalid_request_error_with_status_is_forwarded() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
+2 -1
View File
@@ -2861,7 +2861,7 @@ text(JSON.stringify(tool));
parsed,
serde_json::json!({
"name": "view_image",
"description": "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within <image ...> tags).\n\nexec tool declaration:\n```ts\ndeclare const tools: { view_image(args: {\n // Local filesystem path to an image file\n path: string;\n}): Promise<{\n // Image detail hint returned by view_image. Returns `high` for default resized behavior or `original` when original resolution is preserved.\n detail: \"high\" | \"original\";\n // Data URL for the loaded image.\n image_url: string;\n}>; };\n```",
"description": "View a local image file from the filesystem when visual inspection is needed. Use this for images already available on disk.\n\nexec tool declaration:\n```ts\ndeclare const tools: { view_image(args: {\n // Local filesystem path to an image file\n path: string;\n}): Promise<{\n // Image detail hint returned by view_image. Returns `high` for default resized behavior or `original` when original resolution is preserved.\n detail: \"high\" | \"original\";\n // Data URL for the loaded image.\n image_url: string;\n}>; };\n```",
})
);
@@ -2991,6 +2991,7 @@ text(
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(cwd),
approval_policy: Some(AskForApproval::Never),
@@ -79,6 +79,7 @@ async fn no_collaboration_instructions_by_default() -> Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -133,6 +134,7 @@ async fn user_input_includes_collaboration_instructions_after_override() -> Resu
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -170,6 +172,7 @@ async fn collaboration_instructions_added_on_user_turn() -> Result<()> {
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(test.config.cwd.to_path_buf()),
approval_policy: Some(test.config.permissions.approval_policy.value()),
@@ -220,6 +223,7 @@ async fn collaboration_instructions_omitted_when_disabled() -> Result<()> {
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(test.config.cwd.to_path_buf()),
approval_policy: Some(test.config.permissions.approval_policy.value()),
@@ -279,6 +283,7 @@ async fn override_then_next_turn_uses_updated_collaboration_instructions() -> Re
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -327,6 +332,7 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(test.config.cwd.to_path_buf()),
approval_policy: Some(test.config.permissions.approval_policy.value()),
@@ -391,6 +397,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()>
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -414,6 +421,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()>
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -466,6 +474,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -489,6 +498,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -543,6 +553,7 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -569,6 +580,7 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -624,6 +636,7 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged()
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -650,6 +663,7 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged()
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -708,6 +722,7 @@ async fn resume_replays_collaboration_instructions() -> Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -724,6 +739,7 @@ async fn resume_replays_collaboration_instructions() -> Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -776,6 +792,7 @@ async fn empty_collaboration_instructions_are_ignored() -> Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
+33
View File
@@ -98,6 +98,7 @@ fn disabled_permission_user_turn(text: impl Into<String>, cwd: PathBuf, model: S
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(cwd),
approval_policy: Some(AskForApproval::Never),
@@ -409,6 +410,7 @@ async fn summarize_context_three_requests_and_instructions() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -434,6 +436,7 @@ async fn summarize_context_three_requests_and_instructions() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -611,6 +614,7 @@ async fn manual_pre_compact_block_decision_does_not_block_compaction() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -684,6 +688,7 @@ async fn compact_hooks_respect_matchers_and_post_runs_after_compaction() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -754,6 +759,7 @@ async fn manual_compact_uses_custom_prompt() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -901,6 +907,7 @@ async fn manual_compact_emits_context_compaction_items() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1067,6 +1074,7 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1566,6 +1574,7 @@ async fn auto_compact_stops_after_consecutive_no_progress_compactions() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1654,6 +1663,7 @@ async fn auto_compact_runs_after_token_limit_hit() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1670,6 +1680,7 @@ async fn auto_compact_runs_after_token_limit_hit() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1686,6 +1697,7 @@ async fn auto_compact_runs_after_token_limit_hit() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1857,6 +1869,7 @@ async fn auto_compact_emits_context_compaction_items() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1938,6 +1951,7 @@ async fn auto_compact_starts_after_turn_started() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1953,6 +1967,7 @@ async fn auto_compact_starts_after_turn_started() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1968,6 +1983,7 @@ async fn auto_compact_starts_after_turn_started() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -2496,6 +2512,7 @@ async fn auto_compact_persists_rollout_entries() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -2511,6 +2528,7 @@ async fn auto_compact_persists_rollout_entries() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -2526,6 +2544,7 @@ async fn auto_compact_persists_rollout_entries() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -2615,6 +2634,7 @@ async fn manual_compact_retries_after_context_window_error() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -2719,6 +2739,7 @@ async fn manual_compact_non_context_failure_retries_then_emits_task_error() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -2814,6 +2835,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -2832,6 +2854,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -2850,6 +2873,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -3014,6 +3038,7 @@ async fn auto_compact_allows_multiple_attempts_when_interleaved_with_other_turn_
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -3119,6 +3144,7 @@ async fn snapshot_request_shape_mid_turn_continuation_compaction() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -3550,6 +3576,7 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -3669,6 +3696,7 @@ async fn auto_compact_runs_when_reasoning_header_clears_between_turns() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -3731,6 +3759,7 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -3763,6 +3792,7 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess
],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -3952,6 +3982,7 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -3967,6 +3998,7 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -4040,6 +4072,7 @@ async fn snapshot_request_shape_manual_compact_without_previous_user_messages()
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -326,6 +326,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -343,6 +344,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -518,6 +520,7 @@ async fn assert_remote_manual_compact_request_parity(
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -538,6 +541,7 @@ async fn assert_remote_manual_compact_request_parity(
],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -552,6 +556,7 @@ async fn assert_remote_manual_compact_request_parity(
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -572,6 +577,7 @@ async fn assert_remote_manual_compact_request_parity(
],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -586,6 +592,7 @@ async fn assert_remote_manual_compact_request_parity(
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -754,6 +761,7 @@ async fn remote_compact_v2_reuses_compaction_trigger_for_followups() -> Result<(
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -771,6 +779,7 @@ async fn remote_compact_v2_reuses_compaction_trigger_for_followups() -> Result<(
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -874,6 +883,7 @@ async fn remote_compact_v2_retries_failures_with_stream_retry_budget() -> Result
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -891,6 +901,7 @@ async fn remote_compact_v2_retries_failures_with_stream_retry_budget() -> Result
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -977,6 +988,7 @@ async fn remote_compact_v2_accepts_additional_output_items_before_compaction() -
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -994,6 +1006,7 @@ async fn remote_compact_v2_accepts_additional_output_items_before_compaction() -
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -1085,6 +1098,7 @@ async fn remote_compact_filters_deferred_dynamic_tools() -> Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -1157,6 +1171,7 @@ async fn remote_compact_runs_automatically() -> Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -1238,6 +1253,7 @@ async fn remote_compact_trims_function_call_history_to_fit_context_window() -> R
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -1252,6 +1268,7 @@ async fn remote_compact_trims_function_call_history_to_fit_context_window() -> R
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -1370,6 +1387,7 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window()
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -1384,6 +1402,7 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window()
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -1404,6 +1423,7 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window()
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -1504,6 +1524,7 @@ async fn auto_remote_compact_failure_stops_agent_loop() -> Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -1518,6 +1539,7 @@ async fn auto_remote_compact_failure_stops_agent_loop() -> Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -1612,6 +1634,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -1629,6 +1652,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -1720,6 +1744,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -1737,6 +1762,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -1808,6 +1834,7 @@ async fn remote_manual_compact_emits_context_compaction_items() -> Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -1889,6 +1916,7 @@ async fn remote_manual_compact_failure_emits_task_error_event() -> Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -1973,6 +2001,7 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()>
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2116,6 +2145,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2134,6 +2164,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2159,6 +2190,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2255,6 +2287,7 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume()
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2272,6 +2305,7 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume()
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2344,6 +2378,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_sta
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2358,6 +2393,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_sta
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2425,6 +2461,7 @@ async fn remote_request_uses_custom_experimental_realtime_start_instructions() -
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2486,6 +2523,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_end
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2502,6 +2540,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_end
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2577,6 +2616,7 @@ async fn snapshot_request_shape_remote_manual_compact_restates_realtime_start()
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2594,6 +2634,7 @@ async fn snapshot_request_shape_remote_manual_compact_restates_realtime_start()
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2677,6 +2718,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_does_not_restate_real
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2693,6 +2735,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_does_not_restate_real
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2784,6 +2827,7 @@ async fn snapshot_request_shape_remote_compact_resume_restates_realtime_end() ->
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2814,6 +2858,7 @@ async fn snapshot_request_shape_remote_compact_resume_restates_realtime_end() ->
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2903,6 +2948,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_us
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -2990,6 +3036,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -3012,6 +3059,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -3131,6 +3179,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceed
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -3145,6 +3194,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceed
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -3230,6 +3280,7 @@ async fn snapshot_request_shape_remote_mid_turn_continuation_compaction() -> Res
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -3308,6 +3359,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_reinject
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -3394,6 +3446,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_reinjec
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -3411,6 +3464,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_reinjec
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -3493,6 +3547,7 @@ async fn snapshot_request_shape_remote_manual_compact_without_previous_user_mess
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -606,6 +606,7 @@ async fn submit_user_input(codex: &codex_core::CodexThread, items: Vec<UserInput
items,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -783,6 +783,7 @@ async fn user_turn(conversation: &Arc<CodexThread>, text: &str) {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
+2
View File
@@ -54,6 +54,7 @@ async fn submit_user_turn(
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(test.cwd_path().to_path_buf()),
approval_policy: Some(approval_policy),
@@ -141,6 +142,7 @@ async fn execpolicy_blocks_shell_invocation() -> Result<()> {
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(test.cwd_path().to_path_buf()),
approval_policy: Some(AskForApproval::Never),
+2
View File
@@ -57,6 +57,7 @@ async fn fork_thread_twice_drops_to_first_message() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -181,6 +182,7 @@ async fn fork_thread_from_history_does_not_require_source_rollout_path() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
+2
View File
@@ -1850,6 +1850,7 @@ async fn blocked_queued_prompt_does_not_strand_earlier_accepted_prompt() -> Resu
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -1869,6 +1870,7 @@ async fn blocked_queued_prompt_does_not_strand_earlier_accepted_prompt() -> Resu
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -127,6 +127,7 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(cwd.path().to_path_buf()),
approval_policy: Some(AskForApproval::Never),
@@ -223,6 +224,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()>
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(cwd.path().to_path_buf()),
approval_policy: Some(AskForApproval::Never),
+10
View File
@@ -58,6 +58,7 @@ fn disabled_plan_turn(
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(cwd),
approval_policy: Some(AskForApproval::Never),
@@ -119,6 +120,7 @@ async fn user_message_item_is_emitted() -> anyhow::Result<()> {
items: vec![expected_input.clone()],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -178,6 +180,7 @@ async fn assistant_message_item_is_emitted() -> anyhow::Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -239,6 +242,7 @@ async fn reasoning_item_is_emitted() -> anyhow::Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -301,6 +305,7 @@ async fn web_search_item_is_emitted() -> anyhow::Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -381,6 +386,7 @@ async fn image_generation_call_event_is_emitted() -> anyhow::Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -468,6 +474,7 @@ async fn image_generation_call_event_is_emitted_when_image_save_fails() -> anyho
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -524,6 +531,7 @@ async fn agent_message_content_delta_has_item_metadata() -> anyhow::Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -1108,6 +1116,7 @@ async fn reasoning_content_delta_has_item_metadata() -> anyhow::Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -1163,6 +1172,7 @@ async fn reasoning_raw_content_delta_respects_flag() -> anyhow::Result<()> {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
+1
View File
@@ -83,6 +83,7 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> {
environments: None,
final_output_json_schema: Some(serde_json::from_str(SCHEMA)?),
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(cwd.path().to_path_buf()),
approval_policy: Some(AskForApproval::Never),
@@ -76,6 +76,7 @@ async fn submit_user_turn(
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(test.cwd.path().to_path_buf()),
approval_policy: Some(approval_policy),
+1
View File
@@ -29,6 +29,7 @@ pub static CODEX_ALIASES_TEMP_DIR: Option<TestBinaryDispatchGuard> = {
#[cfg(not(target_os = "windows"))]
mod abort_tasks;
mod additional_context;
mod agent_jobs;
mod agent_websocket;
mod agents_md;
@@ -48,6 +48,7 @@ fn read_only_user_turn(test: &TestCodex, items: Vec<UserInput>, model: String) -
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(test.cwd_path().to_path_buf()),
approval_policy: Some(AskForApproval::Never),
@@ -125,6 +125,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(first_turn_cwd),
approval_policy: Some(AskForApproval::Never),
@@ -160,6 +161,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(preturn_context_diff_cwd),
approval_policy: Some(AskForApproval::OnRequest),
@@ -249,6 +251,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(cwd_one.clone()),
approval_policy: Some(AskForApproval::Never),
@@ -282,6 +285,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(cwd_two),
approval_policy: Some(AskForApproval::Never),
@@ -365,6 +369,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -406,6 +411,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(resume_override_cwd),
approval_policy: Some(AskForApproval::Never),
@@ -479,6 +485,7 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() -
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -520,6 +527,7 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() -
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await?;
@@ -100,6 +100,7 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> {
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(test.cwd_path().to_path_buf()),
approval_policy: Some(codex_protocol::protocol::AskForApproval::Never),
@@ -112,6 +112,7 @@ async fn refresh_models_on_models_etag_mismatch_and_avoid_duplicate_models_fetch
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(cwd_path),
approval_policy: Some(AskForApproval::Never),
+22
View File
@@ -122,6 +122,7 @@ async fn responses_api_emits_api_request_event() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -168,6 +169,7 @@ async fn process_sse_emits_tracing_for_output_item() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -214,6 +216,7 @@ async fn process_sse_emits_failed_event_on_parse_error() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -261,6 +264,7 @@ async fn process_sse_records_failed_event_when_stream_closes_without_completed()
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -328,6 +332,7 @@ async fn process_sse_failed_event_records_response_error_message() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -393,6 +398,7 @@ async fn process_sse_failed_event_logs_parse_error() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -445,6 +451,7 @@ async fn process_sse_failed_event_logs_missing_error() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -506,6 +513,7 @@ async fn process_sse_failed_event_logs_response_completed_parse_error() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -561,6 +569,7 @@ async fn process_sse_emits_completed_telemetry() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -640,6 +649,7 @@ async fn turn_and_completed_response_spans_record_token_usage() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -728,6 +738,7 @@ async fn handle_responses_span_records_response_kind_and_tool_name() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -822,6 +833,7 @@ async fn record_responses_sets_span_fields_for_response_events() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -911,6 +923,7 @@ async fn handle_response_item_records_tool_result_for_custom_tool_call() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -987,6 +1000,7 @@ async fn handle_response_item_records_tool_result_for_function_call() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1064,6 +1078,7 @@ async fn handle_response_item_records_tool_result_for_shell_command_call() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1173,6 +1188,7 @@ async fn handle_shell_command_autoapprove_from_config_records_tool_decision() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1228,6 +1244,7 @@ async fn handle_shell_command_user_approved_records_tool_decision() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1298,6 +1315,7 @@ async fn handle_shell_command_user_approved_for_session_records_tool_decision()
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1368,6 +1386,7 @@ async fn handle_sandbox_error_user_approves_retry_records_tool_decision() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1438,6 +1457,7 @@ async fn handle_shell_command_user_denies_records_tool_decision() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1508,6 +1528,7 @@ async fn handle_sandbox_error_user_approves_for_session_records_tool_decision()
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -1579,6 +1600,7 @@ async fn handle_sandbox_error_user_denies_records_tool_decision() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -166,6 +166,7 @@ async fn submit_user_input(codex: &CodexThread, text: &str) {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -184,6 +185,7 @@ async fn submit_danger_full_access_user_turn(test: &TestCodex, text: &str) {
environments: None,
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(test.config.cwd.to_path_buf()),
approval_policy: Some(AskForApproval::Never),
@@ -211,6 +213,7 @@ async fn steer_user_input(codex: &CodexThread, text: &str) {
text: text.to_string(),
text_elements: Vec::new(),
}],
/*additional_context*/ Default::default(),
/*expected_turn_id*/ None,
/*responsesapi_client_metadata*/ None,
)
@@ -354,6 +357,7 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await
@@ -373,6 +377,7 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
additional_context: Default::default(),
thread_settings: Default::default(),
})
.await

Some files were not shown because too many files have changed in this diff Show More