1use std::collections::HashMap;
21use std::path::{Path, PathBuf};
22use std::sync::atomic::{AtomicU64, Ordering};
23use std::sync::Arc;
24
25use crate::acp::protocol::ClientCapabilities;
26use crate::tools::AcpClientFsState;
27use tokio::sync::Mutex;
28
29use std::io::Write;
30
31use serde::{Deserialize, Serialize};
32
33use crate::acp::bridge::sha256_first_12;
34use crate::acp::permission::PendingPermissionStore;
35use crate::message::Message;
36use crate::runtime::AgentRuntime;
37
38pub type SessionId = String;
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct SessionMetadata {
48 pub session_id: SessionId,
50 pub cwd: PathBuf,
52 pub turn: u64,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub system_prompt: Option<String>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub mcp_servers: Option<serde_json::Value>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct SavedMessage {
69 pub id: String,
71 #[serde(flatten)]
73 pub message: Message,
74 #[serde(default)]
76 pub compressible: bool,
77}
78
79impl SavedMessage {
80 pub fn from_message(msg: Message) -> Self {
82 let id = sha256_first_12(&msg.content);
83 Self {
84 id,
85 message: msg,
86 compressible: false,
87 }
88 }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct CompactionHint {
98 pub turn_index: usize,
100 pub compressible: bool,
102}
103
104#[derive(Debug, Clone)]
110pub struct SummarizedContext {
111 pub summary: String,
113 pub message_count: usize,
115}
116
117pub struct AcpSession {
123 pub runtime: AgentRuntime,
125 pub cwd: PathBuf,
127 pub turn: u64,
129 pub session_id: SessionId,
131 pub transcript: Vec<Message>,
133 pub cancel_token: Arc<tokio_util::sync::CancellationToken>,
142 pub system_prompt: Option<String>,
144 pub mcp_servers: Option<serde_json::Value>,
146 pub client_capabilities: Option<ClientCapabilities>,
149 pub acp_state: Arc<Mutex<AcpClientFsState>>,
151 pub mcp_child_pids: Vec<u32>,
153}
154
155impl AcpSession {
156 pub fn refresh_cancel_token(&mut self) -> Arc<tokio_util::sync::CancellationToken> {
162 let token = Arc::new(tokio_util::sync::CancellationToken::new());
163 self.runtime.set_interrupt_token((*token).clone());
165 self.cancel_token = token.clone();
166 token
167 }
168
169 pub async fn kill_mcp_children(&mut self) {
173 for pid in std::mem::take(&mut self.mcp_child_pids) {
174 #[cfg(unix)]
175 {
176 use tokio::time::timeout;
177
178 let grace = crate::mcp::DEFAULT_KILL_GRACE_PERIOD;
179 tracing::info!(server_pid = pid, "AcpSession: killing MCP child process");
180
181 tracing::info!(server_pid = pid, "kill-gracefully: sending SIGTERM");
183 let _ = std::process::Command::new("kill")
184 .args(["-TERM", &pid.to_string()])
185 .status();
186
187 let waited = timeout(grace, async {
189 loop {
190 let status = std::process::Command::new("kill")
192 .args(["-0", &pid.to_string()])
193 .status()
194 .ok();
195 match status {
196 Some(s) if s.success() => {
197 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
199 }
200 _ => {
201 break;
203 }
204 }
205 }
206 })
207 .await;
208
209 if waited.is_err() {
210 tracing::warn!(
212 server_pid = pid,
213 grace_ms = grace.as_millis(),
214 "kill-gracefully: SIGTERM timeout, sending SIGKILL"
215 );
216 let _ = std::process::Command::new("kill")
217 .args(["-9", &pid.to_string()])
218 .status();
219 } else {
220 tracing::info!(
221 server_pid = pid,
222 "kill-gracefully: process exited gracefully"
223 );
224 }
225
226 let _ = std::process::Command::new("wait")
228 .args([&pid.to_string()])
229 .status();
230 }
231 #[cfg(not(unix))]
232 {
233 let _ = pid;
234 tracing::info!("AcpSession: killing MCP child (non-Unix)");
235 let _ = std::process::Command::new("taskkill")
236 .args(["/F", "/PID", &pid.to_string()])
237 .status();
238 }
239 }
240 }
241}
242
243pub struct AcpSessionManager {
251 sessions: HashMap<SessionId, AcpSession>,
252 next_id: AtomicU64,
253 persistence_dir: Option<PathBuf>,
257 permission_store: PendingPermissionStore,
259 client_capabilities: Option<ClientCapabilities>,
262}
263
264impl Default for AcpSessionManager {
265 fn default() -> Self {
266 Self::new()
267 }
268}
269
270impl AcpSessionManager {
271 pub fn new() -> Self {
273 Self {
274 sessions: HashMap::new(),
275 next_id: AtomicU64::new(1),
276 persistence_dir: None,
277 permission_store: PendingPermissionStore::new(),
278 client_capabilities: None,
279 }
280 }
281
282 pub fn set_client_capabilities(&mut self, caps: Option<ClientCapabilities>) {
285 self.client_capabilities = caps;
286 }
287
288 pub fn get_client_capabilities(&self) -> Option<&ClientCapabilities> {
290 self.client_capabilities.as_ref()
291 }
292
293 pub fn with_persistence_dir(mut self, dir: PathBuf) -> Self {
296 self.persistence_dir = Some(dir);
297 self
298 }
299
300 pub fn persistence_dir(&self) -> Option<&Path> {
302 self.persistence_dir.as_deref()
303 }
304
305 pub fn next_session_id(&self) -> SessionId {
307 let n = self.next_id.fetch_add(1, Ordering::Relaxed);
308 format!("acp-sess-{n}")
309 }
310
311 pub fn insert_with_id(&mut self, session_id: SessionId, session: AcpSession) {
316 if let Some(pdir) = &self.persistence_dir {
319 let session_dir = pdir.join(&session_id);
320 let _ = std::fs::create_dir_all(&session_dir);
321 let _ = self.save_metadata_to_disk(&session_id, &session);
322 }
323 self.sessions.insert(session_id, session);
324 }
325
326 pub fn get_mut(&mut self, sid: &str) -> Option<&mut AcpSession> {
328 self.sessions.get_mut(sid)
329 }
330
331 pub fn remove(&mut self, sid: &str) -> Option<AcpSession> {
333 self.sessions.remove(sid)
334 }
335
336 pub fn all_turns(&self) -> Vec<(String, u64)> {
340 self.sessions
341 .iter()
342 .map(|(k, v)| (k.clone(), v.turn))
343 .collect()
344 }
345
346 pub fn get(&self, sid: &str) -> Option<&AcpSession> {
348 self.sessions.get(sid)
349 }
350
351 pub fn contains(&self, sid: &str) -> bool {
353 self.sessions.contains_key(sid)
354 }
355
356 pub fn permission_store(&mut self) -> &mut PendingPermissionStore {
358 &mut self.permission_store
359 }
360
361 pub fn session_dir(&self, sid: &str) -> Option<PathBuf> {
365 self.persistence_dir
366 .as_ref()
367 .map(|p| p.join(sanitize_session_id(sid)))
368 }
369
370 pub fn save_transcript(&self, sid: &str) -> std::io::Result<()> {
373 let session = match self.sessions.get(sid) {
374 Some(s) => s,
375 None => {
376 return Err(std::io::Error::new(
377 std::io::ErrorKind::NotFound,
378 "session not found",
379 ))
380 }
381 };
382
383 let session_dir = match self.session_dir(sid) {
384 Some(d) => d,
385 None => return Ok(()), };
387
388 let _ = std::fs::create_dir_all(&session_dir);
390
391 let path = session_dir.join("transcript.jsonl");
392 let mut file = std::fs::File::create(&path)?;
393
394 for msg in &session.transcript {
395 let saved = SavedMessage::from_message(msg.clone());
396 let line = serde_json::to_string(&saved)?;
397 writeln!(&mut file, "{line}")?;
398 }
399
400 Ok(())
401 }
402
403 pub fn load_transcript_from_disk(&self, sid: &str) -> std::io::Result<Vec<SavedMessage>> {
405 let session_dir = self.session_dir(sid).ok_or_else(|| {
406 std::io::Error::new(std::io::ErrorKind::NotFound, "no persistence dir")
407 })?;
408
409 let path = session_dir.join("transcript.jsonl");
410 let content = std::fs::read_to_string(&path)?;
411 let mut messages = Vec::new();
412
413 for line in content.lines() {
414 let trimmed = line.trim();
415 if trimmed.is_empty() {
416 continue;
417 }
418 let saved: SavedMessage = serde_json::from_str(trimmed).map_err(|e| {
419 std::io::Error::new(
420 std::io::ErrorKind::InvalidData,
421 format!("corrupt transcript: {e}"),
422 )
423 })?;
424 messages.push(saved);
425 }
426
427 Ok(messages)
428 }
429
430 fn save_metadata_to_disk(&self, sid: &str, session: &AcpSession) -> std::io::Result<()> {
432 let session_dir = match self.session_dir(sid) {
433 Some(d) => d,
434 None => return Ok(()),
435 };
436
437 let _ = std::fs::create_dir_all(&session_dir);
438
439 let metadata = SessionMetadata {
440 session_id: sid.to_string(),
441 cwd: session.cwd.clone(),
442 turn: session.turn,
443 system_prompt: session.system_prompt.clone(),
444 mcp_servers: session.mcp_servers.clone(),
445 };
446
447 let path = session_dir.join("metadata.json");
448 let json = serde_json::to_string_pretty(&metadata)?;
449 std::fs::write(&path, json)?;
450
451 Ok(())
452 }
453
454 pub fn save_metadata(&self, sid: &str) -> std::io::Result<()> {
456 let session = match self.sessions.get(sid) {
457 Some(s) => s,
458 None => {
459 return Err(std::io::Error::new(
460 std::io::ErrorKind::NotFound,
461 "session not found",
462 ))
463 }
464 };
465 self.save_metadata_to_disk(sid, session)
466 }
467
468 pub fn load_metadata_from_disk(&self, sid: &str) -> std::io::Result<SessionMetadata> {
470 let session_dir = self.session_dir(sid).ok_or_else(|| {
471 std::io::Error::new(std::io::ErrorKind::NotFound, "no persistence dir")
472 })?;
473
474 let path = session_dir.join("metadata.json");
475 let content = std::fs::read_to_string(&path)?;
476 let metadata: SessionMetadata = serde_json::from_str(&content).map_err(|e| {
477 std::io::Error::new(
478 std::io::ErrorKind::InvalidData,
479 format!("corrupt metadata: {e}"),
480 )
481 })?;
482 Ok(metadata)
483 }
484
485 pub fn session_exists_on_disk(&self, sid: &str) -> bool {
487 self.session_dir(sid)
488 .map(|d| d.join("metadata.json").exists())
489 .unwrap_or(false)
490 }
491}
492
493pub fn summarize_transcript(messages: &[Message]) -> SummarizedContext {
506 let mut accomplishments: Vec<String> = Vec::new();
507 let mut blockers: Vec<String> = Vec::new();
508 let mut in_flight: Option<String> = None;
509
510 for (i, msg) in messages.iter().enumerate() {
511 let content = msg.content.trim();
512
513 match msg.role {
514 crate::message::Role::User => {
515 if i == messages.len() - 1
517 || messages[messages.len() - 1].role == crate::message::Role::User
518 {
519 in_flight = Some(content.to_string());
520 } else {
521 let mut goal = content.chars().take(80).collect::<String>();
523 if content.len() > 80 {
524 goal.push_str("...");
525 }
526 accomplishments.push(format!("user request: \"{goal}\""));
527 }
528 }
529 crate::message::Role::Assistant => {
530 if !content.is_empty() {
532 let mut snippet = content.chars().take(80).collect::<String>();
533 if content.len() > 80 {
534 snippet.push_str("...");
535 }
536 accomplishments.push(format!("assistant responded: \"{snippet}\""));
537 }
538 }
539 crate::message::Role::Tool => {
540 if content.contains("error")
542 || content.contains("Error")
543 || content.contains("failed")
544 {
545 let mut snippet = content.chars().take(60).collect::<String>();
546 if content.len() > 60 {
547 snippet.push_str("...");
548 }
549 blockers.push(format!("tool error: \"{snippet}\""));
550 } else if !content.is_empty() {
551 let mut snippet = content.chars().take(60).collect::<String>();
552 if content.len() > 60 {
553 snippet.push_str("...");
554 }
555 accomplishments.push(format!("tool result: \"{snippet}\""));
556 }
557 }
558 crate::message::Role::System => {
559 }
561 }
562 }
563
564 let mut summary_parts: Vec<String> = Vec::new();
565
566 if accomplishments.is_empty() {
568 summary_parts.push("No accomplishments recorded yet.".to_string());
569 } else {
570 let n = accomplishments.len();
571 let sample: Vec<&str> = accomplishments.iter().take(3).map(|s| s.as_str()).collect();
572 summary_parts.push(format!(
573 "The session has {n} recorded action(s): {}.",
574 sample.join("; ")
575 ));
576 if accomplishments.len() > 3 {
577 summary_parts.push(format!("And {} more action(s).", accomplishments.len() - 3));
578 }
579 }
580
581 match in_flight {
583 Some(ref text) => summary_parts.push(format!("The user's current request is: \"{text}\".")),
584 None => summary_parts.push("No active user request.".to_string()),
585 }
586
587 if blockers.is_empty() {
589 summary_parts.push("No blockers detected.".to_string());
590 } else {
591 let sample: Vec<&str> = blockers.iter().take(2).map(|s| s.as_str()).collect();
592 summary_parts.push(format!(
593 "The session encountered {} blocker(s): {}.",
594 blockers.len(),
595 sample.join("; ")
596 ));
597 }
598
599 SummarizedContext {
600 summary: summary_parts.join(" "),
601 message_count: messages.len(),
602 }
603}
604
605fn sanitize_session_id(sid: &str) -> String {
611 sid.replace(['/', '\\', ':', ' ', '\0'], "_")
614 .replace("..", "__")
615}
616
617pub fn compute_compaction_hints(
627 messages: &[SavedMessage],
628 recency_threshold: usize,
629) -> Vec<CompactionHint> {
630 let total = messages.len();
631 messages
632 .iter()
633 .enumerate()
634 .map(|(i, _msg)| {
635 let compressible = i < total.saturating_sub(recency_threshold);
637 CompactionHint {
638 turn_index: i,
639 compressible,
640 }
641 })
642 .collect()
643}
644
645pub fn apply_compaction_hints(messages: &mut [SavedMessage], hints: &[CompactionHint]) {
647 for hint in hints {
648 if let Some(msg) = messages.get_mut(hint.turn_index) {
649 msg.compressible = hint.compressible;
650 }
651 }
652}
653
654#[cfg(test)]
659mod tests {
660 use super::*;
661 use crate::message::Message;
662 use std::io::Write;
663
664 #[test]
667 fn new_manager_is_empty() {
668 let mgr = AcpSessionManager::new();
669 assert!(mgr.get("nonexistent").is_none());
671 }
672
673 #[test]
674 fn next_session_id_is_monotonic() {
675 let mgr = AcpSessionManager::new();
676 let a = mgr.next_session_id();
677 let b = mgr.next_session_id();
678 assert_ne!(a, b);
679 assert!(a.contains("acp-sess-"));
680 assert!(b.contains("acp-sess-"));
681 }
682
683 #[test]
684 fn insert_and_get() {
685 let mut mgr = AcpSessionManager::new();
686 let _sid = mgr.next_session_id();
687 assert!(mgr.remove("nonexistent").is_none());
689 }
690
691 #[test]
692 fn contains_works() {
693 let mgr = AcpSessionManager::new();
694 assert!(!mgr.contains("nonexistent"));
697 }
698
699 #[test]
702 fn save_and_load_transcript_roundtrip() {
703 let tmp = tempfile::TempDir::new().unwrap();
704 let pdir = tmp.path().to_path_buf();
705
706 let _mgr = AcpSessionManager::new();
708 let _ = pdir;
711
712 let session_dir = pdir.join("test-sess-1");
714 std::fs::create_dir_all(&session_dir).unwrap();
715
716 let messages = vec![
717 Message::user("Hello"),
718 Message::assistant("Hi there!"),
719 Message::user("What is the weather?"),
720 ];
721
722 let path = session_dir.join("transcript.jsonl");
723 let mut file = std::fs::File::create(&path).unwrap();
724 for msg in &messages {
725 let saved = SavedMessage::from_message(msg.clone());
726 let line = serde_json::to_string(&saved).unwrap();
727 writeln!(file, "{line}").unwrap();
728 }
729 drop(file);
730
731 let content = std::fs::read_to_string(&path).unwrap();
733 let loaded: Vec<SavedMessage> = content
734 .lines()
735 .filter(|l| !l.trim().is_empty())
736 .map(|l| serde_json::from_str(l).unwrap())
737 .collect();
738
739 assert_eq!(loaded.len(), 3);
740 assert_eq!(loaded[0].message.content, "Hello");
741 assert_eq!(loaded[1].message.content, "Hi there!");
742 assert_eq!(loaded[2].message.content, "What is the weather?");
743
744 assert_eq!(loaded[0].id, sha256_first_12("Hello"));
746 assert_eq!(loaded[1].id, sha256_first_12("Hi there!"));
747 assert_eq!(loaded[2].id, sha256_first_12("What is the weather?"));
748 }
749
750 #[test]
751 fn saved_message_id_matches_content_hash() {
752 let msg = Message::user("test content");
753 let saved = SavedMessage::from_message(msg.clone());
754 assert_eq!(saved.id, sha256_first_12("test content"));
755 }
756
757 #[test]
758 fn same_content_same_id() {
759 let a = SavedMessage::from_message(Message::user("hello"));
760 let b = SavedMessage::from_message(Message::user("hello"));
761 assert_eq!(a.id, b.id);
762 }
763
764 #[test]
765 fn different_content_different_id() {
766 let a = SavedMessage::from_message(Message::user("hello"));
767 let b = SavedMessage::from_message(Message::user("world"));
768 assert_ne!(a.id, b.id);
769 }
770
771 #[test]
772 fn save_and_load_metadata_roundtrip() {
773 let tmp = tempfile::TempDir::new().unwrap();
774 let session_dir = tmp.path().join("test-sess-meta");
775 std::fs::create_dir_all(&session_dir).unwrap();
776
777 let metadata = SessionMetadata {
778 session_id: "test-sess-meta".to_string(),
779 cwd: PathBuf::from("/tmp"),
780 turn: 5,
781 system_prompt: Some("Be helpful.".to_string()),
782 mcp_servers: None,
783 };
784
785 let path = session_dir.join("metadata.json");
786 let json = serde_json::to_string_pretty(&metadata).unwrap();
787 std::fs::write(&path, json).unwrap();
788
789 let content = std::fs::read_to_string(&path).unwrap();
791 let loaded: SessionMetadata = serde_json::from_str(&content).unwrap();
792 assert_eq!(loaded.session_id, "test-sess-meta");
793 assert_eq!(loaded.turn, 5);
794 assert_eq!(loaded.system_prompt, Some("Be helpful.".to_string()));
795 }
796
797 #[test]
798 fn corrupt_transcript_returns_error() {
799 let tmp = tempfile::TempDir::new().unwrap();
800 let session_dir = tmp.path().join("bad-sess");
801 std::fs::create_dir_all(&session_dir).unwrap();
802
803 let path = session_dir.join("transcript.jsonl");
805 std::fs::write(&path, "not valid json\n").unwrap();
806
807 let _mgr = AcpSessionManager::new();
809 let content = std::fs::read_to_string(&path).unwrap();
811 let result: Result<SavedMessage, _> = serde_json::from_str(content.trim());
812 assert!(result.is_err());
813 }
814
815 #[test]
818 fn summarize_empty_transcript() {
819 let ctx = summarize_transcript(&[]);
820 assert!(ctx.summary.contains("No accomplishments"));
821 assert_eq!(ctx.message_count, 0);
822 }
823
824 #[test]
825 fn summarize_with_messages() {
826 let messages = vec![
827 Message::user("What is the capital of France?"),
828 Message::assistant("The capital of France is Paris."),
829 Message::user("Tell me more about Paris."),
830 ];
831 let ctx = summarize_transcript(&messages);
832 assert!(ctx.summary.contains("capital of France"));
833 assert!(ctx.summary.contains("Tell me more"));
834 assert_eq!(ctx.message_count, 3);
835 }
836
837 #[test]
838 fn summarize_with_tool_errors_detects_blockers() {
839 let messages = vec![
840 Message::user("List the files"),
841 Message::assistant("Let me check the directory."),
842 Message::tool_result("tc-1", "error: permission denied"),
843 ];
844 let ctx = summarize_transcript(&messages);
845 assert!(ctx.summary.contains("blocker"));
846 assert_eq!(ctx.message_count, 3);
847 }
848
849 #[test]
852 fn compaction_hints_all_compressible_when_threshold_zero() {
853 let mut messages = Vec::new();
854 for i in 0..5 {
855 let msg = SavedMessage::from_message(Message::user(format!("msg {i}")));
856 messages.push(msg);
857 }
858
859 let hints = compute_compaction_hints(&messages, 0);
860 assert_eq!(hints.len(), 5);
861 for hint in &hints {
862 assert!(
863 hint.compressible,
864 "all should be compressible with threshold 0"
865 );
866 }
867 }
868
869 #[test]
870 fn compaction_hints_recency_protected() {
871 let mut messages = Vec::new();
872 for i in 0..10 {
873 let msg = SavedMessage::from_message(Message::user(format!("msg {i}")));
874 messages.push(msg);
875 }
876
877 let hints = compute_compaction_hints(&messages, 2);
879 assert_eq!(hints.len(), 10);
880
881 for (i, hint) in hints.iter().enumerate().take(8) {
883 assert!(hint.compressible, "msg {i} should be compressible");
884 }
885 assert!(!hints[8].compressible, "msg 8 should NOT be compressible");
887 assert!(!hints[9].compressible, "msg 9 should NOT be compressible");
888 }
889
890 #[test]
891 fn compaction_hints_configurable_threshold() {
892 let mut messages = Vec::new();
893 for i in 0..10 {
894 let msg = SavedMessage::from_message(Message::user(format!("msg {i}")));
895 messages.push(msg);
896 }
897
898 let hints = compute_compaction_hints(&messages, 5);
899 for (i, hint) in hints.iter().enumerate().take(5) {
901 assert!(hint.compressible, "msg {i} should be compressible");
902 }
903 for (i, hint) in hints.iter().enumerate().skip(5) {
905 assert!(!hint.compressible, "msg {i} should NOT be compressible");
906 }
907 }
908
909 #[test]
910 fn apply_compaction_hints_sets_compressible() {
911 let mut messages = vec![
912 SavedMessage::from_message(Message::user("old")),
913 SavedMessage::from_message(Message::user("new")),
914 ];
915 let hints = vec![
916 CompactionHint {
917 turn_index: 0,
918 compressible: true,
919 },
920 CompactionHint {
921 turn_index: 1,
922 compressible: false,
923 },
924 ];
925 apply_compaction_hints(&mut messages, &hints);
926 assert!(messages[0].compressible);
927 assert!(!messages[1].compressible);
928 }
929
930 #[test]
934 fn cancel_token_is_arc_type() {
935 let _type_check: Arc<tokio_util::sync::CancellationToken> =
939 Arc::new(tokio_util::sync::CancellationToken::new());
940 }
942
943 #[test]
944 fn acp_session_field_cancel_token_is_arc() {
945 fn assert_arc_cancel_token(t: &Arc<tokio_util::sync::CancellationToken>) {
948 assert!(!t.is_cancelled());
949 }
950 let token = Arc::new(tokio_util::sync::CancellationToken::new());
951 assert_arc_cancel_token(&token);
952 }
953
954 #[tokio::test]
957 async fn drop_acp_session_does_not_cancel_token() {
958 use crate::llm::MockProvider;
960 let llm = Arc::new(MockProvider::new(vec![]));
961 let runtime = AgentRuntime::builder()
962 .llm(llm)
963 .build()
964 .expect("build runtime");
965
966 let shared_token = Arc::new(tokio_util::sync::CancellationToken::new());
967 let clone_for_drop = shared_token.clone();
968
969 {
970 let _session = AcpSession {
971 runtime,
972 cwd: PathBuf::from("/tmp"),
973 turn: 0,
974 session_id: "test".to_string(),
975 transcript: vec![],
976 cancel_token: clone_for_drop,
977 system_prompt: None,
978 mcp_servers: None,
979 client_capabilities: None,
980 acp_state: Arc::new(Mutex::new(AcpClientFsState::new())),
981 mcp_child_pids: Vec::new(),
982 };
983 }
985
986 assert!(
987 !shared_token.is_cancelled(),
988 "dropping AcpSession must not cancel token if other Arc clones exist"
989 );
990 }
991}