Skip to main content

supercode_codex_frontend/
codex_app_server_v0_144.rs

1//! Compatibility namespace for the app-server protocol spoken by
2//! `codex-cli 0.144.4`.
3
4use std::collections::{BTreeMap, BTreeSet};
5use std::path::PathBuf;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::Arc;
8use std::time::{SystemTime, UNIX_EPOCH};
9
10use serde_json::{json, Map, Value};
11use supercode::{
12    ChatMessage, FrontendApprovalDecision, FrontendAttachment, FrontendElicitationAction,
13    FrontendEvent, FrontendRequestKind, FrontendResponse, FrontendRuntimeDescriptor, Role,
14    SdkError, SdkErrorCode, SdkRuntime,
15};
16
17/// Exact protocol namespace pinned by the SUP-66 stock-client corpus.
18pub const PROTOCOL_NAMESPACE: &str = "codex_app_server/v0_144";
19/// Only stock client release accepted by this compatibility namespace.
20pub const CODEX_CLI_VERSION: &str = "0.144.4";
21/// Maximum historical messages projected into a stock-client attachment.
22pub const HISTORY_LIMIT: usize = 4096;
23
24const TRACED_METHODS: &[&str] = &[
25    "account/read",
26    "configRequirements/read",
27    "hooks/list",
28    "initialize",
29    "initialized",
30    "model/list",
31    "skills/list",
32    "thread/goal/get",
33    "thread/read",
34    "thread/resume",
35    "thread/start",
36    "thread/unsubscribe",
37    "turn/start",
38];
39
40const SCHEMA_ONLY_METHODS: &[&str] = &[
41    "thread/archive",
42    "thread/fork",
43    "thread/list",
44    "turn/interrupt",
45    "turn/steer",
46];
47
48/// Protocol surface enabled for one adapter.
49#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
50pub enum CodexCompatibilityMode {
51    /// Exact SUP-66 trace. Every untraced method is denied.
52    #[default]
53    TracedOnly,
54    /// Schema-defined controls used only by explicit compatibility proofs.
55    SchemaExtended,
56}
57
58/// Named adapter failures. Wire projections keep stable compatible codes.
59#[derive(Debug, thiserror::Error)]
60pub enum AdapterError {
61    #[error("codex protocol version mismatch: expected {expected}, received {received}")]
62    ProtocolVersionMismatch {
63        expected: &'static str,
64        received: String,
65    },
66    #[error("method not supported by this adapter version: {0}")]
67    UnsupportedMethod(String),
68    #[error("invalid parameters for `{method}`: {message}")]
69    InvalidParams { method: String, message: String },
70    #[error("thread `{0}` is not attached to this runtime")]
71    UnknownThread(String),
72    #[error("deterministic identifier collision for `{0}`")]
73    IdentifierCollision(String),
74    #[error(transparent)]
75    Sdk(#[from] SdkError),
76}
77
78impl AdapterError {
79    fn wire(&self, id: Value) -> Value {
80        let (code, name) = match self {
81            Self::ProtocolVersionMismatch { .. } => (-32040, "protocol_version_mismatch"),
82            Self::UnsupportedMethod(_) => (-32020, "unsupported_action"),
83            Self::InvalidParams { .. } | Self::UnknownThread(_) => (-32602, "invalid_params"),
84            Self::IdentifierCollision(_) => (-32603, "identifier_collision"),
85            Self::Sdk(error) if error.code() == SdkErrorCode::Unauthenticated => {
86                (-32030, "unauthenticated")
87            }
88            Self::Sdk(error) if error.code() == SdkErrorCode::Unauthorized => {
89                (-32031, "unauthorized")
90            }
91            Self::Sdk(error) if error.code() == SdkErrorCode::ControllerRequired => {
92                (-32032, "controller_required")
93            }
94            Self::Sdk(error) if error.code() == SdkErrorCode::LeaseExpired => {
95                (-32033, "lease_expired")
96            }
97            Self::Sdk(error) if error.code() == SdkErrorCode::UnsupportedAction => {
98                (-32020, "unsupported_action")
99            }
100            Self::Sdk(error) if error.code() == SdkErrorCode::Busy => (-32000, "busy"),
101            Self::Sdk(_) => (-32000, "sdk_error"),
102        };
103        json!({"id": id, "error": {"code": code, "message": self.to_string(), "data": {"name": name}}})
104    }
105}
106
107/// One version-pinned adapter around an existing SDK-owned runtime.
108pub struct CodexAppServerAdapter {
109    runtime: Arc<dyn SdkRuntime>,
110    mode: CodexCompatibilityMode,
111    cwd: PathBuf,
112    codex_home: PathBuf,
113    next_turn: AtomicU64,
114}
115
116impl CodexAppServerAdapter {
117    pub fn new(runtime: Arc<dyn SdkRuntime>, cwd: impl Into<PathBuf>) -> Arc<Self> {
118        Self::with_mode(runtime, cwd, CodexCompatibilityMode::TracedOnly)
119    }
120
121    pub fn with_mode(
122        runtime: Arc<dyn SdkRuntime>,
123        cwd: impl Into<PathBuf>,
124        mode: CodexCompatibilityMode,
125    ) -> Arc<Self> {
126        let cwd = cwd.into();
127        Self::with_mode_and_client_home(runtime, cwd.clone(), cwd, mode)
128    }
129
130    /// Construct an adapter whose initialize response points the stock client
131    /// at its isolated preference home. This path is frontend-only metadata;
132    /// it never enters canonical history or native session exports.
133    pub fn with_mode_and_client_home(
134        runtime: Arc<dyn SdkRuntime>,
135        cwd: impl Into<PathBuf>,
136        codex_home: impl Into<PathBuf>,
137        mode: CodexCompatibilityMode,
138    ) -> Arc<Self> {
139        Arc::new(Self {
140            runtime,
141            mode,
142            cwd: cwd.into(),
143            codex_home: codex_home.into(),
144            next_turn: AtomicU64::new(0),
145        })
146    }
147
148    pub fn namespace(&self) -> &'static str {
149        PROTOCOL_NAMESPACE
150    }
151
152    pub fn enabled_methods(&self) -> BTreeSet<&'static str> {
153        let mut methods = TRACED_METHODS.iter().copied().collect::<BTreeSet<_>>();
154        if self.mode == CodexCompatibilityMode::SchemaExtended {
155            methods.extend(SCHEMA_ONLY_METHODS.iter().copied());
156        }
157        methods
158    }
159
160    pub(crate) async fn detach(&self) {
161        let _ = self.runtime.detach().await;
162    }
163
164    pub fn connection(self: &Arc<Self>) -> CodexConnection {
165        CodexConnection {
166            adapter: self.clone(),
167            initialize_seen: false,
168            initialized: false,
169            attached: None,
170            descriptor: None,
171            thread_id: None,
172            active_turn: None,
173            live_event_turn: None,
174            open_agent_item: None,
175            open_agent_text: String::new(),
176            open_reasoning_item: None,
177            open_reasoning_text: String::new(),
178            open_tools: BTreeMap::new(),
179            pending_requests: BTreeMap::new(),
180        }
181    }
182
183    fn mapped_thread_id(&self, canonical: &str) -> Result<String, AdapterError> {
184        Ok(deterministic_uuid("thread", canonical))
185    }
186
187    fn validate_thread(&self, thread: &str, canonical: &str) -> Result<(), AdapterError> {
188        if thread == deterministic_uuid("thread", canonical) {
189            Ok(())
190        } else {
191            Err(AdapterError::UnknownThread(thread.to_owned()))
192        }
193    }
194}
195
196/// Connection-local Codex protocol state. JSON ids and subscriptions never
197/// enter canonical runtime state.
198pub struct CodexConnection {
199    adapter: Arc<CodexAppServerAdapter>,
200    initialize_seen: bool,
201    initialized: bool,
202    attached: Option<FrontendAttachment>,
203    descriptor: Option<FrontendRuntimeDescriptor>,
204    thread_id: Option<String>,
205    active_turn: Option<String>,
206    live_event_turn: Option<String>,
207    open_agent_item: Option<String>,
208    open_agent_text: String,
209    open_reasoning_item: Option<String>,
210    open_reasoning_text: String,
211    open_tools: BTreeMap<String, (String, Value)>,
212    pending_requests: BTreeMap<ValueKey, u64>,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
216enum ValueKey {
217    Number(u64),
218    String(String),
219}
220
221impl CodexConnection {
222    pub(crate) fn is_attached(&self) -> bool {
223        self.attached.is_some()
224    }
225
226    /// Handle one stock app-server request/notification. The returned values
227    /// are already ordered exactly as they must be written to the client.
228    pub async fn handle(&mut self, message: Value) -> Vec<Value> {
229        let id = message.get("id").cloned().unwrap_or(Value::Null);
230        match self.handle_inner(&message).await {
231            Ok(mut response) => {
232                if !id.is_null() {
233                    let result = if response.is_empty() {
234                        Value::Null
235                    } else {
236                        response.remove(0)
237                    };
238                    response.insert(0, json!({"id": id, "result": result}));
239                }
240                response
241            }
242            Err(error) if id.is_null() => vec![json!({"method": "error", "params": {
243                "error": {"message": error.to_string(), "codexErrorInfo": "other", "additionalDetails": {"name": wire_error_name(&error)}},
244                "willRetry": false,
245                "threadId": self.thread_id,
246                "turnId": self.active_turn,
247            }})],
248            Err(error) => vec![error.wire(id)],
249        }
250    }
251
252    async fn handle_inner(&mut self, message: &Value) -> Result<Vec<Value>, AdapterError> {
253        let method = message
254            .get("method")
255            .and_then(Value::as_str)
256            .ok_or_else(|| AdapterError::InvalidParams {
257                method: "<missing>".into(),
258                message: "method must be a string".into(),
259            })?;
260        if !self.adapter.enabled_methods().contains(method) {
261            return Err(AdapterError::UnsupportedMethod(method.to_owned()));
262        }
263        if method != "initialize" && !self.initialize_seen {
264            return Err(invalid(method, "initialize must be the first request"));
265        }
266        if method != "initialize" && method != "initialized" && !self.initialized {
267            return Err(invalid(
268                method,
269                "initialized notification is required first",
270            ));
271        }
272        let params = message.get("params").cloned().unwrap_or_else(|| json!({}));
273        match method {
274            "initialize" => self.initialize(params).await,
275            "initialized" => {
276                if self.initialized {
277                    return Err(invalid("initialized", "notification already received"));
278                }
279                self.initialized = true;
280                Ok(Vec::new())
281            }
282            "account/read" => Ok(vec![json!({"account": null, "requiresOpenaiAuth": false})]),
283            "configRequirements/read" => Ok(vec![json!({"requirements": null})]),
284            "hooks/list" => Ok(vec![
285                json!({"data": [{"cwd": self.cwd(), "hooks": [], "warnings": [], "errors": []}]}),
286            ]),
287            "model/list" => self.model_list().await,
288            "skills/list" => self.skills_list().await,
289            "thread/goal/get" => Ok(vec![json!({"goal": null})]),
290            "thread/start" | "thread/resume" | "thread/read" => {
291                self.attach_thread(method, &params).await
292            }
293            "thread/unsubscribe" => self.unsubscribe(&params).await,
294            "turn/start" => self.start_turn(&params).await,
295            "turn/steer" => self.steer(&params).await,
296            "turn/interrupt" => self.interrupt(&params).await,
297            "thread/list" => self.thread_list().await,
298            "thread/fork" => self.thread_fork(&params).await,
299            "thread/archive" => self.thread_archive(&params).await,
300            _ => Err(AdapterError::UnsupportedMethod(method.to_owned())),
301        }
302    }
303
304    async fn initialize(&mut self, params: Value) -> Result<Vec<Value>, AdapterError> {
305        if self.initialize_seen {
306            return Err(invalid("initialize", "request already received"));
307        }
308        let received = params
309            .pointer("/clientInfo/version")
310            .and_then(Value::as_str)
311            .unwrap_or("<missing>");
312        if received != CODEX_CLI_VERSION {
313            return Err(AdapterError::ProtocolVersionMismatch {
314                expected: CODEX_CLI_VERSION,
315                received: received.to_owned(),
316            });
317        }
318        self.initialize_seen = true;
319        Ok(vec![
320            json!({
321                "userAgent": format!("supercode-codex-adapter/{CODEX_CLI_VERSION}"),
322                "codexHome": self.adapter.codex_home,
323                "platformFamily": if cfg!(unix) { "unix" } else { "windows" },
324                "platformOs": std::env::consts::OS,
325            }),
326            json!({"method": "remoteControl/status/changed", "params": {
327                "status": "disabled", "serverName": "supercode", "installationId": "supercode",
328                "environmentId": null
329            }}),
330        ])
331    }
332
333    async fn ensure_descriptor(&mut self) -> Result<FrontendRuntimeDescriptor, AdapterError> {
334        if let Some(descriptor) = &self.descriptor {
335            return Ok(descriptor.clone());
336        }
337        let descriptor = self.adapter.runtime.describe().await?;
338        self.descriptor = Some(descriptor.clone());
339        Ok(descriptor)
340    }
341
342    async fn model_list(&mut self) -> Result<Vec<Value>, AdapterError> {
343        let descriptor = self.ensure_descriptor().await?;
344        let model = descriptor.model;
345        Ok(vec![json!({"data": [{
346            "id": model, "model": model, "upgrade": null, "upgradeInfo": null,
347            "availabilityNux": null, "displayName": model, "description": "Active Supercode runtime model",
348            "hidden": false, "supportedReasoningEfforts": [], "defaultReasoningEffort": "medium",
349            "inputModalities": ["text", "image"], "supportsPersonality": false,
350            "additionalSpeedTiers": [], "serviceTiers": [], "defaultServiceTier": null, "isDefault": true
351        }], "nextCursor": null})])
352    }
353
354    async fn skills_list(&mut self) -> Result<Vec<Value>, AdapterError> {
355        Ok(vec![
356            // SDK operations are not Codex SKILL.md files. Advertising them as
357            // filesystem skills would make the stock client attempt to load
358            // nonexistent paths and would overstate the frontend capability.
359            json!({"data": [{"cwd": self.cwd(), "skills": [], "errors": []}]}),
360        ])
361    }
362
363    async fn attach_thread(
364        &mut self,
365        method: &str,
366        params: &Value,
367    ) -> Result<Vec<Value>, AdapterError> {
368        if method != "thread/start" {
369            let requested = required_str(params, "threadId", method)?;
370            let descriptor = self.ensure_descriptor().await?;
371            self.adapter
372                .validate_thread(requested, &descriptor.session_id)?;
373        }
374        let attachment = self.adapter.runtime.attach(HISTORY_LIMIT).await?;
375        let canonical = attachment.descriptor.session_id.clone();
376        let thread_id = self.adapter.mapped_thread_id(&canonical)?;
377        if let Some(descriptor) = &self.descriptor {
378            if descriptor.session_id != canonical {
379                return Err(AdapterError::IdentifierCollision(canonical));
380            }
381        }
382        let response = thread_response(
383            &attachment,
384            &thread_id,
385            &self.adapter.cwd,
386            method == "thread/read",
387        );
388        let started = json!({"method": "thread/started", "params": {"thread": thread_json(&attachment, &thread_id, &self.adapter.cwd)}});
389        self.descriptor = Some(attachment.descriptor.clone());
390        self.thread_id = Some(thread_id);
391        self.attached = Some(attachment);
392        if method == "thread/start" {
393            let thread_id = self.thread_id.as_deref().unwrap();
394            let descriptor = self.descriptor.as_ref().unwrap();
395            Ok(vec![
396                response,
397                started,
398                notification(
399                    "thread/settings/updated",
400                    json!({"threadId":thread_id,"threadSettings":{
401                        "cwd":self.cwd(),"approvalPolicy":"on-request","approvalsReviewer":"user",
402                        "sandboxPolicy":{"type":"workspaceWrite","writableRoots":[],"networkAccess":false,"excludeTmpdirEnvVar":false,"excludeSlashTmp":false},
403                        "activePermissionProfile":null,"model":descriptor.model,"modelProvider":"supercode",
404                        "serviceTier":null,"effort":null,"summary":null,
405                        "collaborationMode":{"mode":"default","settings":{"model":descriptor.model,"reasoning_effort":null,"developer_instructions":""}},
406                        "multiAgentMode":"explicitRequestOnly","personality":null
407                    }}),
408                ),
409                notification(
410                    "account/rateLimits/updated",
411                    json!({"rateLimits":{"limitId":"supercode","limitName":null,"primary":null,"secondary":null,"credits":null,"individualLimit":null,"planType":null,"rateLimitReachedType":null}}),
412                ),
413                notification("thread/goal/cleared", json!({"threadId":thread_id})),
414            ])
415        } else {
416            Ok(vec![response])
417        }
418    }
419
420    async fn unsubscribe(&mut self, params: &Value) -> Result<Vec<Value>, AdapterError> {
421        let thread = required_str(params, "threadId", "thread/unsubscribe")?;
422        let canonical = self
423            .descriptor
424            .as_ref()
425            .map(|d| d.session_id.as_str())
426            .ok_or_else(|| AdapterError::UnknownThread(thread.to_owned()))?;
427        self.adapter.validate_thread(thread, canonical)?;
428        self.adapter.runtime.detach().await?;
429        self.attached = None;
430        Ok(vec![json!({"status": "unsubscribed"})])
431    }
432
433    async fn start_turn(&mut self, params: &Value) -> Result<Vec<Value>, AdapterError> {
434        let thread = required_str(params, "threadId", "turn/start")?;
435        self.validate_attached_thread(thread)?;
436        let prompt = params
437            .get("input")
438            .and_then(Value::as_array)
439            .ok_or_else(|| invalid("turn/start", "input must be an array"))?
440            .iter()
441            .filter(|item| item.get("type").and_then(Value::as_str) == Some("text"))
442            .filter_map(|item| item.get("text").and_then(Value::as_str))
443            .collect::<Vec<_>>()
444            .join("\n");
445        if prompt.is_empty() {
446            return Err(invalid("turn/start", "at least one text input is required"));
447        }
448        let next_turn = self.adapter.next_turn.fetch_add(1, Ordering::Relaxed) + 1;
449        let canonical = self.descriptor.as_ref().unwrap().session_id.clone();
450        let turn_id = deterministic_uuid("turn", &format!("{canonical}:{next_turn}"));
451        self.adapter.runtime.clone().send_input(prompt).await?;
452        self.active_turn = Some(turn_id.clone());
453        Ok(vec![
454            json!({"turn": turn_json(&turn_id, "inProgress", None)}),
455        ])
456    }
457
458    async fn steer(&mut self, params: &Value) -> Result<Vec<Value>, AdapterError> {
459        let thread = required_str(params, "threadId", "turn/steer")?;
460        self.validate_attached_thread(thread)?;
461        let expected = required_str(params, "expectedTurnId", "turn/steer")?;
462        let active = self
463            .active_turn
464            .as_deref()
465            .ok_or_else(|| invalid("turn/steer", "no active turn"))?;
466        if expected != active {
467            return Err(invalid("turn/steer", "expectedTurnId is not active"));
468        }
469        let prompt = text_from_input(params, "turn/steer")?;
470        self.adapter.runtime.steer(prompt).await?;
471        Ok(vec![json!({"turnId": active})])
472    }
473
474    async fn interrupt(&mut self, params: &Value) -> Result<Vec<Value>, AdapterError> {
475        let thread = required_str(params, "threadId", "turn/interrupt")?;
476        self.validate_attached_thread(thread)?;
477        let turn = required_str(params, "turnId", "turn/interrupt")?;
478        if self.active_turn.as_deref() != Some(turn) {
479            return Err(invalid("turn/interrupt", "turnId is not active"));
480        }
481        self.adapter.runtime.interrupt().await?;
482        Ok(vec![json!({})])
483    }
484
485    async fn thread_list(&mut self) -> Result<Vec<Value>, AdapterError> {
486        let Some(attachment) = self.attached.as_ref() else {
487            return Ok(vec![json!({"data": [], "nextCursor": null})]);
488        };
489        let thread_id = self.thread_id.as_ref().unwrap();
490        Ok(vec![
491            json!({"data": [thread_json(attachment, thread_id, &self.adapter.cwd)], "nextCursor": null}),
492        ])
493    }
494
495    async fn thread_fork(&mut self, params: &Value) -> Result<Vec<Value>, AdapterError> {
496        let thread = required_str(params, "threadId", "thread/fork")?;
497        self.validate_attached_thread(thread)?;
498        Err(AdapterError::UnsupportedMethod(
499            "thread/fork: canonical runtime cannot be duplicated by a frontend".into(),
500        ))
501    }
502
503    async fn thread_archive(&mut self, params: &Value) -> Result<Vec<Value>, AdapterError> {
504        let thread = required_str(params, "threadId", "thread/archive")?;
505        self.validate_attached_thread(thread)?;
506        Err(AdapterError::UnsupportedMethod(
507            "thread/archive: frontend cannot mutate canonical persistence".into(),
508        ))
509    }
510
511    fn validate_attached_thread(&self, thread: &str) -> Result<(), AdapterError> {
512        let canonical = self
513            .descriptor
514            .as_ref()
515            .map(|d| d.session_id.as_str())
516            .ok_or_else(|| AdapterError::UnknownThread(thread.to_owned()))?;
517        self.adapter.validate_thread(thread, canonical)
518    }
519
520    /// Read and translate the next canonical event. No canonical payload is
521    /// modified; unrecognized payloads remain in a warning's `_supercode.raw`.
522    pub async fn next_notifications(&mut self) -> Result<Vec<Value>, AdapterError> {
523        let event = self
524            .attached
525            .as_mut()
526            .ok_or_else(|| AdapterError::UnknownThread("<detached>".into()))?
527            .next_event()
528            .await?;
529        self.event_notifications(event)
530    }
531
532    fn event_notifications(&mut self, event: FrontendEvent) -> Result<Vec<Value>, AdapterError> {
533        let thread = self.thread_id.clone().unwrap_or_default();
534        if self.active_turn.is_none()
535            && self.live_event_turn.is_none()
536            && matches!(
537                event.kind.as_str(),
538                "turn_started"
539                    | "user_message"
540                    | "text_delta"
541                    | "tool_call_started"
542                    | "tool_call_completed"
543                    | "usage"
544                    | "request"
545                    | "request_resolved"
546                    | "reasoning_delta"
547                    | "reasoning_summary_delta"
548                    | "reasoning_completed"
549                    | "plan_update"
550                    | "plan_updated"
551                    | "diff_update"
552                    | "diff_updated"
553                    | "turn_succeeded"
554                    | "turn_interrupted"
555                    | "turn_failed"
556                    | "turn_completed"
557            )
558        {
559            self.live_event_turn = Some(deterministic_uuid(
560                "turn",
561                &format!("{thread}:event:{}", event.sequence),
562            ));
563        }
564        let turn = self
565            .active_turn
566            .clone()
567            .or_else(|| self.live_event_turn.clone())
568            .unwrap_or_else(|| deterministic_uuid("turn", &format!("{thread}:idle")));
569        let p = &event.payload;
570        let messages = match event.kind.as_str() {
571            "turn_started" => {
572                vec![
573                    notification(
574                        "thread/status/changed",
575                        json!({"threadId": thread, "status": {"type": "active"}}),
576                    ),
577                    notification(
578                        "turn/started",
579                        json!({"threadId": thread, "turn": turn_json(&turn, "inProgress", None)}),
580                    ),
581                ]
582            }
583            "user_message" => {
584                let id = item_id(event.sequence, "user");
585                let text = p.get("text").and_then(Value::as_str).unwrap_or_default();
586                let item = json!({"type": "userMessage", "id": id, "clientId": null, "content": [{"type":"text", "text": text, "text_elements": []}]});
587                vec![
588                    item_notification("item/started", &thread, &turn, item.clone(), "startedAtMs"),
589                    item_notification("item/completed", &thread, &turn, item, "completedAtMs"),
590                ]
591            }
592            "text_delta" => {
593                let delta = p.get("text").and_then(Value::as_str).unwrap_or_default();
594                self.open_agent_text.push_str(delta);
595                let (id, started) = match &self.open_agent_item {
596                    Some(id) => (id.clone(), None),
597                    None => {
598                        let id = item_id(event.sequence, "agent");
599                        self.open_agent_item = Some(id.clone());
600                        let item = json!({"type":"agentMessage","id":id,"text":"","phase":null,"memoryCitation":null});
601                        (
602                            id,
603                            Some(item_notification(
604                                "item/started",
605                                &thread,
606                                &turn,
607                                item,
608                                "startedAtMs",
609                            )),
610                        )
611                    }
612                };
613                let mut out = Vec::with_capacity(2);
614                if let Some(started) = started {
615                    out.push(started);
616                }
617                out.push(notification(
618                    "item/agentMessage/delta",
619                    json!({"threadId": thread, "turnId": turn, "itemId": id, "delta": delta}),
620                ));
621                out
622            }
623            "tool_call_started" => {
624                let id = p
625                    .get("id")
626                    .and_then(Value::as_str)
627                    .map(str::to_owned)
628                    .unwrap_or_else(|| item_id(event.sequence, "tool"));
629                let name = p
630                    .get("name")
631                    .and_then(Value::as_str)
632                    .unwrap_or("unknown_tool")
633                    .to_owned();
634                let arguments = parsed_tool_arguments(p.get("arguments"));
635                self.open_tools
636                    .insert(id.clone(), (name.clone(), arguments.clone()));
637                let item = tool_item(&id, &name, &arguments, None, false, p);
638                vec![item_notification(
639                    "item/started",
640                    &thread,
641                    &turn,
642                    item,
643                    "startedAtMs",
644                )]
645            }
646            "tool_call_completed" => {
647                let id = p
648                    .get("id")
649                    .and_then(Value::as_str)
650                    .map(str::to_owned)
651                    .unwrap_or_else(|| item_id(event.sequence, "tool"));
652                let (name, arguments) = self.open_tools.remove(&id).unwrap_or_else(|| {
653                    (
654                        p.get("name")
655                            .and_then(Value::as_str)
656                            .unwrap_or("unknown_tool")
657                            .to_owned(),
658                        json!({}),
659                    )
660                });
661                let failed = p.get("is_error").and_then(Value::as_bool) == Some(true);
662                let output = p
663                    .get("output")
664                    .and_then(Value::as_str)
665                    .map(str::to_owned)
666                    .unwrap_or_else(|| p.get("output").cloned().unwrap_or(Value::Null).to_string());
667                let item = tool_item(&id, &name, &arguments, Some(&output), failed, p);
668                let mut out = vec![item_notification(
669                    "item/completed",
670                    &thread,
671                    &turn,
672                    item,
673                    "completedAtMs",
674                )];
675                if !failed {
676                    if let Some(diff) = file_tool_diff(&name, &arguments) {
677                        out.push(notification(
678                            "turn/diff/updated",
679                            json!({"threadId":thread,"turnId":turn,"diff":diff,"_supercode":{"tool":name,"arguments":arguments}}),
680                        ));
681                    }
682                }
683                out
684            }
685            "usage" => vec![notification(
686                "thread/tokenUsage/updated",
687                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}}),
688            )],
689            "reasoning_delta" | "reasoning_summary_delta" => {
690                let delta = p
691                    .get("text")
692                    .or_else(|| p.get("delta"))
693                    .and_then(Value::as_str)
694                    .unwrap_or_default();
695                self.open_reasoning_text.push_str(delta);
696                let (id, started) = match &self.open_reasoning_item {
697                    Some(id) => (id.clone(), Vec::new()),
698                    None => {
699                        let id = item_id(event.sequence, "reasoning");
700                        self.open_reasoning_item = Some(id.clone());
701                        let started = vec![
702                            item_notification(
703                                "item/started",
704                                &thread,
705                                &turn,
706                                json!({"type":"reasoning","id":id,"summary":[],"content":[]}),
707                                "startedAtMs",
708                            ),
709                            notification(
710                                "item/reasoning/summaryPartAdded",
711                                json!({"threadId":thread,"turnId":turn,"itemId":id,"summaryIndex":0}),
712                            ),
713                        ];
714                        (id, started)
715                    }
716                };
717                let mut out = started;
718                out.push(notification(
719                    "item/reasoning/summaryTextDelta",
720                    json!({"threadId":thread,"turnId":turn,"itemId":id,"summaryIndex":0,"delta":delta,"_supercode":{"raw":p}}),
721                ));
722                out
723            }
724            "reasoning_completed" => self.finish_reasoning(&thread, &turn, event.sequence),
725            "plan_update" | "plan_updated" => vec![notification(
726                "turn/plan/updated",
727                json!({"threadId":thread,"turnId":turn,"plan":codex_plan(p.get("plan")),"explanation":p.get("explanation"),"_supercode":{"raw":p}}),
728            )],
729            "diff_update" | "diff_updated" => vec![notification(
730                "turn/diff/updated",
731                json!({"threadId":thread,"turnId":turn,"diff":p.get("diff").and_then(Value::as_str).unwrap_or_default(),"_supercode":{"raw":p}}),
732            )],
733            "request" => return self.request_notifications(event, &thread, &turn),
734            "request_resolved" => vec![notification(
735                "serverRequest/resolved",
736                json!({"threadId": thread, "requestId": p.get("request_id")}),
737            )],
738            "turn_succeeded" => self.finish_turn(&thread, &turn, "completed", None, event.sequence),
739            "turn_interrupted" => {
740                self.finish_turn(&thread, &turn, "interrupted", None, event.sequence)
741            }
742            "turn_failed" => {
743                let error = json!({"message":p.get("message").cloned().unwrap_or(Value::String("turn failed".into())),"codexErrorInfo":"other","additionalDetails":{"_supercode":{"raw":p}}});
744                let mut out = vec![notification(
745                    "error",
746                    json!({"error":error,"willRetry":false,"threadId":thread,"turnId":turn}),
747                )];
748                out.extend(self.finish_turn(&thread, &turn, "failed", Some(error), event.sequence));
749                out
750            }
751            // AgentEvent::TurnCompleted is a model-loop cadence marker. The
752            // SDK runtime publishes one authoritative outcome event after it:
753            // turn_succeeded, turn_interrupted, or turn_failed. Completing on
754            // both would make the stock client observe two terminal turns.
755            "turn_completed" => Vec::new(),
756            "cache_warning" => vec![notification(
757                "warning",
758                json!({"message":p.get("message"),"_supercode":{"sequence":event.sequence,"raw":p}}),
759            )],
760            _ => vec![notification(
761                "warning",
762                json!({"message":format!("Supercode event `{}` has no native Codex 0.144.4 display item", event.kind),"_supercode":{"sequence":event.sequence,"raw":p}}),
763            )],
764        };
765        Ok(messages)
766    }
767
768    fn request_notifications(
769        &mut self,
770        event: FrontendEvent,
771        thread: &str,
772        turn: &str,
773    ) -> Result<Vec<Value>, AdapterError> {
774        let request = event
775            .payload
776            .get("request")
777            .ok_or_else(|| invalid("event/request", "request object missing"))?;
778        let id = request
779            .get("id")
780            .and_then(Value::as_u64)
781            .ok_or_else(|| invalid("event/request", "numeric id missing"))?;
782        let wire_id = ValueKey::Number(id);
783        self.pending_requests.insert(wire_id, id);
784        let kind: FrontendRequestKind = serde_json::from_value(
785            request
786                .get("kind")
787                .cloned()
788                .unwrap_or(Value::String("other".into())),
789        )
790        .unwrap_or(FrontendRequestKind::Other);
791        let payload = request.get("payload").cloned().unwrap_or_else(|| json!({}));
792        let method = match kind {
793            FrontendRequestKind::Approval => "item/commandExecution/requestApproval",
794            FrontendRequestKind::Elicitation => "item/tool/requestUserInput",
795            FrontendRequestKind::Other => "item/tool/requestUserInput",
796        };
797        let raw_args = payload.get("raw_args").unwrap_or(&Value::Null);
798        let command_value = payload
799            .get("command")
800            .or_else(|| payload.get("subject"))
801            .or_else(|| raw_args.get("command"))
802            .or_else(|| raw_args.get("cmd"))
803            .or_else(|| raw_args.get("path"))
804            .or_else(|| raw_args.get("file_path"))
805            .cloned()
806            .unwrap_or_else(|| {
807                Value::String(
808                    payload
809                        .get("tool")
810                        .and_then(Value::as_str)
811                        .unwrap_or("tool")
812                        .to_owned(),
813                )
814            });
815        let command = match command_value {
816            Value::String(command) => command,
817            other => serde_json::to_string(&other).unwrap_or_else(|_| "tool".to_owned()),
818        };
819        let cwd = payload
820            .get("cwd")
821            .or_else(|| raw_args.get("cwd"))
822            .cloned()
823            .unwrap_or_else(|| Value::String(self.cwd()));
824        let reason = payload.get("reason").cloned().unwrap_or_else(|| {
825            json!(format!(
826                "Allow Supercode to run `{}`?",
827                payload
828                    .get("tool")
829                    .and_then(Value::as_str)
830                    .unwrap_or("tool")
831            ))
832        });
833        let started_at_ms = std::time::SystemTime::now()
834            .duration_since(std::time::UNIX_EPOCH)
835            .map(|duration| duration.as_millis() as u64)
836            .unwrap_or_default();
837        Ok(vec![
838            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}}}),
839        ])
840    }
841
842    fn finish_turn(
843        &mut self,
844        thread: &str,
845        turn: &str,
846        status: &str,
847        error: Option<Value>,
848        sequence: u64,
849    ) -> Vec<Value> {
850        let mut out = Vec::new();
851        if let Some(item) = self.open_agent_item.take() {
852            let text = std::mem::take(&mut self.open_agent_text);
853            out.push(item_notification("item/completed", thread, turn, json!({"type":"agentMessage","id":item,"text":text,"phase":null,"memoryCitation":null,"_supercode":{"sequence":sequence}}), "completedAtMs"));
854        }
855        out.extend(self.finish_reasoning(thread, turn, sequence));
856        out.push(notification(
857            "thread/status/changed",
858            json!({"threadId":thread,"status":{"type":"idle"}}),
859        ));
860        out.push(notification(
861            "turn/completed",
862            json!({"threadId":thread,"turn":turn_json(turn,status,error)}),
863        ));
864        self.active_turn = None;
865        self.live_event_turn = None;
866        out
867    }
868
869    fn finish_reasoning(&mut self, thread: &str, turn: &str, sequence: u64) -> Vec<Value> {
870        let Some(item) = self.open_reasoning_item.take() else {
871            return Vec::new();
872        };
873        let text = std::mem::take(&mut self.open_reasoning_text);
874        vec![item_notification(
875            "item/completed",
876            thread,
877            turn,
878            json!({"type":"reasoning","id":item,"summary":[text],"content":[],"_supercode":{"sequence":sequence}}),
879            "completedAtMs",
880        )]
881    }
882
883    /// Resolve a server request response, which has an id but no method.
884    pub async fn handle_server_response(
885        &mut self,
886        message: &Value,
887    ) -> Result<Vec<Value>, AdapterError> {
888        let key = value_key(
889            message
890                .get("id")
891                .ok_or_else(|| invalid("serverResponse", "id missing"))?,
892        )?;
893        let request_id = self
894            .pending_requests
895            .remove(&key)
896            .ok_or_else(|| invalid("serverResponse", "unknown request id"))?;
897        let result = message.get("result").cloned().unwrap_or_else(|| json!({}));
898        let response = match result.get("decision").and_then(Value::as_str) {
899            Some("accept") | Some("approved") => FrontendResponse::Approval {
900                request_id,
901                decision: FrontendApprovalDecision::Allow,
902            },
903            Some("acceptForSession") => FrontendResponse::Approval {
904                request_id,
905                decision: FrontendApprovalDecision::AllowForSession,
906            },
907            Some("decline") | Some("cancel") | Some("denied") => FrontendResponse::Approval {
908                request_id,
909                decision: FrontendApprovalDecision::Deny,
910            },
911            _ => FrontendResponse::Other {
912                request_id,
913                action: FrontendElicitationAction::Accept,
914                content: Some(result),
915            },
916        };
917        self.adapter.runtime.respond(response).await?;
918        Ok(vec![notification(
919            "serverRequest/resolved",
920            json!({"threadId":self.thread_id,"requestId":request_id}),
921        )])
922    }
923
924    fn cwd(&self) -> String {
925        self.adapter.cwd.to_string_lossy().into_owned()
926    }
927}
928
929fn wire_error_name(error: &AdapterError) -> &'static str {
930    match error {
931        AdapterError::ProtocolVersionMismatch { .. } => "protocol_version_mismatch",
932        AdapterError::UnsupportedMethod(_) => "unsupported_action",
933        AdapterError::InvalidParams { .. } | AdapterError::UnknownThread(_) => "invalid_params",
934        AdapterError::IdentifierCollision(_) => "identifier_collision",
935        AdapterError::Sdk(error) => match error.code() {
936            SdkErrorCode::Unauthenticated => "unauthenticated",
937            SdkErrorCode::Unauthorized => "unauthorized",
938            SdkErrorCode::ControllerRequired => "controller_required",
939            SdkErrorCode::LeaseExpired => "lease_expired",
940            SdkErrorCode::Busy => "busy",
941            SdkErrorCode::UnsupportedAction => "unsupported_action",
942            _ => "sdk_error",
943        },
944    }
945}
946
947fn invalid(method: &str, message: &str) -> AdapterError {
948    AdapterError::InvalidParams {
949        method: method.into(),
950        message: message.into(),
951    }
952}
953
954fn required_str<'a>(params: &'a Value, key: &str, method: &str) -> Result<&'a str, AdapterError> {
955    params
956        .get(key)
957        .and_then(Value::as_str)
958        .ok_or_else(|| invalid(method, &format!("{key} must be a string")))
959}
960
961fn text_from_input(params: &Value, method: &str) -> Result<String, AdapterError> {
962    if let Some(text) = params.get("text").and_then(Value::as_str) {
963        return Ok(text.to_owned());
964    }
965    params
966        .get("input")
967        .and_then(Value::as_array)
968        .and_then(|values| {
969            values
970                .iter()
971                .find_map(|value| value.get("text").and_then(Value::as_str))
972        })
973        .map(str::to_owned)
974        .ok_or_else(|| invalid(method, "text input is required"))
975}
976
977fn parsed_tool_arguments(arguments: Option<&Value>) -> Value {
978    match arguments {
979        Some(Value::String(encoded)) => serde_json::from_str(encoded)
980            .unwrap_or_else(|_| json!({"_supercodeUnparsedArguments":encoded})),
981        Some(arguments) => arguments.clone(),
982        None => json!({}),
983    }
984}
985
986fn codex_plan(plan: Option<&Value>) -> Vec<Value> {
987    plan.and_then(Value::as_array)
988        .into_iter()
989        .flatten()
990        .map(|step| {
991            let status = match step.get("status").and_then(Value::as_str) {
992                Some("in_progress") | Some("inProgress") => "inProgress",
993                Some("completed") => "completed",
994                _ => "pending",
995            };
996            json!({
997                "step":step.get("step").and_then(Value::as_str).unwrap_or_default(),
998                "status":status
999            })
1000        })
1001        .collect()
1002}
1003
1004fn tool_item(
1005    id: &str,
1006    name: &str,
1007    arguments: &Value,
1008    output: Option<&str>,
1009    failed: bool,
1010    raw: &Value,
1011) -> Value {
1012    if matches!(name, "bash" | "shell" | "exec_command") {
1013        let command = arguments
1014            .get("command")
1015            .or_else(|| arguments.get("cmd"))
1016            .and_then(Value::as_str)
1017            .unwrap_or_default();
1018        return json!({
1019            "type":"commandExecution", "id":id, "command":command,
1020            "cwd":arguments.get("cwd").and_then(Value::as_str).unwrap_or_default(),
1021            "processId":null, "source":"agent",
1022            "status":if output.is_none() {"inProgress"} else if failed {"failed"} else {"completed"},
1023            "commandActions":[{"type":"unknown","command":command}],
1024            "aggregatedOutput":output, "exitCode":if output.is_none(){Value::Null}else if failed{Value::from(1)}else{Value::from(0)},
1025            "durationMs":null, "_supercode":{"raw":raw}
1026        });
1027    }
1028    if matches!(name, "write_file" | "edit_file" | "apply_patch") {
1029        let path = arguments
1030            .get("path")
1031            .or_else(|| arguments.get("file_path"))
1032            .and_then(Value::as_str)
1033            .unwrap_or("<opaque>");
1034        let diff = file_tool_diff(name, arguments).unwrap_or_else(|| {
1035            arguments
1036                .get("patch")
1037                .or_else(|| arguments.get("content"))
1038                .or_else(|| arguments.get("new_string"))
1039                .and_then(Value::as_str)
1040                .unwrap_or_else(|| output.unwrap_or_default())
1041                .to_owned()
1042        });
1043        return json!({
1044            "type":"fileChange", "id":id,
1045            "changes":[{"path":path,"kind":{"type":"update"},"diff":diff}],
1046            "status":if output.is_none() {"inProgress"} else if failed {"failed"} else {"completed"},
1047            "_supercode":{"tool":name,"arguments":arguments,"raw":raw}
1048        });
1049    }
1050    json!({
1051        "type":"dynamicToolCall", "id":id, "namespace":null,
1052        "tool":name, "arguments":arguments,
1053        "status":if output.is_none() {"inProgress"} else if failed {"failed"} else {"completed"},
1054        "contentItems":output.map(|text| vec![json!({"type":"inputText","text":text})]),
1055        "success":output.map(|_| !failed), "durationMs":null, "_supercode":{"raw":raw}
1056    })
1057}
1058
1059fn file_tool_diff(name: &str, arguments: &Value) -> Option<String> {
1060    if !matches!(name, "write_file" | "edit_file" | "apply_patch") {
1061        return None;
1062    }
1063    if name == "apply_patch" {
1064        return arguments
1065            .get("patch")
1066            .and_then(Value::as_str)
1067            .map(str::to_owned);
1068    }
1069    let path = arguments
1070        .get("path")
1071        .or_else(|| arguments.get("file_path"))
1072        .and_then(Value::as_str)?;
1073    let old = arguments
1074        .get("old_string")
1075        .and_then(Value::as_str)
1076        .unwrap_or_default();
1077    let new = arguments
1078        .get("new_string")
1079        .or_else(|| arguments.get("content"))
1080        .and_then(Value::as_str)?;
1081    let removed = old
1082        .lines()
1083        .map(|line| format!("-{line}"))
1084        .collect::<Vec<_>>()
1085        .join("\n");
1086    let added = new
1087        .lines()
1088        .map(|line| format!("+{line}"))
1089        .collect::<Vec<_>>()
1090        .join("\n");
1091    let old_count = old.lines().count();
1092    let new_count = new.lines().count();
1093    let old_start = usize::from(old_count > 0);
1094    let new_start = usize::from(new_count > 0);
1095    Some(format!(
1096        "--- a/{path}\n+++ b/{path}\n@@ -{old_start},{old_count} +{new_start},{new_count} @@\n{removed}{}{}\n",
1097        if removed.is_empty() || added.is_empty() {
1098            ""
1099        } else {
1100            "\n"
1101        },
1102        added
1103    ))
1104}
1105
1106fn deterministic_uuid(domain: &str, canonical: &str) -> String {
1107    let mut hasher = blake3::Hasher::new();
1108    hasher.update(PROTOCOL_NAMESPACE.as_bytes());
1109    hasher.update(&[0]);
1110    hasher.update(domain.as_bytes());
1111    hasher.update(&[0]);
1112    hasher.update(canonical.as_bytes());
1113    let mut bytes = [0u8; 16];
1114    bytes.copy_from_slice(&hasher.finalize().as_bytes()[..16]);
1115    bytes[6] = (bytes[6] & 0x0f) | 0x80;
1116    bytes[8] = (bytes[8] & 0x3f) | 0x80;
1117    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])
1118}
1119
1120fn item_id(sequence: u64, family: &str) -> String {
1121    format!("sc_{family}_{sequence:016x}")
1122}
1123
1124fn epoch_ms() -> u64 {
1125    SystemTime::now()
1126        .duration_since(UNIX_EPOCH)
1127        .unwrap_or_default()
1128        .as_millis()
1129        .min(u64::MAX as u128) as u64
1130}
1131
1132fn notification(method: &str, params: Value) -> Value {
1133    json!({"method":method,"params":params})
1134}
1135
1136fn item_notification(method: &str, thread: &str, turn: &str, item: Value, time_key: &str) -> Value {
1137    let mut params = Map::new();
1138    params.insert("item".into(), item);
1139    params.insert("threadId".into(), Value::String(thread.into()));
1140    params.insert("turnId".into(), Value::String(turn.into()));
1141    params.insert(time_key.into(), Value::from(epoch_ms()));
1142    notification(method, Value::Object(params))
1143}
1144
1145fn turn_json(id: &str, status: &str, error: Option<Value>) -> Value {
1146    json!({"id":id,"items":[],"itemsView":"notLoaded","status":status,"error":error,"startedAt":null,"completedAt":null,"durationMs":null})
1147}
1148
1149fn thread_json(attachment: &FrontendAttachment, thread_id: &str, cwd: &std::path::Path) -> Value {
1150    let items = history_items(&attachment.history);
1151    let preview = attachment
1152        .history
1153        .iter()
1154        .find(|m| m.role == Role::User)
1155        .and_then(|m| m.content.clone())
1156        .unwrap_or_default();
1157    json!({
1158        "id":thread_id,"extra":null,"sessionId":thread_id,"forkedFromId":null,"parentThreadId":null,
1159        "preview":preview,"ephemeral":false,"historyMode":"legacy","modelProvider":"supercode",
1160        "createdAt":0,"updatedAt":0,"recencyAt":0,
1161        "status":{"type":if attachment.descriptor.turn_state == supercode::FrontendTurnState::Busy {"active"} else {"idle"}},
1162        "path":null,"cwd":cwd,"cliVersion":CODEX_CLI_VERSION,"source":"appServer","threadSource":"user",
1163        "agentNickname":null,"agentRole":null,"gitInfo":null,"name":null,
1164        "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})] }
1165    })
1166}
1167
1168fn thread_response(
1169    attachment: &FrontendAttachment,
1170    thread_id: &str,
1171    cwd: &std::path::Path,
1172    read_only: bool,
1173) -> Value {
1174    let thread = thread_json(attachment, thread_id, cwd);
1175    if read_only {
1176        json!({"thread":thread})
1177    } else {
1178        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"})
1179    }
1180}
1181
1182fn history_items(history: &[ChatMessage]) -> Vec<Value> {
1183    let mut out = Vec::new();
1184    for (index, message) in history.iter().enumerate() {
1185        let id = format!("sc_history_{index:016x}");
1186        let text = message.content.clone().unwrap_or_else(|| {
1187            message
1188                .content_parts
1189                .as_ref()
1190                .map(|parts| Value::Array(parts.clone()).to_string())
1191                .unwrap_or_default()
1192        });
1193        match message.role {
1194            Role::System => out.push(json!({"type":"reasoning","id":id,"summary":[text],"content":[],"_supercode":{"role":"system","metadata":message.metadata}})),
1195            Role::User => out.push(json!({"type":"userMessage","id":id,"clientId":null,"content":[{"type":"text","text":text,"text_elements":[]}],"_supercode":{"metadata":message.metadata}})),
1196            Role::Assistant => {
1197                if !text.is_empty() { out.push(json!({"type":"agentMessage","id":id,"text":text,"phase":null,"memoryCitation":null,"_supercode":{"metadata":message.metadata}})); }
1198                for call in message.tool_calls() {
1199                    let arguments = call.function.parsed_arguments().unwrap_or_else(|_| {
1200                        json!({"_supercodeUnparsedArguments": call.function.arguments})
1201                    });
1202                    out.push(json!({"type":"dynamicToolCall","id":call.id,"namespace":null,
1203                        "tool":call.function.name,"arguments":arguments,"status":"completed",
1204                        "contentItems":null,"success":null,"durationMs":null,
1205                        "_supercode":{"metadata":message.metadata}}));
1206                }
1207            }
1208            Role::Tool => out.push(json!({"type":"dynamicToolCall",
1209                "id":message.tool_call_id.clone().unwrap_or(id),"namespace":null,
1210                "tool":message.name.clone().unwrap_or_else(|| "tool_result".into()),
1211                "arguments":{},"status":"completed",
1212                "contentItems":[{"type":"inputText","text":text}],"success":true,
1213                "durationMs":null,"_supercode":{"metadata":message.metadata}})),
1214        }
1215    }
1216    out
1217}
1218
1219fn value_key(value: &Value) -> Result<ValueKey, AdapterError> {
1220    if let Some(number) = value.as_u64() {
1221        return Ok(ValueKey::Number(number));
1222    }
1223    if let Some(string) = value.as_str() {
1224        return Ok(ValueKey::String(string.into()));
1225    }
1226    Err(invalid(
1227        "serverResponse",
1228        "id must be an unsigned integer or string",
1229    ))
1230}
1231
1232#[cfg(test)]
1233mod tests {
1234    use super::*;
1235    use async_trait::async_trait;
1236    use std::collections::VecDeque;
1237    use std::sync::Mutex;
1238    use tokio::sync::broadcast;
1239
1240    struct RecordingRuntime {
1241        operations: Mutex<Vec<String>>,
1242        events: broadcast::Sender<FrontendEvent>,
1243    }
1244
1245    impl RecordingRuntime {
1246        fn new() -> Arc<Self> {
1247            let (events, _) = broadcast::channel(32);
1248            Arc::new(Self {
1249                operations: Mutex::new(Vec::new()),
1250                events,
1251            })
1252        }
1253
1254        fn descriptor() -> FrontendRuntimeDescriptor {
1255            FrontendRuntimeDescriptor {
1256                schema_version: 2,
1257                session_id: "canonical-session".into(),
1258                source_harness: Some("claude-code".into()),
1259                emulation_profile: Some("claude-code".into()),
1260                active_modules: vec!["reduction".into()],
1261                commands: Vec::new(),
1262                operations: Vec::new(),
1263                actions: supercode::FrontendActions {
1264                    submit: true,
1265                    interrupt: true,
1266                    steer: true,
1267                    respond: true,
1268                    detach: true,
1269                    close: false,
1270                },
1271                display: supercode::FrontendDisplayCapabilities {
1272                    event_kinds: vec!["text_delta".into()],
1273                    opaque_fallback: true,
1274                },
1275                model: "openrouter/z-ai/glm-5.2".into(),
1276                turn_state: supercode::FrontendTurnState::Idle,
1277                connection_state: supercode::FrontendConnectionState::Connected,
1278                extensions: BTreeMap::new(),
1279            }
1280        }
1281
1282        fn record(&self, value: impl Into<String>) {
1283            self.operations
1284                .lock()
1285                .unwrap_or_else(std::sync::PoisonError::into_inner)
1286                .push(value.into());
1287        }
1288    }
1289
1290    #[async_trait]
1291    impl SdkRuntime for RecordingRuntime {
1292        async fn describe(&self) -> Result<FrontendRuntimeDescriptor, SdkError> {
1293            self.record("describe");
1294            Ok(Self::descriptor())
1295        }
1296
1297        async fn attach(&self, _history_limit: usize) -> Result<FrontendAttachment, SdkError> {
1298            self.record("attach");
1299            Ok(FrontendAttachment::from_snapshot(
1300                supercode::FrontendAttachSnapshot {
1301                    descriptor: Self::descriptor(),
1302                    history: vec![
1303                        ChatMessage::user("fact before continuation"),
1304                        ChatMessage::assistant("remembered"),
1305                    ],
1306                    history_cursor: 2,
1307                    replay: VecDeque::new(),
1308                },
1309                self.events.subscribe(),
1310            ))
1311        }
1312
1313        async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), SdkError> {
1314            self.record(format!("input:{prompt}"));
1315            Ok(())
1316        }
1317
1318        async fn submit(&self, _prompt: String) -> Result<String, SdkError> {
1319            unreachable!("adapter must use atomic send_input")
1320        }
1321
1322        async fn interrupt(&self) -> Result<bool, SdkError> {
1323            self.record("interrupt");
1324            Ok(true)
1325        }
1326
1327        async fn steer(&self, prompt: String) -> Result<(), SdkError> {
1328            self.record(format!("steer:{prompt}"));
1329            Ok(())
1330        }
1331
1332        async fn respond(&self, response: FrontendResponse) -> Result<(), SdkError> {
1333            self.record(format!("respond:{response:?}"));
1334            Ok(())
1335        }
1336
1337        async fn detach(&self) -> Result<supercode::RuntimeLeaseSnapshot, SdkError> {
1338            self.record("detach");
1339            Ok(supercode::RuntimeLeaseSnapshot {
1340                controller: None,
1341                observers: Vec::new(),
1342                lease_ttl_ms: supercode::DEFAULT_RUNTIME_LEASE_TTL_MS,
1343            })
1344        }
1345    }
1346
1347    #[test]
1348    fn deterministic_ids_are_uuid_shaped_stable_and_domain_separated() {
1349        let a = deterministic_uuid("thread", "canonical-session");
1350        assert_eq!(a, deterministic_uuid("thread", "canonical-session"));
1351        assert_ne!(a, deterministic_uuid("turn", "canonical-session"));
1352        assert_eq!(a.len(), 36);
1353        assert_eq!(&a[14..15], "8");
1354        assert!(matches!(&a[19..20], "8" | "9" | "a" | "b"));
1355    }
1356
1357    #[test]
1358    fn default_surface_is_exactly_the_sanitized_trace() {
1359        let actual = TRACED_METHODS.iter().copied().collect::<BTreeSet<_>>();
1360        assert_eq!(actual.len(), 13);
1361        assert!(!actual.contains("turn/steer"));
1362        assert!(!actual.contains("thread/fork"));
1363    }
1364
1365    #[test]
1366    fn deterministic_thread_mapping_has_no_collisions_across_large_sample() {
1367        let mut ids = BTreeSet::new();
1368        for index in 0..100_000u64 {
1369            assert!(ids.insert(deterministic_uuid("thread", &format!("canonical-{index}"))));
1370        }
1371    }
1372
1373    #[tokio::test]
1374    async fn traced_protocol_attaches_existing_runtime_and_submits_once() {
1375        let runtime = RecordingRuntime::new();
1376        let adapter = CodexAppServerAdapter::new(runtime.clone(), "/workspace");
1377        let mut connection = adapter.connection();
1378
1379        let initialized = connection
1380            .handle(json!({"id":"initialize","method":"initialize","params":{"clientInfo":{"version":"0.144.4"}}}))
1381            .await;
1382        assert_eq!(initialized[0]["id"], "initialize");
1383        assert_eq!(initialized[1]["method"], "remoteControl/status/changed");
1384        assert!(connection
1385            .handle(json!({"method":"initialized"}))
1386            .await
1387            .is_empty());
1388
1389        let started = connection
1390            .handle(json!({"id":1,"method":"thread/start","params":{}}))
1391            .await;
1392        let thread_id = started[0]["result"]["thread"]["id"]
1393            .as_str()
1394            .unwrap()
1395            .to_owned();
1396        assert_eq!(started[1]["method"], "thread/started");
1397        assert_eq!(
1398            started[0]["result"]["thread"]["turns"][0]["items"][0]["content"][0]["text"],
1399            "fact before continuation"
1400        );
1401
1402        let turn = connection
1403            .handle(json!({"id":2,"method":"turn/start","params":{"threadId":thread_id,"input":[{"type":"text","text":"continue through GLM"}]}}))
1404            .await;
1405        assert_eq!(turn[0]["result"]["turn"]["status"], "inProgress");
1406        let operations = runtime
1407            .operations
1408            .lock()
1409            .unwrap_or_else(std::sync::PoisonError::into_inner)
1410            .clone();
1411        assert_eq!(
1412            operations
1413                .iter()
1414                .filter(|operation| operation.starts_with("input:"))
1415                .count(),
1416            1
1417        );
1418        assert!(operations.contains(&"input:continue through GLM".into()));
1419    }
1420
1421    #[tokio::test]
1422    async fn reconnect_keeps_thread_identity_but_never_reuses_turn_identity() {
1423        let adapter = CodexAppServerAdapter::new(RecordingRuntime::new(), "/workspace");
1424        let mut turn_ids = Vec::new();
1425        let mut thread_ids = Vec::new();
1426        for index in 0..2 {
1427            let mut connection = adapter.connection();
1428            connection
1429                .handle(
1430                    json!({"id":1,"method":"initialize","params":{"clientInfo":{"version":"0.144.4"}}}),
1431                )
1432                .await;
1433            connection.handle(json!({"method":"initialized"})).await;
1434            let started = connection
1435                .handle(json!({"id":2,"method":"thread/start","params":{}}))
1436                .await;
1437            let thread = started[0]["result"]["thread"]["id"]
1438                .as_str()
1439                .unwrap()
1440                .to_owned();
1441            let turn = connection
1442                .handle(json!({"id":3,"method":"turn/start","params":{"threadId":thread,"input":[{"type":"text","text":format!("turn {index}")}]}}))
1443                .await;
1444            thread_ids.push(thread);
1445            turn_ids.push(turn[0]["result"]["turn"]["id"].clone());
1446        }
1447        assert_eq!(thread_ids[0], thread_ids[1]);
1448        assert_ne!(turn_ids[0], turn_ids[1]);
1449    }
1450
1451    #[tokio::test]
1452    async fn version_mismatch_and_untraced_controls_fail_by_name() {
1453        let runtime = RecordingRuntime::new();
1454        let adapter = CodexAppServerAdapter::new(runtime, "/workspace");
1455        let mut connection = adapter.connection();
1456        let mismatch = connection
1457            .handle(
1458                json!({"id":1,"method":"initialize","params":{"clientInfo":{"version":"0.145.0"}}}),
1459            )
1460            .await;
1461        assert_eq!(
1462            mismatch[0]["error"]["data"]["name"],
1463            "protocol_version_mismatch"
1464        );
1465        let steer = connection
1466            .handle(json!({"id":2,"method":"turn/steer","params":{"text":"more"}}))
1467            .await;
1468        assert_eq!(steer[0]["error"]["code"], -32020);
1469        assert_eq!(steer[0]["error"]["data"]["name"], "unsupported_action");
1470    }
1471
1472    #[tokio::test]
1473    async fn explicit_schema_extended_mode_routes_steer_and_interrupt_to_the_sdk() {
1474        let runtime = RecordingRuntime::new();
1475        let adapter = CodexAppServerAdapter::with_mode(
1476            runtime.clone(),
1477            "/workspace",
1478            CodexCompatibilityMode::SchemaExtended,
1479        );
1480        let mut connection = adapter.connection();
1481        connection
1482            .handle(
1483                json!({"id":1,"method":"initialize","params":{"clientInfo":{"version":"0.144.4"}}}),
1484            )
1485            .await;
1486        connection.handle(json!({"method":"initialized"})).await;
1487        let started = connection
1488            .handle(json!({"id":2,"method":"thread/start","params":{}}))
1489            .await;
1490        let thread = started[0]["result"]["thread"]["id"]
1491            .as_str()
1492            .unwrap()
1493            .to_owned();
1494        connection
1495            .handle(json!({"id":3,"method":"turn/start","params":{"threadId":thread,"input":[{"type":"text","text":"begin"}]}}))
1496            .await;
1497        let turn = connection.active_turn.clone().unwrap();
1498        let steered = connection
1499            .handle(json!({"id":4,"method":"turn/steer","params":{"threadId":thread,"expectedTurnId":turn,"input":[{"type":"text","text":"steer now"}]}}))
1500            .await;
1501        assert_eq!(steered[0]["result"]["turnId"], turn);
1502        let interrupted = connection
1503            .handle(json!({"id":5,"method":"turn/interrupt","params":{"threadId":thread,"turnId":turn}}))
1504            .await;
1505        assert_eq!(interrupted[0]["result"], json!({}));
1506
1507        let operations = runtime
1508            .operations
1509            .lock()
1510            .unwrap_or_else(std::sync::PoisonError::into_inner)
1511            .clone();
1512        assert!(operations.contains(&"input:begin".into()));
1513        assert!(operations.contains(&"steer:steer now".into()));
1514        assert!(operations.contains(&"interrupt".into()));
1515    }
1516
1517    #[tokio::test]
1518    async fn thread_identity_is_validated_before_attach_and_persistence_controls_stay_read_only() {
1519        let runtime = RecordingRuntime::new();
1520        let adapter = CodexAppServerAdapter::with_mode(
1521            runtime.clone(),
1522            "/workspace",
1523            CodexCompatibilityMode::SchemaExtended,
1524        );
1525        let mut connection = adapter.connection();
1526        connection
1527            .handle(
1528                json!({"id":1,"method":"initialize","params":{"clientInfo":{"version":"0.144.4"}}}),
1529            )
1530            .await;
1531        connection.handle(json!({"method":"initialized"})).await;
1532
1533        let wrong = connection
1534            .handle(json!({"id":2,"method":"thread/resume","params":{"threadId":"00000000-0000-8000-8000-000000000000"}}))
1535            .await;
1536        assert_eq!(wrong[0]["error"]["data"]["name"], "invalid_params");
1537        let operations = runtime
1538            .operations
1539            .lock()
1540            .unwrap_or_else(std::sync::PoisonError::into_inner)
1541            .clone();
1542        assert_eq!(operations, ["describe"]);
1543
1544        let expected = deterministic_uuid("thread", "canonical-session");
1545        let resumed = connection
1546            .handle(json!({"id":3,"method":"thread/resume","params":{"threadId":expected}}))
1547            .await;
1548        assert_eq!(resumed[0]["result"]["thread"]["id"], expected);
1549        let listed = connection
1550            .handle(json!({"id":4,"method":"thread/list","params":{}}))
1551            .await;
1552        assert_eq!(listed[0]["result"]["data"].as_array().unwrap().len(), 1);
1553        assert_eq!(listed[0]["result"]["data"][0]["id"], expected);
1554
1555        let before_read_only_controls = runtime
1556            .operations
1557            .lock()
1558            .unwrap_or_else(std::sync::PoisonError::into_inner)
1559            .clone();
1560        for (id, method) in [(5, "thread/fork"), (6, "thread/archive")] {
1561            let response = connection
1562                .handle(json!({"id":id,"method":method,"params":{"threadId":expected}}))
1563                .await;
1564            assert_eq!(
1565                response[0]["error"]["data"]["name"], "unsupported_action",
1566                "{method}"
1567            );
1568        }
1569        assert_eq!(
1570            runtime
1571                .operations
1572                .lock()
1573                .unwrap_or_else(std::sync::PoisonError::into_inner)
1574                .as_slice(),
1575            before_read_only_controls.as_slice()
1576        );
1577    }
1578
1579    #[tokio::test]
1580    async fn initialization_order_rejects_early_and_repeated_messages() {
1581        let adapter = CodexAppServerAdapter::new(RecordingRuntime::new(), "/workspace");
1582        let mut connection = adapter.connection();
1583        let early = connection
1584            .handle(json!({"id":1,"method":"thread/start","params":{}}))
1585            .await;
1586        assert_eq!(early[0]["error"]["data"]["name"], "invalid_params");
1587
1588        let initialized_early = connection.handle(json!({"method":"initialized"})).await;
1589        assert_eq!(
1590            initialized_early[0]["params"]["error"]["additionalDetails"]["name"],
1591            "invalid_params"
1592        );
1593        connection
1594            .handle(
1595                json!({"id":2,"method":"initialize","params":{"clientInfo":{"version":"0.144.4"}}}),
1596            )
1597            .await;
1598        let before_notification = connection
1599            .handle(json!({"id":3,"method":"thread/start","params":{}}))
1600            .await;
1601        assert_eq!(
1602            before_notification[0]["error"]["data"]["name"],
1603            "invalid_params"
1604        );
1605        assert!(connection
1606            .handle(json!({"method":"initialized"}))
1607            .await
1608            .is_empty());
1609        let repeated = connection
1610            .handle(
1611                json!({"id":4,"method":"initialize","params":{"clientInfo":{"version":"0.144.4"}}}),
1612            )
1613            .await;
1614        assert_eq!(repeated[0]["error"]["data"]["name"], "invalid_params");
1615    }
1616
1617    #[test]
1618    fn live_text_events_have_one_stable_turn_and_complete_with_exact_text() {
1619        let adapter = CodexAppServerAdapter::new(RecordingRuntime::new(), "/workspace");
1620        let mut connection = adapter.connection();
1621        connection.thread_id = Some(deterministic_uuid("thread", "canonical-session"));
1622
1623        let started = connection
1624            .event_notifications(FrontendEvent {
1625                sequence: 10,
1626                kind: "turn_started".into(),
1627                payload: json!({"type":"turn_started"}),
1628            })
1629            .unwrap();
1630        let turn = started[1]["params"]["turn"]["id"]
1631            .as_str()
1632            .unwrap()
1633            .to_owned();
1634        let first = connection
1635            .event_notifications(FrontendEvent {
1636                sequence: 11,
1637                kind: "text_delta".into(),
1638                payload: json!({"type":"text_delta","text":"hello "}),
1639            })
1640            .unwrap();
1641        assert_eq!(
1642            first
1643                .iter()
1644                .map(|value| value["method"].as_str().unwrap())
1645                .collect::<Vec<_>>(),
1646            vec!["item/started", "item/agentMessage/delta"]
1647        );
1648        assert_eq!(first[0]["params"]["turnId"], turn);
1649        let item = first[0]["params"]["item"]["id"].clone();
1650        let second = connection
1651            .event_notifications(FrontendEvent {
1652                sequence: 12,
1653                kind: "text_delta".into(),
1654                payload: json!({"type":"text_delta","text":"world"}),
1655            })
1656            .unwrap();
1657        assert_eq!(second.len(), 1);
1658        assert_eq!(second[0]["params"]["turnId"], turn);
1659        assert_eq!(second[0]["params"]["itemId"], item);
1660
1661        assert!(connection
1662            .event_notifications(FrontendEvent {
1663                sequence: 13,
1664                kind: "turn_completed".into(),
1665                payload: json!({"type":"turn_completed"}),
1666            })
1667            .unwrap()
1668            .is_empty());
1669        let completed = connection
1670            .event_notifications(FrontendEvent {
1671                sequence: 14,
1672                kind: "turn_succeeded".into(),
1673                payload: json!({"type":"turn_succeeded"}),
1674            })
1675            .unwrap();
1676        assert_eq!(completed[0]["method"], "item/completed");
1677        assert_eq!(completed[0]["params"]["turnId"], turn);
1678        assert_eq!(completed[0]["params"]["item"]["id"], item);
1679        assert_eq!(completed[0]["params"]["item"]["text"], "hello world");
1680        assert_eq!(completed[2]["method"], "turn/completed");
1681    }
1682
1683    #[test]
1684    fn approval_request_decodes_the_sdk_broker_payload_for_stock_codex() {
1685        let adapter = CodexAppServerAdapter::new(RecordingRuntime::new(), "/workspace");
1686        let mut connection = adapter.connection();
1687        connection.thread_id = Some(deterministic_uuid("thread", "canonical-session"));
1688        connection.active_turn = Some(deterministic_uuid("turn", "canonical-turn"));
1689
1690        let messages = connection
1691            .event_notifications(FrontendEvent {
1692                sequence: 15,
1693                kind: "request".into(),
1694                payload: json!({
1695                    "type": "request",
1696                    "request": {
1697                        "id": 77,
1698                        "kind": "approval",
1699                        "payload": {
1700                            "tool": "bash",
1701                            "subject": "printf stock-approval",
1702                            "raw_args": {
1703                                "command": "printf stock-approval",
1704                                "cwd": "/workspace/project"
1705                            }
1706                        }
1707                    }
1708                }),
1709            })
1710            .unwrap();
1711
1712        assert_eq!(messages.len(), 1);
1713        assert_eq!(
1714            messages[0]["method"],
1715            "item/commandExecution/requestApproval"
1716        );
1717        assert_eq!(messages[0]["params"]["command"], "printf stock-approval");
1718        assert_eq!(messages[0]["params"]["cwd"], "/workspace/project");
1719        assert_eq!(
1720            messages[0]["params"]["_supercode"]["raw"]["raw_args"]["command"],
1721            "printf stock-approval"
1722        );
1723    }
1724
1725    #[test]
1726    fn native_command_file_reasoning_plan_and_diff_shapes_preserve_raw_events() {
1727        let adapter = CodexAppServerAdapter::new(RecordingRuntime::new(), "/workspace");
1728        let mut connection = adapter.connection();
1729        connection.thread_id = Some(deterministic_uuid("thread", "canonical-session"));
1730
1731        let command = connection
1732            .event_notifications(FrontendEvent {
1733                sequence: 20,
1734                kind: "tool_call_started".into(),
1735                payload: json!({"type":"tool_call_started","id":"cmd","name":"bash","arguments":"{\"command\":\"cargo test\",\"cwd\":\"/workspace\"}"}),
1736            })
1737            .unwrap();
1738        assert_eq!(command[0]["params"]["item"]["type"], "commandExecution");
1739        assert_eq!(command[0]["params"]["item"]["command"], "cargo test");
1740        let command_done = connection
1741            .event_notifications(FrontendEvent {
1742                sequence: 21,
1743                kind: "tool_call_completed".into(),
1744                payload: json!({"type":"tool_call_completed","id":"cmd","name":"bash","output":"ok","is_error":false}),
1745            })
1746            .unwrap();
1747        assert_eq!(command_done[0]["params"]["item"]["aggregatedOutput"], "ok");
1748
1749        let file = connection
1750            .event_notifications(FrontendEvent {
1751                sequence: 22,
1752                kind: "tool_call_started".into(),
1753                payload: json!({"type":"tool_call_started","id":"file","name":"write_file","arguments":{"path":"proof.txt","content":"proof"}}),
1754            })
1755            .unwrap();
1756        assert_eq!(file[0]["params"]["item"]["type"], "fileChange");
1757        assert_eq!(file[0]["params"]["item"]["changes"][0]["path"], "proof.txt");
1758        assert!(file[0]["params"]["item"]["changes"][0]["diff"]
1759            .as_str()
1760            .unwrap()
1761            .contains("@@ -0,0 +1,1 @@\n+proof"));
1762        assert_eq!(
1763            file[0]["params"]["item"]["_supercode"]["tool"],
1764            "write_file"
1765        );
1766        let file_done = connection
1767            .event_notifications(FrontendEvent {
1768                sequence: 23,
1769                kind: "tool_call_completed".into(),
1770                payload: json!({"type":"tool_call_completed","id":"file","name":"write_file","output":"wrote proof.txt","is_error":false}),
1771            })
1772            .unwrap();
1773        assert_eq!(file_done[1]["method"], "turn/diff/updated");
1774        assert!(file_done[1]["params"]["diff"]
1775            .as_str()
1776            .unwrap()
1777            .contains("+proof"));
1778
1779        let reasoning = connection
1780            .event_notifications(FrontendEvent {
1781                sequence: 24,
1782                kind: "reasoning_delta".into(),
1783                payload: json!({"type":"reasoning_delta","text":"inspect","future":true}),
1784            })
1785            .unwrap();
1786        assert_eq!(reasoning[0]["method"], "item/started");
1787        assert_eq!(reasoning[2]["params"]["_supercode"]["raw"]["future"], true);
1788        let reasoning_done = connection
1789            .event_notifications(FrontendEvent {
1790                sequence: 25,
1791                kind: "reasoning_completed".into(),
1792                payload: json!({"type":"reasoning_completed"}),
1793            })
1794            .unwrap();
1795        assert_eq!(reasoning_done[0]["params"]["item"]["summary"][0], "inspect");
1796
1797        let plan = connection
1798            .event_notifications(FrontendEvent {
1799                sequence: 26,
1800                kind: "plan_update".into(),
1801                payload: json!({"type":"plan_update","plan":[{"step":"test","status":"in_progress"}]}),
1802            })
1803            .unwrap();
1804        assert_eq!(plan[0]["method"], "turn/plan/updated");
1805        let diff = connection
1806            .event_notifications(FrontendEvent {
1807                sequence: 27,
1808                kind: "diff_updated".into(),
1809                payload: json!({"type":"diff_updated","diff":"@@ proof @@"}),
1810            })
1811            .unwrap();
1812        assert_eq!(diff[0]["method"], "turn/diff/updated");
1813        assert_eq!(diff[0]["params"]["diff"], "@@ proof @@");
1814    }
1815
1816    #[tokio::test]
1817    async fn replays_every_request_from_the_pinned_stock_client_corpus() {
1818        const CORPUS: &str = include_str!(
1819            "../../../scripts/client-protocol-corpus/fixtures/codex_app_server_v0_144.jsonl"
1820        );
1821
1822        fn normalize(value: &mut Value, thread_id: Option<&str>) {
1823            match value {
1824                Value::String(text) if text == "<TMP>/project" => {
1825                    *text = "/workspace".into();
1826                }
1827                Value::Object(object) => {
1828                    if object.contains_key("threadId") {
1829                        if let Some(thread_id) = thread_id {
1830                            object.insert("threadId".into(), Value::String(thread_id.into()));
1831                        }
1832                    }
1833                    for child in object.values_mut() {
1834                        normalize(child, thread_id);
1835                    }
1836                }
1837                Value::Array(array) => {
1838                    for child in array {
1839                        normalize(child, thread_id);
1840                    }
1841                }
1842                _ => {}
1843            }
1844        }
1845
1846        let runtime = RecordingRuntime::new();
1847        let adapter = CodexAppServerAdapter::new(runtime, "/workspace");
1848        let mut connection = adapter.connection();
1849        let mut corpus_connection = 1u64;
1850        let mut thread_id = None::<String>;
1851        let mut replayed = 0usize;
1852
1853        for line in CORPUS.lines() {
1854            let envelope: Value = serde_json::from_str(line).unwrap();
1855            if envelope["direction"] != "client_to_server"
1856                || envelope["transport"] != "websocket"
1857                || !envelope["message"]["method"].is_string()
1858            {
1859                continue;
1860            }
1861            let next_connection = envelope["connection"].as_u64().unwrap();
1862            if next_connection != corpus_connection {
1863                connection = adapter.connection();
1864                corpus_connection = next_connection;
1865            }
1866            let mut request = envelope["message"].clone();
1867            normalize(&mut request, thread_id.as_deref());
1868            let method = request["method"].as_str().unwrap().to_owned();
1869            let request_id = request.get("id").cloned();
1870            let output = connection.handle(request).await;
1871            if let Some(request_id) = request_id {
1872                assert!(!output.is_empty(), "{method} returned no response");
1873                assert_eq!(output[0]["id"], request_id, "{method} changed request id");
1874                assert!(
1875                    output[0].get("error").is_none(),
1876                    "{method} failed corpus replay: {}",
1877                    output[0]
1878                );
1879            }
1880            if method == "thread/start" {
1881                thread_id = Some(output[0]["result"]["thread"]["id"].as_str().unwrap().into());
1882            }
1883            replayed += 1;
1884        }
1885
1886        assert_eq!(replayed, 24, "pinned corpus request count drifted");
1887    }
1888
1889    #[tokio::test]
1890    async fn adapter_preserves_the_pinned_stock_notification_order() {
1891        const CORPUS: &str = include_str!(
1892            "../../../scripts/client-protocol-corpus/fixtures/codex_app_server_v0_144.jsonl"
1893        );
1894        let expected = CORPUS
1895            .lines()
1896            .filter_map(|line| serde_json::from_str::<Value>(line).ok())
1897            .filter(|envelope| envelope["direction"] == "server_to_client")
1898            .filter_map(|envelope| envelope["message"]["method"].as_str().map(str::to_owned))
1899            .collect::<BTreeSet<_>>();
1900
1901        let adapter = CodexAppServerAdapter::new(RecordingRuntime::new(), "/workspace");
1902        let mut connection = adapter.connection();
1903        let mut output = connection
1904            .handle(
1905                json!({"id":1,"method":"initialize","params":{"clientInfo":{"version":"0.144.4"}}}),
1906            )
1907            .await;
1908        connection.handle(json!({"method":"initialized"})).await;
1909        output.extend(
1910            connection
1911                .handle(json!({"id":2,"method":"thread/start","params":{}}))
1912                .await,
1913        );
1914        let events = [
1915            (3, "turn_started", json!({"type":"turn_started"})),
1916            (
1917                4,
1918                "user_message",
1919                json!({"type":"user_message","text":"prompt"}),
1920            ),
1921            (
1922                5,
1923                "reasoning_delta",
1924                json!({"type":"reasoning_delta","text":"reason"}),
1925            ),
1926            (
1927                6,
1928                "reasoning_completed",
1929                json!({"type":"reasoning_completed"}),
1930            ),
1931            (
1932                7,
1933                "text_delta",
1934                json!({"type":"text_delta","text":"answer"}),
1935            ),
1936            (
1937                8,
1938                "diff_updated",
1939                json!({"type":"diff_updated","diff":"@@"}),
1940            ),
1941            (
1942                9,
1943                "usage",
1944                json!({"type":"usage","prompt_tokens":1,"completion_tokens":1,"total_tokens":2}),
1945            ),
1946            (
1947                10,
1948                "cache_warning",
1949                json!({"type":"cache_warning","message":"warning"}),
1950            ),
1951            (
1952                11,
1953                "request",
1954                json!({"type":"request","request":{"id":77,"kind":"approval","payload":{"command":"true"}}}),
1955            ),
1956            (
1957                12,
1958                "request_resolved",
1959                json!({"type":"request_resolved","request_id":77}),
1960            ),
1961            (13, "turn_completed", json!({"type":"turn_completed"})),
1962            (
1963                14,
1964                "turn_failed",
1965                json!({"type":"turn_failed","message":"failure"}),
1966            ),
1967        ];
1968        for (sequence, kind, payload) in events {
1969            output.extend(
1970                connection
1971                    .event_notifications(FrontendEvent {
1972                        sequence,
1973                        kind: kind.into(),
1974                        payload,
1975                    })
1976                    .unwrap(),
1977            );
1978        }
1979        let actual = output
1980            .iter()
1981            .filter_map(|message| message["method"].as_str().map(str::to_owned))
1982            .collect::<Vec<_>>();
1983        let actual_families = actual.iter().cloned().collect::<BTreeSet<_>>();
1984        let missing = expected
1985            .difference(&actual_families)
1986            .cloned()
1987            .collect::<Vec<_>>();
1988        assert!(
1989            missing.is_empty(),
1990            "missing notification families: {missing:?}"
1991        );
1992        assert_eq!(
1993            actual,
1994            [
1995                "remoteControl/status/changed",
1996                "thread/started",
1997                "thread/settings/updated",
1998                "account/rateLimits/updated",
1999                "thread/goal/cleared",
2000                "thread/status/changed",
2001                "turn/started",
2002                "item/started",
2003                "item/completed",
2004                "item/started",
2005                "item/reasoning/summaryPartAdded",
2006                "item/reasoning/summaryTextDelta",
2007                "item/completed",
2008                "item/started",
2009                "item/agentMessage/delta",
2010                "turn/diff/updated",
2011                "thread/tokenUsage/updated",
2012                "warning",
2013                "item/commandExecution/requestApproval",
2014                "serverRequest/resolved",
2015                "error",
2016                "item/completed",
2017                "thread/status/changed",
2018                "turn/completed",
2019            ],
2020            "semantic notification order drifted from the pinned stock-client cadence"
2021        );
2022    }
2023}