@team-agent/installer 0.5.45 → 0.5.46
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/mod.rs +6 -0
- package/crates/team-agent/src/cli/status_port.rs +70 -2
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +14 -22
- package/crates/team-agent/src/lifecycle/restart/agent.rs +103 -27
- package/crates/team-agent/src/lifecycle/restart/common.rs +136 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +168 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +7 -7
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +3 -0
- package/crates/team-agent/src/lifecycle/types.rs +3 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +10 -0
- package/crates/team-agent/src/provider/session/capture.rs +113 -22
- package/crates/team-agent/src/provider/session_scan/codex.rs +51 -4
- package/crates/team-agent/src/tmux_backend/tests.rs +116 -10
- package/crates/team-agent/src/tmux_backend.rs +5 -16
- package/crates/team-agent/src/transport/tests/wire.rs +6 -0
- package/crates/team-agent/src/transport.rs +8 -2
- package/package.json +4 -4
|
@@ -39,6 +39,36 @@ struct MockCommandRunner {
|
|
|
39
39
|
default: MockResp,
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/// Models the real build-before-destroy overlap: the spawn command creates `%new`, while a
|
|
43
|
+
/// name-based lookup still resolves the same-named old window `%old`.
|
|
44
|
+
struct SameNameSpawnRunner {
|
|
45
|
+
recorded: RecordedArgv,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
impl CommandRunner for SameNameSpawnRunner {
|
|
49
|
+
fn run(&self, argv: &[String]) -> Result<CommandOutput, std::io::Error> {
|
|
50
|
+
self.recorded.lock().unwrap().push(argv.to_vec());
|
|
51
|
+
let output = match argv.get(1).map(String::as_str) {
|
|
52
|
+
Some("new-window") => ok("%new\n"),
|
|
53
|
+
Some("display-message") => ok("%old\n"),
|
|
54
|
+
Some("list-panes") => ok(
|
|
55
|
+
"%old\tteamsess\t0\tdeveloper\t0\t/dev/ttys001\tnode\t1\t/work/dir\t1\t0\t101\n\
|
|
56
|
+
%new\tteamsess\t1\tdeveloper\t0\t/dev/ttys002\tnode\t1\t/work/dir\t1\t0\t202\n",
|
|
57
|
+
),
|
|
58
|
+
_ => fail(64, "unexpected scripted tmux command"),
|
|
59
|
+
};
|
|
60
|
+
Ok(output)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fn run_with_stdin(
|
|
64
|
+
&self,
|
|
65
|
+
argv: &[String],
|
|
66
|
+
_stdin: &str,
|
|
67
|
+
) -> Result<CommandOutput, std::io::Error> {
|
|
68
|
+
self.run(argv)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
42
72
|
impl CommandRunner for MockCommandRunner {
|
|
43
73
|
fn run(&self, argv: &[String]) -> Result<CommandOutput, std::io::Error> {
|
|
44
74
|
self.recorded.lock().unwrap().push(argv.to_vec());
|
|
@@ -366,8 +396,7 @@ fn spawn_first_frames_via_new_session_builder_and_parses_pane_id() {
|
|
|
366
396
|
let (be, rec) = backend_with(
|
|
367
397
|
MockResp::Out(ok("")),
|
|
368
398
|
vec![
|
|
369
|
-
MockResp::Out(ok("")),
|
|
370
|
-
MockResp::Out(ok("%3")),
|
|
399
|
+
MockResp::Out(ok("%3\n")),
|
|
371
400
|
MockResp::Out(ok(pane_inventory)),
|
|
372
401
|
],
|
|
373
402
|
);
|
|
@@ -383,7 +412,8 @@ fn spawn_first_frames_via_new_session_builder_and_parses_pane_id() {
|
|
|
383
412
|
&env,
|
|
384
413
|
)
|
|
385
414
|
.expect("spawn_first");
|
|
386
|
-
let
|
|
415
|
+
let calls = rec.lock().unwrap().clone();
|
|
416
|
+
let argv = calls[0].clone();
|
|
387
417
|
let cmd = argv.last().expect("the sh -lc command string").clone();
|
|
388
418
|
assert_eq!(
|
|
389
419
|
argv,
|
|
@@ -400,6 +430,12 @@ fn spawn_first_frames_via_new_session_builder_and_parses_pane_id() {
|
|
|
400
430
|
"SpawnResult.pane_id must parse from the tmux output"
|
|
401
431
|
);
|
|
402
432
|
assert_eq!(result.child_pid, Some(123));
|
|
433
|
+
assert!(
|
|
434
|
+
calls
|
|
435
|
+
.iter()
|
|
436
|
+
.all(|call| call.get(1).is_none_or(|arg| arg != "display-message")),
|
|
437
|
+
"spawn_first identity must come from new-session stdout, never a display-message lookup; calls={calls:?}"
|
|
438
|
+
);
|
|
403
439
|
}
|
|
404
440
|
|
|
405
441
|
#[test]
|
|
@@ -408,8 +444,7 @@ fn spawn_into_frames_via_new_window_builder() {
|
|
|
408
444
|
let (be, rec) = backend_with(
|
|
409
445
|
MockResp::Out(ok("")),
|
|
410
446
|
vec![
|
|
411
|
-
MockResp::Out(ok("")),
|
|
412
|
-
MockResp::Out(ok("%4")),
|
|
447
|
+
MockResp::Out(ok("%4\n")),
|
|
413
448
|
MockResp::Out(ok(pane_inventory)),
|
|
414
449
|
],
|
|
415
450
|
);
|
|
@@ -424,7 +459,8 @@ fn spawn_into_frames_via_new_window_builder() {
|
|
|
424
459
|
&BTreeMap::new(),
|
|
425
460
|
)
|
|
426
461
|
.expect("spawn_into");
|
|
427
|
-
let
|
|
462
|
+
let calls = rec.lock().unwrap().clone();
|
|
463
|
+
let argv = calls[0].clone();
|
|
428
464
|
let cmd = argv.last().expect("the sh -lc command string").clone();
|
|
429
465
|
assert_eq!(
|
|
430
466
|
argv,
|
|
@@ -432,16 +468,58 @@ fn spawn_into_frames_via_new_window_builder() {
|
|
|
432
468
|
"spawn_into must frame via tmux_spawn_argv first=false (new-window -t <s> -n <w> sh -lc <cmd>)"
|
|
433
469
|
);
|
|
434
470
|
assert_eq!(result.pane_id.as_str(), "%4");
|
|
471
|
+
assert!(
|
|
472
|
+
calls
|
|
473
|
+
.iter()
|
|
474
|
+
.all(|call| call.get(1).is_none_or(|arg| arg != "display-message")),
|
|
475
|
+
"spawn_into identity must come from new-window stdout, never a display-message lookup; calls={calls:?}"
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
#[test]
|
|
480
|
+
fn spawn_into_same_named_replacement_returns_identity_created_by_spawn_command() {
|
|
481
|
+
let recorded = Arc::new(Mutex::new(Vec::new()));
|
|
482
|
+
let be = TmuxBackend::with_runner(Box::new(SameNameSpawnRunner {
|
|
483
|
+
recorded: Arc::clone(&recorded),
|
|
484
|
+
}));
|
|
485
|
+
|
|
486
|
+
let result = be
|
|
487
|
+
.spawn_into(
|
|
488
|
+
&SessionName::new("teamsess"),
|
|
489
|
+
&WindowName::new("developer"),
|
|
490
|
+
&svec(&["provider-bin"]),
|
|
491
|
+
Path::new("/work/dir"),
|
|
492
|
+
&BTreeMap::new(),
|
|
493
|
+
)
|
|
494
|
+
.expect("same-name replacement spawn");
|
|
495
|
+
let calls = recorded.lock().unwrap().clone();
|
|
496
|
+
let name_based_identity_lookup = calls.iter().any(|call| {
|
|
497
|
+
call.get(1).is_some_and(|arg| arg == "display-message")
|
|
498
|
+
&& call
|
|
499
|
+
.iter()
|
|
500
|
+
.position(|arg| arg == "-t")
|
|
501
|
+
.and_then(|index| call.get(index + 1))
|
|
502
|
+
.is_some_and(|target| target == "teamsess:developer")
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
assert_eq!(
|
|
506
|
+
result.pane_id.as_str(),
|
|
507
|
+
"%new",
|
|
508
|
+
"spawn_into must return the pane identity atomically emitted by this new-window command, not the same-named old window; calls={calls:?}"
|
|
509
|
+
);
|
|
510
|
+
assert!(
|
|
511
|
+
!name_based_identity_lookup,
|
|
512
|
+
"spawn identity must not be re-derived through ambiguous session:window display-message; calls={calls:?}"
|
|
513
|
+
);
|
|
435
514
|
}
|
|
436
515
|
|
|
437
516
|
#[test]
|
|
438
|
-
fn
|
|
517
|
+
fn spawn_with_command_refuses_spawn_pane_owned_by_other_window() {
|
|
439
518
|
let pane_inventory = "%5\tteamsess\t1\tw2\t0\t/dev/ttys005\tnode\t1\t/work/dir\t1\t0\t125\n";
|
|
440
519
|
let (be, _rec) = backend_with(
|
|
441
520
|
MockResp::Out(ok("")),
|
|
442
521
|
vec![
|
|
443
|
-
MockResp::Out(ok("")),
|
|
444
|
-
MockResp::Out(ok("%5")),
|
|
522
|
+
MockResp::Out(ok("%5\n")),
|
|
445
523
|
MockResp::Out(ok(pane_inventory)),
|
|
446
524
|
],
|
|
447
525
|
);
|
|
@@ -453,7 +531,7 @@ fn spawn_with_command_refuses_display_message_pane_owned_by_other_window() {
|
|
|
453
531
|
Path::new("/work/dir"),
|
|
454
532
|
&BTreeMap::new(),
|
|
455
533
|
)
|
|
456
|
-
.expect_err("
|
|
534
|
+
.expect_err("spawn stdout pane owned by w2 must fail closed");
|
|
457
535
|
let msg = err.to_string();
|
|
458
536
|
assert!(
|
|
459
537
|
msg.contains("requested=teamsess:w1")
|
|
@@ -463,6 +541,34 @@ fn spawn_with_command_refuses_display_message_pane_owned_by_other_window() {
|
|
|
463
541
|
);
|
|
464
542
|
}
|
|
465
543
|
|
|
544
|
+
#[test]
|
|
545
|
+
fn spawn_with_command_empty_stdout_fails_closed_without_identity_fallback() {
|
|
546
|
+
let (be, rec) = backend_with(MockResp::Out(ok("")), vec![MockResp::Out(ok(""))]);
|
|
547
|
+
let err = be
|
|
548
|
+
.spawn_into(
|
|
549
|
+
&SessionName::new("teamsess"),
|
|
550
|
+
&WindowName::new("developer"),
|
|
551
|
+
&svec(&["provider-bin"]),
|
|
552
|
+
Path::new("/work/dir"),
|
|
553
|
+
&BTreeMap::new(),
|
|
554
|
+
)
|
|
555
|
+
.expect_err("empty new-window stdout must fail closed");
|
|
556
|
+
let msg = err.to_string().to_ascii_lowercase();
|
|
557
|
+
let calls = rec.lock().unwrap().clone();
|
|
558
|
+
let used_identity_fallback = calls
|
|
559
|
+
.iter()
|
|
560
|
+
.any(|call| call.get(1).is_some_and(|arg| arg == "display-message"));
|
|
561
|
+
|
|
562
|
+
assert!(
|
|
563
|
+
msg.contains("spawn") && (msg.contains("empty") || msg.contains("no pane id")),
|
|
564
|
+
"empty spawn stdout error must name the missing spawn identity; error={err}; calls={calls:?}"
|
|
565
|
+
);
|
|
566
|
+
assert!(
|
|
567
|
+
!used_identity_fallback,
|
|
568
|
+
"empty spawn stdout must not fall back to ambiguous display-message identity lookup; calls={calls:?}"
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
466
572
|
#[test]
|
|
467
573
|
fn spawn_split_selects_even_horizontal_not_tiled() {
|
|
468
574
|
let (be, rec) = backend_with(
|
|
@@ -765,25 +765,14 @@ impl TmuxBackend {
|
|
|
765
765
|
first: bool,
|
|
766
766
|
) -> Result<SpawnResult, TransportError> {
|
|
767
767
|
let spawn_argv = tmux_spawn_argv(session, window, command, first);
|
|
768
|
-
self.run_spawn(&spawn_argv)?;
|
|
769
|
-
let pane_argv = vec![
|
|
770
|
-
"tmux".to_string(),
|
|
771
|
-
"display-message".to_string(),
|
|
772
|
-
"-p".to_string(),
|
|
773
|
-
"-t".to_string(),
|
|
774
|
-
format!("{}:{}", session.as_str(), window.as_str()),
|
|
775
|
-
"#{pane_id}".to_string(),
|
|
776
|
-
];
|
|
777
|
-
let output = self.run_spawn(&pane_argv)?;
|
|
768
|
+
let output = self.run_spawn(&spawn_argv)?;
|
|
778
769
|
let pane = output.stdout.trim();
|
|
779
|
-
// T3-5 (harvest §1): never fabricate a `%0` pane id on an empty reply — a fake
|
|
780
|
-
// pane id mis-addresses every later inject/capture/kill. Surface the miss.
|
|
781
770
|
if pane.is_empty() {
|
|
782
771
|
return Err(TransportError::Subprocess {
|
|
783
|
-
argv:
|
|
772
|
+
argv: spawn_argv,
|
|
784
773
|
code: output.code,
|
|
785
774
|
stderr: format!(
|
|
786
|
-
"tmux
|
|
775
|
+
"tmux spawn returned no pane id for {}:{}",
|
|
787
776
|
session.as_str(),
|
|
788
777
|
window.as_str()
|
|
789
778
|
),
|
|
@@ -822,10 +811,10 @@ impl TmuxBackend {
|
|
|
822
811
|
})
|
|
823
812
|
.unwrap_or_else(|| "<missing-from-list-targets>".to_string());
|
|
824
813
|
Err(TransportError::Subprocess {
|
|
825
|
-
argv:
|
|
814
|
+
argv: spawn_argv,
|
|
826
815
|
code: output.code,
|
|
827
816
|
stderr: format!(
|
|
828
|
-
"tmux spawn pane
|
|
817
|
+
"tmux spawn pane identity mismatch: requested={}:{} observed_pane={} observed={}",
|
|
829
818
|
session.as_str(),
|
|
830
819
|
window.as_str(),
|
|
831
820
|
pane_id.as_str(),
|
|
@@ -238,6 +238,9 @@ fn spawn_first_returns_stable_addressable_target_then_reachable() {
|
|
|
238
238
|
"tmux",
|
|
239
239
|
"new-session",
|
|
240
240
|
"-d",
|
|
241
|
+
"-P",
|
|
242
|
+
"-F",
|
|
243
|
+
"#{pane_id}",
|
|
241
244
|
"-s",
|
|
242
245
|
"team-sess",
|
|
243
246
|
"-n",
|
|
@@ -287,6 +290,9 @@ fn spawn_into_then_list_targets_enumerates_it() {
|
|
|
287
290
|
"tmux",
|
|
288
291
|
"new-window",
|
|
289
292
|
"-d",
|
|
293
|
+
"-P",
|
|
294
|
+
"-F",
|
|
295
|
+
"#{pane_id}",
|
|
290
296
|
"-t",
|
|
291
297
|
"team-sess",
|
|
292
298
|
"-n",
|
|
@@ -918,8 +918,8 @@ pub fn tmux_query_argv(pane: &PaneId, field: PaneField) -> Vec<String> {
|
|
|
918
918
|
|
|
919
919
|
/// spawn argv:首个 session 用 new-session,后续 worker 用 new-window。
|
|
920
920
|
/// golden(terminal.py:44-45 / runtime.py:1019-1020):
|
|
921
|
-
/// first → `new-session -d -s <s> -n <w> sh -lc <cmd>`
|
|
922
|
-
/// into → `new-window -t <s> -n <w> sh -lc <cmd>`
|
|
921
|
+
/// first → `new-session -d -P -F #{pane_id} -s <s> -n <w> sh -lc <cmd>`
|
|
922
|
+
/// into → `new-window -d -P -F #{pane_id} -t <s> -n <w> sh -lc <cmd>`
|
|
923
923
|
/// `argv` 被组装成单条 `sh -lc` 命令字符串(provider 启动行)。
|
|
924
924
|
pub fn tmux_spawn_argv(
|
|
925
925
|
session: &SessionName,
|
|
@@ -932,6 +932,9 @@ pub fn tmux_spawn_argv(
|
|
|
932
932
|
"tmux".to_string(),
|
|
933
933
|
"new-session".to_string(),
|
|
934
934
|
"-d".to_string(),
|
|
935
|
+
"-P".to_string(),
|
|
936
|
+
"-F".to_string(),
|
|
937
|
+
"#{pane_id}".to_string(),
|
|
935
938
|
"-s".to_string(),
|
|
936
939
|
session.as_str().to_string(),
|
|
937
940
|
"-n".to_string(),
|
|
@@ -952,6 +955,9 @@ pub fn tmux_spawn_argv(
|
|
|
952
955
|
"tmux".to_string(),
|
|
953
956
|
"new-window".to_string(),
|
|
954
957
|
"-d".to_string(),
|
|
958
|
+
"-P".to_string(),
|
|
959
|
+
"-F".to_string(),
|
|
960
|
+
"#{pane_id}".to_string(),
|
|
955
961
|
"-t".to_string(),
|
|
956
962
|
session.as_str().to_string(),
|
|
957
963
|
"-n".to_string(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.46",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.46",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.46",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.46"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|