@team-agent/installer 0.4.10 → 0.5.0

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 (74) 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 +62 -4
  6. package/crates/team-agent/src/compiler.rs +11 -20
  7. package/crates/team-agent/src/coordinator/orphan.rs +2 -2
  8. package/crates/team-agent/src/coordinator/runtime_detectors.rs +5 -4
  9. package/crates/team-agent/src/coordinator/steps/abnormal.rs +1776 -2
  10. package/crates/team-agent/src/coordinator/steps/mod.rs +19 -0
  11. package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +78 -7
  12. package/crates/team-agent/src/coordinator/tests/abnormal.rs +195 -0
  13. package/crates/team-agent/src/coordinator/tick.rs +31 -932
  14. package/crates/team-agent/src/diagnose/orphans.rs +66 -8
  15. package/crates/team-agent/src/layout/worker_window_helpers.rs +5 -7
  16. package/crates/team-agent/src/leader/provider_attribution.rs +7 -5
  17. package/crates/team-agent/src/leader/rediscover.rs +1 -11
  18. package/crates/team-agent/src/leader/start.rs +169 -25
  19. package/crates/team-agent/src/lifecycle/launch/plan.rs +19 -8
  20. package/crates/team-agent/src/lifecycle/launch.rs +135 -58
  21. package/crates/team-agent/src/lifecycle/lock.rs +302 -0
  22. package/crates/team-agent/src/lifecycle/mod.rs +1 -0
  23. package/crates/team-agent/src/lifecycle/profile_smoke.rs +1 -11
  24. package/crates/team-agent/src/lifecycle/restart/agent.rs +436 -121
  25. package/crates/team-agent/src/lifecycle/restart/common.rs +108 -17
  26. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -32
  27. package/crates/team-agent/src/lifecycle/restart/remove.rs +16 -2
  28. package/crates/team-agent/src/lifecycle/restart/selection.rs +2 -1
  29. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +193 -0
  30. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +3 -3
  31. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +321 -0
  32. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +54 -15
  33. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +802 -0
  34. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +648 -0
  35. package/crates/team-agent/src/lifecycle/tests.rs +3 -0
  36. package/crates/team-agent/src/lifecycle/types.rs +8 -0
  37. package/crates/team-agent/src/lifecycle/worker_command_context.rs +24 -34
  38. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +24 -9
  39. package/crates/team-agent/src/mcp_server/tools.rs +157 -68
  40. package/crates/team-agent/src/messaging/activity.rs +43 -18
  41. package/crates/team-agent/src/messaging/delivery.rs +161 -97
  42. package/crates/team-agent/src/messaging/helpers.rs +1 -0
  43. package/crates/team-agent/src/messaging/leader_receiver.rs +22 -1
  44. package/crates/team-agent/src/messaging/results.rs +34 -9
  45. package/crates/team-agent/src/messaging/scheduler.rs +52 -9
  46. package/crates/team-agent/src/model/spec.rs +10 -6
  47. package/crates/team-agent/src/provider/adapter.rs +10 -1038
  48. package/crates/team-agent/src/provider/adapters/claude.rs +3 -8
  49. package/crates/team-agent/src/provider/adapters/copilot.rs +2 -5
  50. package/crates/team-agent/src/provider/classify.rs +23 -48
  51. package/crates/team-agent/src/provider/command.rs +16 -7
  52. package/crates/team-agent/src/provider/faults.rs +93 -53
  53. package/crates/team-agent/src/provider/session/capture.rs +4 -15
  54. package/crates/team-agent/src/provider/session_scan/claude.rs +309 -0
  55. package/crates/team-agent/src/provider/session_scan/codex.rs +40 -0
  56. package/crates/team-agent/src/provider/session_scan/common.rs +350 -0
  57. package/crates/team-agent/src/provider/session_scan/copilot.rs +202 -0
  58. package/crates/team-agent/src/provider/session_scan.rs +65 -27
  59. package/crates/team-agent/src/provider/tests/faults.rs +80 -0
  60. package/crates/team-agent/src/provider/types.rs +33 -1
  61. package/crates/team-agent/src/provider/wire.rs +171 -1
  62. package/crates/team-agent/src/state/identity.rs +242 -57
  63. package/crates/team-agent/src/state/identity_keys.rs +9 -12
  64. package/crates/team-agent/src/state/mod.rs +5 -1
  65. package/crates/team-agent/src/state/owner_gate.rs +59 -15
  66. package/crates/team-agent/src/state/ownership.rs +12 -11
  67. package/crates/team-agent/src/state/paths.rs +13 -6
  68. package/crates/team-agent/src/state/persist.rs +671 -128
  69. package/crates/team-agent/src/state/projection.rs +240 -49
  70. package/crates/team-agent/src/state/selector.rs +11 -5
  71. package/crates/team-agent/src/tmux_backend/tests.rs +51 -2
  72. package/crates/team-agent/src/tmux_backend.rs +42 -5
  73. package/crates/team-agent/src/transport/test_support.rs +55 -6
  74. package/package.json +4 -4
@@ -0,0 +1,302 @@
1
+ use std::io::{Seek, Write};
2
+ use std::path::{Path, PathBuf};
3
+ use std::time::{Duration, Instant};
4
+
5
+ use crate::lifecycle::types::LifecycleError;
6
+ use crate::model::ids::AgentId;
7
+
8
+ pub(crate) const AGENT_LIFECYCLE_LOCK_NAME: &str = "agent-lifecycle";
9
+ pub(crate) const LIFECYCLE_LOCK_TIMEOUT: Duration = Duration::from_secs(30);
10
+ pub(crate) const LIFECYCLE_LOCK_HELD_LONG: Duration = Duration::from_secs(5);
11
+
12
+ pub(crate) struct LifecycleLockRequest<'a> {
13
+ pub workspace: &'a Path,
14
+ pub operation: &'static str,
15
+ pub team: Option<&'a str>,
16
+ pub agent_id: Option<&'a AgentId>,
17
+ }
18
+
19
+ pub(crate) struct LifecycleLockGuard {
20
+ #[allow(dead_code)]
21
+ file: std::fs::File,
22
+ }
23
+
24
+ pub(crate) fn acquire_agent_lifecycle_lock(
25
+ request: LifecycleLockRequest<'_>,
26
+ ) -> Result<LifecycleLockGuard, LifecycleError> {
27
+ #[cfg(test)]
28
+ if let Some((timeout, held_long)) = test_lifecycle_lock_deadline_override() {
29
+ return acquire_agent_lifecycle_lock_with_deadlines(request, timeout, held_long);
30
+ }
31
+ acquire_agent_lifecycle_lock_with_deadlines(
32
+ request,
33
+ LIFECYCLE_LOCK_TIMEOUT,
34
+ LIFECYCLE_LOCK_HELD_LONG,
35
+ )
36
+ }
37
+
38
+ #[cfg(test)]
39
+ pub(crate) fn acquire_agent_lifecycle_lock_for_test(
40
+ request: LifecycleLockRequest<'_>,
41
+ timeout: Duration,
42
+ held_long: Duration,
43
+ ) -> Result<LifecycleLockGuard, LifecycleError> {
44
+ acquire_agent_lifecycle_lock_with_deadlines(request, timeout, held_long)
45
+ }
46
+
47
+ #[cfg(test)]
48
+ thread_local! {
49
+ static TEST_DEADLINE_OVERRIDE: std::cell::Cell<Option<(Duration, Duration)>> =
50
+ const { std::cell::Cell::new(None) };
51
+ }
52
+
53
+ #[cfg(test)]
54
+ pub(crate) struct LifecycleLockDeadlineOverrideGuard {
55
+ previous: Option<(Duration, Duration)>,
56
+ }
57
+
58
+ #[cfg(test)]
59
+ pub(crate) fn override_agent_lifecycle_lock_deadlines_for_test(
60
+ timeout: Duration,
61
+ held_long: Duration,
62
+ ) -> LifecycleLockDeadlineOverrideGuard {
63
+ let previous = TEST_DEADLINE_OVERRIDE.with(|override_cell| {
64
+ let previous = override_cell.get();
65
+ override_cell.set(Some((timeout, held_long)));
66
+ previous
67
+ });
68
+ LifecycleLockDeadlineOverrideGuard { previous }
69
+ }
70
+
71
+ #[cfg(test)]
72
+ fn test_lifecycle_lock_deadline_override() -> Option<(Duration, Duration)> {
73
+ TEST_DEADLINE_OVERRIDE.with(std::cell::Cell::get)
74
+ }
75
+
76
+ #[cfg(test)]
77
+ impl Drop for LifecycleLockDeadlineOverrideGuard {
78
+ fn drop(&mut self) {
79
+ TEST_DEADLINE_OVERRIDE.with(|override_cell| override_cell.set(self.previous));
80
+ }
81
+ }
82
+
83
+ fn acquire_agent_lifecycle_lock_with_deadlines(
84
+ request: LifecycleLockRequest<'_>,
85
+ timeout: Duration,
86
+ held_long: Duration,
87
+ ) -> Result<LifecycleLockGuard, LifecycleError> {
88
+ let lock_path = agent_lifecycle_lock_path(request.workspace);
89
+ if let Some(parent) = lock_path.parent() {
90
+ std::fs::create_dir_all(parent)
91
+ .map_err(|e| LifecycleError::StatePersist(format!("create lifecycle lock dir: {e}")))?;
92
+ }
93
+ let mut file = std::fs::OpenOptions::new()
94
+ .create(true)
95
+ .read(true)
96
+ .write(true)
97
+ .truncate(false)
98
+ .open(&lock_path)
99
+ .map_err(|e| LifecycleError::StatePersist(format!("open lifecycle lock: {e}")))?;
100
+ let started = Instant::now();
101
+ let mut long_event_written = false;
102
+ let mut waiter = None;
103
+ #[cfg(unix)]
104
+ {
105
+ use std::os::unix::io::AsRawFd;
106
+ let fd = file.as_raw_fd();
107
+ loop {
108
+ let rc = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
109
+ if rc == 0 {
110
+ write_lock_metadata(&mut file, &request, &lock_path)?;
111
+ return Ok(LifecycleLockGuard { file });
112
+ }
113
+ let elapsed = started.elapsed();
114
+ if waiter.is_none() {
115
+ waiter = WaiterFile::create(&request, &lock_path).ok();
116
+ }
117
+ if let Some(waiter) = waiter.as_ref() {
118
+ let _ = waiter.write(&request, elapsed);
119
+ }
120
+ if !long_event_written && elapsed >= held_long {
121
+ let _ =
122
+ write_lock_held_long_event(&request, &lock_path, elapsed, held_long, timeout);
123
+ long_event_written = true;
124
+ }
125
+ if elapsed >= timeout {
126
+ return Err(lock_timeout_error(&request, &lock_path, elapsed));
127
+ }
128
+ std::thread::sleep(std::cmp::min(
129
+ Duration::from_millis(50),
130
+ timeout.saturating_sub(elapsed),
131
+ ));
132
+ }
133
+ }
134
+ #[cfg(not(unix))]
135
+ {
136
+ let _ = file;
137
+ let elapsed = started.elapsed();
138
+ Err(lock_timeout_error(&request, &lock_path, elapsed))
139
+ }
140
+ }
141
+
142
+ pub(crate) fn agent_lifecycle_lock_path(workspace: &Path) -> PathBuf {
143
+ crate::model::paths::runtime_dir(workspace).join(format!("{AGENT_LIFECYCLE_LOCK_NAME}.lock"))
144
+ }
145
+
146
+ fn write_lock_metadata(
147
+ file: &mut std::fs::File,
148
+ request: &LifecycleLockRequest<'_>,
149
+ lock_path: &Path,
150
+ ) -> Result<(), LifecycleError> {
151
+ let metadata = serde_json::json!({
152
+ "lock_name": AGENT_LIFECYCLE_LOCK_NAME,
153
+ "pid": std::process::id(),
154
+ "operation": request.operation,
155
+ "team": request.team,
156
+ "agent_id": request.agent_id.map(AgentId::as_str),
157
+ "workspace": request.workspace.display().to_string(),
158
+ "lock_path": lock_path.display().to_string(),
159
+ "acquired_at": chrono::Utc::now().to_rfc3339(),
160
+ });
161
+ let mut bytes = serde_json::to_vec(&metadata).map_err(|e| {
162
+ LifecycleError::StatePersist(format!("encode lifecycle lock metadata: {e}"))
163
+ })?;
164
+ bytes.push(b'\n');
165
+ file.set_len(0)
166
+ .map_err(|e| LifecycleError::StatePersist(format!("truncate lifecycle lock: {e}")))?;
167
+ file.seek(std::io::SeekFrom::Start(0))
168
+ .map_err(|e| LifecycleError::StatePersist(format!("seek lifecycle lock: {e}")))?;
169
+ file.write_all(&bytes)
170
+ .map_err(|e| LifecycleError::StatePersist(format!("write lifecycle lock metadata: {e}")))?;
171
+ Ok(())
172
+ }
173
+
174
+ fn lock_timeout_error(
175
+ request: &LifecycleLockRequest<'_>,
176
+ lock_path: &Path,
177
+ elapsed: Duration,
178
+ ) -> LifecycleError {
179
+ LifecycleError::LifecycleLockTimeout {
180
+ lock_path: lock_path.to_path_buf(),
181
+ log_path: crate::model::paths::logs_dir(request.workspace).join("events.jsonl"),
182
+ operation: request.operation.to_string(),
183
+ waited_ms: elapsed.as_millis(),
184
+ }
185
+ }
186
+
187
+ fn write_lock_held_long_event(
188
+ request: &LifecycleLockRequest<'_>,
189
+ lock_path: &Path,
190
+ elapsed: Duration,
191
+ held_long: Duration,
192
+ timeout: Duration,
193
+ ) -> Result<(), crate::event_log::EventLogError> {
194
+ let holder = read_holder(lock_path);
195
+ let holder_duration_ms = holder_duration_ms(holder.as_ref());
196
+ crate::event_log::EventLog::new(request.workspace).write(
197
+ "lifecycle.lock_held_long",
198
+ serde_json::json!({
199
+ "lock_name": AGENT_LIFECYCLE_LOCK_NAME,
200
+ "lock_path": lock_path.display().to_string(),
201
+ "workspace": request.workspace.display().to_string(),
202
+ "operation": request.operation,
203
+ "team": request.team,
204
+ "agent_id": request.agent_id.map(AgentId::as_str),
205
+ "waited_ms": elapsed.as_millis(),
206
+ "threshold_ms": held_long.as_millis(),
207
+ "timeout_ms": timeout.as_millis(),
208
+ "holder": holder,
209
+ "holder_duration_ms": holder_duration_ms,
210
+ "blocked_queue_len": blocked_queue_len(request.workspace),
211
+ }),
212
+ )?;
213
+ Ok(())
214
+ }
215
+
216
+ fn read_holder(lock_path: &Path) -> Option<serde_json::Value> {
217
+ let text = std::fs::read_to_string(lock_path).ok()?;
218
+ let value = serde_json::from_str::<serde_json::Value>(&text).ok()?;
219
+ Some(serde_json::json!({
220
+ "pid": value.get("pid").cloned().unwrap_or(serde_json::Value::Null),
221
+ "operation": value.get("operation").cloned().unwrap_or(serde_json::Value::Null),
222
+ "team": value.get("team").cloned().unwrap_or(serde_json::Value::Null),
223
+ "agent_id": value.get("agent_id").cloned().unwrap_or(serde_json::Value::Null),
224
+ "acquired_at": value.get("acquired_at").cloned().unwrap_or(serde_json::Value::Null),
225
+ }))
226
+ }
227
+
228
+ fn holder_duration_ms(holder: Option<&serde_json::Value>) -> Option<u128> {
229
+ let acquired_at = holder?
230
+ .get("acquired_at")
231
+ .and_then(serde_json::Value::as_str)?;
232
+ let acquired_at = chrono::DateTime::parse_from_rfc3339(acquired_at).ok()?;
233
+ let elapsed = chrono::Utc::now().signed_duration_since(acquired_at.with_timezone(&chrono::Utc));
234
+ elapsed.to_std().ok().map(|d| d.as_millis())
235
+ }
236
+
237
+ fn waiter_dir(workspace: &Path) -> PathBuf {
238
+ crate::model::paths::runtime_dir(workspace).join(format!("{AGENT_LIFECYCLE_LOCK_NAME}.waiters"))
239
+ }
240
+
241
+ fn blocked_queue_len(workspace: &Path) -> Option<usize> {
242
+ std::fs::read_dir(waiter_dir(workspace))
243
+ .ok()
244
+ .map(|entries| entries.filter(|entry| entry.is_ok()).count())
245
+ }
246
+
247
+ struct WaiterFile {
248
+ path: PathBuf,
249
+ }
250
+
251
+ impl WaiterFile {
252
+ fn create(request: &LifecycleLockRequest<'_>, lock_path: &Path) -> std::io::Result<Self> {
253
+ let dir = waiter_dir(request.workspace);
254
+ std::fs::create_dir_all(&dir)?;
255
+ let path = dir.join(format!("{}.json", std::process::id()));
256
+ let file = Self { path };
257
+ file.write_with_lock(request, lock_path, Duration::from_millis(0))?;
258
+ Ok(file)
259
+ }
260
+
261
+ fn write(&self, request: &LifecycleLockRequest<'_>, elapsed: Duration) -> std::io::Result<()> {
262
+ self.write_with_lock(
263
+ request,
264
+ &agent_lifecycle_lock_path(request.workspace),
265
+ elapsed,
266
+ )
267
+ }
268
+
269
+ fn write_with_lock(
270
+ &self,
271
+ request: &LifecycleLockRequest<'_>,
272
+ lock_path: &Path,
273
+ elapsed: Duration,
274
+ ) -> std::io::Result<()> {
275
+ let payload = serde_json::json!({
276
+ "pid": std::process::id(),
277
+ "operation": request.operation,
278
+ "team": request.team,
279
+ "agent_id": request.agent_id.map(AgentId::as_str),
280
+ "workspace": request.workspace.display().to_string(),
281
+ "lock_path": lock_path.display().to_string(),
282
+ "waited_ms": elapsed.as_millis(),
283
+ "updated_at": chrono::Utc::now().to_rfc3339(),
284
+ });
285
+ let bytes = serde_json::to_vec(&payload).map_err(std::io::Error::other)?;
286
+ std::fs::write(&self.path, bytes)
287
+ }
288
+ }
289
+
290
+ impl Drop for WaiterFile {
291
+ fn drop(&mut self) {
292
+ let _ = std::fs::remove_file(&self.path);
293
+ }
294
+ }
295
+
296
+ #[cfg(unix)]
297
+ impl Drop for LifecycleLockGuard {
298
+ fn drop(&mut self) {
299
+ use std::os::unix::io::AsRawFd;
300
+ unsafe { libc::flock(self.file.as_raw_fd(), libc::LOCK_UN) };
301
+ }
302
+ }
@@ -36,6 +36,7 @@
36
36
  pub mod display;
37
37
  pub mod helpers;
38
38
  pub mod launch;
39
+ pub(crate) mod lock;
39
40
  pub(crate) mod profile_launch;
40
41
  pub(crate) mod profile_smoke;
41
42
  pub mod restart;
@@ -9,6 +9,7 @@ use serde_json::{json, Value};
9
9
  use crate::lifecycle::profile_launch;
10
10
  use crate::model::enums::{AuthMode, Provider};
11
11
  use crate::model::yaml::Value as YamlValue;
12
+ use crate::provider::wire::provider_wire;
12
13
 
13
14
  pub(crate) const DEFAULT_PROFILE_SMOKE_TIMEOUT: Duration = Duration::from_secs(8);
14
15
 
@@ -505,17 +506,6 @@ fn redact_text(raw: &str, secrets: &[&str]) -> String {
505
506
  out
506
507
  }
507
508
 
508
- fn provider_wire(provider: Provider) -> &'static str {
509
- match provider {
510
- Provider::Claude => "claude",
511
- Provider::ClaudeCode => "claude_code",
512
- Provider::Codex => "codex",
513
- Provider::Copilot => "copilot",
514
- Provider::GeminiCli => "gemini_cli",
515
- Provider::Fake => "fake",
516
- }
517
- }
518
-
519
509
  fn auth_mode_wire(auth_mode: AuthMode) -> &'static str {
520
510
  match auth_mode {
521
511
  AuthMode::Subscription => "subscription",