@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 +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/mod.rs +46 -1
- package/crates/team-agent/src/cli/status_port.rs +5 -0
- package/crates/team-agent/src/cli/tests/lane_c.rs +17 -7
- package/crates/team-agent/src/coordinator/backoff.rs +15 -14
- package/crates/team-agent/src/coordinator/health.rs +142 -12
- package/crates/team-agent/src/coordinator/tests/basics.rs +4 -0
- package/crates/team-agent/src/coordinator/tests/mod.rs +3 -0
- package/crates/team-agent/src/coordinator/tick.rs +70 -68
- package/crates/team-agent/src/coordinator/types.rs +47 -0
- package/crates/team-agent/src/diagnose/orphans.rs +35 -13
- package/crates/team-agent/src/lifecycle/restart/agent.rs +4 -2
- package/crates/team-agent/src/lifecycle/restart/common.rs +6 -3
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +34 -20
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +133 -11
- package/crates/team-agent/src/lifecycle/types.rs +48 -0
- package/crates/team-agent/src/messaging/results.rs +7 -0
- package/package.json +4 -4
|
@@ -107,20 +107,20 @@ pub enum TickError {
|
|
|
107
107
|
/// returns `None` and the tick falls back to the raw state — preserving
|
|
108
108
|
/// behavior for legacy single-team workspaces and tests that don't seed
|
|
109
109
|
/// `teams.<key>`. Sibling teams under `state.teams.*` are NOT touched.
|
|
110
|
-
fn coordinator_team_scoped_state(
|
|
110
|
+
fn coordinator_team_scoped_state(
|
|
111
|
+
workspace: &std::path::Path,
|
|
112
|
+
raw_state: &Value,
|
|
113
|
+
daemon_team_key: Option<&str>,
|
|
114
|
+
) -> Option<Value> {
|
|
115
|
+
let teams = raw_state.get("teams").and_then(Value::as_object)?;
|
|
111
116
|
let active = raw_state
|
|
112
117
|
.get("active_team_key")
|
|
113
118
|
.and_then(Value::as_str)
|
|
114
|
-
.filter(|s| !s.is_empty())
|
|
115
|
-
let
|
|
116
|
-
.
|
|
117
|
-
.
|
|
118
|
-
|
|
119
|
-
.unwrap_or(false);
|
|
120
|
-
if !has_team_entry {
|
|
121
|
-
return None;
|
|
122
|
-
}
|
|
123
|
-
crate::state::projection::select_runtime_state(workspace, Some(active)).ok()
|
|
119
|
+
.filter(|s| !s.is_empty());
|
|
120
|
+
let selected = daemon_team_key
|
|
121
|
+
.filter(|key| !key.is_empty() && teams.contains_key(*key))
|
|
122
|
+
.or_else(|| active.filter(|key| teams.contains_key(*key)))?;
|
|
123
|
+
crate::state::projection::select_runtime_state(workspace, Some(selected)).ok()
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
// ===========================================================================
|
|
@@ -149,6 +149,10 @@ pub struct Coordinator {
|
|
|
149
149
|
/// transport 控制面(tmux session 存活探测等;经 trait 注入,可 mock)。
|
|
150
150
|
#[allow(dead_code)]
|
|
151
151
|
transport: Box<dyn crate::transport::Transport>,
|
|
152
|
+
/// Daemon CLI-selected team key. When present, tick projection uses this
|
|
153
|
+
/// instead of a stale root active_team_key.
|
|
154
|
+
#[allow(dead_code)]
|
|
155
|
+
daemon_team_key: Option<String>,
|
|
152
156
|
/// bug-084 save 注入钩。`None` ⇔ 真实 `state::save_runtime_state`。
|
|
153
157
|
#[allow(dead_code)]
|
|
154
158
|
save_hook: Option<SaveHook>,
|
|
@@ -168,11 +172,17 @@ impl Coordinator {
|
|
|
168
172
|
workspace,
|
|
169
173
|
provider_registry,
|
|
170
174
|
transport,
|
|
175
|
+
daemon_team_key: None,
|
|
171
176
|
save_hook: None,
|
|
172
177
|
order_recorder: None,
|
|
173
178
|
}
|
|
174
179
|
}
|
|
175
180
|
|
|
181
|
+
pub(crate) fn with_team_key(mut self, team_key: Option<String>) -> Self {
|
|
182
|
+
self.daemon_team_key = team_key.filter(|key| !key.is_empty());
|
|
183
|
+
self
|
|
184
|
+
}
|
|
185
|
+
|
|
176
186
|
/// 测试装配:直接构出 `Coordinator`(不经 `new` 的 `unimplemented!()`),注入 mock
|
|
177
187
|
/// transport + mock provider registry + 可选 save 注入钩 + ORDER 探针。**纯 test-support
|
|
178
188
|
/// 脚手架**(真实 impl,非 `unimplemented!()`):它只装配字段,不执行任何 daemon 逻辑;
|
|
@@ -189,6 +199,7 @@ impl Coordinator {
|
|
|
189
199
|
workspace,
|
|
190
200
|
provider_registry,
|
|
191
201
|
transport,
|
|
202
|
+
daemon_team_key: None,
|
|
192
203
|
save_hook,
|
|
193
204
|
order_recorder,
|
|
194
205
|
}
|
|
@@ -224,8 +235,12 @@ impl Coordinator {
|
|
|
224
235
|
// `team-prerelease-040-round3b`) and emit `coordinator.session_missing`
|
|
225
236
|
// even though the right session is alive. Fall back to raw state when
|
|
226
237
|
// no team scope can be derived (legacy single-team workspaces).
|
|
227
|
-
let mut state = coordinator_team_scoped_state(
|
|
228
|
-
.
|
|
238
|
+
let mut state = coordinator_team_scoped_state(
|
|
239
|
+
self.workspace.as_path(),
|
|
240
|
+
&raw_state,
|
|
241
|
+
self.daemon_team_key.as_deref(),
|
|
242
|
+
)
|
|
243
|
+
.unwrap_or(raw_state);
|
|
229
244
|
let store = crate::message_store::MessageStore::open(self.workspace.as_path())?;
|
|
230
245
|
let event_log = EventLog::new(self.workspace.as_path());
|
|
231
246
|
increment_coordinator_tick_iteration_count(&self.workspace);
|
|
@@ -1264,27 +1279,7 @@ impl Coordinator {
|
|
|
1264
1279
|
/// `coordinator_health`(`lifecycle.py:26`)。pid + meta + schema 三合一健康。
|
|
1265
1280
|
/// doctor / start 前置调它。`ok = running ∧ metadata_ok ∧ schema_ok`。
|
|
1266
1281
|
pub fn health(&self) -> Result<HealthReport, TickError> {
|
|
1267
|
-
|
|
1268
|
-
let pid_path = coordinator_pid_path(&self.workspace);
|
|
1269
|
-
let pid = read_pid_file(&pid_path);
|
|
1270
|
-
let (status, running) = match pid {
|
|
1271
|
-
Some(pid) if pid_is_running(pid).unwrap_or(false) => {
|
|
1272
|
-
(CoordinatorHealthStatus::Running, true)
|
|
1273
|
-
}
|
|
1274
|
-
Some(_) => (CoordinatorHealthStatus::Stale, false),
|
|
1275
|
-
None if pid_path.exists() => (CoordinatorHealthStatus::InvalidPid, false),
|
|
1276
|
-
None => (CoordinatorHealthStatus::Missing, false),
|
|
1277
|
-
};
|
|
1278
|
-
let metadata = read_coordinator_metadata(&self.workspace);
|
|
1279
|
-
let metadata_ok = pid.is_some_and(|p| coordinator_metadata_ok(metadata.as_ref(), p));
|
|
1280
|
-
Ok(HealthReport {
|
|
1281
|
-
ok: running && metadata_ok && schema.ok,
|
|
1282
|
-
status,
|
|
1283
|
-
pid,
|
|
1284
|
-
metadata,
|
|
1285
|
-
metadata_ok,
|
|
1286
|
-
schema,
|
|
1287
|
-
})
|
|
1282
|
+
Ok(super::health::coordinator_health(&self.workspace))
|
|
1288
1283
|
}
|
|
1289
1284
|
|
|
1290
1285
|
/// `start_coordinator`(`lifecycle.py:49`)。幂等启动:已健康 no-op;metadata 不兼容先 stop 再起;
|
|
@@ -1292,40 +1287,7 @@ impl Coordinator {
|
|
|
1292
1287
|
/// Python 是 `python -m team_agent.coordinator`,`lifecycle.py:108`)。
|
|
1293
1288
|
/// **schema 兼容门**:三元任一不匹配 → restart_incompatible,**不可静默继续**(card §89)。
|
|
1294
1289
|
pub fn start(&self) -> Result<StartReport, StartError> {
|
|
1295
|
-
|
|
1296
|
-
.health()
|
|
1297
|
-
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
|
1298
|
-
if health.ok {
|
|
1299
|
-
return Ok(StartReport {
|
|
1300
|
-
ok: true,
|
|
1301
|
-
pid: health.pid,
|
|
1302
|
-
status: StartOutcome::AlreadyRunning,
|
|
1303
|
-
log: Some(coordinator_log_path(&self.workspace)),
|
|
1304
|
-
schema_error: None,
|
|
1305
|
-
action: None,
|
|
1306
|
-
});
|
|
1307
|
-
}
|
|
1308
|
-
if !health.schema.ok {
|
|
1309
|
-
return Ok(StartReport {
|
|
1310
|
-
ok: false,
|
|
1311
|
-
pid: health.pid,
|
|
1312
|
-
status: StartOutcome::SchemaIncompatible,
|
|
1313
|
-
log: None,
|
|
1314
|
-
schema_error: health.schema.error,
|
|
1315
|
-
action: health.schema.action,
|
|
1316
|
-
});
|
|
1317
|
-
}
|
|
1318
|
-
let pid = Pid::new(std::process::id());
|
|
1319
|
-
write_coordinator_metadata(&self.workspace, pid, MetadataSource::Start)?;
|
|
1320
|
-
std::fs::write(coordinator_pid_path(&self.workspace), pid.to_string())?;
|
|
1321
|
-
Ok(StartReport {
|
|
1322
|
-
ok: true,
|
|
1323
|
-
pid: Some(pid),
|
|
1324
|
-
status: StartOutcome::Started,
|
|
1325
|
-
log: Some(coordinator_log_path(&self.workspace)),
|
|
1326
|
-
schema_error: None,
|
|
1327
|
-
action: None,
|
|
1328
|
-
})
|
|
1290
|
+
super::health::start_coordinator(&self.workspace)
|
|
1329
1291
|
}
|
|
1330
1292
|
|
|
1331
1293
|
/// `stop_coordinator`(`lifecycle.py:229`)。SIGTERM + 清 pid/meta。pid 非整数 → 清文件返回。
|
|
@@ -2544,4 +2506,44 @@ mod u1_tests {
|
|
|
2544
2506
|
"capture_missing failure must be visible in events.jsonl; got {events}"
|
|
2545
2507
|
);
|
|
2546
2508
|
}
|
|
2509
|
+
|
|
2510
|
+
#[test]
|
|
2511
|
+
fn daemon_team_key_projection_beats_stale_root_active_team_key() {
|
|
2512
|
+
let dir = std::env::temp_dir().join(format!(
|
|
2513
|
+
"team-agent-daemon-team-key-{}-{}",
|
|
2514
|
+
std::process::id(),
|
|
2515
|
+
std::time::SystemTime::now()
|
|
2516
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
2517
|
+
.unwrap()
|
|
2518
|
+
.as_nanos()
|
|
2519
|
+
));
|
|
2520
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
2521
|
+
let raw = serde_json::json!({
|
|
2522
|
+
"active_team_key": "stale-b",
|
|
2523
|
+
"session_name": "team-stale-b",
|
|
2524
|
+
"teams": {
|
|
2525
|
+
"fresh-a": {
|
|
2526
|
+
"active_team_key": "fresh-a",
|
|
2527
|
+
"team_key": "fresh-a",
|
|
2528
|
+
"session_name": "team-fresh-a",
|
|
2529
|
+
"agents": {}
|
|
2530
|
+
},
|
|
2531
|
+
"stale-b": {
|
|
2532
|
+
"active_team_key": "stale-b",
|
|
2533
|
+
"team_key": "stale-b",
|
|
2534
|
+
"session_name": "team-stale-b",
|
|
2535
|
+
"agents": {}
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
});
|
|
2539
|
+
crate::state::persist::save_runtime_state(&dir, &raw).unwrap();
|
|
2540
|
+
|
|
2541
|
+
let selected =
|
|
2542
|
+
coordinator_team_scoped_state(&dir, &raw, Some("fresh-a")).expect("selected team");
|
|
2543
|
+
|
|
2544
|
+
assert_eq!(
|
|
2545
|
+
selected.get("session_name").and_then(Value::as_str),
|
|
2546
|
+
Some("team-fresh-a")
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2547
2549
|
}
|
|
@@ -112,6 +112,8 @@ pub enum StartOutcome {
|
|
|
112
112
|
SchemaIncompatible,
|
|
113
113
|
/// 正常 spawn(`lifecycle.py:121`)。
|
|
114
114
|
Started,
|
|
115
|
+
/// 当前 CLI 发现 live daemon 的 binary identity 过期,stop 后重新 spawn。
|
|
116
|
+
StartedAfterRotation,
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
/// `stop_coordinator` 结果 status(`lifecycle.py:232,238,243,247`)。
|
|
@@ -170,11 +172,50 @@ pub struct CoordinatorMetadata {
|
|
|
170
172
|
pub pid: Pid,
|
|
171
173
|
pub protocol_version: u32,
|
|
172
174
|
pub message_store_schema_version: i64,
|
|
175
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
176
|
+
pub binary_path: Option<String>,
|
|
177
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
178
|
+
pub binary_version: Option<String>,
|
|
173
179
|
pub source: MetadataSource,
|
|
174
180
|
/// ISO8601(`datetime.now(timezone.utc).isoformat()`,`metadata.py:56`)。
|
|
175
181
|
pub updated_at: String,
|
|
176
182
|
}
|
|
177
183
|
|
|
184
|
+
/// Current coordinator daemon binary identity. Version comes from the compiled
|
|
185
|
+
/// package; path comes from the currently executing CLI binary, not PATH.
|
|
186
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
187
|
+
pub struct CoordinatorBinaryIdentity {
|
|
188
|
+
pub binary_path: String,
|
|
189
|
+
pub binary_version: String,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// Stable machine-readable reason for coordinator metadata rejection.
|
|
193
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
194
|
+
#[serde(rename_all = "snake_case")]
|
|
195
|
+
pub enum CoordinatorMetadataMismatchReason {
|
|
196
|
+
MetadataMissing,
|
|
197
|
+
PidMismatch,
|
|
198
|
+
ProtocolVersionMismatch,
|
|
199
|
+
MessageStoreSchemaVersionMismatch,
|
|
200
|
+
BinaryIdentityMissing,
|
|
201
|
+
BinaryVersionMismatch,
|
|
202
|
+
BinaryPathMismatch,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
impl CoordinatorMetadataMismatchReason {
|
|
206
|
+
pub fn as_str(self) -> &'static str {
|
|
207
|
+
match self {
|
|
208
|
+
Self::MetadataMissing => "metadata_missing",
|
|
209
|
+
Self::PidMismatch => "pid_mismatch",
|
|
210
|
+
Self::ProtocolVersionMismatch => "protocol_version_mismatch",
|
|
211
|
+
Self::MessageStoreSchemaVersionMismatch => "message_store_schema_version_mismatch",
|
|
212
|
+
Self::BinaryIdentityMissing => "binary_identity_missing",
|
|
213
|
+
Self::BinaryVersionMismatch => "binary_version_mismatch",
|
|
214
|
+
Self::BinaryPathMismatch => "binary_path_mismatch",
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
178
219
|
// ===========================================================================
|
|
179
220
|
// SchemaHealth(lifecycle.py:197-226)
|
|
180
221
|
// ===========================================================================
|
|
@@ -244,6 +285,8 @@ pub struct HealthReport {
|
|
|
244
285
|
pub metadata: Option<CoordinatorMetadata>,
|
|
245
286
|
/// 三元全等(`coordinator_metadata_ok`,`metadata.py:37-43`)。
|
|
246
287
|
pub metadata_ok: bool,
|
|
288
|
+
pub metadata_mismatch_reason: Option<String>,
|
|
289
|
+
pub current_binary_identity: CoordinatorBinaryIdentity,
|
|
247
290
|
pub schema: SchemaHealth,
|
|
248
291
|
}
|
|
249
292
|
|
|
@@ -253,6 +296,10 @@ pub struct StartReport {
|
|
|
253
296
|
pub ok: bool,
|
|
254
297
|
pub pid: Option<Pid>,
|
|
255
298
|
pub status: StartOutcome,
|
|
299
|
+
pub previous_pid: Option<Pid>,
|
|
300
|
+
pub binary_path: Option<String>,
|
|
301
|
+
pub binary_version: Option<String>,
|
|
302
|
+
pub rotation_reason: Option<String>,
|
|
256
303
|
/// coordinator.log 路径(成功路径,`lifecycle.py:54/121`)。
|
|
257
304
|
pub log: Option<PathBuf>,
|
|
258
305
|
/// schema_incompatible 时的修复 hint / 失败原因。
|
|
@@ -9,7 +9,9 @@ use crate::cli::CliError;
|
|
|
9
9
|
use crate::coordinator::health::{
|
|
10
10
|
coordinator_metadata_ok, pid_is_running, read_coordinator_metadata, terminate_pid_tree,
|
|
11
11
|
};
|
|
12
|
-
use crate::coordinator::types::{
|
|
12
|
+
use crate::coordinator::types::{
|
|
13
|
+
CoordinatorMetadata, OrphanReason, Pid, WorkspacePath, PROTOCOL_VERSION,
|
|
14
|
+
};
|
|
13
15
|
// Phase 1d Batch 6: `TmuxBackend` import removed — all sites now use
|
|
14
16
|
// `transport_factory::tmux_socket_name_transport` for grep-visibility.
|
|
15
17
|
use crate::transport::{SessionName, Transport};
|
|
@@ -477,8 +479,7 @@ fn ps_command_rows() -> Vec<ProcessRow> {
|
|
|
477
479
|
Command::new("ps").args(["-axww", "-o", "pid=,command="]),
|
|
478
480
|
"ps_table",
|
|
479
481
|
None,
|
|
480
|
-
)
|
|
481
|
-
{
|
|
482
|
+
) {
|
|
482
483
|
Ok(output) if output.status.success() => output,
|
|
483
484
|
_ => return Vec::new(),
|
|
484
485
|
};
|
|
@@ -490,9 +491,7 @@ fn ps_command_rows() -> Vec<ProcessRow> {
|
|
|
490
491
|
|
|
491
492
|
fn parse_ps_command_line(line: &str) -> Option<ProcessRow> {
|
|
492
493
|
let line = line.trim_start();
|
|
493
|
-
let split = line
|
|
494
|
-
.find(char::is_whitespace)
|
|
495
|
-
.unwrap_or(line.len());
|
|
494
|
+
let split = line.find(char::is_whitespace).unwrap_or(line.len());
|
|
496
495
|
let pid = line.get(..split)?.trim().parse::<u32>().ok()?;
|
|
497
496
|
let command = line.get(split..)?.trim().to_string();
|
|
498
497
|
Some(ProcessRow {
|
|
@@ -568,7 +567,7 @@ fn classify_workspace_orphan(workspace: &Path, pid: Pid) -> Option<OrphanReason>
|
|
|
568
567
|
}
|
|
569
568
|
let workspace_path = WorkspacePath::new(workspace.to_path_buf());
|
|
570
569
|
let metadata = read_coordinator_metadata(&workspace_path);
|
|
571
|
-
if
|
|
570
|
+
if coordinator_process_metadata_mismatch(metadata.as_ref(), pid) {
|
|
572
571
|
return Some(OrphanReason::MetadataMismatch);
|
|
573
572
|
}
|
|
574
573
|
if pid_is_running(pid).ok() == Some(false) {
|
|
@@ -577,6 +576,15 @@ fn classify_workspace_orphan(workspace: &Path, pid: Pid) -> Option<OrphanReason>
|
|
|
577
576
|
None
|
|
578
577
|
}
|
|
579
578
|
|
|
579
|
+
fn coordinator_process_metadata_mismatch(metadata: Option<&CoordinatorMetadata>, pid: Pid) -> bool {
|
|
580
|
+
let Some(metadata) = metadata else {
|
|
581
|
+
return false;
|
|
582
|
+
};
|
|
583
|
+
metadata.pid != pid
|
|
584
|
+
|| metadata.protocol_version != PROTOCOL_VERSION
|
|
585
|
+
|| metadata.message_store_schema_version != crate::db::schema::SCHEMA_VERSION
|
|
586
|
+
}
|
|
587
|
+
|
|
580
588
|
fn classify_workspace_without_pid(workspace: &Path) -> Option<OrphanReason> {
|
|
581
589
|
if !workspace.is_absolute() {
|
|
582
590
|
return None;
|
|
@@ -592,10 +600,7 @@ fn classify_workspace_without_pid(workspace: &Path) -> Option<OrphanReason> {
|
|
|
592
600
|
|
|
593
601
|
fn ephemeral_workspace_hint(workspace: &Path) -> Option<String> {
|
|
594
602
|
let text = workspace.to_string_lossy();
|
|
595
|
-
let patterns = [
|
|
596
|
-
"ta_doctor_comms_orphans-",
|
|
597
|
-
"team-agent-watcher-dedupe",
|
|
598
|
-
];
|
|
603
|
+
let patterns = ["ta_doctor_comms_orphans-", "team-agent-watcher-dedupe"];
|
|
599
604
|
patterns
|
|
600
605
|
.iter()
|
|
601
606
|
.find(|pattern| text.contains(**pattern))
|
|
@@ -626,8 +631,7 @@ fn ps_parent_map() -> BTreeMap<u32, u32> {
|
|
|
626
631
|
Command::new("ps").args(["-axo", "pid=,ppid="]),
|
|
627
632
|
"ps_parent",
|
|
628
633
|
None,
|
|
629
|
-
)
|
|
630
|
-
{
|
|
634
|
+
) {
|
|
631
635
|
Ok(output) if output.status.success() => output,
|
|
632
636
|
_ => return BTreeMap::new(),
|
|
633
637
|
};
|
|
@@ -732,4 +736,22 @@ mod tests {
|
|
|
732
736
|
Some(PathBuf::from("/tmp/My Agent"))
|
|
733
737
|
);
|
|
734
738
|
}
|
|
739
|
+
|
|
740
|
+
#[test]
|
|
741
|
+
fn process_orphan_metadata_ignores_binary_identity_rotation() {
|
|
742
|
+
let metadata = CoordinatorMetadata {
|
|
743
|
+
pid: Pid::new(123),
|
|
744
|
+
protocol_version: PROTOCOL_VERSION,
|
|
745
|
+
message_store_schema_version: crate::db::schema::SCHEMA_VERSION,
|
|
746
|
+
binary_path: Some("/old/team-agent".to_string()),
|
|
747
|
+
binary_version: Some("0.5.17".to_string()),
|
|
748
|
+
source: crate::coordinator::MetadataSource::Boot,
|
|
749
|
+
updated_at: "2026-07-09T00:00:00+00:00".to_string(),
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
assert!(
|
|
753
|
+
!coordinator_process_metadata_mismatch(Some(&metadata), Pid::new(123)),
|
|
754
|
+
"binary identity drift is a rotation signal, not orphan cleanup proof"
|
|
755
|
+
);
|
|
756
|
+
}
|
|
735
757
|
}
|
|
@@ -157,7 +157,8 @@ pub(crate) fn start_agent_at_paths(
|
|
|
157
157
|
write_team_state(spec_workspace, &spec, &state)?;
|
|
158
158
|
}
|
|
159
159
|
replay_worker_target_missing_messages(workspace, agent_id, &team_key, &state, transport)?;
|
|
160
|
-
let
|
|
160
|
+
let coordinator = start_coordinator_for_workspace(workspace, Some(&team_key))?;
|
|
161
|
+
let coordinator_started = coordinator.ok;
|
|
161
162
|
let target = format!("{}:{window}", session_name.as_str());
|
|
162
163
|
if let Some(new_binding) = refreshed_binding.as_ref() {
|
|
163
164
|
if old_binding.as_ref() != Some(new_binding) {
|
|
@@ -310,7 +311,8 @@ pub(crate) fn start_agent_at_paths(
|
|
|
310
311
|
tmux_start_mode_for_spawn(&spawn, into_existing_session),
|
|
311
312
|
)?;
|
|
312
313
|
replay_worker_target_missing_messages(workspace, agent_id, &team_key, &state, transport)?;
|
|
313
|
-
let
|
|
314
|
+
let coordinator = start_coordinator_for_workspace(workspace, Some(&team_key))?;
|
|
315
|
+
let coordinator_started = coordinator.ok;
|
|
314
316
|
Ok(StartAgentOutcome::Running {
|
|
315
317
|
env: AgentActionEnvelope {
|
|
316
318
|
agent_id: agent_id.clone(),
|
|
@@ -449,10 +449,13 @@ fn window_present_in_live(
|
|
|
449
449
|
false
|
|
450
450
|
}
|
|
451
451
|
|
|
452
|
-
pub(super) fn start_coordinator_for_workspace(
|
|
452
|
+
pub(super) fn start_coordinator_for_workspace(
|
|
453
|
+
workspace: &Path,
|
|
454
|
+
team_key: Option<&str>,
|
|
455
|
+
) -> Result<crate::lifecycle::CoordinatorStartSummary, LifecycleError> {
|
|
453
456
|
let workspace = crate::coordinator::WorkspacePath::new(workspace.to_path_buf());
|
|
454
|
-
crate::coordinator::
|
|
455
|
-
.map(|report| report
|
|
457
|
+
crate::coordinator::health::start_coordinator_with_team(&workspace, team_key)
|
|
458
|
+
.map(|report| crate::lifecycle::CoordinatorStartSummary::from_start_report(&report))
|
|
456
459
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
457
460
|
}
|
|
458
461
|
|
|
@@ -590,6 +590,7 @@ fn restart_with_selected_team_and_transport(
|
|
|
590
590
|
&successful_agents,
|
|
591
591
|
&failed_agents,
|
|
592
592
|
"fail",
|
|
593
|
+
None,
|
|
593
594
|
)?;
|
|
594
595
|
return Ok(RestartReport::Failed {
|
|
595
596
|
session_name,
|
|
@@ -606,6 +607,7 @@ fn restart_with_selected_team_and_transport(
|
|
|
606
607
|
&successful_agents,
|
|
607
608
|
&failed_agents,
|
|
608
609
|
"fail",
|
|
610
|
+
None,
|
|
609
611
|
)?;
|
|
610
612
|
return Ok(RestartReport::Failed {
|
|
611
613
|
session_name,
|
|
@@ -824,6 +826,7 @@ fn restart_with_selected_team_and_transport(
|
|
|
824
826
|
&successful_agents,
|
|
825
827
|
&failed_agents,
|
|
826
828
|
"fail",
|
|
829
|
+
None,
|
|
827
830
|
)?;
|
|
828
831
|
return Ok(RestartReport::Failed {
|
|
829
832
|
session_name,
|
|
@@ -833,7 +836,9 @@ fn restart_with_selected_team_and_transport(
|
|
|
833
836
|
});
|
|
834
837
|
}
|
|
835
838
|
drop(lifecycle_lock);
|
|
836
|
-
let
|
|
839
|
+
let coordinator =
|
|
840
|
+
start_coordinator_for_workspace(&selected.run_workspace, Some(&selected.team_key))?;
|
|
841
|
+
let coordinator_started = coordinator.ok;
|
|
837
842
|
wait_restart_readiness_or_timeout(
|
|
838
843
|
&selected.run_workspace,
|
|
839
844
|
&state,
|
|
@@ -862,6 +867,7 @@ fn restart_with_selected_team_and_transport(
|
|
|
862
867
|
&successful_agents,
|
|
863
868
|
&failed_agents,
|
|
864
869
|
"partial",
|
|
870
|
+
Some(&coordinator),
|
|
865
871
|
)?;
|
|
866
872
|
// 0.3.30 Bug 1: auto-attach on partial restart too — workers that did
|
|
867
873
|
// come up still need a leader_receiver pane to deliver report_result.
|
|
@@ -875,6 +881,7 @@ fn restart_with_selected_team_and_transport(
|
|
|
875
881
|
agents: successful_agents,
|
|
876
882
|
failed_agents,
|
|
877
883
|
coordinator_started,
|
|
884
|
+
coordinator,
|
|
878
885
|
next_actions,
|
|
879
886
|
attach_commands,
|
|
880
887
|
});
|
|
@@ -884,6 +891,7 @@ fn restart_with_selected_team_and_transport(
|
|
|
884
891
|
&successful_agents,
|
|
885
892
|
&failed_agents,
|
|
886
893
|
"ok",
|
|
894
|
+
Some(&coordinator),
|
|
887
895
|
)?;
|
|
888
896
|
// 0.3.30 Bug 1: auto-attach leader from caller's TMUX_PANE if available.
|
|
889
897
|
// Mirrors quick-start's seed_launched_owner_from_env behaviour: a restart
|
|
@@ -900,6 +908,7 @@ fn restart_with_selected_team_and_transport(
|
|
|
900
908
|
session_name,
|
|
901
909
|
agents: successful_agents,
|
|
902
910
|
coordinator_started,
|
|
911
|
+
coordinator,
|
|
903
912
|
next_actions,
|
|
904
913
|
attach_commands,
|
|
905
914
|
})
|
|
@@ -1953,27 +1962,32 @@ fn write_restart_completed_event(
|
|
|
1953
1962
|
successful_agents: &[RestartedAgent],
|
|
1954
1963
|
failed_agents: &[RestartFailedAgent],
|
|
1955
1964
|
rc: &str,
|
|
1965
|
+
coordinator: Option<&crate::lifecycle::CoordinatorStartSummary>,
|
|
1956
1966
|
) -> Result<(), LifecycleError> {
|
|
1967
|
+
let mut payload = serde_json::json!({
|
|
1968
|
+
"rc": rc,
|
|
1969
|
+
"status": rc,
|
|
1970
|
+
"successful_agents": successful_agents
|
|
1971
|
+
.iter()
|
|
1972
|
+
.map(|agent| agent.agent_id.as_str())
|
|
1973
|
+
.collect::<Vec<_>>(),
|
|
1974
|
+
"failed_agents": failed_agents
|
|
1975
|
+
.iter()
|
|
1976
|
+
.map(|failure| serde_json::json!({
|
|
1977
|
+
"agent_id": failure.agent_id.as_str(),
|
|
1978
|
+
"phase": failure.phase,
|
|
1979
|
+
"error": failure.error,
|
|
1980
|
+
}))
|
|
1981
|
+
.collect::<Vec<_>>(),
|
|
1982
|
+
});
|
|
1983
|
+
if let (Some(object), Some(coordinator)) = (payload.as_object_mut(), coordinator) {
|
|
1984
|
+
object.insert(
|
|
1985
|
+
"coordinator".to_string(),
|
|
1986
|
+
crate::lifecycle::coordinator_start_summary_value(coordinator),
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1957
1989
|
crate::event_log::EventLog::new(workspace)
|
|
1958
|
-
.write(
|
|
1959
|
-
"restart.completed",
|
|
1960
|
-
serde_json::json!({
|
|
1961
|
-
"rc": rc,
|
|
1962
|
-
"status": rc,
|
|
1963
|
-
"successful_agents": successful_agents
|
|
1964
|
-
.iter()
|
|
1965
|
-
.map(|agent| agent.agent_id.as_str())
|
|
1966
|
-
.collect::<Vec<_>>(),
|
|
1967
|
-
"failed_agents": failed_agents
|
|
1968
|
-
.iter()
|
|
1969
|
-
.map(|failure| serde_json::json!({
|
|
1970
|
-
"agent_id": failure.agent_id.as_str(),
|
|
1971
|
-
"phase": failure.phase,
|
|
1972
|
-
"error": failure.error,
|
|
1973
|
-
}))
|
|
1974
|
-
.collect::<Vec<_>>(),
|
|
1975
|
-
}),
|
|
1976
|
-
)
|
|
1990
|
+
.write("restart.completed", payload)
|
|
1977
1991
|
.map(|_| ())
|
|
1978
1992
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
1979
1993
|
}
|
|
@@ -242,7 +242,7 @@ fn r2_lifecycle_lock_exists_precondition() {
|
|
|
242
242
|
assert_public_operation(&rebuild, "restart");
|
|
243
243
|
let drop_idx = rebuild.find("drop(lifecycle_lock);").unwrap();
|
|
244
244
|
let coordinator_idx = rebuild
|
|
245
|
-
.find("start_coordinator_for_workspace(&selected.run_workspace)")
|
|
245
|
+
.find("start_coordinator_for_workspace(&selected.run_workspace, Some(&selected.team_key))")
|
|
246
246
|
.unwrap();
|
|
247
247
|
let readiness_idx = rebuild.find("wait_restart_readiness_or_timeout(").unwrap();
|
|
248
248
|
assert!(
|