@team-agent/installer 0.3.13 → 0.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,12 +2,12 @@
2
2
 
3
3
  use std::collections::BTreeMap;
4
4
  use std::io::IsTerminal;
5
- use std::path::Path;
5
+ use std::path::{Path, PathBuf};
6
6
  use std::process::{Command, Stdio};
7
7
 
8
8
  use crate::provider::{get_adapter, Provider};
9
9
  use crate::tmux_backend::TmuxBackend;
10
- use crate::transport::{SessionName, Transport};
10
+ use crate::transport::{PaneId, SessionName, Target, Transport};
11
11
 
12
12
  use super::helpers::{
13
13
  provider_wire, resolve_workspace_for_hash, sanitize_session_folder, sha1_hex_prefix,
@@ -125,7 +125,10 @@ pub(crate) fn leader_env_for_identity(
125
125
  identity.team_id.as_str().to_string(),
126
126
  );
127
127
  if provider == Provider::Copilot {
128
- leader_env.insert("COPILOT_DISABLE_TERMINAL_TITLE".to_string(), "1".to_string());
128
+ leader_env.insert(
129
+ "COPILOT_DISABLE_TERMINAL_TITLE".to_string(),
130
+ "1".to_string(),
131
+ );
129
132
  }
130
133
  leader_env
131
134
  }
@@ -171,7 +174,7 @@ pub fn execute_leader_plan(
171
174
  let detached = plan.mode == LeaderStartMode::NewTmuxSession
172
175
  && !std::io::stdin().is_terminal()
173
176
  && insert_detach_flag(&mut argv);
174
- let status = run_leader_argv(&argv, &plan.leader_env)?;
177
+ let status = run_leader_argv(&argv, &plan.leader_env, plan, workspace)?;
175
178
  let code = status.code();
176
179
  if !status.success() {
177
180
  return Err(LeaderError::Start(format!(
@@ -294,6 +297,8 @@ fn insert_detach_flag(argv: &mut Vec<String>) -> bool {
294
297
  fn run_leader_argv(
295
298
  argv: &[String],
296
299
  env: &BTreeMap<String, String>,
300
+ plan: &LeaderStartPlan,
301
+ workspace: &Path,
297
302
  ) -> Result<std::process::ExitStatus, LeaderError> {
298
303
  let Some(program) = argv.first() else {
299
304
  return Err(LeaderError::Start(
@@ -307,9 +312,84 @@ fn run_leader_argv(
307
312
  .stdout(Stdio::inherit())
308
313
  .stderr(Stdio::inherit())
309
314
  .spawn()?;
315
+ if plan.mode == LeaderStartMode::ExecProvider {
316
+ spawn_exec_provider_startup_prompt_handler(plan.provider, workspace.to_path_buf());
317
+ }
310
318
  child.wait().map_err(LeaderError::Io)
311
319
  }
312
320
 
321
+ fn spawn_exec_provider_startup_prompt_handler(provider: Provider, workspace: PathBuf) {
322
+ let Some(pane_id) = std::env::var("TMUX_PANE")
323
+ .ok()
324
+ .filter(|value| !value.trim().is_empty())
325
+ else {
326
+ write_leader_startup_prompt_event(
327
+ &workspace,
328
+ "leader.startup_prompt_skipped",
329
+ serde_json::json!({
330
+ "provider": provider_wire(provider),
331
+ "reason": "tmux_pane_missing",
332
+ "action": "skip",
333
+ }),
334
+ );
335
+ return;
336
+ };
337
+ std::thread::spawn(move || {
338
+ let transport = tmux_transport_for_current_pane();
339
+ let _ = handle_exec_provider_startup_prompts(
340
+ provider, &workspace, &pane_id, &transport, 30, 0.5,
341
+ );
342
+ });
343
+ }
344
+
345
+ fn tmux_transport_for_current_pane() -> TmuxBackend {
346
+ crate::tmux_backend::socket_name_from_tmux_env()
347
+ .map(|endpoint| TmuxBackend::for_tmux_endpoint(&endpoint))
348
+ .unwrap_or_else(TmuxBackend::new)
349
+ }
350
+
351
+ pub fn handle_exec_provider_startup_prompts(
352
+ provider: Provider,
353
+ workspace: &Path,
354
+ pane_id: &str,
355
+ transport: &dyn Transport,
356
+ checks: usize,
357
+ sleep_s: f64,
358
+ ) -> crate::provider::StartupPromptOutcome {
359
+ let target = Target::Pane(PaneId::new(pane_id.to_string()));
360
+ let outcome =
361
+ get_adapter(provider).handle_startup_prompts_outcome(transport, &target, checks, sleep_s);
362
+ for handled in &outcome.handled {
363
+ write_leader_startup_prompt_event(
364
+ workspace,
365
+ "leader.startup_prompt_handled",
366
+ serde_json::json!({
367
+ "provider": provider_wire(provider),
368
+ "pane_id": pane_id,
369
+ "prompt": handled.prompt,
370
+ "action": handled.action,
371
+ }),
372
+ );
373
+ }
374
+ if let Some(error) = &outcome.capture_error {
375
+ write_leader_startup_prompt_event(
376
+ workspace,
377
+ "leader.startup_prompt_capture_failed",
378
+ serde_json::json!({
379
+ "provider": provider_wire(provider),
380
+ "pane_id": pane_id,
381
+ "action": "capture",
382
+ "error": error,
383
+ }),
384
+ );
385
+ }
386
+ outcome
387
+ }
388
+
389
+ fn write_leader_startup_prompt_event(workspace: &Path, event: &str, fields: serde_json::Value) {
390
+ let _ = crate::event_log::EventLog::new(workspace).write(event, fields);
391
+ }
392
+
313
393
  fn ensure_tmux_installed() -> Result<(), LeaderError> {
314
394
  match Command::new("tmux").arg("-V").output() {
315
395
  Ok(output) if output.status.success() => Ok(()),
@@ -388,3 +468,198 @@ fn shlex_quote(raw: &str) -> String {
388
468
  format!("'{}'", raw.replace('\'', "'\"'\"'"))
389
469
  }
390
470
  }
471
+
472
+ #[cfg(test)]
473
+ mod tests {
474
+ use std::collections::BTreeMap;
475
+ use std::path::Path;
476
+ use std::sync::Mutex;
477
+
478
+ use crate::model::enums::PaneLiveness;
479
+ use crate::provider::{Provider, COPILOT_READY_MARKER, COPILOT_TRUST_PROMPT_MARKER};
480
+ use crate::transport::{
481
+ AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
482
+ InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, SessionName,
483
+ SetEnvOutcome, SpawnResult, SubmitVerification, Target, Transport, TransportError,
484
+ TurnVerification, WindowName,
485
+ };
486
+
487
+ use super::handle_exec_provider_startup_prompts;
488
+
489
+ struct ScriptedTransport {
490
+ screens: Mutex<Vec<String>>,
491
+ sent: Mutex<Vec<(Target, Vec<Key>)>>,
492
+ }
493
+
494
+ impl ScriptedTransport {
495
+ fn new(screens: Vec<String>) -> Self {
496
+ Self {
497
+ screens: Mutex::new(screens),
498
+ sent: Mutex::new(Vec::new()),
499
+ }
500
+ }
501
+
502
+ fn sent(&self) -> Vec<(Target, Vec<Key>)> {
503
+ match self.sent.lock() {
504
+ Ok(guard) => guard.clone(),
505
+ Err(poisoned) => poisoned.into_inner().clone(),
506
+ }
507
+ }
508
+ }
509
+
510
+ impl Transport for ScriptedTransport {
511
+ fn kind(&self) -> BackendKind {
512
+ BackendKind::Tmux
513
+ }
514
+
515
+ fn spawn_first(
516
+ &self,
517
+ _session: &SessionName,
518
+ _window: &WindowName,
519
+ _argv: &[String],
520
+ _cwd: &Path,
521
+ _env: &BTreeMap<String, String>,
522
+ ) -> Result<SpawnResult, TransportError> {
523
+ Err(TransportError::Io(std::io::Error::other(
524
+ "spawn_first not used by startup-prompt test",
525
+ )))
526
+ }
527
+
528
+ fn spawn_into(
529
+ &self,
530
+ _session: &SessionName,
531
+ _window: &WindowName,
532
+ _argv: &[String],
533
+ _cwd: &Path,
534
+ _env: &BTreeMap<String, String>,
535
+ ) -> Result<SpawnResult, TransportError> {
536
+ Err(TransportError::Io(std::io::Error::other(
537
+ "spawn_into not used by startup-prompt test",
538
+ )))
539
+ }
540
+
541
+ fn inject(
542
+ &self,
543
+ _target: &Target,
544
+ _payload: &InjectPayload,
545
+ _submit: Key,
546
+ _bracketed: bool,
547
+ ) -> Result<InjectReport, TransportError> {
548
+ Ok(InjectReport {
549
+ stage_reached: InjectStage::Submit,
550
+ inject_verification: InjectVerification::CaptureContainsToken,
551
+ submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
552
+ turn_verification: TurnVerification::NotRequired,
553
+ attempts: 1,
554
+ })
555
+ }
556
+
557
+ fn send_keys(&self, target: &Target, keys: &[Key]) -> Result<(), TransportError> {
558
+ match self.sent.lock() {
559
+ Ok(mut guard) => guard.push((target.clone(), keys.to_vec())),
560
+ Err(poisoned) => poisoned.into_inner().push((target.clone(), keys.to_vec())),
561
+ }
562
+ Ok(())
563
+ }
564
+
565
+ fn capture(
566
+ &self,
567
+ _target: &Target,
568
+ range: CaptureRange,
569
+ ) -> Result<CapturedText, TransportError> {
570
+ let text = match self.screens.lock() {
571
+ Ok(mut guard) => {
572
+ if guard.is_empty() {
573
+ String::new()
574
+ } else {
575
+ guard.remove(0)
576
+ }
577
+ }
578
+ Err(poisoned) => {
579
+ let mut guard = poisoned.into_inner();
580
+ if guard.is_empty() {
581
+ String::new()
582
+ } else {
583
+ guard.remove(0)
584
+ }
585
+ }
586
+ };
587
+ Ok(CapturedText { text, range })
588
+ }
589
+
590
+ fn query(
591
+ &self,
592
+ _target: &Target,
593
+ _field: PaneField,
594
+ ) -> Result<Option<String>, TransportError> {
595
+ Ok(None)
596
+ }
597
+
598
+ fn liveness(&self, _pane: &PaneId) -> Result<PaneLiveness, TransportError> {
599
+ Ok(PaneLiveness::Unknown)
600
+ }
601
+
602
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
603
+ Ok(Vec::new())
604
+ }
605
+
606
+ fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
607
+ Ok(false)
608
+ }
609
+
610
+ fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
611
+ Ok(Vec::new())
612
+ }
613
+
614
+ fn set_session_env(
615
+ &self,
616
+ _session: &SessionName,
617
+ _key: &str,
618
+ _value: &str,
619
+ ) -> Result<SetEnvOutcome, TransportError> {
620
+ Ok(SetEnvOutcome::Applied)
621
+ }
622
+
623
+ fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
624
+ Ok(())
625
+ }
626
+
627
+ fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
628
+ Ok(())
629
+ }
630
+
631
+ fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
632
+ Ok(AttachOutcome::Unsupported {
633
+ reason: "not used by startup-prompt test".to_string(),
634
+ })
635
+ }
636
+ }
637
+
638
+ #[test]
639
+ fn exec_provider_leader_startup_prompt_handler_reuses_copilot_adapter() {
640
+ let transport = ScriptedTransport::new(vec![
641
+ COPILOT_TRUST_PROMPT_MARKER.to_string(),
642
+ COPILOT_READY_MARKER.to_string(),
643
+ ]);
644
+ let workspace =
645
+ std::env::temp_dir().join(format!("ta_rs_red2_leader_startup_{}", std::process::id()));
646
+ let _ = std::fs::create_dir_all(&workspace);
647
+
648
+ let outcome = handle_exec_provider_startup_prompts(
649
+ Provider::Copilot,
650
+ &workspace,
651
+ "%0",
652
+ &transport,
653
+ 5,
654
+ 0.0,
655
+ );
656
+
657
+ assert_eq!(outcome.handled.len(), 1);
658
+ assert_eq!(outcome.handled[0].prompt, "copilot_workspace_trust");
659
+ assert_eq!(outcome.handled[0].action, "sent_enter_yes_session");
660
+ let sent = transport.sent();
661
+ assert_eq!(sent.len(), 1);
662
+ assert_eq!(sent[0].0, Target::Pane(PaneId::new("%0")));
663
+ assert_eq!(sent[0].1, vec![Key::Enter]);
664
+ }
665
+ }
@@ -1059,23 +1059,13 @@ fn unbound_launched_provider(launched: &serde_json::Value) -> Option<String> {
1059
1059
  {
1060
1060
  return Some(provider);
1061
1061
  }
1062
- let workspace = launched
1063
- .get("workspace")
1064
- .and_then(serde_json::Value::as_str)
1065
- .filter(|workspace| !workspace.is_empty())?;
1066
1062
  let pane = launched
1067
1063
  .get("team_owner")
1068
1064
  .and_then(|owner| owner.get("pane_id"))
1069
1065
  .and_then(serde_json::Value::as_str)
1070
1066
  .filter(|pane| !pane.is_empty())?;
1071
1067
  let target = PaneId::new(pane);
1072
- crate::tmux_backend::TmuxBackend::for_workspace(Path::new(workspace))
1073
- .list_targets()
1074
- .ok()?
1075
- .into_iter()
1076
- .find(|info| info.pane_id == target)
1077
- .and_then(|info| crate::leader::attribute_pane_provider(&info))
1078
- .and_then(provider_wire_string)
1068
+ attributed_provider_for_pane_across_tmux_sockets(&target).and_then(provider_wire_string)
1079
1069
  }
1080
1070
 
1081
1071
  fn provider_wire_string(provider: Provider) -> Option<String> {
@@ -1084,9 +1074,38 @@ fn provider_wire_string(provider: Provider) -> Option<String> {
1084
1074
  .and_then(|value| value.as_str().map(str::to_string))
1085
1075
  }
1086
1076
 
1077
+ fn attributed_provider_for_pane_across_tmux_sockets(pane: &PaneId) -> Option<Provider> {
1078
+ crate::tmux_backend::tmux_socket_endpoints()
1079
+ .into_iter()
1080
+ .filter_map(|endpoint| {
1081
+ crate::tmux_backend::TmuxBackend::for_tmux_endpoint(&endpoint)
1082
+ .list_targets()
1083
+ .ok()
1084
+ })
1085
+ .flatten()
1086
+ .find(|info| info.pane_id == *pane)
1087
+ .and_then(|info| crate::leader::attribute_pane_provider(&info))
1088
+ }
1089
+
1090
+ fn caller_provider_for_seed_with_lookup(
1091
+ caller: &crate::state::owner_gate::CallerIdentity,
1092
+ lookup_pane_provider: impl Fn(&PaneId) -> Option<Provider>,
1093
+ ) -> Option<String> {
1094
+ if !caller.provider.is_empty() {
1095
+ if let Some(provider) = parse_provider(&caller.provider).and_then(provider_wire_string) {
1096
+ return Some(provider);
1097
+ }
1098
+ }
1099
+ (!caller.pane_id.is_empty())
1100
+ .then(|| PaneId::new(&caller.pane_id))
1101
+ .and_then(|pane| lookup_pane_provider(&pane))
1102
+ .and_then(provider_wire_string)
1103
+ }
1104
+
1087
1105
  #[cfg(test)]
1088
1106
  mod e22_unbound_owner_provider_tests {
1089
1107
  use super::*;
1108
+ use crate::state::owner_gate::CallerIdentity;
1090
1109
 
1091
1110
  #[test]
1092
1111
  fn unbound_owner_preserves_explicit_copilot_provider() {
@@ -1137,6 +1156,83 @@ mod e22_unbound_owner_provider_tests {
1137
1156
  "unattributed unbound owner must not silently become codex: {launched}"
1138
1157
  );
1139
1158
  }
1159
+
1160
+ fn caller(provider: &str, pane_id: &str) -> CallerIdentity {
1161
+ CallerIdentity {
1162
+ pane_id: pane_id.to_string(),
1163
+ provider: provider.to_string(),
1164
+ machine_fingerprint: "machine".to_string(),
1165
+ leader_session_uuid: "leader-uuid".to_string(),
1166
+ leader_session_uuid_source: "derived".to_string(),
1167
+ }
1168
+ }
1169
+
1170
+ #[test]
1171
+ fn env_seed_attributes_in_tmux_node_form_copilot_from_caller_pane() {
1172
+ let mut state = serde_json::json!({
1173
+ "workspace": "/tmp/team-agent-e22",
1174
+ "leader": {"provider": "copilot"},
1175
+ });
1176
+
1177
+ assert!(seed_launched_owner_from_caller_with_provider_lookup(
1178
+ &mut state,
1179
+ caller("", "%0"),
1180
+ |pane| (pane.as_str() == "%0").then_some(Provider::Copilot),
1181
+ ));
1182
+
1183
+ assert_eq!(
1184
+ state
1185
+ .pointer("/team_owner/provider")
1186
+ .and_then(serde_json::Value::as_str),
1187
+ Some("copilot")
1188
+ );
1189
+ assert_eq!(
1190
+ state
1191
+ .pointer("/leader_receiver/provider")
1192
+ .and_then(serde_json::Value::as_str),
1193
+ Some("copilot")
1194
+ );
1195
+ }
1196
+
1197
+ #[test]
1198
+ fn env_seed_unknown_caller_pane_does_not_default_codex() {
1199
+ let mut state = serde_json::json!({
1200
+ "workspace": "/tmp/team-agent-e22",
1201
+ "leader": {"provider": "copilot"},
1202
+ });
1203
+
1204
+ assert!(seed_launched_owner_from_caller_with_provider_lookup(
1205
+ &mut state,
1206
+ caller("", "%0"),
1207
+ |_| None,
1208
+ ));
1209
+ assert_eq!(
1210
+ state
1211
+ .pointer("/team_owner/pane_id")
1212
+ .and_then(serde_json::Value::as_str),
1213
+ Some("%0")
1214
+ );
1215
+ assert_eq!(
1216
+ state
1217
+ .pointer("/leader_receiver/pane_id")
1218
+ .and_then(serde_json::Value::as_str),
1219
+ Some("%0")
1220
+ );
1221
+ assert!(
1222
+ state
1223
+ .pointer("/leader_receiver/provider")
1224
+ .and_then(serde_json::Value::as_str)
1225
+ != Some("codex"),
1226
+ "unknown caller pane must not silently seed a codex receiver: {state}"
1227
+ );
1228
+ assert!(
1229
+ state
1230
+ .pointer("/team_owner/provider")
1231
+ .and_then(serde_json::Value::as_str)
1232
+ != Some("codex"),
1233
+ "unknown caller pane must not silently become codex: {state}"
1234
+ );
1235
+ }
1140
1236
  }
1141
1237
 
1142
1238
  fn owner_pane_belongs_to_other_team(
@@ -3711,19 +3807,26 @@ fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
3711
3807
  ) else {
3712
3808
  return false;
3713
3809
  };
3714
- let provider = if caller.provider.is_empty() {
3715
- "codex".to_string()
3716
- } else {
3717
- caller.provider
3718
- };
3719
- let pane_id = caller.pane_id;
3720
- if pane_id.is_empty() {
3810
+ seed_launched_owner_from_caller_with_provider_lookup(
3811
+ state,
3812
+ caller,
3813
+ attributed_provider_for_pane_across_tmux_sockets,
3814
+ )
3815
+ }
3816
+
3817
+ fn seed_launched_owner_from_caller_with_provider_lookup(
3818
+ state: &mut serde_json::Value,
3819
+ caller: crate::state::owner_gate::CallerIdentity,
3820
+ lookup_pane_provider: impl Fn(&PaneId) -> Option<Provider>,
3821
+ ) -> bool {
3822
+ if caller.pane_id.is_empty() {
3721
3823
  return false;
3722
3824
  }
3825
+ let provider = caller_provider_for_seed_with_lookup(&caller, lookup_pane_provider);
3826
+ let pane_id = caller.pane_id;
3723
3827
  let owner_epoch = 1u64;
3724
- let owner = serde_json::json!({
3828
+ let mut owner = serde_json::json!({
3725
3829
  "pane_id": pane_id,
3726
- "provider": provider.clone(),
3727
3830
  "machine_fingerprint": caller.machine_fingerprint,
3728
3831
  "leader_session_uuid": caller.leader_session_uuid,
3729
3832
  "owner_epoch": owner_epoch,
@@ -3733,17 +3836,23 @@ fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
3733
3836
  .or_else(|_| std::env::var("USERNAME"))
3734
3837
  .unwrap_or_default(),
3735
3838
  });
3736
- let receiver = serde_json::json!({
3839
+ let mut receiver = serde_json::json!({
3737
3840
  "mode": "direct_tmux",
3738
3841
  "status": "attached",
3739
- "provider": provider,
3740
3842
  "pane_id": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
3741
3843
  "pane": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
3742
3844
  "leader_session_uuid": owner.get("leader_session_uuid").cloned().unwrap_or(serde_json::Value::Null),
3743
3845
  "owner_epoch": owner_epoch,
3744
3846
  "discovery": "quick_start",
3745
3847
  });
3746
- let mut receiver = receiver;
3848
+ if let Some(provider) = provider.as_ref() {
3849
+ if let Some(owner) = owner.as_object_mut() {
3850
+ owner.insert("provider".to_string(), serde_json::json!(provider));
3851
+ }
3852
+ if let Some(receiver) = receiver.as_object_mut() {
3853
+ receiver.insert("provider".to_string(), serde_json::json!(provider));
3854
+ }
3855
+ }
3747
3856
  if let (Some(receiver), Some(socket)) = (
3748
3857
  receiver.as_object_mut(),
3749
3858
  crate::tmux_backend::socket_name_from_tmux_env(),
@@ -36,12 +36,11 @@ pub(super) fn spawn_agent_window(
36
36
  detected_safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn()?;
37
37
  &detected_safety
38
38
  };
39
- let command_agent =
40
- crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
41
- agent,
42
- Some(agent_id.as_str()),
43
- provider,
44
- );
39
+ let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
40
+ agent,
41
+ Some(agent_id.as_str()),
42
+ provider,
43
+ );
45
44
  let system_prompt =
46
45
  crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
47
46
  let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
@@ -60,7 +59,8 @@ pub(super) fn spawn_agent_window(
60
59
  .and_then(|v| v.as_str())
61
60
  .map(str::to_string)
62
61
  .or_else(|| {
63
- let key = crate::messaging::leader_receiver::active_team_key(workspace, &state_for_team);
62
+ let key =
63
+ crate::messaging::leader_receiver::active_team_key(workspace, &state_for_team);
64
64
  (!key.is_empty()).then_some(key)
65
65
  });
66
66
  let mcp_config = adapter
@@ -72,8 +72,11 @@ pub(super) fn spawn_agent_window(
72
72
  agent_id.as_str(),
73
73
  team_id.as_deref().unwrap_or(""),
74
74
  );
75
- let mcp_config_path =
76
- crate::lifecycle::launch::write_worker_mcp_config(workspace, agent_id.as_str(), &mcp_config)?;
75
+ let mcp_config_path = crate::lifecycle::launch::write_worker_mcp_config(
76
+ workspace,
77
+ agent_id.as_str(),
78
+ &mcp_config,
79
+ )?;
77
80
  let profile_launch =
78
81
  crate::lifecycle::profile_launch::prepare_provider_profile_launch_from_json(
79
82
  workspace,
@@ -81,11 +84,7 @@ pub(super) fn spawn_agent_window(
81
84
  agent,
82
85
  Some(&mcp_config),
83
86
  )?;
84
- let command_model = profile_launch
85
- .command_overrides
86
- .model
87
- .as_deref()
88
- .or(model);
87
+ let command_model = profile_launch.command_overrides.model.as_deref().or(model);
89
88
  let context = crate::provider::ProviderCommandContext {
90
89
  auth_mode,
91
90
  mcp_config: Some(&mcp_config),
@@ -252,19 +251,30 @@ pub(super) fn resume_backing_exists_for_agent(
252
251
  rollout_path: Option<&RolloutPath>,
253
252
  ) -> bool {
254
253
  match provider {
254
+ provider if !provider_supports_resume(provider) => {
255
+ let _ = (workspace, agent_id, agent, session_id, rollout_path);
256
+ false
257
+ }
255
258
  Provider::Codex => rollout_path_exists(rollout_path),
256
259
  Provider::Claude | Provider::ClaudeCode => {
257
260
  rollout_path_exists(rollout_path)
258
261
  || event_log_transcript_exists(workspace, agent_id.as_str(), session_id.as_str())
259
262
  }
260
263
  Provider::Copilot => copilot_session_store_has_session(session_id.as_str()),
261
- Provider::GeminiCli | Provider::Fake => {
262
- let _ = agent;
263
- true
264
- }
264
+ Provider::GeminiCli | Provider::Fake => false,
265
265
  }
266
266
  }
267
267
 
268
+ pub(super) fn provider_supports_resume(provider: Provider) -> bool {
269
+ crate::provider::get_adapter(provider).caps().resume
270
+ }
271
+
272
+ pub(super) fn provider_wire_supports_resume(provider: &str) -> bool {
273
+ parse_provider(provider)
274
+ .map(provider_supports_resume)
275
+ .unwrap_or(false)
276
+ }
277
+
268
278
  fn rollout_path_exists(rollout_path: Option<&RolloutPath>) -> bool {
269
279
  rollout_path
270
280
  .as_ref()
@@ -308,13 +318,12 @@ fn copilot_session_store_has_session(session_id: &str) -> bool {
308
318
  ) else {
309
319
  return false;
310
320
  };
311
- conn
312
- .query_row(
313
- "select 1 from sessions where id = ?1 limit 1",
314
- [session_id],
315
- |_| Ok(()),
316
- )
317
- .is_ok()
321
+ conn.query_row(
322
+ "select 1 from sessions where id = ?1 limit 1",
323
+ [session_id],
324
+ |_| Ok(()),
325
+ )
326
+ .is_ok()
318
327
  }
319
328
 
320
329
  pub(crate) fn refresh_missing_provider_sessions(
@@ -469,7 +478,10 @@ pub(super) fn load_team_spec(workspace: &Path) -> Result<YamlValue, LifecycleErr
469
478
  yaml::loads(&text).map_err(|e| LifecycleError::Compile(e.to_string()))
470
479
  }
471
480
 
472
- pub(super) fn find_spec_agent<'a>(spec: &'a YamlValue, agent_id: &AgentId) -> Option<&'a YamlValue> {
481
+ pub(super) fn find_spec_agent<'a>(
482
+ spec: &'a YamlValue,
483
+ agent_id: &AgentId,
484
+ ) -> Option<&'a YamlValue> {
473
485
  let leader_is_agent = spec
474
486
  .get("leader")
475
487
  .and_then(|v| v.get("id"))
@@ -479,23 +491,23 @@ pub(super) fn find_spec_agent<'a>(spec: &'a YamlValue, agent_id: &AgentId) -> Op
479
491
  if leader_is_agent {
480
492
  return None;
481
493
  }
482
- spec.get("agents")?
483
- .as_list()?
484
- .iter()
485
- .find(|agent| {
486
- agent
487
- .get("id")
488
- .and_then(YamlValue::as_str)
489
- .map(|id| id == agent_id.as_str())
490
- .unwrap_or(false)
491
- })
494
+ spec.get("agents")?.as_list()?.iter().find(|agent| {
495
+ agent
496
+ .get("id")
497
+ .and_then(YamlValue::as_str)
498
+ .map(|id| id == agent_id.as_str())
499
+ .unwrap_or(false)
500
+ })
492
501
  }
493
502
 
494
503
  pub(super) fn unknown_worker(agent_id: &AgentId) -> LifecycleError {
495
504
  LifecycleError::RequirementUnmet(format!("unknown worker agent id: {agent_id}"))
496
505
  }
497
506
 
498
- pub(super) fn state_session_name_from_spec(state: &serde_json::Value, spec: &YamlValue) -> SessionName {
507
+ pub(super) fn state_session_name_from_spec(
508
+ state: &serde_json::Value,
509
+ spec: &YamlValue,
510
+ ) -> SessionName {
499
511
  state
500
512
  .get("session_name")
501
513
  .and_then(|v| v.as_str())
@@ -709,9 +721,7 @@ pub(super) fn agent_is_running(
709
721
  agent_id: &AgentId,
710
722
  transport: &dyn crate::transport::Transport,
711
723
  ) -> bool {
712
- let agent_state = state
713
- .get("agents")
714
- .and_then(|v| v.get(agent_id.as_str()));
724
+ let agent_state = state.get("agents").and_then(|v| v.get(agent_id.as_str()));
715
725
  let status = agent_state
716
726
  .and_then(|v| v.get("status"))
717
727
  .and_then(|v| v.as_str())