@team-agent/installer 0.5.17 → 0.5.19

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.17"
578
+ version = "0.5.19"
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.17"
12
+ version = "0.5.19"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -732,7 +732,8 @@ pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
732
732
  &selected.run_workspace,
733
733
  )),
734
734
  };
735
- let (issues, suggested_repairs) = diagnose_runtime(&state, backend.as_ref());
735
+ let (issues, suggested_repairs) =
736
+ diagnose_runtime_for_workspace(&selected.run_workspace, &state, backend.as_ref());
736
737
  let ok = issues.as_array().is_some_and(Vec::is_empty);
737
738
  Ok(CmdResult::from_json(
738
739
  json!({
@@ -165,6 +165,125 @@ pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value
165
165
  (Value::Array(issues), Value::Array(repairs))
166
166
  }
167
167
 
168
+ pub(crate) fn diagnose_runtime_for_workspace(
169
+ workspace: &std::path::Path,
170
+ state: &Value,
171
+ backend: &dyn Transport,
172
+ ) -> (Value, Value) {
173
+ let (mut issues, mut repairs) = diagnose_runtime(state, backend);
174
+ append_coordinator_health_issue(workspace, state, &mut issues, &mut repairs);
175
+ (issues, repairs)
176
+ }
177
+
178
+ fn append_coordinator_health_issue(
179
+ workspace: &std::path::Path,
180
+ state: &Value,
181
+ issues: &mut Value,
182
+ repairs: &mut Value,
183
+ ) {
184
+ let workspace = crate::coordinator::WorkspacePath::new(workspace.to_path_buf());
185
+ let health = crate::coordinator::coordinator_health(&workspace);
186
+ let Some(id) = coordinator_issue_id(state, &health) else {
187
+ return;
188
+ };
189
+ if let Some(items) = issues.as_array_mut() {
190
+ items.push(coordinator_issue_value(id, &health, workspace.as_path()));
191
+ }
192
+ if let Some(items) = repairs.as_array_mut() {
193
+ items.push(coordinator_repair_hint(id, &health));
194
+ }
195
+ }
196
+
197
+ fn coordinator_issue_id(
198
+ state: &Value,
199
+ health: &crate::coordinator::HealthReport,
200
+ ) -> Option<&'static str> {
201
+ match health.status {
202
+ crate::coordinator::CoordinatorHealthStatus::Stale
203
+ | crate::coordinator::CoordinatorHealthStatus::InvalidPid => {
204
+ Some("coordinator_unavailable")
205
+ }
206
+ crate::coordinator::CoordinatorHealthStatus::Running => {
207
+ if !health.metadata_ok {
208
+ return match health.metadata_mismatch_reason.as_deref() {
209
+ Some(
210
+ "binary_identity_missing"
211
+ | "binary_version_mismatch"
212
+ | "binary_path_mismatch",
213
+ ) => Some("coordinator_stale_identity"),
214
+ Some(_) | None => Some("coordinator_unavailable"),
215
+ };
216
+ }
217
+ if !health.schema.ok {
218
+ Some("coordinator_schema_incompatible")
219
+ } else {
220
+ None
221
+ }
222
+ }
223
+ crate::coordinator::CoordinatorHealthStatus::Missing => {
224
+ coordinator_expected(state).then_some("coordinator_unavailable")
225
+ }
226
+ }
227
+ }
228
+
229
+ fn coordinator_issue_value(
230
+ id: &str,
231
+ health: &crate::coordinator::HealthReport,
232
+ workspace: &std::path::Path,
233
+ ) -> Value {
234
+ json!({
235
+ "id": id,
236
+ "status": coordinator_status_wire(health.status),
237
+ "pid": health.pid.map(|pid| pid.get()),
238
+ "metadata_ok": health.metadata_ok,
239
+ "metadata_mismatch_reason": health.metadata_mismatch_reason.clone(),
240
+ "binary_path": health.current_binary_identity.binary_path.clone(),
241
+ "binary_version": health.current_binary_identity.binary_version.clone(),
242
+ "schema_ok": health.schema.ok,
243
+ "coordinator_log": crate::coordinator::coordinator_log_path(
244
+ &crate::coordinator::WorkspacePath::new(workspace.to_path_buf())
245
+ )
246
+ .to_string_lossy()
247
+ .to_string(),
248
+ })
249
+ }
250
+
251
+ fn coordinator_repair_hint(id: &str, _health: &crate::coordinator::HealthReport) -> Value {
252
+ let hint_action = match id {
253
+ "coordinator_schema_incompatible" => "team-agent repair-state --schema",
254
+ _ => "team-agent restart",
255
+ };
256
+ json!({
257
+ "issue": id,
258
+ "action_required": true,
259
+ "advisory": true,
260
+ "broken_class": id,
261
+ "hint_action": hint_action,
262
+ "dedupe_key": id,
263
+ "action": format!("{hint_action} # diagnose only reports coordinator health; it does not start, stop, or rotate the coordinator"),
264
+ })
265
+ }
266
+
267
+ fn coordinator_expected(state: &Value) -> bool {
268
+ state
269
+ .get("session_name")
270
+ .and_then(Value::as_str)
271
+ .is_some_and(|session| !session.is_empty())
272
+ || state
273
+ .get("agents")
274
+ .and_then(Value::as_object)
275
+ .is_some_and(|agents| !agents.is_empty())
276
+ }
277
+
278
+ fn coordinator_status_wire(status: crate::coordinator::CoordinatorHealthStatus) -> &'static str {
279
+ match status {
280
+ crate::coordinator::CoordinatorHealthStatus::Missing => "missing",
281
+ crate::coordinator::CoordinatorHealthStatus::InvalidPid => "invalid_pid",
282
+ crate::coordinator::CoordinatorHealthStatus::Running => "running",
283
+ crate::coordinator::CoordinatorHealthStatus::Stale => "stale",
284
+ }
285
+ }
286
+
168
287
  fn topology_repair_hint(issue: &str) -> Value {
169
288
  json!({
170
289
  "issue": issue,
@@ -246,6 +365,72 @@ mod tests {
246
365
  path
247
366
  }
248
367
 
368
+ fn coordinator_health_fixture(
369
+ status: crate::coordinator::CoordinatorHealthStatus,
370
+ metadata_mismatch_reason: Option<&str>,
371
+ schema_ok: bool,
372
+ ) -> crate::coordinator::HealthReport {
373
+ crate::coordinator::HealthReport {
374
+ ok: matches!(status, crate::coordinator::CoordinatorHealthStatus::Running)
375
+ && metadata_mismatch_reason.is_none()
376
+ && schema_ok,
377
+ status,
378
+ pid: Some(crate::coordinator::Pid::new(std::process::id())),
379
+ metadata: None,
380
+ metadata_ok: metadata_mismatch_reason.is_none(),
381
+ metadata_mismatch_reason: metadata_mismatch_reason.map(ToString::to_string),
382
+ current_binary_identity: crate::coordinator::CoordinatorBinaryIdentity {
383
+ binary_path: "/current/team-agent".to_string(),
384
+ binary_version: env!("CARGO_PKG_VERSION").to_string(),
385
+ },
386
+ schema: crate::coordinator::SchemaHealth {
387
+ ok: schema_ok,
388
+ schema_version: crate::db::schema::SCHEMA_VERSION,
389
+ error: None,
390
+ action: None,
391
+ },
392
+ }
393
+ }
394
+
395
+ #[test]
396
+ fn coordinator_issue_id_maps_stale_pid_to_unavailable() {
397
+ let health = coordinator_health_fixture(
398
+ crate::coordinator::CoordinatorHealthStatus::Stale,
399
+ None,
400
+ true,
401
+ );
402
+ assert_eq!(
403
+ coordinator_issue_id(&json!({"session_name": "team-fixture"}), &health),
404
+ Some("coordinator_unavailable")
405
+ );
406
+ }
407
+
408
+ #[test]
409
+ fn coordinator_issue_id_maps_binary_identity_to_stale_identity() {
410
+ let health = coordinator_health_fixture(
411
+ crate::coordinator::CoordinatorHealthStatus::Running,
412
+ Some("binary_version_mismatch"),
413
+ true,
414
+ );
415
+ assert_eq!(
416
+ coordinator_issue_id(&json!({"session_name": "team-fixture"}), &health),
417
+ Some("coordinator_stale_identity")
418
+ );
419
+ }
420
+
421
+ #[test]
422
+ fn coordinator_issue_id_maps_schema_failure_to_schema_incompatible() {
423
+ let health = coordinator_health_fixture(
424
+ crate::coordinator::CoordinatorHealthStatus::Running,
425
+ None,
426
+ false,
427
+ );
428
+ assert_eq!(
429
+ coordinator_issue_id(&json!({"session_name": "team-fixture"}), &health),
430
+ Some("coordinator_schema_incompatible")
431
+ );
432
+ }
433
+
249
434
  #[test]
250
435
  fn diagnose_surfaces_codex_session_identity_mismatch() {
251
436
  let workspace =
@@ -821,7 +821,11 @@ pub mod lifecycle_port {
821
821
  let mut coordinator_timeout = false;
822
822
  let mut coordinator_post_stop = CoordinatorStopObservation::NotNeeded;
823
823
  let mut coordinator_pid_for_report = None;
824
- let stopped = if team.is_none() {
824
+ let coordinator_stop_reason =
825
+ coordinator_stop_reason_for_shutdown(&run_workspace, team, &state)?;
826
+ let should_stop_coordinator =
827
+ matches!(coordinator_stop_reason, "bare_shutdown" | "scoped_last_live_team");
828
+ let stopped = if should_stop_coordinator {
825
829
  let wp = crate::coordinator::WorkspacePath::new(run_workspace.clone());
826
830
  let coordinator_pid_before_stop = crate::coordinator::coordinator_health(&wp).pid;
827
831
  coordinator_pid_for_report = coordinator_pid_before_stop.map(|pid| pid.get());
@@ -929,6 +933,7 @@ pub mod lifecycle_port {
929
933
  "killed_sessions": killed_sessions.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
930
934
  "spared_sessions": spared_sessions.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
931
935
  "coordinator_status": coordinator_status,
936
+ "coordinator_stop_reason": coordinator_stop_reason,
932
937
  "status": status,
933
938
  "phase": phase,
934
939
  "verification_degraded": verification_degraded,
@@ -971,9 +976,11 @@ pub mod lifecycle_port {
971
976
  "owned_files": owned_file_residuals,
972
977
  },
973
978
  "error": kill_error,
979
+ "coordinator_stop_reason": coordinator_stop_reason,
974
980
  "coordinator": {
975
981
  "status": coordinator_status,
976
982
  "pid": coordinator_pid,
983
+ "stop_reason": coordinator_stop_reason,
977
984
  },
978
985
  });
979
986
  #[cfg(windows)]
@@ -2921,6 +2928,7 @@ pub mod lifecycle_port {
2921
2928
  session_name,
2922
2929
  agents,
2923
2930
  coordinator_started,
2931
+ coordinator,
2924
2932
  next_actions,
2925
2933
  attach_commands,
2926
2934
  } => json!({
@@ -2929,6 +2937,7 @@ pub mod lifecycle_port {
2929
2937
  "session_name": session_name.as_str(),
2930
2938
  "agents": agents.iter().map(|a| a.agent_id.as_str()).collect::<Vec<_>>(),
2931
2939
  "coordinator_started": coordinator_started,
2940
+ "coordinator": crate::lifecycle::coordinator_start_summary_value(&coordinator),
2932
2941
  "next_actions": next_actions,
2933
2942
  "attach_commands": attach_commands,
2934
2943
  "reminder": crate::cli::QUICK_START_REMINDER,
@@ -2938,6 +2947,7 @@ pub mod lifecycle_port {
2938
2947
  agents,
2939
2948
  failed_agents,
2940
2949
  coordinator_started,
2950
+ coordinator,
2941
2951
  next_actions,
2942
2952
  attach_commands,
2943
2953
  } => json!({
@@ -2964,6 +2974,7 @@ pub mod lifecycle_port {
2964
2974
  ),
2965
2975
  })).collect::<Vec<_>>(),
2966
2976
  "coordinator_started": coordinator_started,
2977
+ "coordinator": crate::lifecycle::coordinator_start_summary_value(&coordinator),
2967
2978
  "next_actions": next_actions,
2968
2979
  "attach_commands": attach_commands,
2969
2980
  "reminder": crate::cli::QUICK_START_REMINDER,
@@ -3370,6 +3381,35 @@ pub mod lifecycle_port {
3370
3381
  }
3371
3382
  }
3372
3383
 
3384
+ fn coordinator_stop_reason_for_shutdown(
3385
+ workspace: &Path,
3386
+ team: Option<&str>,
3387
+ selected_state: &Value,
3388
+ ) -> Result<&'static str, CliError> {
3389
+ let Some(requested_team) = team.filter(|team| !team.is_empty()) else {
3390
+ return Ok("bare_shutdown");
3391
+ };
3392
+ let stopped_key = selected_state
3393
+ .get("active_team_key")
3394
+ .and_then(Value::as_str)
3395
+ .filter(|key| !key.is_empty())
3396
+ .unwrap_or(requested_team);
3397
+ let raw = crate::state::persist::load_runtime_state(workspace)?;
3398
+ let live_sibling = raw
3399
+ .get("teams")
3400
+ .and_then(Value::as_object)
3401
+ .is_some_and(|teams| {
3402
+ teams
3403
+ .iter()
3404
+ .any(|(key, team)| key.as_str() != stopped_key && team_has_running_agent(team))
3405
+ });
3406
+ if live_sibling {
3407
+ Ok("scoped_live_sibling_present")
3408
+ } else {
3409
+ Ok("scoped_last_live_team")
3410
+ }
3411
+ }
3412
+
3373
3413
  fn mark_matching_session_teams_stopped(
3374
3414
  state: &mut Value,
3375
3415
  session_name: Option<&crate::transport::SessionName>,
@@ -3915,10 +3955,15 @@ pub mod diagnose_port {
3915
3955
  "pid": m.pid.get(),
3916
3956
  "protocol_version": m.protocol_version,
3917
3957
  "message_store_schema_version": m.message_store_schema_version,
3958
+ "binary_path": m.binary_path,
3959
+ "binary_version": m.binary_version,
3918
3960
  "source": m.source,
3919
3961
  "updated_at": m.updated_at,
3920
3962
  })),
3921
3963
  "metadata_ok": health.metadata_ok,
3964
+ "metadata_mismatch_reason": health.metadata_mismatch_reason,
3965
+ "binary_path": health.current_binary_identity.binary_path,
3966
+ "binary_version": health.current_binary_identity.binary_version,
3922
3967
  "schema_ok": health.schema.ok,
3923
3968
  "schema_error": health.schema.error.map(|e| format!("{e:?}")),
3924
3969
  "schema": {
@@ -1151,10 +1151,15 @@ use rusqlite::params;
1151
1151
  "pid": m.pid.get(),
1152
1152
  "protocol_version": m.protocol_version,
1153
1153
  "message_store_schema_version": m.message_store_schema_version,
1154
+ "binary_path": m.binary_path,
1155
+ "binary_version": m.binary_version,
1154
1156
  "source": m.source,
1155
1157
  "updated_at": m.updated_at,
1156
1158
  })),
1157
1159
  "metadata_ok": health.metadata_ok,
1160
+ "metadata_mismatch_reason": health.metadata_mismatch_reason,
1161
+ "binary_path": health.current_binary_identity.binary_path,
1162
+ "binary_version": health.current_binary_identity.binary_version,
1158
1163
  "schema_ok": health.schema.ok,
1159
1164
  "schema_error": health.schema.error.map(|e| format!("{e:?}")),
1160
1165
  "schema": {
@@ -57,20 +57,30 @@ use super::*;
57
57
  let _ = std::fs::remove_dir_all(&ws);
58
58
  }
59
59
 
60
- // ── status #3: coordinator = coordinator_health(ws) FULL 8-key shape in golden order ─────────────
61
- // GOLDEN coordinator_health: {ok,status,pid,metadata,metadata_ok,schema_ok,schema_error,schema}.
62
- // RUST mod.rs:105-112 emits a 6-key subset {status,ok,pid,metadata_ok,schema_ok,schema_version}
63
- // (no metadata/schema_error; flat schema_version instead of nested schema). RED on key-order. ──────
60
+ // ── status #3: coordinator = coordinator_health(ws) FULL shape in golden order ──────────────────
61
+ // 0.5.18 adds binary identity diagnostics to expose stale coordinator rotation reasons.
64
62
  #[test]
65
- fn status_coordinator_is_full_health_eight_key_shape() {
63
+ fn status_coordinator_is_full_health_identity_shape() {
66
64
  let ws = tmp_workspace();
67
65
  let v = status_port::status(&ws, false, true).expect("status");
68
66
  let coord = v["coordinator"].as_object().expect("coordinator dict");
69
67
  let order: Vec<&str> = coord.keys().map(String::as_str).collect();
70
68
  assert_eq!(
71
69
  order,
72
- vec!["ok", "status", "pid", "metadata", "metadata_ok", "schema_ok", "schema_error", "schema"],
73
- "golden coordinator_health 8-key insertion order; Rust emits a 6-key subset. got {order:?}"
70
+ vec![
71
+ "ok",
72
+ "status",
73
+ "pid",
74
+ "metadata",
75
+ "metadata_ok",
76
+ "metadata_mismatch_reason",
77
+ "binary_path",
78
+ "binary_version",
79
+ "schema_ok",
80
+ "schema_error",
81
+ "schema"
82
+ ],
83
+ "golden coordinator_health insertion order with 0.5.18 identity fields; got {order:?}"
74
84
  );
75
85
  assert!(
76
86
  coord.get("schema_version").is_none(),
@@ -61,18 +61,14 @@ pub fn run_daemon(args: DaemonArgs) -> Result<(), DaemonError> {
61
61
  // seed-state trap where writing active_team_key to state ahead
62
62
  // of coord spawn made downstream launch code think "existing
63
63
  // runtime, use restart" and skip spec compile.
64
- let team_key_from_state = args
65
- .team_key
66
- .clone()
67
- .filter(|s| !s.is_empty())
68
- .or_else(|| {
69
- state
70
- .as_ref()
71
- .and_then(|s| s.get("active_team_key"))
72
- .and_then(|v| v.as_str())
73
- .filter(|s| !s.is_empty())
74
- .map(str::to_string)
75
- });
64
+ let team_key_from_state = args.team_key.clone().filter(|s| !s.is_empty()).or_else(|| {
65
+ state
66
+ .as_ref()
67
+ .and_then(|s| s.get("active_team_key"))
68
+ .and_then(|v| v.as_str())
69
+ .filter(|s| !s.is_empty())
70
+ .map(str::to_string)
71
+ });
76
72
  let mut factory_input = crate::transport_factory::TransportFactoryInput::new(
77
73
  args.workspace.as_path(),
78
74
  crate::transport_factory::TransportPurpose::Coordinator,
@@ -104,7 +100,8 @@ pub fn run_daemon(args: DaemonArgs) -> Result<(), DaemonError> {
104
100
  args.workspace.clone(),
105
101
  Box::new(RealProviderRegistry),
106
102
  Box::new(sel.backend),
107
- );
103
+ )
104
+ .with_team_key(team_key_from_state.clone());
108
105
  return run_daemon_with_coordinator_and_boot_tmux(&args, &coord, Some(metadata));
109
106
  }
110
107
  };
@@ -120,7 +117,8 @@ pub fn run_daemon(args: DaemonArgs) -> Result<(), DaemonError> {
120
117
  args.workspace.clone(),
121
118
  Box::new(RealProviderRegistry),
122
119
  resolved.backend,
123
- );
120
+ )
121
+ .with_team_key(team_key_from_state.clone());
124
122
  run_daemon_with_coordinator_and_boot_tmux(&args, &coordinator, Some(tmux_metadata))
125
123
  }
126
124
 
@@ -147,11 +145,14 @@ fn run_daemon_with_coordinator_and_boot_tmux(
147
145
  let pid = Pid::new(std::process::id());
148
146
  std::fs::write(coordinator_pid_path(&args.workspace), pid.to_string())?;
149
147
  write_coordinator_metadata(&args.workspace, pid, MetadataSource::Boot)?;
148
+ let binary_identity = crate::coordinator::current_coordinator_binary_identity();
150
149
 
151
150
  let event_log = EventLog::new(args.workspace.as_path());
152
151
  let mut boot_event = serde_json::json!({
153
152
  "workspace": args.workspace.as_path().to_string_lossy(),
154
153
  "once": args.once,
154
+ "binary_path": binary_identity.binary_path,
155
+ "binary_version": binary_identity.binary_version,
155
156
  });
156
157
  if let Some(metadata) = tmux_metadata {
157
158
  if let Some(object) = boot_event.as_object_mut() {