@team-agent/installer 0.5.0 → 0.5.2

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.
Files changed (40) hide show
  1. package/Cargo.lock +118 -9
  2. package/Cargo.toml +2 -2
  3. package/crates/team-agent/Cargo.toml +1 -0
  4. package/crates/team-agent/src/app_server_test_support.rs +258 -0
  5. package/crates/team-agent/src/cli/adapters.rs +32 -3
  6. package/crates/team-agent/src/cli/attach_app_server_leader.rs +16 -0
  7. package/crates/team-agent/src/cli/diagnose.rs +14 -5
  8. package/crates/team-agent/src/cli/emit.rs +111 -3
  9. package/crates/team-agent/src/cli/mod.rs +72 -25
  10. package/crates/team-agent/src/cli/named_address.rs +975 -0
  11. package/crates/team-agent/src/cli/send.rs +200 -1
  12. package/crates/team-agent/src/cli/status_port.rs +44 -6
  13. package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
  14. package/crates/team-agent/src/cli/tests/mod.rs +1 -0
  15. package/crates/team-agent/src/cli/tests/named_address.rs +586 -0
  16. package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -0
  17. package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
  18. package/crates/team-agent/src/cli/types.rs +21 -0
  19. package/crates/team-agent/src/codex_app_server.rs +488 -0
  20. package/crates/team-agent/src/conpty/backend.rs +710 -0
  21. package/crates/team-agent/src/conpty/mod.rs +32 -0
  22. package/crates/team-agent/src/coordinator/backoff.rs +45 -6
  23. package/crates/team-agent/src/diagnose/orphans.rs +11 -3
  24. package/crates/team-agent/src/leader/lease.rs +144 -7
  25. package/crates/team-agent/src/leader/owner_bind.rs +5 -2
  26. package/crates/team-agent/src/leader/start.rs +18 -5
  27. package/crates/team-agent/src/leader/tests/lease_api.rs +172 -0
  28. package/crates/team-agent/src/lib.rs +23 -0
  29. package/crates/team-agent/src/lifecycle/launch.rs +127 -3
  30. package/crates/team-agent/src/lifecycle/restart/common.rs +70 -0
  31. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +7 -0
  32. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +90 -0
  33. package/crates/team-agent/src/messaging/delivery.rs +329 -9
  34. package/crates/team-agent/src/messaging/leader_receiver.rs +17 -138
  35. package/crates/team-agent/src/messaging/mod.rs +2 -2
  36. package/crates/team-agent/src/messaging/results.rs +78 -21
  37. package/crates/team-agent/src/messaging/tests/runtime.rs +232 -0
  38. package/crates/team-agent/src/tmux_backend.rs +31 -0
  39. package/crates/team-agent/src/transport_factory.rs +632 -0
  40. package/package.json +4 -4
@@ -112,6 +112,10 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
112
112
  "claim-leader" => cmd_claim_leader(&claim_leader_args(args, cwd)).map(emit_result),
113
113
  // Real dispatch: `cmd_attach_leader` writes the `leader_receiver` binding.
114
114
  "attach-leader" => cmd_attach_leader(&attach_leader_args(args, cwd)?).map(emit_result),
115
+ "attach-app-server-leader" => {
116
+ cmd_attach_app_server_leader(&attach_app_server_leader_args(args, cwd)?)
117
+ .map(emit_result)
118
+ }
115
119
  "identity" => cmd_identity(&identity_args(args, cwd)).map(emit_result),
116
120
  "approvals" => cmd_approvals(&approvals_args(args, cwd)).map(emit_result),
117
121
  "inbox" => cmd_inbox(&inbox_args(args, cwd)?).map(emit_result),
@@ -167,6 +171,7 @@ const DISPATCH_COMMANDS: &[&str] = &[
167
171
  "takeover",
168
172
  "claim-leader",
169
173
  "attach-leader",
174
+ "attach-app-server-leader",
170
175
  "identity",
171
176
  "approvals",
172
177
  "inbox",
@@ -210,6 +215,23 @@ fn is_known_subcommand(command: &str) -> bool {
210
215
  DISPATCH_COMMANDS.contains(&command) || SPEC_ONLY_HELP_COMMANDS.contains(&command)
211
216
  }
212
217
 
218
+ /// Test-only public accessor for `command_help` — allows integration
219
+ /// tests to grep the help copy without depending on internal parser
220
+ /// machinery.
221
+ pub fn __test_command_help(command: Option<&str>) -> String {
222
+ command_help(command)
223
+ }
224
+
225
+ /// Test-only public accessor for `quick_start_args` — allows
226
+ /// integration tests to exercise the parser without going through
227
+ /// stdio + the full `main` entrypoint.
228
+ pub fn __test_quick_start_args(
229
+ args: &[String],
230
+ cwd: &std::path::Path,
231
+ ) -> Result<crate::cli::types::QuickStartArgs, crate::cli::CliError> {
232
+ quick_start_args(args, cwd)
233
+ }
234
+
213
235
  fn command_help(command: Option<&str>) -> String {
214
236
  match command {
215
237
  None => {
@@ -222,10 +244,10 @@ fn command_help(command: Option<&str>) -> String {
222
244
  )
223
245
  }
224
246
  Some("init") => "usage: team-agent init [--workspace WORKSPACE] [--force] [--json]".to_string(),
225
- Some("quick-start") => "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--json]\n\ndefaults: display_backend=adaptive; set display_backend: none in TEAM.md or pass --no-display to use one worker window per agent.".to_string(),
247
+ Some("quick-start") => "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json]\n\ndefaults: display_backend=adaptive; set display_backend: none in TEAM.md or pass --no-display to use one worker window per agent.\n\n--backend selects the worker transport (Phase 1d Batch 2): tmux (default on POSIX; unchanged behavior), conpty (Windows-native ConPTY worker transport; requires the shim binary and Windows host).".to_string(),
226
248
  Some("start") => "usage: team-agent start [TEAMDIR] [--yes] [--fresh] [--json]".to_string(),
227
249
  Some("compile") => "usage: team-agent compile --team TEAM [--out FILE] [--json]".to_string(),
228
- Some("send") => "usage: team-agent send TARGET MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--targets AGENTS] [--task TASK] [--sender SENDER] [--watch-result] [--requires-ack|--no-ack] [--no-wait] [--timeout SECONDS] [--confirm-human] [--message-id ID] [--json]".to_string(),
250
+ Some("send") => "usage: team-agent send TARGET MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--targets AGENTS] [--to-name NAME] [--pane PANE] [--task TASK] [--sender SENDER] [--watch-result] [--requires-ack|--no-ack] [--no-wait] [--timeout SECONDS] [--confirm-human] [--message-id ID] [--json]\n\nMVP: name-based cross-workspace addressing assumes trusted local caller; no auth gate.".to_string(),
229
251
  Some("fallback-send-leader") => "usage: team-agent fallback-send-leader --workspace WORKSPACE --team TEAM --sender AGENT --message-id ID --content TEXT --primary-error ERROR [--task TASK] [--json]\n\nEmergency one-shot fallback only after a leader-bound MCP send transport failure; restart-agent after use.".to_string(),
230
252
  Some("fallback-report-result") => "usage: team-agent fallback-report-result --workspace WORKSPACE --team TEAM --agent-id AGENT --task-id TASK --result-json JSON --primary-error ERROR [--json]\n\nEmergency one-shot fallback only after a report_result MCP transport failure; persists the result DB row, then restart-agent after use.".to_string(),
231
253
  Some("allow-peer-talk") => "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]".to_string(),
@@ -247,6 +269,7 @@ fn command_help(command: Option<&str>) -> String {
247
269
  Some("takeover") => "usage: team-agent takeover [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json]".to_string(),
248
270
  Some("claim-leader") => "usage: team-agent claim-leader [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json]".to_string(),
249
271
  Some("attach-leader") => "usage: team-agent attach-leader [--workspace WORKSPACE] [--team TEAM] [--pane PANE] [--provider PROVIDER] [--confirm] [--json]".to_string(),
272
+ Some("attach-app-server-leader") => "usage: team-agent attach-app-server-leader [--workspace WORKSPACE] [--team TEAM] --socket unix:///path.sock --thread-id THREAD_ID [--json]".to_string(),
250
273
  Some("identity") => "usage: team-agent identity [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
251
274
  Some("approvals") => "usage: team-agent approvals [AGENT] [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
252
275
  Some("inbox") => "usage: team-agent inbox AGENT [--workspace WORKSPACE] [--team TEAM] [--limit N] [--since CURSOR] [--json]".to_string(),
@@ -668,13 +691,20 @@ struct ParsedArgs {
668
691
  out: Option<PathBuf>,
669
692
  auth_mode: Option<String>,
670
693
  pane: Option<String>,
694
+ to_name: Option<String>,
671
695
  provider: Option<String>,
696
+ socket: Option<String>,
697
+ thread_id: Option<String>,
672
698
  message_id: Option<String>,
673
699
  content: Option<String>,
674
700
  primary_error: Option<String>,
675
701
  agent_id: Option<String>,
676
702
  task_id: Option<String>,
677
703
  result_json: Option<String>,
704
+ /// 0.5.x Phase 1d Batch 2: quick-start `--backend <tmux|conpty>`.
705
+ /// Raw string (validated at the quick-start builder); the factory
706
+ /// enforces literal semantics.
707
+ backend: Option<String>,
678
708
  }
679
709
 
680
710
  fn parse_args(args: &[String]) -> ParsedArgs {
@@ -716,6 +746,7 @@ fn parse_args(args: &[String]) -> ParsedArgs {
716
746
  }
717
747
  "--force" => parsed.force = true,
718
748
  "--no-display" => parsed.no_display = true,
749
+ "--backend" => parsed.backend = next_arg(args, &mut i),
719
750
  "--discard-session" => parsed.discard_session = true,
720
751
  "--role-file" => parsed.role_file = next_arg(args, &mut i),
721
752
  "--as" => parsed.as_agent = next_arg(args, &mut i),
@@ -750,7 +781,10 @@ fn parse_args(args: &[String]) -> ParsedArgs {
750
781
  "--out" => parsed.out = next_arg(args, &mut i).map(PathBuf::from),
751
782
  "--auth-mode" => parsed.auth_mode = next_arg(args, &mut i),
752
783
  "--pane" => parsed.pane = next_arg(args, &mut i),
784
+ "--to-name" => parsed.to_name = next_arg(args, &mut i),
753
785
  "--provider" => parsed.provider = next_arg(args, &mut i),
786
+ "--socket" => parsed.socket = next_arg(args, &mut i),
787
+ "--thread-id" => parsed.thread_id = next_arg(args, &mut i),
754
788
  "--message-id" => parsed.message_id = next_arg(args, &mut i),
755
789
  "--content" => parsed.content = next_arg(args, &mut i),
756
790
  "--primary-error" => parsed.primary_error = next_arg(args, &mut i),
@@ -762,9 +796,18 @@ fn parse_args(args: &[String]) -> ParsedArgs {
762
796
  other if other.starts_with("--pane=") => {
763
797
  parsed.pane = Some(other.trim_start_matches("--pane=").to_string());
764
798
  }
799
+ other if other.starts_with("--to-name=") => {
800
+ parsed.to_name = Some(other.trim_start_matches("--to-name=").to_string());
801
+ }
765
802
  other if other.starts_with("--provider=") => {
766
803
  parsed.provider = Some(other.trim_start_matches("--provider=").to_string());
767
804
  }
805
+ other if other.starts_with("--socket=") => {
806
+ parsed.socket = Some(other.trim_start_matches("--socket=").to_string());
807
+ }
808
+ other if other.starts_with("--thread-id=") => {
809
+ parsed.thread_id = Some(other.trim_start_matches("--thread-id=").to_string());
810
+ }
768
811
  other if other.starts_with('-') => {}
769
812
  other => parsed.positionals.push(other.to_string()),
770
813
  }
@@ -828,6 +871,20 @@ fn quick_start_args(args: &[String], cwd: &Path) -> Result<QuickStartArgs, CliEr
828
871
  } else {
829
872
  workspace.join(agents_dir)
830
873
  };
874
+ // 0.5.x Phase 1d Batch 2: validate the `--backend` literal up-front
875
+ // so users get a fast, actionable error instead of a downstream
876
+ // factory refusal. Accept the same literals as the factory
877
+ // `RequestedTransportBackend::parse_literal`.
878
+ if let Some(literal) = parsed.backend.as_deref() {
879
+ let normalized = literal.trim().to_ascii_lowercase();
880
+ if normalized != "tmux" && normalized != "conpty" {
881
+ return Err(CliError::Usage(format!(
882
+ "--backend must be `tmux` or `conpty`, got {literal:?}. \
883
+ `pty` is not a supported literal in Phase 1d (design \
884
+ §Non-Goals + CR C-1 ②)."
885
+ )));
886
+ }
887
+ }
831
888
  Ok(QuickStartArgs {
832
889
  workspace,
833
890
  agents_dir,
@@ -836,6 +893,7 @@ fn quick_start_args(args: &[String], cwd: &Path) -> Result<QuickStartArgs, CliEr
836
893
  yes: parsed.yes,
837
894
  no_display: parsed.no_display,
838
895
  json: parsed.json,
896
+ backend: parsed.backend,
839
897
  })
840
898
  }
841
899
 
@@ -875,7 +933,7 @@ fn resolve_cli_path(cwd: &Path, path: &Path) -> PathBuf {
875
933
 
876
934
  fn send_args(args: &[String], cwd: &Path) -> Result<SendArgs, CliError> {
877
935
  let parsed = parse_args(args);
878
- let target = if parsed.targets.is_some() || parsed.pane.is_some() {
936
+ let target = if parsed.targets.is_some() || parsed.pane.is_some() || parsed.to_name.is_some() {
879
937
  None
880
938
  } else {
881
939
  parsed.positionals.first().cloned()
@@ -903,6 +961,7 @@ fn send_args(args: &[String], cwd: &Path) -> Result<SendArgs, CliError> {
903
961
  json: parsed.json,
904
962
  message_id: parsed.message_id,
905
963
  pane: parsed.pane.clone(),
964
+ to_name: parsed.to_name.clone(),
906
965
  })
907
966
  }
908
967
 
@@ -1073,6 +1132,26 @@ fn attach_leader_args(args: &[String], cwd: &Path) -> Result<AttachLeaderArgs, C
1073
1132
  })
1074
1133
  }
1075
1134
 
1135
+ fn attach_app_server_leader_args(
1136
+ args: &[String],
1137
+ cwd: &Path,
1138
+ ) -> Result<AttachAppServerLeaderArgs, CliError> {
1139
+ let parsed = parse_args(args);
1140
+ Ok(AttachAppServerLeaderArgs {
1141
+ workspace: workspace(&parsed, cwd),
1142
+ team: parsed.team,
1143
+ socket: parsed
1144
+ .socket
1145
+ .filter(|socket| !socket.is_empty())
1146
+ .ok_or_else(|| CliError::Usage("missing --socket".to_string()))?,
1147
+ thread_id: parsed
1148
+ .thread_id
1149
+ .filter(|thread_id| !thread_id.is_empty())
1150
+ .ok_or_else(|| CliError::Usage("missing --thread-id".to_string()))?,
1151
+ json: parsed.json,
1152
+ })
1153
+ }
1154
+
1076
1155
  fn identity_args(args: &[String], cwd: &Path) -> IdentityArgs {
1077
1156
  let parsed = parse_args(args);
1078
1157
  IdentityArgs {
@@ -1602,6 +1681,8 @@ mod tests {
1602
1681
  "--workspace",
1603
1682
  "--team",
1604
1683
  "--targets",
1684
+ "--to-name",
1685
+ "--pane",
1605
1686
  "--watch-result",
1606
1687
  "--timeout",
1607
1688
  "--json",
@@ -1781,6 +1862,33 @@ mod tests {
1781
1862
  let _ = std::fs::remove_dir_all(&cwd);
1782
1863
  }
1783
1864
 
1865
+ #[test]
1866
+ fn send_to_name_positionals_are_message_not_target() {
1867
+ let cwd = tmp_workspace();
1868
+ let args = send_args(&cli_argv(&["--to-name", "team-a/qa", "hello"]), &cwd).unwrap();
1869
+ assert_eq!(args.to_name.as_deref(), Some("team-a/qa"));
1870
+ assert_eq!(args.target, None);
1871
+ assert_eq!(args.targets, None);
1872
+ assert_eq!(args.message, vec!["hello".to_string()]);
1873
+
1874
+ let args = send_args(
1875
+ &cli_argv(&["--to-name=team-a/qa", "multi", "word", "message"]),
1876
+ &cwd,
1877
+ )
1878
+ .unwrap();
1879
+ assert_eq!(args.target, None);
1880
+ assert_eq!(
1881
+ args.message,
1882
+ vec![
1883
+ "multi".to_string(),
1884
+ "word".to_string(),
1885
+ "message".to_string()
1886
+ ]
1887
+ );
1888
+
1889
+ let _ = std::fs::remove_dir_all(&cwd);
1890
+ }
1891
+
1784
1892
  #[test]
1785
1893
  fn send_pane_still_rejects_to_and_empty_message() {
1786
1894
  let cwd = tmp_workspace();
@@ -53,19 +53,23 @@ pub(crate) const SEND_REMINDER: &str = "Message delivered. Wait for the worker t
53
53
  pub(crate) const STATUS_REMINDER: &str = "To wait for results use --watch-result or team-agent collect. Do not capture-pane worker terminals.";
54
54
 
55
55
  pub mod adapters;
56
+ pub mod attach_app_server_leader;
56
57
  pub mod diagnose;
57
58
  pub mod emit;
58
59
  pub mod helpers;
59
60
  pub mod leader;
61
+ pub mod named_address;
60
62
  pub mod profile;
61
63
  pub mod send;
62
64
  pub mod status;
63
65
  pub mod types;
64
66
 
65
67
  pub use adapters::*;
68
+ pub use attach_app_server_leader::*;
66
69
  pub use diagnose::*;
67
70
  pub use emit::*;
68
71
  pub use leader::*;
72
+ pub use named_address::*;
69
73
  pub use profile::*;
70
74
  pub use send::*;
71
75
  pub use status::*;
@@ -118,14 +122,16 @@ pub mod lifecycle_port {
118
122
  team_id: Option<&str>,
119
123
  yes: bool,
120
124
  open_display: bool,
125
+ backend: Option<&str>,
121
126
  ) -> Result<Value, CliError> {
122
- match crate::lifecycle::quick_start_in_workspace_with_display(
127
+ match crate::lifecycle::quick_start_in_workspace_with_display_and_backend(
123
128
  workspace,
124
129
  agents_dir,
125
130
  name,
126
131
  yes,
127
132
  team_id,
128
133
  open_display,
134
+ backend,
129
135
  ) {
130
136
  Ok(report) => Ok(quick_start_value(report)),
131
137
  Err(e) => Ok(error_value(e)),
@@ -190,16 +196,40 @@ pub mod lifecycle_port {
190
196
  crate::tmux_backend::attach_command_for_workspace(cwd, session, window.as_str())
191
197
  }
192
198
  /// `runtime.shutdown`(`cmd_shutdown`)。
199
+ ///
200
+ /// 0.5.x Phase 1d Batch 5: the workspace-transport branch now routes
201
+ /// through `transport_factory::resolve_read_only_transport` so a
202
+ /// conpty team's shutdown reaches the shim rather than a
203
+ /// no-op tmux backend. Legacy tmux endpoint literal in state still
204
+ /// takes the `shutdown_transport_for_endpoint` tmux channel helper
205
+ /// path (intentional — this is the "attached explicit tmux
206
+ /// endpoint" branch and stays tmux-typed).
207
+ ///
208
+ /// `shutdown_with_transport_and_state` is generic over `&dyn
209
+ /// Transport`, so we hand the boxed factory backend down as
210
+ /// `&*box`. Factory refusal falls back to the workspace tmux
211
+ /// backend byte-equivalent to today so daemon liveness paths
212
+ /// don't crash.
193
213
  pub fn shutdown(workspace: &Path, keep_logs: bool, team: Option<&str>) -> Result<Value, CliError> {
194
214
  let run_ws = crate::model::paths::canonical_run_workspace(workspace)
195
215
  .map_err(|e| CliError::Runtime(e.to_string()))?;
196
216
  let state = shutdown_state_for_team(&run_ws, team)?;
197
- let transport = if let Some(endpoint) = legacy_worker_tmux_endpoint(&state) {
198
- shutdown_transport_for_endpoint(endpoint)
199
- } else {
200
- shutdown_workspace_transport(&run_ws)
201
- };
202
- shutdown_with_transport_and_state(workspace, keep_logs, team, &transport, Some(state))
217
+ if let Some(endpoint) = legacy_worker_tmux_endpoint(&state) {
218
+ let transport = shutdown_transport_for_endpoint(endpoint);
219
+ return shutdown_with_transport_and_state(
220
+ workspace, keep_logs, team, &transport, Some(state),
221
+ );
222
+ }
223
+ let boxed: Box<dyn crate::transport::Transport> =
224
+ match crate::transport_factory::resolve_read_only_transport(
225
+ &run_ws,
226
+ Some(&state),
227
+ crate::transport_factory::TransportPurpose::Shutdown,
228
+ ) {
229
+ Ok(r) => r.backend,
230
+ Err(_) => Box::new(shutdown_workspace_transport(&run_ws)),
231
+ };
232
+ shutdown_with_transport_and_state(workspace, keep_logs, team, boxed.as_ref(), Some(state))
203
233
  }
204
234
 
205
235
  /// E12 ①:从 state 锚 pane_id(leader_receiver/team_owner,top+teams)映射到其所在 session
@@ -1189,24 +1219,41 @@ pub mod lifecycle_port {
1189
1219
  }
1190
1220
  }
1191
1221
  }
1192
- let workspace_transport = shutdown_workspace_transport(workspace);
1193
- match crate::transport::Transport::has_session(&workspace_transport, session) {
1194
- Ok(true) => residual = true,
1195
- Ok(false) => {}
1196
- Err(err) if tmux_absent_error(&err.to_string()) => {}
1197
- Err(err) => {
1198
- error.get_or_insert_with(|| err.to_string());
1199
- residual = true;
1200
- }
1201
- }
1202
- let default_transport = crate::tmux_backend::TmuxBackend::new();
1203
- match crate::transport::Transport::has_session(&default_transport, session) {
1204
- Ok(true) => residual = true,
1205
- Ok(false) => {}
1206
- Err(err) if tmux_absent_error(&err.to_string()) => {}
1207
- Err(err) => {
1208
- error.get_or_insert_with(|| err.to_string());
1209
- residual = true;
1222
+ // 0.5.x Phase 1d Batch 5 (design §Batch 5 point 4): a ConPTY
1223
+ // team's residual check MUST NOT read the workspace tmux socket
1224
+ // + shared default tmux socket as evidence for/against ConPTY
1225
+ // residual. Those probes are meaningless for a shim-owned pane
1226
+ // universe: they will always return `has_session=false` (no
1227
+ // tmux session exists) which would look like "no residual" but
1228
+ // gives zero honest information about whether the shim / child
1229
+ // panes have actually been reaped. Skip the tmux fallback
1230
+ // probes when the primary transport is ConPTY; the primary
1231
+ // transport check above already covered the honest question
1232
+ // ("does the shim still have this session?").
1233
+ let primary_is_conpty = matches!(
1234
+ transport.kind(),
1235
+ crate::transport::BackendKind::ConPty
1236
+ );
1237
+ if !primary_is_conpty {
1238
+ let workspace_transport = shutdown_workspace_transport(workspace);
1239
+ match crate::transport::Transport::has_session(&workspace_transport, session) {
1240
+ Ok(true) => residual = true,
1241
+ Ok(false) => {}
1242
+ Err(err) if tmux_absent_error(&err.to_string()) => {}
1243
+ Err(err) => {
1244
+ error.get_or_insert_with(|| err.to_string());
1245
+ residual = true;
1246
+ }
1247
+ }
1248
+ let default_transport = crate::tmux_backend::TmuxBackend::new();
1249
+ match crate::transport::Transport::has_session(&default_transport, session) {
1250
+ Ok(true) => residual = true,
1251
+ Ok(false) => {}
1252
+ Err(err) if tmux_absent_error(&err.to_string()) => {}
1253
+ Err(err) => {
1254
+ error.get_or_insert_with(|| err.to_string());
1255
+ residual = true;
1256
+ }
1210
1257
  }
1211
1258
  }
1212
1259
  let sessions = if residual {