@team-agent/installer 0.5.110 → 0.5.111
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/adapters.rs +234 -73
- package/crates/team-agent/src/cli/emit.rs +487 -201
- package/crates/team-agent/src/cli/leader.rs +4 -9
- package/crates/team-agent/src/cli/leaders.rs +90 -29
- package/crates/team-agent/src/cli/mod.rs +346 -160
- package/crates/team-agent/src/cli/models.rs +80 -24
- package/crates/team-agent/src/cli/named_address.rs +114 -25
- package/crates/team-agent/src/cli/route.rs +10 -5
- package/crates/team-agent/src/cli/send/presentation.rs +60 -41
- package/crates/team-agent/src/cli/send/resolve.rs +25 -18
- package/crates/team-agent/src/cli/send.rs +4 -4
- package/crates/team-agent/src/cli/spec.rs +56 -109
- package/crates/team-agent/src/cli/tests/base.rs +189 -82
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -2
- package/crates/team-agent/src/cli/tests/named_address.rs +19 -22
- package/crates/team-agent/src/cli/tests/status_send.rs +15 -10
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +2 -2
- package/crates/team-agent/src/cli/types.rs +22 -13
- package/crates/team-agent/src/leader/registry.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +4 -9
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -1
- package/package.json +4 -4
|
@@ -48,10 +48,13 @@ use thiserror::Error;
|
|
|
48
48
|
use crate::messaging::{self, MessageTarget, SendOptions, TrustedSender};
|
|
49
49
|
use crate::model::ids::{TaskId, TeamKey};
|
|
50
50
|
|
|
51
|
-
pub(crate) const COMMS_BOUNDARY_TEXT: &str =
|
|
52
|
-
|
|
53
|
-
pub(crate) const
|
|
54
|
-
pub(crate) const
|
|
51
|
+
pub(crate) const COMMS_BOUNDARY_TEXT: &str =
|
|
52
|
+
"这里只检查终端连接是否一致,不实际发送消息,也不证明队友已回复。";
|
|
53
|
+
pub(crate) const QUICK_START_REMINDER: &str = "下一步:向队友派发任务;用 team-agent status 看状态,用 team-agent inbox leader -n 3 看回复,不读取队友终端内容。";
|
|
54
|
+
pub(crate) const SEND_REMINDER: &str =
|
|
55
|
+
"等队友真正回复,用 team-agent inbox leader -n 3 查看;不要读取队友终端内容。";
|
|
56
|
+
pub(crate) const STATUS_REMINDER: &str =
|
|
57
|
+
"下一步:用 team-agent inbox leader -n 3 查看回复;状态不是任务完成证明,不读取队友终端内容。";
|
|
55
58
|
|
|
56
59
|
pub mod adapters;
|
|
57
60
|
pub mod attach_app_server_leader;
|
|
@@ -137,12 +140,7 @@ pub mod lifecycle_port {
|
|
|
137
140
|
backend: Option<&str>,
|
|
138
141
|
) -> Result<Value, CliError> {
|
|
139
142
|
match crate::lifecycle::quick_start_in_workspace_with_backend(
|
|
140
|
-
workspace,
|
|
141
|
-
agents_dir,
|
|
142
|
-
name,
|
|
143
|
-
yes,
|
|
144
|
-
team_id,
|
|
145
|
-
backend,
|
|
143
|
+
workspace, agents_dir, name, yes, team_id, backend,
|
|
146
144
|
) {
|
|
147
145
|
Ok(report) => Ok(quick_start_value(report)),
|
|
148
146
|
Err(e) => Ok(error_value(e)),
|
|
@@ -677,7 +675,9 @@ pub mod lifecycle_port {
|
|
|
677
675
|
"session_marker+state_session+pane_path",
|
|
678
676
|
"positive workspace/team ownership",
|
|
679
677
|
) {
|
|
680
|
-
result
|
|
678
|
+
result
|
|
679
|
+
.error
|
|
680
|
+
.get_or_insert_with(|| format!("pre-kill audit failed: {error}"));
|
|
681
681
|
} else if let Err(error) = transport.kill_session(&session) {
|
|
682
682
|
if !tmux_absent_error(&error.to_string()) {
|
|
683
683
|
result.error.get_or_insert_with(|| error.to_string());
|
|
@@ -691,7 +691,9 @@ pub mod lifecycle_port {
|
|
|
691
691
|
}
|
|
692
692
|
SessionOwnership::Unknown => {
|
|
693
693
|
push_unique_session(&mut result.spared_sessions, session);
|
|
694
|
-
result.error.get_or_insert_with(||
|
|
694
|
+
result.error.get_or_insert_with(|| {
|
|
695
|
+
"session ownership is unknown; destructive cleanup refused".to_string()
|
|
696
|
+
});
|
|
695
697
|
}
|
|
696
698
|
SessionOwnership::Gone => {}
|
|
697
699
|
}
|
|
@@ -718,7 +720,11 @@ pub mod lifecycle_port {
|
|
|
718
720
|
state
|
|
719
721
|
.get("teams")
|
|
720
722
|
.and_then(Value::as_object)
|
|
721
|
-
.and_then(|teams|
|
|
723
|
+
.and_then(|teams| {
|
|
724
|
+
teams
|
|
725
|
+
.values()
|
|
726
|
+
.find_map(|team| state_for_session(team, session))
|
|
727
|
+
})
|
|
722
728
|
}
|
|
723
729
|
|
|
724
730
|
fn session_ownership(
|
|
@@ -759,27 +765,33 @@ pub mod lifecycle_port {
|
|
|
759
765
|
if owner.team != expected_team {
|
|
760
766
|
return SessionOwnership::Foreign;
|
|
761
767
|
}
|
|
762
|
-
let Some(expected_generation) =
|
|
763
|
-
.filter(|generation| !generation.is_empty())
|
|
768
|
+
let Some(expected_generation) =
|
|
769
|
+
state_generation(identity_state).filter(|generation| !generation.is_empty())
|
|
770
|
+
else {
|
|
764
771
|
return SessionOwnership::Unknown;
|
|
765
772
|
};
|
|
766
773
|
if owner.generation != expected_generation {
|
|
767
774
|
return SessionOwnership::Foreign;
|
|
768
775
|
}
|
|
769
776
|
for target in session_targets {
|
|
770
|
-
if target
|
|
771
|
-
|
|
772
|
-
|
|
777
|
+
if target
|
|
778
|
+
.current_path
|
|
779
|
+
.as_ref()
|
|
780
|
+
.is_some_and(|path| path_is_under(path.as_path(), workspace))
|
|
781
|
+
{
|
|
773
782
|
continue;
|
|
774
783
|
}
|
|
775
784
|
// An exited keepalive has no cwd or live process. Exempt only the
|
|
776
785
|
// internal anchor, with native positive dead-pane proof, AFTER the
|
|
777
786
|
// session workspace/team/generation ownership gates above.
|
|
778
|
-
let dead_anchor = target
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
787
|
+
let dead_anchor = target
|
|
788
|
+
.current_path
|
|
789
|
+
.as_ref()
|
|
790
|
+
.is_none_or(|path| path.as_os_str().is_empty())
|
|
791
|
+
&& target.window_name.as_ref().is_some_and(|window| {
|
|
792
|
+
window.as_str() == crate::tmux_backend::KEEPALIVE_WINDOW_NAME
|
|
793
|
+
})
|
|
794
|
+
&& matches!(transport.pane_is_dead(&target.pane_id), Ok(Some(true)));
|
|
783
795
|
if !dead_anchor {
|
|
784
796
|
return SessionOwnership::Unknown;
|
|
785
797
|
}
|
|
@@ -795,7 +807,12 @@ pub mod lifecycle_port {
|
|
|
795
807
|
use crate::transport::test_support::OfflineTransport;
|
|
796
808
|
use crate::transport::{PaneId, PaneInfo, SessionName, Transport, WindowName};
|
|
797
809
|
|
|
798
|
-
fn ownership_pane(
|
|
810
|
+
fn ownership_pane(
|
|
811
|
+
session: &SessionName,
|
|
812
|
+
pane: &str,
|
|
813
|
+
window: &str,
|
|
814
|
+
cwd: Option<PathBuf>,
|
|
815
|
+
) -> PaneInfo {
|
|
799
816
|
PaneInfo {
|
|
800
817
|
pane_id: PaneId::new(pane),
|
|
801
818
|
session: session.clone(),
|
|
@@ -817,23 +834,81 @@ pub mod lifecycle_port {
|
|
|
817
834
|
let session = SessionName::new("owned");
|
|
818
835
|
let state = json!({"team_key": "owned", "generation": "g"});
|
|
819
836
|
for (window, cwd, dead, team, generation, expected) in [
|
|
820
|
-
(
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
837
|
+
(
|
|
838
|
+
crate::tmux_backend::KEEPALIVE_WINDOW_NAME,
|
|
839
|
+
None,
|
|
840
|
+
Some(true),
|
|
841
|
+
"owned",
|
|
842
|
+
"g",
|
|
843
|
+
SessionOwnership::Owned,
|
|
844
|
+
),
|
|
845
|
+
(
|
|
846
|
+
crate::tmux_backend::KEEPALIVE_WINDOW_NAME,
|
|
847
|
+
None,
|
|
848
|
+
Some(false),
|
|
849
|
+
"owned",
|
|
850
|
+
"g",
|
|
851
|
+
SessionOwnership::Unknown,
|
|
852
|
+
),
|
|
853
|
+
(
|
|
854
|
+
crate::tmux_backend::KEEPALIVE_WINDOW_NAME,
|
|
855
|
+
None,
|
|
856
|
+
None,
|
|
857
|
+
"owned",
|
|
858
|
+
"g",
|
|
859
|
+
SessionOwnership::Unknown,
|
|
860
|
+
),
|
|
861
|
+
(
|
|
862
|
+
"foreign",
|
|
863
|
+
None,
|
|
864
|
+
Some(true),
|
|
865
|
+
"owned",
|
|
866
|
+
"g",
|
|
867
|
+
SessionOwnership::Unknown,
|
|
868
|
+
),
|
|
869
|
+
(
|
|
870
|
+
crate::tmux_backend::KEEPALIVE_WINDOW_NAME,
|
|
871
|
+
Some(PathBuf::from("/foreign-workspace")),
|
|
872
|
+
Some(true),
|
|
873
|
+
"owned",
|
|
874
|
+
"g",
|
|
875
|
+
SessionOwnership::Unknown,
|
|
876
|
+
),
|
|
877
|
+
(
|
|
878
|
+
crate::tmux_backend::KEEPALIVE_WINDOW_NAME,
|
|
879
|
+
None,
|
|
880
|
+
Some(true),
|
|
881
|
+
"other-team",
|
|
882
|
+
"g",
|
|
883
|
+
SessionOwnership::Foreign,
|
|
884
|
+
),
|
|
885
|
+
(
|
|
886
|
+
crate::tmux_backend::KEEPALIVE_WINDOW_NAME,
|
|
887
|
+
None,
|
|
888
|
+
Some(true),
|
|
889
|
+
"owned",
|
|
890
|
+
"other-generation",
|
|
891
|
+
SessionOwnership::Foreign,
|
|
892
|
+
),
|
|
827
893
|
] {
|
|
828
|
-
let mut transport =
|
|
829
|
-
|
|
894
|
+
let mut transport =
|
|
895
|
+
OfflineTransport::new().with_session_owner(&workspace, team, generation);
|
|
896
|
+
if let Some(dead) = dead {
|
|
897
|
+
transport = transport.with_pane_dead("%1", dead);
|
|
898
|
+
}
|
|
830
899
|
let targets = vec![
|
|
831
900
|
ownership_pane(&session, "%2", "assistant", Some(workspace.clone())),
|
|
832
901
|
ownership_pane(&session, "%1", window, cwd),
|
|
833
902
|
];
|
|
834
|
-
assert_eq!(
|
|
903
|
+
assert_eq!(
|
|
904
|
+
session_ownership(&workspace, &state, &transport, &session, &targets),
|
|
905
|
+
expected
|
|
906
|
+
);
|
|
835
907
|
if expected == SessionOwnership::Foreign {
|
|
836
|
-
assert!(
|
|
908
|
+
assert!(
|
|
909
|
+
!transport.calls().contains(&"pane_is_dead"),
|
|
910
|
+
"owner gates must precede exemption"
|
|
911
|
+
);
|
|
837
912
|
}
|
|
838
913
|
assert!(!transport.calls().contains(&"kill_session"));
|
|
839
914
|
}
|
|
@@ -844,7 +919,9 @@ pub mod lifecycle_port {
|
|
|
844
919
|
#[ignore = "isolated native tmux: explicit Grok execution, no provider subscription"]
|
|
845
920
|
fn dead_anchor_scoped_shutdown_reaps_worker_wrapper_and_spares_other_team() {
|
|
846
921
|
let root = std::env::temp_dir().join(format!(
|
|
847
|
-
"ta-r2-{}-{}",
|
|
922
|
+
"ta-r2-{}-{}",
|
|
923
|
+
std::process::id(),
|
|
924
|
+
chrono::Utc::now().timestamp_nanos_opt().unwrap(),
|
|
848
925
|
));
|
|
849
926
|
std::fs::create_dir(&root).unwrap();
|
|
850
927
|
let root = root.canonicalize().unwrap();
|
|
@@ -854,53 +931,109 @@ pub mod lifecycle_port {
|
|
|
854
931
|
std::fs::create_dir(&foreign_workspace).unwrap();
|
|
855
932
|
let endpoint = root.join("sock");
|
|
856
933
|
let transport = crate::tmux_backend::TmuxBackend::with_runner_for_tmux_endpoint(
|
|
857
|
-
Box::new(crate::tmux_backend::RealCommandRunner),
|
|
934
|
+
Box::new(crate::tmux_backend::RealCommandRunner),
|
|
935
|
+
endpoint.to_str().unwrap(),
|
|
858
936
|
);
|
|
859
937
|
let session = SessionName::new("round2-owned");
|
|
860
938
|
let foreign = SessionName::new("round2-foreign");
|
|
861
|
-
struct Cleanup<'a>(
|
|
939
|
+
struct Cleanup<'a>(
|
|
940
|
+
&'a crate::tmux_backend::TmuxBackend,
|
|
941
|
+
[&'a SessionName; 2],
|
|
942
|
+
&'a Path,
|
|
943
|
+
);
|
|
862
944
|
impl Drop for Cleanup<'_> {
|
|
863
945
|
fn drop(&mut self) {
|
|
864
|
-
for session in self.1 {
|
|
946
|
+
for session in self.1 {
|
|
947
|
+
let _ = self.0.kill_session(session);
|
|
948
|
+
}
|
|
865
949
|
let _ = std::fs::remove_dir_all(self.2);
|
|
866
950
|
}
|
|
867
951
|
}
|
|
868
952
|
let _cleanup = Cleanup(&transport, [&session, &foreign], &root);
|
|
869
953
|
let env = BTreeMap::new();
|
|
870
954
|
let argv = vec!["sh".to_string(), "-c".to_string(), "exit 0".to_string()];
|
|
871
|
-
let first = transport
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
955
|
+
let first = transport
|
|
956
|
+
.spawn_first_with_worker_shell_wrapper(
|
|
957
|
+
&session,
|
|
958
|
+
&WindowName::new("assistant"),
|
|
959
|
+
&argv,
|
|
960
|
+
&workspace,
|
|
961
|
+
&env,
|
|
962
|
+
&[],
|
|
963
|
+
"pi",
|
|
964
|
+
)
|
|
965
|
+
.unwrap();
|
|
966
|
+
transport
|
|
967
|
+
.set_session_owner_with_generation(&session, &workspace, "round2-owned", "g")
|
|
968
|
+
.unwrap();
|
|
969
|
+
transport
|
|
970
|
+
.preserve_session_before_stop(
|
|
971
|
+
&session,
|
|
972
|
+
&[crate::transport::Target::Pane(first.pane_id.clone())],
|
|
973
|
+
)
|
|
974
|
+
.unwrap();
|
|
876
975
|
transport.kill_pane(&first.pane_id).unwrap();
|
|
877
|
-
let worker = transport
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
976
|
+
let worker = transport
|
|
977
|
+
.spawn_into_with_worker_shell_wrapper(
|
|
978
|
+
&session,
|
|
979
|
+
&WindowName::new("assistant"),
|
|
980
|
+
&argv,
|
|
981
|
+
&workspace,
|
|
982
|
+
&env,
|
|
983
|
+
&[],
|
|
984
|
+
"pi",
|
|
985
|
+
)
|
|
986
|
+
.unwrap();
|
|
987
|
+
let foreign_worker = transport
|
|
988
|
+
.spawn_first(
|
|
989
|
+
&foreign,
|
|
990
|
+
&WindowName::new("other"),
|
|
991
|
+
&["sleep".to_string(), "60".to_string()],
|
|
992
|
+
&foreign_workspace,
|
|
993
|
+
&env,
|
|
994
|
+
)
|
|
995
|
+
.unwrap();
|
|
996
|
+
transport
|
|
997
|
+
.set_session_owner_with_generation(
|
|
998
|
+
&foreign,
|
|
999
|
+
&foreign_workspace,
|
|
1000
|
+
"round2-foreign",
|
|
1001
|
+
"foreign-g",
|
|
1002
|
+
)
|
|
1003
|
+
.unwrap();
|
|
885
1004
|
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
|
|
886
|
-
let marker = format!(
|
|
1005
|
+
let marker = format!(
|
|
1006
|
+
"{} 0",
|
|
1007
|
+
crate::tmux_backend::worker_provider_exit_marker("pi")
|
|
1008
|
+
);
|
|
887
1009
|
loop {
|
|
888
1010
|
// This controlled fixture prints nothing: the worker wrapper
|
|
889
1011
|
// and its inert tail supply these two lines. provider_exit_status
|
|
890
1012
|
// is a leader-only receipt, not a worker-wrapper observation.
|
|
891
|
-
let captured = transport
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1013
|
+
let captured = transport
|
|
1014
|
+
.capture(
|
|
1015
|
+
&crate::transport::Target::Pane(worker.pane_id.clone()),
|
|
1016
|
+
crate::transport::CaptureRange::Full,
|
|
1017
|
+
)
|
|
1018
|
+
.unwrap();
|
|
895
1019
|
if captured.text.contains(&marker)
|
|
896
|
-
&& captured
|
|
1020
|
+
&& captured
|
|
1021
|
+
.text
|
|
1022
|
+
.contains("Provider exited; this pane no longer accepts input.")
|
|
897
1023
|
{
|
|
898
1024
|
break;
|
|
899
1025
|
}
|
|
900
|
-
assert!(
|
|
1026
|
+
assert!(
|
|
1027
|
+
std::time::Instant::now() < deadline,
|
|
1028
|
+
"worker must reach its inert exit wrapper: {}",
|
|
1029
|
+
captured.text
|
|
1030
|
+
);
|
|
901
1031
|
std::thread::sleep(std::time::Duration::from_millis(20));
|
|
902
1032
|
}
|
|
903
|
-
assert_eq!(
|
|
1033
|
+
assert_eq!(
|
|
1034
|
+
transport.pane_is_dead(&worker.pane_id).unwrap(),
|
|
1035
|
+
Some(false)
|
|
1036
|
+
);
|
|
904
1037
|
let worker_pid = worker.child_pid.unwrap();
|
|
905
1038
|
assert!(crate::platform::process::pid_is_alive(worker_pid));
|
|
906
1039
|
let state = json!({
|
|
@@ -914,13 +1047,21 @@ pub mod lifecycle_port {
|
|
|
914
1047
|
}}
|
|
915
1048
|
});
|
|
916
1049
|
crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
|
|
917
|
-
let result =
|
|
1050
|
+
let result =
|
|
1051
|
+
shutdown_with_transport_and_state(&workspace, true, None, &transport, Some(state))
|
|
1052
|
+
.unwrap();
|
|
918
1053
|
assert_eq!(result["ok"], true, "{result}");
|
|
919
|
-
assert_eq!(
|
|
1054
|
+
assert_eq!(
|
|
1055
|
+
result["residuals"],
|
|
1056
|
+
json!({"sessions": [], "processes": [], "owned_files": []}),
|
|
1057
|
+
"{result}"
|
|
1058
|
+
);
|
|
920
1059
|
assert!(!transport.has_session(&session).unwrap());
|
|
921
1060
|
assert!(!crate::platform::process::pid_is_alive(worker_pid));
|
|
922
1061
|
assert!(transport.has_session(&foreign).unwrap());
|
|
923
|
-
assert!(crate::platform::process::pid_is_alive(
|
|
1062
|
+
assert!(crate::platform::process::pid_is_alive(
|
|
1063
|
+
foreign_worker.child_pid.unwrap()
|
|
1064
|
+
));
|
|
924
1065
|
}
|
|
925
1066
|
|
|
926
1067
|
#[test]
|
|
@@ -931,7 +1072,13 @@ pub mod lifecycle_port {
|
|
|
931
1072
|
});
|
|
932
1073
|
let table = [(10, 1), (20, 10), (30, 10), (31, 30)]
|
|
933
1074
|
.into_iter()
|
|
934
|
-
.map(|(pid, ppid)| ProcessInfo {
|
|
1075
|
+
.map(|(pid, ppid)| ProcessInfo {
|
|
1076
|
+
pid,
|
|
1077
|
+
ppid,
|
|
1078
|
+
pgid: None,
|
|
1079
|
+
session: None,
|
|
1080
|
+
command: String::new(),
|
|
1081
|
+
})
|
|
935
1082
|
.collect::<Vec<_>>();
|
|
936
1083
|
for (endpoint, pane_pid, should_protect) in [
|
|
937
1084
|
("/tmp/s4-worker-only.sock", 20, false),
|
|
@@ -955,8 +1102,16 @@ pub mod lifecycle_port {
|
|
|
955
1102
|
let mut protected = ShutdownProtection::default();
|
|
956
1103
|
extend_protection_with_leader_panes(&mut protected, &transport, &state, &table);
|
|
957
1104
|
assert_eq!(protected.pids.contains(&pane_pid), should_protect);
|
|
958
|
-
assert_eq!(
|
|
959
|
-
|
|
1105
|
+
assert_eq!(
|
|
1106
|
+
protected.pids.contains(&31),
|
|
1107
|
+
should_protect,
|
|
1108
|
+
"leader descendants"
|
|
1109
|
+
);
|
|
1110
|
+
assert_eq!(
|
|
1111
|
+
protected.pids.contains(&10),
|
|
1112
|
+
should_protect,
|
|
1113
|
+
"leader's server"
|
|
1114
|
+
);
|
|
960
1115
|
}
|
|
961
1116
|
}
|
|
962
1117
|
|
|
@@ -1211,15 +1366,14 @@ pub mod lifecycle_port {
|
|
|
1211
1366
|
// External and managed leaders both pass the same positive ownership
|
|
1212
1367
|
// gate; topology only decides whether a verified session is reduced
|
|
1213
1368
|
// to worker panes or removed as a whole.
|
|
1214
|
-
let leader_anchor_ids =
|
|
1215
|
-
&state,
|
|
1216
|
-
transport.tmux_endpoint().as_deref(),
|
|
1217
|
-
);
|
|
1369
|
+
let leader_anchor_ids =
|
|
1370
|
+
collect_state_leader_anchor_pane_ids(&state, transport.tmux_endpoint().as_deref());
|
|
1218
1371
|
let (live_targets_now, live_targets_known) = match transport.list_targets() {
|
|
1219
1372
|
Ok(targets) => (targets, true),
|
|
1220
1373
|
Err(error) => {
|
|
1221
1374
|
push_unique_session(&mut spared_sessions, session.clone());
|
|
1222
|
-
kill_error
|
|
1375
|
+
kill_error
|
|
1376
|
+
.get_or_insert_with(|| format!("session ownership probe failed: {error}"));
|
|
1223
1377
|
(Vec::new(), false)
|
|
1224
1378
|
}
|
|
1225
1379
|
};
|
|
@@ -1237,7 +1391,9 @@ pub mod lifecycle_port {
|
|
|
1237
1391
|
if ownership != SessionOwnership::Owned {
|
|
1238
1392
|
push_unique_session(&mut spared_sessions, session.clone());
|
|
1239
1393
|
if ownership == SessionOwnership::Unknown {
|
|
1240
|
-
kill_error.get_or_insert_with(||
|
|
1394
|
+
kill_error.get_or_insert_with(|| {
|
|
1395
|
+
"session ownership is unknown; destructive cleanup refused".to_string()
|
|
1396
|
+
});
|
|
1241
1397
|
}
|
|
1242
1398
|
} else {
|
|
1243
1399
|
let session_has_leader_anchor = live_targets_now.iter().any(|target| {
|
|
@@ -1247,7 +1403,8 @@ pub mod lifecycle_port {
|
|
|
1247
1403
|
if crate::state::projection::state_is_managed_leader(&state)
|
|
1248
1404
|
&& session_has_leader_anchor
|
|
1249
1405
|
{
|
|
1250
|
-
let _ =
|
|
1406
|
+
let _ =
|
|
1407
|
+
event_log_write_session_spared(&run_workspace, session, &leader_anchor_ids);
|
|
1251
1408
|
push_unique_session(&mut spared_sessions, session.clone());
|
|
1252
1409
|
let worker_panes = collect_session_worker_panes(
|
|
1253
1410
|
&state,
|
|
@@ -1256,7 +1413,8 @@ pub mod lifecycle_port {
|
|
|
1256
1413
|
&live_targets_now,
|
|
1257
1414
|
);
|
|
1258
1415
|
let event_log = crate::event_log::EventLog::new(&run_workspace);
|
|
1259
|
-
let identity_state =
|
|
1416
|
+
let identity_state =
|
|
1417
|
+
state_for_session(&state, session.as_str()).unwrap_or(&state);
|
|
1260
1418
|
for pane in &worker_panes {
|
|
1261
1419
|
let pane_target = vec![pane.as_str().to_string()];
|
|
1262
1420
|
let audit = crate::kill_audit::pre_kill_audit_scoped(
|
|
@@ -1272,7 +1430,8 @@ pub mod lifecycle_port {
|
|
|
1272
1430
|
"positive workspace/team ownership",
|
|
1273
1431
|
);
|
|
1274
1432
|
if let Err(error) = audit {
|
|
1275
|
-
kill_error
|
|
1433
|
+
kill_error
|
|
1434
|
+
.get_or_insert_with(|| format!("pre-kill audit failed: {error}"));
|
|
1276
1435
|
continue;
|
|
1277
1436
|
}
|
|
1278
1437
|
if let Err(error) = transport.kill_pane(pane) {
|
|
@@ -1324,7 +1483,8 @@ pub mod lifecycle_port {
|
|
|
1324
1483
|
if team.is_none() {
|
|
1325
1484
|
deadline.check("shared_socket_cleanup")?;
|
|
1326
1485
|
let event_log = crate::event_log::EventLog::new(&run_workspace);
|
|
1327
|
-
let cleanup =
|
|
1486
|
+
let cleanup =
|
|
1487
|
+
bare_shutdown_socket_cleanup(&run_workspace, transport, &state, &event_log);
|
|
1328
1488
|
for session in cleanup.killed_sessions {
|
|
1329
1489
|
push_unique_session(&mut killed_sessions, session);
|
|
1330
1490
|
}
|
|
@@ -1619,10 +1779,8 @@ pub mod lifecycle_port {
|
|
|
1619
1779
|
.and_then(Value::as_str)
|
|
1620
1780
|
.filter(|value| !value.is_empty())
|
|
1621
1781
|
.unwrap_or_else(|| session.as_str());
|
|
1622
|
-
let leader_panes =
|
|
1623
|
-
state,
|
|
1624
|
-
transport.tmux_endpoint().as_deref(),
|
|
1625
|
-
);
|
|
1782
|
+
let leader_panes =
|
|
1783
|
+
collect_state_leader_anchor_pane_ids(state, transport.tmux_endpoint().as_deref());
|
|
1626
1784
|
let event_log = crate::event_log::EventLog::new(workspace);
|
|
1627
1785
|
let team_key = state.get("team_key").and_then(Value::as_str);
|
|
1628
1786
|
let generation = state_generation(state);
|
|
@@ -2142,10 +2300,13 @@ pub mod lifecycle_port {
|
|
|
2142
2300
|
let spawn_cwds = state_spawn_cwds(state, scope);
|
|
2143
2301
|
for root in root_pids {
|
|
2144
2302
|
for pid in process_tree_from_table(*root, table) {
|
|
2145
|
-
let owned =
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2303
|
+
let owned =
|
|
2304
|
+
table
|
|
2305
|
+
.iter()
|
|
2306
|
+
.find(|process| process.pid == pid)
|
|
2307
|
+
.is_some_and(|process| {
|
|
2308
|
+
process_matches_workspace(process, workspace, &spawn_cwds)
|
|
2309
|
+
});
|
|
2149
2310
|
if owned && !protected.contains_pid(pid) && seen.insert(pid) {
|
|
2150
2311
|
pids.push(pid);
|
|
2151
2312
|
}
|
|
@@ -2223,14 +2384,8 @@ pub mod lifecycle_port {
|
|
|
2223
2384
|
let round_table = shutdown_table_snapshot(workspace, probe_degraded, "residual_round");
|
|
2224
2385
|
let mut protected = shutdown_protection_set(&round_table);
|
|
2225
2386
|
extend_protection_with_leader_panes(&mut protected, transport, state, &round_table);
|
|
2226
|
-
let residuals =
|
|
2227
|
-
workspace,
|
|
2228
|
-
state,
|
|
2229
|
-
root_pids,
|
|
2230
|
-
&protected,
|
|
2231
|
-
scope,
|
|
2232
|
-
&round_table,
|
|
2233
|
-
);
|
|
2387
|
+
let residuals =
|
|
2388
|
+
matched_processes(workspace, state, root_pids, &protected, scope, &round_table);
|
|
2234
2389
|
if residuals.is_empty() {
|
|
2235
2390
|
return;
|
|
2236
2391
|
}
|
|
@@ -2282,7 +2437,10 @@ pub mod lifecycle_port {
|
|
|
2282
2437
|
) -> T {
|
|
2283
2438
|
let previous = SHUTDOWN_TEST_PROBES.with(|probes| {
|
|
2284
2439
|
let previous = probes.get();
|
|
2285
|
-
probes.set(ShutdownTestProbes {
|
|
2440
|
+
probes.set(ShutdownTestProbes {
|
|
2441
|
+
process_table: Some(probe),
|
|
2442
|
+
..previous
|
|
2443
|
+
});
|
|
2286
2444
|
previous
|
|
2287
2445
|
});
|
|
2288
2446
|
let _restore = ShutdownTestProbeRestore(previous);
|
|
@@ -2296,7 +2454,10 @@ pub mod lifecycle_port {
|
|
|
2296
2454
|
) -> T {
|
|
2297
2455
|
let previous = SHUTDOWN_TEST_PROBES.with(|probes| {
|
|
2298
2456
|
let previous = probes.get();
|
|
2299
|
-
probes.set(ShutdownTestProbes {
|
|
2457
|
+
probes.set(ShutdownTestProbes {
|
|
2458
|
+
pid_is_alive: Some(probe),
|
|
2459
|
+
..previous
|
|
2460
|
+
});
|
|
2300
2461
|
previous
|
|
2301
2462
|
});
|
|
2302
2463
|
let _restore = ShutdownTestProbeRestore(previous);
|
|
@@ -2551,17 +2712,29 @@ pub mod lifecycle_port {
|
|
|
2551
2712
|
endpoint: Option<&str>,
|
|
2552
2713
|
out: &mut std::collections::BTreeSet<String>,
|
|
2553
2714
|
) {
|
|
2554
|
-
for (key, peer_key) in [
|
|
2555
|
-
|
|
2556
|
-
|
|
2715
|
+
for (key, peer_key) in [
|
|
2716
|
+
("team_owner", "leader_receiver"),
|
|
2717
|
+
("leader_receiver", "team_owner"),
|
|
2718
|
+
] {
|
|
2719
|
+
let Some(anchor) = state.get(key) else {
|
|
2720
|
+
continue;
|
|
2721
|
+
};
|
|
2722
|
+
let Some(pane_id) = anchor
|
|
2723
|
+
.get("pane_id")
|
|
2724
|
+
.and_then(Value::as_str)
|
|
2725
|
+
.filter(|s| !s.is_empty())
|
|
2726
|
+
else {
|
|
2557
2727
|
continue;
|
|
2558
2728
|
};
|
|
2559
2729
|
// team_owner often records only a pane ID; its receiver supplies
|
|
2560
2730
|
// the endpoint only when it describes that exact same anchor.
|
|
2561
|
-
let anchor_endpoint = anchor
|
|
2731
|
+
let anchor_endpoint = anchor
|
|
2732
|
+
.get("tmux_socket")
|
|
2733
|
+
.and_then(Value::as_str)
|
|
2562
2734
|
.filter(|s| !s.is_empty())
|
|
2563
2735
|
.or_else(|| {
|
|
2564
|
-
state
|
|
2736
|
+
state
|
|
2737
|
+
.get(peer_key)
|
|
2565
2738
|
.filter(|peer| peer.get("pane_id").and_then(Value::as_str) == Some(pane_id))
|
|
2566
2739
|
.and_then(|peer| peer.get("tmux_socket"))
|
|
2567
2740
|
.and_then(Value::as_str)
|
|
@@ -2569,7 +2742,9 @@ pub mod lifecycle_port {
|
|
|
2569
2742
|
});
|
|
2570
2743
|
if let (Some(anchor_path), Some(transport_path)) = (
|
|
2571
2744
|
anchor_endpoint.and_then(socket_file_for_endpoint),
|
|
2572
|
-
endpoint
|
|
2745
|
+
endpoint
|
|
2746
|
+
.filter(|s| !s.is_empty())
|
|
2747
|
+
.and_then(socket_file_for_endpoint),
|
|
2573
2748
|
) {
|
|
2574
2749
|
let anchor_path = anchor_path.canonicalize().unwrap_or(anchor_path);
|
|
2575
2750
|
let transport_path = transport_path.canonicalize().unwrap_or(transport_path);
|
|
@@ -2688,10 +2863,8 @@ pub mod lifecycle_port {
|
|
|
2688
2863
|
);
|
|
2689
2864
|
// Source 2: state.json team_owner / leader_receiver 真锚 pane_id(top-level +
|
|
2690
2865
|
// teams[*]),per-workspace socket 命中。
|
|
2691
|
-
let anchor_pane_ids =
|
|
2692
|
-
state,
|
|
2693
|
-
transport.tmux_endpoint().as_deref(),
|
|
2694
|
-
);
|
|
2866
|
+
let anchor_pane_ids =
|
|
2867
|
+
collect_state_leader_anchor_pane_ids(state, transport.tmux_endpoint().as_deref());
|
|
2695
2868
|
leader_pane_pids.extend(
|
|
2696
2869
|
pane_targets
|
|
2697
2870
|
.iter()
|
|
@@ -2707,7 +2880,8 @@ pub mod lifecycle_port {
|
|
|
2707
2880
|
// 的 list_targets 找 anchor pane_id → pane_pid → 进入 process_tree 保护。
|
|
2708
2881
|
// 不在 state 中的 socket 不查(MUST-17 不撒宽 / 不主动枚举全机器 sockets)。
|
|
2709
2882
|
for socket_endpoint in collect_state_recorded_tmux_sockets(state) {
|
|
2710
|
-
let anchor_pane_ids =
|
|
2883
|
+
let anchor_pane_ids =
|
|
2884
|
+
collect_state_leader_anchor_pane_ids(state, Some(&socket_endpoint));
|
|
2711
2885
|
let cross_backend =
|
|
2712
2886
|
crate::tmux_backend::TmuxBackend::for_tmux_endpoint(&socket_endpoint);
|
|
2713
2887
|
let cross_panes =
|
|
@@ -2784,11 +2958,7 @@ pub mod lifecycle_port {
|
|
|
2784
2958
|
crate::platform::process::reap_child_if_possible(*pid);
|
|
2785
2959
|
}
|
|
2786
2960
|
let elapsed = start.elapsed();
|
|
2787
|
-
if !pids
|
|
2788
|
-
.iter()
|
|
2789
|
-
.any(|pid| shutdown_pid_is_alive(*pid))
|
|
2790
|
-
|| elapsed >= timeout
|
|
2791
|
-
{
|
|
2961
|
+
if !pids.iter().any(|pid| shutdown_pid_is_alive(*pid)) || elapsed >= timeout {
|
|
2792
2962
|
return;
|
|
2793
2963
|
}
|
|
2794
2964
|
std::thread::sleep(
|
|
@@ -2805,18 +2975,13 @@ pub mod lifecycle_port {
|
|
|
2805
2975
|
scope: ShutdownReapScope,
|
|
2806
2976
|
table: &[ProcessInfo],
|
|
2807
2977
|
) -> Vec<Value> {
|
|
2808
|
-
let mut residuals = matched_processes(
|
|
2809
|
-
workspace, state, root_pids, protected, scope, table,
|
|
2810
|
-
);
|
|
2978
|
+
let mut residuals = matched_processes(workspace, state, root_pids, protected, scope, table);
|
|
2811
2979
|
let mut seen = residuals
|
|
2812
2980
|
.iter()
|
|
2813
2981
|
.map(|process| process.pid)
|
|
2814
2982
|
.collect::<std::collections::BTreeSet<_>>();
|
|
2815
2983
|
for pid in root_pids {
|
|
2816
|
-
if !protected.contains_pid(*pid)
|
|
2817
|
-
&& shutdown_pid_is_alive(*pid)
|
|
2818
|
-
&& seen.insert(*pid)
|
|
2819
|
-
{
|
|
2984
|
+
if !protected.contains_pid(*pid) && shutdown_pid_is_alive(*pid) && seen.insert(*pid) {
|
|
2820
2985
|
residuals.push(ProcessInfo {
|
|
2821
2986
|
pid: *pid,
|
|
2822
2987
|
ppid: 0,
|
|
@@ -2966,7 +3131,11 @@ pub mod lifecycle_port {
|
|
|
2966
3131
|
let _ = force; // Never grants authority to stop an already-running seat.
|
|
2967
3132
|
let agent_id = crate::model::ids::AgentId::new(agent);
|
|
2968
3133
|
match crate::lifecycle::role_config::start_agent_from_role(
|
|
2969
|
-
workspace,
|
|
3134
|
+
workspace,
|
|
3135
|
+
&agent_id,
|
|
3136
|
+
role_config,
|
|
3137
|
+
allow_fresh,
|
|
3138
|
+
team,
|
|
2970
3139
|
) {
|
|
2971
3140
|
Ok(crate::lifecycle::StartAgentOutcome::Running {
|
|
2972
3141
|
env,
|
|
@@ -2995,14 +3164,19 @@ pub mod lifecycle_port {
|
|
|
2995
3164
|
}),
|
|
2996
3165
|
)),
|
|
2997
3166
|
Ok(crate::lifecycle::StartAgentOutcome::Noop { env, target }) => {
|
|
2998
|
-
Ok(agent_action_value(
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3167
|
+
Ok(agent_action_value(
|
|
3168
|
+
agent,
|
|
3169
|
+
workspace,
|
|
3170
|
+
team,
|
|
3171
|
+
json!({
|
|
3172
|
+
"ok": true,
|
|
3173
|
+
"agent_id": env.agent_id.as_str(),
|
|
3174
|
+
"status": "already_running",
|
|
3175
|
+
"target": target,
|
|
3176
|
+
"coordinator_started": env.coordinator_started,
|
|
3177
|
+
"state_file": env.state_file.to_string_lossy(),
|
|
3178
|
+
}),
|
|
3179
|
+
))
|
|
3006
3180
|
}
|
|
3007
3181
|
Ok(crate::lifecycle::StartAgentOutcome::Paused { agent_id }) => Ok(json!({
|
|
3008
3182
|
"ok": false,
|
|
@@ -3035,13 +3209,7 @@ pub mod lifecycle_port {
|
|
|
3035
3209
|
team: Option<&str>,
|
|
3036
3210
|
) -> Result<Value, CliError> {
|
|
3037
3211
|
let agent_id = crate::model::ids::AgentId::new(agent);
|
|
3038
|
-
match crate::lifecycle::reset_agent(
|
|
3039
|
-
workspace,
|
|
3040
|
-
&agent_id,
|
|
3041
|
-
discard_session,
|
|
3042
|
-
true,
|
|
3043
|
-
team,
|
|
3044
|
-
) {
|
|
3212
|
+
match crate::lifecycle::reset_agent(workspace, &agent_id, discard_session, true, team) {
|
|
3045
3213
|
Ok(crate::lifecycle::ResetAgentOutcome::Reset {
|
|
3046
3214
|
env,
|
|
3047
3215
|
start_mode,
|
|
@@ -3089,7 +3257,12 @@ pub mod lifecycle_port {
|
|
|
3089
3257
|
let agent_id = crate::model::ids::AgentId::new(agent);
|
|
3090
3258
|
let source = (!role_file.is_empty()).then(|| Path::new(role_file));
|
|
3091
3259
|
match crate::lifecycle::role_config::add_agent_from_role(
|
|
3092
|
-
workspace,
|
|
3260
|
+
workspace,
|
|
3261
|
+
&agent_id,
|
|
3262
|
+
source,
|
|
3263
|
+
role_config,
|
|
3264
|
+
force,
|
|
3265
|
+
team,
|
|
3093
3266
|
) {
|
|
3094
3267
|
Ok(report) => Ok(agent_action_value(
|
|
3095
3268
|
agent,
|
|
@@ -3108,7 +3281,12 @@ pub mod lifecycle_port {
|
|
|
3108
3281
|
Err(e) => Ok(error_value(e)),
|
|
3109
3282
|
}
|
|
3110
3283
|
}
|
|
3111
|
-
fn agent_action_value(
|
|
3284
|
+
fn agent_action_value(
|
|
3285
|
+
agent: &str,
|
|
3286
|
+
workspace: &Path,
|
|
3287
|
+
team: Option<&str>,
|
|
3288
|
+
mut value: Value,
|
|
3289
|
+
) -> Value {
|
|
3112
3290
|
if value.get("ok").and_then(Value::as_bool) == Some(true) {
|
|
3113
3291
|
if let Some(object) = value.as_object_mut() {
|
|
3114
3292
|
if let Some(command) = super::adapters::send_command(agent, workspace, team) {
|
|
@@ -3749,10 +3927,7 @@ pub mod lifecycle_port {
|
|
|
3749
3927
|
let mut default = detail.clone();
|
|
3750
3928
|
compact_quick_start_value(&mut default);
|
|
3751
3929
|
assert_eq!(default["status"], json!("leader_bind_refused"));
|
|
3752
|
-
assert_eq!(
|
|
3753
|
-
default["reason"],
|
|
3754
|
-
json!("provider_unresolved")
|
|
3755
|
-
);
|
|
3930
|
+
assert_eq!(default["reason"], json!("provider_unresolved"));
|
|
3756
3931
|
assert_eq!(
|
|
3757
3932
|
default["next_actions"],
|
|
3758
3933
|
json!(["set TEAM_AGENT_LEADER_PROVIDER or run from a provider pane; do not run claim-leader"])
|
|
@@ -3772,14 +3947,18 @@ pub mod lifecycle_port {
|
|
|
3772
3947
|
);
|
|
3773
3948
|
assert!(default["readiness"].get("all_spawned").is_none());
|
|
3774
3949
|
assert!(default["readiness"].get("all_attached_receiver").is_none());
|
|
3775
|
-
assert!(default["readiness"]
|
|
3950
|
+
assert!(default["readiness"]
|
|
3951
|
+
.get("leader_receiver_attached")
|
|
3952
|
+
.is_none());
|
|
3776
3953
|
assert_eq!(
|
|
3777
3954
|
default["worker_readiness"]["all_workers_spawned"],
|
|
3778
3955
|
json!(true)
|
|
3779
3956
|
);
|
|
3780
3957
|
assert!(default["worker_readiness"].get("all_spawned").is_none());
|
|
3781
3958
|
assert!(detail["readiness"].get("all_spawned").is_some());
|
|
3782
|
-
assert!(detail["worker_readiness"]
|
|
3959
|
+
assert!(detail["worker_readiness"]
|
|
3960
|
+
.get("leader_receiver_attached")
|
|
3961
|
+
.is_some());
|
|
3783
3962
|
}
|
|
3784
3963
|
|
|
3785
3964
|
#[test]
|
|
@@ -3935,7 +4114,10 @@ pub mod lifecycle_port {
|
|
|
3935
4114
|
compact_restart_value(&mut default);
|
|
3936
4115
|
assert_eq!(default["status"], json!("partial"));
|
|
3937
4116
|
assert_eq!(default["reason"], json!("restart_agent_failed"));
|
|
3938
|
-
assert_eq!(
|
|
4117
|
+
assert_eq!(
|
|
4118
|
+
default["next_actions"],
|
|
4119
|
+
json!(["reset-agent worker --discard-session"])
|
|
4120
|
+
);
|
|
3939
4121
|
assert!(default.get("coordinator").is_none());
|
|
3940
4122
|
assert!(detail.get("coordinator").is_some());
|
|
3941
4123
|
}
|
|
@@ -3975,7 +4157,9 @@ pub mod lifecycle_port {
|
|
|
3975
4157
|
&transport,
|
|
3976
4158
|
)
|
|
3977
4159
|
.expect("first quick-start");
|
|
3978
|
-
let crate::lifecycle::QuickStartReport::Ready {
|
|
4160
|
+
let crate::lifecycle::QuickStartReport::Ready {
|
|
4161
|
+
team: ready_team, ..
|
|
4162
|
+
} = &first
|
|
3979
4163
|
else {
|
|
3980
4164
|
panic!("first producer must return Ready; got {first:?}");
|
|
3981
4165
|
};
|
|
@@ -3992,7 +4176,7 @@ pub mod lifecycle_port {
|
|
|
3992
4176
|
"team-agent".to_string(),
|
|
3993
4177
|
"send".to_string(),
|
|
3994
4178
|
"implementer".to_string(),
|
|
3995
|
-
"
|
|
4179
|
+
"请完成任务并把答案回复给 leader。".to_string(),
|
|
3996
4180
|
"--workspace".to_string(),
|
|
3997
4181
|
team_dir.to_string_lossy().into_owned(),
|
|
3998
4182
|
"--team".to_string(),
|
|
@@ -4029,7 +4213,7 @@ pub mod lifecycle_port {
|
|
|
4029
4213
|
"team-agent".to_string(),
|
|
4030
4214
|
"send".to_string(),
|
|
4031
4215
|
"implementer".to_string(),
|
|
4032
|
-
"
|
|
4216
|
+
"请完成任务并把答案回复给 leader。".to_string(),
|
|
4033
4217
|
"--workspace".to_string(),
|
|
4034
4218
|
team_dir.to_string_lossy().into_owned(),
|
|
4035
4219
|
"--team".to_string(),
|
|
@@ -4054,10 +4238,7 @@ pub mod lifecycle_port {
|
|
|
4054
4238
|
value
|
|
4055
4239
|
.get("agent_ids")
|
|
4056
4240
|
.and_then(Value::as_array)
|
|
4057
|
-
.map(|items| items
|
|
4058
|
-
.iter()
|
|
4059
|
-
.filter_map(Value::as_str)
|
|
4060
|
-
.collect::<Vec<_>>()),
|
|
4241
|
+
.map(|items| items.iter().filter_map(Value::as_str).collect::<Vec<_>>()),
|
|
4061
4242
|
Some(vec!["sol", "luna"])
|
|
4062
4243
|
);
|
|
4063
4244
|
}
|
|
@@ -4087,14 +4268,15 @@ pub mod lifecycle_port {
|
|
|
4087
4268
|
|
|
4088
4269
|
#[test]
|
|
4089
4270
|
fn existing_runtime_send_guidance_does_not_pair_alice_with_bob() {
|
|
4090
|
-
let mut value =
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4271
|
+
let mut value =
|
|
4272
|
+
quick_start_value(crate::lifecycle::QuickStartReport::ExistingRuntime {
|
|
4273
|
+
team: Some("bob".to_string()),
|
|
4274
|
+
session_name: Some(crate::transport::SessionName::new("team-bob")),
|
|
4275
|
+
state_path: Some(PathBuf::from("/tmp/state.json")),
|
|
4276
|
+
next_actions: vec!["restart".to_string()],
|
|
4277
|
+
attach_commands: Vec::new(),
|
|
4278
|
+
agent_ids: vec!["bob".to_string()],
|
|
4279
|
+
});
|
|
4098
4280
|
crate::cli::adapters::append_send_guidance(
|
|
4099
4281
|
&mut value,
|
|
4100
4282
|
Path::new("/tmp/ws"),
|
|
@@ -4189,7 +4371,7 @@ pub mod lifecycle_port {
|
|
|
4189
4371
|
value["reason"] = json!(reason);
|
|
4190
4372
|
}
|
|
4191
4373
|
value
|
|
4192
|
-
}
|
|
4374
|
+
}
|
|
4193
4375
|
crate::lifecycle::RestartReport::Partial {
|
|
4194
4376
|
session_name,
|
|
4195
4377
|
agents,
|
|
@@ -4243,7 +4425,7 @@ pub mod lifecycle_port {
|
|
|
4243
4425
|
value["leader_bind_reason"] = json!(reason);
|
|
4244
4426
|
}
|
|
4245
4427
|
value
|
|
4246
|
-
}
|
|
4428
|
+
}
|
|
4247
4429
|
crate::lifecycle::RestartReport::Failed {
|
|
4248
4430
|
session_name,
|
|
4249
4431
|
failed_agents,
|
|
@@ -5254,7 +5436,11 @@ pub mod diagnose_port {
|
|
|
5254
5436
|
///
|
|
5255
5437
|
/// `orphan_gate(fix, confirm)`(`--gate orphans`)。CI gate。
|
|
5256
5438
|
#[cfg(test)]
|
|
5257
|
-
pub(super) fn orphan_gate(
|
|
5439
|
+
pub(super) fn orphan_gate(
|
|
5440
|
+
workspace: &Path,
|
|
5441
|
+
fix: bool,
|
|
5442
|
+
confirm: bool,
|
|
5443
|
+
) -> Result<Value, CliError> {
|
|
5258
5444
|
crate::diagnose::orphans::orphan_gate_json(workspace, fix, confirm)
|
|
5259
5445
|
}
|
|
5260
5446
|
///
|