agentmux_bashwrap/
sessionstart.rs1use anyhow::Result;
27use serde::{Deserialize, Serialize};
28use std::io::{Read, Write};
29
30use crate::mps_client::WpsClient;
31
32const PART_PATH: &str = "/api/v1/agent/memory/session-start/part";
33const ACK_PATH: &str = "/api/v1/agent/memory/session-start/ack";
34
35const PART_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
39const ACK_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(1500);
40
41#[derive(clap::Parser, Debug)]
43pub struct Args {
44 #[arg(long)]
46 pub part: usize,
47}
48
49#[derive(Deserialize, Default, Debug, PartialEq)]
51struct SessionStartInput {
52 #[serde(default)]
53 session_id: String,
54 #[serde(default)]
55 source: String,
56 #[serde(default)]
59 cwd: String,
60}
61
62#[derive(Serialize)]
63struct PartRequest<'a> {
64 block_id: &'a str,
65 session_id: &'a str,
66 source: &'a str,
67 part: usize,
68 cwd: &'a str,
70 config_dir: &'a str,
72}
73
74pub async fn run(args: Args) -> Result<()> {
76 let mut buf = String::new();
77 let _ = std::io::stdin().read_to_string(&mut buf);
78 let input: SessionStartInput = serde_json::from_str(&buf).unwrap_or_default();
79
80 let Some(client) = WpsClient::from_env() else {
81 tracing::debug!(target: "bashwrap", "sessionstart: sidecar env absent, nothing to deliver");
82 return Ok(());
83 };
84 let Some(block_id) = std::env::var("AGENTMUX_BLOCKID").ok().filter(|v| !v.is_empty()) else {
85 tracing::debug!(target: "bashwrap", "sessionstart: no AGENTMUX_BLOCKID, nothing to deliver");
86 return Ok(());
87 };
88 let token = std::env::var("AGENTMUX_AGENT_TOKEN").ok();
89 let config_dir = std::env::var("CLAUDE_CONFIG_DIR").unwrap_or_default();
90 let req = PartRequest {
91 block_id: &block_id,
92 session_id: &input.session_id,
93 source: &input.source,
94 part: args.part,
95 cwd: &input.cwd,
96 config_dir: &config_dir,
97 };
98
99 let reply = match client.post_json(PART_PATH, token.as_deref(), &req, PART_TIMEOUT).await {
100 Ok(v) => v,
101 Err(e) => {
102 tracing::warn!(target: "bashwrap", part = args.part, error = %e, "sessionstart: part request failed");
103 return Ok(());
104 }
105 };
106 let Some(text) = reply.get("text").and_then(|t| t.as_str()) else {
107 return Ok(());
108 };
109
110 let mut stdout = std::io::stdout().lock();
111 if stdout.write_all(hook_output(text).as_bytes()).and_then(|_| stdout.flush()).is_err() {
112 return Ok(());
113 }
114 drop(stdout);
115
116 if let Err(e) = client.post_json(ACK_PATH, token.as_deref(), &req, ACK_TIMEOUT).await {
117 tracing::warn!(target: "bashwrap", part = args.part, error = %e, "sessionstart: ack failed");
118 }
119 tracing::info!(target: "bashwrap", part = args.part, source = %input.source, chars = text.chars().count(), "sessionstart: part delivered");
120 Ok(())
121}
122
123fn hook_output(text: &str) -> String {
126 serde_json::json!({
127 "hookSpecificOutput": {
128 "hookEventName": "SessionStart",
129 "additionalContext": text,
130 }
131 })
132 .to_string()
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn reads_session_id_source_and_cwd_from_stdin() {
141 let input: SessionStartInput = serde_json::from_str(
142 r#"{"session_id":"s1","transcript_path":"/t","cwd":"/c","hook_event_name":"SessionStart","source":"compact"}"#,
143 )
144 .unwrap();
145 assert_eq!(input, SessionStartInput { session_id: "s1".into(), source: "compact".into(), cwd: "/c".into() });
146 }
147
148 #[test]
149 fn a_malformed_payload_degrades_to_empty() {
150 let input: SessionStartInput = serde_json::from_str("not json").unwrap_or_default();
151 assert_eq!(input, SessionStartInput::default());
152 }
153
154 #[test]
155 fn prints_the_shape_claude_code_reads() {
156 let out: serde_json::Value = serde_json::from_str(&hook_output("[AgentMux memory — part 1 of 1]\nhi")).unwrap();
157 assert_eq!(out["hookSpecificOutput"]["hookEventName"], "SessionStart");
158 assert_eq!(out["hookSpecificOutput"]["additionalContext"], "[AgentMux memory — part 1 of 1]\nhi");
159 }
160
161 #[test]
162 fn the_request_names_block_session_source_part_and_where_the_cli_runs() {
163 let req = PartRequest {
164 block_id: "b",
165 session_id: "s",
166 source: "startup",
167 part: 3,
168 cwd: "/ws",
169 config_dir: "/cfg",
170 };
171 assert_eq!(
172 serde_json::to_value(&req).unwrap(),
173 serde_json::json!({"block_id": "b", "session_id": "s", "source": "startup", "part": 3, "cwd": "/ws", "config_dir": "/cfg"})
174 );
175 }
176}