@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 CHANGED
@@ -575,7 +575,7 @@ dependencies = [
575
575
 
576
576
  [[package]]
577
577
  name = "team-agent"
578
- version = "0.5.34"
578
+ version = "0.5.35"
579
579
  dependencies = [
580
580
  "anyhow",
581
581
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.34"
12
+ version = "0.5.35"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -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
- persist_exec_provider_leader_binding(plan, workspace)?;
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.34",
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.34",
24
- "@team-agent/cli-darwin-x64": "0.5.34",
25
- "@team-agent/cli-linux-x64": "0.5.34"
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",