@team-agent/installer 0.5.21 → 0.5.23
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/diagnose.rs +22 -0
- package/crates/team-agent/src/cli/mod.rs +16 -2
- package/crates/team-agent/src/cli/send.rs +210 -0
- package/crates/team-agent/src/cli/status_port.rs +16 -2
- package/crates/team-agent/src/coordinator/health.rs +190 -14
- package/crates/team-agent/src/coordinator/types.rs +30 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +37 -0
- package/crates/team-agent/src/messaging/send.rs +25 -0
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -205,6 +205,14 @@ fn coordinator_issue_id(
|
|
|
205
205
|
}
|
|
206
206
|
crate::coordinator::CoordinatorHealthStatus::Running => {
|
|
207
207
|
if !health.metadata_ok {
|
|
208
|
+
if health.service_available
|
|
209
|
+
&& matches!(
|
|
210
|
+
health.binary_identity_relation,
|
|
211
|
+
crate::coordinator::CoordinatorBinaryIdentityRelation::DaemonNewerThanCaller
|
|
212
|
+
)
|
|
213
|
+
{
|
|
214
|
+
return None;
|
|
215
|
+
}
|
|
208
216
|
return match health.metadata_mismatch_reason.as_deref() {
|
|
209
217
|
Some(
|
|
210
218
|
"binary_identity_missing"
|
|
@@ -237,6 +245,11 @@ fn coordinator_issue_value(
|
|
|
237
245
|
"pid": health.pid.map(|pid| pid.get()),
|
|
238
246
|
"metadata_ok": health.metadata_ok,
|
|
239
247
|
"metadata_mismatch_reason": health.metadata_mismatch_reason.clone(),
|
|
248
|
+
"process_running": health.process_running,
|
|
249
|
+
"wire_metadata_ok": health.wire_metadata_ok,
|
|
250
|
+
"binary_identity_ok": health.binary_identity_ok,
|
|
251
|
+
"binary_identity_relation": health.binary_identity_relation.as_str(),
|
|
252
|
+
"service_available": health.service_available,
|
|
240
253
|
"binary_path": health.current_binary_identity.binary_path.clone(),
|
|
241
254
|
"binary_version": health.current_binary_identity.binary_version.clone(),
|
|
242
255
|
"schema_ok": health.schema.ok,
|
|
@@ -378,6 +391,15 @@ mod tests {
|
|
|
378
391
|
pid: Some(crate::coordinator::Pid::new(std::process::id())),
|
|
379
392
|
metadata: None,
|
|
380
393
|
metadata_ok: metadata_mismatch_reason.is_none(),
|
|
394
|
+
process_running: matches!(status, crate::coordinator::CoordinatorHealthStatus::Running),
|
|
395
|
+
wire_metadata_ok: metadata_mismatch_reason.is_none(),
|
|
396
|
+
binary_identity_ok: metadata_mismatch_reason.is_none(),
|
|
397
|
+
binary_identity_relation: crate::coordinator::CoordinatorBinaryIdentityRelation::Same,
|
|
398
|
+
service_available: matches!(
|
|
399
|
+
status,
|
|
400
|
+
crate::coordinator::CoordinatorHealthStatus::Running
|
|
401
|
+
) && metadata_mismatch_reason.is_none()
|
|
402
|
+
&& schema_ok,
|
|
381
403
|
metadata_mismatch_reason: metadata_mismatch_reason.map(ToString::to_string),
|
|
382
404
|
current_binary_identity: crate::coordinator::CoordinatorBinaryIdentity {
|
|
383
405
|
binary_path: "/current/team-agent".to_string(),
|
|
@@ -3947,7 +3947,9 @@ pub mod diagnose_port {
|
|
|
3947
3947
|
}
|
|
3948
3948
|
|
|
3949
3949
|
fn coordinator_health_value(health: crate::coordinator::HealthReport) -> Value {
|
|
3950
|
-
|
|
3950
|
+
let expose_binary_drift = health.service_available && !health.metadata_ok;
|
|
3951
|
+
let binary_identity_relation = health.binary_identity_relation.as_str();
|
|
3952
|
+
let mut value = json!({
|
|
3951
3953
|
"ok": health.ok,
|
|
3952
3954
|
"status": coordinator_status_wire(health.status),
|
|
3953
3955
|
"pid": health.pid.map(|p| p.get()),
|
|
@@ -3969,7 +3971,19 @@ pub mod diagnose_port {
|
|
|
3969
3971
|
"schema": {
|
|
3970
3972
|
"message_store_schema_version": health.schema.schema_version,
|
|
3971
3973
|
},
|
|
3972
|
-
})
|
|
3974
|
+
});
|
|
3975
|
+
if expose_binary_drift {
|
|
3976
|
+
if let Some(obj) = value.as_object_mut() {
|
|
3977
|
+
obj.insert("wire_metadata_ok".to_string(), Value::Bool(true));
|
|
3978
|
+
obj.insert("binary_identity_ok".to_string(), Value::Bool(false));
|
|
3979
|
+
obj.insert(
|
|
3980
|
+
"binary_identity_relation".to_string(),
|
|
3981
|
+
Value::String(binary_identity_relation.to_string()),
|
|
3982
|
+
);
|
|
3983
|
+
obj.insert("service_available".to_string(), Value::Bool(true));
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
value
|
|
3973
3987
|
}
|
|
3974
3988
|
|
|
3975
3989
|
fn coordinator_status_wire(
|
|
@@ -142,11 +142,25 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
142
142
|
{
|
|
143
143
|
return Ok(cmd_send_result(amb, args.json));
|
|
144
144
|
}
|
|
145
|
+
if let Some(value) = dirty_topology_refusal_value(&selected, args.team.as_deref()) {
|
|
146
|
+
return Ok(cmd_send_result(value, args.json));
|
|
147
|
+
}
|
|
148
|
+
let coordinator_ensure = if target_has_known_worker(&selected.state, &target, &opts.sender) {
|
|
149
|
+
loud_ensure_coordinator(&selected)?
|
|
150
|
+
} else {
|
|
151
|
+
None
|
|
152
|
+
};
|
|
153
|
+
if let Some(value) =
|
|
154
|
+
coordinator_ensure_unavailable_value(coordinator_ensure.as_ref(), &target, &content, &opts)
|
|
155
|
+
{
|
|
156
|
+
return Ok(cmd_send_result(value, args.json));
|
|
157
|
+
}
|
|
145
158
|
let mut outcome = messaging::send_message(&selected.run_workspace, &target, &content, &opts)?;
|
|
146
159
|
if opts.watch_result {
|
|
147
160
|
outcome = observe_initial_delivery_for_watch(&selected, &target, &outcome, &opts)?;
|
|
148
161
|
}
|
|
149
162
|
let mut value = delivery_outcome_json(&outcome, &target, &content, &opts);
|
|
163
|
+
append_loud_ensure_fields(&mut value, coordinator_ensure.as_ref());
|
|
150
164
|
if opts.watch_result && initial_delivery_allows_watch(outcome.status) {
|
|
151
165
|
if let Some(obj) = value.as_object_mut() {
|
|
152
166
|
obj.insert("watch".to_string(), watch_notice_json(&target, &opts));
|
|
@@ -156,6 +170,202 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
156
170
|
Ok(cmd_send_result(value, args.json))
|
|
157
171
|
}
|
|
158
172
|
|
|
173
|
+
fn dirty_topology_refusal_value(
|
|
174
|
+
selected: &crate::state::selector::SelectedTeam,
|
|
175
|
+
requested_team: Option<&str>,
|
|
176
|
+
) -> Option<Value> {
|
|
177
|
+
let issue_ids = crate::topology::restart_dirty_topology_issue_ids(&selected.state);
|
|
178
|
+
if issue_ids.is_empty() {
|
|
179
|
+
return None;
|
|
180
|
+
}
|
|
181
|
+
let session_name = selected
|
|
182
|
+
.state
|
|
183
|
+
.get("session_name")
|
|
184
|
+
.and_then(Value::as_str)
|
|
185
|
+
.unwrap_or_default()
|
|
186
|
+
.to_string();
|
|
187
|
+
let reason = issue_ids
|
|
188
|
+
.first()
|
|
189
|
+
.cloned()
|
|
190
|
+
.unwrap_or_else(|| "dirty_topology".to_string());
|
|
191
|
+
let repair_team = requested_team
|
|
192
|
+
.filter(|team| !team.is_empty())
|
|
193
|
+
.unwrap_or(selected.team_key.as_str());
|
|
194
|
+
Some(json!({
|
|
195
|
+
"ok": false,
|
|
196
|
+
"status": "refused_dirty_topology",
|
|
197
|
+
"reason": reason,
|
|
198
|
+
"session_name": session_name,
|
|
199
|
+
"error": "send refused: tmux endpoint/socket topology is inconsistent; run diagnose from the intended leader socket before sending",
|
|
200
|
+
"issues": issue_ids
|
|
201
|
+
.iter()
|
|
202
|
+
.map(|id| json!({"id": id}))
|
|
203
|
+
.collect::<Vec<_>>(),
|
|
204
|
+
"next_actions": [
|
|
205
|
+
"team-agent diagnose --json",
|
|
206
|
+
format!("team-agent claim-leader --team {repair_team} --confirm --json"),
|
|
207
|
+
format!("team-agent takeover --team {repair_team} --confirm --json")
|
|
208
|
+
],
|
|
209
|
+
}))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fn target_has_known_worker(state: &Value, target: &MessageTarget, sender: &str) -> bool {
|
|
213
|
+
let Some(agents) = state.get("agents").and_then(Value::as_object) else {
|
|
214
|
+
return false;
|
|
215
|
+
};
|
|
216
|
+
match target {
|
|
217
|
+
MessageTarget::Single(target) => agents.contains_key(target),
|
|
218
|
+
MessageTarget::Broadcast => agents.keys().any(|agent| agent != sender),
|
|
219
|
+
MessageTarget::Fanout(recipients) => recipients
|
|
220
|
+
.iter()
|
|
221
|
+
.any(|recipient| agents.contains_key(recipient)),
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#[derive(Debug, Clone)]
|
|
226
|
+
struct LoudEnsureResult {
|
|
227
|
+
previous_status: String,
|
|
228
|
+
start: crate::coordinator::StartReport,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fn loud_ensure_coordinator(
|
|
232
|
+
selected: &crate::state::selector::SelectedTeam,
|
|
233
|
+
) -> Result<Option<LoudEnsureResult>, CliError> {
|
|
234
|
+
if in_process_unit_test() {
|
|
235
|
+
return Ok(None);
|
|
236
|
+
}
|
|
237
|
+
let workspace = crate::coordinator::WorkspacePath::new(selected.run_workspace.clone());
|
|
238
|
+
let previous = crate::coordinator::coordinator_health(&workspace);
|
|
239
|
+
if previous.ok {
|
|
240
|
+
return Ok(None);
|
|
241
|
+
}
|
|
242
|
+
if previous.service_available
|
|
243
|
+
&& matches!(
|
|
244
|
+
previous.binary_identity_relation,
|
|
245
|
+
crate::coordinator::CoordinatorBinaryIdentityRelation::DaemonNewerThanCaller
|
|
246
|
+
)
|
|
247
|
+
{
|
|
248
|
+
return Ok(None);
|
|
249
|
+
}
|
|
250
|
+
let previous_status = coordinator_health_status_wire(previous.status).to_string();
|
|
251
|
+
let start = crate::coordinator::start_coordinator_with_team(
|
|
252
|
+
&workspace,
|
|
253
|
+
Some(selected.team_key.as_str()),
|
|
254
|
+
)
|
|
255
|
+
.map_err(|error| CliError::Runtime(error.to_string()))?;
|
|
256
|
+
if !start.ok {
|
|
257
|
+
return Ok(Some(LoudEnsureResult {
|
|
258
|
+
previous_status,
|
|
259
|
+
start,
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
if matches!(
|
|
263
|
+
start.status,
|
|
264
|
+
crate::coordinator::StartOutcome::Started
|
|
265
|
+
| crate::coordinator::StartOutcome::StartedAfterRotation
|
|
266
|
+
) {
|
|
267
|
+
crate::event_log::EventLog::new(&selected.run_workspace)
|
|
268
|
+
.write(
|
|
269
|
+
"coordinator.ensure_restarted",
|
|
270
|
+
json!({
|
|
271
|
+
"coordinator_previous_status": previous_status,
|
|
272
|
+
"status": start.status,
|
|
273
|
+
"pid": start.pid.map(|pid| pid.get()),
|
|
274
|
+
"previous_pid": start.previous_pid.map(|pid| pid.get()),
|
|
275
|
+
"binary_path": start.binary_path,
|
|
276
|
+
"binary_version": start.binary_version,
|
|
277
|
+
"rotation_reason": start.rotation_reason,
|
|
278
|
+
}),
|
|
279
|
+
)
|
|
280
|
+
.map_err(|error| CliError::Runtime(error.to_string()))?;
|
|
281
|
+
return Ok(Some(LoudEnsureResult {
|
|
282
|
+
previous_status,
|
|
283
|
+
start,
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
Ok(None)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#[cfg(test)]
|
|
290
|
+
fn in_process_unit_test() -> bool {
|
|
291
|
+
true
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#[cfg(not(test))]
|
|
295
|
+
fn in_process_unit_test() -> bool {
|
|
296
|
+
false
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
fn coordinator_ensure_unavailable_value(
|
|
300
|
+
ensure: Option<&LoudEnsureResult>,
|
|
301
|
+
target: &MessageTarget,
|
|
302
|
+
content: &str,
|
|
303
|
+
opts: &SendOptions,
|
|
304
|
+
) -> Option<Value> {
|
|
305
|
+
let ensure = ensure?;
|
|
306
|
+
if ensure.start.ok {
|
|
307
|
+
return None;
|
|
308
|
+
}
|
|
309
|
+
let warning = format!(
|
|
310
|
+
"coordinator is not running; message was not queued for {}. Run `team-agent diagnose` or restart the team before sending again.",
|
|
311
|
+
first_target(target)
|
|
312
|
+
);
|
|
313
|
+
let mut value = json!({
|
|
314
|
+
"ok": false,
|
|
315
|
+
"status": "degraded",
|
|
316
|
+
"delivery_status": "degraded",
|
|
317
|
+
"delivered": false,
|
|
318
|
+
"target": target_json(target),
|
|
319
|
+
"agent_id": first_target(target),
|
|
320
|
+
"content_length_bytes": content.len(),
|
|
321
|
+
"sender": opts.sender,
|
|
322
|
+
"message_id": Value::Null,
|
|
323
|
+
"message_status": "degraded",
|
|
324
|
+
"verification": warning,
|
|
325
|
+
"stage": Value::Null,
|
|
326
|
+
"reason": "coordinator_unavailable",
|
|
327
|
+
"channel": "coordinator_unavailable",
|
|
328
|
+
});
|
|
329
|
+
append_loud_ensure_fields(&mut value, Some(ensure));
|
|
330
|
+
Some(value)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fn append_loud_ensure_fields(value: &mut Value, ensure: Option<&LoudEnsureResult>) {
|
|
334
|
+
let Some(ensure) = ensure else {
|
|
335
|
+
return;
|
|
336
|
+
};
|
|
337
|
+
if !ensure.start.ok {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if let Some(obj) = value.as_object_mut() {
|
|
341
|
+
obj.insert("coordinator_auto_restarted".to_string(), json!(true));
|
|
342
|
+
obj.insert(
|
|
343
|
+
"coordinator_previous_status".to_string(),
|
|
344
|
+
json!(ensure.previous_status),
|
|
345
|
+
);
|
|
346
|
+
obj.insert(
|
|
347
|
+
"coordinator".to_string(),
|
|
348
|
+
coordinator_start_json(&ensure.start),
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
fn coordinator_start_json(report: &crate::coordinator::StartReport) -> Value {
|
|
354
|
+
let summary = crate::lifecycle::CoordinatorStartSummary::from_start_report(report);
|
|
355
|
+
crate::lifecycle::coordinator_start_summary_value(&summary)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
fn coordinator_health_status_wire(
|
|
359
|
+
status: crate::coordinator::CoordinatorHealthStatus,
|
|
360
|
+
) -> &'static str {
|
|
361
|
+
match status {
|
|
362
|
+
crate::coordinator::CoordinatorHealthStatus::Missing => "missing",
|
|
363
|
+
crate::coordinator::CoordinatorHealthStatus::InvalidPid => "invalid_pid",
|
|
364
|
+
crate::coordinator::CoordinatorHealthStatus::Running => "running",
|
|
365
|
+
crate::coordinator::CoordinatorHealthStatus::Stale => "stale",
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
159
369
|
/// F1 (0.3.26): direct pane-id send — bypasses agent-name routing + team
|
|
160
370
|
/// membership check. Constructs `Target::Pane`, renders the message with
|
|
161
371
|
/// the standard protocol block (Team Agent message from sender + token),
|
|
@@ -1143,7 +1143,9 @@ use rusqlite::params;
|
|
|
1143
1143
|
}
|
|
1144
1144
|
|
|
1145
1145
|
fn coordinator_health_value(health: crate::coordinator::HealthReport) -> Value {
|
|
1146
|
-
|
|
1146
|
+
let expose_binary_drift = health.service_available && !health.metadata_ok;
|
|
1147
|
+
let binary_identity_relation = health.binary_identity_relation.as_str();
|
|
1148
|
+
let mut value = json!({
|
|
1147
1149
|
"ok": health.ok,
|
|
1148
1150
|
"status": coordinator_status_wire(health.status),
|
|
1149
1151
|
"pid": health.pid.map(|p| p.get()),
|
|
@@ -1165,7 +1167,19 @@ use rusqlite::params;
|
|
|
1165
1167
|
"schema": {
|
|
1166
1168
|
"message_store_schema_version": health.schema.schema_version,
|
|
1167
1169
|
},
|
|
1168
|
-
})
|
|
1170
|
+
});
|
|
1171
|
+
if expose_binary_drift {
|
|
1172
|
+
if let Some(obj) = value.as_object_mut() {
|
|
1173
|
+
obj.insert("wire_metadata_ok".to_string(), Value::Bool(true));
|
|
1174
|
+
obj.insert("binary_identity_ok".to_string(), Value::Bool(false));
|
|
1175
|
+
obj.insert(
|
|
1176
|
+
"binary_identity_relation".to_string(),
|
|
1177
|
+
Value::String(binary_identity_relation.to_string()),
|
|
1178
|
+
);
|
|
1179
|
+
obj.insert("service_available".to_string(), Value::Bool(true));
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
value
|
|
1169
1183
|
}
|
|
1170
1184
|
|
|
1171
1185
|
fn coordinator_status_wire(status: crate::coordinator::CoordinatorHealthStatus) -> &'static str {
|
|
@@ -13,10 +13,10 @@ use thiserror::Error;
|
|
|
13
13
|
use crate::message_store::MessageStore;
|
|
14
14
|
|
|
15
15
|
use super::types::{
|
|
16
|
-
CoordinatorBinaryIdentity,
|
|
17
|
-
CoordinatorMetadataMismatchReason, HealthReport, MetadataSource, Pid,
|
|
18
|
-
SchemaHealth, StartError, StartOutcome, StartReport, StopError, StopOutcome,
|
|
19
|
-
WatchCursor, WorkspacePath, PROTOCOL_VERSION, ROTATION_MARKER,
|
|
16
|
+
CoordinatorBinaryIdentity, CoordinatorBinaryIdentityRelation, CoordinatorHealthStatus,
|
|
17
|
+
CoordinatorMetadata, CoordinatorMetadataMismatchReason, HealthReport, MetadataSource, Pid,
|
|
18
|
+
SchemaError, SchemaHealth, StartError, StartOutcome, StartReport, StopError, StopOutcome,
|
|
19
|
+
StopReport, WatchCursor, WorkspacePath, PROTOCOL_VERSION, ROTATION_MARKER,
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
// ===========================================================================
|
|
@@ -40,21 +40,30 @@ pub fn coordinator_health(workspace: &WorkspacePath) -> HealthReport {
|
|
|
40
40
|
None => CoordinatorHealthStatus::Missing,
|
|
41
41
|
};
|
|
42
42
|
let metadata = read_coordinator_metadata(workspace);
|
|
43
|
-
let
|
|
44
|
-
.map(|p|
|
|
45
|
-
metadata.as_ref(),
|
|
46
|
-
p,
|
|
47
|
-
¤t_binary_identity,
|
|
48
|
-
))
|
|
43
|
+
let wire_metadata_mismatch = pid
|
|
44
|
+
.map(|p| coordinator_wire_metadata_mismatch_reason(metadata.as_ref(), p))
|
|
49
45
|
.unwrap_or(Some(CoordinatorMetadataMismatchReason::MetadataMissing));
|
|
50
|
-
let
|
|
51
|
-
|
|
46
|
+
let binary_identity_mismatch =
|
|
47
|
+
coordinator_binary_identity_mismatch_reason(metadata.as_ref(), ¤t_binary_identity);
|
|
48
|
+
let wire_metadata_ok = wire_metadata_mismatch.is_none();
|
|
49
|
+
let binary_identity_ok = binary_identity_mismatch.is_none();
|
|
50
|
+
let metadata_mismatch = wire_metadata_mismatch.or(binary_identity_mismatch);
|
|
51
|
+
let metadata_ok = wire_metadata_ok && binary_identity_ok;
|
|
52
|
+
let process_running = matches!(status, CoordinatorHealthStatus::Running);
|
|
53
|
+
let binary_identity_relation =
|
|
54
|
+
coordinator_binary_identity_relation(metadata.as_ref(), ¤t_binary_identity);
|
|
55
|
+
let service_available = process_running && wire_metadata_ok && schema.ok;
|
|
52
56
|
HealthReport {
|
|
53
|
-
ok:
|
|
57
|
+
ok: process_running && metadata_ok && schema.ok,
|
|
54
58
|
status,
|
|
55
59
|
pid,
|
|
56
60
|
metadata,
|
|
57
61
|
metadata_ok,
|
|
62
|
+
process_running,
|
|
63
|
+
wire_metadata_ok,
|
|
64
|
+
binary_identity_ok,
|
|
65
|
+
binary_identity_relation,
|
|
66
|
+
service_available,
|
|
58
67
|
metadata_mismatch_reason: metadata_mismatch.map(|reason| reason.as_str().to_string()),
|
|
59
68
|
current_binary_identity,
|
|
60
69
|
schema,
|
|
@@ -109,6 +118,62 @@ pub fn start_coordinator_with_team(
|
|
|
109
118
|
action: health.schema.action,
|
|
110
119
|
});
|
|
111
120
|
}
|
|
121
|
+
if matches!(health.status, CoordinatorHealthStatus::Running) && !health.wire_metadata_ok {
|
|
122
|
+
return Ok(StartReport {
|
|
123
|
+
ok: false,
|
|
124
|
+
pid: health.pid,
|
|
125
|
+
status: StartOutcome::RestartIncompatibleStopFailed,
|
|
126
|
+
previous_pid: health.pid,
|
|
127
|
+
binary_path: Some(identity.binary_path),
|
|
128
|
+
binary_version: Some(identity.binary_version),
|
|
129
|
+
rotation_reason: health.metadata_mismatch_reason,
|
|
130
|
+
log: None,
|
|
131
|
+
schema_error: None,
|
|
132
|
+
action: Some(
|
|
133
|
+
"refusing to rotate a coordinator with incompatible protocol or schema metadata"
|
|
134
|
+
.to_string(),
|
|
135
|
+
),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if matches!(health.status, CoordinatorHealthStatus::Running)
|
|
139
|
+
&& health.wire_metadata_ok
|
|
140
|
+
&& !health.binary_identity_ok
|
|
141
|
+
&& matches!(
|
|
142
|
+
health.binary_identity_relation,
|
|
143
|
+
CoordinatorBinaryIdentityRelation::DaemonNewerThanCaller
|
|
144
|
+
)
|
|
145
|
+
{
|
|
146
|
+
crate::event_log::EventLog::new(workspace.as_path()).write(
|
|
147
|
+
"coordinator.newer_daemon_preserved",
|
|
148
|
+
serde_json::json!({
|
|
149
|
+
"pid": health.pid.map(|pid| pid.get()),
|
|
150
|
+
"binary_identity_relation": health.binary_identity_relation.as_str(),
|
|
151
|
+
"reason": "daemon_newer_than_caller",
|
|
152
|
+
"daemon_binary_path": health
|
|
153
|
+
.metadata
|
|
154
|
+
.as_ref()
|
|
155
|
+
.and_then(|metadata| metadata.binary_path.clone()),
|
|
156
|
+
"daemon_binary_version": health
|
|
157
|
+
.metadata
|
|
158
|
+
.as_ref()
|
|
159
|
+
.and_then(|metadata| metadata.binary_version.clone()),
|
|
160
|
+
"caller_binary_path": identity.binary_path.clone(),
|
|
161
|
+
"caller_binary_version": identity.binary_version.clone(),
|
|
162
|
+
}),
|
|
163
|
+
)?;
|
|
164
|
+
return Ok(StartReport {
|
|
165
|
+
ok: true,
|
|
166
|
+
pid: health.pid,
|
|
167
|
+
status: StartOutcome::AlreadyRunning,
|
|
168
|
+
previous_pid: None,
|
|
169
|
+
binary_path: Some(identity.binary_path),
|
|
170
|
+
binary_version: Some(identity.binary_version),
|
|
171
|
+
rotation_reason: Some("daemon_newer_than_caller".to_string()),
|
|
172
|
+
log: Some(coordinator_log_path(workspace)),
|
|
173
|
+
schema_error: None,
|
|
174
|
+
action: None,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
112
177
|
let rotation_reason = if matches!(health.status, CoordinatorHealthStatus::Running)
|
|
113
178
|
&& !health.metadata_ok
|
|
114
179
|
{
|
|
@@ -580,6 +645,13 @@ pub fn read_coordinator_metadata(workspace: &WorkspacePath) -> Option<Coordinato
|
|
|
580
645
|
}
|
|
581
646
|
|
|
582
647
|
pub fn current_coordinator_binary_identity() -> CoordinatorBinaryIdentity {
|
|
648
|
+
if let Ok(raw) = std::env::var("TEAM_AGENT_TEST_CALLER_BINARY_IDENTITY") {
|
|
649
|
+
if let Ok(identity) = serde_json::from_str::<CoordinatorBinaryIdentity>(&raw) {
|
|
650
|
+
if !identity.binary_path.is_empty() && !identity.binary_version.is_empty() {
|
|
651
|
+
return identity;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
583
655
|
let binary_path = std::env::current_exe()
|
|
584
656
|
.map(|path| path.canonicalize().unwrap_or(path))
|
|
585
657
|
.map(|path| path.to_string_lossy().into_owned())
|
|
@@ -608,6 +680,14 @@ fn coordinator_metadata_mismatch_reason_with_identity(
|
|
|
608
680
|
metadata: Option<&CoordinatorMetadata>,
|
|
609
681
|
pid: Pid,
|
|
610
682
|
identity: &CoordinatorBinaryIdentity,
|
|
683
|
+
) -> Option<CoordinatorMetadataMismatchReason> {
|
|
684
|
+
coordinator_wire_metadata_mismatch_reason(metadata, pid)
|
|
685
|
+
.or_else(|| coordinator_binary_identity_mismatch_reason(metadata, identity))
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
fn coordinator_wire_metadata_mismatch_reason(
|
|
689
|
+
metadata: Option<&CoordinatorMetadata>,
|
|
690
|
+
pid: Pid,
|
|
611
691
|
) -> Option<CoordinatorMetadataMismatchReason> {
|
|
612
692
|
let Some(metadata) = metadata else {
|
|
613
693
|
return Some(CoordinatorMetadataMismatchReason::MetadataMissing);
|
|
@@ -621,6 +701,16 @@ fn coordinator_metadata_mismatch_reason_with_identity(
|
|
|
621
701
|
if metadata.message_store_schema_version != crate::db::schema::SCHEMA_VERSION {
|
|
622
702
|
return Some(CoordinatorMetadataMismatchReason::MessageStoreSchemaVersionMismatch);
|
|
623
703
|
}
|
|
704
|
+
None
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
fn coordinator_binary_identity_mismatch_reason(
|
|
708
|
+
metadata: Option<&CoordinatorMetadata>,
|
|
709
|
+
identity: &CoordinatorBinaryIdentity,
|
|
710
|
+
) -> Option<CoordinatorMetadataMismatchReason> {
|
|
711
|
+
let Some(metadata) = metadata else {
|
|
712
|
+
return Some(CoordinatorMetadataMismatchReason::MetadataMissing);
|
|
713
|
+
};
|
|
624
714
|
let Some(binary_version) = metadata.binary_version.as_deref().filter(|value| !value.is_empty())
|
|
625
715
|
else {
|
|
626
716
|
return Some(CoordinatorMetadataMismatchReason::BinaryIdentityMissing);
|
|
@@ -632,12 +722,98 @@ fn coordinator_metadata_mismatch_reason_with_identity(
|
|
|
632
722
|
if binary_version != identity.binary_version {
|
|
633
723
|
return Some(CoordinatorMetadataMismatchReason::BinaryVersionMismatch);
|
|
634
724
|
}
|
|
635
|
-
if binary_path
|
|
725
|
+
if !binary_path_matches_current_identity(binary_path, &identity.binary_path) {
|
|
636
726
|
return Some(CoordinatorMetadataMismatchReason::BinaryPathMismatch);
|
|
637
727
|
}
|
|
638
728
|
None
|
|
639
729
|
}
|
|
640
730
|
|
|
731
|
+
fn coordinator_binary_identity_relation(
|
|
732
|
+
metadata: Option<&CoordinatorMetadata>,
|
|
733
|
+
identity: &CoordinatorBinaryIdentity,
|
|
734
|
+
) -> CoordinatorBinaryIdentityRelation {
|
|
735
|
+
let Some(metadata) = metadata else {
|
|
736
|
+
return CoordinatorBinaryIdentityRelation::Unknown;
|
|
737
|
+
};
|
|
738
|
+
let Some(daemon_version) = metadata
|
|
739
|
+
.binary_version
|
|
740
|
+
.as_deref()
|
|
741
|
+
.filter(|value| !value.is_empty())
|
|
742
|
+
else {
|
|
743
|
+
return CoordinatorBinaryIdentityRelation::Unknown;
|
|
744
|
+
};
|
|
745
|
+
match compare_version_strings(daemon_version, &identity.binary_version) {
|
|
746
|
+
Some(std::cmp::Ordering::Greater) => {
|
|
747
|
+
return CoordinatorBinaryIdentityRelation::DaemonNewerThanCaller;
|
|
748
|
+
}
|
|
749
|
+
Some(std::cmp::Ordering::Less) => {
|
|
750
|
+
return CoordinatorBinaryIdentityRelation::CallerNewerThanDaemon;
|
|
751
|
+
}
|
|
752
|
+
Some(std::cmp::Ordering::Equal) => {}
|
|
753
|
+
None => return CoordinatorBinaryIdentityRelation::Unknown,
|
|
754
|
+
}
|
|
755
|
+
let Some(daemon_path) = metadata
|
|
756
|
+
.binary_path
|
|
757
|
+
.as_deref()
|
|
758
|
+
.filter(|value| !value.is_empty())
|
|
759
|
+
else {
|
|
760
|
+
return CoordinatorBinaryIdentityRelation::Unknown;
|
|
761
|
+
};
|
|
762
|
+
if binary_path_matches_current_identity(daemon_path, &identity.binary_path) {
|
|
763
|
+
CoordinatorBinaryIdentityRelation::Same
|
|
764
|
+
} else {
|
|
765
|
+
CoordinatorBinaryIdentityRelation::SameVersionPathMismatch
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
fn compare_version_strings(left: &str, right: &str) -> Option<std::cmp::Ordering> {
|
|
770
|
+
let left = parse_numeric_version(left)?;
|
|
771
|
+
let right = parse_numeric_version(right)?;
|
|
772
|
+
Some(left.cmp(&right))
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
fn parse_numeric_version(value: &str) -> Option<Vec<u64>> {
|
|
776
|
+
if value.is_empty() {
|
|
777
|
+
return None;
|
|
778
|
+
}
|
|
779
|
+
value
|
|
780
|
+
.split('.')
|
|
781
|
+
.map(|part| part.parse::<u64>().ok())
|
|
782
|
+
.collect()
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
fn binary_path_matches_current_identity(metadata_path: &str, identity_path: &str) -> bool {
|
|
786
|
+
if metadata_path == identity_path {
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
test_harness_binary_path_matches(metadata_path)
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/// Test harness escape hatch for integration tests whose process identity is the
|
|
793
|
+
/// test binary while fixture metadata intentionally points at the built CLI.
|
|
794
|
+
/// Production must not infer this from path shape; callers must set the
|
|
795
|
+
/// `TEAM_AGENT_TEST_HARNESS_BINARY_PATH_MATCH` env explicitly to either the
|
|
796
|
+
/// expected binary path or the target directory containing `team-agent`.
|
|
797
|
+
fn test_harness_binary_path_matches(metadata_path: &str) -> bool {
|
|
798
|
+
let Ok(path) = std::env::var("TEAM_AGENT_TEST_HARNESS_BINARY_PATH_MATCH") else {
|
|
799
|
+
return false;
|
|
800
|
+
};
|
|
801
|
+
let path = PathBuf::from(path);
|
|
802
|
+
path_matches(metadata_path, &path)
|
|
803
|
+
|| path_matches(metadata_path, &path.join("team-agent"))
|
|
804
|
+
|| path
|
|
805
|
+
.parent()
|
|
806
|
+
.is_some_and(|parent| path_matches(metadata_path, &parent.join("team-agent")))
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
fn path_matches(metadata_path: &str, path: &Path) -> bool {
|
|
810
|
+
path.to_string_lossy() == metadata_path
|
|
811
|
+
|| path
|
|
812
|
+
.canonicalize()
|
|
813
|
+
.ok()
|
|
814
|
+
.is_some_and(|path| path.to_string_lossy() == metadata_path)
|
|
815
|
+
}
|
|
816
|
+
|
|
641
817
|
/// `write_coordinator_metadata`(`metadata.py:46-61`)。写 `coordinator.json`(pretty indent=2),
|
|
642
818
|
/// `updated_at = now(utc).isoformat()`。
|
|
643
819
|
pub fn write_coordinator_metadata(
|
|
@@ -189,6 +189,29 @@ pub struct CoordinatorBinaryIdentity {
|
|
|
189
189
|
pub binary_version: String,
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
/// Caller-relative daemon binary identity relation.
|
|
193
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
194
|
+
#[serde(rename_all = "snake_case")]
|
|
195
|
+
pub enum CoordinatorBinaryIdentityRelation {
|
|
196
|
+
Same,
|
|
197
|
+
CallerNewerThanDaemon,
|
|
198
|
+
DaemonNewerThanCaller,
|
|
199
|
+
SameVersionPathMismatch,
|
|
200
|
+
Unknown,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
impl CoordinatorBinaryIdentityRelation {
|
|
204
|
+
pub fn as_str(self) -> &'static str {
|
|
205
|
+
match self {
|
|
206
|
+
Self::Same => "same",
|
|
207
|
+
Self::CallerNewerThanDaemon => "caller_newer_than_daemon",
|
|
208
|
+
Self::DaemonNewerThanCaller => "daemon_newer_than_caller",
|
|
209
|
+
Self::SameVersionPathMismatch => "same_version_path_mismatch",
|
|
210
|
+
Self::Unknown => "unknown",
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
192
215
|
/// Stable machine-readable reason for coordinator metadata rejection.
|
|
193
216
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
194
217
|
#[serde(rename_all = "snake_case")]
|
|
@@ -285,6 +308,13 @@ pub struct HealthReport {
|
|
|
285
308
|
pub metadata: Option<CoordinatorMetadata>,
|
|
286
309
|
/// 三元全等(`coordinator_metadata_ok`,`metadata.py:37-43`)。
|
|
287
310
|
pub metadata_ok: bool,
|
|
311
|
+
pub process_running: bool,
|
|
312
|
+
/// pid/protocol/schema match; excludes daemon binary identity.
|
|
313
|
+
pub wire_metadata_ok: bool,
|
|
314
|
+
pub binary_identity_ok: bool,
|
|
315
|
+
pub binary_identity_relation: CoordinatorBinaryIdentityRelation,
|
|
316
|
+
/// A live daemon that can process this workspace's queue.
|
|
317
|
+
pub service_available: bool,
|
|
288
318
|
pub metadata_mismatch_reason: Option<String>,
|
|
289
319
|
pub current_binary_identity: CoordinatorBinaryIdentity,
|
|
290
320
|
pub schema: SchemaHealth,
|
|
@@ -50,6 +50,7 @@ pub(crate) fn reset_agent(
|
|
|
50
50
|
})),
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
+
emit_newer_daemon_preserved_if_present(workspace)?;
|
|
53
54
|
let lifecycle_workspace = lifecycle_workspace(workspace, owner_team, true)?;
|
|
54
55
|
match crate::lifecycle::reset_agent(
|
|
55
56
|
&lifecycle_workspace,
|
|
@@ -97,6 +98,7 @@ pub(crate) fn fork_agent(
|
|
|
97
98
|
as_agent_id: &str,
|
|
98
99
|
label: Option<&str>,
|
|
99
100
|
) -> ToolResult {
|
|
101
|
+
emit_newer_daemon_preserved_if_present(workspace)?;
|
|
100
102
|
let lifecycle_workspace = lifecycle_workspace(workspace, owner_team, false)?;
|
|
101
103
|
// operations.py:315 — the label becomes the forked agent's role.
|
|
102
104
|
let report = crate::lifecycle::launch::fork_agent(
|
|
@@ -128,6 +130,41 @@ fn reset_refusal_reason(reason: ResetRefusal) -> Value {
|
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
fn emit_newer_daemon_preserved_if_present(workspace: &Path) -> Result<(), super::super::ToolError> {
|
|
134
|
+
let workspace_path = crate::coordinator::WorkspacePath::new(workspace.to_path_buf());
|
|
135
|
+
let health = crate::coordinator::coordinator_health(&workspace_path);
|
|
136
|
+
if !(health.service_available
|
|
137
|
+
&& !health.binary_identity_ok
|
|
138
|
+
&& matches!(
|
|
139
|
+
health.binary_identity_relation,
|
|
140
|
+
crate::coordinator::CoordinatorBinaryIdentityRelation::DaemonNewerThanCaller
|
|
141
|
+
))
|
|
142
|
+
{
|
|
143
|
+
return Ok(());
|
|
144
|
+
}
|
|
145
|
+
crate::event_log::EventLog::new(workspace)
|
|
146
|
+
.write(
|
|
147
|
+
"coordinator.newer_daemon_preserved",
|
|
148
|
+
serde_json::json!({
|
|
149
|
+
"pid": health.pid.map(|pid| pid.get()),
|
|
150
|
+
"binary_identity_relation": health.binary_identity_relation.as_str(),
|
|
151
|
+
"reason": "daemon_newer_than_caller",
|
|
152
|
+
"daemon_binary_path": health
|
|
153
|
+
.metadata
|
|
154
|
+
.as_ref()
|
|
155
|
+
.and_then(|metadata| metadata.binary_path.clone()),
|
|
156
|
+
"daemon_binary_version": health
|
|
157
|
+
.metadata
|
|
158
|
+
.as_ref()
|
|
159
|
+
.and_then(|metadata| metadata.binary_version.clone()),
|
|
160
|
+
"caller_binary_path": health.current_binary_identity.binary_path,
|
|
161
|
+
"caller_binary_version": health.current_binary_identity.binary_version,
|
|
162
|
+
}),
|
|
163
|
+
)
|
|
164
|
+
.map(|_| ())
|
|
165
|
+
.map_err(tool_runtime_error)
|
|
166
|
+
}
|
|
167
|
+
|
|
131
168
|
fn lifecycle_workspace(
|
|
132
169
|
workspace: &Path,
|
|
133
170
|
owner_team: Option<&TeamKey>,
|
|
@@ -319,6 +319,30 @@ fn coordinator_unavailable_outcome(
|
|
|
319
319
|
if health.ok || matches!(health.status, CoordinatorHealthStatus::Missing) {
|
|
320
320
|
return Ok(None);
|
|
321
321
|
}
|
|
322
|
+
if health.service_available {
|
|
323
|
+
event_log.write(
|
|
324
|
+
"send.coordinator_binary_identity_drift_ignored",
|
|
325
|
+
serde_json::json!({
|
|
326
|
+
"recipient": recipient,
|
|
327
|
+
"sender": opts.sender,
|
|
328
|
+
"coordinator_status": health.status,
|
|
329
|
+
"coordinator_pid": health.pid.map(|pid| pid.get()),
|
|
330
|
+
"message_queued": true,
|
|
331
|
+
"binary_identity_relation": health.binary_identity_relation.as_str(),
|
|
332
|
+
"daemon_binary_path": health
|
|
333
|
+
.metadata
|
|
334
|
+
.as_ref()
|
|
335
|
+
.and_then(|metadata| metadata.binary_path.clone()),
|
|
336
|
+
"daemon_binary_version": health
|
|
337
|
+
.metadata
|
|
338
|
+
.as_ref()
|
|
339
|
+
.and_then(|metadata| metadata.binary_version.clone()),
|
|
340
|
+
"caller_binary_path": health.current_binary_identity.binary_path,
|
|
341
|
+
"caller_binary_version": health.current_binary_identity.binary_version,
|
|
342
|
+
}),
|
|
343
|
+
)?;
|
|
344
|
+
return Ok(None);
|
|
345
|
+
}
|
|
322
346
|
let warning = format!(
|
|
323
347
|
"coordinator is not running; message was not queued for {recipient}. Run `team-agent diagnose` or restart the team before sending again."
|
|
324
348
|
);
|
|
@@ -329,6 +353,7 @@ fn coordinator_unavailable_outcome(
|
|
|
329
353
|
"sender": opts.sender,
|
|
330
354
|
"coordinator_status": health.status,
|
|
331
355
|
"coordinator_pid": health.pid.map(|pid| pid.get()),
|
|
356
|
+
"metadata_mismatch_reason": health.metadata_mismatch_reason,
|
|
332
357
|
"message_queued": false,
|
|
333
358
|
"warning": warning,
|
|
334
359
|
"coordinator_log": crate::coordinator::coordinator_log_path(&coordinator_workspace)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.23",
|
|
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.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.23",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.23",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.23"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|