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