Skip to content
Merged

pr #2

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
37ee6bf
chore: remove mention of experimental/unstable from app-server README…
owenlin0 Dec 2, 2025
72b95db
feat: intercept apply_patch for unified_exec (#7446)
jif-oai Dec 2, 2025
c2f8c4e
fix: add ts number annotations for app-server v2 types (#7492)
owenlin0 Dec 2, 2025
2222cab
feat: ignore standard directories (#7483)
jif-oai Dec 2, 2025
349734e
Fix: track only untracked paths in ghost snapshots (#7470)
lionel-oai Dec 2, 2025
21ad1c1
Use non-blocking mutex (#7467)
aibrahim-oai Dec 2, 2025
127e307
Show token used when context window is unknown (#7497)
aibrahim-oai Dec 2, 2025
1d09ac8
execpolicy helpers (#7032)
zhao-oai Dec 2, 2025
f6a7da4
fix: drop lock once it is no longer needed (#7500)
bolinfest Dec 2, 2025
5ebdc9a
persisting credits if new snapshot does not contain credit info (#7490)
zhao-oai Dec 2, 2025
77c4571
fix: remove serde(flatten) annotation for TurnError (#7499)
owenlin0 Dec 2, 2025
4d4778e
Trim `history.jsonl` when `history.max_bytes` is set (#6242)
terror Dec 2, 2025
ec93b6d
chore: make create_approval_requirement_for_command an async fn (#7501)
bolinfest Dec 2, 2025
58e1e57
refactor: tui.rs extract several pieces (#7461)
joshka-oai Dec 2, 2025
6b5b9a6
feat: support --version flag for @openai/codex-shell-tool-mcp (#7504)
bolinfest Dec 2, 2025
ad9eeeb
Ensure duplicate-length paste placeholders stay distinct (#7431)
joshua1s Dec 3, 2025
ee191db
fix: path resolution bug in npx (#7134)
bolinfest Dec 3, 2025
1ef1fe6
improve resume performance (#7303)
aibrahim-oai Dec 3, 2025
06e7667
fix: inline function marked as dead code (#7508)
bolinfest Dec 3, 2025
dbec741
Update device code auth strings. (#7498)
mzeng-openai Dec 3, 2025
f3989f6
fix(unified_exec): use platform default shell when unified_exec shell…
448523760 Dec 3, 2025
00ef9d3
fix(tui) Support image paste from clipboard on native Windows (#7514)
dylan-hurd-oai Dec 3, 2025
42ae738
feat: model warning in case of apply patch (#7494)
jif-oai Dec 3, 2025
51307ea
feat: retroactive image placeholder to prevent poisoning (#6774)
jif-oai Dec 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion codex-rs/app-server-protocol/src/protocol/mappers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ impl From<v1::ExecOneOffCommandParams> for v2::CommandExecParams {
fn from(value: v1::ExecOneOffCommandParams) -> Self {
Self {
command: value.command,
timeout_ms: value.timeout_ms,
timeout_ms: value
.timeout_ms
.map(|timeout| i64::try_from(timeout).unwrap_or(60_000)),
cwd: value.cwd,
sandbox_policy: value.sandbox_policy.map(std::convert::Into::into),
}
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/thread_history.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::protocol::v2::ThreadItem;
use crate::protocol::v2::Turn;
use crate::protocol::v2::TurnError;
use crate::protocol::v2::TurnStatus;
use crate::protocol::v2::UserInput;
use codex_protocol::protocol::AgentReasoningEvent;
Expand Down Expand Up @@ -142,6 +143,7 @@ impl ThreadHistoryBuilder {
PendingTurn {
id: self.next_turn_id(),
items: Vec::new(),
error: None,
status: TurnStatus::Completed,
}
}
Expand Down Expand Up @@ -190,6 +192,7 @@ impl ThreadHistoryBuilder {
struct PendingTurn {
id: String,
items: Vec<ThreadItem>,
error: Option<TurnError>,
status: TurnStatus,
}

Expand All @@ -198,6 +201,7 @@ impl From<PendingTurn> for Turn {
Self {
id: value.id,
items: value.items,
error: value.error,
status: value.status,
}
}
Expand Down
19 changes: 14 additions & 5 deletions codex-rs/app-server-protocol/src/protocol/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,8 @@ pub struct FeedbackUploadResponse {
#[ts(export_to = "v2/")]
pub struct CommandExecParams {
pub command: Vec<String>,
pub timeout_ms: Option<u64>,
#[ts(type = "number | null")]
pub timeout_ms: Option<i64>,
pub cwd: Option<PathBuf>,
pub sandbox_policy: Option<SandboxPolicy>,
}
Expand Down Expand Up @@ -785,6 +786,7 @@ pub struct Thread {
/// Model provider used for this thread (for example, 'openai').
pub model_provider: String,
/// Unix timestamp (in seconds) when the thread was created.
#[ts(type = "number")]
pub created_at: i64,
/// [UNSTABLE] Path to the thread on disk.
pub path: PathBuf,
Expand Down Expand Up @@ -875,8 +877,9 @@ pub struct Turn {
/// For all other responses and notifications returning a Turn,
/// the items field will be an empty list.
pub items: Vec<ThreadItem>,
#[serde(flatten)]
pub status: TurnStatus,
/// Only populated when the Turn's status is failed.
pub error: Option<TurnError>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Error)]
Expand All @@ -898,12 +901,12 @@ pub struct ErrorNotification {
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(tag = "status", rename_all = "camelCase")]
#[ts(tag = "status", export_to = "v2/")]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub enum TurnStatus {
Completed,
Interrupted,
Failed { error: TurnError },
Failed,
InProgress,
}

Expand Down Expand Up @@ -1072,6 +1075,7 @@ pub enum ThreadItem {
/// The command's exit code.
exit_code: Option<i32>,
/// The duration of the command execution in milliseconds.
#[ts(type = "number | null")]
duration_ms: Option<i64>,
},
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -1338,6 +1342,7 @@ pub struct ReasoningSummaryTextDeltaNotification {
pub turn_id: String,
pub item_id: String,
pub delta: String,
#[ts(type = "number")]
pub summary_index: i64,
}

Expand All @@ -1348,6 +1353,7 @@ pub struct ReasoningSummaryPartAddedNotification {
pub thread_id: String,
pub turn_id: String,
pub item_id: String,
#[ts(type = "number")]
pub summary_index: i64,
}

Expand All @@ -1359,6 +1365,7 @@ pub struct ReasoningTextDeltaNotification {
pub turn_id: String,
pub item_id: String,
pub delta: String,
#[ts(type = "number")]
pub content_index: i64,
}

Expand Down Expand Up @@ -1493,7 +1500,9 @@ impl From<CoreRateLimitSnapshot> for RateLimitSnapshot {
#[ts(export_to = "v2/")]
pub struct RateLimitWindow {
pub used_percent: i32,
#[ts(type = "number | null")]
pub window_duration_mins: Option<i64>,
#[ts(type = "number | null")]
pub resets_at: Option<i64>,
}

Expand Down
4 changes: 3 additions & 1 deletion codex-rs/app-server-test-client/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,9 @@ impl CodexClient {
ServerNotification::TurnCompleted(payload) => {
if payload.turn.id == turn_id {
println!("\n< turn/completed notification: {:?}", payload.turn.status);
if let TurnStatus::Failed { error } = &payload.turn.status {
if payload.turn.status == TurnStatus::Failed
&& let Some(error) = payload.turn.error
{
println!("[turn error] {}", error.message);
}
break;
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# codex-app-server

`codex app-server` is the interface Codex uses to power rich interfaces such as the [Codex VS Code extension](https://marketplace.visualstudio.com/items?itemName=openai.chatgpt). The message schema is currently unstable, but those who wish to build experimental UIs on top of Codex may find it valuable.
`codex app-server` is the interface Codex uses to power rich interfaces such as the [Codex VS Code extension](https://marketplace.visualstudio.com/items?itemName=openai.chatgpt).

## Table of Contents
- [Protocol](#protocol)
Expand Down
54 changes: 28 additions & 26 deletions codex-rs/app-server/src/bespoke_event_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -718,13 +718,15 @@ async fn emit_turn_completed_with_status(
conversation_id: ConversationId,
event_turn_id: String,
status: TurnStatus,
error: Option<TurnError>,
outgoing: &OutgoingMessageSender,
) {
let notification = TurnCompletedNotification {
thread_id: conversation_id.to_string(),
turn: Turn {
id: event_turn_id,
items: vec![],
error,
status,
},
};
Expand Down Expand Up @@ -813,13 +815,12 @@ async fn handle_turn_complete(
) {
let turn_summary = find_and_remove_turn_summary(conversation_id, turn_summary_store).await;

let status = if let Some(error) = turn_summary.last_error {
TurnStatus::Failed { error }
} else {
TurnStatus::Completed
let (status, error) = match turn_summary.last_error {
Some(error) => (TurnStatus::Failed, Some(error)),
None => (TurnStatus::Completed, None),
};

emit_turn_completed_with_status(conversation_id, event_turn_id, status, outgoing).await;
emit_turn_completed_with_status(conversation_id, event_turn_id, status, error, outgoing).await;
}

async fn handle_turn_interrupted(
Expand All @@ -834,6 +835,7 @@ async fn handle_turn_interrupted(
conversation_id,
event_turn_id,
TurnStatus::Interrupted,
None,
outgoing,
)
.await;
Expand Down Expand Up @@ -1306,6 +1308,7 @@ mod tests {
OutgoingMessage::AppServerNotification(ServerNotification::TurnCompleted(n)) => {
assert_eq!(n.turn.id, event_turn_id);
assert_eq!(n.turn.status, TurnStatus::Completed);
assert_eq!(n.turn.error, None);
}
other => bail!("unexpected message: {other:?}"),
}
Expand Down Expand Up @@ -1346,6 +1349,7 @@ mod tests {
OutgoingMessage::AppServerNotification(ServerNotification::TurnCompleted(n)) => {
assert_eq!(n.turn.id, event_turn_id);
assert_eq!(n.turn.status, TurnStatus::Interrupted);
assert_eq!(n.turn.error, None);
}
other => bail!("unexpected message: {other:?}"),
}
Expand Down Expand Up @@ -1385,14 +1389,13 @@ mod tests {
match msg {
OutgoingMessage::AppServerNotification(ServerNotification::TurnCompleted(n)) => {
assert_eq!(n.turn.id, event_turn_id);
assert_eq!(n.turn.status, TurnStatus::Failed);
assert_eq!(
n.turn.status,
TurnStatus::Failed {
error: TurnError {
message: "bad".to_string(),
codex_error_info: Some(V2CodexErrorInfo::Other),
}
}
n.turn.error,
Some(TurnError {
message: "bad".to_string(),
codex_error_info: Some(V2CodexErrorInfo::Other),
})
);
}
other => bail!("unexpected message: {other:?}"),
Expand Down Expand Up @@ -1653,14 +1656,13 @@ mod tests {
match msg {
OutgoingMessage::AppServerNotification(ServerNotification::TurnCompleted(n)) => {
assert_eq!(n.turn.id, a_turn1);
assert_eq!(n.turn.status, TurnStatus::Failed);
assert_eq!(
n.turn.status,
TurnStatus::Failed {
error: TurnError {
message: "a1".to_string(),
codex_error_info: Some(V2CodexErrorInfo::BadRequest),
}
}
n.turn.error,
Some(TurnError {
message: "a1".to_string(),
codex_error_info: Some(V2CodexErrorInfo::BadRequest),
})
);
}
other => bail!("unexpected message: {other:?}"),
Expand All @@ -1674,14 +1676,13 @@ mod tests {
match msg {
OutgoingMessage::AppServerNotification(ServerNotification::TurnCompleted(n)) => {
assert_eq!(n.turn.id, b_turn1);
assert_eq!(n.turn.status, TurnStatus::Failed);
assert_eq!(
n.turn.status,
TurnStatus::Failed {
error: TurnError {
message: "b1".to_string(),
codex_error_info: None,
}
}
n.turn.error,
Some(TurnError {
message: "b1".to_string(),
codex_error_info: None,
})
);
}
other => bail!("unexpected message: {other:?}"),
Expand All @@ -1696,6 +1697,7 @@ mod tests {
OutgoingMessage::AppServerNotification(ServerNotification::TurnCompleted(n)) => {
assert_eq!(n.turn.id, a_turn2);
assert_eq!(n.turn.status, TurnStatus::Completed);
assert_eq!(n.turn.error, None);
}
other => bail!("unexpected message: {other:?}"),
}
Expand Down
6 changes: 5 additions & 1 deletion codex-rs/app-server/src/codex_message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1152,7 +1152,9 @@ impl CodexMessageProcessor {

let cwd = params.cwd.unwrap_or_else(|| self.config.cwd.clone());
let env = create_env(&self.config.shell_environment_policy);
let timeout_ms = params.timeout_ms;
let timeout_ms = params
.timeout_ms
.and_then(|timeout_ms| u64::try_from(timeout_ms).ok());
let exec_params = ExecParams {
command: params.command,
cwd,
Expand Down Expand Up @@ -2453,6 +2455,7 @@ impl CodexMessageProcessor {
let turn = Turn {
id: turn_id.clone(),
items: vec![],
error: None,
status: TurnStatus::InProgress,
};

Expand Down Expand Up @@ -2494,6 +2497,7 @@ impl CodexMessageProcessor {
Turn {
id: turn_id,
items,
error: None,
status: TurnStatus::InProgress,
}
}
Expand Down
18 changes: 13 additions & 5 deletions codex-rs/core/src/api_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,20 @@ pub(crate) fn map_api_error(err: ApiError) -> CodexErr {
headers,
body,
} => {
if status == http::StatusCode::INTERNAL_SERVER_ERROR {
let body_text = body.unwrap_or_default();

if status == http::StatusCode::BAD_REQUEST {
if body_text
.contains("The image data you provided does not represent a valid image")
{
CodexErr::InvalidImageRequest()
} else {
CodexErr::InvalidRequest(body_text)
}
} else if status == http::StatusCode::INTERNAL_SERVER_ERROR {
CodexErr::InternalServerError
} else if status == http::StatusCode::TOO_MANY_REQUESTS {
if let Some(body) = body
&& let Ok(err) = serde_json::from_str::<UsageErrorResponse>(&body)
{
if let Ok(err) = serde_json::from_str::<UsageErrorResponse>(&body_text) {
if err.error.error_type.as_deref() == Some("usage_limit_reached") {
let rate_limits = headers.as_ref().and_then(parse_rate_limit);
let resets_at = err
Expand All @@ -62,7 +70,7 @@ pub(crate) fn map_api_error(err: ApiError) -> CodexErr {
} else {
CodexErr::UnexpectedStatus(UnexpectedResponseError {
status,
body: body.unwrap_or_default(),
body: body_text,
request_id: extract_request_id(headers.as_ref()),
})
}
Expand Down
Loading