mirror of
https://github.com/openai/codex.git
synced 2026-11-03 19:53:25 +00:00
Merge e177620c64 into sapling-pr-archive-bolinfest
This commit is contained in:
Generated
+1
@@ -2886,6 +2886,7 @@ dependencies = [
|
||||
"codex-app-server-protocol",
|
||||
"codex-client",
|
||||
"codex-file-system",
|
||||
"codex-network-proxy",
|
||||
"codex-protocol",
|
||||
"codex-sandboxing",
|
||||
"codex-shell-command",
|
||||
|
||||
@@ -177,6 +177,7 @@ fn sample_thread_with_metadata(
|
||||
) -> Thread {
|
||||
Thread {
|
||||
id: thread_id.to_string(),
|
||||
extra: None,
|
||||
session_id: format!("session-{thread_id}"),
|
||||
forked_from_id: None,
|
||||
parent_thread_id,
|
||||
@@ -219,7 +220,7 @@ fn sample_thread_start_response(
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_sources: Vec::new(),
|
||||
approval_policy: AppServerAskForApproval::OnFailure,
|
||||
approval_policy: AppServerAskForApproval::OnRequest,
|
||||
approvals_reviewer: AppServerApprovalsReviewer::User,
|
||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||
active_permission_profile: None,
|
||||
@@ -284,7 +285,7 @@ fn sample_thread_resume_response_with_source(
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_sources: Vec::new(),
|
||||
approval_policy: AppServerAskForApproval::OnFailure,
|
||||
approval_policy: AppServerAskForApproval::OnRequest,
|
||||
approvals_reviewer: AppServerApprovalsReviewer::User,
|
||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||
active_permission_profile: None,
|
||||
@@ -3026,6 +3027,7 @@ fn plugin_used_event_serializes_expected_shape() {
|
||||
"event_type": "codex_plugin_used",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
@@ -3056,6 +3058,7 @@ fn plugin_management_event_serializes_expected_shape() {
|
||||
"event_type": "codex_plugin_installed",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
@@ -3085,6 +3088,7 @@ fn plugin_install_failed_event_serializes_expected_shape() {
|
||||
"event_type": "codex_plugin_install_failed",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
@@ -3098,7 +3102,7 @@ fn plugin_install_failed_event_serializes_expected_shape() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_management_event_can_use_remote_plugin_id_override() {
|
||||
fn plugin_management_event_keeps_plugin_id_local_when_remote_id_exists() {
|
||||
let mut plugin = sample_plugin_metadata();
|
||||
plugin.remote_plugin_id = Some("plugins~Plugin_remote".to_string());
|
||||
let event = TrackEventRequest::PluginInstalled(CodexPluginEventRequest {
|
||||
@@ -3109,11 +3113,21 @@ fn plugin_management_event_can_use_remote_plugin_id_override() {
|
||||
let payload = serde_json::to_value(&event).expect("serialize plugin installed event");
|
||||
|
||||
assert_eq!(
|
||||
payload["event_params"]["plugin_id"],
|
||||
"plugins~Plugin_remote"
|
||||
payload,
|
||||
json!({
|
||||
"event_type": "codex_plugin_installed",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"remote_plugin_id": "plugins~Plugin_remote",
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
"mcp_server_count": 2,
|
||||
"connector_ids": ["calendar", "drive"],
|
||||
"product_client_id": originator().value
|
||||
}
|
||||
})
|
||||
);
|
||||
assert_eq!(payload["event_params"]["plugin_name"], "sample");
|
||||
assert_eq!(payload["event_params"]["marketplace_name"], "test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3453,6 +3467,7 @@ async fn reducer_ingests_plugin_state_changed_fact() {
|
||||
"event_type": "codex_plugin_disabled",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
@@ -3488,6 +3503,7 @@ async fn reducer_ingests_plugin_install_failed_fact() {
|
||||
"event_type": "codex_plugin_install_failed",
|
||||
"event_params": {
|
||||
"plugin_id": "sample@test",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "test",
|
||||
"has_skills": true,
|
||||
@@ -3505,7 +3521,7 @@ async fn reducer_ingests_plugin_install_failed_fact_without_detail() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut events = Vec::new();
|
||||
let plugin = PluginTelemetryMetadata {
|
||||
plugin_id: PluginId::parse("unknown@openai-curated-remote").expect("valid plugin id"),
|
||||
plugin_id: None,
|
||||
remote_plugin_id: Some("plugins~Plugin_00000000000000000000000000000000".to_string()),
|
||||
capability_summary: None,
|
||||
};
|
||||
@@ -3528,9 +3544,10 @@ async fn reducer_ingests_plugin_install_failed_fact_without_detail() {
|
||||
json!([{
|
||||
"event_type": "codex_plugin_install_failed",
|
||||
"event_params": {
|
||||
"plugin_id": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"plugin_name": "unknown",
|
||||
"marketplace_name": "openai-curated-remote",
|
||||
"plugin_id": null,
|
||||
"remote_plugin_id": "plugins~Plugin_00000000000000000000000000000000",
|
||||
"plugin_name": null,
|
||||
"marketplace_name": null,
|
||||
"has_skills": null,
|
||||
"mcp_server_count": null,
|
||||
"connector_ids": null,
|
||||
@@ -4569,7 +4586,7 @@ async fn turn_completed_without_started_notification_emits_null_started_at() {
|
||||
|
||||
fn sample_plugin_metadata() -> PluginTelemetryMetadata {
|
||||
PluginTelemetryMetadata {
|
||||
plugin_id: PluginId::parse("sample@test").expect("valid plugin id"),
|
||||
plugin_id: Some(PluginId::parse("sample@test").expect("valid plugin id")),
|
||||
remote_plugin_id: None,
|
||||
capability_summary: Some(PluginCapabilitySummary {
|
||||
config_name: "sample@test".to_string(),
|
||||
|
||||
@@ -38,6 +38,7 @@ use codex_app_server_protocol::ServerResponse;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::default_client::create_client;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::PluginTelemetryMetadata;
|
||||
use codex_protocol::request_permissions::RequestPermissionsResponse;
|
||||
use std::collections::HashSet;
|
||||
@@ -173,7 +174,15 @@ impl AnalyticsEventsQueue {
|
||||
if emitted.len() >= ANALYTICS_EVENT_DEDUPE_MAX_KEYS {
|
||||
emitted.clear();
|
||||
}
|
||||
emitted.insert((tracking.turn_id.clone(), plugin.plugin_id.as_key()))
|
||||
let Some(plugin_id) = plugin
|
||||
.plugin_id
|
||||
.as_ref()
|
||||
.map(PluginId::as_key)
|
||||
.or_else(|| plugin.remote_plugin_id.clone())
|
||||
else {
|
||||
return true;
|
||||
};
|
||||
emitted.insert((tracking.turn_id.clone(), plugin_id))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -277,6 +277,7 @@ fn sample_thread_archive_request() -> ClientRequest {
|
||||
fn sample_thread(thread_id: &str) -> Thread {
|
||||
Thread {
|
||||
id: thread_id.to_string(),
|
||||
extra: None,
|
||||
session_id: format!("session-{thread_id}"),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
@@ -309,7 +310,7 @@ fn sample_thread_start_response() -> ClientResponsePayload {
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_sources: Vec::new(),
|
||||
approval_policy: AppServerAskForApproval::OnFailure,
|
||||
approval_policy: AppServerAskForApproval::OnRequest,
|
||||
approvals_reviewer: AppServerApprovalsReviewer::User,
|
||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||
active_permission_profile: None,
|
||||
@@ -327,7 +328,7 @@ fn sample_thread_resume_response() -> ClientResponsePayload {
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_sources: Vec::new(),
|
||||
approval_policy: AppServerAskForApproval::OnFailure,
|
||||
approval_policy: AppServerAskForApproval::OnRequest,
|
||||
approvals_reviewer: AppServerApprovalsReviewer::User,
|
||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||
active_permission_profile: None,
|
||||
@@ -346,7 +347,7 @@ fn sample_thread_fork_response() -> ClientResponsePayload {
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_sources: Vec::new(),
|
||||
approval_policy: AppServerAskForApproval::OnFailure,
|
||||
approval_policy: AppServerAskForApproval::OnRequest,
|
||||
approvals_reviewer: AppServerApprovalsReviewer::User,
|
||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||
active_permission_profile: None,
|
||||
|
||||
@@ -26,6 +26,7 @@ use crate::now_unix_millis;
|
||||
use codex_app_server_protocol::CodexErrorInfo;
|
||||
use codex_app_server_protocol::CommandExecutionSource;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::PluginTelemetryMetadata;
|
||||
use codex_protocol::approvals::NetworkApprovalProtocol;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
@@ -933,6 +934,7 @@ pub(crate) struct CodexTurnSteerEventRequest {
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexPluginMetadata {
|
||||
pub(crate) plugin_id: Option<String>,
|
||||
pub(crate) remote_plugin_id: Option<String>,
|
||||
pub(crate) plugin_name: Option<String>,
|
||||
pub(crate) marketplace_name: Option<String>,
|
||||
pub(crate) has_skills: Option<bool>,
|
||||
@@ -1040,11 +1042,13 @@ pub(crate) fn codex_plugin_metadata(plugin: PluginTelemetryMetadata) -> CodexPlu
|
||||
remote_plugin_id,
|
||||
capability_summary,
|
||||
} = plugin;
|
||||
let event_plugin_id = remote_plugin_id.unwrap_or_else(|| plugin_id.as_key());
|
||||
CodexPluginMetadata {
|
||||
plugin_id: Some(event_plugin_id),
|
||||
plugin_name: Some(plugin_id.plugin_name),
|
||||
marketplace_name: Some(plugin_id.marketplace_name),
|
||||
plugin_id: plugin_id.as_ref().map(PluginId::as_key),
|
||||
remote_plugin_id,
|
||||
plugin_name: plugin_id
|
||||
.as_ref()
|
||||
.map(|plugin_id| plugin_id.plugin_name.clone()),
|
||||
marketplace_name: plugin_id.map(|plugin_id| plugin_id.marketplace_name),
|
||||
has_skills: capability_summary
|
||||
.as_ref()
|
||||
.map(|summary| summary.has_skills),
|
||||
|
||||
@@ -125,7 +125,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
|
||||
@@ -450,7 +450,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
@@ -3626,6 +3625,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -3765,6 +3765,10 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadGoal": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
@@ -4440,7 +4444,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
@@ -5111,6 +5115,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
+7
-2
@@ -6864,7 +6864,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
@@ -16831,6 +16830,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -17052,6 +17052,10 @@
|
||||
"title": "ThreadDeletedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadForkParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using a non-empty path, the thread_id param will be ignored. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.",
|
||||
@@ -18034,7 +18038,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/v2/LegacyAppPathString"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
@@ -19699,6 +19703,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
+7
-2
@@ -990,7 +990,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
@@ -14610,6 +14609,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -14831,6 +14831,10 @@
|
||||
"title": "ThreadDeletedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadForkParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "There are two ways to fork a thread: 1. By thread_id: load the thread from disk by thread_id and fork it into a new thread. 2. By path: load the thread from disk by path and fork it into a new thread.\n\nIf using a non-empty path, the thread_id param will be ignored. Empty string path values are treated as absent.\n\nPrefer using thread_id whenever possible.",
|
||||
@@ -15813,7 +15817,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
@@ -17478,6 +17482,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -173,7 +173,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
|
||||
-1
@@ -15,7 +15,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
|
||||
+1
-1
@@ -1106,7 +1106,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
|
||||
@@ -1106,7 +1106,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
|
||||
@@ -1250,7 +1250,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
@@ -1438,6 +1438,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
@@ -1059,6 +1058,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -1165,6 +1165,10 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1751,7 +1755,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
@@ -2017,6 +2021,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -865,6 +865,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -971,6 +972,10 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1557,7 +1562,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
@@ -1823,6 +1828,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
+7
-1
@@ -865,6 +865,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -971,6 +972,10 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1557,7 +1562,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
@@ -1823,6 +1828,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -865,6 +865,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -971,6 +972,10 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1557,7 +1562,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
@@ -1823,6 +1828,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -63,7 +63,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
@@ -1059,6 +1058,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -1165,6 +1165,10 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1751,7 +1755,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
@@ -2017,6 +2021,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -865,6 +865,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -971,6 +972,10 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1557,7 +1562,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
@@ -1823,6 +1828,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
-1
@@ -39,7 +39,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
@@ -1059,6 +1058,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -1165,6 +1165,10 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1751,7 +1755,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
@@ -2017,6 +2021,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
+7
-1
@@ -865,6 +865,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -971,6 +972,10 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1557,7 +1562,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
@@ -1823,6 +1828,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -865,6 +865,7 @@
|
||||
"description": "Optional Git metadata captured when the thread was created."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this thread. Codex-generated thread IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"modelProvider": {
|
||||
@@ -971,6 +972,10 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadExtra": {
|
||||
"description": "Extra app-server data for a thread.",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadId": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1557,7 +1562,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
@@ -1823,6 +1828,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
+2
-1
@@ -1250,7 +1250,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
@@ -1438,6 +1438,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
{
|
||||
"enum": [
|
||||
"untrusted",
|
||||
"on-failure",
|
||||
"on-request",
|
||||
"never"
|
||||
],
|
||||
|
||||
@@ -1250,7 +1250,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
@@ -1438,6 +1438,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -1250,7 +1250,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
"$ref": "#/definitions/LegacyAppPathString"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
@@ -1438,6 +1438,7 @@
|
||||
"description": "Only populated when the Turn's status is failed."
|
||||
},
|
||||
"id": {
|
||||
"description": "Identifier for this turn. Codex-generated turn IDs are UUIDv7.",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
|
||||
@@ -2,4 +2,9 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Identifier for a Codex thread.
|
||||
*
|
||||
* Codex-generated thread IDs are UUIDv7, and some use cases rely on that.
|
||||
*/
|
||||
export type ThreadId = string;
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type AskForApproval = "untrusted" | "on-failure" | "on-request" | { "granular": { sandbox_approval: boolean, rules: boolean, skill_approval: boolean, request_permissions: boolean, mcp_elicitations: boolean, } } | "never";
|
||||
export type AskForApproval = "untrusted" | "on-request" | { "granular": { sandbox_approval: boolean, rules: boolean, skill_approval: boolean, request_permissions: boolean, mcp_elicitations: boolean, } } | "never";
|
||||
|
||||
+24
-41
@@ -8,87 +8,70 @@ import type { ThreadSource } from "./ThreadSource";
|
||||
import type { ThreadStatus } from "./ThreadStatus";
|
||||
import type { Turn } from "./Turn";
|
||||
|
||||
export type Thread = { id: string,
|
||||
/**
|
||||
export type Thread = {/**
|
||||
* Identifier for this thread. Codex-generated thread IDs are UUIDv7.
|
||||
*/
|
||||
id: string, /**
|
||||
* Session id shared by threads that belong to the same session tree.
|
||||
*/
|
||||
sessionId: string,
|
||||
/**
|
||||
sessionId: string, /**
|
||||
* Source thread id when this thread was created by forking another thread.
|
||||
*/
|
||||
forkedFromId: string | null,
|
||||
/**
|
||||
forkedFromId: string | null, /**
|
||||
* The ID of the parent thread. This will only be set if this thread is a subagent.
|
||||
*/
|
||||
parentThreadId: string | null,
|
||||
/**
|
||||
parentThreadId: string | null, /**
|
||||
* Usually the first user message in the thread, if available.
|
||||
*/
|
||||
preview: string,
|
||||
/**
|
||||
preview: string, /**
|
||||
* Whether the thread is ephemeral and should not be materialized on disk.
|
||||
*/
|
||||
ephemeral: boolean,
|
||||
/**
|
||||
ephemeral: boolean, /**
|
||||
* Model provider used for this thread (for example, 'openai').
|
||||
*/
|
||||
modelProvider: string,
|
||||
/**
|
||||
modelProvider: string, /**
|
||||
* Unix timestamp (in seconds) when the thread was created.
|
||||
*/
|
||||
createdAt: number,
|
||||
/**
|
||||
createdAt: number, /**
|
||||
* Unix timestamp (in seconds) when the thread was last updated.
|
||||
*/
|
||||
updatedAt: number,
|
||||
/**
|
||||
updatedAt: number, /**
|
||||
* Unix timestamp (in seconds) used for thread recency ordering.
|
||||
*/
|
||||
recencyAt: number | null,
|
||||
/**
|
||||
recencyAt: number | null, /**
|
||||
* Current runtime status for the thread.
|
||||
*/
|
||||
status: ThreadStatus,
|
||||
/**
|
||||
status: ThreadStatus, /**
|
||||
* [UNSTABLE] Path to the thread on disk.
|
||||
*/
|
||||
path: string | null,
|
||||
/**
|
||||
path: string | null, /**
|
||||
* Working directory captured for the thread.
|
||||
*/
|
||||
cwd: AbsolutePathBuf,
|
||||
/**
|
||||
cwd: AbsolutePathBuf, /**
|
||||
* Version of the CLI that created the thread.
|
||||
*/
|
||||
cliVersion: string,
|
||||
/**
|
||||
cliVersion: string, /**
|
||||
* Origin of the thread (CLI, VSCode, codex exec, codex app-server, etc.).
|
||||
*/
|
||||
source: SessionSource,
|
||||
/**
|
||||
source: SessionSource, /**
|
||||
* Optional analytics source classification for this thread.
|
||||
*/
|
||||
threadSource: ThreadSource | null,
|
||||
/**
|
||||
threadSource: ThreadSource | null, /**
|
||||
* Optional random unique nickname assigned to an AgentControl-spawned sub-agent.
|
||||
*/
|
||||
agentNickname: string | null,
|
||||
/**
|
||||
agentNickname: string | null, /**
|
||||
* Optional role (agent_role) assigned to an AgentControl-spawned sub-agent.
|
||||
*/
|
||||
agentRole: string | null,
|
||||
/**
|
||||
agentRole: string | null, /**
|
||||
* Optional Git metadata captured when the thread was created.
|
||||
*/
|
||||
gitInfo: GitInfo | null,
|
||||
/**
|
||||
gitInfo: GitInfo | null, /**
|
||||
* Optional user-facing thread title.
|
||||
*/
|
||||
name: string | null,
|
||||
/**
|
||||
name: string | null, /**
|
||||
* Only populated on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read`
|
||||
* (when `includeTurns` is true) responses.
|
||||
* For all other responses and notifications returning a Thread,
|
||||
* the turns field will be an empty list.
|
||||
*/
|
||||
turns: Array<Turn>, };
|
||||
turns: Array<Turn>};
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* Extra app-server data for a thread.
|
||||
*/
|
||||
export type ThreadExtra = Record<string, never>;
|
||||
@@ -105,4 +105,4 @@ reasoningEffort: ReasoningEffort | null,
|
||||
/**
|
||||
* Last known status of the target agents, when available.
|
||||
*/
|
||||
agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: AbsolutePathBuf, } | { "type": "sleep", id: string, durationMs: number, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: AbsolutePathBuf, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, };
|
||||
agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: LegacyAppPathString, } | { "type": "sleep", id: string, durationMs: number, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: AbsolutePathBuf, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, };
|
||||
|
||||
+5
-1
@@ -6,7 +6,11 @@ import type { TurnError } from "./TurnError";
|
||||
import type { TurnItemsView } from "./TurnItemsView";
|
||||
import type { TurnStatus } from "./TurnStatus";
|
||||
|
||||
export type Turn = { id: string,
|
||||
export type Turn = {
|
||||
/**
|
||||
* Identifier for this turn. Codex-generated turn IDs are UUIDv7.
|
||||
*/
|
||||
id: string,
|
||||
/**
|
||||
* Thread items currently included in this turn payload.
|
||||
*/
|
||||
|
||||
@@ -395,6 +395,7 @@ export type { ThreadCompactStartResponse } from "./ThreadCompactStartResponse";
|
||||
export type { ThreadDeleteParams } from "./ThreadDeleteParams";
|
||||
export type { ThreadDeleteResponse } from "./ThreadDeleteResponse";
|
||||
export type { ThreadDeletedNotification } from "./ThreadDeletedNotification";
|
||||
export type { ThreadExtra } from "./ThreadExtra";
|
||||
export type { ThreadForkParams } from "./ThreadForkParams";
|
||||
export type { ThreadForkResponse } from "./ThreadForkResponse";
|
||||
export type { ThreadGoal } from "./ThreadGoal";
|
||||
|
||||
@@ -2573,6 +2573,7 @@ mod tests {
|
||||
response: v2::ThreadStartResponse {
|
||||
thread: v2::Thread {
|
||||
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
|
||||
extra: None,
|
||||
session_id: "67e55044-10b1-426f-9247-bb680e5fe0c7".to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
@@ -2604,7 +2605,7 @@ mod tests {
|
||||
"/tmp/AGENTS.md",
|
||||
)),
|
||||
],
|
||||
approval_policy: v2::AskForApproval::OnFailure,
|
||||
approval_policy: v2::AskForApproval::OnRequest,
|
||||
approvals_reviewer: v2::ApprovalsReviewer::User,
|
||||
sandbox: v2::SandboxPolicy::DangerFullAccess,
|
||||
active_permission_profile: None,
|
||||
@@ -2622,6 +2623,7 @@ mod tests {
|
||||
"response": {
|
||||
"thread": {
|
||||
"id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
|
||||
"extra": null,
|
||||
"sessionId": "67e55044-10b1-426f-9247-bb680e5fe0c7",
|
||||
"forkedFromId": null,
|
||||
"parentThreadId": null,
|
||||
@@ -2651,7 +2653,7 @@ mod tests {
|
||||
"cwd": absolute_path_string("tmp"),
|
||||
"runtimeWorkspaceRoots": [],
|
||||
"instructionSources": [absolute_path_string("tmp/AGENTS.md")],
|
||||
"approvalPolicy": "on-failure",
|
||||
"approvalPolicy": "on-request",
|
||||
"approvalsReviewer": "user",
|
||||
"sandbox": {
|
||||
"type": "dangerFullAccess"
|
||||
|
||||
@@ -836,7 +836,7 @@ impl ThreadHistoryBuilder {
|
||||
fn handle_view_image_tool_call(&mut self, payload: &ViewImageToolCallEvent) {
|
||||
let item = ThreadItem::ImageView {
|
||||
id: payload.call_id.clone(),
|
||||
path: payload.path.clone(),
|
||||
path: payload.path.clone().into(),
|
||||
};
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
|
||||
@@ -356,7 +356,10 @@ pub enum ThreadItem {
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
ImageView { id: String, path: AbsolutePathBuf },
|
||||
ImageView {
|
||||
id: String,
|
||||
path: LegacyAppPathString,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
Sleep {
|
||||
@@ -855,7 +858,7 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
},
|
||||
CoreTurnItem::ImageView(image) => ThreadItem::ImageView {
|
||||
id: image.id,
|
||||
path: image.path,
|
||||
path: image.path.into(),
|
||||
},
|
||||
CoreTurnItem::Sleep(sleep) => ThreadItem::Sleep {
|
||||
id: sleep.id,
|
||||
|
||||
@@ -163,7 +163,6 @@ pub enum AskForApproval {
|
||||
#[serde(rename = "untrusted")]
|
||||
#[ts(rename = "untrusted")]
|
||||
UnlessTrusted,
|
||||
OnFailure,
|
||||
OnRequest,
|
||||
#[experimental("askForApproval.granular")]
|
||||
Granular {
|
||||
@@ -182,7 +181,6 @@ impl AskForApproval {
|
||||
pub fn to_core(self) -> CoreAskForApproval {
|
||||
match self {
|
||||
AskForApproval::UnlessTrusted => CoreAskForApproval::UnlessTrusted,
|
||||
AskForApproval::OnFailure => CoreAskForApproval::OnFailure,
|
||||
AskForApproval::OnRequest => CoreAskForApproval::OnRequest,
|
||||
AskForApproval::Granular {
|
||||
sandbox_approval,
|
||||
@@ -206,7 +204,6 @@ impl From<CoreAskForApproval> for AskForApproval {
|
||||
fn from(value: CoreAskForApproval) -> Self {
|
||||
match value {
|
||||
CoreAskForApproval::UnlessTrusted => AskForApproval::UnlessTrusted,
|
||||
CoreAskForApproval::OnFailure => AskForApproval::OnFailure,
|
||||
CoreAskForApproval::OnRequest => AskForApproval::OnRequest,
|
||||
CoreAskForApproval::Granular(granular_config) => AskForApproval::Granular {
|
||||
sandbox_approval: granular_config.sandbox_approval,
|
||||
|
||||
@@ -168,6 +168,7 @@ fn thread_resume_response_round_trips_initial_turns_page() {
|
||||
let response = ThreadResumeResponse {
|
||||
thread: Thread {
|
||||
id: "thr_123".to_string(),
|
||||
extra: None,
|
||||
session_id: "thr_123".to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
@@ -195,7 +196,7 @@ fn thread_resume_response_round_trips_initial_turns_page() {
|
||||
cwd: absolute_path("tmp"),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
instruction_sources: Vec::new(),
|
||||
approval_policy: AskForApproval::OnFailure,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
sandbox: SandboxPolicy::DangerFullAccess,
|
||||
active_permission_profile: None,
|
||||
@@ -2573,14 +2574,14 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
|
||||
let image_view_item = TurnItem::ImageView(ImageViewItem {
|
||||
id: "view-image-1".to_string(),
|
||||
path: test_path_buf("/tmp/view-image.png").abs(),
|
||||
path: PathUri::from_abs_path(&test_path_buf("/tmp/view-image.png").abs()),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
ThreadItem::from(image_view_item),
|
||||
ThreadItem::ImageView {
|
||||
id: "view-image-1".to_string(),
|
||||
path: test_path_buf("/tmp/view-image.png").abs(),
|
||||
path: LegacyAppPathString::from_abs_path(&test_path_buf("/tmp/view-image.png").abs()),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -3689,7 +3690,7 @@ fn thread_lifecycle_responses_default_missing_optional_fields() {
|
||||
"modelProvider": "openai",
|
||||
"serviceTier": null,
|
||||
"cwd": absolute_path_string("tmp"),
|
||||
"approvalPolicy": "on-failure",
|
||||
"approvalPolicy": "on-request",
|
||||
"approvalsReviewer": "user",
|
||||
"sandbox": { "type": "dangerFullAccess" },
|
||||
"reasoningEffort": null
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::CodexErrorInfo;
|
||||
use super::ThreadItem;
|
||||
use super::ThreadStatus;
|
||||
use super::TurnStatus;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
use codex_protocol::protocol::SessionSource as CoreSessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource;
|
||||
use codex_protocol::protocol::ThreadSource as CoreThreadSource;
|
||||
@@ -120,6 +121,12 @@ impl From<ThreadSource> for CoreThreadSource {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extra app-server data for a thread.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
||||
pub struct ThreadExtra {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -129,11 +136,15 @@ pub struct GitInfo {
|
||||
pub origin_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct Thread {
|
||||
/// Identifier for this thread. Codex-generated thread IDs are UUIDv7.
|
||||
pub id: String,
|
||||
/// Optional implementation-specific thread data.
|
||||
#[experimental("thread.extra")]
|
||||
pub extra: Option<ThreadExtra>,
|
||||
/// Session id shared by threads that belong to the same session tree.
|
||||
pub session_id: String,
|
||||
/// Source thread id when this thread was created by forking another thread.
|
||||
@@ -186,6 +197,7 @@ pub struct Thread {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct Turn {
|
||||
/// Identifier for this turn. Codex-generated turn IDs are UUIDv7.
|
||||
pub id: String,
|
||||
/// Thread items currently included in this turn payload.
|
||||
pub items: Vec<ThreadItem>,
|
||||
|
||||
@@ -46,12 +46,13 @@ cargo run -p codex-app-server-test-client -- \
|
||||
|
||||
Use `--capture-file /tmp/plugin-analytics.jsonl` to select the output path.
|
||||
The command validates one `codex_plugin_disabled`, `codex_plugin_enabled`, and
|
||||
`codex_plugin_used` event with the expected local plugin identity and capability
|
||||
metadata. The enabled and disabled events come from successful writes to the
|
||||
temporary config; the command does not mutate the remote enabled state. It
|
||||
prints the events and leaves the JSONL file in place for inspection. It does not
|
||||
install or uninstall plugins and does not modify the profile's persistent
|
||||
config.
|
||||
`codex_plugin_used` event with the expected local and remote plugin identities
|
||||
and capability metadata. Each event includes the local ID in `plugin_id` and the
|
||||
backend ID in `remote_plugin_id`. The enabled and disabled events come from
|
||||
successful writes to the temporary config; the command does not mutate the
|
||||
remote enabled state. It prints the events and leaves the JSONL file in place
|
||||
for inspection. It does not install or uninstall plugins and does not modify
|
||||
the profile's persistent config.
|
||||
|
||||
### Testing remote install and uninstall analytics
|
||||
|
||||
@@ -63,9 +64,11 @@ or CI.
|
||||
Choose a remote plugin that is available to the active account and is not
|
||||
currently installed. The command refuses to run when the plugin is already
|
||||
installed, installs it, validates `codex_plugin_installed`, uninstalls it, and
|
||||
verifies that the original uninstalled state was restored. The current install
|
||||
event uses the backend ID as `plugin_id`. Uninstall is part of cleanup but is
|
||||
not yet an analytics assertion.
|
||||
validates `codex_plugin_uninstalled`, and verifies that the original
|
||||
uninstalled state was restored.
|
||||
|
||||
The mutation events include the local Codex ID in `plugin_id` and the backend ID
|
||||
in `remote_plugin_id`.
|
||||
|
||||
`--remote-plugin-id` takes the backend ID, such as `plugins~Plugin_...`, not the
|
||||
local `<plugin>@<marketplace>` ID.
|
||||
@@ -83,7 +86,7 @@ Analytics use the normal queue, reduction, batching, and serialization path,
|
||||
but the debug capture destination suppresses analytics network delivery. The
|
||||
command prints one of these final states:
|
||||
|
||||
- `PASS`: the install event validated and the plugin is uninstalled.
|
||||
- `PASS`: the install and uninstall events validated and the plugin is uninstalled.
|
||||
- `FAIL-CLEAN`: validation failed, but the original uninstalled state was
|
||||
restored.
|
||||
- `FAIL-LOCAL-CACHE`: the backend is uninstalled, but local cleanup reported
|
||||
|
||||
@@ -35,7 +35,7 @@ pub(super) fn read_events_for_remote_plugin(
|
||||
matching.extend(
|
||||
events
|
||||
.iter()
|
||||
.filter(|event| event["event_params"]["plugin_id"] == remote_plugin_id)
|
||||
.filter(|event| event["event_params"]["remote_plugin_id"] == remote_plugin_id)
|
||||
.cloned(),
|
||||
);
|
||||
}
|
||||
@@ -44,6 +44,7 @@ pub(super) fn read_events_for_remote_plugin(
|
||||
|
||||
pub(super) struct PluginEventIdentity<'a> {
|
||||
pub(super) plugin_id: &'a str,
|
||||
pub(super) remote_plugin_id: &'a str,
|
||||
pub(super) plugin_name: &'a str,
|
||||
pub(super) marketplace_name: &'a str,
|
||||
}
|
||||
@@ -52,25 +53,29 @@ pub(super) fn validate_mutation_events(
|
||||
events: Vec<Value>,
|
||||
expected: PluginEventIdentity<'_>,
|
||||
) -> Result<Vec<Value>> {
|
||||
let event_type = "codex_plugin_installed";
|
||||
let matching = events
|
||||
.iter()
|
||||
.filter(|event| event["event_type"] == event_type)
|
||||
.collect::<Vec<_>>();
|
||||
let [event] = matching.as_slice() else {
|
||||
bail!(
|
||||
"expected exactly one `{event_type}` event for `{}`, found {}",
|
||||
expected.plugin_id,
|
||||
matching.len()
|
||||
);
|
||||
};
|
||||
validate_event(event, &expected)?;
|
||||
Ok(vec![(*event).clone()])
|
||||
let mut validated = Vec::new();
|
||||
for event_type in ["codex_plugin_installed", "codex_plugin_uninstalled"] {
|
||||
let matching = events
|
||||
.iter()
|
||||
.filter(|event| event["event_type"] == event_type)
|
||||
.collect::<Vec<_>>();
|
||||
let [event] = matching.as_slice() else {
|
||||
bail!(
|
||||
"expected exactly one `{event_type}` event for `{}`, found {}",
|
||||
expected.remote_plugin_id,
|
||||
matching.len()
|
||||
);
|
||||
};
|
||||
validate_event(event, &expected)?;
|
||||
validated.push((*event).clone());
|
||||
}
|
||||
Ok(validated)
|
||||
}
|
||||
|
||||
fn validate_event(event: &Value, expected: &PluginEventIdentity<'_>) -> Result<()> {
|
||||
let params = &event["event_params"];
|
||||
require_string(params, "plugin_id", expected.plugin_id)?;
|
||||
require_string(params, "remote_plugin_id", expected.remote_plugin_id)?;
|
||||
require_string(params, "plugin_name", expected.plugin_name)?;
|
||||
require_string(params, "marketplace_name", expected.marketplace_name)?;
|
||||
for field in [
|
||||
|
||||
@@ -14,15 +14,17 @@ const REMOTE_PLUGIN_ID: &str = "plugins~Plugin_test";
|
||||
fn reads_and_validates_remote_plugin_mutation_events() {
|
||||
let path = unique_capture_path("valid");
|
||||
let installed = mutation_event("codex_plugin_installed");
|
||||
let uninstalled = mutation_event("codex_plugin_uninstalled");
|
||||
let unrelated = json!({
|
||||
"event_type": "codex_plugin_installed",
|
||||
"event_params": {
|
||||
"plugin_id": "plugins~Plugin_other"
|
||||
"plugin_id": "other@openai-curated-remote",
|
||||
"remote_plugin_id": "plugins~Plugin_other"
|
||||
}
|
||||
});
|
||||
let contents = [
|
||||
json!({"events": [unrelated]}),
|
||||
json!({"events": [installed]}),
|
||||
json!({"events": [installed, uninstalled]}),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|payload| serde_json::to_string(&payload).expect("serialize capture payload"))
|
||||
@@ -35,7 +37,7 @@ fn reads_and_validates_remote_plugin_mutation_events() {
|
||||
let validated =
|
||||
validate_mutation_events(events, expected_identity()).expect("validate mutation events");
|
||||
|
||||
assert_eq!(validated, vec![installed]);
|
||||
assert_eq!(validated, vec![installed, uninstalled]);
|
||||
fs::remove_file(path).expect("remove capture file");
|
||||
}
|
||||
|
||||
@@ -62,7 +64,8 @@ fn mutation_event(event_type: &str) -> Value {
|
||||
json!({
|
||||
"event_type": event_type,
|
||||
"event_params": {
|
||||
"plugin_id": REMOTE_PLUGIN_ID,
|
||||
"plugin_id": "sample@openai-curated-remote",
|
||||
"remote_plugin_id": REMOTE_PLUGIN_ID,
|
||||
"plugin_name": "sample",
|
||||
"marketplace_name": "openai-curated-remote",
|
||||
"has_skills": true,
|
||||
@@ -75,7 +78,8 @@ fn mutation_event(event_type: &str) -> Value {
|
||||
|
||||
fn expected_identity() -> PluginEventIdentity<'static> {
|
||||
PluginEventIdentity {
|
||||
plugin_id: REMOTE_PLUGIN_ID,
|
||||
plugin_id: "sample@openai-curated-remote",
|
||||
remote_plugin_id: REMOTE_PLUGIN_ID,
|
||||
plugin_name: "sample",
|
||||
marketplace_name: "openai-curated-remote",
|
||||
}
|
||||
|
||||
@@ -312,13 +312,19 @@ fn run_mutation_sequence(
|
||||
state_err
|
||||
}
|
||||
})?;
|
||||
wait_for_remote_plugin_event(
|
||||
capture_path,
|
||||
&expected.remote_plugin_id,
|
||||
"codex_plugin_uninstalled",
|
||||
)?;
|
||||
|
||||
let captured_events =
|
||||
read_events_for_remote_plugin(capture_path, &expected.remote_plugin_id)?;
|
||||
let events = validate_mutation_events(
|
||||
captured_events,
|
||||
PluginEventIdentity {
|
||||
plugin_id: &expected.remote_plugin_id,
|
||||
plugin_id: &expected.plugin_id,
|
||||
remote_plugin_id: &expected.remote_plugin_id,
|
||||
plugin_name: &expected.plugin_name,
|
||||
marketplace_name: &expected.marketplace_name,
|
||||
},
|
||||
|
||||
@@ -159,6 +159,7 @@ fn wait_for_plugin_usage(
|
||||
#[derive(Debug)]
|
||||
struct ExpectedPlugin {
|
||||
plugin_id: String,
|
||||
remote_plugin_id: String,
|
||||
plugin_name: String,
|
||||
marketplace_name: String,
|
||||
}
|
||||
@@ -208,13 +209,15 @@ fn expected_plugin(response: &PluginInstalledResponse, plugin_id: &str) -> Resul
|
||||
plugin.availability
|
||||
);
|
||||
}
|
||||
plugin
|
||||
let remote_plugin_id = plugin
|
||||
.remote_plugin_id
|
||||
.as_ref()
|
||||
.with_context(|| format!("plugin `{plugin_id}` does not have a remote plugin id"))?;
|
||||
.with_context(|| format!("plugin `{plugin_id}` does not have a remote plugin id"))?
|
||||
.clone();
|
||||
|
||||
Ok(ExpectedPlugin {
|
||||
plugin_id: plugin.id.clone(),
|
||||
remote_plugin_id,
|
||||
plugin_name: plugin.name.clone(),
|
||||
marketplace_name: marketplace.name.clone(),
|
||||
})
|
||||
@@ -444,6 +447,7 @@ fn event_count(events: &[Value], event_type: &str) -> usize {
|
||||
fn validate_identity(event: &Value, expected: &ExpectedPlugin) -> Result<()> {
|
||||
let params = &event["event_params"];
|
||||
require_string(params, "plugin_id", &expected.plugin_id)?;
|
||||
require_string(params, "remote_plugin_id", &expected.remote_plugin_id)?;
|
||||
require_string(params, "plugin_name", &expected.plugin_name)?;
|
||||
require_string(params, "marketplace_name", &expected.marketplace_name)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ use codex_mcp::McpOAuthLoginSupport;
|
||||
use codex_mcp::oauth_login_support;
|
||||
use codex_mcp::should_retry_without_scopes;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::PluginTelemetryMetadata;
|
||||
use codex_rmcp_client::perform_oauth_login_silent;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -1528,6 +1529,7 @@ impl PluginRequestProcessor {
|
||||
self.track_plugin_install_failed_for_remote_plugin(
|
||||
&remote_plugin_id,
|
||||
&remote_marketplace_name,
|
||||
/*plugin_id*/ None,
|
||||
error_type,
|
||||
err.to_string(),
|
||||
);
|
||||
@@ -1538,6 +1540,12 @@ impl PluginRequestProcessor {
|
||||
})?;
|
||||
let actual_remote_marketplace_name = remote_detail.marketplace_name.clone();
|
||||
let remote_plugin_name = remote_detail.summary.name.clone();
|
||||
let resolved_plugin_id = PluginId::parse(&remote_detail.summary.id).map_err(|err| {
|
||||
internal_error(format!(
|
||||
"invalid resolved plugin id `{}`: {err}",
|
||||
remote_detail.summary.id
|
||||
))
|
||||
})?;
|
||||
if remote_detail.summary.availability == PluginAvailability::DisabledByAdmin {
|
||||
return Err(invalid_request(format!(
|
||||
"remote plugin {remote_plugin_id} is disabled by admin"
|
||||
@@ -1569,6 +1577,7 @@ impl PluginRequestProcessor {
|
||||
self.track_plugin_install_failed_for_remote_plugin(
|
||||
&remote_plugin_id,
|
||||
&actual_remote_marketplace_name,
|
||||
Some(&resolved_plugin_id),
|
||||
error_type,
|
||||
err.to_string(),
|
||||
);
|
||||
@@ -1585,6 +1594,7 @@ impl PluginRequestProcessor {
|
||||
self.track_plugin_install_failed_for_remote_plugin(
|
||||
&remote_plugin_id,
|
||||
&actual_remote_marketplace_name,
|
||||
Some(&resolved_plugin_id),
|
||||
error_type,
|
||||
err.to_string(),
|
||||
);
|
||||
@@ -1606,6 +1616,7 @@ impl PluginRequestProcessor {
|
||||
self.track_plugin_install_failed_for_remote_plugin(
|
||||
&remote_plugin_id,
|
||||
&actual_remote_marketplace_name,
|
||||
Some(&result.plugin_id),
|
||||
error_type,
|
||||
err.to_string(),
|
||||
);
|
||||
@@ -1702,6 +1713,7 @@ impl PluginRequestProcessor {
|
||||
&self,
|
||||
remote_plugin_id: &str,
|
||||
marketplace_name: &str,
|
||||
plugin_id: Option<&PluginId>,
|
||||
error_type: &'static str,
|
||||
error_message: String,
|
||||
) {
|
||||
@@ -1712,16 +1724,17 @@ impl PluginRequestProcessor {
|
||||
error = %error_message,
|
||||
"remote plugin install failed"
|
||||
);
|
||||
// The remote id is reported separately; this local name only satisfies
|
||||
// PluginId validation before remote details are available.
|
||||
let Ok(plugin_id) = PluginId::new("unknown".to_string(), marketplace_name.to_string())
|
||||
else {
|
||||
return;
|
||||
let plugin = if let Some(plugin_id) = plugin_id {
|
||||
self.thread_manager
|
||||
.plugins_manager()
|
||||
.telemetry_metadata_for_plugin_id_with_remote_id(plugin_id, remote_plugin_id)
|
||||
} else {
|
||||
PluginTelemetryMetadata {
|
||||
plugin_id: None,
|
||||
remote_plugin_id: Some(remote_plugin_id.to_string()),
|
||||
capability_summary: None,
|
||||
}
|
||||
};
|
||||
let plugin = self
|
||||
.thread_manager
|
||||
.plugins_manager()
|
||||
.telemetry_metadata_for_plugin_id_with_remote_id(&plugin_id, remote_plugin_id);
|
||||
self.analytics_events_client
|
||||
.track_plugin_install_failed(plugin, error_type.to_string());
|
||||
}
|
||||
@@ -1980,11 +1993,31 @@ impl PluginRequestProcessor {
|
||||
let remote_plugin_service_config = RemotePluginServiceConfig {
|
||||
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
||||
};
|
||||
let uninstall_target = codex_core_plugins::remote::resolve_remote_plugin_uninstall_target(
|
||||
&remote_plugin_service_config,
|
||||
auth.as_ref(),
|
||||
&plugin_id,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
remote_plugin_catalog_error_to_jsonrpc(err, "resolve remote plugin before uninstall")
|
||||
})?;
|
||||
let plugins_manager = self.thread_manager.plugins_manager();
|
||||
let mut plugin_telemetry = plugins_manager
|
||||
.telemetry_metadata_for_installed_plugin_with_remote_id(
|
||||
&uninstall_target.plugin_id,
|
||||
&uninstall_target.remote_plugin_id,
|
||||
)
|
||||
.await;
|
||||
if plugin_telemetry.capability_summary.is_none() {
|
||||
plugin_telemetry.capability_summary =
|
||||
Some(uninstall_target.fallback_capability_summary.clone());
|
||||
}
|
||||
let uninstall_result = codex_core_plugins::remote::uninstall_remote_plugin(
|
||||
&remote_plugin_service_config,
|
||||
auth.as_ref(),
|
||||
config.codex_home.to_path_buf(),
|
||||
&plugin_id,
|
||||
uninstall_target,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1992,7 +2025,8 @@ impl PluginRequestProcessor {
|
||||
&uninstall_result,
|
||||
Ok(()) | Err(RemotePluginCatalogError::CacheRemove(_))
|
||||
) {
|
||||
let plugins_manager = self.thread_manager.plugins_manager();
|
||||
self.analytics_events_client
|
||||
.track_plugin_uninstalled(plugin_telemetry);
|
||||
if plugins_manager.clear_remote_installed_plugins_cache() {
|
||||
self.on_effective_plugins_changed();
|
||||
}
|
||||
|
||||
@@ -4156,6 +4156,7 @@ pub(crate) fn thread_from_stored_thread(
|
||||
let thread_id = thread.thread_id.to_string();
|
||||
let thread = Thread {
|
||||
id: thread_id.clone(),
|
||||
extra: None,
|
||||
session_id: thread_id,
|
||||
forked_from_id: thread.forked_from_id.map(|id| id.to_string()),
|
||||
parent_thread_id: thread.parent_thread_id.map(|id| id.to_string()),
|
||||
@@ -4366,6 +4367,7 @@ fn build_thread_from_snapshot(
|
||||
let now = time::OffsetDateTime::now_utc().unix_timestamp();
|
||||
Thread {
|
||||
id: thread_id.to_string(),
|
||||
extra: None,
|
||||
session_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: config_snapshot.parent_thread_id.map(|id| id.to_string()),
|
||||
|
||||
@@ -183,6 +183,7 @@ mod tests {
|
||||
fn test_thread(items: Vec<ThreadItem>) -> Thread {
|
||||
Thread {
|
||||
id: "thread-1".to_string(),
|
||||
extra: None,
|
||||
session_id: "session-1".to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
|
||||
@@ -314,6 +314,7 @@ pub(crate) fn summary_to_thread(
|
||||
let thread_id = conversation_id.to_string();
|
||||
Thread {
|
||||
id: thread_id.clone(),
|
||||
extra: None,
|
||||
session_id: thread_id,
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
|
||||
@@ -889,6 +889,7 @@ mod tests {
|
||||
fn test_thread(thread_id: &str, source: codex_app_server_protocol::SessionSource) -> Thread {
|
||||
Thread {
|
||||
id: thread_id.to_string(),
|
||||
extra: None,
|
||||
session_id: thread_id.to_string(),
|
||||
forked_from_id: None,
|
||||
parent_thread_id: None,
|
||||
|
||||
@@ -538,12 +538,10 @@ async fn plugin_install_tracks_analytics_when_remote_detail_fetch_fails() -> Res
|
||||
payload["events"][0]["event_type"],
|
||||
"codex_plugin_install_failed"
|
||||
);
|
||||
assert_eq!(event_params["plugin_id"], REMOTE_PLUGIN_ID);
|
||||
assert_eq!(event_params["plugin_name"], "unknown");
|
||||
assert_eq!(
|
||||
event_params["marketplace_name"],
|
||||
"caller-marketplace-is-ignored"
|
||||
);
|
||||
assert_eq!(event_params["plugin_id"], json!(null));
|
||||
assert_eq!(event_params["remote_plugin_id"], REMOTE_PLUGIN_ID);
|
||||
assert_eq!(event_params["plugin_name"], json!(null));
|
||||
assert_eq!(event_params["marketplace_name"], json!(null));
|
||||
assert_eq!(
|
||||
event_params["error_type"],
|
||||
"remote_catalog_unexpected_status"
|
||||
@@ -883,6 +881,7 @@ async fn plugin_install_tracks_analytics_event() -> Result<()> {
|
||||
"event_type": "codex_plugin_installed",
|
||||
"event_params": {
|
||||
"plugin_id": "sample-plugin@debug",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample-plugin",
|
||||
"marketplace_name": "debug",
|
||||
"has_skills": false,
|
||||
@@ -946,6 +945,7 @@ async fn plugin_install_failure_tracks_analytics_event() -> Result<()> {
|
||||
"codex_plugin_install_failed"
|
||||
);
|
||||
assert_eq!(event_params["plugin_id"], "sample-plugin@debug");
|
||||
assert_eq!(event_params["remote_plugin_id"], json!(null));
|
||||
assert_eq!(event_params["plugin_name"], "sample-plugin");
|
||||
assert_eq!(event_params["marketplace_name"], "debug");
|
||||
assert_eq!(event_params["has_skills"], json!(null));
|
||||
@@ -995,7 +995,8 @@ async fn plugin_install_tracks_remote_plugin_analytics_event() -> Result<()> {
|
||||
"events": [{
|
||||
"event_type": "codex_plugin_installed",
|
||||
"event_params": {
|
||||
"plugin_id": REMOTE_PLUGIN_ID,
|
||||
"plugin_id": "linear@openai-curated-remote",
|
||||
"remote_plugin_id": REMOTE_PLUGIN_ID,
|
||||
"plugin_name": "linear",
|
||||
"marketplace_name": "openai-curated-remote",
|
||||
"has_skills": true,
|
||||
@@ -1072,7 +1073,8 @@ async fn plugin_install_preserves_status_when_remote_bundle_error_body_is_too_la
|
||||
payload["events"][0]["event_type"],
|
||||
"codex_plugin_install_failed"
|
||||
);
|
||||
assert_eq!(event_params["plugin_id"], REMOTE_PLUGIN_ID);
|
||||
assert_eq!(event_params["plugin_id"], "linear@openai-curated-remote");
|
||||
assert_eq!(event_params["remote_plugin_id"], REMOTE_PLUGIN_ID);
|
||||
assert_eq!(event_params["marketplace_name"], "openai-curated-remote");
|
||||
assert_eq!(event_params["error_type"], "remote_bundle_download_status");
|
||||
assert!(
|
||||
|
||||
@@ -139,6 +139,7 @@ async fn plugin_uninstall_tracks_analytics_event() -> Result<()> {
|
||||
"event_type": "codex_plugin_uninstalled",
|
||||
"event_params": {
|
||||
"plugin_id": "sample-plugin@debug",
|
||||
"remote_plugin_id": null,
|
||||
"plugin_name": "sample-plugin",
|
||||
"marketplace_name": "debug",
|
||||
"has_skills": false,
|
||||
@@ -216,6 +217,11 @@ async fn plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabl
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/backend-api/codex/analytics-events/events"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(r#"{"status":"ok"}"#))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let remote_plugin_cache_root = codex_home
|
||||
.path()
|
||||
@@ -225,6 +231,11 @@ async fn plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabl
|
||||
remote_plugin_cache_root.join("1.0.0/.codex-plugin/plugin.json"),
|
||||
r#"{"name":"linear","version":"1.0.0"}"#,
|
||||
)?;
|
||||
std::fs::create_dir_all(remote_plugin_cache_root.join("1.0.0/skills/plan-work"))?;
|
||||
std::fs::write(
|
||||
remote_plugin_cache_root.join("1.0.0/skills/plan-work/SKILL.md"),
|
||||
"---\nname: plan-work\ndescription: Plan work\n---\n",
|
||||
)?;
|
||||
let legacy_remote_plugin_cache_root = codex_home.path().join(format!(
|
||||
"plugins/cache/openai-curated-remote/{REMOTE_PLUGIN_ID}"
|
||||
));
|
||||
@@ -233,6 +244,10 @@ async fn plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabl
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
// Simulate a background remote-cache refresh removing the local bundle
|
||||
// before the uninstall request captures its telemetry metadata.
|
||||
std::fs::remove_dir_all(remote_plugin_cache_root.join("1.0.0"))?;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_uninstall_request(PluginUninstallParams {
|
||||
plugin_id: REMOTE_PLUGIN_ID.to_string(),
|
||||
@@ -255,6 +270,25 @@ async fn plugin_uninstall_writes_remote_plugin_to_cloud_when_remote_plugin_enabl
|
||||
.await?;
|
||||
assert!(!remote_plugin_cache_root.exists());
|
||||
assert!(!legacy_remote_plugin_cache_root.exists());
|
||||
let payload = wait_for_plugin_analytics_payload(&server).await?;
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({
|
||||
"events": [{
|
||||
"event_type": "codex_plugin_uninstalled",
|
||||
"event_params": {
|
||||
"plugin_id": "linear@openai-curated-remote",
|
||||
"remote_plugin_id": REMOTE_PLUGIN_ID,
|
||||
"plugin_name": "linear",
|
||||
"marketplace_name": "openai-curated-remote",
|
||||
"has_skills": true,
|
||||
"mcp_server_count": 0,
|
||||
"connector_ids": [],
|
||||
"product_client_id": DEFAULT_CLIENT_NAME,
|
||||
}
|
||||
}]
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -638,7 +672,11 @@ async fn mount_remote_plugin_detail_with_name(
|
||||
"interface": {{
|
||||
"short_description": "Plan and track work"
|
||||
}},
|
||||
"skills": []
|
||||
"skills": [{{
|
||||
"name": "plan-work",
|
||||
"description": "Plan work",
|
||||
"interface": null
|
||||
}}]
|
||||
}}
|
||||
}}"#
|
||||
);
|
||||
@@ -652,6 +690,29 @@ async fn mount_remote_plugin_detail_with_name(
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn wait_for_plugin_analytics_payload(server: &MockServer) -> Result<serde_json::Value> {
|
||||
timeout(DEFAULT_TIMEOUT, async {
|
||||
loop {
|
||||
let Some(requests) = server.received_requests().await else {
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
continue;
|
||||
};
|
||||
if let Some(request) = requests.iter().find(|request| {
|
||||
request.method == "POST"
|
||||
&& request
|
||||
.url
|
||||
.path()
|
||||
.ends_with("/codex/analytics-events/events")
|
||||
}) {
|
||||
return serde_json::from_slice(&request.body)
|
||||
.map_err(|err| anyhow::anyhow!("invalid analytics payload: {err}"));
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
}
|
||||
})
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn wait_for_remote_plugin_request_count(
|
||||
server: &MockServer,
|
||||
method_name: &str,
|
||||
|
||||
@@ -383,6 +383,7 @@ async fn run_command_under_sandbox(
|
||||
network_sandbox_policy,
|
||||
sandbox_policy_cwd: sandbox_policy_cwd.as_path(),
|
||||
enforce_managed_network,
|
||||
managed_network: None,
|
||||
environment_id: None,
|
||||
network: network.as_ref(),
|
||||
extra_allow_unix_sockets: allow_unix_sockets,
|
||||
@@ -472,6 +473,7 @@ async fn run_command_under_windows_session(
|
||||
) -> ! {
|
||||
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_windows_sandbox::WindowsSandboxProxySettingsMode;
|
||||
use codex_windows_sandbox::WindowsSandboxSessionRequest;
|
||||
use codex_windows_sandbox::spawn_windows_sandbox_session_for_level;
|
||||
|
||||
@@ -484,6 +486,7 @@ async fn run_command_under_windows_session(
|
||||
cwd: cwd.as_path(),
|
||||
env_map: env,
|
||||
windows_sandbox_level: WindowsSandboxLevel::from_config(config),
|
||||
proxy_settings_mode: WindowsSandboxProxySettingsMode::Reconcile,
|
||||
proxy_enforced: false,
|
||||
timeout_ms: None,
|
||||
read_roots_override: None,
|
||||
|
||||
@@ -345,8 +345,15 @@ impl McpConnectionManager {
|
||||
/// Stop all MCP clients owned by this manager and terminate stdio server processes.
|
||||
pub async fn shutdown(&self) {
|
||||
self.startup_cancellation_token.cancel();
|
||||
for client in self.clients.values() {
|
||||
client.shutdown().await;
|
||||
let clients = self.clients.values().cloned().collect::<Vec<_>>();
|
||||
// Keep cleanup alive if an interrupt cancels the refresh that requested it.
|
||||
let shutdown_task = tokio::spawn(async move {
|
||||
for client in clients {
|
||||
client.shutdown().await;
|
||||
}
|
||||
});
|
||||
if let Err(error) = shutdown_task.await {
|
||||
warn!("MCP client shutdown task failed: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -205,9 +205,6 @@ fn tool_with_model_visible_input_schema_leaves_tools_without_file_params_unchang
|
||||
|
||||
#[test]
|
||||
fn elicitation_granular_policy_defaults_to_prompting() {
|
||||
assert!(!elicitation_is_rejected_by_policy(
|
||||
AskForApproval::OnFailure
|
||||
));
|
||||
assert!(!elicitation_is_rejected_by_policy(
|
||||
AskForApproval::OnRequest
|
||||
));
|
||||
@@ -789,7 +786,7 @@ async fn list_all_tools_uses_cached_tool_info_snapshot_while_client_is_pending()
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
@@ -826,7 +823,7 @@ async fn list_available_server_infos_uses_cache_while_client_is_pending() {
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
@@ -865,7 +862,7 @@ async fn list_all_tools_accepts_canonical_namespaced_tool_names() {
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
@@ -909,7 +906,7 @@ async fn list_all_tools_applies_legacy_mcp_prefix_by_default() {
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
@@ -952,7 +949,7 @@ async fn list_all_tools_blocks_while_client_is_pending_without_cached_tool_info_
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
@@ -989,7 +986,7 @@ async fn shutdown_cancels_pending_tool_listing() {
|
||||
}
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
@@ -1020,12 +1017,65 @@ async fn shutdown_cancels_pending_tool_listing() {
|
||||
assert!(tools.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn shutdown_continues_after_caller_is_aborted() {
|
||||
let (started_tx, started_rx) = tokio::sync::oneshot::channel();
|
||||
let (completed_tx, completed_rx) = tokio::sync::oneshot::channel();
|
||||
let release = Arc::new(tokio::sync::Notify::new());
|
||||
let release_for_client = Arc::clone(&release);
|
||||
let blocking_client = async move {
|
||||
let _ = started_tx.send(());
|
||||
release_for_client.notified().await;
|
||||
let _ = completed_tx.send(());
|
||||
Err(StartupOutcomeError::Cancelled)
|
||||
}
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
&permission_profile,
|
||||
/*prefix_mcp_tool_names*/ true,
|
||||
);
|
||||
manager.clients.insert(
|
||||
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
AsyncManagedClient {
|
||||
client: blocking_client,
|
||||
is_codex_apps_mcp_server: true,
|
||||
cached_tool_info_snapshot: None,
|
||||
cached_server_info: None,
|
||||
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
|
||||
cancel_token: CancellationToken::new(),
|
||||
},
|
||||
);
|
||||
let manager = Arc::new(manager);
|
||||
let shutdown_task = tokio::spawn({
|
||||
let manager = Arc::clone(&manager);
|
||||
async move { manager.shutdown().await }
|
||||
});
|
||||
|
||||
started_rx.await.expect("client shutdown should start");
|
||||
shutdown_task.abort();
|
||||
let shutdown_error = shutdown_task
|
||||
.await
|
||||
.expect_err("caller shutdown task should be aborted");
|
||||
assert!(shutdown_error.is_cancelled());
|
||||
release.notify_one();
|
||||
|
||||
tokio::time::timeout(Duration::from_secs(1), completed_rx)
|
||||
.await
|
||||
.expect("client shutdown should survive caller cancellation")
|
||||
.expect("client shutdown completion sender should stay alive");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_all_tools_does_not_block_when_cached_tool_info_snapshot_is_empty() {
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
@@ -1065,7 +1115,7 @@ async fn list_all_tools_uses_cached_tool_info_snapshot_when_client_startup_fails
|
||||
))
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
@@ -1112,7 +1162,7 @@ async fn list_all_tools_adds_server_metadata_to_cached_tools() {
|
||||
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
|
||||
.boxed()
|
||||
.shared();
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
@@ -1179,7 +1229,7 @@ fn server_metadata_preserves_tool_approval_policy() {
|
||||
|
||||
#[test]
|
||||
fn host_owned_codex_apps_requires_server_metadata() {
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
@@ -1192,7 +1242,7 @@ fn host_owned_codex_apps_requires_server_metadata() {
|
||||
|
||||
#[test]
|
||||
fn host_owned_codex_apps_matches_reserved_name_with_server_metadata() {
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let permission_profile = Constrained::allow_any(PermissionProfile::default());
|
||||
let mut manager = McpConnectionManager::new_uninitialized(
|
||||
&approval_policy,
|
||||
@@ -1214,7 +1264,7 @@ fn host_owned_codex_apps_matches_reserved_name_with_server_metadata() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
|
||||
let approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
let (tx_event, rx_event) = async_channel::unbounded();
|
||||
drop(rx_event);
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
|
||||
@@ -247,7 +247,6 @@ impl ElicitationRequestManager {
|
||||
pub(crate) fn elicitation_is_rejected_by_policy(approval_policy: AskForApproval) -> bool {
|
||||
match approval_policy {
|
||||
AskForApproval::Never => true,
|
||||
AskForApproval::OnFailure => false,
|
||||
AskForApproval::OnRequest => false,
|
||||
AskForApproval::UnlessTrusted => false,
|
||||
AskForApproval::Granular(granular_config) => !granular_config.allows_mcp_elicitations(),
|
||||
|
||||
@@ -27,7 +27,7 @@ fn test_mcp_config(codex_home: PathBuf) -> McpConfig {
|
||||
mcp_oauth_callback_port: None,
|
||||
mcp_oauth_callback_url: None,
|
||||
skill_mcp_dependency_install_enabled: true,
|
||||
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
|
||||
approval_policy: Constrained::allow_any(AskForApproval::OnRequest),
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
apps_enabled: false,
|
||||
@@ -83,7 +83,6 @@ fn mcp_prompt_auto_approval_honors_unrestricted_managed_profiles() {
|
||||
fn mcp_prompt_auto_approval_honors_approved_tools_in_all_permission_modes() {
|
||||
for approval_policy in [
|
||||
AskForApproval::UnlessTrusted,
|
||||
AskForApproval::OnFailure,
|
||||
AskForApproval::OnRequest,
|
||||
AskForApproval::Granular(GranularApprovalConfig {
|
||||
sandbox_approval: true,
|
||||
|
||||
@@ -2513,17 +2513,6 @@ allowed_approvals_reviewers = ["user"]
|
||||
.can_set(&AskForApproval::UnlessTrusted)
|
||||
.is_ok()
|
||||
);
|
||||
assert_eq!(
|
||||
requirements
|
||||
.approval_policy
|
||||
.can_set(&AskForApproval::OnFailure),
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "approval_policy",
|
||||
candidate: "OnFailure".into(),
|
||||
allowed: "[UnlessTrusted, OnRequest]".into(),
|
||||
requirement_source: RequirementSource::Unknown,
|
||||
})
|
||||
);
|
||||
assert!(
|
||||
requirements
|
||||
.approval_policy
|
||||
|
||||
@@ -52,7 +52,7 @@ fn active_user_layer_is_highest_precedence_user_layer() {
|
||||
toml::from_str(
|
||||
r#"
|
||||
model = "base"
|
||||
approval_policy = "on-failure"
|
||||
approval_policy = "on-request"
|
||||
"#,
|
||||
)
|
||||
.expect("base config"),
|
||||
@@ -86,7 +86,7 @@ approval_policy = "on-failure"
|
||||
.expect("merged user config")
|
||||
.get("approval_policy")
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("on-failure")
|
||||
Some("on-request")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ fn with_user_config_updates_matching_user_layer_without_replacing_active_profile
|
||||
file: profile_file.clone(),
|
||||
profile: Some("work".to_string()),
|
||||
},
|
||||
toml::from_str(r#"approval_policy = "on-failure""#).expect("profile config"),
|
||||
toml::from_str(r#"approval_policy = "on-request""#).expect("profile config"),
|
||||
);
|
||||
let stack = ConfigLayerStack::new(
|
||||
vec![base_layer, profile_layer],
|
||||
@@ -136,6 +136,6 @@ fn with_user_config_updates_matching_user_layer_without_replacing_active_profile
|
||||
.expect("merged user config")
|
||||
.get("approval_policy")
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("on-failure")
|
||||
Some("on-request")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -708,6 +708,37 @@ impl PluginsManager {
|
||||
remote_installed_plugins_to_config(plugins, &self.store)
|
||||
}
|
||||
|
||||
fn remote_plugin_id_for(&self, plugin_id: &PluginId) -> Option<String> {
|
||||
let cached_remote_plugin_id = {
|
||||
let cache = match self.remote_installed_plugins_cache.read() {
|
||||
Ok(cache) => cache,
|
||||
Err(err) => err.into_inner(),
|
||||
};
|
||||
cache.as_ref().and_then(|plugins| {
|
||||
plugins.iter().find_map(|plugin| {
|
||||
(plugin.name == plugin_id.plugin_name
|
||||
&& plugin.marketplace_name == plugin_id.marketplace_name)
|
||||
.then(|| plugin.id.clone())
|
||||
})
|
||||
})
|
||||
};
|
||||
if cached_remote_plugin_id.is_some() {
|
||||
return cached_remote_plugin_id;
|
||||
}
|
||||
|
||||
match self.store.remote_plugin_id(plugin_id) {
|
||||
Ok(remote_plugin_id) => remote_plugin_id,
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
plugin_id = %plugin_id.as_key(),
|
||||
error = %err,
|
||||
"failed to read persisted remote plugin identity"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn telemetry_metadata_for_installed_plugin(
|
||||
&self,
|
||||
plugin_id: &PluginId,
|
||||
@@ -739,8 +770,8 @@ impl PluginsManager {
|
||||
plugin_id: &PluginId,
|
||||
) -> PluginTelemetryMetadata {
|
||||
PluginTelemetryMetadata {
|
||||
plugin_id: plugin_id.clone(),
|
||||
remote_plugin_id: None,
|
||||
plugin_id: Some(plugin_id.clone()),
|
||||
remote_plugin_id: self.remote_plugin_id_for(plugin_id),
|
||||
capability_summary: None,
|
||||
}
|
||||
}
|
||||
@@ -762,8 +793,8 @@ impl PluginsManager {
|
||||
) -> Option<PluginTelemetryMetadata> {
|
||||
let plugin_id = PluginId::parse(&summary.config_name).ok()?;
|
||||
Some(PluginTelemetryMetadata {
|
||||
plugin_id,
|
||||
remote_plugin_id: None,
|
||||
remote_plugin_id: self.remote_plugin_id_for(&plugin_id),
|
||||
plugin_id: Some(plugin_id),
|
||||
capability_summary: Some(summary.clone()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -859,7 +859,7 @@ async fn installed_plugin_telemetry_metadata_collects_capabilities() {
|
||||
assert_eq!(
|
||||
metadata,
|
||||
PluginTelemetryMetadata {
|
||||
plugin_id,
|
||||
plugin_id: Some(plugin_id),
|
||||
remote_plugin_id: None,
|
||||
capability_summary: Some(PluginCapabilitySummary {
|
||||
config_name: "sample@test".to_string(),
|
||||
@@ -873,6 +873,71 @@ async fn installed_plugin_telemetry_metadata_collects_capabilities() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn installed_plugin_telemetry_metadata_resolves_persisted_remote_identity() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
write_cached_plugin(codex_home.path(), "openai-curated-remote", "linear");
|
||||
let plugin_id =
|
||||
PluginId::parse("linear@openai-curated-remote").expect("plugin id should parse");
|
||||
PluginStore::new(codex_home.path().to_path_buf())
|
||||
.write_remote_plugin_id(&plugin_id, "plugins~Plugin_linear")
|
||||
.expect("persist remote plugin id");
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
|
||||
let metadata = manager
|
||||
.telemetry_metadata_for_installed_plugin(&plugin_id)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
metadata,
|
||||
PluginTelemetryMetadata {
|
||||
plugin_id: Some(plugin_id),
|
||||
remote_plugin_id: Some("plugins~Plugin_linear".to_string()),
|
||||
capability_summary: Some(PluginCapabilitySummary {
|
||||
config_name: "linear@openai-curated-remote".to_string(),
|
||||
display_name: "linear".to_string(),
|
||||
description: None,
|
||||
has_skills: true,
|
||||
mcp_server_names: Vec::new(),
|
||||
app_connector_ids: Vec::new(),
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn installed_plugin_telemetry_metadata_prefers_remote_snapshot_identity() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
write_cached_plugin(codex_home.path(), "openai-curated-remote", "linear");
|
||||
let plugin_id =
|
||||
PluginId::parse("linear@openai-curated-remote").expect("plugin id should parse");
|
||||
PluginStore::new(codex_home.path().to_path_buf())
|
||||
.write_remote_plugin_id(&plugin_id, "plugins~Plugin_stale")
|
||||
.expect("persist remote plugin id");
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
manager.write_remote_installed_plugins_cache(vec![remote_installed_linear_plugin()]);
|
||||
|
||||
let metadata = manager
|
||||
.telemetry_metadata_for_installed_plugin(&plugin_id)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
metadata,
|
||||
PluginTelemetryMetadata {
|
||||
plugin_id: Some(plugin_id),
|
||||
remote_plugin_id: Some("plugins~Plugin_linear".to_string()),
|
||||
capability_summary: Some(PluginCapabilitySummary {
|
||||
config_name: "linear@openai-curated-remote".to_string(),
|
||||
display_name: "linear".to_string(),
|
||||
description: None,
|
||||
has_skills: true,
|
||||
mcp_server_names: Vec::new(),
|
||||
app_connector_ids: Vec::new(),
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn installed_plugin_telemetry_metadata_accepts_authoritative_remote_identity() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
@@ -887,7 +952,7 @@ async fn installed_plugin_telemetry_metadata_accepts_authoritative_remote_identi
|
||||
assert_eq!(
|
||||
metadata,
|
||||
PluginTelemetryMetadata {
|
||||
plugin_id,
|
||||
plugin_id: Some(plugin_id),
|
||||
remote_plugin_id: Some("plugins~Plugin_linear".to_string()),
|
||||
capability_summary: None,
|
||||
}
|
||||
@@ -912,14 +977,46 @@ fn capability_summary_telemetry_metadata_uses_local_identity() {
|
||||
assert_eq!(
|
||||
metadata,
|
||||
Some(PluginTelemetryMetadata {
|
||||
plugin_id: PluginId::parse("linear@openai-curated-remote")
|
||||
.expect("plugin id should parse"),
|
||||
plugin_id: Some(
|
||||
PluginId::parse("linear@openai-curated-remote").expect("plugin id should parse"),
|
||||
),
|
||||
remote_plugin_id: None,
|
||||
capability_summary: Some(summary),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_summary_telemetry_metadata_resolves_persisted_remote_identity() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
write_cached_plugin(codex_home.path(), "openai-curated-remote", "linear");
|
||||
let plugin_id =
|
||||
PluginId::parse("linear@openai-curated-remote").expect("plugin id should parse");
|
||||
PluginStore::new(codex_home.path().to_path_buf())
|
||||
.write_remote_plugin_id(&plugin_id, "plugins~Plugin_linear")
|
||||
.expect("persist remote plugin id");
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let summary = PluginCapabilitySummary {
|
||||
config_name: "linear@openai-curated-remote".to_string(),
|
||||
display_name: "Linear".to_string(),
|
||||
description: Some("Track work".to_string()),
|
||||
has_skills: true,
|
||||
mcp_server_names: vec!["linear".to_string()],
|
||||
app_connector_ids: vec![AppConnectorId("linear-app".to_string())],
|
||||
};
|
||||
|
||||
let metadata = manager.telemetry_metadata_for_capability_summary(&summary);
|
||||
|
||||
assert_eq!(
|
||||
metadata,
|
||||
Some(PluginTelemetryMetadata {
|
||||
plugin_id: Some(plugin_id),
|
||||
remote_plugin_id: Some("plugins~Plugin_linear".to_string()),
|
||||
capability_summary: Some(summary),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_installed_cache_prefers_local_curated_conflicts_when_remote_plugin_disabled() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
|
||||
@@ -12,8 +12,10 @@ use codex_login::CodexAuth;
|
||||
use codex_login::default_client::build_reqwest_client;
|
||||
use codex_plugin::AppConnectorId;
|
||||
use codex_plugin::AppDeclaration;
|
||||
use codex_plugin::PluginCapabilitySummary;
|
||||
use codex_plugin::PluginId;
|
||||
use codex_plugin::app_connector_ids_from_declarations;
|
||||
use codex_plugin::prompt_safe_plugin_description;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use reqwest::RequestBuilder;
|
||||
use serde::Deserialize;
|
||||
@@ -120,6 +122,13 @@ pub struct RemotePluginServiceConfig {
|
||||
pub chatgpt_base_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RemotePluginUninstallTarget {
|
||||
pub plugin_id: PluginId,
|
||||
pub remote_plugin_id: String,
|
||||
pub fallback_capability_summary: PluginCapabilitySummary,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct RemoteMarketplace {
|
||||
pub name: String,
|
||||
@@ -1304,40 +1313,90 @@ pub async fn install_remote_plugin(
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn resolve_remote_plugin_uninstall_target(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
remote_plugin_id: &str,
|
||||
) -> Result<RemotePluginUninstallTarget, RemotePluginCatalogError> {
|
||||
let auth = ensure_chatgpt_auth(auth)?;
|
||||
let plugin = fetch_plugin_detail(
|
||||
config,
|
||||
auth,
|
||||
remote_plugin_id,
|
||||
/*include_download_urls*/ false,
|
||||
)
|
||||
.await?;
|
||||
let marketplace_name = remote_plugin_canonical_marketplace_name(&plugin)?.to_string();
|
||||
let plugin_id = PluginId::new(plugin.name.clone(), marketplace_name).map_err(|err| {
|
||||
RemotePluginCatalogError::UnexpectedResponse(format!(
|
||||
"invalid local plugin id for remote plugin `{}`: {err}",
|
||||
plugin.id
|
||||
))
|
||||
})?;
|
||||
let app_declarations = plugin
|
||||
.release
|
||||
.app_manifest
|
||||
.as_ref()
|
||||
.map(plugin_app_declarations_from_value)
|
||||
.unwrap_or_else(|| app_declarations_from_remote_app_ids(&plugin.release.app_ids));
|
||||
let mut mcp_server_names = plugin
|
||||
.release
|
||||
.mcp_servers
|
||||
.iter()
|
||||
.map(|server| server.key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
mcp_server_names.sort_unstable();
|
||||
mcp_server_names.dedup();
|
||||
let fallback_capability_summary = PluginCapabilitySummary {
|
||||
config_name: plugin_id.as_key(),
|
||||
display_name: plugin.release.display_name,
|
||||
description: prompt_safe_plugin_description(Some(&plugin.release.description)),
|
||||
has_skills: !plugin.release.skills.is_empty(),
|
||||
mcp_server_names,
|
||||
app_connector_ids: app_connector_ids_from_declarations(&app_declarations),
|
||||
};
|
||||
Ok(RemotePluginUninstallTarget {
|
||||
plugin_id,
|
||||
remote_plugin_id: plugin.id,
|
||||
fallback_capability_summary,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn uninstall_remote_plugin(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
codex_home: PathBuf,
|
||||
plugin_id: &str,
|
||||
target: RemotePluginUninstallTarget,
|
||||
) -> Result<(), RemotePluginCatalogError> {
|
||||
let auth = ensure_chatgpt_auth(auth)?;
|
||||
let plugin = fetch_plugin_detail(
|
||||
config, auth, plugin_id, /*include_download_urls*/ false,
|
||||
)
|
||||
.await?;
|
||||
let marketplace_name = remote_plugin_canonical_marketplace_name(&plugin)?.to_string();
|
||||
let plugin_name = plugin.name;
|
||||
let RemotePluginUninstallTarget {
|
||||
plugin_id,
|
||||
remote_plugin_id,
|
||||
fallback_capability_summary: _,
|
||||
} = target;
|
||||
let marketplace_name = plugin_id.marketplace_name.clone();
|
||||
let plugin_name = plugin_id.plugin_name.clone();
|
||||
|
||||
let base_url = config.chatgpt_base_url.trim_end_matches('/');
|
||||
let url = format!("{base_url}/ps/plugins/{plugin_id}/uninstall");
|
||||
let url = format!("{base_url}/ps/plugins/{remote_plugin_id}/uninstall");
|
||||
let client = build_reqwest_client();
|
||||
let request = authenticated_request(client.post(&url), auth)?;
|
||||
let response: RemotePluginMutationResponse = send_and_decode(request, &url).await?;
|
||||
if response.id != plugin_id {
|
||||
if response.id != remote_plugin_id {
|
||||
return Err(RemotePluginCatalogError::UnexpectedPluginId {
|
||||
expected: plugin_id.to_string(),
|
||||
expected: remote_plugin_id,
|
||||
actual: response.id,
|
||||
});
|
||||
}
|
||||
if response.enabled {
|
||||
return Err(RemotePluginCatalogError::UnexpectedEnabledState {
|
||||
plugin_id: plugin_id.to_string(),
|
||||
plugin_id: response.id,
|
||||
expected_enabled: false,
|
||||
actual_enabled: response.enabled,
|
||||
});
|
||||
}
|
||||
|
||||
let legacy_plugin_id = plugin_id.to_string();
|
||||
let legacy_plugin_id = response.id;
|
||||
tokio::task::spawn_blocking(move || {
|
||||
remove_remote_plugin_cache(codex_home, marketplace_name, plugin_name, legacy_plugin_id)
|
||||
})
|
||||
|
||||
@@ -252,13 +252,6 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "DEPRECATED: *All* commands are auto‑approved, but they are expected to run inside a sandbox where network access is disabled and writes are confined to a specific set of paths. If the command fails, it will be escalated to the user to approve execution without a sandbox. Prefer `OnRequest` for interactive runs or `Never` for non-interactive runs.",
|
||||
"enum": [
|
||||
"on-failure"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "The model decides when to ask the user for approval.",
|
||||
"enum": [
|
||||
|
||||
@@ -171,7 +171,7 @@ For all of testing, running, building, and formatting, do not attempt to fix unr
|
||||
|
||||
Be mindful of whether to run validation commands proactively. In the absence of behavioral guidance:
|
||||
|
||||
- When running in non-interactive approval modes like **never** or **on-failure**, you can proactively run tests, lint and do whatever you need to ensure you've completed the task. If you are unable to run tests, you must still do your utmost best to complete the task.
|
||||
- When running in the non-interactive approval mode **never**, you can proactively run tests, lint and do whatever you need to ensure you've completed the task. If you are unable to run tests, you must still do your utmost best to complete the task.
|
||||
- When working in interactive approval modes like **untrusted**, or **on-request**, hold off on running tests or lint commands until the user is ready for you to finalize your output, because these commands take time to run and slow down iteration. Instead suggest what you want to do next, and let the user confirm first.
|
||||
- When working on test-related tasks, such as adding tests, fixing tests, or reproducing a bug to verify behavior, you may proactively run tests regardless of approval mode. Use your judgement to decide whether this is a test-related task.
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ For all of testing, running, building, and formatting, do not attempt to fix unr
|
||||
|
||||
Be mindful of whether to run validation commands proactively. In the absence of behavioral guidance:
|
||||
|
||||
- When running in non-interactive approval modes like **never** or **on-failure**, you can proactively run tests, lint and do whatever you need to ensure you've completed the task. If you are unable to run tests, you must still do your utmost best to complete the task.
|
||||
- When running in the non-interactive approval mode **never**, you can proactively run tests, lint and do whatever you need to ensure you've completed the task. If you are unable to run tests, you must still do your utmost best to complete the task.
|
||||
- When working in interactive approval modes like **untrusted**, or **on-request**, hold off on running tests or lint commands until the user is ready for you to finalize your output, because these commands take time to run and slow down iteration. Instead suggest what you want to do next, and let the user confirm first.
|
||||
- When working on test-related tasks, such as adding tests, fixing tests, or reproducing a bug to verify behavior, you may proactively run tests regardless of approval mode. Use your judgement to decide whether this is a test-related task.
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ For all of testing, running, building, and formatting, do not attempt to fix unr
|
||||
|
||||
Be mindful of whether to run validation commands proactively. In the absence of behavioral guidance:
|
||||
|
||||
- When running in non-interactive approval modes like **never** or **on-failure**, proactively run tests, lint and do whatever you need to ensure you've completed the task.
|
||||
- When running in the non-interactive approval mode **never**, proactively run tests, lint and do whatever you need to ensure you've completed the task.
|
||||
- When working in interactive approval modes like **untrusted**, or **on-request**, hold off on running tests or lint commands until the user is ready for you to finalize your output, because these commands take time to run and slow down iteration. Instead suggest what you want to do next, and let the user confirm first.
|
||||
- When working on test-related tasks, such as adding tests, fixing tests, or reproducing a bug to verify behavior, you may proactively run tests regardless of approval mode. Use your judgement to decide whether this is a test-related task.
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::time::Instant;
|
||||
use crate::Prompt;
|
||||
use crate::client::ModelClientSession;
|
||||
use crate::client_common::ResponseEvent;
|
||||
use crate::context::world_state::WorldState;
|
||||
use crate::hook_runtime::PostCompactHookOutcome;
|
||||
use crate::hook_runtime::PreCompactHookOutcome;
|
||||
use crate::hook_runtime::run_post_compact_hooks;
|
||||
@@ -60,12 +61,29 @@ const COMPACT_USER_MESSAGE_MAX_TOKENS: usize = 20_000;
|
||||
/// Mid-turn compaction must use `BeforeLastUserMessage` because the model is trained to see the
|
||||
/// compaction summary as the last item in history after mid-turn compaction; we therefore inject
|
||||
/// initial context into the replacement history just above the last real user message.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum InitialContextInjection {
|
||||
BeforeLastUserMessage,
|
||||
BeforeLastUserMessage(Arc<WorldState>),
|
||||
DoNotInject,
|
||||
}
|
||||
|
||||
pub(crate) async fn build_compaction_initial_context(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
initial_context_injection: &InitialContextInjection,
|
||||
) -> (Vec<ResponseItem>, Option<Arc<WorldState>>) {
|
||||
// Return the rendered state with its items so history and its baseline stay identical.
|
||||
match initial_context_injection {
|
||||
InitialContextInjection::BeforeLastUserMessage(world_state) => {
|
||||
let items = sess
|
||||
.build_initial_context_with_world_state(turn_context, world_state.as_ref())
|
||||
.await;
|
||||
(items, Some(Arc::clone(world_state)))
|
||||
}
|
||||
InitialContextInjection::DoNotInject => (Vec::new(), None),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn should_use_remote_compact_task(provider: &ModelProviderInfo) -> bool {
|
||||
provider.supports_remote_compaction()
|
||||
}
|
||||
@@ -309,17 +327,21 @@ async fn run_compact_task_inner_impl(
|
||||
}
|
||||
let (window_number, window_ids) = sess.advance_auto_compact_window().await;
|
||||
|
||||
if matches!(
|
||||
initial_context_injection,
|
||||
InitialContextInjection::BeforeLastUserMessage
|
||||
) {
|
||||
let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
|
||||
let (initial_context, world_state_baseline) = build_compaction_initial_context(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
&initial_context_injection,
|
||||
)
|
||||
.await;
|
||||
if !initial_context.is_empty() {
|
||||
new_history =
|
||||
insert_initial_context_before_last_real_user_or_summary(new_history, initial_context);
|
||||
}
|
||||
let reference_context_item = match initial_context_injection {
|
||||
InitialContextInjection::DoNotInject => None,
|
||||
InitialContextInjection::BeforeLastUserMessage => Some(turn_context.to_turn_context_item()),
|
||||
InitialContextInjection::BeforeLastUserMessage(_) => {
|
||||
Some(turn_context.to_turn_context_item())
|
||||
}
|
||||
};
|
||||
let compacted_item = CompactedItem {
|
||||
message: summary_text.clone(),
|
||||
@@ -333,6 +355,7 @@ async fn run_compact_task_inner_impl(
|
||||
turn_context.as_ref(),
|
||||
new_history,
|
||||
reference_context_item,
|
||||
world_state_baseline,
|
||||
compacted_item,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -6,8 +6,10 @@ use crate::client::CompactConversationRequestSettings;
|
||||
use crate::compact::CompactionAnalyticsAttempt;
|
||||
use crate::compact::CompactionAnalyticsDetails;
|
||||
use crate::compact::InitialContextInjection;
|
||||
use crate::compact::build_compaction_initial_context;
|
||||
use crate::compact::compaction_status_from_result;
|
||||
use crate::compact::insert_initial_context_before_last_real_user_or_summary;
|
||||
use crate::context::world_state::WorldState;
|
||||
use crate::context_manager::ContextManager;
|
||||
use crate::hook_runtime::PostCompactHookOutcome;
|
||||
use crate::hook_runtime::PreCompactHookOutcome;
|
||||
@@ -237,7 +239,7 @@ async fn run_remote_compact_task_inner_impl(
|
||||
window_id,
|
||||
CodexResponsesRequestKind::Compaction(compaction_metadata),
|
||||
);
|
||||
let mut new_history = sess
|
||||
let new_history = sess
|
||||
.services
|
||||
.model_client
|
||||
.compact_conversation_history(
|
||||
@@ -259,17 +261,19 @@ async fn run_remote_compact_task_inner_impl(
|
||||
)
|
||||
.await?;
|
||||
let (new_window_number, new_window_ids) = sess.advance_auto_compact_window().await;
|
||||
new_history = process_compacted_history(
|
||||
let (new_history, world_state_baseline) = process_compacted_history(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
new_history,
|
||||
initial_context_injection,
|
||||
&initial_context_injection,
|
||||
)
|
||||
.await;
|
||||
|
||||
let reference_context_item = match initial_context_injection {
|
||||
InitialContextInjection::DoNotInject => None,
|
||||
InitialContextInjection::BeforeLastUserMessage => Some(turn_context.to_turn_context_item()),
|
||||
InitialContextInjection::BeforeLastUserMessage(_) => {
|
||||
Some(turn_context.to_turn_context_item())
|
||||
}
|
||||
};
|
||||
let compacted_item = CompactedItem {
|
||||
message: String::new(),
|
||||
@@ -290,6 +294,7 @@ async fn run_remote_compact_task_inner_impl(
|
||||
turn_context.as_ref(),
|
||||
new_history,
|
||||
reference_context_item,
|
||||
world_state_baseline,
|
||||
compacted_item,
|
||||
)
|
||||
.await;
|
||||
@@ -304,22 +309,19 @@ pub(crate) async fn process_compacted_history(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
mut compacted_history: Vec<ResponseItem>,
|
||||
initial_context_injection: InitialContextInjection,
|
||||
) -> Vec<ResponseItem> {
|
||||
initial_context_injection: &InitialContextInjection,
|
||||
) -> (Vec<ResponseItem>, Option<Arc<WorldState>>) {
|
||||
// Mid-turn compaction is the only path that must inject initial context above the last user
|
||||
// message in the replacement history. Pre-turn compaction instead injects context after the
|
||||
// compaction item, but mid-turn compaction keeps the compaction item last for model training.
|
||||
let initial_context = if matches!(
|
||||
initial_context_injection,
|
||||
InitialContextInjection::BeforeLastUserMessage
|
||||
) {
|
||||
sess.build_initial_context(turn_context).await
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let (initial_context, world_state_baseline) =
|
||||
build_compaction_initial_context(sess, turn_context, initial_context_injection).await;
|
||||
|
||||
compacted_history.retain(should_keep_compacted_history_item);
|
||||
insert_initial_context_before_last_real_user_or_summary(compacted_history, initial_context)
|
||||
(
|
||||
insert_initial_context_before_last_real_user_or_summary(compacted_history, initial_context),
|
||||
world_state_baseline,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns whether an item from remote compaction output should be preserved.
|
||||
|
||||
@@ -293,17 +293,19 @@ async fn run_remote_compact_task_inner_impl(
|
||||
build_v2_compacted_history(&prompt_input, compaction_output);
|
||||
analytics_details.retained_image_count = Some(retained_images);
|
||||
let (new_window_number, new_window_ids) = sess.advance_auto_compact_window().await;
|
||||
let new_history = process_compacted_history(
|
||||
let (new_history, world_state_baseline) = process_compacted_history(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
compacted_history,
|
||||
initial_context_injection,
|
||||
&initial_context_injection,
|
||||
)
|
||||
.await;
|
||||
|
||||
let reference_context_item = match initial_context_injection {
|
||||
InitialContextInjection::DoNotInject => None,
|
||||
InitialContextInjection::BeforeLastUserMessage => Some(turn_context.to_turn_context_item()),
|
||||
InitialContextInjection::BeforeLastUserMessage(_) => {
|
||||
Some(turn_context.to_turn_context_item())
|
||||
}
|
||||
};
|
||||
let compacted_item = CompactedItem {
|
||||
message: String::new(),
|
||||
@@ -321,6 +323,7 @@ async fn run_remote_compact_task_inner_impl(
|
||||
turn_context.as_ref(),
|
||||
new_history,
|
||||
reference_context_item,
|
||||
world_state_baseline,
|
||||
compacted_item,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use super::*;
|
||||
use crate::session::tests::build_world_state_from_turn_context;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
|
||||
use codex_protocol::models::InternalChatMessageMetadataPassthrough;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::sync::Arc;
|
||||
|
||||
async fn process_compacted_history_with_test_session(
|
||||
compacted_history: Vec<ResponseItem>,
|
||||
@@ -13,12 +15,16 @@ async fn process_compacted_history_with_test_session(
|
||||
session
|
||||
.set_previous_turn_settings(previous_turn_settings.cloned())
|
||||
.await;
|
||||
let initial_context = session.build_initial_context(&turn_context).await;
|
||||
let refreshed = crate::compact_remote::process_compacted_history(
|
||||
let world_state = Arc::new(build_world_state_from_turn_context(&session, &turn_context).await);
|
||||
let initial_context = session
|
||||
.build_initial_context_with_world_state(&turn_context, world_state.as_ref())
|
||||
.await;
|
||||
let initial_context_injection = InitialContextInjection::BeforeLastUserMessage(world_state);
|
||||
let (refreshed, _) = crate::compact_remote::process_compacted_history(
|
||||
&session,
|
||||
&turn_context,
|
||||
compacted_history,
|
||||
InitialContextInjection::BeforeLastUserMessage,
|
||||
&initial_context_injection,
|
||||
)
|
||||
.await;
|
||||
(refreshed, initial_context)
|
||||
|
||||
@@ -640,7 +640,7 @@ async fn selected_user_config_file_layers_over_base_user_config() {
|
||||
tmp.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
model = "gpt-main"
|
||||
approval_policy = "on-failure"
|
||||
approval_policy = "on-request"
|
||||
"#,
|
||||
)
|
||||
.expect("write default user config");
|
||||
@@ -697,7 +697,7 @@ approval_policy = "on-failure"
|
||||
.effective_config()
|
||||
.get("approval_policy")
|
||||
.and_then(TomlValue::as_str),
|
||||
Some("on-failure")
|
||||
Some("on-request")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1033,12 +1033,6 @@ personality = true
|
||||
config_requirements
|
||||
.approval_policy
|
||||
.can_set(&AskForApproval::Never)?;
|
||||
assert!(
|
||||
config_requirements
|
||||
.approval_policy
|
||||
.can_set(&AskForApproval::OnFailure)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(
|
||||
config_requirements.web_search_mode.value(),
|
||||
WebSearchMode::Cached
|
||||
|
||||
@@ -8272,7 +8272,7 @@ model_provider = "openai-custom"
|
||||
[profiles.zdr]
|
||||
model = "o3"
|
||||
model_provider = "openai"
|
||||
approval_policy = "on-failure"
|
||||
approval_policy = "on-request"
|
||||
|
||||
[profiles.zdr.analytics]
|
||||
enabled = false
|
||||
@@ -8280,7 +8280,7 @@ enabled = false
|
||||
[profiles.gpt5]
|
||||
model = "gpt-5.4"
|
||||
model_provider = "openai"
|
||||
approval_policy = "on-failure"
|
||||
approval_policy = "on-request"
|
||||
model_reasoning_effort = "high"
|
||||
model_reasoning_summary = "detailed"
|
||||
model_verbosity = "high"
|
||||
|
||||
@@ -24,10 +24,6 @@ pub(crate) struct EnvironmentsState {
|
||||
}
|
||||
|
||||
impl EnvironmentsState {
|
||||
pub(crate) fn from_turn_context(turn_context: &TurnContext) -> Self {
|
||||
Self::from_turn_context_with_environments(turn_context, &turn_context.environments)
|
||||
}
|
||||
|
||||
pub(crate) fn from_turn_context_with_environments(
|
||||
turn_context: &TurnContext,
|
||||
environments: &TurnEnvironmentSnapshot,
|
||||
|
||||
@@ -86,18 +86,18 @@ impl ContextManager {
|
||||
|
||||
pub(crate) fn update_world_state(
|
||||
&mut self,
|
||||
world_state: WorldState,
|
||||
world_state: Arc<WorldState>,
|
||||
) -> Vec<Box<dyn ContextualUserFragment>> {
|
||||
let fragments = self.world_state_baseline.as_deref().map_or_else(
|
||||
|| world_state.render_full(),
|
||||
|previous| world_state.render_diff(previous),
|
||||
);
|
||||
self.world_state_baseline = Some(Arc::new(world_state));
|
||||
self.world_state_baseline = Some(world_state);
|
||||
fragments
|
||||
}
|
||||
|
||||
pub(crate) fn set_world_state_baseline(&mut self, world_state: WorldState) {
|
||||
self.world_state_baseline = Some(Arc::new(world_state));
|
||||
pub(crate) fn set_world_state_baseline(&mut self, world_state: Arc<WorldState>) {
|
||||
self.world_state_baseline = Some(world_state);
|
||||
}
|
||||
|
||||
pub(crate) fn set_token_usage_full(&mut self, context_window: i64) {
|
||||
|
||||
@@ -32,6 +32,7 @@ use image::Luma;
|
||||
use image::Rgba;
|
||||
use pretty_assertions::assert_eq;
|
||||
use regex_lite::Regex;
|
||||
use std::sync::Arc;
|
||||
|
||||
const EXEC_FORMAT_MAX_BYTES: usize = 10_000;
|
||||
const EXEC_FORMAT_MAX_TOKENS: usize = 2_500;
|
||||
@@ -82,7 +83,7 @@ fn world_state_baseline_deduplicates_until_history_is_replaced() {
|
||||
state.add_section(EnvironmentsState::from_turn_context_item(
|
||||
&reference_context_item(),
|
||||
));
|
||||
state
|
||||
Arc::new(state)
|
||||
};
|
||||
let mut history = ContextManager::new();
|
||||
|
||||
|
||||
@@ -377,6 +377,7 @@ pub fn build_exec_request(
|
||||
args: args.to_vec(),
|
||||
cwd,
|
||||
env,
|
||||
managed_network: None,
|
||||
additional_permissions: None,
|
||||
};
|
||||
let options = ExecOptions {
|
||||
@@ -459,6 +460,7 @@ pub(crate) async fn execute_exec_request(
|
||||
arg0,
|
||||
exec_server_sandbox: _,
|
||||
exec_server_enforce_managed_network: _,
|
||||
exec_server_managed_network: _,
|
||||
} = exec_request;
|
||||
|
||||
// TODO(anp): Keep PathUri through the local process launch boundary.
|
||||
|
||||
@@ -177,7 +177,6 @@ pub(crate) fn prompt_is_rejected_by_policy(
|
||||
) -> Option<&'static str> {
|
||||
match approval_policy {
|
||||
AskForApproval::Never => Some(PROMPT_CONFLICT_REASON),
|
||||
AskForApproval::OnFailure => None,
|
||||
AskForApproval::OnRequest => None,
|
||||
AskForApproval::UnlessTrusted => None,
|
||||
AskForApproval::Granular(granular_config) => {
|
||||
@@ -689,15 +688,14 @@ pub(crate) fn render_decision_for_unmatched_command(
|
||||
Decision::Forbidden
|
||||
}
|
||||
}
|
||||
AskForApproval::OnFailure
|
||||
| AskForApproval::OnRequest
|
||||
AskForApproval::OnRequest
|
||||
| AskForApproval::UnlessTrusted
|
||||
| AskForApproval::Granular(_) => Decision::Prompt,
|
||||
};
|
||||
}
|
||||
|
||||
match approval_policy {
|
||||
AskForApproval::Never | AskForApproval::OnFailure => {
|
||||
AskForApproval::Never => {
|
||||
// We allow the command to run, relying on the sandbox for
|
||||
// protection.
|
||||
Decision::Allow
|
||||
|
||||
@@ -738,10 +738,9 @@ fn hook_run_metric_tags(run: &HookRunSummary) -> [(&'static str, &'static str);
|
||||
fn hook_permission_mode(turn_context: &TurnContext) -> String {
|
||||
match turn_context.approval_policy.value() {
|
||||
AskForApproval::Never => "bypassPermissions",
|
||||
AskForApproval::UnlessTrusted
|
||||
| AskForApproval::OnFailure
|
||||
| AskForApproval::OnRequest
|
||||
| AskForApproval::Granular(_) => "default",
|
||||
AskForApproval::UnlessTrusted | AskForApproval::OnRequest | AskForApproval::Granular(_) => {
|
||||
"default"
|
||||
}
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
@@ -653,10 +653,8 @@ async fn maybe_request_codex_apps_auth_elicitation(
|
||||
AskForApproval::Granular(granular_config) if !granular_config.allows_mcp_elicitations() => {
|
||||
return result;
|
||||
}
|
||||
AskForApproval::OnFailure
|
||||
| AskForApproval::OnRequest
|
||||
| AskForApproval::UnlessTrusted
|
||||
| AskForApproval::Granular(_) => {}
|
||||
AskForApproval::OnRequest | AskForApproval::UnlessTrusted | AskForApproval::Granular(_) => {
|
||||
}
|
||||
}
|
||||
|
||||
let connector_id = metadata.and_then(|metadata| metadata.connector_id.as_deref());
|
||||
|
||||
@@ -2915,7 +2915,6 @@ async fn approve_mode_skips_guardian_in_every_permission_mode() {
|
||||
|
||||
for approval_policy in [
|
||||
AskForApproval::UnlessTrusted,
|
||||
AskForApproval::OnFailure,
|
||||
AskForApproval::OnRequest,
|
||||
AskForApproval::Granular(GranularApprovalConfig {
|
||||
sandbox_approval: true,
|
||||
|
||||
@@ -44,10 +44,7 @@ pub fn assess_patch_safety(
|
||||
}
|
||||
|
||||
match policy {
|
||||
AskForApproval::OnFailure
|
||||
| AskForApproval::Never
|
||||
| AskForApproval::OnRequest
|
||||
| AskForApproval::Granular(_) => {
|
||||
AskForApproval::Never | AskForApproval::OnRequest | AskForApproval::Granular(_) => {
|
||||
// Continue to see if this can be auto-approved.
|
||||
}
|
||||
// TODO(ragona): I'm not sure this is actually correct? I believe in this case
|
||||
@@ -66,9 +63,7 @@ pub fn assess_patch_safety(
|
||||
// Even though the patch appears to be constrained to writable paths, it is
|
||||
// possible that paths in the patch are hard links to files outside the
|
||||
// writable roots, so we should still run `apply_patch` in a sandbox in that case.
|
||||
if is_write_patch_constrained_to_writable_paths(action, file_system_sandbox_policy, cwd)
|
||||
|| matches!(policy, AskForApproval::OnFailure)
|
||||
{
|
||||
if is_write_patch_constrained_to_writable_paths(action, file_system_sandbox_policy, cwd) {
|
||||
if matches!(
|
||||
permission_profile,
|
||||
PermissionProfile::Disabled | PermissionProfile::External { .. }
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::exec::execute_exec_request;
|
||||
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
|
||||
use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||
use codex_file_system::FileSystemSandboxContext;
|
||||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
@@ -63,6 +64,7 @@ pub struct ExecRequest {
|
||||
pub arg0: Option<String>,
|
||||
pub(crate) exec_server_sandbox: Option<FileSystemSandboxContext>,
|
||||
pub(crate) exec_server_enforce_managed_network: bool,
|
||||
pub(crate) exec_server_managed_network: Option<ManagedNetworkSandboxContext>,
|
||||
}
|
||||
|
||||
impl ExecRequest {
|
||||
@@ -107,6 +109,7 @@ impl ExecRequest {
|
||||
arg0,
|
||||
exec_server_sandbox: None,
|
||||
exec_server_enforce_managed_network: false,
|
||||
exec_server_managed_network: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +168,7 @@ impl ExecRequest {
|
||||
arg0,
|
||||
exec_server_sandbox: None,
|
||||
exec_server_enforce_managed_network: false,
|
||||
exec_server_managed_network: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
use super::session::Session;
|
||||
use super::turn_context::TurnContext;
|
||||
use codex_protocol::config_types::AutoCompactTokenLimitScope;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ContextWindowTokenStatus {
|
||||
// Full active context usage, independent of the configured auto-compact scope.
|
||||
pub(crate) active_context_tokens: i64,
|
||||
// Usage counted against `model_auto_compact_token_limit` for the current scope.
|
||||
pub(crate) auto_compact_scope_tokens: i64,
|
||||
pub(crate) auto_compact_scope_limit: i64,
|
||||
pub(crate) full_context_window_limit: Option<i64>,
|
||||
pub(crate) tokens_until_compaction: i64,
|
||||
pub(crate) auto_compact_window_prefill_tokens: Option<i64>,
|
||||
pub(crate) full_context_window_limit_reached: bool,
|
||||
pub(crate) token_limit_reached: bool,
|
||||
pub(crate) context_remaining_tokens: Option<i64>,
|
||||
}
|
||||
|
||||
struct BodyAfterPrefixWindowStatus {
|
||||
full_context_window_limit: Option<i64>,
|
||||
auto_compact_window_prefill_tokens: Option<i64>,
|
||||
has_context_remaining_limit: bool,
|
||||
}
|
||||
|
||||
pub(crate) async fn context_window_token_status(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
) -> ContextWindowTokenStatus {
|
||||
let active_context_tokens = sess.get_total_token_usage().await;
|
||||
|
||||
let (auto_compact_scope_tokens, auto_compact_scope_limit, body_window) =
|
||||
match turn_context.config.model_auto_compact_token_limit_scope {
|
||||
AutoCompactTokenLimitScope::Total => (
|
||||
active_context_tokens,
|
||||
turn_context
|
||||
.model_info
|
||||
.auto_compact_token_limit()
|
||||
.unwrap_or(i64::MAX),
|
||||
None,
|
||||
),
|
||||
AutoCompactTokenLimitScope::BodyAfterPrefix => {
|
||||
let window = sess.auto_compact_window_snapshot().await;
|
||||
let baseline = window.prefill_input_tokens.unwrap_or(active_context_tokens);
|
||||
|
||||
let scope_limit = turn_context
|
||||
.config
|
||||
.model_auto_compact_token_limit
|
||||
.or_else(|| turn_context.model_info.auto_compact_token_limit());
|
||||
let full_context_window_limit = turn_context.model_context_window();
|
||||
|
||||
(
|
||||
active_context_tokens.saturating_sub(baseline),
|
||||
scope_limit.unwrap_or(i64::MAX),
|
||||
Some(BodyAfterPrefixWindowStatus {
|
||||
full_context_window_limit,
|
||||
auto_compact_window_prefill_tokens: window.prefill_input_tokens,
|
||||
has_context_remaining_limit: scope_limit.is_some()
|
||||
|| full_context_window_limit.is_some(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let full_context_window_limit = body_window
|
||||
.as_ref()
|
||||
.and_then(|window| window.full_context_window_limit);
|
||||
let auto_compact_window_prefill_tokens = body_window
|
||||
.as_ref()
|
||||
.and_then(|window| window.auto_compact_window_prefill_tokens);
|
||||
|
||||
let full_context_window_limit_reached =
|
||||
full_context_window_limit.is_some_and(|full_context_window_limit| {
|
||||
active_context_tokens >= full_context_window_limit
|
||||
});
|
||||
let token_limit_reached =
|
||||
auto_compact_scope_tokens >= auto_compact_scope_limit || full_context_window_limit_reached;
|
||||
|
||||
let full_context_remaining = full_context_window_limit.map_or(i64::MAX, |limit| {
|
||||
limit.saturating_sub(active_context_tokens)
|
||||
});
|
||||
let tokens_until_compaction = auto_compact_scope_limit
|
||||
.saturating_sub(auto_compact_scope_tokens)
|
||||
.min(full_context_remaining)
|
||||
.max(0);
|
||||
|
||||
let context_remaining_tokens = if let Some(body_window) = body_window {
|
||||
body_window
|
||||
.has_context_remaining_limit
|
||||
.then_some(tokens_until_compaction)
|
||||
} else {
|
||||
turn_context
|
||||
.model_context_window()
|
||||
.map(|limit| limit.saturating_sub(active_context_tokens).max(0))
|
||||
};
|
||||
|
||||
ContextWindowTokenStatus {
|
||||
active_context_tokens,
|
||||
auto_compact_scope_tokens,
|
||||
auto_compact_scope_limit,
|
||||
full_context_window_limit,
|
||||
tokens_until_compaction,
|
||||
auto_compact_window_prefill_tokens,
|
||||
full_context_window_limit_reached,
|
||||
token_limit_reached,
|
||||
context_remaining_tokens,
|
||||
}
|
||||
}
|
||||
@@ -371,9 +371,11 @@ impl Session {
|
||||
let current_manager = self.services.mcp_connection_manager.load_full();
|
||||
refreshed_manager.set_elicitations_auto_deny(current_manager.elicitations_auto_deny());
|
||||
}
|
||||
self.services
|
||||
let superseded_manager = self
|
||||
.services
|
||||
.mcp_connection_manager
|
||||
.store(Arc::new(refreshed_manager));
|
||||
.swap(Arc::new(refreshed_manager));
|
||||
superseded_manager.shutdown().await;
|
||||
}
|
||||
|
||||
pub(crate) async fn refresh_mcp_servers_if_requested(
|
||||
|
||||
@@ -210,6 +210,7 @@ use codex_protocol::exec_output::StreamOutput;
|
||||
|
||||
mod code_mode_warning;
|
||||
mod config_lock;
|
||||
pub(crate) mod context_window;
|
||||
mod handlers;
|
||||
mod inject;
|
||||
mod input_queue;
|
||||
@@ -249,7 +250,6 @@ use self::turn::realtime_text_for_event;
|
||||
use self::turn_context::TurnContext;
|
||||
use self::turn_context::TurnSkillsContext;
|
||||
use self::world_state::build_world_state_from_environment_snapshot;
|
||||
use self::world_state::build_world_state_from_turn_context;
|
||||
use self::world_state::build_world_state_from_turn_context_item;
|
||||
#[cfg(test)]
|
||||
mod rollout_reconstruction_tests;
|
||||
@@ -726,7 +726,7 @@ impl Codex {
|
||||
op: Op,
|
||||
trace: Option<W3cTraceContext>,
|
||||
) -> CodexResult<String> {
|
||||
let id = Uuid::now_v7().to_string();
|
||||
let id = new_submission_id();
|
||||
let sub = Submission {
|
||||
id: id.clone(),
|
||||
op,
|
||||
@@ -744,7 +744,7 @@ impl Codex {
|
||||
client_user_message_id: Option<String>,
|
||||
) -> CodexResult<String> {
|
||||
debug_assert!(matches!(op, Op::UserInput { .. }));
|
||||
let id = Uuid::now_v7().to_string();
|
||||
let id = new_submission_id();
|
||||
let sub = Submission {
|
||||
id: id.clone(),
|
||||
op,
|
||||
@@ -871,6 +871,15 @@ impl Codex {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a core submission ID. App-server exposes submission IDs that
|
||||
/// create turns as a public-facing turn ID.
|
||||
///
|
||||
/// Some use cases take advantage of the fact that these are UUID7 which
|
||||
/// encodes a timestamp, so think carefully before changing this.
|
||||
fn new_submission_id() -> String {
|
||||
Uuid::now_v7().to_string()
|
||||
}
|
||||
|
||||
fn get_service_tier(
|
||||
configured_service_tier: Option<String>,
|
||||
fast_mode_enabled: bool,
|
||||
@@ -1404,7 +1413,9 @@ impl Session {
|
||||
let mut state = self.state.lock().await;
|
||||
state.replace_history(history, reference_context_item);
|
||||
if let Some(world_state) = world_state_baseline {
|
||||
state.history.set_world_state_baseline(world_state);
|
||||
state
|
||||
.history
|
||||
.set_world_state_baseline(Arc::new(world_state));
|
||||
}
|
||||
let fallback_ids = state.auto_compact_window_ids();
|
||||
let window_id = window_id.unwrap_or(fallback_ids.window_id);
|
||||
@@ -2250,8 +2261,7 @@ impl Session {
|
||||
strict_auto_review: false,
|
||||
});
|
||||
}
|
||||
AskForApproval::OnFailure
|
||||
| AskForApproval::OnRequest
|
||||
AskForApproval::OnRequest
|
||||
| AskForApproval::UnlessTrusted
|
||||
| AskForApproval::Granular(_) => {}
|
||||
}
|
||||
@@ -2780,25 +2790,27 @@ impl Session {
|
||||
pub(crate) async fn record_step_environment_context_if_changed(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
previous_world_state: &Arc<WorldState>,
|
||||
step_context: &step_context::StepContext,
|
||||
) {
|
||||
if !turn_context.config.include_environment_context {
|
||||
return;
|
||||
) -> Arc<WorldState> {
|
||||
let world_state = Arc::new(
|
||||
self.build_world_state_for_environments(turn_context, &step_context.environments)
|
||||
.await,
|
||||
);
|
||||
let items = crate::context_manager::updates::merge_contextual_fragments(
|
||||
world_state.render_diff(previous_world_state.as_ref()),
|
||||
);
|
||||
if !items.is_empty() {
|
||||
self.record_conversation_items(turn_context, &items).await;
|
||||
}
|
||||
|
||||
let world_state =
|
||||
build_world_state_from_environment_snapshot(turn_context, &step_context.environments);
|
||||
let items = {
|
||||
let mut state = self.state.lock().await;
|
||||
crate::context_manager::updates::merge_contextual_fragments(
|
||||
state.history.update_world_state(world_state),
|
||||
)
|
||||
};
|
||||
if items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.record_conversation_items(turn_context, &items).await;
|
||||
// ContextManager remembers this for later turns; run_turn owns the live value.
|
||||
self.state
|
||||
.lock()
|
||||
.await
|
||||
.history
|
||||
.set_world_state_baseline(Arc::clone(&world_state));
|
||||
world_state
|
||||
}
|
||||
|
||||
pub(crate) async fn record_inter_agent_communication(
|
||||
@@ -2900,6 +2912,7 @@ impl Session {
|
||||
turn_context: &TurnContext,
|
||||
items: Vec<ResponseItem>,
|
||||
reference_context_item: Option<TurnContextItem>,
|
||||
world_state_baseline: Option<Arc<WorldState>>,
|
||||
compacted_item: CompactedItem,
|
||||
) {
|
||||
let items = if turn_context.config.features.enabled(Feature::ItemIds) {
|
||||
@@ -2911,11 +2924,6 @@ impl Session {
|
||||
replacement_history: Some(items.clone()),
|
||||
..compacted_item
|
||||
};
|
||||
let world_state_baseline = if reference_context_item.is_some() {
|
||||
Some(self.build_world_state(turn_context).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
state.replace_history(items, reference_context_item.clone());
|
||||
@@ -3047,16 +3055,11 @@ impl Session {
|
||||
items
|
||||
}
|
||||
|
||||
pub(crate) async fn build_initial_context(
|
||||
pub(crate) async fn build_world_state_for_environments(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
) -> Vec<ResponseItem> {
|
||||
let world_state = self.build_world_state(turn_context).await;
|
||||
self.build_initial_context_with_world_state(turn_context, &world_state)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn build_world_state(&self, turn_context: &TurnContext) -> WorldState {
|
||||
environments: &TurnEnvironmentSnapshot,
|
||||
) -> WorldState {
|
||||
let environment_subagents = if turn_context.config.include_environment_context {
|
||||
self.services
|
||||
.agent_control
|
||||
@@ -3065,10 +3068,14 @@ impl Session {
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
build_world_state_from_turn_context(turn_context, &environment_subagents)
|
||||
build_world_state_from_environment_snapshot(
|
||||
turn_context,
|
||||
environments,
|
||||
&environment_subagents,
|
||||
)
|
||||
}
|
||||
|
||||
async fn build_initial_context_with_world_state(
|
||||
pub(crate) async fn build_initial_context_with_world_state(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
world_state: &WorldState,
|
||||
@@ -3417,15 +3424,15 @@ impl Session {
|
||||
pub(crate) async fn maybe_start_new_context_window(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
world_state: Arc<WorldState>,
|
||||
) -> Option<u64> {
|
||||
let window = {
|
||||
let mut state = self.state.lock().await;
|
||||
state.start_new_context_window_if_requested()
|
||||
};
|
||||
let (window_number, window_ids) = window?;
|
||||
let world_state = self.build_world_state(turn_context).await;
|
||||
let context_items = self
|
||||
.build_initial_context_with_world_state(turn_context, &world_state)
|
||||
.build_initial_context_with_world_state(turn_context, world_state.as_ref())
|
||||
.await;
|
||||
let turn_context_item = turn_context.to_turn_context_item();
|
||||
let replacement_history = context_items;
|
||||
@@ -3476,7 +3483,7 @@ impl Session {
|
||||
pub(crate) async fn record_context_updates_and_set_reference_context_item(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
) {
|
||||
) -> Arc<WorldState> {
|
||||
let reference_context_item = {
|
||||
let state = self.state.lock().await;
|
||||
state.reference_context_item()
|
||||
@@ -3484,16 +3491,19 @@ impl Session {
|
||||
let turn_context_item = turn_context.to_turn_context_item();
|
||||
let turn_context_changed = reference_context_item.as_ref() != Some(&turn_context_item);
|
||||
let should_inject_full_context = reference_context_item.is_none();
|
||||
let world_state = self.build_world_state(turn_context).await;
|
||||
let world_state = Arc::new(
|
||||
self.build_world_state_for_environments(turn_context, &turn_context.environments)
|
||||
.await,
|
||||
);
|
||||
let mut context_items = if should_inject_full_context {
|
||||
let context_items = self
|
||||
.build_initial_context_with_world_state(turn_context, &world_state)
|
||||
.build_initial_context_with_world_state(turn_context, world_state.as_ref())
|
||||
.await;
|
||||
self.state
|
||||
.lock()
|
||||
.await
|
||||
.history
|
||||
.set_world_state_baseline(world_state);
|
||||
.set_world_state_baseline(Arc::clone(&world_state));
|
||||
context_items
|
||||
} else {
|
||||
// Steady-state path: append only built-in context diffs here; turn-scoped extension
|
||||
@@ -3504,7 +3514,7 @@ impl Session {
|
||||
let world_state_items = {
|
||||
let mut state = self.state.lock().await;
|
||||
crate::context_manager::updates::merge_contextual_fragments(
|
||||
state.history.update_world_state(world_state),
|
||||
state.history.update_world_state(Arc::clone(&world_state)),
|
||||
)
|
||||
};
|
||||
context_items.extend(world_state_items);
|
||||
@@ -3517,7 +3527,7 @@ impl Session {
|
||||
);
|
||||
}
|
||||
if !turn_context_changed && context_items.is_empty() {
|
||||
return;
|
||||
return world_state;
|
||||
}
|
||||
if !context_items.is_empty() {
|
||||
self.record_conversation_items(turn_context, &context_items)
|
||||
@@ -3532,6 +3542,7 @@ impl Session {
|
||||
// context items.
|
||||
let mut state = self.state.lock().await;
|
||||
state.set_reference_context_item(Some(turn_context_item));
|
||||
world_state
|
||||
}
|
||||
|
||||
pub(crate) async fn update_token_usage_info(
|
||||
|
||||
@@ -752,15 +752,6 @@ impl Session {
|
||||
) {
|
||||
post_session_configured_events.push(event);
|
||||
}
|
||||
if config.permissions.approval_policy.value() == AskForApproval::OnFailure {
|
||||
post_session_configured_events.push(Event {
|
||||
id: "".to_owned(),
|
||||
msg: EventMsg::Warning(WarningEvent {
|
||||
message: "`on-failure` approval policy is deprecated and will be removed in a future release. Use `on-request` for interactive approvals or `never` for non-interactive runs.".to_string(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
let auth = auth.as_ref();
|
||||
let auth_mode = auth.map(CodexAuth::auth_mode).map(TelemetryAuthMode::from);
|
||||
let account_id = auth.and_then(CodexAuth::get_account_id);
|
||||
|
||||
@@ -1289,7 +1289,7 @@ async fn reload_user_config_layer_updates_base_and_selected_profile_layers() {
|
||||
let profile_config_path = codex_home.join("work.config.toml");
|
||||
std::fs::write(
|
||||
&base_config_path,
|
||||
"model = \"base\"\napproval_policy = \"on-failure\"\n",
|
||||
"model = \"base\"\napproval_policy = \"on-request\"\n",
|
||||
)
|
||||
.expect("write base user config");
|
||||
std::fs::write(&profile_config_path, "model = \"profile-old\"\n")
|
||||
@@ -2003,7 +2003,7 @@ async fn resumed_history_injects_initial_context_on_first_context_update_only()
|
||||
session
|
||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
||||
.await;
|
||||
let initial_context = session.build_initial_context(&turn_context).await;
|
||||
let initial_context = build_initial_context(&session, &turn_context).await;
|
||||
expected.extend(initial_context);
|
||||
let history_after_seed = session.clone_history().await;
|
||||
assert_eq!(expected, history_after_seed.raw_items());
|
||||
@@ -2915,7 +2915,7 @@ async fn thread_rollback_drops_last_turn_from_history() {
|
||||
)
|
||||
.await;
|
||||
|
||||
let initial_context = sess.build_initial_context(tc.as_ref()).await;
|
||||
let initial_context = build_initial_context(&sess, tc.as_ref()).await;
|
||||
let turn_1 = vec![
|
||||
user_message("turn 1 user"),
|
||||
assistant_message("turn 1 assistant"),
|
||||
@@ -2983,7 +2983,7 @@ async fn thread_rollback_clears_history_when_num_turns_exceeds_existing_turns()
|
||||
)
|
||||
.await;
|
||||
|
||||
let initial_context = sess.build_initial_context(tc.as_ref()).await;
|
||||
let initial_context = build_initial_context(&sess, tc.as_ref()).await;
|
||||
let turn_1 = vec![user_message("turn 1 user")];
|
||||
let mut full_history = Vec::new();
|
||||
full_history.extend(initial_context.clone());
|
||||
@@ -3009,7 +3009,7 @@ async fn thread_rollback_clears_history_when_num_turns_exceeds_existing_turns()
|
||||
async fn thread_rollback_fails_without_persisted_thread_history() {
|
||||
let (sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
|
||||
let initial_context = sess.build_initial_context(tc.as_ref()).await;
|
||||
let initial_context = build_initial_context(&sess, tc.as_ref()).await;
|
||||
sess.record_conversation_items(tc.as_ref(), &initial_context)
|
||||
.await;
|
||||
|
||||
@@ -3419,7 +3419,7 @@ async fn thread_rollback_persists_marker_and_replays_cumulatively() {
|
||||
async fn thread_rollback_fails_when_turn_in_progress() {
|
||||
let (sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
|
||||
let initial_context = sess.build_initial_context(tc.as_ref()).await;
|
||||
let initial_context = build_initial_context(&sess, tc.as_ref()).await;
|
||||
sess.record_conversation_items(tc.as_ref(), &initial_context)
|
||||
.await;
|
||||
|
||||
@@ -3440,7 +3440,7 @@ async fn thread_rollback_fails_when_turn_in_progress() {
|
||||
async fn thread_rollback_fails_when_num_turns_is_zero() {
|
||||
let (sess, tc, rx) = make_session_and_context_with_rx().await;
|
||||
|
||||
let initial_context = sess.build_initial_context(tc.as_ref()).await;
|
||||
let initial_context = build_initial_context(&sess, tc.as_ref()).await;
|
||||
sess.record_conversation_items(tc.as_ref(), &initial_context)
|
||||
.await;
|
||||
|
||||
@@ -5062,6 +5062,22 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
|
||||
assert!(msg.contains("zsh fork feature enabled, but no packaged zsh fork is available"));
|
||||
}
|
||||
|
||||
async fn build_initial_context(session: &Session, turn_context: &TurnContext) -> Vec<ResponseItem> {
|
||||
let world_state = build_world_state_from_turn_context(session, turn_context).await;
|
||||
session
|
||||
.build_initial_context_with_world_state(turn_context, &world_state)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn build_world_state_from_turn_context(
|
||||
session: &Session,
|
||||
turn_context: &TurnContext,
|
||||
) -> WorldState {
|
||||
session
|
||||
.build_world_state_for_environments(turn_context, &turn_context.environments)
|
||||
.await
|
||||
}
|
||||
|
||||
// todo: use online model info
|
||||
pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
let (tx_event, _rx_event) = async_channel::unbounded();
|
||||
@@ -7723,7 +7739,7 @@ async fn build_initial_context_uses_previous_realtime_state() {
|
||||
let (session, mut turn_context) = make_session_and_context().await;
|
||||
turn_context.realtime_active = true;
|
||||
|
||||
let initial_context = session.build_initial_context(&turn_context).await;
|
||||
let initial_context = build_initial_context(&session, &turn_context).await;
|
||||
let developer_texts = developer_input_texts(&initial_context);
|
||||
assert!(
|
||||
developer_texts
|
||||
@@ -7737,7 +7753,7 @@ async fn build_initial_context_uses_previous_realtime_state() {
|
||||
let mut state = session.state.lock().await;
|
||||
state.set_reference_context_item(Some(previous_context_item));
|
||||
}
|
||||
let resumed_context = session.build_initial_context(&turn_context).await;
|
||||
let resumed_context = build_initial_context(&session, &turn_context).await;
|
||||
let resumed_developer_texts = developer_input_texts(&resumed_context);
|
||||
assert!(
|
||||
!resumed_developer_texts
|
||||
@@ -7836,7 +7852,7 @@ async fn build_initial_context_includes_prompt_fragments_from_extensions() {
|
||||
.thread_extension_data
|
||||
.insert(PromptExtensionTestState);
|
||||
|
||||
let initial_context = session.build_initial_context(&turn_context).await;
|
||||
let initial_context = build_initial_context(&session, &turn_context).await;
|
||||
let developer_messages = developer_message_texts(&initial_context);
|
||||
|
||||
assert!(
|
||||
@@ -7862,7 +7878,7 @@ async fn build_initial_context_includes_turn_context_fragments_from_extensions()
|
||||
expected_model_context_window: Some(50),
|
||||
});
|
||||
|
||||
let initial_context = session.build_initial_context(&turn_context).await;
|
||||
let initial_context = build_initial_context(&session, &turn_context).await;
|
||||
let developer_messages = developer_message_texts(&initial_context);
|
||||
|
||||
assert!(
|
||||
@@ -7889,7 +7905,7 @@ async fn record_context_updates_includes_turn_context_fragments_on_steady_state_
|
||||
});
|
||||
let mut previous_context_item = turn_context.to_turn_context_item();
|
||||
previous_context_item.turn_id = Some("previous-turn-id".to_string());
|
||||
let world_state = session.build_world_state(&turn_context).await;
|
||||
let world_state = Arc::new(build_world_state_from_turn_context(&session, &turn_context).await);
|
||||
{
|
||||
let mut state = session.state.lock().await;
|
||||
state.set_reference_context_item(Some(previous_context_item));
|
||||
@@ -7916,7 +7932,7 @@ async fn build_initial_context_omits_prompt_fragments_without_extension_state()
|
||||
let (mut session, turn_context) = make_session_and_context().await;
|
||||
session.services.extensions = prompt_extension_test_registry();
|
||||
|
||||
let initial_context = session.build_initial_context(&turn_context).await;
|
||||
let initial_context = build_initial_context(&session, &turn_context).await;
|
||||
let developer_messages = developer_message_texts(&initial_context);
|
||||
|
||||
assert!(
|
||||
@@ -7933,7 +7949,7 @@ async fn build_initial_context_adds_multi_agent_v2_root_usage_hint_as_developer_
|
||||
let (session, turn_context) =
|
||||
make_multi_agent_v2_usage_hint_test_session(/*enable_multi_agent_v2*/ true).await;
|
||||
|
||||
let initial_context = session.build_initial_context(turn_context.as_ref()).await;
|
||||
let initial_context = build_initial_context(&session, turn_context.as_ref()).await;
|
||||
|
||||
let developer_messages = developer_message_texts(&initial_context);
|
||||
assert!(
|
||||
@@ -7971,7 +7987,7 @@ async fn build_initial_context_adds_multi_agent_v2_subagent_usage_hint_as_develo
|
||||
.expect("thread settings should not be shared")
|
||||
.session_source = session_source;
|
||||
|
||||
let initial_context = session.build_initial_context(turn_context.as_ref()).await;
|
||||
let initial_context = build_initial_context(&session, turn_context.as_ref()).await;
|
||||
|
||||
let developer_messages = developer_message_texts(&initial_context);
|
||||
assert!(
|
||||
@@ -7993,7 +8009,7 @@ async fn build_initial_context_omits_multi_agent_v2_usage_hints_when_feature_dis
|
||||
let (session, turn_context) =
|
||||
make_multi_agent_v2_usage_hint_test_session(/*enable_multi_agent_v2*/ false).await;
|
||||
|
||||
let initial_context = session.build_initial_context(turn_context.as_ref()).await;
|
||||
let initial_context = build_initial_context(&session, turn_context.as_ref()).await;
|
||||
|
||||
let developer_messages = developer_message_texts(&initial_context);
|
||||
assert!(
|
||||
@@ -8020,7 +8036,7 @@ async fn build_initial_context_omits_multi_agent_v2_usage_hints_when_hint_is_emp
|
||||
)
|
||||
.await;
|
||||
|
||||
let initial_context = session.build_initial_context(turn_context.as_ref()).await;
|
||||
let initial_context = build_initial_context(&session, turn_context.as_ref()).await;
|
||||
|
||||
let developer_messages = developer_message_texts(&initial_context);
|
||||
assert!(
|
||||
@@ -8050,7 +8066,7 @@ async fn build_initial_context_omits_default_image_save_location_with_image_hist
|
||||
)
|
||||
.await;
|
||||
|
||||
let initial_context = session.build_initial_context(&turn_context).await;
|
||||
let initial_context = build_initial_context(&session, &turn_context).await;
|
||||
let developer_texts = developer_input_texts(&initial_context);
|
||||
assert!(
|
||||
!developer_texts
|
||||
@@ -8064,7 +8080,7 @@ async fn build_initial_context_omits_default_image_save_location_with_image_hist
|
||||
async fn build_initial_context_omits_default_image_save_location_without_image_history() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
|
||||
let initial_context = session.build_initial_context(&turn_context).await;
|
||||
let initial_context = build_initial_context(&session, &turn_context).await;
|
||||
let developer_texts = developer_input_texts(&initial_context);
|
||||
|
||||
assert!(
|
||||
@@ -8106,7 +8122,7 @@ async fn build_initial_context_trims_skill_metadata_from_context_window_budget()
|
||||
turn_context.model_info.context_window = Some(100);
|
||||
turn_context.turn_skills = TurnSkillsContext::new(HostSkillsSnapshot::new(Arc::new(outcome)));
|
||||
|
||||
let initial_context = session.build_initial_context(&turn_context).await;
|
||||
let initial_context = build_initial_context(&session, &turn_context).await;
|
||||
let developer_texts = developer_input_texts(&initial_context);
|
||||
|
||||
assert!(
|
||||
@@ -8257,7 +8273,7 @@ async fn build_initial_context_emits_thread_start_skill_warning_on_repeated_buil
|
||||
turn_context.model_info.context_window = Some(100);
|
||||
turn_context.turn_skills = TurnSkillsContext::new(HostSkillsSnapshot::new(Arc::new(outcome)));
|
||||
|
||||
let _ = session.build_initial_context(&turn_context).await;
|
||||
let _ = build_initial_context(&session, &turn_context).await;
|
||||
let warning_event = timeout(Duration::from_secs(1), rx.recv())
|
||||
.await
|
||||
.expect("warning event should arrive")
|
||||
@@ -8268,7 +8284,7 @@ async fn build_initial_context_emits_thread_start_skill_warning_on_repeated_buil
|
||||
if message == "Exceeded skills context budget of 2%. All skill descriptions were removed and 2 additional skills were not included in the model-visible skills list."
|
||||
));
|
||||
|
||||
let _ = session.build_initial_context(&turn_context).await;
|
||||
let _ = build_initial_context(&session, &turn_context).await;
|
||||
let warning_event = timeout(Duration::from_secs(1), rx.recv())
|
||||
.await
|
||||
.expect("warning event should arrive on repeated build")
|
||||
@@ -8388,7 +8404,7 @@ async fn build_initial_context_uses_previous_turn_settings_for_realtime_end() {
|
||||
session
|
||||
.set_previous_turn_settings(Some(previous_turn_settings))
|
||||
.await;
|
||||
let initial_context = session.build_initial_context(&turn_context).await;
|
||||
let initial_context = build_initial_context(&session, &turn_context).await;
|
||||
let developer_texts = developer_input_texts(&initial_context);
|
||||
assert!(
|
||||
developer_texts
|
||||
@@ -8411,7 +8427,7 @@ async fn build_initial_context_restates_realtime_start_when_reference_context_is
|
||||
session
|
||||
.set_previous_turn_settings(Some(previous_turn_settings))
|
||||
.await;
|
||||
let initial_context = session.build_initial_context(&turn_context).await;
|
||||
let initial_context = build_initial_context(&session, &turn_context).await;
|
||||
let developer_texts = developer_input_texts(&initial_context);
|
||||
assert!(
|
||||
developer_texts
|
||||
@@ -8498,7 +8514,7 @@ async fn record_context_updates_and_set_reference_context_item_injects_full_cont
|
||||
.record_context_updates_and_set_reference_context_item(&turn_context)
|
||||
.await;
|
||||
let history = session.clone_history().await;
|
||||
let initial_context = session.build_initial_context(&turn_context).await;
|
||||
let initial_context = build_initial_context(&session, &turn_context).await;
|
||||
assert_eq!(history.raw_items().to_vec(), initial_context);
|
||||
|
||||
let current_context = session.reference_context_item().await;
|
||||
@@ -8545,7 +8561,7 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co
|
||||
|
||||
let history = session.clone_history().await;
|
||||
let mut expected_history = vec![compacted_summary];
|
||||
let initial_context = session.build_initial_context(&turn_context).await;
|
||||
let initial_context = build_initial_context(&session, &turn_context).await;
|
||||
expected_history.extend(initial_context);
|
||||
assert_eq!(history.raw_items().to_vec(), expected_history);
|
||||
}
|
||||
@@ -8563,7 +8579,8 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline
|
||||
.with_model(next_model.to_string(), &session.services.models_manager)
|
||||
.await;
|
||||
let previous_context_item = previous_context.to_turn_context_item();
|
||||
let world_state = session.build_world_state(&previous_context).await;
|
||||
let world_state =
|
||||
Arc::new(build_world_state_from_turn_context(&session, &previous_context).await);
|
||||
{
|
||||
let mut state = session.state.lock().await;
|
||||
state.set_reference_context_item(Some(previous_context_item.clone()));
|
||||
@@ -8657,7 +8674,7 @@ async fn build_initial_context_prepends_model_switch_message() {
|
||||
session
|
||||
.set_previous_turn_settings(Some(previous_turn_settings))
|
||||
.await;
|
||||
let initial_context = session.build_initial_context(&turn_context).await;
|
||||
let initial_context = build_initial_context(&session, &turn_context).await;
|
||||
|
||||
let ResponseItem::Message { role, content, .. } = &initial_context[0] else {
|
||||
panic!("expected developer message");
|
||||
@@ -9982,9 +9999,7 @@ async fn sample_rollout(
|
||||
// Use the same turn_context source as record_initial_history so model_info (and thus
|
||||
// personality_spec) matches reconstruction.
|
||||
let reconstruction_turn = session.new_default_turn().await;
|
||||
let mut initial_context = session
|
||||
.build_initial_context(reconstruction_turn.as_ref())
|
||||
.await;
|
||||
let mut initial_context = build_initial_context(session, reconstruction_turn.as_ref()).await;
|
||||
// Ensure personality_spec is present when Personality is enabled, so expected matches
|
||||
// what reconstruction produces (build_initial_context may omit it when baked into model).
|
||||
if !initial_context.iter().any(|m| {
|
||||
@@ -10162,7 +10177,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
|
||||
// Ensure policy is NOT OnRequest so the early rejection path triggers
|
||||
turn_context_raw
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnFailure)
|
||||
.set(AskForApproval::Never)
|
||||
.expect("test setup should allow updating approval policy");
|
||||
let session = Arc::new(session);
|
||||
let mut turn_context = Arc::new(turn_context_raw);
|
||||
@@ -10325,7 +10340,7 @@ async fn unified_exec_rejects_escalated_permissions_when_policy_not_on_request()
|
||||
let (session, mut turn_context_raw) = make_session_and_context().await;
|
||||
turn_context_raw
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnFailure)
|
||||
.set(AskForApproval::Never)
|
||||
.expect("test setup should allow updating approval policy");
|
||||
let session = Arc::new(session);
|
||||
let turn_context = Arc::new(turn_context_raw);
|
||||
|
||||
@@ -407,7 +407,7 @@ async fn strict_auto_review_turn_grant_forces_guardian_for_shell_command_policy_
|
||||
|
||||
turn_context_raw
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnFailure)
|
||||
.set(AskForApproval::Never)
|
||||
.expect("test setup should allow updating approval policy");
|
||||
turn_context_raw.permission_profile = codex_protocol::models::PermissionProfile::Disabled;
|
||||
let mut config = (*turn_context_raw.config).clone();
|
||||
@@ -523,8 +523,10 @@ async fn process_compacted_history_preserves_separate_guardian_developer_message
|
||||
}
|
||||
turn_context.session_source = guardian_source;
|
||||
turn_context.developer_instructions = Some(guardian_policy.clone());
|
||||
let world_state = Arc::new(build_world_state_from_turn_context(&session, &turn_context).await);
|
||||
let initial_context_injection = InitialContextInjection::BeforeLastUserMessage(world_state);
|
||||
|
||||
let refreshed = crate::compact_remote::process_compacted_history(
|
||||
let (refreshed, _) = crate::compact_remote::process_compacted_history(
|
||||
&session,
|
||||
&turn_context,
|
||||
vec![
|
||||
@@ -547,7 +549,7 @@ async fn process_compacted_history_preserves_separate_guardian_developer_message
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
},
|
||||
],
|
||||
InitialContextInjection::BeforeLastUserMessage,
|
||||
&initial_context_injection,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -164,7 +164,9 @@ pub(crate) async fn run_turn(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
sess.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
|
||||
// Keep the exact model-visible state used by this turn and its inline compactions.
|
||||
let mut world_state = sess
|
||||
.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
|
||||
.await;
|
||||
|
||||
let Some((injection_items, explicitly_enabled_connectors)) =
|
||||
@@ -250,11 +252,13 @@ pub(crate) async fn run_turn(
|
||||
let step_context = StepContext {
|
||||
environments: sess.services.turn_environments.snapshot().await,
|
||||
};
|
||||
sess.record_step_environment_context_if_changed(
|
||||
turn_context.as_ref(),
|
||||
&step_context,
|
||||
)
|
||||
.await;
|
||||
world_state = sess
|
||||
.record_step_environment_context_if_changed(
|
||||
turn_context.as_ref(),
|
||||
&world_state,
|
||||
&step_context,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Construct the input that we will send to the model.
|
||||
@@ -294,8 +298,11 @@ pub(crate) async fn run_turn(
|
||||
let (has_pending_input, token_status, estimated_token_count) = async {
|
||||
let has_pending_input =
|
||||
sess.input_queue.has_pending_input(&sess.active_turn).await;
|
||||
let token_status =
|
||||
auto_compact_token_status(sess.as_ref(), turn_context.as_ref()).await;
|
||||
let token_status = super::context_window::context_window_token_status(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
)
|
||||
.await;
|
||||
let estimated_token_count =
|
||||
sess.get_estimated_token_count(turn_context.as_ref()).await;
|
||||
(has_pending_input, token_status, estimated_token_count)
|
||||
@@ -322,26 +329,15 @@ pub(crate) async fn run_turn(
|
||||
"post sampling token usage"
|
||||
);
|
||||
|
||||
let tokens_after_sampling = token_status.active_context_tokens;
|
||||
let full_context_remaining = token_status
|
||||
.full_context_window_limit
|
||||
.map_or(i64::MAX, |limit| {
|
||||
limit.saturating_sub(tokens_after_sampling)
|
||||
});
|
||||
let tokens_until_compaction = token_status
|
||||
.auto_compact_scope_limit
|
||||
.saturating_sub(token_status.auto_compact_scope_tokens)
|
||||
.min(full_context_remaining)
|
||||
.max(0);
|
||||
super::token_budget::maybe_record(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
tokens_until_compaction,
|
||||
token_status.tokens_until_compaction,
|
||||
)
|
||||
.await;
|
||||
|
||||
let started_new_context_window = sess
|
||||
.maybe_start_new_context_window(turn_context.as_ref())
|
||||
.maybe_start_new_context_window(turn_context.as_ref(), Arc::clone(&world_state))
|
||||
.await
|
||||
.is_some();
|
||||
if started_new_context_window && needs_follow_up {
|
||||
@@ -361,7 +357,7 @@ pub(crate) async fn run_turn(
|
||||
&sess,
|
||||
&turn_context,
|
||||
&mut client_session,
|
||||
InitialContextInjection::BeforeLastUserMessage,
|
||||
InitialContextInjection::BeforeLastUserMessage(Arc::clone(&world_state)),
|
||||
CompactionReason::ContextLimit,
|
||||
CompactionPhase::MidTurn,
|
||||
)
|
||||
@@ -802,68 +798,6 @@ async fn track_turn_resolved_config_analytics(
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AutoCompactTokenStatus {
|
||||
// Full active context usage, independent of the configured auto-compact scope.
|
||||
active_context_tokens: i64,
|
||||
// Usage counted against `model_auto_compact_token_limit` for the current scope.
|
||||
auto_compact_scope_tokens: i64,
|
||||
auto_compact_scope_limit: i64,
|
||||
full_context_window_limit: Option<i64>,
|
||||
auto_compact_window_prefill_tokens: Option<i64>,
|
||||
full_context_window_limit_reached: bool,
|
||||
token_limit_reached: bool,
|
||||
}
|
||||
|
||||
async fn auto_compact_token_status(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
) -> AutoCompactTokenStatus {
|
||||
let active_context_tokens = sess.get_total_token_usage().await;
|
||||
let mut auto_compact_window_prefill_tokens = None;
|
||||
let (auto_compact_scope_tokens, auto_compact_scope_limit, full_context_window_limit) =
|
||||
match turn_context.config.model_auto_compact_token_limit_scope {
|
||||
AutoCompactTokenLimitScope::Total => (
|
||||
active_context_tokens,
|
||||
turn_context
|
||||
.model_info
|
||||
.auto_compact_token_limit()
|
||||
.unwrap_or(i64::MAX),
|
||||
None,
|
||||
),
|
||||
AutoCompactTokenLimitScope::BodyAfterPrefix => {
|
||||
let window = sess.auto_compact_window_snapshot().await;
|
||||
auto_compact_window_prefill_tokens = window.prefill_input_tokens;
|
||||
let baseline = window.prefill_input_tokens.unwrap_or(active_context_tokens);
|
||||
(
|
||||
active_context_tokens.saturating_sub(baseline),
|
||||
turn_context
|
||||
.config
|
||||
.model_auto_compact_token_limit
|
||||
.or_else(|| turn_context.model_info.auto_compact_token_limit())
|
||||
.unwrap_or(i64::MAX),
|
||||
turn_context.model_context_window(),
|
||||
)
|
||||
}
|
||||
};
|
||||
let full_context_window_limit_reached =
|
||||
full_context_window_limit.is_some_and(|full_context_window_limit| {
|
||||
active_context_tokens >= full_context_window_limit
|
||||
});
|
||||
let token_limit_reached =
|
||||
auto_compact_scope_tokens >= auto_compact_scope_limit || full_context_window_limit_reached;
|
||||
|
||||
AutoCompactTokenStatus {
|
||||
active_context_tokens,
|
||||
auto_compact_scope_tokens,
|
||||
auto_compact_scope_limit,
|
||||
full_context_window_limit,
|
||||
auto_compact_window_prefill_tokens,
|
||||
full_context_window_limit_reached,
|
||||
token_limit_reached,
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
async fn run_pre_sampling_compact(
|
||||
sess: &Arc<Session>,
|
||||
@@ -879,7 +813,9 @@ async fn run_pre_sampling_compact(
|
||||
}
|
||||
|
||||
maybe_run_previous_model_inline_compact(sess, turn_context, client_session).await?;
|
||||
let token_status = auto_compact_token_status(sess.as_ref(), turn_context.as_ref()).await;
|
||||
let token_status =
|
||||
super::context_window::context_window_token_status(sess.as_ref(), turn_context.as_ref())
|
||||
.await;
|
||||
// Compact if the configured auto-compaction budget or usable context window is exhausted.
|
||||
if token_status.token_limit_reached {
|
||||
run_auto_compact(
|
||||
|
||||
@@ -4,34 +4,21 @@ use crate::context::world_state::WorldState;
|
||||
use crate::environment_selection::TurnEnvironmentSnapshot;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
|
||||
pub(super) fn build_world_state_from_turn_context(
|
||||
pub(super) fn build_world_state_from_environment_snapshot(
|
||||
turn_context: &TurnContext,
|
||||
environments: &TurnEnvironmentSnapshot,
|
||||
environment_subagents: &str,
|
||||
) -> WorldState {
|
||||
let mut world_state = WorldState::default();
|
||||
if turn_context.config.include_environment_context {
|
||||
world_state.add_section(
|
||||
EnvironmentsState::from_turn_context(turn_context)
|
||||
EnvironmentsState::from_turn_context_with_environments(turn_context, environments)
|
||||
.with_subagents(environment_subagents.to_string()),
|
||||
);
|
||||
}
|
||||
world_state
|
||||
}
|
||||
|
||||
pub(super) fn build_world_state_from_environment_snapshot(
|
||||
turn_context: &TurnContext,
|
||||
environments: &TurnEnvironmentSnapshot,
|
||||
) -> WorldState {
|
||||
let mut world_state = WorldState::default();
|
||||
if turn_context.config.include_environment_context {
|
||||
world_state.add_section(EnvironmentsState::from_turn_context_with_environments(
|
||||
turn_context,
|
||||
environments,
|
||||
));
|
||||
}
|
||||
world_state
|
||||
}
|
||||
|
||||
pub(super) fn build_world_state_from_turn_context_item(
|
||||
turn_context_item: &TurnContextItem,
|
||||
) -> WorldState {
|
||||
|
||||
@@ -226,6 +226,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
arg0: None,
|
||||
exec_server_sandbox: None,
|
||||
exec_server_enforce_managed_network: false,
|
||||
exec_server_managed_network: None,
|
||||
};
|
||||
|
||||
let stdout_stream = Some(StdoutStream {
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::init_state_db;
|
||||
use crate::installation_id::INSTALLATION_ID_FILENAME;
|
||||
use crate::rollout::RolloutRecorder;
|
||||
use crate::session::session::SessionSettingsUpdate;
|
||||
use crate::session::tests::build_world_state_from_turn_context;
|
||||
use crate::session::tests::make_session_and_context;
|
||||
use crate::tasks::InterruptedTurnHistoryMarker;
|
||||
use crate::tasks::interrupted_turn_history_marker;
|
||||
@@ -225,7 +226,10 @@ fn out_of_range_truncation_drops_pre_user_active_turn_prefix() {
|
||||
#[tokio::test]
|
||||
async fn ignores_session_prefix_messages_when_truncating() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let mut items = session.build_initial_context(&turn_context).await;
|
||||
let world_state = build_world_state_from_turn_context(&session, &turn_context).await;
|
||||
let mut items = session
|
||||
.build_initial_context_with_world_state(&turn_context, &world_state)
|
||||
.await;
|
||||
items.push(user_msg("feature request"));
|
||||
items.push(assistant_msg("ack"));
|
||||
items.push(user_msg("second question"));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::session::tests::build_world_state_from_turn_context;
|
||||
use crate::session::tests::make_session_and_context;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::models::ContentItem;
|
||||
@@ -166,7 +167,10 @@ fn truncates_rollout_from_start_applies_thread_rollback_markers() {
|
||||
#[tokio::test]
|
||||
async fn ignores_session_prefix_messages_when_truncating_rollout_from_start() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let mut items = session.build_initial_context(&turn_context).await;
|
||||
let world_state = build_world_state_from_turn_context(&session, &turn_context).await;
|
||||
let mut items = session
|
||||
.build_initial_context_with_world_state(&turn_context, &world_state)
|
||||
.await;
|
||||
items.push(user_msg("feature request"));
|
||||
items.push(assistant_msg("ack"));
|
||||
items.push(user_msg("second question"));
|
||||
|
||||
@@ -75,19 +75,15 @@ impl ToolExecutor<ToolInvocation> for GetContextRemainingHandler {
|
||||
));
|
||||
}
|
||||
|
||||
let Some(model_context_window) = invocation.turn.model_context_window() else {
|
||||
return Ok(boxed_tool_output(GetContextRemainingOutput::new(
|
||||
/*tokens_left*/ None,
|
||||
)));
|
||||
};
|
||||
let active_context_tokens = invocation.session.get_total_token_usage().await.max(0);
|
||||
let tokens_left = model_context_window
|
||||
.saturating_sub(active_context_tokens)
|
||||
.max(0);
|
||||
let token_status = crate::session::context_window::context_window_token_status(
|
||||
invocation.session.as_ref(),
|
||||
invocation.turn.as_ref(),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(boxed_tool_output(GetContextRemainingOutput::new(Some(
|
||||
tokens_left,
|
||||
))))
|
||||
Ok(boxed_tool_output(GetContextRemainingOutput::new(
|
||||
token_status.context_remaining_tokens,
|
||||
)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ use crate::tools::registry::CoreToolRuntime;
|
||||
use crate::tools::registry::ToolExecutor;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
|
||||
pub struct ViewImageHandler {
|
||||
options: ViewImageToolOptions,
|
||||
@@ -140,36 +139,31 @@ impl ViewImageHandler {
|
||||
"view_image is unavailable in this session".to_string(),
|
||||
));
|
||||
};
|
||||
// TODO(anp): Resolve tool paths using the selected environment's native path convention
|
||||
// so view_image can support relative paths in foreign environments.
|
||||
let cwd = turn_environment.cwd().to_abs_path().map_err(|err| {
|
||||
let path_uri = turn_environment.cwd().join(&path).map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"environment cwd `{}` is not native to the Codex host: {err}",
|
||||
turn_environment.cwd()
|
||||
"unable to resolve image path `{path}` against environment cwd `{}`: {err}",
|
||||
turn_environment.cwd(),
|
||||
))
|
||||
})?;
|
||||
let abs_path = cwd.join(path);
|
||||
let model_visible_path = path_uri.inferred_native_path_string();
|
||||
let sandbox = turn.file_system_sandbox_context(
|
||||
/*additional_permissions*/ None,
|
||||
turn_environment.cwd(),
|
||||
);
|
||||
let fs = turn_environment.environment.get_filesystem();
|
||||
let path_uri = PathUri::from_abs_path(&abs_path);
|
||||
|
||||
let metadata = fs
|
||||
.get_metadata(&path_uri, Some(&sandbox))
|
||||
.await
|
||||
.map_err(|error| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"unable to locate image at `{}`: {error}",
|
||||
abs_path.display()
|
||||
"unable to locate image at `{model_visible_path}`: {error}"
|
||||
))
|
||||
})?;
|
||||
|
||||
if !metadata.is_file {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"image path `{}` is not a file",
|
||||
abs_path.display()
|
||||
"image path `{model_visible_path}` is not a file"
|
||||
)));
|
||||
}
|
||||
let file_bytes = fs
|
||||
@@ -177,11 +171,9 @@ impl ViewImageHandler {
|
||||
.await
|
||||
.map_err(|error| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"unable to read image at `{}`: {error}",
|
||||
abs_path.display()
|
||||
"unable to read image at `{model_visible_path}`: {error}"
|
||||
))
|
||||
})?;
|
||||
let event_path = abs_path.clone();
|
||||
|
||||
let can_request_original_detail = can_request_original_image_detail(&turn.model_info);
|
||||
let use_original_detail =
|
||||
@@ -197,7 +189,7 @@ impl ViewImageHandler {
|
||||
|
||||
let item = TurnItem::ImageView(ImageViewItem {
|
||||
id: call_id,
|
||||
path: event_path,
|
||||
path: path_uri,
|
||||
});
|
||||
session.emit_turn_item_started(turn.as_ref(), &item).await;
|
||||
session.emit_turn_item_completed(turn.as_ref(), item).await;
|
||||
|
||||
@@ -250,7 +250,6 @@ fn allow_once_and_allow_for_session_both_allow_network() {
|
||||
fn only_never_policy_disables_network_approval_flow() {
|
||||
assert!(!allows_network_approval_flow(AskForApproval::Never));
|
||||
assert!(allows_network_approval_flow(AskForApproval::OnRequest));
|
||||
assert!(allows_network_approval_flow(AskForApproval::OnFailure));
|
||||
assert!(allows_network_approval_flow(AskForApproval::UnlessTrusted));
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user