1use std::collections::{BTreeMap, HashMap};
33use std::path::{Path, PathBuf};
34use std::time::{Duration, Instant};
35
36use serde::{Deserialize, Serialize};
37use serde_json::{json, Value};
38
39use crate::claude_peer::{read_registry, registry_dir, ClaudePeerSession};
40use crate::mailbox::{mail_root, Envelope, MailAddress, MailKind, ReplyVia};
41use crate::HarnessHomes;
42
43pub const RELAY_MODEL: &str = "haiku";
46
47pub const RELAY_NAME_PREFIX: &str = "sc-";
49
50pub const RELAY_STATUS_LINE: &str = "relay, not the session's status";
53
54pub const SEND_TIMEOUT: Duration = Duration::from_secs(90);
56
57const RELAY_TOOLS: &str = "ListAgents,SendMessage";
59
60const NATIVE_PEER_PREAMBLE: &str = "Another Claude session sent a message:";
64const NATIVE_PEER_OPENING: &str = "<cross-session-message ";
66const NATIVE_PEER_CLOSING: &str = "</cross-session-message>";
67const NATIVE_IDLE_NOTICE: &str = "[Cross-session idle notice]";
68const NATIVE_DELIVERY_NOTICE: &str = "[Cross-session delivery notice]";
69
70pub fn relay_name(name: &str, machine: &str) -> String {
75 let base = name.strip_prefix(RELAY_NAME_PREFIX).unwrap_or(name);
76 format!("{RELAY_NAME_PREFIX}{base}-on-{machine}")
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct QueuedSend {
82 pub to: String,
84 pub message: String,
86}
87
88pub fn gate_decision(hook_input: &Value, queued: Option<&QueuedSend>) -> Option<String> {
94 let tool = hook_input
95 .get("tool_name")
96 .and_then(Value::as_str)
97 .unwrap_or_default();
98 if tool == "ListAgents" {
99 return None;
100 }
101 if tool != "SendMessage" {
102 return Some(format!("a relay may not use {tool}"));
103 }
104 let Some(queued) = queued else {
105 return Some(
106 "nothing is queued to send; mail for the host is not answered by the relay".into(),
107 );
108 };
109 let input = hook_input.get("tool_input").cloned().unwrap_or(Value::Null);
110 let to = input.get("to").and_then(Value::as_str).unwrap_or_default();
111 let message = input
112 .get("message")
113 .and_then(Value::as_str)
114 .unwrap_or_default();
115 if to != queued.to {
116 return Some(format!(
117 "not the queued outbound message: `to` is {to:?}, the queue says {:?}",
118 queued.to
119 ));
120 }
121 if message != queued.message {
122 let at = message
123 .char_indices()
124 .zip(queued.message.chars())
125 .find(|((_, sent), queued)| sent != queued)
126 .map(|((index, _), _)| index)
127 .unwrap_or_else(|| message.len().min(queued.message.len()));
128 return Some(format!(
129 "not the queued outbound message: `message` ({} bytes) differs from the queue ({} \
130 bytes) at byte {at}: sent {:?}, queued {:?}",
131 message.len(),
132 queued.message.len(),
133 message
134 .get(at..)
135 .unwrap_or_default()
136 .chars()
137 .take(40)
138 .collect::<String>(),
139 queued
140 .message
141 .get(at..)
142 .unwrap_or_default()
143 .chars()
144 .take(40)
145 .collect::<String>(),
146 ));
147 }
148 None
149}
150
151pub fn gate_denial(reason: &str) -> Value {
153 json!({
154 "hookSpecificOutput": {
155 "hookEventName": "PreToolUse",
156 "permissionDecision": "deny",
157 "permissionDecisionReason": reason,
158 }
159 })
160}
161
162#[derive(Debug, Clone)]
165pub struct RelayPaths {
166 pub directory: PathBuf,
168 pub queue: PathBuf,
170 pub receipt: PathBuf,
172 pub turn: PathBuf,
175 pub settings: PathBuf,
177 pub record: PathBuf,
179 pub sent: PathBuf,
181 pub lock: PathBuf,
183}
184
185impl RelayPaths {
186 pub fn new(root: &Path, relay_name: &str) -> Self {
188 let hash = blake3::hash(relay_name.as_bytes()).to_hex();
189 Self::in_directory(root.join("relays").join(&hash[..24]))
190 }
191
192 pub fn in_directory(directory: PathBuf) -> Self {
194 Self {
195 queue: directory.join("queue.json"),
196 receipt: directory.join("receipt.json"),
197 turn: directory.join("turn.txt"),
198 settings: directory.join("settings.json"),
199 record: directory.join("relay.json"),
200 sent: directory.join("sent.json"),
201 lock: directory.join("send.lock"),
202 directory,
203 }
204 }
205
206 pub fn last_sent(&self) -> HashMap<String, String> {
208 std::fs::read(&self.sent)
209 .ok()
210 .and_then(|bytes| serde_json::from_slice(&bytes).ok())
211 .unwrap_or_default()
212 }
213}
214
215#[derive(Debug, Clone)]
217pub struct RelaySpec {
218 pub represented: MailAddress,
220 pub represented_name: String,
222 pub name: String,
224 pub paths: RelayPaths,
226 pub program: PathBuf,
228}
229
230impl RelaySpec {
231 pub fn for_sender(represented: &MailAddress, represented_name: &str) -> std::io::Result<Self> {
233 let base = represented_name
234 .split('@')
235 .next()
236 .unwrap_or(represented_name);
237 let name = relay_name(base, &represented.machine);
238 Ok(Self {
239 represented: represented.clone(),
240 represented_name: represented_name.to_string(),
241 paths: RelayPaths::new(&mail_root(), &name),
242 name,
243 program: supercode_program()?,
244 })
245 }
246}
247
248pub fn relay_arguments(spec: &RelaySpec) -> Vec<String> {
254 vec![
255 "--print".into(),
256 "--input-format".into(),
257 "stream-json".into(),
258 "--output-format".into(),
259 "stream-json".into(),
260 "--verbose".into(),
261 "--permission-prompt-tool".into(),
262 "stdio".into(),
263 "--model".into(),
264 RELAY_MODEL.into(),
265 "--name".into(),
266 spec.name.clone(),
267 "--permission-mode".into(),
268 "bypassPermissions".into(),
269 "--setting-sources".into(),
270 "project".into(),
271 "--settings".into(),
272 spec.paths.settings.to_string_lossy().into_owned(),
273 "--tools".into(),
274 RELAY_TOOLS.into(),
275 "--no-session-persistence".into(),
276 ]
277}
278
279pub fn relay_environment(spec: &RelaySpec, endpoint: &str) -> BTreeMap<String, String> {
283 let key = spec
284 .paths
285 .directory
286 .file_name()
287 .map(|name| name.to_string_lossy().into_owned())
288 .unwrap_or_default();
289 BTreeMap::from([
290 ("ANTHROPIC_BASE_URL".to_string(), endpoint.to_string()),
291 ("ANTHROPIC_API_KEY".to_string(), key),
292 (
293 "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC".to_string(),
294 "1".to_string(),
295 ),
296 ])
297}
298
299pub fn relay_settings(spec: &RelaySpec) -> Value {
301 let program = shell_quote(&spec.program.to_string_lossy());
302 let directory = shell_quote(&spec.paths.directory.to_string_lossy());
303 let represented = shell_quote(&spec.represented.to_string());
304 let command = |verb: &str| format!("{program} message {verb} {directory}");
305 json!({
306 "hooks": {
307 "PreToolUse": [{
309 "matcher": ".*",
310 "hooks": [{"type": "command", "command": command("gate")}],
311 }],
312 "PostToolUse": [{
313 "matcher": "SendMessage",
314 "hooks": [{"type": "command", "command": command("relay-receipt")}],
315 }],
316 "PostToolUseFailure": [{
318 "matcher": "SendMessage",
319 "hooks": [{"type": "command", "command": command("relay-receipt")}],
320 }],
321 "Stop": [{
322 "hooks": [{"type": "command", "command": command("relay-receipt")}],
323 }],
324 "StopFailure": [{
326 "hooks": [{"type": "command", "command": command("relay-receipt")}],
327 }],
328 "UserPromptSubmit": [{
329 "hooks": [{
330 "type": "command",
331 "command": format!("{program} message relay-inbound {represented} {directory}"),
332 }],
333 }],
334 }
335 })
336}
337
338fn shell_quote(value: &str) -> String {
339 format!("'{}'", value.replace('\'', "'\\''"))
340}
341
342pub fn is_inbound_prompt(prompt: &str) -> bool {
345 let trimmed = prompt.trim_start();
346 trimmed.starts_with(NATIVE_PEER_OPENING)
347 || trimmed.starts_with(NATIVE_IDLE_NOTICE)
348 || trimmed.starts_with(NATIVE_DELIVERY_NOTICE)
349}
350
351pub fn inbound_block() -> Value {
353 json!({
354 "decision": "block",
355 "reason": "filed in the mailbox of the session this relay speaks for",
356 })
357}
358
359pub fn send_turn(send: &QueuedSend) -> String {
361 format!(
362 "Send one message with SendMessage.\n\
363 to: {}\n\
364 message: the exact text between the markers, without the markers\n\
365 ---BEGIN MESSAGE---\n{}\n---END MESSAGE---",
366 send.to, send.message
367 )
368}
369
370#[derive(Debug, Clone, PartialEq, Eq)]
372pub struct NativeEnvelope {
373 pub from: String,
375 pub from_name: Option<String>,
377 pub body: String,
379}
380
381#[derive(Debug, Clone, PartialEq, Eq)]
383pub enum RelayEvent {
384 Peer(NativeEnvelope),
386 IdleNotice(String),
388 DeliveryNotice(String),
390}
391
392pub fn resolve_native_sender(
394 registry: &[ClaudePeerSession],
395 native_from: &str,
396) -> Option<ClaudePeerSession> {
397 let socket = native_from.strip_prefix("uds:").unwrap_or(native_from);
398 registry
399 .iter()
400 .find(|session| session.socket_path.to_string_lossy() == socket)
401 .cloned()
402}
403
404#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
406#[serde(rename_all = "snake_case", tag = "outcome")]
407pub enum RelayReceipt {
408 Delivered {
410 detail: String,
412 native_msg_id: Option<String>,
414 },
415 Failed {
417 detail: String,
419 },
420}
421
422pub fn read_receipt(value: &Value) -> RelayReceipt {
425 if let Some(reason) = value.get("denied").and_then(Value::as_str) {
426 return RelayReceipt::Failed {
427 detail: format!("the relay's gate refused the send: {reason}"),
428 };
429 }
430 if let Some(error) = value.get("send_failed").and_then(Value::as_str) {
431 return RelayReceipt::Failed {
432 detail: format!("Claude Code refused the relay's send: {}", error.trim()),
433 };
434 }
435 if value.get("turn_ended").and_then(Value::as_bool) == Some(true) {
436 let said = value
437 .get("said")
438 .and_then(Value::as_str)
439 .filter(|said| !said.trim().is_empty())
440 .map(|said| format!("; it said: {}", said.trim()))
441 .unwrap_or_default();
442 return RelayReceipt::Failed {
443 detail: format!(
444 "the Claude relay ended its turn without sending; nothing was sent{said}"
445 ),
446 };
447 }
448 let response = value.get("tool_response").cloned().unwrap_or(Value::Null);
449 let parsed = match &response {
450 Value::String(text) => serde_json::from_str::<Value>(text).unwrap_or(response.clone()),
451 Value::Array(blocks) => blocks
452 .iter()
453 .find_map(|block| block.get("text").and_then(Value::as_str))
454 .and_then(|text| serde_json::from_str::<Value>(text).ok())
455 .unwrap_or(response.clone()),
456 _ => response.clone(),
457 };
458 if parsed.get("success").and_then(Value::as_bool) != Some(true) {
459 return RelayReceipt::Failed {
460 detail: format!("Claude did not report the send as successful: {parsed}"),
461 };
462 }
463 RelayReceipt::Delivered {
464 detail: parsed
465 .get("message")
466 .and_then(Value::as_str)
467 .unwrap_or_default()
468 .to_string(),
469 native_msg_id: parsed
470 .get("msg_id")
471 .and_then(Value::as_str)
472 .map(str::to_string),
473 }
474}
475
476pub async fn send_through_relay(
481 sender: &MailAddress,
482 sender_name: &str,
483 to: &str,
484 message: String,
485 message_id: &str,
486) -> RelayReceipt {
487 let failed = |detail: String| RelayReceipt::Failed { detail };
488 let spec = match RelaySpec::for_sender(sender, sender_name) {
489 Ok(spec) => spec,
490 Err(error) => return failed(error.to_string()),
491 };
492 if let Err(error) = std::fs::create_dir_all(&spec.paths.directory) {
493 return failed(error.to_string());
494 }
495 let lock = match tokio::task::spawn_blocking({
497 let path = spec.paths.lock.clone();
498 move || SendLock::acquire(&path)
499 })
500 .await
501 {
502 Ok(Ok(lock)) => lock,
503 Ok(Err(error)) => return failed(format!("could not take the relay's send lock: {error}")),
504 Err(error) => return failed(error.to_string()),
505 };
506 let runtime = match ensure_relay_runtime(&spec).await {
507 Ok(runtime) => runtime,
508 Err(detail) => return failed(detail),
509 };
510 let queued = QueuedSend {
511 to: to.to_string(),
512 message,
513 };
514 std::fs::remove_file(&spec.paths.receipt).ok();
515 if let Err(error) = std::fs::write(
516 &spec.paths.queue,
517 serde_json::to_vec(&queued).unwrap_or_default(),
518 ) {
519 return failed(error.to_string());
520 }
521 let delivered =
522 crate::runtime_mail::deliver_to_runtime(&runtime, send_turn(&queued), true).await;
523 let receipt = match delivered {
524 Err(detail) => failed(format!("could not reach the Claude relay: {detail}")),
525 Ok(_) => wait_for_receipt(&spec.paths.receipt).await,
526 };
527 std::fs::remove_file(&spec.paths.queue).ok();
528 if matches!(receipt, RelayReceipt::Delivered { .. }) {
529 let mut sent = spec.paths.last_sent();
530 sent.insert(to.to_string(), message_id.to_string());
531 std::fs::write(
532 &spec.paths.sent,
533 serde_json::to_vec(&sent).unwrap_or_default(),
534 )
535 .ok();
536 }
537 drop(lock);
538 receipt
539}
540
541async fn wait_for_receipt(path: &Path) -> RelayReceipt {
542 let started = Instant::now();
543 while started.elapsed() < SEND_TIMEOUT {
544 if let Some(value) = std::fs::read(path)
545 .ok()
546 .and_then(|bytes| serde_json::from_slice::<Value>(&bytes).ok())
547 {
548 return read_receipt(&value);
549 }
550 tokio::time::sleep(Duration::from_millis(200)).await;
551 }
552 RelayReceipt::Failed {
553 detail: format!(
554 "the Claude relay did not confirm the send within {} seconds; it may still arrive",
555 SEND_TIMEOUT.as_secs()
556 ),
557 }
558}
559
560async fn ensure_relay_runtime(
563 spec: &RelaySpec,
564) -> Result<crate::live_runtime::LiveRuntimeRecord, String> {
565 #[derive(Serialize, Deserialize)]
566 struct Record {
567 runtime_id: String,
568 #[serde(default)]
569 endpoint: String,
570 }
571 let endpoint = relay_endpoint(&spec.program).await?;
572 std::fs::write(
575 &spec.paths.settings,
576 serde_json::to_vec_pretty(&relay_settings(spec)).unwrap_or_default(),
577 )
578 .map_err(|error| error.to_string())?;
579 if let Some(record) = std::fs::read(&spec.paths.record)
580 .ok()
581 .and_then(|bytes| serde_json::from_slice::<Record>(&bytes).ok())
582 {
583 if let Some(runtime) =
584 crate::runtime_mail::controlled_runtime("claude-code", &record.runtime_id)
585 {
586 if record.endpoint == endpoint {
587 return Ok(runtime);
588 }
589 end_relay_process(&spec.name);
592 }
593 }
594 let params = json!({
595 "harness": "claude-code",
596 "launch": {
597 "program": "claude",
598 "arguments": relay_arguments(spec),
599 "env": relay_environment(spec, &endpoint),
600 },
601 "cwd": spec.paths.directory,
602 });
603 let result = machine_rpc(&spec.program, "runtimes.start", ¶ms).await?;
604 let runtime_id = result
605 .pointer("/handle/runtime_id")
606 .and_then(Value::as_str)
607 .ok_or_else(|| format!("the machine daemon did not start the relay: {result}"))?
608 .to_string();
609 std::fs::write(
610 &spec.paths.record,
611 serde_json::to_vec(&Record {
612 runtime_id: runtime_id.clone(),
613 endpoint,
614 })
615 .unwrap_or_default(),
616 )
617 .map_err(|error| error.to_string())?;
618 crate::runtime_mail::controlled_runtime("claude-code", &runtime_id)
619 .ok_or_else(|| "the relay started but registered no live runtime".to_string())
620}
621
622fn end_relay_process(name: &str) {
624 let registry = registry_dir(&HarnessHomes::default());
625 for session in read_registry(®istry) {
626 #[cfg(unix)]
627 if session.name == name {
628 unsafe {
630 libc::kill(session.pid as libc::pid_t, libc::SIGTERM);
631 }
632 }
633 }
634}
635
636async fn relay_endpoint(program: &Path) -> Result<String, String> {
639 if let Some(url) = crate::relay_endpoint::relay_endpoint_url() {
640 return Ok(url);
641 }
642 ensure_machine_daemon(program).await?;
643 let deadline = Instant::now() + Duration::from_secs(10);
644 while Instant::now() < deadline {
645 if let Some(url) = crate::relay_endpoint::relay_endpoint_url() {
646 return Ok(url);
647 }
648 tokio::time::sleep(Duration::from_millis(200)).await;
649 }
650 Err(
651 "the relay endpoint is not answering; the machine daemon's `supercode message watch` \
652 serves it (see mail/machine-daemon.log)"
653 .into(),
654 )
655}
656
657pub async fn machine_rpc(program: &Path, method: &str, params: &Value) -> Result<Value, String> {
660 let call = || {
661 let mut command = tokio::process::Command::new(program);
662 command
663 .args(["teams", "rpc", method, ¶ms.to_string()])
664 .stdin(std::process::Stdio::null())
665 .stdout(std::process::Stdio::piped())
666 .stderr(std::process::Stdio::piped());
667 command.output()
668 };
669 let output = call().await.map_err(|error| error.to_string())?;
670 if output.status.success() {
671 return serde_json::from_slice(&output.stdout)
672 .map_err(|error| format!("unreadable answer from the machine daemon: {error}"));
673 }
674 ensure_machine_daemon(program).await?;
677 let output = call().await.map_err(|error| error.to_string())?;
678 if output.status.success() {
679 return serde_json::from_slice(&output.stdout)
680 .map_err(|error| format!("unreadable answer from the machine daemon: {error}"));
681 }
682 Err(error_line(&String::from_utf8_lossy(&output.stderr)))
683}
684
685pub async fn ensure_machine_daemon(program: &Path) -> Result<(), String> {
688 let describe = || {
689 tokio::process::Command::new(program)
690 .args(["teams", "describe"])
691 .stdin(std::process::Stdio::null())
692 .stdout(std::process::Stdio::null())
693 .stderr(std::process::Stdio::null())
694 .status()
695 };
696 if describe().await.is_ok_and(|status| status.success()) {
697 return Ok(());
698 }
699 if crate::teams::service_owns_daemon() {
703 for _ in 0..100 {
704 tokio::time::sleep(Duration::from_millis(200)).await;
705 if describe().await.is_ok_and(|status| status.success()) {
706 return Ok(());
707 }
708 }
709 return Err(
710 "this machine's daemon is run by its service manager and did not answer within 20 seconds; \
711 see `supercode teams status`"
712 .into(),
713 );
714 }
715 let root = mail_root();
716 std::fs::create_dir_all(&root).map_err(|error| error.to_string())?;
717 let log = std::fs::OpenOptions::new()
718 .create(true)
719 .append(true)
720 .open(root.join("machine-daemon.log"))
721 .map_err(|error| error.to_string())?;
722 let mut command = std::process::Command::new(program);
723 command
724 .args(["teams", "machine", "start", "--supercode"])
725 .arg(program)
726 .stdin(std::process::Stdio::null())
727 .stdout(log.try_clone().map_err(|error| error.to_string())?)
728 .stderr(log);
729 #[cfg(unix)]
730 {
731 use std::os::unix::process::CommandExt;
732 command.process_group(0);
735 }
736 command
737 .spawn()
738 .map_err(|error| format!("could not start the machine daemon: {error}"))?;
739 for _ in 0..50 {
740 tokio::time::sleep(Duration::from_millis(200)).await;
741 if describe().await.is_ok_and(|status| status.success()) {
742 return Ok(());
743 }
744 }
745 Err(format!(
746 "the machine daemon did not start within 10 seconds; see {}",
747 root.join("machine-daemon.log").display()
748 ))
749}
750
751fn error_line(stderr: &str) -> String {
753 let lines: Vec<&str> = stderr
754 .lines()
755 .map(str::trim)
756 .filter(|line| !line.is_empty())
757 .collect();
758 lines
759 .iter()
760 .find(|line| line.starts_with("Error") || line.starts_with("error"))
761 .or(lines.last())
762 .copied()
763 .unwrap_or("the machine daemon failed without saying why")
764 .chars()
765 .take(300)
766 .collect()
767}
768
769struct SendLock {
771 _file: std::fs::File,
772}
773
774impl SendLock {
775 fn acquire(path: &Path) -> std::io::Result<Self> {
776 let file = std::fs::OpenOptions::new()
777 .create(true)
778 .truncate(false)
779 .write(true)
780 .open(path)?;
781 #[cfg(unix)]
782 {
783 use std::os::unix::io::AsRawFd;
784 if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) } != 0 {
787 return Err(std::io::Error::last_os_error());
788 }
789 }
790 Ok(Self { _file: file })
791 }
792}
793
794pub fn file_inbound_prompt(
797 homes: &HarnessHomes,
798 represented: &MailAddress,
799 prompt: &str,
800 last_sent: Option<&HashMap<String, String>>,
801) {
802 match parse_inbound_text(prompt) {
803 Some(RelayEvent::Peer(native)) => {
804 let registry = read_registry(®istry_dir(homes));
805 let sender = resolve_native_sender(®istry, &native.from);
806 let in_reply_to = sender
807 .as_ref()
808 .and_then(|session| last_sent.and_then(|sent| sent.get(&session.name).cloned()));
809 file_inbound(represented, &native, sender.as_ref(), in_reply_to);
810 }
811 Some(RelayEvent::IdleNotice(text) | RelayEvent::DeliveryNotice(text)) => {
812 file_notice(represented, &text);
813 }
814 _ => {}
815 }
816}
817pub fn parse_inbound_text(text: &str) -> Option<RelayEvent> {
820 let trimmed = text.trim_start();
821 if trimmed.starts_with(NATIVE_IDLE_NOTICE) {
822 return Some(RelayEvent::IdleNotice(trimmed.to_string()));
823 }
824 if trimmed.starts_with(NATIVE_DELIVERY_NOTICE) {
825 return Some(RelayEvent::DeliveryNotice(trimmed.to_string()));
826 }
827 if !trimmed.starts_with(NATIVE_PEER_PREAMBLE) && !trimmed.starts_with(NATIVE_PEER_OPENING) {
828 return None;
829 }
830 let start = text.find(NATIVE_PEER_OPENING)?;
831 let header_end = start + text[start..].find(">\n")?;
832 let header = &text[start + NATIVE_PEER_OPENING.len()..header_end];
833 let body_start = header_end + 2;
834 let body_end = text.rfind(&format!("\n{NATIVE_PEER_CLOSING}"))?;
837 if body_end < body_start {
838 return None;
839 }
840 let attributes = parse_attributes(header);
841 Some(RelayEvent::Peer(NativeEnvelope {
842 from: attributes.get("from")?.clone(),
843 from_name: attributes.get("from-name").cloned(),
844 body: text[body_start..body_end].to_string(),
845 }))
846}
847fn parse_attributes(header: &str) -> BTreeMap<String, String> {
848 let mut attributes = BTreeMap::new();
849 let mut rest = header;
850 while let Some(equals) = rest.find("=\"") {
851 let key = rest[..equals].trim().to_string();
852 let value_start = equals + 2;
853 let Some(length) = rest[value_start..].find('"') else {
854 break;
855 };
856 attributes.insert(key, rest[value_start..value_start + length].to_string());
857 rest = &rest[value_start + length + 1..];
858 }
859 attributes
860}
861fn deliver_home(represented: &MailAddress, envelope: &Envelope) {
865 if let Err(error) = crate::mailbox::deliver_to(represented, envelope) {
866 eprintln!(
867 "could not deliver {} to {represented}: {error}; kept in this machine's mailbox for it",
868 envelope.id
869 );
870 if let Ok(mailbox) = crate::mailbox::Mailbox::open(&mail_root(), represented) {
871 mailbox.deliver(envelope).ok();
872 }
873 return;
874 }
875 if represented.machine == crate::mailbox::local_machine_name() {
880 if let Ok(program) = supercode_program() {
881 std::process::Command::new(program)
882 .args(["message", "push", &represented.to_string(), &envelope.id])
883 .stdin(std::process::Stdio::null())
884 .stdout(std::process::Stdio::null())
885 .stderr(std::process::Stdio::null())
886 .spawn()
887 .ok();
888 }
889 }
890}
891fn file_inbound(
892 represented: &MailAddress,
893 native: &NativeEnvelope,
894 sender: Option<&ClaudePeerSession>,
895 in_reply_to: Option<String>,
896) {
897 let machine = crate::mailbox::local_machine_name();
900 let (from, from_name) = match sender {
901 Some(session) => (
902 MailAddress::new(&machine, "claude-code", &session.session_id),
903 format!("{}@{machine}", session.name),
904 ),
905 None => (
906 MailAddress::new(&machine, "claude-code", "unknown"),
907 native
908 .from_name
909 .clone()
910 .unwrap_or_else(|| "an unknown Claude session".into()),
911 ),
912 };
913 let Ok(from) = from else { return };
914 let Ok(mut envelope) = Envelope::new(
915 from,
916 from_name,
917 MailKind::Peer,
918 ReplyVia::Command,
919 native.body.clone(),
920 ) else {
921 return;
922 };
923 envelope.native_from = Some(native.from.clone());
924 if let Some(in_reply_to) = in_reply_to {
925 envelope.thread = crate::mailbox::thread_of_reply(Some(&in_reply_to));
926 envelope.in_reply_to = Some(in_reply_to);
927 envelope.in_reply_to_inferred = true;
928 }
929 deliver_home(represented, &envelope);
932}
933fn file_notice(represented: &MailAddress, text: &str) {
934 let Ok(from) = MailAddress::new(
935 crate::mailbox::local_machine_name(),
936 "claude-code",
937 "notice",
938 ) else {
939 return;
940 };
941 let Ok(envelope) = Envelope::new(
942 from,
943 "Claude Code",
944 MailKind::Notice,
945 ReplyVia::None,
946 text.to_string(),
947 ) else {
948 return;
949 };
950 deliver_home(represented, &envelope);
953}
954pub fn supercode_program() -> std::io::Result<PathBuf> {
957 let current = std::env::current_exe()?;
958 if current.file_stem().and_then(|stem| stem.to_str()) == Some("supercode") {
959 return Ok(current);
960 }
961 std::env::var_os("PATH")
962 .iter()
963 .flat_map(std::env::split_paths)
964 .map(|directory| directory.join("supercode"))
965 .find(|candidate| candidate.is_file())
966 .ok_or_else(|| {
967 std::io::Error::new(
968 std::io::ErrorKind::NotFound,
969 "the supercode program is not on PATH; Claude relays and the machine daemon need it",
970 )
971 })
972}