@team-agent/installer 0.3.21 → 0.3.23
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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/emit.rs +9 -1
- package/crates/team-agent/src/cli/mod.rs +158 -2
- package/crates/team-agent/src/cli/status.rs +61 -0
- package/crates/team-agent/src/cli/status_port.rs +2 -2
- package/crates/team-agent/src/cli/tests/base.rs +26 -0
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +233 -9
- package/crates/team-agent/src/coordinator/tests/energy.rs +548 -0
- package/crates/team-agent/src/coordinator/tests/mod.rs +1 -0
- package/crates/team-agent/src/coordinator/tick.rs +452 -17
- package/crates/team-agent/src/lifecycle/display.rs +23 -302
- package/crates/team-agent/src/lifecycle/launch.rs +38 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +27 -6
- package/crates/team-agent/src/lifecycle/restart/common.rs +179 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +295 -13
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +30 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +89 -5
- package/crates/team-agent/src/messaging/delivery.rs +324 -95
- package/crates/team-agent/src/messaging/scheduler.rs +95 -73
- package/crates/team-agent/src/messaging/send.rs +10 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +460 -0
- package/crates/team-agent/src/messaging/tests/spine.rs +51 -2
- package/crates/team-agent/src/session_capture.rs +261 -1
- package/crates/team-agent/src/tmux_backend/tests.rs +97 -1
- package/crates/team-agent/src/tmux_backend.rs +138 -2
- package/crates/team-agent/src/transport/test_support.rs +25 -0
- package/crates/team-agent/src/transport.rs +11 -0
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +2 -1
- package/skills/team-agent/references/team-in-team.md +127 -0
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
use crate::cli::lifecycle_port::{sessions_to_kill, KillDecision};
|
|
8
8
|
use crate::transport::{
|
|
9
|
-
AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
|
|
10
|
-
|
|
9
|
+
AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport, Key,
|
|
10
|
+
PaneField, PaneId, PaneInfo, SessionName, SetEnvOutcome, SpawnResult, Target, Transport,
|
|
11
11
|
TransportError, WindowName,
|
|
12
12
|
};
|
|
13
13
|
use serde_json::json;
|
|
@@ -33,7 +33,10 @@ fn rc4_exclusive_socket_kills_server() {
|
|
|
33
33
|
// 空 session 集 → 逐 kill(no-op),不整 server 拆(没东西可拆)。
|
|
34
34
|
assert_eq!(
|
|
35
35
|
sessions_to_kill(&[], &BTreeSet::new()),
|
|
36
|
-
KillDecision::KillIndividually {
|
|
36
|
+
KillDecision::KillIndividually {
|
|
37
|
+
to_kill: vec![],
|
|
38
|
+
spared: vec![]
|
|
39
|
+
}
|
|
37
40
|
);
|
|
38
41
|
}
|
|
39
42
|
|
|
@@ -45,8 +48,16 @@ fn rc1_in_tmux_no_prefix_anchor_spares_user_session() {
|
|
|
45
48
|
let decision = sessions_to_kill(&sessions, &anchor);
|
|
46
49
|
match decision {
|
|
47
50
|
KillDecision::KillIndividually { to_kill, spared } => {
|
|
48
|
-
assert_eq!(
|
|
49
|
-
|
|
51
|
+
assert_eq!(
|
|
52
|
+
to_kill,
|
|
53
|
+
names(&["team-x"]),
|
|
54
|
+
"only non-anchor session killed"
|
|
55
|
+
);
|
|
56
|
+
assert_eq!(
|
|
57
|
+
spared,
|
|
58
|
+
names(&["team-coder-team"]),
|
|
59
|
+
"user/leader session spared by anchor"
|
|
60
|
+
);
|
|
50
61
|
}
|
|
51
62
|
other => panic!("anchor session must force per-session kill, not {other:?}"),
|
|
52
63
|
}
|
|
@@ -106,7 +117,10 @@ fn union_prefix_and_anchor_no_double_count() {
|
|
|
106
117
|
let decision = sessions_to_kill(&sessions, &anchor);
|
|
107
118
|
assert_eq!(
|
|
108
119
|
decision,
|
|
109
|
-
KillDecision::KillIndividually {
|
|
120
|
+
KillDecision::KillIndividually {
|
|
121
|
+
to_kill: vec![],
|
|
122
|
+
spared: names(&["team-agent-leader-claude-ws-beef"])
|
|
123
|
+
}
|
|
110
124
|
);
|
|
111
125
|
}
|
|
112
126
|
|
|
@@ -136,12 +150,214 @@ fn missing_coordinator_is_ok_when_shutdown_cleaned_session() {
|
|
|
136
150
|
.expect("shutdown should complete");
|
|
137
151
|
assert_eq!(out["coordinator"]["status"], json!("missing"));
|
|
138
152
|
assert_eq!(
|
|
139
|
-
out["ok"],
|
|
153
|
+
out["ok"],
|
|
154
|
+
json!(true),
|
|
140
155
|
"coordinator.status=missing alone must not make a fully cleaned shutdown partial: {out}"
|
|
141
156
|
);
|
|
142
157
|
assert_eq!(out["status"], json!("ok"));
|
|
143
158
|
assert_eq!(out["residuals"]["sessions"], json!([]));
|
|
144
159
|
assert_eq!(out["residuals"]["processes"], json!([]));
|
|
160
|
+
assert_eq!(out["residuals"]["owned_files"], json!([]));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#[test]
|
|
164
|
+
fn owned_empty_endpoint_cleanup_removes_socket_file_before_reporting_ok() {
|
|
165
|
+
let ws = tmp_shutdown_workspace("owned-empty-endpoint-clean");
|
|
166
|
+
let socket = ws.join("owned.sock");
|
|
167
|
+
std::fs::write(&socket, b"socket").unwrap();
|
|
168
|
+
crate::state::persist::save_runtime_state(
|
|
169
|
+
&ws,
|
|
170
|
+
&json!({
|
|
171
|
+
"session_name": "team-owned-clean",
|
|
172
|
+
"tmux_endpoint": socket.to_string_lossy(),
|
|
173
|
+
"tmux_socket": socket.to_string_lossy(),
|
|
174
|
+
"tmux_socket_source": "workspace",
|
|
175
|
+
"is_external_leader": false,
|
|
176
|
+
"agents": {}
|
|
177
|
+
}),
|
|
178
|
+
)
|
|
179
|
+
.unwrap();
|
|
180
|
+
let transport = CleanShutdownTransport::new();
|
|
181
|
+
|
|
182
|
+
let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
|
|
183
|
+
.expect("shutdown should complete");
|
|
184
|
+
|
|
185
|
+
assert_eq!(out["ok"], json!(true));
|
|
186
|
+
assert_eq!(out["status"], json!("ok"));
|
|
187
|
+
assert_eq!(out["residuals"]["owned_files"], json!([]));
|
|
188
|
+
assert!(
|
|
189
|
+
!socket.exists(),
|
|
190
|
+
"owned empty endpoint socket file must be removed"
|
|
191
|
+
);
|
|
192
|
+
assert!(
|
|
193
|
+
transport.kill_server_called(),
|
|
194
|
+
"owned empty endpoint should be torn down after session cleanup"
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#[test]
|
|
199
|
+
fn leader_env_endpoint_is_not_owned_or_removed() {
|
|
200
|
+
let ws = tmp_shutdown_workspace("leader-env-endpoint-spared");
|
|
201
|
+
let socket = ws.join("leader-env.sock");
|
|
202
|
+
std::fs::write(&socket, b"socket").unwrap();
|
|
203
|
+
crate::state::persist::save_runtime_state(
|
|
204
|
+
&ws,
|
|
205
|
+
&json!({
|
|
206
|
+
"session_name": "team-external",
|
|
207
|
+
"tmux_endpoint": socket.to_string_lossy(),
|
|
208
|
+
"tmux_socket": socket.to_string_lossy(),
|
|
209
|
+
"tmux_socket_source": "leader_env",
|
|
210
|
+
"is_external_leader": true,
|
|
211
|
+
"agents": {}
|
|
212
|
+
}),
|
|
213
|
+
)
|
|
214
|
+
.unwrap();
|
|
215
|
+
let transport = CleanShutdownTransport::new();
|
|
216
|
+
|
|
217
|
+
let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
|
|
218
|
+
.expect("shutdown should complete");
|
|
219
|
+
|
|
220
|
+
assert_eq!(out["ok"], json!(true));
|
|
221
|
+
assert_eq!(out["residuals"]["owned_files"], json!([]));
|
|
222
|
+
assert!(socket.exists(), "leader_env socket is not team-owned");
|
|
223
|
+
assert!(
|
|
224
|
+
!transport.kill_server_called(),
|
|
225
|
+
"leader_env/shared socket must never be torn down by owned cleanup"
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#[test]
|
|
230
|
+
fn owned_file_residual_makes_shutdown_failed_not_partial() {
|
|
231
|
+
let ws = tmp_shutdown_workspace("owned-file-residual-failed");
|
|
232
|
+
let socket_dir = ws.join("owned-dir.sock");
|
|
233
|
+
std::fs::create_dir_all(&socket_dir).unwrap();
|
|
234
|
+
crate::state::persist::save_runtime_state(
|
|
235
|
+
&ws,
|
|
236
|
+
&json!({
|
|
237
|
+
"session_name": "team-owned-residual",
|
|
238
|
+
"tmux_endpoint": socket_dir.to_string_lossy(),
|
|
239
|
+
"tmux_socket": socket_dir.to_string_lossy(),
|
|
240
|
+
"tmux_socket_source": "workspace",
|
|
241
|
+
"is_external_leader": false,
|
|
242
|
+
"agents": {}
|
|
243
|
+
}),
|
|
244
|
+
)
|
|
245
|
+
.unwrap();
|
|
246
|
+
let transport = CleanShutdownTransport::new();
|
|
247
|
+
|
|
248
|
+
let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
|
|
249
|
+
.expect("shutdown should complete");
|
|
250
|
+
|
|
251
|
+
assert_eq!(out["ok"], json!(false));
|
|
252
|
+
assert_eq!(out["status"], json!("failed"));
|
|
253
|
+
assert_eq!(out["phase"], json!(null));
|
|
254
|
+
assert_eq!(
|
|
255
|
+
out["residuals"]["owned_files"],
|
|
256
|
+
json!([{ "path": socket_dir.display().to_string() }])
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
#[test]
|
|
261
|
+
fn scoped_shutdown_keeps_owned_endpoint_when_sibling_session_remains() {
|
|
262
|
+
let ws = tmp_shutdown_workspace("scoped-owned-endpoint-sibling");
|
|
263
|
+
let socket = ws.join("owned-shared.sock");
|
|
264
|
+
std::fs::write(&socket, b"socket").unwrap();
|
|
265
|
+
crate::state::persist::save_runtime_state(
|
|
266
|
+
&ws,
|
|
267
|
+
&json!({
|
|
268
|
+
"active_team_key": "team-a",
|
|
269
|
+
"teams": {
|
|
270
|
+
"team-a": {
|
|
271
|
+
"team_key": "team-a",
|
|
272
|
+
"status": "alive",
|
|
273
|
+
"session_name": "team-a",
|
|
274
|
+
"tmux_endpoint": socket.to_string_lossy(),
|
|
275
|
+
"tmux_socket": socket.to_string_lossy(),
|
|
276
|
+
"tmux_socket_source": "workspace",
|
|
277
|
+
"is_external_leader": false,
|
|
278
|
+
"agents": {}
|
|
279
|
+
},
|
|
280
|
+
"team-b": {
|
|
281
|
+
"team_key": "team-b",
|
|
282
|
+
"status": "alive",
|
|
283
|
+
"session_name": "team-b",
|
|
284
|
+
"tmux_endpoint": socket.to_string_lossy(),
|
|
285
|
+
"tmux_socket": socket.to_string_lossy(),
|
|
286
|
+
"tmux_socket_source": "workspace",
|
|
287
|
+
"is_external_leader": false,
|
|
288
|
+
"agents": {}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}),
|
|
292
|
+
)
|
|
293
|
+
.unwrap();
|
|
294
|
+
let transport = CleanShutdownTransport::new()
|
|
295
|
+
.with_targets(vec![PaneInfo {
|
|
296
|
+
pane_id: PaneId::new("%2"),
|
|
297
|
+
session: SessionName::new("team-b"),
|
|
298
|
+
window_index: Some(0),
|
|
299
|
+
window_name: Some(WindowName::new("worker")),
|
|
300
|
+
pane_index: Some(0),
|
|
301
|
+
tty: None,
|
|
302
|
+
current_command: Some("fake".to_string()),
|
|
303
|
+
current_path: None,
|
|
304
|
+
active: true,
|
|
305
|
+
pane_pid: None,
|
|
306
|
+
leader_env: BTreeMap::new(),
|
|
307
|
+
}])
|
|
308
|
+
.with_targets_persist_after_kill();
|
|
309
|
+
|
|
310
|
+
let out =
|
|
311
|
+
crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, Some("team-a"), &transport)
|
|
312
|
+
.expect("shutdown should complete");
|
|
313
|
+
|
|
314
|
+
assert_eq!(out["ok"], json!(true));
|
|
315
|
+
assert_eq!(out["residuals"]["owned_files"], json!([]));
|
|
316
|
+
assert!(
|
|
317
|
+
socket.exists(),
|
|
318
|
+
"owned endpoint stays while sibling team session remains"
|
|
319
|
+
);
|
|
320
|
+
assert!(
|
|
321
|
+
!transport.kill_server_called(),
|
|
322
|
+
"scoped shutdown must not tear down a non-empty shared owned endpoint"
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
#[test]
|
|
327
|
+
fn repeated_owned_endpoint_shutdowns_leave_no_socket_file_growth() {
|
|
328
|
+
let ws = tmp_shutdown_workspace("owned-loop-no-growth");
|
|
329
|
+
let sockets = (0..20)
|
|
330
|
+
.map(|idx| ws.join(format!("owned-loop-{idx}.sock")))
|
|
331
|
+
.collect::<Vec<_>>();
|
|
332
|
+
let starting = sockets.iter().filter(|path| path.exists()).count();
|
|
333
|
+
for socket in &sockets {
|
|
334
|
+
std::fs::write(socket, b"socket").unwrap();
|
|
335
|
+
crate::state::persist::save_runtime_state(
|
|
336
|
+
&ws,
|
|
337
|
+
&json!({
|
|
338
|
+
"session_name": "team-owned-loop",
|
|
339
|
+
"tmux_endpoint": socket.to_string_lossy(),
|
|
340
|
+
"tmux_socket": socket.to_string_lossy(),
|
|
341
|
+
"tmux_socket_source": "workspace",
|
|
342
|
+
"is_external_leader": false,
|
|
343
|
+
"agents": {}
|
|
344
|
+
}),
|
|
345
|
+
)
|
|
346
|
+
.unwrap();
|
|
347
|
+
let out = crate::cli::lifecycle_port::shutdown_with_transport(
|
|
348
|
+
&ws,
|
|
349
|
+
true,
|
|
350
|
+
None,
|
|
351
|
+
&CleanShutdownTransport::new(),
|
|
352
|
+
)
|
|
353
|
+
.expect("shutdown should complete");
|
|
354
|
+
assert_eq!(out["ok"], json!(true), "shutdown report: {out}");
|
|
355
|
+
}
|
|
356
|
+
let ending = sockets.iter().filter(|path| path.exists()).count();
|
|
357
|
+
assert_eq!(
|
|
358
|
+
ending, starting,
|
|
359
|
+
"owned socket files must not grow across loops"
|
|
360
|
+
);
|
|
145
361
|
}
|
|
146
362
|
|
|
147
363
|
fn tmp_shutdown_workspace(tag: &str) -> PathBuf {
|
|
@@ -241,7 +457,10 @@ impl Transport for CleanShutdownTransport {
|
|
|
241
457
|
_target: &Target,
|
|
242
458
|
range: CaptureRange,
|
|
243
459
|
) -> Result<CapturedText, TransportError> {
|
|
244
|
-
Ok(CapturedText {
|
|
460
|
+
Ok(CapturedText {
|
|
461
|
+
text: String::new(),
|
|
462
|
+
range,
|
|
463
|
+
})
|
|
245
464
|
}
|
|
246
465
|
|
|
247
466
|
fn query(&self, _target: &Target, _field: PaneField) -> Result<Option<String>, TransportError> {
|
|
@@ -337,7 +556,9 @@ fn lsof_cwd_timeout_is_diagnostic_not_shutdown_partial() {
|
|
|
337
556
|
assert_eq!(out["residuals"]["sessions"], json!([]));
|
|
338
557
|
assert_eq!(out["residuals"]["processes"], json!([]));
|
|
339
558
|
|
|
340
|
-
let events = crate::event_log::EventLog::new(&ws)
|
|
559
|
+
let events = crate::event_log::EventLog::new(&ws)
|
|
560
|
+
.tail(0)
|
|
561
|
+
.expect("events");
|
|
341
562
|
let shutdown = events
|
|
342
563
|
.iter()
|
|
343
564
|
.find(|event| {
|
|
@@ -412,6 +633,7 @@ fn shutdown_outcome_late_or_postcheck_gone_is_ok_with_lsof_diagnostic() {
|
|
|
412
633
|
kill_error: false,
|
|
413
634
|
session_residuals: false,
|
|
414
635
|
process_residuals: false,
|
|
636
|
+
owned_file_residuals: false,
|
|
415
637
|
cleanup_truth_degraded: false,
|
|
416
638
|
coordinator_timeout: true,
|
|
417
639
|
coordinator_stop_ok: None,
|
|
@@ -431,6 +653,7 @@ fn shutdown_outcome_coordinator_timeout_still_running_is_timeout() {
|
|
|
431
653
|
kill_error: false,
|
|
432
654
|
session_residuals: false,
|
|
433
655
|
process_residuals: false,
|
|
656
|
+
owned_file_residuals: false,
|
|
434
657
|
cleanup_truth_degraded: false,
|
|
435
658
|
coordinator_timeout: true,
|
|
436
659
|
coordinator_stop_ok: None,
|
|
@@ -450,6 +673,7 @@ fn shutdown_outcome_ps_table_degraded_still_partial_after_coordinator_gone() {
|
|
|
450
673
|
kill_error: false,
|
|
451
674
|
session_residuals: false,
|
|
452
675
|
process_residuals: false,
|
|
676
|
+
owned_file_residuals: false,
|
|
453
677
|
cleanup_truth_degraded: true,
|
|
454
678
|
coordinator_timeout: true,
|
|
455
679
|
coordinator_stop_ok: None,
|