@team-agent/installer 0.4.11 → 0.5.1

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 (80) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +1 -0
  4. package/crates/team-agent/src/cli/diagnose.rs +2 -15
  5. package/crates/team-agent/src/cli/emit.rs +98 -5
  6. package/crates/team-agent/src/cli/mod.rs +2 -0
  7. package/crates/team-agent/src/cli/named_address.rs +864 -0
  8. package/crates/team-agent/src/cli/send.rs +104 -0
  9. package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
  10. package/crates/team-agent/src/cli/tests/mod.rs +1 -0
  11. package/crates/team-agent/src/cli/tests/named_address.rs +455 -0
  12. package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
  13. package/crates/team-agent/src/cli/types.rs +4 -0
  14. package/crates/team-agent/src/compiler.rs +11 -20
  15. package/crates/team-agent/src/coordinator/orphan.rs +2 -2
  16. package/crates/team-agent/src/coordinator/runtime_detectors.rs +5 -4
  17. package/crates/team-agent/src/coordinator/steps/abnormal.rs +1776 -2
  18. package/crates/team-agent/src/coordinator/steps/mod.rs +19 -0
  19. package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +78 -7
  20. package/crates/team-agent/src/coordinator/tests/abnormal.rs +195 -0
  21. package/crates/team-agent/src/coordinator/tick.rs +31 -932
  22. package/crates/team-agent/src/diagnose/orphans.rs +66 -8
  23. package/crates/team-agent/src/layout/worker_window_helpers.rs +5 -7
  24. package/crates/team-agent/src/leader/provider_attribution.rs +7 -5
  25. package/crates/team-agent/src/leader/rediscover.rs +1 -11
  26. package/crates/team-agent/src/lifecycle/launch/plan.rs +19 -8
  27. package/crates/team-agent/src/lifecycle/launch.rs +135 -58
  28. package/crates/team-agent/src/lifecycle/lock.rs +302 -0
  29. package/crates/team-agent/src/lifecycle/mod.rs +1 -0
  30. package/crates/team-agent/src/lifecycle/profile_smoke.rs +1 -11
  31. package/crates/team-agent/src/lifecycle/restart/agent.rs +169 -113
  32. package/crates/team-agent/src/lifecycle/restart/common.rs +108 -17
  33. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -32
  34. package/crates/team-agent/src/lifecycle/restart/remove.rs +16 -2
  35. package/crates/team-agent/src/lifecycle/restart/selection.rs +2 -1
  36. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +6 -0
  37. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +3 -3
  38. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +321 -0
  39. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +35 -5
  40. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +802 -0
  41. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +675 -0
  42. package/crates/team-agent/src/lifecycle/tests.rs +3 -0
  43. package/crates/team-agent/src/lifecycle/types.rs +8 -0
  44. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +24 -9
  45. package/crates/team-agent/src/mcp_server/tools.rs +157 -68
  46. package/crates/team-agent/src/messaging/activity.rs +43 -18
  47. package/crates/team-agent/src/messaging/delivery.rs +161 -97
  48. package/crates/team-agent/src/messaging/helpers.rs +1 -0
  49. package/crates/team-agent/src/messaging/leader_receiver.rs +22 -1
  50. package/crates/team-agent/src/messaging/results.rs +34 -9
  51. package/crates/team-agent/src/messaging/scheduler.rs +52 -9
  52. package/crates/team-agent/src/model/spec.rs +10 -6
  53. package/crates/team-agent/src/provider/adapter.rs +10 -1038
  54. package/crates/team-agent/src/provider/adapters/claude.rs +3 -8
  55. package/crates/team-agent/src/provider/adapters/copilot.rs +2 -5
  56. package/crates/team-agent/src/provider/classify.rs +23 -48
  57. package/crates/team-agent/src/provider/command.rs +16 -7
  58. package/crates/team-agent/src/provider/faults.rs +93 -53
  59. package/crates/team-agent/src/provider/session/capture.rs +4 -15
  60. package/crates/team-agent/src/provider/session_scan/claude.rs +309 -0
  61. package/crates/team-agent/src/provider/session_scan/codex.rs +40 -0
  62. package/crates/team-agent/src/provider/session_scan/common.rs +350 -0
  63. package/crates/team-agent/src/provider/session_scan/copilot.rs +202 -0
  64. package/crates/team-agent/src/provider/session_scan.rs +65 -27
  65. package/crates/team-agent/src/provider/tests/faults.rs +80 -0
  66. package/crates/team-agent/src/provider/types.rs +33 -1
  67. package/crates/team-agent/src/provider/wire.rs +171 -1
  68. package/crates/team-agent/src/state/identity.rs +242 -57
  69. package/crates/team-agent/src/state/identity_keys.rs +9 -12
  70. package/crates/team-agent/src/state/mod.rs +5 -1
  71. package/crates/team-agent/src/state/owner_gate.rs +59 -15
  72. package/crates/team-agent/src/state/ownership.rs +12 -11
  73. package/crates/team-agent/src/state/paths.rs +13 -6
  74. package/crates/team-agent/src/state/persist.rs +671 -128
  75. package/crates/team-agent/src/state/projection.rs +240 -49
  76. package/crates/team-agent/src/state/selector.rs +11 -5
  77. package/crates/team-agent/src/tmux_backend/tests.rs +51 -2
  78. package/crates/team-agent/src/tmux_backend.rs +42 -5
  79. package/crates/team-agent/src/transport/test_support.rs +55 -6
  80. package/package.json +4 -4
@@ -1,7 +1,8 @@
1
- use super::*;
2
1
  use super::common::*;
3
2
  use super::selection::decide_start_mode;
4
3
  use super::team_state::write_team_state;
4
+ use super::*;
5
+ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
5
6
 
6
7
  /// `start_agent(workspace, agent_id, force, open_display, allow_fresh, team)`
7
8
  /// (`lifecycle/start.py:72`)。`_runtime_lock("start-agent")` 下串行:resume-or-fresh
@@ -17,8 +18,13 @@ pub fn start_agent(
17
18
  team: Option<&str>,
18
19
  ) -> Result<StartAgentOutcome, LifecycleError> {
19
20
  let paths = lifecycle_paths(workspace, team)?;
20
- let transport =
21
- lifecycle_worker_tmux_backend_for_selected_state(&paths.run_workspace, team)?;
21
+ let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
22
+ workspace: &paths.run_workspace,
23
+ operation: "start-agent",
24
+ team,
25
+ agent_id: Some(agent_id),
26
+ })?;
27
+ let transport = lifecycle_worker_tmux_backend_for_selected_state(&paths.run_workspace, team)?;
22
28
  start_agent_at_paths(
23
29
  &paths.run_workspace,
24
30
  &paths.spec_workspace,
@@ -43,9 +49,23 @@ pub fn start_agent_with_transport(
43
49
  team: Option<&str>,
44
50
  transport: &dyn crate::transport::Transport,
45
51
  ) -> Result<StartAgentOutcome, LifecycleError> {
52
+ let paths = match lifecycle_paths(workspace, team) {
53
+ Ok(paths) => paths,
54
+ Err(_) if team.is_none() => LifecyclePaths {
55
+ run_workspace: workspace.to_path_buf(),
56
+ spec_workspace: workspace.to_path_buf(),
57
+ },
58
+ Err(error) => return Err(error),
59
+ };
60
+ let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
61
+ workspace: &paths.run_workspace,
62
+ operation: "start-agent",
63
+ team,
64
+ agent_id: Some(agent_id),
65
+ })?;
46
66
  start_agent_at_paths(
47
- workspace,
48
- workspace,
67
+ &paths.run_workspace,
68
+ &paths.spec_workspace,
49
69
  agent_id,
50
70
  force,
51
71
  open_display,
@@ -74,24 +94,27 @@ pub(crate) fn start_agent_at_paths(
74
94
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?
75
95
  };
76
96
  crate::lifecycle::launch::ensure_owner_allowed_for_state(&state, Some(agent_id))?;
77
- let agent = state
97
+ let raw_agent = state
78
98
  .get("agents")
79
99
  .and_then(|v| v.get(agent_id.as_str()))
80
100
  .ok_or_else(|| LifecycleError::RequirementUnmet(format!("agent {agent_id} not found")))?
81
101
  .clone();
82
- if agent
102
+ let agent = rehydrate_agent_command_context_from_spec(spec_workspace, agent_id, &raw_agent);
103
+ if raw_agent
83
104
  .get("paused")
84
105
  .and_then(serde_json::Value::as_bool)
85
106
  .unwrap_or(false)
86
107
  {
87
- return Ok(StartAgentOutcome::Paused { agent_id: agent_id.clone() });
108
+ return Ok(StartAgentOutcome::Paused {
109
+ agent_id: agent_id.clone(),
110
+ });
88
111
  }
89
112
  let session_name = state_session_name(&state);
90
113
  let window = agent_window(&agent, agent_id);
91
114
  let adaptive_layout =
92
115
  open_display && crate::lifecycle::launch::state_uses_adaptive_layout(&state);
93
116
  let agent_live = if adaptive_layout {
94
- agent_pane_live(transport, &agent)
117
+ agent_pane_live(transport, &raw_agent)
95
118
  } else {
96
119
  window_exists(transport, &session_name, &window)
97
120
  };
@@ -103,7 +126,7 @@ pub(crate) fn start_agent_at_paths(
103
126
  // spawn (defensive). The check itself remains a no-op on healthy
104
127
  // state — assert_topology_invariants from Step 1 catches the
105
128
  // upstream corruption.
106
- let has_collision = pane_conflicts_with_leader_or_other(&state, agent_id, &agent);
129
+ let has_collision = pane_conflicts_with_leader_or_other(&state, agent_id, &raw_agent);
107
130
  if has_collision {
108
131
  eprintln!(
109
132
  "team_agent::layout e51_collision_post_step2 agent_id=`{agent_id}` \
@@ -116,7 +139,7 @@ pub(crate) fn start_agent_at_paths(
116
139
  if !force && agent_live {
117
140
  mark_agent_running_noop(&mut state, agent_id, &session_name, &window)?;
118
141
  let team_key = restart_projection_team_key(&state, team);
119
- save_restart_projected_state(workspace, &mut state, &team_key)?;
142
+ save_restart_projected_state(workspace, &mut state, &team_key, &[agent_id.as_str()])?;
120
143
  if let Ok(spec) = load_team_spec(spec_workspace) {
121
144
  write_team_state(spec_workspace, &spec, &state)?;
122
145
  }
@@ -208,7 +231,22 @@ pub(crate) fn start_agent_at_paths(
208
231
  None,
209
232
  Some(resolved_team_key.as_str()),
210
233
  )?;
211
- mark_agent_started(&mut state, agent_id, &spawn_window, &spawn, transport, &safety, start_mode)?;
234
+ verify_spawned_pane_matches_target(
235
+ transport,
236
+ &spawn.spawn.pane_id,
237
+ &session_name,
238
+ &spawn.spawn.window,
239
+ )?;
240
+ let actual_spawn_window = spawn.spawn.window.as_str().to_string();
241
+ mark_agent_started(
242
+ &mut state,
243
+ agent_id,
244
+ &actual_spawn_window,
245
+ &spawn,
246
+ transport,
247
+ &safety,
248
+ start_mode,
249
+ )?;
212
250
  // **0.3.24 add-agent socket drift fix**: keep `state.tmux_endpoint` /
213
251
  // `state.tmux_socket` synchronized with the transport actually used for the
214
252
  // spawn. Without this, add-agent / fork-agent could spawn to a socket that
@@ -218,24 +256,7 @@ pub(crate) fn start_agent_at_paths(
218
256
  // after spawn" race with coordinator and no double source of truth.
219
257
  crate::lifecycle::launch::annotate_runtime_tmux_endpoint(&mut state, transport, workspace);
220
258
  let team_key = restart_projection_team_key(&state, team);
221
- save_restart_projected_state(workspace, &mut state, &team_key)?;
222
- // **0.3.24 reachability gate (调整 4)** — strict, non-capture verification
223
- // that the spawn actually produced an addressable window/pane on the
224
- // transport's socket. Catches the macmini假绿 (`ok=true` returned by
225
- // `add_agent` while the spawned `claude` process orphaned on a different
226
- // socket and the leader never registered it). We use `has_pane` /
227
- // `liveness` / `list_targets` (structural addressing only) — never
228
- // `capture` — to avoid contention with E31's pane-readback gate timing.
229
- if !spawned_pane_is_reachable(transport, &spawn.spawn.pane_id) {
230
- return Err(LifecycleError::RequirementUnmet(format!(
231
- "add-agent spawn unreachable: pane {} not addressable on transport \
232
- socket {:?} (likely socket drift — see 0.3.24 fix); the agent \
233
- process may have orphaned on a different tmux socket. Re-run after \
234
- confirming the team's tmux_endpoint persisted via `team-agent status`.",
235
- spawn.spawn.pane_id.as_str(),
236
- transport.tmux_endpoint().unwrap_or_default()
237
- )));
238
- }
259
+ save_restart_projected_state(workspace, &mut state, &team_key, &[agent_id.as_str()])?;
239
260
  write_start_agent_start_event(
240
261
  workspace,
241
262
  agent_id,
@@ -243,7 +264,7 @@ pub(crate) fn start_agent_at_paths(
243
264
  provider,
244
265
  start_mode,
245
266
  &session_name,
246
- &spawn_window,
267
+ &actual_spawn_window,
247
268
  spawn_session_id,
248
269
  tmux_start_mode_for_spawn(&spawn, into_existing_session),
249
270
  )?;
@@ -262,40 +283,51 @@ pub(crate) fn start_agent_at_paths(
262
283
  })
263
284
  }
264
285
 
265
- /// **0.3.24 strict-non-capture reachability gate**. Returns `true` IFF the spawn's
266
- /// pane is currently addressable on the transport's tmux socket. Uses structural
267
- /// addressing only (`has_pane` → `liveness` → `list_targets`) — **never**
268
- /// `capture` — so it does not race with E31's pane-readback gate timing
269
- /// (E31 reads pane contents after submit; running our own capture here would
270
- /// either consume the first-paste tokens E31 looks for, or vice versa).
271
- ///
272
- /// The gate fires after `mark_agent_started` + `save_restart_projected_state`,
273
- /// so an unreachable pane returns a structured `LifecycleError` that the CLI
274
- /// surface translates to `ok=false`, replacing the macmini假绿
275
- /// (`ok=true` with orphaned-on-wrong-socket spawn).
276
- fn spawned_pane_is_reachable(
286
+ fn verify_spawned_pane_matches_target(
277
287
  transport: &dyn crate::transport::Transport,
278
288
  pane: &crate::transport::PaneId,
279
- ) -> bool {
280
- // (a) authoritative: has_pane Some(true) → addressable.
281
- if matches!(transport.has_pane(pane), Ok(Some(true))) {
282
- return true;
289
+ session: &crate::transport::SessionName,
290
+ window: &crate::transport::WindowName,
291
+ ) -> Result<(), LifecycleError> {
292
+ let targets = transport
293
+ .list_targets()
294
+ .map_err(|e| LifecycleError::Transport(e.to_string()))?;
295
+ let observed = targets.iter().find(|target| target.pane_id == *pane);
296
+ let Some(observed) = observed else {
297
+ return Err(LifecycleError::RequirementUnmet(format!(
298
+ "start refused: spawned pane not addressable on transport socket (possible socket drift); window disappeared or spawned pane not owned by requested agent window; requested={}:{} pane={} observed=<missing>",
299
+ session.as_str(),
300
+ window.as_str(),
301
+ pane.as_str()
302
+ )));
303
+ };
304
+ let observed_window = observed
305
+ .window_name
306
+ .as_ref()
307
+ .map(crate::transport::WindowName::as_str)
308
+ .unwrap_or("<unknown>");
309
+ if observed.session.as_str() != session.as_str() || observed_window != window.as_str() {
310
+ return Err(LifecycleError::RequirementUnmet(format!(
311
+ "start refused: spawned pane not owned by requested agent window; requested={}:{} pane={} observed={}:{}",
312
+ session.as_str(),
313
+ window.as_str(),
314
+ pane.as_str(),
315
+ observed.session.as_str(),
316
+ observed_window
317
+ )));
283
318
  }
284
- // (b) liveness probe: not-Dead → addressable. Dead → unreachable.
285
319
  if matches!(
286
320
  transport.liveness(pane),
287
- Ok(crate::model::enums::PaneLiveness::Live)
321
+ Ok(crate::model::enums::PaneLiveness::Dead)
288
322
  ) {
289
- return true;
290
- }
291
- // (c) last resort: enumeration. If the pane appears in `list_targets()`
292
- // (cross-session enumeration on the socket), it is addressable.
293
- if let Ok(targets) = transport.list_targets() {
294
- if targets.iter().any(|t| t.pane_id.as_str() == pane.as_str()) {
295
- return true;
296
- }
323
+ return Err(LifecycleError::RequirementUnmet(format!(
324
+ "start refused: spawned pane not addressable on transport socket; pane is dead after spawn; requested={}:{} pane={}",
325
+ session.as_str(),
326
+ window.as_str(),
327
+ pane.as_str()
328
+ )));
297
329
  }
298
- false
330
+ Ok(())
299
331
  }
300
332
 
301
333
  /// E51 (0.3.26 P0, restart self-heal): returns `true` when the agent's pane_id
@@ -318,9 +350,11 @@ fn pane_conflicts_with_leader_or_other(
318
350
  let agent_socket = tmux_socket_field(agent).or(state_socket);
319
351
  // Check leader anchor.
320
352
  for key in ["leader_receiver", "team_owner"] {
321
- if state.get(key).and_then(pane_socket_binding).is_some_and(|leader| {
322
- pane_conflicts_on_same_socket(pane_id, agent_socket, leader)
323
- }) {
353
+ if state
354
+ .get(key)
355
+ .and_then(pane_socket_binding)
356
+ .is_some_and(|leader| pane_conflicts_on_same_socket(pane_id, agent_socket, leader))
357
+ {
324
358
  return true;
325
359
  }
326
360
  }
@@ -392,10 +426,7 @@ fn pane_socket_binding(value: &serde_json::Value) -> Option<PaneSocketBinding<'_
392
426
  })
393
427
  }
394
428
 
395
- fn agent_pane_live(
396
- transport: &dyn crate::transport::Transport,
397
- agent: &serde_json::Value,
398
- ) -> bool {
429
+ fn agent_pane_live(transport: &dyn crate::transport::Transport, agent: &serde_json::Value) -> bool {
399
430
  let Some(pane) = agent
400
431
  .get("pane_id")
401
432
  .and_then(serde_json::Value::as_str)
@@ -458,8 +489,7 @@ fn list_same_role_panes(
458
489
  /// caller falls back to safer behavior (refuse stop, surface
459
490
  /// RequirementUnmet/transport error) when this returns false.
460
491
  fn is_per_agent_window(window: &str, agent_id: &AgentId) -> bool {
461
- window == agent_id.as_str()
462
- && !crate::lifecycle::launch::is_adaptive_layout_window_pub(window)
492
+ window == agent_id.as_str() && !crate::lifecycle::launch::is_adaptive_layout_window_pub(window)
463
493
  }
464
494
 
465
495
  fn tmux_start_mode_for_spawn(
@@ -584,8 +614,13 @@ pub fn stop_agent(
584
614
  team: Option<&str>,
585
615
  ) -> Result<StopAgentReport, LifecycleError> {
586
616
  let paths = lifecycle_paths(workspace, team)?;
587
- let transport =
588
- lifecycle_worker_tmux_backend_for_selected_state(&paths.run_workspace, team)?;
617
+ let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
618
+ workspace: &paths.run_workspace,
619
+ operation: "stop-agent",
620
+ team,
621
+ agent_id: Some(agent_id),
622
+ })?;
623
+ let transport = lifecycle_worker_tmux_backend_for_selected_state(&paths.run_workspace, team)?;
589
624
  stop_agent_at_paths(
590
625
  &paths.run_workspace,
591
626
  &paths.spec_workspace,
@@ -602,6 +637,12 @@ pub fn stop_agent_with_transport(
602
637
  transport: &dyn crate::transport::Transport,
603
638
  ) -> Result<StopAgentReport, LifecycleError> {
604
639
  let paths = lifecycle_paths(workspace, team)?;
640
+ let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
641
+ workspace: &paths.run_workspace,
642
+ operation: "stop-agent",
643
+ team,
644
+ agent_id: Some(agent_id),
645
+ })?;
605
646
  stop_agent_at_paths(
606
647
  &paths.run_workspace,
607
648
  &paths.spec_workspace,
@@ -622,8 +663,7 @@ pub(super) fn stop_agent_at_paths(
622
663
  let mut state = resolve_team_scoped_state_or_refuse(workspace, team)?;
623
664
  crate::lifecycle::launch::ensure_owner_allowed_for_state(&state, Some(agent_id))?;
624
665
  let spec = load_team_spec(spec_workspace)?;
625
- let agent = find_spec_agent(&spec, agent_id)
626
- .ok_or_else(|| unknown_worker(agent_id))?;
666
+ let agent = find_spec_agent(&spec, agent_id).ok_or_else(|| unknown_worker(agent_id))?;
627
667
  let session_name = state_session_name_from_spec(&state, &spec);
628
668
  let window = state
629
669
  .get("agents")
@@ -702,11 +742,15 @@ pub(super) fn stop_agent_at_paths(
702
742
  };
703
743
  if let Err(e) = kill_result {
704
744
  let stderr = match &e {
705
- crate::transport::TransportError::Subprocess { stderr, .. } => stderr.trim().to_string(),
745
+ crate::transport::TransportError::Subprocess { stderr, .. } => {
746
+ stderr.trim().to_string()
747
+ }
706
748
  other => other.to_string(),
707
749
  };
708
750
  let _ = write_stop_window_failed_event(workspace, agent_id, &target_str, &stderr);
709
- return Err(LifecycleError::Transport(format!("failed to stop agent {agent_id}: {stderr}")));
751
+ return Err(LifecycleError::Transport(format!(
752
+ "failed to stop agent {agent_id}: {stderr}"
753
+ )));
710
754
  }
711
755
  // 0.4.6 Stage 1: drain-and-prove. The pre-fix path trusted `kill_pane`'s
712
756
  // success return without waiting for the pane / pid to actually exit.
@@ -729,8 +773,12 @@ pub(super) fn stop_agent_at_paths(
729
773
  mark_agent_stopped(&mut state, agent_id, agent, &window)?;
730
774
  // golden operations.py:95: save_team_scoped_state (team projection) — NOT a raw save, so a
731
775
  // multi-team workspace keeps the other teams' persisted runtime state instead of being clobbered.
732
- crate::state::projection::save_team_scoped_state(workspace, &state)
733
- .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
776
+ crate::state::projection::save_team_scoped_state_with_lifecycle_topology_authority(
777
+ workspace,
778
+ &state,
779
+ &[agent_id.as_str()],
780
+ )
781
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
734
782
  // golden operations.py:96-99: snapshot (side-effect), then state_file = write_team_state path.
735
783
  // snapshot.py:19-21 returns None silently when session_name is falsy — mirror that no-op here so
736
784
  // a workspace whose persisted state lacks session_name proceeds to write team_state and return ok
@@ -772,7 +820,9 @@ pub(super) fn resolve_team_scoped_state_or_refuse(
772
820
  .unwrap_or("");
773
821
  return Err(LifecycleError::TeamSelect(format!("{reason}: {detail}")));
774
822
  }
775
- state.ok_or_else(|| LifecycleError::StatePersist("resolve_team_scoped_state returned no state".to_string()))
823
+ state.ok_or_else(|| {
824
+ LifecycleError::StatePersist("resolve_team_scoped_state returned no state".to_string())
825
+ })
776
826
  }
777
827
 
778
828
  fn mark_agent_started(
@@ -807,7 +857,10 @@ fn mark_agent_started(
807
857
  // codex, claude, copilot. Reset_agent --discard-session already does
808
858
  // this at common.rs:1144-1188; here we mirror it for start-agent /
809
859
  // restart-agent fresh paths so the fresh-tuple invariant is global.
810
- if matches!(start_mode, StartMode::Fresh | StartMode::FreshAfterMissingRollout) {
860
+ if matches!(
861
+ start_mode,
862
+ StartMode::Fresh | StartMode::FreshAfterMissingRollout
863
+ ) {
811
864
  for field in [
812
865
  "session_id",
813
866
  "rollout_path",
@@ -864,17 +917,10 @@ fn mark_agent_started(
864
917
  // so future restart/start cycles read it directly from the agent row.
865
918
  if let Some(ref team_id) = spawn.owner_team_id {
866
919
  if !team_id.is_empty() {
867
- agent.insert(
868
- "owner_team_id".to_string(),
869
- serde_json::json!(team_id),
870
- );
920
+ agent.insert("owner_team_id".to_string(), serde_json::json!(team_id));
871
921
  }
872
922
  }
873
- crate::lifecycle::launch::persist_command_plan_state(
874
- agent,
875
- &spawn.plan,
876
- &spawn.profile_launch,
877
- );
923
+ crate::lifecycle::launch::persist_command_plan_state(agent, &spawn.plan, &spawn.profile_launch);
878
924
  crate::lifecycle::launch::persist_effective_approval_policy(agent, safety);
879
925
  if let Some(placement) = spawn.layout_placement.as_ref() {
880
926
  agent.insert(
@@ -885,7 +931,10 @@ fn mark_agent_started(
885
931
  "layout_index".to_string(),
886
932
  serde_json::json!(placement.layout_index),
887
933
  );
888
- agent.insert("pane_index".to_string(), serde_json::json!(placement.pane_index));
934
+ agent.insert(
935
+ "pane_index".to_string(),
936
+ serde_json::json!(placement.pane_index),
937
+ );
889
938
  agent.insert(
890
939
  "display".to_string(),
891
940
  serde_json::json!({
@@ -926,8 +975,13 @@ pub fn reset_agent(
926
975
  });
927
976
  }
928
977
  let paths = lifecycle_paths(workspace, team)?;
929
- let transport =
930
- lifecycle_worker_tmux_backend_for_selected_state(&paths.run_workspace, team)?;
978
+ let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
979
+ workspace: &paths.run_workspace,
980
+ operation: "reset-agent",
981
+ team,
982
+ agent_id: Some(agent_id),
983
+ })?;
984
+ let transport = lifecycle_worker_tmux_backend_for_selected_state(&paths.run_workspace, team)?;
931
985
  reset_agent_at_paths(
932
986
  &paths.run_workspace,
933
987
  &paths.spec_workspace,
@@ -953,6 +1007,12 @@ pub fn reset_agent_with_transport(
953
1007
  });
954
1008
  }
955
1009
  let paths = lifecycle_paths(workspace, team)?;
1010
+ let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
1011
+ workspace: &paths.run_workspace,
1012
+ operation: "reset-agent",
1013
+ team,
1014
+ agent_id: Some(agent_id),
1015
+ })?;
956
1016
  reset_agent_at_paths(
957
1017
  &paths.run_workspace,
958
1018
  &paths.spec_workspace,
@@ -1160,12 +1220,12 @@ fn reset_agent_at_paths(
1160
1220
  sync_restart_team_projections(&mut state, &team_key);
1161
1221
  // golden operations.py (reset): save_team_scoped_state on the team projection — same multi-team
1162
1222
  // preservation as stop, not a raw save_runtime_state.
1163
- crate::state::projection::save_team_scoped_state_with_tombstoned_agents(
1223
+ crate::state::projection::save_team_scoped_state_with_tombstone_lifecycle_topology_authority(
1164
1224
  workspace,
1165
1225
  &state,
1166
1226
  &[agent_id.as_str()],
1167
1227
  )
1168
- .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
1228
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
1169
1229
  // golden operations.py:125: write_team_state after the discard-save (the intermediate stopped snapshot).
1170
1230
  write_team_state(spec_workspace, &spec, &state)?;
1171
1231
  write_reset_tombstone_event(
@@ -1251,12 +1311,11 @@ fn write_start_agent_start_event(
1251
1311
  // Contract C / F6.4: event log must record the same context-aware argv that the
1252
1312
  // actual spawn used — so the role/tools/MCP context appears in `start_agent.agent_start`.
1253
1313
  let safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn()?;
1254
- let command_agent =
1255
- crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
1256
- agent,
1257
- Some(agent_id.as_str()),
1258
- provider,
1259
- );
1314
+ let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
1315
+ agent,
1316
+ Some(agent_id.as_str()),
1317
+ provider,
1318
+ );
1260
1319
  let system_prompt =
1261
1320
  crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
1262
1321
  let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
@@ -1268,17 +1327,18 @@ fn write_start_agent_start_event(
1268
1327
  let mcp_config = adapter
1269
1328
  .mcp_config(auth_mode)
1270
1329
  .map_err(|e| LifecycleError::Provider(e.to_string()))?;
1271
- let team_id = agent
1272
- .get("owner_team_id")
1273
- .and_then(|v| v.as_str());
1330
+ let team_id = agent.get("owner_team_id").and_then(|v| v.as_str());
1274
1331
  let mcp_config = crate::lifecycle::launch::resolve_mcp_config(
1275
1332
  mcp_config,
1276
1333
  workspace,
1277
1334
  agent_id.as_str(),
1278
1335
  team_id.unwrap_or(""),
1279
1336
  );
1280
- let mcp_config_path =
1281
- crate::lifecycle::launch::write_worker_mcp_config(workspace, agent_id.as_str(), &mcp_config)?;
1337
+ let mcp_config_path = crate::lifecycle::launch::write_worker_mcp_config(
1338
+ workspace,
1339
+ agent_id.as_str(),
1340
+ &mcp_config,
1341
+ )?;
1282
1342
  let profile_launch =
1283
1343
  crate::lifecycle::profile_launch::prepare_provider_profile_launch_from_json(
1284
1344
  workspace,
@@ -1286,16 +1346,15 @@ fn write_start_agent_start_event(
1286
1346
  agent,
1287
1347
  Some(&mcp_config),
1288
1348
  )?;
1289
- let command_model = profile_launch
1290
- .command_overrides
1291
- .model
1292
- .as_deref()
1293
- .or(model);
1349
+ let command_model = profile_launch.command_overrides.model.as_deref().or(model);
1294
1350
  // 0.4.x provider effort MVP: start_agent path preserves effort from the
1295
1351
  // persisted agent JSON.
1296
- let start_agent_effort = crate::lifecycle::launch::provider_effort_for_spawn_json(&agent, provider);
1352
+ let start_agent_effort =
1353
+ crate::lifecycle::launch::provider_effort_for_spawn_json(&agent, provider);
1297
1354
  if let Some(event_value) = crate::lifecycle::launch::provider_effort_event_if_dropped_json(
1298
- &agent, provider, agent_id.as_str(),
1355
+ &agent,
1356
+ provider,
1357
+ agent_id.as_str(),
1299
1358
  ) {
1300
1359
  let _ = crate::event_log::EventLog::new(workspace)
1301
1360
  .write("provider.effort_unsupported", event_value);
@@ -1387,10 +1446,7 @@ pub(super) fn write_stop_drain_event(
1387
1446
  }
1388
1447
  }
1389
1448
  crate::event_log::EventLog::new(workspace)
1390
- .write(
1391
- "stop_agent.drain",
1392
- serde_json::Value::Object(payload),
1393
- )
1449
+ .write("stop_agent.drain", serde_json::Value::Object(payload))
1394
1450
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
1395
1451
  Ok(())
1396
1452
  }