@team-agent/installer 0.5.33 → 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.33"
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.33"
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()
@@ -163,20 +163,27 @@ pub(crate) fn latest_prompt_signal(scrollback: &str) -> Option<AgentActivity> {
163
163
  }
164
164
  let mut has_idle_prompt = false;
165
165
  let mut has_live_working_indicator = false;
166
+ let mut has_fake_ready_structural = false;
166
167
  for line in &active_region {
167
168
  let lower = line.to_ascii_lowercase();
168
169
  if line.contains('❯') || line.contains('›') {
169
170
  has_idle_prompt = true;
170
171
  }
171
- // 0.5.32 follow-up (`0532-r1-real-fail-triage.md` §6/§7):
172
+ // 0.5.34 (`0532-r1-real-fail-triage.md` §6/§7 + te msg_94957b9c55b0):
172
173
  // fake provider owns explicit READY/WORKING markers (fake_worker.rs:47/59).
173
- // Recognize them here as STRUCTURAL bottom-region signals so restart's
174
- // first-capture READY does not fall through to `recent_provider_output`
175
- // false busy. Scope: literal `TEAM_AGENT_FAKE_READY`/`TEAM_AGENT_FAKE_WORKING`
176
- // tokens only do NOT infer that arbitrary "ready" prose from real
177
- // providers means idle.
174
+ // - READY is a STRUCTURAL non-busy signal that suppresses the
175
+ // `recent_provider_output` false-busy on first post-restart capture,
176
+ // but must NOT resolve to Idle (unknown-never-idle discipline —
177
+ // post-restart READY is the fake worker's boot heartbeat, not proof
178
+ // of an idle prompt). Route it through Uncertain with a distinct
179
+ // rationale so callers can trace the source.
180
+ // - WORKING is a structural busy signal (parity with the codex
181
+ // `• Working (` composer indicator).
182
+ // Scope: literal `TEAM_AGENT_FAKE_*` tokens only — do NOT infer that
183
+ // arbitrary "ready" prose from real providers means either idle or
184
+ // non-busy.
178
185
  if line.contains("TEAM_AGENT_FAKE_READY") {
179
- has_idle_prompt = true;
186
+ has_fake_ready_structural = true;
180
187
  }
181
188
  if line.contains("TEAM_AGENT_FAKE_WORKING") {
182
189
  has_live_working_indicator = true;
@@ -208,6 +215,17 @@ pub(crate) fn latest_prompt_signal(scrollback: &str) -> Option<AgentActivity> {
208
215
  if has_idle_prompt {
209
216
  return Some(idle_activity());
210
217
  }
218
+ // 0.5.34: fake READY is structural but not decisive. Return Uncertain
219
+ // so the classifier short-circuits (avoiding recent_provider_output
220
+ // false-busy) without asserting idle. `latest_pane_signal_is_structural`
221
+ // in coordinator/tick.rs treats `Some(_)` as structural.
222
+ if has_fake_ready_structural {
223
+ return Some(AgentActivity {
224
+ status: ActivityStatus::Uncertain,
225
+ confidence: 0.6,
226
+ rationale: "fake_ready_structural".to_string(),
227
+ });
228
+ }
211
229
  // No structural signal in the bottom region → caller (activity.rs:184)
212
230
  // gets None and treats as no-decisive-signal → Uncertain (IRON LAW).
213
231
  None
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.33",
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.33",
24
- "@team-agent/cli-darwin-x64": "0.5.33",
25
- "@team-agent/cli-linux-x64": "0.5.33"
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",