@team-agent/installer 0.4.11 → 0.5.1

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.
Files changed (80) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +1 -0
  4. package/crates/team-agent/src/cli/diagnose.rs +2 -15
  5. package/crates/team-agent/src/cli/emit.rs +98 -5
  6. package/crates/team-agent/src/cli/mod.rs +2 -0
  7. package/crates/team-agent/src/cli/named_address.rs +864 -0
  8. package/crates/team-agent/src/cli/send.rs +104 -0
  9. package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
  10. package/crates/team-agent/src/cli/tests/mod.rs +1 -0
  11. package/crates/team-agent/src/cli/tests/named_address.rs +455 -0
  12. package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
  13. package/crates/team-agent/src/cli/types.rs +4 -0
  14. package/crates/team-agent/src/compiler.rs +11 -20
  15. package/crates/team-agent/src/coordinator/orphan.rs +2 -2
  16. package/crates/team-agent/src/coordinator/runtime_detectors.rs +5 -4
  17. package/crates/team-agent/src/coordinator/steps/abnormal.rs +1776 -2
  18. package/crates/team-agent/src/coordinator/steps/mod.rs +19 -0
  19. package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +78 -7
  20. package/crates/team-agent/src/coordinator/tests/abnormal.rs +195 -0
  21. package/crates/team-agent/src/coordinator/tick.rs +31 -932
  22. package/crates/team-agent/src/diagnose/orphans.rs +66 -8
  23. package/crates/team-agent/src/layout/worker_window_helpers.rs +5 -7
  24. package/crates/team-agent/src/leader/provider_attribution.rs +7 -5
  25. package/crates/team-agent/src/leader/rediscover.rs +1 -11
  26. package/crates/team-agent/src/lifecycle/launch/plan.rs +19 -8
  27. package/crates/team-agent/src/lifecycle/launch.rs +135 -58
  28. package/crates/team-agent/src/lifecycle/lock.rs +302 -0
  29. package/crates/team-agent/src/lifecycle/mod.rs +1 -0
  30. package/crates/team-agent/src/lifecycle/profile_smoke.rs +1 -11
  31. package/crates/team-agent/src/lifecycle/restart/agent.rs +169 -113
  32. package/crates/team-agent/src/lifecycle/restart/common.rs +108 -17
  33. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -32
  34. package/crates/team-agent/src/lifecycle/restart/remove.rs +16 -2
  35. package/crates/team-agent/src/lifecycle/restart/selection.rs +2 -1
  36. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +6 -0
  37. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +3 -3
  38. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +321 -0
  39. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +35 -5
  40. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +802 -0
  41. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +675 -0
  42. package/crates/team-agent/src/lifecycle/tests.rs +3 -0
  43. package/crates/team-agent/src/lifecycle/types.rs +8 -0
  44. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +24 -9
  45. package/crates/team-agent/src/mcp_server/tools.rs +157 -68
  46. package/crates/team-agent/src/messaging/activity.rs +43 -18
  47. package/crates/team-agent/src/messaging/delivery.rs +161 -97
  48. package/crates/team-agent/src/messaging/helpers.rs +1 -0
  49. package/crates/team-agent/src/messaging/leader_receiver.rs +22 -1
  50. package/crates/team-agent/src/messaging/results.rs +34 -9
  51. package/crates/team-agent/src/messaging/scheduler.rs +52 -9
  52. package/crates/team-agent/src/model/spec.rs +10 -6
  53. package/crates/team-agent/src/provider/adapter.rs +10 -1038
  54. package/crates/team-agent/src/provider/adapters/claude.rs +3 -8
  55. package/crates/team-agent/src/provider/adapters/copilot.rs +2 -5
  56. package/crates/team-agent/src/provider/classify.rs +23 -48
  57. package/crates/team-agent/src/provider/command.rs +16 -7
  58. package/crates/team-agent/src/provider/faults.rs +93 -53
  59. package/crates/team-agent/src/provider/session/capture.rs +4 -15
  60. package/crates/team-agent/src/provider/session_scan/claude.rs +309 -0
  61. package/crates/team-agent/src/provider/session_scan/codex.rs +40 -0
  62. package/crates/team-agent/src/provider/session_scan/common.rs +350 -0
  63. package/crates/team-agent/src/provider/session_scan/copilot.rs +202 -0
  64. package/crates/team-agent/src/provider/session_scan.rs +65 -27
  65. package/crates/team-agent/src/provider/tests/faults.rs +80 -0
  66. package/crates/team-agent/src/provider/types.rs +33 -1
  67. package/crates/team-agent/src/provider/wire.rs +171 -1
  68. package/crates/team-agent/src/state/identity.rs +242 -57
  69. package/crates/team-agent/src/state/identity_keys.rs +9 -12
  70. package/crates/team-agent/src/state/mod.rs +5 -1
  71. package/crates/team-agent/src/state/owner_gate.rs +59 -15
  72. package/crates/team-agent/src/state/ownership.rs +12 -11
  73. package/crates/team-agent/src/state/paths.rs +13 -6
  74. package/crates/team-agent/src/state/persist.rs +671 -128
  75. package/crates/team-agent/src/state/projection.rs +240 -49
  76. package/crates/team-agent/src/state/selector.rs +11 -5
  77. package/crates/team-agent/src/tmux_backend/tests.rs +51 -2
  78. package/crates/team-agent/src/tmux_backend.rs +42 -5
  79. package/crates/team-agent/src/transport/test_support.rs +55 -6
  80. package/package.json +4 -4
@@ -26,13 +26,23 @@ pub(crate) fn update_state(
26
26
  let mut state = selected.state;
27
27
  ensure_object(&mut state);
28
28
  append_note(&mut state, note);
29
- crate::state::projection::save_team_scoped_state(&selected.run_workspace, &state)
30
- .map_err(tool_runtime_error)?;
29
+ crate::state::projection::save_team_scoped_state_reapplying_after_conflict(
30
+ &selected.run_workspace,
31
+ &state,
32
+ |latest| {
33
+ ensure_object(latest);
34
+ append_note(latest, note);
35
+ },
36
+ )
37
+ .map_err(tool_runtime_error)?;
31
38
  let spec_path = selected
32
39
  .spec_path
33
40
  .ok_or_else(|| tool_runtime_error("active team spec not found for update_state"))?;
34
41
  let spec_workspace = spec_path.parent().ok_or_else(|| {
35
- tool_runtime_error(format!("active team spec has no parent: {}", spec_path.display()))
42
+ tool_runtime_error(format!(
43
+ "active team spec has no parent: {}",
44
+ spec_path.display()
45
+ ))
36
46
  })?;
37
47
  let spec_text = std::fs::read_to_string(&spec_path).map_err(tool_runtime_error)?;
38
48
  let spec = crate::model::yaml::loads(&spec_text).map_err(tool_runtime_error)?;
@@ -56,8 +66,16 @@ fn update_state_without_spec(
56
66
  ensure_object(&mut state);
57
67
  seed_legacy_team_key(&mut state, &selected.run_workspace, &selected.team_key);
58
68
  append_note(&mut state, note);
59
- crate::state::projection::save_team_scoped_state(&selected.run_workspace, &state)
60
- .map_err(tool_runtime_error)?;
69
+ crate::state::projection::save_team_scoped_state_reapplying_after_conflict(
70
+ &selected.run_workspace,
71
+ &state,
72
+ |latest| {
73
+ ensure_object(latest);
74
+ seed_legacy_team_key(latest, &selected.run_workspace, &selected.team_key);
75
+ append_note(latest, note);
76
+ },
77
+ )
78
+ .map_err(tool_runtime_error)?;
61
79
  let path = crate::lifecycle::restart::write_team_state(
62
80
  &selected.run_workspace,
63
81
  &crate::model::yaml::Value::Null,
@@ -120,10 +138,7 @@ fn is_missing_active_spec(err: &crate::state::StateError) -> bool {
120
138
  )
121
139
  }
122
140
 
123
- pub(crate) fn get_team_status(
124
- workspace: &Path,
125
- owner_team: Option<&TeamKey>,
126
- ) -> ToolResult {
141
+ pub(crate) fn get_team_status(workspace: &Path, owner_team: Option<&TeamKey>) -> ToolResult {
127
142
  let selected = crate::state::selector::resolve_active_team(
128
143
  workspace,
129
144
  owner_team.map(TeamKey::as_str),
@@ -12,7 +12,9 @@ use crate::model::ids::{AgentId, TaskId, TeamKey};
12
12
  use crate::event_log::EventLog;
13
13
 
14
14
  // ── REUSE: step 5 state persist / projection ────────────────────────────────
15
- use crate::state::persist::{load_runtime_state, save_runtime_state};
15
+ use crate::state::persist::{
16
+ load_runtime_state, save_runtime_state, save_runtime_state_reapplying_after_conflict,
17
+ };
16
18
 
17
19
  // ── REUSE: step 11 messaging delegate surface ───────────────────────────────
18
20
  use crate::messaging::{self, MessageTarget, SendOptions};
@@ -20,13 +22,15 @@ use crate::messaging::{self, MessageTarget, SendOptions};
20
22
  use super::helpers::{
21
23
  delivery_outcome_value, ensure_object, enum_value, insert_array, is_worker_recipient,
22
24
  json_dumps_default, latest_task_for_assignee, latest_uncorrelated_delivered_message_for,
23
- non_empty_string, normalized_envelope_value, object_fields,
24
- requires_ack_for_target, tool_runtime_error,
25
+ non_empty_string, normalized_envelope_value, object_fields, requires_ack_for_target,
26
+ tool_runtime_error,
25
27
  };
26
28
  use super::normalize::{
27
29
  compact_tool_result, normalize_report_envelope, normalize_result_status_observed,
28
30
  };
29
- use super::types::{Scope, SendOutcome, ToolError, ToolErrorReason, ToolOk, ToolResult, VisiblePeers};
31
+ use super::types::{
32
+ Scope, SendOutcome, ToolError, ToolErrorReason, ToolOk, ToolResult, VisiblePeers,
33
+ };
30
34
 
31
35
  // ═══════════════════════════════════════════════════════════════════════════
32
36
  // TeamOrchestratorTools (tools.py:72) — the 12 typed tool handlers.
@@ -64,7 +68,11 @@ impl TeamOrchestratorTools {
64
68
 
65
69
  /// Test/explicit-injection constructor: bind identity/scope directly instead of
66
70
  /// reading env (so contracts can exercise scoped behavior deterministically).
67
- pub fn with_identity(workspace: &Path, agent_id: Option<AgentId>, owner_team_id: Option<TeamKey>) -> Self {
71
+ pub fn with_identity(
72
+ workspace: &Path,
73
+ agent_id: Option<AgentId>,
74
+ owner_team_id: Option<TeamKey>,
75
+ ) -> Self {
68
76
  Self {
69
77
  workspace: std::fs::canonicalize(workspace).unwrap_or_else(|_| workspace.to_path_buf()),
70
78
  agent_id,
@@ -85,7 +93,11 @@ impl TeamOrchestratorTools {
85
93
  "ValueError",
86
94
  ));
87
95
  };
88
- let Some(task_id) = task.get("id").and_then(Value::as_str).and_then(non_empty_string) else {
96
+ let Some(task_id) = task
97
+ .get("id")
98
+ .and_then(Value::as_str)
99
+ .and_then(non_empty_string)
100
+ else {
89
101
  return Err(ToolError::new(
90
102
  ToolErrorReason::InvalidToolArguments,
91
103
  "assign_task task.id is required",
@@ -112,7 +124,12 @@ impl TeamOrchestratorTools {
112
124
  .map(|team| team.as_str().to_string())
113
125
  .or_else(|| assignment_team_key(&state));
114
126
  reconcile_assigned_task(&mut state, team_key.as_deref(), &task_value);
115
- save_runtime_state(&self.workspace, &state).map_err(tool_runtime_error)?;
127
+ save_runtime_state_reapplying_after_conflict(&self.workspace, &state, |latest| {
128
+ ensure_object(latest);
129
+ let latest_team_key = team_key.clone().or_else(|| assignment_team_key(latest));
130
+ reconcile_assigned_task(latest, latest_team_key.as_deref(), &task_value);
131
+ })
132
+ .map_err(tool_runtime_error)?;
116
133
 
117
134
  let content = assignment_message(task, message);
118
135
  let out = self.send_message(
@@ -191,7 +208,8 @@ impl TeamOrchestratorTools {
191
208
  ..SendOptions::default()
192
209
  };
193
210
  if is_worker_recipient(to) {
194
- let out = messaging::send_message(&self.workspace, to, content, &opts).map_err(tool_runtime_error)?;
211
+ let out = messaging::send_message(&self.workspace, to, content, &opts)
212
+ .map_err(tool_runtime_error)?;
195
213
  // tools.py:175-181 — accepted+poll_via ONLY for a REAL message_id; any other
196
214
  // outcome falls back to the compacted direct result. Never invent an
197
215
  // `mcp_<timestamp>` id: it does not exist in the store and makes the
@@ -209,7 +227,8 @@ impl TeamOrchestratorTools {
209
227
  message_id,
210
228
  });
211
229
  }
212
- let out = messaging::send_message(&self.workspace, to, content, &opts).map_err(tool_runtime_error)?;
230
+ let out = messaging::send_message(&self.workspace, to, content, &opts)
231
+ .map_err(tool_runtime_error)?;
213
232
  let value = delivery_outcome_value(&out);
214
233
  let ok = compact_tool_result(&value)?;
215
234
  Ok(SendOutcome::Direct(ok))
@@ -219,7 +238,11 @@ impl TeamOrchestratorTools {
219
238
  self.rpc_scope_refused("unknown", None, None)
220
239
  }
221
240
 
222
- pub(crate) fn validate_rpc_scope_args(&self, tool: &str, args: &Value) -> Result<(), ToolError> {
241
+ pub(crate) fn validate_rpc_scope_args(
242
+ &self,
243
+ tool: &str,
244
+ args: &Value,
245
+ ) -> Result<(), ToolError> {
223
246
  if let Some(nested) = args.get("task").or_else(|| args.get("envelope")) {
224
247
  self.validate_rpc_scope_args(tool, nested)?;
225
248
  }
@@ -240,10 +263,11 @@ impl TeamOrchestratorTools {
240
263
  Ok(state) => state,
241
264
  Err(error) => return Err(self.scope_unverifiable(&error.to_string())),
242
265
  };
243
- let requested_canonical = crate::state::projection::resolve_owner_team_id(&state, requested)
244
- .canonical_key()
245
- .unwrap_or(requested)
246
- .to_string();
266
+ let requested_canonical =
267
+ crate::state::projection::resolve_owner_team_id(&state, requested)
268
+ .canonical_key()
269
+ .unwrap_or(requested)
270
+ .to_string();
247
271
  requested_canonical != owner.as_str()
248
272
  }
249
273
  (None, Some(_)) => true,
@@ -280,13 +304,17 @@ impl TeamOrchestratorTools {
280
304
  if let Some(envelope) = envelope {
281
305
  self.validate_rpc_scope_args("report_result", envelope)?;
282
306
  }
283
- let mut base = envelope.cloned().unwrap_or_else(|| Value::Object(serde_json::Map::new()));
307
+ let mut base = envelope
308
+ .cloned()
309
+ .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
284
310
  ensure_object(&mut base);
285
311
  if let Some(obj) = base.as_object_mut() {
286
312
  if !obj.contains_key("summary") {
287
313
  obj.insert(
288
314
  "summary".to_string(),
289
- Value::String(summary.map_or_else(|| "completed".to_string(), ToString::to_string)),
315
+ Value::String(
316
+ summary.map_or_else(|| "completed".to_string(), ToString::to_string),
317
+ ),
290
318
  );
291
319
  }
292
320
  if !obj.contains_key("status") {
@@ -302,33 +330,38 @@ impl TeamOrchestratorTools {
302
330
  // task id and no result yet (message-scope correlation —
303
331
  // collect path: is_message_scoped_result)
304
332
  // 4. "manual" — truly uncorrelated; collect still rejects
305
- let owner_team_id_str = self
306
- .owner_team_id
307
- .as_ref()
308
- .map(|t| t.as_str().to_string());
333
+ let owner_team_id_str = self.owner_team_id.as_ref().map(|t| t.as_str().to_string());
309
334
  let resolved = task_id
310
335
  .map(ToString::to_string)
311
- .or_else(|| self.agent_id.as_ref().and_then(|agent| {
312
- latest_task_for_assignee(
313
- &self.workspace,
314
- agent.as_str(),
315
- owner_team_id_str.as_deref(),
316
- )
317
- }))
318
- .or_else(|| self.agent_id.as_ref().and_then(|agent| {
319
- latest_uncorrelated_delivered_message_for(
320
- &self.workspace,
321
- agent.as_str(),
322
- owner_team_id_str.as_deref(),
323
- )
324
- }))
336
+ .or_else(|| {
337
+ self.agent_id.as_ref().and_then(|agent| {
338
+ latest_task_for_assignee(
339
+ &self.workspace,
340
+ agent.as_str(),
341
+ owner_team_id_str.as_deref(),
342
+ )
343
+ })
344
+ })
345
+ .or_else(|| {
346
+ self.agent_id.as_ref().and_then(|agent| {
347
+ latest_uncorrelated_delivered_message_for(
348
+ &self.workspace,
349
+ agent.as_str(),
350
+ owner_team_id_str.as_deref(),
351
+ )
352
+ })
353
+ })
325
354
  .unwrap_or_else(|| "manual".to_string());
326
355
  obj.insert("task_id".to_string(), Value::String(resolved));
327
356
  }
328
357
  if !obj.contains_key("agent_id") {
329
358
  let resolved = agent_id
330
359
  .map(ToString::to_string)
331
- .or_else(|| self.agent_id.as_ref().map(|env_agent| env_agent.as_str().to_string()))
360
+ .or_else(|| {
361
+ self.agent_id
362
+ .as_ref()
363
+ .map(|env_agent| env_agent.as_str().to_string())
364
+ })
332
365
  .unwrap_or_else(|| "unknown".to_string());
333
366
  obj.insert("agent_id".to_string(), Value::String(resolved));
334
367
  }
@@ -364,8 +397,8 @@ impl TeamOrchestratorTools {
364
397
  &env_value,
365
398
  owner_team.as_ref().map(TeamKey::as_str),
366
399
  )
367
- .map_err(tool_runtime_error)
368
- .and_then(|value| compact_tool_result(&value))
400
+ .map_err(tool_runtime_error)
401
+ .and_then(|value| compact_tool_result(&value))
369
402
  }
370
403
 
371
404
  /// T3-1 cr verdict: the observable record of an unknown→Partial status
@@ -405,7 +438,12 @@ impl TeamOrchestratorTools {
405
438
  /// `reset_agent` (`tools.py:333-334`): delegated through the lifecycle tools facade.
406
439
  pub fn reset_agent(&self, agent_id: &str, discard_session: bool) -> ToolResult {
407
440
  let owner_team = self.canonical_owner_team_key()?;
408
- super::lifecycle_tools::reset_agent(&self.workspace, owner_team.as_ref(), agent_id, discard_session)
441
+ super::lifecycle_tools::reset_agent(
442
+ &self.workspace,
443
+ owner_team.as_ref(),
444
+ agent_id,
445
+ discard_session,
446
+ )
409
447
  }
410
448
 
411
449
  /// `add_agent` (`tools.py:336-337`): delegate to real lifecycle add-agent
@@ -442,7 +480,12 @@ impl TeamOrchestratorTools {
442
480
  }
443
481
 
444
482
  /// `fork_agent` (`tools.py:339-340`): delegated through the lifecycle tools facade.
445
- pub fn fork_agent(&self, source_agent_id: &str, as_agent_id: &str, label: Option<&str>) -> ToolResult {
483
+ pub fn fork_agent(
484
+ &self,
485
+ source_agent_id: &str,
486
+ as_agent_id: &str,
487
+ label: Option<&str>,
488
+ ) -> ToolResult {
446
489
  let owner_team = self.canonical_owner_team_key()?;
447
490
  super::lifecycle_tools::fork_agent(
448
491
  &self.workspace,
@@ -456,7 +499,12 @@ impl TeamOrchestratorTools {
456
499
  /// `request_human` (`tools.py:342-346`): create a `requires_ack` leader message via
457
500
  /// the shared leader-delivery funnel; sender = env / inferred / `"unknown"`.
458
501
  /// Returns `{ok:true, message_id, status:"needs_human"}`.
459
- pub fn request_human(&self, question: &str, task_id: Option<&str>, agent_id: Option<&str>) -> ToolResult {
502
+ pub fn request_human(
503
+ &self,
504
+ question: &str,
505
+ task_id: Option<&str>,
506
+ agent_id: Option<&str>,
507
+ ) -> ToolResult {
460
508
  let _owner_team = self.canonical_owner_team_key()?;
461
509
  let explicit_sender = agent_id.and_then(non_empty_string);
462
510
  let sender = explicit_sender
@@ -495,9 +543,15 @@ impl TeamOrchestratorTools {
495
543
  fields.insert("ok".to_string(), Value::Bool(outcome.ok));
496
544
  fields.insert(
497
545
  "message_id".to_string(),
498
- outcome.message_id.clone().map_or(Value::Null, Value::String),
546
+ outcome
547
+ .message_id
548
+ .clone()
549
+ .map_or(Value::Null, Value::String),
550
+ );
551
+ fields.insert(
552
+ "status".to_string(),
553
+ Value::String("needs_human".to_string()),
499
554
  );
500
- fields.insert("status".to_string(), Value::String("needs_human".to_string()));
501
555
  Ok(ToolOk { fields })
502
556
  }
503
557
 
@@ -507,7 +561,9 @@ impl TeamOrchestratorTools {
507
561
  let _owner_team = self.canonical_owner_team_key()?;
508
562
  messaging::stuck_list(&self.workspace)
509
563
  .map_err(tool_runtime_error)
510
- .map(|v| ToolOk { fields: object_fields(v) })
564
+ .map(|v| ToolOk {
565
+ fields: object_fields(v),
566
+ })
511
567
  }
512
568
 
513
569
  /// `stuck_cancel` (`tools.py:351-352`): delegate to [`messaging::stuck_cancel`];
@@ -532,10 +588,16 @@ impl TeamOrchestratorTools {
532
588
  });
533
589
  }
534
590
  };
535
- let suppressed_by = self.agent_id.as_ref().map(AgentId::as_str).unwrap_or("leader");
591
+ let suppressed_by = self
592
+ .agent_id
593
+ .as_ref()
594
+ .map(AgentId::as_str)
595
+ .unwrap_or("leader");
536
596
  messaging::stuck_cancel(&self.workspace, agent_id, alert, suppressed_by)
537
597
  .map_err(tool_runtime_error)
538
- .map(|v| ToolOk { fields: object_fields(v) })
598
+ .map(|v| ToolOk {
599
+ fields: object_fields(v),
600
+ })
539
601
  }
540
602
 
541
603
  /// `get_visible_peers` (`tools.py:226-247`): C16 scope-filtered peer list — live
@@ -577,7 +639,11 @@ impl TeamOrchestratorTools {
577
639
  /// non-`*`/non-leader string target NOT in the visible-peer scope and not the
578
640
  /// sender itself, with `scope != workspace`, → `Some(ToolError{PeerNotInScope})`
579
641
  /// (also writes `mcp.send_message_refused`). `None` = allowed to proceed.
580
- pub fn refuse_cross_team_peer(&self, to: &MessageTarget, scope_override: Option<Scope>) -> Option<ToolError> {
642
+ pub fn refuse_cross_team_peer(
643
+ &self,
644
+ to: &MessageTarget,
645
+ scope_override: Option<Scope>,
646
+ ) -> Option<ToolError> {
581
647
  let owner_team = match self.canonical_owner_team_key() {
582
648
  Ok(team) => team,
583
649
  Err(error) => return Some(error),
@@ -599,7 +665,10 @@ impl TeamOrchestratorTools {
599
665
  || target == "*"
600
666
  || target == "leader"
601
667
  || target == "Leader"
602
- || self.agent_id.as_ref().is_some_and(|id| id.as_str() == target)
668
+ || self
669
+ .agent_id
670
+ .as_ref()
671
+ .is_some_and(|id| id.as_str() == target)
603
672
  {
604
673
  return None;
605
674
  }
@@ -620,10 +689,7 @@ impl TeamOrchestratorTools {
620
689
  );
621
690
  let mut extra = serde_json::Map::new();
622
691
  extra.insert("status".to_string(), Value::String("refused".to_string()));
623
- extra.insert(
624
- "hint".to_string(),
625
- Value::String(hint.to_string()),
626
- );
692
+ extra.insert("hint".to_string(), Value::String(hint.to_string()));
627
693
  Some(ToolError {
628
694
  reason: ToolErrorReason::PeerNotInScope,
629
695
  exc_type: "PeerNotInScope".to_string(),
@@ -649,7 +715,9 @@ impl TeamOrchestratorTools {
649
715
  .and_then(Value::as_object)
650
716
  .is_some_and(|teams| !teams.is_empty())
651
717
  {
652
- return Err(self.scope_refused("TEAM_AGENT_OWNER_TEAM_ID is required for multi-team MCP"));
718
+ return Err(
719
+ self.scope_refused("TEAM_AGENT_OWNER_TEAM_ID is required for multi-team MCP")
720
+ );
653
721
  }
654
722
  return Ok(None);
655
723
  };
@@ -674,7 +742,9 @@ impl TeamOrchestratorTools {
674
742
  .and_then(Value::as_object)
675
743
  .is_some_and(|teams| !teams.is_empty())
676
744
  {
677
- return Err(self.scope_refused("TEAM_AGENT_OWNER_TEAM_ID is required for multi-team MCP"));
745
+ return Err(
746
+ self.scope_refused("TEAM_AGENT_OWNER_TEAM_ID is required for multi-team MCP")
747
+ );
678
748
  }
679
749
  return Ok(None);
680
750
  };
@@ -701,7 +771,10 @@ impl TeamOrchestratorTools {
701
771
  );
702
772
  let mut extra = serde_json::Map::new();
703
773
  extra.insert("status".to_string(), Value::String("refused".to_string()));
704
- extra.insert("kind".to_string(), Value::String("scope_unverifiable".to_string()));
774
+ extra.insert(
775
+ "kind".to_string(),
776
+ Value::String("scope_unverifiable".to_string()),
777
+ );
705
778
  extra.insert(
706
779
  "next_action".to_string(),
707
780
  Value::String("retry shortly or check the runtime state path".to_string()),
@@ -744,7 +817,11 @@ impl TeamOrchestratorTools {
744
817
  requested_scope: Option<&str>,
745
818
  ) -> ToolError {
746
819
  let owner_team_id = self.canonical_owner_team_key_for_event();
747
- let agent_id = self.agent_id.as_ref().map(AgentId::as_str).unwrap_or("unknown");
820
+ let agent_id = self
821
+ .agent_id
822
+ .as_ref()
823
+ .map(AgentId::as_str)
824
+ .unwrap_or("unknown");
748
825
  let _ = EventLog::new(&self.workspace).write(
749
826
  "mcp.scope_refused",
750
827
  serde_json::json!({
@@ -794,10 +871,20 @@ fn canonicalize_owner_team_id(state: &Value, owner_team_id: &str) -> Option<Stri
794
871
  }
795
872
 
796
873
  fn requested_team_arg(args: &Value) -> Option<String> {
797
- ["team", "team_id", "owner_team_id", "owner_team", "target_team"]
798
- .iter()
799
- .find_map(|key| args.get(*key).and_then(Value::as_str).filter(|s| !s.is_empty()))
800
- .map(ToString::to_string)
874
+ [
875
+ "team",
876
+ "team_id",
877
+ "owner_team_id",
878
+ "owner_team",
879
+ "target_team",
880
+ ]
881
+ .iter()
882
+ .find_map(|key| {
883
+ args.get(*key)
884
+ .and_then(Value::as_str)
885
+ .filter(|s| !s.is_empty())
886
+ })
887
+ .map(ToString::to_string)
801
888
  }
802
889
 
803
890
  fn requested_scope_arg(args: &Value) -> Option<String> {
@@ -875,14 +962,12 @@ fn write_team_tasks(state: &mut Value, team_key: &str, tasks: Vec<Value>) {
875
962
  let Some(teams_obj) = teams.as_object_mut() else {
876
963
  return;
877
964
  };
878
- let team = teams_obj
879
- .entry(team_key.to_string())
880
- .or_insert_with(|| {
881
- let mut team = serde_json::Map::new();
882
- team.insert("tasks".to_string(), Value::Array(Vec::new()));
883
- team.insert("status".to_string(), Value::String("alive".to_string()));
884
- Value::Object(team)
885
- });
965
+ let team = teams_obj.entry(team_key.to_string()).or_insert_with(|| {
966
+ let mut team = serde_json::Map::new();
967
+ team.insert("tasks".to_string(), Value::Array(Vec::new()));
968
+ team.insert("status".to_string(), Value::String("alive".to_string()));
969
+ Value::Object(team)
970
+ });
886
971
  let Some(team_obj) = team.as_object_mut() else {
887
972
  return;
888
973
  };
@@ -894,7 +979,11 @@ fn assignment_message(task: &Value, explicit: Option<&str>) -> String {
894
979
  return message.to_string();
895
980
  }
896
981
  for key in ["description", "title"] {
897
- if let Some(text) = task.get(key).and_then(Value::as_str).and_then(non_empty_string) {
982
+ if let Some(text) = task
983
+ .get(key)
984
+ .and_then(Value::as_str)
985
+ .and_then(non_empty_string)
986
+ {
898
987
  return text.to_string();
899
988
  }
900
989
  }
@@ -59,7 +59,10 @@ pub fn detect_idle_fallbacks(
59
59
  let idle_workers = stmt
60
60
  .query_map(params![team.as_str()], |row| row.get::<_, String>(0))?
61
61
  .collect::<Result<Vec<_>, _>>()?;
62
- let worker_set = workers.iter().cloned().collect::<std::collections::BTreeSet<_>>();
62
+ let worker_set = workers
63
+ .iter()
64
+ .cloned()
65
+ .collect::<std::collections::BTreeSet<_>>();
63
66
  let idle_set = idle_workers
64
67
  .iter()
65
68
  .cloned()
@@ -74,8 +77,16 @@ pub fn detect_idle_fallbacks(
74
77
  }
75
78
  let now = chrono::Utc::now().to_rfc3339();
76
79
  let mut next_state = state.clone();
77
- register_idle_fallback_suppression(&mut next_state, store, &team, &idle_workers, &now)?;
78
- crate::state::persist::save_runtime_state(workspace, &next_state)?;
80
+ let suppression_snapshots =
81
+ idle_fallback_suppression_snapshots(&next_state, store, &team, &idle_workers)?;
82
+ register_idle_fallback_suppression(&mut next_state, &team, &now, &suppression_snapshots);
83
+ crate::state::persist::save_runtime_state_reapplying_after_conflict(
84
+ workspace,
85
+ &next_state,
86
+ |latest| {
87
+ register_idle_fallback_suppression(latest, &team, &now, &suppression_snapshots);
88
+ },
89
+ )?;
79
90
  let alert_count = idle_workers.len();
80
91
  let content = format!(
81
92
  "Idle fallback: all workers idle while {obligation_count} undelivered obligation(s) remain."
@@ -213,7 +224,11 @@ fn active_team_key(workspace: &Path, state: &Value) -> String {
213
224
  .and_then(Value::as_str)
214
225
  .filter(|team| !team.is_empty())
215
226
  .map(ToString::to_string)
216
- .or_else(|| workspace.file_name().map(|name| name.to_string_lossy().to_string()))
227
+ .or_else(|| {
228
+ workspace
229
+ .file_name()
230
+ .map(|name| name.to_string_lossy().to_string())
231
+ })
217
232
  .unwrap_or_else(|| "current".to_string())
218
233
  }
219
234
 
@@ -298,14 +313,13 @@ fn idle_fallback_suppressed(
298
313
  && delivered == delivered_message_ids(store, team, agent_id)?)
299
314
  }
300
315
 
301
- fn register_idle_fallback_suppression(
302
- state: &mut Value,
316
+ fn idle_fallback_suppression_snapshots(
317
+ state: &Value,
303
318
  store: &MessageStore,
304
319
  team: &str,
305
320
  idle_workers: &[String],
306
- now: &str,
307
- ) -> Result<(), MessagingError> {
308
- let snapshots = idle_workers
321
+ ) -> Result<Vec<(String, Vec<String>, Vec<String>)>, MessagingError> {
322
+ idle_workers
309
323
  .iter()
310
324
  .map(|agent_id| {
311
325
  Ok((
@@ -314,23 +328,31 @@ fn register_idle_fallback_suppression(
314
328
  delivered_message_ids(store, team, agent_id)?,
315
329
  ))
316
330
  })
317
- .collect::<Result<Vec<_>, MessagingError>>()?;
331
+ .collect::<Result<Vec<_>, MessagingError>>()
332
+ }
333
+
334
+ fn register_idle_fallback_suppression(
335
+ state: &mut Value,
336
+ team: &str,
337
+ now: &str,
338
+ snapshots: &[(String, Vec<String>, Vec<String>)],
339
+ ) {
318
340
  let Some(root) = state.as_object_mut() else {
319
- return Ok(());
341
+ return;
320
342
  };
321
343
  let Some(coordinator) = root
322
344
  .entry("coordinator")
323
345
  .or_insert_with(|| serde_json::json!({}))
324
346
  .as_object_mut()
325
347
  else {
326
- return Ok(());
348
+ return;
327
349
  };
328
350
  let Some(last) = coordinator
329
351
  .entry("idle_fallback_last_fired_at")
330
352
  .or_insert_with(|| serde_json::json!({}))
331
353
  .as_object_mut()
332
354
  else {
333
- return Ok(());
355
+ return;
334
356
  };
335
357
  last.insert(team.to_string(), serde_json::Value::String(now.to_string()));
336
358
  let Some(all) = coordinator
@@ -338,18 +360,18 @@ fn register_idle_fallback_suppression(
338
360
  .or_insert_with(|| serde_json::json!({}))
339
361
  .as_object_mut()
340
362
  else {
341
- return Ok(());
363
+ return;
342
364
  };
343
365
  let Some(team_map) = all
344
366
  .entry(team.to_string())
345
367
  .or_insert_with(|| serde_json::json!({}))
346
368
  .as_object_mut()
347
369
  else {
348
- return Ok(());
370
+ return;
349
371
  };
350
372
  for (agent_id, assigned, delivered) in snapshots {
351
373
  let Some(agent_map) = team_map
352
- .entry(agent_id)
374
+ .entry(agent_id.clone())
353
375
  .or_insert_with(|| serde_json::json!({}))
354
376
  .as_object_mut()
355
377
  else {
@@ -367,7 +389,6 @@ fn register_idle_fallback_suppression(
367
389
  }),
368
390
  );
369
391
  }
370
- Ok(())
371
392
  }
372
393
 
373
394
  fn timestamp_in_future(ts: &str) -> bool {
@@ -407,7 +428,11 @@ fn assigned_task_ids(state: &Value, agent_id: &str) -> Vec<String> {
407
428
  tasks
408
429
  .iter()
409
430
  .filter(|task| task.get("assignee").and_then(Value::as_str) == Some(agent_id))
410
- .filter_map(|task| task.get("id").and_then(Value::as_str).map(ToString::to_string))
431
+ .filter_map(|task| {
432
+ task.get("id")
433
+ .and_then(Value::as_str)
434
+ .map(ToString::to_string)
435
+ })
411
436
  .collect::<Vec<_>>()
412
437
  })
413
438
  .unwrap_or_default();