@team-agent/installer 0.5.17 → 0.5.18

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.18"
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.18"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -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() {
@@ -13,7 +13,8 @@ use thiserror::Error;
13
13
  use crate::message_store::MessageStore;
14
14
 
15
15
  use super::types::{
16
- CoordinatorHealthStatus, CoordinatorMetadata, HealthReport, MetadataSource, Pid, SchemaError,
16
+ CoordinatorBinaryIdentity, CoordinatorHealthStatus, CoordinatorMetadata,
17
+ CoordinatorMetadataMismatchReason, HealthReport, MetadataSource, Pid, SchemaError,
17
18
  SchemaHealth, StartError, StartOutcome, StartReport, StopError, StopOutcome, StopReport,
18
19
  WatchCursor, WorkspacePath, PROTOCOL_VERSION, ROTATION_MARKER,
19
20
  };
@@ -27,6 +28,7 @@ use super::types::{
27
28
  /// `coordinator_health`(`lifecycle.py:38-46`):`running ∧ metadata_ok ∧ schema_ok` → typed report.
28
29
  pub fn coordinator_health(workspace: &WorkspacePath) -> HealthReport {
29
30
  let schema = message_store_schema_health(workspace);
31
+ let current_binary_identity = current_coordinator_binary_identity();
30
32
  let pid_path = coordinator_pid_path(workspace);
31
33
  let pid = read_pid_file(&pid_path);
32
34
  let status = match pid {
@@ -38,7 +40,14 @@ pub fn coordinator_health(workspace: &WorkspacePath) -> HealthReport {
38
40
  None => CoordinatorHealthStatus::Missing,
39
41
  };
40
42
  let metadata = read_coordinator_metadata(workspace);
41
- let metadata_ok = pid.is_some_and(|p| coordinator_metadata_ok(metadata.as_ref(), p));
43
+ let metadata_mismatch = pid
44
+ .map(|p| coordinator_metadata_mismatch_reason_with_identity(
45
+ metadata.as_ref(),
46
+ p,
47
+ &current_binary_identity,
48
+ ))
49
+ .unwrap_or(Some(CoordinatorMetadataMismatchReason::MetadataMissing));
50
+ let metadata_ok = metadata_mismatch.is_none();
42
51
  let running = matches!(status, CoordinatorHealthStatus::Running);
43
52
  HealthReport {
44
53
  ok: running && metadata_ok && schema.ok,
@@ -46,6 +55,8 @@ pub fn coordinator_health(workspace: &WorkspacePath) -> HealthReport {
46
55
  pid,
47
56
  metadata,
48
57
  metadata_ok,
58
+ metadata_mismatch_reason: metadata_mismatch.map(|reason| reason.as_str().to_string()),
59
+ current_binary_identity,
49
60
  schema,
50
61
  }
51
62
  }
@@ -69,11 +80,16 @@ pub fn start_coordinator_with_team(
69
80
  team_key: Option<&str>,
70
81
  ) -> Result<StartReport, StartError> {
71
82
  let health = coordinator_health(workspace);
83
+ let identity = health.current_binary_identity.clone();
72
84
  if health.ok {
73
85
  return Ok(StartReport {
74
86
  ok: true,
75
87
  pid: health.pid,
76
88
  status: StartOutcome::AlreadyRunning,
89
+ previous_pid: None,
90
+ binary_path: Some(identity.binary_path),
91
+ binary_version: Some(identity.binary_version),
92
+ rotation_reason: None,
77
93
  log: Some(coordinator_log_path(workspace)),
78
94
  schema_error: None,
79
95
  action: None,
@@ -84,12 +100,54 @@ pub fn start_coordinator_with_team(
84
100
  ok: false,
85
101
  pid: health.pid,
86
102
  status: StartOutcome::SchemaIncompatible,
103
+ previous_pid: None,
104
+ binary_path: Some(identity.binary_path),
105
+ binary_version: Some(identity.binary_version),
106
+ rotation_reason: health.metadata_mismatch_reason,
87
107
  log: None,
88
108
  schema_error: health.schema.error,
89
109
  action: health.schema.action,
90
110
  });
91
111
  }
92
- if health.pid.is_some() && !health.metadata_ok && health.metadata.is_some() {
112
+ let rotation_reason = if matches!(health.status, CoordinatorHealthStatus::Running)
113
+ && !health.metadata_ok
114
+ {
115
+ health.metadata_mismatch_reason.clone()
116
+ } else {
117
+ None
118
+ };
119
+ if matches!(health.status, CoordinatorHealthStatus::Running) && !health.metadata_ok {
120
+ crate::event_log::EventLog::new(workspace.as_path()).write(
121
+ "coordinator.rotation_required",
122
+ serde_json::json!({
123
+ "old_pid": health.pid.map(|pid| pid.get()),
124
+ "reason": rotation_reason.clone(),
125
+ "old_binary_path": health
126
+ .metadata
127
+ .as_ref()
128
+ .and_then(|metadata| metadata.binary_path.clone()),
129
+ "old_binary_version": health
130
+ .metadata
131
+ .as_ref()
132
+ .and_then(|metadata| metadata.binary_version.clone()),
133
+ "current_binary_path": identity.binary_path.clone(),
134
+ "current_binary_version": identity.binary_version.clone(),
135
+ }),
136
+ )?;
137
+ if health.pid.map(|pid| pid.get()) == Some(std::process::id()) {
138
+ return Ok(StartReport {
139
+ ok: false,
140
+ pid: health.pid,
141
+ status: StartOutcome::RestartIncompatibleStopFailed,
142
+ previous_pid: health.pid,
143
+ binary_path: Some(identity.binary_path),
144
+ binary_version: Some(identity.binary_version),
145
+ rotation_reason,
146
+ log: None,
147
+ schema_error: None,
148
+ action: Some("refusing to rotate coordinator metadata that points at the caller process".to_string()),
149
+ });
150
+ }
93
151
  match stop_coordinator(workspace) {
94
152
  Ok(stop) if stop.ok => {}
95
153
  Ok(_) | Err(_) => {
@@ -97,6 +155,10 @@ pub fn start_coordinator_with_team(
97
155
  ok: false,
98
156
  pid: health.pid,
99
157
  status: StartOutcome::RestartIncompatibleStopFailed,
158
+ previous_pid: health.pid,
159
+ binary_path: Some(identity.binary_path),
160
+ binary_version: Some(identity.binary_version),
161
+ rotation_reason,
100
162
  log: None,
101
163
  schema_error: None,
102
164
  action: None,
@@ -131,10 +193,19 @@ pub fn start_coordinator_with_team(
131
193
  let pid = Pid::new(child.id());
132
194
  std::fs::write(coordinator_pid_path(workspace), pid.to_string())?;
133
195
  write_coordinator_metadata(workspace, pid, MetadataSource::Start)?;
196
+ let status = if rotation_reason.is_some() {
197
+ StartOutcome::StartedAfterRotation
198
+ } else {
199
+ StartOutcome::Started
200
+ };
134
201
  Ok(StartReport {
135
202
  ok: true,
136
203
  pid: Some(pid),
137
- status: StartOutcome::Started,
204
+ status,
205
+ previous_pid: health.pid,
206
+ binary_path: Some(identity.binary_path),
207
+ binary_version: Some(identity.binary_version),
208
+ rotation_reason,
138
209
  log: Some(log_path),
139
210
  schema_error: None,
140
211
  action: None,
@@ -206,6 +277,15 @@ pub fn stop_coordinator(workspace: &WorkspacePath) -> Result<StopReport, StopErr
206
277
  pid: Some(pid),
207
278
  });
208
279
  }
280
+ if pid.get() == std::process::id() {
281
+ remove_file_if_exists(&pid_path)?;
282
+ remove_file_if_exists(&coordinator_meta_path(workspace))?;
283
+ return Ok(StopReport {
284
+ ok: true,
285
+ status: StopOutcome::Stopped,
286
+ pid: Some(pid),
287
+ });
288
+ }
209
289
  if !terminate_pid(pid) {
210
290
  return Ok(StopReport {
211
291
  ok: false,
@@ -499,15 +579,63 @@ pub fn read_coordinator_metadata(workspace: &WorkspacePath) -> Option<Coordinato
499
579
  serde_json::from_str(&text).ok()
500
580
  }
501
581
 
502
- /// `coordinator_metadata_ok`(`metadata.py:37-43`):三元全等
503
- /// `meta.pid == pid ∧ meta.protocol_version == PROTOCOL_VERSION ∧
504
- /// meta.message_store_schema_version == SCHEMA_VERSION`。任一不符 → false(不静默继续旧 schema)
582
+ pub fn current_coordinator_binary_identity() -> CoordinatorBinaryIdentity {
583
+ let binary_path = std::env::current_exe()
584
+ .map(|path| path.canonicalize().unwrap_or(path))
585
+ .map(|path| path.to_string_lossy().into_owned())
586
+ .unwrap_or_else(|_| "<unknown>".to_string());
587
+ CoordinatorBinaryIdentity {
588
+ binary_path,
589
+ binary_version: crate::packaging::Version::current().as_str().to_string(),
590
+ }
591
+ }
592
+
593
+ /// `coordinator_metadata_ok` now includes daemon binary identity in addition
594
+ /// to the original pid/protocol/schema tuple.
505
595
  pub fn coordinator_metadata_ok(metadata: Option<&CoordinatorMetadata>, pid: Pid) -> bool {
506
- metadata.is_some_and(|m| {
507
- m.pid == pid
508
- && m.protocol_version == PROTOCOL_VERSION
509
- && m.message_store_schema_version == crate::db::schema::SCHEMA_VERSION
510
- })
596
+ coordinator_metadata_mismatch_reason(metadata, pid).is_none()
597
+ }
598
+
599
+ pub fn coordinator_metadata_mismatch_reason(
600
+ metadata: Option<&CoordinatorMetadata>,
601
+ pid: Pid,
602
+ ) -> Option<CoordinatorMetadataMismatchReason> {
603
+ let identity = current_coordinator_binary_identity();
604
+ coordinator_metadata_mismatch_reason_with_identity(metadata, pid, &identity)
605
+ }
606
+
607
+ fn coordinator_metadata_mismatch_reason_with_identity(
608
+ metadata: Option<&CoordinatorMetadata>,
609
+ pid: Pid,
610
+ identity: &CoordinatorBinaryIdentity,
611
+ ) -> Option<CoordinatorMetadataMismatchReason> {
612
+ let Some(metadata) = metadata else {
613
+ return Some(CoordinatorMetadataMismatchReason::MetadataMissing);
614
+ };
615
+ if metadata.pid != pid {
616
+ return Some(CoordinatorMetadataMismatchReason::PidMismatch);
617
+ }
618
+ if metadata.protocol_version != PROTOCOL_VERSION {
619
+ return Some(CoordinatorMetadataMismatchReason::ProtocolVersionMismatch);
620
+ }
621
+ if metadata.message_store_schema_version != crate::db::schema::SCHEMA_VERSION {
622
+ return Some(CoordinatorMetadataMismatchReason::MessageStoreSchemaVersionMismatch);
623
+ }
624
+ let Some(binary_version) = metadata.binary_version.as_deref().filter(|value| !value.is_empty())
625
+ else {
626
+ return Some(CoordinatorMetadataMismatchReason::BinaryIdentityMissing);
627
+ };
628
+ let Some(binary_path) = metadata.binary_path.as_deref().filter(|value| !value.is_empty())
629
+ else {
630
+ return Some(CoordinatorMetadataMismatchReason::BinaryIdentityMissing);
631
+ };
632
+ if binary_version != identity.binary_version {
633
+ return Some(CoordinatorMetadataMismatchReason::BinaryVersionMismatch);
634
+ }
635
+ if binary_path != identity.binary_path {
636
+ return Some(CoordinatorMetadataMismatchReason::BinaryPathMismatch);
637
+ }
638
+ None
511
639
  }
512
640
 
513
641
  /// `write_coordinator_metadata`(`metadata.py:46-61`)。写 `coordinator.json`(pretty indent=2),
@@ -525,6 +653,8 @@ pub fn write_coordinator_metadata(
525
653
  pid,
526
654
  protocol_version: PROTOCOL_VERSION,
527
655
  message_store_schema_version: crate::db::schema::SCHEMA_VERSION,
656
+ binary_path: Some(current_coordinator_binary_identity().binary_path),
657
+ binary_version: Some(crate::packaging::Version::current().as_str().to_string()),
528
658
  source,
529
659
  updated_at: chrono::Utc::now().to_rfc3339(),
530
660
  };
@@ -38,6 +38,7 @@ fn status_enums_serialize_to_exact_python_strings() {
38
38
  (serde_json::to_string(&StartOutcome::RestartIncompatibleStopFailed).unwrap(), "\"restart_incompatible_stop_failed\""),
39
39
  (serde_json::to_string(&StartOutcome::SchemaIncompatible).unwrap(), "\"schema_incompatible\""),
40
40
  (serde_json::to_string(&StartOutcome::Started).unwrap(), "\"started\""),
41
+ (serde_json::to_string(&StartOutcome::StartedAfterRotation).unwrap(), "\"started_after_rotation\""),
41
42
  (serde_json::to_string(&StopOutcome::Missing).unwrap(), "\"missing\""),
42
43
  (serde_json::to_string(&StopOutcome::InvalidPidRemoved).unwrap(), "\"invalid_pid_removed\""),
43
44
  (serde_json::to_string(&StopOutcome::KillFailed).unwrap(), "\"kill_failed\""),
@@ -173,6 +174,9 @@ fn coordinator_metadata_json_field_names_are_stable() {
173
174
  assert_eq!(json["pid"], 123);
174
175
  assert_eq!(json["protocol_version"], 2);
175
176
  assert_eq!(json["message_store_schema_version"], 3);
177
+ let identity = current_coordinator_binary_identity();
178
+ assert_eq!(json["binary_path"], identity.binary_path);
179
+ assert_eq!(json["binary_version"], identity.binary_version);
176
180
  assert_eq!(json["source"], "boot");
177
181
  assert_eq!(json["updated_at"], "2026-06-02T00:00:00+00:00");
178
182
  }
@@ -13,10 +13,13 @@ fn ws() -> WorkspacePath {
13
13
  }
14
14
 
15
15
  fn meta(pid: u32, proto: u32, schema: i64) -> CoordinatorMetadata {
16
+ let identity = current_coordinator_binary_identity();
16
17
  CoordinatorMetadata {
17
18
  pid: Pid(pid),
18
19
  protocol_version: proto,
19
20
  message_store_schema_version: schema,
21
+ binary_path: Some(identity.binary_path),
22
+ binary_version: Some(identity.binary_version),
20
23
  source: MetadataSource::Boot,
21
24
  updated_at: "2026-06-02T00:00:00+00:00".to_string(),
22
25
  }