@team-agent/installer 0.5.50 → 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 (93) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +5 -3
  4. package/crates/team-agent/src/cli/emit.rs +2 -1
  5. package/crates/team-agent/src/cli/mod.rs +32 -14
  6. package/crates/team-agent/src/cli/named_address.rs +46 -115
  7. package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
  8. package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
  9. package/crates/team-agent/src/cli/send/persist.rs +154 -0
  10. package/crates/team-agent/src/cli/send/presentation.rs +333 -0
  11. package/crates/team-agent/src/cli/send/resolve.rs +361 -0
  12. package/crates/team-agent/src/cli/send.rs +44 -1169
  13. package/crates/team-agent/src/cli/spec.rs +1 -1
  14. package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
  15. package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
  16. package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
  17. package/crates/team-agent/src/cli/status_port/format.rs +145 -0
  18. package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
  19. package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
  20. package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
  21. package/crates/team-agent/src/cli/status_port/store.rs +412 -0
  22. package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
  23. package/crates/team-agent/src/cli/status_port.rs +47 -1548
  24. package/crates/team-agent/src/cli/tests/main_preserved.rs +1 -0
  25. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
  26. package/crates/team-agent/src/cli/types.rs +1 -0
  27. package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
  28. package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
  29. package/crates/team-agent/src/coordinator/tick.rs +37 -0
  30. package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
  31. package/crates/team-agent/src/db/message_store.rs +154 -44
  32. package/crates/team-agent/src/event_log.rs +73 -0
  33. package/crates/team-agent/src/leader/lease.rs +202 -16
  34. package/crates/team-agent/src/leader/start.rs +28 -4
  35. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
  36. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +303 -0
  37. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
  38. package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
  39. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
  40. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
  41. package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
  42. package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
  43. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
  44. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
  45. package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
  46. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
  47. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
  48. package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
  49. package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
  50. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
  51. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
  52. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
  53. package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
  54. package/crates/team-agent/src/lifecycle/restart/agent.rs +63 -29
  55. package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
  56. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +167 -27
  57. package/crates/team-agent/src/lifecycle/restart/remove.rs +503 -77
  58. package/crates/team-agent/src/lifecycle/restart.rs +26 -2
  59. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +906 -17
  60. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +67 -2
  61. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
  62. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
  63. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
  64. package/crates/team-agent/src/mcp_server/mod.rs +1 -1
  65. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
  66. package/crates/team-agent/src/mcp_server/tests/tools.rs +18 -1
  67. package/crates/team-agent/src/mcp_server/tests.rs +11 -4
  68. package/crates/team-agent/src/mcp_server/tools.rs +14 -5
  69. package/crates/team-agent/src/messaging/activity.rs +4 -2
  70. package/crates/team-agent/src/messaging/address.rs +86 -0
  71. package/crates/team-agent/src/messaging/delivery.rs +288 -183
  72. package/crates/team-agent/src/messaging/helpers.rs +17 -13
  73. package/crates/team-agent/src/messaging/leader_channel.rs +171 -0
  74. package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
  75. package/crates/team-agent/src/messaging/mod.rs +15 -2
  76. package/crates/team-agent/src/messaging/persist.rs +309 -0
  77. package/crates/team-agent/src/messaging/results.rs +16 -24
  78. package/crates/team-agent/src/messaging/scheduler.rs +4 -2
  79. package/crates/team-agent/src/messaging/selftest.rs +19 -12
  80. package/crates/team-agent/src/messaging/send.rs +101 -49
  81. package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
  82. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
  83. package/crates/team-agent/src/messaging/tests/mod.rs +2 -0
  84. package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
  85. package/crates/team-agent/src/messaging/watchers.rs +13 -3
  86. package/crates/team-agent/src/redaction.rs +72 -2
  87. package/crates/team-agent/src/state/persist.rs +284 -21
  88. package/crates/team-agent/src/state/projection.rs +245 -30
  89. package/crates/team-agent/src/state/repository/intent.rs +13 -0
  90. package/crates/team-agent/src/state/repository/tests.rs +47 -0
  91. package/crates/team-agent/src/state/repository.rs +94 -27
  92. package/crates/team-agent/src/state/selector.rs +9 -18
  93. package/package.json +4 -4
@@ -0,0 +1,313 @@
1
+ use std::collections::{BTreeMap, BTreeSet};
2
+ use std::path::{Path, PathBuf};
3
+ use std::process::Command;
4
+
5
+ use crate::lifecycle::*;
6
+ use crate::model::enums::{AuthMode, DisplayBackend, PaneLiveness, Provider, ProviderEffort};
7
+ use crate::model::ids::AgentId;
8
+ use crate::model::permissions::{self, AgentPermissionInput};
9
+ use crate::model::yaml::{self, Value};
10
+ use crate::state::persist::load_runtime_state;
11
+ use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
12
+
13
+ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
14
+
15
+ use super::*;
16
+
17
+ #[derive(Debug, Clone, PartialEq, Eq)]
18
+ pub(crate) struct LayoutPlacement {
19
+ pub agent_id: AgentId,
20
+ pub layout_window: WindowName,
21
+ pub layout_index: usize,
22
+ pub pane_index: usize,
23
+ pub starts_window: bool,
24
+ }
25
+
26
+ pub(crate) fn adaptive_layout_plan(
27
+ agent_ids: &[AgentId],
28
+ max_per_window: usize,
29
+ ) -> Vec<LayoutPlacement> {
30
+ let max_per_window = max_per_window.max(1);
31
+ agent_ids
32
+ .iter()
33
+ .enumerate()
34
+ .map(|(idx, agent_id)| {
35
+ let layout_index = idx / max_per_window;
36
+ let pane_index = idx % max_per_window;
37
+ LayoutPlacement {
38
+ agent_id: agent_id.clone(),
39
+ layout_window: WindowName::new(format!("team-w{}", layout_index + 1)),
40
+ layout_index,
41
+ pane_index,
42
+ starts_window: pane_index == 0,
43
+ }
44
+ })
45
+ .collect()
46
+ }
47
+
48
+ pub(crate) const ADAPTIVE_LAYOUT_MAX_PER_WINDOW: usize = 3;
49
+
50
+ pub(crate) fn state_uses_adaptive_layout(state: &serde_json::Value) -> bool {
51
+ state
52
+ .get("display_backend")
53
+ .and_then(serde_json::Value::as_str)
54
+ .is_some_and(|backend| backend == "adaptive")
55
+ || state
56
+ .get("runtime")
57
+ .and_then(|runtime| runtime.get("display_backend"))
58
+ .and_then(serde_json::Value::as_str)
59
+ .is_some_and(|backend| backend == "adaptive")
60
+ || state
61
+ .get("agents")
62
+ .and_then(serde_json::Value::as_object)
63
+ .is_some_and(|agents| {
64
+ agents.values().any(|agent| {
65
+ agent
66
+ .get("layout_window")
67
+ .and_then(serde_json::Value::as_str)
68
+ .is_some_and(|window| !window.is_empty())
69
+ })
70
+ })
71
+ }
72
+
73
+ pub(crate) fn adaptive_placement_for_agent(
74
+ state: &serde_json::Value,
75
+ transport: &dyn Transport,
76
+ session_name: &SessionName,
77
+ agent_id: &AgentId,
78
+ ) -> Option<LayoutPlacement> {
79
+ if !state_uses_adaptive_layout(state) {
80
+ return None;
81
+ }
82
+ // E43 Fix A (0.3.24 bug#3, demo-director blocker): cross-check live panes
83
+ // by both pane_id AND window_name. Real-machine state can carry a stale
84
+ // `layout_window` residue (e.g. "team-w2") while the live pane for that
85
+ // agent actually lives under a DIFFERENT window_name (e.g. "architect").
86
+ // The prior pane_id-only check let the stale claim slip through, and the
87
+ // resulting placement asked spawn_agent_window to split a phantom window.
88
+ let live_targets = transport.list_targets().unwrap_or_default();
89
+ let live_panes: BTreeSet<String> = live_targets
90
+ .iter()
91
+ .map(|pane| pane.pane_id.as_str().to_string())
92
+ .collect();
93
+ // Map pane_id → window_name for the window-name cross-check.
94
+ let live_pane_window: BTreeMap<String, String> = live_targets
95
+ .iter()
96
+ .filter_map(|pane| {
97
+ pane.window_name
98
+ .as_ref()
99
+ .map(|name| (pane.pane_id.as_str().to_string(), name.as_str().to_string()))
100
+ })
101
+ .collect();
102
+ let live_windows: BTreeSet<String> = transport
103
+ .list_windows(session_name)
104
+ .unwrap_or_default()
105
+ .into_iter()
106
+ .map(|window| window.as_str().to_string())
107
+ .collect();
108
+ let mut windows: BTreeMap<usize, (String, usize)> = BTreeMap::new();
109
+ if let Some(agents) = state.get("agents").and_then(serde_json::Value::as_object) {
110
+ for (id, agent) in agents {
111
+ if id == agent_id.as_str() {
112
+ continue;
113
+ }
114
+ let Some(window) = agent
115
+ .get("layout_window")
116
+ .or_else(|| agent.get("window"))
117
+ .and_then(serde_json::Value::as_str)
118
+ .filter(|window| !window.is_empty())
119
+ else {
120
+ continue;
121
+ };
122
+ // E45 (0.3.24 bug#4): only canonical `team-w<N>[-suffix]` windows
123
+ // count as adaptive layout windows. Per-agent windows (named
124
+ // after agent_id like `developer`) are NOT layout windows even if
125
+ // the state's `layout_window`/`layout_index` carries them — those
126
+ // are residue from a prior launch or explicit per-agent topology.
127
+ // Treating them as adaptive caused add-agent to split into the
128
+ // developer worker's pane on macmini (split-window -t :developer
129
+ // succeeded → 2 panes in developer window, no new window for
130
+ // demo-director).
131
+ if !is_adaptive_layout_window(window) {
132
+ continue;
133
+ }
134
+ let pane_id = agent.get("pane_id").and_then(serde_json::Value::as_str);
135
+ let pane_live = pane_id.is_some_and(|pane| live_panes.contains(pane));
136
+ // E43 Fix A: window_name match — when the agent's pane is live,
137
+ // its live window_name MUST equal the claimed `window`; otherwise
138
+ // the claim is stale residue from a respawn/rename and must not
139
+ // count toward the layout map.
140
+ let pane_window_matches = pane_id
141
+ .and_then(|pane| live_pane_window.get(pane))
142
+ .is_some_and(|name| name == window);
143
+ if pane_live && !pane_window_matches {
144
+ continue;
145
+ }
146
+ if !pane_live && (!live_panes.is_empty() || !live_windows.contains(window)) {
147
+ continue;
148
+ }
149
+ let layout_index = agent
150
+ .get("layout_index")
151
+ .and_then(serde_json::Value::as_u64)
152
+ .map(|idx| idx as usize)
153
+ .or_else(|| parse_team_layout_index(window))
154
+ .unwrap_or(windows.len());
155
+ let entry = windows
156
+ .entry(layout_index)
157
+ .or_insert_with(|| (window.to_string(), 0));
158
+ entry.1 = entry.1.saturating_add(1);
159
+ }
160
+ }
161
+ if let Some((&layout_index, (window, count))) = windows.iter().next_back() {
162
+ if *count < ADAPTIVE_LAYOUT_MAX_PER_WINDOW {
163
+ return Some(LayoutPlacement {
164
+ agent_id: agent_id.clone(),
165
+ layout_window: WindowName::new(window.clone()),
166
+ layout_index,
167
+ pane_index: *count,
168
+ starts_window: false,
169
+ });
170
+ }
171
+ }
172
+ // E45 (0.3.24 bug#4): when the live session has NO real adaptive layout
173
+ // window (the topology is effectively per-agent, even though state says
174
+ // display_backend=adaptive), DO NOT synthesise a fresh `team-w<N>`
175
+ // window — that would force the new agent into an adaptive-layout pane
176
+ // shape the rest of the session does not actually use. Return None so
177
+ // the caller (`start_agent_at_paths` → `spawn_agent_window`) falls back
178
+ // to its non-placement path, which opens a new window named after the
179
+ // agent_id (canonical per-agent pattern, matches the existing 7 workers).
180
+ if windows.is_empty() {
181
+ return None;
182
+ }
183
+ let next_index = windows.keys().next_back().map(|idx| idx + 1).unwrap_or(0);
184
+ let base = format!("team-w{}", next_index + 1);
185
+ Some(LayoutPlacement {
186
+ agent_id: agent_id.clone(),
187
+ layout_window: unique_layout_window_name(&base, &live_windows),
188
+ layout_index: next_index,
189
+ pane_index: 0,
190
+ starts_window: true,
191
+ })
192
+ }
193
+
194
+ pub(crate) fn adaptive_existing_placement_for_agent(
195
+ state: &serde_json::Value,
196
+ transport: &dyn Transport,
197
+ session_name: &SessionName,
198
+ agent_id: &AgentId,
199
+ ) -> Option<LayoutPlacement> {
200
+ if !state_uses_adaptive_layout(state) {
201
+ return None;
202
+ }
203
+ let agent = state.get("agents")?.get(agent_id.as_str())?;
204
+ let window = agent
205
+ .get("layout_window")
206
+ .or_else(|| agent.get("window"))
207
+ .and_then(serde_json::Value::as_str)
208
+ .filter(|window| !window.is_empty())?;
209
+ // E45 (0.3.24 bug#4): existing-placement is meaningless for a per-agent
210
+ // window name (e.g. `developer`). Return None and let the caller fall
211
+ // back to its non-placement spawn path, which opens / reuses a window
212
+ // named after the agent_id. Only canonical `team-w<N>` windows are
213
+ // honored as existing adaptive placements.
214
+ if !is_adaptive_layout_window(window) {
215
+ return None;
216
+ }
217
+ let layout_index = agent
218
+ .get("layout_index")
219
+ .and_then(serde_json::Value::as_u64)
220
+ .map(|idx| idx as usize)
221
+ .or_else(|| parse_team_layout_index(window))
222
+ .unwrap_or(0);
223
+ let desired_pane_index = agent
224
+ .get("pane_index")
225
+ .and_then(serde_json::Value::as_u64)
226
+ .map(|idx| idx as usize)
227
+ .unwrap_or(0);
228
+ let live_windows: BTreeSet<String> = transport
229
+ .list_windows(session_name)
230
+ .unwrap_or_default()
231
+ .into_iter()
232
+ .map(|window| window.as_str().to_string())
233
+ .collect();
234
+ if !live_windows.contains(window) {
235
+ // E43 Fix B (0.3.24 bug#3): the claimed `layout_window` is NOT in
236
+ // live_windows — it's stale residue. The session is effectively
237
+ // per-agent (live windows are named after agent_ids), so fall back
238
+ // to a new window named after the agent_id itself instead of
239
+ // synthesising a fresh phantom-named window the next spawn would
240
+ // try (and fail) to split.
241
+ return Some(LayoutPlacement {
242
+ agent_id: agent_id.clone(),
243
+ layout_window: WindowName::new(agent_id.as_str()),
244
+ layout_index,
245
+ pane_index: 0,
246
+ starts_window: true,
247
+ });
248
+ }
249
+ let existing_panes = transport
250
+ .list_targets()
251
+ .unwrap_or_default()
252
+ .into_iter()
253
+ .filter(|pane| {
254
+ pane.window_name
255
+ .as_ref()
256
+ .is_some_and(|name| name.as_str() == window)
257
+ && agent
258
+ .get("pane_id")
259
+ .and_then(serde_json::Value::as_str)
260
+ .is_none_or(|agent_pane| agent_pane != pane.pane_id.as_str())
261
+ })
262
+ .count();
263
+ Some(LayoutPlacement {
264
+ agent_id: agent_id.clone(),
265
+ layout_window: WindowName::new(window),
266
+ layout_index,
267
+ pane_index: existing_panes,
268
+ starts_window: false,
269
+ })
270
+ }
271
+
272
+ pub(super) fn parse_team_layout_index(window: &str) -> Option<usize> {
273
+ window
274
+ .strip_prefix("team-w")
275
+ .and_then(|rest| rest.split('-').next())
276
+ .and_then(|raw| raw.parse::<usize>().ok())
277
+ .and_then(|idx| idx.checked_sub(1))
278
+ }
279
+
280
+ /// E45 (0.3.24 bug#4, demo-director second-layer drift): a window name is a
281
+ /// REAL adaptive layout window only when it matches the canonical
282
+ /// `team-w<N>[-suffix]` shape (i.e. `parse_team_layout_index` returns Some).
283
+ /// Per-agent window names like `developer` / `architect` / `demo-director`
284
+ /// are NOT adaptive layout windows even if state happens to carry them in
285
+ /// the agent's `layout_window` or `layout_index` field — they are residue
286
+ /// from a prior launch / explicit per-agent topology. Treating them as
287
+ /// adaptive caused the macmini repro: add-agent demo-director split into
288
+ /// the developer worker's window @453 instead of opening its own.
289
+ pub(super) fn is_adaptive_layout_window(window: &str) -> bool {
290
+ parse_team_layout_index(window).is_some()
291
+ }
292
+
293
+ /// Crate-public wrapper for the defensive guard at
294
+ /// `restart/common.rs::spawn_agent_window`. Same semantics as the private
295
+ /// helper above; promoted to `pub(crate)` so the spawn-time defence-in-depth
296
+ /// layer can refuse to split into a per-agent window even if a stale
297
+ /// placement asks for it.
298
+ pub(crate) fn is_adaptive_layout_window_pub(window: &str) -> bool {
299
+ is_adaptive_layout_window(window)
300
+ }
301
+
302
+ pub(super) fn unique_layout_window_name(base: &str, live_windows: &BTreeSet<String>) -> WindowName {
303
+ if !live_windows.contains(base) {
304
+ return WindowName::new(base);
305
+ }
306
+ for suffix in 2.. {
307
+ let candidate = format!("{base}-{suffix}");
308
+ if !live_windows.contains(&candidate) {
309
+ return WindowName::new(candidate);
310
+ }
311
+ }
312
+ unreachable!("unbounded suffix search always returns")
313
+ }