@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
@@ -0,0 +1,321 @@
1
+ use std::path::{Path, PathBuf};
2
+ use std::time::Duration;
3
+
4
+ use super::*;
5
+ use crate::lifecycle::lock::{
6
+ acquire_agent_lifecycle_lock, acquire_agent_lifecycle_lock_for_test, LifecycleLockRequest,
7
+ AGENT_LIFECYCLE_LOCK_NAME, LIFECYCLE_LOCK_HELD_LONG, LIFECYCLE_LOCK_TIMEOUT,
8
+ };
9
+
10
+ #[test]
11
+ fn lifecycle_lock_timeout_error_contract_and_constants() {
12
+ assert_eq!(AGENT_LIFECYCLE_LOCK_NAME, "agent-lifecycle");
13
+ assert_eq!(LIFECYCLE_LOCK_TIMEOUT, Duration::from_secs(30));
14
+ assert_eq!(LIFECYCLE_LOCK_HELD_LONG, Duration::from_secs(5));
15
+
16
+ let ws = temp_ws();
17
+ let err = LifecycleError::LifecycleLockTimeout {
18
+ lock_path: crate::model::paths::runtime_dir(&ws).join("agent-lifecycle.lock"),
19
+ log_path: crate::model::paths::logs_dir(&ws).join("events.jsonl"),
20
+ operation: "reset-agent".to_string(),
21
+ waited_ms: 30_000,
22
+ };
23
+ assert_eq!(
24
+ err.to_string(),
25
+ format!(
26
+ "error: lifecycle lock timeout after 30s\naction: retry or check for hung reset/start\nlog: {}",
27
+ crate::model::paths::logs_dir(&ws).join("events.jsonl").display()
28
+ )
29
+ );
30
+ }
31
+
32
+ #[test]
33
+ fn lifecycle_lock_lifecycle_rollback_releases_after_timeout() {
34
+ let ws = temp_ws();
35
+ let agent = aid("alpha");
36
+ let held = acquire_agent_lifecycle_lock(LifecycleLockRequest {
37
+ workspace: &ws,
38
+ operation: "reset-agent",
39
+ team: Some("alpha"),
40
+ agent_id: Some(&agent),
41
+ })
42
+ .expect("hold lifecycle lock");
43
+
44
+ let waiter_ws = ws.clone();
45
+ let waiter_agent = aid("beta");
46
+ let result = std::thread::spawn(move || {
47
+ acquire_agent_lifecycle_lock_for_test(
48
+ LifecycleLockRequest {
49
+ workspace: &waiter_ws,
50
+ operation: "start-agent",
51
+ team: Some("alpha"),
52
+ agent_id: Some(&waiter_agent),
53
+ },
54
+ Duration::from_millis(200),
55
+ Duration::from_secs(5),
56
+ )
57
+ })
58
+ .join()
59
+ .expect("waiter thread");
60
+
61
+ assert!(
62
+ matches!(result, Err(LifecycleError::LifecycleLockTimeout { operation, .. }) if operation == "start-agent"),
63
+ "second lifecycle mutator must time out while lock is held"
64
+ );
65
+
66
+ drop(held);
67
+ acquire_agent_lifecycle_lock_for_test(
68
+ LifecycleLockRequest {
69
+ workspace: &ws,
70
+ operation: "stop-agent",
71
+ team: Some("alpha"),
72
+ agent_id: Some(&agent),
73
+ },
74
+ Duration::from_millis(200),
75
+ Duration::from_secs(5),
76
+ )
77
+ .expect("lock released after guard drop");
78
+ }
79
+
80
+ #[test]
81
+ fn lifecycle_lock_long_held_event_records_holder_and_waiter() {
82
+ let ws = temp_ws();
83
+ let holder_agent = aid("holder");
84
+ let waiter_agent = aid("waiter");
85
+ let _held = acquire_agent_lifecycle_lock(LifecycleLockRequest {
86
+ workspace: &ws,
87
+ operation: "restart",
88
+ team: Some("alpha"),
89
+ agent_id: Some(&holder_agent),
90
+ })
91
+ .expect("hold lifecycle lock");
92
+
93
+ let waiter_ws = ws.clone();
94
+ let result = std::thread::spawn(move || {
95
+ acquire_agent_lifecycle_lock_for_test(
96
+ LifecycleLockRequest {
97
+ workspace: &waiter_ws,
98
+ operation: "reset-agent",
99
+ team: Some("alpha"),
100
+ agent_id: Some(&waiter_agent),
101
+ },
102
+ Duration::from_millis(220),
103
+ Duration::from_millis(50),
104
+ )
105
+ })
106
+ .join()
107
+ .expect("waiter thread");
108
+ assert!(matches!(
109
+ result,
110
+ Err(LifecycleError::LifecycleLockTimeout { .. })
111
+ ));
112
+
113
+ let events = read_events(&ws);
114
+ let event = events
115
+ .iter()
116
+ .find(|event| {
117
+ event.get("event").and_then(serde_json::Value::as_str)
118
+ == Some("lifecycle.lock_held_long")
119
+ })
120
+ .unwrap_or_else(|| panic!("missing lifecycle.lock_held_long event: {events:?}"));
121
+ assert_eq!(event["lock_name"], json!("agent-lifecycle"));
122
+ assert_eq!(event["operation"], json!("reset-agent"));
123
+ assert_eq!(event["holder"]["operation"], json!("restart"));
124
+ assert_eq!(event["holder"]["agent_id"], json!("holder"));
125
+ assert_eq!(event["threshold_ms"], json!(50));
126
+ assert_eq!(event["timeout_ms"], json!(220));
127
+ assert!(
128
+ event["blocked_queue_len"].as_u64().unwrap_or(0) >= 1,
129
+ "waiter sidecar should be visible while long-held event is emitted"
130
+ );
131
+ }
132
+
133
+ #[test]
134
+ fn lifecycle_lock_phase_b_golden_source_guard() {
135
+ r2_lifecycle_lock_exists_precondition();
136
+ }
137
+
138
+ #[test]
139
+ fn r2_lifecycle_lock_exists_precondition() {
140
+ let src = manifest_src();
141
+ let lock = read_src("lifecycle/lock.rs");
142
+ assert!(
143
+ lock.contains("AGENT_LIFECYCLE_LOCK_NAME: &str = \"agent-lifecycle\""),
144
+ "lifecycle lock name constant must remain explicit"
145
+ );
146
+ assert!(
147
+ lock.contains("LIFECYCLE_LOCK_TIMEOUT: Duration = Duration::from_secs(30)"),
148
+ "lifecycle lock timeout constant must remain 30s"
149
+ );
150
+ assert!(
151
+ lock.contains("write_lock_held_long_event") && lock.contains("lifecycle.lock_held_long"),
152
+ "long-held lifecycle lock event must remain wired"
153
+ );
154
+
155
+ let state_hits = source_files(&src.join("state"))
156
+ .into_iter()
157
+ .filter(|path| {
158
+ let text = std::fs::read_to_string(path).unwrap();
159
+ text.contains("RuntimeLock::acquire") && text.contains("\"state-save\"")
160
+ })
161
+ .collect::<Vec<_>>();
162
+ assert_eq!(
163
+ state_hits,
164
+ vec![src.join("state").join("persist.rs")],
165
+ "state-save lock must stay owned by state/persist.rs"
166
+ );
167
+
168
+ for path in source_files(&src.join("state")) {
169
+ let text = std::fs::read_to_string(&path).unwrap();
170
+ assert!(
171
+ !text.contains("LifecycleLock")
172
+ && !text.contains("acquire_agent_lifecycle_lock")
173
+ && !text.contains("agent-lifecycle"),
174
+ "state module must not acquire or know lifecycle lock: {}",
175
+ path.display()
176
+ );
177
+ }
178
+
179
+ for path in source_files(&src.join("coordinator")) {
180
+ let text = std::fs::read_to_string(&path).unwrap();
181
+ assert!(
182
+ !text.contains("acquire_agent_lifecycle_lock") && !text.contains("agent-lifecycle"),
183
+ "coordinator tick path must not block on lifecycle lock: {}",
184
+ path.display()
185
+ );
186
+ }
187
+
188
+ for path in source_files(&src) {
189
+ if path == src.join("state").join("persist.rs") || is_test_source(&path) {
190
+ continue;
191
+ }
192
+ let text = std::fs::read_to_string(&path).unwrap();
193
+ assert!(
194
+ !text.contains("RuntimeLock::acquire"),
195
+ "production source outside state/persist.rs must not acquire runtime locks directly: {}",
196
+ path.display()
197
+ );
198
+ }
199
+
200
+ for path in source_files(&src) {
201
+ if path.starts_with(src.join("state")) || path.starts_with(src.join("lifecycle")) {
202
+ continue;
203
+ }
204
+ let text = std::fs::read_to_string(&path).unwrap();
205
+ let lifecycle_topology_authority_entries = [
206
+ "save_runtime_state_with_lifecycle_topology_authority",
207
+ "save_team_scoped_state_with_lifecycle_topology_authority",
208
+ "save_runtime_state_with_team_tombstone_lifecycle_topology_authority",
209
+ "save_team_scoped_state_with_tombstone_lifecycle_topology_authority",
210
+ ];
211
+ assert!(
212
+ !lifecycle_topology_authority_entries
213
+ .iter()
214
+ .any(|entry| text.contains(entry)),
215
+ "lifecycle topology authority save entry must not be referenced outside lifecycle/state modules: {}",
216
+ path.display()
217
+ );
218
+ }
219
+
220
+ let agent = read_src("lifecycle/restart/agent.rs");
221
+ assert_public_operation(&agent, "start-agent");
222
+ assert_public_operation(&agent, "stop-agent");
223
+ assert_public_operation(&agent, "reset-agent");
224
+ assert_body_is_unlocked(&agent, "pub(crate) fn start_agent_at_paths");
225
+ assert_body_is_unlocked(&agent, "pub(super) fn stop_agent_at_paths");
226
+ assert_body_is_unlocked(&agent, "fn reset_agent_at_paths");
227
+
228
+ let remove = read_src("lifecycle/restart/remove.rs");
229
+ assert_public_operation(&remove, "remove-agent");
230
+ assert_body_is_unlocked(&remove, "fn remove_agent_at_paths");
231
+ assert!(
232
+ !remove.contains("start_agent_with_transport"),
233
+ "remove rollback runs while remove-agent holds the lifecycle lock; it must use lock-free start_agent_at_paths"
234
+ );
235
+
236
+ let launch = read_src("lifecycle/launch.rs");
237
+ assert_public_operation(&launch, "add-agent");
238
+ assert_public_operation(&launch, "fork-agent");
239
+ assert_body_is_unlocked(&launch, "fn add_agent_with_transport_at_paths");
240
+
241
+ let rebuild = read_src("lifecycle/restart/rebuild.rs");
242
+ assert_public_operation(&rebuild, "restart");
243
+ let drop_idx = rebuild.find("drop(lifecycle_lock);").unwrap();
244
+ let coordinator_idx = rebuild
245
+ .find("start_coordinator_for_workspace(&selected.run_workspace)")
246
+ .unwrap();
247
+ let readiness_idx = rebuild.find("wait_restart_readiness_or_timeout(").unwrap();
248
+ assert!(
249
+ rebuild.contains("let lifecycle_lock = acquire_agent_lifecycle_lock")
250
+ && drop_idx < coordinator_idx
251
+ && coordinator_idx < readiness_idx,
252
+ "restart must release lifecycle lock before coordinator readiness wait"
253
+ );
254
+ }
255
+
256
+ fn is_test_source(path: &Path) -> bool {
257
+ path.components()
258
+ .any(|component| component.as_os_str() == "tests")
259
+ || path
260
+ .file_name()
261
+ .and_then(|name| name.to_str())
262
+ .is_some_and(|name| name == "tests.rs" || name.ends_with("_tests.rs"))
263
+ }
264
+
265
+ fn read_events(workspace: &Path) -> Vec<serde_json::Value> {
266
+ let path = crate::model::paths::logs_dir(workspace).join("events.jsonl");
267
+ std::fs::read_to_string(path)
268
+ .unwrap_or_default()
269
+ .lines()
270
+ .filter_map(|line| serde_json::from_str(line).ok())
271
+ .collect()
272
+ }
273
+
274
+ fn manifest_src() -> PathBuf {
275
+ PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src")
276
+ }
277
+
278
+ fn read_src(path: &str) -> String {
279
+ std::fs::read_to_string(manifest_src().join(path)).unwrap()
280
+ }
281
+
282
+ fn source_files(root: &Path) -> Vec<PathBuf> {
283
+ let mut out = Vec::new();
284
+ collect_rs_files(root, &mut out);
285
+ out.sort();
286
+ out
287
+ }
288
+
289
+ fn collect_rs_files(root: &Path, out: &mut Vec<PathBuf>) {
290
+ for entry in std::fs::read_dir(root).unwrap() {
291
+ let path = entry.unwrap().path();
292
+ if path.is_dir() {
293
+ collect_rs_files(&path, out);
294
+ } else if path.extension().and_then(|ext| ext.to_str()) == Some("rs") {
295
+ out.push(path);
296
+ }
297
+ }
298
+ }
299
+
300
+ fn assert_public_operation(source: &str, operation: &str) {
301
+ assert!(
302
+ source.contains("acquire_agent_lifecycle_lock")
303
+ && source.contains(&format!("operation: \"{operation}\"")),
304
+ "public lifecycle entry must acquire lifecycle lock for {operation}"
305
+ );
306
+ }
307
+
308
+ fn assert_body_is_unlocked(source: &str, signature: &str) {
309
+ let body = source
310
+ .split_once(signature)
311
+ .unwrap_or_else(|| panic!("missing signature: {signature}"))
312
+ .1;
313
+ let body = body
314
+ .split_once("\n}\n")
315
+ .map(|(body, _)| body)
316
+ .unwrap_or(body);
317
+ assert!(
318
+ !body.contains("acquire_agent_lifecycle_lock"),
319
+ "{signature} must remain lock-free to avoid nested reset deadlocks"
320
+ );
321
+ }
@@ -33,6 +33,7 @@ type RecordedSpawns = LaneSpawns;
33
33
  /// every spawn (kind + argv) like `RecordingLaunchTransport`.
34
34
  struct SessionProbeRecordingTransport {
35
35
  spawns: RecordedSpawns,
36
+ spawn_targets: std::sync::Mutex<Vec<(String, String, String)>>,
36
37
  session_exists: bool,
37
38
  // 0.4.10+ reset hard-gate: record killed panes so liveness/has_pane
38
39
  // reflect the kill (the gate checks "did stop actually remove the
@@ -47,11 +48,22 @@ impl crate::transport::Transport for SessionProbeRecordingTransport {
47
48
  }
48
49
  fn spawn_first(&self, session: &crate::transport::SessionName, window: &crate::transport::WindowName, argv: &[String], _cwd: &std::path::Path, _env: &std::collections::BTreeMap<String, String>) -> Result<crate::transport::SpawnResult, crate::transport::TransportError> {
49
50
  self.spawns.lock().unwrap().push(("spawn_first".to_string(), argv.to_vec()));
51
+ self.spawn_targets.lock().unwrap().push((
52
+ session.as_str().to_string(),
53
+ window.as_str().to_string(),
54
+ "%0".to_string(),
55
+ ));
50
56
  Ok(crate::transport::SpawnResult { pane_id: crate::transport::PaneId::new("%0"), session: session.clone(), window: window.clone(), child_pid: Some(6856) })
51
57
  }
52
58
  fn spawn_into(&self, session: &crate::transport::SessionName, window: &crate::transport::WindowName, argv: &[String], _cwd: &std::path::Path, _env: &std::collections::BTreeMap<String, String>) -> Result<crate::transport::SpawnResult, crate::transport::TransportError> {
53
59
  self.spawns.lock().unwrap().push(("spawn_into".to_string(), argv.to_vec()));
54
- Ok(crate::transport::SpawnResult { pane_id: crate::transport::PaneId::new(format!("%{}", window.as_str())), session: session.clone(), window: window.clone(), child_pid: Some(6856) })
60
+ let pane = format!("%{}", window.as_str());
61
+ self.spawn_targets.lock().unwrap().push((
62
+ session.as_str().to_string(),
63
+ window.as_str().to_string(),
64
+ pane.clone(),
65
+ ));
66
+ Ok(crate::transport::SpawnResult { pane_id: crate::transport::PaneId::new(pane), session: session.clone(), window: window.clone(), child_pid: Some(6856) })
55
67
  }
56
68
  fn inject(&self, _t: &crate::transport::Target, _p: &crate::transport::InjectPayload, _s: crate::transport::Key, _b: bool) -> Result<crate::transport::InjectReport, crate::transport::TransportError> {
57
69
  unimplemented!("not reached by start_agent respawn")
@@ -84,7 +96,25 @@ impl crate::transport::Transport for SessionProbeRecordingTransport {
84
96
  Ok(())
85
97
  }
86
98
  fn list_targets(&self) -> Result<Vec<crate::transport::PaneInfo>, crate::transport::TransportError> {
87
- Ok(Vec::new())
99
+ Ok(self
100
+ .spawn_targets
101
+ .lock()
102
+ .unwrap()
103
+ .iter()
104
+ .map(|(session, window, pane)| crate::transport::PaneInfo {
105
+ pane_id: crate::transport::PaneId::new(pane.as_str()),
106
+ session: crate::transport::SessionName::new(session.as_str()),
107
+ window_index: None,
108
+ window_name: Some(crate::transport::WindowName::new(window.as_str())),
109
+ pane_index: None,
110
+ tty: None,
111
+ current_command: Some("node".to_string()),
112
+ current_path: None,
113
+ active: false,
114
+ pane_pid: Some(6856),
115
+ leader_env: std::collections::BTreeMap::new(),
116
+ })
117
+ .collect())
88
118
  }
89
119
  fn has_session(&self, _s: &crate::transport::SessionName) -> Result<bool, crate::transport::TransportError> {
90
120
  Ok(self.session_exists)
@@ -133,7 +163,7 @@ fn start_agent_respawn_into_dead_session_uses_new_session_not_new_window() {
133
163
  let ws = respawn_ws_one_resumable_worker();
134
164
  let spawns = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
135
165
  // session_exists=false models the dead CP-1 `-L` server after --discard-session.
136
- let transport = SessionProbeRecordingTransport { spawns: std::sync::Arc::clone(&spawns), session_exists: false, killed_panes: std::sync::Mutex::new(std::collections::BTreeSet::new()) };
166
+ let transport = SessionProbeRecordingTransport { spawns: std::sync::Arc::clone(&spawns), spawn_targets: std::sync::Mutex::new(Vec::new()), session_exists: false, killed_panes: std::sync::Mutex::new(std::collections::BTreeSet::new()) };
137
167
  let _ = start_agent_with_transport(&ws, &AgentId::new("alpha"), false, false, false, None, &transport);
138
168
  let recorded = spawns.lock().unwrap().clone();
139
169
  assert_eq!(recorded.len(), 1, "exactly one respawn for alpha; got {recorded:?}");
@@ -165,7 +195,7 @@ fn reset_agent_discard_session_rebuilds_window_via_start_respawn() {
165
195
  let ws = restart_ws_two_resumable_workers(); // compiled spec + state(alpha,bravo running) + seeded coordinator
166
196
  let spawns = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
167
197
  // live session present (other workers keep it alive) but alpha's window was discarded.
168
- let transport = SessionProbeRecordingTransport { spawns: std::sync::Arc::clone(&spawns), session_exists: true, killed_panes: std::sync::Mutex::new(std::collections::BTreeSet::new()) };
198
+ let transport = SessionProbeRecordingTransport { spawns: std::sync::Arc::clone(&spawns), spawn_targets: std::sync::Mutex::new(Vec::new()), session_exists: true, killed_panes: std::sync::Mutex::new(std::collections::BTreeSet::new()) };
169
199
  let _ = reset_agent_with_transport(&ws, &AgentId::new("alpha"), true, false, None, &transport);
170
200
  let recorded = spawns.lock().unwrap().clone();
171
201
  assert!(
@@ -203,7 +233,7 @@ fn reset_agent_discard_session_syncs_projection_epoch_inputs_for_restart_agent_c
203
233
 
204
234
  let before = chrono::Utc::now();
205
235
  let spawns = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
206
- let transport = SessionProbeRecordingTransport { spawns: std::sync::Arc::clone(&spawns), session_exists: true, killed_panes: std::sync::Mutex::new(std::collections::BTreeSet::new()) };
236
+ let transport = SessionProbeRecordingTransport { spawns: std::sync::Arc::clone(&spawns), spawn_targets: std::sync::Mutex::new(Vec::new()), session_exists: true, killed_panes: std::sync::Mutex::new(std::collections::BTreeSet::new()) };
207
237
  let outcome = reset_agent_with_transport(&ws, &AgentId::new("alpha"), true, false, None, &transport)
208
238
  .expect("reset-agent alpha --discard-session --no-display");
209
239
  assert!(