@team-agent/installer 0.5.42 → 0.5.44

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 (156) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +129 -54
  4. package/crates/team-agent/src/cli/diagnose.rs +1 -2
  5. package/crates/team-agent/src/cli/emit.rs +1 -7
  6. package/crates/team-agent/src/cli/helpers.rs +3 -1
  7. package/crates/team-agent/src/cli/leader.rs +2 -1
  8. package/crates/team-agent/src/cli/mod.rs +126 -16
  9. package/crates/team-agent/src/cli/named_address.rs +14 -5
  10. package/crates/team-agent/src/cli/profile.rs +19 -7
  11. package/crates/team-agent/src/cli/send.rs +9 -3
  12. package/crates/team-agent/src/cli/status.rs +8 -30
  13. package/crates/team-agent/src/cli/status_port.rs +1341 -1272
  14. package/crates/team-agent/src/cli/tests/base.rs +738 -660
  15. package/crates/team-agent/src/cli/tests/compile.rs +45 -18
  16. package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
  21. package/crates/team-agent/src/cli/tests/mod.rs +17 -7
  22. package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
  23. package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
  25. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
  26. package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
  27. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
  28. package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
  29. package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
  30. package/crates/team-agent/src/codex_app_server.rs +2 -5
  31. package/crates/team-agent/src/compiler/tests.rs +139 -33
  32. package/crates/team-agent/src/compiler.rs +55 -22
  33. package/crates/team-agent/src/conpty/backend.rs +23 -33
  34. package/crates/team-agent/src/coordinator/backoff.rs +2 -7
  35. package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
  36. package/crates/team-agent/src/coordinator/health.rs +46 -31
  37. package/crates/team-agent/src/coordinator/mod.rs +3 -3
  38. package/crates/team-agent/src/coordinator/orphan.rs +22 -10
  39. package/crates/team-agent/src/coordinator/steps/abnormal.rs +51 -56
  40. package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
  41. package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
  42. package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
  43. package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
  44. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
  45. package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
  46. package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
  47. package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
  48. package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
  49. package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
  50. package/crates/team-agent/src/db/message_store.rs +138 -30
  51. package/crates/team-agent/src/db/migration.rs +249 -61
  52. package/crates/team-agent/src/db/schema.rs +303 -82
  53. package/crates/team-agent/src/diagnose/comms.rs +9 -2
  54. package/crates/team-agent/src/diagnose/mod.rs +1 -3
  55. package/crates/team-agent/src/diagnose/orphans.rs +79 -61
  56. package/crates/team-agent/src/event_log.rs +70 -16
  57. package/crates/team-agent/src/layout/manager.rs +15 -4
  58. package/crates/team-agent/src/layout/mod.rs +4 -4
  59. package/crates/team-agent/src/layout/overlay.rs +10 -3
  60. package/crates/team-agent/src/layout/placement.rs +5 -1
  61. package/crates/team-agent/src/layout/recovery.rs +4 -2
  62. package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
  63. package/crates/team-agent/src/layout/sessions.rs +17 -9
  64. package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
  65. package/crates/team-agent/src/layout/worker_env.rs +87 -19
  66. package/crates/team-agent/src/leader/helpers.rs +7 -1
  67. package/crates/team-agent/src/leader/lease.rs +199 -89
  68. package/crates/team-agent/src/leader/owner_bind.rs +55 -22
  69. package/crates/team-agent/src/leader/provider_attribution.rs +25 -6
  70. package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
  71. package/crates/team-agent/src/leader/rediscover.rs +74 -25
  72. package/crates/team-agent/src/leader/registry.rs +1 -1
  73. package/crates/team-agent/src/leader/start.rs +75 -54
  74. package/crates/team-agent/src/leader/takeover.rs +46 -11
  75. package/crates/team-agent/src/leader/tests/basics.rs +320 -167
  76. package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
  77. package/crates/team-agent/src/leader/tests/identity.rs +428 -356
  78. package/crates/team-agent/src/leader/tests/idle.rs +285 -254
  79. package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
  80. package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
  81. package/crates/team-agent/src/leader/tests/mod.rs +115 -99
  82. package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
  83. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
  84. package/crates/team-agent/src/lib.rs +4 -4
  85. package/crates/team-agent/src/lifecycle/display.rs +7 -3
  86. package/crates/team-agent/src/lifecycle/launch.rs +55 -15
  87. package/crates/team-agent/src/lifecycle/mod.rs +9 -1
  88. package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
  89. package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
  90. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
  91. package/crates/team-agent/src/lifecycle/restart/common.rs +6 -2
  92. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
  93. package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
  94. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
  95. package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
  96. package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
  97. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
  98. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
  99. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
  100. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
  101. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
  102. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +3 -0
  103. package/crates/team-agent/src/lifecycle/tests.rs +2 -2
  104. package/crates/team-agent/src/main.rs +4 -4
  105. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
  106. package/crates/team-agent/src/mcp_server/mod.rs +11 -2
  107. package/crates/team-agent/src/mcp_server/types.rs +14 -3
  108. package/crates/team-agent/src/mcp_server/wire.rs +230 -68
  109. package/crates/team-agent/src/messaging/delivery.rs +20 -18
  110. package/crates/team-agent/src/messaging/helpers.rs +25 -4
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
  112. package/crates/team-agent/src/messaging/mod.rs +2 -3
  113. package/crates/team-agent/src/messaging/selftest.rs +46 -26
  114. package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
  115. package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
  116. package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
  117. package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
  118. package/crates/team-agent/src/messaging/trust.rs +25 -2
  119. package/crates/team-agent/src/messaging/watchers.rs +37 -9
  120. package/crates/team-agent/src/model/enums.rs +65 -16
  121. package/crates/team-agent/src/model/ids.rs +12 -3
  122. package/crates/team-agent/src/model/paths.rs +28 -7
  123. package/crates/team-agent/src/model/permissions.rs +176 -33
  124. package/crates/team-agent/src/model/routing.rs +66 -20
  125. package/crates/team-agent/src/model/spec.rs +365 -69
  126. package/crates/team-agent/src/model/task_graph.rs +36 -9
  127. package/crates/team-agent/src/model/yaml/tests.rs +24 -6
  128. package/crates/team-agent/src/model/yaml.rs +7 -6
  129. package/crates/team-agent/src/packaging/install.rs +23 -9
  130. package/crates/team-agent/src/packaging/migrate.rs +5 -7
  131. package/crates/team-agent/src/packaging/mod.rs +9 -1
  132. package/crates/team-agent/src/packaging/repair.rs +13 -6
  133. package/crates/team-agent/src/packaging/tests.rs +63 -16
  134. package/crates/team-agent/src/packaging/types.rs +22 -7
  135. package/crates/team-agent/src/platform/argv.rs +4 -1
  136. package/crates/team-agent/src/platform/file_lock.rs +22 -8
  137. package/crates/team-agent/src/platform/process.rs +54 -24
  138. package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
  139. package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
  140. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
  141. package/crates/team-agent/src/provider/classify.rs +127 -42
  142. package/crates/team-agent/src/provider/faults.rs +17 -5
  143. package/crates/team-agent/src/provider/helpers.rs +6 -5
  144. package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
  145. package/crates/team-agent/src/state/persist.rs +28 -0
  146. package/crates/team-agent/src/state/repository.rs +8 -3
  147. package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
  148. package/crates/team-agent/src/tmux_backend.rs +80 -44
  149. package/crates/team-agent/src/topology.rs +40 -20
  150. package/crates/team-agent/src/transport/test_support.rs +20 -23
  151. package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
  152. package/crates/team-agent/src/transport/tests/mod.rs +178 -187
  153. package/crates/team-agent/src/transport/tests/wire.rs +561 -525
  154. package/crates/team-agent/src/transport.rs +12 -17
  155. package/crates/team-agent/src/transport_factory.rs +29 -14
  156. package/package.json +4 -4
@@ -81,13 +81,13 @@ pub mod types;
81
81
  // ── Re-export the full module-root surface (RE-EXPORT INVARIANT) ──────────────
82
82
  // 这些 `pub use`/`pub(crate) use` 把每个曾在 module 根可见的 item 重新 surface 到
83
83
  // `crate::coordinator::X`,使外部 crate 路径 + 测试 `use super::*` 解析保持不变。
84
- pub use types::*;
85
- pub use tick::*;
86
84
  pub use backoff::*;
87
- pub use orphan::*;
88
85
  pub use health::*;
86
+ pub use orphan::*;
89
87
  pub use runtime_detectors::*;
90
88
  pub use runtime_observation::*;
89
+ pub use tick::*;
90
+ pub use types::*;
91
91
 
92
92
  #[cfg(test)]
93
93
  mod tests;
@@ -82,7 +82,10 @@ pub fn process_abnormal_records(
82
82
  .unwrap_or("unknown")
83
83
  .to_string();
84
84
  let signature_lower = signature.to_lowercase();
85
- let turn_id = record.get("turn_id").and_then(Value::as_str).map(TurnId::new);
85
+ let turn_id = record
86
+ .get("turn_id")
87
+ .and_then(Value::as_str)
88
+ .map(TurnId::new);
86
89
  let bucket = match &turn_id {
87
90
  Some(id) => id.as_str().to_string(),
88
91
  None => stable_fingerprint(record),
@@ -95,13 +98,14 @@ pub fn process_abnormal_records(
95
98
  if lists.whitelist.iter().any(|needle| raw.contains(needle)) {
96
99
  continue;
97
100
  }
98
- let decision = if lists.blacklist.iter().any(|needle| {
99
- raw.contains(needle) || signature_lower.contains(&needle.to_lowercase())
100
- }) {
101
- AbnormalDecision::NotifyBlacklist
102
- } else {
103
- AbnormalDecision::NotifyDefault
104
- };
101
+ let decision =
102
+ if lists.blacklist.iter().any(|needle| {
103
+ raw.contains(needle) || signature_lower.contains(&needle.to_lowercase())
104
+ }) {
105
+ AbnormalDecision::NotifyBlacklist
106
+ } else {
107
+ AbnormalDecision::NotifyDefault
108
+ };
105
109
  let kind = record.get("kind").and_then(Value::as_str);
106
110
  notifications.push(AbnormalNotification {
107
111
  signature,
@@ -141,7 +145,13 @@ pub fn detect_whole_team_gone(
141
145
  return whole_team_report(true, WholeTeamGoneClass::CleanShutdown, false, false, false);
142
146
  }
143
147
  if snapshot.restart_in_progress {
144
- return whole_team_report(true, WholeTeamGoneClass::RestartInProgress, false, false, false);
148
+ return whole_team_report(
149
+ true,
150
+ WholeTeamGoneClass::RestartInProgress,
151
+ false,
152
+ false,
153
+ false,
154
+ );
145
155
  }
146
156
  let marker_written = marker_store.set_marker(
147
157
  "whole_team_gone",
@@ -158,7 +168,9 @@ pub fn detect_whole_team_gone(
158
168
 
159
169
  fn stable_fingerprint(value: &Value) -> String {
160
170
  let mut hasher = std::collections::hash_map::DefaultHasher::new();
161
- serde_json::to_string(value).unwrap_or_else(|_| value.to_string()).hash(&mut hasher);
171
+ serde_json::to_string(value)
172
+ .unwrap_or_else(|_| value.to_string())
173
+ .hash(&mut hasher);
162
174
  format!("{:016x}", hasher.finish())
163
175
  }
164
176
 
@@ -178,11 +178,8 @@ pub(crate) fn detect_abnormal_exits(
178
178
  // intent (retryable only), then compose the policy-aware tail. This
179
179
  // is INTENT WRITE only — actual lifecycle work happens post-save
180
180
  // via `attempt_due_recoveries` (§7.3, R6 guard).
181
- let manual_command = recovery_manual_command(
182
- agent.agent_id.as_str(),
183
- workspace,
184
- team.as_str(),
185
- );
181
+ let manual_command =
182
+ recovery_manual_command(agent.agent_id.as_str(), workspace, team.as_str());
186
183
  let error_observation_key_string = error_observation_key.clone().unwrap_or_default();
187
184
  let (recovery_class, recovery_schedule, recovery_cohort_key, recovery_backpressure_until) =
188
185
  process_api_error_recovery_intent(
@@ -202,14 +199,8 @@ pub(crate) fn detect_abnormal_exits(
202
199
  &fact,
203
200
  &manual_command,
204
201
  );
205
- let content = format_abnormal_exit_message(
206
- &team,
207
- &agent,
208
- &fact,
209
- &liveness,
210
- size,
211
- &recovery_tail,
212
- );
202
+ let content =
203
+ format_abnormal_exit_message(&team, &agent, &fact, &liveness, size, &recovery_tail);
213
204
  let outcome = crate::messaging::send_to_leader_receiver(
214
205
  workspace,
215
206
  state,
@@ -722,8 +713,8 @@ fn command_process_check_with_marker(
722
713
  return check;
723
714
  }
724
715
  process_check(
725
- ProcessLiveness::Dead,
726
- format!("provider_not_foreground:{command}"),
716
+ ProcessLiveness::Unverifiable,
717
+ format!("provider_evidence_unverifiable:{command}"),
727
718
  )
728
719
  }
729
720
 
@@ -742,8 +733,8 @@ fn pane_command_process_check_with_marker(
742
733
  return check;
743
734
  }
744
735
  process_check(
745
- ProcessLiveness::Dead,
746
- format!("provider_not_foreground:{command}"),
736
+ ProcessLiveness::Unverifiable,
737
+ format!("provider_evidence_unverifiable:{command}"),
747
738
  )
748
739
  }
749
740
 
@@ -1471,7 +1462,10 @@ fn record_backpressure_event(
1471
1462
  if reset_window {
1472
1463
  obj.insert("window_started_at".to_string(), serde_json::json!(now_str));
1473
1464
  obj.insert("count".to_string(), serde_json::json!(0u64));
1474
- obj.insert("agents".to_string(), serde_json::json!(Vec::<String>::new()));
1465
+ obj.insert(
1466
+ "agents".to_string(),
1467
+ serde_json::json!(Vec::<String>::new()),
1468
+ );
1475
1469
  }
1476
1470
  obj.insert("last_seen_at".to_string(), serde_json::json!(now_str));
1477
1471
  let count = obj
@@ -1504,7 +1498,10 @@ fn record_backpressure_event(
1504
1498
  .map(|dt| dt.with_timezone(&chrono::Utc));
1505
1499
  } else if (count as usize) >= BACKPRESSURE_THRESHOLD {
1506
1500
  let until = now + cooldown;
1507
- obj.insert("cooldown_until".to_string(), serde_json::json!(until.to_rfc3339()));
1501
+ obj.insert(
1502
+ "cooldown_until".to_string(),
1503
+ serde_json::json!(until.to_rfc3339()),
1504
+ );
1508
1505
  obj.insert("status".to_string(), serde_json::json!("active"));
1509
1506
  cooldown_until = Some(until);
1510
1507
  just_activated = true;
@@ -1721,9 +1718,9 @@ fn process_api_error_recovery_intent(
1721
1718
  // cohort already holds a scheduled intent, defer the new arrival to
1722
1719
  // the cohort cooldown (or a synthetic short cooldown if none yet).
1723
1720
  if canary_active {
1724
- let synthetic = bp_decision.cooldown_until.unwrap_or_else(|| {
1725
- now + chrono::Duration::seconds(BACKPRESSURE_COOLDOWN_SECS)
1726
- });
1721
+ let synthetic = bp_decision
1722
+ .cooldown_until
1723
+ .unwrap_or_else(|| now + chrono::Duration::seconds(BACKPRESSURE_COOLDOWN_SECS));
1727
1724
  bp_decision.cooldown_until = Some(synthetic);
1728
1725
  }
1729
1726
  if bp_decision.just_activated {
@@ -1816,7 +1813,12 @@ fn process_api_error_recovery_intent(
1816
1813
  }),
1817
1814
  )?;
1818
1815
  }
1819
- Ok((class, schedule, bp_decision.cohort_key, bp_decision.cooldown_until))
1816
+ Ok((
1817
+ class,
1818
+ schedule,
1819
+ bp_decision.cohort_key,
1820
+ bp_decision.cooldown_until,
1821
+ ))
1820
1822
  }
1821
1823
 
1822
1824
  // ─────────────────────────────────────────────────────────────────────────
@@ -1837,16 +1839,22 @@ pub(crate) fn attempt_due_recoveries(
1837
1839
  event_log: &EventLog,
1838
1840
  transport: &dyn crate::transport::Transport,
1839
1841
  ) {
1840
- let Ok(state) = crate::state::persist::load_runtime_state(workspace) else {
1842
+ // 0.5.43 debt-sweep (§5 D-j): single fresh post-save load. Do NOT
1843
+ // re-use the tick's pre-save Value — that would reopen the 0.5.36
1844
+ // R6 lifecycle-save-clobbered-by-tick window (highest risk item in
1845
+ // this slice per locate). We own our load here, mutate in memory
1846
+ // (`clear_stale_terminal_next_retry_at` is now a pure
1847
+ // `(&mut Value) -> bool` scrub), persist through the existing
1848
+ // allowlisted root save when the scrub actually changed a row, and
1849
+ // then collect due agents from the SAME post-scrub Value so
1850
+ // terminal intents newly stripped of `next_retry_at` cannot double-
1851
+ // fire below.
1852
+ let Ok(mut state) = crate::state::persist::load_runtime_state(workspace) else {
1841
1853
  return;
1842
1854
  };
1843
- // 0.5.37 (`architect res_b8a6d40f3765`, R8): clear stale `next_retry_at`
1844
- // on terminal intents (succeeded/blocked/exhausted) BEFORE dispatching.
1845
- // Without this, a terminal record whose past `next_retry_at` survives
1846
- // a coordinator restart could get re-dispatched on the next tick,
1847
- // producing a spurious `recovery_started` event for an intent whose
1848
- // lifecycle already completed.
1849
- clear_stale_terminal_next_retry_at(workspace);
1855
+ if clear_stale_terminal_next_retry_at(&mut state) {
1856
+ let _ = crate::state::persist::save_runtime_state(workspace, &state);
1857
+ }
1850
1858
  let due_agents = collect_due_recovery_agents(&state);
1851
1859
  for agent_id in due_agents {
1852
1860
  run_single_recovery(workspace, event_log, transport, &agent_id);
@@ -1863,10 +1871,7 @@ fn collect_due_recovery_agents(state: &Value) -> Vec<String> {
1863
1871
  };
1864
1872
  let mut due = Vec::new();
1865
1873
  for (agent_id, intent) in agents {
1866
- let status = intent
1867
- .get("status")
1868
- .and_then(Value::as_str)
1869
- .unwrap_or("");
1874
+ let status = intent.get("status").and_then(Value::as_str).unwrap_or("");
1870
1875
  // 0.5.37 R8: dispatch only for non-terminal states. `succeeded`,
1871
1876
  // `blocked`, `exhausted`, `backpressured` are terminal / awaiting
1872
1877
  // manual action — a lifecycle dispatch here would be double-fire
@@ -1890,17 +1895,15 @@ fn collect_due_recovery_agents(state: &Value) -> Vec<String> {
1890
1895
  due
1891
1896
  }
1892
1897
 
1893
- /// 0.5.37 (`architect res_b8a6d40f3765`, R8): terminal recovery states
1894
- /// (`succeeded`, `blocked`, `exhausted`) must not carry a stale
1895
- /// `next_retry_at`. Load state, scrub the key on any terminal entry, and
1896
- /// only persist when at least one row changed so we don't dirty the tick's
1897
- /// atomic_save contract for idle workspaces.
1898
- fn clear_stale_terminal_next_retry_at(workspace: &Path) {
1899
- let Ok(mut state) = crate::state::persist::load_runtime_state(workspace) else {
1900
- return;
1901
- };
1898
+ /// 0.5.37 R8 + 0.5.43 debt-sweep §5 D-j: pure scrub helper. Terminal
1899
+ /// recovery intents (`succeeded`/`blocked`/`exhausted`) must not carry
1900
+ /// a stale `next_retry_at`. Returns true when at least one row was
1901
+ /// stripped, so the caller can save through its own load/write cycle
1902
+ /// (no double-load, no double-save). No I/O in this function — the
1903
+ /// only responsibility is mutation on a caller-owned `Value`.
1904
+ fn clear_stale_terminal_next_retry_at(state: &mut Value) -> bool {
1902
1905
  let mut mutated = false;
1903
- if let Some(agents) = recovery_intent_agents(&mut state) {
1906
+ if let Some(agents) = recovery_intent_agents(state) {
1904
1907
  for (_agent_id, entry) in agents.iter_mut() {
1905
1908
  let Some(obj) = entry.as_object_mut() else {
1906
1909
  continue;
@@ -1913,19 +1916,14 @@ fn clear_stale_terminal_next_retry_at(workspace: &Path) {
1913
1916
  if !matches!(status.as_str(), "succeeded" | "blocked" | "exhausted") {
1914
1917
  continue;
1915
1918
  }
1916
- let had_stale = obj
1917
- .get("next_retry_at")
1918
- .filter(|v| !v.is_null())
1919
- .is_some();
1919
+ let had_stale = obj.get("next_retry_at").filter(|v| !v.is_null()).is_some();
1920
1920
  if had_stale {
1921
1921
  obj.remove("next_retry_at");
1922
1922
  mutated = true;
1923
1923
  }
1924
1924
  }
1925
1925
  }
1926
- if mutated {
1927
- let _ = crate::state::persist::save_runtime_state(workspace, &state);
1928
- }
1926
+ mutated
1929
1927
  }
1930
1928
 
1931
1929
  fn run_single_recovery(
@@ -2094,10 +2092,7 @@ fn write_recovery_intent_result(workspace: &Path, agent_id: &str, update: Recove
2094
2092
  );
2095
2093
  match update.blocked_reason {
2096
2094
  Some(reason) => {
2097
- obj.insert(
2098
- "blocked_reason".to_string(),
2099
- serde_json::json!(reason),
2100
- );
2095
+ obj.insert("blocked_reason".to_string(), serde_json::json!(reason));
2101
2096
  }
2102
2097
  None => {
2103
2098
  obj.remove("blocked_reason");
@@ -22,9 +22,16 @@ fn abnormal_default_notify_for_structured_fault() {
22
22
  )
23
23
  .unwrap();
24
24
  assert_eq!(out.notifications.len(), 1);
25
- assert_eq!(out.notifications[0].decision, AbnormalDecision::NotifyDefault);
25
+ assert_eq!(
26
+ out.notifications[0].decision,
27
+ AbnormalDecision::NotifyDefault
28
+ );
26
29
  // §84:绝不触碰 provider adapter client。
27
- assert_eq!(reg.adapter_calls.get(), 0, "MUST NOT call adapter_for (provider client)");
30
+ assert_eq!(
31
+ reg.adapter_calls.get(),
32
+ 0,
33
+ "MUST NOT call adapter_for (provider client)"
34
+ );
28
35
  }
29
36
 
30
37
  #[test]
@@ -40,7 +47,10 @@ fn abnormal_blacklist_hit_yields_notify_blacklist() {
40
47
  )
41
48
  .unwrap();
42
49
  assert_eq!(out.notifications.len(), 1);
43
- assert_eq!(out.notifications[0].decision, AbnormalDecision::NotifyBlacklist);
50
+ assert_eq!(
51
+ out.notifications[0].decision,
52
+ AbnormalDecision::NotifyBlacklist
53
+ );
44
54
  assert_eq!(reg.adapter_calls.get(), 0);
45
55
  }
46
56
 
@@ -56,7 +66,11 @@ fn abnormal_whitelist_beats_blacklist_and_skips() {
56
66
  &AbnormalNotificationState::default(),
57
67
  )
58
68
  .unwrap();
59
- assert_eq!(out.notifications.len(), 0, "whitelist wins → skip (probe confirmed)");
69
+ assert_eq!(
70
+ out.notifications.len(),
71
+ 0,
72
+ "whitelist wins → skip (probe confirmed)"
73
+ );
60
74
  assert_eq!(reg.adapter_calls.get(), 0);
61
75
  }
62
76
 
@@ -91,7 +105,11 @@ fn abnormal_same_turn_id_folds_to_one_notification() {
91
105
  &AbnormalNotificationState::default(),
92
106
  )
93
107
  .unwrap();
94
- assert_eq!(out.notifications.len(), 1, "retry-loop in SAME turn folds (probe: 1)");
108
+ assert_eq!(
109
+ out.notifications.len(),
110
+ 1,
111
+ "retry-loop in SAME turn folds (probe: 1)"
112
+ );
95
113
  // seen 状态回存,key = "sig_a\0t1"。
96
114
  assert!(out.notification_state.seen.contains("sig_a\u{0}t1"));
97
115
  }
@@ -112,7 +130,11 @@ fn abnormal_missing_turn_id_distinct_faults_each_notify() {
112
130
  &AbnormalNotificationState::default(),
113
131
  )
114
132
  .unwrap();
115
- assert_eq!(out.notifications.len(), 2, "distinct raw → 2 notifies (probe)");
133
+ assert_eq!(
134
+ out.notifications.len(),
135
+ 2,
136
+ "distinct raw → 2 notifies (probe)"
137
+ );
116
138
  }
117
139
 
118
140
  #[test]
@@ -130,7 +152,11 @@ fn abnormal_missing_turn_id_identical_faults_fold() {
130
152
  &AbnormalNotificationState::default(),
131
153
  )
132
154
  .unwrap();
133
- assert_eq!(out.notifications.len(), 1, "identical raw folds even w/o turn_id (probe)");
155
+ assert_eq!(
156
+ out.notifications.len(),
157
+ 1,
158
+ "identical raw folds even w/o turn_id (probe)"
159
+ );
134
160
  }
135
161
 
136
162
  #[test]
@@ -140,14 +166,12 @@ fn abnormal_preseeded_seen_suppresses_duplicate_across_calls() {
140
166
  let mut state = AbnormalNotificationState::default();
141
167
  state.seen.insert("sig_a\u{0}t1".to_string());
142
168
  let records = vec![serde_json::json!({"signature": "sig_a", "turn_id": "t1", "raw": "boom"})];
143
- let out = process_abnormal_records(
144
- &records,
145
- &reg,
146
- Provider::Codex,
147
- &state,
148
- )
149
- .unwrap();
150
- assert_eq!(out.notifications.len(), 0, "pre-seeded seen suppresses (probe: 0)");
169
+ let out = process_abnormal_records(&records, &reg, Provider::Codex, &state).unwrap();
170
+ assert_eq!(
171
+ out.notifications.len(),
172
+ 0,
173
+ "pre-seeded seen suppresses (probe: 0)"
174
+ );
151
175
  }
152
176
 
153
177
  #[test]
@@ -387,7 +411,10 @@ fn whole_team_gone_clean_shutdown_is_silent() {
387
411
  assert!(!r.notify);
388
412
  assert!(!r.escalate_user_on_next_leader_command);
389
413
  assert!(!r.marker_written);
390
- assert!(ms.markers.is_empty(), "clean shutdown writes NO durable marker");
414
+ assert!(
415
+ ms.markers.is_empty(),
416
+ "clean shutdown writes NO durable marker"
417
+ );
391
418
  }
392
419
 
393
420
  #[test]
@@ -425,9 +452,15 @@ fn whole_team_gone_unexpected_writes_marker_and_defers_escalation() {
425
452
  assert!(r.whole_team_gone);
426
453
  assert_eq!(r.classification, WholeTeamGoneClass::UnexpectedExit);
427
454
  assert!(r.notify);
428
- assert!(r.escalate_user_on_next_leader_command, "defer to next leader command");
455
+ assert!(
456
+ r.escalate_user_on_next_leader_command,
457
+ "defer to next leader command"
458
+ );
429
459
  assert!(r.marker_written);
430
- assert!(ms.markers.contains_key("whole_team_gone"), "durable marker named whole_team_gone");
460
+ assert!(
461
+ ms.markers.contains_key("whole_team_gone"),
462
+ "durable marker named whole_team_gone"
463
+ );
431
464
  }
432
465
 
433
466
  #[test]
@@ -446,5 +479,8 @@ fn whole_team_gone_unexpected_marker_failure_reflected() {
446
479
  let r = detect_whole_team_gone(&snap, &mut ms);
447
480
  assert_eq!(r.classification, WholeTeamGoneClass::UnexpectedExit);
448
481
  assert!(r.notify);
449
- assert!(!r.marker_written, "marker set failed → marker_written false");
482
+ assert!(
483
+ !r.marker_written,
484
+ "marker set failed → marker_written false"
485
+ );
450
486
  }