@team-agent/installer 0.5.34 → 0.5.35
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/status.rs +28 -0
- package/crates/team-agent/src/leader/start.rs +194 -1
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -52,6 +52,14 @@ pub fn agent_summary_counts(agents: &Value, health: &Value) -> SummaryCounts {
|
|
|
52
52
|
.and_then(|v| v.get("status"))
|
|
53
53
|
.and_then(Value::as_str)
|
|
54
54
|
.unwrap_or("");
|
|
55
|
+
// 0.5.35 R4 (`.team/artifacts/managed-leader-provider-reentry-locate.md`):
|
|
56
|
+
// canonical `worker_state=UNKNOWN` / `activity.status=uncertain`
|
|
57
|
+
// beats legacy `agent_health.status=WORKING` so the five-line
|
|
58
|
+
// summary stops counting a runtime-honestly-UNKNOWN agent as busy.
|
|
59
|
+
if canonical_worker_state_is_unknown(agent) {
|
|
60
|
+
counts.unknown += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
55
63
|
match classify_agent_bucket(raw, hstatus) {
|
|
56
64
|
SummaryBucket::Running => counts.running += 1,
|
|
57
65
|
SummaryBucket::Busy => counts.busy += 1,
|
|
@@ -64,6 +72,22 @@ pub fn agent_summary_counts(agents: &Value, health: &Value) -> SummaryCounts {
|
|
|
64
72
|
counts
|
|
65
73
|
}
|
|
66
74
|
|
|
75
|
+
/// 0.5.35 R4: return true when the runtime classifier has written a
|
|
76
|
+
/// canonical non-decisive observation onto the agent row. Used by
|
|
77
|
+
/// `agent_summary_counts` and `csv_agent_status` so the "UNKNOWN-first"
|
|
78
|
+
/// signal beats legacy `agent_health.status`.
|
|
79
|
+
fn canonical_worker_state_is_unknown(agent: &Value) -> bool {
|
|
80
|
+
let worker_state_unknown = agent
|
|
81
|
+
.get("worker_state")
|
|
82
|
+
.and_then(Value::as_str)
|
|
83
|
+
.is_some_and(|value| value.eq_ignore_ascii_case("UNKNOWN"));
|
|
84
|
+
let activity_uncertain = agent
|
|
85
|
+
.pointer("/activity/status")
|
|
86
|
+
.and_then(Value::as_str)
|
|
87
|
+
.is_some_and(|value| value.eq_ignore_ascii_case("uncertain"));
|
|
88
|
+
worker_state_unknown || activity_uncertain
|
|
89
|
+
}
|
|
90
|
+
|
|
67
91
|
fn stale_agent_bucket(agent: &Value) -> Option<SummaryBucket> {
|
|
68
92
|
if agent.get("stale").and_then(Value::as_bool) != Some(true) {
|
|
69
93
|
return None;
|
|
@@ -224,6 +248,10 @@ fn csv_agent_status(
|
|
|
224
248
|
if matches!(raw.as_str(), "running" | "busy" | "working") && !pane_present {
|
|
225
249
|
return "错误";
|
|
226
250
|
}
|
|
251
|
+
// 0.5.35 R4: canonical UNKNOWN beats legacy WORKING in human CSV too.
|
|
252
|
+
if canonical_worker_state_is_unknown(agent) {
|
|
253
|
+
return "未知";
|
|
254
|
+
}
|
|
227
255
|
if raw == "idle" || hstatus == "idle" {
|
|
228
256
|
return "空闲";
|
|
229
257
|
}
|
|
@@ -223,7 +223,22 @@ pub fn execute_leader_plan(
|
|
|
223
223
|
&& !std::io::stdin().is_terminal()
|
|
224
224
|
&& insert_detach_flag(&mut argv);
|
|
225
225
|
if plan.mode == LeaderStartMode::ExecProvider && !plan.is_external_leader {
|
|
226
|
-
|
|
226
|
+
// 0.5.35 (`.team/artifacts/managed-leader-provider-reentry-locate.md`
|
|
227
|
+
// §5/§6): three-way classify BEFORE persist. Same physical pane =
|
|
228
|
+
// provider process replacement (refresh only). Different pane with
|
|
229
|
+
// an existing canonical binding = no canonical rewrite (claim /
|
|
230
|
+
// takeover is the recovery path, §7). Unbound = first-time write.
|
|
231
|
+
match classify_exec_provider_binding(workspace, plan)? {
|
|
232
|
+
ExecProviderBinding::ManagedReentry => {
|
|
233
|
+
refresh_managed_leader_provider_binding(plan, workspace)?;
|
|
234
|
+
}
|
|
235
|
+
ExecProviderBinding::DifferentPaneAlreadyBound => {
|
|
236
|
+
// No canonical state write. Provider still runs below.
|
|
237
|
+
}
|
|
238
|
+
ExecProviderBinding::Unbound => {
|
|
239
|
+
persist_exec_provider_leader_binding(plan, workspace)?;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
227
242
|
} else if plan.is_external_leader {
|
|
228
243
|
persist_external_leader_topology_marker(plan, workspace)?;
|
|
229
244
|
}
|
|
@@ -904,6 +919,184 @@ fn persist_exec_provider_leader_binding(
|
|
|
904
919
|
Ok(())
|
|
905
920
|
}
|
|
906
921
|
|
|
922
|
+
/// 0.5.35 (`.team/artifacts/managed-leader-provider-reentry-locate.md` §5/§6):
|
|
923
|
+
/// three-way classification of an in-tmux ExecProvider invocation. Physical-
|
|
924
|
+
/// channel-first — never consults `pane_current_command` or gates on
|
|
925
|
+
/// `leader_session_uuid` equality (§6 forbids uuid-mismatch hard refusal).
|
|
926
|
+
enum ExecProviderBinding {
|
|
927
|
+
ManagedReentry,
|
|
928
|
+
DifferentPaneAlreadyBound,
|
|
929
|
+
Unbound,
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
fn classify_exec_provider_binding(
|
|
933
|
+
workspace: &Path,
|
|
934
|
+
plan: &LeaderStartPlan,
|
|
935
|
+
) -> Result<ExecProviderBinding, LeaderError> {
|
|
936
|
+
let Some(pane) = std::env::var("TMUX_PANE")
|
|
937
|
+
.ok()
|
|
938
|
+
.filter(|value| !value.trim().is_empty())
|
|
939
|
+
else {
|
|
940
|
+
return Ok(ExecProviderBinding::Unbound);
|
|
941
|
+
};
|
|
942
|
+
let state = match crate::state::persist::load_runtime_state(workspace) {
|
|
943
|
+
Ok(state) => state,
|
|
944
|
+
Err(_) => return Ok(ExecProviderBinding::Unbound),
|
|
945
|
+
};
|
|
946
|
+
let team_key = plan
|
|
947
|
+
.identity
|
|
948
|
+
.as_ref()
|
|
949
|
+
.map(|identity| identity.team_id.as_str().to_string())
|
|
950
|
+
.or_else(|| {
|
|
951
|
+
state
|
|
952
|
+
.get("active_team_key")
|
|
953
|
+
.and_then(serde_json::Value::as_str)
|
|
954
|
+
.filter(|s| !s.is_empty())
|
|
955
|
+
.map(str::to_string)
|
|
956
|
+
});
|
|
957
|
+
let Some(team_key) = team_key else {
|
|
958
|
+
return Ok(ExecProviderBinding::Unbound);
|
|
959
|
+
};
|
|
960
|
+
let Some(receiver) = state
|
|
961
|
+
.pointer(&format!("/teams/{team_key}/leader_receiver"))
|
|
962
|
+
.and_then(serde_json::Value::as_object)
|
|
963
|
+
else {
|
|
964
|
+
return Ok(ExecProviderBinding::Unbound);
|
|
965
|
+
};
|
|
966
|
+
let receiver_pane = receiver
|
|
967
|
+
.get("pane_id")
|
|
968
|
+
.or_else(|| receiver.get("pane"))
|
|
969
|
+
.and_then(serde_json::Value::as_str)
|
|
970
|
+
.unwrap_or("");
|
|
971
|
+
if receiver_pane.is_empty() {
|
|
972
|
+
return Ok(ExecProviderBinding::Unbound);
|
|
973
|
+
}
|
|
974
|
+
if receiver_pane != pane {
|
|
975
|
+
return Ok(ExecProviderBinding::DifferentPaneAlreadyBound);
|
|
976
|
+
}
|
|
977
|
+
let pane_id = PaneId::new(pane.clone());
|
|
978
|
+
let target = current_tmux_pane_info(&pane_id);
|
|
979
|
+
let leader_prefixed_session = target
|
|
980
|
+
.as_ref()
|
|
981
|
+
.map(|t| t.session.as_str().starts_with(LEADER_SESSION_PREFIX))
|
|
982
|
+
.unwrap_or(false);
|
|
983
|
+
if !leader_prefixed_session {
|
|
984
|
+
return Ok(ExecProviderBinding::DifferentPaneAlreadyBound);
|
|
985
|
+
}
|
|
986
|
+
let recorded_socket = receiver
|
|
987
|
+
.get("tmux_socket")
|
|
988
|
+
.and_then(serde_json::Value::as_str)
|
|
989
|
+
.filter(|s| !s.is_empty());
|
|
990
|
+
if let Some(recorded_socket) = recorded_socket {
|
|
991
|
+
let caller_socket = crate::tmux_backend::socket_name_from_tmux_env();
|
|
992
|
+
if caller_socket.as_deref() != Some(recorded_socket) {
|
|
993
|
+
return Ok(ExecProviderBinding::DifferentPaneAlreadyBound);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
Ok(ExecProviderBinding::ManagedReentry)
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/// 0.5.35: same-pane provider re-entry refresh path. Preserves canonical
|
|
1000
|
+
/// team runtime identity (`session_name`/`team_dir`/`spec_path`/`agents`/
|
|
1001
|
+
/// `tasks`) and epoch non-regressively; updates only diagnostic receiver
|
|
1002
|
+
/// fields. Stage 3 strip preserved (root `team_owner`/`leader_receiver`/
|
|
1003
|
+
/// `owner_epoch` are NOT reintroduced — writes go through
|
|
1004
|
+
/// `state::ownership::write_owner` into the canonical teams.<key> slot).
|
|
1005
|
+
fn refresh_managed_leader_provider_binding(
|
|
1006
|
+
plan: &LeaderStartPlan,
|
|
1007
|
+
workspace: &Path,
|
|
1008
|
+
) -> Result<(), LeaderError> {
|
|
1009
|
+
let identity = plan
|
|
1010
|
+
.identity
|
|
1011
|
+
.as_ref()
|
|
1012
|
+
.ok_or_else(|| LeaderError::Start("managed leader re-entry identity missing".to_string()))?;
|
|
1013
|
+
let pane = std::env::var("TMUX_PANE")
|
|
1014
|
+
.ok()
|
|
1015
|
+
.filter(|value| !value.trim().is_empty())
|
|
1016
|
+
.ok_or_else(|| LeaderError::Start("managed leader re-entry pane missing".to_string()))?;
|
|
1017
|
+
let pane_id = PaneId::new(pane.clone());
|
|
1018
|
+
let target = current_tmux_pane_info(&pane_id);
|
|
1019
|
+
let mut state = crate::state::persist::load_runtime_state(workspace)
|
|
1020
|
+
.unwrap_or_else(|_| serde_json::json!({}));
|
|
1021
|
+
let team_key = identity.team_id.as_str();
|
|
1022
|
+
let existing_receiver = state
|
|
1023
|
+
.pointer(&format!("/teams/{team_key}/leader_receiver"))
|
|
1024
|
+
.cloned()
|
|
1025
|
+
.unwrap_or_else(|| serde_json::json!({}));
|
|
1026
|
+
let existing_owner = crate::state::ownership::read_owner_value(&state, team_key)
|
|
1027
|
+
.cloned()
|
|
1028
|
+
.unwrap_or_else(|| serde_json::json!({}));
|
|
1029
|
+
let existing_epoch = state
|
|
1030
|
+
.pointer(&format!("/teams/{team_key}/owner_epoch"))
|
|
1031
|
+
.and_then(serde_json::Value::as_u64)
|
|
1032
|
+
.or_else(|| existing_owner.get("owner_epoch").and_then(serde_json::Value::as_u64))
|
|
1033
|
+
.or_else(|| existing_receiver.get("owner_epoch").and_then(serde_json::Value::as_u64))
|
|
1034
|
+
.unwrap_or(0);
|
|
1035
|
+
let now = chrono::Utc::now().to_rfc3339();
|
|
1036
|
+
let provider = serde_json::to_value(plan.provider)?;
|
|
1037
|
+
let mut receiver = match existing_receiver.as_object() {
|
|
1038
|
+
Some(map) => serde_json::Value::Object(map.clone()),
|
|
1039
|
+
None => serde_json::json!({}),
|
|
1040
|
+
};
|
|
1041
|
+
if let Some(obj) = receiver.as_object_mut() {
|
|
1042
|
+
obj.insert("mode".to_string(), serde_json::json!("direct_tmux"));
|
|
1043
|
+
obj.insert("status".to_string(), serde_json::json!("attached"));
|
|
1044
|
+
obj.insert("provider".to_string(), provider.clone());
|
|
1045
|
+
obj.insert("pane_id".to_string(), serde_json::json!(pane));
|
|
1046
|
+
obj.insert("pane".to_string(), serde_json::json!(pane));
|
|
1047
|
+
obj.insert("attached_at".to_string(), serde_json::json!(now.clone()));
|
|
1048
|
+
obj.insert("discovery".to_string(), serde_json::json!("managed_leader"));
|
|
1049
|
+
obj.insert("owner_epoch".to_string(), serde_json::json!(existing_epoch));
|
|
1050
|
+
obj.entry("leader_session_uuid".to_string())
|
|
1051
|
+
.or_insert_with(|| serde_json::json!(identity.leader_session_uuid.as_str()));
|
|
1052
|
+
if let Some(target) = target.as_ref() {
|
|
1053
|
+
obj.insert("session_name".to_string(), serde_json::json!(target.session.as_str()));
|
|
1054
|
+
if let Some(window_name) = target.window_name.as_ref() {
|
|
1055
|
+
obj.insert("window_name".to_string(), serde_json::json!(window_name.as_str()));
|
|
1056
|
+
}
|
|
1057
|
+
if let Some(pane_pid) = target.pane_pid {
|
|
1058
|
+
obj.insert("pane_pid".to_string(), serde_json::json!(pane_pid));
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
let mut owner = match existing_owner.as_object() {
|
|
1063
|
+
Some(map) => serde_json::Value::Object(map.clone()),
|
|
1064
|
+
None => serde_json::json!({}),
|
|
1065
|
+
};
|
|
1066
|
+
if let Some(obj) = owner.as_object_mut() {
|
|
1067
|
+
obj.insert("pane_id".to_string(), serde_json::json!(pane));
|
|
1068
|
+
obj.insert("provider".to_string(), provider);
|
|
1069
|
+
obj.insert("owner_epoch".to_string(), serde_json::json!(existing_epoch));
|
|
1070
|
+
obj.entry("machine_fingerprint".to_string())
|
|
1071
|
+
.or_insert_with(|| serde_json::json!(identity.machine_fingerprint.as_str()));
|
|
1072
|
+
obj.entry("leader_session_uuid".to_string())
|
|
1073
|
+
.or_insert_with(|| serde_json::json!(identity.leader_session_uuid.as_str()));
|
|
1074
|
+
obj.entry("claimed_via".to_string())
|
|
1075
|
+
.or_insert_with(|| serde_json::json!("claim-leader"));
|
|
1076
|
+
obj.entry("os_user".to_string())
|
|
1077
|
+
.or_insert_with(|| serde_json::json!(identity.os_user.as_str()));
|
|
1078
|
+
}
|
|
1079
|
+
if let Some(obj) = state.as_object_mut() {
|
|
1080
|
+
obj.insert("active_team_key".to_string(), serde_json::json!(team_key));
|
|
1081
|
+
obj.insert(
|
|
1082
|
+
"leader_client".to_string(),
|
|
1083
|
+
serde_json::json!({
|
|
1084
|
+
"diagnostic_only": true,
|
|
1085
|
+
"attach_mode": "exec-provider",
|
|
1086
|
+
"tmux": std::env::var("TMUX").ok(),
|
|
1087
|
+
"reentry": true,
|
|
1088
|
+
}),
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
let record = crate::state::ownership::OwnershipWrite::new()
|
|
1092
|
+
.with_leader_receiver(receiver)
|
|
1093
|
+
.with_team_owner(owner)
|
|
1094
|
+
.with_owner_epoch(existing_epoch);
|
|
1095
|
+
crate::state::ownership::write_owner(&mut state, team_key, record);
|
|
1096
|
+
crate::state::persist::save_runtime_state(workspace, &state)?;
|
|
1097
|
+
Ok(())
|
|
1098
|
+
}
|
|
1099
|
+
|
|
907
1100
|
fn current_tmux_pane_info(pane_id: &PaneId) -> Option<crate::transport::PaneInfo> {
|
|
908
1101
|
tmux_transport_for_current_pane()
|
|
909
1102
|
.list_targets()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.35",
|
|
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.35",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.35",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.35"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|