@team-agent/installer 0.5.27 → 0.5.29

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.
@@ -38,10 +38,12 @@ pub enum EndpointConvergenceDecision {
38
38
  Converge {
39
39
  old_endpoint: String,
40
40
  new_endpoint: String,
41
+ reason: &'static str,
41
42
  },
42
43
  RefuseLiveOldEndpoint {
43
44
  old_endpoint: String,
44
45
  new_endpoint: String,
46
+ reason: &'static str,
45
47
  },
46
48
  Unknown,
47
49
  }
@@ -96,13 +98,23 @@ pub fn endpoint_convergence_decision(
96
98
  return EndpointConvergenceDecision::NoConflict;
97
99
  };
98
100
 
101
+ let mut converge_reason = "old_endpoint_dead";
99
102
  for old_endpoint in stale_endpoints {
100
103
  match endpoint_server_alive(&old_endpoint) {
101
104
  Some(true) => {
102
- return EndpointConvergenceDecision::RefuseLiveOldEndpoint {
103
- old_endpoint,
104
- new_endpoint: candidate_endpoint.to_string(),
105
- };
105
+ match old_endpoint_team_liveness(state, team_key, &old_endpoint) {
106
+ OldEndpointTeamLiveness::TeamLive { reason } => {
107
+ return EndpointConvergenceDecision::RefuseLiveOldEndpoint {
108
+ old_endpoint,
109
+ new_endpoint: candidate_endpoint.to_string(),
110
+ reason,
111
+ };
112
+ }
113
+ OldEndpointTeamLiveness::TeamAbsent { reason } => {
114
+ converge_reason = reason;
115
+ }
116
+ OldEndpointTeamLiveness::Unknown => return EndpointConvergenceDecision::Unknown,
117
+ }
106
118
  }
107
119
  Some(false) => {}
108
120
  None => return EndpointConvergenceDecision::Unknown,
@@ -112,9 +124,118 @@ pub fn endpoint_convergence_decision(
112
124
  EndpointConvergenceDecision::Converge {
113
125
  old_endpoint: first_old,
114
126
  new_endpoint: candidate_endpoint.to_string(),
127
+ reason: converge_reason,
128
+ }
129
+ }
130
+
131
+ enum OldEndpointTeamLiveness {
132
+ TeamLive { reason: &'static str },
133
+ TeamAbsent { reason: &'static str },
134
+ Unknown,
135
+ }
136
+
137
+ fn old_endpoint_team_liveness(
138
+ state: &Value,
139
+ team_key: &str,
140
+ old_endpoint: &str,
141
+ ) -> OldEndpointTeamLiveness {
142
+ let team_state = state
143
+ .get("teams")
144
+ .and_then(Value::as_object)
145
+ .and_then(|teams| teams.get(team_key))
146
+ .unwrap_or(state);
147
+ let session = non_empty_str(team_state, "session_name")
148
+ .or_else(|| non_empty_str(state, "session_name"))
149
+ .unwrap_or_default();
150
+ if !session.is_empty() {
151
+ match session_exists_on_endpoint_checked(old_endpoint, session) {
152
+ Some(true) => {
153
+ return OldEndpointTeamLiveness::TeamLive {
154
+ reason: "old_team_session_live",
155
+ };
156
+ }
157
+ Some(false) => {}
158
+ None => return OldEndpointTeamLiveness::Unknown,
159
+ }
160
+ }
161
+ match old_endpoint_has_live_team_tuple(state, team_key, old_endpoint) {
162
+ Some(true) => OldEndpointTeamLiveness::TeamLive {
163
+ reason: "old_team_tuple_live",
164
+ },
165
+ Some(false) => OldEndpointTeamLiveness::TeamAbsent {
166
+ reason: "old_team_session_absent_on_live_endpoint",
167
+ },
168
+ None => OldEndpointTeamLiveness::Unknown,
115
169
  }
116
170
  }
117
171
 
172
+ fn old_endpoint_has_live_team_tuple(
173
+ state: &Value,
174
+ team_key: &str,
175
+ old_endpoint: &str,
176
+ ) -> Option<bool> {
177
+ let backend = crate::tmux_backend::TmuxBackend::for_tmux_endpoint(old_endpoint);
178
+ let targets = backend.list_targets().ok()?;
179
+ let team_state = state
180
+ .get("teams")
181
+ .and_then(Value::as_object)
182
+ .and_then(|teams| teams.get(team_key));
183
+ for observed in &targets {
184
+ if matches!(
185
+ classify_registered_worker_for_observed_pane(team_state.unwrap_or(state), observed),
186
+ WorkerPaneBindingMatch::LiveSameWorker { .. }
187
+ ) || leader_receiver_tuple_matches(team_state.unwrap_or(state), observed)
188
+ {
189
+ return Some(true);
190
+ }
191
+ if team_state.is_some() {
192
+ continue;
193
+ }
194
+ if let Some(teams) = state.get("teams").and_then(Value::as_object) {
195
+ if teams.values().any(|entry| {
196
+ matches!(
197
+ classify_registered_worker_for_observed_pane(entry, observed),
198
+ WorkerPaneBindingMatch::LiveSameWorker { .. }
199
+ ) || leader_receiver_tuple_matches(entry, observed)
200
+ }) {
201
+ return Some(true);
202
+ }
203
+ }
204
+ }
205
+ Some(false)
206
+ }
207
+
208
+ fn leader_receiver_tuple_matches(state: &Value, observed: &PaneInfo) -> bool {
209
+ let Some(receiver) = state.get("leader_receiver") else {
210
+ return false;
211
+ };
212
+ let Some(cached_pane_id) = non_empty_str(receiver, "pane_id") else {
213
+ return false;
214
+ };
215
+ if cached_pane_id != observed.pane_id.as_str() {
216
+ return false;
217
+ }
218
+ let expected_session = non_empty_str(receiver, "session_name")
219
+ .or_else(|| non_empty_str(state, "session_name"))
220
+ .unwrap_or_default();
221
+ if !expected_session.is_empty() && observed.session.as_str() != expected_session {
222
+ return false;
223
+ }
224
+ if let Some(expected_window) = non_empty_str(receiver, "window_name") {
225
+ if observed.window_name.as_ref().map(|w| w.as_str()) != Some(expected_window) {
226
+ return false;
227
+ }
228
+ }
229
+ if let (Some(expected_pid), Some(observed_pid)) =
230
+ (agent_pane_pid(receiver), observed.pane_pid)
231
+ {
232
+ if expected_pid != observed_pid {
233
+ return false;
234
+ }
235
+ }
236
+ true
237
+ }
238
+
118
239
  #[derive(Debug, Clone, PartialEq, Eq)]
119
240
  pub enum WorkerPaneBindingMatch {
120
241
  LiveSameWorker { agent_id: String },
@@ -329,9 +450,13 @@ fn agent_pane_pid(agent: &Value) -> Option<u32> {
329
450
  }
330
451
 
331
452
  fn session_exists_on_endpoint(endpoint: &str, session: &str) -> bool {
453
+ session_exists_on_endpoint_checked(endpoint, session).unwrap_or(false)
454
+ }
455
+
456
+ fn session_exists_on_endpoint_checked(endpoint: &str, session: &str) -> Option<bool> {
332
457
  crate::tmux_backend::TmuxBackend::for_tmux_endpoint(endpoint)
333
458
  .has_session(&SessionName::new(session.to_string()))
334
- .unwrap_or(false)
459
+ .ok()
335
460
  }
336
461
 
337
462
  fn collect_stale_endpoint(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.27",
3
+ "version": "0.5.29",
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.27",
24
- "@team-agent/cli-darwin-x64": "0.5.27",
25
- "@team-agent/cli-linux-x64": "0.5.27"
23
+ "@team-agent/cli-darwin-arm64": "0.5.29",
24
+ "@team-agent/cli-darwin-x64": "0.5.29",
25
+ "@team-agent/cli-linux-x64": "0.5.29"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",