use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use serde_json::{json, Map, Value};
use supercode::{
ChatMessage, FrontendApprovalDecision, FrontendAttachment, FrontendElicitationAction,
FrontendEvent, FrontendRequestKind, FrontendResponse, FrontendRuntimeDescriptor, Role,
SdkError, SdkErrorCode, SdkRuntime,
};
pub const PROTOCOL_NAMESPACE: &str = "codex_app_server/v0_144";
pub const CODEX_CLI_VERSION: &str = "0.144.4";
pub const HISTORY_LIMIT: usize = 4096;
const TRACED_METHODS: &[&str] = &[
"account/read",
"configRequirements/read",
"hooks/list",
"initialize",
"initialized",
"model/list",
"skills/list",
"thread/goal/get",
"thread/read",
"thread/resume",
"thread/start",
"thread/unsubscribe",
"turn/start",
];
const SCHEMA_ONLY_METHODS: &[&str] = &[
"thread/archive",
"thread/fork",
"thread/list",
"turn/interrupt",
"turn/steer",
];
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum CodexCompatibilityMode {
#[default]
TracedOnly,
SchemaExtended,
}
#[derive(Debug, thiserror::Error)]
pub enum AdapterError {
#[error("codex protocol version mismatch: expected {expected}, received {received}")]
ProtocolVersionMismatch {
expected: &'static str,
received: String,
},
#[error("method not supported by this adapter version: {0}")]
UnsupportedMethod(String),
#[error("invalid parameters for `{method}`: {message}")]
InvalidParams { method: String, message: String },
#[error("thread `{0}` is not attached to this runtime")]
UnknownThread(String),
#[error("deterministic identifier collision for `{0}`")]
IdentifierCollision(String),
#[error(transparent)]
Sdk(#[from] SdkError),
}
impl AdapterError {
fn wire(&self, id: Value) -> Value {
let (code, name) = match self {
Self::ProtocolVersionMismatch { .. } => (-32040, "protocol_version_mismatch"),
Self::UnsupportedMethod(_) => (-32020, "unsupported_action"),
Self::InvalidParams { .. } | Self::UnknownThread(_) => (-32602, "invalid_params"),
Self::IdentifierCollision(_) => (-32603, "identifier_collision"),
Self::Sdk(error) if error.code() == SdkErrorCode::Unauthenticated => {
(-32030, "unauthenticated")
}
Self::Sdk(error) if error.code() == SdkErrorCode::Unauthorized => {
(-32031, "unauthorized")
}
Self::Sdk(error) if error.code() == SdkErrorCode::ControllerRequired => {
(-32032, "controller_required")
}
Self::Sdk(error) if error.code() == SdkErrorCode::LeaseExpired => {
(-32033, "lease_expired")
}
Self::Sdk(error) if error.code() == SdkErrorCode::UnsupportedAction => {
(-32020, "unsupported_action")
}
Self::Sdk(error) if error.code() == SdkErrorCode::Busy => (-32000, "busy"),
Self::Sdk(_) => (-32000, "sdk_error"),
};
json!({"id": id, "error": {"code": code, "message": self.to_string(), "data": {"name": name}}})
}
}
pub struct CodexAppServerAdapter {
runtime: Arc<dyn SdkRuntime>,
mode: CodexCompatibilityMode,
cwd: PathBuf,
codex_home: PathBuf,
next_turn: AtomicU64,
}
impl CodexAppServerAdapter {
pub fn new(runtime: Arc<dyn SdkRuntime>, cwd: impl Into<PathBuf>) -> Arc<Self> {
Self::with_mode(runtime, cwd, CodexCompatibilityMode::TracedOnly)
}
pub fn with_mode(
runtime: Arc<dyn SdkRuntime>,
cwd: impl Into<PathBuf>,
mode: CodexCompatibilityMode,
) -> Arc<Self> {
let cwd = cwd.into();
Self::with_mode_and_client_home(runtime, cwd.clone(), cwd, mode)
}
pub fn with_mode_and_client_home(
runtime: Arc<dyn SdkRuntime>,
cwd: impl Into<PathBuf>,
codex_home: impl Into<PathBuf>,
mode: CodexCompatibilityMode,
) -> Arc<Self> {
Arc::new(Self {
runtime,
mode,
cwd: cwd.into(),
codex_home: codex_home.into(),
next_turn: AtomicU64::new(0),
})
}
pub fn namespace(&self) -> &'static str {
PROTOCOL_NAMESPACE
}
pub fn enabled_methods(&self) -> BTreeSet<&'static str> {
let mut methods = TRACED_METHODS.iter().copied().collect::<BTreeSet<_>>();
if self.mode == CodexCompatibilityMode::SchemaExtended {
methods.extend(SCHEMA_ONLY_METHODS.iter().copied());
}
methods
}
pub(crate) async fn detach(&self) {
let _ = self.runtime.detach().await;
}
pub fn connection(self: &Arc<Self>) -> CodexConnection {
CodexConnection {
adapter: self.clone(),
initialize_seen: false,
initialized: false,
attached: None,
descriptor: None,
thread_id: None,
active_turn: None,
live_event_turn: None,
open_agent_item: None,
open_agent_text: String::new(),
open_reasoning_item: None,
open_reasoning_text: String::new(),
open_tools: BTreeMap::new(),
pending_requests: BTreeMap::new(),
}
}
fn mapped_thread_id(&self, canonical: &str) -> Result<String, AdapterError> {
Ok(deterministic_uuid("thread", canonical))
}
fn validate_thread(&self, thread: &str, canonical: &str) -> Result<(), AdapterError> {
if thread == deterministic_uuid("thread", canonical) {
Ok(())
} else {
Err(AdapterError::UnknownThread(thread.to_owned()))
}
}
}
pub struct CodexConnection {
adapter: Arc<CodexAppServerAdapter>,
initialize_seen: bool,
initialized: bool,
attached: Option<FrontendAttachment>,
descriptor: Option<FrontendRuntimeDescriptor>,
thread_id: Option<String>,
active_turn: Option<String>,
live_event_turn: Option<String>,
open_agent_item: Option<String>,
open_agent_text: String,
open_reasoning_item: Option<String>,
open_reasoning_text: String,
open_tools: BTreeMap<String, (String, Value)>,
pending_requests: BTreeMap<ValueKey, u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum ValueKey {
Number(u64),
String(String),
}
impl CodexConnection {
pub(crate) fn is_attached(&self) -> bool {
self.attached.is_some()
}
pub async fn handle(&mut self, message: Value) -> Vec<Value> {
let id = message.get("id").cloned().unwrap_or(Value::Null);
match self.handle_inner(&message).await {
Ok(mut response) => {
if !id.is_null() {
let result = if response.is_empty() {
Value::Null
} else {
response.remove(0)
};
response.insert(0, json!({"id": id, "result": result}));
}
response
}
Err(error) if id.is_null() => vec![json!({"method": "error", "params": {
"error": {"message": error.to_string(), "codexErrorInfo": "other", "additionalDetails": {"name": wire_error_name(&error)}},
"willRetry": false,
"threadId": self.thread_id,
"turnId": self.active_turn,
}})],
Err(error) => vec![error.wire(id)],
}
}
async fn handle_inner(&mut self, message: &Value) -> Result<Vec<Value>, AdapterError> {
let method = message
.get("method")
.and_then(Value::as_str)
.ok_or_else(|| AdapterError::InvalidParams {
method: "<missing>".into(),
message: "method must be a string".into(),
})?;
if !self.adapter.enabled_methods().contains(method) {
return Err(AdapterError::UnsupportedMethod(method.to_owned()));
}
if method != "initialize" && !self.initialize_seen {
return Err(invalid(method, "initialize must be the first request"));
}
if method != "initialize" && method != "initialized" && !self.initialized {
return Err(invalid(
method,
"initialized notification is required first",
));
}
let params = message.get("params").cloned().unwrap_or_else(|| json!({}));
match method {
"initialize" => self.initialize(params).await,
"initialized" => {
if self.initialized {
return Err(invalid("initialized", "notification already received"));
}
self.initialized = true;
Ok(Vec::new())
}
"account/read" => Ok(vec![json!({"account": null, "requiresOpenaiAuth": false})]),
"configRequirements/read" => Ok(vec![json!({"requirements": null})]),
"hooks/list" => Ok(vec![
json!({"data": [{"cwd": self.cwd(), "hooks": [], "warnings": [], "errors": []}]}),
]),
"model/list" => self.model_list().await,
"skills/list" => self.skills_list().await,
"thread/goal/get" => Ok(vec![json!({"goal": null})]),
"thread/start" | "thread/resume" | "thread/read" => {
self.attach_thread(method, ¶ms).await
}
"thread/unsubscribe" => self.unsubscribe(¶ms).await,
"turn/start" => self.start_turn(¶ms).await,
"turn/steer" => self.steer(¶ms).await,
"turn/interrupt" => self.interrupt(¶ms).await,
"thread/list" => self.thread_list().await,
"thread/fork" => self.thread_fork(¶ms).await,
"thread/archive" => self.thread_archive(¶ms).await,
_ => Err(AdapterError::UnsupportedMethod(method.to_owned())),
}
}
async fn initialize(&mut self, params: Value) -> Result<Vec<Value>, AdapterError> {
if self.initialize_seen {
return Err(invalid("initialize", "request already received"));
}
let received = params
.pointer("/clientInfo/version")
.and_then(Value::as_str)
.unwrap_or("<missing>");
if received != CODEX_CLI_VERSION {
return Err(AdapterError::ProtocolVersionMismatch {
expected: CODEX_CLI_VERSION,
received: received.to_owned(),
});
}
self.initialize_seen = true;
Ok(vec![
json!({
"userAgent": format!("supercode-codex-adapter/{CODEX_CLI_VERSION}"),
"codexHome": self.adapter.codex_home,
"platformFamily": if cfg!(unix) { "unix" } else { "windows" },
"platformOs": std::env::consts::OS,
}),
json!({"method": "remoteControl/status/changed", "params": {
"status": "disabled", "serverName": "supercode", "installationId": "supercode",
"environmentId": null
}}),
])
}
async fn ensure_descriptor(&mut self) -> Result<FrontendRuntimeDescriptor, AdapterError> {
if let Some(descriptor) = &self.descriptor {
return Ok(descriptor.clone());
}
let descriptor = self.adapter.runtime.describe().await?;
self.descriptor = Some(descriptor.clone());
Ok(descriptor)
}
async fn model_list(&mut self) -> Result<Vec<Value>, AdapterError> {
let descriptor = self.ensure_descriptor().await?;
let model = descriptor.model;
Ok(vec![json!({"data": [{
"id": model, "model": model, "upgrade": null, "upgradeInfo": null,
"availabilityNux": null, "displayName": model, "description": "Active Supercode runtime model",
"hidden": false, "supportedReasoningEfforts": [], "defaultReasoningEffort": "medium",
"inputModalities": ["text", "image"], "supportsPersonality": false,
"additionalSpeedTiers": [], "serviceTiers": [], "defaultServiceTier": null, "isDefault": true
}], "nextCursor": null})])
}
async fn skills_list(&mut self) -> Result<Vec<Value>, AdapterError> {
Ok(vec![
json!({"data": [{"cwd": self.cwd(), "skills": [], "errors": []}]}),
])
}
async fn attach_thread(
&mut self,
method: &str,
params: &Value,
) -> Result<Vec<Value>, AdapterError> {
if method != "thread/start" {
let requested = required_str(params, "threadId", method)?;
let descriptor = self.ensure_descriptor().await?;
self.adapter
.validate_thread(requested, &descriptor.session_id)?;
}
let attachment = self.adapter.runtime.attach(HISTORY_LIMIT).await?;
let canonical = attachment.descriptor.session_id.clone();
let thread_id = self.adapter.mapped_thread_id(&canonical)?;
if let Some(descriptor) = &self.descriptor {
if descriptor.session_id != canonical {
return Err(AdapterError::IdentifierCollision(canonical));
}
}
let response = thread_response(
&attachment,
&thread_id,
&self.adapter.cwd,
method == "thread/read",
);
let started = json!({"method": "thread/started", "params": {"thread": thread_json(&attachment, &thread_id, &self.adapter.cwd)}});
self.descriptor = Some(attachment.descriptor.clone());
self.thread_id = Some(thread_id);
self.attached = Some(attachment);
if method == "thread/start" {
let thread_id = self.thread_id.as_deref().unwrap();
let descriptor = self.descriptor.as_ref().unwrap();
Ok(vec![
response,
started,
notification(
"thread/settings/updated",
json!({"threadId":thread_id,"threadSettings":{
"cwd":self.cwd(),"approvalPolicy":"on-request","approvalsReviewer":"user",
"sandboxPolicy":{"type":"workspaceWrite","writableRoots":[],"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},
"activePermissionProfile":null,"model":descriptor.model,"modelProvider":"supercode",
"serviceTier":null,"effort":null,"summary":null,
"collaborationMode":{"mode":"default","settings":{"model":descriptor.model,"reasoning_effort":null,"developer_instructions":""}},
"multiAgentMode":"explicitRequestOnly","personality":null
}}),
),
notification(
"account/rateLimits/updated",
json!({"rateLimits":{"limitId":"supercode","limitName":null,"primary":null,"secondary":null,"credits":null,"individualLimit":null,"planType":null,"rateLimitReachedType":null}}),
),
notification("thread/goal/cleared", json!({"threadId":thread_id})),
])
} else {
Ok(vec![response])
}
}
async fn unsubscribe(&mut self, params: &Value) -> Result<Vec<Value>, AdapterError> {
let thread = required_str(params, "threadId", "thread/unsubscribe")?;
let canonical = self
.descriptor
.as_ref()
.map(|d| d.session_id.as_str())
.ok_or_else(|| AdapterError::UnknownThread(thread.to_owned()))?;
self.adapter.validate_thread(thread, canonical)?;
self.adapter.runtime.detach().await?;
self.attached = None;
Ok(vec![json!({"status": "unsubscribed"})])
}
async fn start_turn(&mut self, params: &Value) -> Result<Vec<Value>, AdapterError> {
let thread = required_str(params, "threadId", "turn/start")?;
self.validate_attached_thread(thread)?;
let prompt = params
.get("input")
.and_then(Value::as_array)
.ok_or_else(|| invalid("turn/start", "input must be an array"))?
.iter()
.filter(|item| item.get("type").and_then(Value::as_str) == Some("text"))
.filter_map(|item| item.get("text").and_then(Value::as_str))
.collect::<Vec<_>>()
.join("\n");
if prompt.is_empty() {
return Err(invalid("turn/start", "at least one text input is required"));
}
let next_turn = self.adapter.next_turn.fetch_add(1, Ordering::Relaxed) + 1;
let canonical = self.descriptor.as_ref().unwrap().session_id.clone();
let turn_id = deterministic_uuid("turn", &format!("{canonical}:{next_turn}"));
self.adapter.runtime.clone().send_input(prompt).await?;
self.active_turn = Some(turn_id.clone());
Ok(vec![
json!({"turn": turn_json(&turn_id, "inProgress", None)}),
])
}
async fn steer(&mut self, params: &Value) -> Result<Vec<Value>, AdapterError> {
let thread = required_str(params, "threadId", "turn/steer")?;
self.validate_attached_thread(thread)?;
let expected = required_str(params, "expectedTurnId", "turn/steer")?;
let active = self
.active_turn
.as_deref()
.ok_or_else(|| invalid("turn/steer", "no active turn"))?;
if expected != active {
return Err(invalid("turn/steer", "expectedTurnId is not active"));
}
let prompt = text_from_input(params, "turn/steer")?;
self.adapter.runtime.steer(prompt).await?;
Ok(vec![json!({"turnId": active})])
}
async fn interrupt(&mut self, params: &Value) -> Result<Vec<Value>, AdapterError> {
let thread = required_str(params, "threadId", "turn/interrupt")?;
self.validate_attached_thread(thread)?;
let turn = required_str(params, "turnId", "turn/interrupt")?;
if self.active_turn.as_deref() != Some(turn) {
return Err(invalid("turn/interrupt", "turnId is not active"));
}
self.adapter.runtime.interrupt().await?;
Ok(vec![json!({})])
}
async fn thread_list(&mut self) -> Result<Vec<Value>, AdapterError> {
let Some(attachment) = self.attached.as_ref() else {
return Ok(vec![json!({"data": [], "nextCursor": null})]);
};
let thread_id = self.thread_id.as_ref().unwrap();
Ok(vec![
json!({"data": [thread_json(attachment, thread_id, &self.adapter.cwd)], "nextCursor": null}),
])
}
async fn thread_fork(&mut self, params: &Value) -> Result<Vec<Value>, AdapterError> {
let thread = required_str(params, "threadId", "thread/fork")?;
self.validate_attached_thread(thread)?;
Err(AdapterError::UnsupportedMethod(
"thread/fork: canonical runtime cannot be duplicated by a frontend".into(),
))
}
async fn thread_archive(&mut self, params: &Value) -> Result<Vec<Value>, AdapterError> {
let thread = required_str(params, "threadId", "thread/archive")?;
self.validate_attached_thread(thread)?;
Err(AdapterError::UnsupportedMethod(
"thread/archive: frontend cannot mutate canonical persistence".into(),
))
}
fn validate_attached_thread(&self, thread: &str) -> Result<(), AdapterError> {
let canonical = self
.descriptor
.as_ref()
.map(|d| d.session_id.as_str())
.ok_or_else(|| AdapterError::UnknownThread(thread.to_owned()))?;
self.adapter.validate_thread(thread, canonical)
}
pub async fn next_notifications(&mut self) -> Result<Vec<Value>, AdapterError> {
let event = self
.attached
.as_mut()
.ok_or_else(|| AdapterError::UnknownThread("<detached>".into()))?
.next_event()
.await?;
self.event_notifications(event)
}
fn event_notifications(&mut self, event: FrontendEvent) -> Result<Vec<Value>, AdapterError> {
let thread = self.thread_id.clone().unwrap_or_default();
if self.active_turn.is_none()
&& self.live_event_turn.is_none()
&& matches!(
event.kind.as_str(),
"turn_started"
| "user_message"
| "text_delta"
| "tool_call_started"
| "tool_call_completed"
| "usage"
| "request"
| "request_resolved"
| "reasoning_delta"
| "reasoning_summary_delta"
| "reasoning_completed"
| "plan_update"
| "plan_updated"
| "diff_update"
| "diff_updated"
| "turn_succeeded"
| "turn_interrupted"
| "turn_failed"
| "turn_completed"
)
{
self.live_event_turn = Some(deterministic_uuid(
"turn",
&format!("{thread}:event:{}", event.sequence),
));
}
let turn = self
.active_turn
.clone()
.or_else(|| self.live_event_turn.clone())
.unwrap_or_else(|| deterministic_uuid("turn", &format!("{thread}:idle")));
let p = &event.payload;
let messages = match event.kind.as_str() {
"turn_started" => {
vec![
notification(
"thread/status/changed",
json!({"threadId": thread, "status": {"type": "active"}}),
),
notification(
"turn/started",
json!({"threadId": thread, "turn": turn_json(&turn, "inProgress", None)}),
),
]
}
"user_message" => {
let id = item_id(event.sequence, "user");
let text = p.get("text").and_then(Value::as_str).unwrap_or_default();
let item = json!({"type": "userMessage", "id": id, "clientId": null, "content": [{"type":"text", "text": text, "text_elements": []}]});
vec![
item_notification("item/started", &thread, &turn, item.clone(), "startedAtMs"),
item_notification("item/completed", &thread, &turn, item, "completedAtMs"),
]
}
"text_delta" => {
let delta = p.get("text").and_then(Value::as_str).unwrap_or_default();
self.open_agent_text.push_str(delta);
let (id, started) = match &self.open_agent_item {
Some(id) => (id.clone(), None),
None => {
let id = item_id(event.sequence, "agent");
self.open_agent_item = Some(id.clone());
let item = json!({"type":"agentMessage","id":id,"text":"","phase":null,"memoryCitation":null});
(
id,
Some(item_notification(
"item/started",
&thread,
&turn,
item,
"startedAtMs",
)),
)
}
};
let mut out = Vec::with_capacity(2);
if let Some(started) = started {
out.push(started);
}
out.push(notification(
"item/agentMessage/delta",
json!({"threadId": thread, "turnId": turn, "itemId": id, "delta": delta}),
));
out
}
"tool_call_started" => {
let id = p
.get("id")
.and_then(Value::as_str)
.map(str::to_owned)
.unwrap_or_else(|| item_id(event.sequence, "tool"));
let name = p
.get("name")
.and_then(Value::as_str)
.unwrap_or("unknown_tool")
.to_owned();
let arguments = parsed_tool_arguments(p.get("arguments"));
self.open_tools
.insert(id.clone(), (name.clone(), arguments.clone()));
let item = tool_item(&id, &name, &arguments, None, false, p);
vec![item_notification(
"item/started",
&thread,
&turn,
item,
"startedAtMs",
)]
}
"tool_call_completed" => {
let id = p
.get("id")
.and_then(Value::as_str)
.map(str::to_owned)
.unwrap_or_else(|| item_id(event.sequence, "tool"));
let (name, arguments) = self.open_tools.remove(&id).unwrap_or_else(|| {
(
p.get("name")
.and_then(Value::as_str)
.unwrap_or("unknown_tool")
.to_owned(),
json!({}),
)
});
let failed = p.get("is_error").and_then(Value::as_bool) == Some(true);
let output = p
.get("output")
.and_then(Value::as_str)
.map(str::to_owned)
.unwrap_or_else(|| p.get("output").cloned().unwrap_or(Value::Null).to_string());
let item = tool_item(&id, &name, &arguments, Some(&output), failed, p);
let mut out = vec![item_notification(
"item/completed",
&thread,
&turn,
item,
"completedAtMs",
)];
if !failed {
if let Some(diff) = file_tool_diff(&name, &arguments) {
out.push(notification(
"turn/diff/updated",
json!({"threadId":thread,"turnId":turn,"diff":diff,"_supercode":{"tool":name,"arguments":arguments}}),
));
}
}
out
}
"usage" => vec![notification(
"thread/tokenUsage/updated",
json!({"threadId":thread,"turnId":turn,"tokenUsage":{"total":{"inputTokens":p.get("prompt_tokens").and_then(Value::as_u64).unwrap_or(0),"cachedInputTokens":p.get("cached_tokens").and_then(Value::as_u64).unwrap_or(0),"outputTokens":p.get("completion_tokens").and_then(Value::as_u64).unwrap_or(0),"reasoningOutputTokens":0,"totalTokens":p.get("total_tokens").and_then(Value::as_u64).unwrap_or(0)},"last":null,"modelContextWindow":null}}),
)],
"reasoning_delta" | "reasoning_summary_delta" => {
let delta = p
.get("text")
.or_else(|| p.get("delta"))
.and_then(Value::as_str)
.unwrap_or_default();
self.open_reasoning_text.push_str(delta);
let (id, started) = match &self.open_reasoning_item {
Some(id) => (id.clone(), Vec::new()),
None => {
let id = item_id(event.sequence, "reasoning");
self.open_reasoning_item = Some(id.clone());
let started = vec![
item_notification(
"item/started",
&thread,
&turn,
json!({"type":"reasoning","id":id,"summary":[],"content":[]}),
"startedAtMs",
),
notification(
"item/reasoning/summaryPartAdded",
json!({"threadId":thread,"turnId":turn,"itemId":id,"summaryIndex":0}),
),
];
(id, started)
}
};
let mut out = started;
out.push(notification(
"item/reasoning/summaryTextDelta",
json!({"threadId":thread,"turnId":turn,"itemId":id,"summaryIndex":0,"delta":delta,"_supercode":{"raw":p}}),
));
out
}
"reasoning_completed" => self.finish_reasoning(&thread, &turn, event.sequence),
"plan_update" | "plan_updated" => vec![notification(
"turn/plan/updated",
json!({"threadId":thread,"turnId":turn,"plan":codex_plan(p.get("plan")),"explanation":p.get("explanation"),"_supercode":{"raw":p}}),
)],
"diff_update" | "diff_updated" => vec![notification(
"turn/diff/updated",
json!({"threadId":thread,"turnId":turn,"diff":p.get("diff").and_then(Value::as_str).unwrap_or_default(),"_supercode":{"raw":p}}),
)],
"request" => return self.request_notifications(event, &thread, &turn),
"request_resolved" => vec![notification(
"serverRequest/resolved",
json!({"threadId": thread, "requestId": p.get("request_id")}),
)],
"turn_succeeded" => self.finish_turn(&thread, &turn, "completed", None, event.sequence),
"turn_interrupted" => {
self.finish_turn(&thread, &turn, "interrupted", None, event.sequence)
}
"turn_failed" => {
let error = json!({"message":p.get("message").cloned().unwrap_or(Value::String("turn failed".into())),"codexErrorInfo":"other","additionalDetails":{"_supercode":{"raw":p}}});
let mut out = vec![notification(
"error",
json!({"error":error,"willRetry":false,"threadId":thread,"turnId":turn}),
)];
out.extend(self.finish_turn(&thread, &turn, "failed", Some(error), event.sequence));
out
}
"turn_completed" => Vec::new(),
"cache_warning" => vec![notification(
"warning",
json!({"message":p.get("message"),"_supercode":{"sequence":event.sequence,"raw":p}}),
)],
_ => vec![notification(
"warning",
json!({"message":format!("Supercode event `{}` has no native Codex 0.144.4 display item", event.kind),"_supercode":{"sequence":event.sequence,"raw":p}}),
)],
};
Ok(messages)
}
fn request_notifications(
&mut self,
event: FrontendEvent,
thread: &str,
turn: &str,
) -> Result<Vec<Value>, AdapterError> {
let request = event
.payload
.get("request")
.ok_or_else(|| invalid("event/request", "request object missing"))?;
let id = request
.get("id")
.and_then(Value::as_u64)
.ok_or_else(|| invalid("event/request", "numeric id missing"))?;
let wire_id = ValueKey::Number(id);
self.pending_requests.insert(wire_id, id);
let kind: FrontendRequestKind = serde_json::from_value(
request
.get("kind")
.cloned()
.unwrap_or(Value::String("other".into())),
)
.unwrap_or(FrontendRequestKind::Other);
let payload = request.get("payload").cloned().unwrap_or_else(|| json!({}));
let method = match kind {
FrontendRequestKind::Approval => "item/commandExecution/requestApproval",
FrontendRequestKind::Elicitation => "item/tool/requestUserInput",
FrontendRequestKind::Other => "item/tool/requestUserInput",
};
let raw_args = payload.get("raw_args").unwrap_or(&Value::Null);
let command_value = payload
.get("command")
.or_else(|| payload.get("subject"))
.or_else(|| raw_args.get("command"))
.or_else(|| raw_args.get("cmd"))
.or_else(|| raw_args.get("path"))
.or_else(|| raw_args.get("file_path"))
.cloned()
.unwrap_or_else(|| {
Value::String(
payload
.get("tool")
.and_then(Value::as_str)
.unwrap_or("tool")
.to_owned(),
)
});
let command = match command_value {
Value::String(command) => command,
other => serde_json::to_string(&other).unwrap_or_else(|_| "tool".to_owned()),
};
let cwd = payload
.get("cwd")
.or_else(|| raw_args.get("cwd"))
.cloned()
.unwrap_or_else(|| Value::String(self.cwd()));
let reason = payload.get("reason").cloned().unwrap_or_else(|| {
json!(format!(
"Allow Supercode to run `{}`?",
payload
.get("tool")
.and_then(Value::as_str)
.unwrap_or("tool")
))
});
let started_at_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|duration| duration.as_millis() as u64)
.unwrap_or_default();
Ok(vec![
json!({"id":id,"method":method,"params":{"threadId":thread,"turnId":turn,"itemId":item_id(event.sequence,"request"),"startedAtMs":started_at_ms,"environmentId":"local","reason":reason,"command":command,"cwd":cwd,"commandActions":[{"type":"unknown","command":command}],"availableDecisions":["accept","cancel"],"_supercode":{"kind":kind,"raw":payload}}}),
])
}
fn finish_turn(
&mut self,
thread: &str,
turn: &str,
status: &str,
error: Option<Value>,
sequence: u64,
) -> Vec<Value> {
let mut out = Vec::new();
if let Some(item) = self.open_agent_item.take() {
let text = std::mem::take(&mut self.open_agent_text);
out.push(item_notification("item/completed", thread, turn, json!({"type":"agentMessage","id":item,"text":text,"phase":null,"memoryCitation":null,"_supercode":{"sequence":sequence}}), "completedAtMs"));
}
out.extend(self.finish_reasoning(thread, turn, sequence));
out.push(notification(
"thread/status/changed",
json!({"threadId":thread,"status":{"type":"idle"}}),
));
out.push(notification(
"turn/completed",
json!({"threadId":thread,"turn":turn_json(turn,status,error)}),
));
self.active_turn = None;
self.live_event_turn = None;
out
}
fn finish_reasoning(&mut self, thread: &str, turn: &str, sequence: u64) -> Vec<Value> {
let Some(item) = self.open_reasoning_item.take() else {
return Vec::new();
};
let text = std::mem::take(&mut self.open_reasoning_text);
vec![item_notification(
"item/completed",
thread,
turn,
json!({"type":"reasoning","id":item,"summary":[text],"content":[],"_supercode":{"sequence":sequence}}),
"completedAtMs",
)]
}
pub async fn handle_server_response(
&mut self,
message: &Value,
) -> Result<Vec<Value>, AdapterError> {
let key = value_key(
message
.get("id")
.ok_or_else(|| invalid("serverResponse", "id missing"))?,
)?;
let request_id = self
.pending_requests
.remove(&key)
.ok_or_else(|| invalid("serverResponse", "unknown request id"))?;
let result = message.get("result").cloned().unwrap_or_else(|| json!({}));
let response = match result.get("decision").and_then(Value::as_str) {
Some("accept") | Some("approved") => FrontendResponse::Approval {
request_id,
decision: FrontendApprovalDecision::Allow,
},
Some("acceptForSession") => FrontendResponse::Approval {
request_id,
decision: FrontendApprovalDecision::AllowForSession,
},
Some("decline") | Some("cancel") | Some("denied") => FrontendResponse::Approval {
request_id,
decision: FrontendApprovalDecision::Deny,
},
_ => FrontendResponse::Other {
request_id,
action: FrontendElicitationAction::Accept,
content: Some(result),
},
};
self.adapter.runtime.respond(response).await?;
Ok(vec![notification(
"serverRequest/resolved",
json!({"threadId":self.thread_id,"requestId":request_id}),
)])
}
fn cwd(&self) -> String {
self.adapter.cwd.to_string_lossy().into_owned()
}
}
fn wire_error_name(error: &AdapterError) -> &'static str {
match error {
AdapterError::ProtocolVersionMismatch { .. } => "protocol_version_mismatch",
AdapterError::UnsupportedMethod(_) => "unsupported_action",
AdapterError::InvalidParams { .. } | AdapterError::UnknownThread(_) => "invalid_params",
AdapterError::IdentifierCollision(_) => "identifier_collision",
AdapterError::Sdk(error) => match error.code() {
SdkErrorCode::Unauthenticated => "unauthenticated",
SdkErrorCode::Unauthorized => "unauthorized",
SdkErrorCode::ControllerRequired => "controller_required",
SdkErrorCode::LeaseExpired => "lease_expired",
SdkErrorCode::Busy => "busy",
SdkErrorCode::UnsupportedAction => "unsupported_action",
_ => "sdk_error",
},
}
}
fn invalid(method: &str, message: &str) -> AdapterError {
AdapterError::InvalidParams {
method: method.into(),
message: message.into(),
}
}
fn required_str<'a>(params: &'a Value, key: &str, method: &str) -> Result<&'a str, AdapterError> {
params
.get(key)
.and_then(Value::as_str)
.ok_or_else(|| invalid(method, &format!("{key} must be a string")))
}
fn text_from_input(params: &Value, method: &str) -> Result<String, AdapterError> {
if let Some(text) = params.get("text").and_then(Value::as_str) {
return Ok(text.to_owned());
}
params
.get("input")
.and_then(Value::as_array)
.and_then(|values| {
values
.iter()
.find_map(|value| value.get("text").and_then(Value::as_str))
})
.map(str::to_owned)
.ok_or_else(|| invalid(method, "text input is required"))
}
fn parsed_tool_arguments(arguments: Option<&Value>) -> Value {
match arguments {
Some(Value::String(encoded)) => serde_json::from_str(encoded)
.unwrap_or_else(|_| json!({"_supercodeUnparsedArguments":encoded})),
Some(arguments) => arguments.clone(),
None => json!({}),
}
}
fn codex_plan(plan: Option<&Value>) -> Vec<Value> {
plan.and_then(Value::as_array)
.into_iter()
.flatten()
.map(|step| {
let status = match step.get("status").and_then(Value::as_str) {
Some("in_progress") | Some("inProgress") => "inProgress",
Some("completed") => "completed",
_ => "pending",
};
json!({
"step":step.get("step").and_then(Value::as_str).unwrap_or_default(),
"status":status
})
})
.collect()
}
fn tool_item(
id: &str,
name: &str,
arguments: &Value,
output: Option<&str>,
failed: bool,
raw: &Value,
) -> Value {
if matches!(name, "bash" | "shell" | "exec_command") {
let command = arguments
.get("command")
.or_else(|| arguments.get("cmd"))
.and_then(Value::as_str)
.unwrap_or_default();
return json!({
"type":"commandExecution", "id":id, "command":command,
"cwd":arguments.get("cwd").and_then(Value::as_str).unwrap_or_default(),
"processId":null, "source":"agent",
"status":if output.is_none() {"inProgress"} else if failed {"failed"} else {"completed"},
"commandActions":[{"type":"unknown","command":command}],
"aggregatedOutput":output, "exitCode":if output.is_none(){Value::Null}else if failed{Value::from(1)}else{Value::from(0)},
"durationMs":null, "_supercode":{"raw":raw}
});
}
if matches!(name, "write_file" | "edit_file" | "apply_patch") {
let path = arguments
.get("path")
.or_else(|| arguments.get("file_path"))
.and_then(Value::as_str)
.unwrap_or("<opaque>");
let diff = file_tool_diff(name, arguments).unwrap_or_else(|| {
arguments
.get("patch")
.or_else(|| arguments.get("content"))
.or_else(|| arguments.get("new_string"))
.and_then(Value::as_str)
.unwrap_or_else(|| output.unwrap_or_default())
.to_owned()
});
return json!({
"type":"fileChange", "id":id,
"changes":[{"path":path,"kind":{"type":"update"},"diff":diff}],
"status":if output.is_none() {"inProgress"} else if failed {"failed"} else {"completed"},
"_supercode":{"tool":name,"arguments":arguments,"raw":raw}
});
}
json!({
"type":"dynamicToolCall", "id":id, "namespace":null,
"tool":name, "arguments":arguments,
"status":if output.is_none() {"inProgress"} else if failed {"failed"} else {"completed"},
"contentItems":output.map(|text| vec![json!({"type":"inputText","text":text})]),
"success":output.map(|_| !failed), "durationMs":null, "_supercode":{"raw":raw}
})
}
fn file_tool_diff(name: &str, arguments: &Value) -> Option<String> {
if !matches!(name, "write_file" | "edit_file" | "apply_patch") {
return None;
}
if name == "apply_patch" {
return arguments
.get("patch")
.and_then(Value::as_str)
.map(str::to_owned);
}
let path = arguments
.get("path")
.or_else(|| arguments.get("file_path"))
.and_then(Value::as_str)?;
let old = arguments
.get("old_string")
.and_then(Value::as_str)
.unwrap_or_default();
let new = arguments
.get("new_string")
.or_else(|| arguments.get("content"))
.and_then(Value::as_str)?;
let removed = old
.lines()
.map(|line| format!("-{line}"))
.collect::<Vec<_>>()
.join("\n");
let added = new
.lines()
.map(|line| format!("+{line}"))
.collect::<Vec<_>>()
.join("\n");
let old_count = old.lines().count();
let new_count = new.lines().count();
let old_start = usize::from(old_count > 0);
let new_start = usize::from(new_count > 0);
Some(format!(
"--- a/{path}\n+++ b/{path}\n@@ -{old_start},{old_count} +{new_start},{new_count} @@\n{removed}{}{}\n",
if removed.is_empty() || added.is_empty() {
""
} else {
"\n"
},
added
))
}
fn deterministic_uuid(domain: &str, canonical: &str) -> String {
let mut hasher = blake3::Hasher::new();
hasher.update(PROTOCOL_NAMESPACE.as_bytes());
hasher.update(&[0]);
hasher.update(domain.as_bytes());
hasher.update(&[0]);
hasher.update(canonical.as_bytes());
let mut bytes = [0u8; 16];
bytes.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
bytes[6] = (bytes[6] & 0x0f) | 0x80;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
format!("{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", bytes[0],bytes[1],bytes[2],bytes[3],bytes[4],bytes[5],bytes[6],bytes[7],bytes[8],bytes[9],bytes[10],bytes[11],bytes[12],bytes[13],bytes[14],bytes[15])
}
fn item_id(sequence: u64, family: &str) -> String {
format!("sc_{family}_{sequence:016x}")
}
fn epoch_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
.min(u64::MAX as u128) as u64
}
fn notification(method: &str, params: Value) -> Value {
json!({"method":method,"params":params})
}
fn item_notification(method: &str, thread: &str, turn: &str, item: Value, time_key: &str) -> Value {
let mut params = Map::new();
params.insert("item".into(), item);
params.insert("threadId".into(), Value::String(thread.into()));
params.insert("turnId".into(), Value::String(turn.into()));
params.insert(time_key.into(), Value::from(epoch_ms()));
notification(method, Value::Object(params))
}
fn turn_json(id: &str, status: &str, error: Option<Value>) -> Value {
json!({"id":id,"items":[],"itemsView":"notLoaded","status":status,"error":error,"startedAt":null,"completedAt":null,"durationMs":null})
}
fn thread_json(attachment: &FrontendAttachment, thread_id: &str, cwd: &std::path::Path) -> Value {
let items = history_items(&attachment.history);
let preview = attachment
.history
.iter()
.find(|m| m.role == Role::User)
.and_then(|m| m.content.clone())
.unwrap_or_default();
json!({
"id":thread_id,"extra":null,"sessionId":thread_id,"forkedFromId":null,"parentThreadId":null,
"preview":preview,"ephemeral":false,"historyMode":"legacy","modelProvider":"supercode",
"createdAt":0,"updatedAt":0,"recencyAt":0,
"status":{"type":if attachment.descriptor.turn_state == supercode::FrontendTurnState::Busy {"active"} else {"idle"}},
"path":null,"cwd":cwd,"cliVersion":CODEX_CLI_VERSION,"source":"appServer","threadSource":"user",
"agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,
"turns":if items.is_empty() { vec![] } else { vec![json!({"id":deterministic_uuid("history-turn", &attachment.descriptor.session_id),"items":items,"itemsView":"full","status":"completed","error":null,"startedAt":0,"completedAt":0,"durationMs":0})] }
})
}
fn thread_response(
attachment: &FrontendAttachment,
thread_id: &str,
cwd: &std::path::Path,
read_only: bool,
) -> Value {
let thread = thread_json(attachment, thread_id, cwd);
if read_only {
json!({"thread":thread})
} else {
json!({"thread":thread,"model":attachment.descriptor.model,"modelProvider":"supercode","serviceTier":null,"cwd":cwd,"runtimeWorkspaceRoots":[cwd],"instructionSources":[],"approvalPolicy":"on-request","approvalsReviewer":"user","sandbox":{"type":"workspaceWrite","writableRoots":[],"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},"activePermissionProfile":null,"reasoningEffort":null,"multiAgentMode":"explicitRequestOnly"})
}
}
fn history_items(history: &[ChatMessage]) -> Vec<Value> {
let mut out = Vec::new();
for (index, message) in history.iter().enumerate() {
let id = format!("sc_history_{index:016x}");
let text = message.content.clone().unwrap_or_else(|| {
message
.content_parts
.as_ref()
.map(|parts| Value::Array(parts.clone()).to_string())
.unwrap_or_default()
});
match message.role {
Role::System => out.push(json!({"type":"reasoning","id":id,"summary":[text],"content":[],"_supercode":{"role":"system","metadata":message.metadata}})),
Role::User => out.push(json!({"type":"userMessage","id":id,"clientId":null,"content":[{"type":"text","text":text,"text_elements":[]}],"_supercode":{"metadata":message.metadata}})),
Role::Assistant => {
if !text.is_empty() { out.push(json!({"type":"agentMessage","id":id,"text":text,"phase":null,"memoryCitation":null,"_supercode":{"metadata":message.metadata}})); }
for call in message.tool_calls() {
let arguments = call.function.parsed_arguments().unwrap_or_else(|_| {
json!({"_supercodeUnparsedArguments": call.function.arguments})
});
out.push(json!({"type":"dynamicToolCall","id":call.id,"namespace":null,
"tool":call.function.name,"arguments":arguments,"status":"completed",
"contentItems":null,"success":null,"durationMs":null,
"_supercode":{"metadata":message.metadata}}));
}
}
Role::Tool => out.push(json!({"type":"dynamicToolCall",
"id":message.tool_call_id.clone().unwrap_or(id),"namespace":null,
"tool":message.name.clone().unwrap_or_else(|| "tool_result".into()),
"arguments":{},"status":"completed",
"contentItems":[{"type":"inputText","text":text}],"success":true,
"durationMs":null,"_supercode":{"metadata":message.metadata}})),
}
}
out
}
fn value_key(value: &Value) -> Result<ValueKey, AdapterError> {
if let Some(number) = value.as_u64() {
return Ok(ValueKey::Number(number));
}
if let Some(string) = value.as_str() {
return Ok(ValueKey::String(string.into()));
}
Err(invalid(
"serverResponse",
"id must be an unsigned integer or string",
))
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use std::collections::VecDeque;
use std::sync::Mutex;
use tokio::sync::broadcast;
struct RecordingRuntime {
operations: Mutex<Vec<String>>,
events: broadcast::Sender<FrontendEvent>,
}
impl RecordingRuntime {
fn new() -> Arc<Self> {
let (events, _) = broadcast::channel(32);
Arc::new(Self {
operations: Mutex::new(Vec::new()),
events,
})
}
fn descriptor() -> FrontendRuntimeDescriptor {
FrontendRuntimeDescriptor {
schema_version: 2,
session_id: "canonical-session".into(),
source_harness: Some("claude-code".into()),
emulation_profile: Some("claude-code".into()),
active_modules: vec!["reduction".into()],
commands: Vec::new(),
operations: Vec::new(),
actions: supercode::FrontendActions {
submit: true,
interrupt: true,
steer: true,
respond: true,
detach: true,
close: false,
},
display: supercode::FrontendDisplayCapabilities {
event_kinds: vec!["text_delta".into()],
opaque_fallback: true,
},
model: "openrouter/z-ai/glm-5.2".into(),
turn_state: supercode::FrontendTurnState::Idle,
connection_state: supercode::FrontendConnectionState::Connected,
extensions: BTreeMap::new(),
}
}
fn record(&self, value: impl Into<String>) {
self.operations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(value.into());
}
}
#[async_trait]
impl SdkRuntime for RecordingRuntime {
async fn describe(&self) -> Result<FrontendRuntimeDescriptor, SdkError> {
self.record("describe");
Ok(Self::descriptor())
}
async fn attach(&self, _history_limit: usize) -> Result<FrontendAttachment, SdkError> {
self.record("attach");
Ok(FrontendAttachment::from_snapshot(
supercode::FrontendAttachSnapshot {
descriptor: Self::descriptor(),
history: vec![
ChatMessage::user("fact before continuation"),
ChatMessage::assistant("remembered"),
],
history_cursor: 2,
replay: VecDeque::new(),
},
self.events.subscribe(),
))
}
async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), SdkError> {
self.record(format!("input:{prompt}"));
Ok(())
}
async fn submit(&self, _prompt: String) -> Result<String, SdkError> {
unreachable!("adapter must use atomic send_input")
}
async fn interrupt(&self) -> Result<bool, SdkError> {
self.record("interrupt");
Ok(true)
}
async fn steer(&self, prompt: String) -> Result<(), SdkError> {
self.record(format!("steer:{prompt}"));
Ok(())
}
async fn respond(&self, response: FrontendResponse) -> Result<(), SdkError> {
self.record(format!("respond:{response:?}"));
Ok(())
}
async fn detach(&self) -> Result<supercode::RuntimeLeaseSnapshot, SdkError> {
self.record("detach");
Ok(supercode::RuntimeLeaseSnapshot {
controller: None,
observers: Vec::new(),
lease_ttl_ms: supercode::DEFAULT_RUNTIME_LEASE_TTL_MS,
})
}
}
#[test]
fn deterministic_ids_are_uuid_shaped_stable_and_domain_separated() {
let a = deterministic_uuid("thread", "canonical-session");
assert_eq!(a, deterministic_uuid("thread", "canonical-session"));
assert_ne!(a, deterministic_uuid("turn", "canonical-session"));
assert_eq!(a.len(), 36);
assert_eq!(&a[14..15], "8");
assert!(matches!(&a[19..20], "8" | "9" | "a" | "b"));
}
#[test]
fn default_surface_is_exactly_the_sanitized_trace() {
let actual = TRACED_METHODS.iter().copied().collect::<BTreeSet<_>>();
assert_eq!(actual.len(), 13);
assert!(!actual.contains("turn/steer"));
assert!(!actual.contains("thread/fork"));
}
#[test]
fn deterministic_thread_mapping_has_no_collisions_across_large_sample() {
let mut ids = BTreeSet::new();
for index in 0..100_000u64 {
assert!(ids.insert(deterministic_uuid("thread", &format!("canonical-{index}"))));
}
}
#[tokio::test]
async fn traced_protocol_attaches_existing_runtime_and_submits_once() {
let runtime = RecordingRuntime::new();
let adapter = CodexAppServerAdapter::new(runtime.clone(), "/workspace");
let mut connection = adapter.connection();
let initialized = connection
.handle(json!({"id":"initialize","method":"initialize","params":{"clientInfo":{"version":"0.144.4"}}}))
.await;
assert_eq!(initialized[0]["id"], "initialize");
assert_eq!(initialized[1]["method"], "remoteControl/status/changed");
assert!(connection
.handle(json!({"method":"initialized"}))
.await
.is_empty());
let started = connection
.handle(json!({"id":1,"method":"thread/start","params":{}}))
.await;
let thread_id = started[0]["result"]["thread"]["id"]
.as_str()
.unwrap()
.to_owned();
assert_eq!(started[1]["method"], "thread/started");
assert_eq!(
started[0]["result"]["thread"]["turns"][0]["items"][0]["content"][0]["text"],
"fact before continuation"
);
let turn = connection
.handle(json!({"id":2,"method":"turn/start","params":{"threadId":thread_id,"input":[{"type":"text","text":"continue through GLM"}]}}))
.await;
assert_eq!(turn[0]["result"]["turn"]["status"], "inProgress");
let operations = runtime
.operations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
assert_eq!(
operations
.iter()
.filter(|operation| operation.starts_with("input:"))
.count(),
1
);
assert!(operations.contains(&"input:continue through GLM".into()));
}
#[tokio::test]
async fn reconnect_keeps_thread_identity_but_never_reuses_turn_identity() {
let adapter = CodexAppServerAdapter::new(RecordingRuntime::new(), "/workspace");
let mut turn_ids = Vec::new();
let mut thread_ids = Vec::new();
for index in 0..2 {
let mut connection = adapter.connection();
connection
.handle(
json!({"id":1,"method":"initialize","params":{"clientInfo":{"version":"0.144.4"}}}),
)
.await;
connection.handle(json!({"method":"initialized"})).await;
let started = connection
.handle(json!({"id":2,"method":"thread/start","params":{}}))
.await;
let thread = started[0]["result"]["thread"]["id"]
.as_str()
.unwrap()
.to_owned();
let turn = connection
.handle(json!({"id":3,"method":"turn/start","params":{"threadId":thread,"input":[{"type":"text","text":format!("turn {index}")}]}}))
.await;
thread_ids.push(thread);
turn_ids.push(turn[0]["result"]["turn"]["id"].clone());
}
assert_eq!(thread_ids[0], thread_ids[1]);
assert_ne!(turn_ids[0], turn_ids[1]);
}
#[tokio::test]
async fn version_mismatch_and_untraced_controls_fail_by_name() {
let runtime = RecordingRuntime::new();
let adapter = CodexAppServerAdapter::new(runtime, "/workspace");
let mut connection = adapter.connection();
let mismatch = connection
.handle(
json!({"id":1,"method":"initialize","params":{"clientInfo":{"version":"0.145.0"}}}),
)
.await;
assert_eq!(
mismatch[0]["error"]["data"]["name"],
"protocol_version_mismatch"
);
let steer = connection
.handle(json!({"id":2,"method":"turn/steer","params":{"text":"more"}}))
.await;
assert_eq!(steer[0]["error"]["code"], -32020);
assert_eq!(steer[0]["error"]["data"]["name"], "unsupported_action");
}
#[tokio::test]
async fn explicit_schema_extended_mode_routes_steer_and_interrupt_to_the_sdk() {
let runtime = RecordingRuntime::new();
let adapter = CodexAppServerAdapter::with_mode(
runtime.clone(),
"/workspace",
CodexCompatibilityMode::SchemaExtended,
);
let mut connection = adapter.connection();
connection
.handle(
json!({"id":1,"method":"initialize","params":{"clientInfo":{"version":"0.144.4"}}}),
)
.await;
connection.handle(json!({"method":"initialized"})).await;
let started = connection
.handle(json!({"id":2,"method":"thread/start","params":{}}))
.await;
let thread = started[0]["result"]["thread"]["id"]
.as_str()
.unwrap()
.to_owned();
connection
.handle(json!({"id":3,"method":"turn/start","params":{"threadId":thread,"input":[{"type":"text","text":"begin"}]}}))
.await;
let turn = connection.active_turn.clone().unwrap();
let steered = connection
.handle(json!({"id":4,"method":"turn/steer","params":{"threadId":thread,"expectedTurnId":turn,"input":[{"type":"text","text":"steer now"}]}}))
.await;
assert_eq!(steered[0]["result"]["turnId"], turn);
let interrupted = connection
.handle(json!({"id":5,"method":"turn/interrupt","params":{"threadId":thread,"turnId":turn}}))
.await;
assert_eq!(interrupted[0]["result"], json!({}));
let operations = runtime
.operations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
assert!(operations.contains(&"input:begin".into()));
assert!(operations.contains(&"steer:steer now".into()));
assert!(operations.contains(&"interrupt".into()));
}
#[tokio::test]
async fn thread_identity_is_validated_before_attach_and_persistence_controls_stay_read_only() {
let runtime = RecordingRuntime::new();
let adapter = CodexAppServerAdapter::with_mode(
runtime.clone(),
"/workspace",
CodexCompatibilityMode::SchemaExtended,
);
let mut connection = adapter.connection();
connection
.handle(
json!({"id":1,"method":"initialize","params":{"clientInfo":{"version":"0.144.4"}}}),
)
.await;
connection.handle(json!({"method":"initialized"})).await;
let wrong = connection
.handle(json!({"id":2,"method":"thread/resume","params":{"threadId":"00000000-0000-8000-8000-000000000000"}}))
.await;
assert_eq!(wrong[0]["error"]["data"]["name"], "invalid_params");
let operations = runtime
.operations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
assert_eq!(operations, ["describe"]);
let expected = deterministic_uuid("thread", "canonical-session");
let resumed = connection
.handle(json!({"id":3,"method":"thread/resume","params":{"threadId":expected}}))
.await;
assert_eq!(resumed[0]["result"]["thread"]["id"], expected);
let listed = connection
.handle(json!({"id":4,"method":"thread/list","params":{}}))
.await;
assert_eq!(listed[0]["result"]["data"].as_array().unwrap().len(), 1);
assert_eq!(listed[0]["result"]["data"][0]["id"], expected);
let before_read_only_controls = runtime
.operations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
for (id, method) in [(5, "thread/fork"), (6, "thread/archive")] {
let response = connection
.handle(json!({"id":id,"method":method,"params":{"threadId":expected}}))
.await;
assert_eq!(
response[0]["error"]["data"]["name"], "unsupported_action",
"{method}"
);
}
assert_eq!(
runtime
.operations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_slice(),
before_read_only_controls.as_slice()
);
}
#[tokio::test]
async fn initialization_order_rejects_early_and_repeated_messages() {
let adapter = CodexAppServerAdapter::new(RecordingRuntime::new(), "/workspace");
let mut connection = adapter.connection();
let early = connection
.handle(json!({"id":1,"method":"thread/start","params":{}}))
.await;
assert_eq!(early[0]["error"]["data"]["name"], "invalid_params");
let initialized_early = connection.handle(json!({"method":"initialized"})).await;
assert_eq!(
initialized_early[0]["params"]["error"]["additionalDetails"]["name"],
"invalid_params"
);
connection
.handle(
json!({"id":2,"method":"initialize","params":{"clientInfo":{"version":"0.144.4"}}}),
)
.await;
let before_notification = connection
.handle(json!({"id":3,"method":"thread/start","params":{}}))
.await;
assert_eq!(
before_notification[0]["error"]["data"]["name"],
"invalid_params"
);
assert!(connection
.handle(json!({"method":"initialized"}))
.await
.is_empty());
let repeated = connection
.handle(
json!({"id":4,"method":"initialize","params":{"clientInfo":{"version":"0.144.4"}}}),
)
.await;
assert_eq!(repeated[0]["error"]["data"]["name"], "invalid_params");
}
#[test]
fn live_text_events_have_one_stable_turn_and_complete_with_exact_text() {
let adapter = CodexAppServerAdapter::new(RecordingRuntime::new(), "/workspace");
let mut connection = adapter.connection();
connection.thread_id = Some(deterministic_uuid("thread", "canonical-session"));
let started = connection
.event_notifications(FrontendEvent {
sequence: 10,
kind: "turn_started".into(),
payload: json!({"type":"turn_started"}),
})
.unwrap();
let turn = started[1]["params"]["turn"]["id"]
.as_str()
.unwrap()
.to_owned();
let first = connection
.event_notifications(FrontendEvent {
sequence: 11,
kind: "text_delta".into(),
payload: json!({"type":"text_delta","text":"hello "}),
})
.unwrap();
assert_eq!(
first
.iter()
.map(|value| value["method"].as_str().unwrap())
.collect::<Vec<_>>(),
vec!["item/started", "item/agentMessage/delta"]
);
assert_eq!(first[0]["params"]["turnId"], turn);
let item = first[0]["params"]["item"]["id"].clone();
let second = connection
.event_notifications(FrontendEvent {
sequence: 12,
kind: "text_delta".into(),
payload: json!({"type":"text_delta","text":"world"}),
})
.unwrap();
assert_eq!(second.len(), 1);
assert_eq!(second[0]["params"]["turnId"], turn);
assert_eq!(second[0]["params"]["itemId"], item);
assert!(connection
.event_notifications(FrontendEvent {
sequence: 13,
kind: "turn_completed".into(),
payload: json!({"type":"turn_completed"}),
})
.unwrap()
.is_empty());
let completed = connection
.event_notifications(FrontendEvent {
sequence: 14,
kind: "turn_succeeded".into(),
payload: json!({"type":"turn_succeeded"}),
})
.unwrap();
assert_eq!(completed[0]["method"], "item/completed");
assert_eq!(completed[0]["params"]["turnId"], turn);
assert_eq!(completed[0]["params"]["item"]["id"], item);
assert_eq!(completed[0]["params"]["item"]["text"], "hello world");
assert_eq!(completed[2]["method"], "turn/completed");
}
#[test]
fn approval_request_decodes_the_sdk_broker_payload_for_stock_codex() {
let adapter = CodexAppServerAdapter::new(RecordingRuntime::new(), "/workspace");
let mut connection = adapter.connection();
connection.thread_id = Some(deterministic_uuid("thread", "canonical-session"));
connection.active_turn = Some(deterministic_uuid("turn", "canonical-turn"));
let messages = connection
.event_notifications(FrontendEvent {
sequence: 15,
kind: "request".into(),
payload: json!({
"type": "request",
"request": {
"id": 77,
"kind": "approval",
"payload": {
"tool": "bash",
"subject": "printf stock-approval",
"raw_args": {
"command": "printf stock-approval",
"cwd": "/workspace/project"
}
}
}
}),
})
.unwrap();
assert_eq!(messages.len(), 1);
assert_eq!(
messages[0]["method"],
"item/commandExecution/requestApproval"
);
assert_eq!(messages[0]["params"]["command"], "printf stock-approval");
assert_eq!(messages[0]["params"]["cwd"], "/workspace/project");
assert_eq!(
messages[0]["params"]["_supercode"]["raw"]["raw_args"]["command"],
"printf stock-approval"
);
}
#[test]
fn native_command_file_reasoning_plan_and_diff_shapes_preserve_raw_events() {
let adapter = CodexAppServerAdapter::new(RecordingRuntime::new(), "/workspace");
let mut connection = adapter.connection();
connection.thread_id = Some(deterministic_uuid("thread", "canonical-session"));
let command = connection
.event_notifications(FrontendEvent {
sequence: 20,
kind: "tool_call_started".into(),
payload: json!({"type":"tool_call_started","id":"cmd","name":"bash","arguments":"{\"command\":\"cargo test\",\"cwd\":\"/workspace\"}"}),
})
.unwrap();
assert_eq!(command[0]["params"]["item"]["type"], "commandExecution");
assert_eq!(command[0]["params"]["item"]["command"], "cargo test");
let command_done = connection
.event_notifications(FrontendEvent {
sequence: 21,
kind: "tool_call_completed".into(),
payload: json!({"type":"tool_call_completed","id":"cmd","name":"bash","output":"ok","is_error":false}),
})
.unwrap();
assert_eq!(command_done[0]["params"]["item"]["aggregatedOutput"], "ok");
let file = connection
.event_notifications(FrontendEvent {
sequence: 22,
kind: "tool_call_started".into(),
payload: json!({"type":"tool_call_started","id":"file","name":"write_file","arguments":{"path":"proof.txt","content":"proof"}}),
})
.unwrap();
assert_eq!(file[0]["params"]["item"]["type"], "fileChange");
assert_eq!(file[0]["params"]["item"]["changes"][0]["path"], "proof.txt");
assert!(file[0]["params"]["item"]["changes"][0]["diff"]
.as_str()
.unwrap()
.contains("@@ -0,0 +1,1 @@\n+proof"));
assert_eq!(
file[0]["params"]["item"]["_supercode"]["tool"],
"write_file"
);
let file_done = connection
.event_notifications(FrontendEvent {
sequence: 23,
kind: "tool_call_completed".into(),
payload: json!({"type":"tool_call_completed","id":"file","name":"write_file","output":"wrote proof.txt","is_error":false}),
})
.unwrap();
assert_eq!(file_done[1]["method"], "turn/diff/updated");
assert!(file_done[1]["params"]["diff"]
.as_str()
.unwrap()
.contains("+proof"));
let reasoning = connection
.event_notifications(FrontendEvent {
sequence: 24,
kind: "reasoning_delta".into(),
payload: json!({"type":"reasoning_delta","text":"inspect","future":true}),
})
.unwrap();
assert_eq!(reasoning[0]["method"], "item/started");
assert_eq!(reasoning[2]["params"]["_supercode"]["raw"]["future"], true);
let reasoning_done = connection
.event_notifications(FrontendEvent {
sequence: 25,
kind: "reasoning_completed".into(),
payload: json!({"type":"reasoning_completed"}),
})
.unwrap();
assert_eq!(reasoning_done[0]["params"]["item"]["summary"][0], "inspect");
let plan = connection
.event_notifications(FrontendEvent {
sequence: 26,
kind: "plan_update".into(),
payload: json!({"type":"plan_update","plan":[{"step":"test","status":"in_progress"}]}),
})
.unwrap();
assert_eq!(plan[0]["method"], "turn/plan/updated");
let diff = connection
.event_notifications(FrontendEvent {
sequence: 27,
kind: "diff_updated".into(),
payload: json!({"type":"diff_updated","diff":"@@ proof @@"}),
})
.unwrap();
assert_eq!(diff[0]["method"], "turn/diff/updated");
assert_eq!(diff[0]["params"]["diff"], "@@ proof @@");
}
#[tokio::test]
async fn replays_every_request_from_the_pinned_stock_client_corpus() {
const CORPUS: &str = include_str!(
"../../../scripts/client-protocol-corpus/fixtures/codex_app_server_v0_144.jsonl"
);
fn normalize(value: &mut Value, thread_id: Option<&str>) {
match value {
Value::String(text) if text == "<TMP>/project" => {
*text = "/workspace".into();
}
Value::Object(object) => {
if object.contains_key("threadId") {
if let Some(thread_id) = thread_id {
object.insert("threadId".into(), Value::String(thread_id.into()));
}
}
for child in object.values_mut() {
normalize(child, thread_id);
}
}
Value::Array(array) => {
for child in array {
normalize(child, thread_id);
}
}
_ => {}
}
}
let runtime = RecordingRuntime::new();
let adapter = CodexAppServerAdapter::new(runtime, "/workspace");
let mut connection = adapter.connection();
let mut corpus_connection = 1u64;
let mut thread_id = None::<String>;
let mut replayed = 0usize;
for line in CORPUS.lines() {
let envelope: Value = serde_json::from_str(line).unwrap();
if envelope["direction"] != "client_to_server"
|| envelope["transport"] != "websocket"
|| !envelope["message"]["method"].is_string()
{
continue;
}
let next_connection = envelope["connection"].as_u64().unwrap();
if next_connection != corpus_connection {
connection = adapter.connection();
corpus_connection = next_connection;
}
let mut request = envelope["message"].clone();
normalize(&mut request, thread_id.as_deref());
let method = request["method"].as_str().unwrap().to_owned();
let request_id = request.get("id").cloned();
let output = connection.handle(request).await;
if let Some(request_id) = request_id {
assert!(!output.is_empty(), "{method} returned no response");
assert_eq!(output[0]["id"], request_id, "{method} changed request id");
assert!(
output[0].get("error").is_none(),
"{method} failed corpus replay: {}",
output[0]
);
}
if method == "thread/start" {
thread_id = Some(output[0]["result"]["thread"]["id"].as_str().unwrap().into());
}
replayed += 1;
}
assert_eq!(replayed, 24, "pinned corpus request count drifted");
}
#[tokio::test]
async fn adapter_preserves_the_pinned_stock_notification_order() {
const CORPUS: &str = include_str!(
"../../../scripts/client-protocol-corpus/fixtures/codex_app_server_v0_144.jsonl"
);
let expected = CORPUS
.lines()
.filter_map(|line| serde_json::from_str::<Value>(line).ok())
.filter(|envelope| envelope["direction"] == "server_to_client")
.filter_map(|envelope| envelope["message"]["method"].as_str().map(str::to_owned))
.collect::<BTreeSet<_>>();
let adapter = CodexAppServerAdapter::new(RecordingRuntime::new(), "/workspace");
let mut connection = adapter.connection();
let mut output = connection
.handle(
json!({"id":1,"method":"initialize","params":{"clientInfo":{"version":"0.144.4"}}}),
)
.await;
connection.handle(json!({"method":"initialized"})).await;
output.extend(
connection
.handle(json!({"id":2,"method":"thread/start","params":{}}))
.await,
);
let events = [
(3, "turn_started", json!({"type":"turn_started"})),
(
4,
"user_message",
json!({"type":"user_message","text":"prompt"}),
),
(
5,
"reasoning_delta",
json!({"type":"reasoning_delta","text":"reason"}),
),
(
6,
"reasoning_completed",
json!({"type":"reasoning_completed"}),
),
(
7,
"text_delta",
json!({"type":"text_delta","text":"answer"}),
),
(
8,
"diff_updated",
json!({"type":"diff_updated","diff":"@@"}),
),
(
9,
"usage",
json!({"type":"usage","prompt_tokens":1,"completion_tokens":1,"total_tokens":2}),
),
(
10,
"cache_warning",
json!({"type":"cache_warning","message":"warning"}),
),
(
11,
"request",
json!({"type":"request","request":{"id":77,"kind":"approval","payload":{"command":"true"}}}),
),
(
12,
"request_resolved",
json!({"type":"request_resolved","request_id":77}),
),
(13, "turn_completed", json!({"type":"turn_completed"})),
(
14,
"turn_failed",
json!({"type":"turn_failed","message":"failure"}),
),
];
for (sequence, kind, payload) in events {
output.extend(
connection
.event_notifications(FrontendEvent {
sequence,
kind: kind.into(),
payload,
})
.unwrap(),
);
}
let actual = output
.iter()
.filter_map(|message| message["method"].as_str().map(str::to_owned))
.collect::<Vec<_>>();
let actual_families = actual.iter().cloned().collect::<BTreeSet<_>>();
let missing = expected
.difference(&actual_families)
.cloned()
.collect::<Vec<_>>();
assert!(
missing.is_empty(),
"missing notification families: {missing:?}"
);
assert_eq!(
actual,
[
"remoteControl/status/changed",
"thread/started",
"thread/settings/updated",
"account/rateLimits/updated",
"thread/goal/cleared",
"thread/status/changed",
"turn/started",
"item/started",
"item/completed",
"item/started",
"item/reasoning/summaryPartAdded",
"item/reasoning/summaryTextDelta",
"item/completed",
"item/started",
"item/agentMessage/delta",
"turn/diff/updated",
"thread/tokenUsage/updated",
"warning",
"item/commandExecution/requestApproval",
"serverRequest/resolved",
"error",
"item/completed",
"thread/status/changed",
"turn/completed",
],
"semantic notification order drifted from the pinned stock-client cadence"
);
}
}