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::sync::Arc;
10use std::time::Duration;
11
12use serde::{Deserialize, Serialize};
13use serde_json::{json, Value};
14use tokio::sync::Notify;
15
16use crate::runtime::generated_session_id;
17#[cfg(feature = "adapter-api")]
18use crate::runtime::{HostedHarnessConnection, HostedHarnessRuntime};
19use crate::sdk::{
20    discover_session_page, load_session, load_session_with_fidelity, SdkCapabilities, SdkError,
21    SdkErrorCode, SdkEvent, SdkOperation, SdkRequest, SdkRuntimeEvent, SdkService,
22};
23use crate::watch::{bound_session_view, message_json, normalized_session_json};
24use crate::Fidelity;
25#[cfg(feature = "adapter-api")]
26use crate::SupercodeHttpRuntimeBackend;
27use crate::{
28    discover_live_runtime, harness_support_registry, AcpRuntimeBackend, ClaudeCodeRuntimeBackend,
29    CodexRuntimeBackend, DiscoveryQuery, HarnessCatalog, HarnessHomes, HarnessId,
30    ImplementationKind, LiveRuntimeEndpoint, LiveRuntimeSource, OpenCodeRuntimeBackend,
31    PiRuntimeBackend, Role, RuntimeAttachRequest, RuntimeBackend, RuntimeConnection, RuntimeInput,
32    RuntimeLaunch, RuntimeStartRequest, Session, SessionDescriptor, SessionFollower, SessionFormat,
33    SessionLocator, SessionSource,
34};
35use crate::{reduce, tokens};
36#[cfg(feature = "adapter-api")]
37use crate::{register_live_runtime, resolve_live_runtime, LiveRuntimeRegistration};
38
39/// Every JSON-RPC method the harness service dispatches (`harness.v1.capabilities`
40/// reports it; ORCH-4 registry tiers must cite entries of it).
41pub const HARNESS_SERVICE_METHODS: &[&str] = &[
42    "harness.v1.support.report",
43    "harness.v1.harnesses.list",
44    "harness.v1.harnesses.probe",
45    "harness.v1.harnesses.settings",
46    "harness.v1.harnesses.configure",
47    "harness.v1.harnesses.auth.methods",
48    "harness.v1.harnesses.auth.begin",
49    "harness.v1.harnesses.auth.verify",
50    "harness.v1.sessions.discover",
51    "harness.v1.sessions.load",
52    "harness.v1.sessions.follow",
53    "harness.v1.sessions.unfollow",
54    "harness.v1.sessions.activity.subscribe",
55    "harness.v1.sessions.activity.unsubscribe",
56    "harness.v1.sessions.index.subscribe",
57    "harness.v1.sessions.index.resize",
58    "harness.v1.sessions.index.unsubscribe",
59    "harness.v1.sessions.message",
60    "harness.v1.sessions.import",
61    "harness.v1.sessions.export",
62    "harness.v1.sessions.translate",
63    "harness.v1.sessions.reduce",
64    "harness.v1.sessions.branch",
65    "harness.v1.sessions.handoff",
66    "harness.v1.sessions.resume_instructions",
67    "harness.v1.skills.list",
68    "harness.v1.skills.install",
69    "harness.v1.skills.remove",
70    "harness.v1.memory.show",
71    "harness.v1.memory.search",
72    "harness.v1.jobs.list",
73    "harness.v1.jobs.get",
74    "harness.v1.jobs.create",
75    "harness.v1.jobs.update",
76    "harness.v1.jobs.pause",
77    "harness.v1.jobs.resume",
78    "harness.v1.jobs.run",
79    "harness.v1.jobs.delete",
80    "harness.v1.jobs.apply",
81    "harness.v1.jobs.notepad",
82    "harness.v1.jobs.notepad_set",
83    "harness.v1.jobs.notepad_delete",
84    "harness.v1.model_route.apply",
85    "harness.v1.sessions.new",
86    "harness.v1.sessions.reset",
87    "harness.v1.sessions.archive",
88    "harness.v1.sessions.delete",
89    "harness.v1.runs.list",
90    "harness.v1.runs.get",
91    "harness.v1.approvals.list",
92    "harness.v1.approvals.resolve",
93    "harness.v1.runtimes.capabilities",
94    "harness.v1.runtimes.start",
95    "harness.v1.runtimes.resume",
96    "harness.v1.runtimes.attach_existing",
97    "harness.v1.runtimes.attach",
98    "harness.v1.runtimes.send_input",
99    "harness.v1.runtimes.interrupt",
100    "harness.v1.runtimes.steer",
101    "harness.v1.runtimes.respond",
102    "harness.v1.runtimes.terminal_instructions",
103    "harness.v1.runtimes.close",
104    "harness.v1.profiles.list",
105    "harness.v1.profiles.get",
106    "harness.v1.profiles.create",
107    "harness.v1.profiles.delete",
108    "harness.v1.channels.list",
109    "harness.v1.routes.list",
110    "harness.v1.triggers.list",
111    "harness.v1.channels.status",
112    "harness.v1.orchestration.load",
113    "harness.v1.orchestration.save",
114    "harness.v1.orchestration.compile",
115    "harness.v1.orchestration.decompile",
116    "harness.v1.orchestration.import",
117    "harness.v1.orchestration.export",
118    "harness.v1.workflow.load",
119];
120
121/// Protocol namespace implemented by this service.
122pub const HARNESS_SERVICE_VERSION: &str = "harness.v1";
123/// Notification method emitted for followed-session changes.
124pub const SESSION_EVENT_METHOD: &str = "harness.v1.sessions.event";
125/// Notification method emitted for normalized session-activity transitions.
126pub const SESSION_ACTIVITY_EVENT_METHOD: &str = "harness.v1.sessions.activity_event";
127/// Notification method emitted for revisioned session-list changes.
128pub const SESSION_INDEX_EVENT_METHOD: &str = "harness.v1.sessions.index_event";
129/// Notification method emitted for live runtime events.
130pub const RUNTIME_EVENT_METHOD: &str = "harness.v1.runtimes.event";
131
132/// Stateful persisted-session service. Each instance owns its follow
133/// subscriptions; discovery and loading remain read-only.
134pub struct HarnessSessionService {
135    catalog: HarnessCatalog,
136    followers: BTreeMap<String, SessionFollower>,
137    followed_sources: BTreeMap<String, FollowedSource>,
138    activity_subscriptions: BTreeMap<String, ActivitySubscription>,
139    index_subscriptions: BTreeMap<String, crate::session_index::SessionIndexSubscription>,
140    index_notifier: Arc<Notify>,
141    #[cfg(feature = "adapter-api")]
142    activity_monitor: crate::session_activity::SessionActivityMonitor,
143    next_subscription: u64,
144    runtimes: BTreeMap<String, Box<dyn RuntimeConnection>>,
145    /// Connections lent to a detached call that is running right now. The
146    /// runtime itself is OUT of `runtimes` for that whole call, and these
147    /// names are how a second caller is told the connection is busy rather
148    /// than unknown.
149    runtimes_in_flight: BTreeSet<String>,
150    terminal_launches: BTreeMap<String, StructuredLaunch>,
151    runtime_sequences: BTreeMap<String, u64>,
152    next_runtime: u64,
153    reduction_store_root: Option<PathBuf>,
154    /// ORCH-9: live permission/approval requests outstanding on the open
155    /// runtime connections above, fed by the same event pump that publishes
156    /// `harness.v1.runtimes.event`.
157    approvals: crate::approvals::ApprovalRegistry,
158    /// ORCH-9: supercode's own queued subagent approvals, when the host that
159    /// owns this service publishes its parent queue here.
160    subagent_approvals: Option<Arc<std::sync::Mutex<Vec<crate::subagents::QueuedApproval>>>>,
161}
162
163impl Default for HarnessSessionService {
164    fn default() -> Self {
165        Self::new()
166    }
167}
168
169impl HarnessSessionService {
170    /// Create an empty service instance.
171    pub fn new() -> Self {
172        Self {
173            catalog: HarnessCatalog::new(),
174            followers: BTreeMap::new(),
175            followed_sources: BTreeMap::new(),
176            activity_subscriptions: BTreeMap::new(),
177            index_subscriptions: BTreeMap::new(),
178            index_notifier: Arc::new(Notify::new()),
179            #[cfg(feature = "adapter-api")]
180            activity_monitor: Default::default(),
181            next_subscription: 1,
182            runtimes: BTreeMap::new(),
183            runtimes_in_flight: BTreeSet::new(),
184            terminal_launches: BTreeMap::new(),
185            runtime_sequences: BTreeMap::new(),
186            next_runtime: 1,
187            reduction_store_root: None,
188            approvals: crate::approvals::ApprovalRegistry::new(),
189            subagent_approvals: None,
190        }
191    }
192
193    /// Override the trusted, service-owned store used for durable reduction
194    /// bundles. Embedders and tests use this to keep all writes inside an
195    /// explicitly selected root; the CLI otherwise uses the normal
196    /// `$SUPERCODE_HOME/sessions` location.
197    pub fn with_reduction_store_root(mut self, root: impl Into<PathBuf>) -> Self {
198        self.reduction_store_root = Some(root.into());
199        self
200    }
201
202    /// ORCH-9: publish the parent's own subagent-approval queue into
203    /// `harness.v1.approvals.list`.
204    ///
205    /// This is the SAME `Arc` an [`crate::Agent`] pushes into
206    /// (`Agent::pending_child_approvals`), so a host that runs supercode's own
207    /// loop beside this service surfaces those requests through the uniform
208    /// door without copying them anywhere.
209    pub fn observe_subagent_approvals(
210        &mut self,
211        queue: Arc<std::sync::Mutex<Vec<crate::subagents::QueuedApproval>>>,
212    ) {
213        self.subagent_approvals = Some(queue);
214    }
215
216    /// ORCH-9: every approval request this service can see, newest last.
217    ///
218    /// Two sources, both live: the requests outstanding on the open runtime
219    /// connections, and supercode's own queued subagent approvals. There is
220    /// no file or database source at the pinned harness versions (see
221    /// [`crate::approvals`]), so a stored or proposal row is never produced.
222    pub fn approvals(&self, query: &crate::approvals::ApprovalsQuery) -> Vec<crate::ApprovalRow> {
223        let now = crate::approvals::now_ms();
224        let mut rows = self.approvals.rows(now);
225        if let Some(queue) = self.subagent_approvals.as_ref() {
226            let queued = queue
227                .lock()
228                .unwrap_or_else(std::sync::PoisonError::into_inner)
229                .clone();
230            rows.extend(crate::approvals::subagent_rows(&queued, now));
231        }
232        rows.retain(|row| query.matches(row));
233        rows.sort_by(|left, right| {
234            left.requested_at_ms
235                .cmp(&right.requested_at_ms)
236                .then_with(|| left.id.cmp(&right.id))
237        });
238        rows
239    }
240
241    /// ORCH-20 (controlled tier): answer one listed approval request by its
242    /// row id and one uniform decision.
243    ///
244    /// The decision is translated into the option token and reply envelope
245    /// the door that raised the request already accepts
246    /// ([`crate::approvals::plan_reply`]), and the answer is then sent by
247    /// calling `harness.v1.runtimes.respond` itself — the same code path, the
248    /// same adapter, the same bookkeeping that drops the row. This verb adds
249    /// a translation and nothing else.
250    async fn approvals_resolve(
251        &mut self,
252        params: Value,
253    ) -> std::result::Result<Value, ServiceError> {
254        let params = decode::<crate::approvals::ApprovalsResolveParams>(params)?;
255        if params.id.trim().is_empty() {
256            return Err(ServiceError::InvalidParams(
257                "approvals resolve requires the `id` of a listed approval row".into(),
258            ));
259        }
260        let choice = match (params.decision, params.option_id.as_deref()) {
261            (Some(_), Some(_)) => {
262                return Err(ServiceError::InvalidParams(
263                    "approvals resolve takes either `decision` or `option_id`, not both".into(),
264                ))
265            }
266            (Some(decision), None) => crate::approvals::ApprovalChoice::Decision(decision),
267            (None, Some(option)) => crate::approvals::ApprovalChoice::Option(option.to_string()),
268            (None, None) => {
269                return Err(ServiceError::InvalidParams(format!(
270                    "approvals resolve requires `decision` ({}) or an explicit `option_id`",
271                    crate::approvals::ApprovalDecision::ALL
272                        .map(|decision| decision.as_str())
273                        .join(" | "),
274                )))
275            }
276        };
277        let resolution = self
278            .approvals
279            .resolution(&params.id, &choice)
280            .map_err(|error| ServiceError::InvalidParams(error.to_string()))?;
281        // The harness's own door, unchanged: this is the identical call
282        // `harness.v1.runtimes.respond` performs for a caller who built the
283        // envelope by hand, including dropping the answered row.
284        self.runtime_call(
285            "harness.v1.runtimes.respond",
286            json!({
287                "connection": resolution.connection,
288                "request_id": resolution.request_id,
289                "response": resolution.response,
290            }),
291        )
292        .await?;
293        Ok(json!({
294            "id": params.id,
295            "decision": params.decision.map(|decision| decision.as_str()),
296            "option_id": resolution.option_id,
297            "resolved": true,
298        }))
299    }
300
301    /// Return the edge-triggered wakeup used by session-index filesystem
302    /// subscriptions. Transports can await this instead of polling indexes.
303    #[cfg(feature = "adapter-api")]
304    pub fn session_index_notifier(&self) -> Arc<Notify> {
305        Arc::clone(&self.index_notifier)
306    }
307
308    /// Handle one JSON-RPC 2.0 request and return one JSON-RPC response.
309    #[cfg(feature = "adapter-api")]
310    pub fn handle(&mut self, request: Value) -> Value {
311        let id = request.get("id").cloned().unwrap_or(Value::Null);
312        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
313            return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
314        }
315        let Some(method) = request.get("method").and_then(Value::as_str) else {
316            return rpc_error(id, -32600, "request is missing `method`");
317        };
318        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
319        match self.call(method, params) {
320            Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
321            Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
322            Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
323            Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
324            Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
325            Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
326        }
327    }
328
329    /// Handle either a persisted-session request or an asynchronous live
330    /// runtime request.
331    #[cfg(feature = "adapter-api")]
332    pub async fn handle_async(&mut self, request: Value) -> Value {
333        let method = request
334            .get("method")
335            .and_then(Value::as_str)
336            .unwrap_or_default();
337        if matches!(
338            method,
339            "harness.v1.harnesses.list" | "harness.v1.harnesses.probe"
340        ) {
341            let id = request.get("id").cloned().unwrap_or(Value::Null);
342            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
343                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
344            }
345            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
346            return match self.inventory_call(method, params).await {
347                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
348                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
349                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
350                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
351                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
352                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
353            };
354        }
355        if matches!(
356            method,
357            "harness.v1.harnesses.auth.methods"
358                | "harness.v1.harnesses.auth.begin"
359                | "harness.v1.harnesses.auth.verify"
360        ) {
361            let id = request.get("id").cloned().unwrap_or(Value::Null);
362            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
363                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
364            }
365            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
366            return match self.harness_authentication_call(method, params).await {
367                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
368                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
369                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
370                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
371                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
372                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
373            };
374        }
375        // ORCH-19 controlled tier. Answered here rather than through the SDK
376        // operation dispatch below so the harness's OWN refusal reaches the
377        // caller: `sdk_error` collapses every `UnsupportedAction` to one
378        // generic sentence, and the whole point of this tier is that a
379        // refusal names which door the harness does have.
380        if matches!(
381            method,
382            "harness.v1.sessions.new"
383                | "harness.v1.sessions.reset"
384                | "harness.v1.sessions.archive"
385                | "harness.v1.sessions.delete"
386        ) {
387            let id = request.get("id").cloned().unwrap_or(Value::Null);
388            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
389                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
390            }
391            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
392            let verb = match method {
393                "harness.v1.sessions.new" => crate::SessionVerb::New,
394                "harness.v1.sessions.reset" => crate::SessionVerb::Reset,
395                "harness.v1.sessions.archive" => crate::SessionVerb::Archive,
396                _ => crate::SessionVerb::Delete,
397            };
398            return match self.mutate_session(verb, params).await {
399                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
400                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
401                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
402                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
403                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
404                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
405            };
406        }
407        if method == "harness.v1.sessions.message" {
408            let id = request.get("id").cloned().unwrap_or(Value::Null);
409            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
410                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
411            }
412            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
413            return match self.message_call(params).await {
414                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
415                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
416                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
417                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
418                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
419                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
420            };
421        }
422        if matches!(
423            method,
424            "harness.v1.harnesses.settings" | "harness.v1.harnesses.configure"
425        ) {
426            let id = request.get("id").cloned().unwrap_or(Value::Null);
427            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
428                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
429            }
430            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
431            return match self.harness_settings_call(method, params) {
432                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
433                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
434                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
435                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
436                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
437                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
438            };
439        }
440        if method == "harness.v1.sessions.activity.subscribe" {
441            let id = request.get("id").cloned().unwrap_or(Value::Null);
442            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
443                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
444            }
445            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
446            return match self.subscribe_session_activity(params).await {
447                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
448                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
449                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
450                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
451                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
452                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
453            };
454        }
455        if let Some(operation) = SdkOperation::from_method(method) {
456            let id = request.get("id").cloned().unwrap_or(Value::Null);
457            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
458                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
459            }
460            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
461            return match self.execute(SdkRequest { operation, params }).await {
462                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
463                Err(error) => sdk_rpc_error(id, &error),
464            };
465        }
466        if !method.starts_with("harness.v1.runtimes.") {
467            return self.handle(request);
468        }
469        let id = request.get("id").cloned().unwrap_or(Value::Null);
470        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
471            return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
472        }
473        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
474        match self.runtime_call(method, params).await {
475            Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
476            Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
477            Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
478            Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
479            Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
480            Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
481        }
482    }
483
484    /// Poll all active subscriptions once and return zero or more JSON-RPC
485    /// notifications. Recoverable follower errors are delivered as events.
486    #[cfg(feature = "adapter-api")]
487    pub fn poll(&mut self) -> Vec<Value> {
488        let mut notifications = Vec::new();
489        for (subscription, follower) in &mut self.followers {
490            match follower.poll() {
491                Ok(Some(event)) => notifications.push(json!({
492                    "jsonrpc": "2.0",
493                    "method": SESSION_EVENT_METHOD,
494                    "params": {
495                        "subscription": subscription,
496                        "event": event.to_json(),
497                    }
498                })),
499                Ok(None) => {}
500                Err(error) => notifications.push(json!({
501                    "jsonrpc": "2.0",
502                    "method": SESSION_EVENT_METHOD,
503                    "params": {
504                        "subscription": subscription,
505                        "event": {
506                            "type": "watch_error",
507                            "recoverable": true,
508                            "message": error.to_string(),
509                        },
510                    }
511                })),
512            }
513        }
514        notifications
515    }
516
517    /// Report each followed session's live-runtime lifecycle state on that
518    /// session's own subscription, emitting only when the state changes.
519    ///
520    /// A growing transcript is not evidence that an agent is working, so the
521    /// state comes from the live-runtime registry and nowhere else. A followed
522    /// session with no registered Supercode runtime — a harness running outside
523    /// Supercode — reports `persisted`, which says plainly that its activity is
524    /// unknown rather than guessing at it. These events carry no sequence
525    /// number and no transcript content; they never interleave with the
526    /// content follower's sequenced stream.
527    #[cfg(feature = "adapter-api")]
528    pub async fn poll_session_runtime_states(&mut self) -> Vec<Value> {
529        let registry = crate::LocalRuntimeRegistry::new();
530        let authorization = crate::RuntimeAuthorization::observer();
531        let mut notifications = Vec::new();
532        for (subscription, source) in &mut self.followed_sources {
533            let state = match registry
534                .source_state(&source.harness, &source.session_id, &authorization)
535                .await
536            {
537                Ok(Some(state)) => state,
538                Ok(None) => crate::RuntimeRegistryState::Persisted,
539                // A failed registry read is not evidence of a state change.
540                Err(_) => continue,
541            };
542            if source.reported.as_deref() == Some(state.as_str()) {
543                continue;
544            }
545            source.reported = Some(state.as_str().to_string());
546            notifications.push(json!({
547                "jsonrpc": "2.0",
548                "method": SESSION_EVENT_METHOD,
549                "params": {
550                    "subscription": subscription,
551                    "event": {"type": "runtime_state", "state": state.as_str()},
552                },
553            }));
554        }
555        notifications
556    }
557
558    /// Poll normalized activity subscriptions, emitting only proven state
559    /// transitions. Every subscription is bulk-sampled so stock-harness
560    /// process and registry discovery happens once per UI, not once per row.
561    #[cfg(feature = "adapter-api")]
562    pub async fn poll_session_activities(&mut self) -> Vec<Value> {
563        let subscriptions = self
564            .activity_subscriptions
565            .iter()
566            .map(|(id, subscription)| {
567                (
568                    id.clone(),
569                    subscription.locators.clone(),
570                    subscription.homes.clone(),
571                )
572            })
573            .collect::<Vec<_>>();
574        let mut notifications = Vec::new();
575        for (subscription_id, locators, homes) in subscriptions {
576            let Ok(activities) = self.activity_monitor.resolve(&locators, &homes).await else {
577                // A failed evidence read proves no transition. Retain the last
578                // good state instead of flashing every row to persisted.
579                continue;
580            };
581            let Some(subscription) = self.activity_subscriptions.get_mut(&subscription_id) else {
582                continue;
583            };
584            let mut changed = Vec::new();
585            for activity in activities {
586                let key = activity.key();
587                if subscription
588                    .reported
589                    .get(&key)
590                    .is_some_and(|previous| previous.same_state(&activity))
591                {
592                    continue;
593                }
594                subscription.reported.insert(key, activity.clone());
595                changed.push(activity);
596            }
597            if !changed.is_empty() {
598                notifications.push(json!({
599                    "jsonrpc": "2.0",
600                    "method": SESSION_ACTIVITY_EVENT_METHOD,
601                    "params": {
602                        "subscription": subscription_id,
603                        "activities": changed,
604                    },
605                }));
606            }
607        }
608        notifications
609    }
610
611    /// Drain native-store invalidations and emit revisioned descriptor deltas.
612    /// An idle subscription performs no catalog or transcript reads between
613    /// its minute-scale recovery reconciliations.
614    #[cfg(feature = "adapter-api")]
615    pub fn poll_session_indexes(&mut self) -> Vec<Value> {
616        let mut notifications = Vec::new();
617        for (subscription, index) in &mut self.index_subscriptions {
618            let homes = index.homes().clone();
619            match index.poll() {
620                Ok(Some(delta)) => match live_index_changes(delta.changes, &homes) {
621                    Ok(changes) => notifications.push(json!({
622                        "jsonrpc": "2.0",
623                        "method": SESSION_INDEX_EVENT_METHOD,
624                        "params": {
625                            "subscription": subscription,
626                            "revision": delta.revision,
627                            "changes": changes,
628                        },
629                    })),
630                    Err(error) => notifications.push(json!({
631                        "jsonrpc": "2.0",
632                        "method": SESSION_INDEX_EVENT_METHOD,
633                        "params": {
634                            "subscription": subscription,
635                            "error": {"recoverable": true, "message": error_message(error)},
636                        },
637                    })),
638                },
639                Ok(None) => {}
640                Err(error) => notifications.push(json!({
641                    "jsonrpc": "2.0",
642                    "method": SESSION_INDEX_EVENT_METHOD,
643                    "params": {
644                        "subscription": subscription,
645                        "error": {"recoverable": true, "message": error},
646                    },
647                })),
648            }
649        }
650        notifications
651    }
652
653    #[cfg(feature = "adapter-api")]
654    async fn subscribe_session_activity(
655        &mut self,
656        params: Value,
657    ) -> std::result::Result<Value, ServiceError> {
658        let params = decode::<ActivitySubscribeParams>(params)?;
659        if params.locators.is_empty() {
660            return Err(ServiceError::InvalidParams(
661                "sessions.activity.subscribe requires at least one locator".into(),
662            ));
663        }
664        if params.locators.len() > 2_048 {
665            return Err(ServiceError::InvalidParams(
666                "sessions.activity.subscribe accepts at most 2048 locators".into(),
667            ));
668        }
669        let initial = self
670            .activity_monitor
671            .resolve(&params.locators, &params.homes)
672            .await
673            .map_err(ServiceError::Sdk)?;
674        let subscription = format!("activity-sub-{}", self.next_subscription);
675        self.next_subscription += 1;
676        let reported = initial
677            .iter()
678            .cloned()
679            .map(|activity| (activity.key(), activity))
680            .collect();
681        self.activity_subscriptions.insert(
682            subscription.clone(),
683            ActivitySubscription {
684                locators: params.locators,
685                homes: params.homes,
686                reported,
687            },
688        );
689        Ok(json!({"subscription": subscription, "initial": initial}))
690    }
691
692    /// Non-blockingly sample one event from every connected live runtime.
693    #[cfg(feature = "adapter-api")]
694    pub async fn poll_runtimes(&mut self) -> Vec<Value> {
695        self.poll_sdk_events()
696            .await
697            .into_iter()
698            .map(|(connection, runtime_event)| {
699                json!({
700                    "jsonrpc": "2.0",
701                    "method": RUNTIME_EVENT_METHOD,
702                    "params": {
703                        "connection": connection,
704                        "session_id": runtime_event.session_id,
705                        "sequence": runtime_event.event.sequence,
706                        "event": {
707                            "kind": runtime_event.event.kind,
708                            "payload": runtime_event.event.payload,
709                        },
710                    },
711                })
712            })
713            .collect()
714    }
715
716    async fn poll_sdk_events(&mut self) -> Vec<(String, SdkRuntimeEvent)> {
717        let mut events = Vec::new();
718        let mut closed = Vec::new();
719        let now_ms = crate::approvals::now_ms();
720        for (connection, runtime) in &mut self.runtimes {
721            let session_id = runtime.handle().runtime_id.clone();
722            let harness = runtime.handle().harness.clone();
723            // Drain what the runtime already has: a turn is several events
724            // (updates, then the protocol's completion), and delivering one
725            // per poll would cost a poll interval each. A zero timeout takes
726            // only what is ready — an idle runtime costs nothing.
727            for _ in 0..256 {
728                match tokio::time::timeout(Duration::ZERO, runtime.next_event()).await {
729                    Ok(Ok(Some(event))) => {
730                        let terminal = event.kind == "transport_closed";
731                        // ORCH-9: a permission/approval request arrives as an
732                        // ordinary event; it becomes listable here and stops
733                        // being listable when `runtimes.respond` answers it.
734                        self.approvals
735                            .observe(connection, &harness, &session_id, &event, now_ms);
736                        let next_sequence = self
737                            .runtime_sequences
738                            .entry(session_id.clone())
739                            .or_insert(0);
740                        let sequence = event.sequence.unwrap_or_else(|| {
741                            *next_sequence = next_sequence.saturating_add(1);
742                            *next_sequence
743                        });
744                        *next_sequence = (*next_sequence).max(sequence);
745                        events.push((
746                            connection.clone(),
747                            SdkRuntimeEvent {
748                                session_id: session_id.clone(),
749                                event: SdkEvent {
750                                    sequence,
751                                    kind: event.kind,
752                                    payload: event.payload,
753                                },
754                            },
755                        ));
756                        if terminal {
757                            closed.push(connection.clone());
758                            break;
759                        }
760                    }
761                    Ok(Ok(None)) => {
762                        let sequence = self
763                            .runtime_sequences
764                            .entry(session_id.clone())
765                            .or_insert(0);
766                        *sequence = sequence.saturating_add(1);
767                        events.push((
768                        connection.clone(),
769                        SdkRuntimeEvent {
770                            session_id,
771                            event: SdkEvent {
772                                sequence: *sequence,
773                                kind: "transport_closed".into(),
774                                payload: json!({"message": "Harness runtime transport closed."}),
775                            },
776                        },
777                    ));
778                        closed.push(connection.clone());
779                        break;
780                    }
781                    Err(_) => break,
782                    Ok(Err(error)) => {
783                        let sequence = self
784                            .runtime_sequences
785                            .entry(session_id.clone())
786                            .or_insert(0);
787                        *sequence = sequence.saturating_add(1);
788                        events.push((
789                        connection.clone(),
790                        SdkRuntimeEvent {
791                            session_id,
792                            event: SdkEvent {
793                                sequence: *sequence,
794                                kind: "transport_error".into(),
795                                payload: json!({"message": error.to_string(), "terminal": true}),
796                            },
797                        },
798                    ));
799                        closed.push(connection.clone());
800                        break;
801                    }
802                }
803            }
804        }
805        for connection in closed {
806            if let Some(runtime) = self.runtimes.remove(&connection) {
807                self.runtime_sequences.remove(&runtime.handle().runtime_id);
808            }
809            self.terminal_launches.remove(&connection);
810            // A connection that is gone cannot answer anything it was
811            // holding; those requests stop being listable with it.
812            self.approvals.forget(&connection);
813        }
814        events
815    }
816
817    fn call(&mut self, method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
818        match method {
819            "harness.v1.capabilities" => Ok(json!({
820                "version": HARNESS_SERVICE_VERSION,
821                "sdk": self.capabilities(),
822                "methods": HARNESS_SERVICE_METHODS,
823                "notifications": [
824                    SESSION_EVENT_METHOD,
825                    SESSION_ACTIVITY_EVENT_METHOD,
826                    SESSION_INDEX_EVENT_METHOD,
827                    RUNTIME_EVENT_METHOD
828                ],
829                "harnesses": harness_support_registry()
830                    .harnesses
831                    .into_iter()
832                    .map(|harness| harness.id)
833                    .collect::<Vec<_>>(),
834            })),
835            "harness.v1.support.report" => serde_json::to_value(harness_support_registry())
836                .map_err(|error| ServiceError::Operation(error.to_string())),
837            "harness.v1.profiles.list" | "harness.v1.profiles.get" => profiles_call(method, params),
838            // ORCH-21 controlled tier. Each verb translates to the HARNESS'S
839            // OWN profile verb and runs it (`crate::profiles_control`);
840            // supercode makes and removes nothing itself. The row returned is
841            // re-read through the ORCH-10 loader afterwards, and `ran`
842            // narrates the exact command.
843            "harness.v1.profiles.create" => {
844                mutate_profile(crate::profiles_control::ProfileVerb::Create, params)
845            }
846            "harness.v1.profiles.delete" => {
847                mutate_profile(crate::profiles_control::ProfileVerb::Delete, params)
848            }
849            "harness.v1.channels.list" | "harness.v1.channels.status" => {
850                channels_call(method, params)
851            }
852            // ORCH-15 observed tier: which profile / agent a surface tuple
853            // resolves to, read from each gateway harness's own config.
854            "harness.v1.routes.list" => routes_call(params),
855            // ORCH-16 observed tier: inbound webhook routes / hook mappings.
856            "harness.v1.triggers.list" => triggers_call(params),
857            // ONT-4: the orchestration doors. One home folder in, one typed orchestration
858            // value out (and back). Every one of the four is
859            // `crate::orchestration_doors`, which the `supercode orchestration` verbs call
860            // too — the RPC adds nothing but the envelope. A vault VALUE
861            // never crosses this wire: a load or a compile answers with the
862            // `.env` KEY NAMES, and a caller that needs a value reads the
863            // home's own `.env`.
864            // the workflow layer's read door: a harness's board as one typed value,
865            // the same code the `supercode workflow load` verb calls
866            "harness.v1.workflow.load" => {
867                let params = decode::<WorkflowLoadParams>(params)?;
868                let read =
869                    crate::workflow_doors::load(params.from, &params.home).map_err(operation)?;
870                serde_json::to_value(read)
871                    .map_err(|error| ServiceError::Operation(error.to_string()))
872            }
873            "harness.v1.orchestration.load" => {
874                let params = decode::<OrchestrationLoadParams>(params)?;
875                let read = crate::orchestration_doors::load(&params.root, params.flavor)
876                    .map_err(operation)?;
877                serde_json::to_value(read)
878                    .map_err(|error| ServiceError::Operation(error.to_string()))
879            }
880            "harness.v1.orchestration.save" => {
881                let params = decode::<OrchestrationSaveParams>(params)?;
882                let saved = crate::orchestration_doors::save(
883                    &params.root,
884                    params.orchestration,
885                    params.vault,
886                )
887                .map_err(operation)?;
888                serde_json::to_value(saved)
889                    .map_err(|error| ServiceError::Operation(error.to_string()))
890            }
891            "harness.v1.orchestration.compile" => {
892                let params = decode::<OrchestrationCompileParams>(params)?;
893                let read = crate::orchestration_doors::compile(params.from, &params.home)
894                    .map_err(operation)?;
895                serde_json::to_value(read)
896                    .map_err(|error| ServiceError::Operation(error.to_string()))
897            }
898            "harness.v1.orchestration.decompile" => {
899                let params = decode::<OrchestrationDecompileParams>(params)?;
900                let report = crate::orchestration_doors::decompile(
901                    params.to,
902                    params.orchestration,
903                    &params.source,
904                    params.source_flavor,
905                    &params.dest,
906                    params.vault,
907                )
908                .map_err(operation)?;
909                serde_json::to_value(report)
910                    .map_err(|error| ServiceError::Operation(error.to_string()))
911            }
912            // a migration keeps the credential in this process: a compile and
913            // a save (import), a load and a decompile (export), composed here
914            // because composed by a client the secret would have to cross
915            // the wire
916            "harness.v1.orchestration.import" => {
917                let params = decode::<OrchestrationImportParams>(params)?;
918                let imported =
919                    crate::orchestration_doors::import(params.from, &params.home, &params.into)
920                        .map_err(operation)?;
921                serde_json::to_value(imported)
922                    .map_err(|error| ServiceError::Operation(error.to_string()))
923            }
924            "harness.v1.orchestration.export" => {
925                let params = decode::<OrchestrationExportParams>(params)?;
926                let report =
927                    crate::orchestration_doors::export(params.to, &params.root, &params.dest)
928                        .map_err(operation)?;
929                serde_json::to_value(report)
930                    .map_err(|error| ServiceError::Operation(error.to_string()))
931            }
932            // ORCH-12 observed tier: read and search the persistent memory
933            // documents a harness keeps on disk. Read-only — every write
934            // (`hermes memory off`, `openclaw memory forget|reset`, Claude
935            // Code's `/memory`) stays the harness's own verb. A harness with
936            // no memory store is refused with UnsupportedAction.
937            "harness.v1.memory.show" | "harness.v1.memory.search" => memory_call(method, params),
938            // ORCH-11 observed tier: read-only enumeration of every harness's
939            // installed skill packages. An unknown harness id is refused with
940            // UnsupportedAction — every harness supports skills, so a filter
941            // that matches nothing is a caller error, never an empty listing.
942            "harness.v1.skills.list" => {
943                let query = decode::<crate::skills::SkillsQuery>(params)?;
944                if let Some(harness) = query.harness.as_deref() {
945                    if !crate::skills::SKILL_HARNESSES.contains(&harness) {
946                        return Err(ServiceError::UnsupportedAction(format!(
947                            "`{harness}` has no skills root supercode reads"
948                        )));
949                    }
950                }
951                serde_json::to_value(crate::skills::list_skills(&query))
952                    .map_err(|error| ServiceError::Operation(error.to_string()))
953            }
954            // ORCH-22 controlled tier: each verb goes through the door the
955            // HARNESS publishes — `hermes skills install|uninstall`,
956            // `openclaw skills install`, and for the core four the loader's
957            // own directory, which is the only skills door those harnesses
958            // have. supercode resolves no registry and unpacks no archive.
959            // The row returned is re-read through the ORCH-11 loader
960            // afterwards, and `ran` narrates exactly what was performed.
961            "harness.v1.skills.install" => {
962                mutate_skill(crate::skills_control::SkillVerb::Install, params)
963            }
964            "harness.v1.skills.remove" => {
965                mutate_skill(crate::skills_control::SkillVerb::Remove, params)
966            }
967            // ORCH-9 observed tier: the approval requests waiting for an
968            // answer. At the pinned harness versions the only uniform source
969            // is a LIVE request held by an open runtime connection, plus
970            // supercode's own queued subagent approvals — neither Hermes
971            // 0.21.0 nor OpenClaw 2026.7.1-2 has an approvals door to read
972            // (see `crate::approvals`). A harness whose runtime cannot carry
973            // a protocol request at all is refused by name.
974            "harness.v1.approvals.list" => {
975                let query = decode::<crate::approvals::ApprovalsQuery>(params)?;
976                if let Some(harness) = query.harness.as_deref() {
977                    if !crate::approvals::lists_approvals(harness) {
978                        return Err(ServiceError::UnsupportedAction(format!(
979                            "`{harness}` has no runtime door that carries an approval request"
980                        )));
981                    }
982                }
983                serde_json::to_value(self.approvals(&query))
984                    .map_err(|error| ServiceError::Operation(error.to_string()))
985            }
986            "harness.v1.sessions.discover" => {
987                let query = decode::<DiscoveryQuery>(params)?;
988                let page = discover_session_page(&query).map_err(operation)?;
989                // Claude Code is the one harness that publishes its RUNNING
990                // sessions. The registry is read once per discovery and joined
991                // by session id; every record in it has already survived a
992                // `kill(pid, 0)` liveness check inside `read_registry`.
993                let peers = if page
994                    .sessions
995                    .iter()
996                    .any(|session| session.locator.harness.as_str() == HarnessId::CLAUDE_CODE)
997                {
998                    crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(
999                        &query.homes,
1000                    ))
1001                } else {
1002                    Vec::new()
1003                };
1004                let activities = crate::session_activity::resolve_stock_session_activities(
1005                    &page
1006                        .sessions
1007                        .iter()
1008                        .map(|session| session.locator.clone())
1009                        .collect::<Vec<_>>(),
1010                    &query.homes,
1011                )
1012                .into_iter()
1013                .map(|activity| (activity.key(), activity))
1014                .collect::<BTreeMap<_, _>>();
1015                let sessions = page
1016                    .sessions
1017                    .into_iter()
1018                    .map(|session| {
1019                        let mut value = live_descriptor_value(&session, &peers)?;
1020                        let activity_key = (
1021                            session.locator.harness.as_str().to_string(),
1022                            session.locator.session_id.clone(),
1023                        );
1024                        if let Some(activity) = activities.get(&activity_key) {
1025                            value["activity"] = serde_json::to_value(activity)
1026                                .map_err(|error| ServiceError::Operation(error.to_string()))?;
1027                            if let Some(status) = legacy_live_status(activity) {
1028                                value["live_status"] = json!(status);
1029                            }
1030                        }
1031                        Ok(value)
1032                    })
1033                    .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1034                let mut result = json!({"sessions": sessions, "next_cursor": page.next_cursor});
1035                // Preserve the metadata-only wire shape, but carry the catalog's
1036                // proof/counts when the caller explicitly requests preview search.
1037                if query.search_previews {
1038                    result["receipt"] = serde_json::to_value(page.receipt)
1039                        .map_err(|error| ServiceError::Operation(error.to_string()))?;
1040                }
1041                Ok(result)
1042            }
1043            "harness.v1.sessions.load" => {
1044                let params = decode::<LoadSessionParams>(params)?;
1045                if let Some(options) = &params.options {
1046                    options.validate()?;
1047                    if let Some(result) = indexed_claude_window(&params.read.locator, options)? {
1048                        return Ok(result);
1049                    }
1050                    return load_session(&params.read.locator)
1051                        .map(|session| projected_session_result(&session, options))
1052                        .map_err(operation);
1053                }
1054                let mut session = if params.read.display_history() {
1055                    self.catalog
1056                        .load_display_view(
1057                            &params.read.locator,
1058                            params.read.read_fidelity(),
1059                            params.read.tail_messages().unwrap_or(500),
1060                        )
1061                        .map_err(crate::Error::from)
1062                } else if params.read.include_subagents() {
1063                    load_session_with_fidelity(&params.read.locator, params.read.read_fidelity())
1064                } else {
1065                    self.catalog
1066                        .load_parent_with_fidelity(
1067                            &params.read.locator,
1068                            params.read.read_fidelity(),
1069                        )
1070                        .map_err(crate::Error::from)
1071                }
1072                .map_err(operation)?;
1073                params.read.bound_session(&mut session);
1074                Ok(json!({"session": normalized_session_json(&session)}))
1075            }
1076            "harness.v1.sessions.follow" => {
1077                let params = decode::<LocatorParams>(params)?;
1078                let mut follower = self
1079                    .catalog
1080                    .follow_read_view(
1081                        &params.locator,
1082                        params.read_fidelity(),
1083                        params.include_subagents(),
1084                        params.tail_messages(),
1085                        params.max_message_chars(),
1086                        params.display_history(),
1087                    )
1088                    .map_err(operation)?;
1089                let initial = follower
1090                    .poll()
1091                    .map_err(operation)?
1092                    .map(|event| event.to_json());
1093                let subscription = format!("sub-{}", self.next_subscription);
1094                self.next_subscription += 1;
1095                self.followers.insert(subscription.clone(), follower);
1096                self.followed_sources.insert(
1097                    subscription.clone(),
1098                    FollowedSource {
1099                        harness: params.locator.harness.as_str().to_string(),
1100                        session_id: params.locator.session_id.clone(),
1101                        reported: None,
1102                    },
1103                );
1104                Ok(json!({"subscription": subscription, "initial": initial}))
1105            }
1106            "harness.v1.sessions.unfollow" => {
1107                let params = decode::<UnfollowParams>(params)?;
1108                self.followed_sources.remove(&params.subscription);
1109                Ok(json!({
1110                    "removed": self.followers.remove(&params.subscription).is_some()
1111                }))
1112            }
1113            "harness.v1.sessions.activity.unsubscribe" => {
1114                let params = decode::<UnfollowParams>(params)?;
1115                Ok(json!({
1116                    "removed": self.activity_subscriptions.remove(&params.subscription).is_some()
1117                }))
1118            }
1119            "harness.v1.sessions.index.subscribe" => {
1120                let query = decode::<DiscoveryQuery>(params)?;
1121                crate::session_index::validate_query(&query)
1122                    .map_err(ServiceError::InvalidParams)?;
1123                let homes = query.homes.clone();
1124                let (index, initial) = crate::session_index::SessionIndexSubscription::open(
1125                    query,
1126                    Arc::clone(&self.index_notifier),
1127                )
1128                .map_err(ServiceError::Operation)?;
1129                let peers = peers_for_descriptors(&initial, &homes);
1130                let initial = initial
1131                    .iter()
1132                    .map(|descriptor| live_descriptor_value(descriptor, &peers))
1133                    .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1134                let subscription = format!("index-sub-{}", self.next_subscription);
1135                self.next_subscription += 1;
1136                self.index_subscriptions.insert(subscription.clone(), index);
1137                Ok(json!({
1138                    "subscription": subscription,
1139                    "revision": 1,
1140                    "initial": initial,
1141                }))
1142            }
1143            "harness.v1.sessions.index.resize" => {
1144                let params = decode::<IndexResizeParams>(params)?;
1145                crate::session_index::validate_limit(params.limit)
1146                    .map_err(ServiceError::InvalidParams)?;
1147                let index = self
1148                    .index_subscriptions
1149                    .get_mut(&params.subscription)
1150                    .ok_or_else(|| {
1151                        ServiceError::InvalidParams("unknown session index subscription".into())
1152                    })?;
1153                let prepared = index
1154                    .prepare_resize(params.limit)
1155                    .map_err(ServiceError::Operation)?;
1156                let peers = peers_for_descriptors(&prepared.page.sessions, index.homes());
1157                let initial = prepared
1158                    .page
1159                    .sessions
1160                    .iter()
1161                    .map(|descriptor| live_descriptor_value(descriptor, &peers))
1162                    .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1163                let response = json!({
1164                    "subscription": params.subscription,
1165                    "revision": prepared.revision,
1166                    "initial": initial,
1167                    "receipt": prepared.page.receipt,
1168                });
1169                index.commit_resize(prepared);
1170                Ok(response)
1171            }
1172            "harness.v1.sessions.index.unsubscribe" => {
1173                let params = decode::<UnfollowParams>(params)?;
1174                Ok(json!({
1175                    "removed": self.index_subscriptions.remove(&params.subscription).is_some()
1176                }))
1177            }
1178            "harness.v1.sessions.import" => {
1179                let params = decode::<ImportSessionParams>(params)?;
1180                let session = Session::load_str(&params.content, params.source_harness.into())
1181                    .map_err(operation)?;
1182                Ok(json!({"session": normalized_session_json(&session)}))
1183            }
1184            "harness.v1.sessions.export" | "harness.v1.sessions.translate" => {
1185                let params = decode::<ExportSessionParams>(params)?;
1186                let session = load_session(&params.locator).map_err(operation)?;
1187                let artifact = session_artifact(&params.locator, &session, params.target_harness)?;
1188                if method == "harness.v1.sessions.export"
1189                    && params.target_harness == TransferFormat::Hermes
1190                {
1191                    // UNI-18: write through Hermes's own door, never into its store
1192                    let imported = crate::hermes_import::import_into_hermes(&session, None)
1193                        .map_err(operation)?;
1194                    return Ok(json!({"artifact": artifact, "imported": imported}));
1195                }
1196                Ok(json!({"artifact": artifact}))
1197            }
1198            "harness.v1.sessions.reduce" => {
1199                let params = decode::<ReduceSessionParams>(params)?;
1200                self.reduce_session(params)
1201            }
1202            "harness.v1.sessions.branch" => {
1203                let params = decode::<BranchSessionParams>(params)?;
1204                let session = load_session(&params.locator).map_err(operation)?;
1205                let storage = params.locator.storage.path().display().to_string();
1206                let bootstrap_prompt = format!(
1207                    "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.",
1208                    params.locator.harness.as_str(), params.locator.session_id, storage
1209                );
1210                let artifact = params
1211                    .target_harness
1212                    .map(|target| session_artifact(&params.locator, &session, target))
1213                    .transpose()?;
1214                Ok(json!({
1215                    "parent": params.locator,
1216                    "session": normalized_session_json(&session),
1217                    "bootstrap_prompt": bootstrap_prompt,
1218                    "artifact": artifact,
1219                }))
1220            }
1221            "harness.v1.sessions.handoff" => {
1222                let params = decode::<HandoffSessionParams>(params)?;
1223                let session = load_session(&params.locator).map_err(operation)?;
1224                let cwd = params
1225                    .cwd
1226                    .or_else(|| session.meta.cwd.clone())
1227                    .unwrap_or_else(|| PathBuf::from("."));
1228                let artifact =
1229                    handoff_artifact(&params.locator, &session, params.target_harness, &cwd)?;
1230                let target_session_id = artifact.session_id.as_deref().ok_or_else(|| {
1231                    ServiceError::Operation(
1232                        "handoff artifact omitted target session identity".into(),
1233                    )
1234                })?;
1235                let instructions =
1236                    handoff_instructions(params.target_harness, target_session_id, &cwd);
1237                Ok(json!({
1238                    "artifact": artifact,
1239                    "launch": instructions.launch,
1240                    "materialize": instructions.materialize,
1241                    "requires_materialization": instructions.requires_materialization,
1242                    "note": instructions.note,
1243                }))
1244            }
1245            // ORCH-7 observed tier. Read-only: the handlers open the harness's
1246            // own job store (Claude Code's session JSONL, Hermes's and
1247            // OpenClaw's `cron/jobs.json`) and never write, fire, or schedule.
1248            "harness.v1.jobs.list" => {
1249                let query = decode::<crate::jobs::JobsQuery>(params)?;
1250                if let Some(harness) = query.harness.as_deref() {
1251                    refuse_harness_without_jobs(harness, "jobs.list")?;
1252                }
1253                let listing = crate::jobs::list_jobs(&query).map_err(operation)?;
1254                serde_json::to_value(listing)
1255                    .map_err(|error| ServiceError::Operation(error.to_string()))
1256            }
1257            "harness.v1.jobs.get" => {
1258                let params = decode::<JobsGetParams>(params)?;
1259                refuse_harness_without_jobs(&params.harness, "jobs.get")?;
1260                match crate::jobs::get_job(&params.harness, &params.id, &params.homes)
1261                    .map_err(operation)?
1262                {
1263                    Some((job, source)) => Ok(json!({"job": job, "source": source})),
1264                    None => Err(ServiceError::Operation(format!(
1265                        "`{}` has no scheduled job `{}`",
1266                        params.harness, params.id
1267                    ))),
1268                }
1269            }
1270            // ORCH-18 controlled tier. Each verb translates to the HARNESS'S
1271            // OWN cron verb and runs it (`crate::jobs_control`); supercode
1272            // schedules nothing. The row returned is re-read from the
1273            // harness's store afterwards, and `ran` narrates the exact command
1274            // with any credential redacted.
1275            "harness.v1.jobs.create" => mutate_job(crate::jobs_control::JobVerb::Create, params),
1276            "harness.v1.jobs.update" => mutate_job(crate::jobs_control::JobVerb::Update, params),
1277            "harness.v1.jobs.pause" => mutate_job(crate::jobs_control::JobVerb::Pause, params),
1278            "harness.v1.jobs.resume" => mutate_job(crate::jobs_control::JobVerb::Resume, params),
1279            "harness.v1.jobs.run" => mutate_job(crate::jobs_control::JobVerb::Run, params),
1280            "harness.v1.jobs.delete" => mutate_job(crate::jobs_control::JobVerb::Delete, params),
1281            "harness.v1.jobs.notepad"
1282            | "harness.v1.jobs.notepad_set"
1283            | "harness.v1.jobs.notepad_delete" => {
1284                let request = decode::<crate::jobs_notepad::JobNotepadRequest>(params)?;
1285                refuse_harness_without_jobs(&request.harness, "jobs.notepad")?;
1286                let answer = match method {
1287                    "harness.v1.jobs.notepad_set" => crate::jobs_notepad::set(&request),
1288                    "harness.v1.jobs.notepad_delete" => crate::jobs_notepad::delete(&request),
1289                    _ => crate::jobs_notepad::read(&request),
1290                }
1291                .map_err(job_control_error)?;
1292                serde_json::to_value(answer)
1293                    .map_err(|error| ServiceError::Operation(error.to_string()))
1294            }
1295            "harness.v1.model_route.apply" => {
1296                let request = decode::<crate::model_route::ModelRouteApply>(params)?;
1297                let outcome = crate::model_route::apply(&request).map_err(job_control_error)?;
1298                serde_json::to_value(outcome)
1299                    .map_err(|error| ServiceError::Operation(error.to_string()))
1300            }
1301            "harness.v1.jobs.apply" => {
1302                let request = decode::<crate::jobs_apply::JobsApply>(params)?;
1303                refuse_harness_without_jobs(&request.harness, "jobs.apply")?;
1304                let outcome = crate::jobs_apply::apply(&request).map_err(job_control_error)?;
1305                serde_json::to_value(outcome)
1306                    .map_err(|error| ServiceError::Operation(error.to_string()))
1307            }
1308            // ORCH-8 observed tier. Read-only: the handlers open the harness's
1309            // own run store (Hermes's `cron/executions.db`, OpenClaw's
1310            // `cron_run_logs`) and never claim, retry, or prune a fire.
1311            "harness.v1.runs.list" => {
1312                let query = decode::<crate::runs::RunsQuery>(params)?;
1313                if let Some(harness) = query.harness.as_deref() {
1314                    refuse_harness_without_runs(harness, "runs.list")?;
1315                }
1316                let listing = crate::runs::list_runs(&query).map_err(operation)?;
1317                serde_json::to_value(listing)
1318                    .map_err(|error| ServiceError::Operation(error.to_string()))
1319            }
1320            "harness.v1.runs.get" => {
1321                let params = decode::<RunsGetParams>(params)?;
1322                refuse_harness_without_runs(&params.harness, "runs.get")?;
1323                match crate::runs::get_run(&params.harness, &params.id, &params.homes)
1324                    .map_err(operation)?
1325                {
1326                    Some((run, source)) => Ok(json!({"run": run, "source": source})),
1327                    None => Err(ServiceError::Operation(format!(
1328                        "`{}` has no run `{}`",
1329                        params.harness, params.id
1330                    ))),
1331                }
1332            }
1333            "harness.v1.sessions.resume_instructions" => {
1334                let params = decode::<ResumeInstructionsParams>(params)?;
1335                let session = load_session(&params.locator).map_err(operation)?;
1336                let cwd = params
1337                    .cwd
1338                    .or(session.meta.cwd)
1339                    .unwrap_or_else(|| PathBuf::from("."));
1340                let launch = resume_launch(
1341                    params.locator.harness.as_str(),
1342                    &params.locator.session_id,
1343                    &cwd,
1344                    params.policy,
1345                )?;
1346                Ok(json!({"launch": launch}))
1347            }
1348            _ => Err(ServiceError::MethodNotFound),
1349        }
1350    }
1351
1352    fn reduce_session(
1353        &self,
1354        params: ReduceSessionParams,
1355    ) -> std::result::Result<Value, ServiceError> {
1356        let session = load_session(&params.locator).map_err(operation)?;
1357        if session.messages.is_empty() {
1358            return Err(ServiceError::InvalidParams(
1359                "cannot reduce an empty session".into(),
1360            ));
1361        }
1362        let keep_last = params.keep_last.clamp(1, 128);
1363        let policy = reduce::ReductionPolicy {
1364            clear_turns_older_than: Some(keep_last),
1365            ..Default::default()
1366        };
1367        let (view, log) =
1368            reduce::project_messages(&session.messages, &policy, &reduce::ReductionLog::default());
1369        if log.reductions.is_empty() {
1370            return Err(ServiceError::UnsupportedAction(format!(
1371                "session `{}` is already too small for a meaningful reversible reduction",
1372                params.locator.session_id
1373            )));
1374        }
1375        let source_tokens = tokens::estimate_view_tokens(&session.messages);
1376        let reduced_tokens = tokens::estimate_view_tokens(&view);
1377        if reduced_tokens >= source_tokens {
1378            return Err(ServiceError::UnsupportedAction(format!(
1379                "session `{}` has no token-reducing reversible projection",
1380                params.locator.session_id
1381            )));
1382        }
1383
1384        let store_root = self
1385            .reduction_store_root
1386            .clone()
1387            .unwrap_or_else(default_reduction_store_root);
1388        let store = crate::SessionStore::open(&store_root).map_err(operation)?;
1389        let rescue_id = format!("rescue-{}", generated_session_id());
1390        let imported = session
1391            .imported_message_count
1392            .unwrap_or(session.messages.len())
1393            .min(session.messages.len());
1394        let sidecar_jsonl = session.to_native_jsonl_v2(&session.messages[imported..]);
1395        let view_jsonl = messages_jsonl(&view)?;
1396        let title = format!(
1397            "Reduced {} continuation from {}",
1398            params.target_harness.id(),
1399            params.locator.session_id
1400        );
1401
1402        // Durability order is intentional: the full source of truth lands
1403        // before either object that can refer to it. A crash may leave an
1404        // unused sidecar, but can never leave a reduced view whose originals
1405        // were not durably written first.
1406        store
1407            .save_sidecar(&rescue_id, &sidecar_jsonl)
1408            .map_err(operation)?;
1409        store
1410            .save_reduction_log(&rescue_id, &log)
1411            .map_err(operation)?;
1412        store
1413            .save(&rescue_id, &title, &view_jsonl)
1414            .map_err(operation)?;
1415
1416        let source_bytes = serde_json::to_vec(&session.messages)
1417            .map_err(|error| ServiceError::Operation(error.to_string()))?
1418            .len() as u64;
1419        let reduced_bytes = serde_json::to_vec(&view)
1420            .map_err(|error| ServiceError::Operation(error.to_string()))?
1421            .len() as u64;
1422        store
1423            .set_reduction_stats(
1424                &rescue_id,
1425                &title,
1426                source_bytes,
1427                reduced_bytes,
1428                log.reductions.len() as u32,
1429            )
1430            .map_err(operation)?;
1431
1432        // The receipt is issued only after a real disk reload. This proves
1433        // the exact files another process will consume, not the convenient
1434        // in-memory values that produced them.
1435        let reloaded_sidecar = store
1436            .load_sidecar(&rescue_id)
1437            .map_err(operation)?
1438            .ok_or_else(|| ServiceError::Operation("reduction sidecar disappeared".into()))?;
1439        let reloaded_sidecar = Session::from_sidecar_str(&reloaded_sidecar).map_err(operation)?;
1440        let reloaded_log = store
1441            .load_reduction_log(&rescue_id)
1442            .map_err(operation)?
1443            .ok_or_else(|| ServiceError::Operation("reduction log disappeared".into()))?;
1444        let reloaded_view = parse_messages_jsonl(&store.load(&rescue_id).map_err(operation)?)?;
1445        reduce::verify_log(&reloaded_log, &reloaded_sidecar).map_err(operation)?;
1446        // `sc.reduction` is deliberately in-memory-only metadata: it must
1447        // never leak onto a provider-facing transcript. Reapplying the
1448        // durable log to the durable sidecar restores those ids. Comparing
1449        // its wire form with the transcript reloaded above proves that the
1450        // persisted view is exactly the deterministic projection before we
1451        // use the restamped form for inversion.
1452        let (restamped_view, restamped_log) =
1453            reduce::project_messages(&reloaded_sidecar.messages, &policy, &reloaded_log);
1454        if messages_jsonl(&restamped_view)? != messages_jsonl(&reloaded_view)? {
1455            return Err(ServiceError::Operation(
1456                "persisted reduction view does not match its durable log and sidecar".into(),
1457            ));
1458        }
1459        if restamped_log != reloaded_log {
1460            return Err(ServiceError::Operation(
1461                "reapplying the durable reduction log changed its identity".into(),
1462            ));
1463        }
1464        let inverted =
1465            reduce::invert(&restamped_view, &reloaded_log, &reloaded_sidecar).map_err(operation)?;
1466        if inverted != session.messages {
1467            return Err(ServiceError::Operation(
1468                "reduction inversion did not restore the source messages byte-exactly".into(),
1469            ));
1470        }
1471
1472        let ratio = source_tokens as f64 / reduced_tokens.max(1) as f64;
1473        let sidecar_path = store.sidecar_path(&rescue_id);
1474        let reduction_log_path = store.reduction_log_path(&rescue_id).map_err(operation)?;
1475        let bootstrap_prompt = reduced_bootstrap_prompt(
1476            &params.locator,
1477            params.target_harness,
1478            &view_jsonl,
1479            &sidecar_path,
1480            &reduction_log_path,
1481        );
1482        let mut reduced_session = session.clone();
1483        reduced_session.meta.session_id = Some(rescue_id.clone());
1484        reduced_session.messages = view;
1485
1486        Ok(json!({
1487            "session": normalized_session_json(&reduced_session),
1488            "bootstrap_prompt": bootstrap_prompt,
1489            "receipt": {
1490                "id": rescue_id,
1491                "sidecar_id": rescue_id,
1492                "source_harness": params.locator.harness,
1493                "target_harness": params.target_harness.id(),
1494                "source_tokens": source_tokens,
1495                "reduced_tokens": reduced_tokens,
1496                "ratio": ratio,
1497                "source_bytes": source_bytes,
1498                "reduced_bytes": reduced_bytes,
1499                "reductions": reloaded_log.reductions.len(),
1500                "sidecar_path": sidecar_path,
1501                "reduction_log_path": reduction_log_path,
1502                "verified": true,
1503                "reversible": true,
1504            }
1505        }))
1506    }
1507
1508    /// Recognize the one request family whose waiting happens entirely
1509    /// outside this service's state, and hand a transport the half it can run
1510    /// off the task that owns the service.
1511    ///
1512    /// Opening a runtime is the only door here that waits on a foreign
1513    /// program: it spawns the harness's own binary and completes that
1514    /// program's protocol handshake, which takes as long as the program takes
1515    /// to answer. A transport that awaited the whole request inline would
1516    /// stop reading its own input for that whole time, so ONE slow launch
1517    /// would queue every later request on the same server — including reads
1518    /// like `sessions.discover` that touch no runtime at all. Splitting the
1519    /// request lets the transport spawn [`RuntimeOpen::open`] and keep
1520    /// reading, then pay only the short bookkeeping half
1521    /// ([`Self::register_open_runtime`]) when the runtime is up.
1522    ///
1523    /// `None` for every other method: those are answered by
1524    /// [`Self::handle_async`] as before.
1525    pub fn runtime_open(request: &Value) -> Option<RuntimeOpen> {
1526        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1527            return None;
1528        }
1529        let method = request.get("method").and_then(Value::as_str)?;
1530        if !RUNTIME_OPEN_METHODS.contains(&method) {
1531            return None;
1532        }
1533        Some(RuntimeOpen {
1534            id: request.get("id").cloned().unwrap_or(Value::Null),
1535            method: method.to_string(),
1536            params: request.get("params").cloned().unwrap_or_else(|| json!({})),
1537        })
1538    }
1539
1540    /// Recognize a [`DETACHED_METHODS`] request and hand a transport the
1541    /// whole of it: the service-state half is read here and now, and what
1542    /// remains waits on a foreign program with nothing of this service's in
1543    /// hand.
1544    ///
1545    /// Same reason as [`Self::runtime_open`], different doors. Probing a
1546    /// harness starts it and completes its handshake; couriering a message
1547    /// runs a `claude` process to completion; a conversation verb runs the
1548    /// harness's own CLI or calls its HTTP API. A transport that awaited any
1549    /// of those inline would stop reading its own input for that whole time,
1550    /// so one probe of an unhealthy harness would queue every later request
1551    /// on the same server.
1552    ///
1553    /// Unlike an opening runtime there is no bookkeeping half: the answer
1554    /// [`DetachedCall::run`] produces is the caller's complete response, so a
1555    /// transport writes it without coming back here.
1556    ///
1557    /// `None` for every other method — including the LIVE `sessions.new` /
1558    /// `sessions.reset` door and `runtimes.close`, which wait on a runtime
1559    /// connection this service owns and so are split off by
1560    /// [`Self::detach_runtime`] instead.
1561    pub fn detach(&self, request: &Value) -> Option<DetachedCall> {
1562        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1563            return None;
1564        }
1565        let method = request.get("method").and_then(Value::as_str)?;
1566        if !DETACHED_METHODS.contains(&method) {
1567            return None;
1568        }
1569        let id = request.get("id").cloned().unwrap_or(Value::Null);
1570        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
1571        let work = match method {
1572            "harness.v1.harnesses.list" | "harness.v1.harnesses.probe" => self
1573                .inventory_work(method, params)
1574                .map(DetachedWork::Inventory),
1575            "harness.v1.sessions.message" => {
1576                decode::<MessageSessionParams>(params).map(DetachedWork::Message)
1577            }
1578            _ => {
1579                let verb = match method {
1580                    "harness.v1.sessions.new" => crate::SessionVerb::New,
1581                    "harness.v1.sessions.reset" => crate::SessionVerb::Reset,
1582                    "harness.v1.sessions.archive" => crate::SessionVerb::Archive,
1583                    _ => crate::SessionVerb::Delete,
1584                };
1585                match decode::<crate::SessionMutation>(params) {
1586                    Ok(mutation) => {
1587                        match crate::sessions_control::door(&mutation.harness, verb) {
1588                            // The live door needs the open runtime connection
1589                            // this service owns; it stays inline.
1590                            Ok(crate::SessionDoor::Live(_)) => return None,
1591                            Ok(_) => Ok(DetachedWork::SessionMutation { verb, mutation }),
1592                            Err(error) => Err(session_control_error(error)),
1593                        }
1594                    }
1595                    Err(error) => Err(error),
1596                }
1597            }
1598        };
1599        Some(DetachedCall {
1600            id,
1601            method: method.to_string(),
1602            work: work.map(Work::Free),
1603        })
1604    }
1605
1606    /// Recognize the two doors that wait on a runtime THIS SERVICE OWNS, and
1607    /// hand a transport the whole of each by lending the connection out.
1608    ///
1609    /// `runtimes.close` surrenders its runtime for good; the LIVE
1610    /// `sessions.new` / `sessions.reset` door borrows one for the length of
1611    /// the slash command and gives it back through
1612    /// [`Self::finish_detached`]. Both are bounded by
1613    /// [`RUNTIME_CONTROL_DEADLINE`], and a wedged runtime spends all of it —
1614    /// which is exactly as long as a transport that awaited them inline would
1615    /// stop reading its own input.
1616    ///
1617    /// `None` for every other method, and for the `sessions.new` /
1618    /// `sessions.reset` doors that are not live: [`Self::detach`] owns those.
1619    pub fn detach_runtime(&mut self, request: &Value) -> Option<DetachedCall> {
1620        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1621            return None;
1622        }
1623        let method = request.get("method").and_then(Value::as_str)?;
1624        let id = request.get("id").cloned().unwrap_or(Value::Null);
1625        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
1626        let work = match method {
1627            "harness.v1.runtimes.close" => decode::<RuntimeConnectionParams>(params)
1628                .and_then(|params| self.surrender_runtime(&params.connection))
1629                .map(|(runtime, process_group)| {
1630                    Work::Runtime(RuntimeWork::Close {
1631                        runtime,
1632                        process_group,
1633                    })
1634                }),
1635            "harness.v1.sessions.new" | "harness.v1.sessions.reset" => {
1636                let verb = if method == "harness.v1.sessions.new" {
1637                    crate::SessionVerb::New
1638                } else {
1639                    crate::SessionVerb::Reset
1640                };
1641                let mutation = decode::<crate::SessionMutation>(params).ok()?;
1642                // Everything but the live door — including a refusal and a
1643                // request naming no connection — is `detach`'s or
1644                // `handle_async`'s to answer.
1645                let Ok(crate::SessionDoor::Live(command)) =
1646                    crate::sessions_control::door(&mutation.harness, verb)
1647                else {
1648                    return None;
1649                };
1650                let connection = mutation
1651                    .connection
1652                    .clone()
1653                    .filter(|value| !value.trim().is_empty())?;
1654                self.lend_runtime(&connection).map(|runtime| {
1655                    let session = live_session_name(runtime.as_ref(), &mutation);
1656                    Work::Runtime(RuntimeWork::LiveCommand {
1657                        connection,
1658                        runtime,
1659                        verb,
1660                        mutation,
1661                        command,
1662                        session,
1663                    })
1664                })
1665            }
1666            _ => return None,
1667        };
1668        Some(DetachedCall {
1669            id,
1670            method: method.to_string(),
1671            work,
1672        })
1673    }
1674
1675    /// Take back whatever a detached call borrowed and hand over the caller's
1676    /// response. Every answer from [`DetachedCall::run`] comes through here,
1677    /// so a lent-out connection is back in the service before the response
1678    /// that used it is written.
1679    pub fn finish_detached(&mut self, answer: DetachedAnswer) -> Value {
1680        let DetachedAnswer { response, returned } = answer;
1681        if let Some(ReturnedRuntime {
1682            connection,
1683            runtime,
1684        }) = returned
1685        {
1686            self.runtimes_in_flight.remove(&connection);
1687            self.runtimes.insert(connection, runtime);
1688        }
1689        response
1690    }
1691
1692    /// Answer a request split out by [`Self::runtime_open`] and already
1693    /// awaited by [`RuntimeOpen::open`]: register the runtime this service now
1694    /// owns and build its JSON-RPC response.
1695    pub async fn finish_runtime_open(&mut self, opened: OpenedRuntime) -> Value {
1696        let OpenedRuntime { id, outcome } = opened;
1697        let result = match outcome {
1698            Ok(open) => self.register_open_runtime(open).await,
1699            Err(error) => Err(error),
1700        };
1701        service_response(id, result)
1702    }
1703
1704    /// Take ownership of an opened runtime.
1705    async fn register_open_runtime(
1706        &mut self,
1707        open: OpenRuntime,
1708    ) -> std::result::Result<Value, ServiceError> {
1709        match open {
1710            OpenRuntime::Hosted {
1711                runtime,
1712                capabilities,
1713                workspace,
1714            } => {
1715                self.insert_hosted_runtime(runtime, capabilities, workspace)
1716                    .await
1717            }
1718            OpenRuntime::Joined { runtime } => self.insert_runtime(runtime),
1719        }
1720    }
1721
1722    async fn runtime_call(
1723        &mut self,
1724        method: &str,
1725        params: Value,
1726    ) -> std::result::Result<Value, ServiceError> {
1727        match method {
1728            "harness.v1.runtimes.capabilities" => {
1729                let params = decode::<RuntimeBackendParams>(params)?;
1730                let backend = runtime_backend(&params)?;
1731                Ok(json!({
1732                    "harness": backend.harness(),
1733                    "capabilities": backend.capabilities(),
1734                }))
1735            }
1736            method if RUNTIME_OPEN_METHODS.contains(&method) => {
1737                self.register_open_runtime(open_runtime(method, params).await?)
1738                    .await
1739            }
1740            "harness.v1.runtimes.send_input" => {
1741                let params = decode::<RuntimeInputParams>(params)?;
1742                let image_urls = validate_runtime_image_urls(params.image_urls)?;
1743                let runtime = self.runtime_mut(&params.connection)?;
1744                let turn_id = within_control_deadline(
1745                    method,
1746                    runtime.send_input(RuntimeInput {
1747                        text: params.text,
1748                        image_urls,
1749                    }),
1750                )
1751                .await?
1752                .map_err(operation)?;
1753                Ok(json!({"turn_id": turn_id}))
1754            }
1755            "harness.v1.runtimes.interrupt" => {
1756                let params = decode::<RuntimeConnectionParams>(params)?;
1757                within_control_deadline(method, self.runtime_mut(&params.connection)?.interrupt())
1758                    .await?
1759                    .map_err(operation)?;
1760                Ok(json!({}))
1761            }
1762            "harness.v1.runtimes.steer" => {
1763                let params = decode::<RuntimeInputParams>(params)?;
1764                if !params.image_urls.is_empty() {
1765                    return Err(ServiceError::InvalidParams(
1766                        "runtime steering accepts text only".into(),
1767                    ));
1768                }
1769                let text = params.text.trim();
1770                if text.is_empty() || text.chars().count() > 50_000 {
1771                    return Err(ServiceError::InvalidParams(
1772                        "runtime steering requires 1 to 50,000 text characters".into(),
1773                    ));
1774                }
1775                within_control_deadline(
1776                    method,
1777                    self.runtime_mut(&params.connection)?
1778                        .steer(text.to_string()),
1779                )
1780                .await?
1781                .map_err(operation)?;
1782                Ok(json!({}))
1783            }
1784            "harness.v1.runtimes.respond" => {
1785                let params = decode::<RuntimeRespondParams>(params)?;
1786                let request_id = params.request_id.clone();
1787                within_control_deadline(
1788                    method,
1789                    self.runtime_mut(&params.connection)?
1790                        .respond(params.request_id, params.response),
1791                )
1792                .await?
1793                .map_err(operation)?;
1794                // ORCH-9: an answered request is no longer waiting for one.
1795                self.approvals.answered(&params.connection, &request_id);
1796                Ok(json!({}))
1797            }
1798            "harness.v1.runtimes.terminal_instructions" => {
1799                let params = decode::<RuntimeConnectionParams>(params)?;
1800                let launch = self
1801                    .terminal_launches
1802                    .get(&params.connection)
1803                    .ok_or_else(|| {
1804                        ServiceError::Operation(
1805                            "this runtime is not hosted for terminal attachment".into(),
1806                        )
1807                    })?;
1808                Ok(json!({"launch":launch}))
1809            }
1810            "harness.v1.runtimes.close" => {
1811                let params = decode::<RuntimeConnectionParams>(params)?;
1812                let (runtime, process_group) = self.surrender_runtime(&params.connection)?;
1813                close_runtime(runtime, process_group).await
1814            }
1815            _ => Err(ServiceError::MethodNotFound),
1816        }
1817    }
1818
1819    /// Deliver one message into a session that is running right now.
1820    #[cfg(feature = "adapter-api")]
1821    async fn message_call(&self, params: Value) -> std::result::Result<Value, ServiceError> {
1822        let params = decode::<MessageSessionParams>(params)?;
1823        Ok(message_live_session(&params, &crate::claude_peer::ProcessCourierRunner).await)
1824    }
1825
1826    #[cfg(feature = "adapter-api")]
1827    fn harness_settings_call(
1828        &self,
1829        method: &str,
1830        params: Value,
1831    ) -> std::result::Result<Value, ServiceError> {
1832        let homes = crate::HarnessHomes::default();
1833        match method {
1834            "harness.v1.harnesses.settings" => {
1835                let params = decode::<HarnessSettingsParams>(params)?;
1836                let report = crate::inspect_harness_interop_settings(&homes, &params.harness)
1837                    .map_err(|error| ServiceError::Operation(error.to_string()))?;
1838                serde_json::to_value(report)
1839                    .map_err(|error| ServiceError::Operation(error.to_string()))
1840            }
1841            "harness.v1.harnesses.configure" => {
1842                let params = decode::<ConfigureHarnessParams>(params)?;
1843                let report = crate::configure_harness_interop_settings(
1844                    &homes,
1845                    &params.harness,
1846                    &params.changes,
1847                    params.expected_revision.as_deref(),
1848                )
1849                .map_err(|error| ServiceError::Operation(error.to_string()))?;
1850                serde_json::to_value(report)
1851                    .map_err(|error| ServiceError::Operation(error.to_string()))
1852            }
1853            _ => Err(ServiceError::MethodNotFound),
1854        }
1855    }
1856
1857    fn insert_runtime(
1858        &mut self,
1859        runtime: Box<dyn RuntimeConnection>,
1860    ) -> std::result::Result<Value, ServiceError> {
1861        let connection = format!("runtime-{}", self.next_runtime);
1862        self.next_runtime += 1;
1863        let handle = runtime.handle().clone();
1864        self.runtime_sequences
1865            .entry(handle.runtime_id.clone())
1866            .or_insert(0);
1867        self.runtimes.insert(connection.clone(), runtime);
1868        Ok(json!({"connection": connection, "handle": handle}))
1869    }
1870
1871    #[cfg(feature = "adapter-api")]
1872    async fn insert_hosted_runtime(
1873        &mut self,
1874        runtime: Box<dyn RuntimeConnection>,
1875        capabilities: crate::RuntimeCapabilities,
1876        workspace: PathBuf,
1877    ) -> std::result::Result<Value, ServiceError> {
1878        let (host, connection) = HostedHarnessRuntime::spawn(runtime, capabilities);
1879        let token: std::sync::Arc<str> = crate::server::generate_token().into();
1880        let server = crate::server::run_frontend_http(
1881            host.clone(),
1882            host.frontend_sender(),
1883            "127.0.0.1:0",
1884            token.clone(),
1885            connection.handle().runtime_id.clone(),
1886        )
1887        .await
1888        .map_err(|error| ServiceError::Operation(error.to_string()))?;
1889        let source = LiveRuntimeSource {
1890            harness: connection.handle().harness.as_str().to_string(),
1891            session_id: connection.handle().runtime_id.clone(),
1892            workspace: workspace.clone(),
1893        };
1894        let registration = register_live_runtime(
1895            connection.handle().runtime_id.clone(),
1896            source.clone(),
1897            format!("http://{}", server.address()),
1898            token.to_string(),
1899        )
1900        .map_err(|error| ServiceError::Operation(error.to_string()))?;
1901        let endpoint = registration.endpoint().to_string();
1902        let launch = StructuredLaunch {
1903            cwd: workspace,
1904            // Pin attachment to the executable hosting this runtime. A bare
1905            // `supercode` could resolve to an older global install whose CLI
1906            // does not understand the receipt it is being asked to open.
1907            program: std::env::current_exe()
1908                .ok()
1909                .map(|path| path.to_string_lossy().into_owned())
1910                .unwrap_or_else(|| "supercode".into()),
1911            arguments: vec![
1912                "harness".into(),
1913                "attach".into(),
1914                "--endpoint".into(),
1915                endpoint,
1916                "--harness".into(),
1917                source.harness,
1918                "--session".into(),
1919                source.session_id,
1920            ],
1921            env: BTreeMap::new(),
1922        };
1923        let lease = HostedRuntimeLease {
1924            connection,
1925            _host: host,
1926            _registration: registration,
1927            _server: server,
1928        };
1929        let opened = self.insert_runtime(Box::new(lease))?;
1930        let connection_id = opened["connection"]
1931            .as_str()
1932            .expect("insert_runtime returns a connection id")
1933            .to_string();
1934        self.terminal_launches.insert(connection_id, launch);
1935        Ok(opened)
1936    }
1937
1938    #[cfg(not(feature = "adapter-api"))]
1939    async fn insert_hosted_runtime(
1940        &mut self,
1941        runtime: Box<dyn RuntimeConnection>,
1942        _capabilities: crate::RuntimeCapabilities,
1943        _workspace: PathBuf,
1944    ) -> std::result::Result<Value, ServiceError> {
1945        self.insert_runtime(runtime)
1946    }
1947
1948    fn runtime_mut(
1949        &mut self,
1950        connection: &str,
1951    ) -> std::result::Result<&mut Box<dyn RuntimeConnection>, ServiceError> {
1952        if self.runtimes_in_flight.contains(connection) {
1953            return Err(self.lent_out(connection));
1954        }
1955        self.runtimes.get_mut(connection).ok_or_else(|| {
1956            ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
1957        })
1958    }
1959
1960    /// What a caller is told about a connection that is out on a detached
1961    /// call. It is not gone and it is not free: it is mid-call, which is the
1962    /// same answer the runtime itself gives a second turn.
1963    fn lent_out(&self, connection: &str) -> ServiceError {
1964        ServiceError::Operation(format!(
1965            "runtime connection `{connection}`: a harness turn is already in progress"
1966        ))
1967    }
1968
1969    /// Take a runtime OUT of the service for the duration of one detached
1970    /// call, leaving its name marked as lent out.
1971    fn lend_runtime(
1972        &mut self,
1973        connection: &str,
1974    ) -> std::result::Result<Box<dyn RuntimeConnection>, ServiceError> {
1975        if self.runtimes_in_flight.contains(connection) {
1976            return Err(self.lent_out(connection));
1977        }
1978        let runtime = self.runtimes.remove(connection).ok_or_else(|| {
1979            ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
1980        })?;
1981        self.runtimes_in_flight.insert(connection.to_string());
1982        Ok(runtime)
1983    }
1984
1985    /// Surrender a runtime for good: the connection and everything the
1986    /// service hung off it are gone before its teardown is even attempted.
1987    ///
1988    /// `close` is what a caller reaches for when a runtime has stopped
1989    /// answering, and a runtime that has stopped answering is exactly the one
1990    /// whose graceful close cannot complete: a hosted runtime's own loop
1991    /// parks on the call the runtime never answered, so it never dequeues the
1992    /// shutdown either. Keeping the entry until teardown succeeded made a
1993    /// wedged runtime permanent — every later call on that connection, and
1994    /// every new turn, answered "a harness turn is already in progress" with
1995    /// no way to take the connection back.
1996    fn surrender_runtime(
1997        &mut self,
1998        connection: &str,
1999    ) -> std::result::Result<(Box<dyn RuntimeConnection>, Option<u32>), ServiceError> {
2000        if self.runtimes_in_flight.contains(connection) {
2001            return Err(self.lent_out(connection));
2002        }
2003        let runtime = self.runtimes.remove(connection).ok_or_else(|| {
2004            ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
2005        })?;
2006        let process_group = runtime_process_group(runtime.handle());
2007        let runtime_id = runtime.handle().runtime_id.clone();
2008        self.terminal_launches.remove(connection);
2009        self.runtime_sequences.remove(&runtime_id);
2010        self.approvals.forget(connection);
2011        Ok((runtime, process_group))
2012    }
2013
2014    /// SIGKILL the process group of every runtime this service owns, without
2015    /// waiting on any of them.
2016    ///
2017    /// A host leaving for good calls this BEFORE dropping the service. The
2018    /// handle this service holds is not the runtime's connection: a hosted
2019    /// runtime's real transport lives in the task driving it, so neither
2020    /// exiting the process nor dropping these handles reaches the harness
2021    /// process — while dropping them does remove each runtime's live-runtime
2022    /// receipt. Signalling first is what keeps a removed receipt from
2023    /// advertising a harness that is still running.
2024    pub fn kill_all_runtime_groups(&self) -> usize {
2025        self.runtimes
2026            .values()
2027            .filter(|runtime| kill_runtime_process_group(runtime_process_group(runtime.handle())))
2028            .count()
2029    }
2030
2031    /// ORCH-19: run one conversation-lifecycle verb through the harness's own
2032    /// door.
2033    ///
2034    /// Two doors, one shape. A CLI / HTTP / own-store door is self-contained
2035    /// in [`crate::sessions_control`]. A LIVE door (Hermes's and OpenClaw's
2036    /// `/new` and `/reset`, which are slash commands their gateway interprets
2037    /// INSIDE a session) is performed here, because only the service owns the
2038    /// open runtime connection — the command is typed through the very same
2039    /// `send_input` path a human's message takes, so supercode invents no
2040    /// private channel.
2041    async fn mutate_session(
2042        &mut self,
2043        verb: crate::SessionVerb,
2044        params: Value,
2045    ) -> std::result::Result<Value, ServiceError> {
2046        let mutation = decode::<crate::SessionMutation>(params)?;
2047        let door = crate::sessions_control::door(&mutation.harness, verb)
2048            .map_err(session_control_error)?;
2049        let outcome = match door {
2050            // The live door types the slash command through an open hosted
2051            // runtime, which only exists with the `adapter-api` feature; the
2052            // CLI / HTTP / own-store doors below need nothing extra.
2053            #[cfg(not(feature = "adapter-api"))]
2054            crate::SessionDoor::Live(command) => {
2055                return Err(ServiceError::Operation(format!(
2056                    "`{}` performs `sessions.{}` by typing `{command}` into a live driven \
2057                     session, which needs this build's `adapter-api` feature",
2058                    mutation.harness,
2059                    verb.as_str()
2060                )));
2061            }
2062            #[cfg(feature = "adapter-api")]
2063            crate::SessionDoor::Live(command) => {
2064                let connection = mutation
2065                    .connection
2066                    .clone()
2067                    .filter(|value| !value.trim().is_empty())
2068                    .ok_or_else(|| {
2069                        ServiceError::InvalidParams(format!(
2070                            "`{}` performs `sessions.{}` by typing `{command}` into a live \
2071                             driven session: pass the `connection` of an open runtime \
2072                             (`harness.v1.runtimes.start`)",
2073                            mutation.harness,
2074                            verb.as_str()
2075                        ))
2076                    })?;
2077                let runtime = self.runtime_mut(&connection)?;
2078                let session = live_session_name(runtime.as_ref(), &mutation);
2079                // Typing into a live session is a control call on an open
2080                // runtime, and a wedged runtime never accepts one, so it is
2081                // bounded exactly like the other control verbs. A transport
2082                // with a loop of its own lends the connection out instead of
2083                // waiting here: see [`Self::detach_runtime`].
2084                return type_live_command(runtime.as_mut(), verb, &mutation, command, session)
2085                    .await;
2086            }
2087            _ => run_session_mutation(verb, &mutation).await?,
2088        };
2089        serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
2090    }
2091
2092    /// Answer an inventory request whole, for callers that have nowhere to
2093    /// put the waiting half. A transport with a loop of its own splits it
2094    /// instead: see [`Self::detach`].
2095    async fn inventory_call(
2096        &self,
2097        method: &str,
2098        params: Value,
2099    ) -> std::result::Result<Value, ServiceError> {
2100        run_inventory(self.inventory_work(method, params)?).await
2101    }
2102
2103    /// The half of an inventory request that reads this service's state:
2104    /// resolve the selection and count the persisted sessions each row
2105    /// reports. What remains — finding executables, asking them their
2106    /// version, and (at `probe: handshake`) starting each harness and
2107    /// completing its protocol handshake — touches no service state at all.
2108    fn inventory_work(
2109        &self,
2110        method: &str,
2111        params: Value,
2112    ) -> std::result::Result<InventoryWork, ServiceError> {
2113        let mut params = decode::<HarnessInventoryParams>(params)?;
2114        if method == "harness.v1.harnesses.probe" {
2115            let harness = params.harness.take().ok_or_else(|| {
2116                ServiceError::InvalidParams("harnesses.probe requires `harness`".into())
2117            })?;
2118            params.harnesses = vec![harness];
2119        }
2120        let selected = params
2121            .harnesses
2122            .iter()
2123            .map(HarnessId::as_str)
2124            .collect::<std::collections::BTreeSet<_>>();
2125        let supported = harness_support_registry()
2126            .harnesses
2127            .into_iter()
2128            .filter(|descriptor| selected.is_empty() || selected.contains(descriptor.id.as_str()))
2129            .collect::<Vec<_>>();
2130        if !params.harnesses.is_empty() && supported.len() != selected.len() {
2131            let known = supported
2132                .iter()
2133                .map(|harness| harness.id.as_str())
2134                .collect::<std::collections::BTreeSet<_>>();
2135            let missing = params
2136                .harnesses
2137                .iter()
2138                .filter(|id| !known.contains(id.as_str()))
2139                .map(HarnessId::as_str)
2140                .collect::<Vec<_>>();
2141            return Err(ServiceError::InvalidParams(format!(
2142                "unknown harness(es): {}",
2143                missing.join(", ")
2144            )));
2145        }
2146        let global_counts = params
2147            .include_sessions
2148            .then(|| self.session_counts(None, &params.harnesses));
2149        let workspace_counts = params
2150            .include_sessions
2151            .then(|| {
2152                params
2153                    .workspace
2154                    .as_deref()
2155                    .map(|workspace| self.session_counts(Some(workspace), &params.harnesses))
2156            })
2157            .flatten();
2158        Ok(InventoryWork {
2159            params,
2160            supported,
2161            global_counts,
2162            workspace_counts,
2163        })
2164    }
2165
2166    #[cfg(feature = "adapter-api")]
2167    async fn harness_authentication_call(
2168        &self,
2169        method: &str,
2170        params: Value,
2171    ) -> std::result::Result<Value, ServiceError> {
2172        match method {
2173            "harness.v1.harnesses.auth.methods" | "harness.v1.harnesses.auth.verify" => {
2174                let params = decode::<HarnessAuthenticationParams>(params)?;
2175                serde_json::to_value(crate::inspect_harness_authentication(&params.harness).await)
2176                    .map_err(|error| ServiceError::Operation(error.to_string()))
2177            }
2178            "harness.v1.harnesses.auth.begin" => {
2179                let params = decode::<BeginHarnessAuthenticationParams>(params)?;
2180                let cwd = params
2181                    .cwd
2182                    .or_else(|| std::env::current_dir().ok())
2183                    .unwrap_or_else(|| PathBuf::from("."));
2184                let plan = crate::harness_authentication_plan(
2185                    &params.harness,
2186                    params.environment,
2187                    params.method,
2188                    &cwd,
2189                )
2190                .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
2191                serde_json::to_value(plan)
2192                    .map_err(|error| ServiceError::Operation(error.to_string()))
2193            }
2194            _ => Err(ServiceError::MethodNotFound),
2195        }
2196    }
2197
2198    fn session_counts(
2199        &self,
2200        workspace: Option<&Path>,
2201        harnesses: &[HarnessId],
2202    ) -> BTreeMap<String, usize> {
2203        let mut counts = BTreeMap::new();
2204        for session in self
2205            .catalog
2206            .discover(&DiscoveryQuery {
2207                workspace: workspace.map(Path::to_path_buf),
2208                harnesses: harnesses.to_vec(),
2209                ..DiscoveryQuery::default()
2210            })
2211            .unwrap_or_default()
2212        {
2213            *counts
2214                .entry(session.locator.harness.as_str().to_string())
2215                .or_insert(0) += 1;
2216        }
2217        counts
2218    }
2219}
2220
2221#[async_trait::async_trait]
2222impl SdkService for HarnessSessionService {
2223    fn capabilities(&self) -> SdkCapabilities {
2224        SdkCapabilities::default()
2225    }
2226
2227    async fn execute(&mut self, request: SdkRequest) -> Result<Value, SdkError> {
2228        if request.operation == SdkOperation::Events {
2229            let events = self
2230                .poll_sdk_events()
2231                .await
2232                .into_iter()
2233                .map(|(_, event)| event)
2234                .collect::<Vec<_>>();
2235            return serde_json::to_value(events).map_err(|error| {
2236                SdkError::new(
2237                    SdkErrorCode::Execution,
2238                    request.operation,
2239                    error.to_string(),
2240                )
2241            });
2242        }
2243        if self.runtimes.is_empty()
2244            && matches!(
2245                request.operation,
2246                SdkOperation::Input
2247                    | SdkOperation::Interrupt
2248                    | SdkOperation::Steer
2249                    | SdkOperation::Respond
2250                    | SdkOperation::Close
2251            )
2252        {
2253            return Err(SdkError::unsupported(request.operation));
2254        }
2255        let method = request
2256            .operation
2257            .method()
2258            .ok_or_else(|| SdkError::unsupported(request.operation))?;
2259        let result = match request.operation {
2260            SdkOperation::Discover
2261            | SdkOperation::Load
2262            | SdkOperation::Export
2263            | SdkOperation::ProfilesList
2264            | SdkOperation::ProfilesGet
2265            | SdkOperation::ProfilesCreate
2266            | SdkOperation::ProfilesDelete
2267            | SdkOperation::SkillsList
2268            | SdkOperation::SkillsInstall
2269            | SdkOperation::SkillsRemove
2270            | SdkOperation::ChannelsList
2271            | SdkOperation::RoutesList
2272            | SdkOperation::TriggersList
2273            | SdkOperation::ChannelsStatus
2274            | SdkOperation::MemoryShow
2275            | SdkOperation::MemorySearch
2276            | SdkOperation::JobsList
2277            | SdkOperation::JobsGet
2278            | SdkOperation::JobsCreate
2279            | SdkOperation::JobsUpdate
2280            | SdkOperation::JobsPause
2281            | SdkOperation::JobsResume
2282            | SdkOperation::JobsRun
2283            | SdkOperation::JobsDelete
2284            | SdkOperation::JobsApply
2285            | SdkOperation::JobsNotepad
2286            | SdkOperation::JobsNotepadSet
2287            | SdkOperation::JobsNotepadDelete
2288            | SdkOperation::ModelRouteApply
2289            | SdkOperation::RunsList
2290            | SdkOperation::RunsGet
2291            | SdkOperation::ApprovalsList
2292            | SdkOperation::OrchestrationLoad
2293            | SdkOperation::OrchestrationSave
2294            | SdkOperation::OrchestrationCompile
2295            | SdkOperation::OrchestrationDecompile
2296            | SdkOperation::OrchestrationImport
2297            | SdkOperation::OrchestrationExport
2298            | SdkOperation::WorkflowLoad => self.call(method, request.params),
2299            // ORCH-20: answering needs the live connection, so it takes the
2300            // async door and ends in `harness.v1.runtimes.respond`.
2301            SdkOperation::ApprovalsResolve => self.approvals_resolve(request.params).await,
2302            SdkOperation::Start
2303            | SdkOperation::Resume
2304            | SdkOperation::Input
2305            | SdkOperation::Interrupt
2306            | SdkOperation::Steer
2307            | SdkOperation::Respond
2308            | SdkOperation::Close => self.runtime_call(method, request.params).await,
2309            // ORCH-19 controlled tier. Every verb goes through the HARNESS'S
2310            // OWN door — its CLI, its HTTP API, or its slash command typed
2311            // into a live driven session — and returns the row re-read from
2312            // the harness's store afterwards.
2313            SdkOperation::SessionsNew => {
2314                self.mutate_session(crate::SessionVerb::New, request.params)
2315                    .await
2316            }
2317            SdkOperation::SessionsReset => {
2318                self.mutate_session(crate::SessionVerb::Reset, request.params)
2319                    .await
2320            }
2321            SdkOperation::SessionsArchive => {
2322                self.mutate_session(crate::SessionVerb::Archive, request.params)
2323                    .await
2324            }
2325            SdkOperation::SessionsDelete => {
2326                self.mutate_session(crate::SessionVerb::Delete, request.params)
2327                    .await
2328            }
2329            SdkOperation::Events => unreachable!("handled before method dispatch"),
2330        };
2331        result.map_err(|error| sdk_error(request.operation, error))
2332    }
2333
2334    async fn events(&mut self) -> Result<Vec<SdkRuntimeEvent>, SdkError> {
2335        Ok(self
2336            .poll_sdk_events()
2337            .await
2338            .into_iter()
2339            .map(|(_, event)| event)
2340            .collect())
2341    }
2342}
2343
2344#[cfg(feature = "adapter-api")]
2345struct HostedRuntimeLease {
2346    connection: HostedHarnessConnection,
2347    _host: std::sync::Arc<HostedHarnessRuntime>,
2348    _registration: LiveRuntimeRegistration,
2349    _server: crate::server::FrontendHttpServer,
2350}
2351
2352#[async_trait::async_trait]
2353#[cfg(feature = "adapter-api")]
2354impl RuntimeConnection for HostedRuntimeLease {
2355    fn handle(&self) -> &crate::RuntimeHandle {
2356        self.connection.handle()
2357    }
2358
2359    async fn send_input(&mut self, input: RuntimeInput) -> crate::Result<Option<String>> {
2360        self.connection.send_input(input).await
2361    }
2362
2363    async fn next_event(&mut self) -> crate::Result<Option<crate::HarnessEvent>> {
2364        self.connection.next_event().await
2365    }
2366
2367    async fn interrupt(&mut self) -> crate::Result<()> {
2368        self.connection.interrupt().await
2369    }
2370
2371    // the lease must forward every verb its capabilities advertise; without
2372    // this, steer fell to the trait default and refused a turn it claimed
2373    async fn steer(&mut self, text: String) -> crate::Result<()> {
2374        self.connection.steer(text).await
2375    }
2376
2377    async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
2378        self.connection.respond(request_id, response).await
2379    }
2380
2381    async fn close(&mut self) -> crate::Result<()> {
2382        self.connection.close().await
2383    }
2384}
2385
2386/// One inventory request's waiting half, already separated from the service
2387/// state it reads. See [`HarnessSessionService::inventory_work`].
2388struct InventoryWork {
2389    params: HarnessInventoryParams,
2390    supported: Vec<crate::HarnessSupportDescriptor>,
2391    global_counts: Option<BTreeMap<String, usize>>,
2392    workspace_counts: Option<BTreeMap<String, usize>>,
2393}
2394
2395/// Perform one conversation-lifecycle verb through a door that is
2396/// self-contained in [`crate::sessions_control`]: the harness's own CLI, its
2397/// HTTP API, the orchestrator daemon's socket, or supercode's own store.
2398/// Touches no service state, so this runs on any task. The LIVE door is not
2399/// here — it types its slash command through a runtime connection the service
2400/// owns, and is performed by [`HarnessSessionService::mutate_session`].
2401async fn run_session_mutation(
2402    verb: crate::SessionVerb,
2403    mutation: &crate::SessionMutation,
2404) -> std::result::Result<crate::SessionMutationOutcome, ServiceError> {
2405    // Only the HTTP door actually awaits anything. The CLI, store and daemon
2406    // doors run the harness's own program, or its store, with calls that
2407    // block the calling THREAD from start to finish — a future that never
2408    // yields, which no timeout around it can interrupt and which would hold a
2409    // runtime worker for as long as the harness takes. They go to a blocking
2410    // task, where blocking is what the thread is for.
2411    let door =
2412        crate::sessions_control::door(&mutation.harness, verb).map_err(session_control_error)?;
2413    if let crate::SessionDoor::Http = door {
2414        return crate::sessions_control::mutate(verb, mutation)
2415            .await
2416            .map_err(session_control_error);
2417    }
2418    let mutation = mutation.clone();
2419    tokio::task::spawn_blocking(move || crate::sessions_control::mutate_blocking(verb, &mutation))
2420        .await
2421        .map_err(|error| {
2422            ServiceError::Operation(format!("the conversation verb could not be run: {error}"))
2423        })?
2424        .map_err(session_control_error)
2425}
2426
2427/// Probe every selected harness and assemble the report. Touches no service
2428/// state, so this runs on any task.
2429async fn run_inventory(work: InventoryWork) -> std::result::Result<Value, ServiceError> {
2430    let InventoryWork {
2431        params,
2432        supported,
2433        global_counts,
2434        workspace_counts,
2435    } = work;
2436    let probes = supported.into_iter().map(|descriptor| {
2437        let global = global_counts
2438            .as_ref()
2439            .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
2440        let workspace = workspace_counts
2441            .as_ref()
2442            .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
2443        probe_harness(descriptor, &params, global, workspace)
2444    });
2445    let harnesses = futures::future::join_all(probes).await;
2446    serde_json::to_value(HarnessInventoryReport {
2447        probe: params.probe,
2448        workspace: params.workspace,
2449        harnesses,
2450    })
2451    .map_err(|error| ServiceError::Operation(error.to_string()))
2452}
2453
2454async fn probe_harness(
2455    descriptor: crate::HarnessSupportDescriptor,
2456    params: &HarnessInventoryParams,
2457    global: Option<usize>,
2458    workspace: Option<usize>,
2459) -> LocalHarness {
2460    let launch = descriptor.runtime.default_launch.as_ref();
2461    // ORC-7: the orchestrator publishes no runtime launch — it is not an
2462    // adapter supercode connects a turn to. What "installed" means for it
2463    // is that its Node daemon entry is present, so the row answers from
2464    // that instead of from a PATH lookup it could never satisfy.
2465    let orchestrator_entry = (descriptor.id.as_str() == HarnessId::ORCHESTRATOR)
2466        .then(crate::orchestrator::daemon_entry)
2467        .and_then(Result::ok);
2468    let executable = match &orchestrator_entry {
2469        Some(entry) => Some(entry.clone()),
2470        None => launch.and_then(|launch| find_executable(&launch.program)),
2471    };
2472    let installed = executable.is_some();
2473    let version = if params.skip_versions || orchestrator_entry.is_some() {
2474        // The orchestrator's "executable" is a Node module, not a CLI
2475        // with a `--version` flag; running it to ask would start a daemon.
2476        None
2477    } else {
2478        match executable.as_deref() {
2479            Some(path) => executable_version(path).await,
2480            None => None,
2481        }
2482    };
2483    let configured = auth_evidence(descriptor.id.as_str());
2484    let mut auth = if configured {
2485        HarnessAuthState::Configured
2486    } else if matches!(
2487        descriptor.id.as_str(),
2488        HarnessId::CLAUDE_CODE | HarnessId::CODEX
2489    ) {
2490        // These two adapters have explicit native status/login contracts
2491        // and complete local evidence coverage (including Claude's macOS
2492        // Keychain-backed oauthAccount marker). Treating absent evidence
2493        // as unknown advertises a start that will only fail interactively.
2494        HarnessAuthState::Required
2495    } else {
2496        HarnessAuthState::Unknown
2497    };
2498    let mut runtime = if installed {
2499        HarnessRuntimeState::Degraded
2500    } else {
2501        HarnessRuntimeState::Unavailable
2502    };
2503    let is_orchestrator = descriptor.id.as_str() == HarnessId::ORCHESTRATOR;
2504    let mut reason = (!installed).then(|| {
2505        if is_orchestrator {
2506            format!(
2507                "{} is supported but its daemon entry `{}` was not found",
2508                descriptor.display_name,
2509                crate::orchestrator::DAEMON_ENTRY
2510            )
2511        } else {
2512            format!(
2513                "{} is supported but `{}` was not found on PATH",
2514                descriptor.display_name,
2515                launch
2516                    .map(|launch| launch.program.as_str())
2517                    .unwrap_or("executable")
2518            )
2519        }
2520    });
2521    let mut repair = (!installed).then(|| {
2522        if is_orchestrator {
2523            format!(
2524                "Install the `supercode-orchestrator` package so `{}` resolves.",
2525                crate::orchestrator::DAEMON_ENTRY
2526            )
2527        } else {
2528            format!(
2529                "Install {} and ensure `{}` is on PATH.",
2530                descriptor.display_name,
2531                launch
2532                    .map(|launch| launch.program.as_str())
2533                    .unwrap_or("its executable")
2534            )
2535        }
2536    });
2537
2538    if installed && params.probe == HarnessProbeLevel::Handshake {
2539        let backend_params = RuntimeBackendParams {
2540            harness: descriptor.id.clone(),
2541            protocol: None,
2542            launch: None,
2543            base_url: None,
2544            policy: RuntimePolicy::Default,
2545        };
2546        match runtime_backend(&backend_params) {
2547            Ok(backend) => {
2548                let cwd = params
2549                    .workspace
2550                    .clone()
2551                    .or_else(|| std::env::current_dir().ok())
2552                    .unwrap_or_else(|| PathBuf::from("."));
2553                let isolated = descriptor
2554                    .runtime
2555                    .default_launch
2556                    .clone()
2557                    .and_then(|launch| IsolatedProbeHome::new(descriptor.id.as_str(), launch).ok());
2558                let Some(isolated) = isolated else {
2559                    reason = Some(
2560                        "No-prompt runtime handshake could not create its isolated harness home."
2561                            .into(),
2562                    );
2563                    repair = Some(
2564                        "Check temporary-directory permissions, then run the handshake probe again."
2565                            .into(),
2566                    );
2567                    let running = probe_running_instance(descriptor.id.as_str());
2568                    return LocalHarness {
2569                        gateway: gateway_health(
2570                            descriptor.id.as_str(),
2571                            installed,
2572                            running.as_ref(),
2573                            version.as_deref(),
2574                        ),
2575                        id: descriptor.id,
2576                        display_name: descriptor.display_name,
2577                        supported: true,
2578                        installed,
2579                        executable: executable.map(|path| path.to_string_lossy().into_owned()),
2580                        version,
2581                        auth,
2582                        runtime,
2583                        protocol: descriptor.runtime.protocol,
2584                        capabilities: descriptor.runtime.capabilities.clone(),
2585                        effective_capabilities: descriptor.runtime.capabilities,
2586                        sessions: HarnessSessionCounts { global, workspace },
2587                        running,
2588                        reason,
2589                        repair,
2590                    };
2591                };
2592                match tokio::time::timeout(
2593                    Duration::from_secs(30),
2594                    backend.start(RuntimeStartRequest {
2595                        cwd,
2596                        launch: Some(isolated.launch.clone()),
2597                        mcp_servers: Vec::new(),
2598                    }),
2599                )
2600                .await
2601                {
2602                    Ok(Ok(mut connection)) => {
2603                        match stabilize_handshake(connection.as_mut()).await {
2604                            Ok(()) => {
2605                                auth = HarnessAuthState::Ready;
2606                                runtime = HarnessRuntimeState::Ready;
2607                                reason = Some(
2608                                    "No-prompt runtime handshake remained healthy through the startup stabilization window; no model request was sent."
2609                                        .into(),
2610                                );
2611                                repair = None;
2612                            }
2613                            Err(message) => {
2614                                auth = if looks_like_auth_error(&message) {
2615                                    HarnessAuthState::Required
2616                                } else if configured {
2617                                    HarnessAuthState::Configured
2618                                } else {
2619                                    HarnessAuthState::Unknown
2620                                };
2621                                reason = Some(format!(
2622                                    "No-prompt runtime handshake became unhealthy during startup: {message}"
2623                                ));
2624                                repair = Some(if auth == HarnessAuthState::Required {
2625                                    format!(
2626                                        "Run `{}` interactively once and complete sign-in, then probe again.",
2627                                        launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2628                                    )
2629                                } else {
2630                                    "Run the harness directly to inspect its startup failure, then probe again."
2631                                        .into()
2632                                });
2633                            }
2634                        }
2635                        let _ =
2636                            tokio::time::timeout(Duration::from_secs(3), connection.close()).await;
2637                    }
2638                    Ok(Err(error)) => {
2639                        let message = truncate_text(&error.to_string(), 500);
2640                        auth = if looks_like_auth_error(&message) {
2641                            HarnessAuthState::Required
2642                        } else if configured {
2643                            HarnessAuthState::Configured
2644                        } else {
2645                            HarnessAuthState::Unknown
2646                        };
2647                        reason = Some(format!("No-prompt runtime handshake failed: {message}"));
2648                        repair = Some(if auth == HarnessAuthState::Required {
2649                            format!(
2650                                "Run `{}` interactively once and complete sign-in, then probe again.",
2651                                launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2652                            )
2653                        } else {
2654                            "Check the harness installation and run the handshake probe again."
2655                                .into()
2656                        });
2657                    }
2658                    Err(_) => {
2659                        reason =
2660                            Some("No-prompt runtime handshake timed out after 30 seconds.".into());
2661                        repair = Some("Run the harness directly to check startup or authentication, then probe again.".into());
2662                    }
2663                }
2664                // Keep the isolated home alive through process teardown.
2665                // Otherwise the compiler may release the last meaningful
2666                // use after cloning `launch`, and a still-starting CLI can
2667                // recreate its state directory after Drop removed it.
2668                // Some Node-based launchers finish a short asynchronous
2669                // installation-id write just after their parent process
2670                // is reaped. Remove once immediately, allow that bounded
2671                // writer to settle, then perform the authoritative pass.
2672                let _ = isolated.cleanup();
2673                tokio::time::sleep(Duration::from_millis(250)).await;
2674                if let Err(error) = isolated.cleanup() {
2675                    auth = if configured {
2676                        HarnessAuthState::Configured
2677                    } else {
2678                        HarnessAuthState::Unknown
2679                    };
2680                    runtime = HarnessRuntimeState::Degraded;
2681                    reason = Some(format!(
2682                        "No-prompt runtime handshake could not remove its isolated harness home: {error}"
2683                    ));
2684                    repair = Some(
2685                        "Check temporary-directory permissions, remove the reported disposable probe home, then run the handshake again."
2686                            .into(),
2687                    );
2688                }
2689            }
2690            Err(error) => {
2691                reason = Some(error_message(error));
2692            }
2693        }
2694    } else if installed && configured {
2695        reason = Some("Executable and local authentication evidence found; use a handshake probe to verify readiness.".into());
2696    } else if installed && auth == HarnessAuthState::Required {
2697        reason = Some("Executable found, but no native authentication evidence is present.".into());
2698        repair = Some(format!(
2699            "Run `supercode harness login {}` to use the harness-owned sign-in flow.",
2700            descriptor.id.as_str()
2701        ));
2702    } else if installed {
2703        reason = Some("Executable found; authentication readiness is unknown until a no-prompt handshake succeeds.".into());
2704        repair = Some(format!(
2705            "Run `{}` interactively once if sign-in is required, or use `--probe handshake`.",
2706            launch
2707                .map(|launch| launch.program.as_str())
2708                .unwrap_or("the harness")
2709        ));
2710    }
2711
2712    let effective_capabilities = if installed {
2713        descriptor.runtime.capabilities.clone()
2714    } else {
2715        unavailable_capabilities()
2716    };
2717    let running = probe_running_instance(descriptor.id.as_str());
2718    LocalHarness {
2719        gateway: gateway_health(
2720            descriptor.id.as_str(),
2721            installed,
2722            running.as_ref(),
2723            version.as_deref(),
2724        ),
2725        id: descriptor.id,
2726        display_name: descriptor.display_name,
2727        supported: true,
2728        installed,
2729        executable: executable.map(|path| path.to_string_lossy().into_owned()),
2730        version,
2731        auth,
2732        runtime,
2733        protocol: descriptor.runtime.protocol,
2734        capabilities: descriptor.runtime.capabilities,
2735        effective_capabilities,
2736        sessions: HarnessSessionCounts { global, workspace },
2737        running,
2738        reason,
2739        repair,
2740    }
2741}
2742
2743async fn stabilize_handshake(connection: &mut dyn RuntimeConnection) -> Result<(), String> {
2744    let deadline = tokio::time::Instant::now() + Duration::from_secs(3);
2745    loop {
2746        let now = tokio::time::Instant::now();
2747        if now >= deadline {
2748            return Ok(());
2749        }
2750        match tokio::time::timeout(deadline - now, connection.next_event()).await {
2751            Err(_) => return Ok(()),
2752            Ok(Ok(Some(event))) => {
2753                if let Some(message) = handshake_event_failure(&event) {
2754                    return Err(truncate_text(&message, 500));
2755                }
2756            }
2757            Ok(Ok(None)) => return Err("runtime transport closed during startup".into()),
2758            Ok(Err(error)) => return Err(error.to_string()),
2759        }
2760    }
2761}
2762
2763fn handshake_event_failure(event: &crate::HarnessEvent) -> Option<String> {
2764    let detail = event
2765        .payload
2766        .get("message")
2767        .or_else(|| event.payload.get("line"))
2768        .and_then(Value::as_str)
2769        .unwrap_or(event.kind.as_str());
2770    match event.kind.as_str() {
2771        "transport_closed" => Some("runtime transport closed during startup".into()),
2772        "transport_error" => Some(format!("runtime transport error: {detail}")),
2773        "malformed_output" => Some(format!("runtime emitted non-protocol output: {detail}")),
2774        // Stderr is retained as a runtime event, but is not transport health.
2775        // Grok, for example, can log an AuthorizationRequired error from an
2776        // optional background worker while its ACP session continues to send
2777        // updates and complete prompts normally.
2778        _ => None,
2779    }
2780}
2781
2782fn indexed_claude_window(
2783    locator: &SessionLocator,
2784    options: &SessionLoadOptions,
2785) -> std::result::Result<Option<Value>, ServiceError> {
2786    use supercode_interchange::session::ClaudeReadIndex;
2787    // Exact parent-only window: recursive/full-artifact requests retain the
2788    // existing owner. This is not a bounded display-history substitution.
2789    if locator.harness.as_str() != HarnessId::CLAUDE_CODE
2790        || options.include_subagents != Some(false)
2791    {
2792        return Ok(None);
2793    }
2794    let crate::StorageLocator::File { path } = &locator.storage else {
2795        return Ok(None);
2796    };
2797    if !ClaudeReadIndex::supports(path)
2798        .map_err(|error| ServiceError::Operation(error.to_string()))?
2799    {
2800        return Ok(None);
2801    }
2802    let mut index = ClaudeReadIndex::open(path, Fidelity::ByteLossless)
2803        .map_err(|error| ServiceError::Operation(error.to_string()))?;
2804    let total = index.len();
2805    let (offset, end) = projected_message_window(total, options);
2806    let session = index
2807        .read_messages(offset..end)
2808        .map_err(|error| ServiceError::Operation(error.to_string()))?;
2809    let summary = index
2810        .read_summary()
2811        .map_err(|error| ServiceError::Operation(error.to_string()))?;
2812    let selected_options = SessionLoadOptions {
2813        message_offset: None,
2814        message_limit: None,
2815        message_tail: None,
2816        ..options.clone()
2817    };
2818    let mut selected = projected_session_json(&session, &selected_options);
2819    selected["raw_record_count"] = json!(index.raw_record_count());
2820    Ok(Some(json!({
2821        "session": selected,
2822        "summary": projected_session_summary(&summary, options),
2823        "window": {
2824            "has_more": offset > 0 || end < total, "has_newer": end < total,
2825            "has_older": offset > 0, "newer_items": index.item_count(end..total),
2826            "offset": offset, "older_items": index.item_count(0..offset),
2827            "returned": end - offset, "total_messages": total,
2828        }
2829    })))
2830}
2831
2832fn projected_session_result(session: &Session, options: &SessionLoadOptions) -> Value {
2833    let total_messages = session.messages.len();
2834    let (offset, end) = projected_message_window(total_messages, options);
2835    json!({
2836        "session": projected_session_json(session, options),
2837        "summary": projected_session_summary(session, options),
2838        "window": {
2839            "has_more": offset > 0 || end < total_messages,
2840            "has_newer": end < total_messages,
2841            "has_older": offset > 0,
2842            "newer_items": normalized_item_count(&session.messages[end..]),
2843            "offset": offset,
2844            "older_items": normalized_item_count(&session.messages[..offset]),
2845            "returned": end.saturating_sub(offset),
2846            "total_messages": total_messages,
2847        }
2848    })
2849}
2850
2851fn normalized_item_count(messages: &[crate::ChatMessage]) -> usize {
2852    messages
2853        .iter()
2854        .map(|message| {
2855            let conversation = usize::from(
2856                matches!(message.role, Role::Assistant | Role::User)
2857                    && message_has_content(message),
2858            );
2859            let tool_result =
2860                usize::from(message.role == Role::Tool && message_has_content(message));
2861            conversation + tool_result + message.tool_calls().len()
2862        })
2863        .sum()
2864}
2865
2866fn projected_session_summary(session: &Session, options: &SessionLoadOptions) -> Value {
2867    let mut conversational = session.messages.iter().filter(|message| {
2868        matches!(message.role, Role::Assistant | Role::User) && message_has_content(message)
2869    });
2870    let first_message = conversational.clone().next();
2871    let last_message = conversational.next_back();
2872    let mut assistant = session
2873        .messages
2874        .iter()
2875        .filter(|message| message.role == Role::Assistant && message_has_content(message));
2876    let first_assistant_message = assistant.clone().next();
2877    let last_assistant_message = assistant.next_back();
2878    let end_of_turn = session
2879        .messages
2880        .iter()
2881        .rev()
2882        .find(|message| message.role != Role::System)
2883        .is_some_and(|message| {
2884            message.role == Role::Assistant
2885                && message_has_content(message)
2886                && message.tool_calls().is_empty()
2887        });
2888    let project = |message: Option<&crate::ChatMessage>| {
2889        message.map(|message| project_inline_media(message_json(message), options))
2890    };
2891    json!({
2892        "end_of_turn": end_of_turn,
2893        "first_assistant_message": project(first_assistant_message),
2894        "first_message": project(first_message),
2895        "last_assistant_message": project(last_assistant_message),
2896        "last_assistant_text": last_assistant_message.map(message_text).unwrap_or_default(),
2897        "last_message": project(last_message),
2898    })
2899}
2900
2901fn message_has_content(message: &crate::ChatMessage) -> bool {
2902    message
2903        .content
2904        .as_deref()
2905        .is_some_and(|content| !content.trim().is_empty())
2906        || message
2907            .content_parts
2908            .as_ref()
2909            .is_some_and(|parts| !parts.is_empty())
2910}
2911
2912fn message_text(message: &crate::ChatMessage) -> String {
2913    if let Some(content) = &message.content {
2914        return content.clone();
2915    }
2916    message
2917        .content_parts
2918        .as_ref()
2919        .into_iter()
2920        .flatten()
2921        .filter_map(|part| part.get("text").and_then(Value::as_str))
2922        .collect::<Vec<_>>()
2923        .join("\n")
2924}
2925
2926fn projected_session_json(session: &Session, options: &SessionLoadOptions) -> Value {
2927    let (offset, end) = projected_message_window(session.messages.len(), options);
2928    let messages = session.messages[offset..end]
2929        .iter()
2930        .map(|message| project_inline_media(message_json(message), options))
2931        .collect::<Vec<_>>();
2932    let subagents = if options.include_subagents.unwrap_or(true) {
2933        // The reported window describes the top-level transcript. Applying it
2934        // recursively would silently truncate subagents without returning a
2935        // window for each child. Keep their histories complete while carrying
2936        // the caller's media policy through the tree.
2937        let subagent_options = SessionLoadOptions {
2938            message_limit: None,
2939            message_offset: None,
2940            message_tail: None,
2941            ..options.clone()
2942        };
2943        session
2944            .subagents
2945            .iter()
2946            .map(|subagent| projected_session_json(subagent, &subagent_options))
2947            .collect::<Vec<_>>()
2948    } else {
2949        Vec::new()
2950    };
2951    json!({
2952        "source": match session.meta.source {
2953            SessionSource::ClaudeCode => "claude_code",
2954            SessionSource::Codex => "codex",
2955            SessionSource::Gemini => "gemini",
2956            SessionSource::Goose => "goose",
2957            SessionSource::Grok => "grok",
2958            SessionSource::Native => "native",
2959            SessionSource::OpenClaw => "openclaw",
2960            SessionSource::Hermes => "hermes",
2961            SessionSource::OpenCode => "opencode",
2962            SessionSource::Pi => "pi",
2963        },
2964        "session_id": session.meta.session_id,
2965        "ended_at": session.meta.ended_at,
2966        "end_reason": session.meta.end_reason,
2967        "model": session.meta.model,
2968        "cwd": session.meta.cwd,
2969        "system_prompt": session.meta.system_prompt,
2970        "agent_id": session.meta.agent_id,
2971        "parent_tool_use_id": session.meta.parent_tool_use_id,
2972        "lineage": session.meta.lineage,
2973        "messages": messages,
2974        "subagents": subagents,
2975        "raw_record_count": session.raw.len(),
2976        "parse_error_lines": session.parse_error_lines,
2977    })
2978}
2979
2980fn projected_message_window(total: usize, options: &SessionLoadOptions) -> (usize, usize) {
2981    if let Some(tail) = options.message_tail {
2982        return (total.saturating_sub(tail), total);
2983    }
2984    let offset = options.message_offset.unwrap_or(0).min(total);
2985    let end = options
2986        .message_limit
2987        .map(|limit| offset.saturating_add(limit).min(total))
2988        .unwrap_or(total);
2989    (offset, end)
2990}
2991
2992fn project_inline_media(mut message: Value, options: &SessionLoadOptions) -> Value {
2993    let Some(parts) = message.get_mut("content").and_then(Value::as_array_mut) else {
2994        return message;
2995    };
2996    for part in parts {
2997        let Some(url) = part
2998            .get("image_url")
2999            .and_then(|image| image.get("url"))
3000            .and_then(Value::as_str)
3001        else {
3002            continue;
3003        };
3004        let Some(rest) = url.strip_prefix("data:") else {
3005            continue;
3006        };
3007        let Some((media_type, encoded)) = rest.split_once(";base64,") else {
3008            continue;
3009        };
3010        let padding = usize::from(encoded.ends_with('=')) + usize::from(encoded.ends_with("=="));
3011        let decoded_bytes = encoded.len().saturating_mul(3) / 4;
3012        let decoded_bytes = decoded_bytes.saturating_sub(padding);
3013        let should_elide = matches!(options.inline_media, InlineMediaMode::Metadata)
3014            || options
3015                .max_inline_media_bytes
3016                .is_some_and(|limit| decoded_bytes > limit);
3017        if should_elide {
3018            *part = json!({
3019                "type": "media_reference",
3020                "media_type": media_type,
3021                "encoding": "base64",
3022                "encoded_bytes": encoded.len(),
3023                "decoded_bytes": decoded_bytes,
3024                "omitted": true,
3025            });
3026        }
3027    }
3028    message
3029}
3030
3031#[derive(Deserialize)]
3032struct LocatorParams {
3033    locator: SessionLocator,
3034    /// Optional fidelity for the READ surfaces (`sessions.load`,
3035    /// `sessions.follow`).
3036    ///
3037    /// Omitted means [`Fidelity::Semantic`]: these two methods only ever
3038    /// produce a read-only view, and a compacted or resumed-across-files
3039    /// transcript — the everyday shape of a long Claude Code session — has no
3040    /// losslessly reconstructable record graph, so refusing to render it made
3041    /// the mirror unusable rather than accurate. A caller that intends to
3042    /// CONTINUE from what it reads asks for a lossless level explicitly and
3043    /// gets the strict refusal back. Every other method (export, translate,
3044    /// branch, handoff, resume_instructions) is lossless-only and has no
3045    /// such knob.
3046    #[serde(default)]
3047    fidelity: Option<Fidelity>,
3048    /// Optional bounded frontend projection. Absent preserves the historical
3049    /// complete-session read contract.
3050    #[serde(default)]
3051    view: Option<SessionReadView>,
3052}
3053
3054#[derive(Deserialize)]
3055struct SessionReadView {
3056    /// Number of trailing normalized messages to return. Zero is treated as
3057    /// one so a caller cannot accidentally request an unbounded empty mode.
3058    #[serde(default)]
3059    tail_messages: Option<usize>,
3060    /// Whether Claude Code child transcripts belong in this view. The
3061    /// frontend default is false; the legacy no-view path remains true.
3062    #[serde(default)]
3063    include_subagents: bool,
3064    /// Preserve human-visible native history across model-context compaction.
3065    #[serde(default)]
3066    display_history: bool,
3067    /// Bound each individual text field so a single tool result cannot turn a
3068    /// small message window into a hundred-megabyte RPC response.
3069    #[serde(default)]
3070    max_message_chars: Option<usize>,
3071}
3072
3073impl LocatorParams {
3074    fn read_fidelity(&self) -> Fidelity {
3075        self.fidelity.unwrap_or(Fidelity::Semantic)
3076    }
3077
3078    fn include_subagents(&self) -> bool {
3079        self.view
3080            .as_ref()
3081            .map(|view| view.include_subagents)
3082            .unwrap_or(true)
3083    }
3084
3085    fn tail_messages(&self) -> Option<usize> {
3086        self.view
3087            .as_ref()
3088            .and_then(|view| view.tail_messages)
3089            .map(|limit| limit.clamp(1, 5_000))
3090    }
3091
3092    fn display_history(&self) -> bool {
3093        self.view.as_ref().is_some_and(|view| view.display_history)
3094    }
3095
3096    fn max_message_chars(&self) -> Option<usize> {
3097        self.view
3098            .as_ref()
3099            .and_then(|view| view.max_message_chars)
3100            .map(|limit| limit.clamp(256, 64_000))
3101    }
3102
3103    fn bound_session(&self, session: &mut Session) {
3104        bound_session_view(session, self.tail_messages(), self.max_message_chars());
3105    }
3106}
3107
3108#[derive(Debug, Clone, Copy, Default, Deserialize)]
3109#[serde(rename_all = "snake_case")]
3110enum InlineMediaMode {
3111    #[default]
3112    Full,
3113    Metadata,
3114}
3115
3116#[derive(Debug, Clone, Default, Deserialize)]
3117#[serde(default)]
3118struct SessionLoadOptions {
3119    include_subagents: Option<bool>,
3120    inline_media: InlineMediaMode,
3121    max_inline_media_bytes: Option<usize>,
3122    message_limit: Option<usize>,
3123    message_offset: Option<usize>,
3124    message_tail: Option<usize>,
3125}
3126
3127impl SessionLoadOptions {
3128    fn validate(&self) -> std::result::Result<(), ServiceError> {
3129        if self.message_tail.is_some()
3130            && (self.message_limit.is_some() || self.message_offset.is_some())
3131        {
3132            return Err(ServiceError::InvalidParams(
3133                "sessions.load options.message_tail cannot be combined with message_limit or message_offset"
3134                    .into(),
3135            ));
3136        }
3137        Ok(())
3138    }
3139}
3140
3141#[derive(Deserialize)]
3142struct LoadSessionParams {
3143    #[serde(flatten)]
3144    read: LocatorParams,
3145    #[serde(default)]
3146    options: Option<SessionLoadOptions>,
3147}
3148
3149#[derive(Deserialize)]
3150struct UnfollowParams {
3151    subscription: String,
3152}
3153
3154#[derive(Debug, Deserialize)]
3155#[serde(deny_unknown_fields)]
3156struct IndexResizeParams {
3157    subscription: String,
3158    limit: usize,
3159}
3160
3161#[derive(Deserialize)]
3162struct ActivitySubscribeParams {
3163    locators: Vec<SessionLocator>,
3164    #[serde(default)]
3165    homes: crate::HarnessHomes,
3166}
3167
3168#[derive(Deserialize)]
3169struct MessageSessionParams {
3170    locator: SessionLocator,
3171    text: String,
3172    /// Same storage roots discovery accepts, so a caller (and a test) can
3173    /// point the live-session registry somewhere other than `$HOME`.
3174    #[serde(default)]
3175    homes: crate::HarnessHomes,
3176}
3177
3178#[derive(Deserialize)]
3179#[serde(deny_unknown_fields)]
3180struct HarnessSettingsParams {
3181    harness: String,
3182}
3183
3184#[derive(Deserialize)]
3185#[serde(deny_unknown_fields)]
3186struct ConfigureHarnessParams {
3187    harness: String,
3188    #[serde(default)]
3189    changes: Vec<crate::HarnessSettingChange>,
3190    #[serde(default)]
3191    expected_revision: Option<String>,
3192}
3193
3194fn claude_inbound_controls_or_error(homes: &crate::HarnessHomes) -> (Value, Value) {
3195    match crate::inspect_harness_interop_settings(homes, HarnessId::CLAUDE_CODE) {
3196        Ok(report) => (
3197            serde_json::to_value(report).unwrap_or(Value::Null),
3198            Value::Null,
3199        ),
3200        Err(error) => (
3201            Value::Null,
3202            Value::String(format!(
3203                "Supercode could not inspect Claude Code inbound controls: {error}"
3204            )),
3205        ),
3206    }
3207}
3208
3209/// Deliver `text` into a session that is running right now, or say why not.
3210///
3211/// A refusal is a RESULT, not a JSON-RPC error: "that session is persisted
3212/// only" is an answer about the session, which a mirror renders next to the
3213/// transcript, and this service's error envelope carries no structured data
3214/// field a machine-readable reason could survive in.
3215///
3216/// `delivered_to_bus` is the honest ceiling of what the courier proves. The
3217/// message reached the receiving session's inbox; whether that session ever
3218/// reads it is governed by ITS OWN inbound controls (`crossSessionInbound`,
3219/// approval dialogs), which Supercode neither sees nor overrides.
3220async fn message_live_session(
3221    params: &MessageSessionParams,
3222    runner: &dyn crate::claude_peer::CourierRunner,
3223) -> Value {
3224    if params.locator.harness.as_str() != HarnessId::CLAUDE_CODE {
3225        return json!({
3226            "delivered_to_bus": false,
3227            "refusal": {
3228                "reason": crate::claude_peer::ClaudePeerRefusal::HarnessUnsupported.as_str(),
3229                "message": format!(
3230                    "`{}` does not publish a live-session registry; only claude-code sessions can be messaged in place",
3231                    params.locator.harness.as_str()
3232                ),
3233            },
3234        });
3235    }
3236    let (inbound_controls, inbound_controls_error) =
3237        claude_inbound_controls_or_error(&params.homes);
3238    match crate::claude_peer::message_claude_peer(
3239        &params.homes,
3240        &params.locator.session_id,
3241        &params.text,
3242        runner,
3243    )
3244    .await
3245    {
3246        Ok(delivery) => json!({
3247            "delivered_to_bus": true,
3248            "target": {
3249                "session_id": delivery.target.session_id,
3250                "name": delivery.target.name,
3251                "pid": delivery.target.pid,
3252                "cwd": delivery.target.cwd,
3253                "status": delivery.target.status.map(|status| status.as_str()),
3254            },
3255            "courier": {
3256                "model": crate::claude_peer::COURIER_MODEL,
3257                "report": delivery.courier_report,
3258            },
3259            "inbound_controls": inbound_controls,
3260            "inbound_controls_error": inbound_controls_error,
3261        }),
3262        Err(refusal) => json!({
3263            "delivered_to_bus": false,
3264            "refusal": {"reason": refusal.reason.as_str(), "message": refusal.message},
3265            "inbound_controls": inbound_controls,
3266            "inbound_controls_error": inbound_controls_error,
3267        }),
3268    }
3269}
3270
3271/// Source identity of one follow subscription, plus the last lifecycle state
3272/// already reported on it. The follower itself stays purely persistence-facing.
3273// Only the adapter-api poll reads these; the subscription bookkeeping itself is
3274// shared by both builds.
3275#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
3276struct FollowedSource {
3277    harness: String,
3278    session_id: String,
3279    reported: Option<String>,
3280}
3281
3282#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
3283struct ActivitySubscription {
3284    locators: Vec<SessionLocator>,
3285    homes: crate::HarnessHomes,
3286    reported: BTreeMap<(String, String), crate::SessionActivity>,
3287}
3288
3289fn peers_for_descriptors(
3290    descriptors: &[SessionDescriptor],
3291    homes: &HarnessHomes,
3292) -> Vec<crate::claude_peer::ClaudePeerSession> {
3293    if descriptors
3294        .iter()
3295        .any(|session| session.locator.harness.as_str() == HarnessId::CLAUDE_CODE)
3296    {
3297        crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
3298    } else {
3299        Vec::new()
3300    }
3301}
3302
3303/// Add the live address that makes an indexed row behaviorally equivalent to a discovered row.
3304///
3305/// The durable index owns only persistence metadata. Live endpoints remain projections: every
3306/// message/attach operation revalidates its authority, so publishing one here never trusts a stale
3307/// browser-held handle. Reading the Claude registry once per batch keeps this O(peers + rows).
3308fn live_descriptor_value(
3309    session: &SessionDescriptor,
3310    peers: &[crate::claude_peer::ClaudePeerSession],
3311) -> std::result::Result<Value, ServiceError> {
3312    let mut value = serde_json::to_value(session)
3313        .map_err(|error| ServiceError::Operation(error.to_string()))?;
3314    if let Some(workspace) = &session.cwd {
3315        let source = LiveRuntimeSource {
3316            harness: session.locator.harness.as_str().to_string(),
3317            session_id: session.locator.session_id.clone(),
3318            workspace: workspace.clone(),
3319        };
3320        if let Some(endpoint) = discover_live_runtime(&source)
3321            .map_err(|error| ServiceError::Operation(error.to_string()))?
3322        {
3323            value["live_endpoint"] = json!(endpoint.as_str());
3324        }
3325    }
3326    if value.get("live_endpoint").is_none() {
3327        if let Some(peer) = peers.iter().find(|peer| {
3328            session.locator.harness.as_str() == HarnessId::CLAUDE_CODE
3329                && peer.session_id == session.locator.session_id
3330        }) {
3331            value["live_endpoint"] = json!(peer.endpoint().as_str());
3332        }
3333    }
3334    Ok(value)
3335}
3336
3337fn live_index_changes(
3338    changes: Vec<crate::session_index::SessionIndexChange>,
3339    homes: &HarnessHomes,
3340) -> std::result::Result<Vec<Value>, ServiceError> {
3341    use crate::session_index::SessionIndexChange;
3342    let has_claude = changes.iter().any(|change| match change {
3343        SessionIndexChange::Added { descriptor } | SessionIndexChange::Updated { descriptor } => {
3344            descriptor.locator.harness.as_str() == HarnessId::CLAUDE_CODE
3345        }
3346        SessionIndexChange::Removed { .. } => false,
3347    });
3348    let peers = if has_claude {
3349        crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
3350    } else {
3351        Vec::new()
3352    };
3353    changes
3354        .into_iter()
3355        .map(|change| match change {
3356            SessionIndexChange::Added { descriptor } => Ok(json!({
3357                "kind": "added",
3358                "descriptor": live_descriptor_value(&descriptor, &peers)?,
3359            })),
3360            SessionIndexChange::Updated { descriptor } => Ok(json!({
3361                "kind": "updated",
3362                "descriptor": live_descriptor_value(&descriptor, &peers)?,
3363            })),
3364            SessionIndexChange::Removed { key } => Ok(json!({
3365                "kind": "removed",
3366                "key": key,
3367            })),
3368        })
3369        .collect()
3370}
3371
3372fn legacy_live_status(activity: &crate::SessionActivity) -> Option<&'static str> {
3373    use crate::{SessionPresence, SessionTurnState};
3374    match (activity.presence, activity.turn) {
3375        (SessionPresence::Persisted, _) => None,
3376        (SessionPresence::Running, SessionTurnState::Working) => Some("busy"),
3377        (SessionPresence::Running, SessionTurnState::Idle) => Some("idle"),
3378        // The normalized activity object can honestly report a live owner even
3379        // when the stock harness never published a turn status. Preserve the
3380        // older field's stricter contract instead of guessing `running`.
3381        (SessionPresence::Running, SessionTurnState::Unknown)
3382            if activity.evidence.native_state.is_none() =>
3383        {
3384            None
3385        }
3386        (SessionPresence::Running, _) | (SessionPresence::ShuttingDown, _) => Some("running"),
3387    }
3388}
3389
3390#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
3391#[serde(rename_all = "kebab-case")]
3392enum TransferFormat {
3393    ClaudeCode,
3394    Codex,
3395    #[serde(rename = "opencode", alias = "open-code")]
3396    OpenCode,
3397    Pi,
3398    Grok,
3399    Gemini,
3400    Goose,
3401    /// UNI-18: a Hermes target. Its artifact is the Codex rollout that
3402    /// `hermes sessions import --from codex` reads; `sessions.export` performs
3403    /// that import into the Hermes home.
3404    Hermes,
3405}
3406
3407impl TransferFormat {
3408    fn id(self) -> &'static str {
3409        match self {
3410            Self::ClaudeCode => HarnessId::CLAUDE_CODE,
3411            Self::Codex => HarnessId::CODEX,
3412            Self::OpenCode => HarnessId::OPENCODE,
3413            Self::Pi => HarnessId::PI,
3414            Self::Grok => HarnessId::GROK,
3415            Self::Gemini => HarnessId::GEMINI,
3416            Self::Goose => HarnessId::GOOSE,
3417            Self::Hermes => HarnessId::HERMES,
3418        }
3419    }
3420}
3421
3422impl From<TransferFormat> for SessionFormat {
3423    fn from(value: TransferFormat) -> Self {
3424        match value {
3425            TransferFormat::ClaudeCode => Self::ClaudeCode,
3426            TransferFormat::Codex => Self::Codex,
3427            TransferFormat::OpenCode => Self::OpenCode,
3428            TransferFormat::Pi => Self::Pi,
3429            TransferFormat::Grok => Self::Grok,
3430            TransferFormat::Gemini => Self::Gemini,
3431            TransferFormat::Goose => Self::Goose,
3432            // a Hermes artifact is the Codex rollout Hermes imports
3433            TransferFormat::Hermes => Self::Codex,
3434        }
3435    }
3436}
3437
3438#[derive(Deserialize)]
3439struct ImportSessionParams {
3440    source_harness: TransferFormat,
3441    content: String,
3442}
3443
3444#[derive(Deserialize)]
3445struct ExportSessionParams {
3446    locator: SessionLocator,
3447    target_harness: TransferFormat,
3448}
3449
3450#[derive(Deserialize)]
3451struct ReduceSessionParams {
3452    locator: SessionLocator,
3453    target_harness: TransferFormat,
3454    #[serde(default = "default_keep_last")]
3455    keep_last: usize,
3456}
3457
3458fn default_keep_last() -> usize {
3459    6
3460}
3461
3462#[derive(Deserialize)]
3463struct BranchSessionParams {
3464    locator: SessionLocator,
3465    #[serde(default)]
3466    target_harness: Option<TransferFormat>,
3467}
3468
3469#[derive(Deserialize)]
3470struct HandoffSessionParams {
3471    locator: SessionLocator,
3472    target_harness: TransferFormat,
3473    #[serde(default)]
3474    cwd: Option<PathBuf>,
3475}
3476
3477#[derive(Debug, Clone, Copy, Default, Deserialize)]
3478#[serde(rename_all = "snake_case")]
3479enum ResumePolicy {
3480    #[default]
3481    Default,
3482    Yolo,
3483}
3484
3485#[derive(Deserialize)]
3486struct ResumeInstructionsParams {
3487    locator: SessionLocator,
3488    #[serde(default)]
3489    cwd: Option<PathBuf>,
3490    #[serde(default)]
3491    policy: ResumePolicy,
3492}
3493
3494/// `harness.v1.workflow.load` parameters: which harness's board, and its home.
3495#[derive(Deserialize)]
3496struct WorkflowLoadParams {
3497    from: crate::workflow_doors::WorkflowHarness,
3498    home: PathBuf,
3499}
3500
3501/// ONT-4 `harness.v1.orchestration.load` parameters. `flavor` says which layout the
3502/// folder is read as; our own is the default.
3503#[derive(Deserialize)]
3504struct OrchestrationLoadParams {
3505    root: PathBuf,
3506    #[serde(default)]
3507    flavor: crate::orchestration_doors::HomeFlavor,
3508}
3509
3510/// ONT-4 `harness.v1.orchestration.save` parameters. `vault` is merged into the
3511/// home's own secrets; a caller that sends none keeps what is on disk.
3512#[derive(Deserialize)]
3513struct OrchestrationSaveParams {
3514    root: PathBuf,
3515    orchestration: crate::orchestration::Orchestration,
3516    #[serde(default)]
3517    vault: BTreeMap<String, String>,
3518}
3519
3520/// ONT-4 `harness.v1.orchestration.compile` parameters.
3521#[derive(Deserialize)]
3522struct OrchestrationCompileParams {
3523    from: crate::orchestration_doors::OrchestrationHarness,
3524    home: PathBuf,
3525}
3526
3527/// ONT-4 `harness.v1.orchestration.decompile` parameters. `source` is the home the
3528/// orchestration was compiled from: it is re-compiled to recover the io bookkeeping
3529/// that byte reuse and the live-store refusal (UNI-18) are decided from.
3530#[derive(Deserialize)]
3531struct OrchestrationDecompileParams {
3532    to: crate::orchestration_doors::OrchestrationHarness,
3533    orchestration: crate::orchestration::Orchestration,
3534    source: PathBuf,
3535    #[serde(default)]
3536    source_flavor: crate::orchestration_doors::SourceFlavor,
3537    dest: PathBuf,
3538    #[serde(default)]
3539    vault: BTreeMap<String, String>,
3540}
3541
3542/// `harness.v1.orchestration.import` parameters: another harness's home, and the
3543/// folder of ours it becomes.
3544#[derive(Deserialize)]
3545struct OrchestrationImportParams {
3546    from: crate::orchestration_doors::OrchestrationHarness,
3547    home: PathBuf,
3548    into: PathBuf,
3549}
3550
3551/// `harness.v1.orchestration.export` parameters: a folder of ours, and the home of
3552/// another harness it becomes.
3553#[derive(Deserialize)]
3554struct OrchestrationExportParams {
3555    to: crate::orchestration_doors::OrchestrationHarness,
3556    root: PathBuf,
3557    dest: PathBuf,
3558}
3559
3560/// `harness.v1.jobs.get` parameters.
3561#[derive(Deserialize)]
3562struct JobsGetParams {
3563    harness: String,
3564    id: String,
3565    #[serde(default)]
3566    homes: crate::HarnessHomes,
3567}
3568
3569/// ORCH-18: run one mutating job verb through the harness's own CLI.
3570///
3571/// The refusal ladder is deliberate: a harness with no scheduled-job concept
3572/// at all answers with the SAME sentence `jobs.list` gives it, and a harness
3573/// that has jobs but publishes no client-callable verb (Claude Code, whose
3574/// jobs are created by the model inside a session) answers with its own
3575/// reason. Neither is ever a silent no-op.
3576fn mutate_job(
3577    verb: crate::jobs_control::JobVerb,
3578    params: Value,
3579) -> std::result::Result<Value, ServiceError> {
3580    let mutation = decode::<crate::jobs_control::JobMutation>(params)?;
3581    refuse_harness_without_jobs(&mutation.harness, &format!("jobs.{}", verb.as_str()))?;
3582    let outcome = crate::jobs_control::mutate(verb, &mutation).map_err(job_control_error)?;
3583    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3584}
3585
3586/// ORCH-22: run one mutating skills verb through the harness's own door.
3587///
3588/// The refusal ladder mirrors `jobs.*`: a harness with no skills root at all
3589/// answers with the same sentence `skills.list` gives it, and a harness whose
3590/// door does not publish this verb (OpenClaw has no `skills remove` at the
3591/// pin) answers with its own reason. Neither is ever a silent no-op.
3592fn mutate_skill(
3593    verb: crate::skills_control::SkillVerb,
3594    params: Value,
3595) -> std::result::Result<Value, ServiceError> {
3596    let mutation = decode::<crate::skills_control::SkillMutation>(params)?;
3597    if !crate::skills_control::supports_skill_control(&mutation.harness) {
3598        return Err(ServiceError::UnsupportedAction(format!(
3599            "`{}` has no skills root supercode reads; `skills.{}` is supported for: {}",
3600            mutation.harness,
3601            verb.as_str(),
3602            crate::skills_control::CONTROLLED_SKILL_HARNESSES.join(", ")
3603        )));
3604    }
3605    let outcome =
3606        crate::skills_control::mutate_skill(verb, &mutation).map_err(skill_control_error)?;
3607    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3608}
3609
3610/// The skills twin of [`job_control_error`], with the same mapping rule.
3611fn skill_control_error(error: crate::skills_control::SkillControlError) -> ServiceError {
3612    match error {
3613        crate::skills_control::SkillControlError::Unsupported(message) => {
3614            ServiceError::UnsupportedAction(message)
3615        }
3616        crate::skills_control::SkillControlError::Invalid(message) => {
3617            ServiceError::InvalidParams(message)
3618        }
3619        crate::skills_control::SkillControlError::Failed(message) => {
3620            ServiceError::Operation(message)
3621        }
3622    }
3623}
3624
3625/// ORCH-21: run one mutating profile verb through the harness's own CLI.
3626///
3627/// The refusal ladder mirrors `mutate_job`'s: a harness with no profile
3628/// concept at all answers with the SAME sentence `profiles.list` gives it, and
3629/// a harness that HAS profiles but publishes no client-callable verb (Codex's
3630/// file-authored `[profiles.<name>]` tables, supercode's compiled-in presets)
3631/// answers with its own reason. Neither is ever a silent no-op.
3632fn mutate_profile(
3633    verb: crate::profiles_control::ProfileVerb,
3634    params: Value,
3635) -> std::result::Result<Value, ServiceError> {
3636    let mutation = decode::<crate::profiles_control::ProfileMutation>(params)?;
3637    let outcome =
3638        crate::profiles_control::mutate(verb, &mutation).map_err(profile_control_error)?;
3639    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3640}
3641
3642/// The same mapping `job_control_error` applies, for the profile noun.
3643fn profile_control_error(error: crate::profiles_control::ProfileControlError) -> ServiceError {
3644    match error {
3645        crate::profiles_control::ProfileControlError::Unsupported(message) => {
3646            ServiceError::UnsupportedAction(message)
3647        }
3648        crate::profiles_control::ProfileControlError::Invalid(message) => {
3649            ServiceError::InvalidParams(message)
3650        }
3651        crate::profiles_control::ProfileControlError::Failed(message) => {
3652            ServiceError::Operation(message)
3653        }
3654    }
3655}
3656
3657/// Map a controlled-tier failure onto the service's error vocabulary. A verb
3658/// the harness lacks is `UnsupportedAction`; a harness verb that RAN and
3659/// failed carries its own stderr through as the operation error.
3660fn job_control_error(error: crate::jobs_control::JobControlError) -> ServiceError {
3661    match error {
3662        crate::jobs_control::JobControlError::Unsupported(message) => {
3663            ServiceError::UnsupportedAction(message)
3664        }
3665        crate::jobs_control::JobControlError::Invalid(message) => {
3666            ServiceError::InvalidParams(message)
3667        }
3668        crate::jobs_control::JobControlError::Failed(message) => ServiceError::Operation(message),
3669    }
3670}
3671
3672/// Map an ORCH-19 controlled-tier failure onto the service's error
3673/// vocabulary. A verb the harness has no door for is `UnsupportedAction`; a
3674/// door that RAN and failed carries the harness's own stderr / HTTP body
3675/// through as the operation error.
3676fn session_control_error(error: crate::SessionControlError) -> ServiceError {
3677    match error {
3678        crate::SessionControlError::Unsupported(message) => {
3679            ServiceError::UnsupportedAction(message)
3680        }
3681        crate::SessionControlError::Invalid(message) => ServiceError::InvalidParams(message),
3682        crate::SessionControlError::Failed(message) => ServiceError::Operation(message),
3683    }
3684}
3685
3686/// A harness without a scheduled-job concept refuses the verb rather than
3687/// answering with an empty list — an absent capability and an empty inventory
3688/// are different answers (the same rule `runtimes.capabilities` applies to
3689/// `steer`).
3690fn refuse_harness_without_jobs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3691    if crate::jobs::supports_jobs(harness) {
3692        return Ok(());
3693    }
3694    Err(ServiceError::UnsupportedAction(format!(
3695        "`{harness}` has no scheduled jobs; `{verb}` is supported for: {}",
3696        crate::jobs::JOB_HARNESSES.join(", ")
3697    )))
3698}
3699
3700/// `harness.v1.runs.get` parameters.
3701#[derive(Deserialize)]
3702struct RunsGetParams {
3703    harness: String,
3704    id: String,
3705    #[serde(default)]
3706    homes: crate::HarnessHomes,
3707}
3708
3709/// A harness with no run store refuses the verb rather than answering with an
3710/// empty history — the same rule `jobs.list` applies. Claude Code lands here
3711/// on purpose: its cron fires are ordinary turns inside the session that
3712/// created the job, so there is no fire record to list.
3713fn refuse_harness_without_runs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3714    if crate::runs::supports_runs(harness) {
3715        return Ok(());
3716    }
3717    Err(ServiceError::UnsupportedAction(format!(
3718        "`{harness}` keeps no run store; `{verb}` is supported for: {}",
3719        crate::runs::RUN_HARNESSES.join(", ")
3720    )))
3721}
3722
3723#[derive(Serialize)]
3724struct SessionArtifact {
3725    source_harness: HarnessId,
3726    target_harness: &'static str,
3727    session_id: Option<String>,
3728    content: String,
3729    suggested_filename: String,
3730    files: Vec<SessionArtifactFile>,
3731    fidelity: Fidelity,
3732    residue: Vec<String>,
3733}
3734
3735#[derive(Serialize)]
3736struct SessionArtifactFile {
3737    path: String,
3738    content: String,
3739    role: ArtifactFileRole,
3740}
3741
3742#[derive(Serialize)]
3743#[serde(rename_all = "snake_case")]
3744enum ArtifactFileRole {
3745    Primary,
3746    Subagent,
3747    Bundle,
3748    SourceRecovery,
3749}
3750
3751#[derive(Serialize)]
3752struct StructuredLaunch {
3753    cwd: PathBuf,
3754    program: String,
3755    arguments: Vec<String>,
3756    env: BTreeMap<String, String>,
3757}
3758
3759struct HandoffInstructions {
3760    launch: StructuredLaunch,
3761    materialize: Option<StructuredLaunch>,
3762    requires_materialization: bool,
3763    note: String,
3764}
3765
3766#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
3767#[serde(rename_all = "snake_case")]
3768enum HarnessProbeLevel {
3769    #[default]
3770    Passive,
3771    Handshake,
3772}
3773
3774#[derive(Default, Deserialize)]
3775#[serde(default)]
3776struct HarnessInventoryParams {
3777    harness: Option<HarnessId>,
3778    harnesses: Vec<HarnessId>,
3779    workspace: Option<PathBuf>,
3780    probe: HarnessProbeLevel,
3781    include_sessions: bool,
3782    /// Omit subprocess-based `--version` calls when a latency-sensitive UI only needs readiness.
3783    skip_versions: bool,
3784}
3785
3786#[derive(Deserialize)]
3787struct HarnessAuthenticationParams {
3788    harness: HarnessId,
3789}
3790
3791#[derive(Deserialize)]
3792struct BeginHarnessAuthenticationParams {
3793    harness: HarnessId,
3794    #[serde(default = "local_browser_authentication_environment")]
3795    environment: crate::HarnessAuthenticationEnvironment,
3796    #[serde(default)]
3797    method: Option<crate::HarnessAuthenticationMethodId>,
3798    #[serde(default)]
3799    cwd: Option<PathBuf>,
3800}
3801
3802fn local_browser_authentication_environment() -> crate::HarnessAuthenticationEnvironment {
3803    crate::HarnessAuthenticationEnvironment::LocalBrowser
3804}
3805
3806#[derive(Serialize)]
3807struct HarnessInventoryReport {
3808    probe: HarnessProbeLevel,
3809    workspace: Option<PathBuf>,
3810    harnesses: Vec<LocalHarness>,
3811}
3812
3813#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3814#[serde(rename_all = "snake_case")]
3815enum HarnessAuthState {
3816    Ready,
3817    Configured,
3818    Required,
3819    Unknown,
3820}
3821
3822#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3823#[serde(rename_all = "snake_case")]
3824enum HarnessRuntimeState {
3825    Ready,
3826    Degraded,
3827    Unavailable,
3828}
3829
3830#[derive(Serialize)]
3831struct HarnessSessionCounts {
3832    global: Option<usize>,
3833    workspace: Option<usize>,
3834}
3835
3836/// Receipt-backed evidence that a harness has a RUNNING instance right now,
3837/// distinct from being merely installed (UNI-7). Detection is passive and
3838/// default-on: a gateway liveness connect for daemon harnesses, a fresh
3839/// SQLite WAL stamp for store-writer harnesses (precedent: the opencode
3840/// follower's -wal/-shm freshness). Control stays behind per-connection
3841/// grants — this reports observations only.
3842/// ORCH-17: the gateway-health noun on an inventory row. Derived from the
3843/// UNI-7 running-instance probe (Hermes: `state.db-wal` freshness; OpenClaw:
3844/// a TCP connect to the gateway endpoint resolved from its OWN config) plus
3845/// the executable version — never by starting anything.
3846#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3847#[serde(rename_all = "snake_case")]
3848pub enum GatewayState {
3849    Up,
3850    Down,
3851    Unknown,
3852}
3853
3854/// ORCH-17: `gateway` on a `harness.v1.harnesses.list` row.
3855#[derive(Debug, Clone, Serialize)]
3856pub struct GatewayHealth {
3857    pub state: GatewayState,
3858    /// The endpoint supercode would connect to (OpenClaw: the gateway
3859    /// WebSocket resolved from `openclaw.json`; core harnesses: their
3860    /// declared connect address when one exists). `None` when the harness
3861    /// has no single endpoint (Hermes multiplexes platforms).
3862    #[serde(skip_serializing_if = "Option::is_none")]
3863    pub endpoint: Option<String>,
3864    #[serde(skip_serializing_if = "Option::is_none")]
3865    pub version: Option<String>,
3866    /// What the verdict rests on, or why it is `unknown`.
3867    pub evidence: String,
3868    pub checked_at_ms: u64,
3869}
3870
3871/// OpenClaw's gateway WebSocket endpoint, resolved from its own config the
3872/// way the registry's connect descriptor prescribes (`gateway.url`, else
3873/// `gateway.port`, else the documented default).
3874fn openclaw_gateway_endpoint(home: &Path) -> String {
3875    let config_path = home.join(".openclaw/openclaw.json");
3876    let gateway = std::fs::read_to_string(&config_path)
3877        .ok()
3878        .and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok())
3879        .and_then(|config| config.get("gateway").cloned());
3880    if let Some(url) = gateway
3881        .as_ref()
3882        .and_then(|gateway| gateway.get("url"))
3883        .and_then(serde_json::Value::as_str)
3884    {
3885        return url.to_string();
3886    }
3887    let port = gateway
3888        .as_ref()
3889        .and_then(|gateway| gateway.get("port"))
3890        .and_then(serde_json::Value::as_u64)
3891        .unwrap_or(18789);
3892    format!("ws://127.0.0.1:{port}")
3893}
3894
3895/// Ask Hermes itself (`hermes gateway status`, read-only, ~1 s) whether its
3896/// gateway is up. The command is per-host launchd/systemd text without a JSON
3897/// form at 0.19–0.21; the verdict is read from the lines it prints:
3898/// "supervised by launchd (PID …)" / "is running" → up, "not running" /
3899/// "not installed" → down, anything else → no verdict. `SUPERCODE_HERMES_BIN`
3900/// overrides the executable so a fake can stand in under test.
3901fn hermes_gateway_status() -> Option<(GatewayState, String)> {
3902    let program = crate::harness_command::harness_program(HarnessId::HERMES).ok()?;
3903    let output = std::process::Command::new(&program)
3904        .args(["gateway", "status"])
3905        .stdin(std::process::Stdio::null())
3906        .output()
3907        .ok()?;
3908    let text = format!(
3909        "{}{}",
3910        String::from_utf8_lossy(&output.stdout),
3911        String::from_utf8_lossy(&output.stderr)
3912    );
3913    let verdict = text.lines().find_map(|line| {
3914        let l = line.trim();
3915        if l.contains("supervised by launchd (PID")
3916            || l.contains("supervised by systemd (PID")
3917            || l.contains("Gateway is running")
3918            || l.contains("process is running")
3919        {
3920            Some((GatewayState::Up, format!("`hermes gateway status`: {l}")))
3921        } else if l.contains("not running") || l.contains("not installed") {
3922            Some((GatewayState::Down, format!("`hermes gateway status`: {l}")))
3923        } else {
3924            None
3925        }
3926    });
3927    verdict
3928}
3929
3930fn gateway_health(
3931    id: &str,
3932    installed: bool,
3933    running: Option<&RunningInstance>,
3934    version: Option<&str>,
3935) -> GatewayHealth {
3936    let checked_at_ms = now_epoch_ms();
3937    let home = std::env::var_os("HOME").map(PathBuf::from);
3938    match id {
3939        HarnessId::HERMES | HarnessId::OPENCLAW => {
3940            let endpoint = (id == HarnessId::OPENCLAW)
3941                .then(|| home.as_deref().map(openclaw_gateway_endpoint))
3942                .flatten();
3943            let (state, evidence) = match running {
3944                Some(instance) => (GatewayState::Up, instance.evidence.clone()),
3945                None if !installed => (
3946                    GatewayState::Unknown,
3947                    format!("`{id}` is not installed; no gateway to probe"),
3948                ),
3949                None if id == HarnessId::HERMES => match hermes_gateway_status() {
3950                    // The harness's own door outranks the WAL heuristic: an idle
3951                    // gateway writes nothing for minutes yet is up.
3952                    Some((state, evidence)) => (state, evidence),
3953                    None => (
3954                        GatewayState::Down,
3955                        "no fresh state.db-wal activity under ~/.hermes and `hermes gateway status` gave no verdict".to_string(),
3956                    ),
3957                },
3958                None => (
3959                    GatewayState::Down,
3960                    format!(
3961                        "no TCP listener at {}",
3962                        endpoint.as_deref().unwrap_or("the gateway endpoint")
3963                    ),
3964                ),
3965            };
3966            GatewayHealth {
3967                state,
3968                endpoint,
3969                version: version.map(str::to_string),
3970                evidence,
3971                checked_at_ms,
3972            }
3973        }
3974        // ORC-7: the orchestrator's gateway IS its daemon, and the daemon's
3975        // own lease file is the record of it. A lease naming a live pid is
3976        // up; a lease whose process is gone is down and says so as a STALE
3977        // lease, never as "no lease"; no lease at all is down. Nothing is
3978        // started, and no port is guessed — the daemon multiplexes adapters
3979        // the way Hermes does, so it has no single endpoint either.
3980        HarnessId::ORCHESTRATOR => {
3981            let root = crate::HarnessHomes::default().orchestrator;
3982            let (state, evidence) = match crate::orchestrator::read_lease(&root) {
3983                Some(lease) if crate::orchestrator::pid_is_live(lease.pid) => (
3984                    GatewayState::Up,
3985                    format!(
3986                        "`{}` names pid {} (started {}), which is live",
3987                        crate::orchestrator::lock_path(&root).display(),
3988                        lease.pid,
3989                        lease.started_at
3990                    ),
3991                ),
3992                Some(lease) => (
3993                    GatewayState::Down,
3994                    format!(
3995                        "stale lease `{}`: pid {} is gone",
3996                        crate::orchestrator::lock_path(&root).display(),
3997                        lease.pid
3998                    ),
3999                ),
4000                None => (
4001                    GatewayState::Down,
4002                    format!(
4003                        "no lease at `{}`; `supercode orchestrator start` writes one",
4004                        crate::orchestrator::lock_path(&root).display()
4005                    ),
4006                ),
4007            };
4008            GatewayHealth {
4009                state,
4010                endpoint: None,
4011                version: version.map(str::to_string),
4012                evidence,
4013                checked_at_ms,
4014            }
4015        }
4016        _ => GatewayHealth {
4017            state: GatewayState::Unknown,
4018            endpoint: None,
4019            version: version.map(str::to_string),
4020            evidence: format!("`{id}` runs per session, not as a gateway"),
4021            checked_at_ms,
4022        },
4023    }
4024}
4025
4026#[derive(Debug, Clone, Serialize)]
4027struct RunningInstance {
4028    /// How the instance was detected.
4029    method: RunningInstanceMethod,
4030    /// The evidence the verdict rests on (endpoint reached / WAL path+age).
4031    evidence: String,
4032    /// Epoch-ms instant the probe executed.
4033    checked_at_ms: u64,
4034}
4035
4036#[derive(Debug, Clone, Copy, Serialize)]
4037#[serde(rename_all = "snake_case")]
4038enum RunningInstanceMethod {
4039    /// A TCP connect to the harness's own configured gateway endpoint
4040    /// succeeded.
4041    GatewayConnect,
4042    /// The harness's session store has an active SQLite WAL (a live writer
4043    /// holds the store open and stamped it recently).
4044    StoreWalActivity,
4045}
4046
4047fn now_epoch_ms() -> u64 {
4048    std::time::SystemTime::now()
4049        .duration_since(std::time::UNIX_EPOCH)
4050        .map(|elapsed| elapsed.as_millis() as u64)
4051        .unwrap_or(0)
4052}
4053
4054/// OpenClaw: the gateway endpoint comes from the harness's OWN config
4055/// (`<home>/.openclaw/openclaw.json` — `gateway.url` or `gateway.port`,
4056/// default port 18789); a successful TCP connect is the running signal.
4057fn probe_openclaw_running(home: &Path) -> Option<RunningInstance> {
4058    let config_path = home.join(".openclaw/openclaw.json");
4059    let text = std::fs::read_to_string(&config_path).ok();
4060    let gateway = text
4061        .as_deref()
4062        .and_then(|raw| serde_json::from_str::<serde_json::Value>(raw).ok())
4063        .and_then(|config| config.get("gateway").cloned());
4064    let address = gateway
4065        .as_ref()
4066        .and_then(|gateway| gateway.get("url"))
4067        .and_then(serde_json::Value::as_str)
4068        .and_then(|url| {
4069            url.split("://").nth(1).map(|rest| {
4070                rest.trim_end_matches('/')
4071                    .split('/')
4072                    .next()
4073                    .unwrap_or(rest)
4074                    .to_string()
4075            })
4076        })
4077        .unwrap_or_else(|| {
4078            let port = gateway
4079                .as_ref()
4080                .and_then(|gateway| gateway.get("port"))
4081                .and_then(serde_json::Value::as_u64)
4082                .unwrap_or(18789);
4083            format!("127.0.0.1:{port}")
4084        });
4085    let reachable = std::net::TcpStream::connect_timeout(
4086        &address.parse().ok()?,
4087        std::time::Duration::from_millis(400),
4088    )
4089    .is_ok();
4090    reachable.then(|| RunningInstance {
4091        method: RunningInstanceMethod::GatewayConnect,
4092        evidence: format!(
4093            "gateway endpoint {address} accepted a TCP connect (from {})",
4094            config_path.display()
4095        ),
4096        checked_at_ms: now_epoch_ms(),
4097    })
4098}
4099
4100/// Hermes: `<home>/.hermes/state.db-wal` freshly modified means a live writer
4101/// holds the store open (SQLite WAL exists only while a connection is open;
4102/// a recent stamp distinguishes an active instance from a stale crash
4103/// leftover).
4104fn probe_hermes_running(home: &Path, max_wal_age_ms: u64) -> Option<RunningInstance> {
4105    let wal = home.join(".hermes/state.db-wal");
4106    let modified = std::fs::metadata(&wal).ok()?.modified().ok()?;
4107    let age_ms = std::time::SystemTime::now()
4108        .duration_since(modified)
4109        .map(|age| age.as_millis() as u64)
4110        .unwrap_or(u64::MAX);
4111    (age_ms <= max_wal_age_ms).then(|| RunningInstance {
4112        method: RunningInstanceMethod::StoreWalActivity,
4113        evidence: format!(
4114            "{} stamped {age_ms}ms ago (threshold {max_wal_age_ms}ms)",
4115            wal.display()
4116        ),
4117        checked_at_ms: now_epoch_ms(),
4118    })
4119}
4120
4121/// Default-on running-instance detection for the harnesses that have one.
4122fn probe_running_instance(id: &str) -> Option<RunningInstance> {
4123    let home = std::env::var_os("HOME").map(PathBuf::from)?;
4124    match id {
4125        HarnessId::OPENCLAW => probe_openclaw_running(&home),
4126        HarnessId::HERMES => probe_hermes_running(&home, 300_000),
4127        _ => None,
4128    }
4129}
4130
4131#[derive(Serialize)]
4132struct LocalHarness {
4133    id: HarnessId,
4134    display_name: String,
4135    supported: bool,
4136    installed: bool,
4137    executable: Option<String>,
4138    version: Option<String>,
4139    auth: HarnessAuthState,
4140    runtime: HarnessRuntimeState,
4141    protocol: String,
4142    capabilities: crate::RuntimeCapabilities,
4143    effective_capabilities: crate::RuntimeCapabilities,
4144    sessions: HarnessSessionCounts,
4145    /// Receipt-backed running-instance detection (None = not detected or the
4146    /// harness has no running-instance concept). Distinct from `installed`.
4147    #[serde(skip_serializing_if = "Option::is_none")]
4148    running: Option<RunningInstance>,
4149    /// ORCH-17: gateway health derived from `running` + the harness's own config.
4150    gateway: GatewayHealth,
4151    reason: Option<String>,
4152    repair: Option<String>,
4153}
4154
4155#[derive(Clone, Deserialize)]
4156struct RuntimeBackendParams {
4157    harness: HarnessId,
4158    #[serde(default)]
4159    protocol: Option<String>,
4160    #[serde(default)]
4161    launch: Option<RuntimeLaunch>,
4162    #[serde(default)]
4163    base_url: Option<String>,
4164    #[serde(default)]
4165    policy: RuntimePolicy,
4166}
4167
4168#[derive(Debug, Clone, Copy, Default, Deserialize)]
4169#[serde(rename_all = "snake_case")]
4170enum RuntimePolicy {
4171    #[default]
4172    Default,
4173    Yolo,
4174}
4175
4176#[derive(Deserialize)]
4177struct RuntimeStartParams {
4178    #[serde(flatten)]
4179    backend: RuntimeBackendParams,
4180    cwd: PathBuf,
4181    /// MCP servers to mount into the new session through the harness's own
4182    /// start door (ORC-6). Backends without such a door ignore them.
4183    #[serde(default)]
4184    mcp_servers: Vec<crate::McpServerLaunch>,
4185}
4186
4187#[derive(Deserialize)]
4188struct RuntimeAttachParams {
4189    #[serde(flatten)]
4190    backend: RuntimeBackendParams,
4191    runtime_id: String,
4192    #[serde(default)]
4193    cwd: Option<PathBuf>,
4194    /// MCP servers to mount into the resumed session (the start door's own
4195    /// field, carried again because a session's tools die with its process).
4196    #[serde(default)]
4197    mcp_servers: Vec<crate::McpServerLaunch>,
4198}
4199
4200#[derive(Deserialize)]
4201struct RuntimeConnectionParams {
4202    connection: String,
4203}
4204
4205#[derive(Deserialize)]
4206struct RuntimeInputParams {
4207    connection: String,
4208    text: String,
4209    #[serde(default)]
4210    image_urls: Vec<String>,
4211}
4212
4213const MAX_RUNTIME_IMAGES: usize = 4;
4214const MAX_RUNTIME_IMAGE_URL_BYTES: usize = 12 * 1024 * 1024;
4215const MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL: usize = 32 * 1024 * 1024;
4216
4217fn validate_runtime_image_urls(image_urls: Vec<String>) -> Result<Vec<String>, ServiceError> {
4218    if image_urls.len() > MAX_RUNTIME_IMAGES {
4219        return Err(ServiceError::InvalidParams(format!(
4220            "a runtime prompt accepts at most {MAX_RUNTIME_IMAGES} images"
4221        )));
4222    }
4223    let mut total = 0usize;
4224    for url in &image_urls {
4225        if !(url.starts_with("data:image/")
4226            || url.starts_with("https://")
4227            || url.starts_with("http://"))
4228        {
4229            return Err(ServiceError::InvalidParams(
4230                "runtime images must be image data URLs or HTTP(S) URLs".into(),
4231            ));
4232        }
4233        if url.len() > MAX_RUNTIME_IMAGE_URL_BYTES {
4234            return Err(ServiceError::InvalidParams(format!(
4235                "one runtime image exceeds the {MAX_RUNTIME_IMAGE_URL_BYTES}-byte encoded limit"
4236            )));
4237        }
4238        total = total.saturating_add(url.len());
4239    }
4240    if total > MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL {
4241        return Err(ServiceError::InvalidParams(format!(
4242            "runtime images exceed the {MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL}-byte encoded total limit"
4243        )));
4244    }
4245    Ok(image_urls)
4246}
4247
4248#[derive(Deserialize)]
4249struct RuntimeRespondParams {
4250    connection: String,
4251    request_id: Value,
4252    response: Value,
4253}
4254
4255fn default_reduction_store_root() -> PathBuf {
4256    if let Some(root) = std::env::var_os("SUPERCODE_HOME") {
4257        return PathBuf::from(root).join("sessions");
4258    }
4259    if let Some(home) = std::env::var_os("HOME") {
4260        return PathBuf::from(home).join(".supercode").join("sessions");
4261    }
4262    PathBuf::from(".supercode").join("sessions")
4263}
4264
4265fn messages_jsonl(messages: &[crate::ChatMessage]) -> std::result::Result<String, ServiceError> {
4266    let mut output = String::new();
4267    for message in messages {
4268        output.push_str(
4269            &serde_json::to_string(message)
4270                .map_err(|error| ServiceError::Operation(error.to_string()))?,
4271        );
4272        output.push('\n');
4273    }
4274    Ok(output)
4275}
4276
4277fn parse_messages_jsonl(
4278    content: &str,
4279) -> std::result::Result<Vec<crate::ChatMessage>, ServiceError> {
4280    content
4281        .lines()
4282        .enumerate()
4283        .filter(|(_, line)| !line.trim().is_empty())
4284        .map(|(index, line)| {
4285            serde_json::from_str::<crate::ChatMessage>(line).map_err(|error| {
4286                ServiceError::Operation(format!(
4287                    "reduced transcript line {} is invalid: {error}",
4288                    index + 1
4289                ))
4290            })
4291        })
4292        .collect()
4293}
4294
4295fn reduced_bootstrap_prompt(
4296    source: &SessionLocator,
4297    target: TransferFormat,
4298    view_jsonl: &str,
4299    sidecar_path: &Path,
4300    reduction_log_path: &Path,
4301) -> String {
4302    format!(
4303        "Continue the work from this losslessly reduced {source_harness} session in {target_harness}.\n\
4304         \n\
4305         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\
4306         \n\
4307         <supercode-reduced-session source-session=\"{source_id}\">\n\
4308         {view_jsonl}\
4309         </supercode-reduced-session>\n\
4310         \n\
4311         Resume from the latest unresolved user request and preserve the source session's decisions and constraints.",
4312        source_harness = source.harness.as_str(),
4313        target_harness = target.id(),
4314        sidecar = sidecar_path.display(),
4315        log = reduction_log_path.display(),
4316        source_id = source.session_id,
4317    )
4318}
4319
4320fn session_artifact(
4321    locator: &SessionLocator,
4322    session: &Session,
4323    target: TransferFormat,
4324) -> std::result::Result<SessionArtifact, ServiceError> {
4325    session_artifact_with_id(locator, session, target, None)
4326}
4327
4328fn session_artifact_with_id(
4329    locator: &SessionLocator,
4330    session: &Session,
4331    target: TransferFormat,
4332    target_session_id: Option<&str>,
4333) -> std::result::Result<SessionArtifact, ServiceError> {
4334    let format: SessionFormat = target.into();
4335    let diagonal = format.source() == session.meta.source;
4336    let has_appended_turns = session
4337        .imported_message_count
4338        .is_some_and(|imported| imported < session.messages.len());
4339    let content = if let Some(id) = target_session_id {
4340        if diagonal && format != SessionFormat::OpenCode {
4341            session
4342                .to_jsonl_spliced(format, Some(id))
4343                .map_err(operation)?
4344        } else {
4345            let mut rewritten = session.clone();
4346            rewritten.meta.session_id = Some(id.to_string());
4347            rewritten.to_jsonl(format).map_err(operation)?
4348        }
4349    } else if diagonal && session.raw_is_verbatim && !has_appended_turns {
4350        session.raw_verbatim()
4351    } else if diagonal {
4352        session.to_jsonl_spliced(format, None).map_err(operation)?
4353    } else {
4354        session.to_jsonl(format).map_err(operation)?
4355    };
4356    let stem = sanitize_filename(
4357        target_session_id
4358            .or(session.meta.session_id.as_deref())
4359            .unwrap_or(&locator.session_id),
4360    );
4361    let suggested_filename = if diagonal && target == TransferFormat::Grok {
4362        "chat_history.jsonl".to_string()
4363    } else if target == TransferFormat::Goose {
4364        format!("{stem}.goose.json")
4365    } else {
4366        format!("{stem}.{}.jsonl", target.id())
4367    };
4368    let mut files = vec![SessionArtifactFile {
4369        path: suggested_filename.clone(),
4370        content: content.clone(),
4371        role: ArtifactFileRole::Primary,
4372    }];
4373    if target == TransferFormat::ClaudeCode {
4374        let bundle_stem = Path::new(&suggested_filename)
4375            .file_stem()
4376            .and_then(|stem| stem.to_str())
4377            .unwrap_or(&stem);
4378        let mut child_paths = BTreeSet::new();
4379        for (index, subagent) in session.subagents.iter().enumerate() {
4380            let agent_id = subagent
4381                .meta
4382                .agent_id
4383                .as_deref()
4384                .map(|id| id.strip_prefix("agent-").unwrap_or(id))
4385                .map(sanitize_filename)
4386                .filter(|id| !id.is_empty())
4387                .unwrap_or_else(|| format!("subagent-{}", index + 1));
4388            let child_has_appended_turns = subagent
4389                .imported_message_count
4390                .is_some_and(|imported| imported < subagent.messages.len());
4391            let child_content = if target_session_id.is_none()
4392                && subagent.meta.source == SessionSource::ClaudeCode
4393                && subagent.raw_is_verbatim
4394                && !child_has_appended_turns
4395            {
4396                subagent.raw_verbatim()
4397            } else if subagent.meta.source == SessionSource::ClaudeCode {
4398                subagent
4399                    .to_jsonl_spliced(SessionFormat::ClaudeCode, target_session_id)
4400                    .map_err(operation)?
4401            } else {
4402                let mut child = subagent.clone();
4403                if let Some(id) = target_session_id {
4404                    child.meta.session_id = Some(id.to_string());
4405                }
4406                child
4407                    .to_jsonl(SessionFormat::ClaudeCode)
4408                    .map_err(operation)?
4409            };
4410            let path = format!("{bundle_stem}/subagents/agent-{agent_id}.jsonl");
4411            if !child_paths.insert(path.clone()) {
4412                return Err(ServiceError::Operation(format!(
4413                    "Claude subagent ids collide at artifact path `{path}`"
4414                )));
4415            }
4416            files.push(SessionArtifactFile {
4417                path,
4418                content: child_content,
4419                role: ArtifactFileRole::Subagent,
4420            });
4421        }
4422    }
4423    if diagonal && target == TransferFormat::Grok {
4424        append_grok_bundle_files(locator, "", ArtifactFileRole::Bundle, &mut files)?;
4425    }
4426    if !diagonal || !session.raw_is_verbatim {
4427        files.push(SessionArtifactFile {
4428            path: "recovery/source.supercode.jsonl".into(),
4429            content: session.to_native_jsonl(),
4430            role: ArtifactFileRole::SourceRecovery,
4431        });
4432        for (index, subagent) in session.subagents.iter().enumerate() {
4433            let id = subagent
4434                .meta
4435                .agent_id
4436                .as_deref()
4437                .map(sanitize_filename)
4438                .unwrap_or_else(|| format!("subagent-{}", index + 1));
4439            files.push(SessionArtifactFile {
4440                path: format!("recovery/subagents/{id}.supercode.jsonl"),
4441                content: subagent.to_native_jsonl(),
4442                role: ArtifactFileRole::SourceRecovery,
4443            });
4444        }
4445    }
4446    if !diagonal && session.meta.source == SessionSource::Grok {
4447        append_grok_bundle_files(
4448            locator,
4449            "recovery/grok/",
4450            ArtifactFileRole::SourceRecovery,
4451            &mut files,
4452        )?;
4453    }
4454    let (fidelity, residue) = if diagonal
4455        && target_session_id.is_none()
4456        && session.raw_is_verbatim
4457        && !has_appended_turns
4458    {
4459        (Fidelity::ByteLossless, Vec::new())
4460    } else if diagonal && !(target_session_id.is_some() && target == TransferFormat::OpenCode) {
4461        (
4462            Fidelity::ValueLossless,
4463            vec![if target_session_id.is_some() {
4464                "target identity was rewritten, so the artifact intentionally differs from source bytes".into()
4465            } else {
4466                "source storage was reconstructed as a native-value-equivalent export; original container bytes were not captured".into()
4467            }],
4468        )
4469    } else {
4470        (
4471            Fidelity::Semantic,
4472            vec!["target schema has no portable slot for every source-native record and metadata field".into()],
4473        )
4474    };
4475    Ok(SessionArtifact {
4476        source_harness: locator.harness.clone(),
4477        target_harness: target.id(),
4478        session_id: target_session_id
4479            .map(str::to_string)
4480            .or_else(|| session.meta.session_id.clone()),
4481        content,
4482        suggested_filename,
4483        files,
4484        fidelity,
4485        residue,
4486    })
4487}
4488
4489fn append_grok_bundle_files(
4490    locator: &SessionLocator,
4491    prefix: &str,
4492    role: ArtifactFileRole,
4493    files: &mut Vec<SessionArtifactFile>,
4494) -> std::result::Result<(), ServiceError> {
4495    let primary = locator.storage.path();
4496    if primary.file_name().and_then(|name| name.to_str()) != Some("chat_history.jsonl") {
4497        return Err(ServiceError::Operation(format!(
4498            "Grok bundle locator must name chat_history.jsonl, got {}",
4499            primary.display()
4500        )));
4501    }
4502    let parent = primary.parent().ok_or_else(|| {
4503        ServiceError::Operation("Grok chat_history.jsonl has no session directory".into())
4504    })?;
4505    for name in ["summary.json", "updates.jsonl"] {
4506        let path = parent.join(name);
4507        let metadata = match std::fs::symlink_metadata(&path) {
4508            Ok(metadata) => metadata,
4509            Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
4510            Err(error) => return Err(ServiceError::Operation(error.to_string())),
4511        };
4512        if metadata.file_type().is_symlink() || !metadata.is_file() {
4513            return Err(ServiceError::Operation(format!(
4514                "refusing non-regular Grok bundle member {}",
4515                path.display()
4516            )));
4517        }
4518        let content = std::fs::read_to_string(&path).map_err(|error| {
4519            ServiceError::Operation(format!(
4520                "Grok bundle member {} is not representable as UTF-8: {error}",
4521                path.display()
4522            ))
4523        })?;
4524        files.push(SessionArtifactFile {
4525            path: format!("{prefix}{name}"),
4526            content,
4527            role: match role {
4528                ArtifactFileRole::Bundle => ArtifactFileRole::Bundle,
4529                _ => ArtifactFileRole::SourceRecovery,
4530            },
4531        });
4532    }
4533    Ok(())
4534}
4535
4536fn handoff_artifact(
4537    locator: &SessionLocator,
4538    session: &Session,
4539    target: TransferFormat,
4540    cwd: &Path,
4541) -> std::result::Result<SessionArtifact, ServiceError> {
4542    if target != TransferFormat::Grok {
4543        let target_session_id = target_session_id(target);
4544        return session_artifact_with_id(locator, session, target, Some(&target_session_id));
4545    }
4546
4547    // Stock Grok's importer accepts Claude/Codex transcripts and materializes its own
4548    // multi-file session bundle. A synthesized Grok chat_history.jsonl alone is not a
4549    // resumable handoff because updates.jsonl is the authoritative restore log.
4550    let mut importable = session.clone();
4551    // The Claude importer validates sessionId as a UUID. Source harness identities
4552    // are not portable (OpenCode, for example, uses `ses_...`), and a handoff must
4553    // not overwrite an existing target session when the source already uses UUIDs.
4554    // Mint a distinct target identity and still bind the importer-returned ID at
4555    // launch time because the importer remains the authority on materialization.
4556    importable.meta.session_id = Some(target_session_id(TransferFormat::ClaudeCode));
4557    importable.meta.cwd = Some(if cwd.is_absolute() {
4558        cwd.to_path_buf()
4559    } else {
4560        std::env::current_dir()
4561            .map_err(|error| ServiceError::Operation(error.to_string()))?
4562            .join(cwd)
4563    });
4564    let content = importable
4565        .to_jsonl(SessionFormat::ClaudeCode)
4566        .map_err(operation)?;
4567    let stem = sanitize_filename(
4568        importable
4569            .meta
4570            .session_id
4571            .as_deref()
4572            .unwrap_or(&locator.session_id),
4573    );
4574    let suggested_filename = format!("{stem}.grok-import.claude-code.jsonl");
4575    Ok(SessionArtifact {
4576        source_harness: locator.harness.clone(),
4577        // This names the artifact's actual wire format. The requested handoff target
4578        // remains Grok; its official importer is the materialization boundary.
4579        target_harness: TransferFormat::ClaudeCode.id(),
4580        session_id: importable.meta.session_id.clone(),
4581        content: content.clone(),
4582        suggested_filename: suggested_filename.clone(),
4583        files: vec![SessionArtifactFile {
4584            path: suggested_filename,
4585            content,
4586            role: ArtifactFileRole::Primary,
4587        }],
4588        fidelity: Fidelity::Semantic,
4589        residue: vec!["Grok's stock importer accepts a Claude Code transcript, not a complete Grok updates/session bundle".into()],
4590    })
4591}
4592
4593fn target_session_id(target: TransferFormat) -> String {
4594    let uuid = generated_session_id();
4595    match target {
4596        TransferFormat::OpenCode => format!("ses_{}", uuid.replace('-', "")),
4597        TransferFormat::ClaudeCode
4598        | TransferFormat::Codex
4599        | TransferFormat::Pi
4600        | TransferFormat::Grok
4601        | TransferFormat::Gemini
4602        | TransferFormat::Goose
4603        | TransferFormat::Hermes => uuid,
4604    }
4605}
4606
4607fn sanitize_filename(value: &str) -> String {
4608    let value = value
4609        .chars()
4610        .map(|character| {
4611            if character.is_ascii_alphanumeric() || matches!(character, '-' | '_') {
4612                character
4613            } else {
4614                '-'
4615            }
4616        })
4617        .collect::<String>();
4618    let value = value.trim_matches('-');
4619    if value.is_empty() {
4620        "session".into()
4621    } else {
4622        value.chars().take(100).collect()
4623    }
4624}
4625
4626fn handoff_instructions(
4627    target: TransferFormat,
4628    session_id: &str,
4629    cwd: &Path,
4630) -> HandoffInstructions {
4631    let launch = |program: &str, arguments: Vec<String>| StructuredLaunch {
4632        cwd: cwd.to_path_buf(),
4633        program: program.into(),
4634        arguments,
4635        env: BTreeMap::new(),
4636    };
4637    match target {
4638        TransferFormat::ClaudeCode => HandoffInstructions {
4639            launch: launch("claude", vec!["--resume".into(), session_id.into()]),
4640            materialize: None,
4641            requires_materialization: true,
4642            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(),
4643        },
4644        TransferFormat::Hermes => HandoffInstructions {
4645            launch: launch("hermes", vec!["--resume".into(), session_id.into()]),
4646            materialize: None,
4647            requires_materialization: true,
4648            note: "Hand the artifact (a Codex rollout) to `hermes sessions import --from codex <file>` — `sessions.export --to hermes` does exactly that — and resume the id Hermes prints: Hermes mints its own id and writes its own store.".into(),
4649        },
4650        TransferFormat::Codex => HandoffInstructions {
4651            launch: launch("codex", vec!["resume".into(), session_id.into()]),
4652            materialize: None,
4653            requires_materialization: true,
4654            note: "Write the artifact into Codex's native rollout store before running the resume launch; Codex has no general transcript-import command.".into(),
4655        },
4656        TransferFormat::OpenCode => HandoffInstructions {
4657            launch: launch("opencode", vec!["--session".into(), session_id.into()]),
4658            materialize: Some(launch(
4659                "opencode",
4660                vec!["import".into(), "{artifact_path}".into()],
4661            )),
4662            requires_materialization: true,
4663            note: "Write the artifact to a file, run the materialize command with its path, then launch the imported session.".into(),
4664        },
4665        TransferFormat::Pi => HandoffInstructions {
4666            launch: launch("pi", vec!["--session".into(), "{artifact_path}".into()]),
4667            materialize: None,
4668            requires_materialization: true,
4669            note: "Write the artifact to a file and replace {artifact_path} in the launch arguments; Pi can resume that file directly.".into(),
4670        },
4671        TransferFormat::Grok => HandoffInstructions {
4672            launch: launch(
4673                "grok",
4674                vec![
4675                    "--resume".into(),
4676                    "{imported_session_id}".into(),
4677                    "--fork-session".into(),
4678                ],
4679            ),
4680            materialize: Some(launch(
4681                "grok",
4682                vec!["import".into(), "--json".into(), "{artifact_path}".into()],
4683            )),
4684            requires_materialization: true,
4685            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(),
4686        },
4687        TransferFormat::Gemini => HandoffInstructions {
4688            launch: launch(
4689                "gemini",
4690                vec!["--session-file".into(), "{artifact_path}".into()],
4691            ),
4692            materialize: None,
4693            requires_materialization: true,
4694            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(),
4695        },
4696        TransferFormat::Goose => HandoffInstructions {
4697            launch: launch(
4698                "goose",
4699                vec![
4700                    "session".into(),
4701                    "--resume".into(),
4702                    "--session-id".into(),
4703                    "{imported_session_id}".into(),
4704                ],
4705            ),
4706            materialize: Some(launch(
4707                "goose",
4708                vec!["session".into(), "import".into(), "{artifact_path}".into()],
4709            )),
4710            requires_materialization: true,
4711            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(),
4712        },
4713    }
4714}
4715
4716fn resume_launch(
4717    harness: &str,
4718    session_id: &str,
4719    cwd: &Path,
4720    policy: ResumePolicy,
4721) -> std::result::Result<StructuredLaunch, ServiceError> {
4722    let mut arguments = Vec::new();
4723    let program = match harness {
4724        HarnessId::GROK => {
4725            if matches!(policy, ResumePolicy::Yolo) {
4726                if crate::support::self_sandbox_supported() {
4727                    arguments.extend(["--sandbox".into(), "workspace".into()]);
4728                }
4729                arguments.push("--always-approve".into());
4730            }
4731            arguments.extend(["--resume".into(), session_id.into()]);
4732            "grok"
4733        }
4734        HarnessId::CODEX => {
4735            let cwd_key = serde_json::to_string(cwd.to_string_lossy().as_ref())
4736                .expect("a filesystem path always serializes as JSON text");
4737            arguments.extend([
4738                "-c".into(),
4739                "check_for_update_on_startup=false".into(),
4740                "-c".into(),
4741                format!("projects.{cwd_key}.trust_level=\"trusted\""),
4742            ]);
4743            if matches!(policy, ResumePolicy::Yolo) {
4744                arguments.extend([
4745                    "--dangerously-bypass-approvals-and-sandbox".into(),
4746                    "--dangerously-bypass-hook-trust".into(),
4747                ]);
4748            }
4749            arguments.extend(["resume".into(), session_id.into()]);
4750            "codex"
4751        }
4752        HarnessId::CLAUDE_CODE => {
4753            if matches!(policy, ResumePolicy::Yolo) {
4754                arguments.push("--dangerously-skip-permissions".into());
4755            }
4756            arguments.extend(["--resume".into(), session_id.into()]);
4757            "claude"
4758        }
4759        HarnessId::GEMINI => {
4760            if matches!(policy, ResumePolicy::Yolo) {
4761                arguments.push("--yolo".into());
4762            }
4763            arguments.extend(["--resume".into(), session_id.into()]);
4764            "gemini"
4765        }
4766        HarnessId::GOOSE => {
4767            arguments.extend([
4768                "session".into(),
4769                "--resume".into(),
4770                "--session-id".into(),
4771                session_id.into(),
4772            ]);
4773            "goose"
4774        }
4775        HarnessId::PI => {
4776            if matches!(policy, ResumePolicy::Yolo) {
4777                arguments.push("--approve".into());
4778            }
4779            arguments.extend(["--session".into(), session_id.into()]);
4780            "pi"
4781        }
4782        HarnessId::OPENCODE => {
4783            arguments.extend(["--session".into(), session_id.into()]);
4784            "opencode"
4785        }
4786        HarnessId::SUPERCODE => {
4787            if matches!(policy, ResumePolicy::Yolo) {
4788                arguments.push("--dangerous".into());
4789            }
4790            arguments.extend(["resume".into(), session_id.into()]);
4791            "supercode"
4792        }
4793        other => {
4794            return Err(ServiceError::InvalidParams(format!(
4795                "no structured resume launch is registered for harness `{other}`"
4796            )))
4797        }
4798    };
4799    Ok(StructuredLaunch {
4800        cwd: cwd.to_path_buf(),
4801        program: program.into(),
4802        arguments,
4803        env: BTreeMap::new(),
4804    })
4805}
4806
4807/// Stage the resolved gateway credential in a private (0600) file so the
4808/// bridge can read it via `--token-file` — the delivery the real `openclaw
4809/// acp` accepts. One stable file per endpoint (keyed by an address digest,
4810/// no secret material in the name), overwritten on every connect so files
4811/// never accumulate and a rotated token never goes stale on disk.
4812fn openclaw_gateway_token_file(address: &str, secret: &str) -> std::io::Result<PathBuf> {
4813    let digest = blake3::hash(address.as_bytes()).to_hex();
4814    let path = std::env::temp_dir().join(format!(
4815        "supercode-openclaw-gateway-token-{}",
4816        &digest.as_str()[..16]
4817    ));
4818    #[cfg(unix)]
4819    {
4820        use std::io::Write;
4821        use std::os::unix::fs::OpenOptionsExt;
4822        let mut file = std::fs::OpenOptions::new()
4823            .write(true)
4824            .create(true)
4825            .truncate(true)
4826            .mode(0o600)
4827            .open(&path)?;
4828        file.write_all(secret.as_bytes())?;
4829    }
4830    #[cfg(not(unix))]
4831    std::fs::write(&path, secret)?;
4832    Ok(path)
4833}
4834
4835/// Open a connect-mode descriptor: resolve the endpoint address and
4836/// credential from the harness's own config file and build the backend that
4837/// joins the already-running endpoint. Fails closed with a specific
4838/// diagnostic when the config cannot be resolved or the declared protocol has
4839/// no connect-capable client yet.
4840fn open_connect_descriptor(
4841    descriptor: &crate::HarnessSupportDescriptor,
4842    home: &Path,
4843) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
4844    let Some(connect) = &descriptor.runtime.connect_launch else {
4845        return Err(ServiceError::InvalidParams(format!(
4846            "harness `{}` has no registered connect-mode launch",
4847            descriptor.id.as_str()
4848        )));
4849    };
4850    let resolved = connect
4851        .resolve(home)
4852        .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
4853    match (descriptor.id.as_str(), connect.protocol.as_str()) {
4854        (HarnessId::OPENCODE, protocol) if protocol.starts_with("opencode-http") => {
4855            let mut backend = OpenCodeRuntimeBackend::connect(&resolved.address);
4856            if let Some(token) = resolved.auth {
4857                backend = backend.with_bearer(token);
4858            }
4859            Ok(Box::new(backend))
4860        }
4861        (HarnessId::OPENCLAW, protocol) if protocol.starts_with("acp") => {
4862            // OpenClaw's own `openclaw acp` binary is the gateway client: a
4863            // stdio ACP bridge that joins the RUNNING gateway at the resolved
4864            // endpoint. Blind-walk finding 2026-08-31: the real bridge does
4865            // NOT honor OPENCLAW_GATEWAY_TOKEN from the environment — the
4866            // credential must arrive via `--token-file` (never bare `--token`
4867            // on argv, where process listings could read it). The env var is
4868            // still set for older bridges that did read it. Requires openclaw
4869            // >= 2026.7: the 2026.2 bridge drops its gateway socket
4870            // mid-prompt and advertises no session resume (executed finding,
4871            // docs/interop/research/openclaw-acp-dialect-2026-08-30.json).
4872            let mut env = BTreeMap::new();
4873            let mut arguments = vec!["acp".into(), "--url".into(), resolved.address.clone()];
4874            if let Some(token) = resolved.auth {
4875                let token_path = openclaw_gateway_token_file(&resolved.address, token.secret())
4876                    .map_err(|error| {
4877                        ServiceError::UnsupportedAction(format!(
4878                            "could not stage the gateway credential for the bridge: {error}"
4879                        ))
4880                    })?;
4881                arguments.push("--token-file".into());
4882                arguments.push(token_path.to_string_lossy().into_owned());
4883                env.insert("OPENCLAW_GATEWAY_TOKEN".to_string(), token.secret().to_string());
4884            }
4885            // The bridge program comes from the descriptor's own default
4886            // launch (the compiled registry pins `openclaw`), so tests can
4887            // substitute an absolute mock-bridge path without touching
4888            // process-global state.
4889            let program = descriptor
4890                .runtime
4891                .default_launch
4892                .as_ref()
4893                .map(|launch| launch.program.clone())
4894                .unwrap_or_else(|| "openclaw".into());
4895            let launch = RuntimeLaunch {
4896                program,
4897                arguments,
4898                env,
4899            };
4900            Ok(Box::new(
4901                crate::AcpRuntimeBackend::new(descriptor.id.clone(), launch)
4902                    .with_resume_support(descriptor.runtime.capabilities.resume_session),
4903            ))
4904        }
4905        _ => Err(ServiceError::UnsupportedAction(format!(
4906            "connect-mode endpoint for `{}` speaks `{}`; joining it needs that protocol's gateway client",
4907            descriptor.id.as_str(),
4908            connect.protocol
4909        ))),
4910    }
4911}
4912
4913/// The registry's connect-mode launch for this harness, honored only when the
4914/// caller supplied neither an explicit launch nor a base URL.
4915fn registry_connect_descriptor(
4916    params: &RuntimeBackendParams,
4917) -> Option<crate::HarnessSupportDescriptor> {
4918    if params.launch.is_some() || params.base_url.is_some() {
4919        return None;
4920    }
4921    harness_support_registry()
4922        .harnesses
4923        .into_iter()
4924        .find(|descriptor| descriptor.id == params.harness)
4925        .filter(|descriptor| descriptor.runtime.connect_launch.is_some())
4926}
4927
4928fn service_home() -> std::result::Result<PathBuf, ServiceError> {
4929    std::env::var_os("HOME").map(PathBuf::from).ok_or_else(|| {
4930        ServiceError::UnsupportedAction(
4931            "connect-mode launches need HOME to locate the harness config".into(),
4932        )
4933    })
4934}
4935
4936/// The doors that open a runtime: each spawns or joins a program and waits on
4937/// that program's protocol handshake before it can answer.
4938pub const RUNTIME_OPEN_METHODS: &[&str] = &[
4939    "harness.v1.runtimes.start",
4940    "harness.v1.runtimes.resume",
4941    "harness.v1.runtimes.attach",
4942    "harness.v1.runtimes.attach_existing",
4943];
4944
4945/// How long a runtime gets to finish opening before its caller is answered an
4946/// error instead. A program that never speaks the protocol at all — the wrong
4947/// binary, a shim that prints usage and waits — never answers the handshake,
4948/// so the wait is unbounded without this.
4949pub const RUNTIME_OPEN_DEADLINE: Duration = Duration::from_secs(60);
4950
4951/// How long a control call on an ALREADY-open runtime — send input, interrupt,
4952/// steer, respond, close — gets before its caller is answered an error
4953/// instead. A live runtime answers these in milliseconds; a wedged one never
4954/// answers at all, and `close` is exactly what a caller reaches for when it
4955/// suspects that.
4956pub const RUNTIME_CONTROL_DEADLINE: Duration = Duration::from_secs(30);
4957
4958/// The doors whose work happens entirely OUTSIDE this service's state once
4959/// its state has been read: probing harnesses, couriering a message into a
4960/// live session, and performing a conversation verb through a harness's own
4961/// CLI / HTTP / store door. Every one of them waits on a child process or a
4962/// network peer. See [`HarnessSessionService::detach`].
4963pub const DETACHED_METHODS: &[&str] = &[
4964    "harness.v1.harnesses.list",
4965    "harness.v1.harnesses.probe",
4966    "harness.v1.sessions.message",
4967    "harness.v1.sessions.new",
4968    "harness.v1.sessions.reset",
4969    "harness.v1.sessions.archive",
4970    "harness.v1.sessions.delete",
4971];
4972
4973/// How long a request moved off a transport's loop gets before its caller is
4974/// answered an error instead. Each of these already bounds its own inner
4975/// waits (a probe's handshake, the courier's run); this is the backstop for
4976/// the ones that do not — a harness CLI that never exits — so no caller waits
4977/// forever on a detached task no one is watching.
4978pub const DETACHED_CALL_DEADLINE: Duration = Duration::from_secs(120);
4979
4980/// How long `sessions.discover` gets before its caller is answered an error
4981/// instead. Discovery reads each harness's own store, and a store on a cold
4982/// or unavailable mount answers at the filesystem's pace rather than its own.
4983///
4984/// Deliberately shorter than the clients' own request deadline (30s): the
4985/// server's answer names the store that did not answer, and it is only read
4986/// if it lands before the client stops listening.
4987pub const SESSION_DISCOVER_DEADLINE: Duration = Duration::from_secs(25);
4988
4989/// Bound one control call on an open runtime by [`RUNTIME_CONTROL_DEADLINE`],
4990/// naming the method and the bound when it blows.
4991async fn within_control_deadline<F: std::future::Future>(
4992    method: &str,
4993    call: F,
4994) -> std::result::Result<F::Output, ServiceError> {
4995    tokio::time::timeout(RUNTIME_CONTROL_DEADLINE, call)
4996        .await
4997        .map_err(|_| {
4998            ServiceError::Operation(format!(
4999                "`{method}` gave up after {}s: the runtime did not answer",
5000                RUNTIME_CONTROL_DEADLINE.as_secs()
5001            ))
5002        })
5003}
5004
5005/// One [`RUNTIME_OPEN_METHODS`] request, parsed but not yet started. See
5006/// [`HarnessSessionService::runtime_open`] for why it exists apart from
5007/// [`HarnessSessionService::handle_async`].
5008pub struct RuntimeOpen {
5009    id: Value,
5010    method: String,
5011    params: Value,
5012}
5013
5014impl RuntimeOpen {
5015    /// Do the waiting: spawn or join the program and complete its handshake,
5016    /// bounded by [`RUNTIME_OPEN_DEADLINE`]. Touches no service state, so this
5017    /// runs on any task.
5018    pub async fn open(self) -> OpenedRuntime {
5019        let Self { id, method, params } = self;
5020        let outcome = open_runtime(&method, params).await;
5021        OpenedRuntime { id, outcome }
5022    }
5023}
5024
5025/// The result of [`RuntimeOpen::open`], ready for
5026/// [`HarnessSessionService::finish_runtime_open`].
5027pub struct OpenedRuntime {
5028    id: Value,
5029    outcome: std::result::Result<OpenRuntime, ServiceError>,
5030}
5031
5032/// One detached request: the half that reads this service's state already
5033/// done, and the half that waits not yet started. See
5034/// [`HarnessSessionService::detach`] and
5035/// [`HarnessSessionService::detach_runtime`].
5036pub struct DetachedCall {
5037    id: Value,
5038    method: String,
5039    work: std::result::Result<Work, ServiceError>,
5040}
5041
5042impl DetachedCall {
5043    /// Do the waiting and answer. Runs on any task: whatever this call needed
5044    /// from the service was taken before it left.
5045    pub async fn run(self) -> DetachedAnswer {
5046        let Self { id, method, work } = self;
5047        match work {
5048            // A call holding a runtime is already bounded by
5049            // RUNTIME_CONTROL_DEADLINE, and its future OWNS that connection:
5050            // a second timeout around it would drop the connection mid-call
5051            // and take down a runtime its caller still has.
5052            Ok(Work::Runtime(work)) => {
5053                let (result, returned) = work.run().await;
5054                DetachedAnswer {
5055                    response: service_response(id, result),
5056                    returned,
5057                }
5058            }
5059            Ok(Work::Free(work)) => {
5060                let result = match tokio::time::timeout(DETACHED_CALL_DEADLINE, work.run()).await {
5061                    Ok(result) => result,
5062                    Err(_) => Err(ServiceError::Operation(format!(
5063                        "`{method}` gave up after {}s: the harness it waits on did not answer",
5064                        DETACHED_CALL_DEADLINE.as_secs()
5065                    ))),
5066                };
5067                DetachedAnswer {
5068                    response: service_response(id, result),
5069                    returned: None,
5070                }
5071            }
5072            Err(error) => DetachedAnswer {
5073                response: service_response(id, Err(error)),
5074                returned: None,
5075            },
5076        }
5077    }
5078}
5079
5080/// One detached call's complete answer, plus whatever it must hand back to
5081/// the service before that answer is written. See
5082/// [`HarnessSessionService::finish_detached`].
5083pub struct DetachedAnswer {
5084    response: Value,
5085    returned: Option<ReturnedRuntime>,
5086}
5087
5088impl DetachedAnswer {
5089    /// The caller's JSON-RPC response, for a transport that owns no service
5090    /// to give a borrowed connection back to.
5091    pub fn into_response(self) -> Value {
5092        self.response
5093    }
5094}
5095
5096/// A connection lent to a detached call, on its way back to the service that
5097/// owns it.
5098pub struct ReturnedRuntime {
5099    connection: String,
5100    runtime: Box<dyn RuntimeConnection>,
5101}
5102
5103/// The waiting half of one detached request: with nothing of the service's
5104/// in hand, or holding a connection the service lent out for the call.
5105enum Work {
5106    Free(DetachedWork),
5107    Runtime(RuntimeWork),
5108}
5109
5110/// The waiting half of one detached request that holds nothing of the
5111/// service's.
5112enum DetachedWork {
5113    /// Probe the selected harnesses: find their executables, ask each its
5114    /// version, and at `probe: handshake` start each one and complete its
5115    /// protocol handshake.
5116    Inventory(InventoryWork),
5117    /// Run the courier that delivers one message into a live session.
5118    Message(MessageSessionParams),
5119    /// Perform one conversation verb through the harness's own CLI, HTTP API,
5120    /// daemon socket, or supercode's own store.
5121    SessionMutation {
5122        verb: crate::SessionVerb,
5123        mutation: crate::SessionMutation,
5124    },
5125}
5126
5127impl DetachedWork {
5128    async fn run(self) -> std::result::Result<Value, ServiceError> {
5129        match self {
5130            Self::Inventory(work) => run_inventory(work).await,
5131            Self::Message(params) => {
5132                Ok(message_live_session(&params, &crate::claude_peer::ProcessCourierRunner).await)
5133            }
5134            Self::SessionMutation { verb, mutation } => {
5135                let outcome = run_session_mutation(verb, &mutation).await?;
5136                serde_json::to_value(outcome)
5137                    .map_err(|error| ServiceError::Operation(error.to_string()))
5138            }
5139        }
5140    }
5141}
5142
5143/// One detached call that holds a runtime connection for its whole run.
5144enum RuntimeWork {
5145    /// Tear down a runtime the service has already surrendered.
5146    Close {
5147        runtime: Box<dyn RuntimeConnection>,
5148        process_group: Option<u32>,
5149    },
5150    /// Type one live slash command through a borrowed connection, then give
5151    /// the connection back.
5152    LiveCommand {
5153        connection: String,
5154        runtime: Box<dyn RuntimeConnection>,
5155        verb: crate::SessionVerb,
5156        mutation: crate::SessionMutation,
5157        command: &'static str,
5158        session: String,
5159    },
5160}
5161
5162/// What one [`RuntimeWork`] answers with: the caller's result, and the
5163/// connection to give back when the call only borrowed one.
5164type RuntimeWorkAnswer = (
5165    std::result::Result<Value, ServiceError>,
5166    Option<ReturnedRuntime>,
5167);
5168
5169impl RuntimeWork {
5170    async fn run(self) -> RuntimeWorkAnswer {
5171        match self {
5172            Self::Close {
5173                runtime,
5174                process_group,
5175            } => (close_runtime(runtime, process_group).await, None),
5176            Self::LiveCommand {
5177                connection,
5178                mut runtime,
5179                verb,
5180                mutation,
5181                command,
5182                session,
5183            } => {
5184                let result =
5185                    type_live_command(runtime.as_mut(), verb, &mutation, command, session).await;
5186                (
5187                    result,
5188                    Some(ReturnedRuntime {
5189                        connection,
5190                        runtime,
5191                    }),
5192                )
5193            }
5194        }
5195    }
5196}
5197
5198/// Tear down a runtime already out of the service, within
5199/// [`RUNTIME_CONTROL_DEADLINE`].
5200async fn close_runtime(
5201    mut runtime: Box<dyn RuntimeConnection>,
5202    process_group: Option<u32>,
5203) -> std::result::Result<Value, ServiceError> {
5204    match within_control_deadline("harness.v1.runtimes.close", runtime.close()).await {
5205        Ok(result) => {
5206            result.map_err(operation)?;
5207            Ok(json!({"closed": true}))
5208        }
5209        Err(deadline) => {
5210            // Dropping the handle is not enough: the process that stopped
5211            // answering is held by a task parked on it, so nothing here runs
5212            // its Drop. Signal the group the graceful path would have
5213            // signalled, then say so.
5214            let killed = kill_runtime_process_group(process_group);
5215            drop(runtime);
5216            Ok(json!({
5217                "closed": true,
5218                "killed": killed,
5219                "detail": error_message(deadline),
5220            }))
5221        }
5222    }
5223}
5224
5225/// The conversation a live `sessions.new` / `sessions.reset` acts on: the one
5226/// the request named, or the runtime's own session.
5227fn live_session_name(runtime: &dyn RuntimeConnection, mutation: &crate::SessionMutation) -> String {
5228    mutation
5229        .session
5230        .clone()
5231        .filter(|value| !value.trim().is_empty())
5232        .unwrap_or_else(|| runtime.handle().runtime_id.clone())
5233}
5234
5235/// Type one harness slash command into a live session through the very same
5236/// `send_input` path a human's message takes, within
5237/// [`RUNTIME_CONTROL_DEADLINE`].
5238async fn type_live_command(
5239    runtime: &mut dyn RuntimeConnection,
5240    verb: crate::SessionVerb,
5241    mutation: &crate::SessionMutation,
5242    command: &str,
5243    session: String,
5244) -> std::result::Result<Value, ServiceError> {
5245    within_control_deadline(
5246        &format!("sessions.{}", verb.as_str()),
5247        runtime.send_input(RuntimeInput {
5248            text: command.to_string(),
5249            image_urls: Vec::new(),
5250        }),
5251    )
5252    .await?
5253    .map_err(operation)?;
5254    let outcome = crate::sessions_control::live_outcome(verb, mutation, command, session)
5255        .map_err(session_control_error)?;
5256    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
5257}
5258
5259/// A runtime that is up and whose handshake completed, with what the service
5260/// needs to take ownership of it.
5261enum OpenRuntime {
5262    /// supercode spawned this process, so it also hosts it: a frontend server,
5263    /// a live-runtime registration and a terminal launch of its own.
5264    Hosted {
5265        runtime: Box<dyn RuntimeConnection>,
5266        capabilities: crate::RuntimeCapabilities,
5267        workspace: PathBuf,
5268    },
5269    /// `attach_existing` joined a process supercode does not own. It is
5270    /// registered as a bare connection and hosts nothing.
5271    Joined { runtime: Box<dyn RuntimeConnection> },
5272}
5273
5274/// Open the runtime one [`RUNTIME_OPEN_METHODS`] request asks for, within
5275/// [`RUNTIME_OPEN_DEADLINE`]. The error a blown deadline answers names the
5276/// method and the bound, so a caller reads why it was cut loose instead of
5277/// waiting on a handshake that is never coming.
5278async fn open_runtime(
5279    method: &str,
5280    params: Value,
5281) -> std::result::Result<OpenRuntime, ServiceError> {
5282    match tokio::time::timeout(
5283        RUNTIME_OPEN_DEADLINE,
5284        open_runtime_unbounded(method, params),
5285    )
5286    .await
5287    {
5288        Ok(result) => result,
5289        Err(_) => Err(ServiceError::Operation(format!(
5290            "`{method}` gave up after {}s: the runtime never finished its protocol handshake",
5291            RUNTIME_OPEN_DEADLINE.as_secs()
5292        ))),
5293    }
5294}
5295
5296async fn open_runtime_unbounded(
5297    method: &str,
5298    params: Value,
5299) -> std::result::Result<OpenRuntime, ServiceError> {
5300    match method {
5301        "harness.v1.runtimes.start" => {
5302            let params = decode::<RuntimeStartParams>(params)?;
5303            let backend = runtime_backend(&params.backend)?;
5304            let capabilities = backend.capabilities();
5305            let workspace = params.cwd.clone();
5306            let runtime = backend
5307                .start(RuntimeStartRequest {
5308                    cwd: params.cwd,
5309                    launch: runtime_launch(&params.backend),
5310                    mcp_servers: params.mcp_servers,
5311                })
5312                .await
5313                .map_err(operation)?;
5314            Ok(OpenRuntime::Hosted {
5315                runtime,
5316                capabilities,
5317                workspace,
5318            })
5319        }
5320        "harness.v1.runtimes.resume" | "harness.v1.runtimes.attach" => {
5321            let params = decode::<RuntimeAttachParams>(params)?;
5322            let backend = runtime_backend(&params.backend)?;
5323            let capabilities = backend.capabilities();
5324            let workspace = params
5325                .cwd
5326                .clone()
5327                .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
5328            let runtime = backend
5329                .attach(RuntimeAttachRequest {
5330                    runtime_id: params.runtime_id,
5331                    cwd: params.cwd,
5332                    launch: runtime_launch(&params.backend),
5333                    mcp_servers: params.mcp_servers,
5334                })
5335                .await
5336                .map_err(operation)?;
5337            Ok(OpenRuntime::Hosted {
5338                runtime,
5339                capabilities,
5340                workspace,
5341            })
5342        }
5343        "harness.v1.runtimes.attach_existing" => {
5344            let params = decode::<RuntimeAttachParams>(params)?;
5345            let backend: Box<dyn RuntimeBackend> = match params
5346                .backend
5347                .base_url
5348                .as_deref()
5349                .and_then(|value| LiveRuntimeEndpoint::parse(value).ok())
5350            {
5351                Some(endpoint) => {
5352                    #[cfg(not(feature = "adapter-api"))]
5353                    {
5354                        let _ = endpoint;
5355                        return Err(ServiceError::UnsupportedAction(
5356                            "live HTTP attachment adapter is not compiled".into(),
5357                        ));
5358                    }
5359                    #[cfg(feature = "adapter-api")]
5360                    {
5361                        let workspace = params.cwd.clone().ok_or_else(|| {
5362                            ServiceError::InvalidParams(
5363                                "Supercode live attach requires the project cwd".into(),
5364                            )
5365                        })?;
5366                        let source = LiveRuntimeSource {
5367                            harness: params.backend.harness.as_str().to_string(),
5368                            session_id: params.runtime_id.clone(),
5369                            workspace,
5370                        };
5371                        let receipt = resolve_live_runtime(&endpoint, &source)
5372                            .map_err(|error| ServiceError::Operation(error.to_string()))?;
5373                        Box::new(SupercodeHttpRuntimeBackend::new(receipt))
5374                    }
5375                }
5376                None => runtime_backend(&params.backend)?,
5377            };
5378            let capabilities = backend.capabilities();
5379            if !capabilities.attach_existing_process {
5380                return Err(ServiceError::Operation(format!(
5381                    "{} cannot attach to an already-running process; use runtimes.resume for a persisted session",
5382                    backend.harness().as_str()
5383                )));
5384            }
5385            let runtime = backend
5386                .attach_existing(RuntimeAttachRequest {
5387                    runtime_id: params.runtime_id,
5388                    cwd: params.cwd,
5389                    launch: runtime_launch(&params.backend),
5390                    mcp_servers: params.mcp_servers,
5391                })
5392                .await
5393                .map_err(operation)?;
5394            Ok(OpenRuntime::Joined { runtime })
5395        }
5396        _ => Err(ServiceError::MethodNotFound),
5397    }
5398}
5399
5400/// Wrap one service outcome in its JSON-RPC 2.0 envelope.
5401fn service_response(id: Value, result: std::result::Result<Value, ServiceError>) -> Value {
5402    match result {
5403        Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
5404        Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
5405        Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
5406        Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
5407        Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
5408        Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
5409    }
5410}
5411
5412fn runtime_backend(
5413    params: &RuntimeBackendParams,
5414) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
5415    if let Some(descriptor) = registry_connect_descriptor(params) {
5416        return open_connect_descriptor(&descriptor, &service_home()?);
5417    }
5418    if params.protocol.as_deref() == Some("acp") {
5419        let launch = params
5420            .launch
5421            .clone()
5422            .or_else(|| {
5423                harness_support_registry()
5424                    .harnesses
5425                    .into_iter()
5426                    .find(|harness| harness.id == params.harness)
5427                    .filter(|harness| {
5428                        harness.runtime.implementation == ImplementationKind::GenericProtocol
5429                            && harness.runtime.protocol.starts_with("acp")
5430                    })
5431                    .and_then(|harness| harness.runtime.default_launch)
5432            })
5433            .ok_or_else(|| {
5434                ServiceError::InvalidParams(
5435                    "an ACP runtime requires `launch` unless the harness has a registered default"
5436                        .into(),
5437                )
5438            })?;
5439        let resume_session = harness_support_registry()
5440            .harnesses
5441            .into_iter()
5442            .find(|harness| harness.id == params.harness)
5443            .is_some_and(|harness| harness.runtime.capabilities.resume_session);
5444        return Ok(Box::new(
5445            AcpRuntimeBackend::new(params.harness.clone(), launch)
5446                .with_resume_support(resume_session),
5447        ));
5448    }
5449    let backend: Box<dyn RuntimeBackend> = match params.harness.as_str() {
5450        HarnessId::CODEX => Box::new(CodexRuntimeBackend::new()),
5451        HarnessId::CLAUDE_CODE => Box::new(ClaudeCodeRuntimeBackend::new()),
5452        HarnessId::PI => Box::new(PiRuntimeBackend::new()),
5453        HarnessId::OPENCODE => match &params.base_url {
5454            Some(url) => Box::new(OpenCodeRuntimeBackend::connect(url)),
5455            None => Box::new(OpenCodeRuntimeBackend::new()),
5456        },
5457        harness => {
5458            let descriptor = harness_support_registry()
5459                .harnesses
5460                .into_iter()
5461                .find(|descriptor| descriptor.id.as_str() == harness)
5462                .filter(|descriptor| {
5463                    descriptor.runtime.implementation == ImplementationKind::GenericProtocol
5464                        && descriptor.runtime.protocol.starts_with("acp")
5465                });
5466            let Some(descriptor) = descriptor else {
5467                return Err(ServiceError::InvalidParams(format!(
5468                    "no runtime adapter for harness `{harness}`; use protocol `acp` with a launch command"
5469                )));
5470            };
5471            let resume = descriptor.runtime.capabilities.resume_session;
5472            Box::new(
5473                AcpRuntimeBackend::new(
5474                    descriptor.id,
5475                    descriptor
5476                        .runtime
5477                        .default_launch
5478                        .expect("generic ACP registry entry includes its launch"),
5479                )
5480                .with_resume_support(resume),
5481            )
5482        }
5483    };
5484    Ok(backend)
5485}
5486
5487fn runtime_launch(params: &RuntimeBackendParams) -> Option<RuntimeLaunch> {
5488    if let Some(launch) = &params.launch {
5489        return Some(launch.clone());
5490    }
5491    if !matches!(params.policy, RuntimePolicy::Yolo) {
5492        return None;
5493    }
5494    let launch = match params.harness.as_str() {
5495        HarnessId::GROK => RuntimeLaunch {
5496            program: "grok".into(),
5497            arguments: {
5498                let mut arguments: Vec<String> = Vec::new();
5499                if crate::support::self_sandbox_supported() {
5500                    arguments.extend(["--sandbox".into(), "workspace".into()]);
5501                }
5502                arguments.extend([
5503                    "--always-approve".into(),
5504                    "agent".into(),
5505                    "--no-leader".into(),
5506                    "stdio".into(),
5507                ]);
5508                arguments
5509            },
5510            env: BTreeMap::from([("GROK_AGENT_DASHBOARD".into(), "0".into())]),
5511        },
5512        HarnessId::CODEX => RuntimeLaunch {
5513            program: "codex".into(),
5514            arguments: vec![
5515                "--dangerously-bypass-approvals-and-sandbox".into(),
5516                "--dangerously-bypass-hook-trust".into(),
5517                "app-server".into(),
5518            ],
5519            env: BTreeMap::new(),
5520        },
5521        HarnessId::CLAUDE_CODE => RuntimeLaunch {
5522            program: "claude".into(),
5523            arguments: vec![
5524                "--dangerously-skip-permissions".into(),
5525                "--print".into(),
5526                "--input-format".into(),
5527                "stream-json".into(),
5528                "--output-format".into(),
5529                "stream-json".into(),
5530                "--verbose".into(),
5531            ],
5532            env: BTreeMap::new(),
5533        },
5534        HarnessId::PI => RuntimeLaunch {
5535            program: "pi".into(),
5536            arguments: vec!["--approve".into(), "--mode".into(), "rpc".into()],
5537            env: BTreeMap::new(),
5538        },
5539        HarnessId::OPENCODE => RuntimeLaunch {
5540            program: "opencode".into(),
5541            arguments: vec!["serve".into()],
5542            env: BTreeMap::new(),
5543        },
5544        HarnessId::GEMINI => RuntimeLaunch {
5545            program: "gemini".into(),
5546            arguments: vec!["--acp".into(), "--yolo".into()],
5547            env: BTreeMap::new(),
5548        },
5549        HarnessId::GOOSE => RuntimeLaunch {
5550            program: "goose".into(),
5551            arguments: vec!["acp".into()],
5552            env: BTreeMap::new(),
5553        },
5554        HarnessId::SUPERCODE => RuntimeLaunch {
5555            program: "supercode".into(),
5556            arguments: vec!["acp".into(), "--dangerous".into()],
5557            env: BTreeMap::new(),
5558        },
5559        _ => return None,
5560    };
5561    Some(launch)
5562}
5563
5564/// Disposable harness state for a no-prompt readiness probe. Merely opening
5565/// several stock CLIs writes a session header or migrates configuration, so a
5566/// handshake must never point at the user's real home. Authentication files
5567/// are copied into the private temporary home; all writes disappear with the
5568/// guard after the connection closes.
5569struct IsolatedProbeHome {
5570    launch: RuntimeLaunch,
5571    root: PathBuf,
5572}
5573
5574impl IsolatedProbeHome {
5575    fn new(harness: &str, mut launch: RuntimeLaunch) -> std::io::Result<Self> {
5576        let root = std::env::temp_dir().join(format!(
5577            "supercode-harness-probe-{harness}-{}",
5578            generated_session_id()
5579        ));
5580        std::fs::create_dir_all(&root)?;
5581        set_private_dir_permissions(&root)?;
5582
5583        if let Some(source_home) = std::env::var_os("HOME").map(PathBuf::from) {
5584            for relative in probe_auth_files(harness) {
5585                copy_probe_file(&source_home, &root, relative)?;
5586            }
5587        }
5588        configure_isolated_probe_auth(harness, &root)?;
5589
5590        let root_text = root.to_string_lossy().into_owned();
5591        for (key, value) in [
5592            ("HOME", root_text.clone()),
5593            (
5594                "XDG_CACHE_HOME",
5595                root.join(".cache").to_string_lossy().into_owned(),
5596            ),
5597            (
5598                "XDG_CONFIG_HOME",
5599                root.join(".config").to_string_lossy().into_owned(),
5600            ),
5601            (
5602                "XDG_DATA_HOME",
5603                root.join(".local/share").to_string_lossy().into_owned(),
5604            ),
5605        ] {
5606            launch.env.insert(key.into(), value);
5607        }
5608        let scoped = match harness {
5609            HarnessId::CLAUDE_CODE => Some(("CLAUDE_CONFIG_DIR", root.join(".claude"))),
5610            HarnessId::CODEX => Some(("CODEX_HOME", root.join(".codex"))),
5611            HarnessId::GEMINI => Some(("GEMINI_CLI_HOME", root.clone())),
5612            HarnessId::GROK => Some(("GROK_HOME", root.join(".grok"))),
5613            HarnessId::PI => Some(("PI_CODING_AGENT_DIR", root.join(".pi/agent"))),
5614            HarnessId::SUPERCODE => Some(("SUPERCODE_HOME", root.join(".config/supercode"))),
5615            _ => None,
5616        };
5617        if let Some((key, value)) = scoped {
5618            launch
5619                .env
5620                .insert(key.into(), value.to_string_lossy().into_owned());
5621        }
5622        Ok(Self { launch, root })
5623    }
5624
5625    fn cleanup(&self) -> std::io::Result<()> {
5626        match std::fs::remove_dir_all(&self.root) {
5627            Ok(()) => Ok(()),
5628            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
5629            Err(error) => Err(error),
5630        }
5631    }
5632}
5633
5634impl Drop for IsolatedProbeHome {
5635    fn drop(&mut self) {
5636        let _ = self.cleanup();
5637    }
5638}
5639
5640fn probe_auth_files(harness: &str) -> &'static [&'static str] {
5641    match harness {
5642        HarnessId::CLAUDE_CODE => &[".claude/.credentials.json", ".claude.json"],
5643        // The gateway endpoint + token live in openclaw's own config; without
5644        // it the isolated probe dials the default endpoint unauthenticated
5645        // (PARITY-24 finding 2026-08-31).
5646        HarnessId::OPENCLAW => &[".openclaw/openclaw.json"],
5647        HarnessId::CODEX => &[".codex/auth.json"],
5648        HarnessId::GEMINI => &[
5649            ".gemini/google_accounts.json",
5650            ".gemini/oauth_creds.json",
5651            ".gemini/settings.json",
5652        ],
5653        HarnessId::GROK => &[".grok/auth.json", ".grok/config.toml"],
5654        HarnessId::OPENCODE => &[
5655            ".config/opencode/auth.json",
5656            ".local/share/opencode/auth.json",
5657        ],
5658        HarnessId::PI => &[".pi/agent/auth.json"],
5659        // Hermes keeps its provider selection in config.yaml, its OAuth
5660        // credential pool in auth.json, and API keys in .env; without them
5661        // the isolated probe sees "No LLM provider configured" for a
5662        // hermes that answers fine from the user's real home.
5663        HarnessId::HERMES => &[".hermes/config.yaml", ".hermes/auth.json", ".hermes/.env"],
5664        HarnessId::SUPERCODE => &[
5665            ".config/supercode/config.toml",
5666            ".config/supercode/credentials.toml",
5667        ],
5668        _ => &[],
5669    }
5670}
5671
5672fn copy_probe_file(source_home: &Path, probe_home: &Path, relative: &str) -> std::io::Result<()> {
5673    let source = source_home.join(relative);
5674    if !source.is_file() {
5675        return Ok(());
5676    }
5677    let destination = probe_home.join(relative);
5678    if let Some(parent) = destination.parent() {
5679        std::fs::create_dir_all(parent)?;
5680        set_private_dir_permissions(parent)?;
5681    }
5682    std::fs::copy(source, &destination)?;
5683    set_private_file_permissions(&destination)
5684}
5685
5686fn configure_isolated_probe_auth(harness: &str, probe_home: &Path) -> std::io::Result<()> {
5687    if harness != HarnessId::GEMINI {
5688        return Ok(());
5689    }
5690    let oauth = probe_home.join(".gemini/oauth_creds.json");
5691    if !oauth.is_file() {
5692        return Ok(());
5693    }
5694    let settings_path = probe_home.join(".gemini/settings.json");
5695    let mut settings = std::fs::read_to_string(&settings_path)
5696        .ok()
5697        .and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
5698        .unwrap_or_else(|| json!({}));
5699    settings["security"]["auth"]["selectedType"] = Value::String("oauth-personal".into());
5700    std::fs::write(
5701        &settings_path,
5702        serde_json::to_vec_pretty(&settings).map_err(std::io::Error::other)?,
5703    )?;
5704    set_private_file_permissions(&settings_path)
5705}
5706
5707#[cfg(unix)]
5708fn set_private_dir_permissions(path: &Path) -> std::io::Result<()> {
5709    use std::os::unix::fs::PermissionsExt;
5710    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
5711}
5712
5713#[cfg(not(unix))]
5714fn set_private_dir_permissions(_path: &Path) -> std::io::Result<()> {
5715    Ok(())
5716}
5717
5718#[cfg(unix)]
5719fn set_private_file_permissions(path: &Path) -> std::io::Result<()> {
5720    use std::os::unix::fs::PermissionsExt;
5721    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
5722}
5723
5724#[cfg(not(unix))]
5725fn set_private_file_permissions(_path: &Path) -> std::io::Result<()> {
5726    Ok(())
5727}
5728
5729fn find_executable(program: &str) -> Option<PathBuf> {
5730    let candidate = PathBuf::from(program);
5731    if candidate.components().count() > 1 {
5732        return candidate.is_file().then_some(candidate);
5733    }
5734    let path = std::env::var_os("PATH")?;
5735    for directory in std::env::split_paths(&path) {
5736        let candidate = directory.join(program);
5737        if candidate.is_file() {
5738            return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5739        }
5740        #[cfg(windows)]
5741        {
5742            for extension in ["exe", "cmd", "bat"] {
5743                let candidate = directory.join(format!("{program}.{extension}"));
5744                if candidate.is_file() {
5745                    return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5746                }
5747            }
5748        }
5749    }
5750    None
5751}
5752
5753async fn executable_version(executable: &Path) -> Option<String> {
5754    let mut command = tokio::process::Command::new(executable);
5755    command
5756        .arg("--version")
5757        .stdin(std::process::Stdio::null())
5758        .stdout(std::process::Stdio::piped())
5759        .stderr(std::process::Stdio::piped())
5760        .kill_on_drop(true);
5761    let output = tokio::time::timeout(Duration::from_secs(3), command.output())
5762        .await
5763        .ok()?
5764        .ok()?;
5765    let stdout = String::from_utf8_lossy(&output.stdout);
5766    let stderr = String::from_utf8_lossy(&output.stderr);
5767    stdout
5768        .lines()
5769        .chain(stderr.lines())
5770        .map(str::trim)
5771        .find(|line| !line.is_empty())
5772        .map(|line| truncate_text(line, 200))
5773}
5774
5775pub(crate) fn auth_evidence(harness: &str) -> bool {
5776    let env_names: &[&str] = match harness {
5777        HarnessId::CLAUDE_CODE => &["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
5778        HarnessId::CODEX => &["OPENAI_API_KEY"],
5779        HarnessId::OPENCODE => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5780        HarnessId::PI => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5781        HarnessId::GROK => &["XAI_API_KEY", "GROK_API_KEY"],
5782        HarnessId::GEMINI => &["GEMINI_API_KEY", "GOOGLE_API_KEY"],
5783        HarnessId::SUPERCODE => &["OPENROUTER_API_KEY"],
5784        _ => &[],
5785    };
5786    if env_names
5787        .iter()
5788        .any(|name| std::env::var_os(name).is_some_and(|value| !value.is_empty()))
5789    {
5790        return true;
5791    }
5792    let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
5793        return false;
5794    };
5795    let files: Vec<PathBuf> = match harness {
5796        HarnessId::CLAUDE_CODE => vec![home.join(".claude/.credentials.json")],
5797        HarnessId::CODEX => vec![home.join(".codex/auth.json")],
5798        HarnessId::OPENCODE => vec![
5799            home.join(".local/share/opencode/auth.json"),
5800            home.join(".config/opencode/auth.json"),
5801        ],
5802        HarnessId::PI => vec![home.join(".pi/agent/auth.json")],
5803        HarnessId::GROK => vec![home.join(".grok/auth.json")],
5804        HarnessId::GEMINI => vec![
5805            home.join(".gemini/oauth_creds.json"),
5806            home.join(".gemini/google_accounts.json"),
5807        ],
5808        HarnessId::SUPERCODE => vec![home.join(".config/supercode/credentials.toml")],
5809        HarnessId::HERMES => vec![home.join(".hermes/auth.json"), home.join(".hermes/.env")],
5810        _ => Vec::new(),
5811    };
5812    if files.into_iter().any(|path| {
5813        std::fs::metadata(path)
5814            .map(|metadata| metadata.is_file() && metadata.len() > 2)
5815            .unwrap_or(false)
5816    }) {
5817        return true;
5818    }
5819    // macOS keeps Claude Code's OAuth login in the Keychain, so
5820    // `.claude/.credentials.json` never exists there and the file probe above
5821    // reports a signed-in install as unauthenticated forever. A completed
5822    // login also writes an `oauthAccount` record into `~/.claude.json` on
5823    // every platform — file-based, prompt-free evidence (querying the
5824    // Keychain itself from an unsigned daemon can raise a UI prompt).
5825    if harness == HarnessId::CLAUDE_CODE {
5826        return std::fs::read_to_string(home.join(".claude.json"))
5827            .map(|text| text.contains("\"oauthAccount\""))
5828            .unwrap_or(false);
5829    }
5830    false
5831}
5832
5833fn looks_like_auth_error(message: &str) -> bool {
5834    let message = message.to_ascii_lowercase();
5835    [
5836        "auth",
5837        "login",
5838        "sign in",
5839        "sign-in",
5840        "credential",
5841        "unauthorized",
5842        "forbidden",
5843        "token",
5844    ]
5845    .iter()
5846    .any(|needle| message.contains(needle))
5847}
5848
5849fn unavailable_capabilities() -> crate::RuntimeCapabilities {
5850    crate::RuntimeCapabilities {
5851        start_session: false,
5852        resume_session: false,
5853        attach_existing_process: false,
5854        send_input: false,
5855        stream_events: false,
5856        interrupt: false,
5857        steer: false,
5858        respond_to_requests: false,
5859    }
5860}
5861
5862fn truncate_text(text: &str, max_chars: usize) -> String {
5863    let mut chars = text.chars();
5864    let truncated = chars.by_ref().take(max_chars).collect::<String>();
5865    if chars.next().is_some() {
5866        format!("{truncated}…")
5867    } else {
5868        truncated
5869    }
5870}
5871
5872/// The process group a runtime's own handle names, when it names one.
5873///
5874/// Every adapter that spawns a local process spawns it as its own group
5875/// leader (`Command::process_group(0)`), so the endpoint's pid IS the group
5876/// id. A runtime reached over HTTP, or one supercode joined rather than
5877/// spawned, names no group here and is left alone.
5878fn runtime_process_group(handle: &crate::RuntimeHandle) -> Option<u32> {
5879    match &handle.endpoint {
5880        crate::RuntimeEndpoint::LocalProcess { pid, .. } => *pid,
5881        crate::RuntimeEndpoint::Http { .. } => None,
5882    }
5883}
5884
5885/// SIGKILL a wedged runtime's whole process group, reporting whether there
5886/// was one to signal. This is the same group teardown a graceful `close`
5887/// performs; it runs here only when the graceful path blew its deadline,
5888/// because the task parked on the unanswered call still owns the process
5889/// handle and so no `Drop` of ours can reach it.
5890fn kill_runtime_process_group(process_group: Option<u32>) -> bool {
5891    match process_group {
5892        #[cfg(unix)]
5893        Some(pid) => {
5894            crate::lsp::kill_process_group(pid);
5895            true
5896        }
5897        #[cfg(not(unix))]
5898        Some(_) => false,
5899        None => false,
5900    }
5901}
5902
5903fn error_message(error: ServiceError) -> String {
5904    match error {
5905        ServiceError::InvalidParams(message)
5906        | ServiceError::Operation(message)
5907        | ServiceError::UnsupportedAction(message) => message,
5908        ServiceError::MethodNotFound => "runtime adapter is not available".into(),
5909        ServiceError::Sdk(error) => error.to_string(),
5910    }
5911}
5912
5913#[derive(Debug)]
5914enum ServiceError {
5915    InvalidParams(String),
5916    MethodNotFound,
5917    UnsupportedAction(String),
5918    Operation(String),
5919    Sdk(SdkError),
5920}
5921
5922fn sdk_error(operation: SdkOperation, error: ServiceError) -> SdkError {
5923    match error {
5924        ServiceError::InvalidParams(message) => {
5925            SdkError::new(SdkErrorCode::InvalidArgument, operation, message)
5926        }
5927        ServiceError::MethodNotFound | ServiceError::UnsupportedAction(_) => {
5928            SdkError::unsupported(operation)
5929        }
5930        ServiceError::Operation(message) => {
5931            let code = if message.contains("already in progress") {
5932                SdkErrorCode::Busy
5933            } else if message.contains("not supported by this runtime") {
5934                SdkErrorCode::UnsupportedAction
5935            } else if message.contains("unknown runtime connection") {
5936                SdkErrorCode::NotFound
5937            } else {
5938                SdkErrorCode::Execution
5939            };
5940            SdkError::new(code, operation, message)
5941        }
5942        ServiceError::Sdk(error) => error,
5943    }
5944}
5945
5946fn sdk_rpc_error(id: Value, error: &SdkError) -> Value {
5947    let error_code = error.code();
5948    let code = match error_code {
5949        SdkErrorCode::Unauthenticated => -32030,
5950        SdkErrorCode::Unauthorized => -32031,
5951        SdkErrorCode::ControllerRequired => -32032,
5952        SdkErrorCode::LeaseExpired => -32033,
5953        SdkErrorCode::InvalidArgument => -32602,
5954        SdkErrorCode::NotFound => -32004,
5955        SdkErrorCode::Busy => -32000,
5956        SdkErrorCode::UnsupportedAction => -32020,
5957        SdkErrorCode::Execution => -32002,
5958        SdkErrorCode::Transport => -32003,
5959    };
5960    json!({
5961        "jsonrpc": "2.0",
5962        "id": id,
5963        "error": {
5964            "code": code,
5965            "name": error_code,
5966            "operation": error.operation(),
5967            "message": error.to_string(),
5968        },
5969    })
5970}
5971
5972fn decode<T: for<'de> Deserialize<'de>>(value: Value) -> std::result::Result<T, ServiceError> {
5973    serde_json::from_value(value).map_err(|error| ServiceError::InvalidParams(error.to_string()))
5974}
5975
5976fn operation(error: impl Into<crate::Error>) -> ServiceError {
5977    let error = error.into();
5978    match error {
5979        crate::Error::Sdk(error) => ServiceError::Sdk(error),
5980        error => ServiceError::Operation(error.to_string()),
5981    }
5982}
5983
5984/// ORCH-12 `harness.v1.memory.show|search` params. `homes` is the same
5985/// storage-root override every read-only method accepts, so a caller can
5986/// point the read at a fixture home without touching the real ones.
5987#[derive(Debug, Clone, Deserialize, Default)]
5988#[serde(default)]
5989struct MemoryRequest {
5990    /// Harness whose store is read. Required.
5991    harness: Option<String>,
5992    /// The needle, required by `search`.
5993    query: Option<String>,
5994    /// Hermes profile, OpenClaw agent, or Claude Code project.
5995    profile: Option<String>,
5996    /// Claude Code session id selecting a project store (`show` only).
5997    session: Option<String>,
5998    /// Include each document's whole text (`show` only).
5999    full: bool,
6000    /// Treat `query` as a regular expression (`search` only).
6001    regex: bool,
6002    /// Working tree whose project store is read.
6003    cwd: Option<std::path::PathBuf>,
6004    /// Storage roots to read.
6005    homes: crate::HarnessHomes,
6006}
6007
6008/// Read the memory noun. A harness with no memory store fails with
6009/// `UnsupportedAction` (RPC `-32020`), never an empty list.
6010fn memory_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6011    let request = decode::<MemoryRequest>(params)?;
6012    let harness = request
6013        .harness
6014        .clone()
6015        .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6016    let to_service = |error: crate::memory::MemoryError| match error {
6017        crate::memory::MemoryError::UnsupportedHarness { .. }
6018        | crate::memory::MemoryError::SessionNotScoped { .. } => {
6019            ServiceError::UnsupportedAction(error.to_string())
6020        }
6021        other => ServiceError::InvalidParams(other.to_string()),
6022    };
6023    match method {
6024        "harness.v1.memory.show" => {
6025            let documents = crate::memory::show_memory(&crate::memory::MemoryQuery {
6026                harness,
6027                profile: request.profile,
6028                session: request.session,
6029                full: request.full,
6030                cwd: request.cwd,
6031                homes: request.homes,
6032            })
6033            .map_err(to_service)?;
6034            Ok(json!({
6035                "schema": crate::memory::MEMORY_SCHEMA,
6036                "documents": documents,
6037            }))
6038        }
6039        "harness.v1.memory.search" => {
6040            let query = request
6041                .query
6042                .ok_or_else(|| ServiceError::InvalidParams("`query` is required".into()))?;
6043            let matches = crate::memory::search_memory(&crate::memory::MemorySearchQuery {
6044                harness,
6045                query,
6046                profile: request.profile,
6047                regex: request.regex,
6048                cwd: request.cwd,
6049                homes: request.homes,
6050            })
6051            .map_err(to_service)?;
6052            Ok(json!({
6053                "schema": crate::memory::MEMORY_SCHEMA,
6054                "matches": matches,
6055            }))
6056        }
6057        _ => Err(ServiceError::MethodNotFound),
6058    }
6059}
6060
6061/// ORCH-10 `harness.v1.profiles.list|get` params. `homes` is the same
6062/// storage-root override every read-only method accepts, so a caller can
6063/// point the read at a fixture home without touching the real ones.
6064#[derive(Debug, Clone, Deserialize)]
6065#[serde(default)]
6066struct ProfilesQuery {
6067    /// Restrict the listing to one harness. `get` requires it.
6068    harness: Option<String>,
6069    /// Profile name, required by `get`.
6070    name: Option<String>,
6071    /// Storage roots to read.
6072    homes: crate::HarnessHomes,
6073}
6074
6075impl Default for ProfilesQuery {
6076    fn default() -> Self {
6077        Self {
6078            harness: None,
6079            name: None,
6080            homes: crate::HarnessHomes::default(),
6081        }
6082    }
6083}
6084
6085/// Read the profile noun. A harness with no profile concept fails with
6086/// `UnsupportedAction` (RPC `-32020`), never an empty list.
6087fn profiles_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6088    let query = decode::<ProfilesQuery>(params)?;
6089    let to_service = |error: crate::profiles::ProfileError| match error {
6090        crate::profiles::ProfileError::UnsupportedHarness { .. } => {
6091            ServiceError::UnsupportedAction(error.to_string())
6092        }
6093        crate::profiles::ProfileError::NotFound { .. } => {
6094            ServiceError::InvalidParams(error.to_string())
6095        }
6096    };
6097    match method {
6098        "harness.v1.profiles.list" => {
6099            let profiles = crate::profiles::list_profiles(&query.homes, query.harness.as_deref())
6100                .map_err(to_service)?;
6101            Ok(json!({
6102                "schema": crate::profiles::PROFILES_SCHEMA,
6103                "profiles": profiles,
6104            }))
6105        }
6106        "harness.v1.profiles.get" => {
6107            let harness = query
6108                .harness
6109                .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6110            let name = query
6111                .name
6112                .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6113            let profile =
6114                crate::profiles::get_profile(&query.homes, &harness, &name).map_err(to_service)?;
6115            Ok(json!({
6116                "schema": crate::profiles::PROFILES_SCHEMA,
6117                "profile": profile,
6118            }))
6119        }
6120        _ => Err(ServiceError::MethodNotFound),
6121    }
6122}
6123
6124/// ORCH-14 `harness.v1.channels.list|status` params, the same storage-root
6125/// override every read-only method accepts so a caller can point the read at
6126/// a fixture home without touching the real ones.
6127#[derive(Debug, Clone, Deserialize)]
6128#[serde(default)]
6129struct ChannelsQuery {
6130    /// Restrict the listing to one harness. `status` requires it.
6131    harness: Option<String>,
6132    /// Channel name, required by `status`.
6133    name: Option<String>,
6134    /// Storage roots to read.
6135    homes: crate::HarnessHomes,
6136}
6137
6138impl Default for ChannelsQuery {
6139    fn default() -> Self {
6140        Self {
6141            harness: None,
6142            name: None,
6143            homes: crate::HarnessHomes::default(),
6144        }
6145    }
6146}
6147
6148/// Read the channel noun. A harness with no channel concept fails with
6149/// `UnsupportedAction` (RPC `-32020`), never an empty list. No row carries a
6150/// token, key or secret — see `crate::channels` "Secrecy".
6151#[derive(Debug, Clone, Deserialize)]
6152#[serde(default)]
6153struct RoutesQuery {
6154    harness: Option<String>,
6155    /// Restrict to routes targeting one profile / agent.
6156    profile: Option<String>,
6157    homes: crate::HarnessHomes,
6158}
6159
6160impl Default for RoutesQuery {
6161    fn default() -> Self {
6162        Self {
6163            harness: None,
6164            profile: None,
6165            homes: crate::HarnessHomes::default(),
6166        }
6167    }
6168}
6169
6170#[derive(Debug, Clone, Deserialize)]
6171#[serde(default)]
6172struct TriggersQuery {
6173    harness: Option<String>,
6174    homes: crate::HarnessHomes,
6175}
6176
6177impl Default for TriggersQuery {
6178    fn default() -> Self {
6179        Self {
6180            harness: None,
6181            homes: crate::HarnessHomes::default(),
6182        }
6183    }
6184}
6185
6186fn triggers_call(params: Value) -> std::result::Result<Value, ServiceError> {
6187    let query = decode::<TriggersQuery>(params)?;
6188    let triggers = crate::triggers::list_triggers(&query.homes, query.harness.as_deref())
6189        .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6190    Ok(json!({
6191        "schema": crate::triggers::TRIGGERS_SCHEMA,
6192        "triggers": triggers,
6193    }))
6194}
6195
6196fn routes_call(params: Value) -> std::result::Result<Value, ServiceError> {
6197    let query = decode::<RoutesQuery>(params)?;
6198    let routes = crate::routes::list_routes(
6199        &query.homes,
6200        query.harness.as_deref(),
6201        query.profile.as_deref(),
6202    )
6203    .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6204    Ok(json!({
6205        "schema": crate::routes::ROUTES_SCHEMA,
6206        "routes": routes,
6207    }))
6208}
6209
6210fn channels_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6211    let query = decode::<ChannelsQuery>(params)?;
6212    let to_service = |error: crate::channels::ChannelError| match error {
6213        crate::channels::ChannelError::UnsupportedHarness { .. } => {
6214            ServiceError::UnsupportedAction(error.to_string())
6215        }
6216        crate::channels::ChannelError::NotFound { .. } => {
6217            ServiceError::InvalidParams(error.to_string())
6218        }
6219    };
6220    match method {
6221        "harness.v1.channels.list" => {
6222            let channels = crate::channels::list_channels(&query.homes, query.harness.as_deref())
6223                .map_err(to_service)?;
6224            Ok(json!({
6225                "schema": crate::channels::CHANNELS_SCHEMA,
6226                "channels": channels,
6227            }))
6228        }
6229        "harness.v1.channels.status" => {
6230            let harness = query
6231                .harness
6232                .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6233            let name = query
6234                .name
6235                .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6236            let channel = crate::channels::channel_status(&query.homes, &harness, &name)
6237                .map_err(to_service)?;
6238            Ok(json!({
6239                "schema": crate::channels::CHANNELS_SCHEMA,
6240                "channel": channel,
6241            }))
6242        }
6243        _ => Err(ServiceError::MethodNotFound),
6244    }
6245}
6246
6247fn rpc_error(id: Value, code: i64, message: &str) -> Value {
6248    json!({
6249        "jsonrpc": "2.0",
6250        "id": id,
6251        "error": {"code": code, "message": message},
6252    })
6253}
6254
6255#[cfg(test)]
6256mod tests {
6257    use super::*;
6258    use crate::{HarnessEvent, HarnessId, RuntimeEndpoint, RuntimeHandle, StorageLocator};
6259    use async_trait::async_trait;
6260    use std::io::Write;
6261    use std::path::PathBuf;
6262    use std::time::Instant;
6263
6264    #[test]
6265    fn indexed_claude_descriptor_keeps_the_live_peer_address() {
6266        let descriptor = SessionDescriptor {
6267            locator: SessionLocator {
6268                harness: HarnessId::new(HarnessId::CLAUDE_CODE),
6269                session_id: "live-session".into(),
6270                storage: StorageLocator::File {
6271                    path: PathBuf::from("/tmp/live-session.jsonl"),
6272                },
6273            },
6274            cwd: Some(PathBuf::from("/project")),
6275            title: None,
6276            preview_candidates: Vec::new(),
6277            latest_message_candidates: Vec::new(),
6278            updated_at_ms: Some(1),
6279            message_count: None,
6280            model: None,
6281            parent_session_id: None,
6282            child_session_count: 0,
6283            nouns: Default::default(),
6284        };
6285        let peer = crate::claude_peer::ClaudePeerSession {
6286            pid: 42,
6287            session_id: "live-session".into(),
6288            cwd: Some(PathBuf::from("/project")),
6289            name: "peer".into(),
6290            socket_path: PathBuf::from("/tmp/peer.sock"),
6291            status: Some(crate::claude_peer::ClaudePeerStatus::Busy),
6292            updated_at_ms: Some(1),
6293            version: Some("test".into()),
6294        };
6295
6296        let value = live_descriptor_value(&descriptor, &[peer]).unwrap();
6297        assert!(value["live_endpoint"]
6298            .as_str()
6299            .is_some_and(|endpoint| endpoint.starts_with("cc-peer:v1:42:peer:")));
6300    }
6301
6302    struct EndingRuntime {
6303        handle: RuntimeHandle,
6304        event: Option<HarnessEvent>,
6305        close_failures: usize,
6306    }
6307
6308    #[async_trait]
6309    impl RuntimeConnection for EndingRuntime {
6310        fn handle(&self) -> &RuntimeHandle {
6311            &self.handle
6312        }
6313
6314        async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
6315            unreachable!("ending runtime does not accept input")
6316        }
6317
6318        async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
6319            Ok(self.event.take())
6320        }
6321
6322        async fn interrupt(&mut self) -> crate::Result<()> {
6323            Ok(())
6324        }
6325
6326        async fn respond(&mut self, _request_id: Value, _response: Value) -> crate::Result<()> {
6327            Ok(())
6328        }
6329
6330        async fn close(&mut self) -> crate::Result<()> {
6331            if self.close_failures > 0 {
6332                self.close_failures -= 1;
6333                return Err(crate::Error::Other(
6334                    "cleanup temporarily unavailable".into(),
6335                ));
6336            }
6337            Ok(())
6338        }
6339    }
6340
6341    fn ending_runtime(event: Option<HarnessEvent>) -> Box<dyn RuntimeConnection> {
6342        Box::new(EndingRuntime {
6343            handle: RuntimeHandle {
6344                harness: HarnessId::from(HarnessId::CLAUDE_CODE),
6345                runtime_id: "ending-session".into(),
6346                endpoint: RuntimeEndpoint::LocalProcess {
6347                    pid: None,
6348                    command: vec!["ending-runtime".into()],
6349                    protocol: "test".into(),
6350                },
6351            },
6352            event,
6353            close_failures: 0,
6354        })
6355    }
6356
6357    #[tokio::test]
6358    async fn closing_a_runtime_surrenders_the_connection_even_when_teardown_fails() {
6359        let mut service = HarnessSessionService::new();
6360        let handle = ending_runtime(None).handle().clone();
6361        let runtime_id = handle.runtime_id.clone();
6362        let opened = service
6363            .insert_runtime(Box::new(EndingRuntime {
6364                handle,
6365                event: None,
6366                close_failures: 1,
6367            }))
6368            .unwrap();
6369        let connection = opened["connection"].as_str().unwrap().to_string();
6370        service.terminal_launches.insert(
6371            connection.clone(),
6372            StructuredLaunch {
6373                cwd: PathBuf::from("/fixture"),
6374                program: "fixture".into(),
6375                arguments: Vec::new(),
6376                env: BTreeMap::new(),
6377            },
6378        );
6379        let first = service
6380            .handle_async(request(
6381                1,
6382                "harness.v1.runtimes.close",
6383                json!({"connection": connection}),
6384            ))
6385            .await;
6386        // The harness's own teardown failed and the caller is told so...
6387        assert!(first.get("error").is_some(), "{first}");
6388        // ...but the connection is gone all the same. A connection whose close
6389        // cannot complete is exactly the one that must not stay registered:
6390        // holding it would answer every later call on this node with a turn
6391        // that is never going to end.
6392        assert!(!service.runtimes.contains_key(&connection));
6393        assert!(!service.terminal_launches.contains_key(&connection));
6394        assert!(!service.runtime_sequences.contains_key(&runtime_id));
6395        let again = service
6396            .handle_async(request(
6397                2,
6398                "harness.v1.runtimes.close",
6399                json!({"connection": connection}),
6400            ))
6401            .await;
6402        assert_eq!(again["error"]["code"], -32602, "{again}");
6403    }
6404
6405    fn request(id: u64, method: &str, params: Value) -> Value {
6406        json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params})
6407    }
6408
6409    // ---- ORCH-6: conversation nouns on `sessions.*` ----------------------
6410
6411    fn hermes_store() -> PathBuf {
6412        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/hermes_home/state.db")
6413    }
6414
6415    /// The discovery response for the Hermes fixture home, with the one
6416    /// machine-specific value (the absolute store path) replaced so the exact
6417    /// same JSON can be committed and replayed by the UI story.
6418    fn hermes_discovery(params: Value) -> Value {
6419        let mut response =
6420            HarnessSessionService::new().handle(request(1, "harness.v1.sessions.discover", params));
6421        let store = hermes_store().display().to_string();
6422        for session in response["result"]["sessions"]
6423            .as_array_mut()
6424            .expect("sessions array")
6425        {
6426            if session["locator"]["storage"]["path"] == json!(store) {
6427                session["locator"]["storage"]["path"] = json!("<fixtures>/hermes_home/state.db");
6428            }
6429            // `activity` reports a wall-clock observation instant, not a fact
6430            // about the session; it would make this response differ on every
6431            // call. The nouns under test are all session facts.
6432            session.as_object_mut().unwrap().remove("activity");
6433        }
6434        response["result"].take()
6435    }
6436
6437    fn hermes_query() -> Value {
6438        json!({
6439            "harnesses": ["hermes"],
6440            "homes": {"hermes": hermes_store()},
6441        })
6442    }
6443
6444    fn row<'a>(result: &'a Value, id: &str) -> &'a Value {
6445        result["sessions"]
6446            .as_array()
6447            .expect("sessions array")
6448            .iter()
6449            .find(|session| session["locator"]["session_id"] == json!(id))
6450            .unwrap_or_else(|| panic!("no discovered row for `{id}` in {result:#}"))
6451    }
6452
6453    #[test]
6454    fn orch6_discover_rows_carry_the_conversation_nouns() {
6455        let result = hermes_discovery(hermes_query());
6456
6457        // A Telegram DM: reached on a channel, no repo — the workspace IS the
6458        // channel (D2 precedence), and `main` is not a profile.
6459        let dm = row(&result, "tg-dm-1");
6460        assert_eq!(dm["trigger"], json!("channel"));
6461        assert_eq!(dm["surface"]["platform"], json!("telegram"));
6462        assert_eq!(dm["surface"]["kind"], json!("dm"));
6463        assert_eq!(dm["surface"]["chat_id"], json!("123456"));
6464        assert_eq!(dm["surface"]["participant_id"], json!("u1"));
6465        assert_eq!(
6466            dm["workspace"],
6467            json!({"kind": "channel", "value": "telegram:123456"})
6468        );
6469        assert!(dm.get("profile").is_none(), "{dm:#}");
6470
6471        // A cron fire: recurring, with the job recovered from the minted id.
6472        let fire = row(&result, "cron_job42_20260902_120000");
6473        assert_eq!(fire["trigger"], json!("cron"));
6474        assert_eq!(
6475            fire["recurrence"],
6476            json!({"job_id": "job42", "kind": "cron"})
6477        );
6478        assert_eq!(fire["workspace"]["kind"], json!("repo"));
6479
6480        // A profiled group session with a pending handoff: repo workspace
6481        // wins over the channel, and the chat stays on the surface key.
6482        let coder = row(&result, "tg-coder-1");
6483        assert_eq!(coder["trigger"], json!("channel"));
6484        assert_eq!(coder["profile"], json!("coder"));
6485        assert_eq!(coder["surface"]["thread_id"], json!("55"));
6486        assert_eq!(
6487            coder["surface"]["key"],
6488            json!("agent:coder:telegram:group:-100777:55")
6489        );
6490        assert_eq!(
6491            coder["workspace"],
6492            json!({"kind": "repo", "value": "/workspace/project"})
6493        );
6494        assert_eq!(
6495            coder["cross_surface"],
6496            json!({"state": "pending", "platform": "discord"})
6497        );
6498
6499        // A plain ACP session stays human-triggered with no surface at all.
6500        let acp = row(&result, "cef97234-e8e8-428a-99ab-e8fff4e7e613");
6501        assert_eq!(acp["trigger"], json!("human"));
6502        assert!(acp.get("surface").is_none(), "{acp:#}");
6503        assert_eq!(acp["workspace"], json!({"kind": "none"}));
6504    }
6505
6506    #[test]
6507    fn orch6_discover_filters_by_harness_and_profile() {
6508        let mut params = hermes_query();
6509        params["profile"] = json!("coder");
6510        let result = hermes_discovery(params);
6511        let ids: Vec<&str> = result["sessions"]
6512            .as_array()
6513            .expect("sessions array")
6514            .iter()
6515            .map(|session| session["locator"]["session_id"].as_str().unwrap())
6516            .collect();
6517        assert_eq!(ids, vec!["tg-coder-1"]);
6518
6519        // A profile no session is routed through returns nothing rather than
6520        // silently ignoring the filter.
6521        let mut missing = hermes_query();
6522        missing["profile"] = json!("nobody");
6523        assert_eq!(hermes_discovery(missing)["sessions"], json!([]));
6524
6525        // The harness filter is `harnesses`; an id no harness answers to is
6526        // an empty page, never every store on the box.
6527        let elsewhere = json!({"harnesses": ["codex"], "homes": {"codex": hermes_store()}});
6528        assert_eq!(hermes_discovery(elsewhere)["sessions"], json!([]));
6529    }
6530
6531    #[test]
6532    fn orch6_load_reports_the_same_nouns_as_discovery() {
6533        let mut service = HarnessSessionService::new();
6534        let loaded = service.handle(request(
6535            1,
6536            "harness.v1.sessions.load",
6537            json!({"locator": {
6538                "harness": "hermes",
6539                "session_id": "tg-coder-1",
6540                "storage": {"kind": "file", "path": hermes_store()},
6541            }}),
6542        ));
6543        let session = &loaded["result"]["session"];
6544        let discovered = hermes_discovery(hermes_query());
6545        let row = row(&discovered, "tg-coder-1");
6546        for noun in [
6547            "trigger",
6548            "surface",
6549            "profile",
6550            "recurrence",
6551            "cross_surface",
6552            "workspace",
6553        ] {
6554            assert_eq!(
6555                session[noun],
6556                row.get(noun).cloned().unwrap_or(Value::Null),
6557                "`{noun}` disagrees between sessions.load and sessions.discover"
6558            );
6559        }
6560    }
6561
6562    /// ORCH-10: the fixture homes, as the RPC's `homes` override. Hermes's
6563    /// home is named by its `state.db`; OpenClaw's is the state directory.
6564    fn profile_fixture_homes() -> Value {
6565        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6566        json!({
6567            "hermes": fixtures.join("hermes_home/state.db"),
6568            "openclaw": fixtures.join("openclaw_home"),
6569        })
6570    }
6571
6572    fn profile_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6573        response["result"]["profiles"]
6574            .as_array()
6575            .unwrap_or_else(|| panic!("no profiles array in {response}"))
6576            .iter()
6577            .find(|row| row["harness"] == harness && row["name"] == name)
6578            .unwrap_or_else(|| panic!("no `{harness}` profile `{name}` in {response}"))
6579    }
6580
6581    /// dev/01: every source answers in one row shape, over the committed
6582    /// fixture homes — the Hermes profile directory and its `state.db`
6583    /// partition, the OpenClaw agent directories and `openclaw.json`, and
6584    /// supercode's own presets.
6585    #[test]
6586    fn profiles_list_reads_every_source_uniformly() {
6587        let mut service = HarnessSessionService::new();
6588        let response = service.handle(request(
6589            1,
6590            "harness.v1.profiles.list",
6591            json!({"homes": profile_fixture_homes()}),
6592        ));
6593        assert_eq!(
6594            response["result"]["schema"],
6595            crate::profiles::PROFILES_SCHEMA
6596        );
6597
6598        let default = profile_row(&response, "hermes", "default");
6599        assert_eq!(default["kind"], "hermes_profile");
6600        assert_eq!(default["default"], true);
6601        assert_eq!(default["routes"], 0);
6602        assert_eq!(default["sessions"], 11);
6603        assert_eq!(default["model"], "anthropic/claude-sonnet-4-5");
6604
6605        let coder = profile_row(&response, "hermes", "coder");
6606        assert_eq!(coder["kind"], "hermes_profile");
6607        assert_eq!(coder["default"], false);
6608        assert_eq!(coder["routes"], 1, "gateway.profile_routes targets coder");
6609        assert_eq!(coder["sessions"], 1, "state.db profile_name = 'coder'");
6610        assert_eq!(coder["model"], "anthropic/claude-opus-4-8");
6611        assert!(coder["home"]
6612            .as_str()
6613            .unwrap()
6614            .ends_with("hermes_home/profiles/coder"));
6615
6616        let main = profile_row(&response, "openclaw", "main");
6617        assert_eq!(main["kind"], "openclaw_agent");
6618        // No entry declares `default: true` (real configs do not), so `main`
6619        // wins on OpenClaw's own convention rather than alphabetically.
6620        assert_eq!(main["default"], true);
6621        assert_eq!(main["routes"], 0);
6622        assert_eq!(main["sessions"], 4);
6623        assert_eq!(
6624            main["model"],
6625            Value::Null,
6626            "`agents.defaults.model` is an install default, not this agent's pin"
6627        );
6628
6629        let design = profile_row(&response, "openclaw", "design");
6630        assert_eq!(design["default"], false);
6631        assert_eq!(design["routes"], 1, "one binding names agentId `design`");
6632        assert_eq!(design["sessions"], 0);
6633        assert_eq!(design["model"], "anthropic/claude-opus-4-8");
6634
6635        let preset = profile_row(&response, "supercode", "supercode-default");
6636        assert_eq!(preset["kind"], "preset");
6637        assert_eq!(preset["default"], true);
6638        assert_eq!(preset["home"], Value::Null);
6639        assert_eq!(preset["routes"], Value::Null);
6640    }
6641
6642    /// Codex's own profiles are `[profiles.<name>]` tables, with the
6643    /// top-level `profile` key naming the default.
6644    #[test]
6645    fn profiles_list_reads_codex_profile_tables() {
6646        let codex_home = std::env::temp_dir().join(format!(
6647            "supercode-orch10-codex-{}-{}",
6648            std::process::id(),
6649            std::time::SystemTime::now()
6650                .duration_since(std::time::UNIX_EPOCH)
6651                .unwrap()
6652                .as_nanos()
6653        ));
6654        std::fs::create_dir_all(codex_home.join("sessions")).unwrap();
6655        std::fs::write(
6656            codex_home.join("config.toml"),
6657            "profile = \"review\"\n\n[profiles.review]\nmodel = \"gpt-5.1-codex\"\n\n[profiles.fast]\nmodel = \"gpt-5.1-codex-mini\"\n",
6658        )
6659        .unwrap();
6660
6661        let mut service = HarnessSessionService::new();
6662        let response = service.handle(request(
6663            1,
6664            "harness.v1.profiles.list",
6665            json!({"harness": "codex", "homes": {"codex": codex_home.join("sessions")}}),
6666        ));
6667        let rows = response["result"]["profiles"].as_array().unwrap();
6668        assert_eq!(rows.len(), 2, "{response}");
6669        let review = profile_row(&response, "codex", "review");
6670        assert_eq!(review["kind"], "codex_profile");
6671        assert_eq!(review["default"], true);
6672        assert_eq!(review["model"], "gpt-5.1-codex");
6673        assert_eq!(review["home"], Value::Null);
6674        assert_eq!(profile_row(&response, "codex", "fast")["default"], false);
6675
6676        let got = service.handle(request(
6677            2,
6678            "harness.v1.profiles.get",
6679            json!({
6680                "harness": "codex",
6681                "name": "fast",
6682                "homes": {"codex": codex_home.join("sessions")},
6683            }),
6684        ));
6685        assert_eq!(got["result"]["profile"]["model"], "gpt-5.1-codex-mini");
6686        std::fs::remove_dir_all(&codex_home).ok();
6687    }
6688
6689    /// A verb a harness lacks fails with `UnsupportedAction`, never a silent
6690    /// empty list; an unknown name is an invalid argument, not an empty row.
6691    #[test]
6692    fn profiles_refuse_harnesses_without_the_concept() {
6693        let mut service = HarnessSessionService::new();
6694        let response = service.handle(request(
6695            1,
6696            "harness.v1.profiles.list",
6697            json!({"harness": "claude-code"}),
6698        ));
6699        assert_eq!(response["error"]["code"], -32020, "{response}");
6700
6701        let missing = service.handle(request(
6702            2,
6703            "harness.v1.profiles.get",
6704            json!({
6705                "harness": "hermes",
6706                "name": "no-such-profile",
6707                "homes": profile_fixture_homes(),
6708            }),
6709        ));
6710        assert_eq!(missing["error"]["code"], -32602, "{missing}");
6711    }
6712
6713    /// The two methods are advertised, so a client discovers them from
6714    /// `harness.v1.capabilities` rather than from documentation.
6715    #[test]
6716    fn profiles_methods_are_advertised() {
6717        let mut service = HarnessSessionService::new();
6718        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6719        let methods = response["result"]["methods"].as_array().unwrap();
6720        for method in ["harness.v1.profiles.list", "harness.v1.profiles.get"] {
6721            assert!(
6722                methods.iter().any(|entry| entry == method),
6723                "{method} is not advertised"
6724            );
6725        }
6726    }
6727
6728    // -----------------------------------------------------------------
6729    // ORCH-14 — channels
6730    // -----------------------------------------------------------------
6731
6732    fn channel_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6733        response["result"]["channels"]
6734            .as_array()
6735            .unwrap_or_else(|| panic!("no channels array in {response}"))
6736            .iter()
6737            .find(|row| row["harness"] == harness && row["name"] == name)
6738            .unwrap_or_else(|| panic!("no `{harness}` channel `{name}` in {response}"))
6739    }
6740
6741    fn channels_list(harness: Option<&str>) -> Value {
6742        let mut params = json!({"homes": profile_fixture_homes()});
6743        if let Some(harness) = harness {
6744            params["harness"] = json!(harness);
6745        }
6746        HarnessSessionService::new().handle(request(1, "harness.v1.channels.list", params))
6747    }
6748
6749    /// dev/01: both sources answer in one row shape over the committed
6750    /// fixture homes — Hermes's `platforms:` blocks with their `extra` maps,
6751    /// and OpenClaw's `channels.<name>` entries split per account.
6752    #[test]
6753    fn channels_list_reads_both_gateway_harnesses_uniformly() {
6754        let response = channels_list(None);
6755        assert_eq!(
6756            response["result"]["schema"],
6757            crate::channels::CHANNELS_SCHEMA
6758        );
6759
6760        // Hermes: a credentialed platform, a bridged `extra.key` platform,
6761        // and one the config explicitly disables.
6762        let telegram = channel_row(&response, "hermes", "telegram");
6763        assert_eq!(telegram["kind"], "telegram");
6764        assert_eq!(telegram["enabled"], true);
6765        assert_eq!(telegram["configured"], true);
6766        // The `sessions` count is the discovery rows whose surface platform
6767        // is telegram: the fixture's `agent:main:telegram:…` DM and the
6768        // `agent:coder:telegram:…` group.
6769        assert_eq!(telegram["sessions"], 2);
6770        let api = channel_row(&response, "hermes", "api_server");
6771        assert_eq!(api["configured"], true, "extra.key is a credential key");
6772        assert_eq!(api["sessions"], 0);
6773        let webhook = channel_row(&response, "hermes", "webhook");
6774        assert_eq!(webhook["enabled"], false);
6775        // Hermes lists no credential for `webhook`: declaring it is all it
6776        // needs, so a credential-less entry is still `configured`.
6777        assert_eq!(webhook["configured"], true);
6778
6779        // OpenClaw: one row per account, named `<channel>/<accountId>`.
6780        let linked = channel_row(&response, "openclaw", "slack/T0FIXTURE");
6781        assert_eq!(linked["kind"], "slack");
6782        assert_eq!(linked["account"], "T0FIXTURE");
6783        assert_eq!(linked["enabled"], true);
6784        assert_eq!(linked["configured"], true);
6785        let unlinked = channel_row(&response, "openclaw", "slack/T1FIXTURE");
6786        assert_eq!(unlinked["enabled"], false);
6787        assert_eq!(
6788            unlinked["configured"], false,
6789            "an account with no credential key is not configured"
6790        );
6791        // A single-account channel keeps its own name and names its account
6792        // inline.
6793        let telegram = channel_row(&response, "openclaw", "telegram");
6794        assert_eq!(telegram["account"], "hermes-fixture-bot");
6795        assert_eq!(telegram["configured"], true);
6796
6797        // `status` is never claimed from a config file.
6798        for row in response["result"]["channels"].as_array().unwrap() {
6799            assert_eq!(row["status"], "unknown", "{row}");
6800        }
6801    }
6802
6803    /// dev/01: no field of any emitted row carries a credential. The fixture
6804    /// homes hold four FAKE credential strings; a row that leaked one — as a
6805    /// value, an account label, or a name — fails here.
6806    #[test]
6807    fn channels_rows_never_carry_a_fixture_secret() {
6808        let secrets = [
6809            "FAKE-TOKEN-DO-NOT-EMIT",
6810            "FAKE-API-SERVER-KEY-DO-NOT-EMIT",
6811            "FAKE-SLACK-BOT-TOKEN-DO-NOT-EMIT",
6812            "FAKE-SLACK-APP-TOKEN-DO-NOT-EMIT",
6813            "FAKE-TELEGRAM-TOKEN-DO-NOT-EMIT",
6814        ];
6815        // The strings really are in the fixtures, so this test can fail.
6816        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6817        let raw = format!(
6818            "{}{}",
6819            std::fs::read_to_string(fixtures.join("hermes_home/config.yaml")).unwrap(),
6820            std::fs::read_to_string(fixtures.join("openclaw_home/openclaw.json")).unwrap(),
6821        );
6822        for secret in secrets {
6823            assert!(raw.contains(secret), "fixture no longer holds `{secret}`");
6824        }
6825
6826        let emitted = serde_json::to_string(&channels_list(None)["result"]).unwrap();
6827        for secret in secrets {
6828            assert!(
6829                !emitted.contains(secret),
6830                "`{secret}` leaked into a channel row: {emitted}"
6831            );
6832        }
6833        // Belt and braces: no row FIELD is credential-shaped either, so a
6834        // future field cannot smuggle one past the literal scan.
6835        for row in channels_list(None)["result"]["channels"]
6836            .as_array()
6837            .unwrap()
6838        {
6839            for key in row.as_object().unwrap().keys() {
6840                let key = key.to_ascii_lowercase();
6841                assert!(
6842                    !["token", "key", "secret", "password", "credential"]
6843                        .iter()
6844                        .any(|marker| key.ends_with(marker)),
6845                    "`{key}` is a credential-shaped field on a channel row"
6846                );
6847            }
6848        }
6849    }
6850
6851    /// `status` answers one row by name, and refuses an unknown one.
6852    #[test]
6853    fn channels_status_reads_one_row_by_name() {
6854        let mut service = HarnessSessionService::new();
6855        let got = service.handle(request(
6856            1,
6857            "harness.v1.channels.status",
6858            json!({
6859                "harness": "openclaw",
6860                "name": "slack/T0FIXTURE",
6861                "homes": profile_fixture_homes(),
6862            }),
6863        ));
6864        assert_eq!(got["result"]["channel"]["kind"], "slack");
6865        assert_eq!(got["result"]["channel"]["account"], "T0FIXTURE");
6866        assert_eq!(got["result"]["channel"]["status"], "unknown");
6867
6868        let missing = service.handle(request(
6869            2,
6870            "harness.v1.channels.status",
6871            json!({
6872                "harness": "openclaw",
6873                "name": "no-such-channel",
6874                "homes": profile_fixture_homes(),
6875            }),
6876        ));
6877        assert_eq!(missing["error"]["code"], -32602, "{missing}");
6878    }
6879
6880    /// A harness with no channel concept fails with `UnsupportedAction`,
6881    /// never a silent empty list — Claude Code included, because its channels
6882    /// are MCP-protocol declarations no config file names.
6883    #[test]
6884    fn channels_refuse_harnesses_without_the_concept() {
6885        let response = channels_list(Some("claude-code"));
6886        assert_eq!(response["error"]["code"], -32020, "{response}");
6887        let codex = channels_list(Some("codex"));
6888        assert_eq!(codex["error"]["code"], -32020, "{codex}");
6889    }
6890
6891    /// The harness filter restricts the rows rather than being ignored.
6892    #[test]
6893    fn channels_list_filters_by_harness() {
6894        let response = channels_list(Some("openclaw"));
6895        let rows = response["result"]["channels"].as_array().unwrap();
6896        assert!(!rows.is_empty(), "{response}");
6897        assert!(
6898            rows.iter().all(|row| row["harness"] == "openclaw"),
6899            "harness filter leaked: {response}"
6900        );
6901    }
6902
6903    /// Both methods are advertised, so a client discovers them from
6904    /// `harness.v1.capabilities` rather than from documentation.
6905    #[test]
6906    fn channels_methods_are_advertised() {
6907        let mut service = HarnessSessionService::new();
6908        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6909        let methods = response["result"]["methods"].as_array().unwrap();
6910        for method in ["harness.v1.channels.list", "harness.v1.channels.status"] {
6911            assert!(
6912                methods.iter().any(|entry| entry == method),
6913                "{method} is not advertised"
6914            );
6915        }
6916    }
6917
6918    /// The UI story renders REAL rows: this writes the discovery response the
6919    /// two assertions above pin into the fixture the Storybook
6920    /// `Compositions/Universal nouns` stories import, and fails when the
6921    /// committed copy has drifted from what the service now answers.
6922    #[test]
6923    fn orch6_story_fixture_matches_the_live_discovery_response() {
6924        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6925            .join("../../sdk/ui/stories/fixtures/hermes-discovery.json");
6926        let mut result = hermes_discovery(hermes_query());
6927        // `updated_at_ms` is derived from the fixture's own stored timestamps,
6928        // so the whole response is deterministic; drop only the cursor, which
6929        // is pagination state rather than a session fact.
6930        result.as_object_mut().unwrap().remove("next_cursor");
6931        let rendered = format!("{}\n", serde_json::to_string_pretty(&result).unwrap());
6932        if std::env::var_os("SUPERCODE_UPDATE_FIXTURES").is_some() {
6933            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
6934            std::fs::write(&path, &rendered).unwrap();
6935        }
6936        let committed = std::fs::read_to_string(&path).unwrap_or_default();
6937        assert_eq!(
6938            committed, rendered,
6939            "sdk/ui/stories/fixtures/hermes-discovery.json is stale — \
6940             re-run with SUPERCODE_UPDATE_FIXTURES=1"
6941        );
6942    }
6943
6944    fn pi_locator() -> SessionLocator {
6945        SessionLocator {
6946            harness: HarnessId::from(HarnessId::PI),
6947            session_id: "1e6f2a3b-0000-4000-8000-000000000001".into(),
6948            storage: StorageLocator::File {
6949                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6950                    .join("tests/fixtures/pi_session.jsonl"),
6951            },
6952        }
6953    }
6954
6955    fn opencode_locator() -> SessionLocator {
6956        let session_id = "ses_fixtureAAAAAAAAAAAAAAA1";
6957        SessionLocator {
6958            harness: HarnessId::from(HarnessId::OPENCODE),
6959            session_id: session_id.into(),
6960            storage: StorageLocator::Sqlite {
6961                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6962                    .join("tests/fixtures/opencode_fixture/opencode.db"),
6963                selector: session_id.into(),
6964            },
6965        }
6966    }
6967
6968    fn grok_locator() -> SessionLocator {
6969        SessionLocator {
6970            harness: HarnessId::from(HarnessId::GROK),
6971            session_id: "73c09283-4b33-41fa-90f1-0bcb0f7be523".into(),
6972            storage: StorageLocator::File {
6973                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6974                    .join("tests/fixtures/grok_session/chat_history.jsonl"),
6975            },
6976        }
6977    }
6978
6979    // ---- ORCH-11: `harness.v1.skills.list` -------------------------------
6980
6981    fn fixture_homes() -> Value {
6982        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6983        json!({
6984            "claude_code": fixtures.join("__absent__"),
6985            "codex": fixtures.join("__absent__"),
6986            "opencode": fixtures.join("__absent__"),
6987            "pi": fixtures.join("__absent__"),
6988            "agents": fixtures.join("__absent__"),
6989            "hermes": fixtures.join("hermes_home"),
6990            "openclaw": fixtures.join("openclaw_home"),
6991        })
6992    }
6993
6994    #[test]
6995    fn preview_search_uses_the_discovery_rpc_and_refuses_live_subscription() {
6996        let root = std::env::temp_dir().join(format!(
6997            "supercode-preview-rpc-{}-{}",
6998            std::process::id(),
6999            std::time::SystemTime::now()
7000                .duration_since(std::time::UNIX_EPOCH)
7001                .unwrap()
7002                .as_nanos()
7003        ));
7004        std::fs::create_dir_all(&root).unwrap();
7005        for id in ["first", "second"] {
7006            std::fs::write(root.join(format!("{id}.jsonl")), format!("{}\n{}\n",
7007                json!({"type": "session_meta", "payload": {"id": id, "cwd": "/workspace"}}),
7008                json!({"type": "event_msg", "payload": {"type": "agent_message", "message": "NEBULA result"}}),
7009            )).unwrap();
7010        }
7011        let mut service = HarnessSessionService::new();
7012        let query = json!({
7013            "harnesses": ["codex"], "homes": {"codex": root},
7014            "query": "nebula", "search_previews": true, "limit": 1
7015        });
7016        let first = service.handle(request(1, "harness.v1.sessions.discover", query.clone()));
7017        assert!(first.get("error").is_none(), "{first}");
7018        assert_eq!(first["result"]["receipt"]["searched_previews"], true);
7019        assert_eq!(first["result"]["receipt"]["total_matched"], 2);
7020        let mut next_query = query.clone();
7021        next_query["cursor"] = first["result"]["next_cursor"].clone();
7022        let next = service.handle(request(2, "harness.v1.sessions.discover", next_query));
7023        assert_eq!(next["result"]["receipt"]["returned"], 1);
7024        assert_eq!(next["result"]["receipt"]["total_matched"], 2);
7025        assert_eq!(next["result"]["receipt"]["truncated"], false);
7026        assert_ne!(
7027            first["result"]["sessions"][0]["locator"],
7028            next["result"]["sessions"][0]["locator"]
7029        );
7030        let refused = service.handle(request(3, "harness.v1.sessions.index.subscribe", query));
7031        assert!(
7032            refused["error"]["message"]
7033                .as_str()
7034                .unwrap()
7035                .contains("use sessions.discover"),
7036            "{refused}"
7037        );
7038        std::fs::remove_dir_all(root).unwrap();
7039    }
7040
7041    #[test]
7042    fn session_index_resize_preserves_subscription_and_rejects_invalid_requests() {
7043        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.sessions.index.resize"));
7044        let root = std::env::temp_dir().join(format!(
7045            "supercode-index-rpc-{}-{}",
7046            std::process::id(),
7047            std::time::SystemTime::now()
7048                .duration_since(std::time::UNIX_EPOCH)
7049                .unwrap()
7050                .as_nanos()
7051        ));
7052        std::fs::create_dir_all(&root).unwrap();
7053        for id in ["first", "second"] {
7054            std::fs::write(root.join(format!("{id}.jsonl")), format!(
7055                "{{\"type\":\"session_meta\",\"payload\":{{\"id\":\"{id}\",\"cwd\":\"/workspace\"}}}}\n"
7056            )).unwrap();
7057        }
7058        let mut service = HarnessSessionService::new();
7059        let opened = service.handle(request(
7060            1,
7061            "harness.v1.sessions.index.subscribe",
7062            json!({
7063                "harnesses": ["codex"], "homes": { "codex": root }, "limit": 1
7064            }),
7065        ));
7066        assert!(opened.get("error").is_none(), "{opened:#}");
7067        let subscription = opened["result"]["subscription"]
7068            .as_str()
7069            .unwrap()
7070            .to_owned();
7071        assert_eq!(opened["result"]["initial"].as_array().unwrap().len(), 1);
7072        for params in [
7073            json!({"subscription": subscription, "limit": 0}),
7074            json!({"subscription": subscription, "limit": 2049}),
7075            json!({"subscription": subscription, "limit": 2, "cursor": "not-allowed"}),
7076            json!({"subscription": "unknown", "limit": 2}),
7077        ] {
7078            let rejected = service.handle(request(2, "harness.v1.sessions.index.resize", params));
7079            assert_eq!(rejected["error"]["code"], -32602, "{rejected:#}");
7080        }
7081        for (limit, revision) in [(1, 1), (2, 2), (2, 2), (1, 3)] {
7082            let response = service.handle(request(
7083                3,
7084                "harness.v1.sessions.index.resize",
7085                json!({
7086                    "subscription": subscription, "limit": limit
7087                }),
7088            ));
7089            assert!(response.get("error").is_none(), "{response:#}");
7090            assert_eq!(response["result"]["subscription"], subscription);
7091            assert_eq!(response["result"]["revision"], revision);
7092            assert_eq!(
7093                response["result"]["initial"].as_array().unwrap().len(),
7094                limit
7095            );
7096            assert_eq!(response["result"]["receipt"]["total_matched"], 2);
7097            assert_eq!(service.index_subscriptions.len(), 1);
7098        }
7099        let removed = service.handle(request(
7100            4,
7101            "harness.v1.sessions.index.unsubscribe",
7102            json!({
7103                "subscription": subscription
7104            }),
7105        ));
7106        assert_eq!(removed["result"]["removed"], true);
7107        let stale = service.handle(request(
7108            5,
7109            "harness.v1.sessions.index.resize",
7110            json!({
7111                "subscription": subscription, "limit": 1
7112            }),
7113        ));
7114        assert_eq!(stale["error"]["code"], -32602);
7115        drop(service);
7116        std::fs::remove_dir_all(root).unwrap();
7117    }
7118
7119    fn skills_rows(params: Value) -> Vec<Value> {
7120        let response =
7121            HarnessSessionService::new().handle(request(1, "harness.v1.skills.list", params));
7122        assert!(response.get("error").is_none(), "{response:#}");
7123        response["result"].as_array().cloned().unwrap_or_default()
7124    }
7125
7126    /// The uniform row over two harnesses at once, from the harnesses' own
7127    /// skill roots: name, harness, scope, location, description, version.
7128    #[test]
7129    fn skills_list_reads_the_hermes_and_openclaw_roots() {
7130        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7131        let rows = skills_rows(json!({
7132            "homes": fixture_homes(),
7133            "cwd": fixtures.join("hermes_home"),
7134        }));
7135        let arxiv = rows
7136            .iter()
7137            .find(|row| row["name"] == json!("arxiv-search"))
7138            .unwrap_or_else(|| panic!("no arxiv row in {rows:#?}"));
7139        assert_eq!(arxiv["harness"], json!(HarnessId::HERMES));
7140        assert_eq!(arxiv["scope"], json!("user"));
7141        assert_eq!(arxiv["version"], json!("1.4.0"));
7142        assert!(arxiv["location"]
7143            .as_str()
7144            .unwrap()
7145            .ends_with("hermes_home/skills/research/arxiv"));
7146
7147        // A directory with no SKILL.md still lists, by directory name.
7148        let bare = rows
7149            .iter()
7150            .find(|row| row["name"] == json!("bare-skill"))
7151            .unwrap_or_else(|| panic!("no bare-skill row in {rows:#?}"));
7152        assert_eq!(bare["enabled"], json!(null));
7153        assert!(bare.get("description").is_none());
7154
7155        let demo = rows
7156            .iter()
7157            .find(|row| row["name"] == json!("clawhub-demo"))
7158            .unwrap_or_else(|| panic!("no clawhub-demo row in {rows:#?}"));
7159        assert_eq!(demo["harness"], json!(HarnessId::OPENCLAW));
7160        assert_eq!(demo["scope"], json!("managed"));
7161        assert_eq!(demo["enabled"], json!(false));
7162    }
7163
7164    /// Both filters select against the same rows.
7165    #[test]
7166    fn skills_list_filters_by_harness_and_scope() {
7167        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7168        let hermes = skills_rows(json!({
7169            "homes": fixture_homes(),
7170            "cwd": fixtures.join("hermes_home"),
7171            "harness": HarnessId::HERMES,
7172        }));
7173        assert!(!hermes.is_empty());
7174        assert!(hermes
7175            .iter()
7176            .all(|row| row["harness"] == json!(HarnessId::HERMES)));
7177
7178        let managed = skills_rows(json!({
7179            "homes": fixture_homes(),
7180            "cwd": fixtures.join("openclaw_home"),
7181            "harness": HarnessId::OPENCLAW,
7182            "scope": "managed",
7183        }));
7184        assert_eq!(managed.len(), 1, "{managed:#?}");
7185        assert_eq!(managed[0]["name"], json!("clawhub-demo"));
7186
7187        let bundled = skills_rows(json!({
7188            "homes": fixture_homes(),
7189            "cwd": fixtures.join("openclaw_home"),
7190            "harness": HarnessId::OPENCLAW,
7191            "scope": "bundled",
7192        }));
7193        assert!(bundled.is_empty(), "{bundled:#?}");
7194    }
7195
7196    /// A harness supercode has no skills root for is refused by name, not
7197    /// answered with an empty list.
7198    #[test]
7199    fn skills_list_refuses_an_unknown_harness() {
7200        let response = HarnessSessionService::new().handle(request(
7201            1,
7202            "harness.v1.skills.list",
7203            json!({"harness": "not-a-harness", "homes": fixture_homes()}),
7204        ));
7205        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7206        assert!(response["error"]["message"]
7207            .as_str()
7208            .unwrap()
7209            .contains("not-a-harness"));
7210    }
7211
7212    /// The method is advertised, and its SDK operation resolves it.
7213    #[test]
7214    fn skills_list_is_an_advertised_method_and_sdk_operation() {
7215        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.list"));
7216        assert_eq!(
7217            SdkOperation::from_method("harness.v1.skills.list"),
7218            Some(SdkOperation::SkillsList)
7219        );
7220    }
7221
7222    // ---- ORCH-22: `harness.v1.skills.install|remove` ----------------------
7223
7224    /// Both controlled verbs are advertised and resolve to their operation.
7225    #[test]
7226    fn skills_install_and_remove_are_advertised_methods_and_sdk_operations() {
7227        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.install"));
7228        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.remove"));
7229        assert_eq!(
7230            SdkOperation::from_method("harness.v1.skills.install"),
7231            Some(SdkOperation::SkillsInstall)
7232        );
7233        assert_eq!(
7234            SdkOperation::from_method("harness.v1.skills.remove"),
7235            Some(SdkOperation::SkillsRemove)
7236        );
7237    }
7238
7239    /// The directory door, end to end over the RPC: a local package lands in
7240    /// Claude Code's own user root and the outcome carries the operation and
7241    /// the row the ORCH-11 loader reads back.
7242    #[test]
7243    fn skills_install_and_remove_drive_the_directory_door() {
7244        let root = std::env::temp_dir().join(format!(
7245            "supercode-orch22-rpc-{}-{}",
7246            std::process::id(),
7247            std::time::SystemTime::now()
7248                .duration_since(std::time::UNIX_EPOCH)
7249                .unwrap()
7250                .as_nanos()
7251        ));
7252        let source = root.join("probe-src");
7253        std::fs::create_dir_all(&source).unwrap();
7254        std::fs::write(
7255            source.join("SKILL.md"),
7256            "---\nname: orch22-rpc\ndescription: a probe\n---\nbody\n",
7257        )
7258        .unwrap();
7259        let homes = json!({
7260            "claude_code": root.join("claude_home"),
7261            "codex": root.join("__absent__"),
7262            "opencode": root.join("__absent__"),
7263            "pi": root.join("__absent__"),
7264            "hermes": root.join("__absent__"),
7265            "openclaw": root.join("__absent__"),
7266            "agents": root.join("__absent__"),
7267        });
7268
7269        let mut service = HarnessSessionService::new();
7270        let installed = service.handle(request(
7271            1,
7272            "harness.v1.skills.install",
7273            json!({
7274                "harness": HarnessId::CLAUDE_CODE,
7275                "source": source,
7276                "scope": "user",
7277                "cwd": root,
7278                "homes": homes,
7279            }),
7280        ));
7281        let result = &installed["result"];
7282        assert_eq!(result["name"], json!("orch22-rpc"), "{installed:#}");
7283        assert_eq!(result["verb"], json!("install"));
7284        assert!(result["ran"]
7285            .as_str()
7286            .is_some_and(|ran| ran.starts_with("cp -R ")));
7287        assert_eq!(result["skill"]["scope"], json!("user"));
7288
7289        let removed = service.handle(request(
7290            2,
7291            "harness.v1.skills.remove",
7292            json!({
7293                "harness": HarnessId::CLAUDE_CODE,
7294                "name": "orch22-rpc",
7295                "scope": "user",
7296                "cwd": root,
7297                "homes": homes,
7298            }),
7299        ));
7300        assert_eq!(removed["result"]["removed"], json!(true), "{removed:#}");
7301        assert!(!root.join("claude_home/skills/orch22-rpc").exists());
7302        std::fs::remove_dir_all(&root).ok();
7303    }
7304
7305    /// OpenClaw publishes no `skills remove` at the pin, so the uniform verb
7306    /// refuses with UnsupportedAction instead of deleting files itself.
7307    #[test]
7308    fn skills_remove_refuses_openclaw_at_the_pin() {
7309        let response = HarnessSessionService::new().handle(request(
7310            1,
7311            "harness.v1.skills.remove",
7312            json!({"harness": HarnessId::OPENCLAW, "name": "clawhub-demo"}),
7313        ));
7314        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7315        assert!(response["error"]["message"]
7316            .as_str()
7317            .unwrap()
7318            .contains("no `skills remove` verb"));
7319    }
7320
7321    /// A harness with no skills root at all is refused by name, with the
7322    /// same sentence `skills.list` gives it.
7323    #[test]
7324    fn skills_install_refuses_a_harness_without_a_skills_root() {
7325        let response = HarnessSessionService::new().handle(request(
7326            1,
7327            "harness.v1.skills.install",
7328            json!({"harness": "not-a-harness", "source": "/tmp/x"}),
7329        ));
7330        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7331        assert!(response["error"]["message"]
7332            .as_str()
7333            .unwrap()
7334            .contains("not-a-harness"));
7335    }
7336
7337    // ---- ORCH-12: `harness.v1.memory.show|search` ------------------------
7338
7339    /// `HarnessHomes` for the committed fixture homes. Every root a test does
7340    /// not name is pinned at an absent path, so a read can never fall through
7341    /// to this machine's real harness homes. Note `hermes` is the `state.db`
7342    /// PATH (its parent is HERMES_HOME) and `claude_code` is the `projects`
7343    /// directory — the same contract discovery uses.
7344    fn memory_homes() -> Value {
7345        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7346        json!({
7347            "claude_code": fixtures.join("__absent__"),
7348            "codex": fixtures.join("__absent__"),
7349            "opencode": fixtures.join("__absent__"),
7350            "pi": fixtures.join("__absent__"),
7351            "grok": fixtures.join("__absent__"),
7352            "gemini": fixtures.join("__absent__"),
7353            "goose": fixtures.join("__absent__"),
7354            "supercode": fixtures.join("__absent__"),
7355            "hermes": fixtures.join("hermes_home/state.db"),
7356            "openclaw": fixtures.join("openclaw_home"),
7357        })
7358    }
7359
7360    fn memory_call_ok(method: &str, params: Value, key: &str) -> Vec<Value> {
7361        let response = HarnessSessionService::new().handle(request(1, method, params));
7362        assert!(response.get("error").is_none(), "{response:#}");
7363        assert_eq!(response["result"]["schema"], json!("supercode.memory.v1"));
7364        response["result"][key]
7365            .as_array()
7366            .cloned()
7367            .unwrap_or_default()
7368    }
7369
7370    fn memory_documents(params: Value) -> Vec<Value> {
7371        memory_call_ok("harness.v1.memory.show", params, "documents")
7372    }
7373
7374    fn memory_matches(params: Value) -> Vec<Value> {
7375        memory_call_ok("harness.v1.memory.search", params, "matches")
7376    }
7377
7378    fn find_document<'a>(rows: &'a [Value], profile: &str, name: &str) -> &'a Value {
7379        rows.iter()
7380            .find(|row| row["profile"] == profile && row["name"] == name)
7381            .unwrap_or_else(|| panic!("no `{profile}` document `{name}` in {rows:#?}"))
7382    }
7383
7384    /// Hermes: the built-in `MEMORY.md`/`USER.md` pair and the `memories/`
7385    /// topic files, for HERMES_HOME itself and for every profile home.
7386    #[test]
7387    fn memory_show_reads_the_hermes_profile_homes() {
7388        let rows = memory_documents(json!({"harness": "hermes", "homes": memory_homes()}));
7389
7390        let notes = find_document(&rows, "default", "MEMORY.md");
7391        assert_eq!(notes["harness"], "hermes");
7392        assert_eq!(notes["scope"], "user");
7393        assert!(notes["size"].as_u64().unwrap() > 0);
7394        assert!(notes["updated_at"].is_string(), "{notes:#?}");
7395        // The default answer previews the head and never the whole body.
7396        assert!(notes.get("content").is_none(), "{notes:#?}");
7397        assert_eq!(notes["truncated"], true);
7398        assert_eq!(notes["preview"].as_array().unwrap().len(), 5);
7399
7400        let user = find_document(&rows, "default", "USER.md");
7401        assert_eq!(user["scope"], "user");
7402        assert!(user["preview"]
7403            .as_array()
7404            .unwrap()
7405            .iter()
7406            .any(|line| line.as_str().unwrap().contains("neovim")));
7407
7408        let topic = find_document(&rows, "default", "memories/2026-09-01-notes.md");
7409        assert!(topic["path"]
7410            .as_str()
7411            .unwrap()
7412            .ends_with("hermes_home/memories/2026-09-01-notes.md"));
7413
7414        // Profile mode points HERMES_HOME at `<root>/profiles/<name>`.
7415        let coder = find_document(&rows, "coder", "MEMORY.md");
7416        assert_eq!(coder["scope"], "profile");
7417        assert!(coder["path"]
7418            .as_str()
7419            .unwrap()
7420            .ends_with("hermes_home/profiles/coder/MEMORY.md"));
7421    }
7422
7423    /// `full` is the only way a body crosses the wire, and `profile` narrows
7424    /// the read to one home.
7425    #[test]
7426    fn memory_show_returns_bodies_only_under_full_and_narrows_by_profile() {
7427        let rows = memory_documents(json!({
7428            "harness": "hermes",
7429            "profile": "coder",
7430            "full": true,
7431            "homes": memory_homes(),
7432        }));
7433        assert!(
7434            rows.iter().all(|row| row["profile"] == "coder"),
7435            "{rows:#?}"
7436        );
7437        let coder = find_document(&rows, "coder", "MEMORY.md");
7438        assert!(coder["content"]
7439            .as_str()
7440            .expect("full returns the body")
7441            .contains("anthropic/claude-opus-4-8"));
7442    }
7443
7444    /// OpenClaw: memory-core's files under each agent's workspace —
7445    /// `<state>/workspace` for the default agent, `<state>/workspace-<id>`
7446    /// for any other.
7447    #[test]
7448    fn memory_show_reads_the_openclaw_agent_workspaces() {
7449        let rows = memory_documents(json!({"harness": "openclaw", "homes": memory_homes()}));
7450
7451        let main = find_document(&rows, "main", "MEMORY.md");
7452        assert_eq!(main["scope"], "agent");
7453        assert!(main["path"]
7454            .as_str()
7455            .unwrap()
7456            .ends_with("openclaw_home/workspace/MEMORY.md"));
7457
7458        let topic = find_document(&rows, "main", "memory/2026-09-01-standup.md");
7459        assert!(topic["path"]
7460            .as_str()
7461            .unwrap()
7462            .ends_with("openclaw_home/workspace/memory/2026-09-01-standup.md"));
7463
7464        let design = find_document(&rows, "design", "MEMORY.md");
7465        assert!(design["path"]
7466            .as_str()
7467            .unwrap()
7468            .ends_with("openclaw_home/workspace-design/MEMORY.md"));
7469    }
7470
7471    /// Claude Code: the auto-memory directory of the project the working tree
7472    /// belongs to, keyed by the enclosing git repository.
7473    #[test]
7474    fn memory_show_reads_a_claude_code_project_auto_memory_directory() {
7475        let scratch = std::env::temp_dir().join(format!(
7476            "supercode-orch12-cc-{}-{}",
7477            std::process::id(),
7478            std::time::SystemTime::now()
7479                .duration_since(std::time::UNIX_EPOCH)
7480                .unwrap()
7481                .as_nanos()
7482        ));
7483        let project = scratch.join("repo");
7484        std::fs::create_dir_all(project.join(".git")).unwrap();
7485        // Auto-memory is shared across a repo's worktrees, so a nested
7486        // working directory must resolve to the repo's own project dir.
7487        let worktree = project.join("crates/harness");
7488        std::fs::create_dir_all(&worktree).unwrap();
7489        let slug: String = project
7490            .to_string_lossy()
7491            .chars()
7492            .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
7493            .collect();
7494        let projects = scratch.join("claude/projects");
7495        let memory = projects.join(&slug).join("memory");
7496        std::fs::create_dir_all(&memory).unwrap();
7497        std::fs::write(
7498            memory.join("MEMORY.md"),
7499            "# index\n- [build box](build-box.md) — the pinned harnesses\n",
7500        )
7501        .unwrap();
7502        std::fs::write(
7503            memory.join("build-box.md"),
7504            "hermes 0.21.0 and openclaw 2026.7.1-2 are the pins\n",
7505        )
7506        .unwrap();
7507
7508        let mut homes = memory_homes();
7509        homes["claude_code"] = json!(projects);
7510        let rows = memory_documents(json!({
7511            "harness": "claude-code",
7512            "cwd": worktree,
7513            "homes": homes,
7514        }));
7515        let index = find_document(&rows, &slug, "MEMORY.md");
7516        assert_eq!(index["harness"], "claude-code");
7517        assert_eq!(index["scope"], "project");
7518        let topic = find_document(&rows, &slug, "build-box.md");
7519        assert!(topic["preview"]
7520            .as_array()
7521            .unwrap()
7522            .iter()
7523            .any(|line| line.as_str().unwrap().contains("2026.7.1-2")));
7524
7525        let hits = memory_matches(json!({
7526            "harness": "claude-code",
7527            "query": "pinned harnesses",
7528            "cwd": worktree,
7529            "homes": homes,
7530        }));
7531        assert_eq!(hits.len(), 1, "{hits:#?}");
7532        assert_eq!(hits[0]["name"], "MEMORY.md");
7533        assert_eq!(hits[0]["line"], 2);
7534
7535        let _ = std::fs::remove_dir_all(&scratch);
7536    }
7537
7538    /// A config-less OpenClaw install declares no default agent, but
7539    /// memory-core still resolves ONE agent to the default `workspace`
7540    /// directory — the same `main`-then-first convention the profile rows
7541    /// use. Measured against `openclaw memory status` on the pinned CLI
7542    /// (`docs/interop/research/orch12-memory-receipt-2026-09-03.json`).
7543    #[test]
7544    fn memory_show_resolves_the_default_workspace_without_an_openclaw_config() {
7545        let state = std::env::temp_dir().join(format!(
7546            "supercode-orch12-oc-{}-{}",
7547            std::process::id(),
7548            std::time::SystemTime::now()
7549                .duration_since(std::time::UNIX_EPOCH)
7550                .unwrap()
7551                .as_nanos()
7552        ));
7553        // No `openclaw.json`: only the agent home the gateway creates.
7554        std::fs::create_dir_all(state.join("agents/main/agent")).unwrap();
7555        std::fs::create_dir_all(state.join("workspace")).unwrap();
7556        std::fs::write(
7557            state.join("workspace/MEMORY.md"),
7558            "the gateway websocket needs credentials\n",
7559        )
7560        .unwrap();
7561
7562        let mut homes = memory_homes();
7563        homes["openclaw"] = json!(state);
7564        let rows = memory_documents(json!({"harness": "openclaw", "homes": homes}));
7565        assert_eq!(rows.len(), 1, "{rows:#?}");
7566        let row = find_document(&rows, "main", "MEMORY.md");
7567        assert_eq!(row["scope"], "agent");
7568        assert!(row["path"]
7569            .as_str()
7570            .unwrap()
7571            .ends_with("workspace/MEMORY.md"));
7572
7573        let _ = std::fs::remove_dir_all(&state);
7574    }
7575
7576    /// Search is a plain scan over the same documents: a hit carries the
7577    /// path, line and excerpt; a miss is an empty list, not an error.
7578    #[test]
7579    fn memory_search_reports_hits_by_line_and_misses_as_empty() {
7580        let hit = memory_matches(json!({
7581            "harness": "hermes",
7582            "query": "NEOVIM",
7583            "homes": memory_homes(),
7584        }));
7585        assert_eq!(hit.len(), 1, "{hit:#?}");
7586        assert_eq!(hit[0]["harness"], "hermes");
7587        assert_eq!(hit[0]["name"], "USER.md");
7588        assert_eq!(hit[0]["scope"], "user");
7589        assert_eq!(hit[0]["line"], 5);
7590        assert!(hit[0]["excerpt"].as_str().unwrap().contains("neovim"));
7591
7592        // A regular expression reaches the same lines.
7593        let regex = memory_matches(json!({
7594            "harness": "hermes",
7595            "query": "neo(vim|vi)",
7596            "regex": true,
7597            "homes": memory_homes(),
7598        }));
7599        assert_eq!(regex.len(), 1, "{regex:#?}");
7600
7601        let miss = memory_matches(json!({
7602            "harness": "hermes",
7603            "query": "no-memory-line-says-this",
7604            "homes": memory_homes(),
7605        }));
7606        assert!(miss.is_empty(), "{miss:#?}");
7607    }
7608
7609    /// The uniform-verb contract: a harness with no memory store at the pin
7610    /// is refused by name, and `session` only selects a Claude Code project.
7611    #[test]
7612    fn memory_refuses_harnesses_without_a_store_and_misplaced_session_scoping() {
7613        for method in ["harness.v1.memory.show", "harness.v1.memory.search"] {
7614            let response = HarnessSessionService::new().handle(request(
7615                1,
7616                method,
7617                json!({"harness": "codex", "query": "anything", "homes": memory_homes()}),
7618            ));
7619            assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7620            assert!(response["error"]["message"]
7621                .as_str()
7622                .unwrap()
7623                .contains("codex"));
7624        }
7625
7626        let response = HarnessSessionService::new().handle(request(
7627            1,
7628            "harness.v1.memory.show",
7629            json!({"harness": "hermes", "session": "abc", "homes": memory_homes()}),
7630        ));
7631        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7632
7633        // `harness` is not optional: memory documents are the user's prose.
7634        let response = HarnessSessionService::new().handle(request(
7635            1,
7636            "harness.v1.memory.show",
7637            json!({"homes": memory_homes()}),
7638        ));
7639        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7640    }
7641
7642    /// Both methods are advertised, and their SDK operations resolve them.
7643    #[test]
7644    fn memory_methods_are_advertised_and_map_to_sdk_operations() {
7645        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.show"));
7646        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.search"));
7647        assert_eq!(
7648            SdkOperation::from_method("harness.v1.memory.show"),
7649            Some(SdkOperation::MemoryShow)
7650        );
7651        assert_eq!(
7652            SdkOperation::from_method("harness.v1.memory.search"),
7653            Some(SdkOperation::MemorySearch)
7654        );
7655    }
7656
7657    // ---- ORCH-9: `harness.v1.approvals.list` -----------------------------
7658
7659    /// A runtime that raises one protocol request and then goes quiet, so a
7660    /// single poll delivers the request without closing the connection.
7661    struct RequestingRuntime {
7662        handle: RuntimeHandle,
7663        events: std::collections::VecDeque<HarnessEvent>,
7664        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7665    }
7666
7667    #[async_trait]
7668    impl RuntimeConnection for RequestingRuntime {
7669        fn handle(&self) -> &RuntimeHandle {
7670            &self.handle
7671        }
7672
7673        async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
7674            unreachable!("this runtime only raises requests")
7675        }
7676
7677        async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
7678            match self.events.pop_front() {
7679                Some(event) => Ok(Some(event)),
7680                // Quiet, not closed: `poll_sdk_events` times out and leaves
7681                // the connection open, the way a runtime blocked on a
7682                // permission request behaves.
7683                None => std::future::pending().await,
7684            }
7685        }
7686
7687        async fn interrupt(&mut self) -> crate::Result<()> {
7688            Ok(())
7689        }
7690
7691        async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
7692            // Both halves are recorded: ORCH-20 has to prove not just that the
7693            // right request was answered but that the door received its own
7694            // reply envelope.
7695            self.answered
7696                .lock()
7697                .unwrap_or_else(std::sync::PoisonError::into_inner)
7698                .push(json!({"request_id": request_id, "response": response}));
7699            Ok(())
7700        }
7701
7702        async fn close(&mut self) -> crate::Result<()> {
7703            Ok(())
7704        }
7705    }
7706
7707    fn requesting_runtime(
7708        harness: &str,
7709        events: Vec<HarnessEvent>,
7710        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7711    ) -> Box<dyn RuntimeConnection> {
7712        requesting_runtime_named(harness, "hermes-live-session", events, answered)
7713    }
7714
7715    fn requesting_runtime_named(
7716        harness: &str,
7717        runtime_id: &str,
7718        events: Vec<HarnessEvent>,
7719        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7720    ) -> Box<dyn RuntimeConnection> {
7721        Box::new(RequestingRuntime {
7722            handle: RuntimeHandle {
7723                harness: HarnessId::from(harness),
7724                runtime_id: runtime_id.into(),
7725                endpoint: RuntimeEndpoint::LocalProcess {
7726                    pid: None,
7727                    command: vec!["hermes-acp".into()],
7728                    protocol: "acp".into(),
7729                },
7730            },
7731            events: events.into(),
7732            answered,
7733        })
7734    }
7735
7736    fn permission_event(id: u64, title: &str) -> HarnessEvent {
7737        HarnessEvent {
7738            sequence: None,
7739            kind: "session/request_permission".into(),
7740            payload: json!({
7741                "jsonrpc": "2.0",
7742                "id": id,
7743                "method": "session/request_permission",
7744                "params": {
7745                    "sessionId": "hermes-live-session",
7746                    "toolCall": {"toolCallId": "call-1", "title": title, "kind": "execute"},
7747                    "options": [
7748                        {"optionId": "allow_once", "name": "Allow once", "kind": "allow_once"},
7749                        {"optionId": "allow_for_session", "name": "Allow for session", "kind": "allow_always"},
7750                        {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
7751                    ],
7752                },
7753            }),
7754        }
7755    }
7756
7757    fn approvals(service: &mut HarnessSessionService, params: Value) -> Value {
7758        let response = service.handle(request(1, "harness.v1.approvals.list", params));
7759        assert!(response.get("error").is_none(), "{response:#}");
7760        response["result"].clone()
7761    }
7762
7763    /// ORC-2 dev/01: the same uniform loop over the CLAUDE CODE door. The
7764    /// `can_use_tool` control request the CLI raises to its registered
7765    /// permission handler lists as one pending row, `approvals.resolve <id>
7766    /// allow_once` sends the `{behavior}` result the CLI accepts through
7767    /// `runtimes.respond`, and the row is gone. The frame is the one claude
7768    /// 2.1.258 wrote, transcribed from
7769    /// `docs/interop/research/orc2-claude-respond-receipt-2026-09-04.json`.
7770    #[tokio::test]
7771    async fn a_claude_code_permission_request_lists_and_resolves_on_the_uniform_door() {
7772        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7773        let mut service = HarnessSessionService::new();
7774        service.runtimes.insert(
7775            "runtime-cc".into(),
7776            requesting_runtime_named(
7777                HarnessId::CLAUDE_CODE,
7778                "claude-live-session",
7779                vec![HarnessEvent {
7780                    sequence: None,
7781                    kind: "control_request".into(),
7782                    payload: json!({
7783                        "type": "control_request",
7784                        "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7785                        "request": {
7786                            "subtype": "can_use_tool",
7787                            "tool_name": "Bash",
7788                            "display_name": "Bash",
7789                            "input": {"command": "touch probe-artifact.txt"},
7790                            "tool_use_id": "toolu_mock_1",
7791                        },
7792                    }),
7793                }],
7794                answered.clone(),
7795            ),
7796        );
7797
7798        let notifications = service.poll_runtimes().await;
7799        assert_eq!(notifications.len(), 1, "{notifications:#?}");
7800
7801        let rows = approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}));
7802        assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7803        let row = &rows[0];
7804        assert_eq!(row["id"], "runtime-cc/053f8a2d-3445-4011-a259-4261b31c7326");
7805        assert_eq!(row["harness"], HarnessId::CLAUDE_CODE);
7806        assert_eq!(row["status"], "pending");
7807        assert_eq!(row["subject"], "Bash touch probe-artifact.txt");
7808        assert_eq!(row["runtime_id"], "claude-live-session");
7809        assert_eq!(
7810            row["options"]
7811                .as_array()
7812                .unwrap()
7813                .iter()
7814                .map(|option| option["id"].as_str().unwrap())
7815                .collect::<Vec<_>>(),
7816            vec!["allow", "deny"],
7817        );
7818
7819        let response = resolve(
7820            &mut service,
7821            json!({"id": row["id"], "decision": "allow_once"}),
7822        )
7823        .await;
7824        assert!(response.get("error").is_none(), "{response:#}");
7825        assert_eq!(response["result"]["option_id"], "allow");
7826        assert_eq!(
7827            answered
7828                .lock()
7829                .unwrap_or_else(std::sync::PoisonError::into_inner)
7830                .as_slice(),
7831            &[json!({
7832                "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7833                "response": {"behavior": "allow"},
7834            })],
7835        );
7836        assert_eq!(
7837            approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}))
7838                .as_array()
7839                .map(Vec::len),
7840            Some(0),
7841        );
7842    }
7843
7844    /// dev/01: a live ACP permission request raised on a driven runtime is
7845    /// listable while the turn is blocked on it, and stops being listable
7846    /// the moment `runtimes.respond` answers it.
7847    #[tokio::test]
7848    async fn a_live_permission_request_lists_until_it_is_answered() {
7849        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7850        let mut service = HarnessSessionService::new();
7851        service.runtimes.insert(
7852            "runtime-1".into(),
7853            requesting_runtime(
7854                HarnessId::HERMES,
7855                vec![permission_event(7, "rm -rf build")],
7856                answered.clone(),
7857            ),
7858        );
7859
7860        let notifications = service.poll_runtimes().await;
7861        assert_eq!(notifications.len(), 1, "{notifications:#?}");
7862
7863        let rows = approvals(&mut service, json!({}));
7864        assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7865        let row = &rows[0];
7866        assert_eq!(row["id"], "runtime-1/7");
7867        assert_eq!(row["harness"], HarnessId::HERMES);
7868        assert_eq!(row["kind"], "live");
7869        assert_eq!(row["status"], "pending");
7870        assert_eq!(row["subject"], "rm -rf build");
7871        assert_eq!(row["session_id"], "hermes-live-session");
7872        assert_eq!(row["runtime_id"], "hermes-live-session");
7873        assert!(row["requested_at_ms"].as_i64().is_some(), "{row:#}");
7874        assert!(
7875            row["age_ms"].as_i64().is_some_and(|age| age >= 0),
7876            "{row:#}"
7877        );
7878        assert_eq!(
7879            row["options"]
7880                .as_array()
7881                .unwrap()
7882                .iter()
7883                .map(|option| option["id"].as_str().unwrap())
7884                .collect::<Vec<_>>(),
7885            vec!["allow_once", "allow_for_session", "deny"],
7886        );
7887
7888        // The filters select against the same rows.
7889        assert_eq!(
7890            approvals(&mut service, json!({"harness": HarnessId::HERMES}))
7891                .as_array()
7892                .map(Vec::len),
7893            Some(1),
7894        );
7895        assert_eq!(
7896            approvals(&mut service, json!({"session": "some-other-session"}))
7897                .as_array()
7898                .map(Vec::len),
7899            Some(0),
7900        );
7901
7902        let response = service
7903            .handle_async(request(
7904                2,
7905                "harness.v1.runtimes.respond",
7906                json!({
7907                    "connection": "runtime-1",
7908                    "request_id": 7,
7909                    "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7910                }),
7911            ))
7912            .await;
7913        assert!(response.get("error").is_none(), "{response:#}");
7914        assert_eq!(
7915            answered
7916                .lock()
7917                .unwrap_or_else(std::sync::PoisonError::into_inner)
7918                .as_slice(),
7919            &[json!({
7920                "request_id": 7,
7921                "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7922            })],
7923        );
7924
7925        let rows = approvals(&mut service, json!({}));
7926        assert_eq!(rows.as_array().map(Vec::len), Some(0), "{rows:#}");
7927    }
7928
7929    /// dev/01: supercode's own queued subagent approvals list through the
7930    /// same door, carrying the outcome the record holds.
7931    #[test]
7932    fn queued_subagent_approvals_list_through_the_same_door() {
7933        let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
7934            crate::subagents::QueuedApproval {
7935                child_agent_id: "child-7".into(),
7936                tool: "shell".into(),
7937                subject: Some("cargo publish --dry-run".into()),
7938                queued_at_ms: 1,
7939                outcome: None,
7940            },
7941            crate::subagents::QueuedApproval {
7942                child_agent_id: "child-8".into(),
7943                tool: "write_file".into(),
7944                subject: None,
7945                queued_at_ms: 2,
7946                outcome: Some(crate::subagents::QueuedApprovalOutcome::Denied),
7947            },
7948        ]));
7949        let mut service = HarnessSessionService::new();
7950        service.observe_subagent_approvals(queue);
7951
7952        let rows = approvals(&mut service, json!({}));
7953        assert_eq!(rows.as_array().map(Vec::len), Some(2), "{rows:#}");
7954        assert_eq!(rows[0]["id"], "supercode/subagent/child-7/1/0");
7955        assert_eq!(rows[0]["harness"], HarnessId::SUPERCODE);
7956        assert_eq!(rows[0]["status"], "pending");
7957        assert_eq!(rows[0]["subject"], "shell cargo publish --dry-run");
7958        assert_eq!(rows[1]["status"], "denied");
7959        assert!(rows[1]["options"].as_array().unwrap().is_empty());
7960
7961        // `--session` addresses a subagent row by its child agent id.
7962        let only = approvals(&mut service, json!({"session": "child-8"}));
7963        assert_eq!(only.as_array().map(Vec::len), Some(1), "{only:#}");
7964        assert_eq!(only[0]["id"], "supercode/subagent/child-8/2/1");
7965    }
7966
7967    /// The uniform-verb contract: an id whose runtime door cannot carry a
7968    /// protocol request is refused BY NAME rather than answered with an empty
7969    /// list. Since ORC-2 gave Claude Code a permission-response primitive
7970    /// every registered harness can carry one, so the refusal is exercised on
7971    /// an unknown id — and the registered ids are asserted to be accepted.
7972    #[test]
7973    fn approvals_list_refuses_a_harness_that_cannot_carry_a_request() {
7974        let response = HarnessSessionService::new().handle(request(
7975            1,
7976            "harness.v1.approvals.list",
7977            json!({"harness": "not-a-harness"}),
7978        ));
7979        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7980        assert!(response["error"]["message"]
7981            .as_str()
7982            .unwrap()
7983            .contains("not-a-harness"));
7984        for harness in [HarnessId::CLAUDE_CODE, HarnessId::CODEX] {
7985            let response = HarnessSessionService::new().handle(request(
7986                1,
7987                "harness.v1.approvals.list",
7988                json!({"harness": harness}),
7989            ));
7990            assert!(response.get("error").is_none(), "{harness}: {response:#}");
7991        }
7992    }
7993
7994    /// The method is advertised, its SDK operation resolves it, and the
7995    /// registry reports the concept as observed for every harness whose
7996    /// runtime door can carry a request.
7997    #[test]
7998    fn approvals_list_is_an_advertised_method_and_an_observed_tier() {
7999        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.list"));
8000        assert_eq!(
8001            SdkOperation::from_method("harness.v1.approvals.list"),
8002            Some(SdkOperation::ApprovalsList)
8003        );
8004        let registry = harness_support_registry();
8005        for id in [
8006            HarnessId::HERMES,
8007            HarnessId::OPENCLAW,
8008            HarnessId::CODEX,
8009            // ORC-2: the Claude Code door answers `can_use_tool`, so its
8010            // pending_request concept joins the other driven doors.
8011            HarnessId::CLAUDE_CODE,
8012        ] {
8013            let concept = registry
8014                .harnesses
8015                .iter()
8016                .find(|harness| harness.id.as_str() == id)
8017                .unwrap()
8018                .orchestration
8019                .concepts
8020                .iter()
8021                .find(|concept| concept.concept == "pending_request")
8022                .unwrap();
8023            assert_eq!(concept.observed, crate::ImplementationKind::BuiltIn, "{id}");
8024            assert!(concept
8025                .methods
8026                .iter()
8027                .any(|method| method == "harness.v1.approvals.list"));
8028        }
8029    }
8030
8031    // ---- ORCH-20: `harness.v1.approvals.resolve` -------------------------
8032
8033    async fn resolve(service: &mut HarnessSessionService, params: Value) -> Value {
8034        service
8035            .handle_async(request(3, "harness.v1.approvals.resolve", params))
8036            .await
8037    }
8038
8039    /// dev/01: the whole loop on a driven runtime — list one pending row,
8040    /// answer it by ROW ID with one uniform decision, and see it gone. The
8041    /// door receives its own ACP envelope carrying the option it enumerated.
8042    #[tokio::test]
8043    async fn a_listed_row_resolves_with_one_uniform_decision_and_then_is_gone() {
8044        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8045        let mut service = HarnessSessionService::new();
8046        service.runtimes.insert(
8047            "runtime-1".into(),
8048            requesting_runtime(
8049                HarnessId::HERMES,
8050                vec![permission_event(7, "rm -rf build")],
8051                answered.clone(),
8052            ),
8053        );
8054        service.poll_runtimes().await;
8055
8056        let rows = approvals(&mut service, json!({}));
8057        assert_eq!(rows[0]["id"], "runtime-1/7");
8058
8059        let response = resolve(
8060            &mut service,
8061            json!({"id": "runtime-1/7", "decision": "allow_once"}),
8062        )
8063        .await;
8064        assert!(response.get("error").is_none(), "{response:#}");
8065        assert_eq!(
8066            response["result"],
8067            json!({
8068                "id": "runtime-1/7",
8069                "decision": "allow_once",
8070                "option_id": "allow_once",
8071                "resolved": true,
8072            }),
8073        );
8074        // The harness's own door was called with its own envelope.
8075        assert_eq!(
8076            answered
8077                .lock()
8078                .unwrap_or_else(std::sync::PoisonError::into_inner)
8079                .as_slice(),
8080            &[json!({
8081                "request_id": 7,
8082                "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
8083            })],
8084        );
8085        // And the row is gone, the same way `runtimes.respond` drops it.
8086        assert_eq!(
8087            approvals(&mut service, json!({})).as_array().map(Vec::len),
8088            Some(0),
8089        );
8090        // Answering it twice is an honest miss, not a silent success.
8091        let response = resolve(
8092            &mut service,
8093            json!({"id": "runtime-1/7", "decision": "allow_once"}),
8094        )
8095        .await;
8096        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8097    }
8098
8099    /// dev/01: deny travels the same path and picks the option the request
8100    /// itself classified as a refusal.
8101    #[tokio::test]
8102    async fn deny_selects_the_requests_own_reject_option() {
8103        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8104        let mut service = HarnessSessionService::new();
8105        service.runtimes.insert(
8106            "runtime-1".into(),
8107            requesting_runtime(
8108                HarnessId::HERMES,
8109                vec![permission_event(11, "git push --force")],
8110                answered.clone(),
8111            ),
8112        );
8113        service.poll_runtimes().await;
8114
8115        let response = resolve(
8116            &mut service,
8117            json!({"id": "runtime-1/11", "decision": "deny"}),
8118        )
8119        .await;
8120        assert!(response.get("error").is_none(), "{response:#}");
8121        // `deny` is the optionId whose ACP `kind` is `reject_once`.
8122        assert_eq!(response["result"]["option_id"], "deny");
8123        assert_eq!(
8124            answered
8125                .lock()
8126                .unwrap_or_else(std::sync::PoisonError::into_inner)[0]["response"],
8127            json!({"outcome": {"outcome": "selected", "optionId": "deny"}}),
8128        );
8129        assert_eq!(
8130            approvals(&mut service, json!({})).as_array().map(Vec::len),
8131            Some(0),
8132        );
8133    }
8134
8135    /// dev/01: a decision this request does not offer is refused by name,
8136    /// listing the ones it does — never silently downgraded to a neighbour.
8137    #[tokio::test]
8138    async fn a_decision_the_request_does_not_offer_is_refused_with_the_offered_ones() {
8139        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8140        let mut service = HarnessSessionService::new();
8141        let mut event = permission_event(3, "rm -rf build");
8142        // A request offering only allow-once and deny, as hermes 0.21.0's
8143        // edit-approval layer raises one.
8144        event.payload["params"]["options"] = json!([
8145            {"optionId": "allow_once", "name": "Allow edit", "kind": "allow_once"},
8146            {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
8147        ]);
8148        service.runtimes.insert(
8149            "runtime-1".into(),
8150            requesting_runtime(HarnessId::HERMES, vec![event], answered.clone()),
8151        );
8152        service.poll_runtimes().await;
8153
8154        let response = resolve(
8155            &mut service,
8156            json!({"id": "runtime-1/3", "decision": "allow_always"}),
8157        )
8158        .await;
8159        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8160        let message = response["error"]["message"].as_str().unwrap();
8161        assert!(message.contains("allow_always"), "{message}");
8162        assert!(message.contains("allow_once, deny"), "{message}");
8163        // Nothing was sent, and the request is still waiting for an answer.
8164        assert!(answered
8165            .lock()
8166            .unwrap_or_else(std::sync::PoisonError::into_inner)
8167            .is_empty());
8168        assert_eq!(
8169            approvals(&mut service, json!({})).as_array().map(Vec::len),
8170            Some(1),
8171        );
8172    }
8173
8174    /// dev/01: supercode's own queued subagent row is addressable but not
8175    /// answerable through this door — it is the parent's audit copy of a
8176    /// request its own handler answers. Refused by name, never a no-op.
8177    #[tokio::test]
8178    async fn a_queued_subagent_row_is_refused_by_name_rather_than_silently_answered() {
8179        let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
8180            crate::subagents::QueuedApproval {
8181                child_agent_id: "child-7".into(),
8182                tool: "shell".into(),
8183                subject: Some("cargo publish --dry-run".into()),
8184                queued_at_ms: 1,
8185                outcome: None,
8186            },
8187        ]));
8188        let mut service = HarnessSessionService::new();
8189        service.observe_subagent_approvals(queue.clone());
8190        let row = approvals(&mut service, json!({}))[0]["id"]
8191            .as_str()
8192            .unwrap()
8193            .to_string();
8194        assert_eq!(row, "supercode/subagent/child-7/1/0");
8195
8196        let response = resolve(&mut service, json!({"id": row, "decision": "allow_once"})).await;
8197        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8198        let message = response["error"]["message"].as_str().unwrap();
8199        assert!(message.contains("queued subagent record"), "{message}");
8200        assert!(message.contains("request"), "{message}");
8201        // The audit record is untouched: nothing pretended to answer it.
8202        assert!(queue
8203            .lock()
8204            .unwrap_or_else(std::sync::PoisonError::into_inner)[0]
8205            .outcome
8206            .is_none());
8207    }
8208
8209    /// An id nobody is holding, and a call that names no decision at all,
8210    /// both fail with a message that says why.
8211    #[tokio::test]
8212    async fn an_unknown_row_and_a_missing_decision_are_both_named() {
8213        let mut service = HarnessSessionService::new();
8214        let response = resolve(
8215            &mut service,
8216            json!({"id": "runtime-9/4", "decision": "deny"}),
8217        )
8218        .await;
8219        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8220        assert!(response["error"]["message"]
8221            .as_str()
8222            .unwrap()
8223            .contains("runtime-9/4"));
8224
8225        let response = resolve(&mut service, json!({"id": "runtime-9/4"})).await;
8226        let message = response["error"]["message"].as_str().unwrap();
8227        assert!(
8228            message.contains("allow_once | allow_always | deny"),
8229            "{message}"
8230        );
8231
8232        let response = resolve(
8233            &mut service,
8234            json!({"id": "runtime-9/4", "decision": "deny", "option_id": "deny"}),
8235        )
8236        .await;
8237        assert!(response["error"]["message"]
8238            .as_str()
8239            .unwrap()
8240            .contains("not both"));
8241    }
8242
8243    /// The method is advertised, its SDK operation resolves it, and every
8244    /// harness whose runtime door can carry a request reports it on the
8245    /// CONTROLLED tier beside `runtimes.respond`.
8246    #[test]
8247    fn approvals_resolve_is_an_advertised_method_and_a_controlled_tier() {
8248        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.resolve"));
8249        assert_eq!(
8250            SdkOperation::from_method("harness.v1.approvals.resolve"),
8251            Some(SdkOperation::ApprovalsResolve)
8252        );
8253        assert_eq!(
8254            SdkOperation::ApprovalsResolve.action_name(),
8255            "approvals_resolve"
8256        );
8257        let registry = harness_support_registry();
8258        for id in [
8259            HarnessId::HERMES,
8260            HarnessId::OPENCLAW,
8261            HarnessId::CODEX,
8262            // ORC-2: the Claude Code door answers `can_use_tool`, so its
8263            // pending_request concept joins the other driven doors.
8264            HarnessId::CLAUDE_CODE,
8265        ] {
8266            let concept = registry
8267                .harnesses
8268                .iter()
8269                .find(|harness| harness.id.as_str() == id)
8270                .unwrap()
8271                .orchestration
8272                .concepts
8273                .iter()
8274                .find(|concept| concept.concept == "pending_request")
8275                .unwrap();
8276            assert_eq!(
8277                concept.controlled,
8278                crate::ImplementationKind::BuiltIn,
8279                "{id}"
8280            );
8281            assert!(
8282                concept
8283                    .methods
8284                    .iter()
8285                    .any(|method| method == "harness.v1.approvals.resolve"),
8286                "{id}"
8287            );
8288        }
8289    }
8290
8291    #[test]
8292    fn capabilities_are_explicit_and_versioned() {
8293        let mut service = HarnessSessionService::new();
8294        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
8295        assert_eq!(response["result"]["version"], HARNESS_SERVICE_VERSION);
8296        assert_eq!(
8297            response["result"]["sdk"]["schema_version"],
8298            crate::SDK_SCHEMA_VERSION
8299        );
8300        assert_eq!(
8301            response["result"]["sdk"]["operations"]
8302                .as_array()
8303                .unwrap()
8304                .len(),
8305            SdkOperation::ALL.len()
8306        );
8307        assert_eq!(
8308            response["result"]["harnesses"].as_array().unwrap().len(),
8309            11
8310        );
8311        assert!(response["result"]["harnesses"]
8312            .as_array()
8313            .unwrap()
8314            .iter()
8315            .any(|harness| harness == HarnessId::GROK));
8316        assert!(response["result"]["harnesses"]
8317            .as_array()
8318            .unwrap()
8319            .iter()
8320            .any(|harness| harness == HarnessId::GOOSE));
8321    }
8322
8323    #[test]
8324    fn handshake_health_uses_protocol_liveness_not_stderr_severity() {
8325        let noisy_stderr = crate::HarnessEvent {
8326            sequence: None,
8327            kind: "transport_stderr".into(),
8328            payload: json!({"line": "ERROR optional worker AuthorizationRequired"}),
8329        };
8330        assert_eq!(handshake_event_failure(&noisy_stderr), None);
8331
8332        let closed = crate::HarnessEvent {
8333            sequence: None,
8334            kind: "transport_closed".into(),
8335            payload: json!({}),
8336        };
8337        assert!(handshake_event_failure(&closed).is_some());
8338    }
8339
8340    #[tokio::test]
8341    async fn runtime_eof_is_notified_and_removed_for_raw_and_explicit_close() {
8342        let mut service = HarnessSessionService::new();
8343        service
8344            .runtimes
8345            .insert("raw-eof".into(), ending_runtime(None));
8346        service.runtimes.insert(
8347            "explicit-close".into(),
8348            ending_runtime(Some(HarnessEvent {
8349                sequence: None,
8350                kind: "transport_closed".into(),
8351                payload: json!({"message": "native transport exited"}),
8352            })),
8353        );
8354
8355        let notifications = service.poll_runtimes().await;
8356
8357        assert_eq!(notifications.len(), 2);
8358        assert!(notifications
8359            .iter()
8360            .all(|notification| { notification["params"]["event"]["kind"] == "transport_closed" }));
8361        assert!(notifications.iter().all(|notification| {
8362            notification["params"]["session_id"] == "ending-session"
8363                && notification["params"]["connection"].is_string()
8364        }));
8365        let mut sequences = notifications
8366            .iter()
8367            .filter_map(|notification| notification["params"]["sequence"].as_u64())
8368            .collect::<Vec<_>>();
8369        sequences.sort_unstable();
8370        assert_eq!(sequences, vec![1, 2]);
8371        assert!(service.runtimes.is_empty());
8372    }
8373
8374    #[test]
8375    fn support_report_and_grok_default_binding_share_the_registry() {
8376        let mut service = HarnessSessionService::new();
8377        let response = service.handle(request(1, "harness.v1.support.report", json!({})));
8378        assert_eq!(response["result"]["schema"], crate::SUPPORT_REGISTRY_SCHEMA);
8379        let params = RuntimeBackendParams {
8380            harness: HarnessId::from(HarnessId::GROK),
8381            protocol: None,
8382            launch: None,
8383            base_url: None,
8384            policy: RuntimePolicy::Default,
8385        };
8386        let backend = match runtime_backend(&params) {
8387            Ok(backend) => backend,
8388            Err(_) => panic!("Grok should bind through its registered ACP launch"),
8389        };
8390        assert_eq!(backend.harness().as_str(), HarnessId::GROK);
8391        assert!(backend.capabilities().start_session);
8392        let registered = harness_support_registry()
8393            .harnesses
8394            .into_iter()
8395            .find(|harness| harness.id.as_str() == HarnessId::GROK)
8396            .and_then(|harness| harness.runtime.default_launch)
8397            .unwrap();
8398        assert!(!registered
8399            .arguments
8400            .iter()
8401            .any(|argument| argument == "--always-approve"));
8402        assert!(runtime_launch(&params).is_none());
8403
8404        let yolo = RuntimeBackendParams {
8405            policy: RuntimePolicy::Yolo,
8406            ..params
8407        };
8408        assert!(runtime_launch(&yolo)
8409            .unwrap()
8410            .arguments
8411            .iter()
8412            .any(|argument| argument == "--always-approve"));
8413
8414        let mismatched_protocol = RuntimeBackendParams {
8415            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8416            protocol: Some("acp".into()),
8417            launch: None,
8418            base_url: None,
8419            policy: RuntimePolicy::Default,
8420        };
8421        assert!(runtime_backend(&mismatched_protocol).is_err());
8422    }
8423
8424    #[test]
8425    fn load_follow_and_unfollow_share_the_same_locator() {
8426        let mut service = HarnessSessionService::new();
8427        let locator = pi_locator();
8428        let loaded = service.handle(request(
8429            1,
8430            "harness.v1.sessions.load",
8431            json!({"locator": locator}),
8432        ));
8433        assert_eq!(
8434            loaded["result"]["session"]["session_id"],
8435            locator.session_id
8436        );
8437
8438        let followed = service.handle(request(
8439            2,
8440            "harness.v1.sessions.follow",
8441            json!({"locator": locator}),
8442        ));
8443        assert_eq!(followed["result"]["subscription"], "sub-1");
8444        assert_eq!(followed["result"]["initial"]["type"], "session_snapshot");
8445        assert!(service.poll().is_empty());
8446
8447        let unfollowed = service.handle(request(
8448            3,
8449            "harness.v1.sessions.unfollow",
8450            json!({"subscription": "sub-1"}),
8451        ));
8452        assert_eq!(unfollowed["result"]["removed"], true);
8453    }
8454
8455    #[test]
8456    fn bounded_read_view_excludes_subagents_and_keeps_only_the_tail() {
8457        let temp = std::env::temp_dir().join(format!(
8458            "supercode-bounded-view-{}-{}",
8459            std::process::id(),
8460            generated_session_id()
8461        ));
8462        let path = temp.join("parent.jsonl");
8463        let subagents = temp.join("parent/subagents");
8464        std::fs::create_dir_all(&subagents).unwrap();
8465        let long_last = "x".repeat(300);
8466        let parent_records = [
8467            json!({"type":"user","uuid":"u1","parentUuid":null,"message":{"role":"user","content":"first"}}),
8468            json!({"type":"assistant","uuid":"a1","parentUuid":"u1","message":{"role":"assistant","content":[{"type":"text","text":"middle"}]}}),
8469            json!({"type":"user","uuid":"u2","parentUuid":"a1","message":{"role":"user","content":long_last}}),
8470        ];
8471        std::fs::write(
8472            &path,
8473            format!(
8474                "{}\n",
8475                parent_records
8476                    .iter()
8477                    .map(Value::to_string)
8478                    .collect::<Vec<_>>()
8479                    .join("\n")
8480            ),
8481        )
8482        .unwrap();
8483        std::fs::write(
8484            subagents.join("agent-child.jsonl"),
8485            concat!(
8486                r#"{"type":"user","uuid":"cu","parentUuid":null,"agentId":"child","message":{"role":"user","content":"child work"}}"#,
8487                "\n",
8488            ),
8489        )
8490        .unwrap();
8491        let locator = SessionLocator {
8492            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8493            session_id: "parent".into(),
8494            storage: StorageLocator::File { path },
8495        };
8496        let mut service = HarnessSessionService::new();
8497
8498        let complete = service.handle(request(
8499            1,
8500            "harness.v1.sessions.load",
8501            json!({"locator": locator}),
8502        ));
8503        assert_eq!(
8504            complete["result"]["session"]["subagents"]
8505                .as_array()
8506                .unwrap()
8507                .len(),
8508            1
8509        );
8510
8511        let bounded = service.handle(request(
8512            2,
8513            "harness.v1.sessions.load",
8514            json!({
8515                "locator": locator,
8516                "view": {
8517                    "tail_messages": 1,
8518                    "max_message_chars": 256,
8519                    "include_subagents": false
8520                },
8521            }),
8522        ));
8523        let session = &bounded["result"]["session"];
8524        assert!(session["subagents"].as_array().unwrap().is_empty());
8525        assert_eq!(session["messages"].as_array().unwrap().len(), 1);
8526        assert_eq!(
8527            session["messages"][0]["content"],
8528            format!("{}\n…", "x".repeat(256))
8529        );
8530
8531        let followed = service.handle(request(
8532            3,
8533            "harness.v1.sessions.follow",
8534            json!({
8535                "locator": locator,
8536                "view": {
8537                    "tail_messages": 1,
8538                    "max_message_chars": 256,
8539                    "include_subagents": false
8540                },
8541            }),
8542        ));
8543        let initial = &followed["result"]["initial"]["session"];
8544        assert!(initial["subagents"].as_array().unwrap().is_empty());
8545        assert_eq!(initial["messages"].as_array().unwrap().len(), 1);
8546
8547        let _ = std::fs::remove_dir_all(&temp);
8548    }
8549
8550    #[test]
8551    fn forty_megabyte_display_load_is_bounded_and_prompt() {
8552        let temp = std::env::temp_dir().join(format!(
8553            "supercode-large-display-view-{}-{}",
8554            std::process::id(),
8555            generated_session_id()
8556        ));
8557        std::fs::create_dir_all(&temp).unwrap();
8558        let path = temp.join("rollout.jsonl");
8559        let mut file = std::io::BufWriter::new(std::fs::File::create(&path).unwrap());
8560        writeln!(
8561            file,
8562            r#"{{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{{"id":"large-display","cwd":"/tmp"}}}}"#
8563        )
8564        .unwrap();
8565        let padding = "x".repeat(80 * 1024);
8566        for index in 0..512 {
8567            let marker = if index == 0 {
8568                "OLDEST-SHOULD-NOT-LOAD"
8569            } else if index == 511 {
8570                "LATEST-MUST-LOAD"
8571            } else {
8572                "bulk"
8573            };
8574            writeln!(
8575                file,
8576                "{}",
8577                json!({
8578                    "timestamp": "2026-01-01T00:00:01Z",
8579                    "type": "response_item",
8580                    "payload": {
8581                        "type": "message",
8582                        "role": "assistant",
8583                        "content": [{"type": "output_text", "text": format!("{marker}:{padding}")}],
8584                    },
8585                })
8586            )
8587            .unwrap();
8588        }
8589        file.flush().unwrap();
8590        drop(file);
8591        assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8592
8593        let locator = SessionLocator {
8594            harness: HarnessId::from(HarnessId::CODEX),
8595            session_id: "large-display".into(),
8596            storage: StorageLocator::File { path },
8597        };
8598        let started = Instant::now();
8599        let response = HarnessSessionService::new().handle(request(
8600            1,
8601            "harness.v1.sessions.load",
8602            json!({
8603                "locator": locator,
8604                "view": {
8605                    "tail_messages": 500,
8606                    "max_message_chars": 1024,
8607                    "include_subagents": false,
8608                    "display_history": true,
8609                },
8610            }),
8611        ));
8612        let elapsed = started.elapsed();
8613        let wire = response.to_string();
8614        eprintln!(
8615            "bounded 40 MiB display load: {elapsed:?}, {} response bytes",
8616            wire.len()
8617        );
8618        assert!(response.get("error").is_none(), "{response:#}");
8619        assert!(wire.contains("LATEST-MUST-LOAD"));
8620        assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8621        assert!(
8622            wire.len() < 2 * 1024 * 1024,
8623            "bounded wire was {} bytes",
8624            wire.len()
8625        );
8626        assert!(
8627            elapsed.as_secs_f64() < 3.0,
8628            "bounded 40 MiB load took {elapsed:?}"
8629        );
8630
8631        let _ = std::fs::remove_dir_all(&temp);
8632    }
8633
8634    #[test]
8635    fn forty_megabyte_goose_store_display_load_reads_only_the_tail() {
8636        let temp = std::env::temp_dir().join(format!(
8637            "supercode-large-goose-view-{}-{}",
8638            std::process::id(),
8639            generated_session_id()
8640        ));
8641        std::fs::create_dir_all(&temp).unwrap();
8642        let path = temp.join("sessions.db");
8643        let connection = rusqlite::Connection::open(&path).unwrap();
8644        connection
8645            .execute_batch(
8646                "CREATE TABLE sessions (
8647                    id TEXT PRIMARY KEY, name TEXT NOT NULL, working_dir TEXT NOT NULL,
8648                    created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
8649                    session_type TEXT NOT NULL, extension_data TEXT,
8650                    goose_mode TEXT NOT NULL, provider_name TEXT, model_config_json TEXT,
8651                    archived_at TEXT
8652                 );
8653                 CREATE TABLE messages (
8654                    id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, message_id TEXT,
8655                    role TEXT NOT NULL, content_json TEXT NOT NULL,
8656                    created_timestamp INTEGER NOT NULL, metadata_json TEXT
8657                 );",
8658            )
8659            .unwrap();
8660        connection
8661            .execute(
8662                "INSERT INTO sessions VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, NULL)",
8663                rusqlite::params![
8664                    "goose-large",
8665                    "Large Goose session",
8666                    "/tmp",
8667                    "2026-01-01 00:00:00",
8668                    "2026-01-01 00:00:02",
8669                    "user",
8670                    "{}",
8671                    "auto",
8672                    "anthropic",
8673                    r#"{"model_name":"claude-sonnet"}"#,
8674                ],
8675            )
8676            .unwrap();
8677        let old_content = serde_json::to_string(&vec![json!({
8678            "type": "text",
8679            "text": format!("OLDEST-SHOULD-NOT-LOAD:{}", "x".repeat(40 * 1024 * 1024)),
8680        })])
8681        .unwrap();
8682        connection
8683            .execute(
8684                "INSERT INTO messages VALUES (1, ?1, 'old', 'user', ?2, 1, '{}')",
8685                rusqlite::params!["goose-large", old_content],
8686            )
8687            .unwrap();
8688        connection
8689            .execute(
8690                "INSERT INTO messages VALUES (2, ?1, 'new', 'assistant', ?2, 2, '{}')",
8691                rusqlite::params![
8692                    "goose-large",
8693                    r#"[{"type":"text","text":"LATEST-MUST-LOAD"}]"#
8694                ],
8695            )
8696            .unwrap();
8697        drop(connection);
8698        assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8699
8700        let locator = SessionLocator {
8701            harness: HarnessId::from(HarnessId::GOOSE),
8702            session_id: "goose-large".into(),
8703            storage: StorageLocator::Sqlite {
8704                path,
8705                selector: "goose-large".into(),
8706            },
8707        };
8708        let started = Instant::now();
8709        let response = HarnessSessionService::new().handle(request(
8710            1,
8711            "harness.v1.sessions.load",
8712            json!({
8713                "locator": locator,
8714                "view": {
8715                    "tail_messages": 1,
8716                    "max_message_chars": 1024,
8717                    "include_subagents": false,
8718                    "display_history": true,
8719                },
8720            }),
8721        ));
8722        let elapsed = started.elapsed();
8723        let wire = response.to_string();
8724        eprintln!(
8725            "bounded 40 MiB Goose display load: {elapsed:?}, {} response bytes",
8726            wire.len()
8727        );
8728        assert!(response.get("error").is_none(), "{response:#}");
8729        assert!(wire.contains("LATEST-MUST-LOAD"));
8730        assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8731        assert!(
8732            wire.len() < 64 * 1024,
8733            "bounded wire was {} bytes",
8734            wire.len()
8735        );
8736        assert!(
8737            elapsed.as_secs_f64() < 1.0,
8738            "bounded Goose load took {elapsed:?}"
8739        );
8740
8741        let _ = std::fs::remove_dir_all(&temp);
8742    }
8743
8744    #[test]
8745    fn display_view_keeps_codex_assistant_history_across_compaction() {
8746        let temp = std::env::temp_dir().join(format!(
8747            "supercode-codex-display-view-{}-{}",
8748            std::process::id(),
8749            generated_session_id()
8750        ));
8751        std::fs::create_dir_all(&temp).unwrap();
8752        let path = temp.join("rollout.jsonl");
8753        std::fs::write(
8754            &path,
8755            concat!(
8756                r#"{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{"id":"codex-display","cwd":"/tmp"}}"#,
8757                "\n",
8758                r#"{"timestamp":"2026-01-01T00:00:01Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"old prompt"}]}}"#,
8759                "\n",
8760                r#"{"timestamp":"2026-01-01T00:00:02Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"old answer"}]}}"#,
8761                "\n",
8762                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"}]}}"#,
8763                "\n",
8764                r#"{"timestamp":"2026-01-01T00:00:04Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"new prompt"}]}}"#,
8765                "\n",
8766                r#"{"timestamp":"2026-01-01T00:00:05Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"new answer"}]}}"#,
8767                "\n",
8768            ),
8769        )
8770        .unwrap();
8771        let locator = SessionLocator {
8772            harness: HarnessId::from(HarnessId::CODEX),
8773            session_id: "codex-display".into(),
8774            storage: StorageLocator::File { path },
8775        };
8776        let mut service = HarnessSessionService::new();
8777
8778        let continuation = service.handle(request(
8779            1,
8780            "harness.v1.sessions.load",
8781            json!({"locator": locator}),
8782        ));
8783        let continuation_text = continuation["result"]["session"]["messages"].to_string();
8784        assert!(!continuation_text.contains("old answer"));
8785
8786        let display = service.handle(request(
8787            2,
8788            "harness.v1.sessions.load",
8789            json!({
8790                "locator": locator,
8791                "view": {
8792                    "tail_messages": 10,
8793                    "include_subagents": false,
8794                    "display_history": true,
8795                },
8796            }),
8797        ));
8798        let display_text = display["result"]["session"]["messages"].to_string();
8799        assert!(display_text.contains("old prompt"));
8800        assert!(display_text.contains("old answer"));
8801        assert!(display_text.contains("new prompt"));
8802        assert!(display_text.contains("new answer"));
8803
8804        let _ = std::fs::remove_dir_all(&temp);
8805    }
8806
8807    #[test]
8808    fn indexed_claude_windows_match_the_existing_wire_projection() {
8809        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
8810            .join("tests/fixtures/claude_code_session.jsonl");
8811        let locator = SessionLocator {
8812            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8813            session_id: "fixture".into(),
8814            storage: StorageLocator::File { path },
8815        };
8816        let full = load_session(&locator).unwrap();
8817        for inline_media in [InlineMediaMode::Full, InlineMediaMode::Metadata] {
8818            for offset in [0, 1, full.messages.len(), usize::MAX] {
8819                for limit in [0, 1, 3, usize::MAX] {
8820                    let options = SessionLoadOptions {
8821                        include_subagents: Some(false),
8822                        inline_media,
8823                        message_offset: Some(offset),
8824                        message_limit: Some(limit),
8825                        ..Default::default()
8826                    };
8827                    let expected = projected_session_result(&full, &options);
8828                    assert_eq!(
8829                        indexed_claude_window(&locator, &options).unwrap().unwrap(),
8830                        expected
8831                    );
8832                }
8833            }
8834            for tail in [0, 1, 3, usize::MAX] {
8835                let options = SessionLoadOptions {
8836                    include_subagents: Some(false),
8837                    inline_media,
8838                    message_tail: Some(tail),
8839                    ..Default::default()
8840                };
8841                assert_eq!(
8842                    indexed_claude_window(&locator, &options).unwrap().unwrap(),
8843                    projected_session_result(&full, &options)
8844                );
8845            }
8846        }
8847    }
8848
8849    #[test]
8850    fn load_supports_bounded_windows_and_media_metadata() {
8851        let mut service = HarnessSessionService::new();
8852        let locator = pi_locator();
8853        let bounded = service.handle(request(
8854            1,
8855            "harness.v1.sessions.load",
8856            json!({
8857                "locator": locator,
8858                "options": {
8859                    "include_subagents": false,
8860                    "message_limit": 2,
8861                    "message_offset": 1
8862                }
8863            }),
8864        ));
8865        assert_eq!(bounded["result"]["window"]["offset"], 1);
8866        assert_eq!(bounded["result"]["window"]["returned"], 2);
8867        assert!(bounded["result"]["summary"]["first_message"].is_object());
8868        assert!(bounded["result"]["summary"]["last_message"].is_object());
8869        assert_eq!(
8870            bounded["result"]["session"]["messages"]
8871                .as_array()
8872                .unwrap()
8873                .len(),
8874            2
8875        );
8876        assert!(bounded["result"]["session"]["subagents"]
8877            .as_array()
8878            .unwrap()
8879            .is_empty());
8880
8881        let tail = service.handle(request(
8882            2,
8883            "harness.v1.sessions.load",
8884            json!({"locator": locator, "options": {"message_tail": 1}}),
8885        ));
8886        assert_eq!(tail["result"]["window"]["returned"], 1);
8887        assert_eq!(tail["result"]["window"]["has_more"], true);
8888        assert_eq!(tail["result"]["window"]["has_older"], true);
8889        assert!(tail["result"]["window"]["older_items"].as_u64().unwrap() > 0);
8890        assert!(tail["result"]["summary"]["first_message"].is_object());
8891
8892        let metadata_only = service.handle(request(
8893            3,
8894            "harness.v1.sessions.load",
8895            json!({"locator": locator, "options": {"inline_media": "metadata"}}),
8896        ));
8897        assert!(metadata_only["result"]["session"]
8898            .to_string()
8899            .contains("media_reference"));
8900        assert!(!metadata_only["result"]["session"]
8901            .to_string()
8902            .contains("data:image/"));
8903    }
8904
8905    #[test]
8906    fn import_translate_branch_and_handoff_use_typed_artifacts() {
8907        let mut service = HarnessSessionService::new();
8908        let locator = pi_locator();
8909        let translated = service.handle(request(
8910            1,
8911            "harness.v1.sessions.translate",
8912            json!({"locator": locator, "target_harness": "grok"}),
8913        ));
8914        assert_eq!(translated["result"]["artifact"]["source_harness"], "pi");
8915        assert_eq!(translated["result"]["artifact"]["target_harness"], "grok");
8916        assert!(translated["result"]["artifact"]["content"]
8917            .as_str()
8918            .is_some_and(|content| !content.is_empty()));
8919
8920        for target in ["opencode", "open-code"] {
8921            let opencode = service.handle(request(
8922                6,
8923                "harness.v1.sessions.translate",
8924                json!({"locator": locator, "target_harness": target}),
8925            ));
8926            assert_eq!(opencode["result"]["artifact"]["target_harness"], "opencode");
8927        }
8928        let goose = service.handle(request(
8929            7,
8930            "harness.v1.sessions.translate",
8931            json!({"locator": locator, "target_harness": "goose"}),
8932        ));
8933        assert_eq!(goose["result"]["artifact"]["target_harness"], "goose");
8934        assert!(serde_json::from_str::<Value>(
8935            goose["result"]["artifact"]["content"].as_str().unwrap()
8936        )
8937        .unwrap()["conversation"]
8938            .is_array());
8939
8940        let imported = service.handle(request(
8941            2,
8942            "harness.v1.sessions.import",
8943            json!({
8944                "source_harness": "grok",
8945                "content": translated["result"]["artifact"]["content"],
8946            }),
8947        ));
8948        assert_eq!(imported["result"]["session"]["source"], "grok");
8949
8950        let branched = service.handle(request(
8951            3,
8952            "harness.v1.sessions.branch",
8953            json!({"locator": locator, "target_harness": "codex"}),
8954        ));
8955        assert_eq!(branched["result"]["parent"]["harness"], "pi");
8956        assert!(branched["result"]["bootstrap_prompt"]
8957            .as_str()
8958            .unwrap()
8959            .contains("frozen parent transcript"));
8960        assert_eq!(branched["result"]["artifact"]["target_harness"], "codex");
8961
8962        let handoff = service.handle(request(
8963            4,
8964            "harness.v1.sessions.handoff",
8965            json!({"locator": locator, "target_harness": "pi", "cwd": "/tmp/project"}),
8966        ));
8967        assert_eq!(handoff["result"]["launch"]["program"], "pi");
8968        assert_eq!(handoff["result"]["launch"]["cwd"], "/tmp/project");
8969        assert_eq!(handoff["result"]["requires_materialization"], true);
8970
8971        let goose_handoff = service.handle(request(
8972            8,
8973            "harness.v1.sessions.handoff",
8974            json!({"locator": locator, "target_harness": "goose", "cwd": "/tmp/project"}),
8975        ));
8976        assert_eq!(goose_handoff["result"]["launch"]["program"], "goose");
8977        assert_eq!(
8978            goose_handoff["result"]["materialize"]["arguments"],
8979            json!(["session", "import", "{artifact_path}"])
8980        );
8981
8982        let resumed = service.handle(request(
8983            5,
8984            "harness.v1.sessions.resume_instructions",
8985            json!({"locator": locator, "cwd": "/tmp/project", "policy": "yolo"}),
8986        ));
8987        assert_eq!(resumed["result"]["launch"]["program"], "pi");
8988        assert_eq!(resumed["result"]["launch"]["arguments"][0], "--approve");
8989    }
8990
8991    #[test]
8992    fn reduce_persists_and_reloads_a_byte_exact_reversible_bundle() {
8993        let temp = std::env::temp_dir().join(format!(
8994            "supercode-service-reduce-{}-{}",
8995            std::process::id(),
8996            generated_session_id()
8997        ));
8998        let source_path = temp.join("source.jsonl");
8999        let store_root = temp.join("store");
9000        std::fs::create_dir_all(&temp).unwrap();
9001
9002        let mut records = vec![json!({
9003            "timestamp": "2026-01-01T00:00:00Z",
9004            "type": "session_meta",
9005            "payload": {"id": "codex-reduce", "cwd": "/tmp/project"},
9006        })];
9007        for turn in 0..16 {
9008            records.push(json!({
9009                "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 1),
9010                "type": "response_item",
9011                "payload": {
9012                    "type": "message",
9013                    "role": "user",
9014                    "content": [{
9015                        "type": "input_text",
9016                        "text": format!("request {turn}: {}", "context ".repeat(80)),
9017                    }],
9018                },
9019            }));
9020            records.push(json!({
9021                "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 2),
9022                "type": "response_item",
9023                "payload": {
9024                    "type": "message",
9025                    "role": "assistant",
9026                    "content": [{
9027                        "type": "output_text",
9028                        "text": format!("answer {turn}: {}", "implementation detail ".repeat(80)),
9029                    }],
9030                },
9031            }));
9032        }
9033        let source = format!(
9034            "{}\n",
9035            records
9036                .iter()
9037                .map(Value::to_string)
9038                .collect::<Vec<_>>()
9039                .join("\n")
9040        );
9041        std::fs::write(&source_path, &source).unwrap();
9042        let locator = SessionLocator {
9043            harness: HarnessId::from(HarnessId::CODEX),
9044            session_id: "codex-reduce".into(),
9045            storage: StorageLocator::File {
9046                path: source_path.clone(),
9047            },
9048        };
9049        let original = load_session(&locator).unwrap();
9050        let mut service =
9051            HarnessSessionService::new().with_reduction_store_root(store_root.clone());
9052
9053        let response = service.handle(request(
9054            1,
9055            "harness.v1.sessions.reduce",
9056            json!({
9057                "locator": locator,
9058                "target_harness": "claude-code",
9059                "keep_last": 4,
9060            }),
9061        ));
9062        assert!(response.get("error").is_none(), "{response:#}");
9063        let receipt = &response["result"]["receipt"];
9064        assert_eq!(receipt["source_harness"], "codex");
9065        assert_eq!(receipt["target_harness"], "claude-code");
9066        assert_eq!(receipt["verified"], true);
9067        assert_eq!(receipt["reversible"], true);
9068        assert!(receipt["reductions"].as_u64().unwrap() > 0);
9069        assert!(
9070            receipt["source_tokens"].as_u64().unwrap()
9071                > receipt["reduced_tokens"].as_u64().unwrap()
9072        );
9073        assert!(receipt["ratio"].as_f64().unwrap() > 1.0);
9074        assert!(response["result"]["bootstrap_prompt"]
9075            .as_str()
9076            .unwrap()
9077            .contains("Do not guess hidden content"));
9078
9079        let rescue_id = receipt["id"].as_str().unwrap();
9080        let store = crate::SessionStore::open(&store_root).unwrap();
9081        let sidecar =
9082            Session::from_sidecar_str(&store.load_sidecar(rescue_id).unwrap().unwrap()).unwrap();
9083        let log = store.load_reduction_log(rescue_id).unwrap().unwrap();
9084        let persisted_view = parse_messages_jsonl(&store.load(rescue_id).unwrap()).unwrap();
9085        let policy = reduce::ReductionPolicy {
9086            clear_turns_older_than: Some(4),
9087            ..Default::default()
9088        };
9089        let (restamped_view, reapplied_log) =
9090            reduce::project_messages(&sidecar.messages, &policy, &log);
9091        assert_eq!(
9092            messages_jsonl(&persisted_view).unwrap(),
9093            messages_jsonl(&restamped_view).unwrap()
9094        );
9095        assert_eq!(reapplied_log, log);
9096        reduce::verify_log(&log, &sidecar).unwrap();
9097        assert_eq!(
9098            reduce::invert(&restamped_view, &log, &sidecar).unwrap(),
9099            original.messages
9100        );
9101        assert_eq!(std::fs::read_to_string(&source_path).unwrap(), source);
9102
9103        std::fs::remove_dir_all(temp).ok();
9104    }
9105
9106    #[test]
9107    fn read_surfaces_view_a_severed_claude_graph_while_transfer_still_refuses_it() {
9108        let temp = std::env::temp_dir().join(format!(
9109            "supercode-severed-view-{}-{}",
9110            std::process::id(),
9111            generated_session_id()
9112        ));
9113        std::fs::create_dir_all(&temp).unwrap();
9114        let path = temp.join("severed.jsonl");
9115        // A live record whose parent was pruned — what a compacted or
9116        // resumed-across-files Claude Code session looks like on disk.
9117        std::fs::write(
9118            &path,
9119            concat!(
9120                r#"{"type":"user","uuid":"orphan-u","parentUuid":null,"message":{"role":"user","content":"stranded prompt"}}"#,
9121                "\n",
9122                r#"{"type":"assistant","uuid":"live-a","parentUuid":"pruned","message":{"id":"m","role":"assistant","content":[{"type":"text","text":"live answer"}]}}"#,
9123                "\n",
9124            ),
9125        )
9126        .unwrap();
9127        let locator = SessionLocator {
9128            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9129            session_id: "severed".into(),
9130            storage: StorageLocator::File { path },
9131        };
9132        let mut service = HarnessSessionService::new();
9133
9134        let viewed = service.handle(request(
9135            1,
9136            "harness.v1.sessions.load",
9137            json!({"locator": locator}),
9138        ));
9139        let session = &viewed["result"]["session"];
9140        assert_eq!(session["fidelity"], "semantic");
9141        assert_eq!(session["messages"].as_array().unwrap().len(), 2);
9142        assert!(session["residue"].as_array().unwrap().iter().any(|entry| {
9143            entry
9144                .as_str()
9145                .is_some_and(|entry| entry.contains("live-a") && entry.contains("pruned"))
9146        }));
9147
9148        // Asking a READ surface for a lossless reconstruction gets the strict
9149        // refusal back, unchanged.
9150        let strict = service.handle(request(
9151            2,
9152            "harness.v1.sessions.load",
9153            json!({"locator": locator, "fidelity": "byte_lossless"}),
9154        ));
9155        assert!(strict["error"]["message"]
9156            .as_str()
9157            .unwrap()
9158            .contains("cannot reconstruct lossless Claude continuation"));
9159
9160        // Transfer/continuation surfaces have no view mode at all.
9161        let translated = service.handle(request(
9162            3,
9163            "harness.v1.sessions.translate",
9164            json!({"locator": locator, "target_harness": "codex"}),
9165        ));
9166        assert!(translated["error"]["message"]
9167            .as_str()
9168            .unwrap()
9169            .contains("cannot reconstruct lossless Claude continuation"));
9170        let resumed = service.handle(request(
9171            4,
9172            "harness.v1.sessions.resume_instructions",
9173            json!({"locator": locator}),
9174        ));
9175        assert!(resumed["error"]["message"]
9176            .as_str()
9177            .unwrap()
9178            .contains("cannot reconstruct lossless Claude continuation"));
9179
9180        let _ = std::fs::remove_dir_all(&temp);
9181    }
9182
9183    #[test]
9184    fn structured_resume_launches_cover_gemini_goose_and_supercode() {
9185        let codex = resume_launch(
9186            HarnessId::CODEX,
9187            "codex-session",
9188            Path::new("/tmp/project"),
9189            ResumePolicy::Yolo,
9190        )
9191        .unwrap_or_else(|_| panic!("Codex resume launch must be registered"));
9192        assert_eq!(codex.program, "codex");
9193        assert_eq!(
9194            codex.arguments,
9195            [
9196                "-c",
9197                "check_for_update_on_startup=false",
9198                "-c",
9199                "projects.\"/tmp/project\".trust_level=\"trusted\"",
9200                "--dangerously-bypass-approvals-and-sandbox",
9201                "--dangerously-bypass-hook-trust",
9202                "resume",
9203                "codex-session",
9204            ]
9205        );
9206
9207        let gemini = resume_launch(
9208            HarnessId::GEMINI,
9209            "gemini-session",
9210            Path::new("/tmp/project"),
9211            ResumePolicy::Yolo,
9212        )
9213        .unwrap_or_else(|_| panic!("Gemini resume launch must be registered"));
9214        assert_eq!(gemini.program, "gemini");
9215        assert_eq!(gemini.arguments, ["--yolo", "--resume", "gemini-session"]);
9216
9217        let goose = resume_launch(
9218            HarnessId::GOOSE,
9219            "goose-session",
9220            Path::new("/tmp/project"),
9221            ResumePolicy::Yolo,
9222        )
9223        .unwrap_or_else(|_| panic!("Goose resume launch must be registered"));
9224        assert_eq!(goose.program, "goose");
9225        assert_eq!(
9226            goose.arguments,
9227            ["session", "--resume", "--session-id", "goose-session"]
9228        );
9229
9230        let supercode = resume_launch(
9231            HarnessId::SUPERCODE,
9232            "supercode-session",
9233            Path::new("/tmp/project"),
9234            ResumePolicy::Yolo,
9235        )
9236        .unwrap_or_else(|_| panic!("Supercode resume launch must be registered"));
9237        assert_eq!(supercode.program, "supercode");
9238        assert_eq!(
9239            supercode.arguments,
9240            ["--dangerous", "resume", "supercode-session"]
9241        );
9242    }
9243
9244    #[test]
9245    fn diagonal_artifacts_preserve_claude_subagents_and_grok_bundle_members() {
9246        let temp = std::env::temp_dir().join(format!(
9247            "supercode-harness-artifact-{}-{}",
9248            std::process::id(),
9249            generated_session_id()
9250        ));
9251        let main_path = temp.join("parent.jsonl");
9252        let subagent_path = temp.join("parent/subagents/agent-child.jsonl");
9253        std::fs::create_dir_all(subagent_path.parent().unwrap()).unwrap();
9254        let fixture = std::fs::read_to_string(
9255            PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9256                .join("tests/fixtures/claude_code_session.jsonl"),
9257        )
9258        .unwrap();
9259        let parent = fixture.trim_end_matches('\n');
9260        let child = fixture.trim_end_matches('\n');
9261        std::fs::write(&main_path, parent).unwrap();
9262        std::fs::write(&subagent_path, child).unwrap();
9263        let locator = SessionLocator {
9264            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9265            session_id: "213bb148-51ea-453f-9206-f8b4b1168547".into(),
9266            storage: StorageLocator::File {
9267                path: main_path.clone(),
9268            },
9269        };
9270        let mut service = HarnessSessionService::new();
9271        let claude = service.handle(request(
9272            1,
9273            "harness.v1.sessions.translate",
9274            json!({"locator": locator, "target_harness": "claude-code"}),
9275        ));
9276        let artifact = &claude["result"]["artifact"];
9277        assert_eq!(artifact["fidelity"], "byte_lossless");
9278        assert_eq!(artifact["content"], parent);
9279        let files = artifact["files"].as_array().unwrap();
9280        assert!(files.iter().any(|file| {
9281            file["role"] == "subagent"
9282                && file["path"]
9283                    .as_str()
9284                    .is_some_and(|path| path.ends_with("/subagents/agent-child.jsonl"))
9285                && file["content"] == child
9286        }));
9287        assert!(!artifact["content"].as_str().unwrap().ends_with('\n'));
9288
9289        let grok = service.handle(request(
9290            2,
9291            "harness.v1.sessions.translate",
9292            json!({"locator": grok_locator(), "target_harness": "grok"}),
9293        ));
9294        let files = grok["result"]["artifact"]["files"].as_array().unwrap();
9295        for name in ["summary.json", "updates.jsonl"] {
9296            let expected = std::fs::read_to_string(
9297                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9298                    .join("tests/fixtures/grok_session")
9299                    .join(name),
9300            )
9301            .unwrap();
9302            assert!(files.iter().any(|file| {
9303                file["path"] == name && file["role"] == "bundle" && file["content"] == expected
9304            }));
9305        }
9306        std::fs::remove_dir_all(temp).ok();
9307    }
9308
9309    #[test]
9310    fn every_non_grok_handoff_mints_and_uses_a_fresh_target_identity() {
9311        let mut service = HarnessSessionService::new();
9312        let source = pi_locator();
9313        for (target, format) in [
9314            ("claude-code", SessionFormat::ClaudeCode),
9315            ("codex", SessionFormat::Codex),
9316            ("opencode", SessionFormat::OpenCode),
9317            ("pi", SessionFormat::Pi),
9318        ] {
9319            let result = service.handle(request(
9320                1,
9321                "harness.v1.sessions.handoff",
9322                json!({"locator": source, "target_harness": target, "cwd": "/tmp/project"}),
9323            ));
9324            let artifact = &result["result"]["artifact"];
9325            let target_id = artifact["session_id"].as_str().unwrap();
9326            assert_ne!(target_id, source.session_id, "{target}");
9327            let parsed = Session::load_str(artifact["content"].as_str().unwrap(), format).unwrap();
9328            assert_eq!(
9329                parsed.meta.session_id.as_deref(),
9330                Some(target_id),
9331                "{target}"
9332            );
9333            if target != "pi" {
9334                assert!(result["result"]["launch"]["arguments"]
9335                    .as_array()
9336                    .unwrap()
9337                    .iter()
9338                    .any(|argument| argument == target_id));
9339            }
9340            if target == "opencode" {
9341                assert!(target_id.starts_with("ses_"));
9342                fn assert_session_ids(value: &Value, target_id: &str) {
9343                    match value {
9344                        Value::Object(fields) => {
9345                            if let Some(session_id) = fields.get("sessionID") {
9346                                assert_eq!(session_id, target_id);
9347                            }
9348                            for child in fields.values() {
9349                                assert_session_ids(child, target_id);
9350                            }
9351                        }
9352                        Value::Array(values) => {
9353                            for child in values {
9354                                assert_session_ids(child, target_id);
9355                            }
9356                        }
9357                        _ => {}
9358                    }
9359                }
9360                let document: Value =
9361                    serde_json::from_str(artifact["content"].as_str().unwrap()).unwrap();
9362                assert_session_ids(&document, target_id);
9363            }
9364        }
9365
9366        let first = service.handle(request(
9367            2,
9368            "harness.v1.sessions.handoff",
9369            json!({"locator": source, "target_harness": "codex"}),
9370        ));
9371        let second = service.handle(request(
9372            3,
9373            "harness.v1.sessions.handoff",
9374            json!({"locator": source, "target_harness": "codex"}),
9375        ));
9376        assert_ne!(
9377            first["result"]["artifact"]["session_id"],
9378            second["result"]["artifact"]["session_id"]
9379        );
9380    }
9381
9382    #[test]
9383    fn grok_handoff_uses_the_official_importer_contract() {
9384        let mut service = HarnessSessionService::new();
9385        let source = opencode_locator();
9386        let response = service.handle(request(
9387            1,
9388            "harness.v1.sessions.handoff",
9389            json!({
9390                "locator": source,
9391                "target_harness": "grok",
9392                "cwd": "/tmp/grok-handoff-project",
9393            }),
9394        ));
9395        let result = &response["result"];
9396
9397        // The target is Grok, but the artifact truthfully names the Claude Code wire
9398        // format accepted by Grok's official importer. Raw Grok chat_history JSONL is
9399        // not a complete stock-resumable bundle.
9400        assert_eq!(result["artifact"]["target_harness"], "claude-code");
9401        assert!(result["artifact"]["suggested_filename"]
9402            .as_str()
9403            .unwrap()
9404            .ends_with(".grok-import.claude-code.jsonl"));
9405        let artifact = Session::load_str(
9406            result["artifact"]["content"].as_str().unwrap(),
9407            SessionFormat::ClaudeCode,
9408        )
9409        .unwrap();
9410        assert_eq!(
9411            artifact.meta.cwd.as_deref(),
9412            Some(Path::new("/tmp/grok-handoff-project"))
9413        );
9414        let target_session_id = artifact.meta.session_id.as_deref().unwrap();
9415        assert_eq!(target_session_id.len(), 36);
9416        assert_eq!(target_session_id.as_bytes()[14], b'4');
9417        assert_ne!(target_session_id, opencode_locator().session_id);
9418        assert_eq!(
9419            result["artifact"]["session_id"],
9420            artifact.meta.session_id.as_deref().unwrap()
9421        );
9422
9423        assert_eq!(
9424            result["materialize"]["arguments"],
9425            json!(["import", "--json", "{artifact_path}"])
9426        );
9427        assert_eq!(
9428            result["launch"]["arguments"],
9429            json!(["--resume", "{imported_session_id}", "--fork-session"])
9430        );
9431        assert!(result["note"]
9432            .as_str()
9433            .unwrap()
9434            .contains("outcome=imported"));
9435        assert!(!result["launch"]["arguments"]
9436            .as_array()
9437            .unwrap()
9438            .iter()
9439            .any(|argument| argument == &opencode_locator().session_id));
9440    }
9441
9442    #[tokio::test]
9443    async fn inventory_rejects_unknown_harnesses_and_runtime_attach_is_honest() {
9444        let mut service = HarnessSessionService::new();
9445        let inventory = service
9446            .handle_async(request(
9447                1,
9448                "harness.v1.harnesses.list",
9449                json!({"harnesses": ["missing"]}),
9450            ))
9451            .await;
9452        assert_eq!(inventory["error"]["code"], -32602);
9453
9454        let attached = service
9455            .handle_async(request(
9456                2,
9457                "harness.v1.runtimes.attach_existing",
9458                json!({"harness": "codex", "runtime_id": "thread-1"}),
9459            ))
9460            .await;
9461        assert_eq!(attached["error"]["code"], -32000);
9462        assert!(attached["error"]["message"]
9463            .as_str()
9464            .unwrap()
9465            .contains("runtimes.resume"));
9466    }
9467
9468    #[test]
9469    fn invalid_params_and_unknown_methods_use_json_rpc_errors() {
9470        let mut service = HarnessSessionService::new();
9471        let invalid = service.handle(request(1, "harness.v1.sessions.load", json!({})));
9472        assert_eq!(invalid["error"]["code"], -32602);
9473        let unknown = service.handle(request(2, "harness.v1.unknown", json!({})));
9474        assert_eq!(unknown["error"]["code"], -32601);
9475    }
9476
9477    #[cfg(unix)]
9478    #[tokio::test]
9479    // The test mutates process-wide harness environment and deliberately
9480    // holds the global test lock until every async runtime operation ends.
9481    #[allow(clippy::await_holding_lock)]
9482    async fn async_service_drives_a_generic_acp_runtime() {
9483        let _environment_guard = crate::live_runtime::test_environment_lock();
9484        let script = r#"
9485            i=0
9486            while IFS= read -r line; do
9487              i=$((i + 1))
9488              case "$i" in
9489                1) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{},"authMethods":[]}}' ;;
9490                2) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"svc_acp"}}' ;;
9491                3)
9492                  printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ok"}}}}'
9493                  printf '%s\n' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}'
9494                  ;;
9495                4)
9496                  printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"from terminal"}}}}'
9497                  printf '%s\n' '{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}'
9498                  ;;
9499              esac
9500            done
9501        "#;
9502        let mut service = HarnessSessionService::new();
9503        let started = service
9504            .handle_async(request(
9505                1,
9506                "harness.v1.runtimes.start",
9507                json!({
9508                    "harness": "codex",
9509                    "protocol": "acp",
9510                    "cwd": std::env::current_dir().unwrap(),
9511                    "launch": {"program": "/bin/sh", "arguments": ["-c", script], "env": {}},
9512                }),
9513            ))
9514            .await;
9515        assert_eq!(started["result"]["connection"], "runtime-1");
9516        assert_eq!(started["result"]["handle"]["runtime_id"], "svc_acp");
9517
9518        let terminal = service
9519            .handle_async(request(
9520                9,
9521                "harness.v1.runtimes.terminal_instructions",
9522                json!({"connection":"runtime-1"}),
9523            ))
9524            .await;
9525        let arguments = terminal["result"]["launch"]["arguments"]
9526            .as_array()
9527            .expect("hosted runtime should return terminal arguments");
9528        let endpoint_index = arguments
9529            .iter()
9530            .position(|value| value == "--endpoint")
9531            .expect("terminal command should use an opaque endpoint");
9532        let endpoint = LiveRuntimeEndpoint::parse(
9533            arguments[endpoint_index + 1]
9534                .as_str()
9535                .expect("endpoint argument should be text"),
9536        )
9537        .unwrap();
9538        assert!(!terminal.to_string().contains("Bearer"));
9539        let workspace = std::env::current_dir().unwrap();
9540        let receipt = resolve_live_runtime(
9541            &endpoint,
9542            &LiveRuntimeSource {
9543                harness: "codex".into(),
9544                session_id: "svc_acp".into(),
9545                workspace,
9546            },
9547        )
9548        .unwrap();
9549        let remote = crate::HttpFrontendRuntime::connect(receipt.base_url, receipt.token)
9550            .await
9551            .unwrap();
9552        let mut attachment = crate::FrontendRuntime::attach(remote.as_ref(), 100)
9553            .await
9554            .unwrap();
9555
9556        let sent = service
9557            .handle_async(request(
9558                2,
9559                "harness.v1.runtimes.send_input",
9560                json!({"connection": "runtime-1", "text": "hi"}),
9561            ))
9562            .await;
9563        assert_eq!(sent["result"]["turn_id"], "3");
9564
9565        let mut events = Vec::new();
9566        for _ in 0..20 {
9567            events.extend(service.poll_runtimes().await);
9568            if events.len() >= 2 {
9569                break;
9570            }
9571            tokio::time::sleep(Duration::from_millis(2)).await;
9572        }
9573        assert!(events
9574            .iter()
9575            .any(|event| { event["params"]["event"]["kind"] == "session/update" }));
9576        assert!(events.iter().any(|event| {
9577            event["params"]["event"]["kind"] == "supercode/acp_request_completed"
9578        }));
9579
9580        let saw_editor_reply = tokio::time::timeout(Duration::from_secs(2), async {
9581            loop {
9582                let event = attachment.next_event().await.unwrap();
9583                if event.kind == "text_delta" && event.payload["text"] == "ok" {
9584                    break;
9585                }
9586            }
9587        })
9588        .await;
9589        assert!(
9590            saw_editor_reply.is_ok(),
9591            "terminal should observe the editor-driven turn"
9592        );
9593
9594        crate::FrontendRuntime::submit(remote.as_ref(), "DRIVE FROM TERMINAL".into())
9595            .await
9596            .unwrap();
9597        let saw_terminal_reply = tokio::time::timeout(Duration::from_secs(2), async {
9598            loop {
9599                let event = attachment.next_event().await.unwrap();
9600                if event.kind == "text_delta" && event.payload["text"] == "from terminal" {
9601                    break;
9602                }
9603            }
9604        })
9605        .await;
9606        assert!(
9607            saw_terminal_reply.is_ok(),
9608            "terminal should drive the same runtime"
9609        );
9610
9611        let closed = service
9612            .handle_async(request(
9613                3,
9614                "harness.v1.runtimes.close",
9615                json!({"connection": "runtime-1"}),
9616            ))
9617            .await;
9618        assert_eq!(closed["result"]["closed"], true);
9619    }
9620
9621    /// UNI-7 dev/02: a RUNNING mock gateway is detected through the real
9622    /// openclaw probe (config-declared endpoint, TCP connect), and an ACTIVE
9623    /// hermes WAL is detected through the real WAL-freshness probe; the
9624    /// negative sides (no listener, stale WAL, no config) stay undetected.
9625    #[test]
9626    fn running_instances_are_detected_from_mock_gateway_and_active_wal() {
9627        let home = connect_scratch_home("uni7-running");
9628
9629        // No config at all: hermes has no default endpoint, so no detection.
9630        // (openclaw's no-config behavior now probes its DOCUMENTED default
9631        // endpoint ws://127.0.0.1:18789 — see the connect launch's
9632        // `default_address` — which is real box state a hermetic test must
9633        // not assert either way; the closed-port negative below covers the
9634        // no-listener side deterministically.)
9635        assert!(probe_hermes_running(&home, 300_000).is_none());
9636
9637        // Mock gateway: a real TCP listener on an ephemeral port, declared in
9638        // the harness's own config file.
9639        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9640        let port = listener.local_addr().unwrap().port();
9641        std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9642        std::fs::write(
9643            home.join(".openclaw/openclaw.json"),
9644            format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9645        )
9646        .unwrap();
9647        let running = probe_openclaw_running(&home).expect("listening gateway must be detected");
9648        assert!(matches!(
9649            running.method,
9650            RunningInstanceMethod::GatewayConnect
9651        ));
9652        assert!(running.evidence.contains(&format!("127.0.0.1:{port}")));
9653        drop(listener);
9654        // Parallel tests also bind ephemeral loopback ports, so a just-freed
9655        // port can be re-bound by a NEIGHBORING test between drop and probe.
9656        // Detection on a closed port must fail — retry on a fresh port when
9657        // the freed one was recycled by someone else.
9658        let mut closed_detected = probe_openclaw_running(&home).is_some();
9659        for _ in 0..3 {
9660            if !closed_detected {
9661                break;
9662            }
9663            let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9664            let port = listener.local_addr().unwrap().port();
9665            drop(listener);
9666            std::fs::write(
9667                home.join(".openclaw/openclaw.json"),
9668                format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9669            )
9670            .unwrap();
9671            closed_detected = probe_openclaw_running(&home).is_some();
9672        }
9673        assert!(
9674            !closed_detected,
9675            "a closed gateway must not read as running"
9676        );
9677
9678        // gateway.url form takes precedence over port.
9679        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9680        let port = listener.local_addr().unwrap().port();
9681        std::fs::write(
9682            home.join(".openclaw/openclaw.json"),
9683            format!(r#"{{"gateway": {{"url": "ws://127.0.0.1:{port}", "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9684        )
9685        .unwrap();
9686        assert!(probe_openclaw_running(&home).is_some());
9687        drop(listener);
9688
9689        // Hermes: an ACTIVE WAL (fresh stamp) is detected; a stale one is not.
9690        std::fs::create_dir_all(home.join(".hermes")).unwrap();
9691        let wal = home.join(".hermes/state.db-wal");
9692        std::fs::write(&wal, b"wal").unwrap();
9693        let running = probe_hermes_running(&home, 300_000).expect("fresh WAL must be detected");
9694        assert!(matches!(
9695            running.method,
9696            RunningInstanceMethod::StoreWalActivity
9697        ));
9698        assert!(running.evidence.contains("state.db-wal"));
9699        let stale = std::time::SystemTime::now() - std::time::Duration::from_secs(3_600);
9700        std::fs::File::options()
9701            .append(true)
9702            .open(&wal)
9703            .unwrap()
9704            .set_modified(stale)
9705            .unwrap();
9706        assert!(
9707            probe_hermes_running(&home, 300_000).is_none(),
9708            "a stale WAL (crash leftover) must not read as running"
9709        );
9710    }
9711
9712    fn connect_scratch_home(tag: &str) -> PathBuf {
9713        let dir = std::env::temp_dir().join(format!(
9714            "supercode-connect-service-{tag}-{}-{}",
9715            std::process::id(),
9716            std::time::SystemTime::now()
9717                .duration_since(std::time::UNIX_EPOCH)
9718                .unwrap()
9719                .as_nanos()
9720        ));
9721        std::fs::create_dir_all(&dir).unwrap();
9722        dir
9723    }
9724
9725    /// Minimal HTTP responder that speaks just enough OpenCode server to
9726    /// accept a health check, create a session, and hold an SSE stream open,
9727    /// while recording each request line with its Authorization header.
9728    async fn mock_opencode_endpoint() -> (String, tokio::sync::mpsc::UnboundedReceiver<String>) {
9729        use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
9730        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
9731        let address = listener.local_addr().unwrap();
9732        let (request_sender, request_receiver) = tokio::sync::mpsc::unbounded_channel();
9733        tokio::spawn(async move {
9734            loop {
9735                let Ok((mut stream, _)) = listener.accept().await else {
9736                    break;
9737                };
9738                let request_sender = request_sender.clone();
9739                tokio::spawn(async move {
9740                    let (reader, mut writer) = stream.split();
9741                    let mut reader = BufReader::new(reader);
9742                    let mut request_line = String::new();
9743                    if reader.read_line(&mut request_line).await.unwrap_or(0) == 0 {
9744                        return;
9745                    }
9746                    let request_line = request_line.trim_end().to_string();
9747                    let mut authorization = String::new();
9748                    let mut content_length = 0usize;
9749                    loop {
9750                        let mut line = String::new();
9751                        if reader.read_line(&mut line).await.unwrap_or(0) == 0 {
9752                            return;
9753                        }
9754                        let line = line.trim_end();
9755                        if line.is_empty() {
9756                            break;
9757                        }
9758                        let lower = line.to_ascii_lowercase();
9759                        if let Some(value) = lower.strip_prefix("authorization:") {
9760                            authorization = value.trim().to_string();
9761                        }
9762                        if let Some(value) = lower.strip_prefix("content-length:") {
9763                            content_length = value.trim().parse().unwrap_or(0);
9764                        }
9765                    }
9766                    if content_length > 0 {
9767                        let mut body = vec![0u8; content_length];
9768                        let _ = reader.read_exact(&mut body).await;
9769                    }
9770                    let _ = request_sender.send(format!("{request_line} :: {authorization}"));
9771                    if request_line.starts_with("GET /event") {
9772                        let _ = writer
9773                            .write_all(
9774                                b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n\r\n",
9775                            )
9776                            .await;
9777                        tokio::time::sleep(std::time::Duration::from_secs(5)).await;
9778                        return;
9779                    }
9780                    let body = if request_line.starts_with("POST /session") {
9781                        r#"{"id":"mock-session"}"#
9782                    } else {
9783                        r#"{"status":"ok"}"#
9784                    };
9785                    let response = format!(
9786                        "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
9787                        body.len(),
9788                        body
9789                    );
9790                    let _ = writer.write_all(response.as_bytes()).await;
9791                });
9792            }
9793        });
9794        (format!("http://{address}"), request_receiver)
9795    }
9796
9797    fn connect_descriptor(protocol: &str) -> crate::HarnessSupportDescriptor {
9798        crate::HarnessSupportDescriptor {
9799            orchestration: Default::default(),
9800            id: HarnessId::from(HarnessId::OPENCODE),
9801            display_name: "OpenCode".into(),
9802            native: crate::NativeSupport {
9803                discover: crate::ImplementationKind::Absent,
9804                load: crate::ImplementationKind::Absent,
9805                follow: crate::ImplementationKind::Absent,
9806                import: crate::ImplementationKind::Absent,
9807                export: crate::ImplementationKind::Absent,
9808            },
9809            runtime: crate::RuntimeSupport {
9810                implementation: crate::ImplementationKind::BuiltIn,
9811                protocol: protocol.into(),
9812                default_launch: None,
9813                connect_launch: Some(crate::RuntimeConnectLaunch {
9814                    config_path: "~/opencode-tui.json".into(),
9815                    address_pointer: "/server/url".into(),
9816                    port_pointer: None,
9817                    default_address: None,
9818                    auth_pointer: Some("/server/token".into()),
9819                    protocol: protocol.into(),
9820                }),
9821                capabilities: crate::RuntimeCapabilities {
9822                    start_session: true,
9823                    resume_session: true,
9824                    attach_existing_process: true,
9825                    send_input: true,
9826                    stream_events: true,
9827                    interrupt: true,
9828                    steer: false,
9829                    respond_to_requests: true,
9830                },
9831            },
9832        }
9833    }
9834
9835    #[tokio::test]
9836    async fn connect_mode_descriptor_opens_a_running_endpoint_with_config_sourced_auth() {
9837        let (base_url, mut requests) = mock_opencode_endpoint().await;
9838        let home = connect_scratch_home("open");
9839        std::fs::write(
9840            home.join("opencode-tui.json"),
9841            format!(r#"{{"server": {{"url": "{base_url}", "token": "connect-secret"}}}}"#),
9842        )
9843        .unwrap();
9844
9845        let descriptor = connect_descriptor("opencode-http-sse");
9846        let backend = open_connect_descriptor(&descriptor, &home).unwrap();
9847        assert!(backend.capabilities().attach_existing_process);
9848
9849        let connection = backend
9850            .start(crate::RuntimeStartRequest {
9851                cwd: home.clone(),
9852                launch: None,
9853                mcp_servers: Vec::new(),
9854            })
9855            .await
9856            .unwrap();
9857        let handle = connection.handle();
9858        assert_eq!(handle.runtime_id, "mock-session");
9859        match &handle.endpoint {
9860            crate::RuntimeEndpoint::Http {
9861                base_url: endpoint, ..
9862            } => assert_eq!(endpoint, &base_url),
9863            other => panic!("connect mode must join the running endpoint, got {other:?}"),
9864        }
9865
9866        let mut seen = Vec::new();
9867        while let Ok(line) = requests.try_recv() {
9868            seen.push(line);
9869        }
9870        assert!(seen
9871            .iter()
9872            .any(|line| line.starts_with("GET /global/health")
9873                && line.contains("bearer connect-secret")));
9874        assert!(seen.iter().any(
9875            |line| line.starts_with("POST /session") && line.contains("bearer connect-secret")
9876        ));
9877    }
9878
9879    /// UNI-5 dev/02, contract corrected by the 2026-08-31 blind walk: the
9880    /// full connect-mode attach path against a MOCK gateway bridge — no live
9881    /// gateway, no model spend. A scripted fake `openclaw` binary (a)
9882    /// asserts the REAL bridge contract — the resolved --url on argv and the
9883    /// credential via --token-file (the real bridge ignores the env var; the
9884    /// endpoint comes from openclaw-native `gateway.remote.url`, never the
9885    /// schema-invalid `gateway.url`) — then (b) speaks scripted ACP:
9886    /// initialize advertising sessionCapabilities.{list,resume},
9887    /// session/resume rebinding the requested session (join), and a
9888    /// prompted turn.
9889    #[tokio::test]
9890    async fn openclaw_connect_mode_attaches_lists_and_resumes_via_a_mock_bridge() {
9891        let home = connect_scratch_home("openclaw");
9892        std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9893        std::fs::write(
9894            home.join(".openclaw/openclaw.json"),
9895            r#"{"gateway": {"remote": {"url": "ws://127.0.0.1:19789"}, "auth": {"mode": "token", "token": "mock-gateway-token"}}}"#,
9896        )
9897        .unwrap();
9898        let script = home.join("openclaw");
9899        std::fs::write(
9900            &script,
9901            r#"#!/bin/sh
9902# Fake `openclaw acp` bridge: verify the connect-mode contract, then speak ACP.
9903[ "$1" = "acp" ] || { echo "unexpected argv: $*" >&2; exit 9; }
9904[ "$2" = "--url" ] && [ "$3" = "ws://127.0.0.1:19789" ] || { echo "missing --url: $*" >&2; exit 9; }
9905[ "$4" = "--token-file" ] || { echo "missing --token-file: $*" >&2; exit 9; }
9906[ "$(cat "$5")" = "mock-gateway-token" ] || { echo "token file wrong" >&2; exit 9; }
9907while IFS= read -r line; do
9908  case "$line" in
9909    *'"initialize"'*)
9910      printf '%s
9911' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"sessionCapabilities":{"list":{},"resume":{}}},"agentInfo":{"name":"openclaw-acp","version":"2026.7.1-2"},"authMethods":[]}}' ;;
9912    *'"session/resume"'*)
9913      printf '%s
9914' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:main"}}' ;;
9915    *'"session/new"'*)
9916      printf '%s
9917' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:fresh"}}' ;;
9918    *'"session/prompt"'*)
9919      printf '%s
9920' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"agent:main:main","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"joined"}}}}'
9921      printf '%s
9922' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}' ;;
9923  esac
9924done
9925"#,
9926        )
9927        .unwrap();
9928        use std::os::unix::fs::PermissionsExt;
9929        std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
9930
9931        let mut descriptor = crate::harness_support_registry()
9932            .harnesses
9933            .into_iter()
9934            .find(|harness| harness.id.as_str() == HarnessId::OPENCLAW)
9935            .expect("openclaw must be registered");
9936        descriptor
9937            .runtime
9938            .connect_launch
9939            .as_mut()
9940            .unwrap()
9941            .config_path = "~/.openclaw/openclaw.json".into();
9942        descriptor.runtime.default_launch.as_mut().unwrap().program =
9943            script.to_string_lossy().into_owned();
9944        let backend = open_connect_descriptor(&descriptor, &home).unwrap();
9945        assert!(backend.capabilities().resume_session);
9946
9947        let joined = backend
9948            .attach(crate::RuntimeAttachRequest {
9949                runtime_id: "agent:main:main".into(),
9950                cwd: Some(home.clone()),
9951                launch: None,
9952                mcp_servers: Vec::new(),
9953            })
9954            .await;
9955        let mut connection = joined.expect("mock bridge attach must succeed");
9956        assert_eq!(connection.handle().runtime_id, "agent:main:main");
9957        let turn = connection
9958            .send_input(crate::RuntimeInput {
9959                text: "hello".into(),
9960                image_urls: Vec::new(),
9961            })
9962            .await;
9963        assert!(turn.is_ok(), "prompt through the mock bridge: {turn:?}");
9964        connection.close().await.unwrap();
9965    }
9966
9967    #[tokio::test]
9968    async fn connect_mode_fails_closed_without_a_protocol_client_or_config() {
9969        let home = connect_scratch_home("fail");
9970        std::fs::write(
9971            home.join("opencode-tui.json"),
9972            r#"{"server": {"url": "http://127.0.0.1:1", "token": "connect-secret"}}"#,
9973        )
9974        .unwrap();
9975
9976        let gateway_only = connect_descriptor("acp-v1-jsonrpc");
9977        let Err(error) = open_connect_descriptor(&gateway_only, &home) else {
9978            panic!("an ACP connect endpoint has no gateway client yet");
9979        };
9980        let message = format!("{error:?}");
9981        assert!(message.contains("acp-v1-jsonrpc"));
9982        assert!(!message.contains("connect-secret"));
9983
9984        let unreadable = connect_descriptor("opencode-http-sse");
9985        let missing_home = connect_scratch_home("missing");
9986        let Err(error) = open_connect_descriptor(&unreadable, &missing_home) else {
9987            panic!("an unreadable connect config must fail closed");
9988        };
9989        let message = format!("{error:?}");
9990        assert!(message.contains("opencode-tui.json"));
9991        assert!(!message.contains("connect-secret"));
9992    }
9993
9994    // ---------------------------------------------------------------------
9995    // ORCH-7 — `harness.v1.jobs.list` / `jobs.get` over the committed fixtures
9996    // ---------------------------------------------------------------------
9997
9998    fn jobs_fixture_root() -> PathBuf {
9999        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
10000    }
10001
10002    /// Point only the three job-bearing homes at the fixtures. Nothing else is
10003    /// read, so the host machine's own harness homes cannot leak into a row.
10004    fn jobs_fixture_homes() -> Value {
10005        let root = jobs_fixture_root();
10006        json!({
10007            "claude_code": root.join("claude_jobs_home/projects"),
10008            "hermes": root.join("hermes_home/state.db"),
10009            "openclaw": root.join("openclaw_home"),
10010        })
10011    }
10012
10013    fn jobs_list(params: Value) -> Value {
10014        let mut service = HarnessSessionService::new();
10015        service.handle(request(1, "harness.v1.jobs.list", params))
10016    }
10017
10018    fn job_row<'a>(result: &'a Value, id: &str) -> &'a Value {
10019        result["jobs"]
10020            .as_array()
10021            .expect("jobs is an array")
10022            .iter()
10023            .find(|job| job["id"] == id)
10024            .unwrap_or_else(|| panic!("no job `{id}` in {result}"))
10025    }
10026
10027    #[test]
10028    fn gateway_health_derives_from_running_probe_and_install_state() {
10029        let running = RunningInstance {
10030            method: RunningInstanceMethod::GatewayConnect,
10031            evidence: "gateway endpoint 127.0.0.1:18789 accepted a TCP connect".into(),
10032            checked_at_ms: 1,
10033        };
10034        let up = gateway_health(
10035            HarnessId::OPENCLAW,
10036            true,
10037            Some(&running),
10038            Some("2026.7.1-2"),
10039        );
10040        assert_eq!(up.state, GatewayState::Up);
10041        assert!(up.endpoint.as_deref().unwrap().starts_with("ws://"));
10042        assert_eq!(up.version.as_deref(), Some("2026.7.1-2"));
10043        // Hermes consults its own `gateway status` when the WAL heuristic says
10044        // nothing; a fake binary decides the verdict (the env var is global, so
10045        // the up/down cases run inside this one test, never in parallel).
10046        let dir = std::env::temp_dir().join(format!("supercode-orch17-{}", std::process::id()));
10047        std::fs::create_dir_all(&dir).unwrap();
10048        let fake = dir.join("hermes");
10049        let write_fake = |body: &str| {
10050            std::fs::write(&fake, format!("#!/bin/sh\n{body}\n")).unwrap();
10051            #[cfg(unix)]
10052            {
10053                use std::os::unix::fs::PermissionsExt;
10054                std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
10055            }
10056        };
10057        write_fake("echo '✗ Gateway service is not installed'");
10058        crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| {
10059            *slot.borrow_mut() = Some((
10060                HarnessId::HERMES.to_string(),
10061                fake.to_string_lossy().into_owned(),
10062            ))
10063        });
10064        let down = gateway_health(HarnessId::HERMES, true, None, None);
10065        assert_eq!(down.state, GatewayState::Down, "{down:?}");
10066        assert!(down.endpoint.is_none());
10067        assert!(down.evidence.contains("not installed"));
10068        write_fake("echo 'Launchd plist: /x/ai.hermes.gateway.plist'; echo '✓ Gateway is supervised by launchd (PID 4242)'");
10069        let idle_but_up = gateway_health(HarnessId::HERMES, true, None, Some("0.21.0"));
10070        assert_eq!(idle_but_up.state, GatewayState::Up, "{idle_but_up:?}");
10071        assert!(idle_but_up.evidence.contains("PID 4242"));
10072        write_fake("echo 'something unparseable'");
10073        let no_verdict = gateway_health(HarnessId::HERMES, true, None, None);
10074        assert_eq!(no_verdict.state, GatewayState::Down);
10075        assert!(no_verdict.evidence.contains("no verdict"));
10076        crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| *slot.borrow_mut() = None);
10077        let absent = gateway_health(HarnessId::HERMES, false, None, None);
10078        assert_eq!(absent.state, GatewayState::Unknown);
10079        let core = gateway_health(HarnessId::CODEX, true, None, Some("0.144.4"));
10080        assert_eq!(core.state, GatewayState::Unknown);
10081        assert!(core.evidence.contains("per session"));
10082    }
10083
10084    #[test]
10085    fn triggers_list_reads_both_stores_and_never_emits_secrets() {
10086        let response = triggers_list(json!({"homes": jobs_fixture_homes()}));
10087        let rows = response["result"]["triggers"]
10088            .as_array()
10089            .expect("triggers")
10090            .clone();
10091        let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10092        assert!(
10093            hermes.iter().any(|r| r["name"] == "deploys"
10094                && r["route"] == "/webhooks/deploys"
10095                && r["kind"] == "webhook"),
10096            "{rows:#?}"
10097        );
10098        let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10099        assert!(openclaw
10100            .iter()
10101            .any(|r| r["name"] == "wake" && r["kind"] == "builtin_wake"));
10102        assert!(openclaw.iter().any(|r| r["name"] == "gmail"
10103            && r["kind"] == "hook_mapping"
10104            && r["target"]["action"] == "agent"));
10105        let rendered = response.to_string();
10106        for secret in [
10107            "FAKE-WEBHOOK-HMAC-DO-NOT-EMIT",
10108            "FAKE-HOOK-TOKEN-DO-NOT-EMIT",
10109        ] {
10110            assert!(!rendered.contains(secret), "{rendered}");
10111        }
10112        let refused =
10113            triggers_list(json!({"harness": "claude-code", "homes": jobs_fixture_homes()}));
10114        assert_eq!(refused["error"]["code"], -32020, "{refused}");
10115    }
10116
10117    fn triggers_list(params: Value) -> Value {
10118        let mut service = HarnessSessionService::new();
10119        service.handle(request(1, "harness.v1.triggers.list", params))
10120    }
10121
10122    #[test]
10123    fn routes_list_reads_both_gateway_configs_and_flags_the_defaults() {
10124        let response = routes_list(json!({"homes": jobs_fixture_homes()}));
10125        let rows = response["result"]["routes"]
10126            .as_array()
10127            .expect("routes")
10128            .clone();
10129        let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10130        assert_eq!(hermes.len(), 2, "{rows:#?}");
10131        assert_eq!(hermes[0]["target"], "coder");
10132        assert_eq!(hermes[0]["match"]["platform"], "slack");
10133        assert_eq!(hermes[0]["match"]["chat_id"], "C0FIXTURE");
10134        assert_eq!(hermes[0]["specificity"], 4);
10135        assert_eq!(hermes[1]["default"], true);
10136        let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10137        assert!(
10138            openclaw.iter().any(|r| r["target"] == "design"
10139                && r["match"]["platform"] == "slack"
10140                && r["specificity"] == 1),
10141            "{openclaw:#?}"
10142        );
10143        assert!(openclaw.iter().any(|r| r["default"] == true));
10144        // A core harness has no routing concept and is refused, never an empty list.
10145        let refused = routes_list(json!({"harness": "codex", "homes": jobs_fixture_homes()}));
10146        assert_eq!(refused["error"]["code"], -32020, "{refused}");
10147    }
10148
10149    fn routes_list(params: Value) -> Value {
10150        let mut service = HarnessSessionService::new();
10151        service.handle(request(1, "harness.v1.routes.list", params))
10152    }
10153
10154    #[test]
10155    fn jobs_list_projects_every_fixture_store_onto_the_uniform_row() {
10156        let response = jobs_list(json!({"homes": jobs_fixture_homes()}));
10157        let result = &response["result"];
10158        let ids: Vec<&str> = result["jobs"]
10159            .as_array()
10160            .unwrap()
10161            .iter()
10162            .map(|job| job["id"].as_str().unwrap())
10163            .collect();
10164        assert_eq!(
10165            ids,
10166            vec![
10167                "release-watch",
10168                "toolu_wake_recheck",
10169                "digest-15m",
10170                "nightly-audit",
10171                "coder-standup",
10172                "ops-once-boot",
10173                "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10174                "8bb7d938-ca46-4a6d-90eb-c92331155566",
10175                "cron_standup",
10176                "cron_reindex",
10177            ],
10178            "{result}"
10179        );
10180
10181        // OpenClaw, pinned shape: rows come from `state/openclaw.sqlite`
10182        // (`cron_jobs.job_json` + runtime columns), captured from a real
10183        // 2026.7.1-2 gateway.
10184        let health = job_row(result, "85ad7832-896f-42be-af31-3e1ed2fbdc4b");
10185        assert_eq!(health["harness"], "openclaw");
10186        assert_eq!(health["schedule"]["kind"], "interval");
10187        assert_eq!(health["schedule"]["minutes"], 10.0);
10188        assert_eq!(health["session_target"], "isolated");
10189        assert_eq!(health["payload"]["kind"], "prompt");
10190        assert_eq!(health["payload"]["text"], "nightly health check");
10191        // ORCH-13: the mode word (`announce`) and the channel it announces on
10192        // (`last`) are separate facts, and the store keeps both — in
10193        // `job_json.delivery` and in the `delivery_*` columns beside it.
10194        assert_eq!(health["deliver"]["mode"], "announce");
10195        assert_eq!(health["deliver"]["target"], "last");
10196        assert_eq!(health["next_run_at"], "2026-09-03T06:52:26Z");
10197        let digest = job_row(result, "8bb7d938-ca46-4a6d-90eb-c92331155566");
10198        assert_eq!(digest["schedule"]["kind"], "cron");
10199        assert_eq!(digest["schedule"]["expr"], "0 9 * * 1");
10200        assert_eq!(digest["session_target"], "main");
10201        assert_eq!(digest["payload"]["kind"], "system_event");
10202
10203        // Claude Code: session-scoped, one recurring cron and one one-shot wakeup.
10204        let cron = job_row(result, "release-watch");
10205        assert_eq!(cron["harness"], "claude-code");
10206        assert_eq!(cron["scope"], "session");
10207        assert_eq!(cron["session_id"], "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f");
10208        assert_eq!(cron["schedule"]["kind"], "cron");
10209        assert_eq!(cron["schedule"]["expr"], "*/10 * * * *");
10210        assert_eq!(cron["schedule"]["display"], "*/10 * * * *");
10211        assert_eq!(cron["payload"]["kind"], "prompt");
10212        assert_eq!(cron["recurring"], true);
10213        assert_eq!(cron["deliver"]["target"], "session");
10214        let wakeup = job_row(result, "toolu_wake_recheck");
10215        assert_eq!(wakeup["payload"]["kind"], "wakeup");
10216        assert_eq!(wakeup["schedule"]["kind"], "once");
10217        assert_eq!(wakeup["recurring"], false);
10218        assert_eq!(wakeup["state"], "pending");
10219
10220        // Hermes: install-scoped, interval + origin delivery, and a paused cron.
10221        let interval = job_row(result, "digest-15m");
10222        assert_eq!(interval["harness"], "hermes");
10223        assert_eq!(interval["scope"], "install");
10224        assert_eq!(interval["profile"], Value::Null);
10225        assert_eq!(interval["schedule"]["kind"], "interval");
10226        assert_eq!(interval["schedule"]["minutes"], 15.0);
10227        assert_eq!(interval["schedule"]["display"], "every 15 min");
10228        assert_eq!(interval["deliver"]["target"], "origin");
10229        assert_eq!(interval["deliver"]["chat_id"], "-1002233445566");
10230        assert_eq!(interval["next_run_at"], "2026-09-02T11:15:00Z");
10231        assert_eq!(interval["last_status"], "ok");
10232        let nightly = job_row(result, "nightly-audit");
10233        assert_eq!(nightly["schedule"]["expr"], "0 3 * * *");
10234        assert_eq!(nightly["deliver"]["target"], "local");
10235        assert_eq!(nightly["enabled"], false);
10236        assert_eq!(nightly["state"], "paused");
10237        // The per-profile store carries the profile name from its own path.
10238        let profiled = job_row(result, "ops-once-boot");
10239        assert_eq!(profiled["profile"], "ops");
10240        assert_eq!(profiled["schedule"]["kind"], "once");
10241        assert_eq!(profiled["schedule"]["run_at"], "2026-09-03T06:00:00Z");
10242        assert_eq!(profiled["payload"]["kind"], "script");
10243        // An explicit `<platform>:<chat>` target carries the chat itself.
10244        assert_eq!(profiled["deliver"]["target"], "slack:C0429ABCD");
10245        assert_eq!(profiled["deliver"]["chat_id"], "C0429ABCD");
10246        assert_eq!(profiled["recurring"], false);
10247
10248        // ORCH-13: a job delivering to its creating conversation carries that
10249        // conversation's whole surface — platform word, chat AND thread.
10250        let standup_to_group = job_row(result, "coder-standup");
10251        assert_eq!(standup_to_group["deliver"]["target"], "origin");
10252        assert_eq!(standup_to_group["deliver"]["chat_id"], "-100777");
10253        assert_eq!(standup_to_group["deliver"]["thread_id"], "55");
10254        // Hermes has no mode word and routes by adapter profile, not account.
10255        assert!(standup_to_group["deliver"]["mode"].is_null());
10256        assert!(standup_to_group["deliver"]["account"].is_null());
10257
10258        // OpenClaw: the session target and the delivery mode are the row's own
10259        // columns, not a footnote.
10260        let standup = job_row(result, "cron_standup");
10261        assert_eq!(standup["harness"], "openclaw");
10262        assert_eq!(standup["session_target"], "isolated");
10263        assert_eq!(standup["deliver"]["mode"], "announce");
10264        assert_eq!(standup["deliver"]["target"], "slack");
10265        assert_eq!(standup["deliver"]["chat_id"], "C0429ABCD");
10266        assert_eq!(standup["payload"]["kind"], "prompt");
10267        assert_eq!(standup["profile"], "main");
10268        let reindex = job_row(result, "cron_reindex");
10269        assert_eq!(reindex["session_target"], "main");
10270        assert_eq!(reindex["payload"]["kind"], "system_event");
10271        assert_eq!(reindex["schedule"]["kind"], "interval");
10272        assert_eq!(reindex["schedule"]["display"], "every 240 min");
10273        assert_eq!(reindex["enabled"], false);
10274
10275        // Every store consulted is named, so an empty answer is never silent.
10276        let states: Vec<(&str, &str)> = result["sources"]
10277            .as_array()
10278            .unwrap()
10279            .iter()
10280            .map(|source| {
10281                (
10282                    source["harness"].as_str().unwrap(),
10283                    source["state"].as_str().unwrap(),
10284                )
10285            })
10286            .collect();
10287        // The `coder` profile home has no cron store at all: it is named as
10288        // `absent_store`, not skipped, so "this profile schedules nothing" and
10289        // "this profile was never looked at" stay distinguishable.
10290        assert_eq!(
10291            states,
10292            vec![
10293                ("claude-code", "scanned"),
10294                ("hermes", "read"),
10295                ("hermes", "absent_store"),
10296                ("hermes", "read"),
10297                ("openclaw", "read"),
10298                ("openclaw", "read"),
10299            ],
10300            "{result}"
10301        );
10302    }
10303
10304    #[test]
10305    fn jobs_list_filters_by_harness_session_and_profile() {
10306        let by_harness = jobs_list(json!({"harness": "openclaw", "homes": jobs_fixture_homes()}));
10307        let ids: Vec<&str> = by_harness["result"]["jobs"]
10308            .as_array()
10309            .unwrap()
10310            .iter()
10311            .map(|job| job["id"].as_str().unwrap())
10312            .collect();
10313        assert_eq!(
10314            ids,
10315            vec![
10316                "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10317                "8bb7d938-ca46-4a6d-90eb-c92331155566",
10318                "cron_standup",
10319                "cron_reindex",
10320            ]
10321        );
10322
10323        let by_session = jobs_list(json!({
10324            "session": "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
10325            "homes": jobs_fixture_homes(),
10326        }));
10327        let jobs = by_session["result"]["jobs"].as_array().unwrap();
10328        assert_eq!(jobs.len(), 2, "{by_session}");
10329        assert!(jobs
10330            .iter()
10331            .all(|job| job["harness"] == "claude-code" && job["scope"] == "session"));
10332
10333        let by_profile = jobs_list(json!({
10334            "harness": "hermes",
10335            "profile": "ops",
10336            "homes": jobs_fixture_homes(),
10337        }));
10338        let jobs = by_profile["result"]["jobs"].as_array().unwrap();
10339        assert_eq!(jobs.len(), 1, "{by_profile}");
10340        assert_eq!(jobs[0]["id"], "ops-once-boot");
10341    }
10342
10343    #[test]
10344    fn jobs_get_answers_with_the_row_and_the_verbatim_native_record() {
10345        let mut service = HarnessSessionService::new();
10346        let hermes = service.handle(request(
10347            1,
10348            "harness.v1.jobs.get",
10349            json!({"harness": "hermes", "id": "digest-15m", "homes": jobs_fixture_homes()}),
10350        ));
10351        assert_eq!(hermes["result"]["job"]["schedule"]["kind"], "interval");
10352        // Native fields the uniform row does not carry survive on `source`.
10353        assert_eq!(hermes["result"]["source"]["provider"], "nous");
10354        assert_eq!(hermes["result"]["source"]["failure_deliver"], "local");
10355
10356        let claude = service.handle(request(
10357            2,
10358            "harness.v1.jobs.get",
10359            json!({"harness": "claude-code", "id": "release-watch", "homes": jobs_fixture_homes()}),
10360        ));
10361        assert_eq!(claude["result"]["job"]["payload"]["kind"], "prompt");
10362        assert_eq!(
10363            claude["result"]["source"]["tool_use_id"],
10364            "toolu_cron_release_watch"
10365        );
10366
10367        let missing = service.handle(request(
10368            3,
10369            "harness.v1.jobs.get",
10370            json!({"harness": "hermes", "id": "no-such-job", "homes": jobs_fixture_homes()}),
10371        ));
10372        assert!(missing["error"]["message"]
10373            .as_str()
10374            .is_some_and(|message| message.contains("no scheduled job `no-such-job`")));
10375    }
10376
10377    #[test]
10378    fn jobs_refuse_a_harness_without_a_scheduled_job_concept() {
10379        let mut service = HarnessSessionService::new();
10380        for (id, method, params) in [
10381            (
10382                1,
10383                "harness.v1.jobs.list",
10384                json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10385            ),
10386            (
10387                2,
10388                "harness.v1.jobs.get",
10389                json!({"harness": "codex", "id": "anything"}),
10390            ),
10391        ] {
10392            let response = service.handle(request(id, method, params));
10393            assert_eq!(response["error"]["code"], -32020, "{response}");
10394            assert!(response["error"]["message"]
10395                .as_str()
10396                .is_some_and(|message| message.contains("has no scheduled jobs")));
10397            assert!(response.get("result").is_none());
10398        }
10399    }
10400
10401    #[test]
10402    fn jobs_list_reports_a_migrated_openclaw_store_as_absent_instead_of_failing() {
10403        let scratch = std::env::temp_dir().join(format!(
10404            "supercode-jobs-migrated-{}-{}",
10405            std::process::id(),
10406            generated_session_id()
10407        ));
10408        std::fs::create_dir_all(&scratch).unwrap();
10409        let response = jobs_list(json!({
10410            "harness": "openclaw",
10411            "homes": {"openclaw": scratch.clone()},
10412        }));
10413        let result = &response["result"];
10414        assert_eq!(result["jobs"].as_array().unwrap().len(), 0, "{result}");
10415        assert_eq!(result["sources"][0]["state"], "absent_store");
10416        assert_eq!(result["sources"][0]["harness"], "openclaw");
10417        std::fs::remove_dir_all(&scratch).ok();
10418    }
10419
10420    // ---------------------------------------------------------------------
10421    // ORCH-8 — `harness.v1.runs.list` / `runs.get` over the committed fire
10422    // stores: Hermes's `cron/executions.db` (root home + profile home) and
10423    // OpenClaw's `cron_run_logs`. Every fixture row is written by
10424    // `tests/fixtures/gen_runs_fixtures.py` against the harnesses' own DDL.
10425    // ---------------------------------------------------------------------
10426
10427    /// The health job in the committed OpenClaw fixture, which fired twice.
10428    const OPENCLAW_HEALTH_JOB: &str = "85ad7832-896f-42be-af31-3e1ed2fbdc4b";
10429    /// The digest job, whose single fire predates run ids.
10430    const OPENCLAW_DIGEST_JOB: &str = "8bb7d938-ca46-4a6d-90eb-c92331155566";
10431
10432    fn runs_list(params: Value) -> Value {
10433        let mut service = HarnessSessionService::new();
10434        service.handle(request(1, "harness.v1.runs.list", params))
10435    }
10436
10437    fn run_row<'a>(result: &'a Value, id: &str) -> &'a Value {
10438        result["runs"]
10439            .as_array()
10440            .expect("runs is an array")
10441            .iter()
10442            .find(|run| run["id"] == id)
10443            .unwrap_or_else(|| panic!("no run `{id}` in {result}"))
10444    }
10445
10446    #[test]
10447    fn runs_list_projects_both_fixture_stores_onto_the_uniform_row() {
10448        let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10449        let result = &response["result"];
10450        let ids: Vec<&str> = result["runs"]
10451            .as_array()
10452            .expect("runs is an array")
10453            .iter()
10454            .map(|run| run["id"].as_str().unwrap())
10455            .collect();
10456        let digest_fire = format!("{OPENCLAW_DIGEST_JOB}#1");
10457        assert_eq!(
10458            ids,
10459            vec![
10460                // Hermes, newest claim first, root ledger then profile ledger.
10461                "b2c3d4e5f60718293a4b5c6d7e8f9012",
10462                "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10463                "c3d4e5f60718293a4b5c6d7e8f901234",
10464                "f60718293a4b5c6d7e8f901234567890",
10465                "e5f60718293a4b5c6d7e8f9012345678",
10466                "d4e5f60718293a4b5c6d7e8f90123456",
10467                // OpenClaw, newest `ts` first.
10468                "run_health_0002",
10469                digest_fire.as_str(),
10470                "run_health_0001",
10471            ],
10472            "{result}"
10473        );
10474
10475        // The harness's OWN outcome word survives; nothing is renamed onto a
10476        // shared vocabulary.
10477        let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10478        assert_eq!(failed["harness"], "hermes");
10479        assert_eq!(failed["job_id"], "job42");
10480        assert_eq!(failed["status"], "failed");
10481        assert_eq!(failed["error"], "provider returned 500 after 3 attempts");
10482        assert_eq!(failed["claimed_at"], "2026-09-02T13:05:00.100442");
10483
10484        // Hermes's `unknown` — an attempt whose owner died before writing a
10485        // terminal state — is a fourth status, not folded into `failed`.
10486        let abandoned = run_row(result, "d4e5f60718293a4b5c6d7e8f90123456");
10487        assert_eq!(abandoned["status"], "unknown");
10488        assert_eq!(abandoned["job_id"], "ops-once-boot");
10489
10490        // An unterminated fire has no finish, and no session is invented.
10491        let running = run_row(result, "c3d4e5f60718293a4b5c6d7e8f901234");
10492        assert_eq!(running["status"], "running");
10493        assert!(running["finished_at"].is_null(), "{running}");
10494        assert!(running["session_id"].is_null(), "{running}");
10495
10496        // OpenClaw records the session on the row itself, and epoch-ms
10497        // timestamps are rendered as RFC 3339.
10498        let ok = run_row(result, "run_health_0001");
10499        assert_eq!(ok["harness"], "openclaw");
10500        assert_eq!(ok["job_id"], OPENCLAW_HEALTH_JOB);
10501        assert_eq!(ok["status"], "ok");
10502        assert_eq!(ok["started_at"], "2026-09-02T08:30:00.000Z");
10503        assert_eq!(ok["finished_at"], "2026-09-02T08:30:30.000Z");
10504        assert_eq!(ok["session_id"], "3dd577ae-a0a3-4b5b-8063-f402be4f5fd4");
10505        // OpenClaw's run log is written once, at finish: there is no claim.
10506        assert!(ok["claimed_at"].is_null(), "{ok}");
10507
10508        // A run-log row with no `run_id` falls back to the store's own
10509        // `(job_id, seq)` key rather than being dropped.
10510        assert_eq!(run_row(result, &digest_fire)["status"], "skipped");
10511
10512        // ORCH-13: a fire whose delivery nothing recorded says so, rather than
10513        // borrowing a neighbouring fire's outcome. Both of these ran on jobs
10514        // that deliver `local` (or have no job record at all), so no
10515        // obligation is addressed to a surface they could match.
10516        for id in [
10517            "b2c3d4e5f60718293a4b5c6d7e8f9012",
10518            "d4e5f60718293a4b5c6d7e8f90123456",
10519        ] {
10520            assert!(run_row(result, id)["delivery"].is_null(), "{id}");
10521        }
10522
10523        // Every store consulted is named, including the profile home that has
10524        // no ledger — an empty history and an absent store are different.
10525        let sources = result["sources"].as_array().unwrap();
10526        let states: Vec<(&str, &str)> = sources
10527            .iter()
10528            .map(|source| {
10529                (
10530                    source["harness"].as_str().unwrap(),
10531                    source["state"].as_str().unwrap(),
10532                )
10533            })
10534            .collect();
10535        assert_eq!(
10536            states,
10537            vec![
10538                ("hermes", "read"),
10539                ("hermes", "absent_store"),
10540                ("hermes", "read"),
10541                ("openclaw", "read"),
10542            ],
10543            "{result}"
10544        );
10545        assert_eq!(sources[2]["profile"], "ops");
10546        assert!(sources[3]["path"]
10547            .as_str()
10548            .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10549    }
10550
10551    #[test]
10552    fn runs_list_joins_a_hermes_fire_to_the_session_it_opened() {
10553        let response = runs_list(json!({
10554            "harness": "hermes",
10555            "job": "job42",
10556            "homes": jobs_fixture_homes(),
10557        }));
10558        let result = &response["result"];
10559        assert_eq!(result["runs"].as_array().unwrap().len(), 2, "{result}");
10560
10561        // Hermes writes NO link from an execution to its session. The fire
10562        // that ran the agent is joined to `cron_job42_<stamp>` because that
10563        // id's instant falls inside its [claimed_at, finished_at] window.
10564        let ran = run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90");
10565        assert_eq!(ran["session_id"], "cron_job42_20260902_120000");
10566
10567        // The later fire failed before opening one. Its window holds no
10568        // session, so the row says so instead of re-using the earlier fire's
10569        // — the join is per-FIRE, not per-job.
10570        let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10571        assert!(failed["session_id"].is_null(), "{failed}");
10572    }
10573
10574    /// ORCH-13: where a fire's output went, read from each harness's own
10575    /// delivery record — Hermes's `delivery_obligations` ledger inside
10576    /// `state.db`, OpenClaw's `delivery_*` run-log columns.
10577    #[test]
10578    fn runs_list_reads_the_delivery_each_harness_recorded_for_a_fire() {
10579        let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10580        let result = &response["result"];
10581
10582        // Hermes: the ledger is the GATEWAY's, keyed by conversation and
10583        // surface, so the fire's own [claimed_at, finished_at] window picks
10584        // the obligation. The fire succeeded and so did the send.
10585        let delivered = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10586        assert_eq!(delivered["status"], "completed");
10587        assert_eq!(delivered["delivery"]["state"], "delivered");
10588        assert_eq!(delivered["delivery"]["target"], "telegram:-100777:55");
10589        assert_eq!(delivered["delivery"]["attempts"], 1);
10590        assert!(delivered["delivery"]["last_error"].is_null(), "{delivered}");
10591        assert_eq!(
10592            delivered["delivery"]["delivered_at"],
10593            "2026-09-02T09:00:30.400Z"
10594        );
10595
10596        // The next fire of the same job ALSO succeeded — and its output never
10597        // arrived. That is the fact `status` alone cannot carry.
10598        let undelivered = run_row(result, "f60718293a4b5c6d7e8f901234567890");
10599        assert_eq!(undelivered["status"], "completed");
10600        assert_eq!(undelivered["delivery"]["state"], "failed");
10601        assert_eq!(undelivered["delivery"]["attempts"], 3);
10602        assert_eq!(
10603            undelivered["delivery"]["last_error"],
10604            "telegram send failed: Bad Request: chat not found"
10605        );
10606        // Only a delivered obligation carries an instant of delivery; the
10607        // ledger's `updated_at` on a failed row dates the failure.
10608        assert!(
10609            undelivered["delivery"]["delivered_at"].is_null(),
10610            "{undelivered}"
10611        );
10612
10613        // OpenClaw writes the outcome onto the run-log row and declares the
10614        // address on the job, so the row's target is joined from `cron_jobs`.
10615        let announced = run_row(result, "run_health_0001");
10616        assert_eq!(announced["delivery"]["state"], "delivered");
10617        assert_eq!(announced["delivery"]["target"], "last");
10618        // Its run log counts no attempts and stamps no delivered-at.
10619        assert!(announced["delivery"]["attempts"].is_null(), "{announced}");
10620        assert!(
10621            announced["delivery"]["delivered_at"].is_null(),
10622            "{announced}"
10623        );
10624        let refused = run_row(result, "run_health_0002");
10625        assert_eq!(refused["delivery"]["state"], "not-delivered");
10626        assert_eq!(refused["delivery"]["last_error"], "channel_not_found");
10627
10628        // A run-log row with no delivery columns at all recorded no delivery:
10629        // the job's declared target is not evidence that anything was sent.
10630        let skipped = run_row(result, &format!("{OPENCLAW_DIGEST_JOB}#1"));
10631        assert!(skipped["delivery"].is_null(), "{skipped}");
10632    }
10633
10634    /// A Hermes fire whose session carries a `session_key` is matched on that
10635    /// key FIRST — the most specific question the ledger can answer. Proven by
10636    /// moving the obligations off the job's surface on a COPY of the fixture,
10637    /// so only the session-key question can still find them.
10638    #[test]
10639    fn runs_list_matches_a_hermes_obligation_by_the_session_key_first() {
10640        let scratch = std::env::temp_dir().join(format!(
10641            "supercode-runs-delivery-{}-{}",
10642            std::process::id(),
10643            generated_session_id()
10644        ));
10645        std::fs::create_dir_all(scratch.join("cron")).unwrap();
10646        let fixture = jobs_fixture_root().join("hermes_home");
10647        std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10648        for name in ["cron/executions.db", "cron/jobs.json"] {
10649            std::fs::copy(fixture.join(name), scratch.join(name)).unwrap();
10650        }
10651        {
10652            let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10653            // The obligations now sit on a surface no job in this store
10654            // delivers to, so the surface question cannot match them.
10655            connection
10656                .execute(
10657                    "UPDATE delivery_obligations SET platform = 'slack', chat_id = 'C0FALLBACK'",
10658                    [],
10659                )
10660                .unwrap();
10661            // A cron fire that ran inside a keyed conversation: the session
10662            // the window recovers carries `tg-coder-1`'s key.
10663            connection
10664                .execute(
10665                    "INSERT INTO sessions (id, source, session_key, started_at) VALUES \
10666                     ('cron_coder-standup_20260902_090010', 'cron', \
10667                      'agent:coder:telegram:group:-100777:55', 1788339610.0)",
10668                    [],
10669                )
10670                .unwrap();
10671        }
10672        let response = runs_list(json!({
10673            "harness": "hermes",
10674            "job": "coder-standup",
10675            "homes": {"hermes": scratch.join("state.db")},
10676        }));
10677        let result = &response["result"];
10678        let matched = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10679        assert_eq!(
10680            matched["session_id"], "cron_coder-standup_20260902_090010",
10681            "{result}"
10682        );
10683        assert_eq!(matched["delivery"]["state"], "delivered", "{result}");
10684        assert_eq!(
10685            matched["delivery"]["target"], "slack:C0FALLBACK:55",
10686            "{result}"
10687        );
10688        std::fs::remove_dir_all(&scratch).ok();
10689    }
10690
10691    #[test]
10692    fn runs_list_follows_a_compression_chain_to_the_readable_tip() {
10693        // A fire whose session was compressed mid-run is only readable at the
10694        // continuation, so that is what the row must report. Built on a COPY
10695        // of the committed fixture: no test writes to a fixture or to a real
10696        // harness home.
10697        let scratch = std::env::temp_dir().join(format!(
10698            "supercode-runs-compressed-{}-{}",
10699            std::process::id(),
10700            generated_session_id()
10701        ));
10702        std::fs::create_dir_all(scratch.join("cron")).unwrap();
10703        let fixture = jobs_fixture_root().join("hermes_home");
10704        std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10705        std::fs::copy(
10706            fixture.join("cron/executions.db"),
10707            scratch.join("cron/executions.db"),
10708        )
10709        .unwrap();
10710        {
10711            let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10712            connection
10713                .execute(
10714                    "UPDATE sessions SET end_reason = 'compression' WHERE id = ?1",
10715                    ["cron_job42_20260902_120000"],
10716                )
10717                .unwrap();
10718            connection
10719                .execute(
10720                    "INSERT INTO sessions (id, source, parent_session_id, started_at) \
10721                     VALUES ('job42-after-compaction', 'cron', \
10722                             'cron_job42_20260902_120000', 1788350000.0)",
10723                    [],
10724                )
10725                .unwrap();
10726        }
10727        let response = runs_list(json!({
10728            "harness": "hermes",
10729            "job": "job42",
10730            "homes": {"hermes": scratch.join("state.db")},
10731        }));
10732        let result = &response["result"];
10733        assert_eq!(
10734            run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90")["session_id"],
10735            "job42-after-compaction",
10736            "{result}"
10737        );
10738        std::fs::remove_dir_all(&scratch).ok();
10739    }
10740
10741    #[test]
10742    fn runs_list_filters_by_job_and_caps_by_limit() {
10743        let by_job = runs_list(json!({
10744            "harness": "openclaw",
10745            "job": OPENCLAW_HEALTH_JOB,
10746            "homes": jobs_fixture_homes(),
10747        }));
10748        let ids: Vec<&str> = by_job["result"]["runs"]
10749            .as_array()
10750            .unwrap()
10751            .iter()
10752            .map(|run| run["id"].as_str().unwrap())
10753            .collect();
10754        assert_eq!(ids, vec!["run_health_0002", "run_health_0001"], "{by_job}");
10755
10756        let capped = runs_list(json!({
10757            "harness": "openclaw",
10758            "limit": 1,
10759            "homes": jobs_fixture_homes(),
10760        }));
10761        let runs = capped["result"]["runs"].as_array().unwrap();
10762        assert_eq!(runs.len(), 1, "{capped}");
10763        // Newest first, so the cap keeps the recent fire.
10764        assert_eq!(runs[0]["id"], "run_health_0002");
10765    }
10766
10767    #[test]
10768    fn runs_get_answers_with_the_row_and_the_verbatim_native_record() {
10769        let mut service = HarnessSessionService::new();
10770        let hermes = service.handle(request(
10771            1,
10772            "harness.v1.runs.get",
10773            json!({
10774                "harness": "hermes",
10775                "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10776                "homes": jobs_fixture_homes(),
10777            }),
10778        ));
10779        assert_eq!(hermes["result"]["run"]["status"], "completed");
10780        assert_eq!(
10781            hermes["result"]["run"]["session_id"],
10782            "cron_job42_20260902_120000"
10783        );
10784        // Ledger columns the uniform row does not carry survive on `source`.
10785        assert_eq!(hermes["result"]["source"]["source"], "scheduler");
10786        assert_eq!(hermes["result"]["source"]["pid"], 4242);
10787        assert_eq!(hermes["result"]["source"]["process_id"], "9f1c2d");
10788
10789        let openclaw = service.handle(request(
10790            2,
10791            "harness.v1.runs.get",
10792            json!({
10793                "harness": "openclaw",
10794                "id": "run_health_0002",
10795                "homes": jobs_fixture_homes(),
10796            }),
10797        ));
10798        assert_eq!(openclaw["result"]["run"]["status"], "error");
10799        // ORCH-13: the run's delivery is projected AND the store's own columns
10800        // stay verbatim on `source`, so nothing about the fire is lost.
10801        assert_eq!(
10802            openclaw["result"]["source"]["delivery_status"],
10803            "not-delivered"
10804        );
10805        assert_eq!(
10806            openclaw["result"]["source"]["delivery_error"],
10807            "channel_not_found"
10808        );
10809        assert_eq!(openclaw["result"]["source"]["delivered"], 0);
10810        assert_eq!(
10811            openclaw["result"]["run"]["delivery"]["state"],
10812            "not-delivered"
10813        );
10814        assert_eq!(
10815            openclaw["result"]["run"]["delivery"]["last_error"],
10816            "channel_not_found"
10817        );
10818
10819        let missing = service.handle(request(
10820            3,
10821            "harness.v1.runs.get",
10822            json!({"harness": "hermes", "id": "no-such-run", "homes": jobs_fixture_homes()}),
10823        ));
10824        assert!(missing["error"]["message"]
10825            .as_str()
10826            .is_some_and(|message| message.contains("no run `no-such-run`")));
10827    }
10828
10829    #[test]
10830    fn runs_refuse_a_harness_that_keeps_no_run_store() {
10831        let mut service = HarnessSessionService::new();
10832        for (id, method, params) in [
10833            // Claude Code HAS scheduled jobs but no fire store: its fires are
10834            // ordinary turns. It must refuse, not answer with an empty list.
10835            (
10836                1,
10837                "harness.v1.runs.list",
10838                json!({"harness": "claude-code", "homes": jobs_fixture_homes()}),
10839            ),
10840            (
10841                2,
10842                "harness.v1.runs.get",
10843                json!({"harness": "claude-code", "id": "anything"}),
10844            ),
10845            (
10846                3,
10847                "harness.v1.runs.list",
10848                json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10849            ),
10850        ] {
10851            let response = service.handle(request(id, method, params));
10852            assert_eq!(response["error"]["code"], -32020, "{response}");
10853            assert!(response["error"]["message"]
10854                .as_str()
10855                .is_some_and(|message| message.contains("keeps no run store")));
10856            assert!(response.get("result").is_none());
10857        }
10858    }
10859
10860    #[test]
10861    fn runs_list_reports_an_install_with_no_run_store_as_absent() {
10862        let scratch = std::env::temp_dir().join(format!(
10863            "supercode-runs-empty-{}-{}",
10864            std::process::id(),
10865            generated_session_id()
10866        ));
10867        std::fs::create_dir_all(&scratch).unwrap();
10868        let response = runs_list(json!({
10869            "harness": "openclaw",
10870            "homes": {"openclaw": scratch.clone()},
10871        }));
10872        let result = &response["result"];
10873        assert_eq!(result["runs"].as_array().unwrap().len(), 0, "{result}");
10874        assert_eq!(result["sources"][0]["state"], "absent_store");
10875        assert!(result["sources"][0]["path"]
10876            .as_str()
10877            .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10878        std::fs::remove_dir_all(&scratch).ok();
10879    }
10880}