Skip to main content

supercode_harness/
harness_service.rs

1//! Versioned, language-neutral service over persisted harness sessions.
2//!
3//! The service is transport-agnostic: [`HarnessSessionService::handle`] accepts
4//! one JSON-RPC value and [`HarnessSessionService::poll`] produces subscription
5//! notifications. The CLI exposes those primitives as NDJSON over stdio.
6
7use std::collections::{BTreeMap, BTreeSet};
8use std::path::{Path, PathBuf};
9use std::time::Duration;
10
11use serde::{Deserialize, Serialize};
12use serde_json::{json, Value};
13
14use crate::runtime::generated_session_id;
15#[cfg(feature = "adapter-api")]
16use crate::runtime::{HostedHarnessConnection, HostedHarnessRuntime};
17use crate::sdk::{
18    discover_session_page, load_session, load_session_with_fidelity, SdkCapabilities, SdkError,
19    SdkErrorCode, SdkEvent, SdkOperation, SdkRequest, SdkRuntimeEvent, SdkService,
20};
21use crate::watch::{bound_session_view, message_json, normalized_session_json};
22use crate::Fidelity;
23#[cfg(feature = "adapter-api")]
24use crate::SupercodeHttpRuntimeBackend;
25use crate::{
26    discover_live_runtime, harness_support_registry, AcpRuntimeBackend, ClaudeCodeRuntimeBackend,
27    CodexRuntimeBackend, DiscoveryQuery, HarnessCatalog, HarnessHomes, HarnessId,
28    ImplementationKind, LiveRuntimeEndpoint, LiveRuntimeSource, OpenCodeRuntimeBackend,
29    PiRuntimeBackend, Role, RuntimeAttachRequest, RuntimeBackend, RuntimeConnection, RuntimeInput,
30    RuntimeLaunch, RuntimeStartRequest, Session, SessionDescriptor, SessionFollower, SessionFormat,
31    SessionLocator, SessionSource,
32};
33use crate::{reduce, tokens};
34#[cfg(feature = "adapter-api")]
35use crate::{register_live_runtime, resolve_live_runtime, LiveRuntimeRegistration};
36
37/// Protocol namespace implemented by this service.
38pub const HARNESS_SERVICE_VERSION: &str = "harness.v1";
39/// Notification method emitted for followed-session changes.
40pub const SESSION_EVENT_METHOD: &str = "harness.v1.sessions.event";
41/// Notification method emitted for normalized session-activity transitions.
42pub const SESSION_ACTIVITY_EVENT_METHOD: &str = "harness.v1.sessions.activity_event";
43/// Notification method emitted for revisioned session-list changes.
44pub const SESSION_INDEX_EVENT_METHOD: &str = "harness.v1.sessions.index_event";
45/// Notification method emitted for live runtime events.
46pub const RUNTIME_EVENT_METHOD: &str = "harness.v1.runtimes.event";
47
48/// Stateful persisted-session service. Each instance owns its follow
49/// subscriptions; discovery and loading remain read-only.
50pub struct HarnessSessionService {
51    catalog: HarnessCatalog,
52    followers: BTreeMap<String, SessionFollower>,
53    followed_sources: BTreeMap<String, FollowedSource>,
54    activity_subscriptions: BTreeMap<String, ActivitySubscription>,
55    index_subscriptions: BTreeMap<String, crate::session_index::SessionIndexSubscription>,
56    #[cfg(feature = "adapter-api")]
57    activity_monitor: crate::session_activity::SessionActivityMonitor,
58    next_subscription: u64,
59    runtimes: BTreeMap<String, Box<dyn RuntimeConnection>>,
60    terminal_launches: BTreeMap<String, StructuredLaunch>,
61    runtime_sequences: BTreeMap<String, u64>,
62    next_runtime: u64,
63    reduction_store_root: Option<PathBuf>,
64}
65
66impl Default for HarnessSessionService {
67    fn default() -> Self {
68        Self::new()
69    }
70}
71
72impl HarnessSessionService {
73    /// Create an empty service instance.
74    pub fn new() -> Self {
75        Self {
76            catalog: HarnessCatalog::new(),
77            followers: BTreeMap::new(),
78            followed_sources: BTreeMap::new(),
79            activity_subscriptions: BTreeMap::new(),
80            index_subscriptions: BTreeMap::new(),
81            #[cfg(feature = "adapter-api")]
82            activity_monitor: Default::default(),
83            next_subscription: 1,
84            runtimes: BTreeMap::new(),
85            terminal_launches: BTreeMap::new(),
86            runtime_sequences: BTreeMap::new(),
87            next_runtime: 1,
88            reduction_store_root: None,
89        }
90    }
91
92    /// Override the trusted, service-owned store used for durable reduction
93    /// bundles. Embedders and tests use this to keep all writes inside an
94    /// explicitly selected root; the CLI otherwise uses the normal
95    /// `$SUPERCODE_HOME/sessions` location.
96    pub fn with_reduction_store_root(mut self, root: impl Into<PathBuf>) -> Self {
97        self.reduction_store_root = Some(root.into());
98        self
99    }
100
101    /// Handle one JSON-RPC 2.0 request and return one JSON-RPC response.
102    #[cfg(feature = "adapter-api")]
103    pub fn handle(&mut self, request: Value) -> Value {
104        let id = request.get("id").cloned().unwrap_or(Value::Null);
105        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
106            return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
107        }
108        let Some(method) = request.get("method").and_then(Value::as_str) else {
109            return rpc_error(id, -32600, "request is missing `method`");
110        };
111        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
112        match self.call(method, params) {
113            Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
114            Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
115            Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
116            Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
117            Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
118            Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
119        }
120    }
121
122    /// Handle either a persisted-session request or an asynchronous live
123    /// runtime request.
124    #[cfg(feature = "adapter-api")]
125    pub async fn handle_async(&mut self, request: Value) -> Value {
126        let method = request
127            .get("method")
128            .and_then(Value::as_str)
129            .unwrap_or_default();
130        if matches!(
131            method,
132            "harness.v1.harnesses.list" | "harness.v1.harnesses.probe"
133        ) {
134            let id = request.get("id").cloned().unwrap_or(Value::Null);
135            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
136                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
137            }
138            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
139            return match self.inventory_call(method, params).await {
140                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
141                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
142                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
143                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
144                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
145                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
146            };
147        }
148        if matches!(
149            method,
150            "harness.v1.harnesses.auth.methods"
151                | "harness.v1.harnesses.auth.begin"
152                | "harness.v1.harnesses.auth.verify"
153        ) {
154            let id = request.get("id").cloned().unwrap_or(Value::Null);
155            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
156                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
157            }
158            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
159            return match self.harness_authentication_call(method, params).await {
160                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
161                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
162                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
163                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
164                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
165                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
166            };
167        }
168        if method == "harness.v1.sessions.message" {
169            let id = request.get("id").cloned().unwrap_or(Value::Null);
170            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
171                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
172            }
173            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
174            return match self.message_call(params).await {
175                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
176                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
177                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
178                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
179                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
180                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
181            };
182        }
183        if matches!(
184            method,
185            "harness.v1.harnesses.settings" | "harness.v1.harnesses.configure"
186        ) {
187            let id = request.get("id").cloned().unwrap_or(Value::Null);
188            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
189                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
190            }
191            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
192            return match self.harness_settings_call(method, params) {
193                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
194                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
195                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
196                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
197                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
198                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
199            };
200        }
201        if method == "harness.v1.sessions.activity.subscribe" {
202            let id = request.get("id").cloned().unwrap_or(Value::Null);
203            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
204                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
205            }
206            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
207            return match self.subscribe_session_activity(params).await {
208                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
209                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
210                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
211                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
212                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
213                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
214            };
215        }
216        if let Some(operation) = SdkOperation::from_method(method) {
217            let id = request.get("id").cloned().unwrap_or(Value::Null);
218            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
219                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
220            }
221            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
222            return match self.execute(SdkRequest { operation, params }).await {
223                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
224                Err(error) => sdk_rpc_error(id, &error),
225            };
226        }
227        if !method.starts_with("harness.v1.runtimes.") {
228            return self.handle(request);
229        }
230        let id = request.get("id").cloned().unwrap_or(Value::Null);
231        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
232            return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
233        }
234        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
235        match self.runtime_call(method, params).await {
236            Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
237            Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
238            Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
239            Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
240            Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
241            Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
242        }
243    }
244
245    /// Poll all active subscriptions once and return zero or more JSON-RPC
246    /// notifications. Recoverable follower errors are delivered as events.
247    #[cfg(feature = "adapter-api")]
248    pub fn poll(&mut self) -> Vec<Value> {
249        let mut notifications = Vec::new();
250        for (subscription, follower) in &mut self.followers {
251            match follower.poll() {
252                Ok(Some(event)) => notifications.push(json!({
253                    "jsonrpc": "2.0",
254                    "method": SESSION_EVENT_METHOD,
255                    "params": {
256                        "subscription": subscription,
257                        "event": event.to_json(),
258                    }
259                })),
260                Ok(None) => {}
261                Err(error) => notifications.push(json!({
262                    "jsonrpc": "2.0",
263                    "method": SESSION_EVENT_METHOD,
264                    "params": {
265                        "subscription": subscription,
266                        "event": {
267                            "type": "watch_error",
268                            "recoverable": true,
269                            "message": error.to_string(),
270                        },
271                    }
272                })),
273            }
274        }
275        notifications
276    }
277
278    /// Report each followed session's live-runtime lifecycle state on that
279    /// session's own subscription, emitting only when the state changes.
280    ///
281    /// A growing transcript is not evidence that an agent is working, so the
282    /// state comes from the live-runtime registry and nowhere else. A followed
283    /// session with no registered Supercode runtime — a harness running outside
284    /// Supercode — reports `persisted`, which says plainly that its activity is
285    /// unknown rather than guessing at it. These events carry no sequence
286    /// number and no transcript content; they never interleave with the
287    /// content follower's sequenced stream.
288    #[cfg(feature = "adapter-api")]
289    pub async fn poll_session_runtime_states(&mut self) -> Vec<Value> {
290        let registry = crate::LocalRuntimeRegistry::new();
291        let authorization = crate::RuntimeAuthorization::observer();
292        let mut notifications = Vec::new();
293        for (subscription, source) in &mut self.followed_sources {
294            let state = match registry
295                .source_state(&source.harness, &source.session_id, &authorization)
296                .await
297            {
298                Ok(Some(state)) => state,
299                Ok(None) => crate::RuntimeRegistryState::Persisted,
300                // A failed registry read is not evidence of a state change.
301                Err(_) => continue,
302            };
303            if source.reported.as_deref() == Some(state.as_str()) {
304                continue;
305            }
306            source.reported = Some(state.as_str().to_string());
307            notifications.push(json!({
308                "jsonrpc": "2.0",
309                "method": SESSION_EVENT_METHOD,
310                "params": {
311                    "subscription": subscription,
312                    "event": {"type": "runtime_state", "state": state.as_str()},
313                },
314            }));
315        }
316        notifications
317    }
318
319    /// Poll normalized activity subscriptions, emitting only proven state
320    /// transitions. Every subscription is bulk-sampled so stock-harness
321    /// process and registry discovery happens once per UI, not once per row.
322    #[cfg(feature = "adapter-api")]
323    pub async fn poll_session_activities(&mut self) -> Vec<Value> {
324        let subscriptions = self
325            .activity_subscriptions
326            .iter()
327            .map(|(id, subscription)| {
328                (
329                    id.clone(),
330                    subscription.locators.clone(),
331                    subscription.homes.clone(),
332                )
333            })
334            .collect::<Vec<_>>();
335        let mut notifications = Vec::new();
336        for (subscription_id, locators, homes) in subscriptions {
337            let Ok(activities) = self.activity_monitor.resolve(&locators, &homes).await else {
338                // A failed evidence read proves no transition. Retain the last
339                // good state instead of flashing every row to persisted.
340                continue;
341            };
342            let Some(subscription) = self.activity_subscriptions.get_mut(&subscription_id) else {
343                continue;
344            };
345            let mut changed = Vec::new();
346            for activity in activities {
347                let key = activity.key();
348                if subscription
349                    .reported
350                    .get(&key)
351                    .is_some_and(|previous| previous.same_state(&activity))
352                {
353                    continue;
354                }
355                subscription.reported.insert(key, activity.clone());
356                changed.push(activity);
357            }
358            if !changed.is_empty() {
359                notifications.push(json!({
360                    "jsonrpc": "2.0",
361                    "method": SESSION_ACTIVITY_EVENT_METHOD,
362                    "params": {
363                        "subscription": subscription_id,
364                        "activities": changed,
365                    },
366                }));
367            }
368        }
369        notifications
370    }
371
372    /// Drain native-store invalidations and emit revisioned descriptor deltas.
373    /// An idle subscription performs no catalog or transcript reads between
374    /// its minute-scale recovery reconciliations.
375    #[cfg(feature = "adapter-api")]
376    pub fn poll_session_indexes(&mut self) -> Vec<Value> {
377        let mut notifications = Vec::new();
378        for (subscription, index) in &mut self.index_subscriptions {
379            let homes = index.homes().clone();
380            match index.poll() {
381                Ok(Some(delta)) => match live_index_changes(delta.changes, &homes) {
382                    Ok(changes) => notifications.push(json!({
383                        "jsonrpc": "2.0",
384                        "method": SESSION_INDEX_EVENT_METHOD,
385                        "params": {
386                            "subscription": subscription,
387                            "revision": delta.revision,
388                            "changes": changes,
389                        },
390                    })),
391                    Err(error) => notifications.push(json!({
392                        "jsonrpc": "2.0",
393                        "method": SESSION_INDEX_EVENT_METHOD,
394                        "params": {
395                            "subscription": subscription,
396                            "error": {"recoverable": true, "message": error_message(error)},
397                        },
398                    })),
399                },
400                Ok(None) => {}
401                Err(error) => notifications.push(json!({
402                    "jsonrpc": "2.0",
403                    "method": SESSION_INDEX_EVENT_METHOD,
404                    "params": {
405                        "subscription": subscription,
406                        "error": {"recoverable": true, "message": error},
407                    },
408                })),
409            }
410        }
411        notifications
412    }
413
414    #[cfg(feature = "adapter-api")]
415    async fn subscribe_session_activity(
416        &mut self,
417        params: Value,
418    ) -> std::result::Result<Value, ServiceError> {
419        let params = decode::<ActivitySubscribeParams>(params)?;
420        if params.locators.is_empty() {
421            return Err(ServiceError::InvalidParams(
422                "sessions.activity.subscribe requires at least one locator".into(),
423            ));
424        }
425        if params.locators.len() > 2_048 {
426            return Err(ServiceError::InvalidParams(
427                "sessions.activity.subscribe accepts at most 2048 locators".into(),
428            ));
429        }
430        let initial = self
431            .activity_monitor
432            .resolve(&params.locators, &params.homes)
433            .await
434            .map_err(ServiceError::Sdk)?;
435        let subscription = format!("activity-sub-{}", self.next_subscription);
436        self.next_subscription += 1;
437        let reported = initial
438            .iter()
439            .cloned()
440            .map(|activity| (activity.key(), activity))
441            .collect();
442        self.activity_subscriptions.insert(
443            subscription.clone(),
444            ActivitySubscription {
445                locators: params.locators,
446                homes: params.homes,
447                reported,
448            },
449        );
450        Ok(json!({"subscription": subscription, "initial": initial}))
451    }
452
453    /// Non-blockingly sample one event from every connected live runtime.
454    #[cfg(feature = "adapter-api")]
455    pub async fn poll_runtimes(&mut self) -> Vec<Value> {
456        self.poll_sdk_events()
457            .await
458            .into_iter()
459            .map(|(connection, runtime_event)| {
460                json!({
461                    "jsonrpc": "2.0",
462                    "method": RUNTIME_EVENT_METHOD,
463                    "params": {
464                        "connection": connection,
465                        "session_id": runtime_event.session_id,
466                        "sequence": runtime_event.event.sequence,
467                        "event": {
468                            "kind": runtime_event.event.kind,
469                            "payload": runtime_event.event.payload,
470                        },
471                    },
472                })
473            })
474            .collect()
475    }
476
477    async fn poll_sdk_events(&mut self) -> Vec<(String, SdkRuntimeEvent)> {
478        let mut events = Vec::new();
479        let mut closed = Vec::new();
480        for (connection, runtime) in &mut self.runtimes {
481            let session_id = runtime.handle().runtime_id.clone();
482            match tokio::time::timeout(Duration::from_millis(1), runtime.next_event()).await {
483                Ok(Ok(Some(event))) => {
484                    let terminal = event.kind == "transport_closed";
485                    let next_sequence = self
486                        .runtime_sequences
487                        .entry(session_id.clone())
488                        .or_insert(0);
489                    let sequence = event.sequence.unwrap_or_else(|| {
490                        *next_sequence = next_sequence.saturating_add(1);
491                        *next_sequence
492                    });
493                    *next_sequence = (*next_sequence).max(sequence);
494                    events.push((
495                        connection.clone(),
496                        SdkRuntimeEvent {
497                            session_id: session_id.clone(),
498                            event: SdkEvent {
499                                sequence,
500                                kind: event.kind,
501                                payload: event.payload,
502                            },
503                        },
504                    ));
505                    if terminal {
506                        closed.push(connection.clone());
507                    }
508                }
509                Ok(Ok(None)) => {
510                    let sequence = self
511                        .runtime_sequences
512                        .entry(session_id.clone())
513                        .or_insert(0);
514                    *sequence = sequence.saturating_add(1);
515                    events.push((
516                        connection.clone(),
517                        SdkRuntimeEvent {
518                            session_id,
519                            event: SdkEvent {
520                                sequence: *sequence,
521                                kind: "transport_closed".into(),
522                                payload: json!({"message": "Harness runtime transport closed."}),
523                            },
524                        },
525                    ));
526                    closed.push(connection.clone());
527                }
528                Err(_) => {}
529                Ok(Err(error)) => {
530                    let sequence = self
531                        .runtime_sequences
532                        .entry(session_id.clone())
533                        .or_insert(0);
534                    *sequence = sequence.saturating_add(1);
535                    events.push((
536                        connection.clone(),
537                        SdkRuntimeEvent {
538                            session_id,
539                            event: SdkEvent {
540                                sequence: *sequence,
541                                kind: "transport_error".into(),
542                                payload: json!({"message": error.to_string(), "terminal": true}),
543                            },
544                        },
545                    ));
546                    closed.push(connection.clone());
547                }
548            }
549        }
550        for connection in closed {
551            if let Some(runtime) = self.runtimes.remove(&connection) {
552                self.runtime_sequences.remove(&runtime.handle().runtime_id);
553            }
554            self.terminal_launches.remove(&connection);
555        }
556        events
557    }
558
559    fn call(&mut self, method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
560        match method {
561            "harness.v1.capabilities" => Ok(json!({
562                "version": HARNESS_SERVICE_VERSION,
563                "sdk": self.capabilities(),
564                "methods": [
565                    "harness.v1.support.report",
566                    "harness.v1.harnesses.list",
567                    "harness.v1.harnesses.probe",
568                    "harness.v1.harnesses.settings",
569                    "harness.v1.harnesses.configure",
570                    "harness.v1.harnesses.auth.methods",
571                    "harness.v1.harnesses.auth.begin",
572                    "harness.v1.harnesses.auth.verify",
573                    "harness.v1.sessions.discover",
574                    "harness.v1.sessions.load",
575                    "harness.v1.sessions.follow",
576                    "harness.v1.sessions.unfollow",
577                    "harness.v1.sessions.activity.subscribe",
578                    "harness.v1.sessions.activity.unsubscribe",
579                    "harness.v1.sessions.index.subscribe",
580                    "harness.v1.sessions.index.unsubscribe",
581                    "harness.v1.sessions.message",
582                    "harness.v1.sessions.import",
583                    "harness.v1.sessions.export",
584                    "harness.v1.sessions.translate",
585                    "harness.v1.sessions.reduce",
586                    "harness.v1.sessions.branch",
587                    "harness.v1.sessions.handoff",
588                    "harness.v1.sessions.resume_instructions",
589                    "harness.v1.runtimes.capabilities",
590                    "harness.v1.runtimes.start",
591                    "harness.v1.runtimes.resume",
592                    "harness.v1.runtimes.attach_existing",
593                    "harness.v1.runtimes.attach",
594                    "harness.v1.runtimes.send_input",
595                    "harness.v1.runtimes.interrupt",
596                    "harness.v1.runtimes.steer",
597                    "harness.v1.runtimes.respond",
598                    "harness.v1.runtimes.terminal_instructions",
599                    "harness.v1.runtimes.close",
600                ],
601                "notifications": [
602                    SESSION_EVENT_METHOD,
603                    SESSION_ACTIVITY_EVENT_METHOD,
604                    SESSION_INDEX_EVENT_METHOD,
605                    RUNTIME_EVENT_METHOD
606                ],
607                "harnesses": harness_support_registry()
608                    .harnesses
609                    .into_iter()
610                    .map(|harness| harness.id)
611                    .collect::<Vec<_>>(),
612            })),
613            "harness.v1.support.report" => serde_json::to_value(harness_support_registry())
614                .map_err(|error| ServiceError::Operation(error.to_string())),
615            "harness.v1.sessions.discover" => {
616                let query = decode::<DiscoveryQuery>(params)?;
617                let page = discover_session_page(&query).map_err(operation)?;
618                // Claude Code is the one harness that publishes its RUNNING
619                // sessions. The registry is read once per discovery and joined
620                // by session id; every record in it has already survived a
621                // `kill(pid, 0)` liveness check inside `read_registry`.
622                let peers = if page
623                    .sessions
624                    .iter()
625                    .any(|session| session.locator.harness.as_str() == HarnessId::CLAUDE_CODE)
626                {
627                    crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(
628                        &query.homes,
629                    ))
630                } else {
631                    Vec::new()
632                };
633                let activities = crate::session_activity::resolve_stock_session_activities(
634                    &page
635                        .sessions
636                        .iter()
637                        .map(|session| session.locator.clone())
638                        .collect::<Vec<_>>(),
639                    &query.homes,
640                )
641                .into_iter()
642                .map(|activity| (activity.key(), activity))
643                .collect::<BTreeMap<_, _>>();
644                let sessions = page
645                    .sessions
646                    .into_iter()
647                    .map(|session| {
648                        let mut value = live_descriptor_value(&session, &peers)?;
649                        let activity_key = (
650                            session.locator.harness.as_str().to_string(),
651                            session.locator.session_id.clone(),
652                        );
653                        if let Some(activity) = activities.get(&activity_key) {
654                            value["activity"] = serde_json::to_value(activity)
655                                .map_err(|error| ServiceError::Operation(error.to_string()))?;
656                            if let Some(status) = legacy_live_status(activity) {
657                                value["live_status"] = json!(status);
658                            }
659                        }
660                        Ok(value)
661                    })
662                    .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
663                Ok(json!({"sessions": sessions, "next_cursor": page.next_cursor}))
664            }
665            "harness.v1.sessions.load" => {
666                let params = decode::<LoadSessionParams>(params)?;
667                if let Some(options) = &params.options {
668                    options.validate()?;
669                    return load_session(&params.read.locator)
670                        .map(|session| projected_session_result(&session, options))
671                        .map_err(operation);
672                }
673                let mut session = if params.read.display_history() {
674                    self.catalog
675                        .load_display_view(
676                            &params.read.locator,
677                            params.read.read_fidelity(),
678                            params.read.tail_messages().unwrap_or(500),
679                        )
680                        .map_err(crate::Error::from)
681                } else if params.read.include_subagents() {
682                    load_session_with_fidelity(&params.read.locator, params.read.read_fidelity())
683                } else {
684                    self.catalog
685                        .load_parent_with_fidelity(
686                            &params.read.locator,
687                            params.read.read_fidelity(),
688                        )
689                        .map_err(crate::Error::from)
690                }
691                .map_err(operation)?;
692                params.read.bound_session(&mut session);
693                Ok(json!({"session": normalized_session_json(&session)}))
694            }
695            "harness.v1.sessions.follow" => {
696                let params = decode::<LocatorParams>(params)?;
697                let mut follower = self
698                    .catalog
699                    .follow_read_view(
700                        &params.locator,
701                        params.read_fidelity(),
702                        params.include_subagents(),
703                        params.tail_messages(),
704                        params.max_message_chars(),
705                        params.display_history(),
706                    )
707                    .map_err(operation)?;
708                let initial = follower
709                    .poll()
710                    .map_err(operation)?
711                    .map(|event| event.to_json());
712                let subscription = format!("sub-{}", self.next_subscription);
713                self.next_subscription += 1;
714                self.followers.insert(subscription.clone(), follower);
715                self.followed_sources.insert(
716                    subscription.clone(),
717                    FollowedSource {
718                        harness: params.locator.harness.as_str().to_string(),
719                        session_id: params.locator.session_id.clone(),
720                        reported: None,
721                    },
722                );
723                Ok(json!({"subscription": subscription, "initial": initial}))
724            }
725            "harness.v1.sessions.unfollow" => {
726                let params = decode::<UnfollowParams>(params)?;
727                self.followed_sources.remove(&params.subscription);
728                Ok(json!({
729                    "removed": self.followers.remove(&params.subscription).is_some()
730                }))
731            }
732            "harness.v1.sessions.activity.unsubscribe" => {
733                let params = decode::<UnfollowParams>(params)?;
734                Ok(json!({
735                    "removed": self.activity_subscriptions.remove(&params.subscription).is_some()
736                }))
737            }
738            "harness.v1.sessions.index.subscribe" => {
739                let query = decode::<DiscoveryQuery>(params)?;
740                crate::session_index::validate_query(&query)
741                    .map_err(ServiceError::InvalidParams)?;
742                let homes = query.homes.clone();
743                let (index, initial) = crate::session_index::SessionIndexSubscription::open(query)
744                    .map_err(ServiceError::Operation)?;
745                let peers = peers_for_descriptors(&initial, &homes);
746                let initial = initial
747                    .iter()
748                    .map(|descriptor| live_descriptor_value(descriptor, &peers))
749                    .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
750                let subscription = format!("index-sub-{}", self.next_subscription);
751                self.next_subscription += 1;
752                self.index_subscriptions.insert(subscription.clone(), index);
753                Ok(json!({
754                    "subscription": subscription,
755                    "revision": 1,
756                    "initial": initial,
757                }))
758            }
759            "harness.v1.sessions.index.unsubscribe" => {
760                let params = decode::<UnfollowParams>(params)?;
761                Ok(json!({
762                    "removed": self.index_subscriptions.remove(&params.subscription).is_some()
763                }))
764            }
765            "harness.v1.sessions.import" => {
766                let params = decode::<ImportSessionParams>(params)?;
767                let session = Session::load_str(&params.content, params.source_harness.into())
768                    .map_err(operation)?;
769                Ok(json!({"session": normalized_session_json(&session)}))
770            }
771            "harness.v1.sessions.export" | "harness.v1.sessions.translate" => {
772                let params = decode::<ExportSessionParams>(params)?;
773                let session = load_session(&params.locator).map_err(operation)?;
774                let artifact = session_artifact(&params.locator, &session, params.target_harness)?;
775                Ok(json!({"artifact": artifact}))
776            }
777            "harness.v1.sessions.reduce" => {
778                let params = decode::<ReduceSessionParams>(params)?;
779                self.reduce_session(params)
780            }
781            "harness.v1.sessions.branch" => {
782                let params = decode::<BranchSessionParams>(params)?;
783                let session = load_session(&params.locator).map_err(operation)?;
784                let storage = params.locator.storage.path().display().to_string();
785                let bootstrap_prompt = format!(
786                    "Continue as a new branch from {} session {}. The frozen parent transcript is at {}. Read or load that parent for context, summarize the relevant state, then continue independently without mutating the parent session.",
787                    params.locator.harness.as_str(), params.locator.session_id, storage
788                );
789                let artifact = params
790                    .target_harness
791                    .map(|target| session_artifact(&params.locator, &session, target))
792                    .transpose()?;
793                Ok(json!({
794                    "parent": params.locator,
795                    "session": normalized_session_json(&session),
796                    "bootstrap_prompt": bootstrap_prompt,
797                    "artifact": artifact,
798                }))
799            }
800            "harness.v1.sessions.handoff" => {
801                let params = decode::<HandoffSessionParams>(params)?;
802                let session = load_session(&params.locator).map_err(operation)?;
803                let cwd = params
804                    .cwd
805                    .or_else(|| session.meta.cwd.clone())
806                    .unwrap_or_else(|| PathBuf::from("."));
807                let artifact =
808                    handoff_artifact(&params.locator, &session, params.target_harness, &cwd)?;
809                let target_session_id = artifact.session_id.as_deref().ok_or_else(|| {
810                    ServiceError::Operation(
811                        "handoff artifact omitted target session identity".into(),
812                    )
813                })?;
814                let instructions =
815                    handoff_instructions(params.target_harness, target_session_id, &cwd);
816                Ok(json!({
817                    "artifact": artifact,
818                    "launch": instructions.launch,
819                    "materialize": instructions.materialize,
820                    "requires_materialization": instructions.requires_materialization,
821                    "note": instructions.note,
822                }))
823            }
824            "harness.v1.sessions.resume_instructions" => {
825                let params = decode::<ResumeInstructionsParams>(params)?;
826                let session = load_session(&params.locator).map_err(operation)?;
827                let cwd = params
828                    .cwd
829                    .or(session.meta.cwd)
830                    .unwrap_or_else(|| PathBuf::from("."));
831                let launch = resume_launch(
832                    params.locator.harness.as_str(),
833                    &params.locator.session_id,
834                    &cwd,
835                    params.policy,
836                )?;
837                Ok(json!({"launch": launch}))
838            }
839            _ => Err(ServiceError::MethodNotFound),
840        }
841    }
842
843    fn reduce_session(
844        &self,
845        params: ReduceSessionParams,
846    ) -> std::result::Result<Value, ServiceError> {
847        let session = load_session(&params.locator).map_err(operation)?;
848        if session.messages.is_empty() {
849            return Err(ServiceError::InvalidParams(
850                "cannot reduce an empty session".into(),
851            ));
852        }
853        let keep_last = params.keep_last.clamp(1, 128);
854        let policy = reduce::ReductionPolicy {
855            clear_turns_older_than: Some(keep_last),
856            ..Default::default()
857        };
858        let (view, log) =
859            reduce::project_messages(&session.messages, &policy, &reduce::ReductionLog::default());
860        if log.reductions.is_empty() {
861            return Err(ServiceError::UnsupportedAction(format!(
862                "session `{}` is already too small for a meaningful reversible reduction",
863                params.locator.session_id
864            )));
865        }
866        let source_tokens = tokens::estimate_view_tokens(&session.messages);
867        let reduced_tokens = tokens::estimate_view_tokens(&view);
868        if reduced_tokens >= source_tokens {
869            return Err(ServiceError::UnsupportedAction(format!(
870                "session `{}` has no token-reducing reversible projection",
871                params.locator.session_id
872            )));
873        }
874
875        let store_root = self
876            .reduction_store_root
877            .clone()
878            .unwrap_or_else(default_reduction_store_root);
879        let store = crate::SessionStore::open(&store_root).map_err(operation)?;
880        let rescue_id = format!("rescue-{}", generated_session_id());
881        let imported = session
882            .imported_message_count
883            .unwrap_or(session.messages.len())
884            .min(session.messages.len());
885        let sidecar_jsonl = session.to_native_jsonl_v2(&session.messages[imported..]);
886        let view_jsonl = messages_jsonl(&view)?;
887        let title = format!(
888            "Reduced {} continuation from {}",
889            params.target_harness.id(),
890            params.locator.session_id
891        );
892
893        // Durability order is intentional: the full source of truth lands
894        // before either object that can refer to it. A crash may leave an
895        // unused sidecar, but can never leave a reduced view whose originals
896        // were not durably written first.
897        store
898            .save_sidecar(&rescue_id, &sidecar_jsonl)
899            .map_err(operation)?;
900        store
901            .save_reduction_log(&rescue_id, &log)
902            .map_err(operation)?;
903        store
904            .save(&rescue_id, &title, &view_jsonl)
905            .map_err(operation)?;
906
907        let source_bytes = serde_json::to_vec(&session.messages)
908            .map_err(|error| ServiceError::Operation(error.to_string()))?
909            .len() as u64;
910        let reduced_bytes = serde_json::to_vec(&view)
911            .map_err(|error| ServiceError::Operation(error.to_string()))?
912            .len() as u64;
913        store
914            .set_reduction_stats(
915                &rescue_id,
916                &title,
917                source_bytes,
918                reduced_bytes,
919                log.reductions.len() as u32,
920            )
921            .map_err(operation)?;
922
923        // The receipt is issued only after a real disk reload. This proves
924        // the exact files another process will consume, not the convenient
925        // in-memory values that produced them.
926        let reloaded_sidecar = store
927            .load_sidecar(&rescue_id)
928            .map_err(operation)?
929            .ok_or_else(|| ServiceError::Operation("reduction sidecar disappeared".into()))?;
930        let reloaded_sidecar = Session::from_sidecar_str(&reloaded_sidecar).map_err(operation)?;
931        let reloaded_log = store
932            .load_reduction_log(&rescue_id)
933            .map_err(operation)?
934            .ok_or_else(|| ServiceError::Operation("reduction log disappeared".into()))?;
935        let reloaded_view = parse_messages_jsonl(&store.load(&rescue_id).map_err(operation)?)?;
936        reduce::verify_log(&reloaded_log, &reloaded_sidecar).map_err(operation)?;
937        // `sc.reduction` is deliberately in-memory-only metadata: it must
938        // never leak onto a provider-facing transcript. Reapplying the
939        // durable log to the durable sidecar restores those ids. Comparing
940        // its wire form with the transcript reloaded above proves that the
941        // persisted view is exactly the deterministic projection before we
942        // use the restamped form for inversion.
943        let (restamped_view, restamped_log) =
944            reduce::project_messages(&reloaded_sidecar.messages, &policy, &reloaded_log);
945        if messages_jsonl(&restamped_view)? != messages_jsonl(&reloaded_view)? {
946            return Err(ServiceError::Operation(
947                "persisted reduction view does not match its durable log and sidecar".into(),
948            ));
949        }
950        if restamped_log != reloaded_log {
951            return Err(ServiceError::Operation(
952                "reapplying the durable reduction log changed its identity".into(),
953            ));
954        }
955        let inverted =
956            reduce::invert(&restamped_view, &reloaded_log, &reloaded_sidecar).map_err(operation)?;
957        if inverted != session.messages {
958            return Err(ServiceError::Operation(
959                "reduction inversion did not restore the source messages byte-exactly".into(),
960            ));
961        }
962
963        let ratio = source_tokens as f64 / reduced_tokens.max(1) as f64;
964        let sidecar_path = store.sidecar_path(&rescue_id);
965        let reduction_log_path = store.reduction_log_path(&rescue_id).map_err(operation)?;
966        let bootstrap_prompt = reduced_bootstrap_prompt(
967            &params.locator,
968            params.target_harness,
969            &view_jsonl,
970            &sidecar_path,
971            &reduction_log_path,
972        );
973        let mut reduced_session = session.clone();
974        reduced_session.meta.session_id = Some(rescue_id.clone());
975        reduced_session.messages = view;
976
977        Ok(json!({
978            "session": normalized_session_json(&reduced_session),
979            "bootstrap_prompt": bootstrap_prompt,
980            "receipt": {
981                "id": rescue_id,
982                "sidecar_id": rescue_id,
983                "source_harness": params.locator.harness,
984                "target_harness": params.target_harness.id(),
985                "source_tokens": source_tokens,
986                "reduced_tokens": reduced_tokens,
987                "ratio": ratio,
988                "source_bytes": source_bytes,
989                "reduced_bytes": reduced_bytes,
990                "reductions": reloaded_log.reductions.len(),
991                "sidecar_path": sidecar_path,
992                "reduction_log_path": reduction_log_path,
993                "verified": true,
994                "reversible": true,
995            }
996        }))
997    }
998
999    async fn runtime_call(
1000        &mut self,
1001        method: &str,
1002        params: Value,
1003    ) -> std::result::Result<Value, ServiceError> {
1004        match method {
1005            "harness.v1.runtimes.capabilities" => {
1006                let params = decode::<RuntimeBackendParams>(params)?;
1007                let backend = runtime_backend(&params)?;
1008                Ok(json!({
1009                    "harness": backend.harness(),
1010                    "capabilities": backend.capabilities(),
1011                }))
1012            }
1013            "harness.v1.runtimes.start" => {
1014                let params = decode::<RuntimeStartParams>(params)?;
1015                let backend = runtime_backend(&params.backend)?;
1016                let capabilities = backend.capabilities();
1017                let workspace = params.cwd.clone();
1018                let runtime = backend
1019                    .start(RuntimeStartRequest {
1020                        cwd: params.cwd,
1021                        launch: runtime_launch(&params.backend),
1022                    })
1023                    .await
1024                    .map_err(operation)?;
1025                self.insert_hosted_runtime(runtime, capabilities, workspace)
1026                    .await
1027            }
1028            "harness.v1.runtimes.resume" | "harness.v1.runtimes.attach" => {
1029                let params = decode::<RuntimeAttachParams>(params)?;
1030                let backend = runtime_backend(&params.backend)?;
1031                let capabilities = backend.capabilities();
1032                let workspace = params.cwd.clone().unwrap_or_else(|| {
1033                    std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
1034                });
1035                let runtime = backend
1036                    .attach(RuntimeAttachRequest {
1037                        runtime_id: params.runtime_id,
1038                        cwd: params.cwd,
1039                        launch: runtime_launch(&params.backend),
1040                    })
1041                    .await
1042                    .map_err(operation)?;
1043                self.insert_hosted_runtime(runtime, capabilities, workspace)
1044                    .await
1045            }
1046            "harness.v1.runtimes.attach_existing" => {
1047                let params = decode::<RuntimeAttachParams>(params)?;
1048                let backend: Box<dyn RuntimeBackend> = match params
1049                    .backend
1050                    .base_url
1051                    .as_deref()
1052                    .and_then(|value| LiveRuntimeEndpoint::parse(value).ok())
1053                {
1054                    Some(endpoint) => {
1055                        #[cfg(not(feature = "adapter-api"))]
1056                        {
1057                            let _ = endpoint;
1058                            return Err(ServiceError::UnsupportedAction(
1059                                "live HTTP attachment adapter is not compiled".into(),
1060                            ));
1061                        }
1062                        #[cfg(feature = "adapter-api")]
1063                        {
1064                            let workspace = params.cwd.clone().ok_or_else(|| {
1065                                ServiceError::InvalidParams(
1066                                    "Supercode live attach requires the project cwd".into(),
1067                                )
1068                            })?;
1069                            let source = LiveRuntimeSource {
1070                                harness: params.backend.harness.as_str().to_string(),
1071                                session_id: params.runtime_id.clone(),
1072                                workspace,
1073                            };
1074                            let receipt = resolve_live_runtime(&endpoint, &source)
1075                                .map_err(|error| ServiceError::Operation(error.to_string()))?;
1076                            Box::new(SupercodeHttpRuntimeBackend::new(receipt))
1077                        }
1078                    }
1079                    None => runtime_backend(&params.backend)?,
1080                };
1081                if !backend.capabilities().attach_existing_process {
1082                    return Err(ServiceError::Operation(format!(
1083                        "{} cannot attach to an already-running process; use runtimes.resume for a persisted session",
1084                        backend.harness().as_str()
1085                    )));
1086                }
1087                let runtime = backend
1088                    .attach_existing(RuntimeAttachRequest {
1089                        runtime_id: params.runtime_id,
1090                        cwd: params.cwd,
1091                        launch: runtime_launch(&params.backend),
1092                    })
1093                    .await
1094                    .map_err(operation)?;
1095                self.insert_runtime(runtime)
1096            }
1097            "harness.v1.runtimes.send_input" => {
1098                let params = decode::<RuntimeInputParams>(params)?;
1099                let image_urls = validate_runtime_image_urls(params.image_urls)?;
1100                let runtime = self.runtime_mut(&params.connection)?;
1101                let turn_id = runtime
1102                    .send_input(RuntimeInput {
1103                        text: params.text,
1104                        image_urls,
1105                    })
1106                    .await
1107                    .map_err(operation)?;
1108                Ok(json!({"turn_id": turn_id}))
1109            }
1110            "harness.v1.runtimes.interrupt" => {
1111                let params = decode::<RuntimeConnectionParams>(params)?;
1112                self.runtime_mut(&params.connection)?
1113                    .interrupt()
1114                    .await
1115                    .map_err(operation)?;
1116                Ok(json!({}))
1117            }
1118            "harness.v1.runtimes.steer" => {
1119                let params = decode::<RuntimeInputParams>(params)?;
1120                if !params.image_urls.is_empty() {
1121                    return Err(ServiceError::InvalidParams(
1122                        "runtime steering accepts text only".into(),
1123                    ));
1124                }
1125                let text = params.text.trim();
1126                if text.is_empty() || text.chars().count() > 50_000 {
1127                    return Err(ServiceError::InvalidParams(
1128                        "runtime steering requires 1 to 50,000 text characters".into(),
1129                    ));
1130                }
1131                self.runtime_mut(&params.connection)?
1132                    .steer(text.to_string())
1133                    .await
1134                    .map_err(operation)?;
1135                Ok(json!({}))
1136            }
1137            "harness.v1.runtimes.respond" => {
1138                let params = decode::<RuntimeRespondParams>(params)?;
1139                self.runtime_mut(&params.connection)?
1140                    .respond(params.request_id, params.response)
1141                    .await
1142                    .map_err(operation)?;
1143                Ok(json!({}))
1144            }
1145            "harness.v1.runtimes.terminal_instructions" => {
1146                let params = decode::<RuntimeConnectionParams>(params)?;
1147                let launch = self
1148                    .terminal_launches
1149                    .get(&params.connection)
1150                    .ok_or_else(|| {
1151                        ServiceError::Operation(
1152                            "this runtime is not hosted for terminal attachment".into(),
1153                        )
1154                    })?;
1155                Ok(json!({"launch":launch}))
1156            }
1157            "harness.v1.runtimes.close" => {
1158                let params = decode::<RuntimeConnectionParams>(params)?;
1159                let Some(mut runtime) = self.runtimes.remove(&params.connection) else {
1160                    return Err(ServiceError::InvalidParams(format!(
1161                        "unknown runtime connection `{}`",
1162                        params.connection
1163                    )));
1164                };
1165                self.terminal_launches.remove(&params.connection);
1166                self.runtime_sequences.remove(&runtime.handle().runtime_id);
1167                runtime.close().await.map_err(operation)?;
1168                Ok(json!({"closed": true}))
1169            }
1170            _ => Err(ServiceError::MethodNotFound),
1171        }
1172    }
1173
1174    /// Deliver one message into a session that is running right now.
1175    #[cfg(feature = "adapter-api")]
1176    async fn message_call(&self, params: Value) -> std::result::Result<Value, ServiceError> {
1177        let params = decode::<MessageSessionParams>(params)?;
1178        Ok(message_live_session(&params, &crate::claude_peer::ProcessCourierRunner).await)
1179    }
1180
1181    #[cfg(feature = "adapter-api")]
1182    fn harness_settings_call(
1183        &self,
1184        method: &str,
1185        params: Value,
1186    ) -> std::result::Result<Value, ServiceError> {
1187        let homes = crate::HarnessHomes::default();
1188        match method {
1189            "harness.v1.harnesses.settings" => {
1190                let params = decode::<HarnessSettingsParams>(params)?;
1191                let report = crate::inspect_harness_interop_settings(&homes, &params.harness)
1192                    .map_err(|error| ServiceError::Operation(error.to_string()))?;
1193                serde_json::to_value(report)
1194                    .map_err(|error| ServiceError::Operation(error.to_string()))
1195            }
1196            "harness.v1.harnesses.configure" => {
1197                let params = decode::<ConfigureHarnessParams>(params)?;
1198                let report = crate::configure_harness_interop_settings(
1199                    &homes,
1200                    &params.harness,
1201                    &params.changes,
1202                    params.expected_revision.as_deref(),
1203                )
1204                .map_err(|error| ServiceError::Operation(error.to_string()))?;
1205                serde_json::to_value(report)
1206                    .map_err(|error| ServiceError::Operation(error.to_string()))
1207            }
1208            _ => Err(ServiceError::MethodNotFound),
1209        }
1210    }
1211
1212    fn insert_runtime(
1213        &mut self,
1214        runtime: Box<dyn RuntimeConnection>,
1215    ) -> std::result::Result<Value, ServiceError> {
1216        let connection = format!("runtime-{}", self.next_runtime);
1217        self.next_runtime += 1;
1218        let handle = runtime.handle().clone();
1219        self.runtime_sequences
1220            .entry(handle.runtime_id.clone())
1221            .or_insert(0);
1222        self.runtimes.insert(connection.clone(), runtime);
1223        Ok(json!({"connection": connection, "handle": handle}))
1224    }
1225
1226    #[cfg(feature = "adapter-api")]
1227    async fn insert_hosted_runtime(
1228        &mut self,
1229        runtime: Box<dyn RuntimeConnection>,
1230        capabilities: crate::RuntimeCapabilities,
1231        workspace: PathBuf,
1232    ) -> std::result::Result<Value, ServiceError> {
1233        let (host, connection) = HostedHarnessRuntime::spawn(runtime, capabilities);
1234        let token: std::sync::Arc<str> = crate::server::generate_token().into();
1235        let server = crate::server::run_frontend_http(
1236            host.clone(),
1237            host.frontend_sender(),
1238            "127.0.0.1:0",
1239            token.clone(),
1240        )
1241        .await
1242        .map_err(|error| ServiceError::Operation(error.to_string()))?;
1243        let source = LiveRuntimeSource {
1244            harness: connection.handle().harness.as_str().to_string(),
1245            session_id: connection.handle().runtime_id.clone(),
1246            workspace: workspace.clone(),
1247        };
1248        let registration = register_live_runtime(
1249            connection.handle().runtime_id.clone(),
1250            source.clone(),
1251            format!("http://{}", server.address()),
1252            token.to_string(),
1253        )
1254        .map_err(|error| ServiceError::Operation(error.to_string()))?;
1255        let endpoint = registration.endpoint().to_string();
1256        let launch = StructuredLaunch {
1257            cwd: workspace,
1258            // Pin attachment to the executable hosting this runtime. A bare
1259            // `supercode` could resolve to an older global install whose CLI
1260            // does not understand the receipt it is being asked to open.
1261            program: std::env::current_exe()
1262                .ok()
1263                .map(|path| path.to_string_lossy().into_owned())
1264                .unwrap_or_else(|| "supercode".into()),
1265            arguments: vec![
1266                "harness".into(),
1267                "attach".into(),
1268                "--endpoint".into(),
1269                endpoint,
1270                "--harness".into(),
1271                source.harness,
1272                "--session".into(),
1273                source.session_id,
1274            ],
1275            env: BTreeMap::new(),
1276        };
1277        let lease = HostedRuntimeLease {
1278            connection,
1279            _host: host,
1280            _registration: registration,
1281            _server: server,
1282        };
1283        let opened = self.insert_runtime(Box::new(lease))?;
1284        let connection_id = opened["connection"]
1285            .as_str()
1286            .expect("insert_runtime returns a connection id")
1287            .to_string();
1288        self.terminal_launches.insert(connection_id, launch);
1289        Ok(opened)
1290    }
1291
1292    #[cfg(not(feature = "adapter-api"))]
1293    async fn insert_hosted_runtime(
1294        &mut self,
1295        runtime: Box<dyn RuntimeConnection>,
1296        _capabilities: crate::RuntimeCapabilities,
1297        _workspace: PathBuf,
1298    ) -> std::result::Result<Value, ServiceError> {
1299        self.insert_runtime(runtime)
1300    }
1301
1302    fn runtime_mut(
1303        &mut self,
1304        connection: &str,
1305    ) -> std::result::Result<&mut Box<dyn RuntimeConnection>, ServiceError> {
1306        self.runtimes.get_mut(connection).ok_or_else(|| {
1307            ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
1308        })
1309    }
1310
1311    async fn inventory_call(
1312        &self,
1313        method: &str,
1314        params: Value,
1315    ) -> std::result::Result<Value, ServiceError> {
1316        let mut params = decode::<HarnessInventoryParams>(params)?;
1317        if method == "harness.v1.harnesses.probe" {
1318            let harness = params.harness.take().ok_or_else(|| {
1319                ServiceError::InvalidParams("harnesses.probe requires `harness`".into())
1320            })?;
1321            params.harnesses = vec![harness];
1322        }
1323        let selected = params
1324            .harnesses
1325            .iter()
1326            .map(HarnessId::as_str)
1327            .collect::<std::collections::BTreeSet<_>>();
1328        let supported = harness_support_registry()
1329            .harnesses
1330            .into_iter()
1331            .filter(|descriptor| selected.is_empty() || selected.contains(descriptor.id.as_str()))
1332            .collect::<Vec<_>>();
1333        if !params.harnesses.is_empty() && supported.len() != selected.len() {
1334            let known = supported
1335                .iter()
1336                .map(|harness| harness.id.as_str())
1337                .collect::<std::collections::BTreeSet<_>>();
1338            let missing = params
1339                .harnesses
1340                .iter()
1341                .filter(|id| !known.contains(id.as_str()))
1342                .map(HarnessId::as_str)
1343                .collect::<Vec<_>>();
1344            return Err(ServiceError::InvalidParams(format!(
1345                "unknown harness(es): {}",
1346                missing.join(", ")
1347            )));
1348        }
1349        let global_counts = params
1350            .include_sessions
1351            .then(|| self.session_counts(None, &params.harnesses));
1352        let workspace_counts = params.include_sessions.then(|| {
1353            params
1354                .workspace
1355                .as_deref()
1356                .map(|workspace| self.session_counts(Some(workspace), &params.harnesses))
1357        });
1358        let probes = supported.into_iter().map(|descriptor| {
1359            let global = global_counts
1360                .as_ref()
1361                .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
1362            let workspace = workspace_counts
1363                .as_ref()
1364                .and_then(Option::as_ref)
1365                .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
1366            self.probe_harness(descriptor, &params, global, workspace)
1367        });
1368        let harnesses = futures::future::join_all(probes).await;
1369        serde_json::to_value(HarnessInventoryReport {
1370            probe: params.probe,
1371            workspace: params.workspace,
1372            harnesses,
1373        })
1374        .map_err(|error| ServiceError::Operation(error.to_string()))
1375    }
1376
1377    #[cfg(feature = "adapter-api")]
1378    async fn harness_authentication_call(
1379        &self,
1380        method: &str,
1381        params: Value,
1382    ) -> std::result::Result<Value, ServiceError> {
1383        match method {
1384            "harness.v1.harnesses.auth.methods" | "harness.v1.harnesses.auth.verify" => {
1385                let params = decode::<HarnessAuthenticationParams>(params)?;
1386                serde_json::to_value(crate::inspect_harness_authentication(&params.harness).await)
1387                    .map_err(|error| ServiceError::Operation(error.to_string()))
1388            }
1389            "harness.v1.harnesses.auth.begin" => {
1390                let params = decode::<BeginHarnessAuthenticationParams>(params)?;
1391                let cwd = params
1392                    .cwd
1393                    .or_else(|| std::env::current_dir().ok())
1394                    .unwrap_or_else(|| PathBuf::from("."));
1395                let plan = crate::harness_authentication_plan(
1396                    &params.harness,
1397                    params.environment,
1398                    params.method,
1399                    &cwd,
1400                )
1401                .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
1402                serde_json::to_value(plan)
1403                    .map_err(|error| ServiceError::Operation(error.to_string()))
1404            }
1405            _ => Err(ServiceError::MethodNotFound),
1406        }
1407    }
1408
1409    async fn probe_harness(
1410        &self,
1411        descriptor: crate::HarnessSupportDescriptor,
1412        params: &HarnessInventoryParams,
1413        global: Option<usize>,
1414        workspace: Option<usize>,
1415    ) -> LocalHarness {
1416        let launch = descriptor.runtime.default_launch.as_ref();
1417        let executable = launch.and_then(|launch| find_executable(&launch.program));
1418        let installed = executable.is_some();
1419        let version = if params.skip_versions {
1420            None
1421        } else {
1422            match executable.as_deref() {
1423                Some(path) => executable_version(path).await,
1424                None => None,
1425            }
1426        };
1427        let configured = auth_evidence(descriptor.id.as_str());
1428        let mut auth = if configured {
1429            HarnessAuthState::Configured
1430        } else if matches!(
1431            descriptor.id.as_str(),
1432            HarnessId::CLAUDE_CODE | HarnessId::CODEX
1433        ) {
1434            // These two adapters have explicit native status/login contracts
1435            // and complete local evidence coverage (including Claude's macOS
1436            // Keychain-backed oauthAccount marker). Treating absent evidence
1437            // as unknown advertises a start that will only fail interactively.
1438            HarnessAuthState::Required
1439        } else {
1440            HarnessAuthState::Unknown
1441        };
1442        let mut runtime = if installed {
1443            HarnessRuntimeState::Degraded
1444        } else {
1445            HarnessRuntimeState::Unavailable
1446        };
1447        let mut reason = (!installed).then(|| {
1448            format!(
1449                "{} is supported but `{}` was not found on PATH",
1450                descriptor.display_name,
1451                launch
1452                    .map(|launch| launch.program.as_str())
1453                    .unwrap_or("executable")
1454            )
1455        });
1456        let mut repair = (!installed).then(|| {
1457            format!(
1458                "Install {} and ensure `{}` is on PATH.",
1459                descriptor.display_name,
1460                launch
1461                    .map(|launch| launch.program.as_str())
1462                    .unwrap_or("its executable")
1463            )
1464        });
1465
1466        if installed && params.probe == HarnessProbeLevel::Handshake {
1467            let backend_params = RuntimeBackendParams {
1468                harness: descriptor.id.clone(),
1469                protocol: None,
1470                launch: None,
1471                base_url: None,
1472                policy: RuntimePolicy::Default,
1473            };
1474            match runtime_backend(&backend_params) {
1475                Ok(backend) => {
1476                    let cwd = params
1477                        .workspace
1478                        .clone()
1479                        .or_else(|| std::env::current_dir().ok())
1480                        .unwrap_or_else(|| PathBuf::from("."));
1481                    let isolated = descriptor
1482                        .runtime
1483                        .default_launch
1484                        .clone()
1485                        .and_then(|launch| {
1486                            IsolatedProbeHome::new(descriptor.id.as_str(), launch).ok()
1487                        });
1488                    let Some(isolated) = isolated else {
1489                        reason = Some(
1490                            "No-prompt runtime handshake could not create its isolated harness home."
1491                                .into(),
1492                        );
1493                        repair = Some(
1494                            "Check temporary-directory permissions, then run the handshake probe again."
1495                                .into(),
1496                        );
1497                        return LocalHarness {
1498                            id: descriptor.id,
1499                            display_name: descriptor.display_name,
1500                            supported: true,
1501                            installed,
1502                            executable: executable.map(|path| path.to_string_lossy().into_owned()),
1503                            version,
1504                            auth,
1505                            runtime,
1506                            protocol: descriptor.runtime.protocol,
1507                            capabilities: descriptor.runtime.capabilities.clone(),
1508                            effective_capabilities: descriptor.runtime.capabilities,
1509                            sessions: HarnessSessionCounts { global, workspace },
1510                            reason,
1511                            repair,
1512                        };
1513                    };
1514                    match tokio::time::timeout(
1515                        Duration::from_secs(30),
1516                        backend.start(RuntimeStartRequest {
1517                            cwd,
1518                            launch: Some(isolated.launch.clone()),
1519                        }),
1520                    )
1521                    .await
1522                    {
1523                        Ok(Ok(mut connection)) => {
1524                            match stabilize_handshake(connection.as_mut()).await {
1525                                Ok(()) => {
1526                                    auth = HarnessAuthState::Ready;
1527                                    runtime = HarnessRuntimeState::Ready;
1528                                    reason = Some(
1529                                        "No-prompt runtime handshake remained healthy through the startup stabilization window; no model request was sent."
1530                                            .into(),
1531                                    );
1532                                    repair = None;
1533                                }
1534                                Err(message) => {
1535                                    auth = if looks_like_auth_error(&message) {
1536                                        HarnessAuthState::Required
1537                                    } else if configured {
1538                                        HarnessAuthState::Configured
1539                                    } else {
1540                                        HarnessAuthState::Unknown
1541                                    };
1542                                    reason = Some(format!(
1543                                        "No-prompt runtime handshake became unhealthy during startup: {message}"
1544                                    ));
1545                                    repair = Some(if auth == HarnessAuthState::Required {
1546                                        format!(
1547                                            "Run `{}` interactively once and complete sign-in, then probe again.",
1548                                            launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
1549                                        )
1550                                    } else {
1551                                        "Run the harness directly to inspect its startup failure, then probe again."
1552                                            .into()
1553                                    });
1554                                }
1555                            }
1556                            let _ =
1557                                tokio::time::timeout(Duration::from_secs(3), connection.close())
1558                                    .await;
1559                        }
1560                        Ok(Err(error)) => {
1561                            let message = truncate_text(&error.to_string(), 500);
1562                            auth = if looks_like_auth_error(&message) {
1563                                HarnessAuthState::Required
1564                            } else if configured {
1565                                HarnessAuthState::Configured
1566                            } else {
1567                                HarnessAuthState::Unknown
1568                            };
1569                            reason = Some(format!("No-prompt runtime handshake failed: {message}"));
1570                            repair = Some(if auth == HarnessAuthState::Required {
1571                                format!(
1572                                    "Run `{}` interactively once and complete sign-in, then probe again.",
1573                                    launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
1574                                )
1575                            } else {
1576                                "Check the harness installation and run the handshake probe again."
1577                                    .into()
1578                            });
1579                        }
1580                        Err(_) => {
1581                            reason = Some(
1582                                "No-prompt runtime handshake timed out after 30 seconds.".into(),
1583                            );
1584                            repair = Some("Run the harness directly to check startup or authentication, then probe again.".into());
1585                        }
1586                    }
1587                    // Keep the isolated home alive through process teardown.
1588                    // Otherwise the compiler may release the last meaningful
1589                    // use after cloning `launch`, and a still-starting CLI can
1590                    // recreate its state directory after Drop removed it.
1591                    // Some Node-based launchers finish a short asynchronous
1592                    // installation-id write just after their parent process
1593                    // is reaped. Remove once immediately, allow that bounded
1594                    // writer to settle, then perform the authoritative pass.
1595                    let _ = isolated.cleanup();
1596                    tokio::time::sleep(Duration::from_millis(250)).await;
1597                    if let Err(error) = isolated.cleanup() {
1598                        auth = if configured {
1599                            HarnessAuthState::Configured
1600                        } else {
1601                            HarnessAuthState::Unknown
1602                        };
1603                        runtime = HarnessRuntimeState::Degraded;
1604                        reason = Some(format!(
1605                            "No-prompt runtime handshake could not remove its isolated harness home: {error}"
1606                        ));
1607                        repair = Some(
1608                            "Check temporary-directory permissions, remove the reported disposable probe home, then run the handshake again."
1609                                .into(),
1610                        );
1611                    }
1612                }
1613                Err(error) => {
1614                    reason = Some(error_message(error));
1615                }
1616            }
1617        } else if installed && configured {
1618            reason = Some("Executable and local authentication evidence found; use a handshake probe to verify readiness.".into());
1619        } else if installed && auth == HarnessAuthState::Required {
1620            reason =
1621                Some("Executable found, but no native authentication evidence is present.".into());
1622            repair = Some(format!(
1623                "Run `supercode harness login {}` to use the harness-owned sign-in flow.",
1624                descriptor.id.as_str()
1625            ));
1626        } else if installed {
1627            reason = Some("Executable found; authentication readiness is unknown until a no-prompt handshake succeeds.".into());
1628            repair =
1629                Some(format!(
1630                "Run `{}` interactively once if sign-in is required, or use `--probe handshake`.",
1631                launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
1632            ));
1633        }
1634
1635        let effective_capabilities = if installed {
1636            descriptor.runtime.capabilities.clone()
1637        } else {
1638            unavailable_capabilities()
1639        };
1640        LocalHarness {
1641            id: descriptor.id,
1642            display_name: descriptor.display_name,
1643            supported: true,
1644            installed,
1645            executable: executable.map(|path| path.to_string_lossy().into_owned()),
1646            version,
1647            auth,
1648            runtime,
1649            protocol: descriptor.runtime.protocol,
1650            capabilities: descriptor.runtime.capabilities,
1651            effective_capabilities,
1652            sessions: HarnessSessionCounts { global, workspace },
1653            reason,
1654            repair,
1655        }
1656    }
1657
1658    fn session_counts(
1659        &self,
1660        workspace: Option<&Path>,
1661        harnesses: &[HarnessId],
1662    ) -> BTreeMap<String, usize> {
1663        let mut counts = BTreeMap::new();
1664        for session in self
1665            .catalog
1666            .discover(&DiscoveryQuery {
1667                workspace: workspace.map(Path::to_path_buf),
1668                harnesses: harnesses.to_vec(),
1669                ..DiscoveryQuery::default()
1670            })
1671            .unwrap_or_default()
1672        {
1673            *counts
1674                .entry(session.locator.harness.as_str().to_string())
1675                .or_insert(0) += 1;
1676        }
1677        counts
1678    }
1679}
1680
1681#[async_trait::async_trait]
1682impl SdkService for HarnessSessionService {
1683    fn capabilities(&self) -> SdkCapabilities {
1684        SdkCapabilities::default()
1685    }
1686
1687    async fn execute(&mut self, request: SdkRequest) -> Result<Value, SdkError> {
1688        if request.operation == SdkOperation::Events {
1689            let events = self
1690                .poll_sdk_events()
1691                .await
1692                .into_iter()
1693                .map(|(_, event)| event)
1694                .collect::<Vec<_>>();
1695            return serde_json::to_value(events).map_err(|error| {
1696                SdkError::new(
1697                    SdkErrorCode::Execution,
1698                    request.operation,
1699                    error.to_string(),
1700                )
1701            });
1702        }
1703        let method = request
1704            .operation
1705            .method()
1706            .ok_or_else(|| SdkError::unsupported(request.operation))?;
1707        let result = match request.operation {
1708            SdkOperation::Discover | SdkOperation::Load | SdkOperation::Export => {
1709                self.call(method, request.params)
1710            }
1711            SdkOperation::Start
1712            | SdkOperation::Resume
1713            | SdkOperation::Input
1714            | SdkOperation::Interrupt
1715            | SdkOperation::Steer
1716            | SdkOperation::Respond
1717            | SdkOperation::Close => self.runtime_call(method, request.params).await,
1718            SdkOperation::Events => unreachable!("handled before method dispatch"),
1719        };
1720        result.map_err(|error| sdk_error(request.operation, error))
1721    }
1722
1723    async fn events(&mut self) -> Result<Vec<SdkRuntimeEvent>, SdkError> {
1724        Ok(self
1725            .poll_sdk_events()
1726            .await
1727            .into_iter()
1728            .map(|(_, event)| event)
1729            .collect())
1730    }
1731}
1732
1733#[cfg(feature = "adapter-api")]
1734struct HostedRuntimeLease {
1735    connection: HostedHarnessConnection,
1736    _host: std::sync::Arc<HostedHarnessRuntime>,
1737    _registration: LiveRuntimeRegistration,
1738    _server: crate::server::FrontendHttpServer,
1739}
1740
1741#[async_trait::async_trait]
1742#[cfg(feature = "adapter-api")]
1743impl RuntimeConnection for HostedRuntimeLease {
1744    fn handle(&self) -> &crate::RuntimeHandle {
1745        self.connection.handle()
1746    }
1747
1748    async fn send_input(&mut self, input: RuntimeInput) -> crate::Result<Option<String>> {
1749        self.connection.send_input(input).await
1750    }
1751
1752    async fn next_event(&mut self) -> crate::Result<Option<crate::HarnessEvent>> {
1753        self.connection.next_event().await
1754    }
1755
1756    async fn interrupt(&mut self) -> crate::Result<()> {
1757        self.connection.interrupt().await
1758    }
1759
1760    async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
1761        self.connection.respond(request_id, response).await
1762    }
1763
1764    async fn close(&mut self) -> crate::Result<()> {
1765        self.connection.close().await
1766    }
1767}
1768
1769async fn stabilize_handshake(connection: &mut dyn RuntimeConnection) -> Result<(), String> {
1770    let deadline = tokio::time::Instant::now() + Duration::from_secs(3);
1771    loop {
1772        let now = tokio::time::Instant::now();
1773        if now >= deadline {
1774            return Ok(());
1775        }
1776        match tokio::time::timeout(deadline - now, connection.next_event()).await {
1777            Err(_) => return Ok(()),
1778            Ok(Ok(Some(event))) => {
1779                if let Some(message) = handshake_event_failure(&event) {
1780                    return Err(truncate_text(&message, 500));
1781                }
1782            }
1783            Ok(Ok(None)) => return Err("runtime transport closed during startup".into()),
1784            Ok(Err(error)) => return Err(error.to_string()),
1785        }
1786    }
1787}
1788
1789fn handshake_event_failure(event: &crate::HarnessEvent) -> Option<String> {
1790    let detail = event
1791        .payload
1792        .get("message")
1793        .or_else(|| event.payload.get("line"))
1794        .and_then(Value::as_str)
1795        .unwrap_or(event.kind.as_str());
1796    match event.kind.as_str() {
1797        "transport_closed" => Some("runtime transport closed during startup".into()),
1798        "transport_error" => Some(format!("runtime transport error: {detail}")),
1799        "malformed_output" => Some(format!("runtime emitted non-protocol output: {detail}")),
1800        // Stderr is retained as a runtime event, but is not transport health.
1801        // Grok, for example, can log an AuthorizationRequired error from an
1802        // optional background worker while its ACP session continues to send
1803        // updates and complete prompts normally.
1804        _ => None,
1805    }
1806}
1807
1808fn projected_session_result(session: &Session, options: &SessionLoadOptions) -> Value {
1809    let total_messages = session.messages.len();
1810    let (offset, end) = projected_message_window(total_messages, options);
1811    json!({
1812        "session": projected_session_json(session, options),
1813        "summary": projected_session_summary(session, options),
1814        "window": {
1815            "has_more": offset > 0 || end < total_messages,
1816            "has_newer": end < total_messages,
1817            "has_older": offset > 0,
1818            "newer_items": normalized_item_count(&session.messages[end..]),
1819            "offset": offset,
1820            "older_items": normalized_item_count(&session.messages[..offset]),
1821            "returned": end.saturating_sub(offset),
1822            "total_messages": total_messages,
1823        }
1824    })
1825}
1826
1827fn normalized_item_count(messages: &[crate::ChatMessage]) -> usize {
1828    messages
1829        .iter()
1830        .map(|message| {
1831            let conversation = usize::from(
1832                matches!(message.role, Role::Assistant | Role::User)
1833                    && message_has_content(message),
1834            );
1835            let tool_result =
1836                usize::from(message.role == Role::Tool && message_has_content(message));
1837            conversation + tool_result + message.tool_calls().len()
1838        })
1839        .sum()
1840}
1841
1842fn projected_session_summary(session: &Session, options: &SessionLoadOptions) -> Value {
1843    let mut conversational = session.messages.iter().filter(|message| {
1844        matches!(message.role, Role::Assistant | Role::User) && message_has_content(message)
1845    });
1846    let first_message = conversational.clone().next();
1847    let last_message = conversational.next_back();
1848    let mut assistant = session
1849        .messages
1850        .iter()
1851        .filter(|message| message.role == Role::Assistant && message_has_content(message));
1852    let first_assistant_message = assistant.clone().next();
1853    let last_assistant_message = assistant.next_back();
1854    let end_of_turn = session
1855        .messages
1856        .iter()
1857        .rev()
1858        .find(|message| message.role != Role::System)
1859        .is_some_and(|message| {
1860            message.role == Role::Assistant
1861                && message_has_content(message)
1862                && message.tool_calls().is_empty()
1863        });
1864    let project = |message: Option<&crate::ChatMessage>| {
1865        message.map(|message| project_inline_media(message_json(message), options))
1866    };
1867    json!({
1868        "end_of_turn": end_of_turn,
1869        "first_assistant_message": project(first_assistant_message),
1870        "first_message": project(first_message),
1871        "last_assistant_message": project(last_assistant_message),
1872        "last_assistant_text": last_assistant_message.map(message_text).unwrap_or_default(),
1873        "last_message": project(last_message),
1874    })
1875}
1876
1877fn message_has_content(message: &crate::ChatMessage) -> bool {
1878    message
1879        .content
1880        .as_deref()
1881        .is_some_and(|content| !content.trim().is_empty())
1882        || message
1883            .content_parts
1884            .as_ref()
1885            .is_some_and(|parts| !parts.is_empty())
1886}
1887
1888fn message_text(message: &crate::ChatMessage) -> String {
1889    if let Some(content) = &message.content {
1890        return content.clone();
1891    }
1892    message
1893        .content_parts
1894        .as_ref()
1895        .into_iter()
1896        .flatten()
1897        .filter_map(|part| part.get("text").and_then(Value::as_str))
1898        .collect::<Vec<_>>()
1899        .join("\n")
1900}
1901
1902fn projected_session_json(session: &Session, options: &SessionLoadOptions) -> Value {
1903    let (offset, end) = projected_message_window(session.messages.len(), options);
1904    let messages = session.messages[offset..end]
1905        .iter()
1906        .map(|message| project_inline_media(message_json(message), options))
1907        .collect::<Vec<_>>();
1908    let subagents = if options.include_subagents.unwrap_or(true) {
1909        // The reported window describes the top-level transcript. Applying it
1910        // recursively would silently truncate subagents without returning a
1911        // window for each child. Keep their histories complete while carrying
1912        // the caller's media policy through the tree.
1913        let subagent_options = SessionLoadOptions {
1914            message_limit: None,
1915            message_offset: None,
1916            message_tail: None,
1917            ..options.clone()
1918        };
1919        session
1920            .subagents
1921            .iter()
1922            .map(|subagent| projected_session_json(subagent, &subagent_options))
1923            .collect::<Vec<_>>()
1924    } else {
1925        Vec::new()
1926    };
1927    json!({
1928        "source": match session.meta.source {
1929            SessionSource::ClaudeCode => "claude_code",
1930            SessionSource::Codex => "codex",
1931            SessionSource::Gemini => "gemini",
1932            SessionSource::Goose => "goose",
1933            SessionSource::Grok => "grok",
1934            SessionSource::Native => "native",
1935            SessionSource::OpenCode => "opencode",
1936            SessionSource::Pi => "pi",
1937        },
1938        "session_id": session.meta.session_id,
1939        "model": session.meta.model,
1940        "cwd": session.meta.cwd,
1941        "system_prompt": session.meta.system_prompt,
1942        "agent_id": session.meta.agent_id,
1943        "parent_tool_use_id": session.meta.parent_tool_use_id,
1944        "lineage": session.meta.lineage,
1945        "messages": messages,
1946        "subagents": subagents,
1947        "raw_record_count": session.raw.len(),
1948        "parse_error_lines": session.parse_error_lines,
1949    })
1950}
1951
1952fn projected_message_window(total: usize, options: &SessionLoadOptions) -> (usize, usize) {
1953    if let Some(tail) = options.message_tail {
1954        return (total.saturating_sub(tail), total);
1955    }
1956    let offset = options.message_offset.unwrap_or(0).min(total);
1957    let end = options
1958        .message_limit
1959        .map(|limit| offset.saturating_add(limit).min(total))
1960        .unwrap_or(total);
1961    (offset, end)
1962}
1963
1964fn project_inline_media(mut message: Value, options: &SessionLoadOptions) -> Value {
1965    let Some(parts) = message.get_mut("content").and_then(Value::as_array_mut) else {
1966        return message;
1967    };
1968    for part in parts {
1969        let Some(url) = part
1970            .get("image_url")
1971            .and_then(|image| image.get("url"))
1972            .and_then(Value::as_str)
1973        else {
1974            continue;
1975        };
1976        let Some(rest) = url.strip_prefix("data:") else {
1977            continue;
1978        };
1979        let Some((media_type, encoded)) = rest.split_once(";base64,") else {
1980            continue;
1981        };
1982        let padding = usize::from(encoded.ends_with('=')) + usize::from(encoded.ends_with("=="));
1983        let decoded_bytes = encoded.len().saturating_mul(3) / 4;
1984        let decoded_bytes = decoded_bytes.saturating_sub(padding);
1985        let should_elide = matches!(options.inline_media, InlineMediaMode::Metadata)
1986            || options
1987                .max_inline_media_bytes
1988                .is_some_and(|limit| decoded_bytes > limit);
1989        if should_elide {
1990            *part = json!({
1991                "type": "media_reference",
1992                "media_type": media_type,
1993                "encoding": "base64",
1994                "encoded_bytes": encoded.len(),
1995                "decoded_bytes": decoded_bytes,
1996                "omitted": true,
1997            });
1998        }
1999    }
2000    message
2001}
2002
2003#[derive(Deserialize)]
2004struct LocatorParams {
2005    locator: SessionLocator,
2006    /// Optional fidelity for the READ surfaces (`sessions.load`,
2007    /// `sessions.follow`).
2008    ///
2009    /// Omitted means [`Fidelity::Semantic`]: these two methods only ever
2010    /// produce a read-only view, and a compacted or resumed-across-files
2011    /// transcript — the everyday shape of a long Claude Code session — has no
2012    /// losslessly reconstructable record graph, so refusing to render it made
2013    /// the mirror unusable rather than accurate. A caller that intends to
2014    /// CONTINUE from what it reads asks for a lossless level explicitly and
2015    /// gets the strict refusal back. Every other method (export, translate,
2016    /// branch, handoff, resume_instructions) is lossless-only and has no
2017    /// such knob.
2018    #[serde(default)]
2019    fidelity: Option<Fidelity>,
2020    /// Optional bounded frontend projection. Absent preserves the historical
2021    /// complete-session read contract.
2022    #[serde(default)]
2023    view: Option<SessionReadView>,
2024}
2025
2026#[derive(Deserialize)]
2027struct SessionReadView {
2028    /// Number of trailing normalized messages to return. Zero is treated as
2029    /// one so a caller cannot accidentally request an unbounded empty mode.
2030    #[serde(default)]
2031    tail_messages: Option<usize>,
2032    /// Whether Claude Code child transcripts belong in this view. The
2033    /// frontend default is false; the legacy no-view path remains true.
2034    #[serde(default)]
2035    include_subagents: bool,
2036    /// Preserve human-visible native history across model-context compaction.
2037    #[serde(default)]
2038    display_history: bool,
2039    /// Bound each individual text field so a single tool result cannot turn a
2040    /// small message window into a hundred-megabyte RPC response.
2041    #[serde(default)]
2042    max_message_chars: Option<usize>,
2043}
2044
2045impl LocatorParams {
2046    fn read_fidelity(&self) -> Fidelity {
2047        self.fidelity.unwrap_or(Fidelity::Semantic)
2048    }
2049
2050    fn include_subagents(&self) -> bool {
2051        self.view
2052            .as_ref()
2053            .map(|view| view.include_subagents)
2054            .unwrap_or(true)
2055    }
2056
2057    fn tail_messages(&self) -> Option<usize> {
2058        self.view
2059            .as_ref()
2060            .and_then(|view| view.tail_messages)
2061            .map(|limit| limit.clamp(1, 5_000))
2062    }
2063
2064    fn display_history(&self) -> bool {
2065        self.view.as_ref().is_some_and(|view| view.display_history)
2066    }
2067
2068    fn max_message_chars(&self) -> Option<usize> {
2069        self.view
2070            .as_ref()
2071            .and_then(|view| view.max_message_chars)
2072            .map(|limit| limit.clamp(256, 64_000))
2073    }
2074
2075    fn bound_session(&self, session: &mut Session) {
2076        bound_session_view(session, self.tail_messages(), self.max_message_chars());
2077    }
2078}
2079
2080#[derive(Debug, Clone, Copy, Default, Deserialize)]
2081#[serde(rename_all = "snake_case")]
2082enum InlineMediaMode {
2083    #[default]
2084    Full,
2085    Metadata,
2086}
2087
2088#[derive(Debug, Clone, Default, Deserialize)]
2089#[serde(default)]
2090struct SessionLoadOptions {
2091    include_subagents: Option<bool>,
2092    inline_media: InlineMediaMode,
2093    max_inline_media_bytes: Option<usize>,
2094    message_limit: Option<usize>,
2095    message_offset: Option<usize>,
2096    message_tail: Option<usize>,
2097}
2098
2099impl SessionLoadOptions {
2100    fn validate(&self) -> std::result::Result<(), ServiceError> {
2101        if self.message_tail.is_some()
2102            && (self.message_limit.is_some() || self.message_offset.is_some())
2103        {
2104            return Err(ServiceError::InvalidParams(
2105                "sessions.load options.message_tail cannot be combined with message_limit or message_offset"
2106                    .into(),
2107            ));
2108        }
2109        Ok(())
2110    }
2111}
2112
2113#[derive(Deserialize)]
2114struct LoadSessionParams {
2115    #[serde(flatten)]
2116    read: LocatorParams,
2117    #[serde(default)]
2118    options: Option<SessionLoadOptions>,
2119}
2120
2121#[derive(Deserialize)]
2122struct UnfollowParams {
2123    subscription: String,
2124}
2125
2126#[derive(Deserialize)]
2127struct ActivitySubscribeParams {
2128    locators: Vec<SessionLocator>,
2129    #[serde(default)]
2130    homes: crate::HarnessHomes,
2131}
2132
2133#[derive(Deserialize)]
2134struct MessageSessionParams {
2135    locator: SessionLocator,
2136    text: String,
2137    /// Same storage roots discovery accepts, so a caller (and a test) can
2138    /// point the live-session registry somewhere other than `$HOME`.
2139    #[serde(default)]
2140    homes: crate::HarnessHomes,
2141}
2142
2143#[derive(Deserialize)]
2144#[serde(deny_unknown_fields)]
2145struct HarnessSettingsParams {
2146    harness: String,
2147}
2148
2149#[derive(Deserialize)]
2150#[serde(deny_unknown_fields)]
2151struct ConfigureHarnessParams {
2152    harness: String,
2153    #[serde(default)]
2154    changes: Vec<crate::HarnessSettingChange>,
2155    #[serde(default)]
2156    expected_revision: Option<String>,
2157}
2158
2159fn claude_inbound_controls_or_error(homes: &crate::HarnessHomes) -> (Value, Value) {
2160    match crate::inspect_harness_interop_settings(homes, HarnessId::CLAUDE_CODE) {
2161        Ok(report) => (
2162            serde_json::to_value(report).unwrap_or(Value::Null),
2163            Value::Null,
2164        ),
2165        Err(error) => (
2166            Value::Null,
2167            Value::String(format!(
2168                "Supercode could not inspect Claude Code inbound controls: {error}"
2169            )),
2170        ),
2171    }
2172}
2173
2174/// Deliver `text` into a session that is running right now, or say why not.
2175///
2176/// A refusal is a RESULT, not a JSON-RPC error: "that session is persisted
2177/// only" is an answer about the session, which a mirror renders next to the
2178/// transcript, and this service's error envelope carries no structured data
2179/// field a machine-readable reason could survive in.
2180///
2181/// `delivered_to_bus` is the honest ceiling of what the courier proves. The
2182/// message reached the receiving session's inbox; whether that session ever
2183/// reads it is governed by ITS OWN inbound controls (`crossSessionInbound`,
2184/// approval dialogs), which Supercode neither sees nor overrides.
2185#[cfg(feature = "adapter-api")]
2186async fn message_live_session(
2187    params: &MessageSessionParams,
2188    runner: &dyn crate::claude_peer::CourierRunner,
2189) -> Value {
2190    if params.locator.harness.as_str() != HarnessId::CLAUDE_CODE {
2191        return json!({
2192            "delivered_to_bus": false,
2193            "refusal": {
2194                "reason": crate::claude_peer::ClaudePeerRefusal::HarnessUnsupported.as_str(),
2195                "message": format!(
2196                    "`{}` does not publish a live-session registry; only claude-code sessions can be messaged in place",
2197                    params.locator.harness.as_str()
2198                ),
2199            },
2200        });
2201    }
2202    let (inbound_controls, inbound_controls_error) =
2203        claude_inbound_controls_or_error(&params.homes);
2204    match crate::claude_peer::message_claude_peer(
2205        &params.homes,
2206        &params.locator.session_id,
2207        &params.text,
2208        runner,
2209    )
2210    .await
2211    {
2212        Ok(delivery) => json!({
2213            "delivered_to_bus": true,
2214            "target": {
2215                "session_id": delivery.target.session_id,
2216                "name": delivery.target.name,
2217                "pid": delivery.target.pid,
2218                "cwd": delivery.target.cwd,
2219                "status": delivery.target.status.map(|status| status.as_str()),
2220            },
2221            "courier": {
2222                "model": crate::claude_peer::COURIER_MODEL,
2223                "report": delivery.courier_report,
2224            },
2225            "inbound_controls": inbound_controls,
2226            "inbound_controls_error": inbound_controls_error,
2227        }),
2228        Err(refusal) => json!({
2229            "delivered_to_bus": false,
2230            "refusal": {"reason": refusal.reason.as_str(), "message": refusal.message},
2231            "inbound_controls": inbound_controls,
2232            "inbound_controls_error": inbound_controls_error,
2233        }),
2234    }
2235}
2236
2237/// Source identity of one follow subscription, plus the last lifecycle state
2238/// already reported on it. The follower itself stays purely persistence-facing.
2239// Only the adapter-api poll reads these; the subscription bookkeeping itself is
2240// shared by both builds.
2241#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
2242struct FollowedSource {
2243    harness: String,
2244    session_id: String,
2245    reported: Option<String>,
2246}
2247
2248#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
2249struct ActivitySubscription {
2250    locators: Vec<SessionLocator>,
2251    homes: crate::HarnessHomes,
2252    reported: BTreeMap<(String, String), crate::SessionActivity>,
2253}
2254
2255fn peers_for_descriptors(
2256    descriptors: &[SessionDescriptor],
2257    homes: &HarnessHomes,
2258) -> Vec<crate::claude_peer::ClaudePeerSession> {
2259    if descriptors
2260        .iter()
2261        .any(|session| session.locator.harness.as_str() == HarnessId::CLAUDE_CODE)
2262    {
2263        crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
2264    } else {
2265        Vec::new()
2266    }
2267}
2268
2269/// Add the live address that makes an indexed row behaviorally equivalent to a discovered row.
2270///
2271/// The durable index owns only persistence metadata. Live endpoints remain projections: every
2272/// message/attach operation revalidates its authority, so publishing one here never trusts a stale
2273/// browser-held handle. Reading the Claude registry once per batch keeps this O(peers + rows).
2274fn live_descriptor_value(
2275    session: &SessionDescriptor,
2276    peers: &[crate::claude_peer::ClaudePeerSession],
2277) -> std::result::Result<Value, ServiceError> {
2278    let mut value = serde_json::to_value(session)
2279        .map_err(|error| ServiceError::Operation(error.to_string()))?;
2280    if let Some(workspace) = &session.cwd {
2281        let source = LiveRuntimeSource {
2282            harness: session.locator.harness.as_str().to_string(),
2283            session_id: session.locator.session_id.clone(),
2284            workspace: workspace.clone(),
2285        };
2286        if let Some(endpoint) = discover_live_runtime(&source)
2287            .map_err(|error| ServiceError::Operation(error.to_string()))?
2288        {
2289            value["live_endpoint"] = json!(endpoint.as_str());
2290        }
2291    }
2292    if value.get("live_endpoint").is_none() {
2293        if let Some(peer) = peers.iter().find(|peer| {
2294            session.locator.harness.as_str() == HarnessId::CLAUDE_CODE
2295                && peer.session_id == session.locator.session_id
2296        }) {
2297            value["live_endpoint"] = json!(peer.endpoint().as_str());
2298        }
2299    }
2300    Ok(value)
2301}
2302
2303fn live_index_changes(
2304    changes: Vec<crate::session_index::SessionIndexChange>,
2305    homes: &HarnessHomes,
2306) -> std::result::Result<Vec<Value>, ServiceError> {
2307    use crate::session_index::SessionIndexChange;
2308    let has_claude = changes.iter().any(|change| match change {
2309        SessionIndexChange::Added { descriptor } | SessionIndexChange::Updated { descriptor } => {
2310            descriptor.locator.harness.as_str() == HarnessId::CLAUDE_CODE
2311        }
2312        SessionIndexChange::Removed { .. } => false,
2313    });
2314    let peers = if has_claude {
2315        crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
2316    } else {
2317        Vec::new()
2318    };
2319    changes
2320        .into_iter()
2321        .map(|change| match change {
2322            SessionIndexChange::Added { descriptor } => Ok(json!({
2323                "kind": "added",
2324                "descriptor": live_descriptor_value(&descriptor, &peers)?,
2325            })),
2326            SessionIndexChange::Updated { descriptor } => Ok(json!({
2327                "kind": "updated",
2328                "descriptor": live_descriptor_value(&descriptor, &peers)?,
2329            })),
2330            SessionIndexChange::Removed { key } => Ok(json!({
2331                "kind": "removed",
2332                "key": key,
2333            })),
2334        })
2335        .collect()
2336}
2337
2338fn legacy_live_status(activity: &crate::SessionActivity) -> Option<&'static str> {
2339    use crate::{SessionPresence, SessionTurnState};
2340    match (activity.presence, activity.turn) {
2341        (SessionPresence::Persisted, _) => None,
2342        (SessionPresence::Running, SessionTurnState::Working) => Some("busy"),
2343        (SessionPresence::Running, SessionTurnState::Idle) => Some("idle"),
2344        (SessionPresence::Running, _) | (SessionPresence::ShuttingDown, _) => Some("running"),
2345    }
2346}
2347
2348#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
2349#[serde(rename_all = "kebab-case")]
2350enum TransferFormat {
2351    ClaudeCode,
2352    Codex,
2353    #[serde(rename = "opencode", alias = "open-code")]
2354    OpenCode,
2355    Pi,
2356    Grok,
2357    Gemini,
2358    Goose,
2359}
2360
2361impl TransferFormat {
2362    fn id(self) -> &'static str {
2363        match self {
2364            Self::ClaudeCode => HarnessId::CLAUDE_CODE,
2365            Self::Codex => HarnessId::CODEX,
2366            Self::OpenCode => HarnessId::OPENCODE,
2367            Self::Pi => HarnessId::PI,
2368            Self::Grok => HarnessId::GROK,
2369            Self::Gemini => HarnessId::GEMINI,
2370            Self::Goose => HarnessId::GOOSE,
2371        }
2372    }
2373}
2374
2375impl From<TransferFormat> for SessionFormat {
2376    fn from(value: TransferFormat) -> Self {
2377        match value {
2378            TransferFormat::ClaudeCode => Self::ClaudeCode,
2379            TransferFormat::Codex => Self::Codex,
2380            TransferFormat::OpenCode => Self::OpenCode,
2381            TransferFormat::Pi => Self::Pi,
2382            TransferFormat::Grok => Self::Grok,
2383            TransferFormat::Gemini => Self::Gemini,
2384            TransferFormat::Goose => Self::Goose,
2385        }
2386    }
2387}
2388
2389#[derive(Deserialize)]
2390struct ImportSessionParams {
2391    source_harness: TransferFormat,
2392    content: String,
2393}
2394
2395#[derive(Deserialize)]
2396struct ExportSessionParams {
2397    locator: SessionLocator,
2398    target_harness: TransferFormat,
2399}
2400
2401#[derive(Deserialize)]
2402struct ReduceSessionParams {
2403    locator: SessionLocator,
2404    target_harness: TransferFormat,
2405    #[serde(default = "default_keep_last")]
2406    keep_last: usize,
2407}
2408
2409fn default_keep_last() -> usize {
2410    6
2411}
2412
2413#[derive(Deserialize)]
2414struct BranchSessionParams {
2415    locator: SessionLocator,
2416    #[serde(default)]
2417    target_harness: Option<TransferFormat>,
2418}
2419
2420#[derive(Deserialize)]
2421struct HandoffSessionParams {
2422    locator: SessionLocator,
2423    target_harness: TransferFormat,
2424    #[serde(default)]
2425    cwd: Option<PathBuf>,
2426}
2427
2428#[derive(Debug, Clone, Copy, Default, Deserialize)]
2429#[serde(rename_all = "snake_case")]
2430enum ResumePolicy {
2431    #[default]
2432    Default,
2433    Yolo,
2434}
2435
2436#[derive(Deserialize)]
2437struct ResumeInstructionsParams {
2438    locator: SessionLocator,
2439    #[serde(default)]
2440    cwd: Option<PathBuf>,
2441    #[serde(default)]
2442    policy: ResumePolicy,
2443}
2444
2445#[derive(Serialize)]
2446struct SessionArtifact {
2447    source_harness: HarnessId,
2448    target_harness: &'static str,
2449    session_id: Option<String>,
2450    content: String,
2451    suggested_filename: String,
2452    files: Vec<SessionArtifactFile>,
2453    fidelity: Fidelity,
2454    residue: Vec<String>,
2455}
2456
2457#[derive(Serialize)]
2458struct SessionArtifactFile {
2459    path: String,
2460    content: String,
2461    role: ArtifactFileRole,
2462}
2463
2464#[derive(Serialize)]
2465#[serde(rename_all = "snake_case")]
2466enum ArtifactFileRole {
2467    Primary,
2468    Subagent,
2469    Bundle,
2470    SourceRecovery,
2471}
2472
2473#[derive(Serialize)]
2474struct StructuredLaunch {
2475    cwd: PathBuf,
2476    program: String,
2477    arguments: Vec<String>,
2478    env: BTreeMap<String, String>,
2479}
2480
2481struct HandoffInstructions {
2482    launch: StructuredLaunch,
2483    materialize: Option<StructuredLaunch>,
2484    requires_materialization: bool,
2485    note: String,
2486}
2487
2488#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
2489#[serde(rename_all = "snake_case")]
2490enum HarnessProbeLevel {
2491    #[default]
2492    Passive,
2493    Handshake,
2494}
2495
2496#[derive(Default, Deserialize)]
2497#[serde(default)]
2498struct HarnessInventoryParams {
2499    harness: Option<HarnessId>,
2500    harnesses: Vec<HarnessId>,
2501    workspace: Option<PathBuf>,
2502    probe: HarnessProbeLevel,
2503    include_sessions: bool,
2504    /// Omit subprocess-based `--version` calls when a latency-sensitive UI only needs readiness.
2505    skip_versions: bool,
2506}
2507
2508#[derive(Deserialize)]
2509struct HarnessAuthenticationParams {
2510    harness: HarnessId,
2511}
2512
2513#[derive(Deserialize)]
2514struct BeginHarnessAuthenticationParams {
2515    harness: HarnessId,
2516    #[serde(default = "local_browser_authentication_environment")]
2517    environment: crate::HarnessAuthenticationEnvironment,
2518    #[serde(default)]
2519    method: Option<crate::HarnessAuthenticationMethodId>,
2520    #[serde(default)]
2521    cwd: Option<PathBuf>,
2522}
2523
2524fn local_browser_authentication_environment() -> crate::HarnessAuthenticationEnvironment {
2525    crate::HarnessAuthenticationEnvironment::LocalBrowser
2526}
2527
2528#[derive(Serialize)]
2529struct HarnessInventoryReport {
2530    probe: HarnessProbeLevel,
2531    workspace: Option<PathBuf>,
2532    harnesses: Vec<LocalHarness>,
2533}
2534
2535#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2536#[serde(rename_all = "snake_case")]
2537enum HarnessAuthState {
2538    Ready,
2539    Configured,
2540    Required,
2541    Unknown,
2542}
2543
2544#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
2545#[serde(rename_all = "snake_case")]
2546enum HarnessRuntimeState {
2547    Ready,
2548    Degraded,
2549    Unavailable,
2550}
2551
2552#[derive(Serialize)]
2553struct HarnessSessionCounts {
2554    global: Option<usize>,
2555    workspace: Option<usize>,
2556}
2557
2558#[derive(Serialize)]
2559struct LocalHarness {
2560    id: HarnessId,
2561    display_name: String,
2562    supported: bool,
2563    installed: bool,
2564    executable: Option<String>,
2565    version: Option<String>,
2566    auth: HarnessAuthState,
2567    runtime: HarnessRuntimeState,
2568    protocol: String,
2569    capabilities: crate::RuntimeCapabilities,
2570    effective_capabilities: crate::RuntimeCapabilities,
2571    sessions: HarnessSessionCounts,
2572    reason: Option<String>,
2573    repair: Option<String>,
2574}
2575
2576#[derive(Clone, Deserialize)]
2577struct RuntimeBackendParams {
2578    harness: HarnessId,
2579    #[serde(default)]
2580    protocol: Option<String>,
2581    #[serde(default)]
2582    launch: Option<RuntimeLaunch>,
2583    #[serde(default)]
2584    base_url: Option<String>,
2585    #[serde(default)]
2586    policy: RuntimePolicy,
2587}
2588
2589#[derive(Debug, Clone, Copy, Default, Deserialize)]
2590#[serde(rename_all = "snake_case")]
2591enum RuntimePolicy {
2592    #[default]
2593    Default,
2594    Yolo,
2595}
2596
2597#[derive(Deserialize)]
2598struct RuntimeStartParams {
2599    #[serde(flatten)]
2600    backend: RuntimeBackendParams,
2601    cwd: PathBuf,
2602}
2603
2604#[derive(Deserialize)]
2605struct RuntimeAttachParams {
2606    #[serde(flatten)]
2607    backend: RuntimeBackendParams,
2608    runtime_id: String,
2609    #[serde(default)]
2610    cwd: Option<PathBuf>,
2611}
2612
2613#[derive(Deserialize)]
2614struct RuntimeConnectionParams {
2615    connection: String,
2616}
2617
2618#[derive(Deserialize)]
2619struct RuntimeInputParams {
2620    connection: String,
2621    text: String,
2622    #[serde(default)]
2623    image_urls: Vec<String>,
2624}
2625
2626const MAX_RUNTIME_IMAGES: usize = 4;
2627const MAX_RUNTIME_IMAGE_URL_BYTES: usize = 12 * 1024 * 1024;
2628const MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL: usize = 32 * 1024 * 1024;
2629
2630fn validate_runtime_image_urls(image_urls: Vec<String>) -> Result<Vec<String>, ServiceError> {
2631    if image_urls.len() > MAX_RUNTIME_IMAGES {
2632        return Err(ServiceError::InvalidParams(format!(
2633            "a runtime prompt accepts at most {MAX_RUNTIME_IMAGES} images"
2634        )));
2635    }
2636    let mut total = 0usize;
2637    for url in &image_urls {
2638        if !(url.starts_with("data:image/")
2639            || url.starts_with("https://")
2640            || url.starts_with("http://"))
2641        {
2642            return Err(ServiceError::InvalidParams(
2643                "runtime images must be image data URLs or HTTP(S) URLs".into(),
2644            ));
2645        }
2646        if url.len() > MAX_RUNTIME_IMAGE_URL_BYTES {
2647            return Err(ServiceError::InvalidParams(format!(
2648                "one runtime image exceeds the {MAX_RUNTIME_IMAGE_URL_BYTES}-byte encoded limit"
2649            )));
2650        }
2651        total = total.saturating_add(url.len());
2652    }
2653    if total > MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL {
2654        return Err(ServiceError::InvalidParams(format!(
2655            "runtime images exceed the {MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL}-byte encoded total limit"
2656        )));
2657    }
2658    Ok(image_urls)
2659}
2660
2661#[derive(Deserialize)]
2662struct RuntimeRespondParams {
2663    connection: String,
2664    request_id: Value,
2665    response: Value,
2666}
2667
2668fn default_reduction_store_root() -> PathBuf {
2669    if let Some(root) = std::env::var_os("SUPERCODE_HOME") {
2670        return PathBuf::from(root).join("sessions");
2671    }
2672    if let Some(home) = std::env::var_os("HOME") {
2673        return PathBuf::from(home).join(".supercode").join("sessions");
2674    }
2675    PathBuf::from(".supercode").join("sessions")
2676}
2677
2678fn messages_jsonl(messages: &[crate::ChatMessage]) -> std::result::Result<String, ServiceError> {
2679    let mut output = String::new();
2680    for message in messages {
2681        output.push_str(
2682            &serde_json::to_string(message)
2683                .map_err(|error| ServiceError::Operation(error.to_string()))?,
2684        );
2685        output.push('\n');
2686    }
2687    Ok(output)
2688}
2689
2690fn parse_messages_jsonl(
2691    content: &str,
2692) -> std::result::Result<Vec<crate::ChatMessage>, ServiceError> {
2693    content
2694        .lines()
2695        .enumerate()
2696        .filter(|(_, line)| !line.trim().is_empty())
2697        .map(|(index, line)| {
2698            serde_json::from_str::<crate::ChatMessage>(line).map_err(|error| {
2699                ServiceError::Operation(format!(
2700                    "reduced transcript line {} is invalid: {error}",
2701                    index + 1
2702                ))
2703            })
2704        })
2705        .collect()
2706}
2707
2708fn reduced_bootstrap_prompt(
2709    source: &SessionLocator,
2710    target: TransferFormat,
2711    view_jsonl: &str,
2712    sidecar_path: &Path,
2713    reduction_log_path: &Path,
2714) -> String {
2715    format!(
2716        "Continue the work from this losslessly reduced {source_harness} session in {target_harness}.\n\
2717         \n\
2718         The bounded working transcript is below. Treat reduction markers as transparent placeholders, not missing work. If a detail behind a marker is needed, use ordinary file-reading/search tools against the full Supercode sidecar at `{sidecar}` and its reduction index at `{log}`. Do not guess hidden content. Both files were reloaded and verified before this continuation was issued.\n\
2719         \n\
2720         <supercode-reduced-session source-session=\"{source_id}\">\n\
2721         {view_jsonl}\
2722         </supercode-reduced-session>\n\
2723         \n\
2724         Resume from the latest unresolved user request and preserve the source session's decisions and constraints.",
2725        source_harness = source.harness.as_str(),
2726        target_harness = target.id(),
2727        sidecar = sidecar_path.display(),
2728        log = reduction_log_path.display(),
2729        source_id = source.session_id,
2730    )
2731}
2732
2733fn session_artifact(
2734    locator: &SessionLocator,
2735    session: &Session,
2736    target: TransferFormat,
2737) -> std::result::Result<SessionArtifact, ServiceError> {
2738    session_artifact_with_id(locator, session, target, None)
2739}
2740
2741fn session_artifact_with_id(
2742    locator: &SessionLocator,
2743    session: &Session,
2744    target: TransferFormat,
2745    target_session_id: Option<&str>,
2746) -> std::result::Result<SessionArtifact, ServiceError> {
2747    let format: SessionFormat = target.into();
2748    let diagonal = format.source() == session.meta.source;
2749    let has_appended_turns = session
2750        .imported_message_count
2751        .is_some_and(|imported| imported < session.messages.len());
2752    let content = if let Some(id) = target_session_id {
2753        if diagonal && format != SessionFormat::OpenCode {
2754            session
2755                .to_jsonl_spliced(format, Some(id))
2756                .map_err(operation)?
2757        } else {
2758            let mut rewritten = session.clone();
2759            rewritten.meta.session_id = Some(id.to_string());
2760            rewritten.to_jsonl(format).map_err(operation)?
2761        }
2762    } else if diagonal && session.raw_is_verbatim && !has_appended_turns {
2763        session.raw_verbatim()
2764    } else if diagonal {
2765        session.to_jsonl_spliced(format, None).map_err(operation)?
2766    } else {
2767        session.to_jsonl(format).map_err(operation)?
2768    };
2769    let stem = sanitize_filename(
2770        target_session_id
2771            .or(session.meta.session_id.as_deref())
2772            .unwrap_or(&locator.session_id),
2773    );
2774    let suggested_filename = if diagonal && target == TransferFormat::Grok {
2775        "chat_history.jsonl".to_string()
2776    } else if target == TransferFormat::Goose {
2777        format!("{stem}.goose.json")
2778    } else {
2779        format!("{stem}.{}.jsonl", target.id())
2780    };
2781    let mut files = vec![SessionArtifactFile {
2782        path: suggested_filename.clone(),
2783        content: content.clone(),
2784        role: ArtifactFileRole::Primary,
2785    }];
2786    if target == TransferFormat::ClaudeCode {
2787        let bundle_stem = Path::new(&suggested_filename)
2788            .file_stem()
2789            .and_then(|stem| stem.to_str())
2790            .unwrap_or(&stem);
2791        let mut child_paths = BTreeSet::new();
2792        for (index, subagent) in session.subagents.iter().enumerate() {
2793            let agent_id = subagent
2794                .meta
2795                .agent_id
2796                .as_deref()
2797                .map(|id| id.strip_prefix("agent-").unwrap_or(id))
2798                .map(sanitize_filename)
2799                .filter(|id| !id.is_empty())
2800                .unwrap_or_else(|| format!("subagent-{}", index + 1));
2801            let child_has_appended_turns = subagent
2802                .imported_message_count
2803                .is_some_and(|imported| imported < subagent.messages.len());
2804            let child_content = if target_session_id.is_none()
2805                && subagent.meta.source == SessionSource::ClaudeCode
2806                && subagent.raw_is_verbatim
2807                && !child_has_appended_turns
2808            {
2809                subagent.raw_verbatim()
2810            } else if subagent.meta.source == SessionSource::ClaudeCode {
2811                subagent
2812                    .to_jsonl_spliced(SessionFormat::ClaudeCode, target_session_id)
2813                    .map_err(operation)?
2814            } else {
2815                let mut child = subagent.clone();
2816                if let Some(id) = target_session_id {
2817                    child.meta.session_id = Some(id.to_string());
2818                }
2819                child
2820                    .to_jsonl(SessionFormat::ClaudeCode)
2821                    .map_err(operation)?
2822            };
2823            let path = format!("{bundle_stem}/subagents/agent-{agent_id}.jsonl");
2824            if !child_paths.insert(path.clone()) {
2825                return Err(ServiceError::Operation(format!(
2826                    "Claude subagent ids collide at artifact path `{path}`"
2827                )));
2828            }
2829            files.push(SessionArtifactFile {
2830                path,
2831                content: child_content,
2832                role: ArtifactFileRole::Subagent,
2833            });
2834        }
2835    }
2836    if diagonal && target == TransferFormat::Grok {
2837        append_grok_bundle_files(locator, "", ArtifactFileRole::Bundle, &mut files)?;
2838    }
2839    if !diagonal || !session.raw_is_verbatim {
2840        files.push(SessionArtifactFile {
2841            path: "recovery/source.supercode.jsonl".into(),
2842            content: session.to_native_jsonl(),
2843            role: ArtifactFileRole::SourceRecovery,
2844        });
2845        for (index, subagent) in session.subagents.iter().enumerate() {
2846            let id = subagent
2847                .meta
2848                .agent_id
2849                .as_deref()
2850                .map(sanitize_filename)
2851                .unwrap_or_else(|| format!("subagent-{}", index + 1));
2852            files.push(SessionArtifactFile {
2853                path: format!("recovery/subagents/{id}.supercode.jsonl"),
2854                content: subagent.to_native_jsonl(),
2855                role: ArtifactFileRole::SourceRecovery,
2856            });
2857        }
2858    }
2859    if !diagonal && session.meta.source == SessionSource::Grok {
2860        append_grok_bundle_files(
2861            locator,
2862            "recovery/grok/",
2863            ArtifactFileRole::SourceRecovery,
2864            &mut files,
2865        )?;
2866    }
2867    let (fidelity, residue) = if diagonal
2868        && target_session_id.is_none()
2869        && session.raw_is_verbatim
2870        && !has_appended_turns
2871    {
2872        (Fidelity::ByteLossless, Vec::new())
2873    } else if diagonal && !(target_session_id.is_some() && target == TransferFormat::OpenCode) {
2874        (
2875            Fidelity::ValueLossless,
2876            vec![if target_session_id.is_some() {
2877                "target identity was rewritten, so the artifact intentionally differs from source bytes".into()
2878            } else {
2879                "source storage was reconstructed as a native-value-equivalent export; original container bytes were not captured".into()
2880            }],
2881        )
2882    } else {
2883        (
2884            Fidelity::Semantic,
2885            vec!["target schema has no portable slot for every source-native record and metadata field".into()],
2886        )
2887    };
2888    Ok(SessionArtifact {
2889        source_harness: locator.harness.clone(),
2890        target_harness: target.id(),
2891        session_id: target_session_id
2892            .map(str::to_string)
2893            .or_else(|| session.meta.session_id.clone()),
2894        content,
2895        suggested_filename,
2896        files,
2897        fidelity,
2898        residue,
2899    })
2900}
2901
2902fn append_grok_bundle_files(
2903    locator: &SessionLocator,
2904    prefix: &str,
2905    role: ArtifactFileRole,
2906    files: &mut Vec<SessionArtifactFile>,
2907) -> std::result::Result<(), ServiceError> {
2908    let primary = locator.storage.path();
2909    if primary.file_name().and_then(|name| name.to_str()) != Some("chat_history.jsonl") {
2910        return Err(ServiceError::Operation(format!(
2911            "Grok bundle locator must name chat_history.jsonl, got {}",
2912            primary.display()
2913        )));
2914    }
2915    let parent = primary.parent().ok_or_else(|| {
2916        ServiceError::Operation("Grok chat_history.jsonl has no session directory".into())
2917    })?;
2918    for name in ["summary.json", "updates.jsonl"] {
2919        let path = parent.join(name);
2920        let metadata = match std::fs::symlink_metadata(&path) {
2921            Ok(metadata) => metadata,
2922            Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
2923            Err(error) => return Err(ServiceError::Operation(error.to_string())),
2924        };
2925        if metadata.file_type().is_symlink() || !metadata.is_file() {
2926            return Err(ServiceError::Operation(format!(
2927                "refusing non-regular Grok bundle member {}",
2928                path.display()
2929            )));
2930        }
2931        let content = std::fs::read_to_string(&path).map_err(|error| {
2932            ServiceError::Operation(format!(
2933                "Grok bundle member {} is not representable as UTF-8: {error}",
2934                path.display()
2935            ))
2936        })?;
2937        files.push(SessionArtifactFile {
2938            path: format!("{prefix}{name}"),
2939            content,
2940            role: match role {
2941                ArtifactFileRole::Bundle => ArtifactFileRole::Bundle,
2942                _ => ArtifactFileRole::SourceRecovery,
2943            },
2944        });
2945    }
2946    Ok(())
2947}
2948
2949fn handoff_artifact(
2950    locator: &SessionLocator,
2951    session: &Session,
2952    target: TransferFormat,
2953    cwd: &Path,
2954) -> std::result::Result<SessionArtifact, ServiceError> {
2955    if target != TransferFormat::Grok {
2956        let target_session_id = target_session_id(target);
2957        return session_artifact_with_id(locator, session, target, Some(&target_session_id));
2958    }
2959
2960    // Stock Grok's importer accepts Claude/Codex transcripts and materializes its own
2961    // multi-file session bundle. A synthesized Grok chat_history.jsonl alone is not a
2962    // resumable handoff because updates.jsonl is the authoritative restore log.
2963    let mut importable = session.clone();
2964    // The Claude importer validates sessionId as a UUID. Source harness identities
2965    // are not portable (OpenCode, for example, uses `ses_...`), and a handoff must
2966    // not overwrite an existing target session when the source already uses UUIDs.
2967    // Mint a distinct target identity and still bind the importer-returned ID at
2968    // launch time because the importer remains the authority on materialization.
2969    importable.meta.session_id = Some(target_session_id(TransferFormat::ClaudeCode));
2970    importable.meta.cwd = Some(if cwd.is_absolute() {
2971        cwd.to_path_buf()
2972    } else {
2973        std::env::current_dir()
2974            .map_err(|error| ServiceError::Operation(error.to_string()))?
2975            .join(cwd)
2976    });
2977    let content = importable
2978        .to_jsonl(SessionFormat::ClaudeCode)
2979        .map_err(operation)?;
2980    let stem = sanitize_filename(
2981        importable
2982            .meta
2983            .session_id
2984            .as_deref()
2985            .unwrap_or(&locator.session_id),
2986    );
2987    let suggested_filename = format!("{stem}.grok-import.claude-code.jsonl");
2988    Ok(SessionArtifact {
2989        source_harness: locator.harness.clone(),
2990        // This names the artifact's actual wire format. The requested handoff target
2991        // remains Grok; its official importer is the materialization boundary.
2992        target_harness: TransferFormat::ClaudeCode.id(),
2993        session_id: importable.meta.session_id.clone(),
2994        content: content.clone(),
2995        suggested_filename: suggested_filename.clone(),
2996        files: vec![SessionArtifactFile {
2997            path: suggested_filename,
2998            content,
2999            role: ArtifactFileRole::Primary,
3000        }],
3001        fidelity: Fidelity::Semantic,
3002        residue: vec!["Grok's stock importer accepts a Claude Code transcript, not a complete Grok updates/session bundle".into()],
3003    })
3004}
3005
3006fn target_session_id(target: TransferFormat) -> String {
3007    let uuid = generated_session_id();
3008    match target {
3009        TransferFormat::OpenCode => format!("ses_{}", uuid.replace('-', "")),
3010        TransferFormat::ClaudeCode
3011        | TransferFormat::Codex
3012        | TransferFormat::Pi
3013        | TransferFormat::Grok
3014        | TransferFormat::Gemini
3015        | TransferFormat::Goose => uuid,
3016    }
3017}
3018
3019fn sanitize_filename(value: &str) -> String {
3020    let value = value
3021        .chars()
3022        .map(|character| {
3023            if character.is_ascii_alphanumeric() || matches!(character, '-' | '_') {
3024                character
3025            } else {
3026                '-'
3027            }
3028        })
3029        .collect::<String>();
3030    let value = value.trim_matches('-');
3031    if value.is_empty() {
3032        "session".into()
3033    } else {
3034        value.chars().take(100).collect()
3035    }
3036}
3037
3038fn handoff_instructions(
3039    target: TransferFormat,
3040    session_id: &str,
3041    cwd: &Path,
3042) -> HandoffInstructions {
3043    let launch = |program: &str, arguments: Vec<String>| StructuredLaunch {
3044        cwd: cwd.to_path_buf(),
3045        program: program.into(),
3046        arguments,
3047        env: BTreeMap::new(),
3048    };
3049    match target {
3050        TransferFormat::ClaudeCode => HandoffInstructions {
3051            launch: launch("claude", vec!["--resume".into(), session_id.into()]),
3052            materialize: None,
3053            requires_materialization: true,
3054            note: "Write the artifact into Claude Code's native project session store before running the resume launch; Claude Code has no general transcript-import command.".into(),
3055        },
3056        TransferFormat::Codex => HandoffInstructions {
3057            launch: launch("codex", vec!["resume".into(), session_id.into()]),
3058            materialize: None,
3059            requires_materialization: true,
3060            note: "Write the artifact into Codex's native rollout store before running the resume launch; Codex has no general transcript-import command.".into(),
3061        },
3062        TransferFormat::OpenCode => HandoffInstructions {
3063            launch: launch("opencode", vec!["--session".into(), session_id.into()]),
3064            materialize: Some(launch(
3065                "opencode",
3066                vec!["import".into(), "{artifact_path}".into()],
3067            )),
3068            requires_materialization: true,
3069            note: "Write the artifact to a file, run the materialize command with its path, then launch the imported session.".into(),
3070        },
3071        TransferFormat::Pi => HandoffInstructions {
3072            launch: launch("pi", vec!["--session".into(), "{artifact_path}".into()]),
3073            materialize: None,
3074            requires_materialization: true,
3075            note: "Write the artifact to a file and replace {artifact_path} in the launch arguments; Pi can resume that file directly.".into(),
3076        },
3077        TransferFormat::Grok => HandoffInstructions {
3078            launch: launch(
3079                "grok",
3080                vec![
3081                    "--resume".into(),
3082                    "{imported_session_id}".into(),
3083                    "--fork-session".into(),
3084                ],
3085            ),
3086            materialize: Some(launch(
3087                "grok",
3088                vec!["import".into(), "--json".into(), "{artifact_path}".into()],
3089            )),
3090            requires_materialization: true,
3091            note: "The artifact is Claude Code JSONL for Grok's official importer. Write it to a file, run the materialize command, read sessionId from its NDJSON outcome=imported record, replace {imported_session_id} in the launch arguments, then launch a writable fork of the imported session.".into(),
3092        },
3093        TransferFormat::Gemini => HandoffInstructions {
3094            launch: launch(
3095                "gemini",
3096                vec!["--session-file".into(), "{artifact_path}".into()],
3097            ),
3098            materialize: None,
3099            requires_materialization: true,
3100            note: "Write the Gemini JSONL artifact to a file and replace {artifact_path}; Gemini imports it into the current project's chat store before opening the continuation.".into(),
3101        },
3102        TransferFormat::Goose => HandoffInstructions {
3103            launch: launch(
3104                "goose",
3105                vec![
3106                    "session".into(),
3107                    "--resume".into(),
3108                    "--session-id".into(),
3109                    "{imported_session_id}".into(),
3110                ],
3111            ),
3112            materialize: Some(launch(
3113                "goose",
3114                vec!["session".into(), "import".into(), "{artifact_path}".into()],
3115            )),
3116            requires_materialization: true,
3117            note: "Write the Goose JSON artifact to a file, run the materialize command, read the imported session id from its output, replace {imported_session_id}, then resume that native Goose session.".into(),
3118        },
3119    }
3120}
3121
3122fn resume_launch(
3123    harness: &str,
3124    session_id: &str,
3125    cwd: &Path,
3126    policy: ResumePolicy,
3127) -> std::result::Result<StructuredLaunch, ServiceError> {
3128    let mut arguments = Vec::new();
3129    let program = match harness {
3130        HarnessId::GROK => {
3131            if matches!(policy, ResumePolicy::Yolo) {
3132                arguments.extend([
3133                    "--sandbox".into(),
3134                    "workspace".into(),
3135                    "--always-approve".into(),
3136                ]);
3137            }
3138            arguments.extend(["--resume".into(), session_id.into()]);
3139            "grok"
3140        }
3141        HarnessId::CODEX => {
3142            let cwd_key = serde_json::to_string(cwd.to_string_lossy().as_ref())
3143                .expect("a filesystem path always serializes as JSON text");
3144            arguments.extend([
3145                "-c".into(),
3146                "check_for_update_on_startup=false".into(),
3147                "-c".into(),
3148                format!("projects.{cwd_key}.trust_level=\"trusted\""),
3149            ]);
3150            if matches!(policy, ResumePolicy::Yolo) {
3151                arguments.extend([
3152                    "--dangerously-bypass-approvals-and-sandbox".into(),
3153                    "--dangerously-bypass-hook-trust".into(),
3154                ]);
3155            }
3156            arguments.extend(["resume".into(), session_id.into()]);
3157            "codex"
3158        }
3159        HarnessId::CLAUDE_CODE => {
3160            if matches!(policy, ResumePolicy::Yolo) {
3161                arguments.push("--dangerously-skip-permissions".into());
3162            }
3163            arguments.extend(["--resume".into(), session_id.into()]);
3164            "claude"
3165        }
3166        HarnessId::GEMINI => {
3167            if matches!(policy, ResumePolicy::Yolo) {
3168                arguments.push("--yolo".into());
3169            }
3170            arguments.extend(["--resume".into(), session_id.into()]);
3171            "gemini"
3172        }
3173        HarnessId::GOOSE => {
3174            arguments.extend([
3175                "session".into(),
3176                "--resume".into(),
3177                "--session-id".into(),
3178                session_id.into(),
3179            ]);
3180            "goose"
3181        }
3182        HarnessId::PI => {
3183            if matches!(policy, ResumePolicy::Yolo) {
3184                arguments.push("--approve".into());
3185            }
3186            arguments.extend(["--session".into(), session_id.into()]);
3187            "pi"
3188        }
3189        HarnessId::OPENCODE => {
3190            arguments.extend(["--session".into(), session_id.into()]);
3191            "opencode"
3192        }
3193        HarnessId::SUPERCODE => {
3194            if matches!(policy, ResumePolicy::Yolo) {
3195                arguments.push("--dangerous".into());
3196            }
3197            arguments.extend(["resume".into(), session_id.into()]);
3198            "supercode"
3199        }
3200        other => {
3201            return Err(ServiceError::InvalidParams(format!(
3202                "no structured resume launch is registered for harness `{other}`"
3203            )))
3204        }
3205    };
3206    Ok(StructuredLaunch {
3207        cwd: cwd.to_path_buf(),
3208        program: program.into(),
3209        arguments,
3210        env: BTreeMap::new(),
3211    })
3212}
3213
3214fn runtime_backend(
3215    params: &RuntimeBackendParams,
3216) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
3217    if params.protocol.as_deref() == Some("acp") {
3218        let launch = params
3219            .launch
3220            .clone()
3221            .or_else(|| {
3222                harness_support_registry()
3223                    .harnesses
3224                    .into_iter()
3225                    .find(|harness| harness.id == params.harness)
3226                    .filter(|harness| {
3227                        harness.runtime.implementation == ImplementationKind::GenericProtocol
3228                            && harness.runtime.protocol.starts_with("acp")
3229                    })
3230                    .and_then(|harness| harness.runtime.default_launch)
3231            })
3232            .ok_or_else(|| {
3233                ServiceError::InvalidParams(
3234                    "an ACP runtime requires `launch` unless the harness has a registered default"
3235                        .into(),
3236                )
3237            })?;
3238        let resume_session = harness_support_registry()
3239            .harnesses
3240            .into_iter()
3241            .find(|harness| harness.id == params.harness)
3242            .is_some_and(|harness| harness.runtime.capabilities.resume_session);
3243        return Ok(Box::new(
3244            AcpRuntimeBackend::new(params.harness.clone(), launch)
3245                .with_resume_support(resume_session),
3246        ));
3247    }
3248    let backend: Box<dyn RuntimeBackend> = match params.harness.as_str() {
3249        HarnessId::CODEX => Box::new(CodexRuntimeBackend::new()),
3250        HarnessId::CLAUDE_CODE => Box::new(ClaudeCodeRuntimeBackend::new()),
3251        HarnessId::PI => Box::new(PiRuntimeBackend::new()),
3252        HarnessId::OPENCODE => match &params.base_url {
3253            Some(url) => Box::new(OpenCodeRuntimeBackend::connect(url)),
3254            None => Box::new(OpenCodeRuntimeBackend::new()),
3255        },
3256        harness => {
3257            let descriptor = harness_support_registry()
3258                .harnesses
3259                .into_iter()
3260                .find(|descriptor| descriptor.id.as_str() == harness)
3261                .filter(|descriptor| {
3262                    descriptor.runtime.implementation == ImplementationKind::GenericProtocol
3263                        && descriptor.runtime.protocol.starts_with("acp")
3264                });
3265            let Some(descriptor) = descriptor else {
3266                return Err(ServiceError::InvalidParams(format!(
3267                    "no runtime adapter for harness `{harness}`; use protocol `acp` with a launch command"
3268                )));
3269            };
3270            let resume = descriptor.runtime.capabilities.resume_session;
3271            Box::new(
3272                AcpRuntimeBackend::new(
3273                    descriptor.id,
3274                    descriptor
3275                        .runtime
3276                        .default_launch
3277                        .expect("generic ACP registry entry includes its launch"),
3278                )
3279                .with_resume_support(resume),
3280            )
3281        }
3282    };
3283    Ok(backend)
3284}
3285
3286fn runtime_launch(params: &RuntimeBackendParams) -> Option<RuntimeLaunch> {
3287    if let Some(launch) = &params.launch {
3288        return Some(launch.clone());
3289    }
3290    if !matches!(params.policy, RuntimePolicy::Yolo) {
3291        return None;
3292    }
3293    let launch = match params.harness.as_str() {
3294        HarnessId::GROK => RuntimeLaunch {
3295            program: "grok".into(),
3296            arguments: vec![
3297                "--sandbox".into(),
3298                "workspace".into(),
3299                "--always-approve".into(),
3300                "agent".into(),
3301                "--no-leader".into(),
3302                "stdio".into(),
3303            ],
3304            env: BTreeMap::from([("GROK_AGENT_DASHBOARD".into(), "0".into())]),
3305        },
3306        HarnessId::CODEX => RuntimeLaunch {
3307            program: "codex".into(),
3308            arguments: vec![
3309                "--dangerously-bypass-approvals-and-sandbox".into(),
3310                "--dangerously-bypass-hook-trust".into(),
3311                "app-server".into(),
3312            ],
3313            env: BTreeMap::new(),
3314        },
3315        HarnessId::CLAUDE_CODE => RuntimeLaunch {
3316            program: "claude".into(),
3317            arguments: vec![
3318                "--dangerously-skip-permissions".into(),
3319                "--print".into(),
3320                "--input-format".into(),
3321                "stream-json".into(),
3322                "--output-format".into(),
3323                "stream-json".into(),
3324                "--verbose".into(),
3325            ],
3326            env: BTreeMap::new(),
3327        },
3328        HarnessId::PI => RuntimeLaunch {
3329            program: "pi".into(),
3330            arguments: vec!["--approve".into(), "--mode".into(), "rpc".into()],
3331            env: BTreeMap::new(),
3332        },
3333        HarnessId::OPENCODE => RuntimeLaunch {
3334            program: "opencode".into(),
3335            arguments: vec!["serve".into()],
3336            env: BTreeMap::new(),
3337        },
3338        HarnessId::GEMINI => RuntimeLaunch {
3339            program: "gemini".into(),
3340            arguments: vec!["--acp".into(), "--yolo".into()],
3341            env: BTreeMap::new(),
3342        },
3343        HarnessId::GOOSE => RuntimeLaunch {
3344            program: "goose".into(),
3345            arguments: vec!["acp".into()],
3346            env: BTreeMap::new(),
3347        },
3348        HarnessId::SUPERCODE => RuntimeLaunch {
3349            program: "supercode".into(),
3350            arguments: vec!["acp".into(), "--dangerous".into()],
3351            env: BTreeMap::new(),
3352        },
3353        _ => return None,
3354    };
3355    Some(launch)
3356}
3357
3358/// Disposable harness state for a no-prompt readiness probe. Merely opening
3359/// several stock CLIs writes a session header or migrates configuration, so a
3360/// handshake must never point at the user's real home. Authentication files
3361/// are copied into the private temporary home; all writes disappear with the
3362/// guard after the connection closes.
3363struct IsolatedProbeHome {
3364    launch: RuntimeLaunch,
3365    root: PathBuf,
3366}
3367
3368impl IsolatedProbeHome {
3369    fn new(harness: &str, mut launch: RuntimeLaunch) -> std::io::Result<Self> {
3370        let root = std::env::temp_dir().join(format!(
3371            "supercode-harness-probe-{harness}-{}",
3372            generated_session_id()
3373        ));
3374        std::fs::create_dir_all(&root)?;
3375        set_private_dir_permissions(&root)?;
3376
3377        if let Some(source_home) = std::env::var_os("HOME").map(PathBuf::from) {
3378            for relative in probe_auth_files(harness) {
3379                copy_probe_file(&source_home, &root, relative)?;
3380            }
3381        }
3382        configure_isolated_probe_auth(harness, &root)?;
3383
3384        let root_text = root.to_string_lossy().into_owned();
3385        for (key, value) in [
3386            ("HOME", root_text.clone()),
3387            (
3388                "XDG_CACHE_HOME",
3389                root.join(".cache").to_string_lossy().into_owned(),
3390            ),
3391            (
3392                "XDG_CONFIG_HOME",
3393                root.join(".config").to_string_lossy().into_owned(),
3394            ),
3395            (
3396                "XDG_DATA_HOME",
3397                root.join(".local/share").to_string_lossy().into_owned(),
3398            ),
3399        ] {
3400            launch.env.insert(key.into(), value);
3401        }
3402        let scoped = match harness {
3403            HarnessId::CLAUDE_CODE => Some(("CLAUDE_CONFIG_DIR", root.join(".claude"))),
3404            HarnessId::CODEX => Some(("CODEX_HOME", root.join(".codex"))),
3405            HarnessId::GEMINI => Some(("GEMINI_CLI_HOME", root.clone())),
3406            HarnessId::GROK => Some(("GROK_HOME", root.join(".grok"))),
3407            HarnessId::PI => Some(("PI_CODING_AGENT_DIR", root.join(".pi/agent"))),
3408            HarnessId::SUPERCODE => Some(("SUPERCODE_HOME", root.join(".config/supercode"))),
3409            _ => None,
3410        };
3411        if let Some((key, value)) = scoped {
3412            launch
3413                .env
3414                .insert(key.into(), value.to_string_lossy().into_owned());
3415        }
3416        Ok(Self { launch, root })
3417    }
3418
3419    fn cleanup(&self) -> std::io::Result<()> {
3420        match std::fs::remove_dir_all(&self.root) {
3421            Ok(()) => Ok(()),
3422            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
3423            Err(error) => Err(error),
3424        }
3425    }
3426}
3427
3428impl Drop for IsolatedProbeHome {
3429    fn drop(&mut self) {
3430        let _ = self.cleanup();
3431    }
3432}
3433
3434fn probe_auth_files(harness: &str) -> &'static [&'static str] {
3435    match harness {
3436        HarnessId::CLAUDE_CODE => &[".claude/.credentials.json", ".claude.json"],
3437        HarnessId::CODEX => &[".codex/auth.json"],
3438        HarnessId::GEMINI => &[
3439            ".gemini/google_accounts.json",
3440            ".gemini/oauth_creds.json",
3441            ".gemini/settings.json",
3442        ],
3443        HarnessId::GROK => &[".grok/auth.json", ".grok/config.toml"],
3444        HarnessId::OPENCODE => &[
3445            ".config/opencode/auth.json",
3446            ".local/share/opencode/auth.json",
3447        ],
3448        HarnessId::PI => &[".pi/agent/auth.json"],
3449        HarnessId::SUPERCODE => &[
3450            ".config/supercode/config.toml",
3451            ".config/supercode/credentials.toml",
3452        ],
3453        _ => &[],
3454    }
3455}
3456
3457fn copy_probe_file(source_home: &Path, probe_home: &Path, relative: &str) -> std::io::Result<()> {
3458    let source = source_home.join(relative);
3459    if !source.is_file() {
3460        return Ok(());
3461    }
3462    let destination = probe_home.join(relative);
3463    if let Some(parent) = destination.parent() {
3464        std::fs::create_dir_all(parent)?;
3465        set_private_dir_permissions(parent)?;
3466    }
3467    std::fs::copy(source, &destination)?;
3468    set_private_file_permissions(&destination)
3469}
3470
3471fn configure_isolated_probe_auth(harness: &str, probe_home: &Path) -> std::io::Result<()> {
3472    if harness != HarnessId::GEMINI {
3473        return Ok(());
3474    }
3475    let oauth = probe_home.join(".gemini/oauth_creds.json");
3476    if !oauth.is_file() {
3477        return Ok(());
3478    }
3479    let settings_path = probe_home.join(".gemini/settings.json");
3480    let mut settings = std::fs::read_to_string(&settings_path)
3481        .ok()
3482        .and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
3483        .unwrap_or_else(|| json!({}));
3484    settings["security"]["auth"]["selectedType"] = Value::String("oauth-personal".into());
3485    std::fs::write(
3486        &settings_path,
3487        serde_json::to_vec_pretty(&settings).map_err(std::io::Error::other)?,
3488    )?;
3489    set_private_file_permissions(&settings_path)
3490}
3491
3492#[cfg(unix)]
3493fn set_private_dir_permissions(path: &Path) -> std::io::Result<()> {
3494    use std::os::unix::fs::PermissionsExt;
3495    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
3496}
3497
3498#[cfg(not(unix))]
3499fn set_private_dir_permissions(_path: &Path) -> std::io::Result<()> {
3500    Ok(())
3501}
3502
3503#[cfg(unix)]
3504fn set_private_file_permissions(path: &Path) -> std::io::Result<()> {
3505    use std::os::unix::fs::PermissionsExt;
3506    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
3507}
3508
3509#[cfg(not(unix))]
3510fn set_private_file_permissions(_path: &Path) -> std::io::Result<()> {
3511    Ok(())
3512}
3513
3514fn find_executable(program: &str) -> Option<PathBuf> {
3515    let candidate = PathBuf::from(program);
3516    if candidate.components().count() > 1 {
3517        return candidate.is_file().then_some(candidate);
3518    }
3519    let path = std::env::var_os("PATH")?;
3520    for directory in std::env::split_paths(&path) {
3521        let candidate = directory.join(program);
3522        if candidate.is_file() {
3523            return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
3524        }
3525        #[cfg(windows)]
3526        {
3527            for extension in ["exe", "cmd", "bat"] {
3528                let candidate = directory.join(format!("{program}.{extension}"));
3529                if candidate.is_file() {
3530                    return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
3531                }
3532            }
3533        }
3534    }
3535    None
3536}
3537
3538async fn executable_version(executable: &Path) -> Option<String> {
3539    let mut command = tokio::process::Command::new(executable);
3540    command
3541        .arg("--version")
3542        .stdin(std::process::Stdio::null())
3543        .stdout(std::process::Stdio::piped())
3544        .stderr(std::process::Stdio::piped())
3545        .kill_on_drop(true);
3546    let output = tokio::time::timeout(Duration::from_secs(3), command.output())
3547        .await
3548        .ok()?
3549        .ok()?;
3550    let stdout = String::from_utf8_lossy(&output.stdout);
3551    let stderr = String::from_utf8_lossy(&output.stderr);
3552    stdout
3553        .lines()
3554        .chain(stderr.lines())
3555        .map(str::trim)
3556        .find(|line| !line.is_empty())
3557        .map(|line| truncate_text(line, 200))
3558}
3559
3560pub(crate) fn auth_evidence(harness: &str) -> bool {
3561    let env_names: &[&str] = match harness {
3562        HarnessId::CLAUDE_CODE => &["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
3563        HarnessId::CODEX => &["OPENAI_API_KEY"],
3564        HarnessId::OPENCODE => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
3565        HarnessId::PI => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
3566        HarnessId::GROK => &["XAI_API_KEY", "GROK_API_KEY"],
3567        HarnessId::GEMINI => &["GEMINI_API_KEY", "GOOGLE_API_KEY"],
3568        HarnessId::SUPERCODE => &["OPENROUTER_API_KEY"],
3569        _ => &[],
3570    };
3571    if env_names
3572        .iter()
3573        .any(|name| std::env::var_os(name).is_some_and(|value| !value.is_empty()))
3574    {
3575        return true;
3576    }
3577    let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
3578        return false;
3579    };
3580    let files: Vec<PathBuf> = match harness {
3581        HarnessId::CLAUDE_CODE => vec![home.join(".claude/.credentials.json")],
3582        HarnessId::CODEX => vec![home.join(".codex/auth.json")],
3583        HarnessId::OPENCODE => vec![
3584            home.join(".local/share/opencode/auth.json"),
3585            home.join(".config/opencode/auth.json"),
3586        ],
3587        HarnessId::PI => vec![home.join(".pi/agent/auth.json")],
3588        HarnessId::GROK => vec![home.join(".grok/auth.json")],
3589        HarnessId::GEMINI => vec![
3590            home.join(".gemini/oauth_creds.json"),
3591            home.join(".gemini/google_accounts.json"),
3592        ],
3593        HarnessId::SUPERCODE => vec![home.join(".config/supercode/credentials.toml")],
3594        _ => Vec::new(),
3595    };
3596    if files.into_iter().any(|path| {
3597        std::fs::metadata(path)
3598            .map(|metadata| metadata.is_file() && metadata.len() > 2)
3599            .unwrap_or(false)
3600    }) {
3601        return true;
3602    }
3603    // macOS keeps Claude Code's OAuth login in the Keychain, so
3604    // `.claude/.credentials.json` never exists there and the file probe above
3605    // reports a signed-in install as unauthenticated forever. A completed
3606    // login also writes an `oauthAccount` record into `~/.claude.json` on
3607    // every platform — file-based, prompt-free evidence (querying the
3608    // Keychain itself from an unsigned daemon can raise a UI prompt).
3609    if harness == HarnessId::CLAUDE_CODE {
3610        return std::fs::read_to_string(home.join(".claude.json"))
3611            .map(|text| text.contains("\"oauthAccount\""))
3612            .unwrap_or(false);
3613    }
3614    false
3615}
3616
3617fn looks_like_auth_error(message: &str) -> bool {
3618    let message = message.to_ascii_lowercase();
3619    [
3620        "auth",
3621        "login",
3622        "sign in",
3623        "sign-in",
3624        "credential",
3625        "unauthorized",
3626        "forbidden",
3627        "token",
3628    ]
3629    .iter()
3630    .any(|needle| message.contains(needle))
3631}
3632
3633fn unavailable_capabilities() -> crate::RuntimeCapabilities {
3634    crate::RuntimeCapabilities {
3635        start_session: false,
3636        resume_session: false,
3637        attach_existing_process: false,
3638        send_input: false,
3639        stream_events: false,
3640        interrupt: false,
3641        steer: false,
3642        respond_to_requests: false,
3643    }
3644}
3645
3646fn truncate_text(text: &str, max_chars: usize) -> String {
3647    let mut chars = text.chars();
3648    let truncated = chars.by_ref().take(max_chars).collect::<String>();
3649    if chars.next().is_some() {
3650        format!("{truncated}…")
3651    } else {
3652        truncated
3653    }
3654}
3655
3656fn error_message(error: ServiceError) -> String {
3657    match error {
3658        ServiceError::InvalidParams(message)
3659        | ServiceError::Operation(message)
3660        | ServiceError::UnsupportedAction(message) => message,
3661        ServiceError::MethodNotFound => "runtime adapter is not available".into(),
3662        ServiceError::Sdk(error) => error.to_string(),
3663    }
3664}
3665
3666#[derive(Debug)]
3667enum ServiceError {
3668    InvalidParams(String),
3669    MethodNotFound,
3670    UnsupportedAction(String),
3671    Operation(String),
3672    Sdk(SdkError),
3673}
3674
3675fn sdk_error(operation: SdkOperation, error: ServiceError) -> SdkError {
3676    match error {
3677        ServiceError::InvalidParams(message) => {
3678            SdkError::new(SdkErrorCode::InvalidArgument, operation, message)
3679        }
3680        ServiceError::MethodNotFound | ServiceError::UnsupportedAction(_) => {
3681            SdkError::unsupported(operation)
3682        }
3683        ServiceError::Operation(message) => {
3684            let code = if message.contains("already in progress") {
3685                SdkErrorCode::Busy
3686            } else if message.contains("not supported by this runtime") {
3687                SdkErrorCode::UnsupportedAction
3688            } else if message.contains("unknown runtime connection") {
3689                SdkErrorCode::NotFound
3690            } else {
3691                SdkErrorCode::Execution
3692            };
3693            SdkError::new(code, operation, message)
3694        }
3695        ServiceError::Sdk(error) => error,
3696    }
3697}
3698
3699fn sdk_rpc_error(id: Value, error: &SdkError) -> Value {
3700    let error_code = error.code();
3701    let code = match error_code {
3702        SdkErrorCode::Unauthenticated => -32030,
3703        SdkErrorCode::Unauthorized => -32031,
3704        SdkErrorCode::ControllerRequired => -32032,
3705        SdkErrorCode::LeaseExpired => -32033,
3706        SdkErrorCode::InvalidArgument => -32602,
3707        SdkErrorCode::NotFound => -32004,
3708        SdkErrorCode::Busy => -32000,
3709        SdkErrorCode::UnsupportedAction => -32020,
3710        SdkErrorCode::Execution => -32002,
3711        SdkErrorCode::Transport => -32003,
3712    };
3713    json!({
3714        "jsonrpc": "2.0",
3715        "id": id,
3716        "error": {
3717            "code": code,
3718            "name": error_code,
3719            "operation": error.operation(),
3720            "message": error.to_string(),
3721        },
3722    })
3723}
3724
3725fn decode<T: for<'de> Deserialize<'de>>(value: Value) -> std::result::Result<T, ServiceError> {
3726    serde_json::from_value(value).map_err(|error| ServiceError::InvalidParams(error.to_string()))
3727}
3728
3729fn operation(error: impl Into<crate::Error>) -> ServiceError {
3730    let error = error.into();
3731    match error {
3732        crate::Error::Sdk(error) => ServiceError::Sdk(error),
3733        error => ServiceError::Operation(error.to_string()),
3734    }
3735}
3736
3737fn rpc_error(id: Value, code: i64, message: &str) -> Value {
3738    json!({
3739        "jsonrpc": "2.0",
3740        "id": id,
3741        "error": {"code": code, "message": message},
3742    })
3743}
3744
3745#[cfg(test)]
3746mod tests {
3747    use super::*;
3748    use crate::{HarnessEvent, HarnessId, RuntimeEndpoint, RuntimeHandle, StorageLocator};
3749    use async_trait::async_trait;
3750    use std::io::Write;
3751    use std::path::PathBuf;
3752    use std::time::Instant;
3753
3754    #[test]
3755    fn indexed_claude_descriptor_keeps_the_live_peer_address() {
3756        let descriptor = SessionDescriptor {
3757            locator: SessionLocator {
3758                harness: HarnessId::new(HarnessId::CLAUDE_CODE),
3759                session_id: "live-session".into(),
3760                storage: StorageLocator::File {
3761                    path: PathBuf::from("/tmp/live-session.jsonl"),
3762                },
3763            },
3764            cwd: Some(PathBuf::from("/project")),
3765            title: None,
3766            preview_candidates: Vec::new(),
3767            latest_message_candidates: Vec::new(),
3768            updated_at_ms: Some(1),
3769            message_count: None,
3770            model: None,
3771            parent_session_id: None,
3772            child_session_count: 0,
3773        };
3774        let peer = crate::claude_peer::ClaudePeerSession {
3775            pid: 42,
3776            session_id: "live-session".into(),
3777            cwd: Some(PathBuf::from("/project")),
3778            name: "peer".into(),
3779            socket_path: PathBuf::from("/tmp/peer.sock"),
3780            status: Some(crate::claude_peer::ClaudePeerStatus::Busy),
3781            updated_at_ms: Some(1),
3782            version: Some("test".into()),
3783        };
3784
3785        let value = live_descriptor_value(&descriptor, &[peer]).unwrap();
3786        assert!(value["live_endpoint"]
3787            .as_str()
3788            .is_some_and(|endpoint| endpoint.starts_with("cc-peer:v1:42:peer:")));
3789    }
3790
3791    struct EndingRuntime {
3792        handle: RuntimeHandle,
3793        event: Option<HarnessEvent>,
3794    }
3795
3796    #[async_trait]
3797    impl RuntimeConnection for EndingRuntime {
3798        fn handle(&self) -> &RuntimeHandle {
3799            &self.handle
3800        }
3801
3802        async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
3803            unreachable!("ending runtime does not accept input")
3804        }
3805
3806        async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
3807            Ok(self.event.take())
3808        }
3809
3810        async fn interrupt(&mut self) -> crate::Result<()> {
3811            Ok(())
3812        }
3813
3814        async fn respond(&mut self, _request_id: Value, _response: Value) -> crate::Result<()> {
3815            Ok(())
3816        }
3817
3818        async fn close(&mut self) -> crate::Result<()> {
3819            Ok(())
3820        }
3821    }
3822
3823    fn ending_runtime(event: Option<HarnessEvent>) -> Box<dyn RuntimeConnection> {
3824        Box::new(EndingRuntime {
3825            handle: RuntimeHandle {
3826                harness: HarnessId::from(HarnessId::CLAUDE_CODE),
3827                runtime_id: "ending-session".into(),
3828                endpoint: RuntimeEndpoint::LocalProcess {
3829                    pid: None,
3830                    command: vec!["ending-runtime".into()],
3831                    protocol: "test".into(),
3832                },
3833            },
3834            event,
3835        })
3836    }
3837
3838    fn request(id: u64, method: &str, params: Value) -> Value {
3839        json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params})
3840    }
3841
3842    fn pi_locator() -> SessionLocator {
3843        SessionLocator {
3844            harness: HarnessId::from(HarnessId::PI),
3845            session_id: "1e6f2a3b-0000-4000-8000-000000000001".into(),
3846            storage: StorageLocator::File {
3847                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3848                    .join("tests/fixtures/pi_session.jsonl"),
3849            },
3850        }
3851    }
3852
3853    fn opencode_locator() -> SessionLocator {
3854        let session_id = "ses_fixtureAAAAAAAAAAAAAAA1";
3855        SessionLocator {
3856            harness: HarnessId::from(HarnessId::OPENCODE),
3857            session_id: session_id.into(),
3858            storage: StorageLocator::Sqlite {
3859                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3860                    .join("tests/fixtures/opencode_fixture/opencode.db"),
3861                selector: session_id.into(),
3862            },
3863        }
3864    }
3865
3866    fn grok_locator() -> SessionLocator {
3867        SessionLocator {
3868            harness: HarnessId::from(HarnessId::GROK),
3869            session_id: "73c09283-4b33-41fa-90f1-0bcb0f7be523".into(),
3870            storage: StorageLocator::File {
3871                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3872                    .join("tests/fixtures/grok_session/chat_history.jsonl"),
3873            },
3874        }
3875    }
3876
3877    #[test]
3878    fn capabilities_are_explicit_and_versioned() {
3879        let mut service = HarnessSessionService::new();
3880        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
3881        assert_eq!(response["result"]["version"], HARNESS_SERVICE_VERSION);
3882        assert_eq!(
3883            response["result"]["sdk"]["schema_version"],
3884            crate::SDK_SCHEMA_VERSION
3885        );
3886        assert_eq!(
3887            response["result"]["sdk"]["operations"]
3888                .as_array()
3889                .unwrap()
3890                .len(),
3891            SdkOperation::ALL.len()
3892        );
3893        assert_eq!(response["result"]["harnesses"].as_array().unwrap().len(), 8);
3894        assert!(response["result"]["harnesses"]
3895            .as_array()
3896            .unwrap()
3897            .iter()
3898            .any(|harness| harness == HarnessId::GROK));
3899        assert!(response["result"]["harnesses"]
3900            .as_array()
3901            .unwrap()
3902            .iter()
3903            .any(|harness| harness == HarnessId::GOOSE));
3904    }
3905
3906    #[test]
3907    fn handshake_health_uses_protocol_liveness_not_stderr_severity() {
3908        let noisy_stderr = crate::HarnessEvent {
3909            sequence: None,
3910            kind: "transport_stderr".into(),
3911            payload: json!({"line": "ERROR optional worker AuthorizationRequired"}),
3912        };
3913        assert_eq!(handshake_event_failure(&noisy_stderr), None);
3914
3915        let closed = crate::HarnessEvent {
3916            sequence: None,
3917            kind: "transport_closed".into(),
3918            payload: json!({}),
3919        };
3920        assert!(handshake_event_failure(&closed).is_some());
3921    }
3922
3923    #[tokio::test]
3924    async fn runtime_eof_is_notified_and_removed_for_raw_and_explicit_close() {
3925        let mut service = HarnessSessionService::new();
3926        service
3927            .runtimes
3928            .insert("raw-eof".into(), ending_runtime(None));
3929        service.runtimes.insert(
3930            "explicit-close".into(),
3931            ending_runtime(Some(HarnessEvent {
3932                sequence: None,
3933                kind: "transport_closed".into(),
3934                payload: json!({"message": "native transport exited"}),
3935            })),
3936        );
3937
3938        let notifications = service.poll_runtimes().await;
3939
3940        assert_eq!(notifications.len(), 2);
3941        assert!(notifications
3942            .iter()
3943            .all(|notification| { notification["params"]["event"]["kind"] == "transport_closed" }));
3944        assert!(notifications.iter().all(|notification| {
3945            notification["params"]["session_id"] == "ending-session"
3946                && notification["params"]["connection"].is_string()
3947        }));
3948        let mut sequences = notifications
3949            .iter()
3950            .filter_map(|notification| notification["params"]["sequence"].as_u64())
3951            .collect::<Vec<_>>();
3952        sequences.sort_unstable();
3953        assert_eq!(sequences, vec![1, 2]);
3954        assert!(service.runtimes.is_empty());
3955    }
3956
3957    #[test]
3958    fn support_report_and_grok_default_binding_share_the_registry() {
3959        let mut service = HarnessSessionService::new();
3960        let response = service.handle(request(1, "harness.v1.support.report", json!({})));
3961        assert_eq!(response["result"]["schema"], crate::SUPPORT_REGISTRY_SCHEMA);
3962        let params = RuntimeBackendParams {
3963            harness: HarnessId::from(HarnessId::GROK),
3964            protocol: None,
3965            launch: None,
3966            base_url: None,
3967            policy: RuntimePolicy::Default,
3968        };
3969        let backend = match runtime_backend(&params) {
3970            Ok(backend) => backend,
3971            Err(_) => panic!("Grok should bind through its registered ACP launch"),
3972        };
3973        assert_eq!(backend.harness().as_str(), HarnessId::GROK);
3974        assert!(backend.capabilities().start_session);
3975        let registered = harness_support_registry()
3976            .harnesses
3977            .into_iter()
3978            .find(|harness| harness.id.as_str() == HarnessId::GROK)
3979            .and_then(|harness| harness.runtime.default_launch)
3980            .unwrap();
3981        assert!(!registered
3982            .arguments
3983            .iter()
3984            .any(|argument| argument == "--always-approve"));
3985        assert!(runtime_launch(&params).is_none());
3986
3987        let yolo = RuntimeBackendParams {
3988            policy: RuntimePolicy::Yolo,
3989            ..params
3990        };
3991        assert!(runtime_launch(&yolo)
3992            .unwrap()
3993            .arguments
3994            .iter()
3995            .any(|argument| argument == "--always-approve"));
3996
3997        let mismatched_protocol = RuntimeBackendParams {
3998            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
3999            protocol: Some("acp".into()),
4000            launch: None,
4001            base_url: None,
4002            policy: RuntimePolicy::Default,
4003        };
4004        assert!(runtime_backend(&mismatched_protocol).is_err());
4005    }
4006
4007    #[test]
4008    fn load_follow_and_unfollow_share_the_same_locator() {
4009        let mut service = HarnessSessionService::new();
4010        let locator = pi_locator();
4011        let loaded = service.handle(request(
4012            1,
4013            "harness.v1.sessions.load",
4014            json!({"locator": locator}),
4015        ));
4016        assert_eq!(
4017            loaded["result"]["session"]["session_id"],
4018            locator.session_id
4019        );
4020
4021        let followed = service.handle(request(
4022            2,
4023            "harness.v1.sessions.follow",
4024            json!({"locator": locator}),
4025        ));
4026        assert_eq!(followed["result"]["subscription"], "sub-1");
4027        assert_eq!(followed["result"]["initial"]["type"], "session_snapshot");
4028        assert!(service.poll().is_empty());
4029
4030        let unfollowed = service.handle(request(
4031            3,
4032            "harness.v1.sessions.unfollow",
4033            json!({"subscription": "sub-1"}),
4034        ));
4035        assert_eq!(unfollowed["result"]["removed"], true);
4036    }
4037
4038    #[test]
4039    fn bounded_read_view_excludes_subagents_and_keeps_only_the_tail() {
4040        let temp = std::env::temp_dir().join(format!(
4041            "supercode-bounded-view-{}-{}",
4042            std::process::id(),
4043            generated_session_id()
4044        ));
4045        let path = temp.join("parent.jsonl");
4046        let subagents = temp.join("parent/subagents");
4047        std::fs::create_dir_all(&subagents).unwrap();
4048        let long_last = "x".repeat(300);
4049        let parent_records = [
4050            json!({"type":"user","uuid":"u1","parentUuid":null,"message":{"role":"user","content":"first"}}),
4051            json!({"type":"assistant","uuid":"a1","parentUuid":"u1","message":{"role":"assistant","content":[{"type":"text","text":"middle"}]}}),
4052            json!({"type":"user","uuid":"u2","parentUuid":"a1","message":{"role":"user","content":long_last}}),
4053        ];
4054        std::fs::write(
4055            &path,
4056            format!(
4057                "{}\n",
4058                parent_records
4059                    .iter()
4060                    .map(Value::to_string)
4061                    .collect::<Vec<_>>()
4062                    .join("\n")
4063            ),
4064        )
4065        .unwrap();
4066        std::fs::write(
4067            subagents.join("agent-child.jsonl"),
4068            concat!(
4069                r#"{"type":"user","uuid":"cu","parentUuid":null,"agentId":"child","message":{"role":"user","content":"child work"}}"#,
4070                "\n",
4071            ),
4072        )
4073        .unwrap();
4074        let locator = SessionLocator {
4075            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
4076            session_id: "parent".into(),
4077            storage: StorageLocator::File { path },
4078        };
4079        let mut service = HarnessSessionService::new();
4080
4081        let complete = service.handle(request(
4082            1,
4083            "harness.v1.sessions.load",
4084            json!({"locator": locator}),
4085        ));
4086        assert_eq!(
4087            complete["result"]["session"]["subagents"]
4088                .as_array()
4089                .unwrap()
4090                .len(),
4091            1
4092        );
4093
4094        let bounded = service.handle(request(
4095            2,
4096            "harness.v1.sessions.load",
4097            json!({
4098                "locator": locator,
4099                "view": {
4100                    "tail_messages": 1,
4101                    "max_message_chars": 256,
4102                    "include_subagents": false
4103                },
4104            }),
4105        ));
4106        let session = &bounded["result"]["session"];
4107        assert!(session["subagents"].as_array().unwrap().is_empty());
4108        assert_eq!(session["messages"].as_array().unwrap().len(), 1);
4109        assert_eq!(
4110            session["messages"][0]["content"],
4111            format!("{}\n…", "x".repeat(256))
4112        );
4113
4114        let followed = service.handle(request(
4115            3,
4116            "harness.v1.sessions.follow",
4117            json!({
4118                "locator": locator,
4119                "view": {
4120                    "tail_messages": 1,
4121                    "max_message_chars": 256,
4122                    "include_subagents": false
4123                },
4124            }),
4125        ));
4126        let initial = &followed["result"]["initial"]["session"];
4127        assert!(initial["subagents"].as_array().unwrap().is_empty());
4128        assert_eq!(initial["messages"].as_array().unwrap().len(), 1);
4129
4130        let _ = std::fs::remove_dir_all(&temp);
4131    }
4132
4133    #[test]
4134    fn forty_megabyte_display_load_is_bounded_and_prompt() {
4135        let temp = std::env::temp_dir().join(format!(
4136            "supercode-large-display-view-{}-{}",
4137            std::process::id(),
4138            generated_session_id()
4139        ));
4140        std::fs::create_dir_all(&temp).unwrap();
4141        let path = temp.join("rollout.jsonl");
4142        let mut file = std::io::BufWriter::new(std::fs::File::create(&path).unwrap());
4143        writeln!(
4144            file,
4145            r#"{{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{{"id":"large-display","cwd":"/tmp"}}}}"#
4146        )
4147        .unwrap();
4148        let padding = "x".repeat(80 * 1024);
4149        for index in 0..512 {
4150            let marker = if index == 0 {
4151                "OLDEST-SHOULD-NOT-LOAD"
4152            } else if index == 511 {
4153                "LATEST-MUST-LOAD"
4154            } else {
4155                "bulk"
4156            };
4157            writeln!(
4158                file,
4159                "{}",
4160                json!({
4161                    "timestamp": "2026-01-01T00:00:01Z",
4162                    "type": "response_item",
4163                    "payload": {
4164                        "type": "message",
4165                        "role": "assistant",
4166                        "content": [{"type": "output_text", "text": format!("{marker}:{padding}")}],
4167                    },
4168                })
4169            )
4170            .unwrap();
4171        }
4172        file.flush().unwrap();
4173        drop(file);
4174        assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
4175
4176        let locator = SessionLocator {
4177            harness: HarnessId::from(HarnessId::CODEX),
4178            session_id: "large-display".into(),
4179            storage: StorageLocator::File { path },
4180        };
4181        let started = Instant::now();
4182        let response = HarnessSessionService::new().handle(request(
4183            1,
4184            "harness.v1.sessions.load",
4185            json!({
4186                "locator": locator,
4187                "view": {
4188                    "tail_messages": 500,
4189                    "max_message_chars": 1024,
4190                    "include_subagents": false,
4191                    "display_history": true,
4192                },
4193            }),
4194        ));
4195        let elapsed = started.elapsed();
4196        let wire = response.to_string();
4197        eprintln!(
4198            "bounded 40 MiB display load: {elapsed:?}, {} response bytes",
4199            wire.len()
4200        );
4201        assert!(response.get("error").is_none(), "{response:#}");
4202        assert!(wire.contains("LATEST-MUST-LOAD"));
4203        assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
4204        assert!(
4205            wire.len() < 2 * 1024 * 1024,
4206            "bounded wire was {} bytes",
4207            wire.len()
4208        );
4209        assert!(
4210            elapsed.as_secs_f64() < 3.0,
4211            "bounded 40 MiB load took {elapsed:?}"
4212        );
4213
4214        let _ = std::fs::remove_dir_all(&temp);
4215    }
4216
4217    #[test]
4218    fn forty_megabyte_goose_store_display_load_reads_only_the_tail() {
4219        let temp = std::env::temp_dir().join(format!(
4220            "supercode-large-goose-view-{}-{}",
4221            std::process::id(),
4222            generated_session_id()
4223        ));
4224        std::fs::create_dir_all(&temp).unwrap();
4225        let path = temp.join("sessions.db");
4226        let connection = rusqlite::Connection::open(&path).unwrap();
4227        connection
4228            .execute_batch(
4229                "CREATE TABLE sessions (
4230                    id TEXT PRIMARY KEY, name TEXT NOT NULL, working_dir TEXT NOT NULL,
4231                    created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
4232                    session_type TEXT NOT NULL, extension_data TEXT,
4233                    goose_mode TEXT NOT NULL, provider_name TEXT, model_config_json TEXT,
4234                    archived_at TEXT
4235                 );
4236                 CREATE TABLE messages (
4237                    id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, message_id TEXT,
4238                    role TEXT NOT NULL, content_json TEXT NOT NULL,
4239                    created_timestamp INTEGER NOT NULL, metadata_json TEXT
4240                 );",
4241            )
4242            .unwrap();
4243        connection
4244            .execute(
4245                "INSERT INTO sessions VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, NULL)",
4246                rusqlite::params![
4247                    "goose-large",
4248                    "Large Goose session",
4249                    "/tmp",
4250                    "2026-01-01 00:00:00",
4251                    "2026-01-01 00:00:02",
4252                    "user",
4253                    "{}",
4254                    "auto",
4255                    "anthropic",
4256                    r#"{"model_name":"claude-sonnet"}"#,
4257                ],
4258            )
4259            .unwrap();
4260        let old_content = serde_json::to_string(&vec![json!({
4261            "type": "text",
4262            "text": format!("OLDEST-SHOULD-NOT-LOAD:{}", "x".repeat(40 * 1024 * 1024)),
4263        })])
4264        .unwrap();
4265        connection
4266            .execute(
4267                "INSERT INTO messages VALUES (1, ?1, 'old', 'user', ?2, 1, '{}')",
4268                rusqlite::params!["goose-large", old_content],
4269            )
4270            .unwrap();
4271        connection
4272            .execute(
4273                "INSERT INTO messages VALUES (2, ?1, 'new', 'assistant', ?2, 2, '{}')",
4274                rusqlite::params![
4275                    "goose-large",
4276                    r#"[{"type":"text","text":"LATEST-MUST-LOAD"}]"#
4277                ],
4278            )
4279            .unwrap();
4280        drop(connection);
4281        assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
4282
4283        let locator = SessionLocator {
4284            harness: HarnessId::from(HarnessId::GOOSE),
4285            session_id: "goose-large".into(),
4286            storage: StorageLocator::Sqlite {
4287                path,
4288                selector: "goose-large".into(),
4289            },
4290        };
4291        let started = Instant::now();
4292        let response = HarnessSessionService::new().handle(request(
4293            1,
4294            "harness.v1.sessions.load",
4295            json!({
4296                "locator": locator,
4297                "view": {
4298                    "tail_messages": 1,
4299                    "max_message_chars": 1024,
4300                    "include_subagents": false,
4301                    "display_history": true,
4302                },
4303            }),
4304        ));
4305        let elapsed = started.elapsed();
4306        let wire = response.to_string();
4307        eprintln!(
4308            "bounded 40 MiB Goose display load: {elapsed:?}, {} response bytes",
4309            wire.len()
4310        );
4311        assert!(response.get("error").is_none(), "{response:#}");
4312        assert!(wire.contains("LATEST-MUST-LOAD"));
4313        assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
4314        assert!(
4315            wire.len() < 64 * 1024,
4316            "bounded wire was {} bytes",
4317            wire.len()
4318        );
4319        assert!(
4320            elapsed.as_secs_f64() < 1.0,
4321            "bounded Goose load took {elapsed:?}"
4322        );
4323
4324        let _ = std::fs::remove_dir_all(&temp);
4325    }
4326
4327    #[test]
4328    fn display_view_keeps_codex_assistant_history_across_compaction() {
4329        let temp = std::env::temp_dir().join(format!(
4330            "supercode-codex-display-view-{}-{}",
4331            std::process::id(),
4332            generated_session_id()
4333        ));
4334        std::fs::create_dir_all(&temp).unwrap();
4335        let path = temp.join("rollout.jsonl");
4336        std::fs::write(
4337            &path,
4338            concat!(
4339                r#"{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{"id":"codex-display","cwd":"/tmp"}}"#,
4340                "\n",
4341                r#"{"timestamp":"2026-01-01T00:00:01Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"old prompt"}]}}"#,
4342                "\n",
4343                r#"{"timestamp":"2026-01-01T00:00:02Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"old answer"}]}}"#,
4344                "\n",
4345                r#"{"timestamp":"2026-01-01T00:00:03Z","type":"compacted","payload":{"replacement_history":[{"type":"message","role":"user","content":[{"type":"input_text","text":"old prompt"}]},{"type":"compaction","encrypted_content":"opaque"}]}}"#,
4346                "\n",
4347                r#"{"timestamp":"2026-01-01T00:00:04Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"new prompt"}]}}"#,
4348                "\n",
4349                r#"{"timestamp":"2026-01-01T00:00:05Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"new answer"}]}}"#,
4350                "\n",
4351            ),
4352        )
4353        .unwrap();
4354        let locator = SessionLocator {
4355            harness: HarnessId::from(HarnessId::CODEX),
4356            session_id: "codex-display".into(),
4357            storage: StorageLocator::File { path },
4358        };
4359        let mut service = HarnessSessionService::new();
4360
4361        let continuation = service.handle(request(
4362            1,
4363            "harness.v1.sessions.load",
4364            json!({"locator": locator}),
4365        ));
4366        let continuation_text = continuation["result"]["session"]["messages"].to_string();
4367        assert!(!continuation_text.contains("old answer"));
4368
4369        let display = service.handle(request(
4370            2,
4371            "harness.v1.sessions.load",
4372            json!({
4373                "locator": locator,
4374                "view": {
4375                    "tail_messages": 10,
4376                    "include_subagents": false,
4377                    "display_history": true,
4378                },
4379            }),
4380        ));
4381        let display_text = display["result"]["session"]["messages"].to_string();
4382        assert!(display_text.contains("old prompt"));
4383        assert!(display_text.contains("old answer"));
4384        assert!(display_text.contains("new prompt"));
4385        assert!(display_text.contains("new answer"));
4386
4387        let _ = std::fs::remove_dir_all(&temp);
4388    }
4389
4390    #[test]
4391    fn load_supports_bounded_windows_and_media_metadata() {
4392        let mut service = HarnessSessionService::new();
4393        let locator = pi_locator();
4394        let bounded = service.handle(request(
4395            1,
4396            "harness.v1.sessions.load",
4397            json!({
4398                "locator": locator,
4399                "options": {
4400                    "include_subagents": false,
4401                    "message_limit": 2,
4402                    "message_offset": 1
4403                }
4404            }),
4405        ));
4406        assert_eq!(bounded["result"]["window"]["offset"], 1);
4407        assert_eq!(bounded["result"]["window"]["returned"], 2);
4408        assert!(bounded["result"]["summary"]["first_message"].is_object());
4409        assert!(bounded["result"]["summary"]["last_message"].is_object());
4410        assert_eq!(
4411            bounded["result"]["session"]["messages"]
4412                .as_array()
4413                .unwrap()
4414                .len(),
4415            2
4416        );
4417        assert!(bounded["result"]["session"]["subagents"]
4418            .as_array()
4419            .unwrap()
4420            .is_empty());
4421
4422        let tail = service.handle(request(
4423            2,
4424            "harness.v1.sessions.load",
4425            json!({"locator": locator, "options": {"message_tail": 1}}),
4426        ));
4427        assert_eq!(tail["result"]["window"]["returned"], 1);
4428        assert_eq!(tail["result"]["window"]["has_more"], true);
4429        assert_eq!(tail["result"]["window"]["has_older"], true);
4430        assert!(tail["result"]["window"]["older_items"].as_u64().unwrap() > 0);
4431        assert!(tail["result"]["summary"]["first_message"].is_object());
4432
4433        let metadata_only = service.handle(request(
4434            3,
4435            "harness.v1.sessions.load",
4436            json!({"locator": locator, "options": {"inline_media": "metadata"}}),
4437        ));
4438        assert!(metadata_only["result"]["session"]
4439            .to_string()
4440            .contains("media_reference"));
4441        assert!(!metadata_only["result"]["session"]
4442            .to_string()
4443            .contains("data:image/"));
4444    }
4445
4446    #[test]
4447    fn import_translate_branch_and_handoff_use_typed_artifacts() {
4448        let mut service = HarnessSessionService::new();
4449        let locator = pi_locator();
4450        let translated = service.handle(request(
4451            1,
4452            "harness.v1.sessions.translate",
4453            json!({"locator": locator, "target_harness": "grok"}),
4454        ));
4455        assert_eq!(translated["result"]["artifact"]["source_harness"], "pi");
4456        assert_eq!(translated["result"]["artifact"]["target_harness"], "grok");
4457        assert!(translated["result"]["artifact"]["content"]
4458            .as_str()
4459            .is_some_and(|content| !content.is_empty()));
4460
4461        for target in ["opencode", "open-code"] {
4462            let opencode = service.handle(request(
4463                6,
4464                "harness.v1.sessions.translate",
4465                json!({"locator": locator, "target_harness": target}),
4466            ));
4467            assert_eq!(opencode["result"]["artifact"]["target_harness"], "opencode");
4468        }
4469        let goose = service.handle(request(
4470            7,
4471            "harness.v1.sessions.translate",
4472            json!({"locator": locator, "target_harness": "goose"}),
4473        ));
4474        assert_eq!(goose["result"]["artifact"]["target_harness"], "goose");
4475        assert!(serde_json::from_str::<Value>(
4476            goose["result"]["artifact"]["content"].as_str().unwrap()
4477        )
4478        .unwrap()["conversation"]
4479            .is_array());
4480
4481        let imported = service.handle(request(
4482            2,
4483            "harness.v1.sessions.import",
4484            json!({
4485                "source_harness": "grok",
4486                "content": translated["result"]["artifact"]["content"],
4487            }),
4488        ));
4489        assert_eq!(imported["result"]["session"]["source"], "grok");
4490
4491        let branched = service.handle(request(
4492            3,
4493            "harness.v1.sessions.branch",
4494            json!({"locator": locator, "target_harness": "codex"}),
4495        ));
4496        assert_eq!(branched["result"]["parent"]["harness"], "pi");
4497        assert!(branched["result"]["bootstrap_prompt"]
4498            .as_str()
4499            .unwrap()
4500            .contains("frozen parent transcript"));
4501        assert_eq!(branched["result"]["artifact"]["target_harness"], "codex");
4502
4503        let handoff = service.handle(request(
4504            4,
4505            "harness.v1.sessions.handoff",
4506            json!({"locator": locator, "target_harness": "pi", "cwd": "/tmp/project"}),
4507        ));
4508        assert_eq!(handoff["result"]["launch"]["program"], "pi");
4509        assert_eq!(handoff["result"]["launch"]["cwd"], "/tmp/project");
4510        assert_eq!(handoff["result"]["requires_materialization"], true);
4511
4512        let goose_handoff = service.handle(request(
4513            8,
4514            "harness.v1.sessions.handoff",
4515            json!({"locator": locator, "target_harness": "goose", "cwd": "/tmp/project"}),
4516        ));
4517        assert_eq!(goose_handoff["result"]["launch"]["program"], "goose");
4518        assert_eq!(
4519            goose_handoff["result"]["materialize"]["arguments"],
4520            json!(["session", "import", "{artifact_path}"])
4521        );
4522
4523        let resumed = service.handle(request(
4524            5,
4525            "harness.v1.sessions.resume_instructions",
4526            json!({"locator": locator, "cwd": "/tmp/project", "policy": "yolo"}),
4527        ));
4528        assert_eq!(resumed["result"]["launch"]["program"], "pi");
4529        assert_eq!(resumed["result"]["launch"]["arguments"][0], "--approve");
4530    }
4531
4532    #[test]
4533    fn reduce_persists_and_reloads_a_byte_exact_reversible_bundle() {
4534        let temp = std::env::temp_dir().join(format!(
4535            "supercode-service-reduce-{}-{}",
4536            std::process::id(),
4537            generated_session_id()
4538        ));
4539        let source_path = temp.join("source.jsonl");
4540        let store_root = temp.join("store");
4541        std::fs::create_dir_all(&temp).unwrap();
4542
4543        let mut records = vec![json!({
4544            "timestamp": "2026-01-01T00:00:00Z",
4545            "type": "session_meta",
4546            "payload": {"id": "codex-reduce", "cwd": "/tmp/project"},
4547        })];
4548        for turn in 0..16 {
4549            records.push(json!({
4550                "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 1),
4551                "type": "response_item",
4552                "payload": {
4553                    "type": "message",
4554                    "role": "user",
4555                    "content": [{
4556                        "type": "input_text",
4557                        "text": format!("request {turn}: {}", "context ".repeat(80)),
4558                    }],
4559                },
4560            }));
4561            records.push(json!({
4562                "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 2),
4563                "type": "response_item",
4564                "payload": {
4565                    "type": "message",
4566                    "role": "assistant",
4567                    "content": [{
4568                        "type": "output_text",
4569                        "text": format!("answer {turn}: {}", "implementation detail ".repeat(80)),
4570                    }],
4571                },
4572            }));
4573        }
4574        let source = format!(
4575            "{}\n",
4576            records
4577                .iter()
4578                .map(Value::to_string)
4579                .collect::<Vec<_>>()
4580                .join("\n")
4581        );
4582        std::fs::write(&source_path, &source).unwrap();
4583        let locator = SessionLocator {
4584            harness: HarnessId::from(HarnessId::CODEX),
4585            session_id: "codex-reduce".into(),
4586            storage: StorageLocator::File {
4587                path: source_path.clone(),
4588            },
4589        };
4590        let original = load_session(&locator).unwrap();
4591        let mut service =
4592            HarnessSessionService::new().with_reduction_store_root(store_root.clone());
4593
4594        let response = service.handle(request(
4595            1,
4596            "harness.v1.sessions.reduce",
4597            json!({
4598                "locator": locator,
4599                "target_harness": "claude-code",
4600                "keep_last": 4,
4601            }),
4602        ));
4603        assert!(response.get("error").is_none(), "{response:#}");
4604        let receipt = &response["result"]["receipt"];
4605        assert_eq!(receipt["source_harness"], "codex");
4606        assert_eq!(receipt["target_harness"], "claude-code");
4607        assert_eq!(receipt["verified"], true);
4608        assert_eq!(receipt["reversible"], true);
4609        assert!(receipt["reductions"].as_u64().unwrap() > 0);
4610        assert!(
4611            receipt["source_tokens"].as_u64().unwrap()
4612                > receipt["reduced_tokens"].as_u64().unwrap()
4613        );
4614        assert!(receipt["ratio"].as_f64().unwrap() > 1.0);
4615        assert!(response["result"]["bootstrap_prompt"]
4616            .as_str()
4617            .unwrap()
4618            .contains("Do not guess hidden content"));
4619
4620        let rescue_id = receipt["id"].as_str().unwrap();
4621        let store = crate::SessionStore::open(&store_root).unwrap();
4622        let sidecar =
4623            Session::from_sidecar_str(&store.load_sidecar(rescue_id).unwrap().unwrap()).unwrap();
4624        let log = store.load_reduction_log(rescue_id).unwrap().unwrap();
4625        let persisted_view = parse_messages_jsonl(&store.load(rescue_id).unwrap()).unwrap();
4626        let policy = reduce::ReductionPolicy {
4627            clear_turns_older_than: Some(4),
4628            ..Default::default()
4629        };
4630        let (restamped_view, reapplied_log) =
4631            reduce::project_messages(&sidecar.messages, &policy, &log);
4632        assert_eq!(
4633            messages_jsonl(&persisted_view).unwrap(),
4634            messages_jsonl(&restamped_view).unwrap()
4635        );
4636        assert_eq!(reapplied_log, log);
4637        reduce::verify_log(&log, &sidecar).unwrap();
4638        assert_eq!(
4639            reduce::invert(&restamped_view, &log, &sidecar).unwrap(),
4640            original.messages
4641        );
4642        assert_eq!(std::fs::read_to_string(&source_path).unwrap(), source);
4643
4644        std::fs::remove_dir_all(temp).ok();
4645    }
4646
4647    #[test]
4648    fn read_surfaces_view_a_severed_claude_graph_while_transfer_still_refuses_it() {
4649        let temp = std::env::temp_dir().join(format!(
4650            "supercode-severed-view-{}-{}",
4651            std::process::id(),
4652            generated_session_id()
4653        ));
4654        std::fs::create_dir_all(&temp).unwrap();
4655        let path = temp.join("severed.jsonl");
4656        // A live record whose parent was pruned — what a compacted or
4657        // resumed-across-files Claude Code session looks like on disk.
4658        std::fs::write(
4659            &path,
4660            concat!(
4661                r#"{"type":"user","uuid":"orphan-u","parentUuid":null,"message":{"role":"user","content":"stranded prompt"}}"#,
4662                "\n",
4663                r#"{"type":"assistant","uuid":"live-a","parentUuid":"pruned","message":{"id":"m","role":"assistant","content":[{"type":"text","text":"live answer"}]}}"#,
4664                "\n",
4665            ),
4666        )
4667        .unwrap();
4668        let locator = SessionLocator {
4669            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
4670            session_id: "severed".into(),
4671            storage: StorageLocator::File { path },
4672        };
4673        let mut service = HarnessSessionService::new();
4674
4675        let viewed = service.handle(request(
4676            1,
4677            "harness.v1.sessions.load",
4678            json!({"locator": locator}),
4679        ));
4680        let session = &viewed["result"]["session"];
4681        assert_eq!(session["fidelity"], "semantic");
4682        assert_eq!(session["messages"].as_array().unwrap().len(), 2);
4683        assert!(session["residue"].as_array().unwrap().iter().any(|entry| {
4684            entry
4685                .as_str()
4686                .is_some_and(|entry| entry.contains("live-a") && entry.contains("pruned"))
4687        }));
4688
4689        // Asking a READ surface for a lossless reconstruction gets the strict
4690        // refusal back, unchanged.
4691        let strict = service.handle(request(
4692            2,
4693            "harness.v1.sessions.load",
4694            json!({"locator": locator, "fidelity": "byte_lossless"}),
4695        ));
4696        assert!(strict["error"]["message"]
4697            .as_str()
4698            .unwrap()
4699            .contains("cannot reconstruct lossless Claude continuation"));
4700
4701        // Transfer/continuation surfaces have no view mode at all.
4702        let translated = service.handle(request(
4703            3,
4704            "harness.v1.sessions.translate",
4705            json!({"locator": locator, "target_harness": "codex"}),
4706        ));
4707        assert!(translated["error"]["message"]
4708            .as_str()
4709            .unwrap()
4710            .contains("cannot reconstruct lossless Claude continuation"));
4711        let resumed = service.handle(request(
4712            4,
4713            "harness.v1.sessions.resume_instructions",
4714            json!({"locator": locator}),
4715        ));
4716        assert!(resumed["error"]["message"]
4717            .as_str()
4718            .unwrap()
4719            .contains("cannot reconstruct lossless Claude continuation"));
4720
4721        let _ = std::fs::remove_dir_all(&temp);
4722    }
4723
4724    #[test]
4725    fn structured_resume_launches_cover_gemini_goose_and_supercode() {
4726        let codex = resume_launch(
4727            HarnessId::CODEX,
4728            "codex-session",
4729            Path::new("/tmp/project"),
4730            ResumePolicy::Yolo,
4731        )
4732        .unwrap_or_else(|_| panic!("Codex resume launch must be registered"));
4733        assert_eq!(codex.program, "codex");
4734        assert_eq!(
4735            codex.arguments,
4736            [
4737                "-c",
4738                "check_for_update_on_startup=false",
4739                "-c",
4740                "projects.\"/tmp/project\".trust_level=\"trusted\"",
4741                "--dangerously-bypass-approvals-and-sandbox",
4742                "--dangerously-bypass-hook-trust",
4743                "resume",
4744                "codex-session",
4745            ]
4746        );
4747
4748        let gemini = resume_launch(
4749            HarnessId::GEMINI,
4750            "gemini-session",
4751            Path::new("/tmp/project"),
4752            ResumePolicy::Yolo,
4753        )
4754        .unwrap_or_else(|_| panic!("Gemini resume launch must be registered"));
4755        assert_eq!(gemini.program, "gemini");
4756        assert_eq!(gemini.arguments, ["--yolo", "--resume", "gemini-session"]);
4757
4758        let goose = resume_launch(
4759            HarnessId::GOOSE,
4760            "goose-session",
4761            Path::new("/tmp/project"),
4762            ResumePolicy::Yolo,
4763        )
4764        .unwrap_or_else(|_| panic!("Goose resume launch must be registered"));
4765        assert_eq!(goose.program, "goose");
4766        assert_eq!(
4767            goose.arguments,
4768            ["session", "--resume", "--session-id", "goose-session"]
4769        );
4770
4771        let supercode = resume_launch(
4772            HarnessId::SUPERCODE,
4773            "supercode-session",
4774            Path::new("/tmp/project"),
4775            ResumePolicy::Yolo,
4776        )
4777        .unwrap_or_else(|_| panic!("Supercode resume launch must be registered"));
4778        assert_eq!(supercode.program, "supercode");
4779        assert_eq!(
4780            supercode.arguments,
4781            ["--dangerous", "resume", "supercode-session"]
4782        );
4783    }
4784
4785    #[test]
4786    fn diagonal_artifacts_preserve_claude_subagents_and_grok_bundle_members() {
4787        let temp = std::env::temp_dir().join(format!(
4788            "supercode-harness-artifact-{}-{}",
4789            std::process::id(),
4790            generated_session_id()
4791        ));
4792        let main_path = temp.join("parent.jsonl");
4793        let subagent_path = temp.join("parent/subagents/agent-child.jsonl");
4794        std::fs::create_dir_all(subagent_path.parent().unwrap()).unwrap();
4795        let fixture = std::fs::read_to_string(
4796            PathBuf::from(env!("CARGO_MANIFEST_DIR"))
4797                .join("tests/fixtures/claude_code_session.jsonl"),
4798        )
4799        .unwrap();
4800        let parent = fixture.trim_end_matches('\n');
4801        let child = fixture.trim_end_matches('\n');
4802        std::fs::write(&main_path, parent).unwrap();
4803        std::fs::write(&subagent_path, child).unwrap();
4804        let locator = SessionLocator {
4805            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
4806            session_id: "213bb148-51ea-453f-9206-f8b4b1168547".into(),
4807            storage: StorageLocator::File {
4808                path: main_path.clone(),
4809            },
4810        };
4811        let mut service = HarnessSessionService::new();
4812        let claude = service.handle(request(
4813            1,
4814            "harness.v1.sessions.translate",
4815            json!({"locator": locator, "target_harness": "claude-code"}),
4816        ));
4817        let artifact = &claude["result"]["artifact"];
4818        assert_eq!(artifact["fidelity"], "byte_lossless");
4819        assert_eq!(artifact["content"], parent);
4820        let files = artifact["files"].as_array().unwrap();
4821        assert!(files.iter().any(|file| {
4822            file["role"] == "subagent"
4823                && file["path"]
4824                    .as_str()
4825                    .is_some_and(|path| path.ends_with("/subagents/agent-child.jsonl"))
4826                && file["content"] == child
4827        }));
4828        assert!(!artifact["content"].as_str().unwrap().ends_with('\n'));
4829
4830        let grok = service.handle(request(
4831            2,
4832            "harness.v1.sessions.translate",
4833            json!({"locator": grok_locator(), "target_harness": "grok"}),
4834        ));
4835        let files = grok["result"]["artifact"]["files"].as_array().unwrap();
4836        for name in ["summary.json", "updates.jsonl"] {
4837            let expected = std::fs::read_to_string(
4838                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
4839                    .join("tests/fixtures/grok_session")
4840                    .join(name),
4841            )
4842            .unwrap();
4843            assert!(files.iter().any(|file| {
4844                file["path"] == name && file["role"] == "bundle" && file["content"] == expected
4845            }));
4846        }
4847        std::fs::remove_dir_all(temp).ok();
4848    }
4849
4850    #[test]
4851    fn every_non_grok_handoff_mints_and_uses_a_fresh_target_identity() {
4852        let mut service = HarnessSessionService::new();
4853        let source = pi_locator();
4854        for (target, format) in [
4855            ("claude-code", SessionFormat::ClaudeCode),
4856            ("codex", SessionFormat::Codex),
4857            ("opencode", SessionFormat::OpenCode),
4858            ("pi", SessionFormat::Pi),
4859        ] {
4860            let result = service.handle(request(
4861                1,
4862                "harness.v1.sessions.handoff",
4863                json!({"locator": source, "target_harness": target, "cwd": "/tmp/project"}),
4864            ));
4865            let artifact = &result["result"]["artifact"];
4866            let target_id = artifact["session_id"].as_str().unwrap();
4867            assert_ne!(target_id, source.session_id, "{target}");
4868            let parsed = Session::load_str(artifact["content"].as_str().unwrap(), format).unwrap();
4869            assert_eq!(
4870                parsed.meta.session_id.as_deref(),
4871                Some(target_id),
4872                "{target}"
4873            );
4874            if target != "pi" {
4875                assert!(result["result"]["launch"]["arguments"]
4876                    .as_array()
4877                    .unwrap()
4878                    .iter()
4879                    .any(|argument| argument == target_id));
4880            }
4881            if target == "opencode" {
4882                assert!(target_id.starts_with("ses_"));
4883                fn assert_session_ids(value: &Value, target_id: &str) {
4884                    match value {
4885                        Value::Object(fields) => {
4886                            if let Some(session_id) = fields.get("sessionID") {
4887                                assert_eq!(session_id, target_id);
4888                            }
4889                            for child in fields.values() {
4890                                assert_session_ids(child, target_id);
4891                            }
4892                        }
4893                        Value::Array(values) => {
4894                            for child in values {
4895                                assert_session_ids(child, target_id);
4896                            }
4897                        }
4898                        _ => {}
4899                    }
4900                }
4901                let document: Value =
4902                    serde_json::from_str(artifact["content"].as_str().unwrap()).unwrap();
4903                assert_session_ids(&document, target_id);
4904            }
4905        }
4906
4907        let first = service.handle(request(
4908            2,
4909            "harness.v1.sessions.handoff",
4910            json!({"locator": source, "target_harness": "codex"}),
4911        ));
4912        let second = service.handle(request(
4913            3,
4914            "harness.v1.sessions.handoff",
4915            json!({"locator": source, "target_harness": "codex"}),
4916        ));
4917        assert_ne!(
4918            first["result"]["artifact"]["session_id"],
4919            second["result"]["artifact"]["session_id"]
4920        );
4921    }
4922
4923    #[test]
4924    fn grok_handoff_uses_the_official_importer_contract() {
4925        let mut service = HarnessSessionService::new();
4926        let source = opencode_locator();
4927        let response = service.handle(request(
4928            1,
4929            "harness.v1.sessions.handoff",
4930            json!({
4931                "locator": source,
4932                "target_harness": "grok",
4933                "cwd": "/tmp/grok-handoff-project",
4934            }),
4935        ));
4936        let result = &response["result"];
4937
4938        // The target is Grok, but the artifact truthfully names the Claude Code wire
4939        // format accepted by Grok's official importer. Raw Grok chat_history JSONL is
4940        // not a complete stock-resumable bundle.
4941        assert_eq!(result["artifact"]["target_harness"], "claude-code");
4942        assert!(result["artifact"]["suggested_filename"]
4943            .as_str()
4944            .unwrap()
4945            .ends_with(".grok-import.claude-code.jsonl"));
4946        let artifact = Session::load_str(
4947            result["artifact"]["content"].as_str().unwrap(),
4948            SessionFormat::ClaudeCode,
4949        )
4950        .unwrap();
4951        assert_eq!(
4952            artifact.meta.cwd.as_deref(),
4953            Some(Path::new("/tmp/grok-handoff-project"))
4954        );
4955        let target_session_id = artifact.meta.session_id.as_deref().unwrap();
4956        assert_eq!(target_session_id.len(), 36);
4957        assert_eq!(target_session_id.as_bytes()[14], b'4');
4958        assert_ne!(target_session_id, opencode_locator().session_id);
4959        assert_eq!(
4960            result["artifact"]["session_id"],
4961            artifact.meta.session_id.as_deref().unwrap()
4962        );
4963
4964        assert_eq!(
4965            result["materialize"]["arguments"],
4966            json!(["import", "--json", "{artifact_path}"])
4967        );
4968        assert_eq!(
4969            result["launch"]["arguments"],
4970            json!(["--resume", "{imported_session_id}", "--fork-session"])
4971        );
4972        assert!(result["note"]
4973            .as_str()
4974            .unwrap()
4975            .contains("outcome=imported"));
4976        assert!(!result["launch"]["arguments"]
4977            .as_array()
4978            .unwrap()
4979            .iter()
4980            .any(|argument| argument == &opencode_locator().session_id));
4981    }
4982
4983    #[tokio::test]
4984    async fn inventory_rejects_unknown_harnesses_and_runtime_attach_is_honest() {
4985        let mut service = HarnessSessionService::new();
4986        let inventory = service
4987            .handle_async(request(
4988                1,
4989                "harness.v1.harnesses.list",
4990                json!({"harnesses": ["missing"]}),
4991            ))
4992            .await;
4993        assert_eq!(inventory["error"]["code"], -32602);
4994
4995        let attached = service
4996            .handle_async(request(
4997                2,
4998                "harness.v1.runtimes.attach_existing",
4999                json!({"harness": "codex", "runtime_id": "thread-1"}),
5000            ))
5001            .await;
5002        assert_eq!(attached["error"]["code"], -32000);
5003        assert!(attached["error"]["message"]
5004            .as_str()
5005            .unwrap()
5006            .contains("runtimes.resume"));
5007    }
5008
5009    #[test]
5010    fn invalid_params_and_unknown_methods_use_json_rpc_errors() {
5011        let mut service = HarnessSessionService::new();
5012        let invalid = service.handle(request(1, "harness.v1.sessions.load", json!({})));
5013        assert_eq!(invalid["error"]["code"], -32602);
5014        let unknown = service.handle(request(2, "harness.v1.unknown", json!({})));
5015        assert_eq!(unknown["error"]["code"], -32601);
5016    }
5017
5018    #[cfg(unix)]
5019    #[tokio::test]
5020    // The test mutates process-wide harness environment and deliberately
5021    // holds the global test lock until every async runtime operation ends.
5022    #[allow(clippy::await_holding_lock)]
5023    async fn async_service_drives_a_generic_acp_runtime() {
5024        let _environment_guard = crate::live_runtime::test_environment_lock();
5025        let script = r#"
5026            i=0
5027            while IFS= read -r line; do
5028              i=$((i + 1))
5029              case "$i" in
5030                1) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{},"authMethods":[]}}' ;;
5031                2) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"svc_acp"}}' ;;
5032                3)
5033                  printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ok"}}}}'
5034                  printf '%s\n' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}'
5035                  ;;
5036                4)
5037                  printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"from terminal"}}}}'
5038                  printf '%s\n' '{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}'
5039                  ;;
5040              esac
5041            done
5042        "#;
5043        let mut service = HarnessSessionService::new();
5044        let started = service
5045            .handle_async(request(
5046                1,
5047                "harness.v1.runtimes.start",
5048                json!({
5049                    "harness": "codex",
5050                    "protocol": "acp",
5051                    "cwd": std::env::current_dir().unwrap(),
5052                    "launch": {"program": "/bin/sh", "arguments": ["-c", script], "env": {}},
5053                }),
5054            ))
5055            .await;
5056        assert_eq!(started["result"]["connection"], "runtime-1");
5057        assert_eq!(started["result"]["handle"]["runtime_id"], "svc_acp");
5058
5059        let terminal = service
5060            .handle_async(request(
5061                9,
5062                "harness.v1.runtimes.terminal_instructions",
5063                json!({"connection":"runtime-1"}),
5064            ))
5065            .await;
5066        let arguments = terminal["result"]["launch"]["arguments"]
5067            .as_array()
5068            .expect("hosted runtime should return terminal arguments");
5069        let endpoint_index = arguments
5070            .iter()
5071            .position(|value| value == "--endpoint")
5072            .expect("terminal command should use an opaque endpoint");
5073        let endpoint = LiveRuntimeEndpoint::parse(
5074            arguments[endpoint_index + 1]
5075                .as_str()
5076                .expect("endpoint argument should be text"),
5077        )
5078        .unwrap();
5079        assert!(!terminal.to_string().contains("Bearer"));
5080        let workspace = std::env::current_dir().unwrap();
5081        let receipt = resolve_live_runtime(
5082            &endpoint,
5083            &LiveRuntimeSource {
5084                harness: "codex".into(),
5085                session_id: "svc_acp".into(),
5086                workspace,
5087            },
5088        )
5089        .unwrap();
5090        let remote = crate::HttpFrontendRuntime::connect(receipt.base_url, receipt.token)
5091            .await
5092            .unwrap();
5093        let mut attachment = crate::FrontendRuntime::attach(remote.as_ref(), 100)
5094            .await
5095            .unwrap();
5096
5097        let sent = service
5098            .handle_async(request(
5099                2,
5100                "harness.v1.runtimes.send_input",
5101                json!({"connection": "runtime-1", "text": "hi"}),
5102            ))
5103            .await;
5104        assert_eq!(sent["result"]["turn_id"], "3");
5105
5106        let mut events = Vec::new();
5107        for _ in 0..20 {
5108            events.extend(service.poll_runtimes().await);
5109            if events.len() >= 2 {
5110                break;
5111            }
5112            tokio::time::sleep(Duration::from_millis(2)).await;
5113        }
5114        assert!(events
5115            .iter()
5116            .any(|event| { event["params"]["event"]["kind"] == "session/update" }));
5117        assert!(events.iter().any(|event| {
5118            event["params"]["event"]["kind"] == "supercode/acp_request_completed"
5119        }));
5120
5121        let saw_editor_reply = tokio::time::timeout(Duration::from_secs(2), async {
5122            loop {
5123                let event = attachment.next_event().await.unwrap();
5124                if event.kind == "text_delta" && event.payload["text"] == "ok" {
5125                    break;
5126                }
5127            }
5128        })
5129        .await;
5130        assert!(
5131            saw_editor_reply.is_ok(),
5132            "terminal should observe the editor-driven turn"
5133        );
5134
5135        crate::FrontendRuntime::submit(remote.as_ref(), "DRIVE FROM TERMINAL".into())
5136            .await
5137            .unwrap();
5138        let saw_terminal_reply = tokio::time::timeout(Duration::from_secs(2), async {
5139            loop {
5140                let event = attachment.next_event().await.unwrap();
5141                if event.kind == "text_delta" && event.payload["text"] == "from terminal" {
5142                    break;
5143                }
5144            }
5145        })
5146        .await;
5147        assert!(
5148            saw_terminal_reply.is_ok(),
5149            "terminal should drive the same runtime"
5150        );
5151
5152        let closed = service
5153            .handle_async(request(
5154                3,
5155                "harness.v1.runtimes.close",
5156                json!({"connection": "runtime-1"}),
5157            ))
5158            .await;
5159        assert_eq!(closed["result"]["closed"], true);
5160    }
5161}