@team-agent/installer 0.5.51 → 0.5.52

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 (27) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/named_address.rs +35 -67
  4. package/crates/team-agent/src/cli/tests/main_preserved.rs +1 -0
  5. package/crates/team-agent/src/leader/lease.rs +202 -16
  6. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +7 -1
  7. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1 -1
  8. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +8 -8
  9. package/crates/team-agent/src/lifecycle/restart/agent.rs +19 -3
  10. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +100 -1
  11. package/crates/team-agent/src/lifecycle/restart/remove.rs +73 -10
  12. package/crates/team-agent/src/lifecycle/restart.rs +25 -1
  13. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +331 -0
  14. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +12 -0
  15. package/crates/team-agent/src/mcp_server/tests/tools.rs +18 -1
  16. package/crates/team-agent/src/mcp_server/tests.rs +11 -4
  17. package/crates/team-agent/src/messaging/delivery.rs +126 -147
  18. package/crates/team-agent/src/messaging/leader_channel.rs +171 -0
  19. package/crates/team-agent/src/messaging/mod.rs +5 -0
  20. package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
  21. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  22. package/crates/team-agent/src/state/persist.rs +282 -20
  23. package/crates/team-agent/src/state/projection.rs +245 -30
  24. package/crates/team-agent/src/state/repository/intent.rs +13 -0
  25. package/crates/team-agent/src/state/repository.rs +36 -12
  26. package/crates/team-agent/src/state/selector.rs +9 -18
  27. package/package.json +4 -4
package/Cargo.lock CHANGED
@@ -575,7 +575,7 @@ dependencies = [
575
575
 
576
576
  [[package]]
577
577
  name = "team-agent"
578
- version = "0.5.51"
578
+ version = "0.5.52"
579
579
  dependencies = [
580
580
  "anyhow",
581
581
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.51"
12
+ version = "0.5.52"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -939,103 +939,71 @@ fn resolve_leader(
939
939
  );
940
940
  return Err(err);
941
941
  }
942
- if crate::codex_app_server::receiver_is_app_server(receiver) {
943
- return resolve_app_server_leader(
944
- sender_workspace,
945
- target_workspace,
946
- team,
947
- parsed,
948
- receiver,
949
- );
950
- }
951
- let pane_id = string_field(receiver, "pane_id")
952
- .filter(|pane| !pane.is_empty())
953
- .ok_or_else(|| {
954
- let mut err = leader_not_attached_third_party(
955
- target_workspace,
956
- team,
957
- None,
958
- None,
959
- None,
960
- transport.tmux_endpoint(),
961
- );
962
- err.candidates =
963
- leader_advisory_candidates(state, team, transport, "state_recorded_socket");
964
- err
965
- })?;
966
942
  let session = string_field(receiver, "session_name");
967
943
  let window = string_field(receiver, "window_name");
968
944
  let socket = string_field(receiver, "tmux_socket")
969
945
  .map(str::to_string)
970
946
  .or_else(|| transport.tmux_endpoint());
971
- let targets = list_targets(transport)?;
972
- let matches = targets
973
- .iter()
974
- .filter(|target| {
975
- target.pane_id.as_str() == pane_id
976
- && session.is_none_or(|expected| target.session.as_str() == expected)
977
- && window.is_none_or(|expected| {
978
- target
979
- .window_name
980
- .as_ref()
981
- .is_some_and(|name| name.as_str() == expected)
982
- })
983
- })
984
- .collect::<Vec<_>>();
985
- let stale_pane_seen = targets
986
- .iter()
987
- .any(|target| target.pane_id.as_str() == pane_id);
988
- match matches.len() {
989
- 1 => Ok(ResolvedNamedAddress {
947
+ match crate::messaging::resolve_live_leader_channel(
948
+ target_workspace,
949
+ receiver,
950
+ transport,
951
+ ) {
952
+ crate::messaging::LeaderChannelResolution::Live(
953
+ crate::messaging::LiveLeaderChannel::DirectTmux(channel),
954
+ ) => Ok(ResolvedNamedAddress {
990
955
  raw_name: parsed.display_name(),
991
956
  target_kind: NamedTargetKind::Leader,
992
957
  sender_workspace: sender_workspace.to_path_buf(),
993
958
  target_workspace: target_workspace.to_path_buf(),
994
959
  team_key: Some(team.to_string()),
995
960
  agent_id: None,
996
- pane_id: pane_id.to_string(),
961
+ pane_id: channel.pane_id.clone(),
997
962
  session_name: session.map(str::to_string),
998
963
  window_name: window.map(str::to_string),
999
964
  tmux_endpoint: socket,
1000
965
  transport_kind: Some("direct_tmux".to_string()),
1001
966
  app_server: None,
1002
- state_pane_id: Some(pane_id.to_string()),
967
+ state_pane_id: Some(channel.pane_id),
1003
968
  state_pane_stale: false,
1004
969
  agent_status: None,
1005
- warning: None,
970
+ warning: (!channel.metadata_drift.is_empty()).then(|| {
971
+ format!(
972
+ "leader_channel_metadata_drift:{}",
973
+ channel.metadata_drift.join(",")
974
+ )
975
+ }),
1006
976
  }),
1007
- 0 => {
977
+ crate::messaging::LeaderChannelResolution::Live(
978
+ crate::messaging::LiveLeaderChannel::CodexAppServer(_),
979
+ ) => resolve_app_server_leader(sender_workspace, target_workspace, team, parsed, receiver),
980
+ crate::messaging::LeaderChannelResolution::Unbound(reason) => {
981
+ let pane_id = string_field(receiver, "pane_id");
1008
982
  let mut err = leader_not_attached_third_party(
1009
983
  target_workspace,
1010
984
  team,
1011
- Some(pane_id),
985
+ pane_id,
1012
986
  session,
1013
987
  window,
1014
988
  socket,
1015
989
  );
1016
- if stale_pane_seen {
1017
- err.log = format!("{} stale_tuple_mismatch=true", err.log);
1018
- }
990
+ err.log = format!("{} channel_unbound={reason:?}", err.log);
1019
991
  err.candidates =
1020
992
  leader_advisory_candidates(state, team, transport, "state_recorded_socket");
1021
993
  Err(err)
1022
994
  }
1023
- _ => Err(name_ambiguous(
1024
- "multiple live panes matched leader_receiver pane id",
1025
- matches
1026
- .into_iter()
1027
- .map(|pane| {
1028
- json!({
1029
- "name": format!("{team}/leader"),
1030
- "workspace": target_workspace.display().to_string(),
1031
- "team_key": team,
1032
- "pane_id": pane.pane_id.as_str(),
1033
- "session": pane.session.as_str(),
1034
- "window": pane.window_name.as_ref().map(|w| w.as_str()),
1035
- })
1036
- })
1037
- .collect(),
1038
- )),
995
+ crate::messaging::LeaderChannelResolution::ProbeFailed(error) => {
996
+ let mut err = leader_not_attached_third_party(
997
+ target_workspace,
998
+ team,
999
+ string_field(receiver, "pane_id"),
1000
+ session,
1001
+ window,
1002
+ socket,
1003
+ );
1004
+ err.log = format!("{} channel_probe_failed={error}", err.log);
1005
+ Err(err)
1006
+ }
1039
1007
  }
1040
1008
  }
1041
1009
 
@@ -400,6 +400,7 @@ fn ux_wait_ready_does_not_report_ready_true_without_ready_runtime_state() {
400
400
  crate::state::persist::save_runtime_state(
401
401
  &ws,
402
402
  &json!({
403
+ "active_team_key": "wait-ready",
403
404
  "agents": {"w1": {"status": "starting"}},
404
405
  "tasks": [{"id": "t1", "assignee": "w1", "status": "pending"}],
405
406
  "leader_receiver": {"status": "attached"},
@@ -712,25 +712,46 @@ fn claim_lease_no_incident_with_target(
712
712
  candidate_source,
713
713
  pre_epoch,
714
714
  );
715
- if converged {
715
+ let observation_refreshed = caller_pane_info.is_some_and(|pane_info| {
716
+ refresh_already_bound_receiver_observation(
717
+ state,
718
+ pane_info,
719
+ observed_endpoint.or(current_endpoint.as_deref()),
720
+ scoped_team.or(team),
721
+ )
722
+ });
723
+ if converged || observation_refreshed {
716
724
  write_claim_state(workspace, state, scoped_team, team)?;
717
- topology_convergence = verify_persisted_topology_convergence(
718
- workspace,
719
- team_id.as_str(),
720
- topology_convergence,
721
- pre_epoch,
722
- )?;
723
- if topology_convergence
724
- .as_ref()
725
- .and_then(|metadata| metadata.get("status"))
726
- .and_then(Value::as_str)
727
- != Some("converged")
728
- {
729
- return Ok(convergence_persistence_refusal(
725
+ if observation_refreshed {
726
+ event_log.write(
727
+ "leader_receiver.observation_refreshed",
728
+ json!({
729
+ "pane_id": caller_pane.as_str(),
730
+ "owner_epoch": pre_epoch.0,
731
+ "owner_changed": false,
732
+ "source": "claim_leader_already_bound",
733
+ }),
734
+ )?;
735
+ }
736
+ if converged {
737
+ topology_convergence = verify_persisted_topology_convergence(
738
+ workspace,
739
+ team_id.as_str(),
730
740
  topology_convergence,
731
741
  pre_epoch,
732
- Some(caller_pane.clone()),
733
- ));
742
+ )?;
743
+ if topology_convergence
744
+ .as_ref()
745
+ .and_then(|metadata| metadata.get("status"))
746
+ .and_then(Value::as_str)
747
+ != Some("converged")
748
+ {
749
+ return Ok(convergence_persistence_refusal(
750
+ topology_convergence,
751
+ pre_epoch,
752
+ Some(caller_pane.clone()),
753
+ ));
754
+ }
734
755
  }
735
756
  }
736
757
  return Ok(LeaseResult {
@@ -1659,6 +1680,108 @@ fn receiver_fingerprint(target: &PaneInfo) -> String {
1659
1680
  )
1660
1681
  }
1661
1682
 
1683
+ fn refresh_already_bound_receiver_observation(
1684
+ state: &mut Value,
1685
+ observed: &PaneInfo,
1686
+ observed_endpoint: Option<&str>,
1687
+ team_key: Option<&str>,
1688
+ ) -> bool {
1689
+ let attached_at = now_ts();
1690
+ let mut refreshed = state
1691
+ .get_mut("leader_receiver")
1692
+ .and_then(Value::as_object_mut)
1693
+ .is_some_and(|receiver| {
1694
+ refresh_receiver_observation(
1695
+ receiver,
1696
+ observed,
1697
+ observed_endpoint,
1698
+ attached_at.as_str(),
1699
+ )
1700
+ });
1701
+ if let Some(team_key) = team_key {
1702
+ refreshed |= state
1703
+ .get_mut("teams")
1704
+ .and_then(|teams| teams.get_mut(team_key))
1705
+ .and_then(|entry| entry.get_mut("leader_receiver"))
1706
+ .and_then(Value::as_object_mut)
1707
+ .is_some_and(|receiver| {
1708
+ refresh_receiver_observation(
1709
+ receiver,
1710
+ observed,
1711
+ observed_endpoint,
1712
+ attached_at.as_str(),
1713
+ )
1714
+ });
1715
+ }
1716
+ refreshed
1717
+ }
1718
+
1719
+ fn refresh_receiver_observation(
1720
+ receiver: &mut serde_json::Map<String, Value>,
1721
+ observed: &PaneInfo,
1722
+ observed_endpoint: Option<&str>,
1723
+ attached_at: &str,
1724
+ ) -> bool {
1725
+ let before = Value::Object(receiver.clone());
1726
+ receiver.insert(
1727
+ "session_name".to_string(),
1728
+ Value::String(observed.session.as_str().to_string()),
1729
+ );
1730
+ receiver.insert(
1731
+ "window_index".to_string(),
1732
+ observed
1733
+ .window_index
1734
+ .map(|value| Value::String(value.to_string()))
1735
+ .unwrap_or(Value::Null),
1736
+ );
1737
+ receiver.insert(
1738
+ "window_name".to_string(),
1739
+ observed
1740
+ .window_name
1741
+ .as_ref()
1742
+ .map(|value| Value::String(value.as_str().to_string()))
1743
+ .unwrap_or(Value::Null),
1744
+ );
1745
+ receiver.insert(
1746
+ "pane_index".to_string(),
1747
+ observed
1748
+ .pane_index
1749
+ .map(|value| Value::String(value.to_string()))
1750
+ .unwrap_or(Value::Null),
1751
+ );
1752
+ receiver.insert(
1753
+ "pane_tty".to_string(),
1754
+ observed
1755
+ .tty
1756
+ .as_ref()
1757
+ .map(|value| Value::String(value.clone()))
1758
+ .unwrap_or(Value::Null),
1759
+ );
1760
+ receiver.insert(
1761
+ "pane_current_command".to_string(),
1762
+ observed
1763
+ .current_command
1764
+ .as_ref()
1765
+ .map(|value| Value::String(value.clone()))
1766
+ .unwrap_or(Value::Null),
1767
+ );
1768
+ receiver.insert(
1769
+ "fingerprint".to_string(),
1770
+ Value::String(receiver_fingerprint(observed)),
1771
+ );
1772
+ if let Some(endpoint) = observed_endpoint.filter(|value| !value.is_empty()) {
1773
+ receiver.insert(
1774
+ "tmux_socket".to_string(),
1775
+ Value::String(endpoint.to_string()),
1776
+ );
1777
+ }
1778
+ receiver.insert(
1779
+ "attached_at".to_string(),
1780
+ Value::String(attached_at.to_string()),
1781
+ );
1782
+ Value::Object(receiver.clone()) != before
1783
+ }
1784
+
1662
1785
  fn make_owner(
1663
1786
  provider: Provider,
1664
1787
  pane: &NonEmptyPaneId,
@@ -2124,6 +2247,22 @@ fn readable_team_snapshot_path(w: &BP, n: &str) -> BF /* B0_DIAGNOSTIC_LEGACY_SN
2124
2247
  mod tests {
2125
2248
  use super::*;
2126
2249
 
2250
+ fn observed_pane() -> PaneInfo {
2251
+ PaneInfo {
2252
+ pane_id: PaneId::new("%7"),
2253
+ session: crate::transport::SessionName::new("new-session"),
2254
+ window_index: Some(3),
2255
+ window_name: Some(crate::transport::WindowName::new("new-window")),
2256
+ pane_index: Some(1),
2257
+ tty: Some("/dev/ttys007".to_string()),
2258
+ current_command: Some("codex".to_string()),
2259
+ current_path: None,
2260
+ active: true,
2261
+ pane_pid: Some(700),
2262
+ leader_env: std::collections::BTreeMap::new(),
2263
+ }
2264
+ }
2265
+
2127
2266
  #[test]
2128
2267
  fn attribute_command_provider_recognizes_copilot() {
2129
2268
  assert_eq!(
@@ -2135,4 +2274,51 @@ mod tests {
2135
2274
  Some(Provider::Copilot)
2136
2275
  );
2137
2276
  }
2277
+
2278
+ #[test]
2279
+ fn already_bound_refresh_updates_observations_without_reclaiming() {
2280
+ let mut state = json!({
2281
+ "leader_receiver": {
2282
+ "status": "attached",
2283
+ "pane_id": "%7",
2284
+ "owner_epoch": 10,
2285
+ "owner_agent_id": "leader",
2286
+ "session_name": "old-session",
2287
+ "pane_tty": "/dev/old",
2288
+ "attached_at": "2026-01-01T00:00:00Z"
2289
+ },
2290
+ "teams": {"team-a": {"leader_receiver": {
2291
+ "status": "attached",
2292
+ "pane_id": "%7",
2293
+ "owner_epoch": 10,
2294
+ "owner_agent_id": "leader",
2295
+ "session_name": "old-session",
2296
+ "pane_tty": "/dev/old",
2297
+ "attached_at": "2026-01-01T00:00:00Z"
2298
+ }}}
2299
+ });
2300
+
2301
+ assert!(refresh_already_bound_receiver_observation(
2302
+ &mut state,
2303
+ &observed_pane(),
2304
+ Some("/tmp/team-agent-live.sock"),
2305
+ Some("team-a"),
2306
+ ));
2307
+ let receiver = &state["leader_receiver"];
2308
+ assert_eq!(receiver["owner_epoch"], json!(10));
2309
+ assert_eq!(receiver["owner_agent_id"], json!("leader"));
2310
+ assert_eq!(receiver["session_name"], json!("new-session"));
2311
+ assert_eq!(receiver["window_name"], json!("new-window"));
2312
+ assert_eq!(receiver["pane_tty"], json!("/dev/ttys007"));
2313
+ assert_eq!(receiver["tmux_socket"], json!("/tmp/team-agent-live.sock"));
2314
+ assert_ne!(receiver["attached_at"], json!("2026-01-01T00:00:00Z"));
2315
+ assert_eq!(
2316
+ state["teams"]["team-a"]["leader_receiver"]["session_name"],
2317
+ json!("new-session")
2318
+ );
2319
+ assert_eq!(
2320
+ state["teams"]["team-a"]["leader_receiver"]["owner_epoch"],
2321
+ json!(10)
2322
+ );
2323
+ }
2138
2324
  }
@@ -193,7 +193,13 @@ pub(super) fn upsert_agent_state_from_role(
193
193
  persist_effective_approval_policy(obj, safety);
194
194
  }
195
195
  agent_map.insert(agent_id.as_str().to_string(), entry);
196
- save_launched_team_state_for_key(workspace, &state, Some(canonical_team_key))
196
+ crate::lifecycle::restart::remove::clear_agent_retirement_in_state(&mut state, agent_id);
197
+ save_launched_team_state_for_key(
198
+ workspace,
199
+ &state,
200
+ Some(canonical_team_key),
201
+ Some(agent_id.as_str()),
202
+ )
197
203
  }
198
204
 
199
205
  pub(super) fn role_source_ownership(workspace: &Path, role_file: &Path) -> &'static str {
@@ -379,7 +379,7 @@ pub fn quick_start_with_transport_in_workspace_with_display(
379
379
  // to "unknown" in the payload) — Phase 2 refactor will thread
380
380
  // ResolvedTransport.source through the launch signature.
381
381
  annotate_runtime_transport(&mut state, transport, &workspace, None);
382
- save_launched_team_state_for_key(&workspace, &state, Some(&state_team_key))?;
382
+ save_launched_team_state_for_key(&workspace, &state, Some(&state_team_key), None)?;
383
383
  annotate_persisted_team_depth(
384
384
  &workspace,
385
385
  &state_team_key,
@@ -146,7 +146,7 @@ pub(super) fn persist_spawn_agent_state(
146
146
  obj.insert("agents".to_string(), serde_json::Value::Object(agents));
147
147
  state = serde_json::Value::Object(obj);
148
148
  }
149
- save_launched_team_state_for_key(workspace, &state, Some(&team_id))
149
+ save_launched_team_state_for_key(workspace, &state, Some(&team_id), None)
150
150
  }
151
151
 
152
152
  pub(super) fn pane_pids_by_started_agent(
@@ -178,13 +178,14 @@ pub(super) fn save_launched_team_state(
178
178
  workspace: &Path,
179
179
  launched: &serde_json::Value,
180
180
  ) -> Result<(), LifecycleError> {
181
- save_launched_team_state_for_key(workspace, launched, None)
181
+ save_launched_team_state_for_key(workspace, launched, None, None)
182
182
  }
183
183
 
184
184
  pub(super) fn save_launched_team_state_for_key(
185
185
  workspace: &Path,
186
186
  launched: &serde_json::Value,
187
187
  team_key: Option<&str>,
188
+ added_agent_id: Option<&str>,
188
189
  ) -> Result<(), LifecycleError> {
189
190
  let existing = load_runtime_state(workspace).unwrap_or_else(|_| serde_json::json!({}));
190
191
  let launched_key = team_key
@@ -231,13 +232,12 @@ pub(super) fn save_launched_team_state_for_key(
231
232
  };
232
233
  let mut projected = crate::state::projection::project_top_level_view(&merged, &launched_key);
233
234
  drop_unbound_top_level_owner(&mut projected);
235
+ let intent = crate::state::repository::StateWriteIntent::launch_team_or_add_agent(
236
+ &launched_key,
237
+ added_agent_id,
238
+ );
234
239
  crate::state::repository::StateRepository::new(workspace)
235
- .save(
236
- crate::state::repository::StateWriteIntent::LaunchTeam {
237
- team_key: &launched_key,
238
- },
239
- &projected,
240
- )
240
+ .save(intent, &projected)
241
241
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))
242
242
  }
243
243
 
@@ -18,13 +18,15 @@ pub fn start_agent(
18
18
  team: Option<&str>,
19
19
  ) -> Result<StartAgentOutcome, LifecycleError> {
20
20
  let paths = lifecycle_paths(workspace, team)?;
21
+ let canonical_team = paths.canonical_team(team).map(str::to_string);
22
+ let team = canonical_team.as_deref();
21
23
  let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
22
24
  workspace: &paths.run_workspace,
23
25
  operation: "start-agent",
24
26
  team,
25
27
  agent_id: Some(agent_id),
26
28
  })?;
27
- let transport = lifecycle_worker_tmux_backend_for_selected_state(&paths.run_workspace, team)?;
29
+ let transport = paths.tmux_backend()?;
28
30
  start_agent_at_paths(
29
31
  &paths.run_workspace,
30
32
  &paths.spec_workspace,
@@ -54,9 +56,12 @@ pub fn start_agent_with_transport(
54
56
  Err(_) if team.is_none() => LifecyclePaths {
55
57
  run_workspace: workspace.to_path_buf(),
56
58
  spec_workspace: workspace.to_path_buf(),
59
+ selected: None,
57
60
  },
58
61
  Err(error) => return Err(error),
59
62
  };
63
+ let canonical_team = paths.canonical_team(team).map(str::to_string);
64
+ let team = canonical_team.as_deref();
60
65
  let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
61
66
  workspace: &paths.run_workspace,
62
67
  operation: "start-agent",
@@ -778,13 +783,15 @@ pub fn stop_agent(
778
783
  team: Option<&str>,
779
784
  ) -> Result<StopAgentReport, LifecycleError> {
780
785
  let paths = lifecycle_paths(workspace, team)?;
786
+ let canonical_team = paths.canonical_team(team).map(str::to_string);
787
+ let team = canonical_team.as_deref();
781
788
  let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
782
789
  workspace: &paths.run_workspace,
783
790
  operation: "stop-agent",
784
791
  team,
785
792
  agent_id: Some(agent_id),
786
793
  })?;
787
- let transport = lifecycle_worker_tmux_backend_for_selected_state(&paths.run_workspace, team)?;
794
+ let transport = paths.tmux_backend()?;
788
795
  stop_agent_at_paths(
789
796
  &paths.run_workspace,
790
797
  &paths.spec_workspace,
@@ -801,6 +808,8 @@ pub fn stop_agent_with_transport(
801
808
  transport: &dyn crate::transport::Transport,
802
809
  ) -> Result<StopAgentReport, LifecycleError> {
803
810
  let paths = lifecycle_paths(workspace, team)?;
811
+ let canonical_team = paths.canonical_team(team).map(str::to_string);
812
+ let team = canonical_team.as_deref();
804
813
  let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
805
814
  workspace: &paths.run_workspace,
806
815
  operation: "stop-agent",
@@ -840,9 +849,12 @@ pub(crate) fn start_agent_at_paths_for_recovery(
840
849
  Err(_) if team.is_none() => LifecyclePaths {
841
850
  run_workspace: workspace.to_path_buf(),
842
851
  spec_workspace: workspace.to_path_buf(),
852
+ selected: None,
843
853
  },
844
854
  Err(error) => return Err(RecoveryError::Lifecycle(error.to_string())),
845
855
  };
856
+ let canonical_team = paths.canonical_team(team).map(str::to_string);
857
+ let team = canonical_team.as_deref();
846
858
  // Best-effort stop: if the live pane still exists we tear it down so the
847
859
  // subsequent start creates a fresh provider process instead of a Noop.
848
860
  // Stop errors are absorbed into the start attempt result — the important
@@ -1217,13 +1229,15 @@ pub fn reset_agent(
1217
1229
  });
1218
1230
  }
1219
1231
  let paths = lifecycle_paths(workspace, team)?;
1232
+ let canonical_team = paths.canonical_team(team).map(str::to_string);
1233
+ let team = canonical_team.as_deref();
1220
1234
  let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
1221
1235
  workspace: &paths.run_workspace,
1222
1236
  operation: "reset-agent",
1223
1237
  team,
1224
1238
  agent_id: Some(agent_id),
1225
1239
  })?;
1226
- let transport = lifecycle_worker_tmux_backend_for_selected_state(&paths.run_workspace, team)?;
1240
+ let transport = paths.tmux_backend()?;
1227
1241
  reset_agent_at_paths(
1228
1242
  &paths.run_workspace,
1229
1243
  &paths.spec_workspace,
@@ -1249,6 +1263,8 @@ pub fn reset_agent_with_transport(
1249
1263
  });
1250
1264
  }
1251
1265
  let paths = lifecycle_paths(workspace, team)?;
1266
+ let canonical_team = paths.canonical_team(team).map(str::to_string);
1267
+ let team = canonical_team.as_deref();
1252
1268
  let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
1253
1269
  workspace: &paths.run_workspace,
1254
1270
  operation: "reset-agent",