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.materialize",
67    "harness.v1.sessions.resume_instructions",
68    "harness.v1.skills.list",
69    "harness.v1.skills.install",
70    "harness.v1.skills.remove",
71    "harness.v1.memory.show",
72    "harness.v1.memory.search",
73    "harness.v1.jobs.list",
74    "harness.v1.jobs.get",
75    "harness.v1.jobs.create",
76    "harness.v1.jobs.update",
77    "harness.v1.jobs.pause",
78    "harness.v1.jobs.resume",
79    "harness.v1.jobs.run",
80    "harness.v1.jobs.delete",
81    "harness.v1.jobs.notepad",
82    "harness.v1.jobs.notepad_set",
83    "harness.v1.jobs.notepad_delete",
84    "harness.v1.sessions.new",
85    "harness.v1.sessions.reset",
86    "harness.v1.sessions.archive",
87    "harness.v1.sessions.delete",
88    "harness.v1.runs.list",
89    "harness.v1.runs.get",
90    "harness.v1.approvals.list",
91    "harness.v1.approvals.resolve",
92    "harness.v1.runtimes.capabilities",
93    "harness.v1.runtimes.start",
94    "harness.v1.runtimes.resume",
95    "harness.v1.runtimes.attach_existing",
96    "harness.v1.runtimes.attach",
97    "harness.v1.runtimes.send_input",
98    "harness.v1.runtimes.interrupt",
99    "harness.v1.runtimes.steer",
100    "harness.v1.runtimes.respond",
101    "harness.v1.runtimes.terminal_instructions",
102    "harness.v1.runtimes.acquire_control",
103    "harness.v1.runtimes.heartbeat",
104    "harness.v1.runtimes.detach",
105    "harness.v1.runtimes.close",
106    "harness.v1.profiles.list",
107    "harness.v1.profiles.get",
108    "harness.v1.profiles.create",
109    "harness.v1.profiles.delete",
110    "harness.v1.channels.list",
111    "harness.v1.routes.list",
112    "harness.v1.triggers.list",
113    "harness.v1.channels.status",
114    "harness.v1.orchestration.load",
115    "harness.v1.orchestration.save",
116    "harness.v1.orchestration.compile",
117    "harness.v1.orchestration.decompile",
118    "harness.v1.orchestration.import",
119    "harness.v1.orchestration.export",
120    "harness.v1.workflow.load",
121];
122
123/// Protocol namespace implemented by this service.
124pub const HARNESS_SERVICE_VERSION: &str = "harness.v1";
125/// Notification method emitted for followed-session changes.
126pub const SESSION_EVENT_METHOD: &str = "harness.v1.sessions.event";
127/// Notification method emitted for normalized session-activity transitions.
128pub const SESSION_ACTIVITY_EVENT_METHOD: &str = "harness.v1.sessions.activity_event";
129/// Notification method emitted for revisioned session-list changes.
130pub const SESSION_INDEX_EVENT_METHOD: &str = "harness.v1.sessions.index_event";
131/// Notification method emitted for live runtime events.
132pub const RUNTIME_EVENT_METHOD: &str = "harness.v1.runtimes.event";
133
134/// Stateful persisted-session service. Each instance owns its follow
135/// subscriptions; discovery and loading remain read-only.
136pub struct HarnessSessionService {
137    catalog: HarnessCatalog,
138    followers: BTreeMap<String, SessionFollower>,
139    followed_sources: BTreeMap<String, FollowedSource>,
140    activity_subscriptions: BTreeMap<String, ActivitySubscription>,
141    index_subscriptions: BTreeMap<String, crate::session_index::SessionIndexSubscription>,
142    index_notifier: Arc<Notify>,
143    #[cfg(feature = "adapter-api")]
144    activity_monitor: crate::session_activity::SessionActivityMonitor,
145    next_subscription: u64,
146    runtimes: BTreeMap<String, Box<dyn RuntimeConnection>>,
147    /// Connections lent to a detached call that is running right now. The
148    /// runtime itself is OUT of `runtimes` for that whole call, and these
149    /// names are how a second caller is told the connection is busy rather
150    /// than unknown.
151    runtimes_in_flight: BTreeSet<String>,
152    terminal_launches: BTreeMap<String, StructuredLaunch>,
153    runtime_sequences: BTreeMap<String, u64>,
154    next_runtime: u64,
155    reduction_store_root: Option<PathBuf>,
156    /// ORCH-9: live permission/approval requests outstanding on the open
157    /// runtime connections above, fed by the same event pump that publishes
158    /// `harness.v1.runtimes.event`.
159    approvals: crate::approvals::ApprovalRegistry,
160    /// ORCH-9: supercode's own queued subagent approvals, when the host that
161    /// owns this service publishes its parent queue here.
162    subagent_approvals: Option<Arc<std::sync::Mutex<Vec<crate::subagents::QueuedApproval>>>>,
163}
164
165impl Default for HarnessSessionService {
166    fn default() -> Self {
167        Self::new()
168    }
169}
170
171impl HarnessSessionService {
172    /// Create an empty service instance.
173    pub fn new() -> Self {
174        Self {
175            catalog: HarnessCatalog::new(),
176            followers: BTreeMap::new(),
177            followed_sources: BTreeMap::new(),
178            activity_subscriptions: BTreeMap::new(),
179            index_subscriptions: BTreeMap::new(),
180            index_notifier: Arc::new(Notify::new()),
181            #[cfg(feature = "adapter-api")]
182            activity_monitor: Default::default(),
183            next_subscription: 1,
184            runtimes: BTreeMap::new(),
185            runtimes_in_flight: BTreeSet::new(),
186            terminal_launches: BTreeMap::new(),
187            runtime_sequences: BTreeMap::new(),
188            next_runtime: 1,
189            reduction_store_root: None,
190            approvals: crate::approvals::ApprovalRegistry::new(),
191            subagent_approvals: None,
192        }
193    }
194
195    /// Override the trusted, service-owned store used for durable reduction
196    /// bundles. Embedders and tests use this to keep all writes inside an
197    /// explicitly selected root; the CLI otherwise uses the normal
198    /// `$SUPERCODE_HOME/sessions` location.
199    pub fn with_reduction_store_root(mut self, root: impl Into<PathBuf>) -> Self {
200        self.reduction_store_root = Some(root.into());
201        self
202    }
203
204    /// ORCH-9: publish the parent's own subagent-approval queue into
205    /// `harness.v1.approvals.list`.
206    ///
207    /// This is the SAME `Arc` an [`crate::Agent`] pushes into
208    /// (`Agent::pending_child_approvals`), so a host that runs supercode's own
209    /// loop beside this service surfaces those requests through the uniform
210    /// door without copying them anywhere.
211    pub fn observe_subagent_approvals(
212        &mut self,
213        queue: Arc<std::sync::Mutex<Vec<crate::subagents::QueuedApproval>>>,
214    ) {
215        self.subagent_approvals = Some(queue);
216    }
217
218    /// ORCH-9: every approval request this service can see, newest last.
219    ///
220    /// Two sources, both live: the requests outstanding on the open runtime
221    /// connections, and supercode's own queued subagent approvals. There is
222    /// no file or database source at the pinned harness versions (see
223    /// [`crate::approvals`]), so a stored or proposal row is never produced.
224    pub fn approvals(&self, query: &crate::approvals::ApprovalsQuery) -> Vec<crate::ApprovalRow> {
225        let now = crate::approvals::now_ms();
226        let mut rows = self.approvals.rows(now);
227        if let Some(queue) = self.subagent_approvals.as_ref() {
228            let queued = queue
229                .lock()
230                .unwrap_or_else(std::sync::PoisonError::into_inner)
231                .clone();
232            rows.extend(crate::approvals::subagent_rows(&queued, now));
233        }
234        rows.retain(|row| query.matches(row));
235        rows.sort_by(|left, right| {
236            left.requested_at_ms
237                .cmp(&right.requested_at_ms)
238                .then_with(|| left.id.cmp(&right.id))
239        });
240        rows
241    }
242
243    /// ORCH-20 (controlled tier): answer one listed approval request by its
244    /// row id and one uniform decision.
245    ///
246    /// The decision is translated into the option token and reply envelope
247    /// the door that raised the request already accepts
248    /// ([`crate::approvals::plan_reply`]), and the answer is then sent by
249    /// calling `harness.v1.runtimes.respond` itself — the same code path, the
250    /// same adapter, the same bookkeeping that drops the row. This verb adds
251    /// a translation and nothing else.
252    async fn approvals_resolve(
253        &mut self,
254        params: Value,
255    ) -> std::result::Result<Value, ServiceError> {
256        let params = decode::<crate::approvals::ApprovalsResolveParams>(params)?;
257        if params.id.trim().is_empty() {
258            return Err(ServiceError::InvalidParams(
259                "approvals resolve requires the `id` of a listed approval row".into(),
260            ));
261        }
262        let choice = match (params.decision, params.option_id.as_deref()) {
263            (Some(_), Some(_)) => {
264                return Err(ServiceError::InvalidParams(
265                    "approvals resolve takes either `decision` or `option_id`, not both".into(),
266                ))
267            }
268            (Some(decision), None) => crate::approvals::ApprovalChoice::Decision(decision),
269            (None, Some(option)) => crate::approvals::ApprovalChoice::Option(option.to_string()),
270            (None, None) => {
271                return Err(ServiceError::InvalidParams(format!(
272                    "approvals resolve requires `decision` ({}) or an explicit `option_id`",
273                    crate::approvals::ApprovalDecision::ALL
274                        .map(|decision| decision.as_str())
275                        .join(" | "),
276                )))
277            }
278        };
279        let resolution = self
280            .approvals
281            .resolution(&params.id, &choice)
282            .map_err(|error| ServiceError::InvalidParams(error.to_string()))?;
283        // The harness's own door, unchanged: this is the identical call
284        // `harness.v1.runtimes.respond` performs for a caller who built the
285        // envelope by hand, including dropping the answered row.
286        self.runtime_call(
287            "harness.v1.runtimes.respond",
288            json!({
289                "connection": resolution.connection,
290                "request_id": resolution.request_id,
291                "response": resolution.response,
292            }),
293        )
294        .await?;
295        Ok(json!({
296            "id": params.id,
297            "decision": params.decision.map(|decision| decision.as_str()),
298            "option_id": resolution.option_id,
299            "resolved": true,
300        }))
301    }
302
303    /// Return the edge-triggered wakeup used by session-index filesystem
304    /// subscriptions. Transports can await this instead of polling indexes.
305    #[cfg(feature = "adapter-api")]
306    pub fn session_index_notifier(&self) -> Arc<Notify> {
307        Arc::clone(&self.index_notifier)
308    }
309
310    /// Handle one JSON-RPC 2.0 request and return one JSON-RPC response.
311    #[cfg(feature = "adapter-api")]
312    pub fn handle(&mut self, request: Value) -> Value {
313        let id = request.get("id").cloned().unwrap_or(Value::Null);
314        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
315            return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
316        }
317        let Some(method) = request.get("method").and_then(Value::as_str) else {
318            return rpc_error(id, -32600, "request is missing `method`");
319        };
320        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
321        match self.call(method, params) {
322            Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
323            Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
324            Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
325            Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
326            Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
327            Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
328        }
329    }
330
331    /// Handle either a persisted-session request or an asynchronous live
332    /// runtime request.
333    #[cfg(feature = "adapter-api")]
334    pub async fn handle_async(&mut self, request: Value) -> Value {
335        let method = request
336            .get("method")
337            .and_then(Value::as_str)
338            .unwrap_or_default();
339        if matches!(
340            method,
341            "harness.v1.harnesses.list" | "harness.v1.harnesses.probe"
342        ) {
343            let id = request.get("id").cloned().unwrap_or(Value::Null);
344            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
345                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
346            }
347            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
348            return match self.inventory_call(method, params).await {
349                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
350                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
351                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
352                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
353                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
354                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
355            };
356        }
357        if matches!(
358            method,
359            "harness.v1.harnesses.auth.methods"
360                | "harness.v1.harnesses.auth.begin"
361                | "harness.v1.harnesses.auth.verify"
362        ) {
363            let id = request.get("id").cloned().unwrap_or(Value::Null);
364            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
365                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
366            }
367            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
368            return match self.harness_authentication_call(method, params).await {
369                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
370                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
371                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
372                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
373                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
374                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
375            };
376        }
377        // ORCH-19 controlled tier. Answered here rather than through the SDK
378        // operation dispatch below so the harness's OWN refusal reaches the
379        // caller: `sdk_error` collapses every `UnsupportedAction` to one
380        // generic sentence, and the whole point of this tier is that a
381        // refusal names which door the harness does have.
382        if matches!(
383            method,
384            "harness.v1.sessions.new"
385                | "harness.v1.sessions.reset"
386                | "harness.v1.sessions.archive"
387                | "harness.v1.sessions.delete"
388        ) {
389            let id = request.get("id").cloned().unwrap_or(Value::Null);
390            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
391                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
392            }
393            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
394            let verb = match method {
395                "harness.v1.sessions.new" => crate::SessionVerb::New,
396                "harness.v1.sessions.reset" => crate::SessionVerb::Reset,
397                "harness.v1.sessions.archive" => crate::SessionVerb::Archive,
398                _ => crate::SessionVerb::Delete,
399            };
400            return match self.mutate_session(verb, params).await {
401                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
402                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
403                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
404                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
405                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
406                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
407            };
408        }
409        if method == "harness.v1.sessions.message" {
410            let id = request.get("id").cloned().unwrap_or(Value::Null);
411            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
412                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
413            }
414            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
415            return match self.message_call(params).await {
416                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
417                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
418                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
419                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
420                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
421                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
422            };
423        }
424        if matches!(
425            method,
426            "harness.v1.harnesses.settings" | "harness.v1.harnesses.configure"
427        ) {
428            let id = request.get("id").cloned().unwrap_or(Value::Null);
429            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
430                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
431            }
432            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
433            return match self.harness_settings_call(method, params) {
434                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
435                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
436                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
437                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
438                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
439                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
440            };
441        }
442        if method == "harness.v1.sessions.activity.subscribe" {
443            let id = request.get("id").cloned().unwrap_or(Value::Null);
444            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
445                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
446            }
447            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
448            return match self.subscribe_session_activity(params).await {
449                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
450                Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
451                Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
452                Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
453                Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
454                Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
455            };
456        }
457        if let Some(operation) = SdkOperation::from_method(method) {
458            let id = request.get("id").cloned().unwrap_or(Value::Null);
459            if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
460                return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
461            }
462            let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
463            return match self.execute(SdkRequest { operation, params }).await {
464                Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
465                Err(error) => sdk_rpc_error(id, &error),
466            };
467        }
468        if !method.starts_with("harness.v1.runtimes.") {
469            return self.handle(request);
470        }
471        let id = request.get("id").cloned().unwrap_or(Value::Null);
472        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
473            return rpc_error(id, -32600, "expected a JSON-RPC 2.0 request");
474        }
475        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
476        match self.runtime_call(method, params).await {
477            Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
478            Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
479            Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
480            Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
481            Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
482            Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
483        }
484    }
485
486    /// Poll all active subscriptions once and return zero or more JSON-RPC
487    /// notifications. Recoverable follower errors are delivered as events.
488    #[cfg(feature = "adapter-api")]
489    pub fn poll(&mut self) -> Vec<Value> {
490        let mut notifications = Vec::new();
491        for (subscription, follower) in &mut self.followers {
492            match follower.poll() {
493                Ok(Some(event)) => notifications.push(json!({
494                    "jsonrpc": "2.0",
495                    "method": SESSION_EVENT_METHOD,
496                    "params": {
497                        "subscription": subscription,
498                        "event": event.to_json(),
499                    }
500                })),
501                Ok(None) => {}
502                Err(error) => notifications.push(json!({
503                    "jsonrpc": "2.0",
504                    "method": SESSION_EVENT_METHOD,
505                    "params": {
506                        "subscription": subscription,
507                        "event": {
508                            "type": "watch_error",
509                            "recoverable": true,
510                            "message": error.to_string(),
511                        },
512                    }
513                })),
514            }
515        }
516        notifications
517    }
518
519    /// Report each followed session's live-runtime lifecycle state on that
520    /// session's own subscription, emitting only when the state changes.
521    ///
522    /// A growing transcript is not evidence that an agent is working, so the
523    /// state comes from the live-runtime registry and nowhere else. A followed
524    /// session with no registered Supercode runtime — a harness running outside
525    /// Supercode — reports `persisted`, which says plainly that its activity is
526    /// unknown rather than guessing at it. These events carry no sequence
527    /// number and no transcript content; they never interleave with the
528    /// content follower's sequenced stream.
529    #[cfg(feature = "adapter-api")]
530    pub async fn poll_session_runtime_states(&mut self) -> Vec<Value> {
531        let registry = crate::LocalRuntimeRegistry::new();
532        let authorization = crate::RuntimeAuthorization::observer();
533        let mut notifications = Vec::new();
534        for (subscription, source) in &mut self.followed_sources {
535            let state = match registry
536                .source_state(&source.harness, &source.session_id, &authorization)
537                .await
538            {
539                Ok(Some(state)) => state,
540                Ok(None) => crate::RuntimeRegistryState::Persisted,
541                // A failed registry read is not evidence of a state change.
542                Err(_) => continue,
543            };
544            if source.reported.as_deref() == Some(state.as_str()) {
545                continue;
546            }
547            source.reported = Some(state.as_str().to_string());
548            notifications.push(json!({
549                "jsonrpc": "2.0",
550                "method": SESSION_EVENT_METHOD,
551                "params": {
552                    "subscription": subscription,
553                    "event": {"type": "runtime_state", "state": state.as_str()},
554                },
555            }));
556        }
557        notifications
558    }
559
560    /// Poll normalized activity subscriptions, emitting only proven state
561    /// transitions. Every subscription is bulk-sampled so stock-harness
562    /// process and registry discovery happens once per UI, not once per row.
563    #[cfg(feature = "adapter-api")]
564    pub async fn poll_session_activities(&mut self) -> Vec<Value> {
565        let subscriptions = self
566            .activity_subscriptions
567            .iter()
568            .map(|(id, subscription)| {
569                (
570                    id.clone(),
571                    subscription.locators.clone(),
572                    subscription.homes.clone(),
573                )
574            })
575            .collect::<Vec<_>>();
576        let mut notifications = Vec::new();
577        for (subscription_id, locators, homes) in subscriptions {
578            let Ok(activities) = self.activity_monitor.resolve(&locators, &homes).await else {
579                // A failed evidence read proves no transition. Retain the last
580                // good state instead of flashing every row to persisted.
581                continue;
582            };
583            let Some(subscription) = self.activity_subscriptions.get_mut(&subscription_id) else {
584                continue;
585            };
586            let mut changed = Vec::new();
587            for activity in activities {
588                let key = activity.key();
589                if subscription
590                    .reported
591                    .get(&key)
592                    .is_some_and(|previous| previous.same_state(&activity))
593                {
594                    continue;
595                }
596                subscription.reported.insert(key, activity.clone());
597                changed.push(activity);
598            }
599            if !changed.is_empty() {
600                notifications.push(json!({
601                    "jsonrpc": "2.0",
602                    "method": SESSION_ACTIVITY_EVENT_METHOD,
603                    "params": {
604                        "subscription": subscription_id,
605                        "activities": changed,
606                    },
607                }));
608            }
609        }
610        notifications
611    }
612
613    /// Drain native-store invalidations and emit revisioned descriptor deltas.
614    /// An idle subscription performs no catalog or transcript reads between
615    /// its minute-scale recovery reconciliations.
616    #[cfg(feature = "adapter-api")]
617    pub fn poll_session_indexes(&mut self) -> Vec<Value> {
618        let mut notifications = Vec::new();
619        for (subscription, index) in &mut self.index_subscriptions {
620            let homes = index.homes().clone();
621            match index.poll() {
622                Ok(Some(delta)) => match live_index_changes(delta.changes, &homes) {
623                    Ok(changes) => notifications.push(json!({
624                        "jsonrpc": "2.0",
625                        "method": SESSION_INDEX_EVENT_METHOD,
626                        "params": {
627                            "subscription": subscription,
628                            "revision": delta.revision,
629                            "changes": changes,
630                        },
631                    })),
632                    Err(error) => notifications.push(json!({
633                        "jsonrpc": "2.0",
634                        "method": SESSION_INDEX_EVENT_METHOD,
635                        "params": {
636                            "subscription": subscription,
637                            "error": {"recoverable": true, "message": error_message(error)},
638                        },
639                    })),
640                },
641                Ok(None) => {}
642                Err(error) => notifications.push(json!({
643                    "jsonrpc": "2.0",
644                    "method": SESSION_INDEX_EVENT_METHOD,
645                    "params": {
646                        "subscription": subscription,
647                        "error": {"recoverable": true, "message": error},
648                    },
649                })),
650            }
651        }
652        notifications
653    }
654
655    #[cfg(feature = "adapter-api")]
656    async fn subscribe_session_activity(
657        &mut self,
658        params: Value,
659    ) -> std::result::Result<Value, ServiceError> {
660        let params = decode::<ActivitySubscribeParams>(params)?;
661        if params.locators.is_empty() {
662            return Err(ServiceError::InvalidParams(
663                "sessions.activity.subscribe requires at least one locator".into(),
664            ));
665        }
666        if params.locators.len() > 2_048 {
667            return Err(ServiceError::InvalidParams(
668                "sessions.activity.subscribe accepts at most 2048 locators".into(),
669            ));
670        }
671        let initial = self
672            .activity_monitor
673            .resolve(&params.locators, &params.homes)
674            .await
675            .map_err(ServiceError::Sdk)?;
676        let subscription = format!("activity-sub-{}", self.next_subscription);
677        self.next_subscription += 1;
678        let reported = initial
679            .iter()
680            .cloned()
681            .map(|activity| (activity.key(), activity))
682            .collect();
683        self.activity_subscriptions.insert(
684            subscription.clone(),
685            ActivitySubscription {
686                locators: params.locators,
687                homes: params.homes,
688                reported,
689            },
690        );
691        Ok(json!({"subscription": subscription, "initial": initial}))
692    }
693
694    /// Non-blockingly sample one event from every connected live runtime.
695    #[cfg(feature = "adapter-api")]
696    pub async fn poll_runtimes(&mut self) -> Vec<Value> {
697        self.poll_sdk_events()
698            .await
699            .into_iter()
700            .map(|(connection, runtime_event)| {
701                json!({
702                    "jsonrpc": "2.0",
703                    "method": RUNTIME_EVENT_METHOD,
704                    "params": {
705                        "connection": connection,
706                        "session_id": runtime_event.session_id,
707                        "sequence": runtime_event.event.sequence,
708                        "event": {
709                            "kind": runtime_event.event.kind,
710                            "payload": runtime_event.event.payload,
711                        },
712                    },
713                })
714            })
715            .collect()
716    }
717
718    async fn poll_sdk_events(&mut self) -> Vec<(String, SdkRuntimeEvent)> {
719        let mut events = Vec::new();
720        let mut closed = Vec::new();
721        let now_ms = crate::approvals::now_ms();
722        for (connection, runtime) in &mut self.runtimes {
723            let session_id = runtime.handle().runtime_id.clone();
724            let harness = runtime.handle().harness.clone();
725            // Drain what the runtime already has: a turn is several events
726            // (updates, then the protocol's completion), and delivering one
727            // per poll would cost a poll interval each. A zero timeout takes
728            // only what is ready — an idle runtime costs nothing.
729            for _ in 0..256 {
730                match tokio::time::timeout(Duration::ZERO, runtime.next_event()).await {
731                    Ok(Ok(Some(event))) => {
732                        let terminal = event.kind == "transport_closed";
733                        // ORCH-9: a permission/approval request arrives as an
734                        // ordinary event; it becomes listable here and stops
735                        // being listable when `runtimes.respond` answers it.
736                        self.approvals
737                            .observe(connection, &harness, &session_id, &event, now_ms);
738                        let next_sequence = self
739                            .runtime_sequences
740                            .entry(session_id.clone())
741                            .or_insert(0);
742                        let sequence = event.sequence.unwrap_or_else(|| {
743                            *next_sequence = next_sequence.saturating_add(1);
744                            *next_sequence
745                        });
746                        *next_sequence = (*next_sequence).max(sequence);
747                        events.push((
748                            connection.clone(),
749                            SdkRuntimeEvent {
750                                session_id: session_id.clone(),
751                                event: SdkEvent {
752                                    sequence,
753                                    kind: event.kind,
754                                    payload: event.payload,
755                                },
756                            },
757                        ));
758                        if terminal {
759                            closed.push(connection.clone());
760                            break;
761                        }
762                    }
763                    Ok(Ok(None)) => {
764                        let sequence = self
765                            .runtime_sequences
766                            .entry(session_id.clone())
767                            .or_insert(0);
768                        *sequence = sequence.saturating_add(1);
769                        events.push((
770                        connection.clone(),
771                        SdkRuntimeEvent {
772                            session_id,
773                            event: SdkEvent {
774                                sequence: *sequence,
775                                kind: "transport_closed".into(),
776                                payload: json!({"message": "Harness runtime transport closed."}),
777                            },
778                        },
779                    ));
780                        closed.push(connection.clone());
781                        break;
782                    }
783                    Err(_) => break,
784                    Ok(Err(error)) => {
785                        let sequence = self
786                            .runtime_sequences
787                            .entry(session_id.clone())
788                            .or_insert(0);
789                        *sequence = sequence.saturating_add(1);
790                        events.push((
791                        connection.clone(),
792                        SdkRuntimeEvent {
793                            session_id,
794                            event: SdkEvent {
795                                sequence: *sequence,
796                                kind: "transport_error".into(),
797                                payload: json!({"message": error.to_string(), "terminal": true}),
798                            },
799                        },
800                    ));
801                        closed.push(connection.clone());
802                        break;
803                    }
804                }
805            }
806        }
807        for connection in closed {
808            if let Some(runtime) = self.runtimes.remove(&connection) {
809                self.runtime_sequences.remove(&runtime.handle().runtime_id);
810            }
811            self.terminal_launches.remove(&connection);
812            // A connection that is gone cannot answer anything it was
813            // holding; those requests stop being listable with it.
814            self.approvals.forget(&connection);
815        }
816        events
817    }
818
819    fn call(&mut self, method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
820        match method {
821            "harness.v1.capabilities" => Ok(json!({
822                "version": HARNESS_SERVICE_VERSION,
823                "sdk": self.capabilities(),
824                "methods": HARNESS_SERVICE_METHODS,
825                "notifications": [
826                    SESSION_EVENT_METHOD,
827                    SESSION_ACTIVITY_EVENT_METHOD,
828                    SESSION_INDEX_EVENT_METHOD,
829                    RUNTIME_EVENT_METHOD
830                ],
831                "harnesses": harness_support_registry()
832                    .harnesses
833                    .into_iter()
834                    .map(|harness| harness.id)
835                    .collect::<Vec<_>>(),
836            })),
837            "harness.v1.support.report" => serde_json::to_value(harness_support_registry())
838                .map_err(|error| ServiceError::Operation(error.to_string())),
839            "harness.v1.profiles.list" | "harness.v1.profiles.get" => profiles_call(method, params),
840            // ORCH-21 controlled tier. Each verb translates to the HARNESS'S
841            // OWN profile verb and runs it (`crate::profiles_control`);
842            // supercode makes and removes nothing itself. The row returned is
843            // re-read through the ORCH-10 loader afterwards, and `ran`
844            // narrates the exact command.
845            "harness.v1.profiles.create" => {
846                mutate_profile(crate::profiles_control::ProfileVerb::Create, params)
847            }
848            "harness.v1.profiles.delete" => {
849                mutate_profile(crate::profiles_control::ProfileVerb::Delete, params)
850            }
851            "harness.v1.channels.list" | "harness.v1.channels.status" => {
852                channels_call(method, params)
853            }
854            // ORCH-15 observed tier: which profile / agent a surface tuple
855            // resolves to, read from each gateway harness's own config.
856            "harness.v1.routes.list" => routes_call(params),
857            // ORCH-16 observed tier: inbound webhook routes / hook mappings.
858            "harness.v1.triggers.list" => triggers_call(params),
859            // ONT-4: the orchestration doors. One home folder in, one typed orchestration
860            // value out (and back). Every one of the four is
861            // `crate::orchestration_doors`, which the `supercode orchestration` verbs call
862            // too — the RPC adds nothing but the envelope. A vault VALUE
863            // never crosses this wire: a load or a compile answers with the
864            // `.env` KEY NAMES, and a caller that needs a value reads the
865            // home's own `.env`.
866            // the workflow layer's read door: a harness's board as one typed value,
867            // the same code the `supercode workflow load` verb calls
868            "harness.v1.workflow.load" => {
869                let params = decode::<WorkflowLoadParams>(params)?;
870                let read =
871                    crate::workflow_doors::load(params.from, &params.home).map_err(operation)?;
872                serde_json::to_value(read)
873                    .map_err(|error| ServiceError::Operation(error.to_string()))
874            }
875            "harness.v1.orchestration.load" => {
876                let params = decode::<OrchestrationLoadParams>(params)?;
877                let read = crate::orchestration_doors::load(&params.root, params.flavor)
878                    .map_err(operation)?;
879                serde_json::to_value(read)
880                    .map_err(|error| ServiceError::Operation(error.to_string()))
881            }
882            "harness.v1.orchestration.save" => {
883                let params = decode::<OrchestrationSaveParams>(params)?;
884                let saved = crate::orchestration_doors::save(
885                    &params.root,
886                    params.orchestration,
887                    params.vault,
888                )
889                .map_err(operation)?;
890                serde_json::to_value(saved)
891                    .map_err(|error| ServiceError::Operation(error.to_string()))
892            }
893            "harness.v1.orchestration.compile" => {
894                let params = decode::<OrchestrationCompileParams>(params)?;
895                let read = crate::orchestration_doors::compile(params.from, &params.home)
896                    .map_err(operation)?;
897                serde_json::to_value(read)
898                    .map_err(|error| ServiceError::Operation(error.to_string()))
899            }
900            "harness.v1.orchestration.decompile" => {
901                let params = decode::<OrchestrationDecompileParams>(params)?;
902                let report = crate::orchestration_doors::decompile(
903                    params.to,
904                    params.orchestration,
905                    &params.source,
906                    params.source_flavor,
907                    &params.dest,
908                    params.vault,
909                )
910                .map_err(operation)?;
911                serde_json::to_value(report)
912                    .map_err(|error| ServiceError::Operation(error.to_string()))
913            }
914            // a migration keeps the credential in this process: a compile and
915            // a save (import), a load and a decompile (export), composed here
916            // because composed by a client the secret would have to cross
917            // the wire
918            "harness.v1.orchestration.import" => {
919                let params = decode::<OrchestrationImportParams>(params)?;
920                let imported =
921                    crate::orchestration_doors::import(params.from, &params.home, &params.into)
922                        .map_err(operation)?;
923                serde_json::to_value(imported)
924                    .map_err(|error| ServiceError::Operation(error.to_string()))
925            }
926            "harness.v1.orchestration.export" => {
927                let params = decode::<OrchestrationExportParams>(params)?;
928                let report =
929                    crate::orchestration_doors::export(params.to, &params.root, &params.dest)
930                        .map_err(operation)?;
931                serde_json::to_value(report)
932                    .map_err(|error| ServiceError::Operation(error.to_string()))
933            }
934            // ORCH-12 observed tier: read and search the persistent memory
935            // documents a harness keeps on disk. Read-only — every write
936            // (`hermes memory off`, `openclaw memory forget|reset`, Claude
937            // Code's `/memory`) stays the harness's own verb. A harness with
938            // no memory store is refused with UnsupportedAction.
939            "harness.v1.memory.show" | "harness.v1.memory.search" => memory_call(method, params),
940            // ORCH-11 observed tier: read-only enumeration of every harness's
941            // installed skill packages. An unknown harness id is refused with
942            // UnsupportedAction — every harness supports skills, so a filter
943            // that matches nothing is a caller error, never an empty listing.
944            "harness.v1.skills.list" => {
945                let query = decode::<crate::skills::SkillsQuery>(params)?;
946                if let Some(harness) = query.harness.as_deref() {
947                    if !crate::skills::SKILL_HARNESSES.contains(&harness) {
948                        return Err(ServiceError::UnsupportedAction(format!(
949                            "`{harness}` has no skills root supercode reads"
950                        )));
951                    }
952                }
953                serde_json::to_value(crate::skills::list_skills(&query))
954                    .map_err(|error| ServiceError::Operation(error.to_string()))
955            }
956            // ORCH-22 controlled tier: each verb goes through the door the
957            // HARNESS publishes — `hermes skills install|uninstall`,
958            // `openclaw skills install`, and for the core four the loader's
959            // own directory, which is the only skills door those harnesses
960            // have. supercode resolves no registry and unpacks no archive.
961            // The row returned is re-read through the ORCH-11 loader
962            // afterwards, and `ran` narrates exactly what was performed.
963            "harness.v1.skills.install" => {
964                mutate_skill(crate::skills_control::SkillVerb::Install, params)
965            }
966            "harness.v1.skills.remove" => {
967                mutate_skill(crate::skills_control::SkillVerb::Remove, params)
968            }
969            // ORCH-9 observed tier: the approval requests waiting for an
970            // answer. At the pinned harness versions the only uniform source
971            // is a LIVE request held by an open runtime connection, plus
972            // supercode's own queued subagent approvals — neither Hermes
973            // 0.21.0 nor OpenClaw 2026.7.1-2 has an approvals door to read
974            // (see `crate::approvals`). A harness whose runtime cannot carry
975            // a protocol request at all is refused by name.
976            "harness.v1.approvals.list" => {
977                let query = decode::<crate::approvals::ApprovalsQuery>(params)?;
978                if let Some(harness) = query.harness.as_deref() {
979                    if !crate::approvals::lists_approvals(harness) {
980                        return Err(ServiceError::UnsupportedAction(format!(
981                            "`{harness}` has no runtime door that carries an approval request"
982                        )));
983                    }
984                }
985                serde_json::to_value(self.approvals(&query))
986                    .map_err(|error| ServiceError::Operation(error.to_string()))
987            }
988            "harness.v1.sessions.discover" => {
989                let query = decode::<DiscoveryQuery>(params)?;
990                let page = discover_session_page(&query).map_err(operation)?;
991                // Claude Code is the one harness that publishes its RUNNING
992                // sessions. The registry is read once per discovery and joined
993                // by session id; every record in it has already survived a
994                // `kill(pid, 0)` liveness check inside `read_registry`.
995                let peers = if page
996                    .sessions
997                    .iter()
998                    .any(|session| session.locator.harness.as_str() == HarnessId::CLAUDE_CODE)
999                {
1000                    crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(
1001                        &query.homes,
1002                    ))
1003                } else {
1004                    Vec::new()
1005                };
1006                let activities = crate::session_activity::resolve_stock_session_activities(
1007                    &page
1008                        .sessions
1009                        .iter()
1010                        .map(|session| session.locator.clone())
1011                        .collect::<Vec<_>>(),
1012                    &query.homes,
1013                )
1014                .into_iter()
1015                .map(|activity| (activity.key(), activity))
1016                .collect::<BTreeMap<_, _>>();
1017                let sessions = page
1018                    .sessions
1019                    .into_iter()
1020                    .map(|session| {
1021                        let mut value = live_descriptor_value(&session, &peers)?;
1022                        let activity_key = (
1023                            session.locator.harness.as_str().to_string(),
1024                            session.locator.session_id.clone(),
1025                        );
1026                        if let Some(activity) = activities.get(&activity_key) {
1027                            value["activity"] = serde_json::to_value(activity)
1028                                .map_err(|error| ServiceError::Operation(error.to_string()))?;
1029                            if let Some(status) = legacy_live_status(activity) {
1030                                value["live_status"] = json!(status);
1031                            }
1032                        }
1033                        Ok(value)
1034                    })
1035                    .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1036                let mut result = json!({"sessions": sessions, "next_cursor": page.next_cursor});
1037                // Preserve the metadata-only wire shape, but carry the catalog's
1038                // proof/counts when the caller explicitly requests preview search.
1039                if query.search_previews {
1040                    result["receipt"] = serde_json::to_value(page.receipt)
1041                        .map_err(|error| ServiceError::Operation(error.to_string()))?;
1042                }
1043                Ok(result)
1044            }
1045            "harness.v1.sessions.load" => {
1046                let params = decode::<LoadSessionParams>(params)?;
1047                if let Some(options) = &params.options {
1048                    options.validate()?;
1049                    if let Some(result) = indexed_claude_window(&params.read.locator, options)? {
1050                        return Ok(result);
1051                    }
1052                    return load_session(&params.read.locator)
1053                        .map(|session| projected_session_result(&session, options))
1054                        .map_err(operation);
1055                }
1056                let mut session = if params.read.display_history() {
1057                    self.catalog
1058                        .load_display_view(
1059                            &params.read.locator,
1060                            params.read.read_fidelity(),
1061                            params.read.tail_messages().unwrap_or(500),
1062                        )
1063                        .map_err(crate::Error::from)
1064                } else if params.read.include_subagents() {
1065                    load_session_with_fidelity(&params.read.locator, params.read.read_fidelity())
1066                } else {
1067                    self.catalog
1068                        .load_parent_with_fidelity(
1069                            &params.read.locator,
1070                            params.read.read_fidelity(),
1071                        )
1072                        .map_err(crate::Error::from)
1073                }
1074                .map_err(operation)?;
1075                params.read.bound_session(&mut session);
1076                Ok(json!({"session": normalized_session_json(&session)}))
1077            }
1078            "harness.v1.sessions.follow" => {
1079                let params = decode::<LocatorParams>(params)?;
1080                let mut follower = self
1081                    .catalog
1082                    .follow_read_view(
1083                        &params.locator,
1084                        params.read_fidelity(),
1085                        params.include_subagents(),
1086                        params.tail_messages(),
1087                        params.max_message_chars(),
1088                        params.display_history(),
1089                    )
1090                    .map_err(operation)?;
1091                let initial = follower
1092                    .poll()
1093                    .map_err(operation)?
1094                    .map(|event| event.to_json());
1095                let subscription = format!("sub-{}", self.next_subscription);
1096                self.next_subscription += 1;
1097                self.followers.insert(subscription.clone(), follower);
1098                self.followed_sources.insert(
1099                    subscription.clone(),
1100                    FollowedSource {
1101                        harness: params.locator.harness.as_str().to_string(),
1102                        session_id: params.locator.session_id.clone(),
1103                        reported: None,
1104                    },
1105                );
1106                Ok(json!({"subscription": subscription, "initial": initial}))
1107            }
1108            "harness.v1.sessions.unfollow" => {
1109                let params = decode::<UnfollowParams>(params)?;
1110                self.followed_sources.remove(&params.subscription);
1111                Ok(json!({
1112                    "removed": self.followers.remove(&params.subscription).is_some()
1113                }))
1114            }
1115            "harness.v1.sessions.activity.unsubscribe" => {
1116                let params = decode::<UnfollowParams>(params)?;
1117                Ok(json!({
1118                    "removed": self.activity_subscriptions.remove(&params.subscription).is_some()
1119                }))
1120            }
1121            "harness.v1.sessions.index.subscribe" => {
1122                let query = decode::<DiscoveryQuery>(params)?;
1123                crate::session_index::validate_query(&query)
1124                    .map_err(ServiceError::InvalidParams)?;
1125                let homes = query.homes.clone();
1126                let (index, initial) = crate::session_index::SessionIndexSubscription::open(
1127                    query,
1128                    Arc::clone(&self.index_notifier),
1129                )
1130                .map_err(ServiceError::Operation)?;
1131                let peers = peers_for_descriptors(&initial, &homes);
1132                let initial = initial
1133                    .iter()
1134                    .map(|descriptor| live_descriptor_value(descriptor, &peers))
1135                    .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1136                let subscription = format!("index-sub-{}", self.next_subscription);
1137                self.next_subscription += 1;
1138                self.index_subscriptions.insert(subscription.clone(), index);
1139                Ok(json!({
1140                    "subscription": subscription,
1141                    "revision": 1,
1142                    "initial": initial,
1143                }))
1144            }
1145            "harness.v1.sessions.index.resize" => {
1146                let params = decode::<IndexResizeParams>(params)?;
1147                crate::session_index::validate_limit(params.limit)
1148                    .map_err(ServiceError::InvalidParams)?;
1149                let index = self
1150                    .index_subscriptions
1151                    .get_mut(&params.subscription)
1152                    .ok_or_else(|| {
1153                        ServiceError::InvalidParams("unknown session index subscription".into())
1154                    })?;
1155                let prepared = index
1156                    .prepare_resize(params.limit)
1157                    .map_err(ServiceError::Operation)?;
1158                let peers = peers_for_descriptors(&prepared.page.sessions, index.homes());
1159                let initial = prepared
1160                    .page
1161                    .sessions
1162                    .iter()
1163                    .map(|descriptor| live_descriptor_value(descriptor, &peers))
1164                    .collect::<std::result::Result<Vec<_>, ServiceError>>()?;
1165                let response = json!({
1166                    "subscription": params.subscription,
1167                    "revision": prepared.revision,
1168                    "initial": initial,
1169                    "receipt": prepared.page.receipt,
1170                });
1171                index.commit_resize(prepared);
1172                Ok(response)
1173            }
1174            "harness.v1.sessions.index.unsubscribe" => {
1175                let params = decode::<UnfollowParams>(params)?;
1176                Ok(json!({
1177                    "removed": self.index_subscriptions.remove(&params.subscription).is_some()
1178                }))
1179            }
1180            "harness.v1.sessions.import" => {
1181                let params = decode::<ImportSessionParams>(params)?;
1182                let session = Session::load_str(&params.content, params.source_harness.into())
1183                    .map_err(operation)?;
1184                Ok(json!({"session": normalized_session_json(&session)}))
1185            }
1186            "harness.v1.sessions.export" | "harness.v1.sessions.translate" => {
1187                let params = decode::<ExportSessionParams>(params)?;
1188                let session = load_session(&params.locator).map_err(operation)?;
1189                let artifact = session_artifact(&params.locator, &session, params.target_harness)?;
1190                if method == "harness.v1.sessions.export"
1191                    && params.target_harness == TransferFormat::Hermes
1192                {
1193                    // UNI-18: write through Hermes's own door, never into its store
1194                    let imported = crate::hermes_import::import_into_hermes(&session, None)
1195                        .map_err(operation)?;
1196                    return Ok(json!({"artifact": artifact, "imported": imported}));
1197                }
1198                Ok(json!({"artifact": artifact}))
1199            }
1200            "harness.v1.sessions.reduce" => {
1201                let params = decode::<ReduceSessionParams>(params)?;
1202                self.reduce_session(params)
1203            }
1204            "harness.v1.sessions.branch" => {
1205                let params = decode::<BranchSessionParams>(params)?;
1206                let session = load_session(&params.locator).map_err(operation)?;
1207                let storage = params.locator.storage.path().display().to_string();
1208                let bootstrap_prompt = format!(
1209                    "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.",
1210                    params.locator.harness.as_str(), params.locator.session_id, storage
1211                );
1212                let artifact = params
1213                    .target_harness
1214                    .map(|target| session_artifact(&params.locator, &session, target))
1215                    .transpose()?;
1216                Ok(json!({
1217                    "parent": params.locator,
1218                    "session": normalized_session_json(&session),
1219                    "bootstrap_prompt": bootstrap_prompt,
1220                    "artifact": artifact,
1221                }))
1222            }
1223            "harness.v1.sessions.handoff" => {
1224                let params = decode::<HandoffSessionParams>(params)?;
1225                let session = load_session(&params.locator).map_err(operation)?;
1226                let cwd = params
1227                    .cwd
1228                    .or_else(|| session.meta.cwd.clone())
1229                    .unwrap_or_else(|| PathBuf::from("."));
1230                let artifact = handoff_artifact(&params.locator, &session, params.target_harness)?;
1231                let target_session_id = artifact.session_id.as_deref().ok_or_else(|| {
1232                    ServiceError::Operation(
1233                        "handoff artifact omitted target session identity".into(),
1234                    )
1235                })?;
1236                let instructions =
1237                    handoff_instructions(params.target_harness, target_session_id, &cwd);
1238                Ok(json!({
1239                    "artifact": artifact,
1240                    "launch": instructions.launch,
1241                    "materialize": instructions.materialize,
1242                    "requires_materialization": instructions.requires_materialization,
1243                    "note": instructions.note,
1244                }))
1245            }
1246            "harness.v1.sessions.materialize" => {
1247                let params = decode::<MaterializeSessionParams>(params)?;
1248                // An artifact from another machine carries its whole source as a recovery file;
1249                // keeping its segments here lets a later write back to that format restore it
1250                // byte for byte on this machine too (docs/plans/portable-residue.md).
1251                for file in &params.artifact.files {
1252                    if file.role == "source_recovery"
1253                        && file.path == "recovery/source.supercode.jsonl"
1254                    {
1255                        if let Ok(source) = Session::from_native_str(&file.content) {
1256                            crate::residue_store::store_segments(&source);
1257                        }
1258                    }
1259                }
1260                let locator = crate::native_materialize::materialize_native_artifact(
1261                    params.artifact,
1262                    &params.cwd,
1263                    &params.homes,
1264                )
1265                .map_err(ServiceError::Operation)?;
1266                Ok(json!({"locator": locator}))
1267            }
1268            // ORCH-7 observed tier. Read-only: the handlers open the harness's
1269            // own job store (Claude Code's session JSONL, Hermes's and
1270            // OpenClaw's `cron/jobs.json`) and never write, fire, or schedule.
1271            "harness.v1.jobs.list" => {
1272                let query = decode::<crate::jobs::JobsQuery>(params)?;
1273                if let Some(harness) = query.harness.as_deref() {
1274                    refuse_harness_without_jobs(harness, "jobs.list")?;
1275                }
1276                let listing = crate::jobs::list_jobs(&query).map_err(operation)?;
1277                serde_json::to_value(listing)
1278                    .map_err(|error| ServiceError::Operation(error.to_string()))
1279            }
1280            "harness.v1.jobs.get" => {
1281                let params = decode::<JobsGetParams>(params)?;
1282                refuse_harness_without_jobs(&params.harness, "jobs.get")?;
1283                match crate::jobs::get_job(&params.harness, &params.id, &params.homes)
1284                    .map_err(operation)?
1285                {
1286                    Some((job, source)) => Ok(json!({"job": job, "source": source})),
1287                    None => Err(ServiceError::Operation(format!(
1288                        "`{}` has no scheduled job `{}`",
1289                        params.harness, params.id
1290                    ))),
1291                }
1292            }
1293            // ORCH-18 controlled tier. Each verb translates to the HARNESS'S
1294            // OWN cron verb and runs it (`crate::jobs_control`); supercode
1295            // schedules nothing. The row returned is re-read from the
1296            // harness's store afterwards, and `ran` narrates the exact command
1297            // with any credential redacted.
1298            "harness.v1.jobs.create" => mutate_job(crate::jobs_control::JobVerb::Create, params),
1299            "harness.v1.jobs.update" => mutate_job(crate::jobs_control::JobVerb::Update, params),
1300            "harness.v1.jobs.pause" => mutate_job(crate::jobs_control::JobVerb::Pause, params),
1301            "harness.v1.jobs.resume" => mutate_job(crate::jobs_control::JobVerb::Resume, params),
1302            "harness.v1.jobs.run" => mutate_job(crate::jobs_control::JobVerb::Run, params),
1303            "harness.v1.jobs.delete" => mutate_job(crate::jobs_control::JobVerb::Delete, params),
1304            "harness.v1.jobs.notepad"
1305            | "harness.v1.jobs.notepad_set"
1306            | "harness.v1.jobs.notepad_delete" => {
1307                let request = decode::<crate::jobs_notepad::JobNotepadRequest>(params)?;
1308                refuse_harness_without_jobs(&request.harness, "jobs.notepad")?;
1309                let answer = match method {
1310                    "harness.v1.jobs.notepad_set" => crate::jobs_notepad::set(&request),
1311                    "harness.v1.jobs.notepad_delete" => crate::jobs_notepad::delete(&request),
1312                    _ => crate::jobs_notepad::read(&request),
1313                }
1314                .map_err(job_control_error)?;
1315                serde_json::to_value(answer)
1316                    .map_err(|error| ServiceError::Operation(error.to_string()))
1317            }
1318            // ORCH-8 observed tier. Read-only: the handlers open the harness's
1319            // own run store (Hermes's `cron/executions.db`, OpenClaw's
1320            // `cron_run_logs`) and never claim, retry, or prune a fire.
1321            "harness.v1.runs.list" => {
1322                let query = decode::<crate::runs::RunsQuery>(params)?;
1323                if let Some(harness) = query.harness.as_deref() {
1324                    refuse_harness_without_runs(harness, "runs.list")?;
1325                }
1326                let listing = crate::runs::list_runs(&query).map_err(operation)?;
1327                serde_json::to_value(listing)
1328                    .map_err(|error| ServiceError::Operation(error.to_string()))
1329            }
1330            "harness.v1.runs.get" => {
1331                let params = decode::<RunsGetParams>(params)?;
1332                refuse_harness_without_runs(&params.harness, "runs.get")?;
1333                match crate::runs::get_run(&params.harness, &params.id, &params.homes)
1334                    .map_err(operation)?
1335                {
1336                    Some((run, source)) => Ok(json!({"run": run, "source": source})),
1337                    None => Err(ServiceError::Operation(format!(
1338                        "`{}` has no run `{}`",
1339                        params.harness, params.id
1340                    ))),
1341                }
1342            }
1343            "harness.v1.sessions.resume_instructions" => {
1344                let params = decode::<ResumeInstructionsParams>(params)?;
1345                let session = load_session(&params.locator).map_err(operation)?;
1346                let cwd = params
1347                    .cwd
1348                    .or(session.meta.cwd)
1349                    .unwrap_or_else(|| PathBuf::from("."));
1350                let launch = resume_launch(
1351                    params.locator.harness.as_str(),
1352                    &params.locator.session_id,
1353                    &cwd,
1354                    params.policy,
1355                )?;
1356                Ok(json!({"launch": launch}))
1357            }
1358            _ => Err(ServiceError::MethodNotFound),
1359        }
1360    }
1361
1362    fn reduce_session(
1363        &self,
1364        params: ReduceSessionParams,
1365    ) -> std::result::Result<Value, ServiceError> {
1366        let session = load_session(&params.locator).map_err(operation)?;
1367        if session.messages.is_empty() {
1368            return Err(ServiceError::InvalidParams(
1369                "cannot reduce an empty session".into(),
1370            ));
1371        }
1372        let keep_last = params.keep_last.clamp(1, 128);
1373        let policy = reduce::ReductionPolicy {
1374            clear_turns_older_than: Some(keep_last),
1375            ..Default::default()
1376        };
1377        let (view, log) =
1378            reduce::project_messages(&session.messages, &policy, &reduce::ReductionLog::default());
1379        if log.reductions.is_empty() {
1380            return Err(ServiceError::UnsupportedAction(format!(
1381                "session `{}` is already too small for a meaningful reversible reduction",
1382                params.locator.session_id
1383            )));
1384        }
1385        let source_tokens = tokens::estimate_view_tokens(&session.messages);
1386        let reduced_tokens = tokens::estimate_view_tokens(&view);
1387        if reduced_tokens >= source_tokens {
1388            return Err(ServiceError::UnsupportedAction(format!(
1389                "session `{}` has no token-reducing reversible projection",
1390                params.locator.session_id
1391            )));
1392        }
1393
1394        let store_root = self
1395            .reduction_store_root
1396            .clone()
1397            .unwrap_or_else(default_reduction_store_root);
1398        let store = crate::SessionStore::open(&store_root).map_err(operation)?;
1399        let rescue_id = format!("rescue-{}", generated_session_id());
1400        let imported = session
1401            .imported_message_count
1402            .unwrap_or(session.messages.len())
1403            .min(session.messages.len());
1404        let sidecar_jsonl = session.to_native_jsonl_v2(&session.messages[imported..]);
1405        let view_jsonl = messages_jsonl(&view)?;
1406        let title = format!(
1407            "Reduced {} continuation from {}",
1408            params.target_harness.id(),
1409            params.locator.session_id
1410        );
1411
1412        // Durability order is intentional: the full source of truth lands
1413        // before either object that can refer to it. A crash may leave an
1414        // unused sidecar, but can never leave a reduced view whose originals
1415        // were not durably written first.
1416        store
1417            .save_sidecar(&rescue_id, &sidecar_jsonl)
1418            .map_err(operation)?;
1419        store
1420            .save_reduction_log(&rescue_id, &log)
1421            .map_err(operation)?;
1422        store
1423            .save(&rescue_id, &title, &view_jsonl)
1424            .map_err(operation)?;
1425
1426        let source_bytes = serde_json::to_vec(&session.messages)
1427            .map_err(|error| ServiceError::Operation(error.to_string()))?
1428            .len() as u64;
1429        let reduced_bytes = serde_json::to_vec(&view)
1430            .map_err(|error| ServiceError::Operation(error.to_string()))?
1431            .len() as u64;
1432        store
1433            .set_reduction_stats(
1434                &rescue_id,
1435                &title,
1436                source_bytes,
1437                reduced_bytes,
1438                log.reductions.len() as u32,
1439            )
1440            .map_err(operation)?;
1441
1442        // The receipt is issued only after a real disk reload. This proves
1443        // the exact files another process will consume, not the convenient
1444        // in-memory values that produced them.
1445        let reloaded_sidecar = store
1446            .load_sidecar(&rescue_id)
1447            .map_err(operation)?
1448            .ok_or_else(|| ServiceError::Operation("reduction sidecar disappeared".into()))?;
1449        let reloaded_sidecar = Session::from_sidecar_str(&reloaded_sidecar).map_err(operation)?;
1450        let reloaded_log = store
1451            .load_reduction_log(&rescue_id)
1452            .map_err(operation)?
1453            .ok_or_else(|| ServiceError::Operation("reduction log disappeared".into()))?;
1454        let reloaded_view = parse_messages_jsonl(&store.load(&rescue_id).map_err(operation)?)?;
1455        reduce::verify_log(&reloaded_log, &reloaded_sidecar).map_err(operation)?;
1456        // `sc.reduction` is deliberately in-memory-only metadata: it must
1457        // never leak onto a provider-facing transcript. Reapplying the
1458        // durable log to the durable sidecar restores those ids. Comparing
1459        // its wire form with the transcript reloaded above proves that the
1460        // persisted view is exactly the deterministic projection before we
1461        // use the restamped form for inversion.
1462        let (restamped_view, restamped_log) =
1463            reduce::project_messages(&reloaded_sidecar.messages, &policy, &reloaded_log);
1464        if messages_jsonl(&restamped_view)? != messages_jsonl(&reloaded_view)? {
1465            return Err(ServiceError::Operation(
1466                "persisted reduction view does not match its durable log and sidecar".into(),
1467            ));
1468        }
1469        if restamped_log != reloaded_log {
1470            return Err(ServiceError::Operation(
1471                "reapplying the durable reduction log changed its identity".into(),
1472            ));
1473        }
1474        let inverted =
1475            reduce::invert(&restamped_view, &reloaded_log, &reloaded_sidecar).map_err(operation)?;
1476        if inverted != session.messages {
1477            return Err(ServiceError::Operation(
1478                "reduction inversion did not restore the source messages byte-exactly".into(),
1479            ));
1480        }
1481
1482        let ratio = source_tokens as f64 / reduced_tokens.max(1) as f64;
1483        let sidecar_path = store.sidecar_path(&rescue_id);
1484        let reduction_log_path = store.reduction_log_path(&rescue_id).map_err(operation)?;
1485        let bootstrap_prompt = reduced_bootstrap_prompt(
1486            &params.locator,
1487            params.target_harness,
1488            &view_jsonl,
1489            &sidecar_path,
1490            &reduction_log_path,
1491        );
1492        let mut reduced_session = session.clone();
1493        reduced_session.meta.session_id = Some(rescue_id.clone());
1494        reduced_session.messages = view;
1495
1496        Ok(json!({
1497            "session": normalized_session_json(&reduced_session),
1498            "bootstrap_prompt": bootstrap_prompt,
1499            "receipt": {
1500                "id": rescue_id,
1501                "sidecar_id": rescue_id,
1502                "source_harness": params.locator.harness,
1503                "target_harness": params.target_harness.id(),
1504                "source_tokens": source_tokens,
1505                "reduced_tokens": reduced_tokens,
1506                "ratio": ratio,
1507                "source_bytes": source_bytes,
1508                "reduced_bytes": reduced_bytes,
1509                "reductions": reloaded_log.reductions.len(),
1510                "sidecar_path": sidecar_path,
1511                "reduction_log_path": reduction_log_path,
1512                "verified": true,
1513                "reversible": true,
1514            }
1515        }))
1516    }
1517
1518    /// Recognize the one request family whose waiting happens entirely
1519    /// outside this service's state, and hand a transport the half it can run
1520    /// off the task that owns the service.
1521    ///
1522    /// Opening a runtime is the only door here that waits on a foreign
1523    /// program: it spawns the harness's own binary and completes that
1524    /// program's protocol handshake, which takes as long as the program takes
1525    /// to answer. A transport that awaited the whole request inline would
1526    /// stop reading its own input for that whole time, so ONE slow launch
1527    /// would queue every later request on the same server — including reads
1528    /// like `sessions.discover` that touch no runtime at all. Splitting the
1529    /// request lets the transport spawn [`RuntimeOpen::open`] and keep
1530    /// reading, then pay only the short bookkeeping half
1531    /// ([`Self::register_open_runtime`]) when the runtime is up.
1532    ///
1533    /// `None` for every other method: those are answered by
1534    /// [`Self::handle_async`] as before.
1535    pub fn runtime_open(request: &Value) -> Option<RuntimeOpen> {
1536        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1537            return None;
1538        }
1539        let method = request.get("method").and_then(Value::as_str)?;
1540        if !RUNTIME_OPEN_METHODS.contains(&method) {
1541            return None;
1542        }
1543        Some(RuntimeOpen {
1544            id: request.get("id").cloned().unwrap_or(Value::Null),
1545            method: method.to_string(),
1546            params: request.get("params").cloned().unwrap_or_else(|| json!({})),
1547        })
1548    }
1549
1550    /// Recognize a [`DETACHED_METHODS`] request and hand a transport the
1551    /// whole of it: the service-state half is read here and now, and what
1552    /// remains waits on a foreign program with nothing of this service's in
1553    /// hand.
1554    ///
1555    /// Same reason as [`Self::runtime_open`], different doors. Probing a
1556    /// harness starts it and completes its handshake; couriering a message
1557    /// runs a `claude` process to completion; a conversation verb runs the
1558    /// harness's own CLI or calls its HTTP API. A transport that awaited any
1559    /// of those inline would stop reading its own input for that whole time,
1560    /// so one probe of an unhealthy harness would queue every later request
1561    /// on the same server.
1562    ///
1563    /// Unlike an opening runtime there is no bookkeeping half: the answer
1564    /// [`DetachedCall::run`] produces is the caller's complete response, so a
1565    /// transport writes it without coming back here.
1566    ///
1567    /// `None` for every other method — including the LIVE `sessions.new` /
1568    /// `sessions.reset` door and `runtimes.close`, which wait on a runtime
1569    /// connection this service owns and so are split off by
1570    /// [`Self::detach_runtime`] instead.
1571    pub fn detach(&self, request: &Value) -> Option<DetachedCall> {
1572        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1573            return None;
1574        }
1575        let method = request.get("method").and_then(Value::as_str)?;
1576        if !DETACHED_METHODS.contains(&method) {
1577            return None;
1578        }
1579        let id = request.get("id").cloned().unwrap_or(Value::Null);
1580        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
1581        let work = match method {
1582            "harness.v1.harnesses.list" | "harness.v1.harnesses.probe" => self
1583                .inventory_work(method, params)
1584                .map(DetachedWork::Inventory),
1585            "harness.v1.sessions.message" => {
1586                decode::<MessageSessionParams>(params).map(DetachedWork::Message)
1587            }
1588            _ => {
1589                let verb = match method {
1590                    "harness.v1.sessions.new" => crate::SessionVerb::New,
1591                    "harness.v1.sessions.reset" => crate::SessionVerb::Reset,
1592                    "harness.v1.sessions.archive" => crate::SessionVerb::Archive,
1593                    _ => crate::SessionVerb::Delete,
1594                };
1595                match decode::<crate::SessionMutation>(params) {
1596                    Ok(mutation) => {
1597                        match crate::sessions_control::door(&mutation.harness, verb) {
1598                            // The live door needs the open runtime connection
1599                            // this service owns; it stays inline.
1600                            Ok(crate::SessionDoor::Live(_)) => return None,
1601                            Ok(_) => Ok(DetachedWork::SessionMutation { verb, mutation }),
1602                            Err(error) => Err(session_control_error(error)),
1603                        }
1604                    }
1605                    Err(error) => Err(error),
1606                }
1607            }
1608        };
1609        Some(DetachedCall {
1610            id,
1611            method: method.to_string(),
1612            work: work.map(Work::Free),
1613        })
1614    }
1615
1616    /// Recognize the two doors that wait on a runtime THIS SERVICE OWNS, and
1617    /// hand a transport the whole of each by lending the connection out.
1618    ///
1619    /// `runtimes.close` surrenders its runtime for good; the LIVE
1620    /// `sessions.new` / `sessions.reset` door borrows one for the length of
1621    /// the slash command and gives it back through
1622    /// [`Self::finish_detached`]. Both are bounded by
1623    /// [`RUNTIME_CONTROL_DEADLINE`], and a wedged runtime spends all of it —
1624    /// which is exactly as long as a transport that awaited them inline would
1625    /// stop reading its own input.
1626    ///
1627    /// `None` for every other method, and for the `sessions.new` /
1628    /// `sessions.reset` doors that are not live: [`Self::detach`] owns those.
1629    pub fn detach_runtime(&mut self, request: &Value) -> Option<DetachedCall> {
1630        if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
1631            return None;
1632        }
1633        let method = request.get("method").and_then(Value::as_str)?;
1634        let id = request.get("id").cloned().unwrap_or(Value::Null);
1635        let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
1636        let work = match method {
1637            "harness.v1.runtimes.close" => decode::<RuntimeConnectionParams>(params)
1638                .and_then(|params| self.surrender_runtime(&params.connection))
1639                .map(|(runtime, process_group)| {
1640                    Work::Runtime(RuntimeWork::Close {
1641                        runtime,
1642                        process_group,
1643                    })
1644                }),
1645            "harness.v1.sessions.new" | "harness.v1.sessions.reset" => {
1646                let verb = if method == "harness.v1.sessions.new" {
1647                    crate::SessionVerb::New
1648                } else {
1649                    crate::SessionVerb::Reset
1650                };
1651                let mutation = decode::<crate::SessionMutation>(params).ok()?;
1652                // Everything but the live door — including a refusal and a
1653                // request naming no connection — is `detach`'s or
1654                // `handle_async`'s to answer.
1655                let Ok(crate::SessionDoor::Live(command)) =
1656                    crate::sessions_control::door(&mutation.harness, verb)
1657                else {
1658                    return None;
1659                };
1660                let connection = mutation
1661                    .connection
1662                    .clone()
1663                    .filter(|value| !value.trim().is_empty())?;
1664                self.lend_runtime(&connection).map(|runtime| {
1665                    let session = live_session_name(runtime.as_ref(), &mutation);
1666                    Work::Runtime(RuntimeWork::LiveCommand {
1667                        connection,
1668                        runtime,
1669                        verb,
1670                        mutation,
1671                        command,
1672                        session,
1673                    })
1674                })
1675            }
1676            _ => return None,
1677        };
1678        Some(DetachedCall {
1679            id,
1680            method: method.to_string(),
1681            work,
1682        })
1683    }
1684
1685    /// Take back whatever a detached call borrowed and hand over the caller's
1686    /// response. Every answer from [`DetachedCall::run`] comes through here,
1687    /// so a lent-out connection is back in the service before the response
1688    /// that used it is written.
1689    pub fn finish_detached(&mut self, answer: DetachedAnswer) -> Value {
1690        let DetachedAnswer { response, returned } = answer;
1691        if let Some(ReturnedRuntime {
1692            connection,
1693            runtime,
1694        }) = returned
1695        {
1696            self.runtimes_in_flight.remove(&connection);
1697            self.runtimes.insert(connection, runtime);
1698        }
1699        response
1700    }
1701
1702    /// Answer a request split out by [`Self::runtime_open`] and already
1703    /// awaited by [`RuntimeOpen::open`]: register the runtime this service now
1704    /// owns and build its JSON-RPC response.
1705    pub async fn finish_runtime_open(&mut self, opened: OpenedRuntime) -> Value {
1706        let OpenedRuntime { id, outcome } = opened;
1707        let result = match outcome {
1708            Ok(open) => self.register_open_runtime(open).await,
1709            Err(error) => Err(error),
1710        };
1711        service_response(id, result)
1712    }
1713
1714    /// Take ownership of an opened runtime.
1715    async fn register_open_runtime(
1716        &mut self,
1717        open: OpenRuntime,
1718    ) -> std::result::Result<Value, ServiceError> {
1719        match open {
1720            OpenRuntime::Hosted {
1721                runtime,
1722                capabilities,
1723                workspace,
1724            } => {
1725                self.insert_hosted_runtime(runtime, capabilities, workspace)
1726                    .await
1727            }
1728            OpenRuntime::Joined { runtime } => self.insert_runtime(runtime),
1729        }
1730    }
1731
1732    async fn runtime_call(
1733        &mut self,
1734        method: &str,
1735        params: Value,
1736    ) -> std::result::Result<Value, ServiceError> {
1737        match method {
1738            "harness.v1.runtimes.capabilities" => {
1739                let params = decode::<RuntimeBackendParams>(params)?;
1740                let backend = runtime_backend(&params)?;
1741                Ok(json!({
1742                    "harness": backend.harness(),
1743                    "capabilities": backend.capabilities(),
1744                }))
1745            }
1746            method if RUNTIME_OPEN_METHODS.contains(&method) => {
1747                self.register_open_runtime(open_runtime(method, params).await?)
1748                    .await
1749            }
1750            "harness.v1.runtimes.send_input" => {
1751                let params = decode::<RuntimeInputParams>(params)?;
1752                let image_urls = validate_runtime_image_urls(params.image_urls)?;
1753                let runtime = self.runtime_mut(&params.connection)?;
1754                let turn_id = within_control_deadline(
1755                    method,
1756                    runtime.send_input(RuntimeInput {
1757                        text: params.text,
1758                        image_urls,
1759                    }),
1760                )
1761                .await?
1762                .map_err(operation)?;
1763                Ok(json!({"turn_id": turn_id}))
1764            }
1765            "harness.v1.runtimes.interrupt" => {
1766                let params = decode::<RuntimeConnectionParams>(params)?;
1767                within_control_deadline(method, self.runtime_mut(&params.connection)?.interrupt())
1768                    .await?
1769                    .map_err(operation)?;
1770                Ok(json!({}))
1771            }
1772            "harness.v1.runtimes.steer" => {
1773                let params = decode::<RuntimeInputParams>(params)?;
1774                if !params.image_urls.is_empty() {
1775                    return Err(ServiceError::InvalidParams(
1776                        "runtime steering accepts text only".into(),
1777                    ));
1778                }
1779                let text = params.text.trim();
1780                if text.is_empty() || text.chars().count() > 50_000 {
1781                    return Err(ServiceError::InvalidParams(
1782                        "runtime steering requires 1 to 50,000 text characters".into(),
1783                    ));
1784                }
1785                within_control_deadline(
1786                    method,
1787                    self.runtime_mut(&params.connection)?
1788                        .steer(text.to_string()),
1789                )
1790                .await?
1791                .map_err(operation)?;
1792                Ok(json!({}))
1793            }
1794            "harness.v1.runtimes.respond" => {
1795                let params = decode::<RuntimeRespondParams>(params)?;
1796                let request_id = params.request_id.clone();
1797                within_control_deadline(
1798                    method,
1799                    self.runtime_mut(&params.connection)?
1800                        .respond(params.request_id, params.response),
1801                )
1802                .await?
1803                .map_err(operation)?;
1804                // ORCH-9: an answered request is no longer waiting for one.
1805                self.approvals.answered(&params.connection, &request_id);
1806                Ok(json!({}))
1807            }
1808            "harness.v1.runtimes.acquire_control" => {
1809                let params = decode::<RuntimeConnectionParams>(params)?;
1810                let snapshot = within_control_deadline(
1811                    method,
1812                    self.runtime_mut(&params.connection)?.acquire_control(),
1813                )
1814                .await?
1815                .map_err(operation)?;
1816                serde_json::to_value(snapshot)
1817                    .map_err(|error| ServiceError::Operation(error.to_string()))
1818            }
1819            "harness.v1.runtimes.heartbeat" => {
1820                let params = decode::<RuntimeConnectionParams>(params)?;
1821                let snapshot = within_control_deadline(
1822                    method,
1823                    self.runtime_mut(&params.connection)?.heartbeat(),
1824                )
1825                .await?
1826                .map_err(operation)?;
1827                serde_json::to_value(snapshot)
1828                    .map_err(|error| ServiceError::Operation(error.to_string()))
1829            }
1830            "harness.v1.runtimes.detach" => {
1831                let params = decode::<RuntimeConnectionParams>(params)?;
1832                let snapshot =
1833                    within_control_deadline(method, self.runtime_mut(&params.connection)?.detach())
1834                        .await?
1835                        .map_err(operation)?;
1836                serde_json::to_value(snapshot)
1837                    .map_err(|error| ServiceError::Operation(error.to_string()))
1838            }
1839            "harness.v1.runtimes.terminal_instructions" => {
1840                let params = decode::<RuntimeConnectionParams>(params)?;
1841                let launch = self
1842                    .terminal_launches
1843                    .get(&params.connection)
1844                    .ok_or_else(|| {
1845                        ServiceError::Operation(
1846                            "this runtime is not hosted for terminal attachment".into(),
1847                        )
1848                    })?;
1849                Ok(json!({"launch":launch}))
1850            }
1851            "harness.v1.runtimes.close" => {
1852                let params = decode::<RuntimeConnectionParams>(params)?;
1853                let (runtime, process_group) = self.surrender_runtime(&params.connection)?;
1854                close_runtime(runtime, process_group).await
1855            }
1856            _ => Err(ServiceError::MethodNotFound),
1857        }
1858    }
1859
1860    /// Deliver one message into a session that is running right now.
1861    #[cfg(feature = "adapter-api")]
1862    async fn message_call(&self, params: Value) -> std::result::Result<Value, ServiceError> {
1863        let params = decode::<MessageSessionParams>(params)?;
1864        Ok(message_live_session(&params, &crate::claude_peer::ProcessCourierRunner).await)
1865    }
1866
1867    #[cfg(feature = "adapter-api")]
1868    fn harness_settings_call(
1869        &self,
1870        method: &str,
1871        params: Value,
1872    ) -> std::result::Result<Value, ServiceError> {
1873        let homes = crate::HarnessHomes::default();
1874        match method {
1875            "harness.v1.harnesses.settings" => {
1876                let params = decode::<HarnessSettingsParams>(params)?;
1877                let report = crate::inspect_harness_interop_settings(&homes, &params.harness)
1878                    .map_err(|error| ServiceError::Operation(error.to_string()))?;
1879                serde_json::to_value(report)
1880                    .map_err(|error| ServiceError::Operation(error.to_string()))
1881            }
1882            "harness.v1.harnesses.configure" => {
1883                let params = decode::<ConfigureHarnessParams>(params)?;
1884                let report = crate::configure_harness_interop_settings(
1885                    &homes,
1886                    &params.harness,
1887                    &params.changes,
1888                    params.expected_revision.as_deref(),
1889                )
1890                .map_err(|error| ServiceError::Operation(error.to_string()))?;
1891                serde_json::to_value(report)
1892                    .map_err(|error| ServiceError::Operation(error.to_string()))
1893            }
1894            _ => Err(ServiceError::MethodNotFound),
1895        }
1896    }
1897
1898    fn insert_runtime(
1899        &mut self,
1900        runtime: Box<dyn RuntimeConnection>,
1901    ) -> std::result::Result<Value, ServiceError> {
1902        let connection = format!("runtime-{}", self.next_runtime);
1903        self.next_runtime += 1;
1904        let handle = runtime.handle().clone();
1905        self.runtime_sequences
1906            .entry(handle.runtime_id.clone())
1907            .or_insert(0);
1908        self.runtimes.insert(connection.clone(), runtime);
1909        Ok(json!({"connection": connection, "handle": handle}))
1910    }
1911
1912    #[cfg(feature = "adapter-api")]
1913    async fn insert_hosted_runtime(
1914        &mut self,
1915        runtime: Box<dyn RuntimeConnection>,
1916        capabilities: crate::RuntimeCapabilities,
1917        workspace: PathBuf,
1918    ) -> std::result::Result<Value, ServiceError> {
1919        let (host, connection) = HostedHarnessRuntime::spawn(runtime, capabilities);
1920        let token: std::sync::Arc<str> = crate::server::generate_token().into();
1921        let server = crate::server::run_frontend_http(
1922            host.clone(),
1923            host.frontend_sender(),
1924            "127.0.0.1:0",
1925            token.clone(),
1926            connection.handle().runtime_id.clone(),
1927        )
1928        .await
1929        .map_err(|error| ServiceError::Operation(error.to_string()))?;
1930        let source = LiveRuntimeSource {
1931            harness: connection.handle().harness.as_str().to_string(),
1932            session_id: connection.handle().runtime_id.clone(),
1933            workspace: workspace.clone(),
1934        };
1935        let registration = register_live_runtime(
1936            connection.handle().runtime_id.clone(),
1937            source.clone(),
1938            format!("http://{}", server.address()),
1939            token.to_string(),
1940        )
1941        .map_err(|error| ServiceError::Operation(error.to_string()))?;
1942        let endpoint = registration.endpoint().to_string();
1943        let launch = StructuredLaunch {
1944            cwd: workspace,
1945            // Pin attachment to the executable hosting this runtime. A bare
1946            // `supercode` could resolve to an older global install whose CLI
1947            // does not understand the receipt it is being asked to open.
1948            program: std::env::current_exe()
1949                .ok()
1950                .map(|path| path.to_string_lossy().into_owned())
1951                .unwrap_or_else(|| "supercode".into()),
1952            arguments: vec![
1953                "harness".into(),
1954                "attach".into(),
1955                "--endpoint".into(),
1956                endpoint,
1957                "--harness".into(),
1958                source.harness,
1959                "--session".into(),
1960                source.session_id,
1961            ],
1962            env: BTreeMap::new(),
1963        };
1964        let lease = HostedRuntimeLease {
1965            connection,
1966            _host: host,
1967            _registration: registration,
1968            _server: server,
1969        };
1970        let opened = self.insert_runtime(Box::new(lease))?;
1971        let connection_id = opened["connection"]
1972            .as_str()
1973            .expect("insert_runtime returns a connection id")
1974            .to_string();
1975        self.terminal_launches.insert(connection_id, launch);
1976        Ok(opened)
1977    }
1978
1979    #[cfg(not(feature = "adapter-api"))]
1980    async fn insert_hosted_runtime(
1981        &mut self,
1982        runtime: Box<dyn RuntimeConnection>,
1983        _capabilities: crate::RuntimeCapabilities,
1984        _workspace: PathBuf,
1985    ) -> std::result::Result<Value, ServiceError> {
1986        self.insert_runtime(runtime)
1987    }
1988
1989    fn runtime_mut(
1990        &mut self,
1991        connection: &str,
1992    ) -> std::result::Result<&mut Box<dyn RuntimeConnection>, ServiceError> {
1993        if self.runtimes_in_flight.contains(connection) {
1994            return Err(self.lent_out(connection));
1995        }
1996        self.runtimes.get_mut(connection).ok_or_else(|| {
1997            ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
1998        })
1999    }
2000
2001    /// What a caller is told about a connection that is out on a detached
2002    /// call. It is not gone and it is not free: it is mid-call, which is the
2003    /// same answer the runtime itself gives a second turn.
2004    fn lent_out(&self, connection: &str) -> ServiceError {
2005        ServiceError::Operation(format!(
2006            "runtime connection `{connection}`: a harness turn is already in progress"
2007        ))
2008    }
2009
2010    /// Take a runtime OUT of the service for the duration of one detached
2011    /// call, leaving its name marked as lent out.
2012    fn lend_runtime(
2013        &mut self,
2014        connection: &str,
2015    ) -> std::result::Result<Box<dyn RuntimeConnection>, ServiceError> {
2016        if self.runtimes_in_flight.contains(connection) {
2017            return Err(self.lent_out(connection));
2018        }
2019        let runtime = self.runtimes.remove(connection).ok_or_else(|| {
2020            ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
2021        })?;
2022        self.runtimes_in_flight.insert(connection.to_string());
2023        Ok(runtime)
2024    }
2025
2026    /// Surrender a runtime for good: the connection and everything the
2027    /// service hung off it are gone before its teardown is even attempted.
2028    ///
2029    /// `close` is what a caller reaches for when a runtime has stopped
2030    /// answering, and a runtime that has stopped answering is exactly the one
2031    /// whose graceful close cannot complete: a hosted runtime's own loop
2032    /// parks on the call the runtime never answered, so it never dequeues the
2033    /// shutdown either. Keeping the entry until teardown succeeded made a
2034    /// wedged runtime permanent — every later call on that connection, and
2035    /// every new turn, answered "a harness turn is already in progress" with
2036    /// no way to take the connection back.
2037    fn surrender_runtime(
2038        &mut self,
2039        connection: &str,
2040    ) -> std::result::Result<(Box<dyn RuntimeConnection>, Option<u32>), ServiceError> {
2041        if self.runtimes_in_flight.contains(connection) {
2042            return Err(self.lent_out(connection));
2043        }
2044        let runtime = self.runtimes.remove(connection).ok_or_else(|| {
2045            ServiceError::InvalidParams(format!("unknown runtime connection `{connection}`"))
2046        })?;
2047        let process_group = runtime_process_group(runtime.handle());
2048        let runtime_id = runtime.handle().runtime_id.clone();
2049        self.terminal_launches.remove(connection);
2050        self.runtime_sequences.remove(&runtime_id);
2051        self.approvals.forget(connection);
2052        Ok((runtime, process_group))
2053    }
2054
2055    /// SIGKILL the process group of every runtime this service owns, without
2056    /// waiting on any of them.
2057    ///
2058    /// A host leaving for good calls this BEFORE dropping the service. The
2059    /// handle this service holds is not the runtime's connection: a hosted
2060    /// runtime's real transport lives in the task driving it, so neither
2061    /// exiting the process nor dropping these handles reaches the harness
2062    /// process — while dropping them does remove each runtime's live-runtime
2063    /// receipt. Signalling first is what keeps a removed receipt from
2064    /// advertising a harness that is still running.
2065    pub fn kill_all_runtime_groups(&self) -> usize {
2066        self.runtimes
2067            .values()
2068            .filter(|runtime| kill_runtime_process_group(runtime_process_group(runtime.handle())))
2069            .count()
2070    }
2071
2072    /// ORCH-19: run one conversation-lifecycle verb through the harness's own
2073    /// door.
2074    ///
2075    /// Two doors, one shape. A CLI / HTTP / own-store door is self-contained
2076    /// in [`crate::sessions_control`]. A LIVE door (Hermes's and OpenClaw's
2077    /// `/new` and `/reset`, which are slash commands their gateway interprets
2078    /// INSIDE a session) is performed here, because only the service owns the
2079    /// open runtime connection — the command is typed through the very same
2080    /// `send_input` path a human's message takes, so supercode invents no
2081    /// private channel.
2082    async fn mutate_session(
2083        &mut self,
2084        verb: crate::SessionVerb,
2085        params: Value,
2086    ) -> std::result::Result<Value, ServiceError> {
2087        let mutation = decode::<crate::SessionMutation>(params)?;
2088        let door = crate::sessions_control::door(&mutation.harness, verb)
2089            .map_err(session_control_error)?;
2090        let outcome = match door {
2091            // The live door types the slash command through an open hosted
2092            // runtime, which only exists with the `adapter-api` feature; the
2093            // CLI / HTTP / own-store doors below need nothing extra.
2094            #[cfg(not(feature = "adapter-api"))]
2095            crate::SessionDoor::Live(command) => {
2096                return Err(ServiceError::Operation(format!(
2097                    "`{}` performs `sessions.{}` by typing `{command}` into a live driven \
2098                     session, which needs this build's `adapter-api` feature",
2099                    mutation.harness,
2100                    verb.as_str()
2101                )));
2102            }
2103            #[cfg(feature = "adapter-api")]
2104            crate::SessionDoor::Live(command) => {
2105                let connection = mutation
2106                    .connection
2107                    .clone()
2108                    .filter(|value| !value.trim().is_empty())
2109                    .ok_or_else(|| {
2110                        ServiceError::InvalidParams(format!(
2111                            "`{}` performs `sessions.{}` by typing `{command}` into a live \
2112                             driven session: pass the `connection` of an open runtime \
2113                             (`harness.v1.runtimes.start`)",
2114                            mutation.harness,
2115                            verb.as_str()
2116                        ))
2117                    })?;
2118                let runtime = self.runtime_mut(&connection)?;
2119                let session = live_session_name(runtime.as_ref(), &mutation);
2120                // Typing into a live session is a control call on an open
2121                // runtime, and a wedged runtime never accepts one, so it is
2122                // bounded exactly like the other control verbs. A transport
2123                // with a loop of its own lends the connection out instead of
2124                // waiting here: see [`Self::detach_runtime`].
2125                return type_live_command(runtime.as_mut(), verb, &mutation, command, session)
2126                    .await;
2127            }
2128            _ => run_session_mutation(verb, &mutation).await?,
2129        };
2130        serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
2131    }
2132
2133    /// Answer an inventory request whole, for callers that have nowhere to
2134    /// put the waiting half. A transport with a loop of its own splits it
2135    /// instead: see [`Self::detach`].
2136    async fn inventory_call(
2137        &self,
2138        method: &str,
2139        params: Value,
2140    ) -> std::result::Result<Value, ServiceError> {
2141        run_inventory(self.inventory_work(method, params)?).await
2142    }
2143
2144    /// The half of an inventory request that reads this service's state:
2145    /// resolve the selection and count the persisted sessions each row
2146    /// reports. What remains — finding executables, asking them their
2147    /// version, and (at `probe: handshake`) starting each harness and
2148    /// completing its protocol handshake — touches no service state at all.
2149    fn inventory_work(
2150        &self,
2151        method: &str,
2152        params: Value,
2153    ) -> std::result::Result<InventoryWork, ServiceError> {
2154        let mut params = decode::<HarnessInventoryParams>(params)?;
2155        if method == "harness.v1.harnesses.probe" {
2156            let harness = params.harness.take().ok_or_else(|| {
2157                ServiceError::InvalidParams("harnesses.probe requires `harness`".into())
2158            })?;
2159            params.harnesses = vec![harness];
2160        }
2161        let selected = params
2162            .harnesses
2163            .iter()
2164            .map(HarnessId::as_str)
2165            .collect::<std::collections::BTreeSet<_>>();
2166        let supported = harness_support_registry()
2167            .harnesses
2168            .into_iter()
2169            .filter(|descriptor| selected.is_empty() || selected.contains(descriptor.id.as_str()))
2170            .collect::<Vec<_>>();
2171        if !params.harnesses.is_empty() && supported.len() != selected.len() {
2172            let known = supported
2173                .iter()
2174                .map(|harness| harness.id.as_str())
2175                .collect::<std::collections::BTreeSet<_>>();
2176            let missing = params
2177                .harnesses
2178                .iter()
2179                .filter(|id| !known.contains(id.as_str()))
2180                .map(HarnessId::as_str)
2181                .collect::<Vec<_>>();
2182            return Err(ServiceError::InvalidParams(format!(
2183                "unknown harness(es): {}",
2184                missing.join(", ")
2185            )));
2186        }
2187        let global_counts = params
2188            .include_sessions
2189            .then(|| self.session_counts(None, &params.harnesses));
2190        let workspace_counts = params
2191            .include_sessions
2192            .then(|| {
2193                params
2194                    .workspace
2195                    .as_deref()
2196                    .map(|workspace| self.session_counts(Some(workspace), &params.harnesses))
2197            })
2198            .flatten();
2199        Ok(InventoryWork {
2200            params,
2201            supported,
2202            global_counts,
2203            workspace_counts,
2204        })
2205    }
2206
2207    #[cfg(feature = "adapter-api")]
2208    async fn harness_authentication_call(
2209        &self,
2210        method: &str,
2211        params: Value,
2212    ) -> std::result::Result<Value, ServiceError> {
2213        match method {
2214            "harness.v1.harnesses.auth.methods" | "harness.v1.harnesses.auth.verify" => {
2215                let params = decode::<HarnessAuthenticationParams>(params)?;
2216                serde_json::to_value(crate::inspect_harness_authentication(&params.harness).await)
2217                    .map_err(|error| ServiceError::Operation(error.to_string()))
2218            }
2219            "harness.v1.harnesses.auth.begin" => {
2220                let params = decode::<BeginHarnessAuthenticationParams>(params)?;
2221                let cwd = params
2222                    .cwd
2223                    .or_else(|| std::env::current_dir().ok())
2224                    .unwrap_or_else(|| PathBuf::from("."));
2225                let plan = crate::harness_authentication_plan(
2226                    &params.harness,
2227                    params.environment,
2228                    params.method,
2229                    &cwd,
2230                )
2231                .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
2232                serde_json::to_value(plan)
2233                    .map_err(|error| ServiceError::Operation(error.to_string()))
2234            }
2235            _ => Err(ServiceError::MethodNotFound),
2236        }
2237    }
2238
2239    fn session_counts(
2240        &self,
2241        workspace: Option<&Path>,
2242        harnesses: &[HarnessId],
2243    ) -> BTreeMap<String, usize> {
2244        let mut counts = BTreeMap::new();
2245        for session in self
2246            .catalog
2247            .discover(&DiscoveryQuery {
2248                workspace: workspace.map(Path::to_path_buf),
2249                harnesses: harnesses.to_vec(),
2250                ..DiscoveryQuery::default()
2251            })
2252            .unwrap_or_default()
2253        {
2254            *counts
2255                .entry(session.locator.harness.as_str().to_string())
2256                .or_insert(0) += 1;
2257        }
2258        counts
2259    }
2260}
2261
2262#[async_trait::async_trait]
2263impl SdkService for HarnessSessionService {
2264    fn capabilities(&self) -> SdkCapabilities {
2265        SdkCapabilities::default()
2266    }
2267
2268    async fn execute(&mut self, request: SdkRequest) -> Result<Value, SdkError> {
2269        if request.operation == SdkOperation::Events {
2270            let events = self
2271                .poll_sdk_events()
2272                .await
2273                .into_iter()
2274                .map(|(_, event)| event)
2275                .collect::<Vec<_>>();
2276            return serde_json::to_value(events).map_err(|error| {
2277                SdkError::new(
2278                    SdkErrorCode::Execution,
2279                    request.operation,
2280                    error.to_string(),
2281                )
2282            });
2283        }
2284        if self.runtimes.is_empty()
2285            && matches!(
2286                request.operation,
2287                SdkOperation::Input
2288                    | SdkOperation::Interrupt
2289                    | SdkOperation::Steer
2290                    | SdkOperation::Respond
2291                    | SdkOperation::Close
2292            )
2293        {
2294            return Err(SdkError::unsupported(request.operation));
2295        }
2296        let method = request
2297            .operation
2298            .method()
2299            .ok_or_else(|| SdkError::unsupported(request.operation))?;
2300        let result = match request.operation {
2301            SdkOperation::Discover
2302            | SdkOperation::Load
2303            | SdkOperation::Export
2304            | SdkOperation::ProfilesList
2305            | SdkOperation::ProfilesGet
2306            | SdkOperation::ProfilesCreate
2307            | SdkOperation::ProfilesDelete
2308            | SdkOperation::SkillsList
2309            | SdkOperation::SkillsInstall
2310            | SdkOperation::SkillsRemove
2311            | SdkOperation::ChannelsList
2312            | SdkOperation::RoutesList
2313            | SdkOperation::TriggersList
2314            | SdkOperation::ChannelsStatus
2315            | SdkOperation::MemoryShow
2316            | SdkOperation::MemorySearch
2317            | SdkOperation::JobsList
2318            | SdkOperation::JobsGet
2319            | SdkOperation::JobsCreate
2320            | SdkOperation::JobsUpdate
2321            | SdkOperation::JobsPause
2322            | SdkOperation::JobsResume
2323            | SdkOperation::JobsRun
2324            | SdkOperation::JobsDelete
2325            | SdkOperation::JobsNotepad
2326            | SdkOperation::JobsNotepadSet
2327            | SdkOperation::JobsNotepadDelete
2328            | SdkOperation::RunsList
2329            | SdkOperation::RunsGet
2330            | SdkOperation::ApprovalsList
2331            | SdkOperation::OrchestrationLoad
2332            | SdkOperation::OrchestrationSave
2333            | SdkOperation::OrchestrationCompile
2334            | SdkOperation::OrchestrationDecompile
2335            | SdkOperation::OrchestrationImport
2336            | SdkOperation::OrchestrationExport
2337            | SdkOperation::WorkflowLoad => self.call(method, request.params),
2338            // ORCH-20: answering needs the live connection, so it takes the
2339            // async door and ends in `harness.v1.runtimes.respond`.
2340            SdkOperation::ApprovalsResolve => self.approvals_resolve(request.params).await,
2341            SdkOperation::Start
2342            | SdkOperation::Resume
2343            | SdkOperation::Input
2344            | SdkOperation::Interrupt
2345            | SdkOperation::Steer
2346            | SdkOperation::Respond
2347            | SdkOperation::Close => self.runtime_call(method, request.params).await,
2348            // ORCH-19 controlled tier. Every verb goes through the HARNESS'S
2349            // OWN door — its CLI, its HTTP API, or its slash command typed
2350            // into a live driven session — and returns the row re-read from
2351            // the harness's store afterwards.
2352            SdkOperation::SessionsNew => {
2353                self.mutate_session(crate::SessionVerb::New, request.params)
2354                    .await
2355            }
2356            SdkOperation::SessionsReset => {
2357                self.mutate_session(crate::SessionVerb::Reset, request.params)
2358                    .await
2359            }
2360            SdkOperation::SessionsArchive => {
2361                self.mutate_session(crate::SessionVerb::Archive, request.params)
2362                    .await
2363            }
2364            SdkOperation::SessionsDelete => {
2365                self.mutate_session(crate::SessionVerb::Delete, request.params)
2366                    .await
2367            }
2368            SdkOperation::Events => unreachable!("handled before method dispatch"),
2369        };
2370        result.map_err(|error| sdk_error(request.operation, error))
2371    }
2372
2373    async fn events(&mut self) -> Result<Vec<SdkRuntimeEvent>, SdkError> {
2374        Ok(self
2375            .poll_sdk_events()
2376            .await
2377            .into_iter()
2378            .map(|(_, event)| event)
2379            .collect())
2380    }
2381}
2382
2383#[cfg(feature = "adapter-api")]
2384struct HostedRuntimeLease {
2385    connection: HostedHarnessConnection,
2386    _host: std::sync::Arc<HostedHarnessRuntime>,
2387    _registration: LiveRuntimeRegistration,
2388    _server: crate::server::FrontendHttpServer,
2389}
2390
2391#[async_trait::async_trait]
2392#[cfg(feature = "adapter-api")]
2393impl RuntimeConnection for HostedRuntimeLease {
2394    fn handle(&self) -> &crate::RuntimeHandle {
2395        self.connection.handle()
2396    }
2397
2398    async fn send_input(&mut self, input: RuntimeInput) -> crate::Result<Option<String>> {
2399        self.connection.send_input(input).await
2400    }
2401
2402    async fn next_event(&mut self) -> crate::Result<Option<crate::HarnessEvent>> {
2403        self.connection.next_event().await
2404    }
2405
2406    async fn interrupt(&mut self) -> crate::Result<()> {
2407        self.connection.interrupt().await
2408    }
2409
2410    // the lease must forward every verb its capabilities advertise; without
2411    // this, steer fell to the trait default and refused a turn it claimed
2412    async fn steer(&mut self, text: String) -> crate::Result<()> {
2413        self.connection.steer(text).await
2414    }
2415
2416    async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
2417        self.connection.respond(request_id, response).await
2418    }
2419
2420    async fn close(&mut self) -> crate::Result<()> {
2421        self.connection.close().await
2422    }
2423}
2424
2425/// One inventory request's waiting half, already separated from the service
2426/// state it reads. See [`HarnessSessionService::inventory_work`].
2427struct InventoryWork {
2428    params: HarnessInventoryParams,
2429    supported: Vec<crate::HarnessSupportDescriptor>,
2430    global_counts: Option<BTreeMap<String, usize>>,
2431    workspace_counts: Option<BTreeMap<String, usize>>,
2432}
2433
2434/// Perform one conversation-lifecycle verb through a door that is
2435/// self-contained in [`crate::sessions_control`]: the harness's own CLI, its
2436/// HTTP API, the orchestrator daemon's socket, or supercode's own store.
2437/// Touches no service state, so this runs on any task. The LIVE door is not
2438/// here — it types its slash command through a runtime connection the service
2439/// owns, and is performed by [`HarnessSessionService::mutate_session`].
2440async fn run_session_mutation(
2441    verb: crate::SessionVerb,
2442    mutation: &crate::SessionMutation,
2443) -> std::result::Result<crate::SessionMutationOutcome, ServiceError> {
2444    // Only the HTTP door actually awaits anything. The CLI, store and daemon
2445    // doors run the harness's own program, or its store, with calls that
2446    // block the calling THREAD from start to finish — a future that never
2447    // yields, which no timeout around it can interrupt and which would hold a
2448    // runtime worker for as long as the harness takes. They go to a blocking
2449    // task, where blocking is what the thread is for.
2450    let door =
2451        crate::sessions_control::door(&mutation.harness, verb).map_err(session_control_error)?;
2452    if let crate::SessionDoor::Http = door {
2453        return crate::sessions_control::mutate(verb, mutation)
2454            .await
2455            .map_err(session_control_error);
2456    }
2457    let mutation = mutation.clone();
2458    tokio::task::spawn_blocking(move || crate::sessions_control::mutate_blocking(verb, &mutation))
2459        .await
2460        .map_err(|error| {
2461            ServiceError::Operation(format!("the conversation verb could not be run: {error}"))
2462        })?
2463        .map_err(session_control_error)
2464}
2465
2466/// Probe every selected harness and assemble the report. Touches no service
2467/// state, so this runs on any task.
2468async fn run_inventory(work: InventoryWork) -> std::result::Result<Value, ServiceError> {
2469    let InventoryWork {
2470        params,
2471        supported,
2472        global_counts,
2473        workspace_counts,
2474    } = work;
2475    let probes = supported.into_iter().map(|descriptor| {
2476        let global = global_counts
2477            .as_ref()
2478            .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
2479        let workspace = workspace_counts
2480            .as_ref()
2481            .map(|counts| counts.get(descriptor.id.as_str()).copied().unwrap_or(0));
2482        probe_harness(descriptor, &params, global, workspace)
2483    });
2484    let harnesses = futures::future::join_all(probes).await;
2485    serde_json::to_value(HarnessInventoryReport {
2486        probe: params.probe,
2487        workspace: params.workspace,
2488        harnesses,
2489    })
2490    .map_err(|error| ServiceError::Operation(error.to_string()))
2491}
2492
2493async fn probe_harness(
2494    descriptor: crate::HarnessSupportDescriptor,
2495    params: &HarnessInventoryParams,
2496    global: Option<usize>,
2497    workspace: Option<usize>,
2498) -> LocalHarness {
2499    let launch = descriptor.runtime.default_launch.as_ref();
2500    // ORC-7: the orchestrator publishes no runtime launch — it is not an
2501    // adapter supercode connects a turn to. What "installed" means for it
2502    // is that its Node daemon entry is present, so the row answers from
2503    // that instead of from a PATH lookup it could never satisfy.
2504    let orchestrator_entry = (descriptor.id.as_str() == HarnessId::ORCHESTRATOR)
2505        .then(crate::orchestrator::daemon_entry)
2506        .and_then(Result::ok);
2507    let executable = match &orchestrator_entry {
2508        Some(entry) => Some(entry.clone()),
2509        None => launch.and_then(|launch| find_executable(&launch.program)),
2510    };
2511    let installed = executable.is_some();
2512    let version = if params.skip_versions || orchestrator_entry.is_some() {
2513        // The orchestrator's "executable" is a Node module, not a CLI
2514        // with a `--version` flag; running it to ask would start a daemon.
2515        None
2516    } else {
2517        match executable.as_deref() {
2518            Some(path) => executable_version(path).await,
2519            None => None,
2520        }
2521    };
2522    let configured = auth_evidence(descriptor.id.as_str());
2523    let mut auth = if configured {
2524        HarnessAuthState::Configured
2525    } else if matches!(
2526        descriptor.id.as_str(),
2527        HarnessId::CLAUDE_CODE | HarnessId::CODEX
2528    ) {
2529        // These two adapters have explicit native status/login contracts
2530        // and complete local evidence coverage (including Claude's macOS
2531        // Keychain-backed oauthAccount marker). Treating absent evidence
2532        // as unknown advertises a start that will only fail interactively.
2533        HarnessAuthState::Required
2534    } else {
2535        HarnessAuthState::Unknown
2536    };
2537    let mut runtime = if installed {
2538        HarnessRuntimeState::Degraded
2539    } else {
2540        HarnessRuntimeState::Unavailable
2541    };
2542    let is_orchestrator = descriptor.id.as_str() == HarnessId::ORCHESTRATOR;
2543    let mut reason = (!installed).then(|| {
2544        if is_orchestrator {
2545            format!(
2546                "{} is supported but its daemon entry `{}` was not found",
2547                descriptor.display_name,
2548                crate::orchestrator::DAEMON_ENTRY
2549            )
2550        } else {
2551            format!(
2552                "{} is supported but `{}` was not found on PATH",
2553                descriptor.display_name,
2554                launch
2555                    .map(|launch| launch.program.as_str())
2556                    .unwrap_or("executable")
2557            )
2558        }
2559    });
2560    let mut repair = (!installed).then(|| {
2561        if is_orchestrator {
2562            format!(
2563                "Install the `supercode-orchestrator` package so `{}` resolves.",
2564                crate::orchestrator::DAEMON_ENTRY
2565            )
2566        } else {
2567            format!(
2568                "Install {} and ensure `{}` is on PATH.",
2569                descriptor.display_name,
2570                launch
2571                    .map(|launch| launch.program.as_str())
2572                    .unwrap_or("its executable")
2573            )
2574        }
2575    });
2576
2577    if installed && params.probe == HarnessProbeLevel::Handshake {
2578        let backend_params = RuntimeBackendParams {
2579            harness: descriptor.id.clone(),
2580            protocol: None,
2581            launch: None,
2582            base_url: None,
2583            policy: RuntimePolicy::Default,
2584        };
2585        match runtime_backend(&backend_params) {
2586            Ok(backend) => {
2587                let cwd = params
2588                    .workspace
2589                    .clone()
2590                    .or_else(|| std::env::current_dir().ok())
2591                    .unwrap_or_else(|| PathBuf::from("."));
2592                let isolated = descriptor
2593                    .runtime
2594                    .default_launch
2595                    .clone()
2596                    .and_then(|launch| IsolatedProbeHome::new(descriptor.id.as_str(), launch).ok());
2597                let Some(isolated) = isolated else {
2598                    reason = Some(
2599                        "No-prompt runtime handshake could not create its isolated harness home."
2600                            .into(),
2601                    );
2602                    repair = Some(
2603                        "Check temporary-directory permissions, then run the handshake probe again."
2604                            .into(),
2605                    );
2606                    let running = probe_running_instance(descriptor.id.as_str());
2607                    return LocalHarness {
2608                        gateway: gateway_health(
2609                            descriptor.id.as_str(),
2610                            installed,
2611                            running.as_ref(),
2612                            version.as_deref(),
2613                        ),
2614                        id: descriptor.id,
2615                        display_name: descriptor.display_name,
2616                        supported: true,
2617                        installed,
2618                        executable: executable.map(|path| path.to_string_lossy().into_owned()),
2619                        version,
2620                        auth,
2621                        runtime,
2622                        protocol: descriptor.runtime.protocol,
2623                        capabilities: descriptor.runtime.capabilities.clone(),
2624                        effective_capabilities: descriptor.runtime.capabilities,
2625                        sessions: HarnessSessionCounts { global, workspace },
2626                        running,
2627                        reason,
2628                        repair,
2629                    };
2630                };
2631                match tokio::time::timeout(
2632                    Duration::from_secs(30),
2633                    backend.start(RuntimeStartRequest {
2634                        cwd,
2635                        launch: Some(isolated.launch.clone()),
2636                        mcp_servers: Vec::new(),
2637                    }),
2638                )
2639                .await
2640                {
2641                    Ok(Ok(mut connection)) => {
2642                        match stabilize_handshake(connection.as_mut()).await {
2643                            Ok(()) => {
2644                                auth = HarnessAuthState::Ready;
2645                                runtime = HarnessRuntimeState::Ready;
2646                                reason = Some(
2647                                    "No-prompt runtime handshake remained healthy through the startup stabilization window; no model request was sent."
2648                                        .into(),
2649                                );
2650                                repair = None;
2651                            }
2652                            Err(message) => {
2653                                auth = if looks_like_auth_error(&message) {
2654                                    HarnessAuthState::Required
2655                                } else if configured {
2656                                    HarnessAuthState::Configured
2657                                } else {
2658                                    HarnessAuthState::Unknown
2659                                };
2660                                reason = Some(format!(
2661                                    "No-prompt runtime handshake became unhealthy during startup: {message}"
2662                                ));
2663                                repair = Some(if auth == HarnessAuthState::Required {
2664                                    format!(
2665                                        "Run `{}` interactively once and complete sign-in, then probe again.",
2666                                        launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2667                                    )
2668                                } else {
2669                                    "Run the harness directly to inspect its startup failure, then probe again."
2670                                        .into()
2671                                });
2672                            }
2673                        }
2674                        let _ =
2675                            tokio::time::timeout(Duration::from_secs(3), connection.close()).await;
2676                    }
2677                    Ok(Err(error)) => {
2678                        let message = truncate_text(&error.to_string(), 500);
2679                        auth = if looks_like_auth_error(&message) {
2680                            HarnessAuthState::Required
2681                        } else if configured {
2682                            HarnessAuthState::Configured
2683                        } else {
2684                            HarnessAuthState::Unknown
2685                        };
2686                        reason = Some(format!("No-prompt runtime handshake failed: {message}"));
2687                        repair = Some(if auth == HarnessAuthState::Required {
2688                            format!(
2689                                "Run `{}` interactively once and complete sign-in, then probe again.",
2690                                launch.map(|launch| launch.program.as_str()).unwrap_or("the harness")
2691                            )
2692                        } else {
2693                            "Check the harness installation and run the handshake probe again."
2694                                .into()
2695                        });
2696                    }
2697                    Err(_) => {
2698                        reason =
2699                            Some("No-prompt runtime handshake timed out after 30 seconds.".into());
2700                        repair = Some("Run the harness directly to check startup or authentication, then probe again.".into());
2701                    }
2702                }
2703                // Keep the isolated home alive through process teardown.
2704                // Otherwise the compiler may release the last meaningful
2705                // use after cloning `launch`, and a still-starting CLI can
2706                // recreate its state directory after Drop removed it.
2707                // Some Node-based launchers finish a short asynchronous
2708                // installation-id write just after their parent process
2709                // is reaped. Remove once immediately, allow that bounded
2710                // writer to settle, then perform the authoritative pass.
2711                let _ = isolated.cleanup();
2712                tokio::time::sleep(Duration::from_millis(250)).await;
2713                if let Err(error) = isolated.cleanup() {
2714                    auth = if configured {
2715                        HarnessAuthState::Configured
2716                    } else {
2717                        HarnessAuthState::Unknown
2718                    };
2719                    runtime = HarnessRuntimeState::Degraded;
2720                    reason = Some(format!(
2721                        "No-prompt runtime handshake could not remove its isolated harness home: {error}"
2722                    ));
2723                    repair = Some(
2724                        "Check temporary-directory permissions, remove the reported disposable probe home, then run the handshake again."
2725                            .into(),
2726                    );
2727                }
2728            }
2729            Err(error) => {
2730                reason = Some(error_message(error));
2731            }
2732        }
2733    } else if installed && configured {
2734        reason = Some("Executable and local authentication evidence found; use a handshake probe to verify readiness.".into());
2735    } else if installed && auth == HarnessAuthState::Required {
2736        reason = Some("Executable found, but no native authentication evidence is present.".into());
2737        repair = Some(format!(
2738            "Run `supercode harness login {}` to use the harness-owned sign-in flow.",
2739            descriptor.id.as_str()
2740        ));
2741    } else if installed {
2742        reason = Some("Executable found; authentication readiness is unknown until a no-prompt handshake succeeds.".into());
2743        repair = Some(format!(
2744            "Run `{}` interactively once if sign-in is required, or use `--probe handshake`.",
2745            launch
2746                .map(|launch| launch.program.as_str())
2747                .unwrap_or("the harness")
2748        ));
2749    }
2750
2751    let effective_capabilities = if installed {
2752        descriptor.runtime.capabilities.clone()
2753    } else {
2754        unavailable_capabilities()
2755    };
2756    let running = probe_running_instance(descriptor.id.as_str());
2757    LocalHarness {
2758        gateway: gateway_health(
2759            descriptor.id.as_str(),
2760            installed,
2761            running.as_ref(),
2762            version.as_deref(),
2763        ),
2764        id: descriptor.id,
2765        display_name: descriptor.display_name,
2766        supported: true,
2767        installed,
2768        executable: executable.map(|path| path.to_string_lossy().into_owned()),
2769        version,
2770        auth,
2771        runtime,
2772        protocol: descriptor.runtime.protocol,
2773        capabilities: descriptor.runtime.capabilities,
2774        effective_capabilities,
2775        sessions: HarnessSessionCounts { global, workspace },
2776        running,
2777        reason,
2778        repair,
2779    }
2780}
2781
2782async fn stabilize_handshake(connection: &mut dyn RuntimeConnection) -> Result<(), String> {
2783    let deadline = tokio::time::Instant::now() + Duration::from_secs(3);
2784    loop {
2785        let now = tokio::time::Instant::now();
2786        if now >= deadline {
2787            return Ok(());
2788        }
2789        match tokio::time::timeout(deadline - now, connection.next_event()).await {
2790            Err(_) => return Ok(()),
2791            Ok(Ok(Some(event))) => {
2792                if let Some(message) = handshake_event_failure(&event) {
2793                    return Err(truncate_text(&message, 500));
2794                }
2795            }
2796            Ok(Ok(None)) => return Err("runtime transport closed during startup".into()),
2797            Ok(Err(error)) => return Err(error.to_string()),
2798        }
2799    }
2800}
2801
2802fn handshake_event_failure(event: &crate::HarnessEvent) -> Option<String> {
2803    let detail = event
2804        .payload
2805        .get("message")
2806        .or_else(|| event.payload.get("line"))
2807        .and_then(Value::as_str)
2808        .unwrap_or(event.kind.as_str());
2809    match event.kind.as_str() {
2810        "transport_closed" => Some("runtime transport closed during startup".into()),
2811        "transport_error" => Some(format!("runtime transport error: {detail}")),
2812        "malformed_output" => Some(format!("runtime emitted non-protocol output: {detail}")),
2813        // Stderr is retained as a runtime event, but is not transport health.
2814        // Grok, for example, can log an AuthorizationRequired error from an
2815        // optional background worker while its ACP session continues to send
2816        // updates and complete prompts normally.
2817        _ => None,
2818    }
2819}
2820
2821fn indexed_claude_window(
2822    locator: &SessionLocator,
2823    options: &SessionLoadOptions,
2824) -> std::result::Result<Option<Value>, ServiceError> {
2825    use supercode_interchange::session::ClaudeReadIndex;
2826    // Exact parent-only window: recursive/full-artifact requests retain the
2827    // existing owner. This is not a bounded display-history substitution.
2828    if locator.harness.as_str() != HarnessId::CLAUDE_CODE
2829        || options.include_subagents != Some(false)
2830    {
2831        return Ok(None);
2832    }
2833    let crate::StorageLocator::File { path } = &locator.storage else {
2834        return Ok(None);
2835    };
2836    if !ClaudeReadIndex::supports(path)
2837        .map_err(|error| ServiceError::Operation(error.to_string()))?
2838    {
2839        return Ok(None);
2840    }
2841    let mut index = ClaudeReadIndex::open(path, Fidelity::ByteLossless)
2842        .map_err(|error| ServiceError::Operation(error.to_string()))?;
2843    let total = index.len();
2844    let (offset, end) = projected_message_window(total, options);
2845    let session = index
2846        .read_messages(offset..end)
2847        .map_err(|error| ServiceError::Operation(error.to_string()))?;
2848    let summary = index
2849        .read_summary()
2850        .map_err(|error| ServiceError::Operation(error.to_string()))?;
2851    let selected_options = SessionLoadOptions {
2852        message_offset: None,
2853        message_limit: None,
2854        message_tail: None,
2855        ..options.clone()
2856    };
2857    let mut selected = projected_session_json(&session, &selected_options);
2858    selected["raw_record_count"] = json!(index.raw_record_count());
2859    Ok(Some(json!({
2860        "session": selected,
2861        "summary": projected_session_summary(&summary, options),
2862        "window": {
2863            "has_more": offset > 0 || end < total, "has_newer": end < total,
2864            "has_older": offset > 0, "newer_items": index.item_count(end..total),
2865            "offset": offset, "older_items": index.item_count(0..offset),
2866            "returned": end - offset, "total_messages": total,
2867        }
2868    })))
2869}
2870
2871fn projected_session_result(session: &Session, options: &SessionLoadOptions) -> Value {
2872    let total_messages = session.messages.len();
2873    let (offset, end) = projected_message_window(total_messages, options);
2874    json!({
2875        "session": projected_session_json(session, options),
2876        "summary": projected_session_summary(session, options),
2877        "window": {
2878            "has_more": offset > 0 || end < total_messages,
2879            "has_newer": end < total_messages,
2880            "has_older": offset > 0,
2881            "newer_items": normalized_item_count(&session.messages[end..]),
2882            "offset": offset,
2883            "older_items": normalized_item_count(&session.messages[..offset]),
2884            "returned": end.saturating_sub(offset),
2885            "total_messages": total_messages,
2886        }
2887    })
2888}
2889
2890fn normalized_item_count(messages: &[crate::ChatMessage]) -> usize {
2891    messages
2892        .iter()
2893        .map(|message| {
2894            let conversation = usize::from(
2895                matches!(message.role, Role::Assistant | Role::User)
2896                    && message_has_content(message),
2897            );
2898            let tool_result =
2899                usize::from(message.role == Role::Tool && message_has_content(message));
2900            conversation + tool_result + message.tool_calls().len()
2901        })
2902        .sum()
2903}
2904
2905fn projected_session_summary(session: &Session, options: &SessionLoadOptions) -> Value {
2906    let mut conversational = session.messages.iter().filter(|message| {
2907        matches!(message.role, Role::Assistant | Role::User) && message_has_content(message)
2908    });
2909    let first_message = conversational.clone().next();
2910    let last_message = conversational.next_back();
2911    let mut assistant = session
2912        .messages
2913        .iter()
2914        .filter(|message| message.role == Role::Assistant && message_has_content(message));
2915    let first_assistant_message = assistant.clone().next();
2916    let last_assistant_message = assistant.next_back();
2917    let end_of_turn = session
2918        .messages
2919        .iter()
2920        .rev()
2921        .find(|message| message.role != Role::System)
2922        .is_some_and(|message| {
2923            message.role == Role::Assistant
2924                && message_has_content(message)
2925                && message.tool_calls().is_empty()
2926                // Codex narrates while it works (`phase: commentary`); only its `final_answer` ends a turn
2927                && message.metadata.get("phase").map(String::as_str) != Some("commentary")
2928        });
2929    let project = |message: Option<&crate::ChatMessage>| {
2930        message.map(|message| project_inline_media(message_json(message), options))
2931    };
2932    json!({
2933        "end_of_turn": end_of_turn,
2934        "first_assistant_message": project(first_assistant_message),
2935        "first_message": project(first_message),
2936        "last_assistant_message": project(last_assistant_message),
2937        "last_assistant_text": last_assistant_message.map(message_text).unwrap_or_default(),
2938        "last_message": project(last_message),
2939    })
2940}
2941
2942fn message_has_content(message: &crate::ChatMessage) -> bool {
2943    message
2944        .content
2945        .as_deref()
2946        .is_some_and(|content| !content.trim().is_empty())
2947        || message
2948            .content_parts
2949            .as_ref()
2950            .is_some_and(|parts| !parts.is_empty())
2951}
2952
2953fn message_text(message: &crate::ChatMessage) -> String {
2954    if let Some(content) = &message.content {
2955        return content.clone();
2956    }
2957    message
2958        .content_parts
2959        .as_ref()
2960        .into_iter()
2961        .flatten()
2962        .filter_map(|part| part.get("text").and_then(Value::as_str))
2963        .collect::<Vec<_>>()
2964        .join("\n")
2965}
2966
2967fn projected_session_json(session: &Session, options: &SessionLoadOptions) -> Value {
2968    let (offset, end) = projected_message_window(session.messages.len(), options);
2969    let messages = session.messages[offset..end]
2970        .iter()
2971        .map(|message| project_inline_media(message_json(message), options))
2972        .collect::<Vec<_>>();
2973    let subagents = if options.include_subagents.unwrap_or(true) {
2974        // The reported window describes the top-level transcript. Applying it
2975        // recursively would silently truncate subagents without returning a
2976        // window for each child. Keep their histories complete while carrying
2977        // the caller's media policy through the tree.
2978        let subagent_options = SessionLoadOptions {
2979            message_limit: None,
2980            message_offset: None,
2981            message_tail: None,
2982            ..options.clone()
2983        };
2984        session
2985            .subagents
2986            .iter()
2987            .map(|subagent| projected_session_json(subagent, &subagent_options))
2988            .collect::<Vec<_>>()
2989    } else {
2990        Vec::new()
2991    };
2992    json!({
2993        "source": match session.meta.source {
2994            SessionSource::ClaudeCode => "claude_code",
2995            SessionSource::Codex => "codex",
2996            SessionSource::Gemini => "gemini",
2997            SessionSource::Goose => "goose",
2998            SessionSource::Grok => "grok",
2999            SessionSource::Native => "native",
3000            SessionSource::OpenClaw => "openclaw",
3001            SessionSource::Hermes => "hermes",
3002            SessionSource::OpenCode => "opencode",
3003            SessionSource::Pi => "pi",
3004        },
3005        "session_id": session.meta.session_id,
3006        "ended_at": session.meta.ended_at,
3007        "end_reason": session.meta.end_reason,
3008        "model": session.meta.model,
3009        "cwd": session.meta.cwd,
3010        "system_prompt": session.meta.system_prompt,
3011        "agent_id": session.meta.agent_id,
3012        "parent_tool_use_id": session.meta.parent_tool_use_id,
3013        "lineage": session.meta.lineage,
3014        "messages": messages,
3015        "subagents": subagents,
3016        "raw_record_count": session.raw.len(),
3017        "parse_error_lines": session.parse_error_lines,
3018    })
3019}
3020
3021fn projected_message_window(total: usize, options: &SessionLoadOptions) -> (usize, usize) {
3022    if let Some(tail) = options.message_tail {
3023        return (total.saturating_sub(tail), total);
3024    }
3025    let offset = options.message_offset.unwrap_or(0).min(total);
3026    let end = options
3027        .message_limit
3028        .map(|limit| offset.saturating_add(limit).min(total))
3029        .unwrap_or(total);
3030    (offset, end)
3031}
3032
3033fn project_inline_media(mut message: Value, options: &SessionLoadOptions) -> Value {
3034    let Some(parts) = message.get_mut("content").and_then(Value::as_array_mut) else {
3035        return message;
3036    };
3037    for part in parts {
3038        let Some(url) = part
3039            .get("image_url")
3040            .and_then(|image| image.get("url"))
3041            .and_then(Value::as_str)
3042        else {
3043            continue;
3044        };
3045        let Some(rest) = url.strip_prefix("data:") else {
3046            continue;
3047        };
3048        let Some((media_type, encoded)) = rest.split_once(";base64,") else {
3049            continue;
3050        };
3051        let padding = usize::from(encoded.ends_with('=')) + usize::from(encoded.ends_with("=="));
3052        let decoded_bytes = encoded.len().saturating_mul(3) / 4;
3053        let decoded_bytes = decoded_bytes.saturating_sub(padding);
3054        let should_elide = matches!(options.inline_media, InlineMediaMode::Metadata)
3055            || options
3056                .max_inline_media_bytes
3057                .is_some_and(|limit| decoded_bytes > limit);
3058        if should_elide {
3059            *part = json!({
3060                "type": "media_reference",
3061                "media_type": media_type,
3062                "encoding": "base64",
3063                "encoded_bytes": encoded.len(),
3064                "decoded_bytes": decoded_bytes,
3065                "omitted": true,
3066            });
3067        }
3068    }
3069    message
3070}
3071
3072#[derive(Deserialize)]
3073struct LocatorParams {
3074    locator: SessionLocator,
3075    /// Optional fidelity for the READ surfaces (`sessions.load`,
3076    /// `sessions.follow`).
3077    ///
3078    /// Omitted means [`Fidelity::Semantic`]: these two methods only ever
3079    /// produce a read-only view, and a compacted or resumed-across-files
3080    /// transcript — the everyday shape of a long Claude Code session — has no
3081    /// losslessly reconstructable record graph, so refusing to render it made
3082    /// the mirror unusable rather than accurate. A caller that intends to
3083    /// CONTINUE from what it reads asks for a lossless level explicitly and
3084    /// gets the strict refusal back. Every other method (export, translate,
3085    /// branch, handoff, resume_instructions) is lossless-only and has no
3086    /// such knob.
3087    #[serde(default)]
3088    fidelity: Option<Fidelity>,
3089    /// Optional bounded frontend projection. Absent preserves the historical
3090    /// complete-session read contract.
3091    #[serde(default)]
3092    view: Option<SessionReadView>,
3093}
3094
3095#[derive(Deserialize)]
3096struct SessionReadView {
3097    /// Number of trailing normalized messages to return. Zero is treated as
3098    /// one so a caller cannot accidentally request an unbounded empty mode.
3099    #[serde(default)]
3100    tail_messages: Option<usize>,
3101    /// Whether Claude Code child transcripts belong in this view. The
3102    /// frontend default is false; the legacy no-view path remains true.
3103    #[serde(default)]
3104    include_subagents: bool,
3105    /// Preserve human-visible native history across model-context compaction.
3106    #[serde(default)]
3107    display_history: bool,
3108    /// Bound each individual text field so a single tool result cannot turn a
3109    /// small message window into a hundred-megabyte RPC response.
3110    #[serde(default)]
3111    max_message_chars: Option<usize>,
3112}
3113
3114impl LocatorParams {
3115    fn read_fidelity(&self) -> Fidelity {
3116        self.fidelity.unwrap_or(Fidelity::Semantic)
3117    }
3118
3119    fn include_subagents(&self) -> bool {
3120        self.view
3121            .as_ref()
3122            .map(|view| view.include_subagents)
3123            .unwrap_or(true)
3124    }
3125
3126    fn tail_messages(&self) -> Option<usize> {
3127        self.view
3128            .as_ref()
3129            .and_then(|view| view.tail_messages)
3130            .map(|limit| limit.clamp(1, 5_000))
3131    }
3132
3133    fn display_history(&self) -> bool {
3134        self.view.as_ref().is_some_and(|view| view.display_history)
3135    }
3136
3137    fn max_message_chars(&self) -> Option<usize> {
3138        self.view
3139            .as_ref()
3140            .and_then(|view| view.max_message_chars)
3141            .map(|limit| limit.clamp(256, 64_000))
3142    }
3143
3144    fn bound_session(&self, session: &mut Session) {
3145        bound_session_view(session, self.tail_messages(), self.max_message_chars());
3146    }
3147}
3148
3149#[derive(Debug, Clone, Copy, Default, Deserialize)]
3150#[serde(rename_all = "snake_case")]
3151enum InlineMediaMode {
3152    #[default]
3153    Full,
3154    Metadata,
3155}
3156
3157#[derive(Debug, Clone, Default, Deserialize)]
3158#[serde(default)]
3159struct SessionLoadOptions {
3160    include_subagents: Option<bool>,
3161    inline_media: InlineMediaMode,
3162    max_inline_media_bytes: Option<usize>,
3163    message_limit: Option<usize>,
3164    message_offset: Option<usize>,
3165    message_tail: Option<usize>,
3166}
3167
3168impl SessionLoadOptions {
3169    fn validate(&self) -> std::result::Result<(), ServiceError> {
3170        if self.message_tail.is_some()
3171            && (self.message_limit.is_some() || self.message_offset.is_some())
3172        {
3173            return Err(ServiceError::InvalidParams(
3174                "sessions.load options.message_tail cannot be combined with message_limit or message_offset"
3175                    .into(),
3176            ));
3177        }
3178        Ok(())
3179    }
3180}
3181
3182#[derive(Deserialize)]
3183struct LoadSessionParams {
3184    #[serde(flatten)]
3185    read: LocatorParams,
3186    #[serde(default)]
3187    options: Option<SessionLoadOptions>,
3188}
3189
3190#[derive(Deserialize)]
3191struct UnfollowParams {
3192    subscription: String,
3193}
3194
3195#[derive(Debug, Deserialize)]
3196#[serde(deny_unknown_fields)]
3197struct IndexResizeParams {
3198    subscription: String,
3199    limit: usize,
3200}
3201
3202#[derive(Deserialize)]
3203struct ActivitySubscribeParams {
3204    locators: Vec<SessionLocator>,
3205    #[serde(default)]
3206    homes: crate::HarnessHomes,
3207}
3208
3209#[derive(Deserialize)]
3210struct MessageSessionParams {
3211    locator: SessionLocator,
3212    text: String,
3213    /// Same storage roots discovery accepts, so a caller (and a test) can
3214    /// point the live-session registry somewhere other than `$HOME`.
3215    #[serde(default)]
3216    homes: crate::HarnessHomes,
3217}
3218
3219#[derive(Deserialize)]
3220#[serde(deny_unknown_fields)]
3221struct HarnessSettingsParams {
3222    harness: String,
3223}
3224
3225#[derive(Deserialize)]
3226#[serde(deny_unknown_fields)]
3227struct ConfigureHarnessParams {
3228    harness: String,
3229    #[serde(default)]
3230    changes: Vec<crate::HarnessSettingChange>,
3231    #[serde(default)]
3232    expected_revision: Option<String>,
3233}
3234
3235fn claude_inbound_controls_or_error(homes: &crate::HarnessHomes) -> (Value, Value) {
3236    match crate::inspect_harness_interop_settings(homes, HarnessId::CLAUDE_CODE) {
3237        Ok(report) => (
3238            serde_json::to_value(report).unwrap_or(Value::Null),
3239            Value::Null,
3240        ),
3241        Err(error) => (
3242            Value::Null,
3243            Value::String(format!(
3244                "Supercode could not inspect Claude Code inbound controls: {error}"
3245            )),
3246        ),
3247    }
3248}
3249
3250/// Deliver `text` into a session that is running right now, or say why not.
3251///
3252/// A refusal is a RESULT, not a JSON-RPC error: "that session is persisted
3253/// only" is an answer about the session, which a mirror renders next to the
3254/// transcript, and this service's error envelope carries no structured data
3255/// field a machine-readable reason could survive in.
3256///
3257/// `delivered_to_bus` is the honest ceiling of what the courier proves. The
3258/// message reached the receiving session's inbox; whether that session ever
3259/// reads it is governed by ITS OWN inbound controls (`crossSessionInbound`,
3260/// approval dialogs), which Supercode neither sees nor overrides.
3261async fn message_live_session(
3262    params: &MessageSessionParams,
3263    runner: &dyn crate::claude_peer::CourierRunner,
3264) -> Value {
3265    if params.locator.harness.as_str() != HarnessId::CLAUDE_CODE {
3266        return json!({
3267            "delivered_to_bus": false,
3268            "refusal": {
3269                "reason": crate::claude_peer::ClaudePeerRefusal::HarnessUnsupported.as_str(),
3270                "message": format!(
3271                    "`{}` does not publish a live-session registry; only claude-code sessions can be messaged in place",
3272                    params.locator.harness.as_str()
3273                ),
3274            },
3275        });
3276    }
3277    let (inbound_controls, inbound_controls_error) =
3278        claude_inbound_controls_or_error(&params.homes);
3279    match crate::claude_peer::message_claude_peer(
3280        &params.homes,
3281        &params.locator.session_id,
3282        &params.text,
3283        runner,
3284    )
3285    .await
3286    {
3287        Ok(delivery) => json!({
3288            "delivered_to_bus": true,
3289            "target": {
3290                "session_id": delivery.target.session_id,
3291                "name": delivery.target.name,
3292                "pid": delivery.target.pid,
3293                "cwd": delivery.target.cwd,
3294                "status": delivery.target.status.map(|status| status.as_str()),
3295            },
3296            "courier": {
3297                "model": crate::claude_peer::COURIER_MODEL,
3298                "report": delivery.courier_report,
3299            },
3300            "inbound_controls": inbound_controls,
3301            "inbound_controls_error": inbound_controls_error,
3302        }),
3303        Err(refusal) => json!({
3304            "delivered_to_bus": false,
3305            "refusal": {"reason": refusal.reason.as_str(), "message": refusal.message},
3306            "inbound_controls": inbound_controls,
3307            "inbound_controls_error": inbound_controls_error,
3308        }),
3309    }
3310}
3311
3312/// Source identity of one follow subscription, plus the last lifecycle state
3313/// already reported on it. The follower itself stays purely persistence-facing.
3314// Only the adapter-api poll reads these; the subscription bookkeeping itself is
3315// shared by both builds.
3316#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
3317struct FollowedSource {
3318    harness: String,
3319    session_id: String,
3320    reported: Option<String>,
3321}
3322
3323#[cfg_attr(not(feature = "adapter-api"), allow(dead_code))]
3324struct ActivitySubscription {
3325    locators: Vec<SessionLocator>,
3326    homes: crate::HarnessHomes,
3327    reported: BTreeMap<(String, String), crate::SessionActivity>,
3328}
3329
3330fn peers_for_descriptors(
3331    descriptors: &[SessionDescriptor],
3332    homes: &HarnessHomes,
3333) -> Vec<crate::claude_peer::ClaudePeerSession> {
3334    if descriptors
3335        .iter()
3336        .any(|session| session.locator.harness.as_str() == HarnessId::CLAUDE_CODE)
3337    {
3338        crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
3339    } else {
3340        Vec::new()
3341    }
3342}
3343
3344/// Add the live address that makes an indexed row behaviorally equivalent to a discovered row.
3345///
3346/// The durable index owns only persistence metadata. Live endpoints remain projections: every
3347/// message/attach operation revalidates its authority, so publishing one here never trusts a stale
3348/// browser-held handle. Reading the Claude registry once per batch keeps this O(peers + rows).
3349fn live_descriptor_value(
3350    session: &SessionDescriptor,
3351    peers: &[crate::claude_peer::ClaudePeerSession],
3352) -> std::result::Result<Value, ServiceError> {
3353    let mut value = serde_json::to_value(session)
3354        .map_err(|error| ServiceError::Operation(error.to_string()))?;
3355    if let Some(workspace) = &session.cwd {
3356        let source = LiveRuntimeSource {
3357            harness: session.locator.harness.as_str().to_string(),
3358            session_id: session.locator.session_id.clone(),
3359            workspace: workspace.clone(),
3360        };
3361        if let Some(endpoint) = discover_live_runtime(&source)
3362            .map_err(|error| ServiceError::Operation(error.to_string()))?
3363        {
3364            value["live_endpoint"] = json!(endpoint.as_str());
3365        }
3366    }
3367    if value.get("live_endpoint").is_none() {
3368        if let Some(peer) = peers.iter().find(|peer| {
3369            session.locator.harness.as_str() == HarnessId::CLAUDE_CODE
3370                && peer.session_id == session.locator.session_id
3371        }) {
3372            value["live_endpoint"] = json!(peer.endpoint().as_str());
3373        }
3374    }
3375    Ok(value)
3376}
3377
3378fn live_index_changes(
3379    changes: Vec<crate::session_index::SessionIndexChange>,
3380    homes: &HarnessHomes,
3381) -> std::result::Result<Vec<Value>, ServiceError> {
3382    use crate::session_index::SessionIndexChange;
3383    let has_claude = changes.iter().any(|change| match change {
3384        SessionIndexChange::Added { descriptor } | SessionIndexChange::Updated { descriptor } => {
3385            descriptor.locator.harness.as_str() == HarnessId::CLAUDE_CODE
3386        }
3387        SessionIndexChange::Removed { .. } => false,
3388    });
3389    let peers = if has_claude {
3390        crate::claude_peer::read_registry(&crate::claude_peer::registry_dir(homes))
3391    } else {
3392        Vec::new()
3393    };
3394    changes
3395        .into_iter()
3396        .map(|change| match change {
3397            SessionIndexChange::Added { descriptor } => Ok(json!({
3398                "kind": "added",
3399                "descriptor": live_descriptor_value(&descriptor, &peers)?,
3400            })),
3401            SessionIndexChange::Updated { descriptor } => Ok(json!({
3402                "kind": "updated",
3403                "descriptor": live_descriptor_value(&descriptor, &peers)?,
3404            })),
3405            SessionIndexChange::Removed { key } => Ok(json!({
3406                "kind": "removed",
3407                "key": key,
3408            })),
3409        })
3410        .collect()
3411}
3412
3413fn legacy_live_status(activity: &crate::SessionActivity) -> Option<&'static str> {
3414    use crate::{SessionPresence, SessionTurnState};
3415    match (activity.presence, activity.turn) {
3416        (SessionPresence::Persisted, _) => None,
3417        (SessionPresence::Running, SessionTurnState::Working) => Some("busy"),
3418        (SessionPresence::Running, SessionTurnState::Idle) => Some("idle"),
3419        // The normalized activity object can honestly report a live owner even
3420        // when the stock harness never published a turn status. Preserve the
3421        // older field's stricter contract instead of guessing `running`.
3422        (SessionPresence::Running, SessionTurnState::Unknown)
3423            if activity.evidence.native_state.is_none() =>
3424        {
3425            None
3426        }
3427        (SessionPresence::Running, _) | (SessionPresence::ShuttingDown, _) => Some("running"),
3428    }
3429}
3430
3431#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
3432#[serde(rename_all = "kebab-case")]
3433enum TransferFormat {
3434    ClaudeCode,
3435    Codex,
3436    #[serde(rename = "opencode", alias = "open-code")]
3437    OpenCode,
3438    Pi,
3439    Grok,
3440    Gemini,
3441    Goose,
3442    /// UNI-18: a Hermes target. Its artifact is the Codex rollout that
3443    /// `hermes sessions import --from codex` reads; `sessions.export` performs
3444    /// that import into the Hermes home.
3445    Hermes,
3446}
3447
3448impl TransferFormat {
3449    fn id(self) -> &'static str {
3450        match self {
3451            Self::ClaudeCode => HarnessId::CLAUDE_CODE,
3452            Self::Codex => HarnessId::CODEX,
3453            Self::OpenCode => HarnessId::OPENCODE,
3454            Self::Pi => HarnessId::PI,
3455            Self::Grok => HarnessId::GROK,
3456            Self::Gemini => HarnessId::GEMINI,
3457            Self::Goose => HarnessId::GOOSE,
3458            Self::Hermes => HarnessId::HERMES,
3459        }
3460    }
3461}
3462
3463impl From<TransferFormat> for SessionFormat {
3464    fn from(value: TransferFormat) -> Self {
3465        match value {
3466            TransferFormat::ClaudeCode => Self::ClaudeCode,
3467            TransferFormat::Codex => Self::Codex,
3468            TransferFormat::OpenCode => Self::OpenCode,
3469            TransferFormat::Pi => Self::Pi,
3470            TransferFormat::Grok => Self::Grok,
3471            TransferFormat::Gemini => Self::Gemini,
3472            TransferFormat::Goose => Self::Goose,
3473            // a Hermes artifact is the Codex rollout Hermes imports
3474            TransferFormat::Hermes => Self::Codex,
3475        }
3476    }
3477}
3478
3479#[derive(Deserialize)]
3480struct ImportSessionParams {
3481    source_harness: TransferFormat,
3482    content: String,
3483}
3484
3485#[derive(Deserialize)]
3486struct ExportSessionParams {
3487    locator: SessionLocator,
3488    target_harness: TransferFormat,
3489}
3490
3491#[derive(Deserialize)]
3492struct ReduceSessionParams {
3493    locator: SessionLocator,
3494    target_harness: TransferFormat,
3495    #[serde(default = "default_keep_last")]
3496    keep_last: usize,
3497}
3498
3499fn default_keep_last() -> usize {
3500    6
3501}
3502
3503#[derive(Deserialize)]
3504struct BranchSessionParams {
3505    locator: SessionLocator,
3506    #[serde(default)]
3507    target_harness: Option<TransferFormat>,
3508}
3509
3510#[derive(Deserialize)]
3511struct HandoffSessionParams {
3512    locator: SessionLocator,
3513    target_harness: TransferFormat,
3514    #[serde(default)]
3515    cwd: Option<PathBuf>,
3516}
3517
3518#[derive(Deserialize)]
3519struct MaterializeSessionParams {
3520    artifact: crate::native_materialize::MaterializeArtifact,
3521    cwd: PathBuf,
3522    /// Where the continuation is written; unset roots are the environment's own, as discovery reads them.
3523    #[serde(default)]
3524    homes: HarnessHomes,
3525}
3526
3527#[derive(Debug, Clone, Copy, Default, Deserialize)]
3528#[serde(rename_all = "snake_case")]
3529enum ResumePolicy {
3530    #[default]
3531    Default,
3532    Yolo,
3533}
3534
3535#[derive(Deserialize)]
3536struct ResumeInstructionsParams {
3537    locator: SessionLocator,
3538    #[serde(default)]
3539    cwd: Option<PathBuf>,
3540    #[serde(default)]
3541    policy: ResumePolicy,
3542}
3543
3544/// `harness.v1.workflow.load` parameters: which harness's board, and its home.
3545#[derive(Deserialize)]
3546struct WorkflowLoadParams {
3547    from: crate::workflow_doors::WorkflowHarness,
3548    home: PathBuf,
3549}
3550
3551/// ONT-4 `harness.v1.orchestration.load` parameters. `flavor` says which layout the
3552/// folder is read as; our own is the default.
3553#[derive(Deserialize)]
3554struct OrchestrationLoadParams {
3555    root: PathBuf,
3556    #[serde(default)]
3557    flavor: crate::orchestration_doors::HomeFlavor,
3558}
3559
3560/// ONT-4 `harness.v1.orchestration.save` parameters. `vault` is merged into the
3561/// home's own secrets; a caller that sends none keeps what is on disk.
3562#[derive(Deserialize)]
3563struct OrchestrationSaveParams {
3564    root: PathBuf,
3565    orchestration: crate::orchestration::Orchestration,
3566    #[serde(default)]
3567    vault: BTreeMap<String, String>,
3568}
3569
3570/// ONT-4 `harness.v1.orchestration.compile` parameters.
3571#[derive(Deserialize)]
3572struct OrchestrationCompileParams {
3573    from: crate::orchestration_doors::OrchestrationHarness,
3574    home: PathBuf,
3575}
3576
3577/// ONT-4 `harness.v1.orchestration.decompile` parameters. `source` is the home the
3578/// orchestration was compiled from: it is re-compiled to recover the io bookkeeping
3579/// that byte reuse and the live-store refusal (UNI-18) are decided from.
3580#[derive(Deserialize)]
3581struct OrchestrationDecompileParams {
3582    to: crate::orchestration_doors::OrchestrationHarness,
3583    orchestration: crate::orchestration::Orchestration,
3584    source: PathBuf,
3585    #[serde(default)]
3586    source_flavor: crate::orchestration_doors::SourceFlavor,
3587    dest: PathBuf,
3588    #[serde(default)]
3589    vault: BTreeMap<String, String>,
3590}
3591
3592/// `harness.v1.orchestration.import` parameters: another harness's home, and the
3593/// folder of ours it becomes.
3594#[derive(Deserialize)]
3595struct OrchestrationImportParams {
3596    from: crate::orchestration_doors::OrchestrationHarness,
3597    home: PathBuf,
3598    into: PathBuf,
3599}
3600
3601/// `harness.v1.orchestration.export` parameters: a folder of ours, and the home of
3602/// another harness it becomes.
3603#[derive(Deserialize)]
3604struct OrchestrationExportParams {
3605    to: crate::orchestration_doors::OrchestrationHarness,
3606    root: PathBuf,
3607    dest: PathBuf,
3608}
3609
3610/// `harness.v1.jobs.get` parameters.
3611#[derive(Deserialize)]
3612struct JobsGetParams {
3613    harness: String,
3614    id: String,
3615    #[serde(default)]
3616    homes: crate::HarnessHomes,
3617}
3618
3619/// ORCH-18: run one mutating job verb through the harness's own CLI.
3620///
3621/// The refusal ladder is deliberate: a harness with no scheduled-job concept
3622/// at all answers with the SAME sentence `jobs.list` gives it, and a harness
3623/// that has jobs but publishes no client-callable verb (Claude Code, whose
3624/// jobs are created by the model inside a session) answers with its own
3625/// reason. Neither is ever a silent no-op.
3626fn mutate_job(
3627    verb: crate::jobs_control::JobVerb,
3628    params: Value,
3629) -> std::result::Result<Value, ServiceError> {
3630    let mutation = decode::<crate::jobs_control::JobMutation>(params)?;
3631    refuse_harness_without_jobs(&mutation.harness, &format!("jobs.{}", verb.as_str()))?;
3632    let outcome = crate::jobs_control::mutate(verb, &mutation).map_err(job_control_error)?;
3633    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3634}
3635
3636/// ORCH-22: run one mutating skills verb through the harness's own door.
3637///
3638/// The refusal ladder mirrors `jobs.*`: a harness with no skills root at all
3639/// answers with the same sentence `skills.list` gives it, and a harness whose
3640/// door does not publish this verb (OpenClaw has no `skills remove` at the
3641/// pin) answers with its own reason. Neither is ever a silent no-op.
3642fn mutate_skill(
3643    verb: crate::skills_control::SkillVerb,
3644    params: Value,
3645) -> std::result::Result<Value, ServiceError> {
3646    let mutation = decode::<crate::skills_control::SkillMutation>(params)?;
3647    if !crate::skills_control::supports_skill_control(&mutation.harness) {
3648        return Err(ServiceError::UnsupportedAction(format!(
3649            "`{}` has no skills root supercode reads; `skills.{}` is supported for: {}",
3650            mutation.harness,
3651            verb.as_str(),
3652            crate::skills_control::CONTROLLED_SKILL_HARNESSES.join(", ")
3653        )));
3654    }
3655    let outcome =
3656        crate::skills_control::mutate_skill(verb, &mutation).map_err(skill_control_error)?;
3657    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3658}
3659
3660/// The skills twin of [`job_control_error`], with the same mapping rule.
3661fn skill_control_error(error: crate::skills_control::SkillControlError) -> ServiceError {
3662    match error {
3663        crate::skills_control::SkillControlError::Unsupported(message) => {
3664            ServiceError::UnsupportedAction(message)
3665        }
3666        crate::skills_control::SkillControlError::Invalid(message) => {
3667            ServiceError::InvalidParams(message)
3668        }
3669        crate::skills_control::SkillControlError::Failed(message) => {
3670            ServiceError::Operation(message)
3671        }
3672    }
3673}
3674
3675/// ORCH-21: run one mutating profile verb through the harness's own CLI.
3676///
3677/// The refusal ladder mirrors `mutate_job`'s: a harness with no profile
3678/// concept at all answers with the SAME sentence `profiles.list` gives it, and
3679/// a harness that HAS profiles but publishes no client-callable verb (Codex's
3680/// file-authored `[profiles.<name>]` tables, supercode's compiled-in presets)
3681/// answers with its own reason. Neither is ever a silent no-op.
3682fn mutate_profile(
3683    verb: crate::profiles_control::ProfileVerb,
3684    params: Value,
3685) -> std::result::Result<Value, ServiceError> {
3686    let mutation = decode::<crate::profiles_control::ProfileMutation>(params)?;
3687    let outcome =
3688        crate::profiles_control::mutate(verb, &mutation).map_err(profile_control_error)?;
3689    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
3690}
3691
3692/// The same mapping `job_control_error` applies, for the profile noun.
3693fn profile_control_error(error: crate::profiles_control::ProfileControlError) -> ServiceError {
3694    match error {
3695        crate::profiles_control::ProfileControlError::Unsupported(message) => {
3696            ServiceError::UnsupportedAction(message)
3697        }
3698        crate::profiles_control::ProfileControlError::Invalid(message) => {
3699            ServiceError::InvalidParams(message)
3700        }
3701        crate::profiles_control::ProfileControlError::Failed(message) => {
3702            ServiceError::Operation(message)
3703        }
3704    }
3705}
3706
3707/// Map a controlled-tier failure onto the service's error vocabulary. A verb
3708/// the harness lacks is `UnsupportedAction`; a harness verb that RAN and
3709/// failed carries its own stderr through as the operation error.
3710fn job_control_error(error: crate::jobs_control::JobControlError) -> ServiceError {
3711    match error {
3712        crate::jobs_control::JobControlError::Unsupported(message) => {
3713            ServiceError::UnsupportedAction(message)
3714        }
3715        crate::jobs_control::JobControlError::Invalid(message) => {
3716            ServiceError::InvalidParams(message)
3717        }
3718        crate::jobs_control::JobControlError::Failed(message) => ServiceError::Operation(message),
3719    }
3720}
3721
3722/// Map an ORCH-19 controlled-tier failure onto the service's error
3723/// vocabulary. A verb the harness has no door for is `UnsupportedAction`; a
3724/// door that RAN and failed carries the harness's own stderr / HTTP body
3725/// through as the operation error.
3726fn session_control_error(error: crate::SessionControlError) -> ServiceError {
3727    match error {
3728        crate::SessionControlError::Unsupported(message) => {
3729            ServiceError::UnsupportedAction(message)
3730        }
3731        crate::SessionControlError::Invalid(message) => ServiceError::InvalidParams(message),
3732        crate::SessionControlError::Failed(message) => ServiceError::Operation(message),
3733    }
3734}
3735
3736/// A harness without a scheduled-job concept refuses the verb rather than
3737/// answering with an empty list — an absent capability and an empty inventory
3738/// are different answers (the same rule `runtimes.capabilities` applies to
3739/// `steer`).
3740fn refuse_harness_without_jobs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3741    if crate::jobs::supports_jobs(harness) {
3742        return Ok(());
3743    }
3744    Err(ServiceError::UnsupportedAction(format!(
3745        "`{harness}` has no scheduled jobs; `{verb}` is supported for: {}",
3746        crate::jobs::JOB_HARNESSES.join(", ")
3747    )))
3748}
3749
3750/// `harness.v1.runs.get` parameters.
3751#[derive(Deserialize)]
3752struct RunsGetParams {
3753    harness: String,
3754    id: String,
3755    #[serde(default)]
3756    homes: crate::HarnessHomes,
3757}
3758
3759/// A harness with no run store refuses the verb rather than answering with an
3760/// empty history — the same rule `jobs.list` applies. Claude Code lands here
3761/// on purpose: its cron fires are ordinary turns inside the session that
3762/// created the job, so there is no fire record to list.
3763fn refuse_harness_without_runs(harness: &str, verb: &str) -> std::result::Result<(), ServiceError> {
3764    if crate::runs::supports_runs(harness) {
3765        return Ok(());
3766    }
3767    Err(ServiceError::UnsupportedAction(format!(
3768        "`{harness}` keeps no run store; `{verb}` is supported for: {}",
3769        crate::runs::RUN_HARNESSES.join(", ")
3770    )))
3771}
3772
3773#[derive(Serialize)]
3774struct SessionArtifact {
3775    source_harness: HarnessId,
3776    target_harness: &'static str,
3777    session_id: Option<String>,
3778    content: String,
3779    suggested_filename: String,
3780    files: Vec<SessionArtifactFile>,
3781    fidelity: Fidelity,
3782    residue: Vec<String>,
3783}
3784
3785#[derive(Serialize)]
3786struct SessionArtifactFile {
3787    path: String,
3788    content: String,
3789    role: ArtifactFileRole,
3790}
3791
3792#[derive(Serialize)]
3793#[serde(rename_all = "snake_case")]
3794enum ArtifactFileRole {
3795    Primary,
3796    Subagent,
3797    Bundle,
3798    SourceRecovery,
3799}
3800
3801#[derive(Serialize)]
3802struct StructuredLaunch {
3803    cwd: PathBuf,
3804    program: String,
3805    arguments: Vec<String>,
3806    env: BTreeMap<String, String>,
3807}
3808
3809struct HandoffInstructions {
3810    launch: StructuredLaunch,
3811    materialize: Option<StructuredLaunch>,
3812    requires_materialization: bool,
3813    note: String,
3814}
3815
3816#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
3817#[serde(rename_all = "snake_case")]
3818enum HarnessProbeLevel {
3819    #[default]
3820    Passive,
3821    Handshake,
3822}
3823
3824#[derive(Default, Deserialize)]
3825#[serde(default)]
3826struct HarnessInventoryParams {
3827    harness: Option<HarnessId>,
3828    harnesses: Vec<HarnessId>,
3829    workspace: Option<PathBuf>,
3830    probe: HarnessProbeLevel,
3831    include_sessions: bool,
3832    /// Omit subprocess-based `--version` calls when a latency-sensitive UI only needs readiness.
3833    skip_versions: bool,
3834}
3835
3836#[derive(Deserialize)]
3837struct HarnessAuthenticationParams {
3838    harness: HarnessId,
3839}
3840
3841#[derive(Deserialize)]
3842struct BeginHarnessAuthenticationParams {
3843    harness: HarnessId,
3844    #[serde(default = "local_browser_authentication_environment")]
3845    environment: crate::HarnessAuthenticationEnvironment,
3846    #[serde(default)]
3847    method: Option<crate::HarnessAuthenticationMethodId>,
3848    #[serde(default)]
3849    cwd: Option<PathBuf>,
3850}
3851
3852fn local_browser_authentication_environment() -> crate::HarnessAuthenticationEnvironment {
3853    crate::HarnessAuthenticationEnvironment::LocalBrowser
3854}
3855
3856#[derive(Serialize)]
3857struct HarnessInventoryReport {
3858    probe: HarnessProbeLevel,
3859    workspace: Option<PathBuf>,
3860    harnesses: Vec<LocalHarness>,
3861}
3862
3863#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3864#[serde(rename_all = "snake_case")]
3865enum HarnessAuthState {
3866    Ready,
3867    Configured,
3868    Required,
3869    Unknown,
3870}
3871
3872#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3873#[serde(rename_all = "snake_case")]
3874enum HarnessRuntimeState {
3875    Ready,
3876    Degraded,
3877    Unavailable,
3878}
3879
3880#[derive(Serialize)]
3881struct HarnessSessionCounts {
3882    global: Option<usize>,
3883    workspace: Option<usize>,
3884}
3885
3886/// Receipt-backed evidence that a harness has a RUNNING instance right now,
3887/// distinct from being merely installed (UNI-7). Detection is passive and
3888/// default-on: a gateway liveness connect for daemon harnesses, a fresh
3889/// SQLite WAL stamp for store-writer harnesses (precedent: the opencode
3890/// follower's -wal/-shm freshness). Control stays behind per-connection
3891/// grants — this reports observations only.
3892/// ORCH-17: the gateway-health noun on an inventory row. Derived from the
3893/// UNI-7 running-instance probe (Hermes: `state.db-wal` freshness; OpenClaw:
3894/// a TCP connect to the gateway endpoint resolved from its OWN config) plus
3895/// the executable version — never by starting anything.
3896#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
3897#[serde(rename_all = "snake_case")]
3898pub enum GatewayState {
3899    Up,
3900    Down,
3901    Unknown,
3902}
3903
3904/// ORCH-17: `gateway` on a `harness.v1.harnesses.list` row.
3905#[derive(Debug, Clone, Serialize)]
3906pub struct GatewayHealth {
3907    pub state: GatewayState,
3908    /// The endpoint supercode would connect to (OpenClaw: the gateway
3909    /// WebSocket resolved from `openclaw.json`; core harnesses: their
3910    /// declared connect address when one exists). `None` when the harness
3911    /// has no single endpoint (Hermes multiplexes platforms).
3912    #[serde(skip_serializing_if = "Option::is_none")]
3913    pub endpoint: Option<String>,
3914    #[serde(skip_serializing_if = "Option::is_none")]
3915    pub version: Option<String>,
3916    /// What the verdict rests on, or why it is `unknown`.
3917    pub evidence: String,
3918    pub checked_at_ms: u64,
3919}
3920
3921/// OpenClaw's gateway WebSocket endpoint, resolved from its own config the
3922/// way the registry's connect descriptor prescribes (`gateway.url`, else
3923/// `gateway.port`, else the documented default).
3924fn openclaw_gateway_endpoint(home: &Path) -> String {
3925    let config_path = home.join(".openclaw/openclaw.json");
3926    let gateway = std::fs::read_to_string(&config_path)
3927        .ok()
3928        .and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok())
3929        .and_then(|config| config.get("gateway").cloned());
3930    if let Some(url) = gateway
3931        .as_ref()
3932        .and_then(|gateway| gateway.get("url"))
3933        .and_then(serde_json::Value::as_str)
3934    {
3935        return url.to_string();
3936    }
3937    let port = gateway
3938        .as_ref()
3939        .and_then(|gateway| gateway.get("port"))
3940        .and_then(serde_json::Value::as_u64)
3941        .unwrap_or(18789);
3942    format!("ws://127.0.0.1:{port}")
3943}
3944
3945/// Ask Hermes itself (`hermes gateway status`, read-only, ~1 s) whether its
3946/// gateway is up. The command is per-host launchd/systemd text without a JSON
3947/// form at 0.19–0.21; the verdict is read from the lines it prints:
3948/// "supervised by launchd (PID …)" / "is running" → up, "not running" /
3949/// "not installed" → down, anything else → no verdict. `SUPERCODE_HERMES_BIN`
3950/// overrides the executable so a fake can stand in under test.
3951fn hermes_gateway_status() -> Option<(GatewayState, String)> {
3952    let program = crate::harness_command::harness_program(HarnessId::HERMES).ok()?;
3953    let output = std::process::Command::new(&program)
3954        .args(["gateway", "status"])
3955        .stdin(std::process::Stdio::null())
3956        .output()
3957        .ok()?;
3958    let text = format!(
3959        "{}{}",
3960        String::from_utf8_lossy(&output.stdout),
3961        String::from_utf8_lossy(&output.stderr)
3962    );
3963    let verdict = text.lines().find_map(|line| {
3964        let l = line.trim();
3965        if l.contains("supervised by launchd (PID")
3966            || l.contains("supervised by systemd (PID")
3967            || l.contains("Gateway is running")
3968            || l.contains("process is running")
3969        {
3970            Some((GatewayState::Up, format!("`hermes gateway status`: {l}")))
3971        } else if l.contains("not running") || l.contains("not installed") {
3972            Some((GatewayState::Down, format!("`hermes gateway status`: {l}")))
3973        } else {
3974            None
3975        }
3976    });
3977    verdict
3978}
3979
3980fn gateway_health(
3981    id: &str,
3982    installed: bool,
3983    running: Option<&RunningInstance>,
3984    version: Option<&str>,
3985) -> GatewayHealth {
3986    let checked_at_ms = now_epoch_ms();
3987    let home = std::env::var_os("HOME").map(PathBuf::from);
3988    match id {
3989        HarnessId::HERMES | HarnessId::OPENCLAW => {
3990            let endpoint = (id == HarnessId::OPENCLAW)
3991                .then(|| home.as_deref().map(openclaw_gateway_endpoint))
3992                .flatten();
3993            let (state, evidence) = match running {
3994                Some(instance) => (GatewayState::Up, instance.evidence.clone()),
3995                None if !installed => (
3996                    GatewayState::Unknown,
3997                    format!("`{id}` is not installed; no gateway to probe"),
3998                ),
3999                None if id == HarnessId::HERMES => match hermes_gateway_status() {
4000                    // The harness's own door outranks the WAL heuristic: an idle
4001                    // gateway writes nothing for minutes yet is up.
4002                    Some((state, evidence)) => (state, evidence),
4003                    None => (
4004                        GatewayState::Down,
4005                        "no fresh state.db-wal activity under ~/.hermes and `hermes gateway status` gave no verdict".to_string(),
4006                    ),
4007                },
4008                None => (
4009                    GatewayState::Down,
4010                    format!(
4011                        "no TCP listener at {}",
4012                        endpoint.as_deref().unwrap_or("the gateway endpoint")
4013                    ),
4014                ),
4015            };
4016            GatewayHealth {
4017                state,
4018                endpoint,
4019                version: version.map(str::to_string),
4020                evidence,
4021                checked_at_ms,
4022            }
4023        }
4024        // ORC-7: the orchestrator's gateway IS its daemon, and the daemon's
4025        // own lease file is the record of it. A lease naming a live pid is
4026        // up; a lease whose process is gone is down and says so as a STALE
4027        // lease, never as "no lease"; no lease at all is down. Nothing is
4028        // started, and no port is guessed — the daemon multiplexes adapters
4029        // the way Hermes does, so it has no single endpoint either.
4030        HarnessId::ORCHESTRATOR => {
4031            let root = crate::HarnessHomes::default().orchestrator;
4032            let (state, evidence) = match crate::orchestrator::read_lease(&root) {
4033                Some(lease) if lease.is_live() => (
4034                    GatewayState::Up,
4035                    format!(
4036                        "`{}` names pid {} (started {}), which is live",
4037                        crate::orchestrator::lock_path(&root).display(),
4038                        lease.pid,
4039                        lease.started_at
4040                    ),
4041                ),
4042                Some(lease) => (
4043                    GatewayState::Down,
4044                    format!(
4045                        "stale lease `{}`: pid {} is gone",
4046                        crate::orchestrator::lock_path(&root).display(),
4047                        lease.pid
4048                    ),
4049                ),
4050                None => (
4051                    GatewayState::Down,
4052                    format!(
4053                        "no lease at `{}`; `supercode orchestrator start` writes one",
4054                        crate::orchestrator::lock_path(&root).display()
4055                    ),
4056                ),
4057            };
4058            GatewayHealth {
4059                state,
4060                endpoint: None,
4061                version: version.map(str::to_string),
4062                evidence,
4063                checked_at_ms,
4064            }
4065        }
4066        _ => GatewayHealth {
4067            state: GatewayState::Unknown,
4068            endpoint: None,
4069            version: version.map(str::to_string),
4070            evidence: format!("`{id}` runs per session, not as a gateway"),
4071            checked_at_ms,
4072        },
4073    }
4074}
4075
4076#[derive(Debug, Clone, Serialize)]
4077struct RunningInstance {
4078    /// How the instance was detected.
4079    method: RunningInstanceMethod,
4080    /// The evidence the verdict rests on (endpoint reached / WAL path+age).
4081    evidence: String,
4082    /// Epoch-ms instant the probe executed.
4083    checked_at_ms: u64,
4084}
4085
4086#[derive(Debug, Clone, Copy, Serialize)]
4087#[serde(rename_all = "snake_case")]
4088enum RunningInstanceMethod {
4089    /// A TCP connect to the harness's own configured gateway endpoint
4090    /// succeeded.
4091    GatewayConnect,
4092    /// The harness's session store has an active SQLite WAL (a live writer
4093    /// holds the store open and stamped it recently).
4094    StoreWalActivity,
4095}
4096
4097fn now_epoch_ms() -> u64 {
4098    std::time::SystemTime::now()
4099        .duration_since(std::time::UNIX_EPOCH)
4100        .map(|elapsed| elapsed.as_millis() as u64)
4101        .unwrap_or(0)
4102}
4103
4104/// OpenClaw: the gateway endpoint comes from the harness's OWN config
4105/// (`<home>/.openclaw/openclaw.json` — `gateway.url` or `gateway.port`,
4106/// default port 18789); a successful TCP connect is the running signal.
4107fn probe_openclaw_running(home: &Path) -> Option<RunningInstance> {
4108    let config_path = home.join(".openclaw/openclaw.json");
4109    let text = std::fs::read_to_string(&config_path).ok();
4110    let gateway = text
4111        .as_deref()
4112        .and_then(|raw| serde_json::from_str::<serde_json::Value>(raw).ok())
4113        .and_then(|config| config.get("gateway").cloned());
4114    let address = gateway
4115        .as_ref()
4116        .and_then(|gateway| gateway.get("url"))
4117        .and_then(serde_json::Value::as_str)
4118        .and_then(|url| {
4119            url.split("://").nth(1).map(|rest| {
4120                rest.trim_end_matches('/')
4121                    .split('/')
4122                    .next()
4123                    .unwrap_or(rest)
4124                    .to_string()
4125            })
4126        })
4127        .unwrap_or_else(|| {
4128            let port = gateway
4129                .as_ref()
4130                .and_then(|gateway| gateway.get("port"))
4131                .and_then(serde_json::Value::as_u64)
4132                .unwrap_or(18789);
4133            format!("127.0.0.1:{port}")
4134        });
4135    let reachable = std::net::TcpStream::connect_timeout(
4136        &address.parse().ok()?,
4137        std::time::Duration::from_millis(400),
4138    )
4139    .is_ok();
4140    reachable.then(|| RunningInstance {
4141        method: RunningInstanceMethod::GatewayConnect,
4142        evidence: format!(
4143            "gateway endpoint {address} accepted a TCP connect (from {})",
4144            config_path.display()
4145        ),
4146        checked_at_ms: now_epoch_ms(),
4147    })
4148}
4149
4150/// Hermes: `<home>/.hermes/state.db-wal` freshly modified means a live writer
4151/// holds the store open (SQLite WAL exists only while a connection is open;
4152/// a recent stamp distinguishes an active instance from a stale crash
4153/// leftover).
4154fn probe_hermes_running(home: &Path, max_wal_age_ms: u64) -> Option<RunningInstance> {
4155    let wal = home.join(".hermes/state.db-wal");
4156    let modified = std::fs::metadata(&wal).ok()?.modified().ok()?;
4157    let age_ms = std::time::SystemTime::now()
4158        .duration_since(modified)
4159        .map(|age| age.as_millis() as u64)
4160        .unwrap_or(u64::MAX);
4161    (age_ms <= max_wal_age_ms).then(|| RunningInstance {
4162        method: RunningInstanceMethod::StoreWalActivity,
4163        evidence: format!(
4164            "{} stamped {age_ms}ms ago (threshold {max_wal_age_ms}ms)",
4165            wal.display()
4166        ),
4167        checked_at_ms: now_epoch_ms(),
4168    })
4169}
4170
4171/// Default-on running-instance detection for the harnesses that have one.
4172fn probe_running_instance(id: &str) -> Option<RunningInstance> {
4173    let home = std::env::var_os("HOME").map(PathBuf::from)?;
4174    match id {
4175        HarnessId::OPENCLAW => probe_openclaw_running(&home),
4176        HarnessId::HERMES => probe_hermes_running(&home, 300_000),
4177        _ => None,
4178    }
4179}
4180
4181#[derive(Serialize)]
4182struct LocalHarness {
4183    id: HarnessId,
4184    display_name: String,
4185    supported: bool,
4186    installed: bool,
4187    executable: Option<String>,
4188    version: Option<String>,
4189    auth: HarnessAuthState,
4190    runtime: HarnessRuntimeState,
4191    protocol: String,
4192    capabilities: crate::RuntimeCapabilities,
4193    effective_capabilities: crate::RuntimeCapabilities,
4194    sessions: HarnessSessionCounts,
4195    /// Receipt-backed running-instance detection (None = not detected or the
4196    /// harness has no running-instance concept). Distinct from `installed`.
4197    #[serde(skip_serializing_if = "Option::is_none")]
4198    running: Option<RunningInstance>,
4199    /// ORCH-17: gateway health derived from `running` + the harness's own config.
4200    gateway: GatewayHealth,
4201    reason: Option<String>,
4202    repair: Option<String>,
4203}
4204
4205#[derive(Clone, Deserialize)]
4206struct RuntimeBackendParams {
4207    harness: HarnessId,
4208    #[serde(default)]
4209    protocol: Option<String>,
4210    #[serde(default)]
4211    launch: Option<RuntimeLaunch>,
4212    #[serde(default)]
4213    base_url: Option<String>,
4214    #[serde(default)]
4215    policy: RuntimePolicy,
4216}
4217
4218#[derive(Debug, Clone, Copy, Default, Deserialize)]
4219#[serde(rename_all = "snake_case")]
4220enum RuntimePolicy {
4221    #[default]
4222    Default,
4223    Yolo,
4224}
4225
4226#[derive(Deserialize)]
4227struct RuntimeStartParams {
4228    #[serde(flatten)]
4229    backend: RuntimeBackendParams,
4230    cwd: PathBuf,
4231    /// MCP servers to mount into the new session through the harness's own
4232    /// start door (ORC-6). Backends without such a door ignore them.
4233    #[serde(default)]
4234    mcp_servers: Vec<crate::McpServerLaunch>,
4235}
4236
4237#[derive(Deserialize)]
4238struct RuntimeAttachParams {
4239    #[serde(flatten)]
4240    backend: RuntimeBackendParams,
4241    runtime_id: String,
4242    #[serde(default)]
4243    cwd: Option<PathBuf>,
4244    /// MCP servers to mount into the resumed session (the start door's own
4245    /// field, carried again because a session's tools die with its process).
4246    #[serde(default)]
4247    mcp_servers: Vec<crate::McpServerLaunch>,
4248}
4249
4250#[derive(Deserialize)]
4251struct RuntimeConnectionParams {
4252    connection: String,
4253}
4254
4255#[derive(Deserialize)]
4256struct RuntimeInputParams {
4257    connection: String,
4258    text: String,
4259    #[serde(default)]
4260    image_urls: Vec<String>,
4261}
4262
4263const MAX_RUNTIME_IMAGES: usize = 4;
4264const MAX_RUNTIME_IMAGE_URL_BYTES: usize = 12 * 1024 * 1024;
4265const MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL: usize = 32 * 1024 * 1024;
4266
4267fn validate_runtime_image_urls(image_urls: Vec<String>) -> Result<Vec<String>, ServiceError> {
4268    if image_urls.len() > MAX_RUNTIME_IMAGES {
4269        return Err(ServiceError::InvalidParams(format!(
4270            "a runtime prompt accepts at most {MAX_RUNTIME_IMAGES} images"
4271        )));
4272    }
4273    let mut total = 0usize;
4274    for url in &image_urls {
4275        if !(url.starts_with("data:image/")
4276            || url.starts_with("https://")
4277            || url.starts_with("http://"))
4278        {
4279            return Err(ServiceError::InvalidParams(
4280                "runtime images must be image data URLs or HTTP(S) URLs".into(),
4281            ));
4282        }
4283        if url.len() > MAX_RUNTIME_IMAGE_URL_BYTES {
4284            return Err(ServiceError::InvalidParams(format!(
4285                "one runtime image exceeds the {MAX_RUNTIME_IMAGE_URL_BYTES}-byte encoded limit"
4286            )));
4287        }
4288        total = total.saturating_add(url.len());
4289    }
4290    if total > MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL {
4291        return Err(ServiceError::InvalidParams(format!(
4292            "runtime images exceed the {MAX_RUNTIME_IMAGE_URL_BYTES_TOTAL}-byte encoded total limit"
4293        )));
4294    }
4295    Ok(image_urls)
4296}
4297
4298#[derive(Deserialize)]
4299struct RuntimeRespondParams {
4300    connection: String,
4301    request_id: Value,
4302    response: Value,
4303}
4304
4305fn default_reduction_store_root() -> PathBuf {
4306    if let Some(root) = std::env::var_os("SUPERCODE_HOME") {
4307        return PathBuf::from(root).join("sessions");
4308    }
4309    if let Some(home) = std::env::var_os("HOME") {
4310        return PathBuf::from(home).join(".supercode").join("sessions");
4311    }
4312    PathBuf::from(".supercode").join("sessions")
4313}
4314
4315fn messages_jsonl(messages: &[crate::ChatMessage]) -> std::result::Result<String, ServiceError> {
4316    let mut output = String::new();
4317    for message in messages {
4318        output.push_str(
4319            &serde_json::to_string(message)
4320                .map_err(|error| ServiceError::Operation(error.to_string()))?,
4321        );
4322        output.push('\n');
4323    }
4324    Ok(output)
4325}
4326
4327fn parse_messages_jsonl(
4328    content: &str,
4329) -> std::result::Result<Vec<crate::ChatMessage>, ServiceError> {
4330    content
4331        .lines()
4332        .enumerate()
4333        .filter(|(_, line)| !line.trim().is_empty())
4334        .map(|(index, line)| {
4335            serde_json::from_str::<crate::ChatMessage>(line).map_err(|error| {
4336                ServiceError::Operation(format!(
4337                    "reduced transcript line {} is invalid: {error}",
4338                    index + 1
4339                ))
4340            })
4341        })
4342        .collect()
4343}
4344
4345fn reduced_bootstrap_prompt(
4346    source: &SessionLocator,
4347    target: TransferFormat,
4348    view_jsonl: &str,
4349    sidecar_path: &Path,
4350    reduction_log_path: &Path,
4351) -> String {
4352    format!(
4353        "Continue the work from this losslessly reduced {source_harness} session in {target_harness}.\n\
4354         \n\
4355         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\
4356         \n\
4357         <supercode-reduced-session source-session=\"{source_id}\">\n\
4358         {view_jsonl}\
4359         </supercode-reduced-session>\n\
4360         \n\
4361         Resume from the latest unresolved user request and preserve the source session's decisions and constraints.",
4362        source_harness = source.harness.as_str(),
4363        target_harness = target.id(),
4364        sidecar = sidecar_path.display(),
4365        log = reduction_log_path.display(),
4366        source_id = source.session_id,
4367    )
4368}
4369
4370fn session_artifact(
4371    locator: &SessionLocator,
4372    session: &Session,
4373    target: TransferFormat,
4374) -> std::result::Result<SessionArtifact, ServiceError> {
4375    session_artifact_with_id(locator, session, target, None)
4376}
4377
4378fn session_artifact_with_id(
4379    locator: &SessionLocator,
4380    session: &Session,
4381    target: TransferFormat,
4382    target_session_id: Option<&str>,
4383) -> std::result::Result<SessionArtifact, ServiceError> {
4384    let format: SessionFormat = target.into();
4385    let diagonal = format.source() == session.meta.source;
4386    crate::residue_store::store_segments(session);
4387    let has_appended_turns = session
4388        .imported_message_count
4389        .is_some_and(|imported| imported < session.messages.len());
4390    let mut restoration = None;
4391    let content = if let Some(id) = target_session_id {
4392        if diagonal && format != SessionFormat::OpenCode {
4393            session
4394                .to_jsonl_spliced(format, Some(id))
4395                .map_err(operation)?
4396        } else {
4397            let mut rewritten = session.clone();
4398            rewritten.meta.session_id = Some(id.to_string());
4399            rewritten.to_jsonl(format).map_err(operation)?
4400        }
4401    } else if diagonal && session.raw_is_verbatim && !has_appended_turns {
4402        session.raw_verbatim()
4403    } else if diagonal {
4404        session.to_jsonl_spliced(format, None).map_err(operation)?
4405    } else {
4406        // A session that came from `format` before returns its source records verbatim for the
4407        // prefix the residue store holds (docs/plans/portable-residue.md).
4408        match session
4409            .restore_residue(format, crate::residue_store::lookup)
4410            .map_err(operation)?
4411        {
4412            Some((content, report)) => {
4413                restoration = Some(report);
4414                content
4415            }
4416            None => session.to_jsonl(format).map_err(operation)?,
4417        }
4418    };
4419    let stem = sanitize_filename(
4420        target_session_id
4421            .or(session.meta.session_id.as_deref())
4422            .unwrap_or(&locator.session_id),
4423    );
4424    let suggested_filename = if diagonal && target == TransferFormat::Grok {
4425        "chat_history.jsonl".to_string()
4426    } else if target == TransferFormat::Goose {
4427        format!("{stem}.goose.json")
4428    } else {
4429        format!("{stem}.{}.jsonl", target.id())
4430    };
4431    let mut files = vec![SessionArtifactFile {
4432        path: suggested_filename.clone(),
4433        content: content.clone(),
4434        role: ArtifactFileRole::Primary,
4435    }];
4436    if target == TransferFormat::ClaudeCode {
4437        let bundle_stem = Path::new(&suggested_filename)
4438            .file_stem()
4439            .and_then(|stem| stem.to_str())
4440            .unwrap_or(&stem);
4441        let mut child_paths = BTreeSet::new();
4442        for (index, subagent) in session.subagents.iter().enumerate() {
4443            let agent_id = subagent
4444                .meta
4445                .agent_id
4446                .as_deref()
4447                .map(|id| id.strip_prefix("agent-").unwrap_or(id))
4448                .map(sanitize_filename)
4449                .filter(|id| !id.is_empty())
4450                .unwrap_or_else(|| format!("subagent-{}", index + 1));
4451            let child_has_appended_turns = subagent
4452                .imported_message_count
4453                .is_some_and(|imported| imported < subagent.messages.len());
4454            let child_content = if target_session_id.is_none()
4455                && subagent.meta.source == SessionSource::ClaudeCode
4456                && subagent.raw_is_verbatim
4457                && !child_has_appended_turns
4458            {
4459                subagent.raw_verbatim()
4460            } else if subagent.meta.source == SessionSource::ClaudeCode {
4461                subagent
4462                    .to_jsonl_spliced(SessionFormat::ClaudeCode, target_session_id)
4463                    .map_err(operation)?
4464            } else {
4465                let mut child = subagent.clone();
4466                if let Some(id) = target_session_id {
4467                    child.meta.session_id = Some(id.to_string());
4468                }
4469                child
4470                    .to_jsonl(SessionFormat::ClaudeCode)
4471                    .map_err(operation)?
4472            };
4473            let path = format!("{bundle_stem}/subagents/agent-{agent_id}.jsonl");
4474            if !child_paths.insert(path.clone()) {
4475                return Err(ServiceError::Operation(format!(
4476                    "Claude subagent ids collide at artifact path `{path}`"
4477                )));
4478            }
4479            files.push(SessionArtifactFile {
4480                path,
4481                content: child_content,
4482                role: ArtifactFileRole::Subagent,
4483            });
4484        }
4485    }
4486    if diagonal && target == TransferFormat::Grok {
4487        append_grok_bundle_files(locator, "", ArtifactFileRole::Bundle, &mut files)?;
4488    }
4489    if !diagonal || !session.raw_is_verbatim {
4490        files.push(SessionArtifactFile {
4491            path: "recovery/source.supercode.jsonl".into(),
4492            content: session.to_native_jsonl(),
4493            role: ArtifactFileRole::SourceRecovery,
4494        });
4495        for (index, subagent) in session.subagents.iter().enumerate() {
4496            let id = subagent
4497                .meta
4498                .agent_id
4499                .as_deref()
4500                .map(sanitize_filename)
4501                .unwrap_or_else(|| format!("subagent-{}", index + 1));
4502            files.push(SessionArtifactFile {
4503                path: format!("recovery/subagents/{id}.supercode.jsonl"),
4504                content: subagent.to_native_jsonl(),
4505                role: ArtifactFileRole::SourceRecovery,
4506            });
4507        }
4508    }
4509    if !diagonal && session.meta.source == SessionSource::Grok {
4510        append_grok_bundle_files(
4511            locator,
4512            "recovery/grok/",
4513            ArtifactFileRole::SourceRecovery,
4514            &mut files,
4515        )?;
4516    }
4517    let (fidelity, residue) = if diagonal
4518        && target_session_id.is_none()
4519        && session.raw_is_verbatim
4520        && !has_appended_turns
4521    {
4522        (Fidelity::ByteLossless, Vec::new())
4523    } else if diagonal && !(target_session_id.is_some() && target == TransferFormat::OpenCode) {
4524        (
4525            Fidelity::ValueLossless,
4526            vec![if target_session_id.is_some() {
4527                "target identity was rewritten, so the artifact intentionally differs from source bytes".into()
4528            } else {
4529                "source storage was reconstructed as a native-value-equivalent export; original container bytes were not captured".into()
4530            }],
4531        )
4532    } else {
4533        match restoration {
4534            Some(report) if report.rendered_messages == 0 => (
4535                Fidelity::ByteLossless,
4536                vec![format!(
4537                    "restored verbatim from this conversation's {} source records in the residue store",
4538                    target.id()
4539                )],
4540            ),
4541            Some(report) => (
4542                Fidelity::Semantic,
4543                vec![format!(
4544                    "{} of {} messages restored verbatim from the residue store; the other {} written by the {} writer",
4545                    report.restored_messages,
4546                    report.restored_messages + report.rendered_messages,
4547                    report.rendered_messages,
4548                    target.id()
4549                )],
4550            ),
4551            None => (
4552                Fidelity::Semantic,
4553                vec!["target schema has no portable slot for every source-native record and metadata field".into()],
4554            ),
4555        }
4556    };
4557    Ok(SessionArtifact {
4558        source_harness: locator.harness.clone(),
4559        target_harness: target.id(),
4560        session_id: target_session_id
4561            .map(str::to_string)
4562            .or_else(|| session.meta.session_id.clone()),
4563        content,
4564        suggested_filename,
4565        files,
4566        fidelity,
4567        residue,
4568    })
4569}
4570
4571fn append_grok_bundle_files(
4572    locator: &SessionLocator,
4573    prefix: &str,
4574    role: ArtifactFileRole,
4575    files: &mut Vec<SessionArtifactFile>,
4576) -> std::result::Result<(), ServiceError> {
4577    let primary = locator.storage.path();
4578    if primary.file_name().and_then(|name| name.to_str()) != Some("chat_history.jsonl") {
4579        return Err(ServiceError::Operation(format!(
4580            "Grok bundle locator must name chat_history.jsonl, got {}",
4581            primary.display()
4582        )));
4583    }
4584    let parent = primary.parent().ok_or_else(|| {
4585        ServiceError::Operation("Grok chat_history.jsonl has no session directory".into())
4586    })?;
4587    for name in ["summary.json", "updates.jsonl"] {
4588        let path = parent.join(name);
4589        let metadata = match std::fs::symlink_metadata(&path) {
4590            Ok(metadata) => metadata,
4591            Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
4592            Err(error) => return Err(ServiceError::Operation(error.to_string())),
4593        };
4594        if metadata.file_type().is_symlink() || !metadata.is_file() {
4595            return Err(ServiceError::Operation(format!(
4596                "refusing non-regular Grok bundle member {}",
4597                path.display()
4598            )));
4599        }
4600        let content = std::fs::read_to_string(&path).map_err(|error| {
4601            ServiceError::Operation(format!(
4602                "Grok bundle member {} is not representable as UTF-8: {error}",
4603                path.display()
4604            ))
4605        })?;
4606        files.push(SessionArtifactFile {
4607            path: format!("{prefix}{name}"),
4608            content,
4609            role: match role {
4610                ArtifactFileRole::Bundle => ArtifactFileRole::Bundle,
4611                _ => ArtifactFileRole::SourceRecovery,
4612            },
4613        });
4614    }
4615    Ok(())
4616}
4617
4618fn handoff_artifact(
4619    locator: &SessionLocator,
4620    session: &Session,
4621    target: TransferFormat,
4622) -> std::result::Result<SessionArtifact, ServiceError> {
4623    let target_session_id = target_session_id(target);
4624    session_artifact_with_id(locator, session, target, Some(&target_session_id))
4625}
4626
4627fn target_session_id(target: TransferFormat) -> String {
4628    let uuid = generated_session_id();
4629    match target {
4630        TransferFormat::OpenCode => format!("ses_{}", uuid.replace('-', "")),
4631        TransferFormat::ClaudeCode
4632        | TransferFormat::Codex
4633        | TransferFormat::Pi
4634        | TransferFormat::Grok
4635        | TransferFormat::Gemini
4636        | TransferFormat::Goose
4637        | TransferFormat::Hermes => uuid,
4638    }
4639}
4640
4641fn sanitize_filename(value: &str) -> String {
4642    let value = value
4643        .chars()
4644        .map(|character| {
4645            if character.is_ascii_alphanumeric() || matches!(character, '-' | '_') {
4646                character
4647            } else {
4648                '-'
4649            }
4650        })
4651        .collect::<String>();
4652    let value = value.trim_matches('-');
4653    if value.is_empty() {
4654        "session".into()
4655    } else {
4656        value.chars().take(100).collect()
4657    }
4658}
4659
4660fn handoff_instructions(
4661    target: TransferFormat,
4662    session_id: &str,
4663    cwd: &Path,
4664) -> HandoffInstructions {
4665    let launch = |program: &str, arguments: Vec<String>| StructuredLaunch {
4666        cwd: cwd.to_path_buf(),
4667        program: program.into(),
4668        arguments,
4669        env: BTreeMap::new(),
4670    };
4671    match target {
4672        TransferFormat::ClaudeCode => HandoffInstructions {
4673            launch: launch("claude", vec!["--resume".into(), session_id.into()]),
4674            materialize: None,
4675            requires_materialization: true,
4676            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(),
4677        },
4678        TransferFormat::Hermes => HandoffInstructions {
4679            launch: launch("hermes", vec!["--resume".into(), session_id.into()]),
4680            materialize: None,
4681            requires_materialization: true,
4682            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(),
4683        },
4684        TransferFormat::Codex => HandoffInstructions {
4685            launch: launch("codex", vec!["resume".into(), session_id.into()]),
4686            materialize: None,
4687            requires_materialization: true,
4688            note: "Write the artifact into Codex's native rollout store before running the resume launch; Codex has no general transcript-import command.".into(),
4689        },
4690        TransferFormat::OpenCode => HandoffInstructions {
4691            launch: launch("opencode", vec!["--session".into(), session_id.into()]),
4692            materialize: Some(launch(
4693                "opencode",
4694                vec!["import".into(), "{artifact_path}".into()],
4695            )),
4696            requires_materialization: true,
4697            note: "Write the artifact to a file, run the materialize command with its path, then launch the imported session.".into(),
4698        },
4699        TransferFormat::Pi => HandoffInstructions {
4700            launch: launch("pi", vec!["--session".into(), "{artifact_path}".into()]),
4701            materialize: None,
4702            requires_materialization: true,
4703            note: "Write the artifact to a file and replace {artifact_path} in the launch arguments; Pi can resume that file directly.".into(),
4704        },
4705        TransferFormat::Grok => HandoffInstructions {
4706            launch: launch(
4707                "grok",
4708                vec!["--resume".into(), "{materialized_session_id}".into()],
4709            ),
4710            materialize: None,
4711            requires_materialization: true,
4712            note: "Grok has no import command. Materialize the artifact through `harness.v1.sessions.materialize` (target `grok`, `value_lossless`, the destination cwd): it writes Grok's store entry (`chat_history.jsonl` and the `summary.json` `--resume` requires) under a fresh id; replace {materialized_session_id} with the id it returns.".into(),
4713        },
4714        TransferFormat::Gemini => HandoffInstructions {
4715            launch: launch(
4716                "gemini",
4717                vec!["--session-file".into(), "{artifact_path}".into()],
4718            ),
4719            materialize: None,
4720            requires_materialization: true,
4721            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(),
4722        },
4723        TransferFormat::Goose => HandoffInstructions {
4724            launch: launch(
4725                "goose",
4726                vec![
4727                    "session".into(),
4728                    "--resume".into(),
4729                    "--session-id".into(),
4730                    "{imported_session_id}".into(),
4731                ],
4732            ),
4733            materialize: Some(launch(
4734                "goose",
4735                vec!["session".into(), "import".into(), "{artifact_path}".into()],
4736            )),
4737            requires_materialization: true,
4738            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(),
4739        },
4740    }
4741}
4742
4743fn resume_launch(
4744    harness: &str,
4745    session_id: &str,
4746    cwd: &Path,
4747    policy: ResumePolicy,
4748) -> std::result::Result<StructuredLaunch, ServiceError> {
4749    let mut arguments = Vec::new();
4750    let program = match harness {
4751        HarnessId::GROK => {
4752            if matches!(policy, ResumePolicy::Yolo) {
4753                if crate::support::self_sandbox_supported() {
4754                    arguments.extend(["--sandbox".into(), "workspace".into()]);
4755                }
4756                arguments.push("--always-approve".into());
4757            }
4758            arguments.extend(["--resume".into(), session_id.into()]);
4759            "grok"
4760        }
4761        HarnessId::CODEX => {
4762            let cwd_key = serde_json::to_string(cwd.to_string_lossy().as_ref())
4763                .expect("a filesystem path always serializes as JSON text");
4764            arguments.extend([
4765                "-c".into(),
4766                "check_for_update_on_startup=false".into(),
4767                "-c".into(),
4768                format!("projects.{cwd_key}.trust_level=\"trusted\""),
4769            ]);
4770            if matches!(policy, ResumePolicy::Yolo) {
4771                arguments.extend([
4772                    "--dangerously-bypass-approvals-and-sandbox".into(),
4773                    "--dangerously-bypass-hook-trust".into(),
4774                ]);
4775            }
4776            arguments.extend(["resume".into(), session_id.into()]);
4777            "codex"
4778        }
4779        HarnessId::CLAUDE_CODE => {
4780            if matches!(policy, ResumePolicy::Yolo) {
4781                arguments.push("--dangerously-skip-permissions".into());
4782            }
4783            arguments.extend(["--resume".into(), session_id.into()]);
4784            "claude"
4785        }
4786        HarnessId::GEMINI => {
4787            if matches!(policy, ResumePolicy::Yolo) {
4788                arguments.push("--yolo".into());
4789            }
4790            arguments.extend(["--resume".into(), session_id.into()]);
4791            "gemini"
4792        }
4793        HarnessId::GOOSE => {
4794            arguments.extend([
4795                "session".into(),
4796                "--resume".into(),
4797                "--session-id".into(),
4798                session_id.into(),
4799            ]);
4800            "goose"
4801        }
4802        HarnessId::PI => {
4803            if matches!(policy, ResumePolicy::Yolo) {
4804                arguments.push("--approve".into());
4805            }
4806            arguments.extend(["--session".into(), session_id.into()]);
4807            "pi"
4808        }
4809        HarnessId::OPENCODE => {
4810            arguments.extend(["--session".into(), session_id.into()]);
4811            "opencode"
4812        }
4813        HarnessId::SUPERCODE => {
4814            if matches!(policy, ResumePolicy::Yolo) {
4815                arguments.push("--dangerous".into());
4816            }
4817            arguments.extend(["resume".into(), session_id.into()]);
4818            "supercode"
4819        }
4820        other => {
4821            return Err(ServiceError::InvalidParams(format!(
4822                "no structured resume launch is registered for harness `{other}`"
4823            )))
4824        }
4825    };
4826    Ok(StructuredLaunch {
4827        cwd: cwd.to_path_buf(),
4828        env: if program == "grok" {
4829            crate::support::grok_home_env()
4830        } else {
4831            BTreeMap::new()
4832        },
4833        program: program.into(),
4834        arguments,
4835    })
4836}
4837
4838/// Stage the resolved gateway credential in a private (0600) file so the
4839/// bridge can read it via `--token-file` — the delivery the real `openclaw
4840/// acp` accepts. One stable file per endpoint (keyed by an address digest,
4841/// no secret material in the name), overwritten on every connect so files
4842/// never accumulate and a rotated token never goes stale on disk.
4843fn openclaw_gateway_token_file(address: &str, secret: &str) -> std::io::Result<PathBuf> {
4844    let digest = blake3::hash(address.as_bytes()).to_hex();
4845    let path = std::env::temp_dir().join(format!(
4846        "supercode-openclaw-gateway-token-{}",
4847        &digest.as_str()[..16]
4848    ));
4849    #[cfg(unix)]
4850    {
4851        use std::io::Write;
4852        use std::os::unix::fs::OpenOptionsExt;
4853        let mut file = std::fs::OpenOptions::new()
4854            .write(true)
4855            .create(true)
4856            .truncate(true)
4857            .mode(0o600)
4858            .open(&path)?;
4859        file.write_all(secret.as_bytes())?;
4860    }
4861    #[cfg(not(unix))]
4862    std::fs::write(&path, secret)?;
4863    Ok(path)
4864}
4865
4866/// Open a connect-mode descriptor: resolve the endpoint address and
4867/// credential from the harness's own config file and build the backend that
4868/// joins the already-running endpoint. Fails closed with a specific
4869/// diagnostic when the config cannot be resolved or the declared protocol has
4870/// no connect-capable client yet.
4871fn open_connect_descriptor(
4872    descriptor: &crate::HarnessSupportDescriptor,
4873    home: &Path,
4874) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
4875    let Some(connect) = &descriptor.runtime.connect_launch else {
4876        return Err(ServiceError::InvalidParams(format!(
4877            "harness `{}` has no registered connect-mode launch",
4878            descriptor.id.as_str()
4879        )));
4880    };
4881    let resolved = connect
4882        .resolve(home)
4883        .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
4884    match (descriptor.id.as_str(), connect.protocol.as_str()) {
4885        (HarnessId::OPENCODE, protocol) if protocol.starts_with("opencode-http") => {
4886            let mut backend = OpenCodeRuntimeBackend::connect(&resolved.address);
4887            if let Some(token) = resolved.auth {
4888                backend = backend.with_bearer(token);
4889            }
4890            Ok(Box::new(backend))
4891        }
4892        (HarnessId::OPENCLAW, protocol) if protocol.starts_with("acp") => {
4893            // OpenClaw's own `openclaw acp` binary is the gateway client: a
4894            // stdio ACP bridge that joins the RUNNING gateway at the resolved
4895            // endpoint. Blind-walk finding 2026-08-31: the real bridge does
4896            // NOT honor OPENCLAW_GATEWAY_TOKEN from the environment — the
4897            // credential must arrive via `--token-file` (never bare `--token`
4898            // on argv, where process listings could read it). The env var is
4899            // still set for older bridges that did read it. Requires openclaw
4900            // >= 2026.7: the 2026.2 bridge drops its gateway socket
4901            // mid-prompt and advertises no session resume (executed finding,
4902            // docs/interop/research/openclaw-acp-dialect-2026-08-30.json).
4903            let mut env = BTreeMap::new();
4904            let mut arguments = vec!["acp".into(), "--url".into(), resolved.address.clone()];
4905            if let Some(token) = resolved.auth {
4906                let token_path = openclaw_gateway_token_file(&resolved.address, token.secret())
4907                    .map_err(|error| {
4908                        ServiceError::UnsupportedAction(format!(
4909                            "could not stage the gateway credential for the bridge: {error}"
4910                        ))
4911                    })?;
4912                arguments.push("--token-file".into());
4913                arguments.push(token_path.to_string_lossy().into_owned());
4914                env.insert("OPENCLAW_GATEWAY_TOKEN".to_string(), token.secret().to_string());
4915            }
4916            // The bridge program comes from the descriptor's own default
4917            // launch (the compiled registry pins `openclaw`), so tests can
4918            // substitute an absolute mock-bridge path without touching
4919            // process-global state.
4920            let program = descriptor
4921                .runtime
4922                .default_launch
4923                .as_ref()
4924                .map(|launch| launch.program.clone())
4925                .unwrap_or_else(|| "openclaw".into());
4926            let launch = RuntimeLaunch {
4927                program,
4928                arguments,
4929                env,
4930            };
4931            Ok(Box::new(
4932                crate::AcpRuntimeBackend::new(descriptor.id.clone(), launch)
4933                    .with_resume_support(descriptor.runtime.capabilities.resume_session),
4934            ))
4935        }
4936        _ => Err(ServiceError::UnsupportedAction(format!(
4937            "connect-mode endpoint for `{}` speaks `{}`; joining it needs that protocol's gateway client",
4938            descriptor.id.as_str(),
4939            connect.protocol
4940        ))),
4941    }
4942}
4943
4944/// The registry's connect-mode launch for this harness, honored only when the
4945/// caller supplied neither an explicit launch nor a base URL.
4946fn registry_connect_descriptor(
4947    params: &RuntimeBackendParams,
4948) -> Option<crate::HarnessSupportDescriptor> {
4949    if params.launch.is_some() || params.base_url.is_some() {
4950        return None;
4951    }
4952    harness_support_registry()
4953        .harnesses
4954        .into_iter()
4955        .find(|descriptor| descriptor.id == params.harness)
4956        .filter(|descriptor| descriptor.runtime.connect_launch.is_some())
4957}
4958
4959fn service_home() -> std::result::Result<PathBuf, ServiceError> {
4960    std::env::var_os("HOME").map(PathBuf::from).ok_or_else(|| {
4961        ServiceError::UnsupportedAction(
4962            "connect-mode launches need HOME to locate the harness config".into(),
4963        )
4964    })
4965}
4966
4967/// The doors that open a runtime: each spawns or joins a program and waits on
4968/// that program's protocol handshake before it can answer.
4969pub const RUNTIME_OPEN_METHODS: &[&str] = &[
4970    "harness.v1.runtimes.start",
4971    "harness.v1.runtimes.resume",
4972    "harness.v1.runtimes.attach",
4973    "harness.v1.runtimes.attach_existing",
4974];
4975
4976/// How long a runtime gets to finish opening before its caller is answered an
4977/// error instead. A program that never speaks the protocol at all — the wrong
4978/// binary, a shim that prints usage and waits — never answers the handshake,
4979/// so the wait is unbounded without this.
4980pub const RUNTIME_OPEN_DEADLINE: Duration = Duration::from_secs(60);
4981
4982/// How long a control call on an ALREADY-open runtime — send input, interrupt,
4983/// steer, respond, close — gets before its caller is answered an error
4984/// instead. A live runtime answers these in milliseconds; a wedged one never
4985/// answers at all, and `close` is exactly what a caller reaches for when it
4986/// suspects that.
4987pub const RUNTIME_CONTROL_DEADLINE: Duration = Duration::from_secs(30);
4988
4989/// The doors whose work happens entirely OUTSIDE this service's state once
4990/// its state has been read: probing harnesses, couriering a message into a
4991/// live session, and performing a conversation verb through a harness's own
4992/// CLI / HTTP / store door. Every one of them waits on a child process or a
4993/// network peer. See [`HarnessSessionService::detach`].
4994pub const DETACHED_METHODS: &[&str] = &[
4995    "harness.v1.harnesses.list",
4996    "harness.v1.harnesses.probe",
4997    "harness.v1.sessions.message",
4998    "harness.v1.sessions.new",
4999    "harness.v1.sessions.reset",
5000    "harness.v1.sessions.archive",
5001    "harness.v1.sessions.delete",
5002];
5003
5004/// How long a request moved off a transport's loop gets before its caller is
5005/// answered an error instead. Each of these already bounds its own inner
5006/// waits (a probe's handshake, the courier's run); this is the backstop for
5007/// the ones that do not — a harness CLI that never exits — so no caller waits
5008/// forever on a detached task no one is watching.
5009pub const DETACHED_CALL_DEADLINE: Duration = Duration::from_secs(120);
5010
5011/// How long `sessions.discover` gets before its caller is answered an error
5012/// instead. Discovery reads each harness's own store, and a store on a cold
5013/// or unavailable mount answers at the filesystem's pace rather than its own.
5014///
5015/// Deliberately shorter than the clients' own request deadline (30s): the
5016/// server's answer names the store that did not answer, and it is only read
5017/// if it lands before the client stops listening.
5018pub const SESSION_DISCOVER_DEADLINE: Duration = Duration::from_secs(25);
5019
5020/// Bound one control call on an open runtime by [`RUNTIME_CONTROL_DEADLINE`],
5021/// naming the method and the bound when it blows.
5022async fn within_control_deadline<F: std::future::Future>(
5023    method: &str,
5024    call: F,
5025) -> std::result::Result<F::Output, ServiceError> {
5026    tokio::time::timeout(RUNTIME_CONTROL_DEADLINE, call)
5027        .await
5028        .map_err(|_| {
5029            ServiceError::Operation(format!(
5030                "`{method}` gave up after {}s: the runtime did not answer",
5031                RUNTIME_CONTROL_DEADLINE.as_secs()
5032            ))
5033        })
5034}
5035
5036/// One [`RUNTIME_OPEN_METHODS`] request, parsed but not yet started. See
5037/// [`HarnessSessionService::runtime_open`] for why it exists apart from
5038/// [`HarnessSessionService::handle_async`].
5039pub struct RuntimeOpen {
5040    id: Value,
5041    method: String,
5042    params: Value,
5043}
5044
5045impl RuntimeOpen {
5046    /// Do the waiting: spawn or join the program and complete its handshake,
5047    /// bounded by [`RUNTIME_OPEN_DEADLINE`]. Touches no service state, so this
5048    /// runs on any task.
5049    pub async fn open(self) -> OpenedRuntime {
5050        let Self { id, method, params } = self;
5051        let outcome = open_runtime(&method, params).await;
5052        OpenedRuntime { id, outcome }
5053    }
5054}
5055
5056/// The result of [`RuntimeOpen::open`], ready for
5057/// [`HarnessSessionService::finish_runtime_open`].
5058pub struct OpenedRuntime {
5059    id: Value,
5060    outcome: std::result::Result<OpenRuntime, ServiceError>,
5061}
5062
5063/// One detached request: the half that reads this service's state already
5064/// done, and the half that waits not yet started. See
5065/// [`HarnessSessionService::detach`] and
5066/// [`HarnessSessionService::detach_runtime`].
5067pub struct DetachedCall {
5068    id: Value,
5069    method: String,
5070    work: std::result::Result<Work, ServiceError>,
5071}
5072
5073impl DetachedCall {
5074    /// Do the waiting and answer. Runs on any task: whatever this call needed
5075    /// from the service was taken before it left.
5076    pub async fn run(self) -> DetachedAnswer {
5077        let Self { id, method, work } = self;
5078        match work {
5079            // A call holding a runtime is already bounded by
5080            // RUNTIME_CONTROL_DEADLINE, and its future OWNS that connection:
5081            // a second timeout around it would drop the connection mid-call
5082            // and take down a runtime its caller still has.
5083            Ok(Work::Runtime(work)) => {
5084                let (result, returned) = work.run().await;
5085                DetachedAnswer {
5086                    response: service_response(id, result),
5087                    returned,
5088                }
5089            }
5090            Ok(Work::Free(work)) => {
5091                let result = match tokio::time::timeout(DETACHED_CALL_DEADLINE, work.run()).await {
5092                    Ok(result) => result,
5093                    Err(_) => Err(ServiceError::Operation(format!(
5094                        "`{method}` gave up after {}s: the harness it waits on did not answer",
5095                        DETACHED_CALL_DEADLINE.as_secs()
5096                    ))),
5097                };
5098                DetachedAnswer {
5099                    response: service_response(id, result),
5100                    returned: None,
5101                }
5102            }
5103            Err(error) => DetachedAnswer {
5104                response: service_response(id, Err(error)),
5105                returned: None,
5106            },
5107        }
5108    }
5109}
5110
5111/// One detached call's complete answer, plus whatever it must hand back to
5112/// the service before that answer is written. See
5113/// [`HarnessSessionService::finish_detached`].
5114pub struct DetachedAnswer {
5115    response: Value,
5116    returned: Option<ReturnedRuntime>,
5117}
5118
5119impl DetachedAnswer {
5120    /// The caller's JSON-RPC response, for a transport that owns no service
5121    /// to give a borrowed connection back to.
5122    pub fn into_response(self) -> Value {
5123        self.response
5124    }
5125}
5126
5127/// A connection lent to a detached call, on its way back to the service that
5128/// owns it.
5129pub struct ReturnedRuntime {
5130    connection: String,
5131    runtime: Box<dyn RuntimeConnection>,
5132}
5133
5134/// The waiting half of one detached request: with nothing of the service's
5135/// in hand, or holding a connection the service lent out for the call.
5136enum Work {
5137    Free(DetachedWork),
5138    Runtime(RuntimeWork),
5139}
5140
5141/// The waiting half of one detached request that holds nothing of the
5142/// service's.
5143enum DetachedWork {
5144    /// Probe the selected harnesses: find their executables, ask each its
5145    /// version, and at `probe: handshake` start each one and complete its
5146    /// protocol handshake.
5147    Inventory(InventoryWork),
5148    /// Run the courier that delivers one message into a live session.
5149    Message(MessageSessionParams),
5150    /// Perform one conversation verb through the harness's own CLI, HTTP API,
5151    /// daemon socket, or supercode's own store.
5152    SessionMutation {
5153        verb: crate::SessionVerb,
5154        mutation: crate::SessionMutation,
5155    },
5156}
5157
5158impl DetachedWork {
5159    async fn run(self) -> std::result::Result<Value, ServiceError> {
5160        match self {
5161            Self::Inventory(work) => run_inventory(work).await,
5162            Self::Message(params) => {
5163                Ok(message_live_session(&params, &crate::claude_peer::ProcessCourierRunner).await)
5164            }
5165            Self::SessionMutation { verb, mutation } => {
5166                let outcome = run_session_mutation(verb, &mutation).await?;
5167                serde_json::to_value(outcome)
5168                    .map_err(|error| ServiceError::Operation(error.to_string()))
5169            }
5170        }
5171    }
5172}
5173
5174/// One detached call that holds a runtime connection for its whole run.
5175enum RuntimeWork {
5176    /// Tear down a runtime the service has already surrendered.
5177    Close {
5178        runtime: Box<dyn RuntimeConnection>,
5179        process_group: Option<u32>,
5180    },
5181    /// Type one live slash command through a borrowed connection, then give
5182    /// the connection back.
5183    LiveCommand {
5184        connection: String,
5185        runtime: Box<dyn RuntimeConnection>,
5186        verb: crate::SessionVerb,
5187        mutation: crate::SessionMutation,
5188        command: &'static str,
5189        session: String,
5190    },
5191}
5192
5193/// What one [`RuntimeWork`] answers with: the caller's result, and the
5194/// connection to give back when the call only borrowed one.
5195type RuntimeWorkAnswer = (
5196    std::result::Result<Value, ServiceError>,
5197    Option<ReturnedRuntime>,
5198);
5199
5200impl RuntimeWork {
5201    async fn run(self) -> RuntimeWorkAnswer {
5202        match self {
5203            Self::Close {
5204                runtime,
5205                process_group,
5206            } => (close_runtime(runtime, process_group).await, None),
5207            Self::LiveCommand {
5208                connection,
5209                mut runtime,
5210                verb,
5211                mutation,
5212                command,
5213                session,
5214            } => {
5215                let result =
5216                    type_live_command(runtime.as_mut(), verb, &mutation, command, session).await;
5217                (
5218                    result,
5219                    Some(ReturnedRuntime {
5220                        connection,
5221                        runtime,
5222                    }),
5223                )
5224            }
5225        }
5226    }
5227}
5228
5229/// Tear down a runtime already out of the service, within
5230/// [`RUNTIME_CONTROL_DEADLINE`].
5231async fn close_runtime(
5232    mut runtime: Box<dyn RuntimeConnection>,
5233    process_group: Option<u32>,
5234) -> std::result::Result<Value, ServiceError> {
5235    match within_control_deadline("harness.v1.runtimes.close", runtime.close()).await {
5236        Ok(result) => {
5237            result.map_err(operation)?;
5238            Ok(json!({"closed": true}))
5239        }
5240        Err(deadline) => {
5241            // Dropping the handle is not enough: the process that stopped
5242            // answering is held by a task parked on it, so nothing here runs
5243            // its Drop. Signal the group the graceful path would have
5244            // signalled, then say so.
5245            let killed = kill_runtime_process_group(process_group);
5246            drop(runtime);
5247            Ok(json!({
5248                "closed": true,
5249                "killed": killed,
5250                "detail": error_message(deadline),
5251            }))
5252        }
5253    }
5254}
5255
5256/// The conversation a live `sessions.new` / `sessions.reset` acts on: the one
5257/// the request named, or the runtime's own session.
5258fn live_session_name(runtime: &dyn RuntimeConnection, mutation: &crate::SessionMutation) -> String {
5259    mutation
5260        .session
5261        .clone()
5262        .filter(|value| !value.trim().is_empty())
5263        .unwrap_or_else(|| runtime.handle().runtime_id.clone())
5264}
5265
5266/// Type one harness slash command into a live session through the very same
5267/// `send_input` path a human's message takes, within
5268/// [`RUNTIME_CONTROL_DEADLINE`].
5269async fn type_live_command(
5270    runtime: &mut dyn RuntimeConnection,
5271    verb: crate::SessionVerb,
5272    mutation: &crate::SessionMutation,
5273    command: &str,
5274    session: String,
5275) -> std::result::Result<Value, ServiceError> {
5276    within_control_deadline(
5277        &format!("sessions.{}", verb.as_str()),
5278        runtime.send_input(RuntimeInput {
5279            text: command.to_string(),
5280            image_urls: Vec::new(),
5281        }),
5282    )
5283    .await?
5284    .map_err(operation)?;
5285    let outcome = crate::sessions_control::live_outcome(verb, mutation, command, session)
5286        .map_err(session_control_error)?;
5287    serde_json::to_value(outcome).map_err(|error| ServiceError::Operation(error.to_string()))
5288}
5289
5290/// A runtime that is up and whose handshake completed, with what the service
5291/// needs to take ownership of it.
5292enum OpenRuntime {
5293    /// supercode spawned this process, so it also hosts it: a frontend server,
5294    /// a live-runtime registration and a terminal launch of its own.
5295    Hosted {
5296        runtime: Box<dyn RuntimeConnection>,
5297        capabilities: crate::RuntimeCapabilities,
5298        workspace: PathBuf,
5299    },
5300    /// `attach_existing` joined a process supercode does not own. It is
5301    /// registered as a bare connection and hosts nothing.
5302    Joined { runtime: Box<dyn RuntimeConnection> },
5303}
5304
5305/// Open the runtime one [`RUNTIME_OPEN_METHODS`] request asks for, within
5306/// [`RUNTIME_OPEN_DEADLINE`]. The error a blown deadline answers names the
5307/// method and the bound, so a caller reads why it was cut loose instead of
5308/// waiting on a handshake that is never coming.
5309async fn open_runtime(
5310    method: &str,
5311    params: Value,
5312) -> std::result::Result<OpenRuntime, ServiceError> {
5313    match tokio::time::timeout(
5314        RUNTIME_OPEN_DEADLINE,
5315        open_runtime_unbounded(method, params),
5316    )
5317    .await
5318    {
5319        Ok(result) => result,
5320        Err(_) => Err(ServiceError::Operation(format!(
5321            "`{method}` gave up after {}s: the runtime never finished its protocol handshake",
5322            RUNTIME_OPEN_DEADLINE.as_secs()
5323        ))),
5324    }
5325}
5326
5327async fn open_runtime_unbounded(
5328    method: &str,
5329    params: Value,
5330) -> std::result::Result<OpenRuntime, ServiceError> {
5331    match method {
5332        "harness.v1.runtimes.start" => {
5333            let params = decode::<RuntimeStartParams>(params)?;
5334            let backend = runtime_backend(&params.backend)?;
5335            let capabilities = backend.capabilities();
5336            let workspace = params.cwd.clone();
5337            let runtime = backend
5338                .start(RuntimeStartRequest {
5339                    cwd: params.cwd,
5340                    launch: runtime_launch(&params.backend),
5341                    mcp_servers: params.mcp_servers,
5342                })
5343                .await
5344                .map_err(operation)?;
5345            Ok(OpenRuntime::Hosted {
5346                runtime,
5347                capabilities,
5348                workspace,
5349            })
5350        }
5351        "harness.v1.runtimes.resume" | "harness.v1.runtimes.attach" => {
5352            let params = decode::<RuntimeAttachParams>(params)?;
5353            let backend = runtime_backend(&params.backend)?;
5354            let capabilities = backend.capabilities();
5355            let workspace = params
5356                .cwd
5357                .clone()
5358                .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
5359            let runtime = backend
5360                .attach(RuntimeAttachRequest {
5361                    runtime_id: params.runtime_id,
5362                    cwd: params.cwd,
5363                    launch: runtime_launch(&params.backend),
5364                    mcp_servers: params.mcp_servers,
5365                })
5366                .await
5367                .map_err(operation)?;
5368            Ok(OpenRuntime::Hosted {
5369                runtime,
5370                capabilities,
5371                workspace,
5372            })
5373        }
5374        "harness.v1.runtimes.attach_existing" => {
5375            let params = decode::<RuntimeAttachParams>(params)?;
5376            let backend: Box<dyn RuntimeBackend> = match params
5377                .backend
5378                .base_url
5379                .as_deref()
5380                .and_then(|value| LiveRuntimeEndpoint::parse(value).ok())
5381            {
5382                Some(endpoint) => {
5383                    #[cfg(not(feature = "adapter-api"))]
5384                    {
5385                        let _ = endpoint;
5386                        return Err(ServiceError::UnsupportedAction(
5387                            "live HTTP attachment adapter is not compiled".into(),
5388                        ));
5389                    }
5390                    #[cfg(feature = "adapter-api")]
5391                    {
5392                        let workspace = params.cwd.clone().ok_or_else(|| {
5393                            ServiceError::InvalidParams(
5394                                "Supercode live attach requires the project cwd".into(),
5395                            )
5396                        })?;
5397                        let source = LiveRuntimeSource {
5398                            harness: params.backend.harness.as_str().to_string(),
5399                            session_id: params.runtime_id.clone(),
5400                            workspace,
5401                        };
5402                        let receipt = resolve_live_runtime(&endpoint, &source)
5403                            .map_err(|error| ServiceError::Operation(error.to_string()))?;
5404                        Box::new(SupercodeHttpRuntimeBackend::new(receipt))
5405                    }
5406                }
5407                None => runtime_backend(&params.backend)?,
5408            };
5409            let capabilities = backend.capabilities();
5410            if !capabilities.attach_existing_process {
5411                return Err(ServiceError::Operation(format!(
5412                    "{} cannot attach to an already-running process; use runtimes.resume for a persisted session",
5413                    backend.harness().as_str()
5414                )));
5415            }
5416            let runtime = backend
5417                .attach_existing(RuntimeAttachRequest {
5418                    runtime_id: params.runtime_id,
5419                    cwd: params.cwd,
5420                    launch: runtime_launch(&params.backend),
5421                    mcp_servers: params.mcp_servers,
5422                })
5423                .await
5424                .map_err(operation)?;
5425            Ok(OpenRuntime::Joined { runtime })
5426        }
5427        _ => Err(ServiceError::MethodNotFound),
5428    }
5429}
5430
5431/// Wrap one service outcome in its JSON-RPC 2.0 envelope.
5432fn service_response(id: Value, result: std::result::Result<Value, ServiceError>) -> Value {
5433    match result {
5434        Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
5435        Err(ServiceError::InvalidParams(message)) => rpc_error(id, -32602, &message),
5436        Err(ServiceError::MethodNotFound) => rpc_error(id, -32601, "method not found"),
5437        Err(ServiceError::UnsupportedAction(message)) => rpc_error(id, -32020, &message),
5438        Err(ServiceError::Operation(message)) => rpc_error(id, -32000, &message),
5439        Err(ServiceError::Sdk(error)) => sdk_rpc_error(id, &error),
5440    }
5441}
5442
5443fn runtime_backend(
5444    params: &RuntimeBackendParams,
5445) -> std::result::Result<Box<dyn RuntimeBackend>, ServiceError> {
5446    if let Some(descriptor) = registry_connect_descriptor(params) {
5447        return open_connect_descriptor(&descriptor, &service_home()?);
5448    }
5449    if params.protocol.as_deref() == Some("acp") {
5450        let launch = params
5451            .launch
5452            .clone()
5453            .or_else(|| {
5454                harness_support_registry()
5455                    .harnesses
5456                    .into_iter()
5457                    .find(|harness| harness.id == params.harness)
5458                    .filter(|harness| {
5459                        harness.runtime.implementation == ImplementationKind::GenericProtocol
5460                            && harness.runtime.protocol.starts_with("acp")
5461                    })
5462                    .and_then(|harness| harness.runtime.default_launch)
5463            })
5464            .ok_or_else(|| {
5465                ServiceError::InvalidParams(
5466                    "an ACP runtime requires `launch` unless the harness has a registered default"
5467                        .into(),
5468                )
5469            })?;
5470        let resume_session = harness_support_registry()
5471            .harnesses
5472            .into_iter()
5473            .find(|harness| harness.id == params.harness)
5474            .is_some_and(|harness| harness.runtime.capabilities.resume_session);
5475        return Ok(Box::new(
5476            AcpRuntimeBackend::new(params.harness.clone(), launch)
5477                .with_resume_support(resume_session),
5478        ));
5479    }
5480    let backend: Box<dyn RuntimeBackend> = match params.harness.as_str() {
5481        HarnessId::CODEX => Box::new(CodexRuntimeBackend::new()),
5482        HarnessId::CLAUDE_CODE => Box::new(ClaudeCodeRuntimeBackend::new()),
5483        HarnessId::PI => Box::new(PiRuntimeBackend::new()),
5484        HarnessId::OPENCODE => match &params.base_url {
5485            Some(url) => Box::new(OpenCodeRuntimeBackend::connect(url)),
5486            None => Box::new(OpenCodeRuntimeBackend::new()),
5487        },
5488        harness => {
5489            let descriptor = harness_support_registry()
5490                .harnesses
5491                .into_iter()
5492                .find(|descriptor| descriptor.id.as_str() == harness)
5493                .filter(|descriptor| {
5494                    descriptor.runtime.implementation == ImplementationKind::GenericProtocol
5495                        && descriptor.runtime.protocol.starts_with("acp")
5496                });
5497            let Some(descriptor) = descriptor else {
5498                return Err(ServiceError::InvalidParams(format!(
5499                    "no runtime adapter for harness `{harness}`; use protocol `acp` with a launch command"
5500                )));
5501            };
5502            let resume = descriptor.runtime.capabilities.resume_session;
5503            Box::new(
5504                AcpRuntimeBackend::new(
5505                    descriptor.id,
5506                    descriptor
5507                        .runtime
5508                        .default_launch
5509                        .expect("generic ACP registry entry includes its launch"),
5510                )
5511                .with_resume_support(resume),
5512            )
5513        }
5514    };
5515    Ok(backend)
5516}
5517
5518fn runtime_launch(params: &RuntimeBackendParams) -> Option<RuntimeLaunch> {
5519    if let Some(launch) = &params.launch {
5520        return Some(launch.clone());
5521    }
5522    if !matches!(params.policy, RuntimePolicy::Yolo) {
5523        return None;
5524    }
5525    let launch = match params.harness.as_str() {
5526        HarnessId::GROK => RuntimeLaunch {
5527            program: "grok".into(),
5528            arguments: {
5529                let mut arguments: Vec<String> = Vec::new();
5530                if crate::support::self_sandbox_supported() {
5531                    arguments.extend(["--sandbox".into(), "workspace".into()]);
5532                }
5533                arguments.extend([
5534                    "--always-approve".into(),
5535                    "agent".into(),
5536                    "--no-leader".into(),
5537                    "stdio".into(),
5538                ]);
5539                arguments
5540            },
5541            env: crate::support::grok_env(),
5542        },
5543        HarnessId::CODEX => RuntimeLaunch {
5544            program: "codex".into(),
5545            arguments: vec![
5546                "--dangerously-bypass-approvals-and-sandbox".into(),
5547                "--dangerously-bypass-hook-trust".into(),
5548                "app-server".into(),
5549            ],
5550            env: BTreeMap::new(),
5551        },
5552        HarnessId::CLAUDE_CODE => RuntimeLaunch {
5553            program: "claude".into(),
5554            arguments: vec![
5555                "--dangerously-skip-permissions".into(),
5556                "--print".into(),
5557                "--input-format".into(),
5558                "stream-json".into(),
5559                "--output-format".into(),
5560                "stream-json".into(),
5561                "--verbose".into(),
5562            ],
5563            env: BTreeMap::new(),
5564        },
5565        HarnessId::PI => RuntimeLaunch {
5566            program: "pi".into(),
5567            arguments: vec!["--approve".into(), "--mode".into(), "rpc".into()],
5568            env: BTreeMap::new(),
5569        },
5570        HarnessId::OPENCODE => RuntimeLaunch {
5571            program: "opencode".into(),
5572            arguments: vec!["serve".into()],
5573            env: BTreeMap::new(),
5574        },
5575        HarnessId::GEMINI => RuntimeLaunch {
5576            program: "gemini".into(),
5577            arguments: vec!["--acp".into(), "--yolo".into()],
5578            env: BTreeMap::new(),
5579        },
5580        HarnessId::GOOSE => RuntimeLaunch {
5581            program: "goose".into(),
5582            arguments: vec!["acp".into()],
5583            env: BTreeMap::new(),
5584        },
5585        HarnessId::SUPERCODE => RuntimeLaunch {
5586            program: "supercode".into(),
5587            arguments: vec!["acp".into(), "--dangerous".into()],
5588            env: BTreeMap::new(),
5589        },
5590        _ => return None,
5591    };
5592    Some(launch)
5593}
5594
5595/// Disposable harness state for a no-prompt readiness probe. Merely opening
5596/// several stock CLIs writes a session header or migrates configuration, so a
5597/// handshake must never point at the user's real home. Authentication files
5598/// are copied into the private temporary home; all writes disappear with the
5599/// guard after the connection closes.
5600struct IsolatedProbeHome {
5601    launch: RuntimeLaunch,
5602    root: PathBuf,
5603}
5604
5605impl IsolatedProbeHome {
5606    fn new(harness: &str, mut launch: RuntimeLaunch) -> std::io::Result<Self> {
5607        let root = std::env::temp_dir().join(format!(
5608            "supercode-harness-probe-{harness}-{}",
5609            generated_session_id()
5610        ));
5611        std::fs::create_dir_all(&root)?;
5612        set_private_dir_permissions(&root)?;
5613
5614        if let Some(source_home) = std::env::var_os("HOME").map(PathBuf::from) {
5615            for relative in probe_auth_files(harness) {
5616                copy_probe_file(&source_home, &root, relative)?;
5617            }
5618        }
5619        // supercode reads its own config home ($SUPERCODE_HOME, else
5620        // $XDG_CONFIG_HOME/supercode, else ~/.config/supercode), not a fixed
5621        // place under HOME: a login kept under XDG_CONFIG_HOME probed as
5622        // "no API key found" while `supercode run` answered.
5623        if harness == HarnessId::SUPERCODE {
5624            let config_home = crate::agent::global_instructions_dir();
5625            for file in ["config.toml", "credentials.toml"] {
5626                copy_probe_path(
5627                    &config_home.join(file),
5628                    &root.join(".config/supercode").join(file),
5629                )?;
5630            }
5631        }
5632        configure_isolated_probe_auth(harness, &root)?;
5633
5634        let root_text = root.to_string_lossy().into_owned();
5635        for (key, value) in [
5636            ("HOME", root_text.clone()),
5637            (
5638                "XDG_CACHE_HOME",
5639                root.join(".cache").to_string_lossy().into_owned(),
5640            ),
5641            (
5642                "XDG_CONFIG_HOME",
5643                root.join(".config").to_string_lossy().into_owned(),
5644            ),
5645            (
5646                "XDG_DATA_HOME",
5647                root.join(".local/share").to_string_lossy().into_owned(),
5648            ),
5649        ] {
5650            launch.env.insert(key.into(), value);
5651        }
5652        let scoped = match harness {
5653            HarnessId::CLAUDE_CODE => Some(("CLAUDE_CONFIG_DIR", root.join(".claude"))),
5654            HarnessId::CODEX => Some(("CODEX_HOME", root.join(".codex"))),
5655            HarnessId::GEMINI => Some(("GEMINI_CLI_HOME", root.clone())),
5656            HarnessId::GROK => Some(("GROK_HOME", root.join(".grok"))),
5657            HarnessId::PI => Some(("PI_CODING_AGENT_DIR", root.join(".pi/agent"))),
5658            HarnessId::SUPERCODE => Some(("SUPERCODE_HOME", root.join(".config/supercode"))),
5659            _ => None,
5660        };
5661        if let Some((key, value)) = scoped {
5662            launch
5663                .env
5664                .insert(key.into(), value.to_string_lossy().into_owned());
5665        }
5666        Ok(Self { launch, root })
5667    }
5668
5669    fn cleanup(&self) -> std::io::Result<()> {
5670        match std::fs::remove_dir_all(&self.root) {
5671            Ok(()) => Ok(()),
5672            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
5673            Err(error) => Err(error),
5674        }
5675    }
5676}
5677
5678impl Drop for IsolatedProbeHome {
5679    fn drop(&mut self) {
5680        let _ = self.cleanup();
5681    }
5682}
5683
5684fn probe_auth_files(harness: &str) -> &'static [&'static str] {
5685    match harness {
5686        HarnessId::CLAUDE_CODE => &[".claude/.credentials.json", ".claude.json"],
5687        // The gateway endpoint + token live in openclaw's own config; without
5688        // it the isolated probe dials the default endpoint unauthenticated
5689        // (PARITY-24 finding 2026-08-31).
5690        HarnessId::OPENCLAW => &[".openclaw/openclaw.json"],
5691        HarnessId::CODEX => &[".codex/auth.json"],
5692        HarnessId::GEMINI => &[
5693            ".gemini/google_accounts.json",
5694            ".gemini/oauth_creds.json",
5695            ".gemini/settings.json",
5696        ],
5697        HarnessId::GROK => &[".grok/auth.json", ".grok/config.toml"],
5698        HarnessId::OPENCODE => &[
5699            ".config/opencode/auth.json",
5700            ".local/share/opencode/auth.json",
5701        ],
5702        HarnessId::PI => &[".pi/agent/auth.json"],
5703        // Hermes keeps its provider selection in config.yaml, its OAuth
5704        // credential pool in auth.json, and API keys in .env; without them
5705        // the isolated probe sees "No LLM provider configured" for a
5706        // hermes that answers fine from the user's real home.
5707        HarnessId::HERMES => &[".hermes/config.yaml", ".hermes/auth.json", ".hermes/.env"],
5708        _ => &[],
5709    }
5710}
5711
5712fn copy_probe_file(source_home: &Path, probe_home: &Path, relative: &str) -> std::io::Result<()> {
5713    copy_probe_path(&source_home.join(relative), &probe_home.join(relative))
5714}
5715
5716fn copy_probe_path(source: &Path, destination: &Path) -> std::io::Result<()> {
5717    if !source.is_file() {
5718        return Ok(());
5719    }
5720    if let Some(parent) = destination.parent() {
5721        std::fs::create_dir_all(parent)?;
5722        set_private_dir_permissions(parent)?;
5723    }
5724    std::fs::copy(source, destination)?;
5725    set_private_file_permissions(destination)
5726}
5727
5728fn configure_isolated_probe_auth(harness: &str, probe_home: &Path) -> std::io::Result<()> {
5729    if harness != HarnessId::GEMINI {
5730        return Ok(());
5731    }
5732    let oauth = probe_home.join(".gemini/oauth_creds.json");
5733    if !oauth.is_file() {
5734        return Ok(());
5735    }
5736    let settings_path = probe_home.join(".gemini/settings.json");
5737    let mut settings = std::fs::read_to_string(&settings_path)
5738        .ok()
5739        .and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
5740        .unwrap_or_else(|| json!({}));
5741    settings["security"]["auth"]["selectedType"] = Value::String("oauth-personal".into());
5742    std::fs::write(
5743        &settings_path,
5744        serde_json::to_vec_pretty(&settings).map_err(std::io::Error::other)?,
5745    )?;
5746    set_private_file_permissions(&settings_path)
5747}
5748
5749#[cfg(unix)]
5750fn set_private_dir_permissions(path: &Path) -> std::io::Result<()> {
5751    use std::os::unix::fs::PermissionsExt;
5752    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
5753}
5754
5755#[cfg(not(unix))]
5756fn set_private_dir_permissions(_path: &Path) -> std::io::Result<()> {
5757    Ok(())
5758}
5759
5760#[cfg(unix)]
5761fn set_private_file_permissions(path: &Path) -> std::io::Result<()> {
5762    use std::os::unix::fs::PermissionsExt;
5763    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
5764}
5765
5766#[cfg(not(unix))]
5767fn set_private_file_permissions(_path: &Path) -> std::io::Result<()> {
5768    Ok(())
5769}
5770
5771fn find_executable(program: &str) -> Option<PathBuf> {
5772    let candidate = PathBuf::from(program);
5773    if candidate.components().count() > 1 {
5774        return candidate.is_file().then_some(candidate);
5775    }
5776    let path = std::env::var_os("PATH")?;
5777    for directory in std::env::split_paths(&path) {
5778        let candidate = directory.join(program);
5779        if candidate.is_file() {
5780            return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5781        }
5782        #[cfg(windows)]
5783        {
5784            for extension in ["exe", "cmd", "bat"] {
5785                let candidate = directory.join(format!("{program}.{extension}"));
5786                if candidate.is_file() {
5787                    return std::fs::canonicalize(&candidate).ok().or(Some(candidate));
5788                }
5789            }
5790        }
5791    }
5792    None
5793}
5794
5795async fn executable_version(executable: &Path) -> Option<String> {
5796    let mut command = tokio::process::Command::new(executable);
5797    command
5798        .arg("--version")
5799        .stdin(std::process::Stdio::null())
5800        .stdout(std::process::Stdio::piped())
5801        .stderr(std::process::Stdio::piped())
5802        .kill_on_drop(true);
5803    let output = tokio::time::timeout(Duration::from_secs(3), command.output())
5804        .await
5805        .ok()?
5806        .ok()?;
5807    let stdout = String::from_utf8_lossy(&output.stdout);
5808    let stderr = String::from_utf8_lossy(&output.stderr);
5809    stdout
5810        .lines()
5811        .chain(stderr.lines())
5812        .map(str::trim)
5813        .find(|line| !line.is_empty())
5814        .map(|line| truncate_text(line, 200))
5815}
5816
5817pub(crate) fn auth_evidence(harness: &str) -> bool {
5818    let env_names: &[&str] = match harness {
5819        HarnessId::CLAUDE_CODE => &["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
5820        HarnessId::CODEX => &["OPENAI_API_KEY"],
5821        HarnessId::OPENCODE => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5822        HarnessId::PI => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
5823        HarnessId::GROK => &["XAI_API_KEY", "GROK_API_KEY"],
5824        HarnessId::GEMINI => &["GEMINI_API_KEY", "GOOGLE_API_KEY"],
5825        HarnessId::SUPERCODE => &["OPENROUTER_API_KEY"],
5826        _ => &[],
5827    };
5828    if env_names
5829        .iter()
5830        .any(|name| std::env::var_os(name).is_some_and(|value| !value.is_empty()))
5831    {
5832        return true;
5833    }
5834    let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
5835        return false;
5836    };
5837    let files: Vec<PathBuf> = match harness {
5838        HarnessId::CLAUDE_CODE => vec![home.join(".claude/.credentials.json")],
5839        HarnessId::CODEX => vec![home.join(".codex/auth.json")],
5840        HarnessId::OPENCODE => vec![
5841            home.join(".local/share/opencode/auth.json"),
5842            home.join(".config/opencode/auth.json"),
5843        ],
5844        HarnessId::PI => vec![home.join(".pi/agent/auth.json")],
5845        HarnessId::GROK => vec![home.join(".grok/auth.json")],
5846        HarnessId::GEMINI => vec![
5847            home.join(".gemini/oauth_creds.json"),
5848            home.join(".gemini/google_accounts.json"),
5849        ],
5850        HarnessId::SUPERCODE => vec![home.join(".config/supercode/credentials.toml")],
5851        HarnessId::HERMES => vec![home.join(".hermes/auth.json"), home.join(".hermes/.env")],
5852        _ => Vec::new(),
5853    };
5854    if files.into_iter().any(|path| {
5855        std::fs::metadata(path)
5856            .map(|metadata| metadata.is_file() && metadata.len() > 2)
5857            .unwrap_or(false)
5858    }) {
5859        return true;
5860    }
5861    // macOS keeps Claude Code's OAuth login in the Keychain, so
5862    // `.claude/.credentials.json` never exists there and the file probe above
5863    // reports a signed-in install as unauthenticated forever. A completed
5864    // login also writes an `oauthAccount` record into `~/.claude.json` on
5865    // every platform — file-based, prompt-free evidence (querying the
5866    // Keychain itself from an unsigned daemon can raise a UI prompt).
5867    if harness == HarnessId::CLAUDE_CODE {
5868        return std::fs::read_to_string(home.join(".claude.json"))
5869            .map(|text| text.contains("\"oauthAccount\""))
5870            .unwrap_or(false);
5871    }
5872    false
5873}
5874
5875fn looks_like_auth_error(message: &str) -> bool {
5876    let message = message.to_ascii_lowercase();
5877    [
5878        "auth",
5879        "login",
5880        "sign in",
5881        "sign-in",
5882        "credential",
5883        "unauthorized",
5884        "forbidden",
5885        "token",
5886    ]
5887    .iter()
5888    .any(|needle| message.contains(needle))
5889}
5890
5891fn unavailable_capabilities() -> crate::RuntimeCapabilities {
5892    crate::RuntimeCapabilities {
5893        start_session: false,
5894        resume_session: false,
5895        attach_existing_process: false,
5896        send_input: false,
5897        stream_events: false,
5898        interrupt: false,
5899        steer: false,
5900        respond_to_requests: false,
5901    }
5902}
5903
5904fn truncate_text(text: &str, max_chars: usize) -> String {
5905    let mut chars = text.chars();
5906    let truncated = chars.by_ref().take(max_chars).collect::<String>();
5907    if chars.next().is_some() {
5908        format!("{truncated}…")
5909    } else {
5910        truncated
5911    }
5912}
5913
5914/// The process group a runtime's own handle names, when it names one.
5915///
5916/// Every adapter that spawns a local process spawns it as its own group
5917/// leader (`Command::process_group(0)`), so the endpoint's pid IS the group
5918/// id. A runtime reached over HTTP, or one supercode joined rather than
5919/// spawned, names no group here and is left alone.
5920fn runtime_process_group(handle: &crate::RuntimeHandle) -> Option<u32> {
5921    match &handle.endpoint {
5922        crate::RuntimeEndpoint::LocalProcess { pid, .. } => *pid,
5923        crate::RuntimeEndpoint::Http { .. } => None,
5924    }
5925}
5926
5927/// SIGKILL a wedged runtime's whole process group, reporting whether there
5928/// was one to signal. This is the same group teardown a graceful `close`
5929/// performs; it runs here only when the graceful path blew its deadline,
5930/// because the task parked on the unanswered call still owns the process
5931/// handle and so no `Drop` of ours can reach it.
5932fn kill_runtime_process_group(process_group: Option<u32>) -> bool {
5933    match process_group {
5934        #[cfg(unix)]
5935        Some(pid) => {
5936            crate::lsp::kill_process_group(pid);
5937            true
5938        }
5939        #[cfg(not(unix))]
5940        Some(_) => false,
5941        None => false,
5942    }
5943}
5944
5945fn error_message(error: ServiceError) -> String {
5946    match error {
5947        ServiceError::InvalidParams(message)
5948        | ServiceError::Operation(message)
5949        | ServiceError::UnsupportedAction(message) => message,
5950        ServiceError::MethodNotFound => "runtime adapter is not available".into(),
5951        ServiceError::Sdk(error) => error.to_string(),
5952    }
5953}
5954
5955#[derive(Debug)]
5956enum ServiceError {
5957    InvalidParams(String),
5958    MethodNotFound,
5959    UnsupportedAction(String),
5960    Operation(String),
5961    Sdk(SdkError),
5962}
5963
5964fn sdk_error(operation: SdkOperation, error: ServiceError) -> SdkError {
5965    match error {
5966        ServiceError::InvalidParams(message) => {
5967            SdkError::new(SdkErrorCode::InvalidArgument, operation, message)
5968        }
5969        ServiceError::MethodNotFound | ServiceError::UnsupportedAction(_) => {
5970            SdkError::unsupported(operation)
5971        }
5972        ServiceError::Operation(message) => {
5973            let code = if message.contains("already in progress") {
5974                SdkErrorCode::Busy
5975            } else if message.contains("not supported by this runtime") {
5976                SdkErrorCode::UnsupportedAction
5977            } else if message.contains("unknown runtime connection") {
5978                SdkErrorCode::NotFound
5979            } else {
5980                SdkErrorCode::Execution
5981            };
5982            SdkError::new(code, operation, message)
5983        }
5984        ServiceError::Sdk(error) => error,
5985    }
5986}
5987
5988fn sdk_rpc_error(id: Value, error: &SdkError) -> Value {
5989    let error_code = error.code();
5990    let code = match error_code {
5991        SdkErrorCode::Unauthenticated => -32030,
5992        SdkErrorCode::Unauthorized => -32031,
5993        SdkErrorCode::ControllerRequired => -32032,
5994        SdkErrorCode::LeaseExpired => -32033,
5995        SdkErrorCode::InvalidArgument => -32602,
5996        SdkErrorCode::NotFound => -32004,
5997        SdkErrorCode::Busy => -32000,
5998        SdkErrorCode::UnsupportedAction => -32020,
5999        SdkErrorCode::Execution => -32002,
6000        SdkErrorCode::Transport => -32003,
6001    };
6002    json!({
6003        "jsonrpc": "2.0",
6004        "id": id,
6005        "error": {
6006            "code": code,
6007            "name": error_code,
6008            "operation": error.operation(),
6009            "message": error.to_string(),
6010        },
6011    })
6012}
6013
6014fn decode<T: for<'de> Deserialize<'de>>(value: Value) -> std::result::Result<T, ServiceError> {
6015    serde_json::from_value(value).map_err(|error| ServiceError::InvalidParams(error.to_string()))
6016}
6017
6018fn operation(error: impl Into<crate::Error>) -> ServiceError {
6019    let error = error.into();
6020    match error {
6021        crate::Error::Sdk(error) => ServiceError::Sdk(error),
6022        error => ServiceError::Operation(error.to_string()),
6023    }
6024}
6025
6026/// ORCH-12 `harness.v1.memory.show|search` params. `homes` is the same
6027/// storage-root override every read-only method accepts, so a caller can
6028/// point the read at a fixture home without touching the real ones.
6029#[derive(Debug, Clone, Deserialize, Default)]
6030#[serde(default)]
6031struct MemoryRequest {
6032    /// Harness whose store is read. Required.
6033    harness: Option<String>,
6034    /// The needle, required by `search`.
6035    query: Option<String>,
6036    /// Hermes profile, OpenClaw agent, or Claude Code project.
6037    profile: Option<String>,
6038    /// Claude Code session id selecting a project store (`show` only).
6039    session: Option<String>,
6040    /// Include each document's whole text (`show` only).
6041    full: bool,
6042    /// Treat `query` as a regular expression (`search` only).
6043    regex: bool,
6044    /// Working tree whose project store is read.
6045    cwd: Option<std::path::PathBuf>,
6046    /// Storage roots to read.
6047    homes: crate::HarnessHomes,
6048}
6049
6050/// Read the memory noun. A harness with no memory store fails with
6051/// `UnsupportedAction` (RPC `-32020`), never an empty list.
6052fn memory_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6053    let request = decode::<MemoryRequest>(params)?;
6054    let harness = request
6055        .harness
6056        .clone()
6057        .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6058    let to_service = |error: crate::memory::MemoryError| match error {
6059        crate::memory::MemoryError::UnsupportedHarness { .. }
6060        | crate::memory::MemoryError::SessionNotScoped { .. } => {
6061            ServiceError::UnsupportedAction(error.to_string())
6062        }
6063        other => ServiceError::InvalidParams(other.to_string()),
6064    };
6065    match method {
6066        "harness.v1.memory.show" => {
6067            let documents = crate::memory::show_memory(&crate::memory::MemoryQuery {
6068                harness,
6069                profile: request.profile,
6070                session: request.session,
6071                full: request.full,
6072                cwd: request.cwd,
6073                homes: request.homes,
6074            })
6075            .map_err(to_service)?;
6076            Ok(json!({
6077                "schema": crate::memory::MEMORY_SCHEMA,
6078                "documents": documents,
6079            }))
6080        }
6081        "harness.v1.memory.search" => {
6082            let query = request
6083                .query
6084                .ok_or_else(|| ServiceError::InvalidParams("`query` is required".into()))?;
6085            let matches = crate::memory::search_memory(&crate::memory::MemorySearchQuery {
6086                harness,
6087                query,
6088                profile: request.profile,
6089                regex: request.regex,
6090                cwd: request.cwd,
6091                homes: request.homes,
6092            })
6093            .map_err(to_service)?;
6094            Ok(json!({
6095                "schema": crate::memory::MEMORY_SCHEMA,
6096                "matches": matches,
6097            }))
6098        }
6099        _ => Err(ServiceError::MethodNotFound),
6100    }
6101}
6102
6103/// ORCH-10 `harness.v1.profiles.list|get` params. `homes` is the same
6104/// storage-root override every read-only method accepts, so a caller can
6105/// point the read at a fixture home without touching the real ones.
6106#[derive(Debug, Clone, Deserialize)]
6107#[serde(default)]
6108struct ProfilesQuery {
6109    /// Restrict the listing to one harness. `get` requires it.
6110    harness: Option<String>,
6111    /// Profile name, required by `get`.
6112    name: Option<String>,
6113    /// Storage roots to read.
6114    homes: crate::HarnessHomes,
6115}
6116
6117impl Default for ProfilesQuery {
6118    fn default() -> Self {
6119        Self {
6120            harness: None,
6121            name: None,
6122            homes: crate::HarnessHomes::default(),
6123        }
6124    }
6125}
6126
6127/// Read the profile noun. A harness with no profile concept fails with
6128/// `UnsupportedAction` (RPC `-32020`), never an empty list.
6129fn profiles_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6130    let query = decode::<ProfilesQuery>(params)?;
6131    let to_service = |error: crate::profiles::ProfileError| match error {
6132        crate::profiles::ProfileError::UnsupportedHarness { .. } => {
6133            ServiceError::UnsupportedAction(error.to_string())
6134        }
6135        crate::profiles::ProfileError::NotFound { .. } => {
6136            ServiceError::InvalidParams(error.to_string())
6137        }
6138    };
6139    match method {
6140        "harness.v1.profiles.list" => {
6141            let profiles = crate::profiles::list_profiles(&query.homes, query.harness.as_deref())
6142                .map_err(to_service)?;
6143            Ok(json!({
6144                "schema": crate::profiles::PROFILES_SCHEMA,
6145                "profiles": profiles,
6146            }))
6147        }
6148        "harness.v1.profiles.get" => {
6149            let harness = query
6150                .harness
6151                .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6152            let name = query
6153                .name
6154                .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6155            let profile =
6156                crate::profiles::get_profile(&query.homes, &harness, &name).map_err(to_service)?;
6157            Ok(json!({
6158                "schema": crate::profiles::PROFILES_SCHEMA,
6159                "profile": profile,
6160            }))
6161        }
6162        _ => Err(ServiceError::MethodNotFound),
6163    }
6164}
6165
6166/// ORCH-14 `harness.v1.channels.list|status` params, the same storage-root
6167/// override every read-only method accepts so a caller can point the read at
6168/// a fixture home without touching the real ones.
6169#[derive(Debug, Clone, Deserialize)]
6170#[serde(default)]
6171struct ChannelsQuery {
6172    /// Restrict the listing to one harness. `status` requires it.
6173    harness: Option<String>,
6174    /// Channel name, required by `status`.
6175    name: Option<String>,
6176    /// Storage roots to read.
6177    homes: crate::HarnessHomes,
6178}
6179
6180impl Default for ChannelsQuery {
6181    fn default() -> Self {
6182        Self {
6183            harness: None,
6184            name: None,
6185            homes: crate::HarnessHomes::default(),
6186        }
6187    }
6188}
6189
6190/// Read the channel noun. A harness with no channel concept fails with
6191/// `UnsupportedAction` (RPC `-32020`), never an empty list. No row carries a
6192/// token, key or secret — see `crate::channels` "Secrecy".
6193#[derive(Debug, Clone, Deserialize)]
6194#[serde(default)]
6195struct RoutesQuery {
6196    harness: Option<String>,
6197    /// Restrict to routes targeting one profile / agent.
6198    profile: Option<String>,
6199    homes: crate::HarnessHomes,
6200}
6201
6202impl Default for RoutesQuery {
6203    fn default() -> Self {
6204        Self {
6205            harness: None,
6206            profile: None,
6207            homes: crate::HarnessHomes::default(),
6208        }
6209    }
6210}
6211
6212#[derive(Debug, Clone, Deserialize)]
6213#[serde(default)]
6214struct TriggersQuery {
6215    harness: Option<String>,
6216    homes: crate::HarnessHomes,
6217}
6218
6219impl Default for TriggersQuery {
6220    fn default() -> Self {
6221        Self {
6222            harness: None,
6223            homes: crate::HarnessHomes::default(),
6224        }
6225    }
6226}
6227
6228fn triggers_call(params: Value) -> std::result::Result<Value, ServiceError> {
6229    let query = decode::<TriggersQuery>(params)?;
6230    let triggers = crate::triggers::list_triggers(&query.homes, query.harness.as_deref())
6231        .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6232    Ok(json!({
6233        "schema": crate::triggers::TRIGGERS_SCHEMA,
6234        "triggers": triggers,
6235    }))
6236}
6237
6238fn routes_call(params: Value) -> std::result::Result<Value, ServiceError> {
6239    let query = decode::<RoutesQuery>(params)?;
6240    let routes = crate::routes::list_routes(
6241        &query.homes,
6242        query.harness.as_deref(),
6243        query.profile.as_deref(),
6244    )
6245    .map_err(|error| ServiceError::UnsupportedAction(error.to_string()))?;
6246    Ok(json!({
6247        "schema": crate::routes::ROUTES_SCHEMA,
6248        "routes": routes,
6249    }))
6250}
6251
6252fn channels_call(method: &str, params: Value) -> std::result::Result<Value, ServiceError> {
6253    let query = decode::<ChannelsQuery>(params)?;
6254    let to_service = |error: crate::channels::ChannelError| match error {
6255        crate::channels::ChannelError::UnsupportedHarness { .. } => {
6256            ServiceError::UnsupportedAction(error.to_string())
6257        }
6258        crate::channels::ChannelError::NotFound { .. } => {
6259            ServiceError::InvalidParams(error.to_string())
6260        }
6261    };
6262    match method {
6263        "harness.v1.channels.list" => {
6264            let channels = crate::channels::list_channels(&query.homes, query.harness.as_deref())
6265                .map_err(to_service)?;
6266            Ok(json!({
6267                "schema": crate::channels::CHANNELS_SCHEMA,
6268                "channels": channels,
6269            }))
6270        }
6271        "harness.v1.channels.status" => {
6272            let harness = query
6273                .harness
6274                .ok_or_else(|| ServiceError::InvalidParams("`harness` is required".into()))?;
6275            let name = query
6276                .name
6277                .ok_or_else(|| ServiceError::InvalidParams("`name` is required".into()))?;
6278            let channel = crate::channels::channel_status(&query.homes, &harness, &name)
6279                .map_err(to_service)?;
6280            Ok(json!({
6281                "schema": crate::channels::CHANNELS_SCHEMA,
6282                "channel": channel,
6283            }))
6284        }
6285        _ => Err(ServiceError::MethodNotFound),
6286    }
6287}
6288
6289fn rpc_error(id: Value, code: i64, message: &str) -> Value {
6290    json!({
6291        "jsonrpc": "2.0",
6292        "id": id,
6293        "error": {"code": code, "message": message},
6294    })
6295}
6296
6297#[cfg(test)]
6298mod tests {
6299    use super::*;
6300    use crate::{HarnessEvent, HarnessId, RuntimeEndpoint, RuntimeHandle, StorageLocator};
6301    use async_trait::async_trait;
6302    use std::io::Write;
6303    use std::path::PathBuf;
6304    use std::time::Instant;
6305
6306    #[test]
6307    fn indexed_claude_descriptor_keeps_the_live_peer_address() {
6308        let descriptor = SessionDescriptor {
6309            locator: SessionLocator {
6310                harness: HarnessId::new(HarnessId::CLAUDE_CODE),
6311                session_id: "live-session".into(),
6312                storage: StorageLocator::File {
6313                    path: PathBuf::from("/tmp/live-session.jsonl"),
6314                },
6315            },
6316            cwd: Some(PathBuf::from("/project")),
6317            title: None,
6318            preview_candidates: Vec::new(),
6319            latest_message_candidates: Vec::new(),
6320            updated_at_ms: Some(1),
6321            message_count: None,
6322            model: None,
6323            parent_session_id: None,
6324            child_session_count: 0,
6325            nouns: Default::default(),
6326        };
6327        let peer = crate::claude_peer::ClaudePeerSession {
6328            pid: 42,
6329            session_id: "live-session".into(),
6330            cwd: Some(PathBuf::from("/project")),
6331            name: "peer".into(),
6332            socket_path: PathBuf::from("/tmp/peer.sock"),
6333            status: Some(crate::claude_peer::ClaudePeerStatus::Busy),
6334            updated_at_ms: Some(1),
6335            version: Some("test".into()),
6336        };
6337
6338        let value = live_descriptor_value(&descriptor, &[peer]).unwrap();
6339        assert!(value["live_endpoint"]
6340            .as_str()
6341            .is_some_and(|endpoint| endpoint.starts_with("cc-peer:v1:42:peer:")));
6342    }
6343
6344    struct EndingRuntime {
6345        handle: RuntimeHandle,
6346        event: Option<HarnessEvent>,
6347        close_failures: usize,
6348    }
6349
6350    #[async_trait]
6351    impl RuntimeConnection for EndingRuntime {
6352        fn handle(&self) -> &RuntimeHandle {
6353            &self.handle
6354        }
6355
6356        async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
6357            unreachable!("ending runtime does not accept input")
6358        }
6359
6360        async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
6361            Ok(self.event.take())
6362        }
6363
6364        async fn interrupt(&mut self) -> crate::Result<()> {
6365            Ok(())
6366        }
6367
6368        async fn respond(&mut self, _request_id: Value, _response: Value) -> crate::Result<()> {
6369            Ok(())
6370        }
6371
6372        async fn close(&mut self) -> crate::Result<()> {
6373            if self.close_failures > 0 {
6374                self.close_failures -= 1;
6375                return Err(crate::Error::Other(
6376                    "cleanup temporarily unavailable".into(),
6377                ));
6378            }
6379            Ok(())
6380        }
6381    }
6382
6383    fn ending_runtime(event: Option<HarnessEvent>) -> Box<dyn RuntimeConnection> {
6384        Box::new(EndingRuntime {
6385            handle: RuntimeHandle {
6386                harness: HarnessId::from(HarnessId::CLAUDE_CODE),
6387                runtime_id: "ending-session".into(),
6388                endpoint: RuntimeEndpoint::LocalProcess {
6389                    pid: None,
6390                    command: vec!["ending-runtime".into()],
6391                    protocol: "test".into(),
6392                },
6393            },
6394            event,
6395            close_failures: 0,
6396        })
6397    }
6398
6399    #[tokio::test]
6400    async fn closing_a_runtime_surrenders_the_connection_even_when_teardown_fails() {
6401        let mut service = HarnessSessionService::new();
6402        let handle = ending_runtime(None).handle().clone();
6403        let runtime_id = handle.runtime_id.clone();
6404        let opened = service
6405            .insert_runtime(Box::new(EndingRuntime {
6406                handle,
6407                event: None,
6408                close_failures: 1,
6409            }))
6410            .unwrap();
6411        let connection = opened["connection"].as_str().unwrap().to_string();
6412        service.terminal_launches.insert(
6413            connection.clone(),
6414            StructuredLaunch {
6415                cwd: PathBuf::from("/fixture"),
6416                program: "fixture".into(),
6417                arguments: Vec::new(),
6418                env: BTreeMap::new(),
6419            },
6420        );
6421        let first = service
6422            .handle_async(request(
6423                1,
6424                "harness.v1.runtimes.close",
6425                json!({"connection": connection}),
6426            ))
6427            .await;
6428        // The harness's own teardown failed and the caller is told so...
6429        assert!(first.get("error").is_some(), "{first}");
6430        // ...but the connection is gone all the same. A connection whose close
6431        // cannot complete is exactly the one that must not stay registered:
6432        // holding it would answer every later call on this node with a turn
6433        // that is never going to end.
6434        assert!(!service.runtimes.contains_key(&connection));
6435        assert!(!service.terminal_launches.contains_key(&connection));
6436        assert!(!service.runtime_sequences.contains_key(&runtime_id));
6437        let again = service
6438            .handle_async(request(
6439                2,
6440                "harness.v1.runtimes.close",
6441                json!({"connection": connection}),
6442            ))
6443            .await;
6444        assert_eq!(again["error"]["code"], -32602, "{again}");
6445    }
6446
6447    fn request(id: u64, method: &str, params: Value) -> Value {
6448        json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params})
6449    }
6450
6451    // ---- ORCH-6: conversation nouns on `sessions.*` ----------------------
6452
6453    fn hermes_store() -> PathBuf {
6454        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/hermes_home/state.db")
6455    }
6456
6457    /// The discovery response for the Hermes fixture home, with the one
6458    /// machine-specific value (the absolute store path) replaced so the exact
6459    /// same JSON can be committed and replayed by the UI story.
6460    fn hermes_discovery(params: Value) -> Value {
6461        let mut response =
6462            HarnessSessionService::new().handle(request(1, "harness.v1.sessions.discover", params));
6463        let store = hermes_store().display().to_string();
6464        for session in response["result"]["sessions"]
6465            .as_array_mut()
6466            .expect("sessions array")
6467        {
6468            if session["locator"]["storage"]["path"] == json!(store) {
6469                session["locator"]["storage"]["path"] = json!("<fixtures>/hermes_home/state.db");
6470            }
6471            // `activity` reports a wall-clock observation instant, not a fact
6472            // about the session; it would make this response differ on every
6473            // call. The nouns under test are all session facts.
6474            session.as_object_mut().unwrap().remove("activity");
6475        }
6476        response["result"].take()
6477    }
6478
6479    fn hermes_query() -> Value {
6480        json!({
6481            "harnesses": ["hermes"],
6482            "homes": {"hermes": hermes_store()},
6483        })
6484    }
6485
6486    fn row<'a>(result: &'a Value, id: &str) -> &'a Value {
6487        result["sessions"]
6488            .as_array()
6489            .expect("sessions array")
6490            .iter()
6491            .find(|session| session["locator"]["session_id"] == json!(id))
6492            .unwrap_or_else(|| panic!("no discovered row for `{id}` in {result:#}"))
6493    }
6494
6495    #[test]
6496    fn orch6_discover_rows_carry_the_conversation_nouns() {
6497        let result = hermes_discovery(hermes_query());
6498
6499        // A Telegram DM: reached on a channel, no repo — the workspace IS the
6500        // channel (D2 precedence), and `main` is not a profile.
6501        let dm = row(&result, "tg-dm-1");
6502        assert_eq!(dm["trigger"], json!("channel"));
6503        assert_eq!(dm["surface"]["platform"], json!("telegram"));
6504        assert_eq!(dm["surface"]["kind"], json!("dm"));
6505        assert_eq!(dm["surface"]["chat_id"], json!("123456"));
6506        assert_eq!(dm["surface"]["participant_id"], json!("u1"));
6507        assert_eq!(
6508            dm["workspace"],
6509            json!({"kind": "channel", "value": "telegram:123456"})
6510        );
6511        assert!(dm.get("profile").is_none(), "{dm:#}");
6512
6513        // A cron fire: recurring, with the job recovered from the minted id.
6514        let fire = row(&result, "cron_job42_20260902_120000");
6515        assert_eq!(fire["trigger"], json!("cron"));
6516        assert_eq!(
6517            fire["recurrence"],
6518            json!({"job_id": "job42", "kind": "cron"})
6519        );
6520        assert_eq!(fire["workspace"]["kind"], json!("repo"));
6521
6522        // A profiled group session with a pending handoff: repo workspace
6523        // wins over the channel, and the chat stays on the surface key.
6524        let coder = row(&result, "tg-coder-1");
6525        assert_eq!(coder["trigger"], json!("channel"));
6526        assert_eq!(coder["profile"], json!("coder"));
6527        assert_eq!(coder["surface"]["thread_id"], json!("55"));
6528        assert_eq!(
6529            coder["surface"]["key"],
6530            json!("agent:coder:telegram:group:-100777:55")
6531        );
6532        assert_eq!(
6533            coder["workspace"],
6534            json!({"kind": "repo", "value": "/workspace/project"})
6535        );
6536        assert_eq!(
6537            coder["cross_surface"],
6538            json!({"state": "pending", "platform": "discord"})
6539        );
6540
6541        // A plain ACP session stays human-triggered with no surface at all.
6542        let acp = row(&result, "cef97234-e8e8-428a-99ab-e8fff4e7e613");
6543        assert_eq!(acp["trigger"], json!("human"));
6544        assert!(acp.get("surface").is_none(), "{acp:#}");
6545        assert_eq!(acp["workspace"], json!({"kind": "none"}));
6546    }
6547
6548    #[test]
6549    fn orch6_discover_filters_by_harness_and_profile() {
6550        let mut params = hermes_query();
6551        params["profile"] = json!("coder");
6552        let result = hermes_discovery(params);
6553        let ids: Vec<&str> = result["sessions"]
6554            .as_array()
6555            .expect("sessions array")
6556            .iter()
6557            .map(|session| session["locator"]["session_id"].as_str().unwrap())
6558            .collect();
6559        assert_eq!(ids, vec!["tg-coder-1"]);
6560
6561        // A profile no session is routed through returns nothing rather than
6562        // silently ignoring the filter.
6563        let mut missing = hermes_query();
6564        missing["profile"] = json!("nobody");
6565        assert_eq!(hermes_discovery(missing)["sessions"], json!([]));
6566
6567        // The harness filter is `harnesses`; an id no harness answers to is
6568        // an empty page, never every store on the box.
6569        let elsewhere = json!({"harnesses": ["codex"], "homes": {"codex": hermes_store()}});
6570        assert_eq!(hermes_discovery(elsewhere)["sessions"], json!([]));
6571    }
6572
6573    #[test]
6574    fn orch6_load_reports_the_same_nouns_as_discovery() {
6575        let mut service = HarnessSessionService::new();
6576        let loaded = service.handle(request(
6577            1,
6578            "harness.v1.sessions.load",
6579            json!({"locator": {
6580                "harness": "hermes",
6581                "session_id": "tg-coder-1",
6582                "storage": {"kind": "file", "path": hermes_store()},
6583            }}),
6584        ));
6585        let session = &loaded["result"]["session"];
6586        let discovered = hermes_discovery(hermes_query());
6587        let row = row(&discovered, "tg-coder-1");
6588        for noun in [
6589            "trigger",
6590            "surface",
6591            "profile",
6592            "recurrence",
6593            "cross_surface",
6594            "workspace",
6595        ] {
6596            assert_eq!(
6597                session[noun],
6598                row.get(noun).cloned().unwrap_or(Value::Null),
6599                "`{noun}` disagrees between sessions.load and sessions.discover"
6600            );
6601        }
6602    }
6603
6604    /// ORCH-10: the fixture homes, as the RPC's `homes` override. Hermes's
6605    /// home is named by its `state.db`; OpenClaw's is the state directory.
6606    fn profile_fixture_homes() -> Value {
6607        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6608        json!({
6609            "hermes": fixtures.join("hermes_home/state.db"),
6610            "openclaw": fixtures.join("openclaw_home"),
6611        })
6612    }
6613
6614    fn profile_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6615        response["result"]["profiles"]
6616            .as_array()
6617            .unwrap_or_else(|| panic!("no profiles array in {response}"))
6618            .iter()
6619            .find(|row| row["harness"] == harness && row["name"] == name)
6620            .unwrap_or_else(|| panic!("no `{harness}` profile `{name}` in {response}"))
6621    }
6622
6623    /// dev/01: every source answers in one row shape, over the committed
6624    /// fixture homes — the Hermes profile directory and its `state.db`
6625    /// partition, the OpenClaw agent directories and `openclaw.json`, and
6626    /// supercode's own presets.
6627    #[test]
6628    fn profiles_list_reads_every_source_uniformly() {
6629        let mut service = HarnessSessionService::new();
6630        let response = service.handle(request(
6631            1,
6632            "harness.v1.profiles.list",
6633            json!({"homes": profile_fixture_homes()}),
6634        ));
6635        assert_eq!(
6636            response["result"]["schema"],
6637            crate::profiles::PROFILES_SCHEMA
6638        );
6639
6640        let default = profile_row(&response, "hermes", "default");
6641        assert_eq!(default["kind"], "hermes_profile");
6642        assert_eq!(default["default"], true);
6643        assert_eq!(default["routes"], 0);
6644        assert_eq!(default["sessions"], 11);
6645        assert_eq!(default["model"], "anthropic/claude-sonnet-4-5");
6646
6647        let coder = profile_row(&response, "hermes", "coder");
6648        assert_eq!(coder["kind"], "hermes_profile");
6649        assert_eq!(coder["default"], false);
6650        assert_eq!(coder["routes"], 1, "gateway.profile_routes targets coder");
6651        assert_eq!(coder["sessions"], 1, "state.db profile_name = 'coder'");
6652        assert_eq!(coder["model"], "anthropic/claude-opus-4-8");
6653        assert!(coder["home"]
6654            .as_str()
6655            .unwrap()
6656            .ends_with("hermes_home/profiles/coder"));
6657
6658        let main = profile_row(&response, "openclaw", "main");
6659        assert_eq!(main["kind"], "openclaw_agent");
6660        // No entry declares `default: true` (real configs do not), so `main`
6661        // wins on OpenClaw's own convention rather than alphabetically.
6662        assert_eq!(main["default"], true);
6663        assert_eq!(main["routes"], 0);
6664        assert_eq!(main["sessions"], 4);
6665        assert_eq!(
6666            main["model"],
6667            Value::Null,
6668            "`agents.defaults.model` is an install default, not this agent's pin"
6669        );
6670
6671        let design = profile_row(&response, "openclaw", "design");
6672        assert_eq!(design["default"], false);
6673        assert_eq!(design["routes"], 1, "one binding names agentId `design`");
6674        assert_eq!(design["sessions"], 0);
6675        assert_eq!(design["model"], "anthropic/claude-opus-4-8");
6676
6677        let preset = profile_row(&response, "supercode", "supercode-default");
6678        assert_eq!(preset["kind"], "preset");
6679        assert_eq!(preset["default"], true);
6680        assert_eq!(preset["home"], Value::Null);
6681        assert_eq!(preset["routes"], Value::Null);
6682    }
6683
6684    /// Codex's own profiles are `[profiles.<name>]` tables, with the
6685    /// top-level `profile` key naming the default.
6686    #[test]
6687    fn profiles_list_reads_codex_profile_tables() {
6688        let codex_home = std::env::temp_dir().join(format!(
6689            "supercode-orch10-codex-{}-{}",
6690            std::process::id(),
6691            std::time::SystemTime::now()
6692                .duration_since(std::time::UNIX_EPOCH)
6693                .unwrap()
6694                .as_nanos()
6695        ));
6696        std::fs::create_dir_all(codex_home.join("sessions")).unwrap();
6697        std::fs::write(
6698            codex_home.join("config.toml"),
6699            "profile = \"review\"\n\n[profiles.review]\nmodel = \"gpt-5.1-codex\"\n\n[profiles.fast]\nmodel = \"gpt-5.1-codex-mini\"\n",
6700        )
6701        .unwrap();
6702
6703        let mut service = HarnessSessionService::new();
6704        let response = service.handle(request(
6705            1,
6706            "harness.v1.profiles.list",
6707            json!({"harness": "codex", "homes": {"codex": codex_home.join("sessions")}}),
6708        ));
6709        let rows = response["result"]["profiles"].as_array().unwrap();
6710        assert_eq!(rows.len(), 2, "{response}");
6711        let review = profile_row(&response, "codex", "review");
6712        assert_eq!(review["kind"], "codex_profile");
6713        assert_eq!(review["default"], true);
6714        assert_eq!(review["model"], "gpt-5.1-codex");
6715        assert_eq!(review["home"], Value::Null);
6716        assert_eq!(profile_row(&response, "codex", "fast")["default"], false);
6717
6718        let got = service.handle(request(
6719            2,
6720            "harness.v1.profiles.get",
6721            json!({
6722                "harness": "codex",
6723                "name": "fast",
6724                "homes": {"codex": codex_home.join("sessions")},
6725            }),
6726        ));
6727        assert_eq!(got["result"]["profile"]["model"], "gpt-5.1-codex-mini");
6728        std::fs::remove_dir_all(&codex_home).ok();
6729    }
6730
6731    /// A verb a harness lacks fails with `UnsupportedAction`, never a silent
6732    /// empty list; an unknown name is an invalid argument, not an empty row.
6733    #[test]
6734    fn profiles_refuse_harnesses_without_the_concept() {
6735        let mut service = HarnessSessionService::new();
6736        let response = service.handle(request(
6737            1,
6738            "harness.v1.profiles.list",
6739            json!({"harness": "claude-code"}),
6740        ));
6741        assert_eq!(response["error"]["code"], -32020, "{response}");
6742
6743        let missing = service.handle(request(
6744            2,
6745            "harness.v1.profiles.get",
6746            json!({
6747                "harness": "hermes",
6748                "name": "no-such-profile",
6749                "homes": profile_fixture_homes(),
6750            }),
6751        ));
6752        assert_eq!(missing["error"]["code"], -32602, "{missing}");
6753    }
6754
6755    /// The two methods are advertised, so a client discovers them from
6756    /// `harness.v1.capabilities` rather than from documentation.
6757    #[test]
6758    fn profiles_methods_are_advertised() {
6759        let mut service = HarnessSessionService::new();
6760        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6761        let methods = response["result"]["methods"].as_array().unwrap();
6762        for method in ["harness.v1.profiles.list", "harness.v1.profiles.get"] {
6763            assert!(
6764                methods.iter().any(|entry| entry == method),
6765                "{method} is not advertised"
6766            );
6767        }
6768    }
6769
6770    // -----------------------------------------------------------------
6771    // ORCH-14 — channels
6772    // -----------------------------------------------------------------
6773
6774    fn channel_row<'a>(response: &'a Value, harness: &str, name: &str) -> &'a Value {
6775        response["result"]["channels"]
6776            .as_array()
6777            .unwrap_or_else(|| panic!("no channels array in {response}"))
6778            .iter()
6779            .find(|row| row["harness"] == harness && row["name"] == name)
6780            .unwrap_or_else(|| panic!("no `{harness}` channel `{name}` in {response}"))
6781    }
6782
6783    fn channels_list(harness: Option<&str>) -> Value {
6784        let mut params = json!({"homes": profile_fixture_homes()});
6785        if let Some(harness) = harness {
6786            params["harness"] = json!(harness);
6787        }
6788        HarnessSessionService::new().handle(request(1, "harness.v1.channels.list", params))
6789    }
6790
6791    /// dev/01: both sources answer in one row shape over the committed
6792    /// fixture homes — Hermes's `platforms:` blocks with their `extra` maps,
6793    /// and OpenClaw's `channels.<name>` entries split per account.
6794    #[test]
6795    fn channels_list_reads_both_gateway_harnesses_uniformly() {
6796        let response = channels_list(None);
6797        assert_eq!(
6798            response["result"]["schema"],
6799            crate::channels::CHANNELS_SCHEMA
6800        );
6801
6802        // Hermes: a credentialed platform, a bridged `extra.key` platform,
6803        // and one the config explicitly disables.
6804        let telegram = channel_row(&response, "hermes", "telegram");
6805        assert_eq!(telegram["kind"], "telegram");
6806        assert_eq!(telegram["enabled"], true);
6807        assert_eq!(telegram["configured"], true);
6808        // The `sessions` count is the discovery rows whose surface platform
6809        // is telegram: the fixture's `agent:main:telegram:…` DM and the
6810        // `agent:coder:telegram:…` group.
6811        assert_eq!(telegram["sessions"], 2);
6812        let api = channel_row(&response, "hermes", "api_server");
6813        assert_eq!(api["configured"], true, "extra.key is a credential key");
6814        assert_eq!(api["sessions"], 0);
6815        let webhook = channel_row(&response, "hermes", "webhook");
6816        assert_eq!(webhook["enabled"], false);
6817        // Hermes lists no credential for `webhook`: declaring it is all it
6818        // needs, so a credential-less entry is still `configured`.
6819        assert_eq!(webhook["configured"], true);
6820
6821        // OpenClaw: one row per account, named `<channel>/<accountId>`.
6822        let linked = channel_row(&response, "openclaw", "slack/T0FIXTURE");
6823        assert_eq!(linked["kind"], "slack");
6824        assert_eq!(linked["account"], "T0FIXTURE");
6825        assert_eq!(linked["enabled"], true);
6826        assert_eq!(linked["configured"], true);
6827        let unlinked = channel_row(&response, "openclaw", "slack/T1FIXTURE");
6828        assert_eq!(unlinked["enabled"], false);
6829        assert_eq!(
6830            unlinked["configured"], false,
6831            "an account with no credential key is not configured"
6832        );
6833        // A single-account channel keeps its own name and names its account
6834        // inline.
6835        let telegram = channel_row(&response, "openclaw", "telegram");
6836        assert_eq!(telegram["account"], "hermes-fixture-bot");
6837        assert_eq!(telegram["configured"], true);
6838
6839        // `status` is never claimed from a config file.
6840        for row in response["result"]["channels"].as_array().unwrap() {
6841            assert_eq!(row["status"], "unknown", "{row}");
6842        }
6843    }
6844
6845    /// dev/01: no field of any emitted row carries a credential. The fixture
6846    /// homes hold four FAKE credential strings; a row that leaked one — as a
6847    /// value, an account label, or a name — fails here.
6848    #[test]
6849    fn channels_rows_never_carry_a_fixture_secret() {
6850        let secrets = [
6851            "FAKE-TOKEN-DO-NOT-EMIT",
6852            "FAKE-API-SERVER-KEY-DO-NOT-EMIT",
6853            "FAKE-SLACK-BOT-TOKEN-DO-NOT-EMIT",
6854            "FAKE-SLACK-APP-TOKEN-DO-NOT-EMIT",
6855            "FAKE-TELEGRAM-TOKEN-DO-NOT-EMIT",
6856        ];
6857        // The strings really are in the fixtures, so this test can fail.
6858        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
6859        let raw = format!(
6860            "{}{}",
6861            std::fs::read_to_string(fixtures.join("hermes_home/config.yaml")).unwrap(),
6862            std::fs::read_to_string(fixtures.join("openclaw_home/openclaw.json")).unwrap(),
6863        );
6864        for secret in secrets {
6865            assert!(raw.contains(secret), "fixture no longer holds `{secret}`");
6866        }
6867
6868        let emitted = serde_json::to_string(&channels_list(None)["result"]).unwrap();
6869        for secret in secrets {
6870            assert!(
6871                !emitted.contains(secret),
6872                "`{secret}` leaked into a channel row: {emitted}"
6873            );
6874        }
6875        // Belt and braces: no row FIELD is credential-shaped either, so a
6876        // future field cannot smuggle one past the literal scan.
6877        for row in channels_list(None)["result"]["channels"]
6878            .as_array()
6879            .unwrap()
6880        {
6881            for key in row.as_object().unwrap().keys() {
6882                let key = key.to_ascii_lowercase();
6883                assert!(
6884                    !["token", "key", "secret", "password", "credential"]
6885                        .iter()
6886                        .any(|marker| key.ends_with(marker)),
6887                    "`{key}` is a credential-shaped field on a channel row"
6888                );
6889            }
6890        }
6891    }
6892
6893    /// `status` answers one row by name, and refuses an unknown one.
6894    #[test]
6895    fn channels_status_reads_one_row_by_name() {
6896        let mut service = HarnessSessionService::new();
6897        let got = service.handle(request(
6898            1,
6899            "harness.v1.channels.status",
6900            json!({
6901                "harness": "openclaw",
6902                "name": "slack/T0FIXTURE",
6903                "homes": profile_fixture_homes(),
6904            }),
6905        ));
6906        assert_eq!(got["result"]["channel"]["kind"], "slack");
6907        assert_eq!(got["result"]["channel"]["account"], "T0FIXTURE");
6908        assert_eq!(got["result"]["channel"]["status"], "unknown");
6909
6910        let missing = service.handle(request(
6911            2,
6912            "harness.v1.channels.status",
6913            json!({
6914                "harness": "openclaw",
6915                "name": "no-such-channel",
6916                "homes": profile_fixture_homes(),
6917            }),
6918        ));
6919        assert_eq!(missing["error"]["code"], -32602, "{missing}");
6920    }
6921
6922    /// A harness with no channel concept fails with `UnsupportedAction`,
6923    /// never a silent empty list — Claude Code included, because its channels
6924    /// are MCP-protocol declarations no config file names.
6925    #[test]
6926    fn channels_refuse_harnesses_without_the_concept() {
6927        let response = channels_list(Some("claude-code"));
6928        assert_eq!(response["error"]["code"], -32020, "{response}");
6929        let codex = channels_list(Some("codex"));
6930        assert_eq!(codex["error"]["code"], -32020, "{codex}");
6931    }
6932
6933    /// The harness filter restricts the rows rather than being ignored.
6934    #[test]
6935    fn channels_list_filters_by_harness() {
6936        let response = channels_list(Some("openclaw"));
6937        let rows = response["result"]["channels"].as_array().unwrap();
6938        assert!(!rows.is_empty(), "{response}");
6939        assert!(
6940            rows.iter().all(|row| row["harness"] == "openclaw"),
6941            "harness filter leaked: {response}"
6942        );
6943    }
6944
6945    /// Both methods are advertised, so a client discovers them from
6946    /// `harness.v1.capabilities` rather than from documentation.
6947    #[test]
6948    fn channels_methods_are_advertised() {
6949        let mut service = HarnessSessionService::new();
6950        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
6951        let methods = response["result"]["methods"].as_array().unwrap();
6952        for method in ["harness.v1.channels.list", "harness.v1.channels.status"] {
6953            assert!(
6954                methods.iter().any(|entry| entry == method),
6955                "{method} is not advertised"
6956            );
6957        }
6958    }
6959
6960    /// The UI story renders REAL rows: this writes the discovery response the
6961    /// two assertions above pin into the fixture the Storybook
6962    /// `Compositions/Universal nouns` stories import, and fails when the
6963    /// committed copy has drifted from what the service now answers.
6964    #[test]
6965    fn orch6_story_fixture_matches_the_live_discovery_response() {
6966        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6967            .join("../../sdk/ui/stories/fixtures/hermes-discovery.json");
6968        let mut result = hermes_discovery(hermes_query());
6969        // `updated_at_ms` is derived from the fixture's own stored timestamps,
6970        // so the whole response is deterministic; drop only the cursor, which
6971        // is pagination state rather than a session fact.
6972        result.as_object_mut().unwrap().remove("next_cursor");
6973        let rendered = format!("{}\n", serde_json::to_string_pretty(&result).unwrap());
6974        if std::env::var_os("SUPERCODE_UPDATE_FIXTURES").is_some() {
6975            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
6976            std::fs::write(&path, &rendered).unwrap();
6977        }
6978        let committed = std::fs::read_to_string(&path).unwrap_or_default();
6979        assert_eq!(
6980            committed, rendered,
6981            "sdk/ui/stories/fixtures/hermes-discovery.json is stale — \
6982             re-run with SUPERCODE_UPDATE_FIXTURES=1"
6983        );
6984    }
6985
6986    fn pi_locator() -> SessionLocator {
6987        SessionLocator {
6988            harness: HarnessId::from(HarnessId::PI),
6989            session_id: "1e6f2a3b-0000-4000-8000-000000000001".into(),
6990            storage: StorageLocator::File {
6991                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6992                    .join("tests/fixtures/pi_session.jsonl"),
6993            },
6994        }
6995    }
6996
6997    fn opencode_locator() -> SessionLocator {
6998        let session_id = "ses_fixtureAAAAAAAAAAAAAAA1";
6999        SessionLocator {
7000            harness: HarnessId::from(HarnessId::OPENCODE),
7001            session_id: session_id.into(),
7002            storage: StorageLocator::Sqlite {
7003                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7004                    .join("tests/fixtures/opencode_fixture/opencode.db"),
7005                selector: session_id.into(),
7006            },
7007        }
7008    }
7009
7010    fn grok_locator() -> SessionLocator {
7011        SessionLocator {
7012            harness: HarnessId::from(HarnessId::GROK),
7013            session_id: "73c09283-4b33-41fa-90f1-0bcb0f7be523".into(),
7014            storage: StorageLocator::File {
7015                path: PathBuf::from(env!("CARGO_MANIFEST_DIR"))
7016                    .join("tests/fixtures/grok_session/chat_history.jsonl"),
7017            },
7018        }
7019    }
7020
7021    // ---- ORCH-11: `harness.v1.skills.list` -------------------------------
7022
7023    fn fixture_homes() -> Value {
7024        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7025        json!({
7026            "claude_code": fixtures.join("__absent__"),
7027            "codex": fixtures.join("__absent__"),
7028            "opencode": fixtures.join("__absent__"),
7029            "pi": fixtures.join("__absent__"),
7030            "agents": fixtures.join("__absent__"),
7031            "hermes": fixtures.join("hermes_home"),
7032            "openclaw": fixtures.join("openclaw_home"),
7033        })
7034    }
7035
7036    #[test]
7037    fn preview_search_uses_the_discovery_rpc_and_refuses_live_subscription() {
7038        let root = std::env::temp_dir().join(format!(
7039            "supercode-preview-rpc-{}-{}",
7040            std::process::id(),
7041            std::time::SystemTime::now()
7042                .duration_since(std::time::UNIX_EPOCH)
7043                .unwrap()
7044                .as_nanos()
7045        ));
7046        std::fs::create_dir_all(&root).unwrap();
7047        for id in ["first", "second"] {
7048            std::fs::write(root.join(format!("{id}.jsonl")), format!("{}\n{}\n",
7049                json!({"type": "session_meta", "payload": {"id": id, "cwd": "/workspace"}}),
7050                json!({"type": "event_msg", "payload": {"type": "agent_message", "message": "NEBULA result"}}),
7051            )).unwrap();
7052        }
7053        let mut service = HarnessSessionService::new();
7054        let query = json!({
7055            "harnesses": ["codex"], "homes": {"codex": root},
7056            "query": "nebula", "search_previews": true, "limit": 1
7057        });
7058        let first = service.handle(request(1, "harness.v1.sessions.discover", query.clone()));
7059        assert!(first.get("error").is_none(), "{first}");
7060        assert_eq!(first["result"]["receipt"]["searched_previews"], true);
7061        assert_eq!(first["result"]["receipt"]["total_matched"], 2);
7062        let mut next_query = query.clone();
7063        next_query["cursor"] = first["result"]["next_cursor"].clone();
7064        let next = service.handle(request(2, "harness.v1.sessions.discover", next_query));
7065        assert_eq!(next["result"]["receipt"]["returned"], 1);
7066        assert_eq!(next["result"]["receipt"]["total_matched"], 2);
7067        assert_eq!(next["result"]["receipt"]["truncated"], false);
7068        assert_ne!(
7069            first["result"]["sessions"][0]["locator"],
7070            next["result"]["sessions"][0]["locator"]
7071        );
7072        let refused = service.handle(request(3, "harness.v1.sessions.index.subscribe", query));
7073        assert!(
7074            refused["error"]["message"]
7075                .as_str()
7076                .unwrap()
7077                .contains("use sessions.discover"),
7078            "{refused}"
7079        );
7080        std::fs::remove_dir_all(root).unwrap();
7081    }
7082
7083    #[test]
7084    fn session_index_resize_preserves_subscription_and_rejects_invalid_requests() {
7085        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.sessions.index.resize"));
7086        let root = std::env::temp_dir().join(format!(
7087            "supercode-index-rpc-{}-{}",
7088            std::process::id(),
7089            std::time::SystemTime::now()
7090                .duration_since(std::time::UNIX_EPOCH)
7091                .unwrap()
7092                .as_nanos()
7093        ));
7094        std::fs::create_dir_all(&root).unwrap();
7095        for id in ["first", "second"] {
7096            std::fs::write(root.join(format!("{id}.jsonl")), format!(
7097                "{{\"type\":\"session_meta\",\"payload\":{{\"id\":\"{id}\",\"cwd\":\"/workspace\"}}}}\n"
7098            )).unwrap();
7099        }
7100        let mut service = HarnessSessionService::new();
7101        let opened = service.handle(request(
7102            1,
7103            "harness.v1.sessions.index.subscribe",
7104            json!({
7105                "harnesses": ["codex"], "homes": { "codex": root }, "limit": 1
7106            }),
7107        ));
7108        assert!(opened.get("error").is_none(), "{opened:#}");
7109        let subscription = opened["result"]["subscription"]
7110            .as_str()
7111            .unwrap()
7112            .to_owned();
7113        assert_eq!(opened["result"]["initial"].as_array().unwrap().len(), 1);
7114        for params in [
7115            json!({"subscription": subscription, "limit": 0}),
7116            json!({"subscription": subscription, "limit": 2049}),
7117            json!({"subscription": subscription, "limit": 2, "cursor": "not-allowed"}),
7118            json!({"subscription": "unknown", "limit": 2}),
7119        ] {
7120            let rejected = service.handle(request(2, "harness.v1.sessions.index.resize", params));
7121            assert_eq!(rejected["error"]["code"], -32602, "{rejected:#}");
7122        }
7123        for (limit, revision) in [(1, 1), (2, 2), (2, 2), (1, 3)] {
7124            let response = service.handle(request(
7125                3,
7126                "harness.v1.sessions.index.resize",
7127                json!({
7128                    "subscription": subscription, "limit": limit
7129                }),
7130            ));
7131            assert!(response.get("error").is_none(), "{response:#}");
7132            assert_eq!(response["result"]["subscription"], subscription);
7133            assert_eq!(response["result"]["revision"], revision);
7134            assert_eq!(
7135                response["result"]["initial"].as_array().unwrap().len(),
7136                limit
7137            );
7138            assert_eq!(response["result"]["receipt"]["total_matched"], 2);
7139            assert_eq!(service.index_subscriptions.len(), 1);
7140        }
7141        let removed = service.handle(request(
7142            4,
7143            "harness.v1.sessions.index.unsubscribe",
7144            json!({
7145                "subscription": subscription
7146            }),
7147        ));
7148        assert_eq!(removed["result"]["removed"], true);
7149        let stale = service.handle(request(
7150            5,
7151            "harness.v1.sessions.index.resize",
7152            json!({
7153                "subscription": subscription, "limit": 1
7154            }),
7155        ));
7156        assert_eq!(stale["error"]["code"], -32602);
7157        drop(service);
7158        std::fs::remove_dir_all(root).unwrap();
7159    }
7160
7161    fn skills_rows(params: Value) -> Vec<Value> {
7162        let response =
7163            HarnessSessionService::new().handle(request(1, "harness.v1.skills.list", params));
7164        assert!(response.get("error").is_none(), "{response:#}");
7165        response["result"].as_array().cloned().unwrap_or_default()
7166    }
7167
7168    /// The uniform row over two harnesses at once, from the harnesses' own
7169    /// skill roots: name, harness, scope, location, description, version.
7170    #[test]
7171    fn skills_list_reads_the_hermes_and_openclaw_roots() {
7172        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7173        let rows = skills_rows(json!({
7174            "homes": fixture_homes(),
7175            "cwd": fixtures.join("hermes_home"),
7176        }));
7177        let arxiv = rows
7178            .iter()
7179            .find(|row| row["name"] == json!("arxiv-search"))
7180            .unwrap_or_else(|| panic!("no arxiv row in {rows:#?}"));
7181        assert_eq!(arxiv["harness"], json!(HarnessId::HERMES));
7182        assert_eq!(arxiv["scope"], json!("user"));
7183        assert_eq!(arxiv["version"], json!("1.4.0"));
7184        assert!(arxiv["location"]
7185            .as_str()
7186            .unwrap()
7187            .ends_with("hermes_home/skills/research/arxiv"));
7188
7189        // A directory with no SKILL.md still lists, by directory name.
7190        let bare = rows
7191            .iter()
7192            .find(|row| row["name"] == json!("bare-skill"))
7193            .unwrap_or_else(|| panic!("no bare-skill row in {rows:#?}"));
7194        assert_eq!(bare["enabled"], json!(null));
7195        assert!(bare.get("description").is_none());
7196
7197        let demo = rows
7198            .iter()
7199            .find(|row| row["name"] == json!("clawhub-demo"))
7200            .unwrap_or_else(|| panic!("no clawhub-demo row in {rows:#?}"));
7201        assert_eq!(demo["harness"], json!(HarnessId::OPENCLAW));
7202        assert_eq!(demo["scope"], json!("managed"));
7203        assert_eq!(demo["enabled"], json!(false));
7204    }
7205
7206    /// Both filters select against the same rows.
7207    #[test]
7208    fn skills_list_filters_by_harness_and_scope() {
7209        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7210        let hermes = skills_rows(json!({
7211            "homes": fixture_homes(),
7212            "cwd": fixtures.join("hermes_home"),
7213            "harness": HarnessId::HERMES,
7214        }));
7215        assert!(!hermes.is_empty());
7216        assert!(hermes
7217            .iter()
7218            .all(|row| row["harness"] == json!(HarnessId::HERMES)));
7219
7220        let managed = skills_rows(json!({
7221            "homes": fixture_homes(),
7222            "cwd": fixtures.join("openclaw_home"),
7223            "harness": HarnessId::OPENCLAW,
7224            "scope": "managed",
7225        }));
7226        assert_eq!(managed.len(), 1, "{managed:#?}");
7227        assert_eq!(managed[0]["name"], json!("clawhub-demo"));
7228
7229        let bundled = skills_rows(json!({
7230            "homes": fixture_homes(),
7231            "cwd": fixtures.join("openclaw_home"),
7232            "harness": HarnessId::OPENCLAW,
7233            "scope": "bundled",
7234        }));
7235        assert!(bundled.is_empty(), "{bundled:#?}");
7236    }
7237
7238    /// A harness supercode has no skills root for is refused by name, not
7239    /// answered with an empty list.
7240    #[test]
7241    fn skills_list_refuses_an_unknown_harness() {
7242        let response = HarnessSessionService::new().handle(request(
7243            1,
7244            "harness.v1.skills.list",
7245            json!({"harness": "not-a-harness", "homes": fixture_homes()}),
7246        ));
7247        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7248        assert!(response["error"]["message"]
7249            .as_str()
7250            .unwrap()
7251            .contains("not-a-harness"));
7252    }
7253
7254    /// The method is advertised, and its SDK operation resolves it.
7255    #[test]
7256    fn skills_list_is_an_advertised_method_and_sdk_operation() {
7257        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.list"));
7258        assert_eq!(
7259            SdkOperation::from_method("harness.v1.skills.list"),
7260            Some(SdkOperation::SkillsList)
7261        );
7262    }
7263
7264    // ---- ORCH-22: `harness.v1.skills.install|remove` ----------------------
7265
7266    /// Both controlled verbs are advertised and resolve to their operation.
7267    #[test]
7268    fn skills_install_and_remove_are_advertised_methods_and_sdk_operations() {
7269        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.install"));
7270        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.skills.remove"));
7271        assert_eq!(
7272            SdkOperation::from_method("harness.v1.skills.install"),
7273            Some(SdkOperation::SkillsInstall)
7274        );
7275        assert_eq!(
7276            SdkOperation::from_method("harness.v1.skills.remove"),
7277            Some(SdkOperation::SkillsRemove)
7278        );
7279    }
7280
7281    /// The directory door, end to end over the RPC: a local package lands in
7282    /// Claude Code's own user root and the outcome carries the operation and
7283    /// the row the ORCH-11 loader reads back.
7284    #[test]
7285    fn skills_install_and_remove_drive_the_directory_door() {
7286        let root = std::env::temp_dir().join(format!(
7287            "supercode-orch22-rpc-{}-{}",
7288            std::process::id(),
7289            std::time::SystemTime::now()
7290                .duration_since(std::time::UNIX_EPOCH)
7291                .unwrap()
7292                .as_nanos()
7293        ));
7294        let source = root.join("probe-src");
7295        std::fs::create_dir_all(&source).unwrap();
7296        std::fs::write(
7297            source.join("SKILL.md"),
7298            "---\nname: orch22-rpc\ndescription: a probe\n---\nbody\n",
7299        )
7300        .unwrap();
7301        let homes = json!({
7302            "claude_code": root.join("claude_home"),
7303            "codex": root.join("__absent__"),
7304            "opencode": root.join("__absent__"),
7305            "pi": root.join("__absent__"),
7306            "hermes": root.join("__absent__"),
7307            "openclaw": root.join("__absent__"),
7308            "agents": root.join("__absent__"),
7309        });
7310
7311        let mut service = HarnessSessionService::new();
7312        let installed = service.handle(request(
7313            1,
7314            "harness.v1.skills.install",
7315            json!({
7316                "harness": HarnessId::CLAUDE_CODE,
7317                "source": source,
7318                "scope": "user",
7319                "cwd": root,
7320                "homes": homes,
7321            }),
7322        ));
7323        let result = &installed["result"];
7324        assert_eq!(result["name"], json!("orch22-rpc"), "{installed:#}");
7325        assert_eq!(result["verb"], json!("install"));
7326        assert!(result["ran"]
7327            .as_str()
7328            .is_some_and(|ran| ran.starts_with("cp -R ")));
7329        assert_eq!(result["skill"]["scope"], json!("user"));
7330
7331        let removed = service.handle(request(
7332            2,
7333            "harness.v1.skills.remove",
7334            json!({
7335                "harness": HarnessId::CLAUDE_CODE,
7336                "name": "orch22-rpc",
7337                "scope": "user",
7338                "cwd": root,
7339                "homes": homes,
7340            }),
7341        ));
7342        assert_eq!(removed["result"]["removed"], json!(true), "{removed:#}");
7343        assert!(!root.join("claude_home/skills/orch22-rpc").exists());
7344        std::fs::remove_dir_all(&root).ok();
7345    }
7346
7347    /// OpenClaw publishes no `skills remove` at the pin, so the uniform verb
7348    /// refuses with UnsupportedAction instead of deleting files itself.
7349    #[test]
7350    fn skills_remove_refuses_openclaw_at_the_pin() {
7351        let response = HarnessSessionService::new().handle(request(
7352            1,
7353            "harness.v1.skills.remove",
7354            json!({"harness": HarnessId::OPENCLAW, "name": "clawhub-demo"}),
7355        ));
7356        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7357        assert!(response["error"]["message"]
7358            .as_str()
7359            .unwrap()
7360            .contains("no `skills remove` verb"));
7361    }
7362
7363    /// A harness with no skills root at all is refused by name, with the
7364    /// same sentence `skills.list` gives it.
7365    #[test]
7366    fn skills_install_refuses_a_harness_without_a_skills_root() {
7367        let response = HarnessSessionService::new().handle(request(
7368            1,
7369            "harness.v1.skills.install",
7370            json!({"harness": "not-a-harness", "source": "/tmp/x"}),
7371        ));
7372        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7373        assert!(response["error"]["message"]
7374            .as_str()
7375            .unwrap()
7376            .contains("not-a-harness"));
7377    }
7378
7379    // ---- ORCH-12: `harness.v1.memory.show|search` ------------------------
7380
7381    /// `HarnessHomes` for the committed fixture homes. Every root a test does
7382    /// not name is pinned at an absent path, so a read can never fall through
7383    /// to this machine's real harness homes. Note `hermes` is the `state.db`
7384    /// PATH (its parent is HERMES_HOME) and `claude_code` is the `projects`
7385    /// directory — the same contract discovery uses.
7386    fn memory_homes() -> Value {
7387        let fixtures = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
7388        json!({
7389            "claude_code": fixtures.join("__absent__"),
7390            "codex": fixtures.join("__absent__"),
7391            "opencode": fixtures.join("__absent__"),
7392            "pi": fixtures.join("__absent__"),
7393            "grok": fixtures.join("__absent__"),
7394            "gemini": fixtures.join("__absent__"),
7395            "goose": fixtures.join("__absent__"),
7396            "supercode": fixtures.join("__absent__"),
7397            "hermes": fixtures.join("hermes_home/state.db"),
7398            "openclaw": fixtures.join("openclaw_home"),
7399        })
7400    }
7401
7402    fn memory_call_ok(method: &str, params: Value, key: &str) -> Vec<Value> {
7403        let response = HarnessSessionService::new().handle(request(1, method, params));
7404        assert!(response.get("error").is_none(), "{response:#}");
7405        assert_eq!(response["result"]["schema"], json!("supercode.memory.v1"));
7406        response["result"][key]
7407            .as_array()
7408            .cloned()
7409            .unwrap_or_default()
7410    }
7411
7412    fn memory_documents(params: Value) -> Vec<Value> {
7413        memory_call_ok("harness.v1.memory.show", params, "documents")
7414    }
7415
7416    fn memory_matches(params: Value) -> Vec<Value> {
7417        memory_call_ok("harness.v1.memory.search", params, "matches")
7418    }
7419
7420    fn find_document<'a>(rows: &'a [Value], profile: &str, name: &str) -> &'a Value {
7421        rows.iter()
7422            .find(|row| row["profile"] == profile && row["name"] == name)
7423            .unwrap_or_else(|| panic!("no `{profile}` document `{name}` in {rows:#?}"))
7424    }
7425
7426    /// Hermes: the built-in `MEMORY.md`/`USER.md` pair and the `memories/`
7427    /// topic files, for HERMES_HOME itself and for every profile home.
7428    #[test]
7429    fn memory_show_reads_the_hermes_profile_homes() {
7430        let rows = memory_documents(json!({"harness": "hermes", "homes": memory_homes()}));
7431
7432        let notes = find_document(&rows, "default", "MEMORY.md");
7433        assert_eq!(notes["harness"], "hermes");
7434        assert_eq!(notes["scope"], "user");
7435        assert!(notes["size"].as_u64().unwrap() > 0);
7436        assert!(notes["updated_at"].is_string(), "{notes:#?}");
7437        // The default answer previews the head and never the whole body.
7438        assert!(notes.get("content").is_none(), "{notes:#?}");
7439        assert_eq!(notes["truncated"], true);
7440        assert_eq!(notes["preview"].as_array().unwrap().len(), 5);
7441
7442        let user = find_document(&rows, "default", "USER.md");
7443        assert_eq!(user["scope"], "user");
7444        assert!(user["preview"]
7445            .as_array()
7446            .unwrap()
7447            .iter()
7448            .any(|line| line.as_str().unwrap().contains("neovim")));
7449
7450        let topic = find_document(&rows, "default", "memories/2026-09-01-notes.md");
7451        assert!(topic["path"]
7452            .as_str()
7453            .unwrap()
7454            .ends_with("hermes_home/memories/2026-09-01-notes.md"));
7455
7456        // Profile mode points HERMES_HOME at `<root>/profiles/<name>`.
7457        let coder = find_document(&rows, "coder", "MEMORY.md");
7458        assert_eq!(coder["scope"], "profile");
7459        assert!(coder["path"]
7460            .as_str()
7461            .unwrap()
7462            .ends_with("hermes_home/profiles/coder/MEMORY.md"));
7463    }
7464
7465    /// `full` is the only way a body crosses the wire, and `profile` narrows
7466    /// the read to one home.
7467    #[test]
7468    fn memory_show_returns_bodies_only_under_full_and_narrows_by_profile() {
7469        let rows = memory_documents(json!({
7470            "harness": "hermes",
7471            "profile": "coder",
7472            "full": true,
7473            "homes": memory_homes(),
7474        }));
7475        assert!(
7476            rows.iter().all(|row| row["profile"] == "coder"),
7477            "{rows:#?}"
7478        );
7479        let coder = find_document(&rows, "coder", "MEMORY.md");
7480        assert!(coder["content"]
7481            .as_str()
7482            .expect("full returns the body")
7483            .contains("anthropic/claude-opus-4-8"));
7484    }
7485
7486    /// OpenClaw: memory-core's files under each agent's workspace —
7487    /// `<state>/workspace` for the default agent, `<state>/workspace-<id>`
7488    /// for any other.
7489    #[test]
7490    fn memory_show_reads_the_openclaw_agent_workspaces() {
7491        let rows = memory_documents(json!({"harness": "openclaw", "homes": memory_homes()}));
7492
7493        let main = find_document(&rows, "main", "MEMORY.md");
7494        assert_eq!(main["scope"], "agent");
7495        assert!(main["path"]
7496            .as_str()
7497            .unwrap()
7498            .ends_with("openclaw_home/workspace/MEMORY.md"));
7499
7500        let topic = find_document(&rows, "main", "memory/2026-09-01-standup.md");
7501        assert!(topic["path"]
7502            .as_str()
7503            .unwrap()
7504            .ends_with("openclaw_home/workspace/memory/2026-09-01-standup.md"));
7505
7506        let design = find_document(&rows, "design", "MEMORY.md");
7507        assert!(design["path"]
7508            .as_str()
7509            .unwrap()
7510            .ends_with("openclaw_home/workspace-design/MEMORY.md"));
7511    }
7512
7513    /// Claude Code: the auto-memory directory of the project the working tree
7514    /// belongs to, keyed by the enclosing git repository.
7515    #[test]
7516    fn memory_show_reads_a_claude_code_project_auto_memory_directory() {
7517        let scratch = std::env::temp_dir().join(format!(
7518            "supercode-orch12-cc-{}-{}",
7519            std::process::id(),
7520            std::time::SystemTime::now()
7521                .duration_since(std::time::UNIX_EPOCH)
7522                .unwrap()
7523                .as_nanos()
7524        ));
7525        let project = scratch.join("repo");
7526        std::fs::create_dir_all(project.join(".git")).unwrap();
7527        // Auto-memory is shared across a repo's worktrees, so a nested
7528        // working directory must resolve to the repo's own project dir.
7529        let worktree = project.join("crates/harness");
7530        std::fs::create_dir_all(&worktree).unwrap();
7531        let slug: String = project
7532            .to_string_lossy()
7533            .chars()
7534            .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
7535            .collect();
7536        let projects = scratch.join("claude/projects");
7537        let memory = projects.join(&slug).join("memory");
7538        std::fs::create_dir_all(&memory).unwrap();
7539        std::fs::write(
7540            memory.join("MEMORY.md"),
7541            "# index\n- [build box](build-box.md) — the pinned harnesses\n",
7542        )
7543        .unwrap();
7544        std::fs::write(
7545            memory.join("build-box.md"),
7546            "hermes 0.21.0 and openclaw 2026.7.1-2 are the pins\n",
7547        )
7548        .unwrap();
7549
7550        let mut homes = memory_homes();
7551        homes["claude_code"] = json!(projects);
7552        let rows = memory_documents(json!({
7553            "harness": "claude-code",
7554            "cwd": worktree,
7555            "homes": homes,
7556        }));
7557        let index = find_document(&rows, &slug, "MEMORY.md");
7558        assert_eq!(index["harness"], "claude-code");
7559        assert_eq!(index["scope"], "project");
7560        let topic = find_document(&rows, &slug, "build-box.md");
7561        assert!(topic["preview"]
7562            .as_array()
7563            .unwrap()
7564            .iter()
7565            .any(|line| line.as_str().unwrap().contains("2026.7.1-2")));
7566
7567        let hits = memory_matches(json!({
7568            "harness": "claude-code",
7569            "query": "pinned harnesses",
7570            "cwd": worktree,
7571            "homes": homes,
7572        }));
7573        assert_eq!(hits.len(), 1, "{hits:#?}");
7574        assert_eq!(hits[0]["name"], "MEMORY.md");
7575        assert_eq!(hits[0]["line"], 2);
7576
7577        let _ = std::fs::remove_dir_all(&scratch);
7578    }
7579
7580    /// A config-less OpenClaw install declares no default agent, but
7581    /// memory-core still resolves ONE agent to the default `workspace`
7582    /// directory — the same `main`-then-first convention the profile rows
7583    /// use. Measured against `openclaw memory status` on the pinned CLI
7584    /// (`docs/interop/research/orch12-memory-receipt-2026-09-03.json`).
7585    #[test]
7586    fn memory_show_resolves_the_default_workspace_without_an_openclaw_config() {
7587        let state = std::env::temp_dir().join(format!(
7588            "supercode-orch12-oc-{}-{}",
7589            std::process::id(),
7590            std::time::SystemTime::now()
7591                .duration_since(std::time::UNIX_EPOCH)
7592                .unwrap()
7593                .as_nanos()
7594        ));
7595        // No `openclaw.json`: only the agent home the gateway creates.
7596        std::fs::create_dir_all(state.join("agents/main/agent")).unwrap();
7597        std::fs::create_dir_all(state.join("workspace")).unwrap();
7598        std::fs::write(
7599            state.join("workspace/MEMORY.md"),
7600            "the gateway websocket needs credentials\n",
7601        )
7602        .unwrap();
7603
7604        let mut homes = memory_homes();
7605        homes["openclaw"] = json!(state);
7606        let rows = memory_documents(json!({"harness": "openclaw", "homes": homes}));
7607        assert_eq!(rows.len(), 1, "{rows:#?}");
7608        let row = find_document(&rows, "main", "MEMORY.md");
7609        assert_eq!(row["scope"], "agent");
7610        assert!(row["path"]
7611            .as_str()
7612            .unwrap()
7613            .ends_with("workspace/MEMORY.md"));
7614
7615        let _ = std::fs::remove_dir_all(&state);
7616    }
7617
7618    /// Search is a plain scan over the same documents: a hit carries the
7619    /// path, line and excerpt; a miss is an empty list, not an error.
7620    #[test]
7621    fn memory_search_reports_hits_by_line_and_misses_as_empty() {
7622        let hit = memory_matches(json!({
7623            "harness": "hermes",
7624            "query": "NEOVIM",
7625            "homes": memory_homes(),
7626        }));
7627        assert_eq!(hit.len(), 1, "{hit:#?}");
7628        assert_eq!(hit[0]["harness"], "hermes");
7629        assert_eq!(hit[0]["name"], "USER.md");
7630        assert_eq!(hit[0]["scope"], "user");
7631        assert_eq!(hit[0]["line"], 5);
7632        assert!(hit[0]["excerpt"].as_str().unwrap().contains("neovim"));
7633
7634        // A regular expression reaches the same lines.
7635        let regex = memory_matches(json!({
7636            "harness": "hermes",
7637            "query": "neo(vim|vi)",
7638            "regex": true,
7639            "homes": memory_homes(),
7640        }));
7641        assert_eq!(regex.len(), 1, "{regex:#?}");
7642
7643        let miss = memory_matches(json!({
7644            "harness": "hermes",
7645            "query": "no-memory-line-says-this",
7646            "homes": memory_homes(),
7647        }));
7648        assert!(miss.is_empty(), "{miss:#?}");
7649    }
7650
7651    /// The uniform-verb contract: a harness with no memory store at the pin
7652    /// is refused by name, and `session` only selects a Claude Code project.
7653    #[test]
7654    fn memory_refuses_harnesses_without_a_store_and_misplaced_session_scoping() {
7655        for method in ["harness.v1.memory.show", "harness.v1.memory.search"] {
7656            let response = HarnessSessionService::new().handle(request(
7657                1,
7658                method,
7659                json!({"harness": "codex", "query": "anything", "homes": memory_homes()}),
7660            ));
7661            assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7662            assert!(response["error"]["message"]
7663                .as_str()
7664                .unwrap()
7665                .contains("codex"));
7666        }
7667
7668        let response = HarnessSessionService::new().handle(request(
7669            1,
7670            "harness.v1.memory.show",
7671            json!({"harness": "hermes", "session": "abc", "homes": memory_homes()}),
7672        ));
7673        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
7674
7675        // `harness` is not optional: memory documents are the user's prose.
7676        let response = HarnessSessionService::new().handle(request(
7677            1,
7678            "harness.v1.memory.show",
7679            json!({"homes": memory_homes()}),
7680        ));
7681        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
7682    }
7683
7684    /// Both methods are advertised, and their SDK operations resolve them.
7685    #[test]
7686    fn memory_methods_are_advertised_and_map_to_sdk_operations() {
7687        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.show"));
7688        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.memory.search"));
7689        assert_eq!(
7690            SdkOperation::from_method("harness.v1.memory.show"),
7691            Some(SdkOperation::MemoryShow)
7692        );
7693        assert_eq!(
7694            SdkOperation::from_method("harness.v1.memory.search"),
7695            Some(SdkOperation::MemorySearch)
7696        );
7697    }
7698
7699    // ---- ORCH-9: `harness.v1.approvals.list` -----------------------------
7700
7701    /// A runtime that raises one protocol request and then goes quiet, so a
7702    /// single poll delivers the request without closing the connection.
7703    struct RequestingRuntime {
7704        handle: RuntimeHandle,
7705        events: std::collections::VecDeque<HarnessEvent>,
7706        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7707    }
7708
7709    #[async_trait]
7710    impl RuntimeConnection for RequestingRuntime {
7711        fn handle(&self) -> &RuntimeHandle {
7712            &self.handle
7713        }
7714
7715        async fn send_input(&mut self, _input: RuntimeInput) -> crate::Result<Option<String>> {
7716            unreachable!("this runtime only raises requests")
7717        }
7718
7719        async fn next_event(&mut self) -> crate::Result<Option<HarnessEvent>> {
7720            match self.events.pop_front() {
7721                Some(event) => Ok(Some(event)),
7722                // Quiet, not closed: `poll_sdk_events` times out and leaves
7723                // the connection open, the way a runtime blocked on a
7724                // permission request behaves.
7725                None => std::future::pending().await,
7726            }
7727        }
7728
7729        async fn interrupt(&mut self) -> crate::Result<()> {
7730            Ok(())
7731        }
7732
7733        async fn respond(&mut self, request_id: Value, response: Value) -> crate::Result<()> {
7734            // Both halves are recorded: ORCH-20 has to prove not just that the
7735            // right request was answered but that the door received its own
7736            // reply envelope.
7737            self.answered
7738                .lock()
7739                .unwrap_or_else(std::sync::PoisonError::into_inner)
7740                .push(json!({"request_id": request_id, "response": response}));
7741            Ok(())
7742        }
7743
7744        async fn close(&mut self) -> crate::Result<()> {
7745            Ok(())
7746        }
7747    }
7748
7749    fn requesting_runtime(
7750        harness: &str,
7751        events: Vec<HarnessEvent>,
7752        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7753    ) -> Box<dyn RuntimeConnection> {
7754        requesting_runtime_named(harness, "hermes-live-session", events, answered)
7755    }
7756
7757    fn requesting_runtime_named(
7758        harness: &str,
7759        runtime_id: &str,
7760        events: Vec<HarnessEvent>,
7761        answered: std::sync::Arc<std::sync::Mutex<Vec<Value>>>,
7762    ) -> Box<dyn RuntimeConnection> {
7763        Box::new(RequestingRuntime {
7764            handle: RuntimeHandle {
7765                harness: HarnessId::from(harness),
7766                runtime_id: runtime_id.into(),
7767                endpoint: RuntimeEndpoint::LocalProcess {
7768                    pid: None,
7769                    command: vec!["hermes-acp".into()],
7770                    protocol: "acp".into(),
7771                },
7772            },
7773            events: events.into(),
7774            answered,
7775        })
7776    }
7777
7778    fn permission_event(id: u64, title: &str) -> HarnessEvent {
7779        HarnessEvent {
7780            sequence: None,
7781            kind: "session/request_permission".into(),
7782            payload: json!({
7783                "jsonrpc": "2.0",
7784                "id": id,
7785                "method": "session/request_permission",
7786                "params": {
7787                    "sessionId": "hermes-live-session",
7788                    "toolCall": {"toolCallId": "call-1", "title": title, "kind": "execute"},
7789                    "options": [
7790                        {"optionId": "allow_once", "name": "Allow once", "kind": "allow_once"},
7791                        {"optionId": "allow_for_session", "name": "Allow for session", "kind": "allow_always"},
7792                        {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
7793                    ],
7794                },
7795            }),
7796        }
7797    }
7798
7799    fn approvals(service: &mut HarnessSessionService, params: Value) -> Value {
7800        let response = service.handle(request(1, "harness.v1.approvals.list", params));
7801        assert!(response.get("error").is_none(), "{response:#}");
7802        response["result"].clone()
7803    }
7804
7805    /// ORC-2 dev/01: the same uniform loop over the CLAUDE CODE door. The
7806    /// `can_use_tool` control request the CLI raises to its registered
7807    /// permission handler lists as one pending row, `approvals.resolve <id>
7808    /// allow_once` sends the `{behavior}` result the CLI accepts through
7809    /// `runtimes.respond`, and the row is gone. The frame is the one claude
7810    /// 2.1.258 wrote, transcribed from
7811    /// `docs/interop/research/orc2-claude-respond-receipt-2026-09-04.json`.
7812    #[tokio::test]
7813    async fn a_claude_code_permission_request_lists_and_resolves_on_the_uniform_door() {
7814        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7815        let mut service = HarnessSessionService::new();
7816        service.runtimes.insert(
7817            "runtime-cc".into(),
7818            requesting_runtime_named(
7819                HarnessId::CLAUDE_CODE,
7820                "claude-live-session",
7821                vec![HarnessEvent {
7822                    sequence: None,
7823                    kind: "control_request".into(),
7824                    payload: json!({
7825                        "type": "control_request",
7826                        "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7827                        "request": {
7828                            "subtype": "can_use_tool",
7829                            "tool_name": "Bash",
7830                            "display_name": "Bash",
7831                            "input": {"command": "touch probe-artifact.txt"},
7832                            "tool_use_id": "toolu_mock_1",
7833                        },
7834                    }),
7835                }],
7836                answered.clone(),
7837            ),
7838        );
7839
7840        let notifications = service.poll_runtimes().await;
7841        assert_eq!(notifications.len(), 1, "{notifications:#?}");
7842
7843        let rows = approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}));
7844        assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7845        let row = &rows[0];
7846        assert_eq!(row["id"], "runtime-cc/053f8a2d-3445-4011-a259-4261b31c7326");
7847        assert_eq!(row["harness"], HarnessId::CLAUDE_CODE);
7848        assert_eq!(row["status"], "pending");
7849        assert_eq!(row["subject"], "Bash touch probe-artifact.txt");
7850        assert_eq!(row["runtime_id"], "claude-live-session");
7851        assert_eq!(
7852            row["options"]
7853                .as_array()
7854                .unwrap()
7855                .iter()
7856                .map(|option| option["id"].as_str().unwrap())
7857                .collect::<Vec<_>>(),
7858            vec!["allow", "deny"],
7859        );
7860
7861        let response = resolve(
7862            &mut service,
7863            json!({"id": row["id"], "decision": "allow_once"}),
7864        )
7865        .await;
7866        assert!(response.get("error").is_none(), "{response:#}");
7867        assert_eq!(response["result"]["option_id"], "allow");
7868        assert_eq!(
7869            answered
7870                .lock()
7871                .unwrap_or_else(std::sync::PoisonError::into_inner)
7872                .as_slice(),
7873            &[json!({
7874                "request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
7875                "response": {"behavior": "allow"},
7876            })],
7877        );
7878        assert_eq!(
7879            approvals(&mut service, json!({"harness": HarnessId::CLAUDE_CODE}))
7880                .as_array()
7881                .map(Vec::len),
7882            Some(0),
7883        );
7884    }
7885
7886    /// dev/01: a live ACP permission request raised on a driven runtime is
7887    /// listable while the turn is blocked on it, and stops being listable
7888    /// the moment `runtimes.respond` answers it.
7889    #[tokio::test]
7890    async fn a_live_permission_request_lists_until_it_is_answered() {
7891        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
7892        let mut service = HarnessSessionService::new();
7893        service.runtimes.insert(
7894            "runtime-1".into(),
7895            requesting_runtime(
7896                HarnessId::HERMES,
7897                vec![permission_event(7, "rm -rf build")],
7898                answered.clone(),
7899            ),
7900        );
7901
7902        let notifications = service.poll_runtimes().await;
7903        assert_eq!(notifications.len(), 1, "{notifications:#?}");
7904
7905        let rows = approvals(&mut service, json!({}));
7906        assert_eq!(rows.as_array().map(Vec::len), Some(1), "{rows:#}");
7907        let row = &rows[0];
7908        assert_eq!(row["id"], "runtime-1/7");
7909        assert_eq!(row["harness"], HarnessId::HERMES);
7910        assert_eq!(row["kind"], "live");
7911        assert_eq!(row["status"], "pending");
7912        assert_eq!(row["subject"], "rm -rf build");
7913        assert_eq!(row["session_id"], "hermes-live-session");
7914        assert_eq!(row["runtime_id"], "hermes-live-session");
7915        assert!(row["requested_at_ms"].as_i64().is_some(), "{row:#}");
7916        assert!(
7917            row["age_ms"].as_i64().is_some_and(|age| age >= 0),
7918            "{row:#}"
7919        );
7920        assert_eq!(
7921            row["options"]
7922                .as_array()
7923                .unwrap()
7924                .iter()
7925                .map(|option| option["id"].as_str().unwrap())
7926                .collect::<Vec<_>>(),
7927            vec!["allow_once", "allow_for_session", "deny"],
7928        );
7929
7930        // The filters select against the same rows.
7931        assert_eq!(
7932            approvals(&mut service, json!({"harness": HarnessId::HERMES}))
7933                .as_array()
7934                .map(Vec::len),
7935            Some(1),
7936        );
7937        assert_eq!(
7938            approvals(&mut service, json!({"session": "some-other-session"}))
7939                .as_array()
7940                .map(Vec::len),
7941            Some(0),
7942        );
7943
7944        let response = service
7945            .handle_async(request(
7946                2,
7947                "harness.v1.runtimes.respond",
7948                json!({
7949                    "connection": "runtime-1",
7950                    "request_id": 7,
7951                    "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7952                }),
7953            ))
7954            .await;
7955        assert!(response.get("error").is_none(), "{response:#}");
7956        assert_eq!(
7957            answered
7958                .lock()
7959                .unwrap_or_else(std::sync::PoisonError::into_inner)
7960                .as_slice(),
7961            &[json!({
7962                "request_id": 7,
7963                "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
7964            })],
7965        );
7966
7967        let rows = approvals(&mut service, json!({}));
7968        assert_eq!(rows.as_array().map(Vec::len), Some(0), "{rows:#}");
7969    }
7970
7971    /// dev/01: supercode's own queued subagent approvals list through the
7972    /// same door, carrying the outcome the record holds.
7973    #[test]
7974    fn queued_subagent_approvals_list_through_the_same_door() {
7975        let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
7976            crate::subagents::QueuedApproval {
7977                child_agent_id: "child-7".into(),
7978                tool: "shell".into(),
7979                subject: Some("cargo publish --dry-run".into()),
7980                queued_at_ms: 1,
7981                outcome: None,
7982            },
7983            crate::subagents::QueuedApproval {
7984                child_agent_id: "child-8".into(),
7985                tool: "write_file".into(),
7986                subject: None,
7987                queued_at_ms: 2,
7988                outcome: Some(crate::subagents::QueuedApprovalOutcome::Denied),
7989            },
7990        ]));
7991        let mut service = HarnessSessionService::new();
7992        service.observe_subagent_approvals(queue);
7993
7994        let rows = approvals(&mut service, json!({}));
7995        assert_eq!(rows.as_array().map(Vec::len), Some(2), "{rows:#}");
7996        assert_eq!(rows[0]["id"], "supercode/subagent/child-7/1/0");
7997        assert_eq!(rows[0]["harness"], HarnessId::SUPERCODE);
7998        assert_eq!(rows[0]["status"], "pending");
7999        assert_eq!(rows[0]["subject"], "shell cargo publish --dry-run");
8000        assert_eq!(rows[1]["status"], "denied");
8001        assert!(rows[1]["options"].as_array().unwrap().is_empty());
8002
8003        // `--session` addresses a subagent row by its child agent id.
8004        let only = approvals(&mut service, json!({"session": "child-8"}));
8005        assert_eq!(only.as_array().map(Vec::len), Some(1), "{only:#}");
8006        assert_eq!(only[0]["id"], "supercode/subagent/child-8/2/1");
8007    }
8008
8009    /// The uniform-verb contract: an id whose runtime door cannot carry a
8010    /// protocol request is refused BY NAME rather than answered with an empty
8011    /// list. Since ORC-2 gave Claude Code a permission-response primitive
8012    /// every registered harness can carry one, so the refusal is exercised on
8013    /// an unknown id — and the registered ids are asserted to be accepted.
8014    #[test]
8015    fn approvals_list_refuses_a_harness_that_cannot_carry_a_request() {
8016        let response = HarnessSessionService::new().handle(request(
8017            1,
8018            "harness.v1.approvals.list",
8019            json!({"harness": "not-a-harness"}),
8020        ));
8021        assert_eq!(response["error"]["code"], json!(-32020), "{response:#}");
8022        assert!(response["error"]["message"]
8023            .as_str()
8024            .unwrap()
8025            .contains("not-a-harness"));
8026        for harness in [HarnessId::CLAUDE_CODE, HarnessId::CODEX] {
8027            let response = HarnessSessionService::new().handle(request(
8028                1,
8029                "harness.v1.approvals.list",
8030                json!({"harness": harness}),
8031            ));
8032            assert!(response.get("error").is_none(), "{harness}: {response:#}");
8033        }
8034    }
8035
8036    /// The method is advertised, its SDK operation resolves it, and the
8037    /// registry reports the concept as observed for every harness whose
8038    /// runtime door can carry a request.
8039    #[test]
8040    fn approvals_list_is_an_advertised_method_and_an_observed_tier() {
8041        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.list"));
8042        assert_eq!(
8043            SdkOperation::from_method("harness.v1.approvals.list"),
8044            Some(SdkOperation::ApprovalsList)
8045        );
8046        let registry = harness_support_registry();
8047        for id in [
8048            HarnessId::HERMES,
8049            HarnessId::OPENCLAW,
8050            HarnessId::CODEX,
8051            // ORC-2: the Claude Code door answers `can_use_tool`, so its
8052            // pending_request concept joins the other driven doors.
8053            HarnessId::CLAUDE_CODE,
8054        ] {
8055            let concept = registry
8056                .harnesses
8057                .iter()
8058                .find(|harness| harness.id.as_str() == id)
8059                .unwrap()
8060                .orchestration
8061                .concepts
8062                .iter()
8063                .find(|concept| concept.concept == "pending_request")
8064                .unwrap();
8065            assert_eq!(concept.observed, crate::ImplementationKind::BuiltIn, "{id}");
8066            assert!(concept
8067                .methods
8068                .iter()
8069                .any(|method| method == "harness.v1.approvals.list"));
8070        }
8071    }
8072
8073    // ---- ORCH-20: `harness.v1.approvals.resolve` -------------------------
8074
8075    async fn resolve(service: &mut HarnessSessionService, params: Value) -> Value {
8076        service
8077            .handle_async(request(3, "harness.v1.approvals.resolve", params))
8078            .await
8079    }
8080
8081    /// dev/01: the whole loop on a driven runtime — list one pending row,
8082    /// answer it by ROW ID with one uniform decision, and see it gone. The
8083    /// door receives its own ACP envelope carrying the option it enumerated.
8084    #[tokio::test]
8085    async fn a_listed_row_resolves_with_one_uniform_decision_and_then_is_gone() {
8086        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8087        let mut service = HarnessSessionService::new();
8088        service.runtimes.insert(
8089            "runtime-1".into(),
8090            requesting_runtime(
8091                HarnessId::HERMES,
8092                vec![permission_event(7, "rm -rf build")],
8093                answered.clone(),
8094            ),
8095        );
8096        service.poll_runtimes().await;
8097
8098        let rows = approvals(&mut service, json!({}));
8099        assert_eq!(rows[0]["id"], "runtime-1/7");
8100
8101        let response = resolve(
8102            &mut service,
8103            json!({"id": "runtime-1/7", "decision": "allow_once"}),
8104        )
8105        .await;
8106        assert!(response.get("error").is_none(), "{response:#}");
8107        assert_eq!(
8108            response["result"],
8109            json!({
8110                "id": "runtime-1/7",
8111                "decision": "allow_once",
8112                "option_id": "allow_once",
8113                "resolved": true,
8114            }),
8115        );
8116        // The harness's own door was called with its own envelope.
8117        assert_eq!(
8118            answered
8119                .lock()
8120                .unwrap_or_else(std::sync::PoisonError::into_inner)
8121                .as_slice(),
8122            &[json!({
8123                "request_id": 7,
8124                "response": {"outcome": {"outcome": "selected", "optionId": "allow_once"}},
8125            })],
8126        );
8127        // And the row is gone, the same way `runtimes.respond` drops it.
8128        assert_eq!(
8129            approvals(&mut service, json!({})).as_array().map(Vec::len),
8130            Some(0),
8131        );
8132        // Answering it twice is an honest miss, not a silent success.
8133        let response = resolve(
8134            &mut service,
8135            json!({"id": "runtime-1/7", "decision": "allow_once"}),
8136        )
8137        .await;
8138        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8139    }
8140
8141    /// dev/01: deny travels the same path and picks the option the request
8142    /// itself classified as a refusal.
8143    #[tokio::test]
8144    async fn deny_selects_the_requests_own_reject_option() {
8145        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8146        let mut service = HarnessSessionService::new();
8147        service.runtimes.insert(
8148            "runtime-1".into(),
8149            requesting_runtime(
8150                HarnessId::HERMES,
8151                vec![permission_event(11, "git push --force")],
8152                answered.clone(),
8153            ),
8154        );
8155        service.poll_runtimes().await;
8156
8157        let response = resolve(
8158            &mut service,
8159            json!({"id": "runtime-1/11", "decision": "deny"}),
8160        )
8161        .await;
8162        assert!(response.get("error").is_none(), "{response:#}");
8163        // `deny` is the optionId whose ACP `kind` is `reject_once`.
8164        assert_eq!(response["result"]["option_id"], "deny");
8165        assert_eq!(
8166            answered
8167                .lock()
8168                .unwrap_or_else(std::sync::PoisonError::into_inner)[0]["response"],
8169            json!({"outcome": {"outcome": "selected", "optionId": "deny"}}),
8170        );
8171        assert_eq!(
8172            approvals(&mut service, json!({})).as_array().map(Vec::len),
8173            Some(0),
8174        );
8175    }
8176
8177    /// dev/01: a decision this request does not offer is refused by name,
8178    /// listing the ones it does — never silently downgraded to a neighbour.
8179    #[tokio::test]
8180    async fn a_decision_the_request_does_not_offer_is_refused_with_the_offered_ones() {
8181        let answered = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
8182        let mut service = HarnessSessionService::new();
8183        let mut event = permission_event(3, "rm -rf build");
8184        // A request offering only allow-once and deny, as hermes 0.21.0's
8185        // edit-approval layer raises one.
8186        event.payload["params"]["options"] = json!([
8187            {"optionId": "allow_once", "name": "Allow edit", "kind": "allow_once"},
8188            {"optionId": "deny", "name": "Deny", "kind": "reject_once"},
8189        ]);
8190        service.runtimes.insert(
8191            "runtime-1".into(),
8192            requesting_runtime(HarnessId::HERMES, vec![event], answered.clone()),
8193        );
8194        service.poll_runtimes().await;
8195
8196        let response = resolve(
8197            &mut service,
8198            json!({"id": "runtime-1/3", "decision": "allow_always"}),
8199        )
8200        .await;
8201        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8202        let message = response["error"]["message"].as_str().unwrap();
8203        assert!(message.contains("allow_always"), "{message}");
8204        assert!(message.contains("allow_once, deny"), "{message}");
8205        // Nothing was sent, and the request is still waiting for an answer.
8206        assert!(answered
8207            .lock()
8208            .unwrap_or_else(std::sync::PoisonError::into_inner)
8209            .is_empty());
8210        assert_eq!(
8211            approvals(&mut service, json!({})).as_array().map(Vec::len),
8212            Some(1),
8213        );
8214    }
8215
8216    /// dev/01: supercode's own queued subagent row is addressable but not
8217    /// answerable through this door — it is the parent's audit copy of a
8218    /// request its own handler answers. Refused by name, never a no-op.
8219    #[tokio::test]
8220    async fn a_queued_subagent_row_is_refused_by_name_rather_than_silently_answered() {
8221        let queue = std::sync::Arc::new(std::sync::Mutex::new(vec![
8222            crate::subagents::QueuedApproval {
8223                child_agent_id: "child-7".into(),
8224                tool: "shell".into(),
8225                subject: Some("cargo publish --dry-run".into()),
8226                queued_at_ms: 1,
8227                outcome: None,
8228            },
8229        ]));
8230        let mut service = HarnessSessionService::new();
8231        service.observe_subagent_approvals(queue.clone());
8232        let row = approvals(&mut service, json!({}))[0]["id"]
8233            .as_str()
8234            .unwrap()
8235            .to_string();
8236        assert_eq!(row, "supercode/subagent/child-7/1/0");
8237
8238        let response = resolve(&mut service, json!({"id": row, "decision": "allow_once"})).await;
8239        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8240        let message = response["error"]["message"].as_str().unwrap();
8241        assert!(message.contains("queued subagent record"), "{message}");
8242        assert!(message.contains("request"), "{message}");
8243        // The audit record is untouched: nothing pretended to answer it.
8244        assert!(queue
8245            .lock()
8246            .unwrap_or_else(std::sync::PoisonError::into_inner)[0]
8247            .outcome
8248            .is_none());
8249    }
8250
8251    /// An id nobody is holding, and a call that names no decision at all,
8252    /// both fail with a message that says why.
8253    #[tokio::test]
8254    async fn an_unknown_row_and_a_missing_decision_are_both_named() {
8255        let mut service = HarnessSessionService::new();
8256        let response = resolve(
8257            &mut service,
8258            json!({"id": "runtime-9/4", "decision": "deny"}),
8259        )
8260        .await;
8261        assert_eq!(response["error"]["code"], json!(-32602), "{response:#}");
8262        assert!(response["error"]["message"]
8263            .as_str()
8264            .unwrap()
8265            .contains("runtime-9/4"));
8266
8267        let response = resolve(&mut service, json!({"id": "runtime-9/4"})).await;
8268        let message = response["error"]["message"].as_str().unwrap();
8269        assert!(
8270            message.contains("allow_once | allow_always | deny"),
8271            "{message}"
8272        );
8273
8274        let response = resolve(
8275            &mut service,
8276            json!({"id": "runtime-9/4", "decision": "deny", "option_id": "deny"}),
8277        )
8278        .await;
8279        assert!(response["error"]["message"]
8280            .as_str()
8281            .unwrap()
8282            .contains("not both"));
8283    }
8284
8285    /// The method is advertised, its SDK operation resolves it, and every
8286    /// harness whose runtime door can carry a request reports it on the
8287    /// CONTROLLED tier beside `runtimes.respond`.
8288    #[test]
8289    fn approvals_resolve_is_an_advertised_method_and_a_controlled_tier() {
8290        assert!(HARNESS_SERVICE_METHODS.contains(&"harness.v1.approvals.resolve"));
8291        assert_eq!(
8292            SdkOperation::from_method("harness.v1.approvals.resolve"),
8293            Some(SdkOperation::ApprovalsResolve)
8294        );
8295        assert_eq!(
8296            SdkOperation::ApprovalsResolve.action_name(),
8297            "approvals_resolve"
8298        );
8299        let registry = harness_support_registry();
8300        for id in [
8301            HarnessId::HERMES,
8302            HarnessId::OPENCLAW,
8303            HarnessId::CODEX,
8304            // ORC-2: the Claude Code door answers `can_use_tool`, so its
8305            // pending_request concept joins the other driven doors.
8306            HarnessId::CLAUDE_CODE,
8307        ] {
8308            let concept = registry
8309                .harnesses
8310                .iter()
8311                .find(|harness| harness.id.as_str() == id)
8312                .unwrap()
8313                .orchestration
8314                .concepts
8315                .iter()
8316                .find(|concept| concept.concept == "pending_request")
8317                .unwrap();
8318            assert_eq!(
8319                concept.controlled,
8320                crate::ImplementationKind::BuiltIn,
8321                "{id}"
8322            );
8323            assert!(
8324                concept
8325                    .methods
8326                    .iter()
8327                    .any(|method| method == "harness.v1.approvals.resolve"),
8328                "{id}"
8329            );
8330        }
8331    }
8332
8333    #[test]
8334    fn capabilities_are_explicit_and_versioned() {
8335        let mut service = HarnessSessionService::new();
8336        let response = service.handle(request(1, "harness.v1.capabilities", json!({})));
8337        assert_eq!(response["result"]["version"], HARNESS_SERVICE_VERSION);
8338        assert_eq!(
8339            response["result"]["sdk"]["schema_version"],
8340            crate::SDK_SCHEMA_VERSION
8341        );
8342        assert_eq!(
8343            response["result"]["sdk"]["operations"]
8344                .as_array()
8345                .unwrap()
8346                .len(),
8347            SdkOperation::ALL.len()
8348        );
8349        assert_eq!(
8350            response["result"]["harnesses"].as_array().unwrap().len(),
8351            11
8352        );
8353        assert!(response["result"]["harnesses"]
8354            .as_array()
8355            .unwrap()
8356            .iter()
8357            .any(|harness| harness == HarnessId::GROK));
8358        assert!(response["result"]["harnesses"]
8359            .as_array()
8360            .unwrap()
8361            .iter()
8362            .any(|harness| harness == HarnessId::GOOSE));
8363    }
8364
8365    #[test]
8366    fn handshake_health_uses_protocol_liveness_not_stderr_severity() {
8367        let noisy_stderr = crate::HarnessEvent {
8368            sequence: None,
8369            kind: "transport_stderr".into(),
8370            payload: json!({"line": "ERROR optional worker AuthorizationRequired"}),
8371        };
8372        assert_eq!(handshake_event_failure(&noisy_stderr), None);
8373
8374        let closed = crate::HarnessEvent {
8375            sequence: None,
8376            kind: "transport_closed".into(),
8377            payload: json!({}),
8378        };
8379        assert!(handshake_event_failure(&closed).is_some());
8380    }
8381
8382    #[tokio::test]
8383    async fn runtime_eof_is_notified_and_removed_for_raw_and_explicit_close() {
8384        let mut service = HarnessSessionService::new();
8385        service
8386            .runtimes
8387            .insert("raw-eof".into(), ending_runtime(None));
8388        service.runtimes.insert(
8389            "explicit-close".into(),
8390            ending_runtime(Some(HarnessEvent {
8391                sequence: None,
8392                kind: "transport_closed".into(),
8393                payload: json!({"message": "native transport exited"}),
8394            })),
8395        );
8396
8397        let notifications = service.poll_runtimes().await;
8398
8399        assert_eq!(notifications.len(), 2);
8400        assert!(notifications
8401            .iter()
8402            .all(|notification| { notification["params"]["event"]["kind"] == "transport_closed" }));
8403        assert!(notifications.iter().all(|notification| {
8404            notification["params"]["session_id"] == "ending-session"
8405                && notification["params"]["connection"].is_string()
8406        }));
8407        let mut sequences = notifications
8408            .iter()
8409            .filter_map(|notification| notification["params"]["sequence"].as_u64())
8410            .collect::<Vec<_>>();
8411        sequences.sort_unstable();
8412        assert_eq!(sequences, vec![1, 2]);
8413        assert!(service.runtimes.is_empty());
8414    }
8415
8416    #[test]
8417    fn support_report_and_grok_default_binding_share_the_registry() {
8418        let mut service = HarnessSessionService::new();
8419        let response = service.handle(request(1, "harness.v1.support.report", json!({})));
8420        assert_eq!(response["result"]["schema"], crate::SUPPORT_REGISTRY_SCHEMA);
8421        let params = RuntimeBackendParams {
8422            harness: HarnessId::from(HarnessId::GROK),
8423            protocol: None,
8424            launch: None,
8425            base_url: None,
8426            policy: RuntimePolicy::Default,
8427        };
8428        let backend = match runtime_backend(&params) {
8429            Ok(backend) => backend,
8430            Err(_) => panic!("Grok should bind through its registered ACP launch"),
8431        };
8432        assert_eq!(backend.harness().as_str(), HarnessId::GROK);
8433        assert!(backend.capabilities().start_session);
8434        let registered = harness_support_registry()
8435            .harnesses
8436            .into_iter()
8437            .find(|harness| harness.id.as_str() == HarnessId::GROK)
8438            .and_then(|harness| harness.runtime.default_launch)
8439            .unwrap();
8440        assert!(!registered
8441            .arguments
8442            .iter()
8443            .any(|argument| argument == "--always-approve"));
8444        assert!(runtime_launch(&params).is_none());
8445
8446        let yolo = RuntimeBackendParams {
8447            policy: RuntimePolicy::Yolo,
8448            ..params
8449        };
8450        assert!(runtime_launch(&yolo)
8451            .unwrap()
8452            .arguments
8453            .iter()
8454            .any(|argument| argument == "--always-approve"));
8455
8456        let mismatched_protocol = RuntimeBackendParams {
8457            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8458            protocol: Some("acp".into()),
8459            launch: None,
8460            base_url: None,
8461            policy: RuntimePolicy::Default,
8462        };
8463        assert!(runtime_backend(&mismatched_protocol).is_err());
8464    }
8465
8466    #[test]
8467    fn load_follow_and_unfollow_share_the_same_locator() {
8468        let mut service = HarnessSessionService::new();
8469        let locator = pi_locator();
8470        let loaded = service.handle(request(
8471            1,
8472            "harness.v1.sessions.load",
8473            json!({"locator": locator}),
8474        ));
8475        assert_eq!(
8476            loaded["result"]["session"]["session_id"],
8477            locator.session_id
8478        );
8479
8480        let followed = service.handle(request(
8481            2,
8482            "harness.v1.sessions.follow",
8483            json!({"locator": locator}),
8484        ));
8485        assert_eq!(followed["result"]["subscription"], "sub-1");
8486        assert_eq!(followed["result"]["initial"]["type"], "session_snapshot");
8487        assert!(service.poll().is_empty());
8488
8489        let unfollowed = service.handle(request(
8490            3,
8491            "harness.v1.sessions.unfollow",
8492            json!({"subscription": "sub-1"}),
8493        ));
8494        assert_eq!(unfollowed["result"]["removed"], true);
8495    }
8496
8497    #[test]
8498    fn bounded_read_view_excludes_subagents_and_keeps_only_the_tail() {
8499        let temp = std::env::temp_dir().join(format!(
8500            "supercode-bounded-view-{}-{}",
8501            std::process::id(),
8502            generated_session_id()
8503        ));
8504        let path = temp.join("parent.jsonl");
8505        let subagents = temp.join("parent/subagents");
8506        std::fs::create_dir_all(&subagents).unwrap();
8507        let long_last = "x".repeat(300);
8508        let parent_records = [
8509            json!({"type":"user","uuid":"u1","parentUuid":null,"message":{"role":"user","content":"first"}}),
8510            json!({"type":"assistant","uuid":"a1","parentUuid":"u1","message":{"role":"assistant","content":[{"type":"text","text":"middle"}]}}),
8511            json!({"type":"user","uuid":"u2","parentUuid":"a1","message":{"role":"user","content":long_last}}),
8512        ];
8513        std::fs::write(
8514            &path,
8515            format!(
8516                "{}\n",
8517                parent_records
8518                    .iter()
8519                    .map(Value::to_string)
8520                    .collect::<Vec<_>>()
8521                    .join("\n")
8522            ),
8523        )
8524        .unwrap();
8525        std::fs::write(
8526            subagents.join("agent-child.jsonl"),
8527            concat!(
8528                r#"{"type":"user","uuid":"cu","parentUuid":null,"agentId":"child","message":{"role":"user","content":"child work"}}"#,
8529                "\n",
8530            ),
8531        )
8532        .unwrap();
8533        let locator = SessionLocator {
8534            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8535            session_id: "parent".into(),
8536            storage: StorageLocator::File { path },
8537        };
8538        let mut service = HarnessSessionService::new();
8539
8540        let complete = service.handle(request(
8541            1,
8542            "harness.v1.sessions.load",
8543            json!({"locator": locator}),
8544        ));
8545        assert_eq!(
8546            complete["result"]["session"]["subagents"]
8547                .as_array()
8548                .unwrap()
8549                .len(),
8550            1
8551        );
8552
8553        let bounded = service.handle(request(
8554            2,
8555            "harness.v1.sessions.load",
8556            json!({
8557                "locator": locator,
8558                "view": {
8559                    "tail_messages": 1,
8560                    "max_message_chars": 256,
8561                    "include_subagents": false
8562                },
8563            }),
8564        ));
8565        let session = &bounded["result"]["session"];
8566        assert!(session["subagents"].as_array().unwrap().is_empty());
8567        assert_eq!(session["messages"].as_array().unwrap().len(), 1);
8568        assert_eq!(
8569            session["messages"][0]["content"],
8570            format!("{}\n…", "x".repeat(256))
8571        );
8572
8573        let followed = service.handle(request(
8574            3,
8575            "harness.v1.sessions.follow",
8576            json!({
8577                "locator": locator,
8578                "view": {
8579                    "tail_messages": 1,
8580                    "max_message_chars": 256,
8581                    "include_subagents": false
8582                },
8583            }),
8584        ));
8585        let initial = &followed["result"]["initial"]["session"];
8586        assert!(initial["subagents"].as_array().unwrap().is_empty());
8587        assert_eq!(initial["messages"].as_array().unwrap().len(), 1);
8588
8589        let _ = std::fs::remove_dir_all(&temp);
8590    }
8591
8592    #[test]
8593    fn forty_megabyte_display_load_is_bounded_and_prompt() {
8594        let temp = std::env::temp_dir().join(format!(
8595            "supercode-large-display-view-{}-{}",
8596            std::process::id(),
8597            generated_session_id()
8598        ));
8599        std::fs::create_dir_all(&temp).unwrap();
8600        let path = temp.join("rollout.jsonl");
8601        let mut file = std::io::BufWriter::new(std::fs::File::create(&path).unwrap());
8602        writeln!(
8603            file,
8604            r#"{{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{{"id":"large-display","cwd":"/tmp"}}}}"#
8605        )
8606        .unwrap();
8607        let padding = "x".repeat(80 * 1024);
8608        for index in 0..512 {
8609            let marker = if index == 0 {
8610                "OLDEST-SHOULD-NOT-LOAD"
8611            } else if index == 511 {
8612                "LATEST-MUST-LOAD"
8613            } else {
8614                "bulk"
8615            };
8616            writeln!(
8617                file,
8618                "{}",
8619                json!({
8620                    "timestamp": "2026-01-01T00:00:01Z",
8621                    "type": "response_item",
8622                    "payload": {
8623                        "type": "message",
8624                        "role": "assistant",
8625                        "content": [{"type": "output_text", "text": format!("{marker}:{padding}")}],
8626                    },
8627                })
8628            )
8629            .unwrap();
8630        }
8631        file.flush().unwrap();
8632        drop(file);
8633        assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8634
8635        let locator = SessionLocator {
8636            harness: HarnessId::from(HarnessId::CODEX),
8637            session_id: "large-display".into(),
8638            storage: StorageLocator::File { path },
8639        };
8640        let started = Instant::now();
8641        let response = HarnessSessionService::new().handle(request(
8642            1,
8643            "harness.v1.sessions.load",
8644            json!({
8645                "locator": locator,
8646                "view": {
8647                    "tail_messages": 500,
8648                    "max_message_chars": 1024,
8649                    "include_subagents": false,
8650                    "display_history": true,
8651                },
8652            }),
8653        ));
8654        let elapsed = started.elapsed();
8655        let wire = response.to_string();
8656        eprintln!(
8657            "bounded 40 MiB display load: {elapsed:?}, {} response bytes",
8658            wire.len()
8659        );
8660        assert!(response.get("error").is_none(), "{response:#}");
8661        assert!(wire.contains("LATEST-MUST-LOAD"));
8662        assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8663        assert!(
8664            wire.len() < 2 * 1024 * 1024,
8665            "bounded wire was {} bytes",
8666            wire.len()
8667        );
8668        assert!(
8669            elapsed.as_secs_f64() < 3.0,
8670            "bounded 40 MiB load took {elapsed:?}"
8671        );
8672
8673        // Timing-free: a store with no human turn widens its window to the 64 MiB ceiling
8674        // looking for anchors, so the bounded read shows on one with a human turn every eight
8675        // records: a short view stops well short of the first record and says so.
8676        let anchored = temp.join("anchored.jsonl");
8677        let mut file = std::io::BufWriter::new(std::fs::File::create(&anchored).unwrap());
8678        writeln!(
8679            file,
8680            r#"{{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{{"id":"large-display","cwd":"/tmp"}}}}"#
8681        )
8682        .unwrap();
8683        for index in 0..512 {
8684            let marker = if index == 0 {
8685                "OLDEST-SHOULD-NOT-LOAD"
8686            } else if index == 511 {
8687                "LATEST-MUST-LOAD"
8688            } else {
8689                "bulk"
8690            };
8691            let (role, kind) = if index % 8 == 0 {
8692                ("user", "input_text")
8693            } else {
8694                ("assistant", "output_text")
8695            };
8696            writeln!(
8697                file,
8698                "{}",
8699                json!({
8700                    "timestamp": "2026-01-01T00:00:01Z",
8701                    "type": "response_item",
8702                    "payload": {
8703                        "type": "message",
8704                        "role": role,
8705                        "content": [{"type": kind, "text": format!("{marker}:{padding}")}],
8706                    },
8707                })
8708            )
8709            .unwrap();
8710        }
8711        file.flush().unwrap();
8712        drop(file);
8713        let short = HarnessSessionService::new().handle(request(
8714            2,
8715            "harness.v1.sessions.load",
8716            json!({
8717                "locator": SessionLocator {
8718                    harness: HarnessId::from(HarnessId::CODEX),
8719                    session_id: "large-display".into(),
8720                    storage: StorageLocator::File { path: anchored },
8721                },
8722                "view": {
8723                    "tail_messages": 20,
8724                    "max_message_chars": 1024,
8725                    "include_subagents": false,
8726                    "display_history": true,
8727                },
8728            }),
8729        ));
8730        let records = short["result"]["session"]["raw_record_count"].as_u64();
8731        assert!(
8732            records.is_some_and(|records| records < 128),
8733            "{records:?} records read"
8734        );
8735        let short = short.to_string();
8736        assert!(short.contains("LATEST-MUST-LOAD"));
8737        assert!(short.contains("older native records remain outside this bounded display window"));
8738
8739        let _ = std::fs::remove_dir_all(&temp);
8740    }
8741
8742    #[test]
8743    fn forty_megabyte_goose_store_display_load_reads_only_the_tail() {
8744        let temp = std::env::temp_dir().join(format!(
8745            "supercode-large-goose-view-{}-{}",
8746            std::process::id(),
8747            generated_session_id()
8748        ));
8749        std::fs::create_dir_all(&temp).unwrap();
8750        let path = temp.join("sessions.db");
8751        let connection = rusqlite::Connection::open(&path).unwrap();
8752        connection
8753            .execute_batch(
8754                "CREATE TABLE sessions (
8755                    id TEXT PRIMARY KEY, name TEXT NOT NULL, working_dir TEXT NOT NULL,
8756                    created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
8757                    session_type TEXT NOT NULL, extension_data TEXT,
8758                    goose_mode TEXT NOT NULL, provider_name TEXT, model_config_json TEXT,
8759                    archived_at TEXT
8760                 );
8761                 CREATE TABLE messages (
8762                    id INTEGER PRIMARY KEY, session_id TEXT NOT NULL, message_id TEXT,
8763                    role TEXT NOT NULL, content_json TEXT NOT NULL,
8764                    created_timestamp INTEGER NOT NULL, metadata_json TEXT
8765                 );",
8766            )
8767            .unwrap();
8768        connection
8769            .execute(
8770                "INSERT INTO sessions VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, NULL)",
8771                rusqlite::params![
8772                    "goose-large",
8773                    "Large Goose session",
8774                    "/tmp",
8775                    "2026-01-01 00:00:00",
8776                    "2026-01-01 00:00:02",
8777                    "user",
8778                    "{}",
8779                    "auto",
8780                    "anthropic",
8781                    r#"{"model_name":"claude-sonnet"}"#,
8782                ],
8783            )
8784            .unwrap();
8785        let old_content = serde_json::to_string(&vec![json!({
8786            "type": "text",
8787            "text": format!("OLDEST-SHOULD-NOT-LOAD:{}", "x".repeat(40 * 1024 * 1024)),
8788        })])
8789        .unwrap();
8790        connection
8791            .execute(
8792                "INSERT INTO messages VALUES (1, ?1, 'old', 'user', ?2, 1, '{}')",
8793                rusqlite::params!["goose-large", old_content],
8794            )
8795            .unwrap();
8796        connection
8797            .execute(
8798                "INSERT INTO messages VALUES (2, ?1, 'new', 'assistant', ?2, 2, '{}')",
8799                rusqlite::params![
8800                    "goose-large",
8801                    r#"[{"type":"text","text":"LATEST-MUST-LOAD"}]"#
8802                ],
8803            )
8804            .unwrap();
8805        drop(connection);
8806        assert!(std::fs::metadata(&path).unwrap().len() >= 40 * 1024 * 1024);
8807
8808        let locator = SessionLocator {
8809            harness: HarnessId::from(HarnessId::GOOSE),
8810            session_id: "goose-large".into(),
8811            storage: StorageLocator::Sqlite {
8812                path,
8813                selector: "goose-large".into(),
8814            },
8815        };
8816        let started = Instant::now();
8817        let response = HarnessSessionService::new().handle(request(
8818            1,
8819            "harness.v1.sessions.load",
8820            json!({
8821                "locator": locator,
8822                "view": {
8823                    "tail_messages": 1,
8824                    "max_message_chars": 1024,
8825                    "include_subagents": false,
8826                    "display_history": true,
8827                },
8828            }),
8829        ));
8830        let elapsed = started.elapsed();
8831        let wire = response.to_string();
8832        eprintln!(
8833            "bounded 40 MiB Goose display load: {elapsed:?}, {} response bytes",
8834            wire.len()
8835        );
8836        assert!(response.get("error").is_none(), "{response:#}");
8837        assert!(wire.contains("LATEST-MUST-LOAD"));
8838        assert!(!wire.contains("OLDEST-SHOULD-NOT-LOAD"));
8839        assert!(
8840            wire.len() < 64 * 1024,
8841            "bounded wire was {} bytes",
8842            wire.len()
8843        );
8844        assert!(
8845            elapsed.as_secs_f64() < 1.0,
8846            "bounded Goose load took {elapsed:?}"
8847        );
8848
8849        let _ = std::fs::remove_dir_all(&temp);
8850    }
8851
8852    #[test]
8853    fn display_view_keeps_codex_assistant_history_across_compaction() {
8854        let temp = std::env::temp_dir().join(format!(
8855            "supercode-codex-display-view-{}-{}",
8856            std::process::id(),
8857            generated_session_id()
8858        ));
8859        std::fs::create_dir_all(&temp).unwrap();
8860        let path = temp.join("rollout.jsonl");
8861        std::fs::write(
8862            &path,
8863            concat!(
8864                r#"{"timestamp":"2026-01-01T00:00:00Z","type":"session_meta","payload":{"id":"codex-display","cwd":"/tmp"}}"#,
8865                "\n",
8866                r#"{"timestamp":"2026-01-01T00:00:01Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"old prompt"}]}}"#,
8867                "\n",
8868                r#"{"timestamp":"2026-01-01T00:00:02Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"old answer"}]}}"#,
8869                "\n",
8870                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"}]}}"#,
8871                "\n",
8872                r#"{"timestamp":"2026-01-01T00:00:04Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"new prompt"}]}}"#,
8873                "\n",
8874                r#"{"timestamp":"2026-01-01T00:00:05Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"new answer"}]}}"#,
8875                "\n",
8876            ),
8877        )
8878        .unwrap();
8879        let locator = SessionLocator {
8880            harness: HarnessId::from(HarnessId::CODEX),
8881            session_id: "codex-display".into(),
8882            storage: StorageLocator::File { path },
8883        };
8884        let mut service = HarnessSessionService::new();
8885
8886        let continuation = service.handle(request(
8887            1,
8888            "harness.v1.sessions.load",
8889            json!({"locator": locator}),
8890        ));
8891        let continuation_text = continuation["result"]["session"]["messages"].to_string();
8892        assert!(!continuation_text.contains("old answer"));
8893
8894        let display = service.handle(request(
8895            2,
8896            "harness.v1.sessions.load",
8897            json!({
8898                "locator": locator,
8899                "view": {
8900                    "tail_messages": 10,
8901                    "include_subagents": false,
8902                    "display_history": true,
8903                },
8904            }),
8905        ));
8906        let display_text = display["result"]["session"]["messages"].to_string();
8907        assert!(display_text.contains("old prompt"));
8908        assert!(display_text.contains("old answer"));
8909        assert!(display_text.contains("new prompt"));
8910        assert!(display_text.contains("new answer"));
8911
8912        let _ = std::fs::remove_dir_all(&temp);
8913    }
8914
8915    #[test]
8916    fn indexed_claude_windows_match_the_existing_wire_projection() {
8917        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
8918            .join("tests/fixtures/claude_code_session.jsonl");
8919        let locator = SessionLocator {
8920            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
8921            session_id: "fixture".into(),
8922            storage: StorageLocator::File { path },
8923        };
8924        let full = load_session(&locator).unwrap();
8925        for inline_media in [InlineMediaMode::Full, InlineMediaMode::Metadata] {
8926            for offset in [0, 1, full.messages.len(), usize::MAX] {
8927                for limit in [0, 1, 3, usize::MAX] {
8928                    let options = SessionLoadOptions {
8929                        include_subagents: Some(false),
8930                        inline_media,
8931                        message_offset: Some(offset),
8932                        message_limit: Some(limit),
8933                        ..Default::default()
8934                    };
8935                    let expected = projected_session_result(&full, &options);
8936                    assert_eq!(
8937                        indexed_claude_window(&locator, &options).unwrap().unwrap(),
8938                        expected
8939                    );
8940                }
8941            }
8942            for tail in [0, 1, 3, usize::MAX] {
8943                let options = SessionLoadOptions {
8944                    include_subagents: Some(false),
8945                    inline_media,
8946                    message_tail: Some(tail),
8947                    ..Default::default()
8948                };
8949                assert_eq!(
8950                    indexed_claude_window(&locator, &options).unwrap().unwrap(),
8951                    projected_session_result(&full, &options)
8952                );
8953            }
8954        }
8955    }
8956
8957    #[test]
8958    fn load_supports_bounded_windows_and_media_metadata() {
8959        let mut service = HarnessSessionService::new();
8960        let locator = pi_locator();
8961        let bounded = service.handle(request(
8962            1,
8963            "harness.v1.sessions.load",
8964            json!({
8965                "locator": locator,
8966                "options": {
8967                    "include_subagents": false,
8968                    "message_limit": 2,
8969                    "message_offset": 1
8970                }
8971            }),
8972        ));
8973        assert_eq!(bounded["result"]["window"]["offset"], 1);
8974        assert_eq!(bounded["result"]["window"]["returned"], 2);
8975        assert!(bounded["result"]["summary"]["first_message"].is_object());
8976        assert!(bounded["result"]["summary"]["last_message"].is_object());
8977        assert_eq!(
8978            bounded["result"]["session"]["messages"]
8979                .as_array()
8980                .unwrap()
8981                .len(),
8982            2
8983        );
8984        assert!(bounded["result"]["session"]["subagents"]
8985            .as_array()
8986            .unwrap()
8987            .is_empty());
8988
8989        let tail = service.handle(request(
8990            2,
8991            "harness.v1.sessions.load",
8992            json!({"locator": locator, "options": {"message_tail": 1}}),
8993        ));
8994        assert_eq!(tail["result"]["window"]["returned"], 1);
8995        assert_eq!(tail["result"]["window"]["has_more"], true);
8996        assert_eq!(tail["result"]["window"]["has_older"], true);
8997        assert!(tail["result"]["window"]["older_items"].as_u64().unwrap() > 0);
8998        assert!(tail["result"]["summary"]["first_message"].is_object());
8999
9000        let metadata_only = service.handle(request(
9001            3,
9002            "harness.v1.sessions.load",
9003            json!({"locator": locator, "options": {"inline_media": "metadata"}}),
9004        ));
9005        assert!(metadata_only["result"]["session"]
9006            .to_string()
9007            .contains("media_reference"));
9008        assert!(!metadata_only["result"]["session"]
9009            .to_string()
9010            .contains("data:image/"));
9011    }
9012
9013    #[test]
9014    fn import_translate_branch_and_handoff_use_typed_artifacts() {
9015        let mut service = HarnessSessionService::new();
9016        let locator = pi_locator();
9017        let translated = service.handle(request(
9018            1,
9019            "harness.v1.sessions.translate",
9020            json!({"locator": locator, "target_harness": "grok"}),
9021        ));
9022        assert_eq!(translated["result"]["artifact"]["source_harness"], "pi");
9023        assert_eq!(translated["result"]["artifact"]["target_harness"], "grok");
9024        assert!(translated["result"]["artifact"]["content"]
9025            .as_str()
9026            .is_some_and(|content| !content.is_empty()));
9027
9028        for target in ["opencode", "open-code"] {
9029            let opencode = service.handle(request(
9030                6,
9031                "harness.v1.sessions.translate",
9032                json!({"locator": locator, "target_harness": target}),
9033            ));
9034            assert_eq!(opencode["result"]["artifact"]["target_harness"], "opencode");
9035        }
9036        let goose = service.handle(request(
9037            7,
9038            "harness.v1.sessions.translate",
9039            json!({"locator": locator, "target_harness": "goose"}),
9040        ));
9041        assert_eq!(goose["result"]["artifact"]["target_harness"], "goose");
9042        assert!(serde_json::from_str::<Value>(
9043            goose["result"]["artifact"]["content"].as_str().unwrap()
9044        )
9045        .unwrap()["conversation"]
9046            .is_array());
9047
9048        let imported = service.handle(request(
9049            2,
9050            "harness.v1.sessions.import",
9051            json!({
9052                "source_harness": "grok",
9053                "content": translated["result"]["artifact"]["content"],
9054            }),
9055        ));
9056        assert_eq!(imported["result"]["session"]["source"], "grok");
9057
9058        let branched = service.handle(request(
9059            3,
9060            "harness.v1.sessions.branch",
9061            json!({"locator": locator, "target_harness": "codex"}),
9062        ));
9063        assert_eq!(branched["result"]["parent"]["harness"], "pi");
9064        assert!(branched["result"]["bootstrap_prompt"]
9065            .as_str()
9066            .unwrap()
9067            .contains("frozen parent transcript"));
9068        assert_eq!(branched["result"]["artifact"]["target_harness"], "codex");
9069
9070        let handoff = service.handle(request(
9071            4,
9072            "harness.v1.sessions.handoff",
9073            json!({"locator": locator, "target_harness": "pi", "cwd": "/tmp/project"}),
9074        ));
9075        assert_eq!(handoff["result"]["launch"]["program"], "pi");
9076        assert_eq!(handoff["result"]["launch"]["cwd"], "/tmp/project");
9077        assert_eq!(handoff["result"]["requires_materialization"], true);
9078
9079        let goose_handoff = service.handle(request(
9080            8,
9081            "harness.v1.sessions.handoff",
9082            json!({"locator": locator, "target_harness": "goose", "cwd": "/tmp/project"}),
9083        ));
9084        assert_eq!(goose_handoff["result"]["launch"]["program"], "goose");
9085        assert_eq!(
9086            goose_handoff["result"]["materialize"]["arguments"],
9087            json!(["session", "import", "{artifact_path}"])
9088        );
9089
9090        let resumed = service.handle(request(
9091            5,
9092            "harness.v1.sessions.resume_instructions",
9093            json!({"locator": locator, "cwd": "/tmp/project", "policy": "yolo"}),
9094        ));
9095        assert_eq!(resumed["result"]["launch"]["program"], "pi");
9096        assert_eq!(resumed["result"]["launch"]["arguments"][0], "--approve");
9097    }
9098
9099    #[test]
9100    fn reduce_persists_and_reloads_a_byte_exact_reversible_bundle() {
9101        let temp = std::env::temp_dir().join(format!(
9102            "supercode-service-reduce-{}-{}",
9103            std::process::id(),
9104            generated_session_id()
9105        ));
9106        let source_path = temp.join("source.jsonl");
9107        let store_root = temp.join("store");
9108        std::fs::create_dir_all(&temp).unwrap();
9109
9110        let mut records = vec![json!({
9111            "timestamp": "2026-01-01T00:00:00Z",
9112            "type": "session_meta",
9113            "payload": {"id": "codex-reduce", "cwd": "/tmp/project"},
9114        })];
9115        for turn in 0..16 {
9116            records.push(json!({
9117                "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 1),
9118                "type": "response_item",
9119                "payload": {
9120                    "type": "message",
9121                    "role": "user",
9122                    "content": [{
9123                        "type": "input_text",
9124                        "text": format!("request {turn}: {}", "context ".repeat(80)),
9125                    }],
9126                },
9127            }));
9128            records.push(json!({
9129                "timestamp": format!("2026-01-01T00:00:{:02}Z", turn * 2 + 2),
9130                "type": "response_item",
9131                "payload": {
9132                    "type": "message",
9133                    "role": "assistant",
9134                    "content": [{
9135                        "type": "output_text",
9136                        "text": format!("answer {turn}: {}", "implementation detail ".repeat(80)),
9137                    }],
9138                },
9139            }));
9140        }
9141        let source = format!(
9142            "{}\n",
9143            records
9144                .iter()
9145                .map(Value::to_string)
9146                .collect::<Vec<_>>()
9147                .join("\n")
9148        );
9149        std::fs::write(&source_path, &source).unwrap();
9150        let locator = SessionLocator {
9151            harness: HarnessId::from(HarnessId::CODEX),
9152            session_id: "codex-reduce".into(),
9153            storage: StorageLocator::File {
9154                path: source_path.clone(),
9155            },
9156        };
9157        let original = load_session(&locator).unwrap();
9158        let mut service =
9159            HarnessSessionService::new().with_reduction_store_root(store_root.clone());
9160
9161        let response = service.handle(request(
9162            1,
9163            "harness.v1.sessions.reduce",
9164            json!({
9165                "locator": locator,
9166                "target_harness": "claude-code",
9167                "keep_last": 4,
9168            }),
9169        ));
9170        assert!(response.get("error").is_none(), "{response:#}");
9171        let receipt = &response["result"]["receipt"];
9172        assert_eq!(receipt["source_harness"], "codex");
9173        assert_eq!(receipt["target_harness"], "claude-code");
9174        assert_eq!(receipt["verified"], true);
9175        assert_eq!(receipt["reversible"], true);
9176        assert!(receipt["reductions"].as_u64().unwrap() > 0);
9177        assert!(
9178            receipt["source_tokens"].as_u64().unwrap()
9179                > receipt["reduced_tokens"].as_u64().unwrap()
9180        );
9181        assert!(receipt["ratio"].as_f64().unwrap() > 1.0);
9182        assert!(response["result"]["bootstrap_prompt"]
9183            .as_str()
9184            .unwrap()
9185            .contains("Do not guess hidden content"));
9186
9187        let rescue_id = receipt["id"].as_str().unwrap();
9188        let store = crate::SessionStore::open(&store_root).unwrap();
9189        let sidecar =
9190            Session::from_sidecar_str(&store.load_sidecar(rescue_id).unwrap().unwrap()).unwrap();
9191        let log = store.load_reduction_log(rescue_id).unwrap().unwrap();
9192        let persisted_view = parse_messages_jsonl(&store.load(rescue_id).unwrap()).unwrap();
9193        let policy = reduce::ReductionPolicy {
9194            clear_turns_older_than: Some(4),
9195            ..Default::default()
9196        };
9197        let (restamped_view, reapplied_log) =
9198            reduce::project_messages(&sidecar.messages, &policy, &log);
9199        assert_eq!(
9200            messages_jsonl(&persisted_view).unwrap(),
9201            messages_jsonl(&restamped_view).unwrap()
9202        );
9203        assert_eq!(reapplied_log, log);
9204        reduce::verify_log(&log, &sidecar).unwrap();
9205        assert_eq!(
9206            reduce::invert(&restamped_view, &log, &sidecar).unwrap(),
9207            original.messages
9208        );
9209        assert_eq!(std::fs::read_to_string(&source_path).unwrap(), source);
9210
9211        std::fs::remove_dir_all(temp).ok();
9212    }
9213
9214    #[test]
9215    fn read_surfaces_view_a_severed_claude_graph_while_transfer_still_refuses_it() {
9216        let temp = std::env::temp_dir().join(format!(
9217            "supercode-severed-view-{}-{}",
9218            std::process::id(),
9219            generated_session_id()
9220        ));
9221        std::fs::create_dir_all(&temp).unwrap();
9222        let path = temp.join("severed.jsonl");
9223        // A live record whose parent was pruned — what a compacted or
9224        // resumed-across-files Claude Code session looks like on disk.
9225        std::fs::write(
9226            &path,
9227            concat!(
9228                r#"{"type":"user","uuid":"orphan-u","parentUuid":null,"message":{"role":"user","content":"stranded prompt"}}"#,
9229                "\n",
9230                r#"{"type":"assistant","uuid":"live-a","parentUuid":"pruned","message":{"id":"m","role":"assistant","content":[{"type":"text","text":"live answer"}]}}"#,
9231                "\n",
9232            ),
9233        )
9234        .unwrap();
9235        let locator = SessionLocator {
9236            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9237            session_id: "severed".into(),
9238            storage: StorageLocator::File { path },
9239        };
9240        let mut service = HarnessSessionService::new();
9241
9242        let viewed = service.handle(request(
9243            1,
9244            "harness.v1.sessions.load",
9245            json!({"locator": locator}),
9246        ));
9247        let session = &viewed["result"]["session"];
9248        assert_eq!(session["fidelity"], "semantic");
9249        assert_eq!(session["messages"].as_array().unwrap().len(), 2);
9250        assert!(session["residue"].as_array().unwrap().iter().any(|entry| {
9251            entry
9252                .as_str()
9253                .is_some_and(|entry| entry.contains("live-a") && entry.contains("pruned"))
9254        }));
9255
9256        // Asking a READ surface for a lossless reconstruction gets the strict
9257        // refusal back, unchanged.
9258        let strict = service.handle(request(
9259            2,
9260            "harness.v1.sessions.load",
9261            json!({"locator": locator, "fidelity": "byte_lossless"}),
9262        ));
9263        assert!(strict["error"]["message"]
9264            .as_str()
9265            .unwrap()
9266            .contains("cannot reconstruct lossless Claude continuation"));
9267
9268        // Transfer/continuation surfaces have no view mode at all.
9269        let translated = service.handle(request(
9270            3,
9271            "harness.v1.sessions.translate",
9272            json!({"locator": locator, "target_harness": "codex"}),
9273        ));
9274        assert!(translated["error"]["message"]
9275            .as_str()
9276            .unwrap()
9277            .contains("cannot reconstruct lossless Claude continuation"));
9278        let resumed = service.handle(request(
9279            4,
9280            "harness.v1.sessions.resume_instructions",
9281            json!({"locator": locator}),
9282        ));
9283        assert!(resumed["error"]["message"]
9284            .as_str()
9285            .unwrap()
9286            .contains("cannot reconstruct lossless Claude continuation"));
9287
9288        let _ = std::fs::remove_dir_all(&temp);
9289    }
9290
9291    #[test]
9292    fn structured_resume_launches_cover_gemini_goose_and_supercode() {
9293        let codex = resume_launch(
9294            HarnessId::CODEX,
9295            "codex-session",
9296            Path::new("/tmp/project"),
9297            ResumePolicy::Yolo,
9298        )
9299        .unwrap_or_else(|_| panic!("Codex resume launch must be registered"));
9300        assert_eq!(codex.program, "codex");
9301        assert_eq!(
9302            codex.arguments,
9303            [
9304                "-c",
9305                "check_for_update_on_startup=false",
9306                "-c",
9307                "projects.\"/tmp/project\".trust_level=\"trusted\"",
9308                "--dangerously-bypass-approvals-and-sandbox",
9309                "--dangerously-bypass-hook-trust",
9310                "resume",
9311                "codex-session",
9312            ]
9313        );
9314
9315        let gemini = resume_launch(
9316            HarnessId::GEMINI,
9317            "gemini-session",
9318            Path::new("/tmp/project"),
9319            ResumePolicy::Yolo,
9320        )
9321        .unwrap_or_else(|_| panic!("Gemini resume launch must be registered"));
9322        assert_eq!(gemini.program, "gemini");
9323        assert_eq!(gemini.arguments, ["--yolo", "--resume", "gemini-session"]);
9324
9325        let goose = resume_launch(
9326            HarnessId::GOOSE,
9327            "goose-session",
9328            Path::new("/tmp/project"),
9329            ResumePolicy::Yolo,
9330        )
9331        .unwrap_or_else(|_| panic!("Goose resume launch must be registered"));
9332        assert_eq!(goose.program, "goose");
9333        assert_eq!(
9334            goose.arguments,
9335            ["session", "--resume", "--session-id", "goose-session"]
9336        );
9337
9338        let supercode = resume_launch(
9339            HarnessId::SUPERCODE,
9340            "supercode-session",
9341            Path::new("/tmp/project"),
9342            ResumePolicy::Yolo,
9343        )
9344        .unwrap_or_else(|_| panic!("Supercode resume launch must be registered"));
9345        assert_eq!(supercode.program, "supercode");
9346        assert_eq!(
9347            supercode.arguments,
9348            ["--dangerous", "resume", "supercode-session"]
9349        );
9350    }
9351
9352    #[test]
9353    fn diagonal_artifacts_preserve_claude_subagents_and_grok_bundle_members() {
9354        let temp = std::env::temp_dir().join(format!(
9355            "supercode-harness-artifact-{}-{}",
9356            std::process::id(),
9357            generated_session_id()
9358        ));
9359        let main_path = temp.join("parent.jsonl");
9360        let subagent_path = temp.join("parent/subagents/agent-child.jsonl");
9361        std::fs::create_dir_all(subagent_path.parent().unwrap()).unwrap();
9362        let fixture = std::fs::read_to_string(
9363            PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9364                .join("tests/fixtures/claude_code_session.jsonl"),
9365        )
9366        .unwrap();
9367        let parent = fixture.trim_end_matches('\n');
9368        let child = fixture.trim_end_matches('\n');
9369        std::fs::write(&main_path, parent).unwrap();
9370        std::fs::write(&subagent_path, child).unwrap();
9371        let locator = SessionLocator {
9372            harness: HarnessId::from(HarnessId::CLAUDE_CODE),
9373            session_id: "213bb148-51ea-453f-9206-f8b4b1168547".into(),
9374            storage: StorageLocator::File {
9375                path: main_path.clone(),
9376            },
9377        };
9378        let mut service = HarnessSessionService::new();
9379        let claude = service.handle(request(
9380            1,
9381            "harness.v1.sessions.translate",
9382            json!({"locator": locator, "target_harness": "claude-code"}),
9383        ));
9384        let artifact = &claude["result"]["artifact"];
9385        assert_eq!(artifact["fidelity"], "byte_lossless");
9386        assert_eq!(artifact["content"], parent);
9387        let files = artifact["files"].as_array().unwrap();
9388        assert!(files.iter().any(|file| {
9389            file["role"] == "subagent"
9390                && file["path"]
9391                    .as_str()
9392                    .is_some_and(|path| path.ends_with("/subagents/agent-child.jsonl"))
9393                && file["content"] == child
9394        }));
9395        assert!(!artifact["content"].as_str().unwrap().ends_with('\n'));
9396
9397        let grok = service.handle(request(
9398            2,
9399            "harness.v1.sessions.translate",
9400            json!({"locator": grok_locator(), "target_harness": "grok"}),
9401        ));
9402        let files = grok["result"]["artifact"]["files"].as_array().unwrap();
9403        for name in ["summary.json", "updates.jsonl"] {
9404            let expected = std::fs::read_to_string(
9405                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
9406                    .join("tests/fixtures/grok_session")
9407                    .join(name),
9408            )
9409            .unwrap();
9410            assert!(files.iter().any(|file| {
9411                file["path"] == name && file["role"] == "bundle" && file["content"] == expected
9412            }));
9413        }
9414        std::fs::remove_dir_all(temp).ok();
9415    }
9416
9417    #[test]
9418    fn every_non_grok_handoff_mints_and_uses_a_fresh_target_identity() {
9419        let mut service = HarnessSessionService::new();
9420        let source = pi_locator();
9421        for (target, format) in [
9422            ("claude-code", SessionFormat::ClaudeCode),
9423            ("codex", SessionFormat::Codex),
9424            ("opencode", SessionFormat::OpenCode),
9425            ("pi", SessionFormat::Pi),
9426        ] {
9427            let result = service.handle(request(
9428                1,
9429                "harness.v1.sessions.handoff",
9430                json!({"locator": source, "target_harness": target, "cwd": "/tmp/project"}),
9431            ));
9432            let artifact = &result["result"]["artifact"];
9433            let target_id = artifact["session_id"].as_str().unwrap();
9434            assert_ne!(target_id, source.session_id, "{target}");
9435            let parsed = Session::load_str(artifact["content"].as_str().unwrap(), format).unwrap();
9436            assert_eq!(
9437                parsed.meta.session_id.as_deref(),
9438                Some(target_id),
9439                "{target}"
9440            );
9441            if target != "pi" {
9442                assert!(result["result"]["launch"]["arguments"]
9443                    .as_array()
9444                    .unwrap()
9445                    .iter()
9446                    .any(|argument| argument == target_id));
9447            }
9448            if target == "opencode" {
9449                assert!(target_id.starts_with("ses_"));
9450                fn assert_session_ids(value: &Value, target_id: &str) {
9451                    match value {
9452                        Value::Object(fields) => {
9453                            if let Some(session_id) = fields.get("sessionID") {
9454                                assert_eq!(session_id, target_id);
9455                            }
9456                            for child in fields.values() {
9457                                assert_session_ids(child, target_id);
9458                            }
9459                        }
9460                        Value::Array(values) => {
9461                            for child in values {
9462                                assert_session_ids(child, target_id);
9463                            }
9464                        }
9465                        _ => {}
9466                    }
9467                }
9468                let document: Value =
9469                    serde_json::from_str(artifact["content"].as_str().unwrap()).unwrap();
9470                assert_session_ids(&document, target_id);
9471            }
9472        }
9473
9474        let first = service.handle(request(
9475            2,
9476            "harness.v1.sessions.handoff",
9477            json!({"locator": source, "target_harness": "codex"}),
9478        ));
9479        let second = service.handle(request(
9480            3,
9481            "harness.v1.sessions.handoff",
9482            json!({"locator": source, "target_harness": "codex"}),
9483        ));
9484        assert_ne!(
9485            first["result"]["artifact"]["session_id"],
9486            second["result"]["artifact"]["session_id"]
9487        );
9488    }
9489
9490    #[test]
9491    fn grok_handoff_materializes_through_the_core_door() {
9492        let mut service = HarnessSessionService::new();
9493        let source = opencode_locator();
9494        let response = service.handle(request(
9495            1,
9496            "harness.v1.sessions.handoff",
9497            json!({
9498                "locator": source,
9499                "target_harness": "grok",
9500                "cwd": "/tmp/grok-handoff-project",
9501            }),
9502        ));
9503        let result = &response["result"];
9504
9505        // Grok has no import command: the artifact is Grok's own transcript under a fresh
9506        // identity, and `harness.v1.sessions.materialize` writes its store entry.
9507        assert_eq!(result["artifact"]["target_harness"], "grok");
9508        let artifact = Session::load_str(
9509            result["artifact"]["content"].as_str().unwrap(),
9510            SessionFormat::Grok,
9511        )
9512        .unwrap();
9513        assert!(!artifact.messages.is_empty());
9514        let target_session_id = result["artifact"]["session_id"].as_str().unwrap();
9515        assert_eq!(target_session_id.len(), 36);
9516        assert_ne!(target_session_id, opencode_locator().session_id);
9517        assert!(result["materialize"].is_null());
9518        assert_eq!(
9519            result["launch"]["arguments"],
9520            json!(["--resume", "{materialized_session_id}"])
9521        );
9522        assert!(result["note"]
9523            .as_str()
9524            .unwrap()
9525            .contains("harness.v1.sessions.materialize"));
9526    }
9527
9528    #[tokio::test]
9529    async fn inventory_rejects_unknown_harnesses_and_runtime_attach_is_honest() {
9530        let mut service = HarnessSessionService::new();
9531        let inventory = service
9532            .handle_async(request(
9533                1,
9534                "harness.v1.harnesses.list",
9535                json!({"harnesses": ["missing"]}),
9536            ))
9537            .await;
9538        assert_eq!(inventory["error"]["code"], -32602);
9539
9540        let attached = service
9541            .handle_async(request(
9542                2,
9543                "harness.v1.runtimes.attach_existing",
9544                json!({"harness": "codex", "runtime_id": "thread-1"}),
9545            ))
9546            .await;
9547        assert_eq!(attached["error"]["code"], -32000);
9548        assert!(attached["error"]["message"]
9549            .as_str()
9550            .unwrap()
9551            .contains("runtimes.resume"));
9552    }
9553
9554    #[test]
9555    fn invalid_params_and_unknown_methods_use_json_rpc_errors() {
9556        let mut service = HarnessSessionService::new();
9557        let invalid = service.handle(request(1, "harness.v1.sessions.load", json!({})));
9558        assert_eq!(invalid["error"]["code"], -32602);
9559        let unknown = service.handle(request(2, "harness.v1.unknown", json!({})));
9560        assert_eq!(unknown["error"]["code"], -32601);
9561    }
9562
9563    #[cfg(unix)]
9564    #[tokio::test]
9565    // The test mutates process-wide harness environment and deliberately
9566    // holds the global test lock until every async runtime operation ends.
9567    #[allow(clippy::await_holding_lock)]
9568    async fn async_service_drives_a_generic_acp_runtime() {
9569        let _environment_guard = crate::live_runtime::test_environment_lock();
9570        let script = r#"
9571            i=0
9572            while IFS= read -r line; do
9573              i=$((i + 1))
9574              case "$i" in
9575                1) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{},"authMethods":[]}}' ;;
9576                2) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"svc_acp"}}' ;;
9577                3)
9578                  printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ok"}}}}'
9579                  printf '%s\n' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}'
9580                  ;;
9581                4)
9582                  printf '%s\n' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"svc_acp","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"from terminal"}}}}'
9583                  printf '%s\n' '{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}}'
9584                  ;;
9585              esac
9586            done
9587        "#;
9588        let mut service = HarnessSessionService::new();
9589        let started = service
9590            .handle_async(request(
9591                1,
9592                "harness.v1.runtimes.start",
9593                json!({
9594                    "harness": "codex",
9595                    "protocol": "acp",
9596                    "cwd": std::env::current_dir().unwrap(),
9597                    "launch": {"program": "/bin/sh", "arguments": ["-c", script], "env": {}},
9598                }),
9599            ))
9600            .await;
9601        assert_eq!(started["result"]["connection"], "runtime-1");
9602        assert_eq!(started["result"]["handle"]["runtime_id"], "svc_acp");
9603
9604        let terminal = service
9605            .handle_async(request(
9606                9,
9607                "harness.v1.runtimes.terminal_instructions",
9608                json!({"connection":"runtime-1"}),
9609            ))
9610            .await;
9611        let arguments = terminal["result"]["launch"]["arguments"]
9612            .as_array()
9613            .expect("hosted runtime should return terminal arguments");
9614        let endpoint_index = arguments
9615            .iter()
9616            .position(|value| value == "--endpoint")
9617            .expect("terminal command should use an opaque endpoint");
9618        let endpoint = LiveRuntimeEndpoint::parse(
9619            arguments[endpoint_index + 1]
9620                .as_str()
9621                .expect("endpoint argument should be text"),
9622        )
9623        .unwrap();
9624        assert!(!terminal.to_string().contains("Bearer"));
9625        let workspace = std::env::current_dir().unwrap();
9626        let receipt = resolve_live_runtime(
9627            &endpoint,
9628            &LiveRuntimeSource {
9629                harness: "codex".into(),
9630                session_id: "svc_acp".into(),
9631                workspace,
9632            },
9633        )
9634        .unwrap();
9635        let remote = crate::HttpFrontendRuntime::connect(receipt.base_url, receipt.token)
9636            .await
9637            .unwrap();
9638        let mut attachment = crate::FrontendRuntime::attach(remote.as_ref(), 100)
9639            .await
9640            .unwrap();
9641
9642        let sent = service
9643            .handle_async(request(
9644                2,
9645                "harness.v1.runtimes.send_input",
9646                json!({"connection": "runtime-1", "text": "hi"}),
9647            ))
9648            .await;
9649        assert_eq!(sent["result"]["turn_id"], "3");
9650
9651        let mut events = Vec::new();
9652        for _ in 0..20 {
9653            events.extend(service.poll_runtimes().await);
9654            if events.len() >= 2 {
9655                break;
9656            }
9657            tokio::time::sleep(Duration::from_millis(2)).await;
9658        }
9659        assert!(events
9660            .iter()
9661            .any(|event| { event["params"]["event"]["kind"] == "session/update" }));
9662        assert!(events.iter().any(|event| {
9663            event["params"]["event"]["kind"] == "supercode/acp_request_completed"
9664        }));
9665
9666        let saw_editor_reply = tokio::time::timeout(Duration::from_secs(2), async {
9667            loop {
9668                let event = attachment.next_event().await.unwrap();
9669                if event.kind == "text_delta" && event.payload["text"] == "ok" {
9670                    break;
9671                }
9672            }
9673        })
9674        .await;
9675        assert!(
9676            saw_editor_reply.is_ok(),
9677            "terminal should observe the editor-driven turn"
9678        );
9679
9680        crate::FrontendRuntime::submit(remote.as_ref(), "DRIVE FROM TERMINAL".into())
9681            .await
9682            .unwrap();
9683        let saw_terminal_reply = tokio::time::timeout(Duration::from_secs(2), async {
9684            loop {
9685                let event = attachment.next_event().await.unwrap();
9686                if event.kind == "text_delta" && event.payload["text"] == "from terminal" {
9687                    break;
9688                }
9689            }
9690        })
9691        .await;
9692        assert!(
9693            saw_terminal_reply.is_ok(),
9694            "terminal should drive the same runtime"
9695        );
9696
9697        let closed = service
9698            .handle_async(request(
9699                3,
9700                "harness.v1.runtimes.close",
9701                json!({"connection": "runtime-1"}),
9702            ))
9703            .await;
9704        assert_eq!(closed["result"]["closed"], true);
9705    }
9706
9707    /// UNI-7 dev/02: a RUNNING mock gateway is detected through the real
9708    /// openclaw probe (config-declared endpoint, TCP connect), and an ACTIVE
9709    /// hermes WAL is detected through the real WAL-freshness probe; the
9710    /// negative sides (no listener, stale WAL, no config) stay undetected.
9711    #[test]
9712    fn running_instances_are_detected_from_mock_gateway_and_active_wal() {
9713        let home = connect_scratch_home("uni7-running");
9714
9715        // No config at all: hermes has no default endpoint, so no detection.
9716        // (openclaw's no-config behavior now probes its DOCUMENTED default
9717        // endpoint ws://127.0.0.1:18789 — see the connect launch's
9718        // `default_address` — which is real box state a hermetic test must
9719        // not assert either way; the closed-port negative below covers the
9720        // no-listener side deterministically.)
9721        assert!(probe_hermes_running(&home, 300_000).is_none());
9722
9723        // Mock gateway: a real TCP listener on an ephemeral port, declared in
9724        // the harness's own config file.
9725        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9726        let port = listener.local_addr().unwrap().port();
9727        std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9728        std::fs::write(
9729            home.join(".openclaw/openclaw.json"),
9730            format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9731        )
9732        .unwrap();
9733        let running = probe_openclaw_running(&home).expect("listening gateway must be detected");
9734        assert!(matches!(
9735            running.method,
9736            RunningInstanceMethod::GatewayConnect
9737        ));
9738        assert!(running.evidence.contains(&format!("127.0.0.1:{port}")));
9739        drop(listener);
9740        // Parallel tests also bind ephemeral loopback ports, so a just-freed
9741        // port can be re-bound by a NEIGHBORING test between drop and probe.
9742        // Detection on a closed port must fail — retry on a fresh port when
9743        // the freed one was recycled by someone else.
9744        let mut closed_detected = probe_openclaw_running(&home).is_some();
9745        for _ in 0..3 {
9746            if !closed_detected {
9747                break;
9748            }
9749            let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9750            let port = listener.local_addr().unwrap().port();
9751            drop(listener);
9752            std::fs::write(
9753                home.join(".openclaw/openclaw.json"),
9754                format!(r#"{{"gateway": {{"mode": "local", "port": {port}, "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9755            )
9756            .unwrap();
9757            closed_detected = probe_openclaw_running(&home).is_some();
9758        }
9759        assert!(
9760            !closed_detected,
9761            "a closed gateway must not read as running"
9762        );
9763
9764        // gateway.url form takes precedence over port.
9765        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
9766        let port = listener.local_addr().unwrap().port();
9767        std::fs::write(
9768            home.join(".openclaw/openclaw.json"),
9769            format!(r#"{{"gateway": {{"url": "ws://127.0.0.1:{port}", "auth": {{"mode": "token", "token": "t"}}}}}}"#),
9770        )
9771        .unwrap();
9772        assert!(probe_openclaw_running(&home).is_some());
9773        drop(listener);
9774
9775        // Hermes: an ACTIVE WAL (fresh stamp) is detected; a stale one is not.
9776        std::fs::create_dir_all(home.join(".hermes")).unwrap();
9777        let wal = home.join(".hermes/state.db-wal");
9778        std::fs::write(&wal, b"wal").unwrap();
9779        let running = probe_hermes_running(&home, 300_000).expect("fresh WAL must be detected");
9780        assert!(matches!(
9781            running.method,
9782            RunningInstanceMethod::StoreWalActivity
9783        ));
9784        assert!(running.evidence.contains("state.db-wal"));
9785        let stale = std::time::SystemTime::now() - std::time::Duration::from_secs(3_600);
9786        std::fs::File::options()
9787            .append(true)
9788            .open(&wal)
9789            .unwrap()
9790            .set_modified(stale)
9791            .unwrap();
9792        assert!(
9793            probe_hermes_running(&home, 300_000).is_none(),
9794            "a stale WAL (crash leftover) must not read as running"
9795        );
9796    }
9797
9798    fn connect_scratch_home(tag: &str) -> PathBuf {
9799        let dir = std::env::temp_dir().join(format!(
9800            "supercode-connect-service-{tag}-{}-{}",
9801            std::process::id(),
9802            std::time::SystemTime::now()
9803                .duration_since(std::time::UNIX_EPOCH)
9804                .unwrap()
9805                .as_nanos()
9806        ));
9807        std::fs::create_dir_all(&dir).unwrap();
9808        dir
9809    }
9810
9811    /// Minimal HTTP responder that speaks just enough OpenCode server to
9812    /// accept a health check, create a session, and hold an SSE stream open,
9813    /// while recording each request line with its Authorization header.
9814    async fn mock_opencode_endpoint() -> (String, tokio::sync::mpsc::UnboundedReceiver<String>) {
9815        use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
9816        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
9817        let address = listener.local_addr().unwrap();
9818        let (request_sender, request_receiver) = tokio::sync::mpsc::unbounded_channel();
9819        tokio::spawn(async move {
9820            loop {
9821                let Ok((mut stream, _)) = listener.accept().await else {
9822                    break;
9823                };
9824                let request_sender = request_sender.clone();
9825                tokio::spawn(async move {
9826                    let (reader, mut writer) = stream.split();
9827                    let mut reader = BufReader::new(reader);
9828                    let mut request_line = String::new();
9829                    if reader.read_line(&mut request_line).await.unwrap_or(0) == 0 {
9830                        return;
9831                    }
9832                    let request_line = request_line.trim_end().to_string();
9833                    let mut authorization = String::new();
9834                    let mut content_length = 0usize;
9835                    loop {
9836                        let mut line = String::new();
9837                        if reader.read_line(&mut line).await.unwrap_or(0) == 0 {
9838                            return;
9839                        }
9840                        let line = line.trim_end();
9841                        if line.is_empty() {
9842                            break;
9843                        }
9844                        let lower = line.to_ascii_lowercase();
9845                        if let Some(value) = lower.strip_prefix("authorization:") {
9846                            authorization = value.trim().to_string();
9847                        }
9848                        if let Some(value) = lower.strip_prefix("content-length:") {
9849                            content_length = value.trim().parse().unwrap_or(0);
9850                        }
9851                    }
9852                    if content_length > 0 {
9853                        let mut body = vec![0u8; content_length];
9854                        let _ = reader.read_exact(&mut body).await;
9855                    }
9856                    let _ = request_sender.send(format!("{request_line} :: {authorization}"));
9857                    if request_line.starts_with("GET /event") {
9858                        let _ = writer
9859                            .write_all(
9860                                b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n\r\n",
9861                            )
9862                            .await;
9863                        tokio::time::sleep(std::time::Duration::from_secs(5)).await;
9864                        return;
9865                    }
9866                    let body = if request_line.starts_with("POST /session") {
9867                        r#"{"id":"mock-session"}"#
9868                    } else {
9869                        r#"{"status":"ok"}"#
9870                    };
9871                    let response = format!(
9872                        "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
9873                        body.len(),
9874                        body
9875                    );
9876                    let _ = writer.write_all(response.as_bytes()).await;
9877                });
9878            }
9879        });
9880        (format!("http://{address}"), request_receiver)
9881    }
9882
9883    fn connect_descriptor(protocol: &str) -> crate::HarnessSupportDescriptor {
9884        crate::HarnessSupportDescriptor {
9885            orchestration: Default::default(),
9886            id: HarnessId::from(HarnessId::OPENCODE),
9887            display_name: "OpenCode".into(),
9888            native: crate::NativeSupport {
9889                discover: crate::ImplementationKind::Absent,
9890                load: crate::ImplementationKind::Absent,
9891                follow: crate::ImplementationKind::Absent,
9892                import: crate::ImplementationKind::Absent,
9893                export: crate::ImplementationKind::Absent,
9894            },
9895            runtime: crate::RuntimeSupport {
9896                implementation: crate::ImplementationKind::BuiltIn,
9897                protocol: protocol.into(),
9898                default_launch: None,
9899                connect_launch: Some(crate::RuntimeConnectLaunch {
9900                    config_path: "~/opencode-tui.json".into(),
9901                    address_pointer: "/server/url".into(),
9902                    port_pointer: None,
9903                    default_address: None,
9904                    auth_pointer: Some("/server/token".into()),
9905                    protocol: protocol.into(),
9906                }),
9907                capabilities: crate::RuntimeCapabilities {
9908                    start_session: true,
9909                    resume_session: true,
9910                    attach_existing_process: true,
9911                    send_input: true,
9912                    stream_events: true,
9913                    interrupt: true,
9914                    steer: false,
9915                    respond_to_requests: true,
9916                },
9917            },
9918        }
9919    }
9920
9921    #[tokio::test]
9922    async fn connect_mode_descriptor_opens_a_running_endpoint_with_config_sourced_auth() {
9923        let (base_url, mut requests) = mock_opencode_endpoint().await;
9924        let home = connect_scratch_home("open");
9925        std::fs::write(
9926            home.join("opencode-tui.json"),
9927            format!(r#"{{"server": {{"url": "{base_url}", "token": "connect-secret"}}}}"#),
9928        )
9929        .unwrap();
9930
9931        let descriptor = connect_descriptor("opencode-http-sse");
9932        let backend = open_connect_descriptor(&descriptor, &home).unwrap();
9933        assert!(backend.capabilities().attach_existing_process);
9934
9935        let connection = backend
9936            .start(crate::RuntimeStartRequest {
9937                cwd: home.clone(),
9938                launch: None,
9939                mcp_servers: Vec::new(),
9940            })
9941            .await
9942            .unwrap();
9943        let handle = connection.handle();
9944        assert_eq!(handle.runtime_id, "mock-session");
9945        match &handle.endpoint {
9946            crate::RuntimeEndpoint::Http {
9947                base_url: endpoint, ..
9948            } => assert_eq!(endpoint, &base_url),
9949            other => panic!("connect mode must join the running endpoint, got {other:?}"),
9950        }
9951
9952        let mut seen = Vec::new();
9953        while let Ok(line) = requests.try_recv() {
9954            seen.push(line);
9955        }
9956        assert!(seen
9957            .iter()
9958            .any(|line| line.starts_with("GET /global/health")
9959                && line.contains("bearer connect-secret")));
9960        assert!(seen.iter().any(
9961            |line| line.starts_with("POST /session") && line.contains("bearer connect-secret")
9962        ));
9963    }
9964
9965    /// UNI-5 dev/02, contract corrected by the 2026-08-31 blind walk: the
9966    /// full connect-mode attach path against a MOCK gateway bridge — no live
9967    /// gateway, no model spend. A scripted fake `openclaw` binary (a)
9968    /// asserts the REAL bridge contract — the resolved --url on argv and the
9969    /// credential via --token-file (the real bridge ignores the env var; the
9970    /// endpoint comes from openclaw-native `gateway.remote.url`, never the
9971    /// schema-invalid `gateway.url`) — then (b) speaks scripted ACP:
9972    /// initialize advertising sessionCapabilities.{list,resume},
9973    /// session/resume rebinding the requested session (join), and a
9974    /// prompted turn.
9975    #[tokio::test]
9976    async fn openclaw_connect_mode_attaches_lists_and_resumes_via_a_mock_bridge() {
9977        let home = connect_scratch_home("openclaw");
9978        std::fs::create_dir_all(home.join(".openclaw")).unwrap();
9979        std::fs::write(
9980            home.join(".openclaw/openclaw.json"),
9981            r#"{"gateway": {"remote": {"url": "ws://127.0.0.1:19789"}, "auth": {"mode": "token", "token": "mock-gateway-token"}}}"#,
9982        )
9983        .unwrap();
9984        let script = home.join("openclaw");
9985        std::fs::write(
9986            &script,
9987            r#"#!/bin/sh
9988# Fake `openclaw acp` bridge: verify the connect-mode contract, then speak ACP.
9989[ "$1" = "acp" ] || { echo "unexpected argv: $*" >&2; exit 9; }
9990[ "$2" = "--url" ] && [ "$3" = "ws://127.0.0.1:19789" ] || { echo "missing --url: $*" >&2; exit 9; }
9991[ "$4" = "--token-file" ] || { echo "missing --token-file: $*" >&2; exit 9; }
9992[ "$(cat "$5")" = "mock-gateway-token" ] || { echo "token file wrong" >&2; exit 9; }
9993while IFS= read -r line; do
9994  case "$line" in
9995    *'"initialize"'*)
9996      printf '%s
9997' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"sessionCapabilities":{"list":{},"resume":{}}},"agentInfo":{"name":"openclaw-acp","version":"2026.7.1-2"},"authMethods":[]}}' ;;
9998    *'"session/resume"'*)
9999      printf '%s
10000' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:main"}}' ;;
10001    *'"session/new"'*)
10002      printf '%s
10003' '{"jsonrpc":"2.0","id":2,"result":{"sessionId":"agent:main:fresh"}}' ;;
10004    *'"session/prompt"'*)
10005      printf '%s
10006' '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"agent:main:main","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"joined"}}}}'
10007      printf '%s
10008' '{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}}' ;;
10009  esac
10010done
10011"#,
10012        )
10013        .unwrap();
10014        use std::os::unix::fs::PermissionsExt;
10015        std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
10016
10017        let mut descriptor = crate::harness_support_registry()
10018            .harnesses
10019            .into_iter()
10020            .find(|harness| harness.id.as_str() == HarnessId::OPENCLAW)
10021            .expect("openclaw must be registered");
10022        descriptor
10023            .runtime
10024            .connect_launch
10025            .as_mut()
10026            .unwrap()
10027            .config_path = "~/.openclaw/openclaw.json".into();
10028        descriptor.runtime.default_launch.as_mut().unwrap().program =
10029            script.to_string_lossy().into_owned();
10030        let backend = open_connect_descriptor(&descriptor, &home).unwrap();
10031        assert!(backend.capabilities().resume_session);
10032
10033        let joined = backend
10034            .attach(crate::RuntimeAttachRequest {
10035                runtime_id: "agent:main:main".into(),
10036                cwd: Some(home.clone()),
10037                launch: None,
10038                mcp_servers: Vec::new(),
10039            })
10040            .await;
10041        let mut connection = joined.expect("mock bridge attach must succeed");
10042        assert_eq!(connection.handle().runtime_id, "agent:main:main");
10043        let turn = connection
10044            .send_input(crate::RuntimeInput {
10045                text: "hello".into(),
10046                image_urls: Vec::new(),
10047            })
10048            .await;
10049        assert!(turn.is_ok(), "prompt through the mock bridge: {turn:?}");
10050        connection.close().await.unwrap();
10051    }
10052
10053    #[tokio::test]
10054    async fn connect_mode_fails_closed_without_a_protocol_client_or_config() {
10055        let home = connect_scratch_home("fail");
10056        std::fs::write(
10057            home.join("opencode-tui.json"),
10058            r#"{"server": {"url": "http://127.0.0.1:1", "token": "connect-secret"}}"#,
10059        )
10060        .unwrap();
10061
10062        let gateway_only = connect_descriptor("acp-v1-jsonrpc");
10063        let Err(error) = open_connect_descriptor(&gateway_only, &home) else {
10064            panic!("an ACP connect endpoint has no gateway client yet");
10065        };
10066        let message = format!("{error:?}");
10067        assert!(message.contains("acp-v1-jsonrpc"));
10068        assert!(!message.contains("connect-secret"));
10069
10070        let unreadable = connect_descriptor("opencode-http-sse");
10071        let missing_home = connect_scratch_home("missing");
10072        let Err(error) = open_connect_descriptor(&unreadable, &missing_home) else {
10073            panic!("an unreadable connect config must fail closed");
10074        };
10075        let message = format!("{error:?}");
10076        assert!(message.contains("opencode-tui.json"));
10077        assert!(!message.contains("connect-secret"));
10078    }
10079
10080    // ---------------------------------------------------------------------
10081    // ORCH-7 — `harness.v1.jobs.list` / `jobs.get` over the committed fixtures
10082    // ---------------------------------------------------------------------
10083
10084    fn jobs_fixture_root() -> PathBuf {
10085        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
10086    }
10087
10088    /// Point only the three job-bearing homes at the fixtures. Nothing else is
10089    /// read, so the host machine's own harness homes cannot leak into a row.
10090    fn jobs_fixture_homes() -> Value {
10091        let root = jobs_fixture_root();
10092        json!({
10093            "claude_code": root.join("claude_jobs_home/projects"),
10094            "hermes": root.join("hermes_home/state.db"),
10095            "openclaw": root.join("openclaw_home"),
10096        })
10097    }
10098
10099    fn jobs_list(params: Value) -> Value {
10100        let mut service = HarnessSessionService::new();
10101        service.handle(request(1, "harness.v1.jobs.list", params))
10102    }
10103
10104    fn job_row<'a>(result: &'a Value, id: &str) -> &'a Value {
10105        result["jobs"]
10106            .as_array()
10107            .expect("jobs is an array")
10108            .iter()
10109            .find(|job| job["id"] == id)
10110            .unwrap_or_else(|| panic!("no job `{id}` in {result}"))
10111    }
10112
10113    #[test]
10114    fn gateway_health_derives_from_running_probe_and_install_state() {
10115        let running = RunningInstance {
10116            method: RunningInstanceMethod::GatewayConnect,
10117            evidence: "gateway endpoint 127.0.0.1:18789 accepted a TCP connect".into(),
10118            checked_at_ms: 1,
10119        };
10120        let up = gateway_health(
10121            HarnessId::OPENCLAW,
10122            true,
10123            Some(&running),
10124            Some("2026.7.1-2"),
10125        );
10126        assert_eq!(up.state, GatewayState::Up);
10127        assert!(up.endpoint.as_deref().unwrap().starts_with("ws://"));
10128        assert_eq!(up.version.as_deref(), Some("2026.7.1-2"));
10129        // Hermes consults its own `gateway status` when the WAL heuristic says
10130        // nothing; a fake binary decides the verdict (the env var is global, so
10131        // the up/down cases run inside this one test, never in parallel).
10132        let dir = std::env::temp_dir().join(format!("supercode-orch17-{}", std::process::id()));
10133        std::fs::create_dir_all(&dir).unwrap();
10134        let fake = dir.join("hermes");
10135        let write_fake = |body: &str| {
10136            std::fs::write(&fake, format!("#!/bin/sh\n{body}\n")).unwrap();
10137            #[cfg(unix)]
10138            {
10139                use std::os::unix::fs::PermissionsExt;
10140                std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
10141            }
10142        };
10143        write_fake("echo '✗ Gateway service is not installed'");
10144        crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| {
10145            *slot.borrow_mut() = Some((
10146                HarnessId::HERMES.to_string(),
10147                fake.to_string_lossy().into_owned(),
10148            ))
10149        });
10150        let down = gateway_health(HarnessId::HERMES, true, None, None);
10151        assert_eq!(down.state, GatewayState::Down, "{down:?}");
10152        assert!(down.endpoint.is_none());
10153        assert!(down.evidence.contains("not installed"));
10154        write_fake("echo 'Launchd plist: /x/ai.hermes.gateway.plist'; echo '✓ Gateway is supervised by launchd (PID 4242)'");
10155        let idle_but_up = gateway_health(HarnessId::HERMES, true, None, Some("0.21.0"));
10156        assert_eq!(idle_but_up.state, GatewayState::Up, "{idle_but_up:?}");
10157        assert!(idle_but_up.evidence.contains("PID 4242"));
10158        write_fake("echo 'something unparseable'");
10159        let no_verdict = gateway_health(HarnessId::HERMES, true, None, None);
10160        assert_eq!(no_verdict.state, GatewayState::Down);
10161        assert!(no_verdict.evidence.contains("no verdict"));
10162        crate::harness_command::TEST_PROGRAM_OVERRIDE.with(|slot| *slot.borrow_mut() = None);
10163        let absent = gateway_health(HarnessId::HERMES, false, None, None);
10164        assert_eq!(absent.state, GatewayState::Unknown);
10165        let core = gateway_health(HarnessId::CODEX, true, None, Some("0.144.4"));
10166        assert_eq!(core.state, GatewayState::Unknown);
10167        assert!(core.evidence.contains("per session"));
10168    }
10169
10170    #[test]
10171    fn triggers_list_reads_both_stores_and_never_emits_secrets() {
10172        let response = triggers_list(json!({"homes": jobs_fixture_homes()}));
10173        let rows = response["result"]["triggers"]
10174            .as_array()
10175            .expect("triggers")
10176            .clone();
10177        let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10178        assert!(
10179            hermes.iter().any(|r| r["name"] == "deploys"
10180                && r["route"] == "/webhooks/deploys"
10181                && r["kind"] == "webhook"),
10182            "{rows:#?}"
10183        );
10184        let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10185        assert!(openclaw
10186            .iter()
10187            .any(|r| r["name"] == "wake" && r["kind"] == "builtin_wake"));
10188        assert!(openclaw.iter().any(|r| r["name"] == "gmail"
10189            && r["kind"] == "hook_mapping"
10190            && r["target"]["action"] == "agent"));
10191        let rendered = response.to_string();
10192        for secret in [
10193            "FAKE-WEBHOOK-HMAC-DO-NOT-EMIT",
10194            "FAKE-HOOK-TOKEN-DO-NOT-EMIT",
10195        ] {
10196            assert!(!rendered.contains(secret), "{rendered}");
10197        }
10198        let refused =
10199            triggers_list(json!({"harness": "claude-code", "homes": jobs_fixture_homes()}));
10200        assert_eq!(refused["error"]["code"], -32020, "{refused}");
10201    }
10202
10203    fn triggers_list(params: Value) -> Value {
10204        let mut service = HarnessSessionService::new();
10205        service.handle(request(1, "harness.v1.triggers.list", params))
10206    }
10207
10208    #[test]
10209    fn routes_list_reads_both_gateway_configs_and_flags_the_defaults() {
10210        let response = routes_list(json!({"homes": jobs_fixture_homes()}));
10211        let rows = response["result"]["routes"]
10212            .as_array()
10213            .expect("routes")
10214            .clone();
10215        let hermes: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "hermes").collect();
10216        assert_eq!(hermes.len(), 2, "{rows:#?}");
10217        assert_eq!(hermes[0]["target"], "coder");
10218        assert_eq!(hermes[0]["match"]["platform"], "slack");
10219        assert_eq!(hermes[0]["match"]["chat_id"], "C0FIXTURE");
10220        assert_eq!(hermes[0]["specificity"], 4);
10221        assert_eq!(hermes[1]["default"], true);
10222        let openclaw: Vec<&Value> = rows.iter().filter(|r| r["harness"] == "openclaw").collect();
10223        assert!(
10224            openclaw.iter().any(|r| r["target"] == "design"
10225                && r["match"]["platform"] == "slack"
10226                && r["specificity"] == 1),
10227            "{openclaw:#?}"
10228        );
10229        assert!(openclaw.iter().any(|r| r["default"] == true));
10230        // A core harness has no routing concept and is refused, never an empty list.
10231        let refused = routes_list(json!({"harness": "codex", "homes": jobs_fixture_homes()}));
10232        assert_eq!(refused["error"]["code"], -32020, "{refused}");
10233    }
10234
10235    fn routes_list(params: Value) -> Value {
10236        let mut service = HarnessSessionService::new();
10237        service.handle(request(1, "harness.v1.routes.list", params))
10238    }
10239
10240    #[test]
10241    fn jobs_list_projects_every_fixture_store_onto_the_uniform_row() {
10242        let response = jobs_list(json!({"homes": jobs_fixture_homes()}));
10243        let result = &response["result"];
10244        let ids: Vec<&str> = result["jobs"]
10245            .as_array()
10246            .unwrap()
10247            .iter()
10248            .map(|job| job["id"].as_str().unwrap())
10249            .collect();
10250        assert_eq!(
10251            ids,
10252            vec![
10253                "release-watch",
10254                "toolu_wake_recheck",
10255                "digest-15m",
10256                "nightly-audit",
10257                "coder-standup",
10258                "ops-once-boot",
10259                "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10260                "8bb7d938-ca46-4a6d-90eb-c92331155566",
10261                "cron_standup",
10262                "cron_reindex",
10263            ],
10264            "{result}"
10265        );
10266
10267        // OpenClaw, pinned shape: rows come from `state/openclaw.sqlite`
10268        // (`cron_jobs.job_json` + runtime columns), captured from a real
10269        // 2026.7.1-2 gateway.
10270        let health = job_row(result, "85ad7832-896f-42be-af31-3e1ed2fbdc4b");
10271        assert_eq!(health["harness"], "openclaw");
10272        assert_eq!(health["schedule"]["kind"], "interval");
10273        assert_eq!(health["schedule"]["minutes"], 10.0);
10274        assert_eq!(health["session_target"], "isolated");
10275        assert_eq!(health["payload"]["kind"], "prompt");
10276        assert_eq!(health["payload"]["text"], "nightly health check");
10277        // ORCH-13: the mode word (`announce`) and the channel it announces on
10278        // (`last`) are separate facts, and the store keeps both — in
10279        // `job_json.delivery` and in the `delivery_*` columns beside it.
10280        assert_eq!(health["deliver"]["mode"], "announce");
10281        assert_eq!(health["deliver"]["target"], "last");
10282        assert_eq!(health["next_run_at"], "2026-09-03T06:52:26Z");
10283        let digest = job_row(result, "8bb7d938-ca46-4a6d-90eb-c92331155566");
10284        assert_eq!(digest["schedule"]["kind"], "cron");
10285        assert_eq!(digest["schedule"]["expr"], "0 9 * * 1");
10286        assert_eq!(digest["session_target"], "main");
10287        assert_eq!(digest["payload"]["kind"], "system_event");
10288
10289        // Claude Code: session-scoped, one recurring cron and one one-shot wakeup.
10290        let cron = job_row(result, "release-watch");
10291        assert_eq!(cron["harness"], "claude-code");
10292        assert_eq!(cron["scope"], "session");
10293        assert_eq!(cron["session_id"], "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f");
10294        assert_eq!(cron["schedule"]["kind"], "cron");
10295        assert_eq!(cron["schedule"]["expr"], "*/10 * * * *");
10296        assert_eq!(cron["schedule"]["display"], "*/10 * * * *");
10297        assert_eq!(cron["payload"]["kind"], "prompt");
10298        assert_eq!(cron["recurring"], true);
10299        assert_eq!(cron["deliver"]["target"], "session");
10300        let wakeup = job_row(result, "toolu_wake_recheck");
10301        assert_eq!(wakeup["payload"]["kind"], "wakeup");
10302        assert_eq!(wakeup["schedule"]["kind"], "once");
10303        assert_eq!(wakeup["recurring"], false);
10304        assert_eq!(wakeup["state"], "pending");
10305
10306        // Hermes: install-scoped, interval + origin delivery, and a paused cron.
10307        let interval = job_row(result, "digest-15m");
10308        assert_eq!(interval["harness"], "hermes");
10309        assert_eq!(interval["scope"], "install");
10310        assert_eq!(interval["profile"], Value::Null);
10311        assert_eq!(interval["schedule"]["kind"], "interval");
10312        assert_eq!(interval["schedule"]["minutes"], 15.0);
10313        assert_eq!(interval["schedule"]["display"], "every 15 min");
10314        assert_eq!(interval["deliver"]["target"], "origin");
10315        assert_eq!(interval["deliver"]["chat_id"], "-1002233445566");
10316        assert_eq!(interval["next_run_at"], "2026-09-02T11:15:00Z");
10317        assert_eq!(interval["last_status"], "ok");
10318        let nightly = job_row(result, "nightly-audit");
10319        assert_eq!(nightly["schedule"]["expr"], "0 3 * * *");
10320        assert_eq!(nightly["deliver"]["target"], "local");
10321        assert_eq!(nightly["enabled"], false);
10322        assert_eq!(nightly["state"], "paused");
10323        // The per-profile store carries the profile name from its own path.
10324        let profiled = job_row(result, "ops-once-boot");
10325        assert_eq!(profiled["profile"], "ops");
10326        assert_eq!(profiled["schedule"]["kind"], "once");
10327        assert_eq!(profiled["schedule"]["run_at"], "2026-09-03T06:00:00Z");
10328        assert_eq!(profiled["payload"]["kind"], "script");
10329        // An explicit `<platform>:<chat>` target carries the chat itself.
10330        assert_eq!(profiled["deliver"]["target"], "slack:C0429ABCD");
10331        assert_eq!(profiled["deliver"]["chat_id"], "C0429ABCD");
10332        assert_eq!(profiled["recurring"], false);
10333
10334        // ORCH-13: a job delivering to its creating conversation carries that
10335        // conversation's whole surface — platform word, chat AND thread.
10336        let standup_to_group = job_row(result, "coder-standup");
10337        assert_eq!(standup_to_group["deliver"]["target"], "origin");
10338        assert_eq!(standup_to_group["deliver"]["chat_id"], "-100777");
10339        assert_eq!(standup_to_group["deliver"]["thread_id"], "55");
10340        // Hermes has no mode word and routes by adapter profile, not account.
10341        assert!(standup_to_group["deliver"]["mode"].is_null());
10342        assert!(standup_to_group["deliver"]["account"].is_null());
10343
10344        // OpenClaw: the session target and the delivery mode are the row's own
10345        // columns, not a footnote.
10346        let standup = job_row(result, "cron_standup");
10347        assert_eq!(standup["harness"], "openclaw");
10348        assert_eq!(standup["session_target"], "isolated");
10349        assert_eq!(standup["deliver"]["mode"], "announce");
10350        assert_eq!(standup["deliver"]["target"], "slack");
10351        assert_eq!(standup["deliver"]["chat_id"], "C0429ABCD");
10352        assert_eq!(standup["payload"]["kind"], "prompt");
10353        assert_eq!(standup["profile"], "main");
10354        let reindex = job_row(result, "cron_reindex");
10355        assert_eq!(reindex["session_target"], "main");
10356        assert_eq!(reindex["payload"]["kind"], "system_event");
10357        assert_eq!(reindex["schedule"]["kind"], "interval");
10358        assert_eq!(reindex["schedule"]["display"], "every 240 min");
10359        assert_eq!(reindex["enabled"], false);
10360
10361        // Every store consulted is named, so an empty answer is never silent.
10362        let states: Vec<(&str, &str)> = result["sources"]
10363            .as_array()
10364            .unwrap()
10365            .iter()
10366            .map(|source| {
10367                (
10368                    source["harness"].as_str().unwrap(),
10369                    source["state"].as_str().unwrap(),
10370                )
10371            })
10372            .collect();
10373        // The `coder` profile home has no cron store at all: it is named as
10374        // `absent_store`, not skipped, so "this profile schedules nothing" and
10375        // "this profile was never looked at" stay distinguishable.
10376        assert_eq!(
10377            states,
10378            vec![
10379                ("claude-code", "scanned"),
10380                ("hermes", "read"),
10381                ("hermes", "absent_store"),
10382                ("hermes", "read"),
10383                ("openclaw", "read"),
10384                ("openclaw", "read"),
10385            ],
10386            "{result}"
10387        );
10388    }
10389
10390    #[test]
10391    fn jobs_list_filters_by_harness_session_and_profile() {
10392        let by_harness = jobs_list(json!({"harness": "openclaw", "homes": jobs_fixture_homes()}));
10393        let ids: Vec<&str> = by_harness["result"]["jobs"]
10394            .as_array()
10395            .unwrap()
10396            .iter()
10397            .map(|job| job["id"].as_str().unwrap())
10398            .collect();
10399        assert_eq!(
10400            ids,
10401            vec![
10402                "85ad7832-896f-42be-af31-3e1ed2fbdc4b",
10403                "8bb7d938-ca46-4a6d-90eb-c92331155566",
10404                "cron_standup",
10405                "cron_reindex",
10406            ]
10407        );
10408
10409        let by_session = jobs_list(json!({
10410            "session": "7c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
10411            "homes": jobs_fixture_homes(),
10412        }));
10413        let jobs = by_session["result"]["jobs"].as_array().unwrap();
10414        assert_eq!(jobs.len(), 2, "{by_session}");
10415        assert!(jobs
10416            .iter()
10417            .all(|job| job["harness"] == "claude-code" && job["scope"] == "session"));
10418
10419        let by_profile = jobs_list(json!({
10420            "harness": "hermes",
10421            "profile": "ops",
10422            "homes": jobs_fixture_homes(),
10423        }));
10424        let jobs = by_profile["result"]["jobs"].as_array().unwrap();
10425        assert_eq!(jobs.len(), 1, "{by_profile}");
10426        assert_eq!(jobs[0]["id"], "ops-once-boot");
10427    }
10428
10429    #[test]
10430    fn jobs_get_answers_with_the_row_and_the_verbatim_native_record() {
10431        let mut service = HarnessSessionService::new();
10432        let hermes = service.handle(request(
10433            1,
10434            "harness.v1.jobs.get",
10435            json!({"harness": "hermes", "id": "digest-15m", "homes": jobs_fixture_homes()}),
10436        ));
10437        assert_eq!(hermes["result"]["job"]["schedule"]["kind"], "interval");
10438        // Native fields the uniform row does not carry survive on `source`.
10439        assert_eq!(hermes["result"]["source"]["provider"], "nous");
10440        assert_eq!(hermes["result"]["source"]["failure_deliver"], "local");
10441
10442        let claude = service.handle(request(
10443            2,
10444            "harness.v1.jobs.get",
10445            json!({"harness": "claude-code", "id": "release-watch", "homes": jobs_fixture_homes()}),
10446        ));
10447        assert_eq!(claude["result"]["job"]["payload"]["kind"], "prompt");
10448        assert_eq!(
10449            claude["result"]["source"]["tool_use_id"],
10450            "toolu_cron_release_watch"
10451        );
10452
10453        let missing = service.handle(request(
10454            3,
10455            "harness.v1.jobs.get",
10456            json!({"harness": "hermes", "id": "no-such-job", "homes": jobs_fixture_homes()}),
10457        ));
10458        assert!(missing["error"]["message"]
10459            .as_str()
10460            .is_some_and(|message| message.contains("no scheduled job `no-such-job`")));
10461    }
10462
10463    #[test]
10464    fn jobs_refuse_a_harness_without_a_scheduled_job_concept() {
10465        let mut service = HarnessSessionService::new();
10466        for (id, method, params) in [
10467            (
10468                1,
10469                "harness.v1.jobs.list",
10470                json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10471            ),
10472            (
10473                2,
10474                "harness.v1.jobs.get",
10475                json!({"harness": "codex", "id": "anything"}),
10476            ),
10477        ] {
10478            let response = service.handle(request(id, method, params));
10479            assert_eq!(response["error"]["code"], -32020, "{response}");
10480            assert!(response["error"]["message"]
10481                .as_str()
10482                .is_some_and(|message| message.contains("has no scheduled jobs")));
10483            assert!(response.get("result").is_none());
10484        }
10485    }
10486
10487    #[test]
10488    fn jobs_list_reports_a_migrated_openclaw_store_as_absent_instead_of_failing() {
10489        let scratch = std::env::temp_dir().join(format!(
10490            "supercode-jobs-migrated-{}-{}",
10491            std::process::id(),
10492            generated_session_id()
10493        ));
10494        std::fs::create_dir_all(&scratch).unwrap();
10495        let response = jobs_list(json!({
10496            "harness": "openclaw",
10497            "homes": {"openclaw": scratch.clone()},
10498        }));
10499        let result = &response["result"];
10500        assert_eq!(result["jobs"].as_array().unwrap().len(), 0, "{result}");
10501        assert_eq!(result["sources"][0]["state"], "absent_store");
10502        assert_eq!(result["sources"][0]["harness"], "openclaw");
10503        std::fs::remove_dir_all(&scratch).ok();
10504    }
10505
10506    // ---------------------------------------------------------------------
10507    // ORCH-8 — `harness.v1.runs.list` / `runs.get` over the committed fire
10508    // stores: Hermes's `cron/executions.db` (root home + profile home) and
10509    // OpenClaw's `cron_run_logs`. Every fixture row is written by
10510    // `tests/fixtures/gen_runs_fixtures.py` against the harnesses' own DDL.
10511    // ---------------------------------------------------------------------
10512
10513    /// The health job in the committed OpenClaw fixture, which fired twice.
10514    const OPENCLAW_HEALTH_JOB: &str = "85ad7832-896f-42be-af31-3e1ed2fbdc4b";
10515    /// The digest job, whose single fire predates run ids.
10516    const OPENCLAW_DIGEST_JOB: &str = "8bb7d938-ca46-4a6d-90eb-c92331155566";
10517
10518    fn runs_list(params: Value) -> Value {
10519        let mut service = HarnessSessionService::new();
10520        service.handle(request(1, "harness.v1.runs.list", params))
10521    }
10522
10523    fn run_row<'a>(result: &'a Value, id: &str) -> &'a Value {
10524        result["runs"]
10525            .as_array()
10526            .expect("runs is an array")
10527            .iter()
10528            .find(|run| run["id"] == id)
10529            .unwrap_or_else(|| panic!("no run `{id}` in {result}"))
10530    }
10531
10532    #[test]
10533    fn runs_list_projects_both_fixture_stores_onto_the_uniform_row() {
10534        let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10535        let result = &response["result"];
10536        let ids: Vec<&str> = result["runs"]
10537            .as_array()
10538            .expect("runs is an array")
10539            .iter()
10540            .map(|run| run["id"].as_str().unwrap())
10541            .collect();
10542        let digest_fire = format!("{OPENCLAW_DIGEST_JOB}#1");
10543        assert_eq!(
10544            ids,
10545            vec![
10546                // Hermes, newest claim first, root ledger then profile ledger.
10547                "b2c3d4e5f60718293a4b5c6d7e8f9012",
10548                "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10549                "c3d4e5f60718293a4b5c6d7e8f901234",
10550                "f60718293a4b5c6d7e8f901234567890",
10551                "e5f60718293a4b5c6d7e8f9012345678",
10552                "d4e5f60718293a4b5c6d7e8f90123456",
10553                // OpenClaw, newest `ts` first.
10554                "run_health_0002",
10555                digest_fire.as_str(),
10556                "run_health_0001",
10557            ],
10558            "{result}"
10559        );
10560
10561        // The harness's OWN outcome word survives; nothing is renamed onto a
10562        // shared vocabulary.
10563        let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10564        assert_eq!(failed["harness"], "hermes");
10565        assert_eq!(failed["job_id"], "job42");
10566        assert_eq!(failed["status"], "failed");
10567        assert_eq!(failed["error"], "provider returned 500 after 3 attempts");
10568        assert_eq!(failed["claimed_at"], "2026-09-02T13:05:00.100442");
10569
10570        // Hermes's `unknown` — an attempt whose owner died before writing a
10571        // terminal state — is a fourth status, not folded into `failed`.
10572        let abandoned = run_row(result, "d4e5f60718293a4b5c6d7e8f90123456");
10573        assert_eq!(abandoned["status"], "unknown");
10574        assert_eq!(abandoned["job_id"], "ops-once-boot");
10575
10576        // An unterminated fire has no finish, and no session is invented.
10577        let running = run_row(result, "c3d4e5f60718293a4b5c6d7e8f901234");
10578        assert_eq!(running["status"], "running");
10579        assert!(running["finished_at"].is_null(), "{running}");
10580        assert!(running["session_id"].is_null(), "{running}");
10581
10582        // OpenClaw records the session on the row itself, and epoch-ms
10583        // timestamps are rendered as RFC 3339.
10584        let ok = run_row(result, "run_health_0001");
10585        assert_eq!(ok["harness"], "openclaw");
10586        assert_eq!(ok["job_id"], OPENCLAW_HEALTH_JOB);
10587        assert_eq!(ok["status"], "ok");
10588        assert_eq!(ok["started_at"], "2026-09-02T08:30:00.000Z");
10589        assert_eq!(ok["finished_at"], "2026-09-02T08:30:30.000Z");
10590        assert_eq!(ok["session_id"], "3dd577ae-a0a3-4b5b-8063-f402be4f5fd4");
10591        // OpenClaw's run log is written once, at finish: there is no claim.
10592        assert!(ok["claimed_at"].is_null(), "{ok}");
10593
10594        // A run-log row with no `run_id` falls back to the store's own
10595        // `(job_id, seq)` key rather than being dropped.
10596        assert_eq!(run_row(result, &digest_fire)["status"], "skipped");
10597
10598        // ORCH-13: a fire whose delivery nothing recorded says so, rather than
10599        // borrowing a neighbouring fire's outcome. Both of these ran on jobs
10600        // that deliver `local` (or have no job record at all), so no
10601        // obligation is addressed to a surface they could match.
10602        for id in [
10603            "b2c3d4e5f60718293a4b5c6d7e8f9012",
10604            "d4e5f60718293a4b5c6d7e8f90123456",
10605        ] {
10606            assert!(run_row(result, id)["delivery"].is_null(), "{id}");
10607        }
10608
10609        // Every store consulted is named, including the profile home that has
10610        // no ledger — an empty history and an absent store are different.
10611        let sources = result["sources"].as_array().unwrap();
10612        let states: Vec<(&str, &str)> = sources
10613            .iter()
10614            .map(|source| {
10615                (
10616                    source["harness"].as_str().unwrap(),
10617                    source["state"].as_str().unwrap(),
10618                )
10619            })
10620            .collect();
10621        assert_eq!(
10622            states,
10623            vec![
10624                ("hermes", "read"),
10625                ("hermes", "absent_store"),
10626                ("hermes", "read"),
10627                ("openclaw", "read"),
10628            ],
10629            "{result}"
10630        );
10631        assert_eq!(sources[2]["profile"], "ops");
10632        assert!(sources[3]["path"]
10633            .as_str()
10634            .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10635    }
10636
10637    #[test]
10638    fn runs_list_joins_a_hermes_fire_to_the_session_it_opened() {
10639        let response = runs_list(json!({
10640            "harness": "hermes",
10641            "job": "job42",
10642            "homes": jobs_fixture_homes(),
10643        }));
10644        let result = &response["result"];
10645        assert_eq!(result["runs"].as_array().unwrap().len(), 2, "{result}");
10646
10647        // Hermes writes NO link from an execution to its session. The fire
10648        // that ran the agent is joined to `cron_job42_<stamp>` because that
10649        // id's instant falls inside its [claimed_at, finished_at] window.
10650        let ran = run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90");
10651        assert_eq!(ran["session_id"], "cron_job42_20260902_120000");
10652
10653        // The later fire failed before opening one. Its window holds no
10654        // session, so the row says so instead of re-using the earlier fire's
10655        // — the join is per-FIRE, not per-job.
10656        let failed = run_row(result, "b2c3d4e5f60718293a4b5c6d7e8f9012");
10657        assert!(failed["session_id"].is_null(), "{failed}");
10658    }
10659
10660    /// ORCH-13: where a fire's output went, read from each harness's own
10661    /// delivery record — Hermes's `delivery_obligations` ledger inside
10662    /// `state.db`, OpenClaw's `delivery_*` run-log columns.
10663    #[test]
10664    fn runs_list_reads_the_delivery_each_harness_recorded_for_a_fire() {
10665        let response = runs_list(json!({"homes": jobs_fixture_homes()}));
10666        let result = &response["result"];
10667
10668        // Hermes: the ledger is the GATEWAY's, keyed by conversation and
10669        // surface, so the fire's own [claimed_at, finished_at] window picks
10670        // the obligation. The fire succeeded and so did the send.
10671        let delivered = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10672        assert_eq!(delivered["status"], "completed");
10673        assert_eq!(delivered["delivery"]["state"], "delivered");
10674        assert_eq!(delivered["delivery"]["target"], "telegram:-100777:55");
10675        assert_eq!(delivered["delivery"]["attempts"], 1);
10676        assert!(delivered["delivery"]["last_error"].is_null(), "{delivered}");
10677        assert_eq!(
10678            delivered["delivery"]["delivered_at"],
10679            "2026-09-02T09:00:30.400Z"
10680        );
10681
10682        // The next fire of the same job ALSO succeeded — and its output never
10683        // arrived. That is the fact `status` alone cannot carry.
10684        let undelivered = run_row(result, "f60718293a4b5c6d7e8f901234567890");
10685        assert_eq!(undelivered["status"], "completed");
10686        assert_eq!(undelivered["delivery"]["state"], "failed");
10687        assert_eq!(undelivered["delivery"]["attempts"], 3);
10688        assert_eq!(
10689            undelivered["delivery"]["last_error"],
10690            "telegram send failed: Bad Request: chat not found"
10691        );
10692        // Only a delivered obligation carries an instant of delivery; the
10693        // ledger's `updated_at` on a failed row dates the failure.
10694        assert!(
10695            undelivered["delivery"]["delivered_at"].is_null(),
10696            "{undelivered}"
10697        );
10698
10699        // OpenClaw writes the outcome onto the run-log row and declares the
10700        // address on the job, so the row's target is joined from `cron_jobs`.
10701        let announced = run_row(result, "run_health_0001");
10702        assert_eq!(announced["delivery"]["state"], "delivered");
10703        assert_eq!(announced["delivery"]["target"], "last");
10704        // Its run log counts no attempts and stamps no delivered-at.
10705        assert!(announced["delivery"]["attempts"].is_null(), "{announced}");
10706        assert!(
10707            announced["delivery"]["delivered_at"].is_null(),
10708            "{announced}"
10709        );
10710        let refused = run_row(result, "run_health_0002");
10711        assert_eq!(refused["delivery"]["state"], "not-delivered");
10712        assert_eq!(refused["delivery"]["last_error"], "channel_not_found");
10713
10714        // A run-log row with no delivery columns at all recorded no delivery:
10715        // the job's declared target is not evidence that anything was sent.
10716        let skipped = run_row(result, &format!("{OPENCLAW_DIGEST_JOB}#1"));
10717        assert!(skipped["delivery"].is_null(), "{skipped}");
10718    }
10719
10720    /// A Hermes fire whose session carries a `session_key` is matched on that
10721    /// key FIRST — the most specific question the ledger can answer. Proven by
10722    /// moving the obligations off the job's surface on a COPY of the fixture,
10723    /// so only the session-key question can still find them.
10724    #[test]
10725    fn runs_list_matches_a_hermes_obligation_by_the_session_key_first() {
10726        let scratch = std::env::temp_dir().join(format!(
10727            "supercode-runs-delivery-{}-{}",
10728            std::process::id(),
10729            generated_session_id()
10730        ));
10731        std::fs::create_dir_all(scratch.join("cron")).unwrap();
10732        let fixture = jobs_fixture_root().join("hermes_home");
10733        std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10734        for name in ["cron/executions.db", "cron/jobs.json"] {
10735            std::fs::copy(fixture.join(name), scratch.join(name)).unwrap();
10736        }
10737        {
10738            let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10739            // The obligations now sit on a surface no job in this store
10740            // delivers to, so the surface question cannot match them.
10741            connection
10742                .execute(
10743                    "UPDATE delivery_obligations SET platform = 'slack', chat_id = 'C0FALLBACK'",
10744                    [],
10745                )
10746                .unwrap();
10747            // A cron fire that ran inside a keyed conversation: the session
10748            // the window recovers carries `tg-coder-1`'s key.
10749            connection
10750                .execute(
10751                    "INSERT INTO sessions (id, source, session_key, started_at) VALUES \
10752                     ('cron_coder-standup_20260902_090010', 'cron', \
10753                      'agent:coder:telegram:group:-100777:55', 1788339610.0)",
10754                    [],
10755                )
10756                .unwrap();
10757        }
10758        let response = runs_list(json!({
10759            "harness": "hermes",
10760            "job": "coder-standup",
10761            "homes": {"hermes": scratch.join("state.db")},
10762        }));
10763        let result = &response["result"];
10764        let matched = run_row(result, "e5f60718293a4b5c6d7e8f9012345678");
10765        assert_eq!(
10766            matched["session_id"], "cron_coder-standup_20260902_090010",
10767            "{result}"
10768        );
10769        assert_eq!(matched["delivery"]["state"], "delivered", "{result}");
10770        assert_eq!(
10771            matched["delivery"]["target"], "slack:C0FALLBACK:55",
10772            "{result}"
10773        );
10774        std::fs::remove_dir_all(&scratch).ok();
10775    }
10776
10777    #[test]
10778    fn runs_list_follows_a_compression_chain_to_the_readable_tip() {
10779        // A fire whose session was compressed mid-run is only readable at the
10780        // continuation, so that is what the row must report. Built on a COPY
10781        // of the committed fixture: no test writes to a fixture or to a real
10782        // harness home.
10783        let scratch = std::env::temp_dir().join(format!(
10784            "supercode-runs-compressed-{}-{}",
10785            std::process::id(),
10786            generated_session_id()
10787        ));
10788        std::fs::create_dir_all(scratch.join("cron")).unwrap();
10789        let fixture = jobs_fixture_root().join("hermes_home");
10790        std::fs::copy(fixture.join("state.db"), scratch.join("state.db")).unwrap();
10791        std::fs::copy(
10792            fixture.join("cron/executions.db"),
10793            scratch.join("cron/executions.db"),
10794        )
10795        .unwrap();
10796        {
10797            let connection = rusqlite::Connection::open(scratch.join("state.db")).unwrap();
10798            connection
10799                .execute(
10800                    "UPDATE sessions SET end_reason = 'compression' WHERE id = ?1",
10801                    ["cron_job42_20260902_120000"],
10802                )
10803                .unwrap();
10804            connection
10805                .execute(
10806                    "INSERT INTO sessions (id, source, parent_session_id, started_at) \
10807                     VALUES ('job42-after-compaction', 'cron', \
10808                             'cron_job42_20260902_120000', 1788350000.0)",
10809                    [],
10810                )
10811                .unwrap();
10812        }
10813        let response = runs_list(json!({
10814            "harness": "hermes",
10815            "job": "job42",
10816            "homes": {"hermes": scratch.join("state.db")},
10817        }));
10818        let result = &response["result"];
10819        assert_eq!(
10820            run_row(result, "a1b2c3d4e5f60718293a4b5c6d7e8f90")["session_id"],
10821            "job42-after-compaction",
10822            "{result}"
10823        );
10824        std::fs::remove_dir_all(&scratch).ok();
10825    }
10826
10827    #[test]
10828    fn runs_list_filters_by_job_and_caps_by_limit() {
10829        let by_job = runs_list(json!({
10830            "harness": "openclaw",
10831            "job": OPENCLAW_HEALTH_JOB,
10832            "homes": jobs_fixture_homes(),
10833        }));
10834        let ids: Vec<&str> = by_job["result"]["runs"]
10835            .as_array()
10836            .unwrap()
10837            .iter()
10838            .map(|run| run["id"].as_str().unwrap())
10839            .collect();
10840        assert_eq!(ids, vec!["run_health_0002", "run_health_0001"], "{by_job}");
10841
10842        let capped = runs_list(json!({
10843            "harness": "openclaw",
10844            "limit": 1,
10845            "homes": jobs_fixture_homes(),
10846        }));
10847        let runs = capped["result"]["runs"].as_array().unwrap();
10848        assert_eq!(runs.len(), 1, "{capped}");
10849        // Newest first, so the cap keeps the recent fire.
10850        assert_eq!(runs[0]["id"], "run_health_0002");
10851    }
10852
10853    #[test]
10854    fn runs_get_answers_with_the_row_and_the_verbatim_native_record() {
10855        let mut service = HarnessSessionService::new();
10856        let hermes = service.handle(request(
10857            1,
10858            "harness.v1.runs.get",
10859            json!({
10860                "harness": "hermes",
10861                "id": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
10862                "homes": jobs_fixture_homes(),
10863            }),
10864        ));
10865        assert_eq!(hermes["result"]["run"]["status"], "completed");
10866        assert_eq!(
10867            hermes["result"]["run"]["session_id"],
10868            "cron_job42_20260902_120000"
10869        );
10870        // Ledger columns the uniform row does not carry survive on `source`.
10871        assert_eq!(hermes["result"]["source"]["source"], "scheduler");
10872        assert_eq!(hermes["result"]["source"]["pid"], 4242);
10873        assert_eq!(hermes["result"]["source"]["process_id"], "9f1c2d");
10874
10875        let openclaw = service.handle(request(
10876            2,
10877            "harness.v1.runs.get",
10878            json!({
10879                "harness": "openclaw",
10880                "id": "run_health_0002",
10881                "homes": jobs_fixture_homes(),
10882            }),
10883        ));
10884        assert_eq!(openclaw["result"]["run"]["status"], "error");
10885        // ORCH-13: the run's delivery is projected AND the store's own columns
10886        // stay verbatim on `source`, so nothing about the fire is lost.
10887        assert_eq!(
10888            openclaw["result"]["source"]["delivery_status"],
10889            "not-delivered"
10890        );
10891        assert_eq!(
10892            openclaw["result"]["source"]["delivery_error"],
10893            "channel_not_found"
10894        );
10895        assert_eq!(openclaw["result"]["source"]["delivered"], 0);
10896        assert_eq!(
10897            openclaw["result"]["run"]["delivery"]["state"],
10898            "not-delivered"
10899        );
10900        assert_eq!(
10901            openclaw["result"]["run"]["delivery"]["last_error"],
10902            "channel_not_found"
10903        );
10904
10905        let missing = service.handle(request(
10906            3,
10907            "harness.v1.runs.get",
10908            json!({"harness": "hermes", "id": "no-such-run", "homes": jobs_fixture_homes()}),
10909        ));
10910        assert!(missing["error"]["message"]
10911            .as_str()
10912            .is_some_and(|message| message.contains("no run `no-such-run`")));
10913    }
10914
10915    #[test]
10916    fn runs_refuse_a_harness_that_keeps_no_run_store() {
10917        let mut service = HarnessSessionService::new();
10918        for (id, method, params) in [
10919            // Claude Code HAS scheduled jobs but no fire store: its fires are
10920            // ordinary turns. It must refuse, not answer with an empty list.
10921            (
10922                1,
10923                "harness.v1.runs.list",
10924                json!({"harness": "claude-code", "homes": jobs_fixture_homes()}),
10925            ),
10926            (
10927                2,
10928                "harness.v1.runs.get",
10929                json!({"harness": "claude-code", "id": "anything"}),
10930            ),
10931            (
10932                3,
10933                "harness.v1.runs.list",
10934                json!({"harness": "codex", "homes": jobs_fixture_homes()}),
10935            ),
10936        ] {
10937            let response = service.handle(request(id, method, params));
10938            assert_eq!(response["error"]["code"], -32020, "{response}");
10939            assert!(response["error"]["message"]
10940                .as_str()
10941                .is_some_and(|message| message.contains("keeps no run store")));
10942            assert!(response.get("result").is_none());
10943        }
10944    }
10945
10946    #[test]
10947    fn runs_list_reports_an_install_with_no_run_store_as_absent() {
10948        let scratch = std::env::temp_dir().join(format!(
10949            "supercode-runs-empty-{}-{}",
10950            std::process::id(),
10951            generated_session_id()
10952        ));
10953        std::fs::create_dir_all(&scratch).unwrap();
10954        let response = runs_list(json!({
10955            "harness": "openclaw",
10956            "homes": {"openclaw": scratch.clone()},
10957        }));
10958        let result = &response["result"];
10959        assert_eq!(result["runs"].as_array().unwrap().len(), 0, "{result}");
10960        assert_eq!(result["sources"][0]["state"], "absent_store");
10961        assert!(result["sources"][0]["path"]
10962            .as_str()
10963            .is_some_and(|path| path.ends_with("state/openclaw.sqlite")));
10964        std::fs::remove_dir_all(&scratch).ok();
10965    }
10966}