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.sessions.new",
81    "harness.v1.sessions.reset",
82    "harness.v1.sessions.archive",
83    "harness.v1.sessions.delete",
84    "harness.v1.runs.list",
85    "harness.v1.runs.get",
86    "harness.v1.approvals.list",
87    "harness.v1.approvals.resolve",
88    "harness.v1.runtimes.capabilities",
89    "harness.v1.runtimes.start",
90    "harness.v1.runtimes.resume",
91    "harness.v1.runtimes.attach_existing",
92    "harness.v1.runtimes.attach",
93    "harness.v1.runtimes.send_input",
94    "harness.v1.runtimes.interrupt",
95    "harness.v1.runtimes.steer",
96    "harness.v1.runtimes.respond",
97    "harness.v1.runtimes.terminal_instructions",
98    "harness.v1.runtimes.close",
99    "harness.v1.profiles.list",
100    "harness.v1.profiles.get",
101    "harness.v1.profiles.create",
102    "harness.v1.profiles.delete",
103    "harness.v1.channels.list",
104    "harness.v1.routes.list",
105    "harness.v1.triggers.list",
106    "harness.v1.channels.status",
107    "harness.v1.orchestration.load",
108    "harness.v1.orchestration.save",
109    "harness.v1.orchestration.compile",
110    "harness.v1.orchestration.decompile",
111    "harness.v1.orchestration.import",
112    "harness.v1.orchestration.export",
113    "harness.v1.workflow.load",
114];
115
116/// Protocol namespace implemented by this service.
117pub const HARNESS_SERVICE_VERSION: &str = "harness.v1";
118/// Notification method emitted for followed-session changes.
119pub const SESSION_EVENT_METHOD: &str = "harness.v1.sessions.event";
120/// Notification method emitted for normalized session-activity transitions.
121pub const SESSION_ACTIVITY_EVENT_METHOD: &str = "harness.v1.sessions.activity_event";
122/// Notification method emitted for revisioned session-list changes.
123pub const SESSION_INDEX_EVENT_METHOD: &str = "harness.v1.sessions.index_event";
124/// Notification method emitted for live runtime events.
125pub const RUNTIME_EVENT_METHOD: &str = "harness.v1.runtimes.event";
126
127/// Stateful persisted-session service. Each instance owns its follow
128/// subscriptions; discovery and loading remain read-only.
129pub struct HarnessSessionService {
130    catalog: HarnessCatalog,
131    followers: BTreeMap<String, SessionFollower>,
132    followed_sources: BTreeMap<String, FollowedSource>,
133    activity_subscriptions: BTreeMap<String, ActivitySubscription>,
134    index_subscriptions: BTreeMap<String, crate::session_index::SessionIndexSubscription>,
135    index_notifier: Arc<Notify>,
136    #[cfg(feature = "adapter-api")]
137    activity_monitor: crate::session_activity::SessionActivityMonitor,
138    next_subscription: u64,
139    runtimes: BTreeMap<String, Box<dyn RuntimeConnection>>,
140    /// Connections lent to a detached call that is running right now. The
141    /// runtime itself is OUT of `runtimes` for that whole call, and these
142    /// names are how a second caller is told the connection is busy rather
143    /// than unknown.
144    runtimes_in_flight: BTreeSet<String>,
145    terminal_launches: BTreeMap<String, StructuredLaunch>,
146    runtime_sequences: BTreeMap<String, u64>,
147    next_runtime: u64,
148    reduction_store_root: Option<PathBuf>,
149    /// ORCH-9: live permission/approval requests outstanding on the open
150    /// runtime connections above, fed by the same event pump that publishes
151    /// `harness.v1.runtimes.event`.
152    approvals: crate::approvals::ApprovalRegistry,
153    /// ORCH-9: supercode's own queued subagent approvals, when the host that
154    /// owns this service publishes its parent queue here.
155    subagent_approvals: Option<Arc<std::sync::Mutex<Vec<crate::subagents::QueuedApproval>>>>,
156}
157
158impl Default for HarnessSessionService {
159    fn default() -> Self {
160        Self::new()
161    }
162}
163
164impl HarnessSessionService {
165    /// Create an empty service instance.
166    pub fn new() -> Self {
167        Self {
168            catalog: HarnessCatalog::new(),
169            followers: BTreeMap::new(),
170            followed_sources: BTreeMap::new(),
171            activity_subscriptions: BTreeMap::new(),
172            index_subscriptions: BTreeMap::new(),
173            index_notifier: Arc::new(Notify::new()),
174            #[cfg(feature = "adapter-api")]
175            activity_monitor: Default::default(),
176            next_subscription: 1,
177            runtimes: BTreeMap::new(),
178            runtimes_in_flight: BTreeSet::new(),
179            terminal_launches: BTreeMap::new(),
180            runtime_sequences: BTreeMap::new(),
181            next_runtime: 1,
182            reduction_store_root: None,
183            approvals: crate::approvals::ApprovalRegistry::new(),
184            subagent_approvals: None,
185        }
186    }
187
188    /// Override the trusted, service-owned store used for durable reduction
189    /// bundles. Embedders and tests use this to keep all writes inside an
190    /// explicitly selected root; the CLI otherwise uses the normal
191    /// `$SUPERCODE_HOME/sessions` location.
192    pub fn with_reduction_store_root(mut self, root: impl Into<PathBuf>) -> Self {
193        self.reduction_store_root = Some(root.into());
194        self
195    }
196
197    /// ORCH-9: publish the parent's own subagent-approval queue into
198    /// `harness.v1.approvals.list`.
199    ///
200    /// This is the SAME `Arc` an [`crate::Agent`] pushes into
201    /// (`Agent::pending_child_approvals`), so a host that runs supercode's own
202    /// loop beside this service surfaces those requests through the uniform
203    /// door without copying them anywhere.
204    pub fn observe_subagent_approvals(
205        &mut self,
206        queue: Arc<std::sync::Mutex<Vec<crate::subagents::QueuedApproval>>>,
207    ) {
208        self.subagent_approvals = Some(queue);
209    }
210
211    /// ORCH-9: every approval request this service can see, newest last.
212    ///
213    /// Two sources, both live: the requests outstanding on the open runtime
214    /// connections, and supercode's own queued subagent approvals. There is
215    /// no file or database source at the pinned harness versions (see
216    /// [`crate::approvals`]), so a stored or proposal row is never produced.
217    pub fn approvals(&self, query: &crate::approvals::ApprovalsQuery) -> Vec<crate::ApprovalRow> {
218        let now = crate::approvals::now_ms();
219        let mut rows = self.approvals.rows(now);
220        if let Some(queue) = self.subagent_approvals.as_ref() {
221            let queued = queue
222                .lock()
223                .unwrap_or_else(std::sync::PoisonError::into_inner)
224                .clone();
225            rows.extend(crate::approvals::subagent_rows(&queued, now));
226        }
227        rows.retain(|row| query.matches(row));
228        rows.sort_by(|left, right| {
229            left.requested_at_ms
230                .cmp(&right.requested_at_ms)
231                .then_with(|| left.id.cmp(&right.id))
232        });
233        rows
234    }
235
236    /// ORCH-20 (controlled tier): answer one listed approval request by its
237    /// row id and one uniform decision.
238    ///
239    /// The decision is translated into the option token and reply envelope
240    /// the door that raised the request already accepts
241    /// ([`crate::approvals::plan_reply`]), and the answer is then sent by
242    /// calling `harness.v1.runtimes.respond` itself — the same code path, the
243    /// same adapter, the same bookkeeping that drops the row. This verb adds
244    /// a translation and nothing else.
245    async fn approvals_resolve(
246        &mut self,
247        params: Value,
248    ) -> std::result::Result<Value, ServiceError> {
249        let params = decode::<crate::approvals::ApprovalsResolveParams>(params)?;
250        if params.id.trim().is_empty() {
251            return Err(ServiceError::InvalidParams(
252                "approvals resolve requires the `id` of a listed approval row".into(),
253            ));
254        }
255        let choice = match (params.decision, params.option_id.as_deref()) {
256            (Some(_), Some(_)) => {
257                return Err(ServiceError::InvalidParams(
258                    "approvals resolve takes either `decision` or `option_id`, not both".into(),
259                ))
260            }
261            (Some(decision), None) => crate::approvals::ApprovalChoice::Decision(decision),
262            (None, Some(option)) => crate::approvals::ApprovalChoice::Option(option.to_string()),
263            (None, None) => {
264                return Err(ServiceError::InvalidParams(format!(
265                    "approvals resolve requires `decision` ({}) or an explicit `option_id`",
266                    crate::approvals::ApprovalDecision::ALL
267                        .map(|decision| decision.as_str())
268                        .join(" | "),
269                )))
270            }
271        };
272        let resolution = self
273            .approvals
274            .resolution(&params.id, &choice)
275            .map_err(|error| ServiceError::InvalidParams(error.to_string()))?;
276        // The harness's own door, unchanged: this is the identical call
277        // `harness.v1.runtimes.respond` performs for a caller who built the
278        // envelope by hand, including dropping the answered row.
279        self.runtime_call(
280            "harness.v1.runtimes.respond",
281            json!({
282                "connection": resolution.connection,
283                "request_id": resolution.request_id,
284                "response": resolution.response,
285            }),
286        )
287        .await?;
288        Ok(json!({
289            "id": params.id,
290            "decision": params.decision.map(|decision| decision.as_str()),
291            "option_id": resolution.option_id,
292            "resolved": true,
293        }))
294    }
295
296    /// Return the edge-triggered wakeup used by session-index filesystem
297    /// subscriptions. Transports can await this instead of polling indexes.
298    #[cfg(feature = "adapter-api")]
299    pub fn session_index_notifier(&self) -> Arc<Notify> {
300        Arc::clone(&self.index_notifier)
301    }
302
303    /// Handle one JSON-RPC 2.0 request and return one JSON-RPC response.
304    #[cfg(feature = "adapter-api")]
305    pub fn handle(&mut self, request: Value) -> Value {
306        let id = request.get("id").cloned().unwrap_or(Value::Null);
307        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
308            return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
309        }
310        let Some(method) = request.get("method").and_then(Value::as_str) else {
311            return rpc_error(id, -32600, "request is missing `method`");
312        };
313        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
314        match self.call(method, params) {
315            Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
316            Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
317            Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
318            Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
319            Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
320            Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
321        }
322    }
323
324    /// Handle either a persisted-session request or an asynchronous live
325    /// runtime request.
326    #[cfg(feature = "adapter-api")]
327    pub async fn handle_async(&mut self, request: Value) -> Value {
328        let method = request
329            .get("method")
330            .and_then(Value::as_str)
331            .unwrap_or_default();
332        if matches!(
333            method,
334            "harness.v1.harnesses.list" | "harness.v1.harnesses.probe"
335        ) {
336            let id = request.get("id").cloned().unwrap_or(Value::Null);
337            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
338                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
339            }
340            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
341            return match self.inventory_call(method, params).await {
342                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
343                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
344                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
345                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
346                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
347                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
348            };
349        }
350        if matches!(
351            method,
352            "harness.v1.harnesses.auth.methods"
353                | "harness.v1.harnesses.auth.begin"
354                | "harness.v1.harnesses.auth.verify"
355        ) {
356            let id = request.get("id").cloned().unwrap_or(Value::Null);
357            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
358                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
359            }
360            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
361            return match self.harness_authentication_call(method, params).await {
362                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
363                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
364                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
365                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
366                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
367                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
368            };
369        }
370        // ORCH-19 controlled tier. Answered here rather than through the SDK
371        // operation dispatch below so the harness's OWN refusal reaches the
372        // caller: `sdk_error` collapses every `UnsupportedAction` to one
373        // generic sentence, and the whole point of this tier is that a
374        // refusal names which door the harness does have.
375        if matches!(
376            method,
377            "harness.v1.sessions.new"
378                | "harness.v1.sessions.reset"
379                | "harness.v1.sessions.archive"
380                | "harness.v1.sessions.delete"
381        ) {
382            let id = request.get("id").cloned().unwrap_or(Value::Null);
383            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
384                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
385            }
386            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
387            let verb = match method {
388                "harness.v1.sessions.new" => crate::SessionVerb::New,
389                "harness.v1.sessions.reset" => crate::SessionVerb::Reset,
390                "harness.v1.sessions.archive" => crate::SessionVerb::Archive,
391                _ => crate::SessionVerb::Delete,
392            };
393            return match self.mutate_session(verb, params).await {
394                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
395                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
396                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
397                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
398                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
399                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
400            };
401        }
402        if method == "harness.v1.sessions.message" {
403            let id = request.get("id").cloned().unwrap_or(Value::Null);
404            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
405                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
406            }
407            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
408            return match self.message_call(params).await {
409                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
410                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
411                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
412                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
413                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
414                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
415            };
416        }
417        if matches!(
418            method,
419            "harness.v1.harnesses.settings" | "harness.v1.harnesses.configure"
420        ) {
421            let id = request.get("id").cloned().unwrap_or(Value::Null);
422            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
423                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
424            }
425            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
426            return match self.harness_settings_call(method, params) {
427                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
428                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
429                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
430                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
431                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
432                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
433            };
434        }
435        if method == "harness.v1.sessions.activity.subscribe" {
436            let id = request.get("id").cloned().unwrap_or(Value::Null);
437            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
438                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
439            }
440            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
441            return match self.subscribe_session_activity(params).await {
442                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
443                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
444                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
445                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
446                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
447                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
448            };
449        }
450        if let Some(operation) = SdkOperation::from_method(method) {
451            let id = request.get("id").cloned().unwrap_or(Value::Null);
452            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
453                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
454            }
455            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
456            return match self.execute(SdkRequest { operation, params }).await {
457                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
458                Err(error) => sdk_rpc_error(id, &error),
459            };
460        }
461        if !method.starts_with("harness.v1.runtimes.") {
462            return self.handle(request);
463        }
464        let id = request.get("id").cloned().unwrap_or(Value::Null);
465        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
466            return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
467        }
468        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
469        match self.runtime_call(method, params).await {
470            Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
471            Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
472            Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
473            Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
474            Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
475            Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
476        }
477    }
478
479    /// Poll all active subscriptions once and return zero or more JSON-RPC
480    /// notifications. Recoverable follower errors are delivered as events.
481    #[cfg(feature = "adapter-api")]
482    pub fn poll(&mut self) -> Vec<Value> {
483        let mut notifications = Vec::new();
484        for (subscription, follower) in &mut self.followers {
485            match follower.poll() {
486                Ok(Some(event)) => notifications.push(json!({
487                    "jsonrpc": "2.0",
488                    "method": SESSION_EVENT_METHOD,
489                    "params": {
490                        "subscription": subscription,
491                        "event": event.to_json(),
492                    }
493                })),
494                Ok(None) => {}
495                Err(error) => notifications.push(json!({
496                    "jsonrpc": "2.0",
497                    "method": SESSION_EVENT_METHOD,
498                    "params": {
499                        "subscription": subscription,
500                        "event": {
501                            "type": "watch_error",
502                            "recoverable": true,
503                            "message": error.to_string(),
504                        },
505                    }
506                })),
507            }
508        }
509        notifications
510    }
511
512    /// Report each followed session's live-runtime lifecycle state on that
513    /// session's own subscription, emitting only when the state changes.
514    ///
515    /// A growing transcript is not evidence that an agent is working, so the
516    /// state comes from the live-runtime registry and nowhere else. A followed
517    /// session with no registered Supercode runtime — a harness running outside
518    /// Supercode — reports `persisted`, which says plainly that its activity is
519    /// unknown rather than guessing at it. These events carry no sequence
520    /// number and no transcript content; they never interleave with the
521    /// content follower's sequenced stream.
522    #[cfg(feature = "adapter-api")]
523    pub async fn poll_session_runtime_states(&mut self) -> Vec<Value> {
524        let registry = crate::LocalRuntimeRegistry::new();
525        let authorization = crate::RuntimeAuthorization::observer();
526        let mut notifications = Vec::new();
527        for (subscription, source) in &mut self.followed_sources {
528            let state = match registry
529                .source_state(&source.harness, &source.session_id, &authorization)
530                .await
531            {
532                Ok(Some(state)) => state,
533                Ok(None) => crate::RuntimeRegistryState::Persisted,
534                // A failed registry read is not evidence of a state change.
535                Err(_) => continue,
536            };
537            if source.reported.as_deref() == Some(state.as_str()) {
538                continue;
539            }
540            source.reported = Some(state.as_str().to_string());
541            notifications.push(json!({
542                "jsonrpc": "2.0",
543                "method": SESSION_EVENT_METHOD,
544                "params": {
545                    "subscription": subscription,
546                    "event": {"type": "runtime_state", "state": state.as_str()},
547                },
548            }));
549        }
550        notifications
551    }
552
553    /// Poll normalized activity subscriptions, emitting only proven state
554    /// transitions. Every subscription is bulk-sampled so stock-harness
555    /// process and registry discovery happens once per UI, not once per row.
556    #[cfg(feature = "adapter-api")]
557    pub async fn poll_session_activities(&mut self) -> Vec<Value> {
558        let subscriptions = self
559            .activity_subscriptions
560            .iter()
561            .map(|(id, subscription)| {
562                (
563                    id.clone(),
564                    subscription.locators.clone(),
565                    subscription.homes.clone(),
566                )
567            })
568            .collect::<Vec<_>>();
569        let mut notifications = Vec::new();
570        for (subscription_id, locators, homes) in subscriptions {
571            let Ok(activities) = self.activity_monitor.resolve(&locators, &homes).await else {
572                // A failed evidence read proves no transition. Retain the last
573                // good state instead of flashing every row to persisted.
574                continue;
575            };
576            let Some(subscription) = self.activity_subscriptions.get_mut(&subscription_id) else {
577                continue;
578            };
579            let mut changed = Vec::new();
580            for activity in activities {
581                let key = activity.key();
582                if subscription
583                    .reported
584                    .get(&key)
585                    .is_some_and(|previous| previous.same_state(&activity))
586                {
587                    continue;
588                }
589                subscription.reported.insert(key, activity.clone());
590                changed.push(activity);
591            }
592            if !changed.is_empty() {
593                notifications.push(json!({
594                    "jsonrpc": "2.0",
595                    "method": SESSION_ACTIVITY_EVENT_METHOD,
596                    "params": {
597                        "subscription": subscription_id,
598                        "activities": changed,
599                    },
600                }));
601            }
602        }
603        notifications
604    }
605
606    /// Drain native-store invalidations and emit revisioned descriptor deltas.
607    /// An idle subscription performs no catalog or transcript reads between
608    /// its minute-scale recovery reconciliations.
609    #[cfg(feature = "adapter-api")]
610    pub fn poll_session_indexes(&mut self) -> Vec<Value> {
611        let mut notifications = Vec::new();
612        for (subscription, index) in &mut self.index_subscriptions {
613            let homes = index.homes().clone();
614            match index.poll() {
615                Ok(Some(delta)) => match live_index_changes(delta.changes, &homes) {
616                    Ok(changes) => notifications.push(json!({
617                        "jsonrpc": "2.0",
618                        "method": SESSION_INDEX_EVENT_METHOD,
619                        "params": {
620                            "subscription": subscription,
621                            "revision": delta.revision,
622                            "changes": changes,
623                        },
624                    })),
625                    Err(error) => notifications.push(json!({
626                        "jsonrpc": "2.0",
627                        "method": SESSION_INDEX_EVENT_METHOD,
628                        "params": {
629                            "subscription": subscription,
630                            "error": {"recoverable": true, "message": error_message(error)},
631                        },
632                    })),
633                },
634                Ok(None) => {}
635                Err(error) => notifications.push(json!({
636                    "jsonrpc": "2.0",
637                    "method": SESSION_INDEX_EVENT_METHOD,
638                    "params": {
639                        "subscription": subscription,
640                        "error": {"recoverable": true, "message": error},
641                    },
642                })),
643            }
644        }
645        notifications
646    }
647
648    #[cfg(feature = "adapter-api")]
649    async fn subscribe_session_activity(
650        &mut self,
651        params: Value,
652    ) -> std::result::Result<Value, ServiceError> {
653        let params = decode::<ActivitySubscribeParams>(params)?;
654        if params.locators.is_empty() {
655            return Err(ServiceError::InvalidParams(
656                "sessions.activity.subscribe requires at least one locator".into(),
657            ));
658        }
659        if params.locators.len() > 2_048 {
660            return Err(ServiceError::InvalidParams(
661                "sessions.activity.subscribe accepts at most 2048 locators".into(),
662            ));
663        }
664        let initial = self
665            .activity_monitor
666            .resolve(&params.locators, &params.homes)
667            .await
668            .map_err(ServiceError::Sdk)?;
669        let subscription = format!("activity-sub-{}", self.next_subscription);
670        self.next_subscription += 1;
671        let reported = initial
672            .iter()
673            .cloned()
674            .map(|activity| (activity.key(), activity))
675            .collect();
676        self.activity_subscriptions.insert(
677            subscription.clone(),
678            ActivitySubscription {
679                locators: params.locators,
680                homes: params.homes,
681                reported,
682            },
683        );
684        Ok(json!({"subscription": subscription, "initial": initial}))
685    }
686
687    /// Non-blockingly sample one event from every connected live runtime.
688    #[cfg(feature = "adapter-api")]
689    pub async fn poll_runtimes(&mut self) -> Vec<Value> {
690        self.poll_sdk_events()
691            .await
692            .into_iter()
693            .map(|(connection, runtime_event)| {
694                json!({
695                    "jsonrpc": "2.0",
696                    "method": RUNTIME_EVENT_METHOD,
697                    "params": {
698                        "connection": connection,
699                        "session_id": runtime_event.session_id,
700                        "sequence": runtime_event.event.sequence,
701                        "event": {
702                            "kind": runtime_event.event.kind,
703                            "payload": runtime_event.event.payload,
704                        },
705                    },
706                })
707            })
708            .collect()
709    }
710
711    async fn poll_sdk_events(&mut self) -> Vec<(String, SdkRuntimeEvent)> {
712        let mut events = Vec::new();
713        let mut closed = Vec::new();
714        let now_ms = crate::approvals::now_ms();
715        for (connection, runtime) in &mut self.runtimes {
716            let session_id = runtime.handle().runtime_id.clone();
717            let harness = runtime.handle().harness.clone();
718            // Drain what the runtime already has: a turn is several events
719            // (updates, then the protocol's completion), and delivering one
720            // per poll would cost a poll interval each. A zero timeout takes
721            // only what is ready — an idle runtime costs nothing.
722            for _ in 0..256 {
723                match tokio::time::timeout(Duration::ZERO, runtime.next_event()).await {
724                    Ok(Ok(Some(event))) => {
725                        let terminal = event.kind == "transport_closed";
726                        // ORCH-9: a permission/approval request arrives as an
727                        // ordinary event; it becomes listable here and stops
728                        // being listable when `runtimes.respond` answers it.
729                        self.approvals
730                            .observe(connection, &harness, &session_id, &event, now_ms);
731                        let next_sequence = self
732                            .runtime_sequences
733                            .entry(session_id.clone())
734                            .or_insert(0);
735                        let sequence = event.sequence.unwrap_or_else(|| {
736                            *next_sequence = next_sequence.saturating_add(1);
737                            *next_sequence
738                        });
739                        *next_sequence = (*next_sequence).max(sequence);
740                        events.push((
741                            connection.clone(),
742                            SdkRuntimeEvent {
743                                session_id: session_id.clone(),
744                                event: SdkEvent {
745                                    sequence,
746                                    kind: event.kind,
747                                    payload: event.payload,
748                                },
749                            },
750                        ));
751                        if terminal {
752                            closed.push(connection.clone());
753                            break;
754                        }
755                    }
756                    Ok(Ok(None)) => {
757                        let sequence = self
758                            .runtime_sequences
759                            .entry(session_id.clone())
760                            .or_insert(0);
761                        *sequence = sequence.saturating_add(1);
762                        events.push((
763                        connection.clone(),
764                        SdkRuntimeEvent {
765                            session_id,
766                            event: SdkEvent {
767                                sequence: *sequence,
768                                kind: "transport_closed".into(),
769                                payload: json!({"message": "Harness runtime transport closed."}),
770                            },
771                        },
772                    ));
773                        closed.push(connection.clone());
774                        break;
775                    }
776                    Err(_) => break,
777                    Ok(Err(error)) => {
778                        let sequence = self
779                            .runtime_sequences
780                            .entry(session_id.clone())
781                            .or_insert(0);
782                        *sequence = sequence.saturating_add(1);
783                        events.push((
784                        connection.clone(),
785                        SdkRuntimeEvent {
786                            session_id,
787                            event: SdkEvent {
788                                sequence: *sequence,
789                                kind: "transport_error".into(),
790                                payload: json!({"message": error.to_string(), "terminal": true}),
791                            },
792                        },
793                    ));
794                        closed.push(connection.clone());
795                        break;
796                    }
797                }
798            }
799        }
800        for connection in closed {
801            if let Some(runtime) = self.runtimes.remove(&connection) {
802                self.runtime_sequences.remove(&runtime.handle().runtime_id);
803            }
804            self.terminal_launches.remove(&connection);
805            // A connection that is gone cannot answer anything it was
806            // holding; those requests stop being listable with it.
807            self.approvals.forget(&connection);
808        }
809        events
810    }
811
812    fn call(&mut self, method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
813        match method {
814            "harness.v1.capabilities" => Ok(json!({
815                "version": HARNESS_SERVICE_VERSION,
816                "sdk": self.capabilities(),
817                "methods": HARNESS_SERVICE_METHODS,
818                "notifications": [
819                    SESSION_EVENT_METHOD,
820                    SESSION_ACTIVITY_EVENT_METHOD,
821                    SESSION_INDEX_EVENT_METHOD,
822                    RUNTIME_EVENT_METHOD
823                ],
824                "harnesses": harness_support_registry()
825                    .harnesses
826                    .into_iter()
827                    .map(|harness| harness.id)
828                    .collect::<Vec<_>>(),
829            })),
830            "harness.v1.support.report" => serde_json::to_value(harness_support_registry())
831                .map_err(|error| ServiceError::Operation(error.to_string())),
832            "harness.v1.profiles.list" | "harness.v1.profiles.get" => profiles_call(method, params),
833            // ORCH-21 controlled tier. Each verb translates to the HARNESS'S
834            // OWN profile verb and runs it (`crate::profiles_control`);
835            // supercode makes and removes nothing itself. The row returned is
836            // re-read through the ORCH-10 loader afterwards, and `ran`
837            // narrates the exact command.
838            "harness.v1.profiles.create" => {
839                mutate_profile(crate::profiles_control::ProfileVerb::Create, params)
840            }
841            "harness.v1.profiles.delete" => {
842                mutate_profile(crate::profiles_control::ProfileVerb::Delete, params)
843            }
844            "harness.v1.channels.list" | "harness.v1.channels.status" => {
845                channels_call(method, params)
846            }
847            // ORCH-15 observed tier: which profile / agent a surface tuple
848            // resolves to, read from each gateway harness's own config.
849            "harness.v1.routes.list" => routes_call(params),
850            // ORCH-16 observed tier: inbound webhook routes / hook mappings.
851            "harness.v1.triggers.list" => triggers_call(params),
852            // ONT-4: the orchestration doors. One home folder in, one typed orchestration
853            // value out (and back). Every one of the four is
854            // `crate::orchestration_doors`, which the `supercode orchestration` verbs call
855            // too — the RPC adds nothing but the envelope. A vault VALUE
856            // never crosses this wire: a load or a compile answers with the
857            // `.env` KEY NAMES, and a caller that needs a value reads the
858            // home's own `.env`.
859            // the workflow layer's read door: a harness's board as one typed value,
860            // the same code the `supercode workflow load` verb calls
861            "harness.v1.workflow.load" => {
862                let params = decode::<WorkflowLoadParams>(params)?;
863                let read =
864                    crate::workflow_doors::load(params.from, &params.home).map_err(operation)?;
865                serde_json::to_value(read)
866                    .map_err(|error| ServiceError::Operation(error.to_string()))
867            }
868            "harness.v1.orchestration.load" => {
869                let params = decode::<OrchestrationLoadParams>(params)?;
870                let read = crate::orchestration_doors::load(&params.root, params.flavor)
871                    .map_err(operation)?;
872                serde_json::to_value(read)
873                    .map_err(|error| ServiceError::Operation(error.to_string()))
874            }
875            "harness.v1.orchestration.save" => {
876                let params = decode::<OrchestrationSaveParams>(params)?;
877                let saved = crate::orchestration_doors::save(
878                    &params.root,
879                    params.orchestration,
880                    params.vault,
881                )
882                .map_err(operation)?;
883                serde_json::to_value(saved)
884                    .map_err(|error| ServiceError::Operation(error.to_string()))
885            }
886            "harness.v1.orchestration.compile" => {
887                let params = decode::<OrchestrationCompileParams>(params)?;
888                let read = crate::orchestration_doors::compile(params.from, &params.home)
889                    .map_err(operation)?;
890                serde_json::to_value(read)
891                    .map_err(|error| ServiceError::Operation(error.to_string()))
892            }
893            "harness.v1.orchestration.decompile" => {
894                let params = decode::<OrchestrationDecompileParams>(params)?;
895                let report = crate::orchestration_doors::decompile(
896                    params.to,
897                    params.orchestration,
898                    &params.source,
899                    params.source_flavor,
900                    &params.dest,
901                    params.vault,
902                )
903                .map_err(operation)?;
904                serde_json::to_value(report)
905                    .map_err(|error| ServiceError::Operation(error.to_string()))
906            }
907            // a migration keeps the credential in this process: a compile and
908            // a save (import), a load and a decompile (export), composed here
909            // because composed by a client the secret would have to cross
910            // the wire
911            "harness.v1.orchestration.import" => {
912                let params = decode::<OrchestrationImportParams>(params)?;
913                let imported =
914                    crate::orchestration_doors::import(params.from, &params.home, &params.into)
915                        .map_err(operation)?;
916                serde_json::to_value(imported)
917                    .map_err(|error| ServiceError::Operation(error.to_string()))
918            }
919            "harness.v1.orchestration.export" => {
920                let params = decode::<OrchestrationExportParams>(params)?;
921                let report =
922                    crate::orchestration_doors::export(params.to, &params.root, &params.dest)
923                        .map_err(operation)?;
924                serde_json::to_value(report)
925                    .map_err(|error| ServiceError::Operation(error.to_string()))
926            }
927            // ORCH-12 observed tier: read and search the persistent memory
928            // documents a harness keeps on disk. Read-only — every write
929            // (`hermes memory off`, `openclaw memory forget|reset`, Claude
930            // Code's `/memory`) stays the harness's own verb. A harness with
931            // no memory store is refused with UnsupportedAction.
932            "harness.v1.memory.show" | "harness.v1.memory.search" => memory_call(method, params),
933            // ORCH-11 observed tier: read-only enumeration of every harness's
934            // installed skill packages. An unknown harness id is refused with
935            // UnsupportedAction — every harness supports skills, so a filter
936            // that matches nothing is a caller error, never an empty listing.
937            "harness.v1.skills.list" => {
938                let query = decode::<crate::skills::SkillsQuery>(params)?;
939                if let Some(harness) = query.harness.as_deref() {
940                    if !crate::skills::SKILL_HARNESSES.contains(&harness) {
941                        return Err(ServiceError::UnsupportedAction(format!(
942                            "`{harness}` has no skills root supercode reads"
943                        )));
944                    }
945                }
946                serde_json::to_value(crate::skills::list_skills(&query))
947                    .map_err(|error| ServiceError::Operation(error.to_string()))
948            }
949            // ORCH-22 controlled tier: each verb goes through the door the
950            // HARNESS publishes — `hermes skills install|uninstall`,
951            // `openclaw skills install`, and for the core four the loader's
952            // own directory, which is the only skills door those harnesses
953            // have. supercode resolves no registry and unpacks no archive.
954            // The row returned is re-read through the ORCH-11 loader
955            // afterwards, and `ran` narrates exactly what was performed.
956            "harness.v1.skills.install" => {
957                mutate_skill(crate::skills_control::SkillVerb::Install, params)
958            }
959            "harness.v1.skills.remove" => {
960                mutate_skill(crate::skills_control::SkillVerb::Remove, params)
961            }
962            // ORCH-9 observed tier: the approval requests waiting for an
963            // answer. At the pinned harness versions the only uniform source
964            // is a LIVE request held by an open runtime connection, plus
965            // supercode's own queued subagent approvals — neither Hermes
966            // 0.21.0 nor OpenClaw 2026.7.1-2 has an approvals door to read
967            // (see `crate::approvals`). A harness whose runtime cannot carry
968            // a protocol request at all is refused by name.
969            "harness.v1.approvals.list" => {
970                let query = decode::<crate::approvals::ApprovalsQuery>(params)?;
971                if let Some(harness) = query.harness.as_deref() {
972                    if !crate::approvals::lists_approvals(harness) {
973                        return Err(ServiceError::UnsupportedAction(format!(
974                            "`{harness}` has no runtime door that carries an approval request"
975                        )));
976                    }
977                }
978                serde_json::to_value(self.approvals(&query))
979                    .map_err(|error| ServiceError::Operation(error.to_string()))
980            }
981            "harness.v1.sessions.discover" => {
982                let query = decode::<DiscoveryQuery>(params)?;
983                let page = discover_session_page(&query).map_err(operation)?;
984                // Claude Code is the one harness that publishes its RUNNING
985                // sessions. The registry is read once per discovery and joined
986                // by session id; every record in it has already survived a
987                // `kill(pid, 0)` liveness check inside `read_registry`.
988                let peers = if page
989                    .sessions
990                    .iter()
991                    .any(|session| session.locator.harness.as_str() == HarnessId::CLAUDE_CODE)
992                {
993                    crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(
994                        &query.homes,
995                    ))
996                } else {
997                    Vec::new()
998                };
999                let activities = crate::session_activity::resolve_stock_session_activities(
1000                    &page
1001                        .sessions
1002                        .iter()
1003                        .map(|session| session.locator.clone())
1004                        .collect::<Vec<_>>(),
1005                    &query.homes,
1006                )
1007                .into_iter()
1008                .map(|activity| (activity.key(), activity))
1009                .collect::<BTreeMap<_, _>>();
1010                let sessions = page
1011                    .sessions
1012                    .into_iter()
1013                    .map(|session| {
1014                        let mut value = live_descriptor_value(&session, &peers)?;
1015                        let activity_key = (
1016                            session.locator.harness.as_str().to_string(),
1017                            session.locator.session_id.clone(),
1018                        );
1019                        if let Some(activity) = activities.get(&activity_key) {
1020                            value["activity"] = serde_json::to_value(activity)
1021                                .map_err(|error| ServiceError::Operation(error.to_string()))?;
1022                            if let Some(status) = legacy_live_status(activity) {
1023                                value["live_status"] = json!(status);
1024                            }
1025                        }
1026                        Ok(value)
1027                    })
1028                    .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1029                let mut result = json!({"sessions": sessions, "next_cursor": page.next_cursor});
1030                // Preserve the metadata-only wire shape, but carry the catalog's
1031                // proof/counts when the caller explicitly requests preview search.
1032                if query.search_previews {
1033                    result["receipt"] = serde_json::to_value(page.receipt)
1034                        .map_err(|error| ServiceError::Operation(error.to_string()))?;
1035                }
1036                Ok(result)
1037            }
1038            "harness.v1.sessions.load" => {
1039                let params = decode::<LoadSessionParams>(params)?;
1040                if let Some(options) = &params.options {
1041                    options.validate()?;
1042                    if let Some(result) = indexed_claude_window(&params.read.locator, options)? {
1043                        return Ok(result);
1044                    }
1045                    return load_session(&params.read.locator)
1046                        .map(|session| projected_session_result(&session, options))
1047                        .map_err(operation);
1048                }
1049                let mut session = if params.read.display_history() {
1050                    self.catalog
1051                        .load_display_view(
1052                            &params.read.locator,
1053                            params.read.read_fidelity(),
1054                            params.read.tail_messages().unwrap_or(500),
1055                        )
1056                        .map_err(crate::Error::from)
1057                } else if params.read.include_subagents() {
1058                    load_session_with_fidelity(&params.read.locator, params.read.read_fidelity())
1059                } else {
1060                    self.catalog
1061                        .load_parent_with_fidelity(
1062                            &params.read.locator,
1063                            params.read.read_fidelity(),
1064                        )
1065                        .map_err(crate::Error::from)
1066                }
1067                .map_err(operation)?;
1068                params.read.bound_session(&mut session);
1069                Ok(json!({"session": normalized_session_json(&session)}))
1070            }
1071            "harness.v1.sessions.follow" => {
1072                let params = decode::<LocatorParams>(params)?;
1073                let mut follower = self
1074                    .catalog
1075                    .follow_read_view(
1076                        &params.locator,
1077                        params.read_fidelity(),
1078                        params.include_subagents(),
1079                        params.tail_messages(),
1080                        params.max_message_chars(),
1081                        params.display_history(),
1082                    )
1083                    .map_err(operation)?;
1084                let initial = follower
1085                    .poll()
1086                    .map_err(operation)?
1087                    .map(|event| event.to_json());
1088                let subscription = format!("sub-{}", self.next_subscription);
1089                self.next_subscription += 1;
1090                self.followers.insert(subscription.clone(), follower);
1091                self.followed_sources.insert(
1092                    subscription.clone(),
1093                    FollowedSource {
1094                        harness: params.locator.harness.as_str().to_string(),
1095                        session_id: params.locator.session_id.clone(),
1096                        reported: None,
1097                    },
1098                );
1099                Ok(json!({"subscription": subscription, "initial": initial}))
1100            }
1101            "harness.v1.sessions.unfollow" => {
1102                let params = decode::<UnfollowParams>(params)?;
1103                self.followed_sources.remove(&params.subscription);
1104                Ok(json!({
1105                    "removed": self.followers.remove(&params.subscription).is_some()
1106                }))
1107            }
1108            "harness.v1.sessions.activity.unsubscribe" => {
1109                let params = decode::<UnfollowParams>(params)?;
1110                Ok(json!({
1111                    "removed": self.activity_subscriptions.remove(&params.subscription).is_some()
1112                }))
1113            }
1114            "harness.v1.sessions.index.subscribe" => {
1115                let query = decode::<DiscoveryQuery>(params)?;
1116                crate::session_index::validate_query(&query)
1117                    .map_err(ServiceError::InvalidParams)?;
1118                let homes = query.homes.clone();
1119                let (index, initial) = crate::session_index::SessionIndexSubscription::open(
1120                    query,
1121                    Arc::clone(&self.index_notifier),
1122                )
1123                .map_err(ServiceError::Operation)?;
1124                let peers = peers_for_descriptors(&initial, &homes);
1125                let initial = initial
1126                    .iter()
1127                    .map(|descriptor| live_descriptor_value(descriptor, &peers))
1128                    .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1129                let subscription = format!("index-sub-{}", self.next_subscription);
1130                self.next_subscription += 1;
1131                self.index_subscriptions.insert(subscription.clone(), index);
1132                Ok(json!({
1133                    "subscription": subscription,
1134                    "revision": 1,
1135                    "initial": initial,
1136                }))
1137            }
1138            "harness.v1.sessions.index.resize" => {
1139                let params = decode::<IndexResizeParams>(params)?;
1140                crate::session_index::validate_limit(params.limit)
1141                    .map_err(ServiceError::InvalidParams)?;
1142                let index = self
1143                    .index_subscriptions
1144                    .get_mut(&params.subscription)
1145                    .ok_or_else(|| {
1146                        ServiceError::InvalidParams("unknown session index subscription".into())
1147                    })?;
1148                let prepared = index
1149                    .prepare_resize(params.limit)
1150                    .map_err(ServiceError::Operation)?;
1151                let peers = peers_for_descriptors(&prepared.page.sessions, index.homes());
1152                let initial = prepared
1153                    .page
1154                    .sessions
1155                    .iter()
1156                    .map(|descriptor| live_descriptor_value(descriptor, &peers))
1157                    .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1158                let response = json!({
1159                    "subscription": params.subscription,
1160                    "revision": prepared.revision,
1161                    "initial": initial,
1162                    "receipt": prepared.page.receipt,
1163                });
1164                index.commit_resize(prepared);
1165                Ok(response)
1166            }
1167            "harness.v1.sessions.index.unsubscribe" => {
1168                let params = decode::<UnfollowParams>(params)?;
1169                Ok(json!({
1170                    "removed": self.index_subscriptions.remove(&params.subscription).is_some()
1171                }))
1172            }
1173            "harness.v1.sessions.import" => {
1174                let params = decode::<ImportSessionParams>(params)?;
1175                let session = Session::load_str(&params.content, params.source_harness.into())
1176                    .map_err(operation)?;
1177                Ok(json!({"session": normalized_session_json(&session)}))
1178            }
1179            "harness.v1.sessions.export" | "harness.v1.sessions.translate" => {
1180                let params = decode::<ExportSessionParams>(params)?;
1181                let session = load_session(&params.locator).map_err(operation)?;
1182                let artifact = session_artifact(&params.locator, &session, params.target_harness)?;
1183                if method == "harness.v1.sessions.export"
1184                    && params.target_harness == TransferFormat::Hermes
1185                {
1186                    // UNI-18: write through Hermes's own door, never into its store
1187                    let imported = crate::hermes_import::import_into_hermes(&session, None)
1188                        .map_err(operation)?;
1189                    return Ok(json!({"artifact": artifact, "imported": imported}));
1190                }
1191                Ok(json!({"artifact": artifact}))
1192            }
1193            "harness.v1.sessions.reduce" => {
1194                let params = decode::<ReduceSessionParams>(params)?;
1195                self.reduce_session(params)
1196            }
1197            "harness.v1.sessions.branch" => {
1198                let params = decode::<BranchSessionParams>(params)?;
1199                let session = load_session(&params.locator).map_err(operation)?;
1200                let storage = params.locator.storage.path().display().to_string();
1201                let bootstrap_prompt = format!(
1202                    "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.",
1203                    params.locator.harness.as_str(), params.locator.session_id, storage
1204                );
1205                let artifact = params
1206                    .target_harness
1207                    .map(|target| session_artifact(&params.locator, &session, target))
1208                    .transpose()?;
1209                Ok(json!({
1210                    "parent": params.locator,
1211                    "session": normalized_session_json(&session),
1212                    "bootstrap_prompt": bootstrap_prompt,
1213                    "artifact": artifact,
1214                }))
1215            }
1216            "harness.v1.sessions.handoff" => {
1217                let params = decode::<HandoffSessionParams>(params)?;
1218                let session = load_session(&params.locator).map_err(operation)?;
1219                let cwd = params
1220                    .cwd
1221                    .or_else(|| session.meta.cwd.clone())
1222                    .unwrap_or_else(|| PathBuf::from("."));
1223                let artifact =
1224                    handoff_artifact(&params.locator, &session, params.target_harness, &cwd)?;
1225                let target_session_id = artifact.session_id.as_deref().ok_or_else(|| {
1226                    ServiceError::Operation(
1227                        "handoff artifact omitted target session identity".into(),
1228                    )
1229                })?;
1230                let instructions =
1231                    handoff_instructions(params.target_harness, target_session_id, &cwd);
1232                Ok(json!({
1233                    "artifact": artifact,
1234                    "launch": instructions.launch,
1235                    "materialize": instructions.materialize,
1236                    "requires_materialization": instructions.requires_materialization,
1237                    "note": instructions.note,
1238                }))
1239            }
1240            // ORCH-7 observed tier. Read-only: the handlers open the harness's
1241            // own job store (Claude Code's session JSONL, Hermes's and
1242            // OpenClaw's `cron/jobs.json`) and never write, fire, or schedule.
1243            "harness.v1.jobs.list" => {
1244                let query = decode::<crate::jobs::JobsQuery>(params)?;
1245                if let Some(harness) = query.harness.as_deref() {
1246                    refuse_harness_without_jobs(harness, "jobs.list")?;
1247                }
1248                let listing = crate::jobs::list_jobs(&query).map_err(operation)?;
1249                serde_json::to_value(listing)
1250                    .map_err(|error| ServiceError::Operation(error.to_string()))
1251            }
1252            "harness.v1.jobs.get" => {
1253                let params = decode::<JobsGetParams>(params)?;
1254                refuse_harness_without_jobs(&params.harness, "jobs.get")?;
1255                match crate::jobs::get_job(&params.harness, &params.id, &params.homes)
1256                    .map_err(operation)?
1257                {
1258                    Some((job, source)) => Ok(json!({"job": job, "source": source})),
1259                    None => Err(ServiceError::Operation(format!(
1260                        "`{}` has no scheduled job `{}`",
1261                        params.harness, params.id
1262                    ))),
1263                }
1264            }
1265            // ORCH-18 controlled tier. Each verb translates to the HARNESS'S
1266            // OWN cron verb and runs it (`crate::jobs_control`); supercode
1267            // schedules nothing. The row returned is re-read from the
1268            // harness's store afterwards, and `ran` narrates the exact command
1269            // with any credential redacted.
1270            "harness.v1.jobs.create" => mutate_job(crate::jobs_control::JobVerb::Create, params),
1271            "harness.v1.jobs.update" => mutate_job(crate::jobs_control::JobVerb::Update, params),
1272            "harness.v1.jobs.pause" => mutate_job(crate::jobs_control::JobVerb::Pause, params),
1273            "harness.v1.jobs.resume" => mutate_job(crate::jobs_control::JobVerb::Resume, params),
1274            "harness.v1.jobs.run" => mutate_job(crate::jobs_control::JobVerb::Run, params),
1275            "harness.v1.jobs.delete" => mutate_job(crate::jobs_control::JobVerb::Delete, params),
1276            // ORCH-8 observed tier. Read-only: the handlers open the harness's
1277            // own run store (Hermes's `cron/executions.db`, OpenClaw's
1278            // `cron_run_logs`) and never claim, retry, or prune a fire.
1279            "harness.v1.runs.list" => {
1280                let query = decode::<crate::runs::RunsQuery>(params)?;
1281                if let Some(harness) = query.harness.as_deref() {
1282                    refuse_harness_without_runs(harness, "runs.list")?;
1283                }
1284                let listing = crate::runs::list_runs(&query).map_err(operation)?;
1285                serde_json::to_value(listing)
1286                    .map_err(|error| ServiceError::Operation(error.to_string()))
1287            }
1288            "harness.v1.runs.get" => {
1289                let params = decode::<RunsGetParams>(params)?;
1290                refuse_harness_without_runs(&params.harness, "runs.get")?;
1291                match crate::runs::get_run(&params.harness, &params.id, &params.homes)
1292                    .map_err(operation)?
1293                {
1294                    Some((run, source)) => Ok(json!({"run": run, "source": source})),
1295                    None => Err(ServiceError::Operation(format!(
1296                        "`{}` has no run `{}`",
1297                        params.harness, params.id
1298                    ))),
1299                }
1300            }
1301            "harness.v1.sessions.resume_instructions" => {
1302                let params = decode::<ResumeInstructionsParams>(params)?;
1303                let session = load_session(&params.locator).map_err(operation)?;
1304                let cwd = params
1305                    .cwd
1306                    .or(session.meta.cwd)
1307                    .unwrap_or_else(|| PathBuf::from("."));
1308                let launch = resume_launch(
1309                    params.locator.harness.as_str(),
1310                    &params.locator.session_id,
1311                    &cwd,
1312                    params.policy,
1313                )?;
1314                Ok(json!({"launch": launch}))
1315            }
1316            _ => Err(ServiceError::MethodNotFound),
1317        }
1318    }
1319
1320    fn reduce_session(
1321        &self,
1322        params: ReduceSessionParams,
1323    ) -> std::result::Result<Value, ServiceError> {
1324        let session = load_session(&params.locator).map_err(operation)?;
1325        if session.messages.is_empty() {
1326            return Err(ServiceError::InvalidParams(
1327                "cannot reduce an empty session".into(),
1328            ));
1329        }
1330        let keep_last = params.keep_last.clamp(1, 128);
1331        let policy = reduce::ReductionPolicy {
1332            clear_turns_older_than: Some(keep_last),
1333            ..Default::default()
1334        };
1335        let (view, log) =
1336            reduce::project_messages(&session.messages, &policy, &reduce::ReductionLog::default());
1337        if log.reductions.is_empty() {
1338            return Err(ServiceError::UnsupportedAction(format!(
1339                "session `{}` is already too small for a meaningful reversible reduction",
1340                params.locator.session_id
1341            )));
1342        }
1343        let source_tokens = tokens::estimate_view_tokens(&session.messages);
1344        let reduced_tokens = tokens::estimate_view_tokens(&view);
1345        if reduced_tokens >= source_tokens {
1346            return Err(ServiceError::UnsupportedAction(format!(
1347                "session `{}` has no token-reducing reversible projection",
1348                params.locator.session_id
1349            )));
1350        }
1351
1352        let store_root = self
1353            .reduction_store_root
1354            .clone()
1355            .unwrap_or_else(default_reduction_store_root);
1356        let store = crate::SessionStore::open(&store_root).map_err(operation)?;
1357        let rescue_id = format!("rescue-{}", generated_session_id());
1358        let imported = session
1359            .imported_message_count
1360            .unwrap_or(session.messages.len())
1361            .min(session.messages.len());
1362        let sidecar_jsonl = session.to_native_jsonl_v2(&session.messages[imported..]);
1363        let view_jsonl = messages_jsonl(&view)?;
1364        let title = format!(
1365            "Reduced {} continuation from {}",
1366            params.target_harness.id(),
1367            params.locator.session_id
1368        );
1369
1370        // Durability order is intentional: the full source of truth lands
1371        // before either object that can refer to it. A crash may leave an
1372        // unused sidecar, but can never leave a reduced view whose originals
1373        // were not durably written first.
1374        store
1375            .save_sidecar(&rescue_id, &sidecar_jsonl)
1376            .map_err(operation)?;
1377        store
1378            .save_reduction_log(&rescue_id, &log)
1379            .map_err(operation)?;
1380        store
1381            .save(&rescue_id, &title, &view_jsonl)
1382            .map_err(operation)?;
1383
1384        let source_bytes = serde_json::to_vec(&session.messages)
1385            .map_err(|error| ServiceError::Operation(error.to_string()))?
1386            .len() as u64;
1387        let reduced_bytes = serde_json::to_vec(&view)
1388            .map_err(|error| ServiceError::Operation(error.to_string()))?
1389            .len() as u64;
1390        store
1391            .set_reduction_stats(
1392                &rescue_id,
1393                &title,
1394                source_bytes,
1395                reduced_bytes,
1396                log.reductions.len() as u32,
1397            )
1398            .map_err(operation)?;
1399
1400        // The receipt is issued only after a real disk reload. This proves
1401        // the exact files another process will consume, not the convenient
1402        // in-memory values that produced them.
1403        let reloaded_sidecar = store
1404            .load_sidecar(&rescue_id)
1405            .map_err(operation)?
1406            .ok_or_else(|| ServiceError::Operation("reduction sidecar disappeared".into()))?;
1407        let reloaded_sidecar = Session::from_sidecar_str(&reloaded_sidecar).map_err(operation)?;
1408        let reloaded_log = store
1409            .load_reduction_log(&rescue_id)
1410            .map_err(operation)?
1411            .ok_or_else(|| ServiceError::Operation("reduction log disappeared".into()))?;
1412        let reloaded_view = parse_messages_jsonl(&store.load(&rescue_id).map_err(operation)?)?;
1413        reduce::verify_log(&reloaded_log, &reloaded_sidecar).map_err(operation)?;
1414        // `sc.reduction` is deliberately in-memory-only metadata: it must
1415        // never leak onto a provider-facing transcript. Reapplying the
1416        // durable log to the durable sidecar restores those ids. Comparing
1417        // its wire form with the transcript reloaded above proves that the
1418        // persisted view is exactly the deterministic projection before we
1419        // use the restamped form for inversion.
1420        let (restamped_view, restamped_log) =
1421            reduce::project_messages(&reloaded_sidecar.messages, &policy, &reloaded_log);
1422        if messages_jsonl(&restamped_view)? != messages_jsonl(&reloaded_view)? {
1423            return Err(ServiceError::Operation(
1424                "persisted reduction view does not match its durable log and sidecar".into(),
1425            ));
1426        }
1427        if restamped_log != reloaded_log {
1428            return Err(ServiceError::Operation(
1429                "reapplying the durable reduction log changed its identity".into(),
1430            ));
1431        }
1432        let inverted =
1433            reduce::invert(&restamped_view, &reloaded_log, &reloaded_sidecar).map_err(operation)?;
1434        if inverted != session.messages {
1435            return Err(ServiceError::Operation(
1436                "reduction inversion did not restore the source messages byte-exactly".into(),
1437            ));
1438        }
1439
1440        let ratio = source_tokens as f64 / reduced_tokens.max(1) as f64;
1441        let sidecar_path = store.sidecar_path(&rescue_id);
1442        let reduction_log_path = store.reduction_log_path(&rescue_id).map_err(operation)?;
1443        let bootstrap_prompt = reduced_bootstrap_prompt(
1444            &params.locator,
1445            params.target_harness,
1446            &view_jsonl,
1447            &sidecar_path,
1448            &reduction_log_path,
1449        );
1450        let mut reduced_session = session.clone();
1451        reduced_session.meta.session_id = Some(rescue_id.clone());
1452        reduced_session.messages = view;
1453
1454        Ok(json!({
1455            "session": normalized_session_json(&reduced_session),
1456            "bootstrap_prompt": bootstrap_prompt,
1457            "receipt": {
1458                "id": rescue_id,
1459                "sidecar_id": rescue_id,
1460                "source_harness": params.locator.harness,
1461                "target_harness": params.target_harness.id(),
1462                "source_tokens": source_tokens,
1463                "reduced_tokens": reduced_tokens,
1464                "ratio": ratio,
1465                "source_bytes": source_bytes,
1466                "reduced_bytes": reduced_bytes,
1467                "reductions": reloaded_log.reductions.len(),
1468                "sidecar_path": sidecar_path,
1469                "reduction_log_path": reduction_log_path,
1470                "verified": true,
1471                "reversible": true,
1472            }
1473        }))
1474    }
1475
1476    /// Recognize the one request family whose waiting happens entirely
1477    /// outside this service's state, and hand a transport the half it can run
1478    /// off the task that owns the service.
1479    ///
1480    /// Opening a runtime is the only door here that waits on a foreign
1481    /// program: it spawns the harness's own binary and completes that
1482    /// program's protocol handshake, which takes as long as the program takes
1483    /// to answer. A transport that awaited the whole request inline would
1484    /// stop reading its own input for that whole time, so ONE slow launch
1485    /// would queue every later request on the same server — including reads
1486    /// like `sessions.discover` that touch no runtime at all. Splitting the
1487    /// request lets the transport spawn [`RuntimeOpen::open`] and keep
1488    /// reading, then pay only the short bookkeeping half
1489    /// ([`Self::register_open_runtime`]) when the runtime is up.
1490    ///
1491    /// `None` for every other method: those are answered by
1492    /// [`Self::handle_async`] as before.
1493    pub fn runtime_open(request: &Value) -> Option<RuntimeOpen> {
1494        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1495            return None;
1496        }
1497        let method = request.get("method").and_then(Value::as_str)?;
1498        if !RUNTIME_OPEN_METHODS.contains(&method) {
1499            return None;
1500        }
1501        Some(RuntimeOpen {
1502            id: request.get("id").cloned().unwrap_or(Value::Null),
1503            method: method.to_string(),
1504            params: request.get("params").cloned().unwrap_or_else(|| json!({})),
1505        })
1506    }
1507
1508    /// Recognize a [`DETACHED_METHODS`] request and hand a transport the
1509    /// whole of it: the service-state half is read here and now, and what
1510    /// remains waits on a foreign program with nothing of this service's in
1511    /// hand.
1512    ///
1513    /// Same reason as [`Self::runtime_open`], different doors. Probing a
1514    /// harness starts it and completes its handshake; couriering a message
1515    /// runs a `claude` process to completion; a conversation verb runs the
1516    /// harness's own CLI or calls its HTTP API. A transport that awaited any
1517    /// of those inline would stop reading its own input for that whole time,
1518    /// so one probe of an unhealthy harness would queue every later request
1519    /// on the same server.
1520    ///
1521    /// Unlike an opening runtime there is no bookkeeping half: the answer
1522    /// [`DetachedCall::run`] produces is the caller's complete response, so a
1523    /// transport writes it without coming back here.
1524    ///
1525    /// `None` for every other method — including the LIVE `sessions.new` /
1526    /// `sessions.reset` door and `runtimes.close`, which wait on a runtime
1527    /// connection this service owns and so are split off by
1528    /// [`Self::detach_runtime`] instead.
1529    pub fn detach(&self, request: &Value) -> Option<DetachedCall> {
1530        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1531            return None;
1532        }
1533        let method = request.get("method").and_then(Value::as_str)?;
1534        if !DETACHED_METHODS.contains(&method) {
1535            return None;
1536        }
1537        let id = request.get("id").cloned().unwrap_or(Value::Null);
1538        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
1539        let work = match method {
1540            "harness.v1.harnesses.list" | "harness.v1.harnesses.probe" => self
1541                .inventory_work(method, params)
1542                .map(DetachedWork::Inventory),
1543            "harness.v1.sessions.message" => {
1544                decode::<MessageSessionParams>(params).map(DetachedWork::Message)
1545            }
1546            _ => {
1547                let verb = match method {
1548                    "harness.v1.sessions.new" => crate::SessionVerb::New,
1549                    "harness.v1.sessions.reset" => crate::SessionVerb::Reset,
1550                    "harness.v1.sessions.archive" => crate::SessionVerb::Archive,
1551                    _ => crate::SessionVerb::Delete,
1552                };
1553                match decode::<crate::SessionMutation>(params) {
1554                    Ok(mutation) => {
1555                        match crate::sessions_control::door(&mutation.harness, verb) {
1556                            // The live door needs the open runtime connection
1557                            // this service owns; it stays inline.
1558                            Ok(crate::SessionDoor::Live(_)) => return None,
1559                            Ok(_) => Ok(DetachedWork::SessionMutation { verb, mutation }),
1560                            Err(error) => Err(session_control_error(error)),
1561                        }
1562                    }
1563                    Err(error) => Err(error),
1564                }
1565            }
1566        };
1567        Some(DetachedCall {
1568            id,
1569            method: method.to_string(),
1570            work: work.map(Work::Free),
1571        })
1572    }
1573
1574    /// Recognize the two doors that wait on a runtime THIS SERVICE OWNS, and
1575    /// hand a transport the whole of each by lending the connection out.
1576    ///
1577    /// `runtimes.close` surrenders its runtime for good; the LIVE
1578    /// `sessions.new` / `sessions.reset` door borrows one for the length of
1579    /// the slash command and gives it back through
1580    /// [`Self::finish_detached`]. Both are bounded by
1581    /// [`RUNTIME_CONTROL_DEADLINE`], and a wedged runtime spends all of it —
1582    /// which is exactly as long as a transport that awaited them inline would
1583    /// stop reading its own input.
1584    ///
1585    /// `None` for every other method, and for the `sessions.new` /
1586    /// `sessions.reset` doors that are not live: [`Self::detach`] owns those.
1587    pub fn detach_runtime(&mut self, request: &Value) -> Option<DetachedCall> {
1588        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1589            return None;
1590        }
1591        let method = request.get("method").and_then(Value::as_str)?;
1592        let id = request.get("id").cloned().unwrap_or(Value::Null);
1593        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
1594        let work = match method {
1595            "harness.v1.runtimes.close" => decode::<RuntimeConnectionParams>(params)
1596                .and_then(|params| self.surrender_runtime(&params.connection))
1597                .map(|(runtime, process_group)| {
1598                    Work::Runtime(RuntimeWork::Close {
1599                        runtime,
1600                        process_group,
1601                    })
1602                }),
1603            "harness.v1.sessions.new" | "harness.v1.sessions.reset" => {
1604                let verb = if method == "harness.v1.sessions.new" {
1605                    crate::SessionVerb::New
1606                } else {
1607                    crate::SessionVerb::Reset
1608                };
1609                let mutation = decode::<crate::SessionMutation>(params).ok()?;
1610                // Everything but the live door — including a refusal and a
1611                // request naming no connection — is `detach`'s or
1612                // `handle_async`'s to answer.
1613                let Ok(crate::SessionDoor::Live(command)) =
1614                    crate::sessions_control::door(&mutation.harness, verb)
1615                else {
1616                    return None;
1617                };
1618                let connection = mutation
1619                    .connection
1620                    .clone()
1621                    .filter(|value| !value.trim().is_empty())?;
1622                self.lend_runtime(&connection).map(|runtime| {
1623                    let session = live_session_name(runtime.as_ref(), &mutation);
1624                    Work::Runtime(RuntimeWork::LiveCommand {
1625                        connection,
1626                        runtime,
1627                        verb,
1628                        mutation,
1629                        command,
1630                        session,
1631                    })
1632                })
1633            }
1634            _ => return None,
1635        };
1636        Some(DetachedCall {
1637            id,
1638            method: method.to_string(),
1639            work,
1640        })
1641    }
1642
1643    /// Take back whatever a detached call borrowed and hand over the caller's
1644    /// response. Every answer from [`DetachedCall::run`] comes through here,
1645    /// so a lent-out connection is back in the service before the response
1646    /// that used it is written.
1647    pub fn finish_detached(&mut self, answer: DetachedAnswer) -> Value {
1648        let DetachedAnswer { response, returned } = answer;
1649        if let Some(ReturnedRuntime {
1650            connection,
1651            runtime,
1652        }) = returned
1653        {
1654            self.runtimes_in_flight.remove(&connection);
1655            self.runtimes.insert(connection, runtime);
1656        }
1657        response
1658    }
1659
1660    /// Answer a request split out by [`Self::runtime_open`] and already
1661    /// awaited by [`RuntimeOpen::open`]: register the runtime this service now
1662    /// owns and build its JSON-RPC response.
1663    pub async fn finish_runtime_open(&mut self, opened: OpenedRuntime) -> Value {
1664        let OpenedRuntime { id, outcome } = opened;
1665        let result = match outcome {
1666            Ok(open) => self.register_open_runtime(open).await,
1667            Err(error) => Err(error),
1668        };
1669        service_response(id, result)
1670    }
1671
1672    /// Take ownership of an opened runtime.
1673    async fn register_open_runtime(
1674        &mut self,
1675        open: OpenRuntime,
1676    ) -> std::result::Result<Value, ServiceError> {
1677        match open {
1678            OpenRuntime::Hosted {
1679                runtime,
1680                capabilities,
1681                workspace,
1682            } => {
1683                self.insert_hosted_runtime(runtime, capabilities, workspace)
1684                    .await
1685            }
1686            OpenRuntime::Joined { runtime } => self.insert_runtime(runtime),
1687        }
1688    }
1689
1690    async fn runtime_call(
1691        &mut self,
1692        method: &str,
1693        params: Value,
1694    ) -> std::result::Result<Value, ServiceError> {
1695        match method {
1696            "harness.v1.runtimes.capabilities" => {
1697                let params = decode::<RuntimeBackendParams>(params)?;
1698                let backend = runtime_backend(&params)?;
1699                Ok(json!({
1700                    "harness": backend.harness(),
1701                    "capabilities": backend.capabilities(),
1702                }))
1703            }
1704            method if RUNTIME_OPEN_METHODS.contains(&method) => {
1705                self.register_open_runtime(open_runtime(method, params).await?)
1706                    .await
1707            }
1708            "harness.v1.runtimes.send_input" => {
1709                let params = decode::<RuntimeInputParams>(params)?;
1710                let image_urls = validate_runtime_image_urls(params.image_urls)?;
1711                let runtime = self.runtime_mut(&params.connection)?;
1712                let turn_id = within_control_deadline(
1713                    method,
1714                    runtime.send_input(RuntimeInput {
1715                        text: params.text,
1716                        image_urls,
1717                    }),
1718                )
1719                .await?
1720                .map_err(operation)?;
1721                Ok(json!({"turn_id": turn_id}))
1722            }
1723            "harness.v1.runtimes.interrupt" => {
1724                let params = decode::<RuntimeConnectionParams>(params)?;
1725                within_control_deadline(method, self.runtime_mut(&params.connection)?.interrupt())
1726                    .await?
1727                    .map_err(operation)?;
1728                Ok(json!({}))
1729            }
1730            "harness.v1.runtimes.steer" => {
1731                let params = decode::<RuntimeInputParams>(params)?;
1732                if !params.image_urls.is_empty() {
1733                    return Err(ServiceError::InvalidParams(
1734                        "runtime steering accepts text only".into(),
1735                    ));
1736                }
1737                let text = params.text.trim();
1738                if text.is_empty() || text.chars().count() > 50_000 {
1739                    return Err(ServiceError::InvalidParams(
1740                        "runtime steering requires 1 to 50,000 text characters".into(),
1741                    ));
1742                }
1743                within_control_deadline(
1744                    method,
1745                    self.runtime_mut(&params.connection)?
1746                        .steer(text.to_string()),
1747                )
1748                .await?
1749                .map_err(operation)?;
1750                Ok(json!({}))
1751            }
1752            "harness.v1.runtimes.respond" => {
1753                let params = decode::<RuntimeRespondParams>(params)?;
1754                let request_id = params.request_id.clone();
1755                within_control_deadline(
1756                    method,
1757                    self.runtime_mut(&params.connection)?
1758                        .respond(params.request_id, params.response),
1759                )
1760                .await?
1761                .map_err(operation)?;
1762                // ORCH-9: an answered request is no longer waiting for one.
1763                self.approvals.answered(&params.connection, &request_id);
1764                Ok(json!({}))
1765            }
1766            "harness.v1.runtimes.terminal_instructions" => {
1767                let params = decode::<RuntimeConnectionParams>(params)?;
1768                let launch = self
1769                    .terminal_launches
1770                    .get(&params.connection)
1771                    .ok_or_else(|| {
1772                        ServiceError::Operation(
1773                            "this runtime is not hosted for terminal attachment".into(),
1774                        )
1775                    })?;
1776                Ok(json!({"launch":launch}))
1777            }
1778            "harness.v1.runtimes.close" => {
1779                let params = decode::<RuntimeConnectionParams>(params)?;
1780                let (runtime, process_group) = self.surrender_runtime(&params.connection)?;
1781                close_runtime(runtime, process_group).await
1782            }
1783            _ => Err(ServiceError::MethodNotFound),
1784        }
1785    }
1786
1787    /// Deliver one message into a session that is running right now.
1788    #[cfg(feature = "adapter-api")]
1789    async fn message_call(&self, params: Value) -> std::result::Result<Value, ServiceError> {
1790        let params = decode::<MessageSessionParams>(params)?;
1791        Ok(message_live_session(&params, &crate::claude_peer::ProcessCourierRunner).await)
1792    }
1793
1794    #[cfg(feature = "adapter-api")]
1795    fn harness_settings_call(
1796        &self,
1797        method: &str,
1798        params: Value,
1799    ) -> std::result::Result<Value, ServiceError> {
1800        let homes = crate::HarnessHomes::default();
1801        match method {
1802            "harness.v1.harnesses.settings" => {
1803                let params = decode::<HarnessSettingsParams>(params)?;
1804                let report = crate::inspect_harness_interop_settings(&homes, &params.harness)
1805                    .map_err(|error| ServiceError::Operation(error.to_string()))?;
1806                serde_json::to_value(report)
1807                    .map_err(|error| ServiceError::Operation(error.to_string()))
1808            }
1809            "harness.v1.harnesses.configure" => {
1810                let params = decode::<ConfigureHarnessParams>(params)?;
1811                let report = crate::configure_harness_interop_settings(
1812                    &homes,
1813                    &params.harness,
1814                    &params.changes,
1815                    params.expected_revision.as_deref(),
1816                )
1817                .map_err(|error| ServiceError::Operation(error.to_string()))?;
1818                serde_json::to_value(report)
1819                    .map_err(|error| ServiceError::Operation(error.to_string()))
1820            }
1821            _ => Err(ServiceError::MethodNotFound),
1822        }
1823    }
1824
1825    fn insert_runtime(
1826        &mut self,
1827        runtime: Box<dyn RuntimeConnection>,
1828    ) -> std::result::Result<Value, ServiceError> {
1829        let connection = format!("runtime-{}", self.next_runtime);
1830        self.next_runtime += 1;
1831        let handle = runtime.handle().clone();
1832        self.runtime_sequences
1833            .entry(handle.runtime_id.clone())
1834            .or_insert(0);
1835        self.runtimes.insert(connection.clone(), runtime);
1836        Ok(json!({"connection": connection, "handle": handle}))
1837    }
1838
1839    #[cfg(feature = "adapter-api")]
1840    async fn insert_hosted_runtime(
1841        &mut self,
1842        runtime: Box<dyn RuntimeConnection>,
1843        capabilities: crate::RuntimeCapabilities,
1844        workspace: PathBuf,
1845    ) -> std::result::Result<Value, ServiceError> {
1846        let (host, connection) = HostedHarnessRuntime::spawn(runtime, capabilities);
1847        let token: std::sync::Arc<str> = crate::server::generate_token().into();
1848        let server = crate::server::run_frontend_http(
1849            host.clone(),
1850            host.frontend_sender(),
1851            "127.0.0.1:0",
1852            token.clone(),
1853        )
1854        .await
1855        .map_err(|error| ServiceError::Operation(error.to_string()))?;
1856        let source = LiveRuntimeSource {
1857            harness: connection.handle().harness.as_str().to_string(),
1858            session_id: connection.handle().runtime_id.clone(),
1859            workspace: workspace.clone(),
1860        };
1861        let registration = register_live_runtime(
1862            connection.handle().runtime_id.clone(),
1863            source.clone(),
1864            format!("http://{}", server.address()),
1865            token.to_string(),
1866        )
1867        .map_err(|error| ServiceError::Operation(error.to_string()))?;
1868        let endpoint = registration.endpoint().to_string();
1869        let launch = StructuredLaunch {
1870            cwd: workspace,
1871            // Pin attachment to the executable hosting this runtime. A bare
1872            // `supercode` could resolve to an older global install whose CLI
1873            // does not understand the receipt it is being asked to open.
1874            program: std::env::current_exe()
1875                .ok()
1876                .map(|path| path.to_string_lossy().into_owned())
1877                .unwrap_or_else(|| "supercode".into()),
1878            arguments: vec![
1879                "harness".into(),
1880                "attach".into(),
1881                "--endpoint".into(),
1882                endpoint,
1883                "--harness".into(),
1884                source.harness,
1885                "--session".into(),
1886                source.session_id,
1887            ],
1888            env: BTreeMap::new(),
1889        };
1890        let lease = HostedRuntimeLease {
1891            connection,
1892            _host: host,
1893            _registration: registration,
1894            _server: server,
1895        };
1896        let opened = self.insert_runtime(Box::new(lease))?;
1897        let connection_id = opened["connection"]
1898            .as_str()
1899            .expect("insert_runtime returns a connection id")
1900            .to_string();
1901        self.terminal_launches.insert(connection_id, launch);
1902        Ok(opened)
1903    }
1904
1905    #[cfg(not(feature = "adapter-api"))]
1906    async fn insert_hosted_runtime(
1907        &mut self,
1908        runtime: Box<dyn RuntimeConnection>,
1909        _capabilities: crate::RuntimeCapabilities,
1910        _workspace: PathBuf,
1911    ) -> std::result::Result<Value, ServiceError> {
1912        self.insert_runtime(runtime)
1913    }
1914
1915    fn runtime_mut(
1916        &mut self,
1917        connection: &str,
1918    ) -> std::result::Result<&mut Box<dyn RuntimeConnection>, ServiceError> {
1919        if self.runtimes_in_flight.contains(connection) {
1920            return Err(self.lent_out(connection));
1921        }
1922        self.runtimes.get_mut(connection).ok_or_else(|| {
1923            ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
1924        })
1925    }
1926
1927    /// What a caller is told about a connection that is out on a detached
1928    /// call. It is not gone and it is not free: it is mid-call, which is the
1929    /// same answer the runtime itself gives a second turn.
1930    fn lent_out(&self, connection: &str) -> ServiceError {
1931        ServiceError::Operation(format!(
1932            "runtime connection `{connection}`: a harness turn is already in progress"
1933        ))
1934    }
1935
1936    /// Take a runtime OUT of the service for the duration of one detached
1937    /// call, leaving its name marked as lent out.
1938    fn lend_runtime(
1939        &mut self,
1940        connection: &str,
1941    ) -> std::result::Result<Box<dyn RuntimeConnection>, ServiceError> {
1942        if self.runtimes_in_flight.contains(connection) {
1943            return Err(self.lent_out(connection));
1944        }
1945        let runtime = self.runtimes.remove(connection).ok_or_else(|| {
1946            ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
1947        })?;
1948        self.runtimes_in_flight.insert(connection.to_string());
1949        Ok(runtime)
1950    }
1951
1952    /// Surrender a runtime for good: the connection and everything the
1953    /// service hung off it are gone before its teardown is even attempted.
1954    ///
1955    /// `close` is what a caller reaches for when a runtime has stopped
1956    /// answering, and a runtime that has stopped answering is exactly the one
1957    /// whose graceful close cannot complete: a hosted runtime's own loop
1958    /// parks on the call the runtime never answered, so it never dequeues the
1959    /// shutdown either. Keeping the entry until teardown succeeded made a
1960    /// wedged runtime permanent — every later call on that connection, and
1961    /// every new turn, answered "a harness turn is already in progress" with
1962    /// no way to take the connection back.
1963    fn surrender_runtime(
1964        &mut self,
1965        connection: &str,
1966    ) -> std::result::Result<(Box<dyn RuntimeConnection>, Option<u32>), ServiceError> {
1967        if self.runtimes_in_flight.contains(connection) {
1968            return Err(self.lent_out(connection));
1969        }
1970        let runtime = self.runtimes.remove(connection).ok_or_else(|| {
1971            ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
1972        })?;
1973        let process_group = runtime_process_group(runtime.handle());
1974        let runtime_id = runtime.handle().runtime_id.clone();
1975        self.terminal_launches.remove(connection);
1976        self.runtime_sequences.remove(&runtime_id);
1977        self.approvals.forget(connection);
1978        Ok((runtime, process_group))
1979    }
1980
1981    /// SIGKILL the process group of every runtime this service owns, without
1982    /// waiting on any of them.
1983    ///
1984    /// A host leaving for good calls this BEFORE dropping the service. The
1985    /// handle this service holds is not the runtime's connection: a hosted
1986    /// runtime's real transport lives in the task driving it, so neither
1987    /// exiting the process nor dropping these handles reaches the harness
1988    /// process — while dropping them does remove each runtime's live-runtime
1989    /// receipt. Signalling first is what keeps a removed receipt from
1990    /// advertising a harness that is still running.
1991    pub fn kill_all_runtime_groups(&self) -> usize {
1992        self.runtimes
1993            .values()
1994            .filter(|runtime| kill_runtime_process_group(runtime_process_group(runtime.handle())))
1995            .count()
1996    }
1997
1998    /// ORCH-19: run one conversation-lifecycle verb through the harness's own
1999    /// door.
2000    ///
2001    /// Two doors, one shape. A CLI / HTTP / own-store door is self-contained
2002    /// in [`crate::sessions_control`]. A LIVE door (Hermes's and OpenClaw's
2003    /// `/new` and `/reset`, which are slash commands their gateway interprets
2004    /// INSIDE a session) is performed here, because only the service owns the
2005    /// open runtime connection — the command is typed through the very same
2006    /// `send_input` path a human's message takes, so supercode invents no
2007    /// private channel.
2008    async fn mutate_session(
2009        &mut self,
2010        verb: crate::SessionVerb,
2011        params: Value,
2012    ) -> std::result::Result<Value, ServiceError> {
2013        let mutation = decode::<crate::SessionMutation>(params)?;
2014        let door = crate::sessions_control::door(&mutation.harness, verb)
2015            .map_err(session_control_error)?;
2016        let outcome = match door {
2017            // The live door types the slash command through an open hosted
2018            // runtime, which only exists with the `adapter-api` feature; the
2019            // CLI / HTTP / own-store doors below need nothing extra.
2020            #[cfg(not(feature = "adapter-api"))]
2021            crate::SessionDoor::Live(command) => {
2022                return Err(ServiceError::Operation(format!(
2023                    "`{}` performs `sessions.{}` by typing `{command}` into a live driven \
2024                     session, which needs this build's `adapter-api` feature",
2025                    mutation.harness,
2026                    verb.as_str()
2027                )));
2028            }
2029            #[cfg(feature = "adapter-api")]
2030            crate::SessionDoor::Live(command) => {
2031                let connection = mutation
2032                    .connection
2033                    .clone()
2034                    .filter(|value| !value.trim().is_empty())
2035                    .ok_or_else(|| {
2036                        ServiceError::InvalidParams(format!(
2037                            "`{}` performs `sessions.{}` by typing `{command}` into a live \
2038                             driven session: pass the `connection` of an open runtime \
2039                             (`harness.v1.runtimes.start`)",
2040                            mutation.harness,
2041                            verb.as_str()
2042                        ))
2043                    })?;
2044                let runtime = self.runtime_mut(&connection)?;
2045                let session = live_session_name(runtime.as_ref(), &mutation);
2046                // Typing into a live session is a control call on an open
2047                // runtime, and a wedged runtime never accepts one, so it is
2048                // bounded exactly like the other control verbs. A transport
2049                // with a loop of its own lends the connection out instead of
2050                // waiting here: see [`Self::detach_runtime`].
2051                return type_live_command(runtime.as_mut(), verb, &mutation, command, session)
2052                    .await;
2053            }
2054            _ => run_session_mutation(verb, &mutation).await?,
2055        };
2056        serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
2057    }
2058
2059    /// Answer an inventory request whole, for callers that have nowhere to
2060    /// put the waiting half. A transport with a loop of its own splits it
2061    /// instead: see [`Self::detach`].
2062    async fn inventory_call(
2063        &self,
2064        method: &str,
2065        params: Value,
2066    ) -> std::result::Result<Value, ServiceError> {
2067        run_inventory(self.inventory_work(method, params)?).await
2068    }
2069
2070    /// The half of an inventory request that reads this service's state:
2071    /// resolve the selection and count the persisted sessions each row
2072    /// reports. What remains — finding executables, asking them their
2073    /// version, and (at `probe: handshake`) starting each harness and
2074    /// completing its protocol handshake — touches no service state at all.
2075    fn inventory_work(
2076        &self,
2077        method: &str,
2078        params: Value,
2079    ) -> std::result::Result<InventoryWork, ServiceError> {
2080        let mut params = decode::<HarnessInventoryParams>(params)?;
2081        if method == "harness.v1.harnesses.probe" {
2082            let harness = params.harness.take().ok_or_else(|| {
2083                ServiceError::InvalidParams("harnesses.probe requires `harness`".into())
2084            })?;
2085            params.harnesses = vec![harness];
2086        }
2087        let selected = params
2088            .harnesses
2089            .iter()
2090            .map(HarnessId::as_str)
2091            .collect::<std::collections::BTreeSet<_>>();
2092        let supported = harness_support_registry()
2093            .harnesses
2094            .into_iter()
2095            .filter(|descriptor| selected.is_empty() || selected.contains(descriptor.id.as_str()))
2096            .collect::<Vec<_>>();
2097        if !params.harnesses.is_empty() && supported.len() != selected.len() {
2098            let known = supported
2099                .iter()
2100                .map(|harness| harness.id.as_str())
2101                .collect::<std::collections::BTreeSet<_>>();
2102            let missing = params
2103                .harnesses
2104                .iter()
2105                .filter(|id| !known.contains(id.as_str()))
2106                .map(HarnessId::as_str)
2107                .collect::<Vec<_>>();
2108            return Err(ServiceError::InvalidParams(format!(
2109                "unknown harness(es): {}",
2110                missing.join(", ")
2111            )));
2112        }
2113        let global_counts = params
2114            .include_sessions
2115            .then(|| self.session_counts(None, &params.harnesses));
2116        let workspace_counts = params
2117            .include_sessions
2118            .then(|| {
2119                params
2120                    .workspace
2121                    .as_deref()
2122                    .map(|workspace| self.session_counts(Some(workspace), &params.harnesses))
2123            })
2124            .flatten();
2125        Ok(InventoryWork {
2126            params,
2127            supported,
2128            global_counts,
2129            workspace_counts,
2130        })
2131    }
2132
2133    #[cfg(feature = "adapter-api")]
2134    async fn harness_authentication_call(
2135        &self,
2136        method: &str,
2137        params: Value,
2138    ) -> std::result::Result<Value, ServiceError> {
2139        match method {
2140            "harness.v1.harnesses.auth.methods" | "harness.v1.harnesses.auth.verify" => {
2141                let params = decode::<HarnessAuthenticationParams>(params)?;
2142                serde_json::to_value(crate::inspect_harness_authentication(&params.harness).await)
2143                    .map_err(|error| ServiceError::Operation(error.to_string()))
2144            }
2145            "harness.v1.harnesses.auth.begin" => {
2146                let params = decode::<BeginHarnessAuthenticationParams>(params)?;
2147                let cwd = params
2148                    .cwd
2149                    .or_else(|| std::env::current_dir().ok())
2150                    .unwrap_or_else(|| PathBuf::from("."));
2151                let plan = crate::harness_authentication_plan(
2152                    &params.harness,
2153                    params.environment,
2154                    params.method,
2155                    &cwd,
2156                )
2157                .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
2158                serde_json::to_value(plan)
2159                    .map_err(|error| ServiceError::Operation(error.to_string()))
2160            }
2161            _ => Err(ServiceError::MethodNotFound),
2162        }
2163    }
2164
2165    fn session_counts(
2166        &self,
2167        workspace: Option<&Path>,
2168        harnesses: &[HarnessId],
2169    ) -> BTreeMap<String, usize> {
2170        let mut counts = BTreeMap::new();
2171        for session in self
2172            .catalog
2173            .discover(&DiscoveryQuery {
2174                workspace: workspace.map(Path::to_path_buf),
2175                harnesses: harnesses.to_vec(),
2176                ..DiscoveryQuery::default()
2177            })
2178            .unwrap_or_default()
2179        {
2180            *counts
2181                .entry(session.locator.harness.as_str().to_string())
2182                .or_insert(0) += 1;
2183        }
2184        counts
2185    }
2186}
2187
2188#[async_trait::async_trait]
2189impl SdkService for HarnessSessionService {
2190    fn capabilities(&self) -> SdkCapabilities {
2191        SdkCapabilities::default()
2192    }
2193
2194    async fn execute(&mut self, request: SdkRequest) -> Result<Value, SdkError> {
2195        if request.operation == SdkOperation::Events {
2196            let events = self
2197                .poll_sdk_events()
2198                .await
2199                .into_iter()
2200                .map(|(_, event)| event)
2201                .collect::<Vec<_>>();
2202            return serde_json::to_value(events).map_err(|error| {
2203                SdkError::new(
2204                    SdkErrorCode::Execution,
2205                    request.operation,
2206                    error.to_string(),
2207                )
2208            });
2209        }
2210        if self.runtimes.is_empty()
2211            && matches!(
2212                request.operation,
2213                SdkOperation::Input
2214                    | SdkOperation::Interrupt
2215                    | SdkOperation::Steer
2216                    | SdkOperation::Respond
2217                    | SdkOperation::Close
2218            )
2219        {
2220            return Err(SdkError::unsupported(request.operation));
2221        }
2222        let method = request
2223            .operation
2224            .method()
2225            .ok_or_else(|| SdkError::unsupported(request.operation))?;
2226        let result = match request.operation {
2227            SdkOperation::Discover
2228            | SdkOperation::Load
2229            | SdkOperation::Export
2230            | SdkOperation::ProfilesList
2231            | SdkOperation::ProfilesGet
2232            | SdkOperation::ProfilesCreate
2233            | SdkOperation::ProfilesDelete
2234            | SdkOperation::SkillsList
2235            | SdkOperation::SkillsInstall
2236            | SdkOperation::SkillsRemove
2237            | SdkOperation::ChannelsList
2238            | SdkOperation::RoutesList
2239            | SdkOperation::TriggersList
2240            | SdkOperation::ChannelsStatus
2241            | SdkOperation::MemoryShow
2242            | SdkOperation::MemorySearch
2243            | SdkOperation::JobsList
2244            | SdkOperation::JobsGet
2245            | SdkOperation::JobsCreate
2246            | SdkOperation::JobsUpdate
2247            | SdkOperation::JobsPause
2248            | SdkOperation::JobsResume
2249            | SdkOperation::JobsRun
2250            | SdkOperation::JobsDelete
2251            | SdkOperation::RunsList
2252            | SdkOperation::RunsGet
2253            | SdkOperation::ApprovalsList
2254            | SdkOperation::OrchestrationLoad
2255            | SdkOperation::OrchestrationSave
2256            | SdkOperation::OrchestrationCompile
2257            | SdkOperation::OrchestrationDecompile
2258            | SdkOperation::OrchestrationImport
2259            | SdkOperation::OrchestrationExport
2260            | SdkOperation::WorkflowLoad => self.call(method, request.params),
2261            // ORCH-20: answering needs the live connection, so it takes the
2262            // async door and ends in `harness.v1.runtimes.respond`.
2263            SdkOperation::ApprovalsResolve => self.approvals_resolve(request.params).await,
2264            SdkOperation::Start
2265            | SdkOperation::Resume
2266            | SdkOperation::Input
2267            | SdkOperation::Interrupt
2268            | SdkOperation::Steer
2269            | SdkOperation::Respond
2270            | SdkOperation::Close => self.runtime_call(method, request.params).await,
2271            // ORCH-19 controlled tier. Every verb goes through the HARNESS'S
2272            // OWN door — its CLI, its HTTP API, or its slash command typed
2273            // into a live driven session — and returns the row re-read from
2274            // the harness's store afterwards.
2275            SdkOperation::SessionsNew => {
2276                self.mutate_session(crate::SessionVerb::New, request.params)
2277                    .await
2278            }
2279            SdkOperation::SessionsReset => {
2280                self.mutate_session(crate::SessionVerb::Reset, request.params)
2281                    .await
2282            }
2283            SdkOperation::SessionsArchive => {
2284                self.mutate_session(crate::SessionVerb::Archive, request.params)
2285                    .await
2286            }
2287            SdkOperation::SessionsDelete => {
2288                self.mutate_session(crate::SessionVerb::Delete, request.params)
2289                    .await
2290            }
2291            SdkOperation::Events => unreachable!("handled before method dispatch"),
2292        };
2293        result.map_err(|error| sdk_error(request.operation, error))
2294    }
2295
2296    async fn events(&mut self) -> Result<Vec<SdkRuntimeEvent>, SdkError> {
2297        Ok(self
2298            .poll_sdk_events()
2299            .await
2300            .into_iter()
2301            .map(|(_, event)| event)
2302            .collect())
2303    }
2304}
2305
2306#[cfg(feature = "adapter-api")]
2307struct HostedRuntimeLease {
2308    connection: HostedHarnessConnection,
2309    _host: std::sync::Arc<HostedHarnessRuntime>,
2310    _registration: LiveRuntimeRegistration,
2311    _server: crate::server::FrontendHttpServer,
2312}
2313
2314#[async_trait::async_trait]
2315#[cfg(feature = "adapter-api")]
2316impl RuntimeConnection for HostedRuntimeLease {
2317    fn handle(&self) -> &crate::RuntimeHandle {
2318        self.connection.handle()
2319    }
2320
2321    async fn send_input(&mut self, input: RuntimeInput) -> crate::Result<Option<String>> {
2322        self.connection.send_input(input).await
2323    }
2324
2325    async fn next_event(&mut self) -> crate::Result<Option<crate::HarnessEvent>> {
2326        self.connection.next_event().await
2327    }
2328
2329    async fn interrupt(&mut self) -> crate::Result<()> {
2330        self.connection.interrupt().await
2331    }
2332
2333    async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
2334        self.connection.respond(request_id, response).await
2335    }
2336
2337    async fn close(&mut self) -> crate::Result<()> {
2338        self.connection.close().await
2339    }
2340}
2341
2342/// One inventory request's waiting half, already separated from the service
2343/// state it reads. See [`HarnessSessionService::inventory_work`].
2344struct InventoryWork {
2345    params: HarnessInventoryParams,
2346    supported: Vec<crate::HarnessSupportDescriptor>,
2347    global_counts: Option<BTreeMap<String, usize>>,
2348    workspace_counts: Option<BTreeMap<String, usize>>,
2349}
2350
2351/// Perform one conversation-lifecycle verb through a door that is
2352/// self-contained in [`crate::sessions_control`]: the harness's own CLI, its
2353/// HTTP API, the orchestrator daemon's socket, or supercode's own store.
2354/// Touches no service state, so this runs on any task. The LIVE door is not
2355/// here — it types its slash command through a runtime connection the service
2356/// owns, and is performed by [`HarnessSessionService::mutate_session`].
2357async fn run_session_mutation(
2358    verb: crate::SessionVerb,
2359    mutation: &crate::SessionMutation,
2360) -> std::result::Result<crate::SessionMutationOutcome, ServiceError> {
2361    // Only the HTTP door actually awaits anything. The CLI, store and daemon
2362    // doors run the harness's own program, or its store, with calls that
2363    // block the calling THREAD from start to finish — a future that never
2364    // yields, which no timeout around it can interrupt and which would hold a
2365    // runtime worker for as long as the harness takes. They go to a blocking
2366    // task, where blocking is what the thread is for.
2367    let door =
2368        crate::sessions_control::door(&mutation.harness, verb).map_err(session_control_error)?;
2369    if let crate::SessionDoor::Http = door {
2370        return crate::sessions_control::mutate(verb, mutation)
2371            .await
2372            .map_err(session_control_error);
2373    }
2374    let mutation = mutation.clone();
2375    tokio::task::spawn_blocking(move || crate::sessions_control::mutate_blocking(verb, &mutation))
2376        .await
2377        .map_err(|error| {
2378            ServiceError::Operation(format!("the conversation verb could not be run: {error}"))
2379        })?
2380        .map_err(session_control_error)
2381}
2382
2383/// Probe every selected harness and assemble the report. Touches no service
2384/// state, so this runs on any task.
2385async fn run_inventory(work: InventoryWork) -> std::result::Result<Value, ServiceError> {
2386    let InventoryWork {
2387        params,
2388        supported,
2389        global_counts,
2390        workspace_counts,
2391    } = work;
2392    let probes = supported.into_iter().map(|descriptor| {
2393        let global = global_counts
2394            .as_ref()
2395            .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
2396        let workspace = workspace_counts
2397            .as_ref()
2398            .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
2399        probe_harness(descriptor, &params, global, workspace)
2400    });
2401    let harnesses = futures::future::join_all(probes).await;
2402    serde_json::to_value(HarnessInventoryReport {
2403        probe: params.probe,
2404        workspace: params.workspace,
2405        harnesses,
2406    })
2407    .map_err(|error| ServiceError::Operation(error.to_string()))
2408}
2409
2410async fn probe_harness(
2411    descriptor: crate::HarnessSupportDescriptor,
2412    params: &HarnessInventoryParams,
2413    global: Option<usize>,
2414    workspace: Option<usize>,
2415) -> LocalHarness {
2416    let launch = descriptor.runtime.default_launch.as_ref();
2417    // ORC-7: the orchestrator publishes no runtime launch — it is not an
2418    // adapter supercode connects a turn to. What "installed" means for it
2419    // is that its Node daemon entry is present, so the row answers from
2420    // that instead of from a PATH lookup it could never satisfy.
2421    let orchestrator_entry = (descriptor.id.as_str() == HarnessId::ORCHESTRATOR)
2422        .then(crate::orchestrator::daemon_entry)
2423        .and_then(Result::ok);
2424    let executable = match &orchestrator_entry {
2425        Some(entry) => Some(entry.clone()),
2426        None => launch.and_then(|launch| find_executable(&launch.program)),
2427    };
2428    let installed = executable.is_some();
2429    let version = if params.skip_versions || orchestrator_entry.is_some() {
2430        // The orchestrator's "executable" is a Node module, not a CLI
2431        // with a `--version` flag; running it to ask would start a daemon.
2432        None
2433    } else {
2434        match executable.as_deref() {
2435            Some(path) => executable_version(path).await,
2436            None => None,
2437        }
2438    };
2439    let configured = auth_evidence(descriptor.id.as_str());
2440    let mut auth = if configured {
2441        HarnessAuthState::Configured
2442    } else if matches!(
2443        descriptor.id.as_str(),
2444        HarnessId::CLAUDE_CODE | HarnessId::CODEX
2445    ) {
2446        // These two adapters have explicit native status/login contracts
2447        // and complete local evidence coverage (including Claude's macOS
2448        // Keychain-backed oauthAccount marker). Treating absent evidence
2449        // as unknown advertises a start that will only fail interactively.
2450        HarnessAuthState::Required
2451    } else {
2452        HarnessAuthState::Unknown
2453    };
2454    let mut runtime = if installed {
2455        HarnessRuntimeState::Degraded
2456    } else {
2457        HarnessRuntimeState::Unavailable
2458    };
2459    let is_orchestrator = descriptor.id.as_str() == HarnessId::ORCHESTRATOR;
2460    let mut reason = (!installed).then(|| {
2461        if is_orchestrator {
2462            format!(
2463                "{} is supported but its daemon entry `{}` was not found",
2464                descriptor.display_name,
2465                crate::orchestrator::DAEMON_ENTRY
2466            )
2467        } else {
2468            format!(
2469                "{} is supported but `{}` was not found on PATH",
2470                descriptor.display_name,
2471                launch
2472                    .map(|launch| launch.program.as_str())
2473                    .unwrap_or("executable")
2474            )
2475        }
2476    });
2477    let mut repair = (!installed).then(|| {
2478        if is_orchestrator {
2479            format!(
2480                "Install the `supercode-orchestrator` package so `{}` resolves.",
2481                crate::orchestrator::DAEMON_ENTRY
2482            )
2483        } else {
2484            format!(
2485                "Install {} and ensure `{}` is on PATH.",
2486                descriptor.display_name,
2487                launch
2488                    .map(|launch| launch.program.as_str())
2489                    .unwrap_or("its executable")
2490            )
2491        }
2492    });
2493
2494    if installed && params.probe == HarnessProbeLevel::Handshake {
2495        let backend_params = RuntimeBackendParams {
2496            harness: descriptor.id.clone(),
2497            protocol: None,
2498            launch: None,
2499            base_url: None,
2500            policy: RuntimePolicy::Default,
2501        };
2502        match runtime_backend(&backend_params) {
2503            Ok(backend) => {
2504                let cwd = params
2505                    .workspace
2506                    .clone()
2507                    .or_else(|| std::env::current_dir().ok())
2508                    .unwrap_or_else(|| PathBuf::from("."));
2509                let isolated = descriptor
2510                    .runtime
2511                    .default_launch
2512                    .clone()
2513                    .and_then(|launch| IsolatedProbeHome::new(descriptor.id.as_str(), launch).ok());
2514                let Some(isolated) = isolated else {
2515                    reason = Some(
2516                        "No-prompt runtime handshake could not create its isolated harness home."
2517                            .into(),
2518                    );
2519                    repair = Some(
2520                        "Check temporary-directory permissions, then run the handshake probe again."
2521                            .into(),
2522                    );
2523                    let running = probe_running_instance(descriptor.id.as_str());
2524                    return LocalHarness {
2525                        gateway: gateway_health(
2526                            descriptor.id.as_str(),
2527                            installed,
2528                            running.as_ref(),
2529                            version.as_deref(),
2530                        ),
2531                        id: descriptor.id,
2532                        display_name: descriptor.display_name,
2533                        supported: true,
2534                        installed,
2535                        executable: executable.map(|path| path.to_string_lossy().into_owned()),
2536                        version,
2537                        auth,
2538                        runtime,
2539                        protocol: descriptor.runtime.protocol,
2540                        capabilities: descriptor.runtime.capabilities.clone(),
2541                        effective_capabilities: descriptor.runtime.capabilities,
2542                        sessions: HarnessSessionCounts { global, workspace },
2543                        running,
2544                        reason,
2545                        repair,
2546                    };
2547                };
2548                match tokio::time::timeout(
2549                    Duration::from_secs(30),
2550                    backend.start(RuntimeStartRequest {
2551                        cwd,
2552                        launch: Some(isolated.launch.clone()),
2553                        mcp_servers: Vec::new(),
2554                    }),
2555                )
2556                .await
2557                {
2558                    Ok(Ok(mut connection)) => {
2559                        match stabilize_handshake(connection.as_mut()).await {
2560                            Ok(()) => {
2561                                auth = HarnessAuthState::Ready;
2562                                runtime = HarnessRuntimeState::Ready;
2563                                reason = Some(
2564                                    "No-prompt runtime handshake remained healthy through the startup stabilization window; no model request was sent."
2565                                        .into(),
2566                                );
2567                                repair = None;
2568                            }
2569                            Err(message) => {
2570                                auth = if looks_like_auth_error(&message) {
2571                                    HarnessAuthState::Required
2572                                } else if configured {
2573                                    HarnessAuthState::Configured
2574                                } else {
2575                                    HarnessAuthState::Unknown
2576                                };
2577                                reason = Some(format!(
2578                                    "No-prompt runtime handshake became unhealthy during startup: {message}"
2579                                ));
2580                                repair = Some(if auth == HarnessAuthState::Required {
2581                                    format!(
2582                                        "Run `{}` interactively once and complete sign-in, then probe again.",
2583                                        launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2584                                    )
2585                                } else {
2586                                    "Run the harness directly to inspect its startup failure, then probe again."
2587                                        .into()
2588                                });
2589                            }
2590                        }
2591                        let _ =
2592                            tokio::time::timeout(Duration::from_secs(3), connection.close()).await;
2593                    }
2594                    Ok(Err(error)) => {
2595                        let message = truncate_text(&error.to_string(), 500);
2596                        auth = if looks_like_auth_error(&message) {
2597                            HarnessAuthState::Required
2598                        } else if configured {
2599                            HarnessAuthState::Configured
2600                        } else {
2601                            HarnessAuthState::Unknown
2602                        };
2603                        reason = Some(format!("No-prompt runtime handshake failed: {message}"));
2604                        repair = Some(if auth == HarnessAuthState::Required {
2605                            format!(
2606                                "Run `{}` interactively once and complete sign-in, then probe again.",
2607                                launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2608                            )
2609                        } else {
2610                            "Check the harness installation and run the handshake probe again."
2611                                .into()
2612                        });
2613                    }
2614                    Err(_) => {
2615                        reason =
2616                            Some("No-prompt runtime handshake timed out after 30 seconds.".into());
2617                        repair = Some("Run the harness directly to check startup or authentication, then probe again.".into());
2618                    }
2619                }
2620                // Keep the isolated home alive through process teardown.
2621                // Otherwise the compiler may release the last meaningful
2622                // use after cloning `launch`, and a still-starting CLI can
2623                // recreate its state directory after Drop removed it.
2624                // Some Node-based launchers finish a short asynchronous
2625                // installation-id write just after their parent process
2626                // is reaped. Remove once immediately, allow that bounded
2627                // writer to settle, then perform the authoritative pass.
2628                let _ = isolated.cleanup();
2629                tokio::time::sleep(Duration::from_millis(250)).await;
2630                if let Err(error) = isolated.cleanup() {
2631                    auth = if configured {
2632                        HarnessAuthState::Configured
2633                    } else {
2634                        HarnessAuthState::Unknown
2635                    };
2636                    runtime = HarnessRuntimeState::Degraded;
2637                    reason = Some(format!(
2638                        "No-prompt runtime handshake could not remove its isolated harness home: {error}"
2639                    ));
2640                    repair = Some(
2641                        "Check temporary-directory permissions, remove the reported disposable probe home, then run the handshake again."
2642                            .into(),
2643                    );
2644                }
2645            }
2646            Err(error) => {
2647                reason = Some(error_message(error));
2648            }
2649        }
2650    } else if installed && configured {
2651        reason = Some("Executable and local authentication evidence found; use a handshake probe to verify readiness.".into());
2652    } else if installed && auth == HarnessAuthState::Required {
2653        reason = Some("Executable found, but no native authentication evidence is present.".into());
2654        repair = Some(format!(
2655            "Run `supercode harness login {}` to use the harness-owned sign-in flow.",
2656            descriptor.id.as_str()
2657        ));
2658    } else if installed {
2659        reason = Some("Executable found; authentication readiness is unknown until a no-prompt handshake succeeds.".into());
2660        repair = Some(format!(
2661            "Run `{}` interactively once if sign-in is required, or use `--probe handshake`.",
2662            launch
2663                .map(|launch| launch.program.as_str())
2664                .unwrap_or("the harness")
2665        ));
2666    }
2667
2668    let effective_capabilities = if installed {
2669        descriptor.runtime.capabilities.clone()
2670    } else {
2671        unavailable_capabilities()
2672    };
2673    let running = probe_running_instance(descriptor.id.as_str());
2674    LocalHarness {
2675        gateway: gateway_health(
2676            descriptor.id.as_str(),
2677            installed,
2678            running.as_ref(),
2679            version.as_deref(),
2680        ),
2681        id: descriptor.id,
2682        display_name: descriptor.display_name,
2683        supported: true,
2684        installed,
2685        executable: executable.map(|path| path.to_string_lossy().into_owned()),
2686        version,
2687        auth,
2688        runtime,
2689        protocol: descriptor.runtime.protocol,
2690        capabilities: descriptor.runtime.capabilities,
2691        effective_capabilities,
2692        sessions: HarnessSessionCounts { global, workspace },
2693        running,
2694        reason,
2695        repair,
2696    }
2697}
2698
2699async fn stabilize_handshake(connection: &mut dyn RuntimeConnection) -> Result<(), String> {
2700    let deadline = tokio::time::Instant::now() + Duration::from_secs(3);
2701    loop {
2702        let now = tokio::time::Instant::now();
2703        if now >= deadline {
2704            return Ok(());
2705        }
2706        match tokio::time::timeout(deadline - now, connection.next_event()).await {
2707            Err(_) => return Ok(()),
2708            Ok(Ok(Some(event))) => {
2709                if let Some(message) = handshake_event_failure(&event) {
2710                    return Err(truncate_text(&message, 500));
2711                }
2712            }
2713            Ok(Ok(None)) => return Err("runtime transport closed during startup".into()),
2714            Ok(Err(error)) => return Err(error.to_string()),
2715        }
2716    }
2717}
2718
2719fn handshake_event_failure(event: &crate::HarnessEvent) -> Option<String> {
2720    let detail = event
2721        .payload
2722        .get("message")
2723        .or_else(|| event.payload.get("line"))
2724        .and_then(Value::as_str)
2725        .unwrap_or(event.kind.as_str());
2726    match event.kind.as_str() {
2727        "transport_closed" => Some("runtime transport closed during startup".into()),
2728        "transport_error" => Some(format!("runtime transport error: {detail}")),
2729        "malformed_output" => Some(format!("runtime emitted non-protocol output: {detail}")),
2730        // Stderr is retained as a runtime event, but is not transport health.
2731        // Grok, for example, can log an AuthorizationRequired error from an
2732        // optional background worker while its ACP session continues to send
2733        // updates and complete prompts normally.
2734        _ => None,
2735    }
2736}
2737
2738fn indexed_claude_window(
2739    locator: &SessionLocator,
2740    options: &SessionLoadOptions,
2741) -> std::result::Result<Option<Value>, ServiceError> {
2742    use supercode_interchange::session::ClaudeReadIndex;
2743    // Exact parent-only window: recursive/full-artifact requests retain the
2744    // existing owner. This is not a bounded display-history substitution.
2745    if locator.harness.as_str() != HarnessId::CLAUDE_CODE
2746        || options.include_subagents != Some(false)
2747    {
2748        return Ok(None);
2749    }
2750    let crate::StorageLocator::File { path } = &locator.storage else {
2751        return Ok(None);
2752    };
2753    if !ClaudeReadIndex::supports(path)
2754        .map_err(|error| ServiceError::Operation(error.to_string()))?
2755    {
2756        return Ok(None);
2757    }
2758    let mut index = ClaudeReadIndex::open(path, Fidelity::ByteLossless)
2759        .map_err(|error| ServiceError::Operation(error.to_string()))?;
2760    let total = index.len();
2761    let (offset, end) = projected_message_window(total, options);
2762    let session = index
2763        .read_messages(offset..end)
2764        .map_err(|error| ServiceError::Operation(error.to_string()))?;
2765    let summary = index
2766        .read_summary()
2767        .map_err(|error| ServiceError::Operation(error.to_string()))?;
2768    let selected_options = SessionLoadOptions {
2769        message_offset: None,
2770        message_limit: None,
2771        message_tail: None,
2772        ..options.clone()
2773    };
2774    let mut selected = projected_session_json(&session, &selected_options);
2775    selected["raw_record_count"] = json!(index.raw_record_count());
2776    Ok(Some(json!({
2777        "session": selected,
2778        "summary": projected_session_summary(&summary, options),
2779        "window": {
2780            "has_more": offset > 0 || end < total, "has_newer": end < total,
2781            "has_older": offset > 0, "newer_items": index.item_count(end..total),
2782            "offset": offset, "older_items": index.item_count(0..offset),
2783            "returned": end - offset, "total_messages": total,
2784        }
2785    })))
2786}
2787
2788fn projected_session_result(session: &Session, options: &SessionLoadOptions) -> Value {
2789    let total_messages = session.messages.len();
2790    let (offset, end) = projected_message_window(total_messages, options);
2791    json!({
2792        "session": projected_session_json(session, options),
2793        "summary": projected_session_summary(session, options),
2794        "window": {
2795            "has_more": offset > 0 || end < total_messages,
2796            "has_newer": end < total_messages,
2797            "has_older": offset > 0,
2798            "newer_items": normalized_item_count(&session.messages[end..]),
2799            "offset": offset,
2800            "older_items": normalized_item_count(&session.messages[..offset]),
2801            "returned": end.saturating_sub(offset),
2802            "total_messages": total_messages,
2803        }
2804    })
2805}
2806
2807fn normalized_item_count(messages: &[crate::ChatMessage]) -> usize {
2808    messages
2809        .iter()
2810        .map(|message| {
2811            let conversation = usize::from(
2812                matches!(message.role, Role::Assistant | Role::User)
2813                    && message_has_content(message),
2814            );
2815            let tool_result =
2816                usize::from(message.role == Role::Tool && message_has_content(message));
2817            conversation + tool_result + message.tool_calls().len()
2818        })
2819        .sum()
2820}
2821
2822fn projected_session_summary(session: &Session, options: &SessionLoadOptions) -> Value {
2823    let mut conversational = session.messages.iter().filter(|message| {
2824        matches!(message.role, Role::Assistant | Role::User) && message_has_content(message)
2825    });
2826    let first_message = conversational.clone().next();
2827    let last_message = conversational.next_back();
2828    let mut assistant = session
2829        .messages
2830        .iter()
2831        .filter(|message| message.role == Role::Assistant && message_has_content(message));
2832    let first_assistant_message = assistant.clone().next();
2833    let last_assistant_message = assistant.next_back();
2834    let end_of_turn = session
2835        .messages
2836        .iter()
2837        .rev()
2838        .find(|message| message.role != Role::System)
2839        .is_some_and(|message| {
2840            message.role == Role::Assistant
2841                && message_has_content(message)
2842                && message.tool_calls().is_empty()
2843        });
2844    let project = |message: Option<&crate::ChatMessage>| {
2845        message.map(|message| project_inline_media(message_json(message), options))
2846    };
2847    json!({
2848        "end_of_turn": end_of_turn,
2849        "first_assistant_message": project(first_assistant_message),
2850        "first_message": project(first_message),
2851        "last_assistant_message": project(last_assistant_message),
2852        "last_assistant_text": last_assistant_message.map(message_text).unwrap_or_default(),
2853        "last_message": project(last_message),
2854    })
2855}
2856
2857fn message_has_content(message: &crate::ChatMessage) -> bool {
2858    message
2859        .content
2860        .as_deref()
2861        .is_some_and(|content| !content.trim().is_empty())
2862        || message
2863            .content_parts
2864            .as_ref()
2865            .is_some_and(|parts| !parts.is_empty())
2866}
2867
2868fn message_text(message: &crate::ChatMessage) -> String {
2869    if let Some(content) = &message.content {
2870        return content.clone();
2871    }
2872    message
2873        .content_parts
2874        .as_ref()
2875        .into_iter()
2876        .flatten()
2877        .filter_map(|part| part.get("text").and_then(Value::as_str))
2878        .collect::<Vec<_>>()
2879        .join("\n")
2880}
2881
2882fn projected_session_json(session: &Session, options: &SessionLoadOptions) -> Value {
2883    let (offset, end) = projected_message_window(session.messages.len(), options);
2884    let messages = session.messages[offset..end]
2885        .iter()
2886        .map(|message| project_inline_media(message_json(message), options))
2887        .collect::<Vec<_>>();
2888    let subagents = if options.include_subagents.unwrap_or(true) {
2889        // The reported window describes the top-level transcript. Applying it
2890        // recursively would silently truncate subagents without returning a
2891        // window for each child. Keep their histories complete while carrying
2892        // the caller's media policy through the tree.
2893        let subagent_options = SessionLoadOptions {
2894            message_limit: None,
2895            message_offset: None,
2896            message_tail: None,
2897            ..options.clone()
2898        };
2899        session
2900            .subagents
2901            .iter()
2902            .map(|subagent| projected_session_json(subagent, &subagent_options))
2903            .collect::<Vec<_>>()
2904    } else {
2905        Vec::new()
2906    };
2907    json!({
2908        "source": match session.meta.source {
2909            SessionSource::ClaudeCode => "claude_code",
2910            SessionSource::Codex => "codex",
2911            SessionSource::Gemini => "gemini",
2912            SessionSource::Goose => "goose",
2913            SessionSource::Grok => "grok",
2914            SessionSource::Native => "native",
2915            SessionSource::OpenClaw => "openclaw",
2916            SessionSource::Hermes => "hermes",
2917            SessionSource::OpenCode => "opencode",
2918            SessionSource::Pi => "pi",
2919        },
2920        "session_id": session.meta.session_id,
2921        "ended_at": session.meta.ended_at,
2922        "end_reason": session.meta.end_reason,
2923        "model": session.meta.model,
2924        "cwd": session.meta.cwd,
2925        "system_prompt": session.meta.system_prompt,
2926        "agent_id": session.meta.agent_id,
2927        "parent_tool_use_id": session.meta.parent_tool_use_id,
2928        "lineage": session.meta.lineage,
2929        "messages": messages,
2930        "subagents": subagents,
2931        "raw_record_count": session.raw.len(),
2932        "parse_error_lines": session.parse_error_lines,
2933    })
2934}
2935
2936fn projected_message_window(total: usize, options: &SessionLoadOptions) -> (usize, usize) {
2937    if let Some(tail) = options.message_tail {
2938        return (total.saturating_sub(tail), total);
2939    }
2940    let offset = options.message_offset.unwrap_or(0).min(total);
2941    let end = options
2942        .message_limit
2943        .map(|limit| offset.saturating_add(limit).min(total))
2944        .unwrap_or(total);
2945    (offset, end)
2946}
2947
2948fn project_inline_media(mut message: Value, options: &SessionLoadOptions) -> Value {
2949    let Some(parts) = message.get_mut("content").and_then(Value::as_array_mut) else {
2950        return message;
2951    };
2952    for part in parts {
2953        let Some(url) = part
2954            .get("image_url")
2955            .and_then(|image| image.get("url"))
2956            .and_then(Value::as_str)
2957        else {
2958            continue;
2959        };
2960        let Some(rest) = url.strip_prefix("data:") else {
2961            continue;
2962        };
2963        let Some((media_type, encoded)) = rest.split_once(";base64,") else {
2964            continue;
2965        };
2966        let padding = usize::from(encoded.ends_with('=')) + usize::from(encoded.ends_with("=="));
2967        let decoded_bytes = encoded.len().saturating_mul(3) / 4;
2968        let decoded_bytes = decoded_bytes.saturating_sub(padding);
2969        let should_elide = matches!(options.inline_media, InlineMediaMode::Metadata)
2970            || options
2971                .max_inline_media_bytes
2972                .is_some_and(|limit| decoded_bytes > limit);
2973        if should_elide {
2974            *part = json!({
2975                "type": "media_reference",
2976                "media_type": media_type,
2977                "encoding": "base64",
2978                "encoded_bytes": encoded.len(),
2979                "decoded_bytes": decoded_bytes,
2980                "omitted": true,
2981            });
2982        }
2983    }
2984    message
2985}
2986
2987#[derive(Deserialize)]
2988struct LocatorParams {
2989    locator: SessionLocator,
2990    /// Optional fidelity for the READ surfaces (`sessions.load`,
2991    /// `sessions.follow`).
2992    ///
2993    /// Omitted means [`Fidelity::Semantic`]: these two methods only ever
2994    /// produce a read-only view, and a compacted or resumed-across-files
2995    /// transcript — the everyday shape of a long Claude Code session — has no
2996    /// losslessly reconstructable record graph, so refusing to render it made
2997    /// the mirror unusable rather than accurate. A caller that intends to
2998    /// CONTINUE from what it reads asks for a lossless level explicitly and
2999    /// gets the strict refusal back. Every other method (export, translate,
3000    /// branch, handoff, resume_instructions) is lossless-only and has no
3001    /// such knob.
3002    #[serde(default)]
3003    fidelity: Option<Fidelity>,
3004    /// Optional bounded frontend projection. Absent preserves the historical
3005    /// complete-session read contract.
3006    #[serde(default)]
3007    view: Option<SessionReadView>,
3008}
3009
3010#[derive(Deserialize)]
3011struct SessionReadView {
3012    /// Number of trailing normalized messages to return. Zero is treated as
3013    /// one so a caller cannot accidentally request an unbounded empty mode.
3014    #[serde(default)]
3015    tail_messages: Option<usize>,
3016    /// Whether Claude Code child transcripts belong in this view. The
3017    /// frontend default is false; the legacy no-view path remains true.
3018    #[serde(default)]
3019    include_subagents: bool,
3020    /// Preserve human-visible native history across model-context compaction.
3021    #[serde(default)]
3022    display_history: bool,
3023    /// Bound each individual text field so a single tool result cannot turn a
3024    /// small message window into a hundred-megabyte RPC response.
3025    #[serde(default)]
3026    max_message_chars: Option<usize>,
3027}
3028
3029impl LocatorParams {
3030    fn read_fidelity(&self) -> Fidelity {
3031        self.fidelity.unwrap_or(Fidelity::Semantic)
3032    }
3033
3034    fn include_subagents(&self) -> bool {
3035        self.view
3036            .as_ref()
3037            .map(|view| view.include_subagents)
3038            .unwrap_or(true)
3039    }
3040
3041    fn tail_messages(&self) -> Option<usize> {
3042        self.view
3043            .as_ref()
3044            .and_then(|view| view.tail_messages)
3045            .map(|limit| limit.clamp(1, 5_000))
3046    }
3047
3048    fn display_history(&self) -> bool {
3049        self.view.as_ref().is_some_and(|view| view.display_history)
3050    }
3051
3052    fn max_message_chars(&self) -> Option<usize> {
3053        self.view
3054            .as_ref()
3055            .and_then(|view| view.max_message_chars)
3056            .map(|limit| limit.clamp(256, 64_000))
3057    }
3058
3059    fn bound_session(&self, session: &mut Session) {
3060        bound_session_view(session, self.tail_messages(), self.max_message_chars());
3061    }
3062}
3063
3064#[derive(Debug, Clone, Copy, Default, Deserialize)]
3065#[serde(rename_all = "snake_case")]
3066enum InlineMediaMode {
3067    #[default]
3068    Full,
3069    Metadata,
3070}
3071
3072#[derive(Debug, Clone, Default, Deserialize)]
3073#[serde(default)]
3074struct SessionLoadOptions {
3075    include_subagents: Option<bool>,
3076    inline_media: InlineMediaMode,
3077    max_inline_media_bytes: Option<usize>,
3078    message_limit: Option<usize>,
3079    message_offset: Option<usize>,
3080    message_tail: Option<usize>,
3081}
3082
3083impl SessionLoadOptions {
3084    fn validate(&self) -> std::result::Result<(), ServiceError> {
3085        if self.message_tail.is_some()
3086            && (self.message_limit.is_some() || self.message_offset.is_some())
3087        {
3088            return Err(ServiceError::InvalidParams(
3089                "sessions.load options.message_tail cannot be combined with message_limit or message_offset"
3090                    .into(),
3091            ));
3092        }
3093        Ok(())
3094    }
3095}
3096
3097#[derive(Deserialize)]
3098struct LoadSessionParams {
3099    #[serde(flatten)]
3100    read: LocatorParams,
3101    #[serde(default)]
3102    options: Option<SessionLoadOptions>,
3103}
3104
3105#[derive(Deserialize)]
3106struct UnfollowParams {
3107    subscription: String,
3108}
3109
3110#[derive(Debug, Deserialize)]
3111#[serde(deny_unknown_fields)]
3112struct IndexResizeParams {
3113    subscription: String,
3114    limit: usize,
3115}
3116
3117#[derive(Deserialize)]
3118struct ActivitySubscribeParams {
3119    locators: Vec<SessionLocator>,
3120    #[serde(default)]
3121    homes: crate::HarnessHomes,
3122}
3123
3124#[derive(Deserialize)]
3125struct MessageSessionParams {
3126    locator: SessionLocator,
3127    text: String,
3128    /// Same storage roots discovery accepts, so a caller (and a test) can
3129    /// point the live-session registry somewhere other than `$HOME`.
3130    #[serde(default)]
3131    homes: crate::HarnessHomes,
3132}
3133
3134#[derive(Deserialize)]
3135#[serde(deny_unknown_fields)]
3136struct HarnessSettingsParams {
3137    harness: String,
3138}
3139
3140#[derive(Deserialize)]
3141#[serde(deny_unknown_fields)]
3142struct ConfigureHarnessParams {
3143    harness: String,
3144    #[serde(default)]
3145    changes: Vec<crate::HarnessSettingChange>,
3146    #[serde(default)]
3147    expected_revision: Option<String>,
3148}
3149
3150fn claude_inbound_controls_or_error(homes: &crate::HarnessHomes) -> (Value, Value) {
3151    match crate::inspect_harness_interop_settings(homes, HarnessId::CLAUDE_CODE) {
3152        Ok(report) => (
3153            serde_json::to_value(report).unwrap_or(Value::Null),
3154            Value::Null,
3155        ),
3156        Err(error) => (
3157            Value::Null,
3158            Value::String(format!(
3159                "Supercode could not inspect Claude Code inbound controls: {error}"
3160            )),
3161        ),
3162    }
3163}
3164
3165/// Deliver `text` into a session that is running right now, or say why not.
3166///
3167/// A refusal is a RESULT, not a JSON-RPC error: "that session is persisted
3168/// only" is an answer about the session, which a mirror renders next to the
3169/// transcript, and this service's error envelope carries no structured data
3170/// field a machine-readable reason could survive in.
3171///
3172/// `delivered_to_bus` is the honest ceiling of what the courier proves. The
3173/// message reached the receiving session's inbox; whether that session ever
3174/// reads it is governed by ITS OWN inbound controls (`crossSessionInbound`,
3175/// approval dialogs), which Supercode neither sees nor overrides.
3176async fn message_live_session(
3177    params: &MessageSessionParams,
3178    runner: &dyn crate::claude_peer::CourierRunner,
3179) -> Value {
3180    if params.locator.harness.as_str() != HarnessId::CLAUDE_CODE {
3181        return json!({
3182            "delivered_to_bus": false,
3183            "refusal": {
3184                "reason": crate::claude_peer::ClaudePeerRefusal::HarnessUnsupported.as_str(),
3185                "message": format!(
3186                    "`{}` does not publish a live-session registry; only claude-code sessions can be messaged in place",
3187                    params.locator.harness.as_str()
3188                ),
3189            },
3190        });
3191    }
3192    let (inbound_controls, inbound_controls_error) =
3193        claude_inbound_controls_or_error(&params.homes);
3194    match crate::claude_peer::message_claude_peer(
3195        &params.homes,
3196        &params.locator.session_id,
3197        &params.text,
3198        runner,
3199    )
3200    .await
3201    {
3202        Ok(delivery) => json!({
3203            "delivered_to_bus": true,
3204            "target": {
3205                "session_id": delivery.target.session_id,
3206                "name": delivery.target.name,
3207                "pid": delivery.target.pid,
3208                "cwd": delivery.target.cwd,
3209                "status": delivery.target.status.map(|status| status.as_str()),
3210            },
3211            "courier": {
3212                "model": crate::claude_peer::COURIER_MODEL,
3213                "report": delivery.courier_report,
3214            },
3215            "inbound_controls": inbound_controls,
3216            "inbound_controls_error": inbound_controls_error,
3217        }),
3218        Err(refusal) => json!({
3219            "delivered_to_bus": false,
3220            "refusal": {"reason": refusal.reason.as_str(), "message": refusal.message},
3221            "inbound_controls": inbound_controls,
3222            "inbound_controls_error": inbound_controls_error,
3223        }),
3224    }
3225}
3226
3227/// Source identity of one follow subscription, plus the last lifecycle state
3228/// already reported on it. The follower itself stays purely persistence-facing.
3229// Only the adapter-api poll reads these; the subscription bookkeeping itself is
3230// shared by both builds.
3231#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
3232struct FollowedSource {
3233    harness: String,
3234    session_id: String,
3235    reported: Option<String>,
3236}
3237
3238#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
3239struct ActivitySubscription {
3240    locators: Vec<SessionLocator>,
3241    homes: crate::HarnessHomes,
3242    reported: BTreeMap<(String, String), crate::SessionActivity>,
3243}
3244
3245fn peers_for_descriptors(
3246    descriptors: &[SessionDescriptor],
3247    homes: &HarnessHomes,
3248) -> Vec<crate::claude_peer::ClaudePeerSession> {
3249    if descriptors
3250        .iter()
3251        .any(|session| session.locator.harness.as_str() == HarnessId::CLAUDE_CODE)
3252    {
3253        crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
3254    } else {
3255        Vec::new()
3256    }
3257}
3258
3259/// Add the live address that makes an indexed row behaviorally equivalent to a discovered row.
3260///
3261/// The durable index owns only persistence metadata. Live endpoints remain projections: every
3262/// message/attach operation revalidates its authority, so publishing one here never trusts a stale
3263/// browser-held handle. Reading the Claude registry once per batch keeps this O(peers + rows).
3264fn live_descriptor_value(
3265    session: &SessionDescriptor,
3266    peers: &[crate::claude_peer::ClaudePeerSession],
3267) -> std::result::Result<Value, ServiceError> {
3268    let mut value = serde_json::to_value(session)
3269        .map_err(|error| ServiceError::Operation(error.to_string()))?;
3270    if let Some(workspace) = &session.cwd {
3271        let source = LiveRuntimeSource {
3272            harness: session.locator.harness.as_str().to_string(),
3273            session_id: session.locator.session_id.clone(),
3274            workspace: workspace.clone(),
3275        };
3276        if let Some(endpoint) = discover_live_runtime(&source)
3277            .map_err(|error| ServiceError::Operation(error.to_string()))?
3278        {
3279            value["live_endpoint"] = json!(endpoint.as_str());
3280        }
3281    }
3282    if value.get("live_endpoint").is_none() {
3283        if let Some(peer) = peers.iter().find(|peer| {
3284            session.locator.harness.as_str() == HarnessId::CLAUDE_CODE
3285                && peer.session_id == session.locator.session_id
3286        }) {
3287            value["live_endpoint"] = json!(peer.endpoint().as_str());
3288        }
3289    }
3290    Ok(value)
3291}
3292
3293fn live_index_changes(
3294    changes: Vec<crate::session_index::SessionIndexChange>,
3295    homes: &HarnessHomes,
3296) -> std::result::Result<Vec<Value>, ServiceError> {
3297    use crate::session_index::SessionIndexChange;
3298    let has_claude = changes.iter().any(|change| match change {
3299        SessionIndexChange::Added { descriptor } | SessionIndexChange::Updated { descriptor } => {
3300            descriptor.locator.harness.as_str() == HarnessId::CLAUDE_CODE
3301        }
3302        SessionIndexChange::Removed { .. } => false,
3303    });
3304    let peers = if has_claude {
3305        crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
3306    } else {
3307        Vec::new()
3308    };
3309    changes
3310        .into_iter()
3311        .map(|change| match change {
3312            SessionIndexChange::Added { descriptor } => Ok(json!({
3313                "kind": "added",
3314                "descriptor": live_descriptor_value(&descriptor, &peers)?,
3315            })),
3316            SessionIndexChange::Updated { descriptor } => Ok(json!({
3317                "kind": "updated",
3318                "descriptor": live_descriptor_value(&descriptor, &peers)?,
3319            })),
3320            SessionIndexChange::Removed { key } => Ok(json!({
3321                "kind": "removed",
3322                "key": key,
3323            })),
3324        })
3325        .collect()
3326}
3327
3328fn legacy_live_status(activity: &crate::SessionActivity) -> Option<&'static str> {
3329    use crate::{SessionPresence, SessionTurnState};
3330    match (activity.presence, activity.turn) {
3331        (SessionPresence::Persisted, _) => None,
3332        (SessionPresence::Running, SessionTurnState::Working) => Some("busy"),
3333        (SessionPresence::Running, SessionTurnState::Idle) => Some("idle"),
3334        // The normalized activity object can honestly report a live owner even
3335        // when the stock harness never published a turn status. Preserve the
3336        // older field's stricter contract instead of guessing `running`.
3337        (SessionPresence::Running, SessionTurnState::Unknown)
3338            if activity.evidence.native_state.is_none() =>
3339        {
3340            None
3341        }
3342        (SessionPresence::Running, _) | (SessionPresence::ShuttingDown, _) => Some("running"),
3343    }
3344}
3345
3346#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
3347#[serde(rename_all = "kebab-case")]
3348enum TransferFormat {
3349    ClaudeCode,
3350    Codex,
3351    #[serde(rename = "opencode", alias = "open-code")]
3352    OpenCode,
3353    Pi,
3354    Grok,
3355    Gemini,
3356    Goose,
3357    /// UNI-18: a Hermes target. Its artifact is the Codex rollout that
3358    /// `hermes sessions import --from codex` reads; `sessions.export` performs
3359    /// that import into the Hermes home.
3360    Hermes,
3361}
3362
3363impl TransferFormat {
3364    fn id(self) -> &'static str {
3365        match self {
3366            Self::ClaudeCode => HarnessId::CLAUDE_CODE,
3367            Self::Codex => HarnessId::CODEX,
3368            Self::OpenCode => HarnessId::OPENCODE,
3369            Self::Pi => HarnessId::PI,
3370            Self::Grok => HarnessId::GROK,
3371            Self::Gemini => HarnessId::GEMINI,
3372            Self::Goose => HarnessId::GOOSE,
3373            Self::Hermes => HarnessId::HERMES,
3374        }
3375    }
3376}
3377
3378impl From<TransferFormat> for SessionFormat {
3379    fn from(value: TransferFormat) -> Self {
3380        match value {
3381            TransferFormat::ClaudeCode => Self::ClaudeCode,
3382            TransferFormat::Codex => Self::Codex,
3383            TransferFormat::OpenCode => Self::OpenCode,
3384            TransferFormat::Pi => Self::Pi,
3385            TransferFormat::Grok => Self::Grok,
3386            TransferFormat::Gemini => Self::Gemini,
3387            TransferFormat::Goose => Self::Goose,
3388            // a Hermes artifact is the Codex rollout Hermes imports
3389            TransferFormat::Hermes => Self::Codex,
3390        }
3391    }
3392}
3393
3394#[derive(Deserialize)]
3395struct ImportSessionParams {
3396    source_harness: TransferFormat,
3397    content: String,
3398}
3399
3400#[derive(Deserialize)]
3401struct ExportSessionParams {
3402    locator: SessionLocator,
3403    target_harness: TransferFormat,
3404}
3405
3406#[derive(Deserialize)]
3407struct ReduceSessionParams {
3408    locator: SessionLocator,
3409    target_harness: TransferFormat,
3410    #[serde(default = "default_keep_last")]
3411    keep_last: usize,
3412}
3413
3414fn default_keep_last() -> usize {
3415    6
3416}
3417
3418#[derive(Deserialize)]
3419struct BranchSessionParams {
3420    locator: SessionLocator,
3421    #[serde(default)]
3422    target_harness: Option<TransferFormat>,
3423}
3424
3425#[derive(Deserialize)]
3426struct HandoffSessionParams {
3427    locator: SessionLocator,
3428    target_harness: TransferFormat,
3429    #[serde(default)]
3430    cwd: Option<PathBuf>,
3431}
3432
3433#[derive(Debug, Clone, Copy, Default, Deserialize)]
3434#[serde(rename_all = "snake_case")]
3435enum ResumePolicy {
3436    #[default]
3437    Default,
3438    Yolo,
3439}
3440
3441#[derive(Deserialize)]
3442struct ResumeInstructionsParams {
3443    locator: SessionLocator,
3444    #[serde(default)]
3445    cwd: Option<PathBuf>,
3446    #[serde(default)]
3447    policy: ResumePolicy,
3448}
3449
3450/// `harness.v1.workflow.load` parameters: which harness's board, and its home.
3451#[derive(Deserialize)]
3452struct WorkflowLoadParams {
3453    from: crate::workflow_doors::WorkflowHarness,
3454    home: PathBuf,
3455}
3456
3457/// ONT-4 `harness.v1.orchestration.load` parameters. `flavor` says which layout the
3458/// folder is read as; our own is the default.
3459#[derive(Deserialize)]
3460struct OrchestrationLoadParams {
3461    root: PathBuf,
3462    #[serde(default)]
3463    flavor: crate::orchestration_doors::HomeFlavor,
3464}
3465
3466/// ONT-4 `harness.v1.orchestration.save` parameters. `vault` is merged into the
3467/// home's own secrets; a caller that sends none keeps what is on disk.
3468#[derive(Deserialize)]
3469struct OrchestrationSaveParams {
3470    root: PathBuf,
3471    orchestration: crate::orchestration::Orchestration,
3472    #[serde(default)]
3473    vault: BTreeMap<String, String>,
3474}
3475
3476/// ONT-4 `harness.v1.orchestration.compile` parameters.
3477#[derive(Deserialize)]
3478struct OrchestrationCompileParams {
3479    from: crate::orchestration_doors::OrchestrationHarness,
3480    home: PathBuf,
3481}
3482
3483/// ONT-4 `harness.v1.orchestration.decompile` parameters. `source` is the home the
3484/// orchestration was compiled from: it is re-compiled to recover the io bookkeeping
3485/// that byte reuse and the live-store refusal (UNI-18) are decided from.
3486#[derive(Deserialize)]
3487struct OrchestrationDecompileParams {
3488    to: crate::orchestration_doors::OrchestrationHarness,
3489    orchestration: crate::orchestration::Orchestration,
3490    source: PathBuf,
3491    #[serde(default)]
3492    source_flavor: crate::orchestration_doors::SourceFlavor,
3493    dest: PathBuf,
3494    #[serde(default)]
3495    vault: BTreeMap<String, String>,
3496}
3497
3498/// `harness.v1.orchestration.import` parameters: another harness's home, and the
3499/// folder of ours it becomes.
3500#[derive(Deserialize)]
3501struct OrchestrationImportParams {
3502    from: crate::orchestration_doors::OrchestrationHarness,
3503    home: PathBuf,
3504    into: PathBuf,
3505}
3506
3507/// `harness.v1.orchestration.export` parameters: a folder of ours, and the home of
3508/// another harness it becomes.
3509#[derive(Deserialize)]
3510struct OrchestrationExportParams {
3511    to: crate::orchestration_doors::OrchestrationHarness,
3512    root: PathBuf,
3513    dest: PathBuf,
3514}
3515
3516/// `harness.v1.jobs.get` parameters.
3517#[derive(Deserialize)]
3518struct JobsGetParams {
3519    harness: String,
3520    id: String,
3521    #[serde(default)]
3522    homes: crate::HarnessHomes,
3523}
3524
3525/// ORCH-18: run one mutating job verb through the harness's own CLI.
3526///
3527/// The refusal ladder is deliberate: a harness with no scheduled-job concept
3528/// at all answers with the SAME sentence `jobs.list` gives it, and a harness
3529/// that has jobs but publishes no client-callable verb (Claude Code, whose
3530/// jobs are created by the model inside a session) answers with its own
3531/// reason. Neither is ever a silent no-op.
3532fn mutate_job(
3533    verb: crate::jobs_control::JobVerb,
3534    params: Value,
3535) -> std::result::Result<Value, ServiceError> {
3536    let mutation = decode::<crate::jobs_control::JobMutation>(params)?;
3537    refuse_harness_without_jobs(&mutation.harness, &format!("jobs.{}", verb.as_str()))?;
3538    let outcome = crate::jobs_control::mutate(verb, &mutation).map_err(job_control_error)?;
3539    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3540}
3541
3542/// ORCH-22: run one mutating skills verb through the harness's own door.
3543///
3544/// The refusal ladder mirrors `jobs.*`: a harness with no skills root at all
3545/// answers with the same sentence `skills.list` gives it, and a harness whose
3546/// door does not publish this verb (OpenClaw has no `skills remove` at the
3547/// pin) answers with its own reason. Neither is ever a silent no-op.
3548fn mutate_skill(
3549    verb: crate::skills_control::SkillVerb,
3550    params: Value,
3551) -> std::result::Result<Value, ServiceError> {
3552    let mutation = decode::<crate::skills_control::SkillMutation>(params)?;
3553    if !crate::skills_control::supports_skill_control(&mutation.harness) {
3554        return Err(ServiceError::UnsupportedAction(format!(
3555            "`{}` has no skills root supercode reads; `skills.{}` is supported for: {}",
3556            mutation.harness,
3557            verb.as_str(),
3558            crate::skills_control::CONTROLLED_SKILL_HARNESSES.join(", ")
3559        )));
3560    }
3561    let outcome =
3562        crate::skills_control::mutate_skill(verb, &mutation).map_err(skill_control_error)?;
3563    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3564}
3565
3566/// The skills twin of [`job_control_error`], with the same mapping rule.
3567fn skill_control_error(error: crate::skills_control::SkillControlError) -> ServiceError {
3568    match error {
3569        crate::skills_control::SkillControlError::Unsupported(message) => {
3570            ServiceError::UnsupportedAction(message)
3571        }
3572        crate::skills_control::SkillControlError::Invalid(message) => {
3573            ServiceError::InvalidParams(message)
3574        }
3575        crate::skills_control::SkillControlError::Failed(message) => {
3576            ServiceError::Operation(message)
3577        }
3578    }
3579}
3580
3581/// ORCH-21: run one mutating profile verb through the harness's own CLI.
3582///
3583/// The refusal ladder mirrors `mutate_job`'s: a harness with no profile
3584/// concept at all answers with the SAME sentence `profiles.list` gives it, and
3585/// a harness that HAS profiles but publishes no client-callable verb (Codex's
3586/// file-authored `[profiles.<name>]` tables, supercode's compiled-in presets)
3587/// answers with its own reason. Neither is ever a silent no-op.
3588fn mutate_profile(
3589    verb: crate::profiles_control::ProfileVerb,
3590    params: Value,
3591) -> std::result::Result<Value, ServiceError> {
3592    let mutation = decode::<crate::profiles_control::ProfileMutation>(params)?;
3593    let outcome =
3594        crate::profiles_control::mutate(verb, &mutation).map_err(profile_control_error)?;
3595    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3596}
3597
3598/// The same mapping `job_control_error` applies, for the profile noun.
3599fn profile_control_error(error: crate::profiles_control::ProfileControlError) -> ServiceError {
3600    match error {
3601        crate::profiles_control::ProfileControlError::Unsupported(message) => {
3602            ServiceError::UnsupportedAction(message)
3603        }
3604        crate::profiles_control::ProfileControlError::Invalid(message) => {
3605            ServiceError::InvalidParams(message)
3606        }
3607        crate::profiles_control::ProfileControlError::Failed(message) => {
3608            ServiceError::Operation(message)
3609        }
3610    }
3611}
3612
3613/// Map a controlled-tier failure onto the service's error vocabulary. A verb
3614/// the harness lacks is `UnsupportedAction`; a harness verb that RAN and
3615/// failed carries its own stderr through as the operation error.
3616fn job_control_error(error: crate::jobs_control::JobControlError) -> ServiceError {
3617    match error {
3618        crate::jobs_control::JobControlError::Unsupported(message) => {
3619            ServiceError::UnsupportedAction(message)
3620        }
3621        crate::jobs_control::JobControlError::Invalid(message) => {
3622            ServiceError::InvalidParams(message)
3623        }
3624        crate::jobs_control::JobControlError::Failed(message) => ServiceError::Operation(message),
3625    }
3626}
3627
3628/// Map an ORCH-19 controlled-tier failure onto the service's error
3629/// vocabulary. A verb the harness has no door for is `UnsupportedAction`; a
3630/// door that RAN and failed carries the harness's own stderr / HTTP body
3631/// through as the operation error.
3632fn session_control_error(error: crate::SessionControlError) -> ServiceError {
3633    match error {
3634        crate::SessionControlError::Unsupported(message) => {
3635            ServiceError::UnsupportedAction(message)
3636        }
3637        crate::SessionControlError::Invalid(message) => ServiceError::InvalidParams(message),
3638        crate::SessionControlError::Failed(message) => ServiceError::Operation(message),
3639    }
3640}
3641
3642/// A harness without a scheduled-job concept refuses the verb rather than
3643/// answering with an empty list — an absent capability and an empty inventory
3644/// are different answers (the same rule `runtimes.capabilities` applies to
3645/// `steer`).
3646fn refuse_harness_without_jobs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3647    if crate::jobs::supports_jobs(harness) {
3648        return Ok(());
3649    }
3650    Err(ServiceError::UnsupportedAction(format!(
3651        "`{harness}` has no scheduled jobs; `{verb}` is supported for: {}",
3652        crate::jobs::JOB_HARNESSES.join(", ")
3653    )))
3654}
3655
3656/// `harness.v1.runs.get` parameters.
3657#[derive(Deserialize)]
3658struct RunsGetParams {
3659    harness: String,
3660    id: String,
3661    #[serde(default)]
3662    homes: crate::HarnessHomes,
3663}
3664
3665/// A harness with no run store refuses the verb rather than answering with an
3666/// empty history — the same rule `jobs.list` applies. Claude Code lands here
3667/// on purpose: its cron fires are ordinary turns inside the session that
3668/// created the job, so there is no fire record to list.
3669fn refuse_harness_without_runs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3670    if crate::runs::supports_runs(harness) {
3671        return Ok(());
3672    }
3673    Err(ServiceError::UnsupportedAction(format!(
3674        "`{harness}` keeps no run store; `{verb}` is supported for: {}",
3675        crate::runs::RUN_HARNESSES.join(", ")
3676    )))
3677}
3678
3679#[derive(Serialize)]
3680struct SessionArtifact {
3681    source_harness: HarnessId,
3682    target_harness: &'static str,
3683    session_id: Option<String>,
3684    content: String,
3685    suggested_filename: String,
3686    files: Vec<SessionArtifactFile>,
3687    fidelity: Fidelity,
3688    residue: Vec<String>,
3689}
3690
3691#[derive(Serialize)]
3692struct SessionArtifactFile {
3693    path: String,
3694    content: String,
3695    role: ArtifactFileRole,
3696}
3697
3698#[derive(Serialize)]
3699#[serde(rename_all = "snake_case")]
3700enum ArtifactFileRole {
3701    Primary,
3702    Subagent,
3703    Bundle,
3704    SourceRecovery,
3705}
3706
3707#[derive(Serialize)]
3708struct StructuredLaunch {
3709    cwd: PathBuf,
3710    program: String,
3711    arguments: Vec<String>,
3712    env: BTreeMap<String, String>,
3713}
3714
3715struct HandoffInstructions {
3716    launch: StructuredLaunch,
3717    materialize: Option<StructuredLaunch>,
3718    requires_materialization: bool,
3719    note: String,
3720}
3721
3722#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
3723#[serde(rename_all = "snake_case")]
3724enum HarnessProbeLevel {
3725    #[default]
3726    Passive,
3727    Handshake,
3728}
3729
3730#[derive(Default, Deserialize)]
3731#[serde(default)]
3732struct HarnessInventoryParams {
3733    harness: Option<HarnessId>,
3734    harnesses: Vec<HarnessId>,
3735    workspace: Option<PathBuf>,
3736    probe: HarnessProbeLevel,
3737    include_sessions: bool,
3738    /// Omit subprocess-based `--version` calls when a latency-sensitive UI only needs readiness.
3739    skip_versions: bool,
3740}
3741
3742#[derive(Deserialize)]
3743struct HarnessAuthenticationParams {
3744    harness: HarnessId,
3745}
3746
3747#[derive(Deserialize)]
3748struct BeginHarnessAuthenticationParams {
3749    harness: HarnessId,
3750    #[serde(default = "local_browser_authentication_environment")]
3751    environment: crate::HarnessAuthenticationEnvironment,
3752    #[serde(default)]
3753    method: Option<crate::HarnessAuthenticationMethodId>,
3754    #[serde(default)]
3755    cwd: Option<PathBuf>,
3756}
3757
3758fn local_browser_authentication_environment() -> crate::HarnessAuthenticationEnvironment {
3759    crate::HarnessAuthenticationEnvironment::LocalBrowser
3760}
3761
3762#[derive(Serialize)]
3763struct HarnessInventoryReport {
3764    probe: HarnessProbeLevel,
3765    workspace: Option<PathBuf>,
3766    harnesses: Vec<LocalHarness>,
3767}
3768
3769#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3770#[serde(rename_all = "snake_case")]
3771enum HarnessAuthState {
3772    Ready,
3773    Configured,
3774    Required,
3775    Unknown,
3776}
3777
3778#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3779#[serde(rename_all = "snake_case")]
3780enum HarnessRuntimeState {
3781    Ready,
3782    Degraded,
3783    Unavailable,
3784}
3785
3786#[derive(Serialize)]
3787struct HarnessSessionCounts {
3788    global: Option<usize>,
3789    workspace: Option<usize>,
3790}
3791
3792/// Receipt-backed evidence that a harness has a RUNNING instance right now,
3793/// distinct from being merely installed (UNI-7). Detection is passive and
3794/// default-on: a gateway liveness connect for daemon harnesses, a fresh
3795/// SQLite WAL stamp for store-writer harnesses (precedent: the opencode
3796/// follower's -wal/-shm freshness). Control stays behind per-connection
3797/// grants — this reports observations only.
3798/// ORCH-17: the gateway-health noun on an inventory row. Derived from the
3799/// UNI-7 running-instance probe (Hermes: `state.db-wal` freshness; OpenClaw:
3800/// a TCP connect to the gateway endpoint resolved from its OWN config) plus
3801/// the executable version — never by starting anything.
3802#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3803#[serde(rename_all = "snake_case")]
3804pub enum GatewayState {
3805    Up,
3806    Down,
3807    Unknown,
3808}
3809
3810/// ORCH-17: `gateway` on a `harness.v1.harnesses.list` row.
3811#[derive(Debug, Clone, Serialize)]
3812pub struct GatewayHealth {
3813    pub state: GatewayState,
3814    /// The endpoint supercode would connect to (OpenClaw: the gateway
3815    /// WebSocket resolved from `openclaw.json`; core harnesses: their
3816    /// declared connect address when one exists). `None` when the harness
3817    /// has no single endpoint (Hermes multiplexes platforms).
3818    #[serde(skip_serializing_if = "Option::is_none")]
3819    pub endpoint: Option<String>,
3820    #[serde(skip_serializing_if = "Option::is_none")]
3821    pub version: Option<String>,
3822    /// What the verdict rests on, or why it is `unknown`.
3823    pub evidence: String,
3824    pub checked_at_ms: u64,
3825}
3826
3827/// OpenClaw's gateway WebSocket endpoint, resolved from its own config the
3828/// way the registry's connect descriptor prescribes (`gateway.url`, else
3829/// `gateway.port`, else the documented default).
3830fn openclaw_gateway_endpoint(home: &Path) -> String {
3831    let config_path = home.join(".openclaw/openclaw.json");
3832    let gateway = std::fs::read_to_string(&config_path)
3833        .ok()
3834        .and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok())
3835        .and_then(|config| config.get("gateway").cloned());
3836    if let Some(url) = gateway
3837        .as_ref()
3838        .and_then(|gateway| gateway.get("url"))
3839        .and_then(serde_json::Value::as_str)
3840    {
3841        return url.to_string();
3842    }
3843    let port = gateway
3844        .as_ref()
3845        .and_then(|gateway| gateway.get("port"))
3846        .and_then(serde_json::Value::as_u64)
3847        .unwrap_or(18789);
3848    format!("ws://127.0.0.1:{port}")
3849}
3850
3851/// Ask Hermes itself (`hermes gateway status`, read-only, ~1 s) whether its
3852/// gateway is up. The command is per-host launchd/systemd text without a JSON
3853/// form at 0.19–0.21; the verdict is read from the lines it prints:
3854/// "supervised by launchd (PID …)" / "is running" → up, "not running" /
3855/// "not installed" → down, anything else → no verdict. `SUPERCODE_HERMES_BIN`
3856/// overrides the executable so a fake can stand in under test.
3857fn hermes_gateway_status() -> Option<(GatewayState, String)> {
3858    let program = crate::harness_command::harness_program(HarnessId::HERMES).ok()?;
3859    let output = std::process::Command::new(&program)
3860        .args(["gateway", "status"])
3861        .stdin(std::process::Stdio::null())
3862        .output()
3863        .ok()?;
3864    let text = format!(
3865        "{}{}",
3866        String::from_utf8_lossy(&output.stdout),
3867        String::from_utf8_lossy(&output.stderr)
3868    );
3869    let verdict = text.lines().find_map(|line| {
3870        let l = line.trim();
3871        if l.contains("supervised by launchd (PID")
3872            || l.contains("supervised by systemd (PID")
3873            || l.contains("Gateway is running")
3874            || l.contains("process is running")
3875        {
3876            Some((GatewayState::Up, format!("`hermes gateway status`: {l}")))
3877        } else if l.contains("not running") || l.contains("not installed") {
3878            Some((GatewayState::Down, format!("`hermes gateway status`: {l}")))
3879        } else {
3880            None
3881        }
3882    });
3883    verdict
3884}
3885
3886fn gateway_health(
3887    id: &str,
3888    installed: bool,
3889    running: Option<&RunningInstance>,
3890    version: Option<&str>,
3891) -> GatewayHealth {
3892    let checked_at_ms = now_epoch_ms();
3893    let home = std::env::var_os("HOME").map(PathBuf::from);
3894    match id {
3895        HarnessId::HERMES | HarnessId::OPENCLAW => {
3896            let endpoint = (id == HarnessId::OPENCLAW)
3897                .then(|| home.as_deref().map(openclaw_gateway_endpoint))
3898                .flatten();
3899            let (state, evidence) = match running {
3900                Some(instance) => (GatewayState::Up, instance.evidence.clone()),
3901                None if !installed => (
3902                    GatewayState::Unknown,
3903                    format!("`{id}` is not installed; no gateway to probe"),
3904                ),
3905                None if id == HarnessId::HERMES => match hermes_gateway_status() {
3906                    // The harness's own door outranks the WAL heuristic: an idle
3907                    // gateway writes nothing for minutes yet is up.
3908                    Some((state, evidence)) => (state, evidence),
3909                    None => (
3910                        GatewayState::Down,
3911                        "no fresh state.db-wal activity under ~/.hermes and `hermes gateway status` gave no verdict".to_string(),
3912                    ),
3913                },
3914                None => (
3915                    GatewayState::Down,
3916                    format!(
3917                        "no TCP listener at {}",
3918                        endpoint.as_deref().unwrap_or("the gateway endpoint")
3919                    ),
3920                ),
3921            };
3922            GatewayHealth {
3923                state,
3924                endpoint,
3925                version: version.map(str::to_string),
3926                evidence,
3927                checked_at_ms,
3928            }
3929        }
3930        // ORC-7: the orchestrator's gateway IS its daemon, and the daemon's
3931        // own lease file is the record of it. A lease naming a live pid is
3932        // up; a lease whose process is gone is down and says so as a STALE
3933        // lease, never as "no lease"; no lease at all is down. Nothing is
3934        // started, and no port is guessed — the daemon multiplexes adapters
3935        // the way Hermes does, so it has no single endpoint either.
3936        HarnessId::ORCHESTRATOR => {
3937            let root = crate::HarnessHomes::default().orchestrator;
3938            let (state, evidence) = match crate::orchestrator::read_lease(&root) {
3939                Some(lease) if crate::orchestrator::pid_is_live(lease.pid) => (
3940                    GatewayState::Up,
3941                    format!(
3942                        "`{}` names pid {} (started {}), which is live",
3943                        crate::orchestrator::lock_path(&root).display(),
3944                        lease.pid,
3945                        lease.started_at
3946                    ),
3947                ),
3948                Some(lease) => (
3949                    GatewayState::Down,
3950                    format!(
3951                        "stale lease `{}`: pid {} is gone",
3952                        crate::orchestrator::lock_path(&root).display(),
3953                        lease.pid
3954                    ),
3955                ),
3956                None => (
3957                    GatewayState::Down,
3958                    format!(
3959                        "no lease at `{}`; `supercode orchestrator start` writes one",
3960                        crate::orchestrator::lock_path(&root).display()
3961                    ),
3962                ),
3963            };
3964            GatewayHealth {
3965                state,
3966                endpoint: None,
3967                version: version.map(str::to_string),
3968                evidence,
3969                checked_at_ms,
3970            }
3971        }
3972        _ => GatewayHealth {
3973            state: GatewayState::Unknown,
3974            endpoint: None,
3975            version: version.map(str::to_string),
3976            evidence: format!("`{id}` runs per session, not as a gateway"),
3977            checked_at_ms,
3978        },
3979    }
3980}
3981
3982#[derive(Debug, Clone, Serialize)]
3983struct RunningInstance {
3984    /// How the instance was detected.
3985    method: RunningInstanceMethod,
3986    /// The evidence the verdict rests on (endpoint reached / WAL path+age).
3987    evidence: String,
3988    /// Epoch-ms instant the probe executed.
3989    checked_at_ms: u64,
3990}
3991
3992#[derive(Debug, Clone, Copy, Serialize)]
3993#[serde(rename_all = "snake_case")]
3994enum RunningInstanceMethod {
3995    /// A TCP connect to the harness's own configured gateway endpoint
3996    /// succeeded.
3997    GatewayConnect,
3998    /// The harness's session store has an active SQLite WAL (a live writer
3999    /// holds the store open and stamped it recently).
4000    StoreWalActivity,
4001}
4002
4003fn now_epoch_ms() -> u64 {
4004    std::time::SystemTime::now()
4005        .duration_since(std::time::UNIX_EPOCH)
4006        .map(|elapsed| elapsed.as_millis() as u64)
4007        .unwrap_or(0)
4008}
4009
4010/// OpenClaw: the gateway endpoint comes from the harness's OWN config
4011/// (`<home>/.openclaw/openclaw.json` — `gateway.url` or `gateway.port`,
4012/// default port 18789); a successful TCP connect is the running signal.
4013fn probe_openclaw_running(home: &Path) -> Option<RunningInstance> {
4014    let config_path = home.join(".openclaw/openclaw.json");
4015    let text = std::fs::read_to_string(&config_path).ok();
4016    let gateway = text
4017        .as_deref()
4018        .and_then(|raw| serde_json::from_str::<serde_json::Value>(raw).ok())
4019        .and_then(|config| config.get("gateway").cloned());
4020    let address = gateway
4021        .as_ref()
4022        .and_then(|gateway| gateway.get("url"))
4023        .and_then(serde_json::Value::as_str)
4024        .and_then(|url| {
4025            url.split("://").nth(1).map(|rest| {
4026                rest.trim_end_matches('/')
4027                    .split('/')
4028                    .next()
4029                    .unwrap_or(rest)
4030                    .to_string()
4031            })
4032        })
4033        .unwrap_or_else(|| {
4034            let port = gateway
4035                .as_ref()
4036                .and_then(|gateway| gateway.get("port"))
4037                .and_then(serde_json::Value::as_u64)
4038                .unwrap_or(18789);
4039            format!("127.0.0.1:{port}")
4040        });
4041    let reachable = std::net::TcpStream::connect_timeout(
4042        &address.parse().ok()?,
4043        std::time::Duration::from_millis(400),
4044    )
4045    .is_ok();
4046    reachable.then(|| RunningInstance {
4047        method: RunningInstanceMethod::GatewayConnect,
4048        evidence: format!(
4049            "gateway endpoint {address} accepted a TCP connect (from {})",
4050            config_path.display()
4051        ),
4052        checked_at_ms: now_epoch_ms(),
4053    })
4054}
4055
4056/// Hermes: `<home>/.hermes/state.db-wal` freshly modified means a live writer
4057/// holds the store open (SQLite WAL exists only while a connection is open;
4058/// a recent stamp distinguishes an active instance from a stale crash
4059/// leftover).
4060fn probe_hermes_running(home: &Path, max_wal_age_ms: u64) -> Option<RunningInstance> {
4061    let wal = home.join(".hermes/state.db-wal");
4062    let modified = std::fs::metadata(&wal).ok()?.modified().ok()?;
4063    let age_ms = std::time::SystemTime::now()
4064        .duration_since(modified)
4065        .map(|age| age.as_millis() as u64)
4066        .unwrap_or(u64::MAX);
4067    (age_ms <= max_wal_age_ms).then(|| RunningInstance {
4068        method: RunningInstanceMethod::StoreWalActivity,
4069        evidence: format!(
4070            "{} stamped {age_ms}ms ago (threshold {max_wal_age_ms}ms)",
4071            wal.display()
4072        ),
4073        checked_at_ms: now_epoch_ms(),
4074    })
4075}
4076
4077/// Default-on running-instance detection for the harnesses that have one.
4078fn probe_running_instance(id: &str) -> Option<RunningInstance> {
4079    let home = std::env::var_os("HOME").map(PathBuf::from)?;
4080    match id {
4081        HarnessId::OPENCLAW => probe_openclaw_running(&home),
4082        HarnessId::HERMES => probe_hermes_running(&home, 300_000),
4083        _ => None,
4084    }
4085}
4086
4087#[derive(Serialize)]
4088struct LocalHarness {
4089    id: HarnessId,
4090    display_name: String,
4091    supported: bool,
4092    installed: bool,
4093    executable: Option<String>,
4094    version: Option<String>,
4095    auth: HarnessAuthState,
4096    runtime: HarnessRuntimeState,
4097    protocol: String,
4098    capabilities: crate::RuntimeCapabilities,
4099    effective_capabilities: crate::RuntimeCapabilities,
4100    sessions: HarnessSessionCounts,
4101    /// Receipt-backed running-instance detection (None = not detected or the
4102    /// harness has no running-instance concept). Distinct from `installed`.
4103    #[serde(skip_serializing_if = "Option::is_none")]
4104    running: Option<RunningInstance>,
4105    /// ORCH-17: gateway health derived from `running` + the harness's own config.
4106    gateway: GatewayHealth,
4107    reason: Option<String>,
4108    repair: Option<String>,
4109}
4110
4111#[derive(Clone, Deserialize)]
4112struct RuntimeBackendParams {
4113    harness: HarnessId,
4114    #[serde(default)]
4115    protocol: Option<String>,
4116    #[serde(default)]
4117    launch: Option<RuntimeLaunch>,
4118    #[serde(default)]
4119    base_url: Option<String>,
4120    #[serde(default)]
4121    policy: RuntimePolicy,
4122}
4123
4124#[derive(Debug, Clone, Copy, Default, Deserialize)]
4125#[serde(rename_all = "snake_case")]
4126enum RuntimePolicy {
4127    #[default]
4128    Default,
4129    Yolo,
4130}
4131
4132#[derive(Deserialize)]
4133struct RuntimeStartParams {
4134    #[serde(flatten)]
4135    backend: RuntimeBackendParams,
4136    cwd: PathBuf,
4137    /// MCP servers to mount into the new session through the harness's own
4138    /// start door (ORC-6). Backends without such a door ignore them.
4139    #[serde(default)]
4140    mcp_servers: Vec<crate::McpServerLaunch>,
4141}
4142
4143#[derive(Deserialize)]
4144struct RuntimeAttachParams {
4145    #[serde(flatten)]
4146    backend: RuntimeBackendParams,
4147    runtime_id: String,
4148    #[serde(default)]
4149    cwd: Option<PathBuf>,
4150}
4151
4152#[derive(Deserialize)]
4153struct RuntimeConnectionParams {
4154    connection: String,
4155}
4156
4157#[derive(Deserialize)]
4158struct RuntimeInputParams {
4159    connection: String,
4160    text: String,
4161    #[serde(default)]
4162    image_urls: Vec<String>,
4163}
4164
4165const MAX_RUNTIME_IMAGES: usize = 4;
4166const MAX_RUNTIME_IMAGE_URL_BYTES: usize = 12 * 1024 * 1024;
4167const MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL: usize = 32 * 1024 * 1024;
4168
4169fn validate_runtime_image_urls(image_urls: Vec<String>) -> Result<Vec<String>, ServiceError> {
4170    if image_urls.len() > MAX_RUNTIME_IMAGES {
4171        return Err(ServiceError::InvalidParams(format!(
4172            "a runtime prompt accepts at most {MAX_RUNTIME_IMAGES} images"
4173        )));
4174    }
4175    let mut total = 0usize;
4176    for url in &image_urls {
4177        if !(url.starts_with("data:image/")
4178            || url.starts_with("https://")
4179            || url.starts_with("http://"))
4180        {
4181            return Err(ServiceError::InvalidParams(
4182                "runtime images must be image data URLs or HTTP(S) URLs".into(),
4183            ));
4184        }
4185        if url.len() > MAX_RUNTIME_IMAGE_URL_BYTES {
4186            return Err(ServiceError::InvalidParams(format!(
4187                "one runtime image exceeds the {MAX_RUNTIME_IMAGE_URL_BYTES}-byte encoded limit"
4188            )));
4189        }
4190        total = total.saturating_add(url.len());
4191    }
4192    if total > MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL {
4193        return Err(ServiceError::InvalidParams(format!(
4194            "runtime images exceed the {MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL}-byte encoded total limit"
4195        )));
4196    }
4197    Ok(image_urls)
4198}
4199
4200#[derive(Deserialize)]
4201struct RuntimeRespondParams {
4202    connection: String,
4203    request_id: Value,
4204    response: Value,
4205}
4206
4207fn default_reduction_store_root() -> PathBuf {
4208    if let Some(root) = std::env::var_os("SUPERCODE_HOME") {
4209        return PathBuf::from(root).join("sessions");
4210    }
4211    if let Some(home) = std::env::var_os("HOME") {
4212        return PathBuf::from(home).join(".supercode").join("sessions");
4213    }
4214    PathBuf::from(".supercode").join("sessions")
4215}
4216
4217fn messages_jsonl(messages: &[crate::ChatMessage]) -> std::result::Result<String, ServiceError> {
4218    let mut output = String::new();
4219    for message in messages {
4220        output.push_str(
4221            &serde_json::to_string(message)
4222                .map_err(|error| ServiceError::Operation(error.to_string()))?,
4223        );
4224        output.push('\n');
4225    }
4226    Ok(output)
4227}
4228
4229fn parse_messages_jsonl(
4230    content: &str,
4231) -> std::result::Result<Vec<crate::ChatMessage>, ServiceError> {
4232    content
4233        .lines()
4234        .enumerate()
4235        .filter(|(_, line)| !line.trim().is_empty())
4236        .map(|(index, line)| {
4237            serde_json::from_str::<crate::ChatMessage>(line).map_err(|error| {
4238                ServiceError::Operation(format!(
4239                    "reduced transcript line {} is invalid: {error}",
4240                    index + 1
4241                ))
4242            })
4243        })
4244        .collect()
4245}
4246
4247fn reduced_bootstrap_prompt(
4248    source: &SessionLocator,
4249    target: TransferFormat,
4250    view_jsonl: &str,
4251    sidecar_path: &Path,
4252    reduction_log_path: &Path,
4253) -> String {
4254    format!(
4255        "Continue the work from this losslessly reduced {source_harness} session in {target_harness}.\n\
4256         \n\
4257         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\
4258         \n\
4259         <supercode-reduced-session source-session=\"{source_id}\">\n\
4260         {view_jsonl}\
4261         </supercode-reduced-session>\n\
4262         \n\
4263         Resume from the latest unresolved user request and preserve the source session's decisions and constraints.",
4264        source_harness = source.harness.as_str(),
4265        target_harness = target.id(),
4266        sidecar = sidecar_path.display(),
4267        log = reduction_log_path.display(),
4268        source_id = source.session_id,
4269    )
4270}
4271
4272fn session_artifact(
4273    locator: &SessionLocator,
4274    session: &Session,
4275    target: TransferFormat,
4276) -> std::result::Result<SessionArtifact, ServiceError> {
4277    session_artifact_with_id(locator, session, target, None)
4278}
4279
4280fn session_artifact_with_id(
4281    locator: &SessionLocator,
4282    session: &Session,
4283    target: TransferFormat,
4284    target_session_id: Option<&str>,
4285) -> std::result::Result<SessionArtifact, ServiceError> {
4286    let format: SessionFormat = target.into();
4287    let diagonal = format.source() == session.meta.source;
4288    let has_appended_turns = session
4289        .imported_message_count
4290        .is_some_and(|imported| imported < session.messages.len());
4291    let content = if let Some(id) = target_session_id {
4292        if diagonal && format != SessionFormat::OpenCode {
4293            session
4294                .to_jsonl_spliced(format, Some(id))
4295                .map_err(operation)?
4296        } else {
4297            let mut rewritten = session.clone();
4298            rewritten.meta.session_id = Some(id.to_string());
4299            rewritten.to_jsonl(format).map_err(operation)?
4300        }
4301    } else if diagonal && session.raw_is_verbatim && !has_appended_turns {
4302        session.raw_verbatim()
4303    } else if diagonal {
4304        session.to_jsonl_spliced(format, None).map_err(operation)?
4305    } else {
4306        session.to_jsonl(format).map_err(operation)?
4307    };
4308    let stem = sanitize_filename(
4309        target_session_id
4310            .or(session.meta.session_id.as_deref())
4311            .unwrap_or(&locator.session_id),
4312    );
4313    let suggested_filename = if diagonal && target == TransferFormat::Grok {
4314        "chat_history.jsonl".to_string()
4315    } else if target == TransferFormat::Goose {
4316        format!("{stem}.goose.json")
4317    } else {
4318        format!("{stem}.{}.jsonl", target.id())
4319    };
4320    let mut files = vec![SessionArtifactFile {
4321        path: suggested_filename.clone(),
4322        content: content.clone(),
4323        role: ArtifactFileRole::Primary,
4324    }];
4325    if target == TransferFormat::ClaudeCode {
4326        let bundle_stem = Path::new(&suggested_filename)
4327            .file_stem()
4328            .and_then(|stem| stem.to_str())
4329            .unwrap_or(&stem);
4330        let mut child_paths = BTreeSet::new();
4331        for (index, subagent) in session.subagents.iter().enumerate() {
4332            let agent_id = subagent
4333                .meta
4334                .agent_id
4335                .as_deref()
4336                .map(|id| id.strip_prefix("agent-").unwrap_or(id))
4337                .map(sanitize_filename)
4338                .filter(|id| !id.is_empty())
4339                .unwrap_or_else(|| format!("subagent-{}", index + 1));
4340            let child_has_appended_turns = subagent
4341                .imported_message_count
4342                .is_some_and(|imported| imported < subagent.messages.len());
4343            let child_content = if target_session_id.is_none()
4344                && subagent.meta.source == SessionSource::ClaudeCode
4345                && subagent.raw_is_verbatim
4346                && !child_has_appended_turns
4347            {
4348                subagent.raw_verbatim()
4349            } else if subagent.meta.source == SessionSource::ClaudeCode {
4350                subagent
4351                    .to_jsonl_spliced(SessionFormat::ClaudeCode, target_session_id)
4352                    .map_err(operation)?
4353            } else {
4354                let mut child = subagent.clone();
4355                if let Some(id) = target_session_id {
4356                    child.meta.session_id = Some(id.to_string());
4357                }
4358                child
4359                    .to_jsonl(SessionFormat::ClaudeCode)
4360                    .map_err(operation)?
4361            };
4362            let path = format!("{bundle_stem}/subagents/agent-{agent_id}.jsonl");
4363            if !child_paths.insert(path.clone()) {
4364                return Err(ServiceError::Operation(format!(
4365                    "Claude subagent ids collide at artifact path `{path}`"
4366                )));
4367            }
4368            files.push(SessionArtifactFile {
4369                path,
4370                content: child_content,
4371                role: ArtifactFileRole::Subagent,
4372            });
4373        }
4374    }
4375    if diagonal && target == TransferFormat::Grok {
4376        append_grok_bundle_files(locator, "", ArtifactFileRole::Bundle, &mut files)?;
4377    }
4378    if !diagonal || !session.raw_is_verbatim {
4379        files.push(SessionArtifactFile {
4380            path: "recovery/source.supercode.jsonl".into(),
4381            content: session.to_native_jsonl(),
4382            role: ArtifactFileRole::SourceRecovery,
4383        });
4384        for (index, subagent) in session.subagents.iter().enumerate() {
4385            let id = subagent
4386                .meta
4387                .agent_id
4388                .as_deref()
4389                .map(sanitize_filename)
4390                .unwrap_or_else(|| format!("subagent-{}", index + 1));
4391            files.push(SessionArtifactFile {
4392                path: format!("recovery/subagents/{id}.supercode.jsonl"),
4393                content: subagent.to_native_jsonl(),
4394                role: ArtifactFileRole::SourceRecovery,
4395            });
4396        }
4397    }
4398    if !diagonal && session.meta.source == SessionSource::Grok {
4399        append_grok_bundle_files(
4400            locator,
4401            "recovery/grok/",
4402            ArtifactFileRole::SourceRecovery,
4403            &mut files,
4404        )?;
4405    }
4406    let (fidelity, residue) = if diagonal
4407        && target_session_id.is_none()
4408        && session.raw_is_verbatim
4409        && !has_appended_turns
4410    {
4411        (Fidelity::ByteLossless, Vec::new())
4412    } else if diagonal && !(target_session_id.is_some() && target == TransferFormat::OpenCode) {
4413        (
4414            Fidelity::ValueLossless,
4415            vec![if target_session_id.is_some() {
4416                "target identity was rewritten, so the artifact intentionally differs from source bytes".into()
4417            } else {
4418                "source storage was reconstructed as a native-value-equivalent export; original container bytes were not captured".into()
4419            }],
4420        )
4421    } else {
4422        (
4423            Fidelity::Semantic,
4424            vec!["target schema has no portable slot for every source-native record and metadata field".into()],
4425        )
4426    };
4427    Ok(SessionArtifact {
4428        source_harness: locator.harness.clone(),
4429        target_harness: target.id(),
4430        session_id: target_session_id
4431            .map(str::to_string)
4432            .or_else(|| session.meta.session_id.clone()),
4433        content,
4434        suggested_filename,
4435        files,
4436        fidelity,
4437        residue,
4438    })
4439}
4440
4441fn append_grok_bundle_files(
4442    locator: &SessionLocator,
4443    prefix: &str,
4444    role: ArtifactFileRole,
4445    files: &mut Vec<SessionArtifactFile>,
4446) -> std::result::Result<(), ServiceError> {
4447    let primary = locator.storage.path();
4448    if primary.file_name().and_then(|name| name.to_str()) != Some("chat_history.jsonl") {
4449        return Err(ServiceError::Operation(format!(
4450            "Grok bundle locator must name chat_history.jsonl, got {}",
4451            primary.display()
4452        )));
4453    }
4454    let parent = primary.parent().ok_or_else(|| {
4455        ServiceError::Operation("Grok chat_history.jsonl has no session directory".into())
4456    })?;
4457    for name in ["summary.json", "updates.jsonl"] {
4458        let path = parent.join(name);
4459        let metadata = match std::fs::symlink_metadata(&path) {
4460            Ok(metadata) => metadata,
4461            Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
4462            Err(error) => return Err(ServiceError::Operation(error.to_string())),
4463        };
4464        if metadata.file_type().is_symlink() || !metadata.is_file() {
4465            return Err(ServiceError::Operation(format!(
4466                "refusing non-regular Grok bundle member {}",
4467                path.display()
4468            )));
4469        }
4470        let content = std::fs::read_to_string(&path).map_err(|error| {
4471            ServiceError::Operation(format!(
4472                "Grok bundle member {} is not representable as UTF-8: {error}",
4473                path.display()
4474            ))
4475        })?;
4476        files.push(SessionArtifactFile {
4477            path: format!("{prefix}{name}"),
4478            content,
4479            role: match role {
4480                ArtifactFileRole::Bundle => ArtifactFileRole::Bundle,
4481                _ => ArtifactFileRole::SourceRecovery,
4482            },
4483        });
4484    }
4485    Ok(())
4486}
4487
4488fn handoff_artifact(
4489    locator: &SessionLocator,
4490    session: &Session,
4491    target: TransferFormat,
4492    cwd: &Path,
4493) -> std::result::Result<SessionArtifact, ServiceError> {
4494    if target != TransferFormat::Grok {
4495        let target_session_id = target_session_id(target);
4496        return session_artifact_with_id(locator, session, target, Some(&target_session_id));
4497    }
4498
4499    // Stock Grok's importer accepts Claude/Codex transcripts and materializes its own
4500    // multi-file session bundle. A synthesized Grok chat_history.jsonl alone is not a
4501    // resumable handoff because updates.jsonl is the authoritative restore log.
4502    let mut importable = session.clone();
4503    // The Claude importer validates sessionId as a UUID. Source harness identities
4504    // are not portable (OpenCode, for example, uses `ses_...`), and a handoff must
4505    // not overwrite an existing target session when the source already uses UUIDs.
4506    // Mint a distinct target identity and still bind the importer-returned ID at
4507    // launch time because the importer remains the authority on materialization.
4508    importable.meta.session_id = Some(target_session_id(TransferFormat::ClaudeCode));
4509    importable.meta.cwd = Some(if cwd.is_absolute() {
4510        cwd.to_path_buf()
4511    } else {
4512        std::env::current_dir()
4513            .map_err(|error| ServiceError::Operation(error.to_string()))?
4514            .join(cwd)
4515    });
4516    let content = importable
4517        .to_jsonl(SessionFormat::ClaudeCode)
4518        .map_err(operation)?;
4519    let stem = sanitize_filename(
4520        importable
4521            .meta
4522            .session_id
4523            .as_deref()
4524            .unwrap_or(&locator.session_id),
4525    );
4526    let suggested_filename = format!("{stem}.grok-import.claude-code.jsonl");
4527    Ok(SessionArtifact {
4528        source_harness: locator.harness.clone(),
4529        // This names the artifact's actual wire format. The requested handoff target
4530        // remains Grok; its official importer is the materialization boundary.
4531        target_harness: TransferFormat::ClaudeCode.id(),
4532        session_id: importable.meta.session_id.clone(),
4533        content: content.clone(),
4534        suggested_filename: suggested_filename.clone(),
4535        files: vec![SessionArtifactFile {
4536            path: suggested_filename,
4537            content,
4538            role: ArtifactFileRole::Primary,
4539        }],
4540        fidelity: Fidelity::Semantic,
4541        residue: vec!["Grok's stock importer accepts a Claude Code transcript, not a complete Grok updates/session bundle".into()],
4542    })
4543}
4544
4545fn target_session_id(target: TransferFormat) -> String {
4546    let uuid = generated_session_id();
4547    match target {
4548        TransferFormat::OpenCode => format!("ses_{}", uuid.replace('-', "")),
4549        TransferFormat::ClaudeCode
4550        | TransferFormat::Codex
4551        | TransferFormat::Pi
4552        | TransferFormat::Grok
4553        | TransferFormat::Gemini
4554        | TransferFormat::Goose
4555        | TransferFormat::Hermes => uuid,
4556    }
4557}
4558
4559fn sanitize_filename(value: &str) -> String {
4560    let value = value
4561        .chars()
4562        .map(|character| {
4563            if character.is_ascii_alphanumeric() || matches!(character, '-' | '_') {
4564                character
4565            } else {
4566                '-'
4567            }
4568        })
4569        .collect::<String>();
4570    let value = value.trim_matches('-');
4571    if value.is_empty() {
4572        "session".into()
4573    } else {
4574        value.chars().take(100).collect()
4575    }
4576}
4577
4578fn handoff_instructions(
4579    target: TransferFormat,
4580    session_id: &str,
4581    cwd: &Path,
4582) -> HandoffInstructions {
4583    let launch = |program: &str, arguments: Vec<String>| StructuredLaunch {
4584        cwd: cwd.to_path_buf(),
4585        program: program.into(),
4586        arguments,
4587        env: BTreeMap::new(),
4588    };
4589    match target {
4590        TransferFormat::ClaudeCode => HandoffInstructions {
4591            launch: launch("claude", vec!["--resume".into(), session_id.into()]),
4592            materialize: None,
4593            requires_materialization: true,
4594            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(),
4595        },
4596        TransferFormat::Hermes => HandoffInstructions {
4597            launch: launch("hermes", vec!["--resume".into(), session_id.into()]),
4598            materialize: None,
4599            requires_materialization: true,
4600            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(),
4601        },
4602        TransferFormat::Codex => HandoffInstructions {
4603            launch: launch("codex", vec!["resume".into(), session_id.into()]),
4604            materialize: None,
4605            requires_materialization: true,
4606            note: "Write the artifact into Codex's native rollout store before running the resume launch; Codex has no general transcript-import command.".into(),
4607        },
4608        TransferFormat::OpenCode => HandoffInstructions {
4609            launch: launch("opencode", vec!["--session".into(), session_id.into()]),
4610            materialize: Some(launch(
4611                "opencode",
4612                vec!["import".into(), "{artifact_path}".into()],
4613            )),
4614            requires_materialization: true,
4615            note: "Write the artifact to a file, run the materialize command with its path, then launch the imported session.".into(),
4616        },
4617        TransferFormat::Pi => HandoffInstructions {
4618            launch: launch("pi", vec!["--session".into(), "{artifact_path}".into()]),
4619            materialize: None,
4620            requires_materialization: true,
4621            note: "Write the artifact to a file and replace {artifact_path} in the launch arguments; Pi can resume that file directly.".into(),
4622        },
4623        TransferFormat::Grok => HandoffInstructions {
4624            launch: launch(
4625                "grok",
4626                vec![
4627                    "--resume".into(),
4628                    "{imported_session_id}".into(),
4629                    "--fork-session".into(),
4630                ],
4631            ),
4632            materialize: Some(launch(
4633                "grok",
4634                vec!["import".into(), "--json".into(), "{artifact_path}".into()],
4635            )),
4636            requires_materialization: true,
4637            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(),
4638        },
4639        TransferFormat::Gemini => HandoffInstructions {
4640            launch: launch(
4641                "gemini",
4642                vec!["--session-file".into(), "{artifact_path}".into()],
4643            ),
4644            materialize: None,
4645            requires_materialization: true,
4646            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(),
4647        },
4648        TransferFormat::Goose => HandoffInstructions {
4649            launch: launch(
4650                "goose",
4651                vec![
4652                    "session".into(),
4653                    "--resume".into(),
4654                    "--session-id".into(),
4655                    "{imported_session_id}".into(),
4656                ],
4657            ),
4658            materialize: Some(launch(
4659                "goose",
4660                vec!["session".into(), "import".into(), "{artifact_path}".into()],
4661            )),
4662            requires_materialization: true,
4663            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(),
4664        },
4665    }
4666}
4667
4668fn resume_launch(
4669    harness: &str,
4670    session_id: &str,
4671    cwd: &Path,
4672    policy: ResumePolicy,
4673) -> std::result::Result<StructuredLaunch, ServiceError> {
4674    let mut arguments = Vec::new();
4675    let program = match harness {
4676        HarnessId::GROK => {
4677            if matches!(policy, ResumePolicy::Yolo) {
4678                if crate::support::self_sandbox_supported() {
4679                    arguments.extend(["--sandbox".into(), "workspace".into()]);
4680                }
4681                arguments.push("--always-approve".into());
4682            }
4683            arguments.extend(["--resume".into(), session_id.into()]);
4684            "grok"
4685        }
4686        HarnessId::CODEX => {
4687            let cwd_key = serde_json::to_string(cwd.to_string_lossy().as_ref())
4688                .expect("a filesystem path always serializes as JSON text");
4689            arguments.extend([
4690                "-c".into(),
4691                "check_for_update_on_startup=false".into(),
4692                "-c".into(),
4693                format!("projects.{cwd_key}.trust_level=\"trusted\""),
4694            ]);
4695            if matches!(policy, ResumePolicy::Yolo) {
4696                arguments.extend([
4697                    "--dangerously-bypass-approvals-and-sandbox".into(),
4698                    "--dangerously-bypass-hook-trust".into(),
4699                ]);
4700            }
4701            arguments.extend(["resume".into(), session_id.into()]);
4702            "codex"
4703        }
4704        HarnessId::CLAUDE_CODE => {
4705            if matches!(policy, ResumePolicy::Yolo) {
4706                arguments.push("--dangerously-skip-permissions".into());
4707            }
4708            arguments.extend(["--resume".into(), session_id.into()]);
4709            "claude"
4710        }
4711        HarnessId::GEMINI => {
4712            if matches!(policy, ResumePolicy::Yolo) {
4713                arguments.push("--yolo".into());
4714            }
4715            arguments.extend(["--resume".into(), session_id.into()]);
4716            "gemini"
4717        }
4718        HarnessId::GOOSE => {
4719            arguments.extend([
4720                "session".into(),
4721                "--resume".into(),
4722                "--session-id".into(),
4723                session_id.into(),
4724            ]);
4725            "goose"
4726        }
4727        HarnessId::PI => {
4728            if matches!(policy, ResumePolicy::Yolo) {
4729                arguments.push("--approve".into());
4730            }
4731            arguments.extend(["--session".into(), session_id.into()]);
4732            "pi"
4733        }
4734        HarnessId::OPENCODE => {
4735            arguments.extend(["--session".into(), session_id.into()]);
4736            "opencode"
4737        }
4738        HarnessId::SUPERCODE => {
4739            if matches!(policy, ResumePolicy::Yolo) {
4740                arguments.push("--dangerous".into());
4741            }
4742            arguments.extend(["resume".into(), session_id.into()]);
4743            "supercode"
4744        }
4745        other => {
4746            return Err(ServiceError::InvalidParams(format!(
4747                "no structured resume launch is registered for harness `{other}`"
4748            )))
4749        }
4750    };
4751    Ok(StructuredLaunch {
4752        cwd: cwd.to_path_buf(),
4753        program: program.into(),
4754        arguments,
4755        env: BTreeMap::new(),
4756    })
4757}
4758
4759/// Stage the resolved gateway credential in a private (0600) file so the
4760/// bridge can read it via `--token-file` — the delivery the real `openclaw
4761/// acp` accepts. One stable file per endpoint (keyed by an address digest,
4762/// no secret material in the name), overwritten on every connect so files
4763/// never accumulate and a rotated token never goes stale on disk.
4764fn openclaw_gateway_token_file(address: &str, secret: &str) -> std::io::Result<PathBuf> {
4765    let digest = blake3::hash(address.as_bytes()).to_hex();
4766    let path = std::env::temp_dir().join(format!(
4767        "supercode-openclaw-gateway-token-{}",
4768        &digest.as_str()[..16]
4769    ));
4770    #[cfg(unix)]
4771    {
4772        use std::io::Write;
4773        use std::os::unix::fs::OpenOptionsExt;
4774        let mut file = std::fs::OpenOptions::new()
4775            .write(true)
4776            .create(true)
4777            .truncate(true)
4778            .mode(0o600)
4779            .open(&path)?;
4780        file.write_all(secret.as_bytes())?;
4781    }
4782    #[cfg(not(unix))]
4783    std::fs::write(&path, secret)?;
4784    Ok(path)
4785}
4786
4787/// Open a connect-mode descriptor: resolve the endpoint address and
4788/// credential from the harness's own config file and build the backend that
4789/// joins the already-running endpoint. Fails closed with a specific
4790/// diagnostic when the config cannot be resolved or the declared protocol has
4791/// no connect-capable client yet.
4792fn open_connect_descriptor(
4793    descriptor: &crate::HarnessSupportDescriptor,
4794    home: &Path,
4795) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
4796    let Some(connect) = &descriptor.runtime.connect_launch else {
4797        return Err(ServiceError::InvalidParams(format!(
4798            "harness `{}` has no registered connect-mode launch",
4799            descriptor.id.as_str()
4800        )));
4801    };
4802    let resolved = connect
4803        .resolve(home)
4804        .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
4805    match (descriptor.id.as_str(), connect.protocol.as_str()) {
4806        (HarnessId::OPENCODE, protocol) if protocol.starts_with("opencode-http") => {
4807            let mut backend = OpenCodeRuntimeBackend::connect(&resolved.address);
4808            if let Some(token) = resolved.auth {
4809                backend = backend.with_bearer(token);
4810            }
4811            Ok(Box::new(backend))
4812        }
4813        (HarnessId::OPENCLAW, protocol) if protocol.starts_with("acp") => {
4814            // OpenClaw's own `openclaw acp` binary is the gateway client: a
4815            // stdio ACP bridge that joins the RUNNING gateway at the resolved
4816            // endpoint. Blind-walk finding 2026-08-31: the real bridge does
4817            // NOT honor OPENCLAW_GATEWAY_TOKEN from the environment — the
4818            // credential must arrive via `--token-file` (never bare `--token`
4819            // on argv, where process listings could read it). The env var is
4820            // still set for older bridges that did read it. Requires openclaw
4821            // >= 2026.7: the 2026.2 bridge drops its gateway socket
4822            // mid-prompt and advertises no session resume (executed finding,
4823            // docs/interop/research/openclaw-acp-dialect-2026-08-30.json).
4824            let mut env = BTreeMap::new();
4825            let mut arguments = vec!["acp".into(), "--url".into(), resolved.address.clone()];
4826            if let Some(token) = resolved.auth {
4827                let token_path = openclaw_gateway_token_file(&resolved.address, token.secret())
4828                    .map_err(|error| {
4829                        ServiceError::UnsupportedAction(format!(
4830                            "could not stage the gateway credential for the bridge: {error}"
4831                        ))
4832                    })?;
4833                arguments.push("--token-file".into());
4834                arguments.push(token_path.to_string_lossy().into_owned());
4835                env.insert("OPENCLAW_GATEWAY_TOKEN".to_string(), token.secret().to_string());
4836            }
4837            // The bridge program comes from the descriptor's own default
4838            // launch (the compiled registry pins `openclaw`), so tests can
4839            // substitute an absolute mock-bridge path without touching
4840            // process-global state.
4841            let program = descriptor
4842                .runtime
4843                .default_launch
4844                .as_ref()
4845                .map(|launch| launch.program.clone())
4846                .unwrap_or_else(|| "openclaw".into());
4847            let launch = RuntimeLaunch {
4848                program,
4849                arguments,
4850                env,
4851            };
4852            Ok(Box::new(
4853                crate::AcpRuntimeBackend::new(descriptor.id.clone(), launch)
4854                    .with_resume_support(descriptor.runtime.capabilities.resume_session),
4855            ))
4856        }
4857        _ => Err(ServiceError::UnsupportedAction(format!(
4858            "connect-mode endpoint for `{}` speaks `{}`; joining it needs that protocol's gateway client",
4859            descriptor.id.as_str(),
4860            connect.protocol
4861        ))),
4862    }
4863}
4864
4865/// The registry's connect-mode launch for this harness, honored only when the
4866/// caller supplied neither an explicit launch nor a base URL.
4867fn registry_connect_descriptor(
4868    params: &RuntimeBackendParams,
4869) -> Option<crate::HarnessSupportDescriptor> {
4870    if params.launch.is_some() || params.base_url.is_some() {
4871        return None;
4872    }
4873    harness_support_registry()
4874        .harnesses
4875        .into_iter()
4876        .find(|descriptor| descriptor.id == params.harness)
4877        .filter(|descriptor| descriptor.runtime.connect_launch.is_some())
4878}
4879
4880fn service_home() -> std::result::Result<PathBuf, ServiceError> {
4881    std::env::var_os("HOME").map(PathBuf::from).ok_or_else(|| {
4882        ServiceError::UnsupportedAction(
4883            "connect-mode launches need HOME to locate the harness config".into(),
4884        )
4885    })
4886}
4887
4888/// The doors that open a runtime: each spawns or joins a program and waits on
4889/// that program's protocol handshake before it can answer.
4890pub const RUNTIME_OPEN_METHODS: &[&str] = &[
4891    "harness.v1.runtimes.start",
4892    "harness.v1.runtimes.resume",
4893    "harness.v1.runtimes.attach",
4894    "harness.v1.runtimes.attach_existing",
4895];
4896
4897/// How long a runtime gets to finish opening before its caller is answered an
4898/// error instead. A program that never speaks the protocol at all — the wrong
4899/// binary, a shim that prints usage and waits — never answers the handshake,
4900/// so the wait is unbounded without this.
4901pub const RUNTIME_OPEN_DEADLINE: Duration = Duration::from_secs(60);
4902
4903/// How long a control call on an ALREADY-open runtime — send input, interrupt,
4904/// steer, respond, close — gets before its caller is answered an error
4905/// instead. A live runtime answers these in milliseconds; a wedged one never
4906/// answers at all, and `close` is exactly what a caller reaches for when it
4907/// suspects that.
4908pub const RUNTIME_CONTROL_DEADLINE: Duration = Duration::from_secs(30);
4909
4910/// The doors whose work happens entirely OUTSIDE this service's state once
4911/// its state has been read: probing harnesses, couriering a message into a
4912/// live session, and performing a conversation verb through a harness's own
4913/// CLI / HTTP / store door. Every one of them waits on a child process or a
4914/// network peer. See [`HarnessSessionService::detach`].
4915pub const DETACHED_METHODS: &[&str] = &[
4916    "harness.v1.harnesses.list",
4917    "harness.v1.harnesses.probe",
4918    "harness.v1.sessions.message",
4919    "harness.v1.sessions.new",
4920    "harness.v1.sessions.reset",
4921    "harness.v1.sessions.archive",
4922    "harness.v1.sessions.delete",
4923];
4924
4925/// How long a request moved off a transport's loop gets before its caller is
4926/// answered an error instead. Each of these already bounds its own inner
4927/// waits (a probe's handshake, the courier's run); this is the backstop for
4928/// the ones that do not — a harness CLI that never exits — so no caller waits
4929/// forever on a detached task no one is watching.
4930pub const DETACHED_CALL_DEADLINE: Duration = Duration::from_secs(120);
4931
4932/// How long `sessions.discover` gets before its caller is answered an error
4933/// instead. Discovery reads each harness's own store, and a store on a cold
4934/// or unavailable mount answers at the filesystem's pace rather than its own.
4935///
4936/// Deliberately shorter than the clients' own request deadline (30s): the
4937/// server's answer names the store that did not answer, and it is only read
4938/// if it lands before the client stops listening.
4939pub const SESSION_DISCOVER_DEADLINE: Duration = Duration::from_secs(25);
4940
4941/// Bound one control call on an open runtime by [`RUNTIME_CONTROL_DEADLINE`],
4942/// naming the method and the bound when it blows.
4943async fn within_control_deadline<F: std::future::Future>(
4944    method: &str,
4945    call: F,
4946) -> std::result::Result<F::Output, ServiceError> {
4947    tokio::time::timeout(RUNTIME_CONTROL_DEADLINE, call)
4948        .await
4949        .map_err(|_| {
4950            ServiceError::Operation(format!(
4951                "`{method}` gave up after {}s: the runtime did not answer",
4952                RUNTIME_CONTROL_DEADLINE.as_secs()
4953            ))
4954        })
4955}
4956
4957/// One [`RUNTIME_OPEN_METHODS`] request, parsed but not yet started. See
4958/// [`HarnessSessionService::runtime_open`] for why it exists apart from
4959/// [`HarnessSessionService::handle_async`].
4960pub struct RuntimeOpen {
4961    id: Value,
4962    method: String,
4963    params: Value,
4964}
4965
4966impl RuntimeOpen {
4967    /// Do the waiting: spawn or join the program and complete its handshake,
4968    /// bounded by [`RUNTIME_OPEN_DEADLINE`]. Touches no service state, so this
4969    /// runs on any task.
4970    pub async fn open(self) -> OpenedRuntime {
4971        let Self { id, method, params } = self;
4972        let outcome = open_runtime(&method, params).await;
4973        OpenedRuntime { id, outcome }
4974    }
4975}
4976
4977/// The result of [`RuntimeOpen::open`], ready for
4978/// [`HarnessSessionService::finish_runtime_open`].
4979pub struct OpenedRuntime {
4980    id: Value,
4981    outcome: std::result::Result<OpenRuntime, ServiceError>,
4982}
4983
4984/// One detached request: the half that reads this service's state already
4985/// done, and the half that waits not yet started. See
4986/// [`HarnessSessionService::detach`] and
4987/// [`HarnessSessionService::detach_runtime`].
4988pub struct DetachedCall {
4989    id: Value,
4990    method: String,
4991    work: std::result::Result<Work, ServiceError>,
4992}
4993
4994impl DetachedCall {
4995    /// Do the waiting and answer. Runs on any task: whatever this call needed
4996    /// from the service was taken before it left.
4997    pub async fn run(self) -> DetachedAnswer {
4998        let Self { id, method, work } = self;
4999        match work {
5000            // A call holding a runtime is already bounded by
5001            // RUNTIME_CONTROL_DEADLINE, and its future OWNS that connection:
5002            // a second timeout around it would drop the connection mid-call
5003            // and take down a runtime its caller still has.
5004            Ok(Work::Runtime(work)) => {
5005                let (result, returned) = work.run().await;
5006                DetachedAnswer {
5007                    response: service_response(id, result),
5008                    returned,
5009                }
5010            }
5011            Ok(Work::Free(work)) => {
5012                let result = match tokio::time::timeout(DETACHED_CALL_DEADLINE, work.run()).await {
5013                    Ok(result) => result,
5014                    Err(_) => Err(ServiceError::Operation(format!(
5015                        "`{method}` gave up after {}s: the harness it waits on did not answer",
5016                        DETACHED_CALL_DEADLINE.as_secs()
5017                    ))),
5018                };
5019                DetachedAnswer {
5020                    response: service_response(id, result),
5021                    returned: None,
5022                }
5023            }
5024            Err(error) => DetachedAnswer {
5025                response: service_response(id, Err(error)),
5026                returned: None,
5027            },
5028        }
5029    }
5030}
5031
5032/// One detached call's complete answer, plus whatever it must hand back to
5033/// the service before that answer is written. See
5034/// [`HarnessSessionService::finish_detached`].
5035pub struct DetachedAnswer {
5036    response: Value,
5037    returned: Option<ReturnedRuntime>,
5038}
5039
5040impl DetachedAnswer {
5041    /// The caller's JSON-RPC response, for a transport that owns no service
5042    /// to give a borrowed connection back to.
5043    pub fn into_response(self) -> Value {
5044        self.response
5045    }
5046}
5047
5048/// A connection lent to a detached call, on its way back to the service that
5049/// owns it.
5050pub struct ReturnedRuntime {
5051    connection: String,
5052    runtime: Box<dyn RuntimeConnection>,
5053}
5054
5055/// The waiting half of one detached request: with nothing of the service's
5056/// in hand, or holding a connection the service lent out for the call.
5057enum Work {
5058    Free(DetachedWork),
5059    Runtime(RuntimeWork),
5060}
5061
5062/// The waiting half of one detached request that holds nothing of the
5063/// service's.
5064enum DetachedWork {
5065    /// Probe the selected harnesses: find their executables, ask each its
5066    /// version, and at `probe: handshake` start each one and complete its
5067    /// protocol handshake.
5068    Inventory(InventoryWork),
5069    /// Run the courier that delivers one message into a live session.
5070    Message(MessageSessionParams),
5071    /// Perform one conversation verb through the harness's own CLI, HTTP API,
5072    /// daemon socket, or supercode's own store.
5073    SessionMutation {
5074        verb: crate::SessionVerb,
5075        mutation: crate::SessionMutation,
5076    },
5077}
5078
5079impl DetachedWork {
5080    async fn run(self) -> std::result::Result<Value, ServiceError> {
5081        match self {
5082            Self::Inventory(work) => run_inventory(work).await,
5083            Self::Message(params) => {
5084                Ok(message_live_session(&params, &crate::claude_peer::ProcessCourierRunner).await)
5085            }
5086            Self::SessionMutation { verb, mutation } => {
5087                let outcome = run_session_mutation(verb, &mutation).await?;
5088                serde_json::to_value(outcome)
5089                    .map_err(|error| ServiceError::Operation(error.to_string()))
5090            }
5091        }
5092    }
5093}
5094
5095/// One detached call that holds a runtime connection for its whole run.
5096enum RuntimeWork {
5097    /// Tear down a runtime the service has already surrendered.
5098    Close {
5099        runtime: Box<dyn RuntimeConnection>,
5100        process_group: Option<u32>,
5101    },
5102    /// Type one live slash command through a borrowed connection, then give
5103    /// the connection back.
5104    LiveCommand {
5105        connection: String,
5106        runtime: Box<dyn RuntimeConnection>,
5107        verb: crate::SessionVerb,
5108        mutation: crate::SessionMutation,
5109        command: &'static str,
5110        session: String,
5111    },
5112}
5113
5114/// What one [`RuntimeWork`] answers with: the caller's result, and the
5115/// connection to give back when the call only borrowed one.
5116type RuntimeWorkAnswer = (
5117    std::result::Result<Value, ServiceError>,
5118    Option<ReturnedRuntime>,
5119);
5120
5121impl RuntimeWork {
5122    async fn run(self) -> RuntimeWorkAnswer {
5123        match self {
5124            Self::Close {
5125                runtime,
5126                process_group,
5127            } => (close_runtime(runtime, process_group).await, None),
5128            Self::LiveCommand {
5129                connection,
5130                mut runtime,
5131                verb,
5132                mutation,
5133                command,
5134                session,
5135            } => {
5136                let result =
5137                    type_live_command(runtime.as_mut(), verb, &mutation, command, session).await;
5138                (
5139                    result,
5140                    Some(ReturnedRuntime {
5141                        connection,
5142                        runtime,
5143                    }),
5144                )
5145            }
5146        }
5147    }
5148}
5149
5150/// Tear down a runtime already out of the service, within
5151/// [`RUNTIME_CONTROL_DEADLINE`].
5152async fn close_runtime(
5153    mut runtime: Box<dyn RuntimeConnection>,
5154    process_group: Option<u32>,
5155) -> std::result::Result<Value, ServiceError> {
5156    match within_control_deadline("harness.v1.runtimes.close", runtime.close()).await {
5157        Ok(result) => {
5158            result.map_err(operation)?;
5159            Ok(json!({"closed": true}))
5160        }
5161        Err(deadline) => {
5162            // Dropping the handle is not enough: the process that stopped
5163            // answering is held by a task parked on it, so nothing here runs
5164            // its Drop. Signal the group the graceful path would have
5165            // signalled, then say so.
5166            let killed = kill_runtime_process_group(process_group);
5167            drop(runtime);
5168            Ok(json!({
5169                "closed": true,
5170                "killed": killed,
5171                "detail": error_message(deadline),
5172            }))
5173        }
5174    }
5175}
5176
5177/// The conversation a live `sessions.new` / `sessions.reset` acts on: the one
5178/// the request named, or the runtime's own session.
5179fn live_session_name(runtime: &dyn RuntimeConnection, mutation: &crate::SessionMutation) -> String {
5180    mutation
5181        .session
5182        .clone()
5183        .filter(|value| !value.trim().is_empty())
5184        .unwrap_or_else(|| runtime.handle().runtime_id.clone())
5185}
5186
5187/// Type one harness slash command into a live session through the very same
5188/// `send_input` path a human's message takes, within
5189/// [`RUNTIME_CONTROL_DEADLINE`].
5190async fn type_live_command(
5191    runtime: &mut dyn RuntimeConnection,
5192    verb: crate::SessionVerb,
5193    mutation: &crate::SessionMutation,
5194    command: &str,
5195    session: String,
5196) -> std::result::Result<Value, ServiceError> {
5197    within_control_deadline(
5198        &format!("sessions.{}", verb.as_str()),
5199        runtime.send_input(RuntimeInput {
5200            text: command.to_string(),
5201            image_urls: Vec::new(),
5202        }),
5203    )
5204    .await?
5205    .map_err(operation)?;
5206    let outcome = crate::sessions_control::live_outcome(verb, mutation, command, session)
5207        .map_err(session_control_error)?;
5208    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
5209}
5210
5211/// A runtime that is up and whose handshake completed, with what the service
5212/// needs to take ownership of it.
5213enum OpenRuntime {
5214    /// supercode spawned this process, so it also hosts it: a frontend server,
5215    /// a live-runtime registration and a terminal launch of its own.
5216    Hosted {
5217        runtime: Box<dyn RuntimeConnection>,
5218        capabilities: crate::RuntimeCapabilities,
5219        workspace: PathBuf,
5220    },
5221    /// `attach_existing` joined a process supercode does not own. It is
5222    /// registered as a bare connection and hosts nothing.
5223    Joined { runtime: Box<dyn RuntimeConnection> },
5224}
5225
5226/// Open the runtime one [`RUNTIME_OPEN_METHODS`] request asks for, within
5227/// [`RUNTIME_OPEN_DEADLINE`]. The error a blown deadline answers names the
5228/// method and the bound, so a caller reads why it was cut loose instead of
5229/// waiting on a handshake that is never coming.
5230async fn open_runtime(
5231    method: &str,
5232    params: Value,
5233) -> std::result::Result<OpenRuntime, ServiceError> {
5234    match tokio::time::timeout(
5235        RUNTIME_OPEN_DEADLINE,
5236        open_runtime_unbounded(method, params),
5237    )
5238    .await
5239    {
5240        Ok(result) => result,
5241        Err(_) => Err(ServiceError::Operation(format!(
5242            "`{method}` gave up after {}s: the runtime never finished its protocol handshake",
5243            RUNTIME_OPEN_DEADLINE.as_secs()
5244        ))),
5245    }
5246}
5247
5248async fn open_runtime_unbounded(
5249    method: &str,
5250    params: Value,
5251) -> std::result::Result<OpenRuntime, ServiceError> {
5252    match method {
5253        "harness.v1.runtimes.start" => {
5254            let params = decode::<RuntimeStartParams>(params)?;
5255            let backend = runtime_backend(&params.backend)?;
5256            let capabilities = backend.capabilities();
5257            let workspace = params.cwd.clone();
5258            let runtime = backend
5259                .start(RuntimeStartRequest {
5260                    cwd: params.cwd,
5261                    launch: runtime_launch(&params.backend),
5262                    mcp_servers: params.mcp_servers,
5263                })
5264                .await
5265                .map_err(operation)?;
5266            Ok(OpenRuntime::Hosted {
5267                runtime,
5268                capabilities,
5269                workspace,
5270            })
5271        }
5272        "harness.v1.runtimes.resume" | "harness.v1.runtimes.attach" => {
5273            let params = decode::<RuntimeAttachParams>(params)?;
5274            let backend = runtime_backend(&params.backend)?;
5275            let capabilities = backend.capabilities();
5276            let workspace = params
5277                .cwd
5278                .clone()
5279                .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
5280            let runtime = backend
5281                .attach(RuntimeAttachRequest {
5282                    runtime_id: params.runtime_id,
5283                    cwd: params.cwd,
5284                    launch: runtime_launch(&params.backend),
5285                })
5286                .await
5287                .map_err(operation)?;
5288            Ok(OpenRuntime::Hosted {
5289                runtime,
5290                capabilities,
5291                workspace,
5292            })
5293        }
5294        "harness.v1.runtimes.attach_existing" => {
5295            let params = decode::<RuntimeAttachParams>(params)?;
5296            let backend: Box<dyn RuntimeBackend> = match params
5297                .backend
5298                .base_url
5299                .as_deref()
5300                .and_then(|value| LiveRuntimeEndpoint::parse(value).ok())
5301            {
5302                Some(endpoint) => {
5303                    #[cfg(not(feature = "adapter-api"))]
5304                    {
5305                        let _ = endpoint;
5306                        return Err(ServiceError::UnsupportedAction(
5307                            "live HTTP attachment adapter is not compiled".into(),
5308                        ));
5309                    }
5310                    #[cfg(feature = "adapter-api")]
5311                    {
5312                        let workspace = params.cwd.clone().ok_or_else(|| {
5313                            ServiceError::InvalidParams(
5314                                "Supercode live attach requires the project cwd".into(),
5315                            )
5316                        })?;
5317                        let source = LiveRuntimeSource {
5318                            harness: params.backend.harness.as_str().to_string(),
5319                            session_id: params.runtime_id.clone(),
5320                            workspace,
5321                        };
5322                        let receipt = resolve_live_runtime(&endpoint, &source)
5323                            .map_err(|error| ServiceError::Operation(error.to_string()))?;
5324                        Box::new(SupercodeHttpRuntimeBackend::new(receipt))
5325                    }
5326                }
5327                None => runtime_backend(&params.backend)?,
5328            };
5329            let capabilities = backend.capabilities();
5330            if !capabilities.attach_existing_process {
5331                return Err(ServiceError::Operation(format!(
5332                    "{} cannot attach to an already-running process; use runtimes.resume for a persisted session",
5333                    backend.harness().as_str()
5334                )));
5335            }
5336            let runtime = backend
5337                .attach_existing(RuntimeAttachRequest {
5338                    runtime_id: params.runtime_id,
5339                    cwd: params.cwd,
5340                    launch: runtime_launch(&params.backend),
5341                })
5342                .await
5343                .map_err(operation)?;
5344            Ok(OpenRuntime::Joined { runtime })
5345        }
5346        _ => Err(ServiceError::MethodNotFound),
5347    }
5348}
5349
5350/// Wrap one service outcome in its JSON-RPC 2.0 envelope.
5351fn service_response(id: Value, result: std::result::Result<Value, ServiceError>) -> Value {
5352    match result {
5353        Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
5354        Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
5355        Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
5356        Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
5357        Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
5358        Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
5359    }
5360}
5361
5362fn runtime_backend(
5363    params: &RuntimeBackendParams,
5364) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
5365    if let Some(descriptor) = registry_connect_descriptor(params) {
5366        return open_connect_descriptor(&descriptor, &service_home()?);
5367    }
5368    if params.protocol.as_deref() == Some("acp") {
5369        let launch = params
5370            .launch
5371            .clone()
5372            .or_else(|| {
5373                harness_support_registry()
5374                    .harnesses
5375                    .into_iter()
5376                    .find(|harness| harness.id == params.harness)
5377                    .filter(|harness| {
5378                        harness.runtime.implementation == ImplementationKind::GenericProtocol
5379                            && harness.runtime.protocol.starts_with("acp")
5380                    })
5381                    .and_then(|harness| harness.runtime.default_launch)
5382            })
5383            .ok_or_else(|| {
5384                ServiceError::InvalidParams(
5385                    "an ACP runtime requires `launch` unless the harness has a registered default"
5386                        .into(),
5387                )
5388            })?;
5389        let resume_session = harness_support_registry()
5390            .harnesses
5391            .into_iter()
5392            .find(|harness| harness.id == params.harness)
5393            .is_some_and(|harness| harness.runtime.capabilities.resume_session);
5394        return Ok(Box::new(
5395            AcpRuntimeBackend::new(params.harness.clone(), launch)
5396                .with_resume_support(resume_session),
5397        ));
5398    }
5399    let backend: Box<dyn RuntimeBackend> = match params.harness.as_str() {
5400        HarnessId::CODEX => Box::new(CodexRuntimeBackend::new()),
5401        HarnessId::CLAUDE_CODE => Box::new(ClaudeCodeRuntimeBackend::new()),
5402        HarnessId::PI => Box::new(PiRuntimeBackend::new()),
5403        HarnessId::OPENCODE => match &params.base_url {
5404            Some(url) => Box::new(OpenCodeRuntimeBackend::connect(url)),
5405            None => Box::new(OpenCodeRuntimeBackend::new()),
5406        },
5407        harness => {
5408            let descriptor = harness_support_registry()
5409                .harnesses
5410                .into_iter()
5411                .find(|descriptor| descriptor.id.as_str() == harness)
5412                .filter(|descriptor| {
5413                    descriptor.runtime.implementation == ImplementationKind::GenericProtocol
5414                        && descriptor.runtime.protocol.starts_with("acp")
5415                });
5416            let Some(descriptor) = descriptor else {
5417                return Err(ServiceError::InvalidParams(format!(
5418                    "no runtime adapter for harness `{harness}`; use protocol `acp` with a launch command"
5419                )));
5420            };
5421            let resume = descriptor.runtime.capabilities.resume_session;
5422            Box::new(
5423                AcpRuntimeBackend::new(
5424                    descriptor.id,
5425                    descriptor
5426                        .runtime
5427                        .default_launch
5428                        .expect("generic ACP registry entry includes its launch"),
5429                )
5430                .with_resume_support(resume),
5431            )
5432        }
5433    };
5434    Ok(backend)
5435}
5436
5437fn runtime_launch(params: &RuntimeBackendParams) -> Option<RuntimeLaunch> {
5438    if let Some(launch) = &params.launch {
5439        return Some(launch.clone());
5440    }
5441    if !matches!(params.policy, RuntimePolicy::Yolo) {
5442        return None;
5443    }
5444    let launch = match params.harness.as_str() {
5445        HarnessId::GROK => RuntimeLaunch {
5446            program: "grok".into(),
5447            arguments: {
5448                let mut arguments: Vec<String> = Vec::new();
5449                if crate::support::self_sandbox_supported() {
5450                    arguments.extend(["--sandbox".into(), "workspace".into()]);
5451                }
5452                arguments.extend([
5453                    "--always-approve".into(),
5454                    "agent".into(),
5455                    "--no-leader".into(),
5456                    "stdio".into(),
5457                ]);
5458                arguments
5459            },
5460            env: BTreeMap::from([("GROK_AGENT_DASHBOARD".into(), "0".into())]),
5461        },
5462        HarnessId::CODEX => RuntimeLaunch {
5463            program: "codex".into(),
5464            arguments: vec![
5465                "--dangerously-bypass-approvals-and-sandbox".into(),
5466                "--dangerously-bypass-hook-trust".into(),
5467                "app-server".into(),
5468            ],
5469            env: BTreeMap::new(),
5470        },
5471        HarnessId::CLAUDE_CODE => RuntimeLaunch {
5472            program: "claude".into(),
5473            arguments: vec![
5474                "--dangerously-skip-permissions".into(),
5475                "--print".into(),
5476                "--input-format".into(),
5477                "stream-json".into(),
5478                "--output-format".into(),
5479                "stream-json".into(),
5480                "--verbose".into(),
5481            ],
5482            env: BTreeMap::new(),
5483        },
5484        HarnessId::PI => RuntimeLaunch {
5485            program: "pi".into(),
5486            arguments: vec!["--approve".into(), "--mode".into(), "rpc".into()],
5487            env: BTreeMap::new(),
5488        },
5489        HarnessId::OPENCODE => RuntimeLaunch {
5490            program: "opencode".into(),
5491            arguments: vec!["serve".into()],
5492            env: BTreeMap::new(),
5493        },
5494        HarnessId::GEMINI => RuntimeLaunch {
5495            program: "gemini".into(),
5496            arguments: vec!["--acp".into(), "--yolo".into()],
5497            env: BTreeMap::new(),
5498        },
5499        HarnessId::GOOSE => RuntimeLaunch {
5500            program: "goose".into(),
5501            arguments: vec!["acp".into()],
5502            env: BTreeMap::new(),
5503        },
5504        HarnessId::SUPERCODE => RuntimeLaunch {
5505            program: "supercode".into(),
5506            arguments: vec!["acp".into(), "--dangerous".into()],
5507            env: BTreeMap::new(),
5508        },
5509        _ => return None,
5510    };
5511    Some(launch)
5512}
5513
5514/// Disposable harness state for a no-prompt readiness probe. Merely opening
5515/// several stock CLIs writes a session header or migrates configuration, so a
5516/// handshake must never point at the user's real home. Authentication files
5517/// are copied into the private temporary home; all writes disappear with the
5518/// guard after the connection closes.
5519struct IsolatedProbeHome {
5520    launch: RuntimeLaunch,
5521    root: PathBuf,
5522}
5523
5524impl IsolatedProbeHome {
5525    fn new(harness: &str, mut launch: RuntimeLaunch) -> std::io::Result<Self> {
5526        let root = std::env::temp_dir().join(format!(
5527            "supercode-harness-probe-{harness}-{}",
5528            generated_session_id()
5529        ));
5530        std::fs::create_dir_all(&root)?;
5531        set_private_dir_permissions(&root)?;
5532
5533        if let Some(source_home) = std::env::var_os("HOME").map(PathBuf::from) {
5534            for relative in probe_auth_files(harness) {
5535                copy_probe_file(&source_home, &root, relative)?;
5536            }
5537        }
5538        configure_isolated_probe_auth(harness, &root)?;
5539
5540        let root_text = root.to_string_lossy().into_owned();
5541        for (key, value) in [
5542            ("HOME", root_text.clone()),
5543            (
5544                "XDG_CACHE_HOME",
5545                root.join(".cache").to_string_lossy().into_owned(),
5546            ),
5547            (
5548                "XDG_CONFIG_HOME",
5549                root.join(".config").to_string_lossy().into_owned(),
5550            ),
5551            (
5552                "XDG_DATA_HOME",
5553                root.join(".local/share").to_string_lossy().into_owned(),
5554            ),
5555        ] {
5556            launch.env.insert(key.into(), value);
5557        }
5558        let scoped = match harness {
5559            HarnessId::CLAUDE_CODE => Some(("CLAUDE_CONFIG_DIR", root.join(".claude"))),
5560            HarnessId::CODEX => Some(("CODEX_HOME", root.join(".codex"))),
5561            HarnessId::GEMINI => Some(("GEMINI_CLI_HOME", root.clone())),
5562            HarnessId::GROK => Some(("GROK_HOME", root.join(".grok"))),
5563            HarnessId::PI => Some(("PI_CODING_AGENT_DIR", root.join(".pi/agent"))),
5564            HarnessId::SUPERCODE => Some(("SUPERCODE_HOME", root.join(".config/supercode"))),
5565            _ => None,
5566        };
5567        if let Some((key, value)) = scoped {
5568            launch
5569                .env
5570                .insert(key.into(), value.to_string_lossy().into_owned());
5571        }
5572        Ok(Self { launch, root })
5573    }
5574
5575    fn cleanup(&self) -> std::io::Result<()> {
5576        match std::fs::remove_dir_all(&self.root) {
5577            Ok(()) => Ok(()),
5578            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
5579            Err(error) => Err(error),
5580        }
5581    }
5582}
5583
5584impl Drop for IsolatedProbeHome {
5585    fn drop(&mut self) {
5586        let _ = self.cleanup();
5587    }
5588}
5589
5590fn probe_auth_files(harness: &str) -> &'static [&'static str] {
5591    match harness {
5592        HarnessId::CLAUDE_CODE => &[".claude/.credentials.json", ".claude.json"],
5593        // The gateway endpoint + token live in openclaw's own config; without
5594        // it the isolated probe dials the default endpoint unauthenticated
5595        // (PARITY-24 finding 2026-08-31).
5596        HarnessId::OPENCLAW => &[".openclaw/openclaw.json"],
5597        HarnessId::CODEX => &[".codex/auth.json"],
5598        HarnessId::GEMINI => &[
5599            ".gemini/google_accounts.json",
5600            ".gemini/oauth_creds.json",
5601            ".gemini/settings.json",
5602        ],
5603        HarnessId::GROK => &[".grok/auth.json", ".grok/config.toml"],
5604        HarnessId::OPENCODE => &[
5605            ".config/opencode/auth.json",
5606            ".local/share/opencode/auth.json",
5607        ],
5608        HarnessId::PI => &[".pi/agent/auth.json"],
5609        // Hermes keeps its provider selection in config.yaml, its OAuth
5610        // credential pool in auth.json, and API keys in .env; without them
5611        // the isolated probe sees "No LLM provider configured" for a
5612        // hermes that answers fine from the user's real home.
5613        HarnessId::HERMES => &[".hermes/config.yaml", ".hermes/auth.json", ".hermes/.env"],
5614        HarnessId::SUPERCODE => &[
5615            ".config/supercode/config.toml",
5616            ".config/supercode/credentials.toml",
5617        ],
5618        _ => &[],
5619    }
5620}
5621
5622fn copy_probe_file(source_home: &Path, probe_home: &Path, relative: &str) -> std::io::Result<()> {
5623    let source = source_home.join(relative);
5624    if !source.is_file() {
5625        return Ok(());
5626    }
5627    let destination = probe_home.join(relative);
5628    if let Some(parent) = destination.parent() {
5629        std::fs::create_dir_all(parent)?;
5630        set_private_dir_permissions(parent)?;
5631    }
5632    std::fs::copy(source, &destination)?;
5633    set_private_file_permissions(&destination)
5634}
5635
5636fn configure_isolated_probe_auth(harness: &str, probe_home: &Path) -> std::io::Result<()> {
5637    if harness != HarnessId::GEMINI {
5638        return Ok(());
5639    }
5640    let oauth = probe_home.join(".gemini/oauth_creds.json");
5641    if !oauth.is_file() {
5642        return Ok(());
5643    }
5644    let settings_path = probe_home.join(".gemini/settings.json");
5645    let mut settings = std::fs::read_to_string(&settings_path)
5646        .ok()
5647        .and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
5648        .unwrap_or_else(|| json!({}));
5649    settings["security"]["auth"]["selectedType"] = Value::String("oauth-personal".into());
5650    std::fs::write(
5651        &settings_path,
5652        serde_json::to_vec_pretty(&settings).map_err(std::io::Error::other)?,
5653    )?;
5654    set_private_file_permissions(&settings_path)
5655}
5656
5657#[cfg(unix)]
5658fn set_private_dir_permissions(path: &Path) -> std::io::Result<()> {
5659    use std::os::unix::fs::PermissionsExt;
5660    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
5661}
5662
5663#[cfg(not(unix))]
5664fn set_private_dir_permissions(_path: &Path) -> std::io::Result<()> {
5665    Ok(())
5666}
5667
5668#[cfg(unix)]
5669fn set_private_file_permissions(path: &Path) -> std::io::Result<()> {
5670    use std::os::unix::fs::PermissionsExt;
5671    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
5672}
5673
5674#[cfg(not(unix))]
5675fn set_private_file_permissions(_path: &Path) -> std::io::Result<()> {
5676    Ok(())
5677}
5678
5679fn find_executable(program: &str) -> Option<PathBuf> {
5680    let candidate = PathBuf::from(program);
5681    if candidate.components().count() > 1 {
5682        return candidate.is_file().then_some(candidate);
5683    }
5684    let path = std::env::var_os("PATH")?;
5685    for directory in std::env::split_paths(&path) {
5686        let candidate = directory.join(program);
5687        if candidate.is_file() {
5688            return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5689        }
5690        #[cfg(windows)]
5691        {
5692            for extension in ["exe", "cmd", "bat"] {
5693                let candidate = directory.join(format!("{program}.{extension}"));
5694                if candidate.is_file() {
5695                    return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5696                }
5697            }
5698        }
5699    }
5700    None
5701}
5702
5703async fn executable_version(executable: &Path) -> Option<String> {
5704    let mut command = tokio::process::Command::new(executable);
5705    command
5706        .arg("--version")
5707        .stdin(std::process::Stdio::null())
5708        .stdout(std::process::Stdio::piped())
5709        .stderr(std::process::Stdio::piped())
5710        .kill_on_drop(true);
5711    let output = tokio::time::timeout(Duration::from_secs(3), command.output())
5712        .await
5713        .ok()?
5714        .ok()?;
5715    let stdout = String::from_utf8_lossy(&output.stdout);
5716    let stderr = String::from_utf8_lossy(&output.stderr);
5717    stdout
5718        .lines()
5719        .chain(stderr.lines())
5720        .map(str::trim)
5721        .find(|line| !line.is_empty())
5722        .map(|line| truncate_text(line, 200))
5723}
5724
5725pub(crate) fn auth_evidence(harness: &str) -> bool {
5726    let env_names: &[&str] = match harness {
5727        HarnessId::CLAUDE_CODE => &["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
5728        HarnessId::CODEX => &["OPENAI_API_KEY"],
5729        HarnessId::OPENCODE => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5730        HarnessId::PI => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5731        HarnessId::GROK => &["XAI_API_KEY", "GROK_API_KEY"],
5732        HarnessId::GEMINI => &["GEMINI_API_KEY", "GOOGLE_API_KEY"],
5733        HarnessId::SUPERCODE => &["OPENROUTER_API_KEY"],
5734        _ => &[],
5735    };
5736    if env_names
5737        .iter()
5738        .any(|name| std::env::var_os(name).is_some_and(|value| !value.is_empty()))
5739    {
5740        return true;
5741    }
5742    let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
5743        return false;
5744    };
5745    let files: Vec<PathBuf> = match harness {
5746        HarnessId::CLAUDE_CODE => vec![home.join(".claude/.credentials.json")],
5747        HarnessId::CODEX => vec![home.join(".codex/auth.json")],
5748        HarnessId::OPENCODE => vec![
5749            home.join(".local/share/opencode/auth.json"),
5750            home.join(".config/opencode/auth.json"),
5751        ],
5752        HarnessId::PI => vec![home.join(".pi/agent/auth.json")],
5753        HarnessId::GROK => vec![home.join(".grok/auth.json")],
5754        HarnessId::GEMINI => vec![
5755            home.join(".gemini/oauth_creds.json"),
5756            home.join(".gemini/google_accounts.json"),
5757        ],
5758        HarnessId::SUPERCODE => vec![home.join(".config/supercode/credentials.toml")],
5759        HarnessId::HERMES => vec![home.join(".hermes/auth.json"), home.join(".hermes/.env")],
5760        _ => Vec::new(),
5761    };
5762    if files.into_iter().any(|path| {
5763        std::fs::metadata(path)
5764            .map(|metadata| metadata.is_file() && metadata.len() > 2)
5765            .unwrap_or(false)
5766    }) {
5767        return true;
5768    }
5769    // macOS keeps Claude Code's OAuth login in the Keychain, so
5770    // `.claude/.credentials.json` never exists there and the file probe above
5771    // reports a signed-in install as unauthenticated forever. A completed
5772    // login also writes an `oauthAccount` record into `~/.claude.json` on
5773    // every platform — file-based, prompt-free evidence (querying the
5774    // Keychain itself from an unsigned daemon can raise a UI prompt).
5775    if harness == HarnessId::CLAUDE_CODE {
5776        return std::fs::read_to_string(home.join(".claude.json"))
5777            .map(|text| text.contains("\"oauthAccount\""))
5778            .unwrap_or(false);
5779    }
5780    false
5781}
5782
5783fn looks_like_auth_error(message: &str) -> bool {
5784    let message = message.to_ascii_lowercase();
5785    [
5786        "auth",
5787        "login",
5788        "sign in",
5789        "sign-in",
5790        "credential",
5791        "unauthorized",
5792        "forbidden",
5793        "token",
5794    ]
5795    .iter()
5796    .any(|needle| message.contains(needle))
5797}
5798
5799fn unavailable_capabilities() -> crate::RuntimeCapabilities {
5800    crate::RuntimeCapabilities {
5801        start_session: false,
5802        resume_session: false,
5803        attach_existing_process: false,
5804        send_input: false,
5805        stream_events: false,
5806        interrupt: false,
5807        steer: false,
5808        respond_to_requests: false,
5809    }
5810}
5811
5812fn truncate_text(text: &str, max_chars: usize) -> String {
5813    let mut chars = text.chars();
5814    let truncated = chars.by_ref().take(max_chars).collect::<String>();
5815    if chars.next().is_some() {
5816        format!("{truncated}…")
5817    } else {
5818        truncated
5819    }
5820}
5821
5822/// The process group a runtime's own handle names, when it names one.
5823///
5824/// Every adapter that spawns a local process spawns it as its own group
5825/// leader (`Command::process_group(0)`), so the endpoint's pid IS the group
5826/// id. A runtime reached over HTTP, or one supercode joined rather than
5827/// spawned, names no group here and is left alone.
5828fn runtime_process_group(handle: &crate::RuntimeHandle) -> Option<u32> {
5829    match &handle.endpoint {
5830        crate::RuntimeEndpoint::LocalProcess { pid, .. } => *pid,
5831        crate::RuntimeEndpoint::Http { .. } => None,
5832    }
5833}
5834
5835/// SIGKILL a wedged runtime's whole process group, reporting whether there
5836/// was one to signal. This is the same group teardown a graceful `close`
5837/// performs; it runs here only when the graceful path blew its deadline,
5838/// because the task parked on the unanswered call still owns the process
5839/// handle and so no `Drop` of ours can reach it.
5840fn kill_runtime_process_group(process_group: Option<u32>) -> bool {
5841    match process_group {
5842        #[cfg(unix)]
5843        Some(pid) => {
5844            crate::lsp::kill_process_group(pid);
5845            true
5846        }
5847        #[cfg(not(unix))]
5848        Some(_) => false,
5849        None => false,
5850    }
5851}
5852
5853fn error_message(error: ServiceError) -> String {
5854    match error {
5855        ServiceError::InvalidParams(message)
5856        | ServiceError::Operation(message)
5857        | ServiceError::UnsupportedAction(message) => message,
5858        ServiceError::MethodNotFound => "runtime adapter is not available".into(),
5859        ServiceError::Sdk(error) => error.to_string(),
5860    }
5861}
5862
5863#[derive(Debug)]
5864enum ServiceError {
5865    InvalidParams(String),
5866    MethodNotFound,
5867    UnsupportedAction(String),
5868    Operation(String),
5869    Sdk(SdkError),
5870}
5871
5872fn sdk_error(operation: SdkOperation, error: ServiceError) -> SdkError {
5873    match error {
5874        ServiceError::InvalidParams(message) => {
5875            SdkError::new(SdkErrorCode::InvalidArgument, operation, message)
5876        }
5877        ServiceError::MethodNotFound | ServiceError::UnsupportedAction(_) => {
5878            SdkError::unsupported(operation)
5879        }
5880        ServiceError::Operation(message) => {
5881            let code = if message.contains("already in progress") {
5882                SdkErrorCode::Busy
5883            } else if message.contains("not supported by this runtime") {
5884                SdkErrorCode::UnsupportedAction
5885            } else if message.contains("unknown runtime connection") {
5886                SdkErrorCode::NotFound
5887            } else {
5888                SdkErrorCode::Execution
5889            };
5890            SdkError::new(code, operation, message)
5891        }
5892        ServiceError::Sdk(error) => error,
5893    }
5894}
5895
5896fn sdk_rpc_error(id: Value, error: &SdkError) -> Value {
5897    let error_code = error.code();
5898    let code = match error_code {
5899        SdkErrorCode::Unauthenticated => -32030,
5900        SdkErrorCode::Unauthorized => -32031,
5901        SdkErrorCode::ControllerRequired => -32032,
5902        SdkErrorCode::LeaseExpired => -32033,
5903        SdkErrorCode::InvalidArgument => -32602,
5904        SdkErrorCode::NotFound => -32004,
5905        SdkErrorCode::Busy => -32000,
5906        SdkErrorCode::UnsupportedAction => -32020,
5907        SdkErrorCode::Execution => -32002,
5908        SdkErrorCode::Transport => -32003,
5909    };
5910    json!({
5911        "jsonrpc": "2.0",
5912        "id": id,
5913        "error": {
5914            "code": code,
5915            "name": error_code,
5916            "operation": error.operation(),
5917            "message": error.to_string(),
5918        },
5919    })
5920}
5921
5922fn decode<T: for<'de> Deserialize<'de>>(value: Value) -> std::result::Result<T, ServiceError> {
5923    serde_json::from_value(value).map_err(|error| ServiceError::InvalidParams(error.to_string()))
5924}
5925
5926fn operation(error: impl Into<crate::Error>) -> ServiceError {
5927    let error = error.into();
5928    match error {
5929        crate::Error::Sdk(error) => ServiceError::Sdk(error),
5930        error => ServiceError::Operation(error.to_string()),
5931    }
5932}
5933
5934/// ORCH-12 `harness.v1.memory.show|search` params. `homes` is the same
5935/// storage-root override every read-only method accepts, so a caller can
5936/// point the read at a fixture home without touching the real ones.
5937#[derive(Debug, Clone, Deserialize, Default)]
5938#[serde(default)]
5939struct MemoryRequest {
5940    /// Harness whose store is read. Required.
5941    harness: Option<String>,
5942    /// The needle, required by `search`.
5943    query: Option<String>,
5944    /// Hermes profile, OpenClaw agent, or Claude Code project.
5945    profile: Option<String>,
5946    /// Claude Code session id selecting a project store (`show` only).
5947    session: Option<String>,
5948    /// Include each document's whole text (`show` only).
5949    full: bool,
5950    /// Treat `query` as a regular expression (`search` only).
5951    regex: bool,
5952    /// Working tree whose project store is read.
5953    cwd: Option<std::path::PathBuf>,
5954    /// Storage roots to read.
5955    homes: crate::HarnessHomes,
5956}
5957
5958/// Read the memory noun. A harness with no memory store fails with
5959/// `UnsupportedAction` (RPC `-32020`), never an empty list.
5960fn memory_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
5961    let request = decode::<MemoryRequest>(params)?;
5962    let harness = request
5963        .harness
5964        .clone()
5965        .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
5966    let to_service = |error: crate::memory::MemoryError| match error {
5967        crate::memory::MemoryError::UnsupportedHarness { .. }
5968        | crate::memory::MemoryError::SessionNotScoped { .. } => {
5969            ServiceError::UnsupportedAction(error.to_string())
5970        }
5971        other => ServiceError::InvalidParams(other.to_string()),
5972    };
5973    match method {
5974        "harness.v1.memory.show" => {
5975            let documents = crate::memory::show_memory(&crate::memory::MemoryQuery {
5976                harness,
5977                profile: request.profile,
5978                session: request.session,
5979                full: request.full,
5980                cwd: request.cwd,
5981                homes: request.homes,
5982            })
5983            .map_err(to_service)?;
5984            Ok(json!({
5985                "schema": crate::memory::MEMORY_SCHEMA,
5986                "documents": documents,
5987            }))
5988        }
5989        "harness.v1.memory.search" => {
5990            let query = request
5991                .query
5992                .ok_or_else(|| ServiceError::InvalidParams("`query` is required".into()))?;
5993            let matches = crate::memory::search_memory(&crate::memory::MemorySearchQuery {
5994                harness,
5995                query,
5996                profile: request.profile,
5997                regex: request.regex,
5998                cwd: request.cwd,
5999                homes: request.homes,
6000            })
6001            .map_err(to_service)?;
6002            Ok(json!({
6003                "schema": crate::memory::MEMORY_SCHEMA,
6004                "matches": matches,
6005            }))
6006        }
6007        _ => Err(ServiceError::MethodNotFound),
6008    }
6009}
6010
6011/// ORCH-10 `harness.v1.profiles.list|get` params. `homes` is the same
6012/// storage-root override every read-only method accepts, so a caller can
6013/// point the read at a fixture home without touching the real ones.
6014#[derive(Debug, Clone, Deserialize)]
6015#[serde(default)]
6016struct ProfilesQuery {
6017    /// Restrict the listing to one harness. `get` requires it.
6018    harness: Option<String>,
6019    /// Profile name, required by `get`.
6020    name: Option<String>,
6021    /// Storage roots to read.
6022    homes: crate::HarnessHomes,
6023}
6024
6025impl Default for ProfilesQuery {
6026    fn default() -> Self {
6027        Self {
6028            harness: None,
6029            name: None,
6030            homes: crate::HarnessHomes::default(),
6031        }
6032    }
6033}
6034
6035/// Read the profile noun. A harness with no profile concept fails with
6036/// `UnsupportedAction` (RPC `-32020`), never an empty list.
6037fn profiles_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6038    let query = decode::<ProfilesQuery>(params)?;
6039    let to_service = |error: crate::profiles::ProfileError| match error {
6040        crate::profiles::ProfileError::UnsupportedHarness { .. } => {
6041            ServiceError::UnsupportedAction(error.to_string())
6042        }
6043        crate::profiles::ProfileError::NotFound { .. } => {
6044            ServiceError::InvalidParams(error.to_string())
6045        }
6046    };
6047    match method {
6048        "harness.v1.profiles.list" => {
6049            let profiles = crate::profiles::list_profiles(&query.homes, query.harness.as_deref())
6050                .map_err(to_service)?;
6051            Ok(json!({
6052                "schema": crate::profiles::PROFILES_SCHEMA,
6053                "profiles": profiles,
6054            }))
6055        }
6056        "harness.v1.profiles.get" => {
6057            let harness = query
6058                .harness
6059                .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6060            let name = query
6061                .name
6062                .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6063            let profile =
6064                crate::profiles::get_profile(&query.homes, &harness, &name).map_err(to_service)?;
6065            Ok(json!({
6066                "schema": crate::profiles::PROFILES_SCHEMA,
6067                "profile": profile,
6068            }))
6069        }
6070        _ => Err(ServiceError::MethodNotFound),
6071    }
6072}
6073
6074/// ORCH-14 `harness.v1.channels.list|status` params, the same storage-root
6075/// override every read-only method accepts so a caller can point the read at
6076/// a fixture home without touching the real ones.
6077#[derive(Debug, Clone, Deserialize)]
6078#[serde(default)]
6079struct ChannelsQuery {
6080    /// Restrict the listing to one harness. `status` requires it.
6081    harness: Option<String>,
6082    /// Channel name, required by `status`.
6083    name: Option<String>,
6084    /// Storage roots to read.
6085    homes: crate::HarnessHomes,
6086}
6087
6088impl Default for ChannelsQuery {
6089    fn default() -> Self {
6090        Self {
6091            harness: None,
6092            name: None,
6093            homes: crate::HarnessHomes::default(),
6094        }
6095    }
6096}
6097
6098/// Read the channel noun. A harness with no channel concept fails with
6099/// `UnsupportedAction` (RPC `-32020`), never an empty list. No row carries a
6100/// token, key or secret — see `crate::channels` "Secrecy".
6101#[derive(Debug, Clone, Deserialize)]
6102#[serde(default)]
6103struct RoutesQuery {
6104    harness: Option<String>,
6105    /// Restrict to routes targeting one profile / agent.
6106    profile: Option<String>,
6107    homes: crate::HarnessHomes,
6108}
6109
6110impl Default for RoutesQuery {
6111    fn default() -> Self {
6112        Self {
6113            harness: None,
6114            profile: None,
6115            homes: crate::HarnessHomes::default(),
6116        }
6117    }
6118}
6119
6120#[derive(Debug, Clone, Deserialize)]
6121#[serde(default)]
6122struct TriggersQuery {
6123    harness: Option<String>,
6124    homes: crate::HarnessHomes,
6125}
6126
6127impl Default for TriggersQuery {
6128    fn default() -> Self {
6129        Self {
6130            harness: None,
6131            homes: crate::HarnessHomes::default(),
6132        }
6133    }
6134}
6135
6136fn triggers_call(params: Value) -> std::result::Result<Value, ServiceError> {
6137    let query = decode::<TriggersQuery>(params)?;
6138    let triggers = crate::triggers::list_triggers(&query.homes, query.harness.as_deref())
6139        .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6140    Ok(json!({
6141        "schema": crate::triggers::TRIGGERS_SCHEMA,
6142        "triggers": triggers,
6143    }))
6144}
6145
6146fn routes_call(params: Value) -> std::result::Result<Value, ServiceError> {
6147    let query = decode::<RoutesQuery>(params)?;
6148    let routes = crate::routes::list_routes(
6149        &query.homes,
6150        query.harness.as_deref(),
6151        query.profile.as_deref(),
6152    )
6153    .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6154    Ok(json!({
6155        "schema": crate::routes::ROUTES_SCHEMA,
6156        "routes": routes,
6157    }))
6158}
6159
6160fn channels_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6161    let query = decode::<ChannelsQuery>(params)?;
6162    let to_service = |error: crate::channels::ChannelError| match error {
6163        crate::channels::ChannelError::UnsupportedHarness { .. } => {
6164            ServiceError::UnsupportedAction(error.to_string())
6165        }
6166        crate::channels::ChannelError::NotFound { .. } => {
6167            ServiceError::InvalidParams(error.to_string())
6168        }
6169    };
6170    match method {
6171        "harness.v1.channels.list" => {
6172            let channels = crate::channels::list_channels(&query.homes, query.harness.as_deref())
6173                .map_err(to_service)?;
6174            Ok(json!({
6175                "schema": crate::channels::CHANNELS_SCHEMA,
6176                "channels": channels,
6177            }))
6178        }
6179        "harness.v1.channels.status" => {
6180            let harness = query
6181                .harness
6182                .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6183            let name = query
6184                .name
6185                .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6186            let channel = crate::channels::channel_status(&query.homes, &harness, &name)
6187                .map_err(to_service)?;
6188            Ok(json!({
6189                "schema": crate::channels::CHANNELS_SCHEMA,
6190                "channel": channel,
6191            }))
6192        }
6193        _ => Err(ServiceError::MethodNotFound),
6194    }
6195}
6196
6197fn rpc_error(id: Value, code: i64, message: &str) -> Value {
6198    json!({
6199        "jsonrpc": "2.0",
6200        "id": id,
6201        "error": {"code": code, "message": message},
6202    })
6203}
6204
6205#[cfg(test)]
6206mod tests {
6207    use super::*;
6208    use crate::{HarnessEvent, HarnessId, RuntimeEndpoint, RuntimeHandle, StorageLocator};
6209    use async_trait::async_trait;
6210    use std::io::Write;
6211    use std::path::PathBuf;
6212    use std::time::Instant;
6213
6214    #[test]
6215    fn indexed_claude_descriptor_keeps_the_live_peer_address() {
6216        let descriptor = SessionDescriptor {
6217            locator: SessionLocator {
6218                harness: HarnessId::new(HarnessId::CLAUDE_CODE),
6219                session_id: "live-session".into(),
6220                storage: StorageLocator::File {
6221                    path: PathBuf::from("/tmp/live-session.jsonl"),
6222                },
6223            },
6224            cwd: Some(PathBuf::from("/project")),
6225            title: None,
6226            preview_candidates: Vec::new(),
6227            latest_message_candidates: Vec::new(),
6228            updated_at_ms: Some(1),
6229            message_count: None,
6230            model: None,
6231            parent_session_id: None,
6232            child_session_count: 0,
6233            nouns: Default::default(),
6234        };
6235        let peer = crate::claude_peer::ClaudePeerSession {
6236            pid: 42,
6237            session_id: "live-session".into(),
6238            cwd: Some(PathBuf::from("/project")),
6239            name: "peer".into(),
6240            socket_path: PathBuf::from("/tmp/peer.sock"),
6241            status: Some(crate::claude_peer::ClaudePeerStatus::Busy),
6242            updated_at_ms: Some(1),
6243            version: Some("test".into()),
6244        };
6245
6246        let value = live_descriptor_value(&descriptor, &[peer]).unwrap();
6247        assert!(value["live_endpoint"]
6248            .as_str()
6249            .is_some_and(|endpoint| endpoint.starts_with("cc-peer:v1:42:peer:")));
6250    }
6251
6252    struct EndingRuntime {
6253        handle: RuntimeHandle,
6254        event: Option<HarnessEvent>,
6255        close_failures: usize,
6256    }
6257
6258    #[async_trait]
6259    impl RuntimeConnection for EndingRuntime {
6260        fn handle(&self) -> &RuntimeHandle {
6261            &self.handle
6262        }
6263
6264        async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
6265            unreachable!("ending runtime does not accept input")
6266        }
6267
6268        async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
6269            Ok(self.event.take())
6270        }
6271
6272        async fn interrupt(&mut self) -> crate::Result<()> {
6273            Ok(())
6274        }
6275
6276        async fn respond(&mut self, _request_id: Value, _response: Value) -> crate::Result<()> {
6277            Ok(())
6278        }
6279
6280        async fn close(&mut self) -> crate::Result<()> {
6281            if self.close_failures > 0 {
6282                self.close_failures -= 1;
6283                return Err(crate::Error::Other(
6284                    "cleanup temporarily unavailable".into(),
6285                ));
6286            }
6287            Ok(())
6288        }
6289    }
6290
6291    fn ending_runtime(event: Option<HarnessEvent>) -> Box<dyn RuntimeConnection> {
6292        Box::new(EndingRuntime {
6293            handle: RuntimeHandle {
6294                harness: HarnessId::from(HarnessId::CLAUDE_CODE),
6295                runtime_id: "ending-session".into(),
6296                endpoint: RuntimeEndpoint::LocalProcess {
6297                    pid: None,
6298                    command: vec!["ending-runtime".into()],
6299                    protocol: "test".into(),
6300                },
6301            },
6302            event,
6303            close_failures: 0,
6304        })
6305    }
6306
6307    #[tokio::test]
6308    async fn closing_a_runtime_surrenders_the_connection_even_when_teardown_fails() {
6309        let mut service = HarnessSessionService::new();
6310        let handle = ending_runtime(None).handle().clone();
6311        let runtime_id = handle.runtime_id.clone();
6312        let opened = service
6313            .insert_runtime(Box::new(EndingRuntime {
6314                handle,
6315                event: None,
6316                close_failures: 1,
6317            }))
6318            .unwrap();
6319        let connection = opened["connection"].as_str().unwrap().to_string();
6320        service.terminal_launches.insert(
6321            connection.clone(),
6322            StructuredLaunch {
6323                cwd: PathBuf::from("/fixture"),
6324                program: "fixture".into(),
6325                arguments: Vec::new(),
6326                env: BTreeMap::new(),
6327            },
6328        );
6329        let first = service
6330            .handle_async(request(
6331                1,
6332                "harness.v1.runtimes.close",
6333                json!({"connection": connection}),
6334            ))
6335            .await;
6336        // The harness's own teardown failed and the caller is told so...
6337        assert!(first.get("error").is_some(), "{first}");
6338        // ...but the connection is gone all the same. A connection whose close
6339        // cannot complete is exactly the one that must not stay registered:
6340        // holding it would answer every later call on this node with a turn
6341        // that is never going to end.
6342        assert!(!service.runtimes.contains_key(&connection));
6343        assert!(!service.terminal_launches.contains_key(&connection));
6344        assert!(!service.runtime_sequences.contains_key(&runtime_id));
6345        let again = service
6346            .handle_async(request(
6347                2,
6348                "harness.v1.runtimes.close",
6349                json!({"connection": connection}),
6350            ))
6351            .await;
6352        assert_eq!(again["error"]["code"], -32602, "{again}");
6353    }
6354
6355    fn request(id: u64, method: &str, params: Value) -> Value {
6356        json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params})
6357    }
6358
6359    // ---- ORCH-6: conversation nouns on `sessions.*` ----------------------
6360
6361    fn hermes_store() -> PathBuf {
6362        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/hermes_home/state.db")
6363    }
6364
6365    /// The discovery response for the Hermes fixture home, with the one
6366    /// machine-specific value (the absolute store path) replaced so the exact
6367    /// same JSON can be committed and replayed by the UI story.
6368    fn hermes_discovery(params: Value) -> Value {
6369        let mut response =
6370            HarnessSessionService::new().handle(request(1, "harness.v1.sessions.discover", params));
6371        let store = hermes_store().display().to_string();
6372        for session in response["result"]["sessions"]
6373            .as_array_mut()
6374            .expect("sessions array")
6375        {
6376            if session["locator"]["storage"]["path"] == json!(store) {
6377                session["locator"]["storage"]["path"] = json!("<fixtures>/hermes_home/state.db");
6378            }
6379            // `activity` reports a wall-clock observation instant, not a fact
6380            // about the session; it would make this response differ on every
6381            // call. The nouns under test are all session facts.
6382            session.as_object_mut().unwrap().remove("activity");
6383        }
6384        response["result"].take()
6385    }
6386
6387    fn hermes_query() -> Value {
6388        json!({
6389            "harnesses": ["hermes"],
6390            "homes": {"hermes": hermes_store()},
6391        })
6392    }
6393
6394    fn row<'a>(result: &'a Value, id: &str) -> &'a Value {
6395        result["sessions"]
6396            .as_array()
6397            .expect("sessions array")
6398            .iter()
6399            .find(|session| session["locator"]["session_id"] == json!(id))
6400            .unwrap_or_else(|| panic!("no discovered row for `{id}` in {result:#}"))
6401    }
6402
6403    #[test]
6404    fn orch6_discover_rows_carry_the_conversation_nouns() {
6405        let result = hermes_discovery(hermes_query());
6406
6407        // A Telegram DM: reached on a channel, no repo — the workspace IS the
6408        // channel (D2 precedence), and `main` is not a profile.
6409        let dm = row(&result, "tg-dm-1");
6410        assert_eq!(dm["trigger"], json!("channel"));
6411        assert_eq!(dm["surface"]["platform"], json!("telegram"));
6412        assert_eq!(dm["surface"]["kind"], json!("dm"));
6413        assert_eq!(dm["surface"]["chat_id"], json!("123456"));
6414        assert_eq!(dm["surface"]["participant_id"], json!("u1"));
6415        assert_eq!(
6416            dm["workspace"],
6417            json!({"kind": "channel", "value": "telegram:123456"})
6418        );
6419        assert!(dm.get("profile").is_none(), "{dm:#}");
6420
6421        // A cron fire: recurring, with the job recovered from the minted id.
6422        let fire = row(&result, "cron_job42_20260902_120000");
6423        assert_eq!(fire["trigger"], json!("cron"));
6424        assert_eq!(
6425            fire["recurrence"],
6426            json!({"job_id": "job42", "kind": "cron"})
6427        );
6428        assert_eq!(fire["workspace"]["kind"], json!("repo"));
6429
6430        // A profiled group session with a pending handoff: repo workspace
6431        // wins over the channel, and the chat stays on the surface key.
6432        let coder = row(&result, "tg-coder-1");
6433        assert_eq!(coder["trigger"], json!("channel"));
6434        assert_eq!(coder["profile"], json!("coder"));
6435        assert_eq!(coder["surface"]["thread_id"], json!("55"));
6436        assert_eq!(
6437            coder["surface"]["key"],
6438            json!("agent:coder:telegram:group:-100777:55")
6439        );
6440        assert_eq!(
6441            coder["workspace"],
6442            json!({"kind": "repo", "value": "/workspace/project"})
6443        );
6444        assert_eq!(
6445            coder["cross_surface"],
6446            json!({"state": "pending", "platform": "discord"})
6447        );
6448
6449        // A plain ACP session stays human-triggered with no surface at all.
6450        let acp = row(&result, "cef97234-e8e8-428a-99ab-e8fff4e7e613");
6451        assert_eq!(acp["trigger"], json!("human"));
6452        assert!(acp.get("surface").is_none(), "{acp:#}");
6453        assert_eq!(acp["workspace"], json!({"kind": "none"}));
6454    }
6455
6456    #[test]
6457    fn orch6_discover_filters_by_harness_and_profile() {
6458        let mut params = hermes_query();
6459        params["profile"] = json!("coder");
6460        let result = hermes_discovery(params);
6461        let ids: Vec<&str> = result["sessions"]
6462            .as_array()
6463            .expect("sessions array")
6464            .iter()
6465            .map(|session| session["locator"]["session_id"].as_str().unwrap())
6466            .collect();
6467        assert_eq!(ids, vec!["tg-coder-1"]);
6468
6469        // A profile no session is routed through returns nothing rather than
6470        // silently ignoring the filter.
6471        let mut missing = hermes_query();
6472        missing["profile"] = json!("nobody");
6473        assert_eq!(hermes_discovery(missing)["sessions"], json!([]));
6474
6475        // The harness filter is `harnesses`; an id no harness answers to is
6476        // an empty page, never every store on the box.
6477        let elsewhere = json!({"harnesses": ["codex"], "homes": {"codex": hermes_store()}});
6478        assert_eq!(hermes_discovery(elsewhere)["sessions"], json!([]));
6479    }
6480
6481    #[test]
6482    fn orch6_load_reports_the_same_nouns_as_discovery() {
6483        let mut service = HarnessSessionService::new();
6484        let loaded = service.handle(request(
6485            1,
6486            "harness.v1.sessions.load",
6487            json!({"locator": {
6488                "harness": "hermes",
6489                "session_id": "tg-coder-1",
6490                "storage": {"kind": "file", "path": hermes_store()},
6491            }}),
6492        ));
6493        let session = &loaded["result"]["session"];
6494        let discovered = hermes_discovery(hermes_query());
6495        let row = row(&discovered, "tg-coder-1");
6496        for noun in [
6497            "trigger",
6498            "surface",
6499            "profile",
6500            "recurrence",
6501            "cross_surface",
6502            "workspace",
6503        ] {
6504            assert_eq!(
6505                session[noun],
6506                row.get(noun).cloned().unwrap_or(Value::Null),
6507                "`{noun}` disagrees between sessions.load and sessions.discover"
6508            );
6509        }
6510    }
6511
6512    /// ORCH-10: the fixture homes, as the RPC's `homes` override. Hermes's
6513    /// home is named by its `state.db`; OpenClaw's is the state directory.
6514    fn profile_fixture_homes() -> Value {
6515        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6516        json!({
6517            "hermes": fixtures.join("hermes_home/state.db"),
6518            "openclaw": fixtures.join("openclaw_home"),
6519        })
6520    }
6521
6522    fn profile_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6523        response["result"]["profiles"]
6524            .as_array()
6525            .unwrap_or_else(|| panic!("no profiles array in {response}"))
6526            .iter()
6527            .find(|row| row["harness"] == harness && row["name"] == name)
6528            .unwrap_or_else(|| panic!("no `{harness}` profile `{name}` in {response}"))
6529    }
6530
6531    /// dev/01: every source answers in one row shape, over the committed
6532    /// fixture homes — the Hermes profile directory and its `state.db`
6533    /// partition, the OpenClaw agent directories and `openclaw.json`, and
6534    /// supercode's own presets.
6535    #[test]
6536    fn profiles_list_reads_every_source_uniformly() {
6537        let mut service = HarnessSessionService::new();
6538        let response = service.handle(request(
6539            1,
6540            "harness.v1.profiles.list",
6541            json!({"homes": profile_fixture_homes()}),
6542        ));
6543        assert_eq!(
6544            response["result"]["schema"],
6545            crate::profiles::PROFILES_SCHEMA
6546        );
6547
6548        let default = profile_row(&response, "hermes", "default");
6549        assert_eq!(default["kind"], "hermes_profile");
6550        assert_eq!(default["default"], true);
6551        assert_eq!(default["routes"], 0);
6552        assert_eq!(default["sessions"], 11);
6553        assert_eq!(default["model"], "anthropic/claude-sonnet-4-5");
6554
6555        let coder = profile_row(&response, "hermes", "coder");
6556        assert_eq!(coder["kind"], "hermes_profile");
6557        assert_eq!(coder["default"], false);
6558        assert_eq!(coder["routes"], 1, "gateway.profile_routes targets coder");
6559        assert_eq!(coder["sessions"], 1, "state.db profile_name = 'coder'");
6560        assert_eq!(coder["model"], "anthropic/claude-opus-4-8");
6561        assert!(coder["home"]
6562            .as_str()
6563            .unwrap()
6564            .ends_with("hermes_home/profiles/coder"));
6565
6566        let main = profile_row(&response, "openclaw", "main");
6567        assert_eq!(main["kind"], "openclaw_agent");
6568        // No entry declares `default: true` (real configs do not), so `main`
6569        // wins on OpenClaw's own convention rather than alphabetically.
6570        assert_eq!(main["default"], true);
6571        assert_eq!(main["routes"], 0);
6572        assert_eq!(main["sessions"], 4);
6573        assert_eq!(
6574            main["model"],
6575            Value::Null,
6576            "`agents.defaults.model` is an install default, not this agent's pin"
6577        );
6578
6579        let design = profile_row(&response, "openclaw", "design");
6580        assert_eq!(design["default"], false);
6581        assert_eq!(design["routes"], 1, "one binding names agentId `design`");
6582        assert_eq!(design["sessions"], 0);
6583        assert_eq!(design["model"], "anthropic/claude-opus-4-8");
6584
6585        let preset = profile_row(&response, "supercode", "supercode-default");
6586        assert_eq!(preset["kind"], "preset");
6587        assert_eq!(preset["default"], true);
6588        assert_eq!(preset["home"], Value::Null);
6589        assert_eq!(preset["routes"], Value::Null);
6590    }
6591
6592    /// Codex's own profiles are `[profiles.<name>]` tables, with the
6593    /// top-level `profile` key naming the default.
6594    #[test]
6595    fn profiles_list_reads_codex_profile_tables() {
6596        let codex_home = std::env::temp_dir().join(format!(
6597            "supercode-orch10-codex-{}-{}",
6598            std::process::id(),
6599            std::time::SystemTime::now()
6600                .duration_since(std::time::UNIX_EPOCH)
6601                .unwrap()
6602                .as_nanos()
6603        ));
6604        std::fs::create_dir_all(codex_home.join("sessions")).unwrap();
6605        std::fs::write(
6606            codex_home.join("config.toml"),
6607            "profile = \"review\"\n\n[profiles.review]\nmodel = \"gpt-5.1-codex\"\n\n[profiles.fast]\nmodel = \"gpt-5.1-codex-mini\"\n",
6608        )
6609        .unwrap();
6610
6611        let mut service = HarnessSessionService::new();
6612        let response = service.handle(request(
6613            1,
6614            "harness.v1.profiles.list",
6615            json!({"harness": "codex", "homes": {"codex": codex_home.join("sessions")}}),
6616        ));
6617        let rows = response["result"]["profiles"].as_array().unwrap();
6618        assert_eq!(rows.len(), 2, "{response}");
6619        let review = profile_row(&response, "codex", "review");
6620        assert_eq!(review["kind"], "codex_profile");
6621        assert_eq!(review["default"], true);
6622        assert_eq!(review["model"], "gpt-5.1-codex");
6623        assert_eq!(review["home"], Value::Null);
6624        assert_eq!(profile_row(&response, "codex", "fast")["default"], false);
6625
6626        let got = service.handle(request(
6627            2,
6628            "harness.v1.profiles.get",
6629            json!({
6630                "harness": "codex",
6631                "name": "fast",
6632                "homes": {"codex": codex_home.join("sessions")},
6633            }),
6634        ));
6635        assert_eq!(got["result"]["profile"]["model"], "gpt-5.1-codex-mini");
6636        std::fs::remove_dir_all(&codex_home).ok();
6637    }
6638
6639    /// A verb a harness lacks fails with `UnsupportedAction`, never a silent
6640    /// empty list; an unknown name is an invalid argument, not an empty row.
6641    #[test]
6642    fn profiles_refuse_harnesses_without_the_concept() {
6643        let mut service = HarnessSessionService::new();
6644        let response = service.handle(request(
6645            1,
6646            "harness.v1.profiles.list",
6647            json!({"harness": "claude-code"}),
6648        ));
6649        assert_eq!(response["error"]["code"], -32020, "{response}");
6650
6651        let missing = service.handle(request(
6652            2,
6653            "harness.v1.profiles.get",
6654            json!({
6655                "harness": "hermes",
6656                "name": "no-such-profile",
6657                "homes": profile_fixture_homes(),
6658            }),
6659        ));
6660        assert_eq!(missing["error"]["code"], -32602, "{missing}");
6661    }
6662
6663    /// The two methods are advertised, so a client discovers them from
6664    /// `harness.v1.capabilities` rather than from documentation.
6665    #[test]
6666    fn profiles_methods_are_advertised() {
6667        let mut service = HarnessSessionService::new();
6668        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6669        let methods = response["result"]["methods"].as_array().unwrap();
6670        for method in ["harness.v1.profiles.list", "harness.v1.profiles.get"] {
6671            assert!(
6672                methods.iter().any(|entry| entry == method),
6673                "{method} is not advertised"
6674            );
6675        }
6676    }
6677
6678    // -----------------------------------------------------------------
6679    // ORCH-14 — channels
6680    // -----------------------------------------------------------------
6681
6682    fn channel_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6683        response["result"]["channels"]
6684            .as_array()
6685            .unwrap_or_else(|| panic!("no channels array in {response}"))
6686            .iter()
6687            .find(|row| row["harness"] == harness && row["name"] == name)
6688            .unwrap_or_else(|| panic!("no `{harness}` channel `{name}` in {response}"))
6689    }
6690
6691    fn channels_list(harness: Option<&str>) -> Value {
6692        let mut params = json!({"homes": profile_fixture_homes()});
6693        if let Some(harness) = harness {
6694            params["harness"] = json!(harness);
6695        }
6696        HarnessSessionService::new().handle(request(1, "harness.v1.channels.list", params))
6697    }
6698
6699    /// dev/01: both sources answer in one row shape over the committed
6700    /// fixture homes — Hermes's `platforms:` blocks with their `extra` maps,
6701    /// and OpenClaw's `channels.<name>` entries split per account.
6702    #[test]
6703    fn channels_list_reads_both_gateway_harnesses_uniformly() {
6704        let response = channels_list(None);
6705        assert_eq!(
6706            response["result"]["schema"],
6707            crate::channels::CHANNELS_SCHEMA
6708        );
6709
6710        // Hermes: a credentialed platform, a bridged `extra.key` platform,
6711        // and one the config explicitly disables.
6712        let telegram = channel_row(&response, "hermes", "telegram");
6713        assert_eq!(telegram["kind"], "telegram");
6714        assert_eq!(telegram["enabled"], true);
6715        assert_eq!(telegram["configured"], true);
6716        // The `sessions` count is the discovery rows whose surface platform
6717        // is telegram: the fixture's `agent:main:telegram:…` DM and the
6718        // `agent:coder:telegram:…` group.
6719        assert_eq!(telegram["sessions"], 2);
6720        let api = channel_row(&response, "hermes", "api_server");
6721        assert_eq!(api["configured"], true, "extra.key is a credential key");
6722        assert_eq!(api["sessions"], 0);
6723        let webhook = channel_row(&response, "hermes", "webhook");
6724        assert_eq!(webhook["enabled"], false);
6725        // Hermes lists no credential for `webhook`: declaring it is all it
6726        // needs, so a credential-less entry is still `configured`.
6727        assert_eq!(webhook["configured"], true);
6728
6729        // OpenClaw: one row per account, named `<channel>/<accountId>`.
6730        let linked = channel_row(&response, "openclaw", "slack/T0FIXTURE");
6731        assert_eq!(linked["kind"], "slack");
6732        assert_eq!(linked["account"], "T0FIXTURE");
6733        assert_eq!(linked["enabled"], true);
6734        assert_eq!(linked["configured"], true);
6735        let unlinked = channel_row(&response, "openclaw", "slack/T1FIXTURE");
6736        assert_eq!(unlinked["enabled"], false);
6737        assert_eq!(
6738            unlinked["configured"], false,
6739            "an account with no credential key is not configured"
6740        );
6741        // A single-account channel keeps its own name and names its account
6742        // inline.
6743        let telegram = channel_row(&response, "openclaw", "telegram");
6744        assert_eq!(telegram["account"], "hermes-fixture-bot");
6745        assert_eq!(telegram["configured"], true);
6746
6747        // `status` is never claimed from a config file.
6748        for row in response["result"]["channels"].as_array().unwrap() {
6749            assert_eq!(row["status"], "unknown", "{row}");
6750        }
6751    }
6752
6753    /// dev/01: no field of any emitted row carries a credential. The fixture
6754    /// homes hold four FAKE credential strings; a row that leaked one — as a
6755    /// value, an account label, or a name — fails here.
6756    #[test]
6757    fn channels_rows_never_carry_a_fixture_secret() {
6758        let secrets = [
6759            "FAKE-TOKEN-DO-NOT-EMIT",
6760            "FAKE-API-SERVER-KEY-DO-NOT-EMIT",
6761            "FAKE-SLACK-BOT-TOKEN-DO-NOT-EMIT",
6762            "FAKE-SLACK-APP-TOKEN-DO-NOT-EMIT",
6763            "FAKE-TELEGRAM-TOKEN-DO-NOT-EMIT",
6764        ];
6765        // The strings really are in the fixtures, so this test can fail.
6766        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6767        let raw = format!(
6768            "{}{}",
6769            std::fs::read_to_string(fixtures.join("hermes_home/config.yaml")).unwrap(),
6770            std::fs::read_to_string(fixtures.join("openclaw_home/openclaw.json")).unwrap(),
6771        );
6772        for secret in secrets {
6773            assert!(raw.contains(secret), "fixture no longer holds `{secret}`");
6774        }
6775
6776        let emitted = serde_json::to_string(&channels_list(None)["result"]).unwrap();
6777        for secret in secrets {
6778            assert!(
6779                !emitted.contains(secret),
6780                "`{secret}` leaked into a channel row: {emitted}"
6781            );
6782        }
6783        // Belt and braces: no row FIELD is credential-shaped either, so a
6784        // future field cannot smuggle one past the literal scan.
6785        for row in channels_list(None)["result"]["channels"]
6786            .as_array()
6787            .unwrap()
6788        {
6789            for key in row.as_object().unwrap().keys() {
6790                let key = key.to_ascii_lowercase();
6791                assert!(
6792                    !["token", "key", "secret", "password", "credential"]
6793                        .iter()
6794                        .any(|marker| key.ends_with(marker)),
6795                    "`{key}` is a credential-shaped field on a channel row"
6796                );
6797            }
6798        }
6799    }
6800
6801    /// `status` answers one row by name, and refuses an unknown one.
6802    #[test]
6803    fn channels_status_reads_one_row_by_name() {
6804        let mut service = HarnessSessionService::new();
6805        let got = service.handle(request(
6806            1,
6807            "harness.v1.channels.status",
6808            json!({
6809                "harness": "openclaw",
6810                "name": "slack/T0FIXTURE",
6811                "homes": profile_fixture_homes(),
6812            }),
6813        ));
6814        assert_eq!(got["result"]["channel"]["kind"], "slack");
6815        assert_eq!(got["result"]["channel"]["account"], "T0FIXTURE");
6816        assert_eq!(got["result"]["channel"]["status"], "unknown");
6817
6818        let missing = service.handle(request(
6819            2,
6820            "harness.v1.channels.status",
6821            json!({
6822                "harness": "openclaw",
6823                "name": "no-such-channel",
6824                "homes": profile_fixture_homes(),
6825            }),
6826        ));
6827        assert_eq!(missing["error"]["code"], -32602, "{missing}");
6828    }
6829
6830    /// A harness with no channel concept fails with `UnsupportedAction`,
6831    /// never a silent empty list — Claude Code included, because its channels
6832    /// are MCP-protocol declarations no config file names.
6833    #[test]
6834    fn channels_refuse_harnesses_without_the_concept() {
6835        let response = channels_list(Some("claude-code"));
6836        assert_eq!(response["error"]["code"], -32020, "{response}");
6837        let codex = channels_list(Some("codex"));
6838        assert_eq!(codex["error"]["code"], -32020, "{codex}");
6839    }
6840
6841    /// The harness filter restricts the rows rather than being ignored.
6842    #[test]
6843    fn channels_list_filters_by_harness() {
6844        let response = channels_list(Some("openclaw"));
6845        let rows = response["result"]["channels"].as_array().unwrap();
6846        assert!(!rows.is_empty(), "{response}");
6847        assert!(
6848            rows.iter().all(|row| row["harness"] == "openclaw"),
6849            "harness filter leaked: {response}"
6850        );
6851    }
6852
6853    /// Both methods are advertised, so a client discovers them from
6854    /// `harness.v1.capabilities` rather than from documentation.
6855    #[test]
6856    fn channels_methods_are_advertised() {
6857        let mut service = HarnessSessionService::new();
6858        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6859        let methods = response["result"]["methods"].as_array().unwrap();
6860        for method in ["harness.v1.channels.list", "harness.v1.channels.status"] {
6861            assert!(
6862                methods.iter().any(|entry| entry == method),
6863                "{method} is not advertised"
6864            );
6865        }
6866    }
6867
6868    /// The UI story renders REAL rows: this writes the discovery response the
6869    /// two assertions above pin into the fixture the Storybook
6870    /// `Compositions/Universal nouns` stories import, and fails when the
6871    /// committed copy has drifted from what the service now answers.
6872    #[test]
6873    fn orch6_story_fixture_matches_the_live_discovery_response() {
6874        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6875            .join("../../sdk/ui/stories/fixtures/hermes-discovery.json");
6876        let mut result = hermes_discovery(hermes_query());
6877        // `updated_at_ms` is derived from the fixture's own stored timestamps,
6878        // so the whole response is deterministic; drop only the cursor, which
6879        // is pagination state rather than a session fact.
6880        result.as_object_mut().unwrap().remove("next_cursor");
6881        let rendered = format!("{}\n", serde_json::to_string_pretty(&result).unwrap());
6882        if std::env::var_os("SUPERCODE_UPDATE_FIXTURES").is_some() {
6883            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
6884            std::fs::write(&path, &rendered).unwrap();
6885        }
6886        let committed = std::fs::read_to_string(&path).unwrap_or_default();
6887        assert_eq!(
6888            committed, rendered,
6889            "sdk/ui/stories/fixtures/hermes-discovery.json is stale — \
6890             re-run with SUPERCODE_UPDATE_FIXTURES=1"
6891        );
6892    }
6893
6894    fn pi_locator() -> SessionLocator {
6895        SessionLocator {
6896            harness: HarnessId::from(HarnessId::PI),
6897            session_id: "1e6f2a3b-0000-4000-8000-000000000001".into(),
6898            storage: StorageLocator::File {
6899                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6900                    .join("tests/fixtures/pi_session.jsonl"),
6901            },
6902        }
6903    }
6904
6905    fn opencode_locator() -> SessionLocator {
6906        let session_id = "ses_fixtureAAAAAAAAAAAAAAA1";
6907        SessionLocator {
6908            harness: HarnessId::from(HarnessId::OPENCODE),
6909            session_id: session_id.into(),
6910            storage: StorageLocator::Sqlite {
6911                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6912                    .join("tests/fixtures/opencode_fixture/opencode.db"),
6913                selector: session_id.into(),
6914            },
6915        }
6916    }
6917
6918    fn grok_locator() -> SessionLocator {
6919        SessionLocator {
6920            harness: HarnessId::from(HarnessId::GROK),
6921            session_id: "73c09283-4b33-41fa-90f1-0bcb0f7be523".into(),
6922            storage: StorageLocator::File {
6923                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6924                    .join("tests/fixtures/grok_session/chat_history.jsonl"),
6925            },
6926        }
6927    }
6928
6929    // ---- ORCH-11: `harness.v1.skills.list` -------------------------------
6930
6931    fn fixture_homes() -> Value {
6932        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6933        json!({
6934            "claude_code": fixtures.join("__absent__"),
6935            "codex": fixtures.join("__absent__"),
6936            "opencode": fixtures.join("__absent__"),
6937            "pi": fixtures.join("__absent__"),
6938            "agents": fixtures.join("__absent__"),
6939            "hermes": fixtures.join("hermes_home"),
6940            "openclaw": fixtures.join("openclaw_home"),
6941        })
6942    }
6943
6944    #[test]
6945    fn preview_search_uses_the_discovery_rpc_and_refuses_live_subscription() {
6946        let root = std::env::temp_dir().join(format!(
6947            "supercode-preview-rpc-{}-{}",
6948            std::process::id(),
6949            std::time::SystemTime::now()
6950                .duration_since(std::time::UNIX_EPOCH)
6951                .unwrap()
6952                .as_nanos()
6953        ));
6954        std::fs::create_dir_all(&root).unwrap();
6955        for id in ["first", "second"] {
6956            std::fs::write(root.join(format!("{id}.jsonl")), format!("{}\n{}\n",
6957                json!({"type": "session_meta", "payload": {"id": id, "cwd": "/workspace"}}),
6958                json!({"type": "event_msg", "payload": {"type": "agent_message", "message": "NEBULA result"}}),
6959            )).unwrap();
6960        }
6961        let mut service = HarnessSessionService::new();
6962        let query = json!({
6963            "harnesses": ["codex"], "homes": {"codex": root},
6964            "query": "nebula", "search_previews": true, "limit": 1
6965        });
6966        let first = service.handle(request(1, "harness.v1.sessions.discover", query.clone()));
6967        assert!(first.get("error").is_none(), "{first}");
6968        assert_eq!(first["result"]["receipt"]["searched_previews"], true);
6969        assert_eq!(first["result"]["receipt"]["total_matched"], 2);
6970        let mut next_query = query.clone();
6971        next_query["cursor"] = first["result"]["next_cursor"].clone();
6972        let next = service.handle(request(2, "harness.v1.sessions.discover", next_query));
6973        assert_eq!(next["result"]["receipt"]["returned"], 1);
6974        assert_eq!(next["result"]["receipt"]["total_matched"], 2);
6975        assert_eq!(next["result"]["receipt"]["truncated"], false);
6976        assert_ne!(
6977            first["result"]["sessions"][0]["locator"],
6978            next["result"]["sessions"][0]["locator"]
6979        );
6980        let refused = service.handle(request(3, "harness.v1.sessions.index.subscribe", query));
6981        assert!(
6982            refused["error"]["message"]
6983                .as_str()
6984                .unwrap()
6985                .contains("use sessions.discover"),
6986            "{refused}"
6987        );
6988        std::fs::remove_dir_all(root).unwrap();
6989    }
6990
6991    #[test]
6992    fn session_index_resize_preserves_subscription_and_rejects_invalid_requests() {
6993        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.sessions.index.resize"));
6994        let root = std::env::temp_dir().join(format!(
6995            "supercode-index-rpc-{}-{}",
6996            std::process::id(),
6997            std::time::SystemTime::now()
6998                .duration_since(std::time::UNIX_EPOCH)
6999                .unwrap()
7000                .as_nanos()
7001        ));
7002        std::fs::create_dir_all(&root).unwrap();
7003        for id in ["first", "second"] {
7004            std::fs::write(root.join(format!("{id}.jsonl")), format!(
7005                "{{\"type\":\"session_meta\",\"payload\":{{\"id\":\"{id}\",\"cwd\":\"/workspace\"}}}}\n"
7006            )).unwrap();
7007        }
7008        let mut service = HarnessSessionService::new();
7009        let opened = service.handle(request(
7010            1,
7011            "harness.v1.sessions.index.subscribe",
7012            json!({
7013                "harnesses": ["codex"], "homes": { "codex": root }, "limit": 1
7014            }),
7015        ));
7016        assert!(opened.get("error").is_none(), "{opened:#}");
7017        let subscription = opened["result"]["subscription"]
7018            .as_str()
7019            .unwrap()
7020            .to_owned();
7021        assert_eq!(opened["result"]["initial"].as_array().unwrap().len(), 1);
7022        for params in [
7023            json!({"subscription": subscription, "limit": 0}),
7024            json!({"subscription": subscription, "limit": 2049}),
7025            json!({"subscription": subscription, "limit": 2, "cursor": "not-allowed"}),
7026            json!({"subscription": "unknown", "limit": 2}),
7027        ] {
7028            let rejected = service.handle(request(2, "harness.v1.sessions.index.resize", params));
7029            assert_eq!(rejected["error"]["code"], -32602, "{rejected:#}");
7030        }
7031        for (limit, revision) in [(1, 1), (2, 2), (2, 2), (1, 3)] {
7032            let response = service.handle(request(
7033                3,
7034                "harness.v1.sessions.index.resize",
7035                json!({
7036                    "subscription": subscription, "limit": limit
7037                }),
7038            ));
7039            assert!(response.get("error").is_none(), "{response:#}");
7040            assert_eq!(response["result"]["subscription"], subscription);
7041            assert_eq!(response["result"]["revision"], revision);
7042            assert_eq!(
7043                response["result"]["initial"].as_array().unwrap().len(),
7044                limit
7045            );
7046            assert_eq!(response["result"]["receipt"]["total_matched"], 2);
7047            assert_eq!(service.index_subscriptions.len(), 1);
7048        }
7049        let removed = service.handle(request(
7050            4,
7051            "harness.v1.sessions.index.unsubscribe",
7052            json!({
7053                "subscription": subscription
7054            }),
7055        ));
7056        assert_eq!(removed["result"]["removed"], true);
7057        let stale = service.handle(request(
7058            5,
7059            "harness.v1.sessions.index.resize",
7060            json!({
7061                "subscription": subscription, "limit": 1
7062            }),
7063        ));
7064        assert_eq!(stale["error"]["code"], -32602);
7065        drop(service);
7066        std::fs::remove_dir_all(root).unwrap();
7067    }
7068
7069    fn skills_rows(params: Value) -> Vec<Value> {
7070        let response =
7071            HarnessSessionService::new().handle(request(1, "harness.v1.skills.list", params));
7072        assert!(response.get("error").is_none(), "{response:#}");
7073        response["result"].as_array().cloned().unwrap_or_default()
7074    }
7075
7076    /// The uniform row over two harnesses at once, from the harnesses' own
7077    /// skill roots: name, harness, scope, location, description, version.
7078    #[test]
7079    fn skills_list_reads_the_hermes_and_openclaw_roots() {
7080        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7081        let rows = skills_rows(json!({
7082            "homes": fixture_homes(),
7083            "cwd": fixtures.join("hermes_home"),
7084        }));
7085        let arxiv = rows
7086            .iter()
7087            .find(|row| row["name"] == json!("arxiv-search"))
7088            .unwrap_or_else(|| panic!("no arxiv row in {rows:#?}"));
7089        assert_eq!(arxiv["harness"], json!(HarnessId::HERMES));
7090        assert_eq!(arxiv["scope"], json!("user"));
7091        assert_eq!(arxiv["version"], json!("1.4.0"));
7092        assert!(arxiv["location"]
7093            .as_str()
7094            .unwrap()
7095            .ends_with("hermes_home/skills/research/arxiv"));
7096
7097        // A directory with no SKILL.md still lists, by directory name.
7098        let bare = rows
7099            .iter()
7100            .find(|row| row["name"] == json!("bare-skill"))
7101            .unwrap_or_else(|| panic!("no bare-skill row in {rows:#?}"));
7102        assert_eq!(bare["enabled"], json!(null));
7103        assert!(bare.get("description").is_none());
7104
7105        let demo = rows
7106            .iter()
7107            .find(|row| row["name"] == json!("clawhub-demo"))
7108            .unwrap_or_else(|| panic!("no clawhub-demo row in {rows:#?}"));
7109        assert_eq!(demo["harness"], json!(HarnessId::OPENCLAW));
7110        assert_eq!(demo["scope"], json!("managed"));
7111        assert_eq!(demo["enabled"], json!(false));
7112    }
7113
7114    /// Both filters select against the same rows.
7115    #[test]
7116    fn skills_list_filters_by_harness_and_scope() {
7117        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7118        let hermes = skills_rows(json!({
7119            "homes": fixture_homes(),
7120            "cwd": fixtures.join("hermes_home"),
7121            "harness": HarnessId::HERMES,
7122        }));
7123        assert!(!hermes.is_empty());
7124        assert!(hermes
7125            .iter()
7126            .all(|row| row["harness"] == json!(HarnessId::HERMES)));
7127
7128        let managed = skills_rows(json!({
7129            "homes": fixture_homes(),
7130            "cwd": fixtures.join("openclaw_home"),
7131            "harness": HarnessId::OPENCLAW,
7132            "scope": "managed",
7133        }));
7134        assert_eq!(managed.len(), 1, "{managed:#?}");
7135        assert_eq!(managed[0]["name"], json!("clawhub-demo"));
7136
7137        let bundled = skills_rows(json!({
7138            "homes": fixture_homes(),
7139            "cwd": fixtures.join("openclaw_home"),
7140            "harness": HarnessId::OPENCLAW,
7141            "scope": "bundled",
7142        }));
7143        assert!(bundled.is_empty(), "{bundled:#?}");
7144    }
7145
7146    /// A harness supercode has no skills root for is refused by name, not
7147    /// answered with an empty list.
7148    #[test]
7149    fn skills_list_refuses_an_unknown_harness() {
7150        let response = HarnessSessionService::new().handle(request(
7151            1,
7152            "harness.v1.skills.list",
7153            json!({"harness": "not-a-harness", "homes": fixture_homes()}),
7154        ));
7155        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7156        assert!(response["error"]["message"]
7157            .as_str()
7158            .unwrap()
7159            .contains("not-a-harness"));
7160    }
7161
7162    /// The method is advertised, and its SDK operation resolves it.
7163    #[test]
7164    fn skills_list_is_an_advertised_method_and_sdk_operation() {
7165        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.list"));
7166        assert_eq!(
7167            SdkOperation::from_method("harness.v1.skills.list"),
7168            Some(SdkOperation::SkillsList)
7169        );
7170    }
7171
7172    // ---- ORCH-22: `harness.v1.skills.install|remove` ----------------------
7173
7174    /// Both controlled verbs are advertised and resolve to their operation.
7175    #[test]
7176    fn skills_install_and_remove_are_advertised_methods_and_sdk_operations() {
7177        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.install"));
7178        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.remove"));
7179        assert_eq!(
7180            SdkOperation::from_method("harness.v1.skills.install"),
7181            Some(SdkOperation::SkillsInstall)
7182        );
7183        assert_eq!(
7184            SdkOperation::from_method("harness.v1.skills.remove"),
7185            Some(SdkOperation::SkillsRemove)
7186        );
7187    }
7188
7189    /// The directory door, end to end over the RPC: a local package lands in
7190    /// Claude Code's own user root and the outcome carries the operation and
7191    /// the row the ORCH-11 loader reads back.
7192    #[test]
7193    fn skills_install_and_remove_drive_the_directory_door() {
7194        let root = std::env::temp_dir().join(format!(
7195            "supercode-orch22-rpc-{}-{}",
7196            std::process::id(),
7197            std::time::SystemTime::now()
7198                .duration_since(std::time::UNIX_EPOCH)
7199                .unwrap()
7200                .as_nanos()
7201        ));
7202        let source = root.join("probe-src");
7203        std::fs::create_dir_all(&source).unwrap();
7204        std::fs::write(
7205            source.join("SKILL.md"),
7206            "---\nname: orch22-rpc\ndescription: a probe\n---\nbody\n",
7207        )
7208        .unwrap();
7209        let homes = json!({
7210            "claude_code": root.join("claude_home"),
7211            "codex": root.join("__absent__"),
7212            "opencode": root.join("__absent__"),
7213            "pi": root.join("__absent__"),
7214            "hermes": root.join("__absent__"),
7215            "openclaw": root.join("__absent__"),
7216            "agents": root.join("__absent__"),
7217        });
7218
7219        let mut service = HarnessSessionService::new();
7220        let installed = service.handle(request(
7221            1,
7222            "harness.v1.skills.install",
7223            json!({
7224                "harness": HarnessId::CLAUDE_CODE,
7225                "source": source,
7226                "scope": "user",
7227                "cwd": root,
7228                "homes": homes,
7229            }),
7230        ));
7231        let result = &installed["result"];
7232        assert_eq!(result["name"], json!("orch22-rpc"), "{installed:#}");
7233        assert_eq!(result["verb"], json!("install"));
7234        assert!(result["ran"]
7235            .as_str()
7236            .is_some_and(|ran| ran.starts_with("cp -R ")));
7237        assert_eq!(result["skill"]["scope"], json!("user"));
7238
7239        let removed = service.handle(request(
7240            2,
7241            "harness.v1.skills.remove",
7242            json!({
7243                "harness": HarnessId::CLAUDE_CODE,
7244                "name": "orch22-rpc",
7245                "scope": "user",
7246                "cwd": root,
7247                "homes": homes,
7248            }),
7249        ));
7250        assert_eq!(removed["result"]["removed"], json!(true), "{removed:#}");
7251        assert!(!root.join("claude_home/skills/orch22-rpc").exists());
7252        std::fs::remove_dir_all(&root).ok();
7253    }
7254
7255    /// OpenClaw publishes no `skills remove` at the pin, so the uniform verb
7256    /// refuses with UnsupportedAction instead of deleting files itself.
7257    #[test]
7258    fn skills_remove_refuses_openclaw_at_the_pin() {
7259        let response = HarnessSessionService::new().handle(request(
7260            1,
7261            "harness.v1.skills.remove",
7262            json!({"harness": HarnessId::OPENCLAW, "name": "clawhub-demo"}),
7263        ));
7264        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7265        assert!(response["error"]["message"]
7266            .as_str()
7267            .unwrap()
7268            .contains("no `skills remove` verb"));
7269    }
7270
7271    /// A harness with no skills root at all is refused by name, with the
7272    /// same sentence `skills.list` gives it.
7273    #[test]
7274    fn skills_install_refuses_a_harness_without_a_skills_root() {
7275        let response = HarnessSessionService::new().handle(request(
7276            1,
7277            "harness.v1.skills.install",
7278            json!({"harness": "not-a-harness", "source": "/tmp/x"}),
7279        ));
7280        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7281        assert!(response["error"]["message"]
7282            .as_str()
7283            .unwrap()
7284            .contains("not-a-harness"));
7285    }
7286
7287    // ---- ORCH-12: `harness.v1.memory.show|search` ------------------------
7288
7289    /// `HarnessHomes` for the committed fixture homes. Every root a test does
7290    /// not name is pinned at an absent path, so a read can never fall through
7291    /// to this machine's real harness homes. Note `hermes` is the `state.db`
7292    /// PATH (its parent is HERMES_HOME) and `claude_code` is the `projects`
7293    /// directory — the same contract discovery uses.
7294    fn memory_homes() -> Value {
7295        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7296        json!({
7297            "claude_code": fixtures.join("__absent__"),
7298            "codex": fixtures.join("__absent__"),
7299            "opencode": fixtures.join("__absent__"),
7300            "pi": fixtures.join("__absent__"),
7301            "grok": fixtures.join("__absent__"),
7302            "gemini": fixtures.join("__absent__"),
7303            "goose": fixtures.join("__absent__"),
7304            "supercode": fixtures.join("__absent__"),
7305            "hermes": fixtures.join("hermes_home/state.db"),
7306            "openclaw": fixtures.join("openclaw_home"),
7307        })
7308    }
7309
7310    fn memory_call_ok(method: &str, params: Value, key: &str) -> Vec<Value> {
7311        let response = HarnessSessionService::new().handle(request(1, method, params));
7312        assert!(response.get("error").is_none(), "{response:#}");
7313        assert_eq!(response["result"]["schema"], json!("supercode.memory.v1"));
7314        response["result"][key]
7315            .as_array()
7316            .cloned()
7317            .unwrap_or_default()
7318    }
7319
7320    fn memory_documents(params: Value) -> Vec<Value> {
7321        memory_call_ok("harness.v1.memory.show", params, "documents")
7322    }
7323
7324    fn memory_matches(params: Value) -> Vec<Value> {
7325        memory_call_ok("harness.v1.memory.search", params, "matches")
7326    }
7327
7328    fn find_document<'a>(rows: &'a [Value], profile: &str, name: &str) -> &'a Value {
7329        rows.iter()
7330            .find(|row| row["profile"] == profile && row["name"] == name)
7331            .unwrap_or_else(|| panic!("no `{profile}` document `{name}` in {rows:#?}"))
7332    }
7333
7334    /// Hermes: the built-in `MEMORY.md`/`USER.md` pair and the `memories/`
7335    /// topic files, for HERMES_HOME itself and for every profile home.
7336    #[test]
7337    fn memory_show_reads_the_hermes_profile_homes() {
7338        let rows = memory_documents(json!({"harness": "hermes", "homes": memory_homes()}));
7339
7340        let notes = find_document(&rows, "default", "MEMORY.md");
7341        assert_eq!(notes["harness"], "hermes");
7342        assert_eq!(notes["scope"], "user");
7343        assert!(notes["size"].as_u64().unwrap() > 0);
7344        assert!(notes["updated_at"].is_string(), "{notes:#?}");
7345        // The default answer previews the head and never the whole body.
7346        assert!(notes.get("content").is_none(), "{notes:#?}");
7347        assert_eq!(notes["truncated"], true);
7348        assert_eq!(notes["preview"].as_array().unwrap().len(), 5);
7349
7350        let user = find_document(&rows, "default", "USER.md");
7351        assert_eq!(user["scope"], "user");
7352        assert!(user["preview"]
7353            .as_array()
7354            .unwrap()
7355            .iter()
7356            .any(|line| line.as_str().unwrap().contains("neovim")));
7357
7358        let topic = find_document(&rows, "default", "memories/2026-09-01-notes.md");
7359        assert!(topic["path"]
7360            .as_str()
7361            .unwrap()
7362            .ends_with("hermes_home/memories/2026-09-01-notes.md"));
7363
7364        // Profile mode points HERMES_HOME at `<root>/profiles/<name>`.
7365        let coder = find_document(&rows, "coder", "MEMORY.md");
7366        assert_eq!(coder["scope"], "profile");
7367        assert!(coder["path"]
7368            .as_str()
7369            .unwrap()
7370            .ends_with("hermes_home/profiles/coder/MEMORY.md"));
7371    }
7372
7373    /// `full` is the only way a body crosses the wire, and `profile` narrows
7374    /// the read to one home.
7375    #[test]
7376    fn memory_show_returns_bodies_only_under_full_and_narrows_by_profile() {
7377        let rows = memory_documents(json!({
7378            "harness": "hermes",
7379            "profile": "coder",
7380            "full": true,
7381            "homes": memory_homes(),
7382        }));
7383        assert!(
7384            rows.iter().all(|row| row["profile"] == "coder"),
7385            "{rows:#?}"
7386        );
7387        let coder = find_document(&rows, "coder", "MEMORY.md");
7388        assert!(coder["content"]
7389            .as_str()
7390            .expect("full returns the body")
7391            .contains("anthropic/claude-opus-4-8"));
7392    }
7393
7394    /// OpenClaw: memory-core's files under each agent's workspace —
7395    /// `<state>/workspace` for the default agent, `<state>/workspace-<id>`
7396    /// for any other.
7397    #[test]
7398    fn memory_show_reads_the_openclaw_agent_workspaces() {
7399        let rows = memory_documents(json!({"harness": "openclaw", "homes": memory_homes()}));
7400
7401        let main = find_document(&rows, "main", "MEMORY.md");
7402        assert_eq!(main["scope"], "agent");
7403        assert!(main["path"]
7404            .as_str()
7405            .unwrap()
7406            .ends_with("openclaw_home/workspace/MEMORY.md"));
7407
7408        let topic = find_document(&rows, "main", "memory/2026-09-01-standup.md");
7409        assert!(topic["path"]
7410            .as_str()
7411            .unwrap()
7412            .ends_with("openclaw_home/workspace/memory/2026-09-01-standup.md"));
7413
7414        let design = find_document(&rows, "design", "MEMORY.md");
7415        assert!(design["path"]
7416            .as_str()
7417            .unwrap()
7418            .ends_with("openclaw_home/workspace-design/MEMORY.md"));
7419    }
7420
7421    /// Claude Code: the auto-memory directory of the project the working tree
7422    /// belongs to, keyed by the enclosing git repository.
7423    #[test]
7424    fn memory_show_reads_a_claude_code_project_auto_memory_directory() {
7425        let scratch = std::env::temp_dir().join(format!(
7426            "supercode-orch12-cc-{}-{}",
7427            std::process::id(),
7428            std::time::SystemTime::now()
7429                .duration_since(std::time::UNIX_EPOCH)
7430                .unwrap()
7431                .as_nanos()
7432        ));
7433        let project = scratch.join("repo");
7434        std::fs::create_dir_all(project.join(".git")).unwrap();
7435        // Auto-memory is shared across a repo's worktrees, so a nested
7436        // working directory must resolve to the repo's own project dir.
7437        let worktree = project.join("crates/harness");
7438        std::fs::create_dir_all(&worktree).unwrap();
7439        let slug: String = project
7440            .to_string_lossy()
7441            .chars()
7442            .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
7443            .collect();
7444        let projects = scratch.join("claude/projects");
7445        let memory = projects.join(&slug).join("memory");
7446        std::fs::create_dir_all(&memory).unwrap();
7447        std::fs::write(
7448            memory.join("MEMORY.md"),
7449            "# index\n- [build box](build-box.md) — the pinned harnesses\n",
7450        )
7451        .unwrap();
7452        std::fs::write(
7453            memory.join("build-box.md"),
7454            "hermes 0.21.0 and openclaw 2026.7.1-2 are the pins\n",
7455        )
7456        .unwrap();
7457
7458        let mut homes = memory_homes();
7459        homes["claude_code"] = json!(projects);
7460        let rows = memory_documents(json!({
7461            "harness": "claude-code",
7462            "cwd": worktree,
7463            "homes": homes,
7464        }));
7465        let index = find_document(&rows, &slug, "MEMORY.md");
7466        assert_eq!(index["harness"], "claude-code");
7467        assert_eq!(index["scope"], "project");
7468        let topic = find_document(&rows, &slug, "build-box.md");
7469        assert!(topic["preview"]
7470            .as_array()
7471            .unwrap()
7472            .iter()
7473            .any(|line| line.as_str().unwrap().contains("2026.7.1-2")));
7474
7475        let hits = memory_matches(json!({
7476            "harness": "claude-code",
7477            "query": "pinned harnesses",
7478            "cwd": worktree,
7479            "homes": homes,
7480        }));
7481        assert_eq!(hits.len(), 1, "{hits:#?}");
7482        assert_eq!(hits[0]["name"], "MEMORY.md");
7483        assert_eq!(hits[0]["line"], 2);
7484
7485        let _ = std::fs::remove_dir_all(&scratch);
7486    }
7487
7488    /// A config-less OpenClaw install declares no default agent, but
7489    /// memory-core still resolves ONE agent to the default `workspace`
7490    /// directory — the same `main`-then-first convention the profile rows
7491    /// use. Measured against `openclaw memory status` on the pinned CLI
7492    /// (`docs/interop/research/orch12-memory-receipt-2026-09-03.json`).
7493    #[test]
7494    fn memory_show_resolves_the_default_workspace_without_an_openclaw_config() {
7495        let state = std::env::temp_dir().join(format!(
7496            "supercode-orch12-oc-{}-{}",
7497            std::process::id(),
7498            std::time::SystemTime::now()
7499                .duration_since(std::time::UNIX_EPOCH)
7500                .unwrap()
7501                .as_nanos()
7502        ));
7503        // No `openclaw.json`: only the agent home the gateway creates.
7504        std::fs::create_dir_all(state.join("agents/main/agent")).unwrap();
7505        std::fs::create_dir_all(state.join("workspace")).unwrap();
7506        std::fs::write(
7507            state.join("workspace/MEMORY.md"),
7508            "the gateway websocket needs credentials\n",
7509        )
7510        .unwrap();
7511
7512        let mut homes = memory_homes();
7513        homes["openclaw"] = json!(state);
7514        let rows = memory_documents(json!({"harness": "openclaw", "homes": homes}));
7515        assert_eq!(rows.len(), 1, "{rows:#?}");
7516        let row = find_document(&rows, "main", "MEMORY.md");
7517        assert_eq!(row["scope"], "agent");
7518        assert!(row["path"]
7519            .as_str()
7520            .unwrap()
7521            .ends_with("workspace/MEMORY.md"));
7522
7523        let _ = std::fs::remove_dir_all(&state);
7524    }
7525
7526    /// Search is a plain scan over the same documents: a hit carries the
7527    /// path, line and excerpt; a miss is an empty list, not an error.
7528    #[test]
7529    fn memory_search_reports_hits_by_line_and_misses_as_empty() {
7530        let hit = memory_matches(json!({
7531            "harness": "hermes",
7532            "query": "NEOVIM",
7533            "homes": memory_homes(),
7534        }));
7535        assert_eq!(hit.len(), 1, "{hit:#?}");
7536        assert_eq!(hit[0]["harness"], "hermes");
7537        assert_eq!(hit[0]["name"], "USER.md");
7538        assert_eq!(hit[0]["scope"], "user");
7539        assert_eq!(hit[0]["line"], 5);
7540        assert!(hit[0]["excerpt"].as_str().unwrap().contains("neovim"));
7541
7542        // A regular expression reaches the same lines.
7543        let regex = memory_matches(json!({
7544            "harness": "hermes",
7545            "query": "neo(vim|vi)",
7546            "regex": true,
7547            "homes": memory_homes(),
7548        }));
7549        assert_eq!(regex.len(), 1, "{regex:#?}");
7550
7551        let miss = memory_matches(json!({
7552            "harness": "hermes",
7553            "query": "no-memory-line-says-this",
7554            "homes": memory_homes(),
7555        }));
7556        assert!(miss.is_empty(), "{miss:#?}");
7557    }
7558
7559    /// The uniform-verb contract: a harness with no memory store at the pin
7560    /// is refused by name, and `session` only selects a Claude Code project.
7561    #[test]
7562    fn memory_refuses_harnesses_without_a_store_and_misplaced_session_scoping() {
7563        for method in ["harness.v1.memory.show", "harness.v1.memory.search"] {
7564            let response = HarnessSessionService::new().handle(request(
7565                1,
7566                method,
7567                json!({"harness": "codex", "query": "anything", "homes": memory_homes()}),
7568            ));
7569            assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7570            assert!(response["error"]["message"]
7571                .as_str()
7572                .unwrap()
7573                .contains("codex"));
7574        }
7575
7576        let response = HarnessSessionService::new().handle(request(
7577            1,
7578            "harness.v1.memory.show",
7579            json!({"harness": "hermes", "session": "abc", "homes": memory_homes()}),
7580        ));
7581        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7582
7583        // `harness` is not optional: memory documents are the user's prose.
7584        let response = HarnessSessionService::new().handle(request(
7585            1,
7586            "harness.v1.memory.show",
7587            json!({"homes": memory_homes()}),
7588        ));
7589        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7590    }
7591
7592    /// Both methods are advertised, and their SDK operations resolve them.
7593    #[test]
7594    fn memory_methods_are_advertised_and_map_to_sdk_operations() {
7595        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.show"));
7596        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.search"));
7597        assert_eq!(
7598            SdkOperation::from_method("harness.v1.memory.show"),
7599            Some(SdkOperation::MemoryShow)
7600        );
7601        assert_eq!(
7602            SdkOperation::from_method("harness.v1.memory.search"),
7603            Some(SdkOperation::MemorySearch)
7604        );
7605    }
7606
7607    // ---- ORCH-9: `harness.v1.approvals.list` -----------------------------
7608
7609    /// A runtime that raises one protocol request and then goes quiet, so a
7610    /// single poll delivers the request without closing the connection.
7611    struct RequestingRuntime {
7612        handle: RuntimeHandle,
7613        events: std::collections::VecDeque<HarnessEvent>,
7614        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7615    }
7616
7617    #[async_trait]
7618    impl RuntimeConnection for RequestingRuntime {
7619        fn handle(&self) -> &RuntimeHandle {
7620            &self.handle
7621        }
7622
7623        async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
7624            unreachable!("this runtime only raises requests")
7625        }
7626
7627        async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
7628            match self.events.pop_front() {
7629                Some(event) => Ok(Some(event)),
7630                // Quiet, not closed: `poll_sdk_events` times out and leaves
7631                // the connection open, the way a runtime blocked on a
7632                // permission request behaves.
7633                None => std::future::pending().await,
7634            }
7635        }
7636
7637        async fn interrupt(&mut self) -> crate::Result<()> {
7638            Ok(())
7639        }
7640
7641        async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
7642            // Both halves are recorded: ORCH-20 has to prove not just that the
7643            // right request was answered but that the door received its own
7644            // reply envelope.
7645            self.answered
7646                .lock()
7647                .unwrap_or_else(std::sync::PoisonError::into_inner)
7648                .push(json!({"request_id": request_id, "response": response}));
7649            Ok(())
7650        }
7651
7652        async fn close(&mut self) -> crate::Result<()> {
7653            Ok(())
7654        }
7655    }
7656
7657    fn requesting_runtime(
7658        harness: &str,
7659        events: Vec<HarnessEvent>,
7660        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7661    ) -> Box<dyn RuntimeConnection> {
7662        requesting_runtime_named(harness, "hermes-live-session", events, answered)
7663    }
7664
7665    fn requesting_runtime_named(
7666        harness: &str,
7667        runtime_id: &str,
7668        events: Vec<HarnessEvent>,
7669        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7670    ) -> Box<dyn RuntimeConnection> {
7671        Box::new(RequestingRuntime {
7672            handle: RuntimeHandle {
7673                harness: HarnessId::from(harness),
7674                runtime_id: runtime_id.into(),
7675                endpoint: RuntimeEndpoint::LocalProcess {
7676                    pid: None,
7677                    command: vec!["hermes-acp".into()],
7678                    protocol: "acp".into(),
7679                },
7680            },
7681            events: events.into(),
7682            answered,
7683        })
7684    }
7685
7686    fn permission_event(id: u64, title: &str) -> HarnessEvent {
7687        HarnessEvent {
7688            sequence: None,
7689            kind: "session/request_permission".into(),
7690            payload: json!({
7691                "jsonrpc": "2.0",
7692                "id": id,
7693                "method": "session/request_permission",
7694                "params": {
7695                    "sessionId": "hermes-live-session",
7696                    "toolCall": {"toolCallId": "call-1", "title": title, "kind": "execute"},
7697                    "options": [
7698                        {"optionId": "allow_once", "name": "Allow once", "kind": "allow_once"},
7699                        {"optionId": "allow_for_session", "name": "Allow for session", "kind": "allow_always"},
7700                        {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
7701                    ],
7702                },
7703            }),
7704        }
7705    }
7706
7707    fn approvals(service: &mut HarnessSessionService, params: Value) -> Value {
7708        let response = service.handle(request(1, "harness.v1.approvals.list", params));
7709        assert!(response.get("error").is_none(), "{response:#}");
7710        response["result"].clone()
7711    }
7712
7713    /// ORC-2 dev/01: the same uniform loop over the CLAUDE CODE door. The
7714    /// `can_use_tool` control request the CLI raises to its registered
7715    /// permission handler lists as one pending row, `approvals.resolve <id>
7716    /// allow_once` sends the `{behavior}` result the CLI accepts through
7717    /// `runtimes.respond`, and the row is gone. The frame is the one claude
7718    /// 2.1.258 wrote, transcribed from
7719    /// `docs/interop/research/orc2-claude-respond-receipt-2026-09-04.json`.
7720    #[tokio::test]
7721    async fn a_claude_code_permission_request_lists_and_resolves_on_the_uniform_door() {
7722        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7723        let mut service = HarnessSessionService::new();
7724        service.runtimes.insert(
7725            "runtime-cc".into(),
7726            requesting_runtime_named(
7727                HarnessId::CLAUDE_CODE,
7728                "claude-live-session",
7729                vec![HarnessEvent {
7730                    sequence: None,
7731                    kind: "control_request".into(),
7732                    payload: json!({
7733                        "type": "control_request",
7734                        "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7735                        "request": {
7736                            "subtype": "can_use_tool",
7737                            "tool_name": "Bash",
7738                            "display_name": "Bash",
7739                            "input": {"command": "touch probe-artifact.txt"},
7740                            "tool_use_id": "toolu_mock_1",
7741                        },
7742                    }),
7743                }],
7744                answered.clone(),
7745            ),
7746        );
7747
7748        let notifications = service.poll_runtimes().await;
7749        assert_eq!(notifications.len(), 1, "{notifications:#?}");
7750
7751        let rows = approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}));
7752        assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7753        let row = &rows[0];
7754        assert_eq!(row["id"], "runtime-cc/053f8a2d-3445-4011-a259-4261b31c7326");
7755        assert_eq!(row["harness"], HarnessId::CLAUDE_CODE);
7756        assert_eq!(row["status"], "pending");
7757        assert_eq!(row["subject"], "Bash touch probe-artifact.txt");
7758        assert_eq!(row["runtime_id"], "claude-live-session");
7759        assert_eq!(
7760            row["options"]
7761                .as_array()
7762                .unwrap()
7763                .iter()
7764                .map(|option| option["id"].as_str().unwrap())
7765                .collect::<Vec<_>>(),
7766            vec!["allow", "deny"],
7767        );
7768
7769        let response = resolve(
7770            &mut service,
7771            json!({"id": row["id"], "decision": "allow_once"}),
7772        )
7773        .await;
7774        assert!(response.get("error").is_none(), "{response:#}");
7775        assert_eq!(response["result"]["option_id"], "allow");
7776        assert_eq!(
7777            answered
7778                .lock()
7779                .unwrap_or_else(std::sync::PoisonError::into_inner)
7780                .as_slice(),
7781            &[json!({
7782                "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7783                "response": {"behavior": "allow"},
7784            })],
7785        );
7786        assert_eq!(
7787            approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}))
7788                .as_array()
7789                .map(Vec::len),
7790            Some(0),
7791        );
7792    }
7793
7794    /// dev/01: a live ACP permission request raised on a driven runtime is
7795    /// listable while the turn is blocked on it, and stops being listable
7796    /// the moment `runtimes.respond` answers it.
7797    #[tokio::test]
7798    async fn a_live_permission_request_lists_until_it_is_answered() {
7799        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7800        let mut service = HarnessSessionService::new();
7801        service.runtimes.insert(
7802            "runtime-1".into(),
7803            requesting_runtime(
7804                HarnessId::HERMES,
7805                vec![permission_event(7, "rm -rf build")],
7806                answered.clone(),
7807            ),
7808        );
7809
7810        let notifications = service.poll_runtimes().await;
7811        assert_eq!(notifications.len(), 1, "{notifications:#?}");
7812
7813        let rows = approvals(&mut service, json!({}));
7814        assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7815        let row = &rows[0];
7816        assert_eq!(row["id"], "runtime-1/7");
7817        assert_eq!(row["harness"], HarnessId::HERMES);
7818        assert_eq!(row["kind"], "live");
7819        assert_eq!(row["status"], "pending");
7820        assert_eq!(row["subject"], "rm -rf build");
7821        assert_eq!(row["session_id"], "hermes-live-session");
7822        assert_eq!(row["runtime_id"], "hermes-live-session");
7823        assert!(row["requested_at_ms"].as_i64().is_some(), "{row:#}");
7824        assert!(
7825            row["age_ms"].as_i64().is_some_and(|age| age >= 0),
7826            "{row:#}"
7827        );
7828        assert_eq!(
7829            row["options"]
7830                .as_array()
7831                .unwrap()
7832                .iter()
7833                .map(|option| option["id"].as_str().unwrap())
7834                .collect::<Vec<_>>(),
7835            vec!["allow_once", "allow_for_session", "deny"],
7836        );
7837
7838        // The filters select against the same rows.
7839        assert_eq!(
7840            approvals(&mut service, json!({"harness": HarnessId::HERMES}))
7841                .as_array()
7842                .map(Vec::len),
7843            Some(1),
7844        );
7845        assert_eq!(
7846            approvals(&mut service, json!({"session": "some-other-session"}))
7847                .as_array()
7848                .map(Vec::len),
7849            Some(0),
7850        );
7851
7852        let response = service
7853            .handle_async(request(
7854                2,
7855                "harness.v1.runtimes.respond",
7856                json!({
7857                    "connection": "runtime-1",
7858                    "request_id": 7,
7859                    "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7860                }),
7861            ))
7862            .await;
7863        assert!(response.get("error").is_none(), "{response:#}");
7864        assert_eq!(
7865            answered
7866                .lock()
7867                .unwrap_or_else(std::sync::PoisonError::into_inner)
7868                .as_slice(),
7869            &[json!({
7870                "request_id": 7,
7871                "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7872            })],
7873        );
7874
7875        let rows = approvals(&mut service, json!({}));
7876        assert_eq!(rows.as_array().map(Vec::len), Some(0), "{rows:#}");
7877    }
7878
7879    /// dev/01: supercode's own queued subagent approvals list through the
7880    /// same door, carrying the outcome the record holds.
7881    #[test]
7882    fn queued_subagent_approvals_list_through_the_same_door() {
7883        let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
7884            crate::subagents::QueuedApproval {
7885                child_agent_id: "child-7".into(),
7886                tool: "shell".into(),
7887                subject: Some("cargo publish --dry-run".into()),
7888                queued_at_ms: 1,
7889                outcome: None,
7890            },
7891            crate::subagents::QueuedApproval {
7892                child_agent_id: "child-8".into(),
7893                tool: "write_file".into(),
7894                subject: None,
7895                queued_at_ms: 2,
7896                outcome: Some(crate::subagents::QueuedApprovalOutcome::Denied),
7897            },
7898        ]));
7899        let mut service = HarnessSessionService::new();
7900        service.observe_subagent_approvals(queue);
7901
7902        let rows = approvals(&mut service, json!({}));
7903        assert_eq!(rows.as_array().map(Vec::len), Some(2), "{rows:#}");
7904        assert_eq!(rows[0]["id"], "supercode/subagent/child-7/1/0");
7905        assert_eq!(rows[0]["harness"], HarnessId::SUPERCODE);
7906        assert_eq!(rows[0]["status"], "pending");
7907        assert_eq!(rows[0]["subject"], "shell cargo publish --dry-run");
7908        assert_eq!(rows[1]["status"], "denied");
7909        assert!(rows[1]["options"].as_array().unwrap().is_empty());
7910
7911        // `--session` addresses a subagent row by its child agent id.
7912        let only = approvals(&mut service, json!({"session": "child-8"}));
7913        assert_eq!(only.as_array().map(Vec::len), Some(1), "{only:#}");
7914        assert_eq!(only[0]["id"], "supercode/subagent/child-8/2/1");
7915    }
7916
7917    /// The uniform-verb contract: an id whose runtime door cannot carry a
7918    /// protocol request is refused BY NAME rather than answered with an empty
7919    /// list. Since ORC-2 gave Claude Code a permission-response primitive
7920    /// every registered harness can carry one, so the refusal is exercised on
7921    /// an unknown id — and the registered ids are asserted to be accepted.
7922    #[test]
7923    fn approvals_list_refuses_a_harness_that_cannot_carry_a_request() {
7924        let response = HarnessSessionService::new().handle(request(
7925            1,
7926            "harness.v1.approvals.list",
7927            json!({"harness": "not-a-harness"}),
7928        ));
7929        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7930        assert!(response["error"]["message"]
7931            .as_str()
7932            .unwrap()
7933            .contains("not-a-harness"));
7934        for harness in [HarnessId::CLAUDE_CODE, HarnessId::CODEX] {
7935            let response = HarnessSessionService::new().handle(request(
7936                1,
7937                "harness.v1.approvals.list",
7938                json!({"harness": harness}),
7939            ));
7940            assert!(response.get("error").is_none(), "{harness}: {response:#}");
7941        }
7942    }
7943
7944    /// The method is advertised, its SDK operation resolves it, and the
7945    /// registry reports the concept as observed for every harness whose
7946    /// runtime door can carry a request.
7947    #[test]
7948    fn approvals_list_is_an_advertised_method_and_an_observed_tier() {
7949        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.list"));
7950        assert_eq!(
7951            SdkOperation::from_method("harness.v1.approvals.list"),
7952            Some(SdkOperation::ApprovalsList)
7953        );
7954        let registry = harness_support_registry();
7955        for id in [
7956            HarnessId::HERMES,
7957            HarnessId::OPENCLAW,
7958            HarnessId::CODEX,
7959            // ORC-2: the Claude Code door answers `can_use_tool`, so its
7960            // pending_request concept joins the other driven doors.
7961            HarnessId::CLAUDE_CODE,
7962        ] {
7963            let concept = registry
7964                .harnesses
7965                .iter()
7966                .find(|harness| harness.id.as_str() == id)
7967                .unwrap()
7968                .orchestration
7969                .concepts
7970                .iter()
7971                .find(|concept| concept.concept == "pending_request")
7972                .unwrap();
7973            assert_eq!(concept.observed, crate::ImplementationKind::BuiltIn, "{id}");
7974            assert!(concept
7975                .methods
7976                .iter()
7977                .any(|method| method == "harness.v1.approvals.list"));
7978        }
7979    }
7980
7981    // ---- ORCH-20: `harness.v1.approvals.resolve` -------------------------
7982
7983    async fn resolve(service: &mut HarnessSessionService, params: Value) -> Value {
7984        service
7985            .handle_async(request(3, "harness.v1.approvals.resolve", params))
7986            .await
7987    }
7988
7989    /// dev/01: the whole loop on a driven runtime — list one pending row,
7990    /// answer it by ROW ID with one uniform decision, and see it gone. The
7991    /// door receives its own ACP envelope carrying the option it enumerated.
7992    #[tokio::test]
7993    async fn a_listed_row_resolves_with_one_uniform_decision_and_then_is_gone() {
7994        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7995        let mut service = HarnessSessionService::new();
7996        service.runtimes.insert(
7997            "runtime-1".into(),
7998            requesting_runtime(
7999                HarnessId::HERMES,
8000                vec![permission_event(7, "rm -rf build")],
8001                answered.clone(),
8002            ),
8003        );
8004        service.poll_runtimes().await;
8005
8006        let rows = approvals(&mut service, json!({}));
8007        assert_eq!(rows[0]["id"], "runtime-1/7");
8008
8009        let response = resolve(
8010            &mut service,
8011            json!({"id": "runtime-1/7", "decision": "allow_once"}),
8012        )
8013        .await;
8014        assert!(response.get("error").is_none(), "{response:#}");
8015        assert_eq!(
8016            response["result"],
8017            json!({
8018                "id": "runtime-1/7",
8019                "decision": "allow_once",
8020                "option_id": "allow_once",
8021                "resolved": true,
8022            }),
8023        );
8024        // The harness's own door was called with its own envelope.
8025        assert_eq!(
8026            answered
8027                .lock()
8028                .unwrap_or_else(std::sync::PoisonError::into_inner)
8029                .as_slice(),
8030            &[json!({
8031                "request_id": 7,
8032                "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
8033            })],
8034        );
8035        // And the row is gone, the same way `runtimes.respond` drops it.
8036        assert_eq!(
8037            approvals(&mut service, json!({})).as_array().map(Vec::len),
8038            Some(0),
8039        );
8040        // Answering it twice is an honest miss, not a silent success.
8041        let response = resolve(
8042            &mut service,
8043            json!({"id": "runtime-1/7", "decision": "allow_once"}),
8044        )
8045        .await;
8046        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8047    }
8048
8049    /// dev/01: deny travels the same path and picks the option the request
8050    /// itself classified as a refusal.
8051    #[tokio::test]
8052    async fn deny_selects_the_requests_own_reject_option() {
8053        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8054        let mut service = HarnessSessionService::new();
8055        service.runtimes.insert(
8056            "runtime-1".into(),
8057            requesting_runtime(
8058                HarnessId::HERMES,
8059                vec![permission_event(11, "git push --force")],
8060                answered.clone(),
8061            ),
8062        );
8063        service.poll_runtimes().await;
8064
8065        let response = resolve(
8066            &mut service,
8067            json!({"id": "runtime-1/11", "decision": "deny"}),
8068        )
8069        .await;
8070        assert!(response.get("error").is_none(), "{response:#}");
8071        // `deny` is the optionId whose ACP `kind` is `reject_once`.
8072        assert_eq!(response["result"]["option_id"], "deny");
8073        assert_eq!(
8074            answered
8075                .lock()
8076                .unwrap_or_else(std::sync::PoisonError::into_inner)[0]["response"],
8077            json!({"outcome": {"outcome": "selected", "optionId": "deny"}}),
8078        );
8079        assert_eq!(
8080            approvals(&mut service, json!({})).as_array().map(Vec::len),
8081            Some(0),
8082        );
8083    }
8084
8085    /// dev/01: a decision this request does not offer is refused by name,
8086    /// listing the ones it does — never silently downgraded to a neighbour.
8087    #[tokio::test]
8088    async fn a_decision_the_request_does_not_offer_is_refused_with_the_offered_ones() {
8089        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8090        let mut service = HarnessSessionService::new();
8091        let mut event = permission_event(3, "rm -rf build");
8092        // A request offering only allow-once and deny, as hermes 0.21.0's
8093        // edit-approval layer raises one.
8094        event.payload["params"]["options"] = json!([
8095            {"optionId": "allow_once", "name": "Allow edit", "kind": "allow_once"},
8096            {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
8097        ]);
8098        service.runtimes.insert(
8099            "runtime-1".into(),
8100            requesting_runtime(HarnessId::HERMES, vec![event], answered.clone()),
8101        );
8102        service.poll_runtimes().await;
8103
8104        let response = resolve(
8105            &mut service,
8106            json!({"id": "runtime-1/3", "decision": "allow_always"}),
8107        )
8108        .await;
8109        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8110        let message = response["error"]["message"].as_str().unwrap();
8111        assert!(message.contains("allow_always"), "{message}");
8112        assert!(message.contains("allow_once, deny"), "{message}");
8113        // Nothing was sent, and the request is still waiting for an answer.
8114        assert!(answered
8115            .lock()
8116            .unwrap_or_else(std::sync::PoisonError::into_inner)
8117            .is_empty());
8118        assert_eq!(
8119            approvals(&mut service, json!({})).as_array().map(Vec::len),
8120            Some(1),
8121        );
8122    }
8123
8124    /// dev/01: supercode's own queued subagent row is addressable but not
8125    /// answerable through this door — it is the parent's audit copy of a
8126    /// request its own handler answers. Refused by name, never a no-op.
8127    #[tokio::test]
8128    async fn a_queued_subagent_row_is_refused_by_name_rather_than_silently_answered() {
8129        let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
8130            crate::subagents::QueuedApproval {
8131                child_agent_id: "child-7".into(),
8132                tool: "shell".into(),
8133                subject: Some("cargo publish --dry-run".into()),
8134                queued_at_ms: 1,
8135                outcome: None,
8136            },
8137        ]));
8138        let mut service = HarnessSessionService::new();
8139        service.observe_subagent_approvals(queue.clone());
8140        let row = approvals(&mut service, json!({}))[0]["id"]
8141            .as_str()
8142            .unwrap()
8143            .to_string();
8144        assert_eq!(row, "supercode/subagent/child-7/1/0");
8145
8146        let response = resolve(&mut service, json!({"id": row, "decision": "allow_once"})).await;
8147        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8148        let message = response["error"]["message"].as_str().unwrap();
8149        assert!(message.contains("queued subagent record"), "{message}");
8150        assert!(message.contains("request"), "{message}");
8151        // The audit record is untouched: nothing pretended to answer it.
8152        assert!(queue
8153            .lock()
8154            .unwrap_or_else(std::sync::PoisonError::into_inner)[0]
8155            .outcome
8156            .is_none());
8157    }
8158
8159    /// An id nobody is holding, and a call that names no decision at all,
8160    /// both fail with a message that says why.
8161    #[tokio::test]
8162    async fn an_unknown_row_and_a_missing_decision_are_both_named() {
8163        let mut service = HarnessSessionService::new();
8164        let response = resolve(
8165            &mut service,
8166            json!({"id": "runtime-9/4", "decision": "deny"}),
8167        )
8168        .await;
8169        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8170        assert!(response["error"]["message"]
8171            .as_str()
8172            .unwrap()
8173            .contains("runtime-9/4"));
8174
8175        let response = resolve(&mut service, json!({"id": "runtime-9/4"})).await;
8176        let message = response["error"]["message"].as_str().unwrap();
8177        assert!(
8178            message.contains("allow_once | allow_always | deny"),
8179            "{message}"
8180        );
8181
8182        let response = resolve(
8183            &mut service,
8184            json!({"id": "runtime-9/4", "decision": "deny", "option_id": "deny"}),
8185        )
8186        .await;
8187        assert!(response["error"]["message"]
8188            .as_str()
8189            .unwrap()
8190            .contains("not both"));
8191    }
8192
8193    /// The method is advertised, its SDK operation resolves it, and every
8194    /// harness whose runtime door can carry a request reports it on the
8195    /// CONTROLLED tier beside `runtimes.respond`.
8196    #[test]
8197    fn approvals_resolve_is_an_advertised_method_and_a_controlled_tier() {
8198        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.resolve"));
8199        assert_eq!(
8200            SdkOperation::from_method("harness.v1.approvals.resolve"),
8201            Some(SdkOperation::ApprovalsResolve)
8202        );
8203        assert_eq!(
8204            SdkOperation::ApprovalsResolve.action_name(),
8205            "approvals_resolve"
8206        );
8207        let registry = harness_support_registry();
8208        for id in [
8209            HarnessId::HERMES,
8210            HarnessId::OPENCLAW,
8211            HarnessId::CODEX,
8212            // ORC-2: the Claude Code door answers `can_use_tool`, so its
8213            // pending_request concept joins the other driven doors.
8214            HarnessId::CLAUDE_CODE,
8215        ] {
8216            let concept = registry
8217                .harnesses
8218                .iter()
8219                .find(|harness| harness.id.as_str() == id)
8220                .unwrap()
8221                .orchestration
8222                .concepts
8223                .iter()
8224                .find(|concept| concept.concept == "pending_request")
8225                .unwrap();
8226            assert_eq!(
8227                concept.controlled,
8228                crate::ImplementationKind::BuiltIn,
8229                "{id}"
8230            );
8231            assert!(
8232                concept
8233                    .methods
8234                    .iter()
8235                    .any(|method| method == "harness.v1.approvals.resolve"),
8236                "{id}"
8237            );
8238        }
8239    }
8240
8241    #[test]
8242    fn capabilities_are_explicit_and_versioned() {
8243        let mut service = HarnessSessionService::new();
8244        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
8245        assert_eq!(response["result"]["version"], HARNESS_SERVICE_VERSION);
8246        assert_eq!(
8247            response["result"]["sdk"]["schema_version"],
8248            crate::SDK_SCHEMA_VERSION
8249        );
8250        assert_eq!(
8251            response["result"]["sdk"]["operations"]
8252                .as_array()
8253                .unwrap()
8254                .len(),
8255            SdkOperation::ALL.len()
8256        );
8257        assert_eq!(
8258            response["result"]["harnesses"].as_array().unwrap().len(),
8259            11
8260        );
8261        assert!(response["result"]["harnesses"]
8262            .as_array()
8263            .unwrap()
8264            .iter()
8265            .any(|harness| harness == HarnessId::GROK));
8266        assert!(response["result"]["harnesses"]
8267            .as_array()
8268            .unwrap()
8269            .iter()
8270            .any(|harness| harness == HarnessId::GOOSE));
8271    }
8272
8273    #[test]
8274    fn handshake_health_uses_protocol_liveness_not_stderr_severity() {
8275        let noisy_stderr = crate::HarnessEvent {
8276            sequence: None,
8277            kind: "transport_stderr".into(),
8278            payload: json!({"line": "ERROR optional worker AuthorizationRequired"}),
8279        };
8280        assert_eq!(handshake_event_failure(&noisy_stderr), None);
8281
8282        let closed = crate::HarnessEvent {
8283            sequence: None,
8284            kind: "transport_closed".into(),
8285            payload: json!({}),
8286        };
8287        assert!(handshake_event_failure(&closed).is_some());
8288    }
8289
8290    #[tokio::test]
8291    async fn runtime_eof_is_notified_and_removed_for_raw_and_explicit_close() {
8292        let mut service = HarnessSessionService::new();
8293        service
8294            .runtimes
8295            .insert("raw-eof".into(), ending_runtime(None));
8296        service.runtimes.insert(
8297            "explicit-close".into(),
8298            ending_runtime(Some(HarnessEvent {
8299                sequence: None,
8300                kind: "transport_closed".into(),
8301                payload: json!({"message": "native transport exited"}),
8302            })),
8303        );
8304
8305        let notifications = service.poll_runtimes().await;
8306
8307        assert_eq!(notifications.len(), 2);
8308        assert!(notifications
8309            .iter()
8310            .all(|notification| { notification["params"]["event"]["kind"] == "transport_closed" }));
8311        assert!(notifications.iter().all(|notification| {
8312            notification["params"]["session_id"] == "ending-session"
8313                && notification["params"]["connection"].is_string()
8314        }));
8315        let mut sequences = notifications
8316            .iter()
8317            .filter_map(|notification| notification["params"]["sequence"].as_u64())
8318            .collect::<Vec<_>>();
8319        sequences.sort_unstable();
8320        assert_eq!(sequences, vec![1, 2]);
8321        assert!(service.runtimes.is_empty());
8322    }
8323
8324    #[test]
8325    fn support_report_and_grok_default_binding_share_the_registry() {
8326        let mut service = HarnessSessionService::new();
8327        let response = service.handle(request(1, "harness.v1.support.report", json!({})));
8328        assert_eq!(response["result"]["schema"], crate::SUPPORT_REGISTRY_SCHEMA);
8329        let params = RuntimeBackendParams {
8330            harness: HarnessId::from(HarnessId::GROK),
8331            protocol: None,
8332            launch: None,
8333            base_url: None,
8334            policy: RuntimePolicy::Default,
8335        };
8336        let backend = match runtime_backend(&params) {
8337            Ok(backend) => backend,
8338            Err(_) => panic!("Grok should bind through its registered ACP launch"),
8339        };
8340        assert_eq!(backend.harness().as_str(), HarnessId::GROK);
8341        assert!(backend.capabilities().start_session);
8342        let registered = harness_support_registry()
8343            .harnesses
8344            .into_iter()
8345            .find(|harness| harness.id.as_str() == HarnessId::GROK)
8346            .and_then(|harness| harness.runtime.default_launch)
8347            .unwrap();
8348        assert!(!registered
8349            .arguments
8350            .iter()
8351            .any(|argument| argument == "--always-approve"));
8352        assert!(runtime_launch(&params).is_none());
8353
8354        let yolo = RuntimeBackendParams {
8355            policy: RuntimePolicy::Yolo,
8356            ..params
8357        };
8358        assert!(runtime_launch(&yolo)
8359            .unwrap()
8360            .arguments
8361            .iter()
8362            .any(|argument| argument == "--always-approve"));
8363
8364        let mismatched_protocol = RuntimeBackendParams {
8365            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8366            protocol: Some("acp".into()),
8367            launch: None,
8368            base_url: None,
8369            policy: RuntimePolicy::Default,
8370        };
8371        assert!(runtime_backend(&mismatched_protocol).is_err());
8372    }
8373
8374    #[test]
8375    fn load_follow_and_unfollow_share_the_same_locator() {
8376        let mut service = HarnessSessionService::new();
8377        let locator = pi_locator();
8378        let loaded = service.handle(request(
8379            1,
8380            "harness.v1.sessions.load",
8381            json!({"locator": locator}),
8382        ));
8383        assert_eq!(
8384            loaded["result"]["session"]["session_id"],
8385            locator.session_id
8386        );
8387
8388        let followed = service.handle(request(
8389            2,
8390            "harness.v1.sessions.follow",
8391            json!({"locator": locator}),
8392        ));
8393        assert_eq!(followed["result"]["subscription"], "sub-1");
8394        assert_eq!(followed["result"]["initial"]["type"], "session_snapshot");
8395        assert!(service.poll().is_empty());
8396
8397        let unfollowed = service.handle(request(
8398            3,
8399            "harness.v1.sessions.unfollow",
8400            json!({"subscription": "sub-1"}),
8401        ));
8402        assert_eq!(unfollowed["result"]["removed"], true);
8403    }
8404
8405    #[test]
8406    fn bounded_read_view_excludes_subagents_and_keeps_only_the_tail() {
8407        let temp = std::env::temp_dir().join(format!(
8408            "supercode-bounded-view-{}-{}",
8409            std::process::id(),
8410            generated_session_id()
8411        ));
8412        let path = temp.join("parent.jsonl");
8413        let subagents = temp.join("parent/subagents");
8414        std::fs::create_dir_all(&subagents).unwrap();
8415        let long_last = "x".repeat(300);
8416        let parent_records = [
8417            json!({"type":"user","uuid":"u1","parentUuid":null,"message":{"role":"user","content":"first"}}),
8418            json!({"type":"assistant","uuid":"a1","parentUuid":"u1","message":{"role":"assistant","content":[{"type":"text","text":"middle"}]}}),
8419            json!({"type":"user","uuid":"u2","parentUuid":"a1","message":{"role":"user","content":long_last}}),
8420        ];
8421        std::fs::write(
8422            &path,
8423            format!(
8424                "{}\n",
8425                parent_records
8426                    .iter()
8427                    .map(Value::to_string)
8428                    .collect::<Vec<_>>()
8429                    .join("\n")
8430            ),
8431        )
8432        .unwrap();
8433        std::fs::write(
8434            subagents.join("agent-child.jsonl"),
8435            concat!(
8436                r#"{"type":"user","uuid":"cu","parentUuid":null,"agentId":"child","message":{"role":"user","content":"child work"}}"#,
8437                "\n",
8438            ),
8439        )
8440        .unwrap();
8441        let locator = SessionLocator {
8442            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8443            session_id: "parent".into(),
8444            storage: StorageLocator::File { path },
8445        };
8446        let mut service = HarnessSessionService::new();
8447
8448        let complete = service.handle(request(
8449            1,
8450            "harness.v1.sessions.load",
8451            json!({"locator": locator}),
8452        ));
8453        assert_eq!(
8454            complete["result"]["session"]["subagents"]
8455                .as_array()
8456                .unwrap()
8457                .len(),
8458            1
8459        );
8460
8461        let bounded = service.handle(request(
8462            2,
8463            "harness.v1.sessions.load",
8464            json!({
8465                "locator": locator,
8466                "view": {
8467                    "tail_messages": 1,
8468                    "max_message_chars": 256,
8469                    "include_subagents": false
8470                },
8471            }),
8472        ));
8473        let session = &bounded["result"]["session"];
8474        assert!(session["subagents"].as_array().unwrap().is_empty());
8475        assert_eq!(session["messages"].as_array().unwrap().len(), 1);
8476        assert_eq!(
8477            session["messages"][0]["content"],
8478            format!("{}\n…", "x".repeat(256))
8479        );
8480
8481        let followed = service.handle(request(
8482            3,
8483            "harness.v1.sessions.follow",
8484            json!({
8485                "locator": locator,
8486                "view": {
8487                    "tail_messages": 1,
8488                    "max_message_chars": 256,
8489                    "include_subagents": false
8490                },
8491            }),
8492        ));
8493        let initial = &followed["result"]["initial"]["session"];
8494        assert!(initial["subagents"].as_array().unwrap().is_empty());
8495        assert_eq!(initial["messages"].as_array().unwrap().len(), 1);
8496
8497        let _ = std::fs::remove_dir_all(&temp);
8498    }
8499
8500    #[test]
8501    fn forty_megabyte_display_load_is_bounded_and_prompt() {
8502        let temp = std::env::temp_dir().join(format!(
8503            "supercode-large-display-view-{}-{}",
8504            std::process::id(),
8505            generated_session_id()
8506        ));
8507        std::fs::create_dir_all(&temp).unwrap();
8508        let path = temp.join("rollout.jsonl");
8509        let mut file = std::io::BufWriter::new(std::fs::File::create(&path).unwrap());
8510        writeln!(
8511            file,
8512            r#"{{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{{"id":"large-display","cwd":"/tmp"}}}}"#
8513        )
8514        .unwrap();
8515        let padding = "x".repeat(80 * 1024);
8516        for index in 0..512 {
8517            let marker = if index == 0 {
8518                "OLDEST-SHOULD-NOT-LOAD"
8519            } else if index == 511 {
8520                "LATEST-MUST-LOAD"
8521            } else {
8522                "bulk"
8523            };
8524            writeln!(
8525                file,
8526                "{}",
8527                json!({
8528                    "timestamp": "2026-01-01T00:00:01Z",
8529                    "type": "response_item",
8530                    "payload": {
8531                        "type": "message",
8532                        "role": "assistant",
8533                        "content": [{"type": "output_text", "text": format!("{marker}:{padding}")}],
8534                    },
8535                })
8536            )
8537            .unwrap();
8538        }
8539        file.flush().unwrap();
8540        drop(file);
8541        assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8542
8543        let locator = SessionLocator {
8544            harness: HarnessId::from(HarnessId::CODEX),
8545            session_id: "large-display".into(),
8546            storage: StorageLocator::File { path },
8547        };
8548        let started = Instant::now();
8549        let response = HarnessSessionService::new().handle(request(
8550            1,
8551            "harness.v1.sessions.load",
8552            json!({
8553                "locator": locator,
8554                "view": {
8555                    "tail_messages": 500,
8556                    "max_message_chars": 1024,
8557                    "include_subagents": false,
8558                    "display_history": true,
8559                },
8560            }),
8561        ));
8562        let elapsed = started.elapsed();
8563        let wire = response.to_string();
8564        eprintln!(
8565            "bounded 40 MiB display load: {elapsed:?}, {} response bytes",
8566            wire.len()
8567        );
8568        assert!(response.get("error").is_none(), "{response:#}");
8569        assert!(wire.contains("LATEST-MUST-LOAD"));
8570        assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8571        assert!(
8572            wire.len() < 2 * 1024 * 1024,
8573            "bounded wire was {} bytes",
8574            wire.len()
8575        );
8576        assert!(
8577            elapsed.as_secs_f64() < 3.0,
8578            "bounded 40 MiB load took {elapsed:?}"
8579        );
8580
8581        let _ = std::fs::remove_dir_all(&temp);
8582    }
8583
8584    #[test]
8585    fn forty_megabyte_goose_store_display_load_reads_only_the_tail() {
8586        let temp = std::env::temp_dir().join(format!(
8587            "supercode-large-goose-view-{}-{}",
8588            std::process::id(),
8589            generated_session_id()
8590        ));
8591        std::fs::create_dir_all(&temp).unwrap();
8592        let path = temp.join("sessions.db");
8593        let connection = rusqlite::Connection::open(&path).unwrap();
8594        connection
8595            .execute_batch(
8596                "CREATE TABLE sessions (
8597                    id TEXT PRIMARY KEY, name TEXT NOT NULL, working_dir TEXT NOT NULL,
8598                    created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
8599                    session_type TEXT NOT NULL, extension_data TEXT,
8600                    goose_mode TEXT NOT NULL, provider_name TEXT, model_config_json TEXT,
8601                    archived_at TEXT
8602                 );
8603                 CREATE TABLE messages (
8604                    id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, message_id TEXT,
8605                    role TEXT NOT NULL, content_json TEXT NOT NULL,
8606                    created_timestamp INTEGER NOT NULL, metadata_json TEXT
8607                 );",
8608            )
8609            .unwrap();
8610        connection
8611            .execute(
8612                "INSERT INTO sessions VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, NULL)",
8613                rusqlite::params![
8614                    "goose-large",
8615                    "Large Goose session",
8616                    "/tmp",
8617                    "2026-01-01 00:00:00",
8618                    "2026-01-01 00:00:02",
8619                    "user",
8620                    "{}",
8621                    "auto",
8622                    "anthropic",
8623                    r#"{"model_name":"claude-sonnet"}"#,
8624                ],
8625            )
8626            .unwrap();
8627        let old_content = serde_json::to_string(&vec![json!({
8628            "type": "text",
8629            "text": format!("OLDEST-SHOULD-NOT-LOAD:{}", "x".repeat(40 * 1024 * 1024)),
8630        })])
8631        .unwrap();
8632        connection
8633            .execute(
8634                "INSERT INTO messages VALUES (1, ?1, 'old', 'user', ?2, 1, '{}')",
8635                rusqlite::params!["goose-large", old_content],
8636            )
8637            .unwrap();
8638        connection
8639            .execute(
8640                "INSERT INTO messages VALUES (2, ?1, 'new', 'assistant', ?2, 2, '{}')",
8641                rusqlite::params![
8642                    "goose-large",
8643                    r#"[{"type":"text","text":"LATEST-MUST-LOAD"}]"#
8644                ],
8645            )
8646            .unwrap();
8647        drop(connection);
8648        assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8649
8650        let locator = SessionLocator {
8651            harness: HarnessId::from(HarnessId::GOOSE),
8652            session_id: "goose-large".into(),
8653            storage: StorageLocator::Sqlite {
8654                path,
8655                selector: "goose-large".into(),
8656            },
8657        };
8658        let started = Instant::now();
8659        let response = HarnessSessionService::new().handle(request(
8660            1,
8661            "harness.v1.sessions.load",
8662            json!({
8663                "locator": locator,
8664                "view": {
8665                    "tail_messages": 1,
8666                    "max_message_chars": 1024,
8667                    "include_subagents": false,
8668                    "display_history": true,
8669                },
8670            }),
8671        ));
8672        let elapsed = started.elapsed();
8673        let wire = response.to_string();
8674        eprintln!(
8675            "bounded 40 MiB Goose display load: {elapsed:?}, {} response bytes",
8676            wire.len()
8677        );
8678        assert!(response.get("error").is_none(), "{response:#}");
8679        assert!(wire.contains("LATEST-MUST-LOAD"));
8680        assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8681        assert!(
8682            wire.len() < 64 * 1024,
8683            "bounded wire was {} bytes",
8684            wire.len()
8685        );
8686        assert!(
8687            elapsed.as_secs_f64() < 1.0,
8688            "bounded Goose load took {elapsed:?}"
8689        );
8690
8691        let _ = std::fs::remove_dir_all(&temp);
8692    }
8693
8694    #[test]
8695    fn display_view_keeps_codex_assistant_history_across_compaction() {
8696        let temp = std::env::temp_dir().join(format!(
8697            "supercode-codex-display-view-{}-{}",
8698            std::process::id(),
8699            generated_session_id()
8700        ));
8701        std::fs::create_dir_all(&temp).unwrap();
8702        let path = temp.join("rollout.jsonl");
8703        std::fs::write(
8704            &path,
8705            concat!(
8706                r#"{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{"id":"codex-display","cwd":"/tmp"}}"#,
8707                "\n",
8708                r#"{"timestamp":"2026-01-01T00:00:01Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"old prompt"}]}}"#,
8709                "\n",
8710                r#"{"timestamp":"2026-01-01T00:00:02Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"old answer"}]}}"#,
8711                "\n",
8712                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"}]}}"#,
8713                "\n",
8714                r#"{"timestamp":"2026-01-01T00:00:04Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"new prompt"}]}}"#,
8715                "\n",
8716                r#"{"timestamp":"2026-01-01T00:00:05Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"new answer"}]}}"#,
8717                "\n",
8718            ),
8719        )
8720        .unwrap();
8721        let locator = SessionLocator {
8722            harness: HarnessId::from(HarnessId::CODEX),
8723            session_id: "codex-display".into(),
8724            storage: StorageLocator::File { path },
8725        };
8726        let mut service = HarnessSessionService::new();
8727
8728        let continuation = service.handle(request(
8729            1,
8730            "harness.v1.sessions.load",
8731            json!({"locator": locator}),
8732        ));
8733        let continuation_text = continuation["result"]["session"]["messages"].to_string();
8734        assert!(!continuation_text.contains("old answer"));
8735
8736        let display = service.handle(request(
8737            2,
8738            "harness.v1.sessions.load",
8739            json!({
8740                "locator": locator,
8741                "view": {
8742                    "tail_messages": 10,
8743                    "include_subagents": false,
8744                    "display_history": true,
8745                },
8746            }),
8747        ));
8748        let display_text = display["result"]["session"]["messages"].to_string();
8749        assert!(display_text.contains("old prompt"));
8750        assert!(display_text.contains("old answer"));
8751        assert!(display_text.contains("new prompt"));
8752        assert!(display_text.contains("new answer"));
8753
8754        let _ = std::fs::remove_dir_all(&temp);
8755    }
8756
8757    #[test]
8758    fn indexed_claude_windows_match_the_existing_wire_projection() {
8759        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
8760            .join("tests/fixtures/claude_code_session.jsonl");
8761        let locator = SessionLocator {
8762            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8763            session_id: "fixture".into(),
8764            storage: StorageLocator::File { path },
8765        };
8766        let full = load_session(&locator).unwrap();
8767        for inline_media in [InlineMediaMode::Full, InlineMediaMode::Metadata] {
8768            for offset in [0, 1, full.messages.len(), usize::MAX] {
8769                for limit in [0, 1, 3, usize::MAX] {
8770                    let options = SessionLoadOptions {
8771                        include_subagents: Some(false),
8772                        inline_media,
8773                        message_offset: Some(offset),
8774                        message_limit: Some(limit),
8775                        ..Default::default()
8776                    };
8777                    let expected = projected_session_result(&full, &options);
8778                    assert_eq!(
8779                        indexed_claude_window(&locator, &options).unwrap().unwrap(),
8780                        expected
8781                    );
8782                }
8783            }
8784            for tail in [0, 1, 3, usize::MAX] {
8785                let options = SessionLoadOptions {
8786                    include_subagents: Some(false),
8787                    inline_media,
8788                    message_tail: Some(tail),
8789                    ..Default::default()
8790                };
8791                assert_eq!(
8792                    indexed_claude_window(&locator, &options).unwrap().unwrap(),
8793                    projected_session_result(&full, &options)
8794                );
8795            }
8796        }
8797    }
8798
8799    #[test]
8800    fn load_supports_bounded_windows_and_media_metadata() {
8801        let mut service = HarnessSessionService::new();
8802        let locator = pi_locator();
8803        let bounded = service.handle(request(
8804            1,
8805            "harness.v1.sessions.load",
8806            json!({
8807                "locator": locator,
8808                "options": {
8809                    "include_subagents": false,
8810                    "message_limit": 2,
8811                    "message_offset": 1
8812                }
8813            }),
8814        ));
8815        assert_eq!(bounded["result"]["window"]["offset"], 1);
8816        assert_eq!(bounded["result"]["window"]["returned"], 2);
8817        assert!(bounded["result"]["summary"]["first_message"].is_object());
8818        assert!(bounded["result"]["summary"]["last_message"].is_object());
8819        assert_eq!(
8820            bounded["result"]["session"]["messages"]
8821                .as_array()
8822                .unwrap()
8823                .len(),
8824            2
8825        );
8826        assert!(bounded["result"]["session"]["subagents"]
8827            .as_array()
8828            .unwrap()
8829            .is_empty());
8830
8831        let tail = service.handle(request(
8832            2,
8833            "harness.v1.sessions.load",
8834            json!({"locator": locator, "options": {"message_tail": 1}}),
8835        ));
8836        assert_eq!(tail["result"]["window"]["returned"], 1);
8837        assert_eq!(tail["result"]["window"]["has_more"], true);
8838        assert_eq!(tail["result"]["window"]["has_older"], true);
8839        assert!(tail["result"]["window"]["older_items"].as_u64().unwrap() > 0);
8840        assert!(tail["result"]["summary"]["first_message"].is_object());
8841
8842        let metadata_only = service.handle(request(
8843            3,
8844            "harness.v1.sessions.load",
8845            json!({"locator": locator, "options": {"inline_media": "metadata"}}),
8846        ));
8847        assert!(metadata_only["result"]["session"]
8848            .to_string()
8849            .contains("media_reference"));
8850        assert!(!metadata_only["result"]["session"]
8851            .to_string()
8852            .contains("data:image/"));
8853    }
8854
8855    #[test]
8856    fn import_translate_branch_and_handoff_use_typed_artifacts() {
8857        let mut service = HarnessSessionService::new();
8858        let locator = pi_locator();
8859        let translated = service.handle(request(
8860            1,
8861            "harness.v1.sessions.translate",
8862            json!({"locator": locator, "target_harness": "grok"}),
8863        ));
8864        assert_eq!(translated["result"]["artifact"]["source_harness"], "pi");
8865        assert_eq!(translated["result"]["artifact"]["target_harness"], "grok");
8866        assert!(translated["result"]["artifact"]["content"]
8867            .as_str()
8868            .is_some_and(|content| !content.is_empty()));
8869
8870        for target in ["opencode", "open-code"] {
8871            let opencode = service.handle(request(
8872                6,
8873                "harness.v1.sessions.translate",
8874                json!({"locator": locator, "target_harness": target}),
8875            ));
8876            assert_eq!(opencode["result"]["artifact"]["target_harness"], "opencode");
8877        }
8878        let goose = service.handle(request(
8879            7,
8880            "harness.v1.sessions.translate",
8881            json!({"locator": locator, "target_harness": "goose"}),
8882        ));
8883        assert_eq!(goose["result"]["artifact"]["target_harness"], "goose");
8884        assert!(serde_json::from_str::<Value>(
8885            goose["result"]["artifact"]["content"].as_str().unwrap()
8886        )
8887        .unwrap()["conversation"]
8888            .is_array());
8889
8890        let imported = service.handle(request(
8891            2,
8892            "harness.v1.sessions.import",
8893            json!({
8894                "source_harness": "grok",
8895                "content": translated["result"]["artifact"]["content"],
8896            }),
8897        ));
8898        assert_eq!(imported["result"]["session"]["source"], "grok");
8899
8900        let branched = service.handle(request(
8901            3,
8902            "harness.v1.sessions.branch",
8903            json!({"locator": locator, "target_harness": "codex"}),
8904        ));
8905        assert_eq!(branched["result"]["parent"]["harness"], "pi");
8906        assert!(branched["result"]["bootstrap_prompt"]
8907            .as_str()
8908            .unwrap()
8909            .contains("frozen parent transcript"));
8910        assert_eq!(branched["result"]["artifact"]["target_harness"], "codex");
8911
8912        let handoff = service.handle(request(
8913            4,
8914            "harness.v1.sessions.handoff",
8915            json!({"locator": locator, "target_harness": "pi", "cwd": "/tmp/project"}),
8916        ));
8917        assert_eq!(handoff["result"]["launch"]["program"], "pi");
8918        assert_eq!(handoff["result"]["launch"]["cwd"], "/tmp/project");
8919        assert_eq!(handoff["result"]["requires_materialization"], true);
8920
8921        let goose_handoff = service.handle(request(
8922            8,
8923            "harness.v1.sessions.handoff",
8924            json!({"locator": locator, "target_harness": "goose", "cwd": "/tmp/project"}),
8925        ));
8926        assert_eq!(goose_handoff["result"]["launch"]["program"], "goose");
8927        assert_eq!(
8928            goose_handoff["result"]["materialize"]["arguments"],
8929            json!(["session", "import", "{artifact_path}"])
8930        );
8931
8932        let resumed = service.handle(request(
8933            5,
8934            "harness.v1.sessions.resume_instructions",
8935            json!({"locator": locator, "cwd": "/tmp/project", "policy": "yolo"}),
8936        ));
8937        assert_eq!(resumed["result"]["launch"]["program"], "pi");
8938        assert_eq!(resumed["result"]["launch"]["arguments"][0], "--approve");
8939    }
8940
8941    #[test]
8942    fn reduce_persists_and_reloads_a_byte_exact_reversible_bundle() {
8943        let temp = std::env::temp_dir().join(format!(
8944            "supercode-service-reduce-{}-{}",
8945            std::process::id(),
8946            generated_session_id()
8947        ));
8948        let source_path = temp.join("source.jsonl");
8949        let store_root = temp.join("store");
8950        std::fs::create_dir_all(&temp).unwrap();
8951
8952        let mut records = vec![json!({
8953            "timestamp": "2026-01-01T00:00:00Z",
8954            "type": "session_meta",
8955            "payload": {"id": "codex-reduce", "cwd": "/tmp/project"},
8956        })];
8957        for turn in 0..16 {
8958            records.push(json!({
8959                "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 1),
8960                "type": "response_item",
8961                "payload": {
8962                    "type": "message",
8963                    "role": "user",
8964                    "content": [{
8965                        "type": "input_text",
8966                        "text": format!("request {turn}: {}", "context ".repeat(80)),
8967                    }],
8968                },
8969            }));
8970            records.push(json!({
8971                "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 2),
8972                "type": "response_item",
8973                "payload": {
8974                    "type": "message",
8975                    "role": "assistant",
8976                    "content": [{
8977                        "type": "output_text",
8978                        "text": format!("answer {turn}: {}", "implementation detail ".repeat(80)),
8979                    }],
8980                },
8981            }));
8982        }
8983        let source = format!(
8984            "{}\n",
8985            records
8986                .iter()
8987                .map(Value::to_string)
8988                .collect::<Vec<_>>()
8989                .join("\n")
8990        );
8991        std::fs::write(&source_path, &source).unwrap();
8992        let locator = SessionLocator {
8993            harness: HarnessId::from(HarnessId::CODEX),
8994            session_id: "codex-reduce".into(),
8995            storage: StorageLocator::File {
8996                path: source_path.clone(),
8997            },
8998        };
8999        let original = load_session(&locator).unwrap();
9000        let mut service =
9001            HarnessSessionService::new().with_reduction_store_root(store_root.clone());
9002
9003        let response = service.handle(request(
9004            1,
9005            "harness.v1.sessions.reduce",
9006            json!({
9007                "locator": locator,
9008                "target_harness": "claude-code",
9009                "keep_last": 4,
9010            }),
9011        ));
9012        assert!(response.get("error").is_none(), "{response:#}");
9013        let receipt = &response["result"]["receipt"];
9014        assert_eq!(receipt["source_harness"], "codex");
9015        assert_eq!(receipt["target_harness"], "claude-code");
9016        assert_eq!(receipt["verified"], true);
9017        assert_eq!(receipt["reversible"], true);
9018        assert!(receipt["reductions"].as_u64().unwrap() > 0);
9019        assert!(
9020            receipt["source_tokens"].as_u64().unwrap()
9021                > receipt["reduced_tokens"].as_u64().unwrap()
9022        );
9023        assert!(receipt["ratio"].as_f64().unwrap() > 1.0);
9024        assert!(response["result"]["bootstrap_prompt"]
9025            .as_str()
9026            .unwrap()
9027            .contains("Do not guess hidden content"));
9028
9029        let rescue_id = receipt["id"].as_str().unwrap();
9030        let store = crate::SessionStore::open(&store_root).unwrap();
9031        let sidecar =
9032            Session::from_sidecar_str(&store.load_sidecar(rescue_id).unwrap().unwrap()).unwrap();
9033        let log = store.load_reduction_log(rescue_id).unwrap().unwrap();
9034        let persisted_view = parse_messages_jsonl(&store.load(rescue_id).unwrap()).unwrap();
9035        let policy = reduce::ReductionPolicy {
9036            clear_turns_older_than: Some(4),
9037            ..Default::default()
9038        };
9039        let (restamped_view, reapplied_log) =
9040            reduce::project_messages(&sidecar.messages, &policy, &log);
9041        assert_eq!(
9042            messages_jsonl(&persisted_view).unwrap(),
9043            messages_jsonl(&restamped_view).unwrap()
9044        );
9045        assert_eq!(reapplied_log, log);
9046        reduce::verify_log(&log, &sidecar).unwrap();
9047        assert_eq!(
9048            reduce::invert(&restamped_view, &log, &sidecar).unwrap(),
9049            original.messages
9050        );
9051        assert_eq!(std::fs::read_to_string(&source_path).unwrap(), source);
9052
9053        std::fs::remove_dir_all(temp).ok();
9054    }
9055
9056    #[test]
9057    fn read_surfaces_view_a_severed_claude_graph_while_transfer_still_refuses_it() {
9058        let temp = std::env::temp_dir().join(format!(
9059            "supercode-severed-view-{}-{}",
9060            std::process::id(),
9061            generated_session_id()
9062        ));
9063        std::fs::create_dir_all(&temp).unwrap();
9064        let path = temp.join("severed.jsonl");
9065        // A live record whose parent was pruned — what a compacted or
9066        // resumed-across-files Claude Code session looks like on disk.
9067        std::fs::write(
9068            &path,
9069            concat!(
9070                r#"{"type":"user","uuid":"orphan-u","parentUuid":null,"message":{"role":"user","content":"stranded prompt"}}"#,
9071                "\n",
9072                r#"{"type":"assistant","uuid":"live-a","parentUuid":"pruned","message":{"id":"m","role":"assistant","content":[{"type":"text","text":"live answer"}]}}"#,
9073                "\n",
9074            ),
9075        )
9076        .unwrap();
9077        let locator = SessionLocator {
9078            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9079            session_id: "severed".into(),
9080            storage: StorageLocator::File { path },
9081        };
9082        let mut service = HarnessSessionService::new();
9083
9084        let viewed = service.handle(request(
9085            1,
9086            "harness.v1.sessions.load",
9087            json!({"locator": locator}),
9088        ));
9089        let session = &viewed["result"]["session"];
9090        assert_eq!(session["fidelity"], "semantic");
9091        assert_eq!(session["messages"].as_array().unwrap().len(), 2);
9092        assert!(session["residue"].as_array().unwrap().iter().any(|entry| {
9093            entry
9094                .as_str()
9095                .is_some_and(|entry| entry.contains("live-a") && entry.contains("pruned"))
9096        }));
9097
9098        // Asking a READ surface for a lossless reconstruction gets the strict
9099        // refusal back, unchanged.
9100        let strict = service.handle(request(
9101            2,
9102            "harness.v1.sessions.load",
9103            json!({"locator": locator, "fidelity": "byte_lossless"}),
9104        ));
9105        assert!(strict["error"]["message"]
9106            .as_str()
9107            .unwrap()
9108            .contains("cannot reconstruct lossless Claude continuation"));
9109
9110        // Transfer/continuation surfaces have no view mode at all.
9111        let translated = service.handle(request(
9112            3,
9113            "harness.v1.sessions.translate",
9114            json!({"locator": locator, "target_harness": "codex"}),
9115        ));
9116        assert!(translated["error"]["message"]
9117            .as_str()
9118            .unwrap()
9119            .contains("cannot reconstruct lossless Claude continuation"));
9120        let resumed = service.handle(request(
9121            4,
9122            "harness.v1.sessions.resume_instructions",
9123            json!({"locator": locator}),
9124        ));
9125        assert!(resumed["error"]["message"]
9126            .as_str()
9127            .unwrap()
9128            .contains("cannot reconstruct lossless Claude continuation"));
9129
9130        let _ = std::fs::remove_dir_all(&temp);
9131    }
9132
9133    #[test]
9134    fn structured_resume_launches_cover_gemini_goose_and_supercode() {
9135        let codex = resume_launch(
9136            HarnessId::CODEX,
9137            "codex-session",
9138            Path::new("/tmp/project"),
9139            ResumePolicy::Yolo,
9140        )
9141        .unwrap_or_else(|_| panic!("Codex resume launch must be registered"));
9142        assert_eq!(codex.program, "codex");
9143        assert_eq!(
9144            codex.arguments,
9145            [
9146                "-c",
9147                "check_for_update_on_startup=false",
9148                "-c",
9149                "projects.\"/tmp/project\".trust_level=\"trusted\"",
9150                "--dangerously-bypass-approvals-and-sandbox",
9151                "--dangerously-bypass-hook-trust",
9152                "resume",
9153                "codex-session",
9154            ]
9155        );
9156
9157        let gemini = resume_launch(
9158            HarnessId::GEMINI,
9159            "gemini-session",
9160            Path::new("/tmp/project"),
9161            ResumePolicy::Yolo,
9162        )
9163        .unwrap_or_else(|_| panic!("Gemini resume launch must be registered"));
9164        assert_eq!(gemini.program, "gemini");
9165        assert_eq!(gemini.arguments, ["--yolo", "--resume", "gemini-session"]);
9166
9167        let goose = resume_launch(
9168            HarnessId::GOOSE,
9169            "goose-session",
9170            Path::new("/tmp/project"),
9171            ResumePolicy::Yolo,
9172        )
9173        .unwrap_or_else(|_| panic!("Goose resume launch must be registered"));
9174        assert_eq!(goose.program, "goose");
9175        assert_eq!(
9176            goose.arguments,
9177            ["session", "--resume", "--session-id", "goose-session"]
9178        );
9179
9180        let supercode = resume_launch(
9181            HarnessId::SUPERCODE,
9182            "supercode-session",
9183            Path::new("/tmp/project"),
9184            ResumePolicy::Yolo,
9185        )
9186        .unwrap_or_else(|_| panic!("Supercode resume launch must be registered"));
9187        assert_eq!(supercode.program, "supercode");
9188        assert_eq!(
9189            supercode.arguments,
9190            ["--dangerous", "resume", "supercode-session"]
9191        );
9192    }
9193
9194    #[test]
9195    fn diagonal_artifacts_preserve_claude_subagents_and_grok_bundle_members() {
9196        let temp = std::env::temp_dir().join(format!(
9197            "supercode-harness-artifact-{}-{}",
9198            std::process::id(),
9199            generated_session_id()
9200        ));
9201        let main_path = temp.join("parent.jsonl");
9202        let subagent_path = temp.join("parent/subagents/agent-child.jsonl");
9203        std::fs::create_dir_all(subagent_path.parent().unwrap()).unwrap();
9204        let fixture = std::fs::read_to_string(
9205            PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9206                .join("tests/fixtures/claude_code_session.jsonl"),
9207        )
9208        .unwrap();
9209        let parent = fixture.trim_end_matches('\n');
9210        let child = fixture.trim_end_matches('\n');
9211        std::fs::write(&main_path, parent).unwrap();
9212        std::fs::write(&subagent_path, child).unwrap();
9213        let locator = SessionLocator {
9214            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9215            session_id: "213bb148-51ea-453f-9206-f8b4b1168547".into(),
9216            storage: StorageLocator::File {
9217                path: main_path.clone(),
9218            },
9219        };
9220        let mut service = HarnessSessionService::new();
9221        let claude = service.handle(request(
9222            1,
9223            "harness.v1.sessions.translate",
9224            json!({"locator": locator, "target_harness": "claude-code"}),
9225        ));
9226        let artifact = &claude["result"]["artifact"];
9227        assert_eq!(artifact["fidelity"], "byte_lossless");
9228        assert_eq!(artifact["content"], parent);
9229        let files = artifact["files"].as_array().unwrap();
9230        assert!(files.iter().any(|file| {
9231            file["role"] == "subagent"
9232                && file["path"]
9233                    .as_str()
9234                    .is_some_and(|path| path.ends_with("/subagents/agent-child.jsonl"))
9235                && file["content"] == child
9236        }));
9237        assert!(!artifact["content"].as_str().unwrap().ends_with('\n'));
9238
9239        let grok = service.handle(request(
9240            2,
9241            "harness.v1.sessions.translate",
9242            json!({"locator": grok_locator(), "target_harness": "grok"}),
9243        ));
9244        let files = grok["result"]["artifact"]["files"].as_array().unwrap();
9245        for name in ["summary.json", "updates.jsonl"] {
9246            let expected = std::fs::read_to_string(
9247                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9248                    .join("tests/fixtures/grok_session")
9249                    .join(name),
9250            )
9251            .unwrap();
9252            assert!(files.iter().any(|file| {
9253                file["path"] == name && file["role"] == "bundle" && file["content"] == expected
9254            }));
9255        }
9256        std::fs::remove_dir_all(temp).ok();
9257    }
9258
9259    #[test]
9260    fn every_non_grok_handoff_mints_and_uses_a_fresh_target_identity() {
9261        let mut service = HarnessSessionService::new();
9262        let source = pi_locator();
9263        for (target, format) in [
9264            ("claude-code", SessionFormat::ClaudeCode),
9265            ("codex", SessionFormat::Codex),
9266            ("opencode", SessionFormat::OpenCode),
9267            ("pi", SessionFormat::Pi),
9268        ] {
9269            let result = service.handle(request(
9270                1,
9271                "harness.v1.sessions.handoff",
9272                json!({"locator": source, "target_harness": target, "cwd": "/tmp/project"}),
9273            ));
9274            let artifact = &result["result"]["artifact"];
9275            let target_id = artifact["session_id"].as_str().unwrap();
9276            assert_ne!(target_id, source.session_id, "{target}");
9277            let parsed = Session::load_str(artifact["content"].as_str().unwrap(), format).unwrap();
9278            assert_eq!(
9279                parsed.meta.session_id.as_deref(),
9280                Some(target_id),
9281                "{target}"
9282            );
9283            if target != "pi" {
9284                assert!(result["result"]["launch"]["arguments"]
9285                    .as_array()
9286                    .unwrap()
9287                    .iter()
9288                    .any(|argument| argument == target_id));
9289            }
9290            if target == "opencode" {
9291                assert!(target_id.starts_with("ses_"));
9292                fn assert_session_ids(value: &Value, target_id: &str) {
9293                    match value {
9294                        Value::Object(fields) => {
9295                            if let Some(session_id) = fields.get("sessionID") {
9296                                assert_eq!(session_id, target_id);
9297                            }
9298                            for child in fields.values() {
9299                                assert_session_ids(child, target_id);
9300                            }
9301                        }
9302                        Value::Array(values) => {
9303                            for child in values {
9304                                assert_session_ids(child, target_id);
9305                            }
9306                        }
9307                        _ => {}
9308                    }
9309                }
9310                let document: Value =
9311                    serde_json::from_str(artifact["content"].as_str().unwrap()).unwrap();
9312                assert_session_ids(&document, target_id);
9313            }
9314        }
9315
9316        let first = service.handle(request(
9317            2,
9318            "harness.v1.sessions.handoff",
9319            json!({"locator": source, "target_harness": "codex"}),
9320        ));
9321        let second = service.handle(request(
9322            3,
9323            "harness.v1.sessions.handoff",
9324            json!({"locator": source, "target_harness": "codex"}),
9325        ));
9326        assert_ne!(
9327            first["result"]["artifact"]["session_id"],
9328            second["result"]["artifact"]["session_id"]
9329        );
9330    }
9331
9332    #[test]
9333    fn grok_handoff_uses_the_official_importer_contract() {
9334        let mut service = HarnessSessionService::new();
9335        let source = opencode_locator();
9336        let response = service.handle(request(
9337            1,
9338            "harness.v1.sessions.handoff",
9339            json!({
9340                "locator": source,
9341                "target_harness": "grok",
9342                "cwd": "/tmp/grok-handoff-project",
9343            }),
9344        ));
9345        let result = &response["result"];
9346
9347        // The target is Grok, but the artifact truthfully names the Claude Code wire
9348        // format accepted by Grok's official importer. Raw Grok chat_history JSONL is
9349        // not a complete stock-resumable bundle.
9350        assert_eq!(result["artifact"]["target_harness"], "claude-code");
9351        assert!(result["artifact"]["suggested_filename"]
9352            .as_str()
9353            .unwrap()
9354            .ends_with(".grok-import.claude-code.jsonl"));
9355        let artifact = Session::load_str(
9356            result["artifact"]["content"].as_str().unwrap(),
9357            SessionFormat::ClaudeCode,
9358        )
9359        .unwrap();
9360        assert_eq!(
9361            artifact.meta.cwd.as_deref(),
9362            Some(Path::new("/tmp/grok-handoff-project"))
9363        );
9364        let target_session_id = artifact.meta.session_id.as_deref().unwrap();
9365        assert_eq!(target_session_id.len(), 36);
9366        assert_eq!(target_session_id.as_bytes()[14], b'4');
9367        assert_ne!(target_session_id, opencode_locator().session_id);
9368        assert_eq!(
9369            result["artifact"]["session_id"],
9370            artifact.meta.session_id.as_deref().unwrap()
9371        );
9372
9373        assert_eq!(
9374            result["materialize"]["arguments"],
9375            json!(["import", "--json", "{artifact_path}"])
9376        );
9377        assert_eq!(
9378            result["launch"]["arguments"],
9379            json!(["--resume", "{imported_session_id}", "--fork-session"])
9380        );
9381        assert!(result["note"]
9382            .as_str()
9383            .unwrap()
9384            .contains("outcome=imported"));
9385        assert!(!result["launch"]["arguments"]
9386            .as_array()
9387            .unwrap()
9388            .iter()
9389            .any(|argument| argument == &opencode_locator().session_id));
9390    }
9391
9392    #[tokio::test]
9393    async fn inventory_rejects_unknown_harnesses_and_runtime_attach_is_honest() {
9394        let mut service = HarnessSessionService::new();
9395        let inventory = service
9396            .handle_async(request(
9397                1,
9398                "harness.v1.harnesses.list",
9399                json!({"harnesses": ["missing"]}),
9400            ))
9401            .await;
9402        assert_eq!(inventory["error"]["code"], -32602);
9403
9404        let attached = service
9405            .handle_async(request(
9406                2,
9407                "harness.v1.runtimes.attach_existing",
9408                json!({"harness": "codex", "runtime_id": "thread-1"}),
9409            ))
9410            .await;
9411        assert_eq!(attached["error"]["code"], -32000);
9412        assert!(attached["error"]["message"]
9413            .as_str()
9414            .unwrap()
9415            .contains("runtimes.resume"));
9416    }
9417
9418    #[test]
9419    fn invalid_params_and_unknown_methods_use_json_rpc_errors() {
9420        let mut service = HarnessSessionService::new();
9421        let invalid = service.handle(request(1, "harness.v1.sessions.load", json!({})));
9422        assert_eq!(invalid["error"]["code"], -32602);
9423        let unknown = service.handle(request(2, "harness.v1.unknown", json!({})));
9424        assert_eq!(unknown["error"]["code"], -32601);
9425    }
9426
9427    #[cfg(unix)]
9428    #[tokio::test]
9429    // The test mutates process-wide harness environment and deliberately
9430    // holds the global test lock until every async runtime operation ends.
9431    #[allow(clippy::await_holding_lock)]
9432    async fn async_service_drives_a_generic_acp_runtime() {
9433        let _environment_guard = crate::live_runtime::test_environment_lock();
9434        let script = r#"
9435            i=0
9436            while IFS= read -r line; do
9437              i=$((i + 1))
9438              case "$i" in
9439                1) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{},"authMethods":[]}}' ;;
9440                2) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"svc_acp"}}' ;;
9441                3)
9442                  printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ok"}}}}'
9443                  printf '%s\n' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}'
9444                  ;;
9445                4)
9446                  printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"from terminal"}}}}'
9447                  printf '%s\n' '{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}'
9448                  ;;
9449              esac
9450            done
9451        "#;
9452        let mut service = HarnessSessionService::new();
9453        let started = service
9454            .handle_async(request(
9455                1,
9456                "harness.v1.runtimes.start",
9457                json!({
9458                    "harness": "codex",
9459                    "protocol": "acp",
9460                    "cwd": std::env::current_dir().unwrap(),
9461                    "launch": {"program": "/bin/sh", "arguments": ["-c", script], "env": {}},
9462                }),
9463            ))
9464            .await;
9465        assert_eq!(started["result"]["connection"], "runtime-1");
9466        assert_eq!(started["result"]["handle"]["runtime_id"], "svc_acp");
9467
9468        let terminal = service
9469            .handle_async(request(
9470                9,
9471                "harness.v1.runtimes.terminal_instructions",
9472                json!({"connection":"runtime-1"}),
9473            ))
9474            .await;
9475        let arguments = terminal["result"]["launch"]["arguments"]
9476            .as_array()
9477            .expect("hosted runtime should return terminal arguments");
9478        let endpoint_index = arguments
9479            .iter()
9480            .position(|value| value == "--endpoint")
9481            .expect("terminal command should use an opaque endpoint");
9482        let endpoint = LiveRuntimeEndpoint::parse(
9483            arguments[endpoint_index + 1]
9484                .as_str()
9485                .expect("endpoint argument should be text"),
9486        )
9487        .unwrap();
9488        assert!(!terminal.to_string().contains("Bearer"));
9489        let workspace = std::env::current_dir().unwrap();
9490        let receipt = resolve_live_runtime(
9491            &endpoint,
9492            &LiveRuntimeSource {
9493                harness: "codex".into(),
9494                session_id: "svc_acp".into(),
9495                workspace,
9496            },
9497        )
9498        .unwrap();
9499        let remote = crate::HttpFrontendRuntime::connect(receipt.base_url, receipt.token)
9500            .await
9501            .unwrap();
9502        let mut attachment = crate::FrontendRuntime::attach(remote.as_ref(), 100)
9503            .await
9504            .unwrap();
9505
9506        let sent = service
9507            .handle_async(request(
9508                2,
9509                "harness.v1.runtimes.send_input",
9510                json!({"connection": "runtime-1", "text": "hi"}),
9511            ))
9512            .await;
9513        assert_eq!(sent["result"]["turn_id"], "3");
9514
9515        let mut events = Vec::new();
9516        for _ in 0..20 {
9517            events.extend(service.poll_runtimes().await);
9518            if events.len() >= 2 {
9519                break;
9520            }
9521            tokio::time::sleep(Duration::from_millis(2)).await;
9522        }
9523        assert!(events
9524            .iter()
9525            .any(|event| { event["params"]["event"]["kind"] == "session/update" }));
9526        assert!(events.iter().any(|event| {
9527            event["params"]["event"]["kind"] == "supercode/acp_request_completed"
9528        }));
9529
9530        let saw_editor_reply = tokio::time::timeout(Duration::from_secs(2), async {
9531            loop {
9532                let event = attachment.next_event().await.unwrap();
9533                if event.kind == "text_delta" && event.payload["text"] == "ok" {
9534                    break;
9535                }
9536            }
9537        })
9538        .await;
9539        assert!(
9540            saw_editor_reply.is_ok(),
9541            "terminal should observe the editor-driven turn"
9542        );
9543
9544        crate::FrontendRuntime::submit(remote.as_ref(), "DRIVE FROM TERMINAL".into())
9545            .await
9546            .unwrap();
9547        let saw_terminal_reply = tokio::time::timeout(Duration::from_secs(2), async {
9548            loop {
9549                let event = attachment.next_event().await.unwrap();
9550                if event.kind == "text_delta" && event.payload["text"] == "from terminal" {
9551                    break;
9552                }
9553            }
9554        })
9555        .await;
9556        assert!(
9557            saw_terminal_reply.is_ok(),
9558            "terminal should drive the same runtime"
9559        );
9560
9561        let closed = service
9562            .handle_async(request(
9563                3,
9564                "harness.v1.runtimes.close",
9565                json!({"connection": "runtime-1"}),
9566            ))
9567            .await;
9568        assert_eq!(closed["result"]["closed"], true);
9569    }
9570
9571    /// UNI-7 dev/02: a RUNNING mock gateway is detected through the real
9572    /// openclaw probe (config-declared endpoint, TCP connect), and an ACTIVE
9573    /// hermes WAL is detected through the real WAL-freshness probe; the
9574    /// negative sides (no listener, stale WAL, no config) stay undetected.
9575    #[test]
9576    fn running_instances_are_detected_from_mock_gateway_and_active_wal() {
9577        let home = connect_scratch_home("uni7-running");
9578
9579        // No config at all: hermes has no default endpoint, so no detection.
9580        // (openclaw's no-config behavior now probes its DOCUMENTED default
9581        // endpoint ws://127.0.0.1:18789 — see the connect launch's
9582        // `default_address` — which is real box state a hermetic test must
9583        // not assert either way; the closed-port negative below covers the
9584        // no-listener side deterministically.)
9585        assert!(probe_hermes_running(&home, 300_000).is_none());
9586
9587        // Mock gateway: a real TCP listener on an ephemeral port, declared in
9588        // the harness's own config file.
9589        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9590        let port = listener.local_addr().unwrap().port();
9591        std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9592        std::fs::write(
9593            home.join(".openclaw/openclaw.json"),
9594            format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9595        )
9596        .unwrap();
9597        let running = probe_openclaw_running(&home).expect("listening gateway must be detected");
9598        assert!(matches!(
9599            running.method,
9600            RunningInstanceMethod::GatewayConnect
9601        ));
9602        assert!(running.evidence.contains(&format!("127.0.0.1:{port}")));
9603        drop(listener);
9604        // Parallel tests also bind ephemeral loopback ports, so a just-freed
9605        // port can be re-bound by a NEIGHBORING test between drop and probe.
9606        // Detection on a closed port must fail — retry on a fresh port when
9607        // the freed one was recycled by someone else.
9608        let mut closed_detected = probe_openclaw_running(&home).is_some();
9609        for _ in 0..3 {
9610            if !closed_detected {
9611                break;
9612            }
9613            let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9614            let port = listener.local_addr().unwrap().port();
9615            drop(listener);
9616            std::fs::write(
9617                home.join(".openclaw/openclaw.json"),
9618                format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9619            )
9620            .unwrap();
9621            closed_detected = probe_openclaw_running(&home).is_some();
9622        }
9623        assert!(
9624            !closed_detected,
9625            "a closed gateway must not read as running"
9626        );
9627
9628        // gateway.url form takes precedence over port.
9629        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9630        let port = listener.local_addr().unwrap().port();
9631        std::fs::write(
9632            home.join(".openclaw/openclaw.json"),
9633            format!(r#"{{"gateway": {{"url": "ws://127.0.0.1:{port}", "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9634        )
9635        .unwrap();
9636        assert!(probe_openclaw_running(&home).is_some());
9637        drop(listener);
9638
9639        // Hermes: an ACTIVE WAL (fresh stamp) is detected; a stale one is not.
9640        std::fs::create_dir_all(home.join(".hermes")).unwrap();
9641        let wal = home.join(".hermes/state.db-wal");
9642        std::fs::write(&wal, b"wal").unwrap();
9643        let running = probe_hermes_running(&home, 300_000).expect("fresh WAL must be detected");
9644        assert!(matches!(
9645            running.method,
9646            RunningInstanceMethod::StoreWalActivity
9647        ));
9648        assert!(running.evidence.contains("state.db-wal"));
9649        let stale = std::time::SystemTime::now() - std::time::Duration::from_secs(3_600);
9650        std::fs::File::options()
9651            .append(true)
9652            .open(&wal)
9653            .unwrap()
9654            .set_modified(stale)
9655            .unwrap();
9656        assert!(
9657            probe_hermes_running(&home, 300_000).is_none(),
9658            "a stale WAL (crash leftover) must not read as running"
9659        );
9660    }
9661
9662    fn connect_scratch_home(tag: &str) -> PathBuf {
9663        let dir = std::env::temp_dir().join(format!(
9664            "supercode-connect-service-{tag}-{}-{}",
9665            std::process::id(),
9666            std::time::SystemTime::now()
9667                .duration_since(std::time::UNIX_EPOCH)
9668                .unwrap()
9669                .as_nanos()
9670        ));
9671        std::fs::create_dir_all(&dir).unwrap();
9672        dir
9673    }
9674
9675    /// Minimal HTTP responder that speaks just enough OpenCode server to
9676    /// accept a health check, create a session, and hold an SSE stream open,
9677    /// while recording each request line with its Authorization header.
9678    async fn mock_opencode_endpoint() -> (String, tokio::sync::mpsc::UnboundedReceiver<String>) {
9679        use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
9680        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
9681        let address = listener.local_addr().unwrap();
9682        let (request_sender, request_receiver) = tokio::sync::mpsc::unbounded_channel();
9683        tokio::spawn(async move {
9684            loop {
9685                let Ok((mut stream, _)) = listener.accept().await else {
9686                    break;
9687                };
9688                let request_sender = request_sender.clone();
9689                tokio::spawn(async move {
9690                    let (reader, mut writer) = stream.split();
9691                    let mut reader = BufReader::new(reader);
9692                    let mut request_line = String::new();
9693                    if reader.read_line(&mut request_line).await.unwrap_or(0) == 0 {
9694                        return;
9695                    }
9696                    let request_line = request_line.trim_end().to_string();
9697                    let mut authorization = String::new();
9698                    let mut content_length = 0usize;
9699                    loop {
9700                        let mut line = String::new();
9701                        if reader.read_line(&mut line).await.unwrap_or(0) == 0 {
9702                            return;
9703                        }
9704                        let line = line.trim_end();
9705                        if line.is_empty() {
9706                            break;
9707                        }
9708                        let lower = line.to_ascii_lowercase();
9709                        if let Some(value) = lower.strip_prefix("authorization:") {
9710                            authorization = value.trim().to_string();
9711                        }
9712                        if let Some(value) = lower.strip_prefix("content-length:") {
9713                            content_length = value.trim().parse().unwrap_or(0);
9714                        }
9715                    }
9716                    if content_length > 0 {
9717                        let mut body = vec![0u8; content_length];
9718                        let _ = reader.read_exact(&mut body).await;
9719                    }
9720                    let _ = request_sender.send(format!("{request_line} :: {authorization}"));
9721                    if request_line.starts_with("GET /event") {
9722                        let _ = writer
9723                            .write_all(
9724                                b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n\r\n",
9725                            )
9726                            .await;
9727                        tokio::time::sleep(std::time::Duration::from_secs(5)).await;
9728                        return;
9729                    }
9730                    let body = if request_line.starts_with("POST /session") {
9731                        r#"{"id":"mock-session"}"#
9732                    } else {
9733                        r#"{"status":"ok"}"#
9734                    };
9735                    let response = format!(
9736                        "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
9737                        body.len(),
9738                        body
9739                    );
9740                    let _ = writer.write_all(response.as_bytes()).await;
9741                });
9742            }
9743        });
9744        (format!("http://{address}"), request_receiver)
9745    }
9746
9747    fn connect_descriptor(protocol: &str) -> crate::HarnessSupportDescriptor {
9748        crate::HarnessSupportDescriptor {
9749            orchestration: Default::default(),
9750            id: HarnessId::from(HarnessId::OPENCODE),
9751            display_name: "OpenCode".into(),
9752            native: crate::NativeSupport {
9753                discover: crate::ImplementationKind::Absent,
9754                load: crate::ImplementationKind::Absent,
9755                follow: crate::ImplementationKind::Absent,
9756                import: crate::ImplementationKind::Absent,
9757                export: crate::ImplementationKind::Absent,
9758            },
9759            runtime: crate::RuntimeSupport {
9760                implementation: crate::ImplementationKind::BuiltIn,
9761                protocol: protocol.into(),
9762                default_launch: None,
9763                connect_launch: Some(crate::RuntimeConnectLaunch {
9764                    config_path: "~/opencode-tui.json".into(),
9765                    address_pointer: "/server/url".into(),
9766                    port_pointer: None,
9767                    default_address: None,
9768                    auth_pointer: Some("/server/token".into()),
9769                    protocol: protocol.into(),
9770                }),
9771                capabilities: crate::RuntimeCapabilities {
9772                    start_session: true,
9773                    resume_session: true,
9774                    attach_existing_process: true,
9775                    send_input: true,
9776                    stream_events: true,
9777                    interrupt: true,
9778                    steer: false,
9779                    respond_to_requests: true,
9780                },
9781            },
9782        }
9783    }
9784
9785    #[tokio::test]
9786    async fn connect_mode_descriptor_opens_a_running_endpoint_with_config_sourced_auth() {
9787        let (base_url, mut requests) = mock_opencode_endpoint().await;
9788        let home = connect_scratch_home("open");
9789        std::fs::write(
9790            home.join("opencode-tui.json"),
9791            format!(r#"{{"server": {{"url": "{base_url}", "token": "connect-secret"}}}}"#),
9792        )
9793        .unwrap();
9794
9795        let descriptor = connect_descriptor("opencode-http-sse");
9796        let backend = open_connect_descriptor(&descriptor, &home).unwrap();
9797        assert!(backend.capabilities().attach_existing_process);
9798
9799        let connection = backend
9800            .start(crate::RuntimeStartRequest {
9801                cwd: home.clone(),
9802                launch: None,
9803                mcp_servers: Vec::new(),
9804            })
9805            .await
9806            .unwrap();
9807        let handle = connection.handle();
9808        assert_eq!(handle.runtime_id, "mock-session");
9809        match &handle.endpoint {
9810            crate::RuntimeEndpoint::Http {
9811                base_url: endpoint, ..
9812            } => assert_eq!(endpoint, &base_url),
9813            other => panic!("connect mode must join the running endpoint, got {other:?}"),
9814        }
9815
9816        let mut seen = Vec::new();
9817        while let Ok(line) = requests.try_recv() {
9818            seen.push(line);
9819        }
9820        assert!(seen
9821            .iter()
9822            .any(|line| line.starts_with("GET /global/health")
9823                && line.contains("bearer connect-secret")));
9824        assert!(seen.iter().any(
9825            |line| line.starts_with("POST /session") && line.contains("bearer connect-secret")
9826        ));
9827    }
9828
9829    /// UNI-5 dev/02, contract corrected by the 2026-08-31 blind walk: the
9830    /// full connect-mode attach path against a MOCK gateway bridge — no live
9831    /// gateway, no model spend. A scripted fake `openclaw` binary (a)
9832    /// asserts the REAL bridge contract — the resolved --url on argv and the
9833    /// credential via --token-file (the real bridge ignores the env var; the
9834    /// endpoint comes from openclaw-native `gateway.remote.url`, never the
9835    /// schema-invalid `gateway.url`) — then (b) speaks scripted ACP:
9836    /// initialize advertising sessionCapabilities.{list,resume},
9837    /// session/resume rebinding the requested session (join), and a
9838    /// prompted turn.
9839    #[tokio::test]
9840    async fn openclaw_connect_mode_attaches_lists_and_resumes_via_a_mock_bridge() {
9841        let home = connect_scratch_home("openclaw");
9842        std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9843        std::fs::write(
9844            home.join(".openclaw/openclaw.json"),
9845            r#"{"gateway": {"remote": {"url": "ws://127.0.0.1:19789"}, "auth": {"mode": "token", "token": "mock-gateway-token"}}}"#,
9846        )
9847        .unwrap();
9848        let script = home.join("openclaw");
9849        std::fs::write(
9850            &script,
9851            r#"#!/bin/sh
9852# Fake `openclaw acp` bridge: verify the connect-mode contract, then speak ACP.
9853[ "$1" = "acp" ] || { echo "unexpected argv: $*" >&2; exit 9; }
9854[ "$2" = "--url" ] && [ "$3" = "ws://127.0.0.1:19789" ] || { echo "missing --url: $*" >&2; exit 9; }
9855[ "$4" = "--token-file" ] || { echo "missing --token-file: $*" >&2; exit 9; }
9856[ "$(cat "$5")" = "mock-gateway-token" ] || { echo "token file wrong" >&2; exit 9; }
9857while IFS= read -r line; do
9858  case "$line" in
9859    *'"initialize"'*)
9860      printf '%s
9861' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"sessionCapabilities":{"list":{},"resume":{}}},"agentInfo":{"name":"openclaw-acp","version":"2026.7.1-2"},"authMethods":[]}}' ;;
9862    *'"session/resume"'*)
9863      printf '%s
9864' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:main"}}' ;;
9865    *'"session/new"'*)
9866      printf '%s
9867' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:fresh"}}' ;;
9868    *'"session/prompt"'*)
9869      printf '%s
9870' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"agent:main:main","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"joined"}}}}'
9871      printf '%s
9872' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}' ;;
9873  esac
9874done
9875"#,
9876        )
9877        .unwrap();
9878        use std::os::unix::fs::PermissionsExt;
9879        std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
9880
9881        let mut descriptor = crate::harness_support_registry()
9882            .harnesses
9883            .into_iter()
9884            .find(|harness| harness.id.as_str() == HarnessId::OPENCLAW)
9885            .expect("openclaw must be registered");
9886        descriptor
9887            .runtime
9888            .connect_launch
9889            .as_mut()
9890            .unwrap()
9891            .config_path = "~/.openclaw/openclaw.json".into();
9892        descriptor.runtime.default_launch.as_mut().unwrap().program =
9893            script.to_string_lossy().into_owned();
9894        let backend = open_connect_descriptor(&descriptor, &home).unwrap();
9895        assert!(backend.capabilities().resume_session);
9896
9897        let joined = backend
9898            .attach(crate::RuntimeAttachRequest {
9899                runtime_id: "agent:main:main".into(),
9900                cwd: Some(home.clone()),
9901                launch: None,
9902            })
9903            .await;
9904        let mut connection = joined.expect("mock bridge attach must succeed");
9905        assert_eq!(connection.handle().runtime_id, "agent:main:main");
9906        let turn = connection
9907            .send_input(crate::RuntimeInput {
9908                text: "hello".into(),
9909                image_urls: Vec::new(),
9910            })
9911            .await;
9912        assert!(turn.is_ok(), "prompt through the mock bridge: {turn:?}");
9913        connection.close().await.unwrap();
9914    }
9915
9916    #[tokio::test]
9917    async fn connect_mode_fails_closed_without_a_protocol_client_or_config() {
9918        let home = connect_scratch_home("fail");
9919        std::fs::write(
9920            home.join("opencode-tui.json"),
9921            r#"{"server": {"url": "http://127.0.0.1:1", "token": "connect-secret"}}"#,
9922        )
9923        .unwrap();
9924
9925        let gateway_only = connect_descriptor("acp-v1-jsonrpc");
9926        let Err(error) = open_connect_descriptor(&gateway_only, &home) else {
9927            panic!("an ACP connect endpoint has no gateway client yet");
9928        };
9929        let message = format!("{error:?}");
9930        assert!(message.contains("acp-v1-jsonrpc"));
9931        assert!(!message.contains("connect-secret"));
9932
9933        let unreadable = connect_descriptor("opencode-http-sse");
9934        let missing_home = connect_scratch_home("missing");
9935        let Err(error) = open_connect_descriptor(&unreadable, &missing_home) else {
9936            panic!("an unreadable connect config must fail closed");
9937        };
9938        let message = format!("{error:?}");
9939        assert!(message.contains("opencode-tui.json"));
9940        assert!(!message.contains("connect-secret"));
9941    }
9942
9943    // ---------------------------------------------------------------------
9944    // ORCH-7 — `harness.v1.jobs.list` / `jobs.get` over the committed fixtures
9945    // ---------------------------------------------------------------------
9946
9947    fn jobs_fixture_root() -> PathBuf {
9948        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
9949    }
9950
9951    /// Point only the three job-bearing homes at the fixtures. Nothing else is
9952    /// read, so the host machine's own harness homes cannot leak into a row.
9953    fn jobs_fixture_homes() -> Value {
9954        let root = jobs_fixture_root();
9955        json!({
9956            "claude_code": root.join("claude_jobs_home/projects"),
9957            "hermes": root.join("hermes_home/state.db"),
9958            "openclaw": root.join("openclaw_home"),
9959        })
9960    }
9961
9962    fn jobs_list(params: Value) -> Value {
9963        let mut service = HarnessSessionService::new();
9964        service.handle(request(1, "harness.v1.jobs.list", params))
9965    }
9966
9967    fn job_row<'a>(result: &'a Value, id: &str) -> &'a Value {
9968        result["jobs"]
9969            .as_array()
9970            .expect("jobs is an array")
9971            .iter()
9972            .find(|job| job["id"] == id)
9973            .unwrap_or_else(|| panic!("no job `{id}` in {result}"))
9974    }
9975
9976    #[test]
9977    fn gateway_health_derives_from_running_probe_and_install_state() {
9978        let running = RunningInstance {
9979            method: RunningInstanceMethod::GatewayConnect,
9980            evidence: "gateway endpoint 127.0.0.1:18789 accepted a TCP connect".into(),
9981            checked_at_ms: 1,
9982        };
9983        let up = gateway_health(
9984            HarnessId::OPENCLAW,
9985            true,
9986            Some(&running),
9987            Some("2026.7.1-2"),
9988        );
9989        assert_eq!(up.state, GatewayState::Up);
9990        assert!(up.endpoint.as_deref().unwrap().starts_with("ws://"));
9991        assert_eq!(up.version.as_deref(), Some("2026.7.1-2"));
9992        // Hermes consults its own `gateway status` when the WAL heuristic says
9993        // nothing; a fake binary decides the verdict (the env var is global, so
9994        // the up/down cases run inside this one test, never in parallel).
9995        let dir = std::env::temp_dir().join(format!("supercode-orch17-{}", std::process::id()));
9996        std::fs::create_dir_all(&dir).unwrap();
9997        let fake = dir.join("hermes");
9998        let write_fake = |body: &str| {
9999            std::fs::write(&fake, format!("#!/bin/sh\n{body}\n")).unwrap();
10000            #[cfg(unix)]
10001            {
10002                use std::os::unix::fs::PermissionsExt;
10003                std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
10004            }
10005        };
10006        write_fake("echo '✗ Gateway service is not installed'");
10007        crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| {
10008            *slot.borrow_mut() = Some((
10009                HarnessId::HERMES.to_string(),
10010                fake.to_string_lossy().into_owned(),
10011            ))
10012        });
10013        let down = gateway_health(HarnessId::HERMES, true, None, None);
10014        assert_eq!(down.state, GatewayState::Down, "{down:?}");
10015        assert!(down.endpoint.is_none());
10016        assert!(down.evidence.contains("not installed"));
10017        write_fake("echo 'Launchd plist: /x/ai.hermes.gateway.plist'; echo '✓ Gateway is supervised by launchd (PID 4242)'");
10018        let idle_but_up = gateway_health(HarnessId::HERMES, true, None, Some("0.21.0"));
10019        assert_eq!(idle_but_up.state, GatewayState::Up, "{idle_but_up:?}");
10020        assert!(idle_but_up.evidence.contains("PID 4242"));
10021        write_fake("echo 'something unparseable'");
10022        let no_verdict = gateway_health(HarnessId::HERMES, true, None, None);
10023        assert_eq!(no_verdict.state, GatewayState::Down);
10024        assert!(no_verdict.evidence.contains("no verdict"));
10025        crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| *slot.borrow_mut() = None);
10026        let absent = gateway_health(HarnessId::HERMES, false, None, None);
10027        assert_eq!(absent.state, GatewayState::Unknown);
10028        let core = gateway_health(HarnessId::CODEX, true, None, Some("0.144.4"));
10029        assert_eq!(core.state, GatewayState::Unknown);
10030        assert!(core.evidence.contains("per session"));
10031    }
10032
10033    #[test]
10034    fn triggers_list_reads_both_stores_and_never_emits_secrets() {
10035        let response = triggers_list(json!({"homes": jobs_fixture_homes()}));
10036        let rows = response["result"]["triggers"]
10037            .as_array()
10038            .expect("triggers")
10039            .clone();
10040        let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10041        assert!(
10042            hermes.iter().any(|r| r["name"] == "deploys"
10043                && r["route"] == "/webhooks/deploys"
10044                && r["kind"] == "webhook"),
10045            "{rows:#?}"
10046        );
10047        let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10048        assert!(openclaw
10049            .iter()
10050            .any(|r| r["name"] == "wake" && r["kind"] == "builtin_wake"));
10051        assert!(openclaw.iter().any(|r| r["name"] == "gmail"
10052            && r["kind"] == "hook_mapping"
10053            && r["target"]["action"] == "agent"));
10054        let rendered = response.to_string();
10055        for secret in [
10056            "FAKE-WEBHOOK-HMAC-DO-NOT-EMIT",
10057            "FAKE-HOOK-TOKEN-DO-NOT-EMIT",
10058        ] {
10059            assert!(!rendered.contains(secret), "{rendered}");
10060        }
10061        let refused =
10062            triggers_list(json!({"harness": "claude-code", "homes": jobs_fixture_homes()}));
10063        assert_eq!(refused["error"]["code"], -32020, "{refused}");
10064    }
10065
10066    fn triggers_list(params: Value) -> Value {
10067        let mut service = HarnessSessionService::new();
10068        service.handle(request(1, "harness.v1.triggers.list", params))
10069    }
10070
10071    #[test]
10072    fn routes_list_reads_both_gateway_configs_and_flags_the_defaults() {
10073        let response = routes_list(json!({"homes": jobs_fixture_homes()}));
10074        let rows = response["result"]["routes"]
10075            .as_array()
10076            .expect("routes")
10077            .clone();
10078        let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10079        assert_eq!(hermes.len(), 2, "{rows:#?}");
10080        assert_eq!(hermes[0]["target"], "coder");
10081        assert_eq!(hermes[0]["match"]["platform"], "slack");
10082        assert_eq!(hermes[0]["match"]["chat_id"], "C0FIXTURE");
10083        assert_eq!(hermes[0]["specificity"], 4);
10084        assert_eq!(hermes[1]["default"], true);
10085        let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10086        assert!(
10087            openclaw.iter().any(|r| r["target"] == "design"
10088                && r["match"]["platform"] == "slack"
10089                && r["specificity"] == 1),
10090            "{openclaw:#?}"
10091        );
10092        assert!(openclaw.iter().any(|r| r["default"] == true));
10093        // A core harness has no routing concept and is refused, never an empty list.
10094        let refused = routes_list(json!({"harness": "codex", "homes": jobs_fixture_homes()}));
10095        assert_eq!(refused["error"]["code"], -32020, "{refused}");
10096    }
10097
10098    fn routes_list(params: Value) -> Value {
10099        let mut service = HarnessSessionService::new();
10100        service.handle(request(1, "harness.v1.routes.list", params))
10101    }
10102
10103    #[test]
10104    fn jobs_list_projects_every_fixture_store_onto_the_uniform_row() {
10105        let response = jobs_list(json!({"homes": jobs_fixture_homes()}));
10106        let result = &response["result"];
10107        let ids: Vec<&str> = result["jobs"]
10108            .as_array()
10109            .unwrap()
10110            .iter()
10111            .map(|job| job["id"].as_str().unwrap())
10112            .collect();
10113        assert_eq!(
10114            ids,
10115            vec![
10116                "release-watch",
10117                "toolu_wake_recheck",
10118                "digest-15m",
10119                "nightly-audit",
10120                "coder-standup",
10121                "ops-once-boot",
10122                "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10123                "8bb7d938-ca46-4a6d-90eb-c92331155566",
10124                "cron_standup",
10125                "cron_reindex",
10126            ],
10127            "{result}"
10128        );
10129
10130        // OpenClaw, pinned shape: rows come from `state/openclaw.sqlite`
10131        // (`cron_jobs.job_json` + runtime columns), captured from a real
10132        // 2026.7.1-2 gateway.
10133        let health = job_row(result, "85ad7832-896f-42be-af31-3e1ed2fbdc4b");
10134        assert_eq!(health["harness"], "openclaw");
10135        assert_eq!(health["schedule"]["kind"], "interval");
10136        assert_eq!(health["schedule"]["minutes"], 10.0);
10137        assert_eq!(health["session_target"], "isolated");
10138        assert_eq!(health["payload"]["kind"], "prompt");
10139        assert_eq!(health["payload"]["text"], "nightly health check");
10140        // ORCH-13: the mode word (`announce`) and the channel it announces on
10141        // (`last`) are separate facts, and the store keeps both — in
10142        // `job_json.delivery` and in the `delivery_*` columns beside it.
10143        assert_eq!(health["deliver"]["mode"], "announce");
10144        assert_eq!(health["deliver"]["target"], "last");
10145        assert_eq!(health["next_run_at"], "2026-09-03T06:52:26Z");
10146        let digest = job_row(result, "8bb7d938-ca46-4a6d-90eb-c92331155566");
10147        assert_eq!(digest["schedule"]["kind"], "cron");
10148        assert_eq!(digest["schedule"]["expr"], "0 9 * * 1");
10149        assert_eq!(digest["session_target"], "main");
10150        assert_eq!(digest["payload"]["kind"], "system_event");
10151
10152        // Claude Code: session-scoped, one recurring cron and one one-shot wakeup.
10153        let cron = job_row(result, "release-watch");
10154        assert_eq!(cron["harness"], "claude-code");
10155        assert_eq!(cron["scope"], "session");
10156        assert_eq!(cron["session_id"], "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f");
10157        assert_eq!(cron["schedule"]["kind"], "cron");
10158        assert_eq!(cron["schedule"]["expr"], "*/10 * * * *");
10159        assert_eq!(cron["schedule"]["display"], "*/10 * * * *");
10160        assert_eq!(cron["payload"]["kind"], "prompt");
10161        assert_eq!(cron["recurring"], true);
10162        assert_eq!(cron["deliver"]["target"], "session");
10163        let wakeup = job_row(result, "toolu_wake_recheck");
10164        assert_eq!(wakeup["payload"]["kind"], "wakeup");
10165        assert_eq!(wakeup["schedule"]["kind"], "once");
10166        assert_eq!(wakeup["recurring"], false);
10167        assert_eq!(wakeup["state"], "pending");
10168
10169        // Hermes: install-scoped, interval + origin delivery, and a paused cron.
10170        let interval = job_row(result, "digest-15m");
10171        assert_eq!(interval["harness"], "hermes");
10172        assert_eq!(interval["scope"], "install");
10173        assert_eq!(interval["profile"], Value::Null);
10174        assert_eq!(interval["schedule"]["kind"], "interval");
10175        assert_eq!(interval["schedule"]["minutes"], 15.0);
10176        assert_eq!(interval["schedule"]["display"], "every 15 min");
10177        assert_eq!(interval["deliver"]["target"], "origin");
10178        assert_eq!(interval["deliver"]["chat_id"], "-1002233445566");
10179        assert_eq!(interval["next_run_at"], "2026-09-02T11:15:00Z");
10180        assert_eq!(interval["last_status"], "ok");
10181        let nightly = job_row(result, "nightly-audit");
10182        assert_eq!(nightly["schedule"]["expr"], "0 3 * * *");
10183        assert_eq!(nightly["deliver"]["target"], "local");
10184        assert_eq!(nightly["enabled"], false);
10185        assert_eq!(nightly["state"], "paused");
10186        // The per-profile store carries the profile name from its own path.
10187        let profiled = job_row(result, "ops-once-boot");
10188        assert_eq!(profiled["profile"], "ops");
10189        assert_eq!(profiled["schedule"]["kind"], "once");
10190        assert_eq!(profiled["schedule"]["run_at"], "2026-09-03T06:00:00Z");
10191        assert_eq!(profiled["payload"]["kind"], "script");
10192        // An explicit `<platform>:<chat>` target carries the chat itself.
10193        assert_eq!(profiled["deliver"]["target"], "slack:C0429ABCD");
10194        assert_eq!(profiled["deliver"]["chat_id"], "C0429ABCD");
10195        assert_eq!(profiled["recurring"], false);
10196
10197        // ORCH-13: a job delivering to its creating conversation carries that
10198        // conversation's whole surface — platform word, chat AND thread.
10199        let standup_to_group = job_row(result, "coder-standup");
10200        assert_eq!(standup_to_group["deliver"]["target"], "origin");
10201        assert_eq!(standup_to_group["deliver"]["chat_id"], "-100777");
10202        assert_eq!(standup_to_group["deliver"]["thread_id"], "55");
10203        // Hermes has no mode word and routes by adapter profile, not account.
10204        assert!(standup_to_group["deliver"]["mode"].is_null());
10205        assert!(standup_to_group["deliver"]["account"].is_null());
10206
10207        // OpenClaw: the session target and the delivery mode are the row's own
10208        // columns, not a footnote.
10209        let standup = job_row(result, "cron_standup");
10210        assert_eq!(standup["harness"], "openclaw");
10211        assert_eq!(standup["session_target"], "isolated");
10212        assert_eq!(standup["deliver"]["mode"], "announce");
10213        assert_eq!(standup["deliver"]["target"], "slack");
10214        assert_eq!(standup["deliver"]["chat_id"], "C0429ABCD");
10215        assert_eq!(standup["payload"]["kind"], "prompt");
10216        assert_eq!(standup["profile"], "main");
10217        let reindex = job_row(result, "cron_reindex");
10218        assert_eq!(reindex["session_target"], "main");
10219        assert_eq!(reindex["payload"]["kind"], "system_event");
10220        assert_eq!(reindex["schedule"]["kind"], "interval");
10221        assert_eq!(reindex["schedule"]["display"], "every 240 min");
10222        assert_eq!(reindex["enabled"], false);
10223
10224        // Every store consulted is named, so an empty answer is never silent.
10225        let states: Vec<(&str, &str)> = result["sources"]
10226            .as_array()
10227            .unwrap()
10228            .iter()
10229            .map(|source| {
10230                (
10231                    source["harness"].as_str().unwrap(),
10232                    source["state"].as_str().unwrap(),
10233                )
10234            })
10235            .collect();
10236        // The `coder` profile home has no cron store at all: it is named as
10237        // `absent_store`, not skipped, so "this profile schedules nothing" and
10238        // "this profile was never looked at" stay distinguishable.
10239        assert_eq!(
10240            states,
10241            vec![
10242                ("claude-code", "scanned"),
10243                ("hermes", "read"),
10244                ("hermes", "absent_store"),
10245                ("hermes", "read"),
10246                ("openclaw", "read"),
10247                ("openclaw", "read"),
10248            ],
10249            "{result}"
10250        );
10251    }
10252
10253    #[test]
10254    fn jobs_list_filters_by_harness_session_and_profile() {
10255        let by_harness = jobs_list(json!({"harness": "openclaw", "homes": jobs_fixture_homes()}));
10256        let ids: Vec<&str> = by_harness["result"]["jobs"]
10257            .as_array()
10258            .unwrap()
10259            .iter()
10260            .map(|job| job["id"].as_str().unwrap())
10261            .collect();
10262        assert_eq!(
10263            ids,
10264            vec![
10265                "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10266                "8bb7d938-ca46-4a6d-90eb-c92331155566",
10267                "cron_standup",
10268                "cron_reindex",
10269            ]
10270        );
10271
10272        let by_session = jobs_list(json!({
10273            "session": "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
10274            "homes": jobs_fixture_homes(),
10275        }));
10276        let jobs = by_session["result"]["jobs"].as_array().unwrap();
10277        assert_eq!(jobs.len(), 2, "{by_session}");
10278        assert!(jobs
10279            .iter()
10280            .all(|job| job["harness"] == "claude-code" && job["scope"] == "session"));
10281
10282        let by_profile = jobs_list(json!({
10283            "harness": "hermes",
10284            "profile": "ops",
10285            "homes": jobs_fixture_homes(),
10286        }));
10287        let jobs = by_profile["result"]["jobs"].as_array().unwrap();
10288        assert_eq!(jobs.len(), 1, "{by_profile}");
10289        assert_eq!(jobs[0]["id"], "ops-once-boot");
10290    }
10291
10292    #[test]
10293    fn jobs_get_answers_with_the_row_and_the_verbatim_native_record() {
10294        let mut service = HarnessSessionService::new();
10295        let hermes = service.handle(request(
10296            1,
10297            "harness.v1.jobs.get",
10298            json!({"harness": "hermes", "id": "digest-15m", "homes": jobs_fixture_homes()}),
10299        ));
10300        assert_eq!(hermes["result"]["job"]["schedule"]["kind"], "interval");
10301        // Native fields the uniform row does not carry survive on `source`.
10302        assert_eq!(hermes["result"]["source"]["provider"], "nous");
10303        assert_eq!(hermes["result"]["source"]["failure_deliver"], "local");
10304
10305        let claude = service.handle(request(
10306            2,
10307            "harness.v1.jobs.get",
10308            json!({"harness": "claude-code", "id": "release-watch", "homes": jobs_fixture_homes()}),
10309        ));
10310        assert_eq!(claude["result"]["job"]["payload"]["kind"], "prompt");
10311        assert_eq!(
10312            claude["result"]["source"]["tool_use_id"],
10313            "toolu_cron_release_watch"
10314        );
10315
10316        let missing = service.handle(request(
10317            3,
10318            "harness.v1.jobs.get",
10319            json!({"harness": "hermes", "id": "no-such-job", "homes": jobs_fixture_homes()}),
10320        ));
10321        assert!(missing["error"]["message"]
10322            .as_str()
10323            .is_some_and(|message| message.contains("no scheduled job `no-such-job`")));
10324    }
10325
10326    #[test]
10327    fn jobs_refuse_a_harness_without_a_scheduled_job_concept() {
10328        let mut service = HarnessSessionService::new();
10329        for (id, method, params) in [
10330            (
10331                1,
10332                "harness.v1.jobs.list",
10333                json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10334            ),
10335            (
10336                2,
10337                "harness.v1.jobs.get",
10338                json!({"harness": "codex", "id": "anything"}),
10339            ),
10340        ] {
10341            let response = service.handle(request(id, method, params));
10342            assert_eq!(response["error"]["code"], -32020, "{response}");
10343            assert!(response["error"]["message"]
10344                .as_str()
10345                .is_some_and(|message| message.contains("has no scheduled jobs")));
10346            assert!(response.get("result").is_none());
10347        }
10348    }
10349
10350    #[test]
10351    fn jobs_list_reports_a_migrated_openclaw_store_as_absent_instead_of_failing() {
10352        let scratch = std::env::temp_dir().join(format!(
10353            "supercode-jobs-migrated-{}-{}",
10354            std::process::id(),
10355            generated_session_id()
10356        ));
10357        std::fs::create_dir_all(&scratch).unwrap();
10358        let response = jobs_list(json!({
10359            "harness": "openclaw",
10360            "homes": {"openclaw": scratch.clone()},
10361        }));
10362        let result = &response["result"];
10363        assert_eq!(result["jobs"].as_array().unwrap().len(), 0, "{result}");
10364        assert_eq!(result["sources"][0]["state"], "absent_store");
10365        assert_eq!(result["sources"][0]["harness"], "openclaw");
10366        std::fs::remove_dir_all(&scratch).ok();
10367    }
10368
10369    // ---------------------------------------------------------------------
10370    // ORCH-8 — `harness.v1.runs.list` / `runs.get` over the committed fire
10371    // stores: Hermes's `cron/executions.db` (root home + profile home) and
10372    // OpenClaw's `cron_run_logs`. Every fixture row is written by
10373    // `tests/fixtures/gen_runs_fixtures.py` against the harnesses' own DDL.
10374    // ---------------------------------------------------------------------
10375
10376    /// The health job in the committed OpenClaw fixture, which fired twice.
10377    const OPENCLAW_HEALTH_JOB: &str = "85ad7832-896f-42be-af31-3e1ed2fbdc4b";
10378    /// The digest job, whose single fire predates run ids.
10379    const OPENCLAW_DIGEST_JOB: &str = "8bb7d938-ca46-4a6d-90eb-c92331155566";
10380
10381    fn runs_list(params: Value) -> Value {
10382        let mut service = HarnessSessionService::new();
10383        service.handle(request(1, "harness.v1.runs.list", params))
10384    }
10385
10386    fn run_row<'a>(result: &'a Value, id: &str) -> &'a Value {
10387        result["runs"]
10388            .as_array()
10389            .expect("runs is an array")
10390            .iter()
10391            .find(|run| run["id"] == id)
10392            .unwrap_or_else(|| panic!("no run `{id}` in {result}"))
10393    }
10394
10395    #[test]
10396    fn runs_list_projects_both_fixture_stores_onto_the_uniform_row() {
10397        let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10398        let result = &response["result"];
10399        let ids: Vec<&str> = result["runs"]
10400            .as_array()
10401            .expect("runs is an array")
10402            .iter()
10403            .map(|run| run["id"].as_str().unwrap())
10404            .collect();
10405        let digest_fire = format!("{OPENCLAW_DIGEST_JOB}#1");
10406        assert_eq!(
10407            ids,
10408            vec![
10409                // Hermes, newest claim first, root ledger then profile ledger.
10410                "b2c3d4e5f60718293a4b5c6d7e8f9012",
10411                "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10412                "c3d4e5f60718293a4b5c6d7e8f901234",
10413                "f60718293a4b5c6d7e8f901234567890",
10414                "e5f60718293a4b5c6d7e8f9012345678",
10415                "d4e5f60718293a4b5c6d7e8f90123456",
10416                // OpenClaw, newest `ts` first.
10417                "run_health_0002",
10418                digest_fire.as_str(),
10419                "run_health_0001",
10420            ],
10421            "{result}"
10422        );
10423
10424        // The harness's OWN outcome word survives; nothing is renamed onto a
10425        // shared vocabulary.
10426        let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10427        assert_eq!(failed["harness"], "hermes");
10428        assert_eq!(failed["job_id"], "job42");
10429        assert_eq!(failed["status"], "failed");
10430        assert_eq!(failed["error"], "provider returned 500 after 3 attempts");
10431        assert_eq!(failed["claimed_at"], "2026-09-02T13:05:00.100442");
10432
10433        // Hermes's `unknown` — an attempt whose owner died before writing a
10434        // terminal state — is a fourth status, not folded into `failed`.
10435        let abandoned = run_row(result, "d4e5f60718293a4b5c6d7e8f90123456");
10436        assert_eq!(abandoned["status"], "unknown");
10437        assert_eq!(abandoned["job_id"], "ops-once-boot");
10438
10439        // An unterminated fire has no finish, and no session is invented.
10440        let running = run_row(result, "c3d4e5f60718293a4b5c6d7e8f901234");
10441        assert_eq!(running["status"], "running");
10442        assert!(running["finished_at"].is_null(), "{running}");
10443        assert!(running["session_id"].is_null(), "{running}");
10444
10445        // OpenClaw records the session on the row itself, and epoch-ms
10446        // timestamps are rendered as RFC 3339.
10447        let ok = run_row(result, "run_health_0001");
10448        assert_eq!(ok["harness"], "openclaw");
10449        assert_eq!(ok["job_id"], OPENCLAW_HEALTH_JOB);
10450        assert_eq!(ok["status"], "ok");
10451        assert_eq!(ok["started_at"], "2026-09-02T08:30:00.000Z");
10452        assert_eq!(ok["finished_at"], "2026-09-02T08:30:30.000Z");
10453        assert_eq!(ok["session_id"], "3dd577ae-a0a3-4b5b-8063-f402be4f5fd4");
10454        // OpenClaw's run log is written once, at finish: there is no claim.
10455        assert!(ok["claimed_at"].is_null(), "{ok}");
10456
10457        // A run-log row with no `run_id` falls back to the store's own
10458        // `(job_id, seq)` key rather than being dropped.
10459        assert_eq!(run_row(result, &digest_fire)["status"], "skipped");
10460
10461        // ORCH-13: a fire whose delivery nothing recorded says so, rather than
10462        // borrowing a neighbouring fire's outcome. Both of these ran on jobs
10463        // that deliver `local` (or have no job record at all), so no
10464        // obligation is addressed to a surface they could match.
10465        for id in [
10466            "b2c3d4e5f60718293a4b5c6d7e8f9012",
10467            "d4e5f60718293a4b5c6d7e8f90123456",
10468        ] {
10469            assert!(run_row(result, id)["delivery"].is_null(), "{id}");
10470        }
10471
10472        // Every store consulted is named, including the profile home that has
10473        // no ledger — an empty history and an absent store are different.
10474        let sources = result["sources"].as_array().unwrap();
10475        let states: Vec<(&str, &str)> = sources
10476            .iter()
10477            .map(|source| {
10478                (
10479                    source["harness"].as_str().unwrap(),
10480                    source["state"].as_str().unwrap(),
10481                )
10482            })
10483            .collect();
10484        assert_eq!(
10485            states,
10486            vec![
10487                ("hermes", "read"),
10488                ("hermes", "absent_store"),
10489                ("hermes", "read"),
10490                ("openclaw", "read"),
10491            ],
10492            "{result}"
10493        );
10494        assert_eq!(sources[2]["profile"], "ops");
10495        assert!(sources[3]["path"]
10496            .as_str()
10497            .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10498    }
10499
10500    #[test]
10501    fn runs_list_joins_a_hermes_fire_to_the_session_it_opened() {
10502        let response = runs_list(json!({
10503            "harness": "hermes",
10504            "job": "job42",
10505            "homes": jobs_fixture_homes(),
10506        }));
10507        let result = &response["result"];
10508        assert_eq!(result["runs"].as_array().unwrap().len(), 2, "{result}");
10509
10510        // Hermes writes NO link from an execution to its session. The fire
10511        // that ran the agent is joined to `cron_job42_<stamp>` because that
10512        // id's instant falls inside its [claimed_at, finished_at] window.
10513        let ran = run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90");
10514        assert_eq!(ran["session_id"], "cron_job42_20260902_120000");
10515
10516        // The later fire failed before opening one. Its window holds no
10517        // session, so the row says so instead of re-using the earlier fire's
10518        // — the join is per-FIRE, not per-job.
10519        let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10520        assert!(failed["session_id"].is_null(), "{failed}");
10521    }
10522
10523    /// ORCH-13: where a fire's output went, read from each harness's own
10524    /// delivery record — Hermes's `delivery_obligations` ledger inside
10525    /// `state.db`, OpenClaw's `delivery_*` run-log columns.
10526    #[test]
10527    fn runs_list_reads_the_delivery_each_harness_recorded_for_a_fire() {
10528        let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10529        let result = &response["result"];
10530
10531        // Hermes: the ledger is the GATEWAY's, keyed by conversation and
10532        // surface, so the fire's own [claimed_at, finished_at] window picks
10533        // the obligation. The fire succeeded and so did the send.
10534        let delivered = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10535        assert_eq!(delivered["status"], "completed");
10536        assert_eq!(delivered["delivery"]["state"], "delivered");
10537        assert_eq!(delivered["delivery"]["target"], "telegram:-100777:55");
10538        assert_eq!(delivered["delivery"]["attempts"], 1);
10539        assert!(delivered["delivery"]["last_error"].is_null(), "{delivered}");
10540        assert_eq!(
10541            delivered["delivery"]["delivered_at"],
10542            "2026-09-02T09:00:30.400Z"
10543        );
10544
10545        // The next fire of the same job ALSO succeeded — and its output never
10546        // arrived. That is the fact `status` alone cannot carry.
10547        let undelivered = run_row(result, "f60718293a4b5c6d7e8f901234567890");
10548        assert_eq!(undelivered["status"], "completed");
10549        assert_eq!(undelivered["delivery"]["state"], "failed");
10550        assert_eq!(undelivered["delivery"]["attempts"], 3);
10551        assert_eq!(
10552            undelivered["delivery"]["last_error"],
10553            "telegram send failed: Bad Request: chat not found"
10554        );
10555        // Only a delivered obligation carries an instant of delivery; the
10556        // ledger's `updated_at` on a failed row dates the failure.
10557        assert!(
10558            undelivered["delivery"]["delivered_at"].is_null(),
10559            "{undelivered}"
10560        );
10561
10562        // OpenClaw writes the outcome onto the run-log row and declares the
10563        // address on the job, so the row's target is joined from `cron_jobs`.
10564        let announced = run_row(result, "run_health_0001");
10565        assert_eq!(announced["delivery"]["state"], "delivered");
10566        assert_eq!(announced["delivery"]["target"], "last");
10567        // Its run log counts no attempts and stamps no delivered-at.
10568        assert!(announced["delivery"]["attempts"].is_null(), "{announced}");
10569        assert!(
10570            announced["delivery"]["delivered_at"].is_null(),
10571            "{announced}"
10572        );
10573        let refused = run_row(result, "run_health_0002");
10574        assert_eq!(refused["delivery"]["state"], "not-delivered");
10575        assert_eq!(refused["delivery"]["last_error"], "channel_not_found");
10576
10577        // A run-log row with no delivery columns at all recorded no delivery:
10578        // the job's declared target is not evidence that anything was sent.
10579        let skipped = run_row(result, &format!("{OPENCLAW_DIGEST_JOB}#1"));
10580        assert!(skipped["delivery"].is_null(), "{skipped}");
10581    }
10582
10583    /// A Hermes fire whose session carries a `session_key` is matched on that
10584    /// key FIRST — the most specific question the ledger can answer. Proven by
10585    /// moving the obligations off the job's surface on a COPY of the fixture,
10586    /// so only the session-key question can still find them.
10587    #[test]
10588    fn runs_list_matches_a_hermes_obligation_by_the_session_key_first() {
10589        let scratch = std::env::temp_dir().join(format!(
10590            "supercode-runs-delivery-{}-{}",
10591            std::process::id(),
10592            generated_session_id()
10593        ));
10594        std::fs::create_dir_all(scratch.join("cron")).unwrap();
10595        let fixture = jobs_fixture_root().join("hermes_home");
10596        std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10597        for name in ["cron/executions.db", "cron/jobs.json"] {
10598            std::fs::copy(fixture.join(name), scratch.join(name)).unwrap();
10599        }
10600        {
10601            let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10602            // The obligations now sit on a surface no job in this store
10603            // delivers to, so the surface question cannot match them.
10604            connection
10605                .execute(
10606                    "UPDATE delivery_obligations SET platform = 'slack', chat_id = 'C0FALLBACK'",
10607                    [],
10608                )
10609                .unwrap();
10610            // A cron fire that ran inside a keyed conversation: the session
10611            // the window recovers carries `tg-coder-1`'s key.
10612            connection
10613                .execute(
10614                    "INSERT INTO sessions (id, source, session_key, started_at) VALUES \
10615                     ('cron_coder-standup_20260902_090010', 'cron', \
10616                      'agent:coder:telegram:group:-100777:55', 1788339610.0)",
10617                    [],
10618                )
10619                .unwrap();
10620        }
10621        let response = runs_list(json!({
10622            "harness": "hermes",
10623            "job": "coder-standup",
10624            "homes": {"hermes": scratch.join("state.db")},
10625        }));
10626        let result = &response["result"];
10627        let matched = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10628        assert_eq!(
10629            matched["session_id"], "cron_coder-standup_20260902_090010",
10630            "{result}"
10631        );
10632        assert_eq!(matched["delivery"]["state"], "delivered", "{result}");
10633        assert_eq!(
10634            matched["delivery"]["target"], "slack:C0FALLBACK:55",
10635            "{result}"
10636        );
10637        std::fs::remove_dir_all(&scratch).ok();
10638    }
10639
10640    #[test]
10641    fn runs_list_follows_a_compression_chain_to_the_readable_tip() {
10642        // A fire whose session was compressed mid-run is only readable at the
10643        // continuation, so that is what the row must report. Built on a COPY
10644        // of the committed fixture: no test writes to a fixture or to a real
10645        // harness home.
10646        let scratch = std::env::temp_dir().join(format!(
10647            "supercode-runs-compressed-{}-{}",
10648            std::process::id(),
10649            generated_session_id()
10650        ));
10651        std::fs::create_dir_all(scratch.join("cron")).unwrap();
10652        let fixture = jobs_fixture_root().join("hermes_home");
10653        std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10654        std::fs::copy(
10655            fixture.join("cron/executions.db"),
10656            scratch.join("cron/executions.db"),
10657        )
10658        .unwrap();
10659        {
10660            let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10661            connection
10662                .execute(
10663                    "UPDATE sessions SET end_reason = 'compression' WHERE id = ?1",
10664                    ["cron_job42_20260902_120000"],
10665                )
10666                .unwrap();
10667            connection
10668                .execute(
10669                    "INSERT INTO sessions (id, source, parent_session_id, started_at) \
10670                     VALUES ('job42-after-compaction', 'cron', \
10671                             'cron_job42_20260902_120000', 1788350000.0)",
10672                    [],
10673                )
10674                .unwrap();
10675        }
10676        let response = runs_list(json!({
10677            "harness": "hermes",
10678            "job": "job42",
10679            "homes": {"hermes": scratch.join("state.db")},
10680        }));
10681        let result = &response["result"];
10682        assert_eq!(
10683            run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90")["session_id"],
10684            "job42-after-compaction",
10685            "{result}"
10686        );
10687        std::fs::remove_dir_all(&scratch).ok();
10688    }
10689
10690    #[test]
10691    fn runs_list_filters_by_job_and_caps_by_limit() {
10692        let by_job = runs_list(json!({
10693            "harness": "openclaw",
10694            "job": OPENCLAW_HEALTH_JOB,
10695            "homes": jobs_fixture_homes(),
10696        }));
10697        let ids: Vec<&str> = by_job["result"]["runs"]
10698            .as_array()
10699            .unwrap()
10700            .iter()
10701            .map(|run| run["id"].as_str().unwrap())
10702            .collect();
10703        assert_eq!(ids, vec!["run_health_0002", "run_health_0001"], "{by_job}");
10704
10705        let capped = runs_list(json!({
10706            "harness": "openclaw",
10707            "limit": 1,
10708            "homes": jobs_fixture_homes(),
10709        }));
10710        let runs = capped["result"]["runs"].as_array().unwrap();
10711        assert_eq!(runs.len(), 1, "{capped}");
10712        // Newest first, so the cap keeps the recent fire.
10713        assert_eq!(runs[0]["id"], "run_health_0002");
10714    }
10715
10716    #[test]
10717    fn runs_get_answers_with_the_row_and_the_verbatim_native_record() {
10718        let mut service = HarnessSessionService::new();
10719        let hermes = service.handle(request(
10720            1,
10721            "harness.v1.runs.get",
10722            json!({
10723                "harness": "hermes",
10724                "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10725                "homes": jobs_fixture_homes(),
10726            }),
10727        ));
10728        assert_eq!(hermes["result"]["run"]["status"], "completed");
10729        assert_eq!(
10730            hermes["result"]["run"]["session_id"],
10731            "cron_job42_20260902_120000"
10732        );
10733        // Ledger columns the uniform row does not carry survive on `source`.
10734        assert_eq!(hermes["result"]["source"]["source"], "scheduler");
10735        assert_eq!(hermes["result"]["source"]["pid"], 4242);
10736        assert_eq!(hermes["result"]["source"]["process_id"], "9f1c2d");
10737
10738        let openclaw = service.handle(request(
10739            2,
10740            "harness.v1.runs.get",
10741            json!({
10742                "harness": "openclaw",
10743                "id": "run_health_0002",
10744                "homes": jobs_fixture_homes(),
10745            }),
10746        ));
10747        assert_eq!(openclaw["result"]["run"]["status"], "error");
10748        // ORCH-13: the run's delivery is projected AND the store's own columns
10749        // stay verbatim on `source`, so nothing about the fire is lost.
10750        assert_eq!(
10751            openclaw["result"]["source"]["delivery_status"],
10752            "not-delivered"
10753        );
10754        assert_eq!(
10755            openclaw["result"]["source"]["delivery_error"],
10756            "channel_not_found"
10757        );
10758        assert_eq!(openclaw["result"]["source"]["delivered"], 0);
10759        assert_eq!(
10760            openclaw["result"]["run"]["delivery"]["state"],
10761            "not-delivered"
10762        );
10763        assert_eq!(
10764            openclaw["result"]["run"]["delivery"]["last_error"],
10765            "channel_not_found"
10766        );
10767
10768        let missing = service.handle(request(
10769            3,
10770            "harness.v1.runs.get",
10771            json!({"harness": "hermes", "id": "no-such-run", "homes": jobs_fixture_homes()}),
10772        ));
10773        assert!(missing["error"]["message"]
10774            .as_str()
10775            .is_some_and(|message| message.contains("no run `no-such-run`")));
10776    }
10777
10778    #[test]
10779    fn runs_refuse_a_harness_that_keeps_no_run_store() {
10780        let mut service = HarnessSessionService::new();
10781        for (id, method, params) in [
10782            // Claude Code HAS scheduled jobs but no fire store: its fires are
10783            // ordinary turns. It must refuse, not answer with an empty list.
10784            (
10785                1,
10786                "harness.v1.runs.list",
10787                json!({"harness": "claude-code", "homes": jobs_fixture_homes()}),
10788            ),
10789            (
10790                2,
10791                "harness.v1.runs.get",
10792                json!({"harness": "claude-code", "id": "anything"}),
10793            ),
10794            (
10795                3,
10796                "harness.v1.runs.list",
10797                json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10798            ),
10799        ] {
10800            let response = service.handle(request(id, method, params));
10801            assert_eq!(response["error"]["code"], -32020, "{response}");
10802            assert!(response["error"]["message"]
10803                .as_str()
10804                .is_some_and(|message| message.contains("keeps no run store")));
10805            assert!(response.get("result").is_none());
10806        }
10807    }
10808
10809    #[test]
10810    fn runs_list_reports_an_install_with_no_run_store_as_absent() {
10811        let scratch = std::env::temp_dir().join(format!(
10812            "supercode-runs-empty-{}-{}",
10813            std::process::id(),
10814            generated_session_id()
10815        ));
10816        std::fs::create_dir_all(&scratch).unwrap();
10817        let response = runs_list(json!({
10818            "harness": "openclaw",
10819            "homes": {"openclaw": scratch.clone()},
10820        }));
10821        let result = &response["result"];
10822        assert_eq!(result["runs"].as_array().unwrap().len(), 0, "{result}");
10823        assert_eq!(result["sources"][0]["state"], "absent_store");
10824        assert!(result["sources"][0]["path"]
10825            .as_str()
10826            .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10827        std::fs::remove_dir_all(&scratch).ok();
10828    }
10829}