@team-agent/installer 0.5.36 → 0.5.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.lock CHANGED
@@ -575,7 +575,7 @@ dependencies = [
575
575
 
576
576
  [[package]]
577
577
  name = "team-agent"
578
- version = "0.5.36"
578
+ version = "0.5.37"
579
579
  dependencies = [
580
580
  "anyhow",
581
581
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.36"
12
+ version = "0.5.37"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -1703,6 +1703,13 @@ pub(crate) fn attempt_due_recoveries(
1703
1703
  let Ok(state) = crate::state::persist::load_runtime_state(workspace) else {
1704
1704
  return;
1705
1705
  };
1706
+ // 0.5.37 (`architect res_b8a6d40f3765`, R8): clear stale `next_retry_at`
1707
+ // on terminal intents (succeeded/blocked/exhausted) BEFORE dispatching.
1708
+ // Without this, a terminal record whose past `next_retry_at` survives
1709
+ // a coordinator restart could get re-dispatched on the next tick,
1710
+ // producing a spurious `recovery_started` event for an intent whose
1711
+ // lifecycle already completed.
1712
+ clear_stale_terminal_next_retry_at(workspace);
1706
1713
  let due_agents = collect_due_recovery_agents(&state);
1707
1714
  for agent_id in due_agents {
1708
1715
  run_single_recovery(workspace, event_log, transport, &agent_id);
@@ -1723,7 +1730,12 @@ fn collect_due_recovery_agents(state: &Value) -> Vec<String> {
1723
1730
  .get("status")
1724
1731
  .and_then(Value::as_str)
1725
1732
  .unwrap_or("");
1726
- if !matches!(status, "scheduled") {
1733
+ // 0.5.37 R8: dispatch only for non-terminal states. `succeeded`,
1734
+ // `blocked`, `exhausted`, `backpressured` are terminal / awaiting
1735
+ // manual action — a lifecycle dispatch here would be double-fire
1736
+ // or wasted work. `scheduled` / `running` remain dispatchable so
1737
+ // an interrupted tick can resume.
1738
+ if !matches!(status, "scheduled" | "running") {
1727
1739
  continue;
1728
1740
  }
1729
1741
  let Some(next_retry_at) = intent
@@ -1741,6 +1753,44 @@ fn collect_due_recovery_agents(state: &Value) -> Vec<String> {
1741
1753
  due
1742
1754
  }
1743
1755
 
1756
+ /// 0.5.37 (`architect res_b8a6d40f3765`, R8): terminal recovery states
1757
+ /// (`succeeded`, `blocked`, `exhausted`) must not carry a stale
1758
+ /// `next_retry_at`. Load state, scrub the key on any terminal entry, and
1759
+ /// only persist when at least one row changed so we don't dirty the tick's
1760
+ /// atomic_save contract for idle workspaces.
1761
+ fn clear_stale_terminal_next_retry_at(workspace: &Path) {
1762
+ let Ok(mut state) = crate::state::persist::load_runtime_state(workspace) else {
1763
+ return;
1764
+ };
1765
+ let mut mutated = false;
1766
+ if let Some(agents) = recovery_intent_agents(&mut state) {
1767
+ for (_agent_id, entry) in agents.iter_mut() {
1768
+ let Some(obj) = entry.as_object_mut() else {
1769
+ continue;
1770
+ };
1771
+ let status = obj
1772
+ .get("status")
1773
+ .and_then(Value::as_str)
1774
+ .unwrap_or("")
1775
+ .to_string();
1776
+ if !matches!(status.as_str(), "succeeded" | "blocked" | "exhausted") {
1777
+ continue;
1778
+ }
1779
+ let had_stale = obj
1780
+ .get("next_retry_at")
1781
+ .filter(|v| !v.is_null())
1782
+ .is_some();
1783
+ if had_stale {
1784
+ obj.remove("next_retry_at");
1785
+ mutated = true;
1786
+ }
1787
+ }
1788
+ }
1789
+ if mutated {
1790
+ let _ = crate::state::persist::save_runtime_state(workspace, &state);
1791
+ }
1792
+ }
1793
+
1744
1794
  fn run_single_recovery(
1745
1795
  workspace: &Path,
1746
1796
  event_log: &EventLog,
@@ -1916,6 +1966,12 @@ fn write_recovery_intent_result(workspace: &Path, agent_id: &str, update: Recove
1916
1966
  obj.remove("blocked_reason");
1917
1967
  }
1918
1968
  }
1969
+ // 0.5.37 R8: transitioning into a terminal state clears the
1970
+ // stale `next_retry_at`; a future retry earns a fresh due
1971
+ // time when it re-enters `scheduled` through the detector.
1972
+ if matches!(update.status, "succeeded" | "blocked" | "exhausted") {
1973
+ obj.remove("next_retry_at");
1974
+ }
1919
1975
  }
1920
1976
  }
1921
1977
  let _ = crate::state::persist::save_runtime_state(workspace, &state);
@@ -257,6 +257,50 @@ fn r7_recovery_never_synthesizes_hidden_provider_turn_or_sdk_call() {
257
257
  );
258
258
  }
259
259
 
260
+ #[test]
261
+ fn r8_terminal_recovery_intents_are_idempotent_and_clear_due_time() {
262
+ for status in ["succeeded", "blocked", "exhausted"] {
263
+ let case = RecoveryCase::new(&format!("r8-terminal-{status}"));
264
+ case.seed_agents(&[("fe-admin", 429, "rate_limit", "baseline-429")]);
265
+ case.seed_due_recovery_with_status("fe-admin", status);
266
+
267
+ run_due_recoveries(&case);
268
+
269
+ let events = case.events();
270
+ assert!(
271
+ find_event(&events, "worker.abnormal_exit.recovery_started").is_none(),
272
+ "R8: terminal status `{status}` with stale next_retry_at must be skipped before lifecycle dispatch; events={events:?}"
273
+ );
274
+ let state = case.state();
275
+ let recovery = state
276
+ .pointer("/coordinator/abnormal_api_error_recovery/agents/fe-admin")
277
+ .expect("R8: terminal recovery entry remains auditable");
278
+ assert_eq!(recovery["status"], serde_json::json!(status));
279
+ assert!(
280
+ recovery.get("next_retry_at").is_none()
281
+ || recovery.get("next_retry_at") == Some(&serde_json::Value::Null),
282
+ "R8: terminal status `{status}` must clear stale next_retry_at so future ticks cannot reuse it; recovery={recovery}"
283
+ );
284
+ }
285
+ }
286
+
287
+ #[test]
288
+ fn r8_non_terminal_due_intents_still_dispatch() {
289
+ for status in ["scheduled", "running"] {
290
+ let case = RecoveryCase::new(&format!("r8-nonterminal-{status}"));
291
+ case.seed_agents(&[("fe-admin", 429, "rate_limit", "baseline-429")]);
292
+ case.seed_due_recovery_with_status("fe-admin", status);
293
+
294
+ run_due_recoveries(&case);
295
+
296
+ let events = case.events();
297
+ assert!(
298
+ find_event(&events, "worker.abnormal_exit.recovery_started").is_some(),
299
+ "R8: non-terminal status `{status}` with due next_retry_at must still dispatch recovery; events={events:?}"
300
+ );
301
+ }
302
+ }
303
+
260
304
  struct RecoveryCase {
261
305
  root: std::path::PathBuf,
262
306
  }
@@ -328,11 +372,15 @@ impl RecoveryCase {
328
372
  }
329
373
 
330
374
  fn seed_due_recovery(&self, agent: &str) {
375
+ self.seed_due_recovery_with_status(agent, "scheduled");
376
+ }
377
+
378
+ fn seed_due_recovery_with_status(&self, agent: &str, status: &str) {
331
379
  let mut state = self.state();
332
380
  state["coordinator"]["abnormal_api_error_recovery"]["agents"][agent] = serde_json::json!({
333
381
  "error_key": format!("worker.abnormal_exit.error:{agent}:rollout:api_error:seed"),
334
382
  "cohort_key": "research:claude_code:api_error:429:rate_limit",
335
- "status": "scheduled",
383
+ "status": status,
336
384
  "attempts": 0,
337
385
  "max_attempts": 2,
338
386
  "next_retry_at": "2000-01-01T00:00:00Z",
@@ -478,3 +526,11 @@ fn detect_abnormal_body() -> String {
478
526
  .expect("derive after detect_abnormal_exits");
479
527
  source[start..end].to_string()
480
528
  }
529
+
530
+ fn run_due_recoveries(case: &RecoveryCase) {
531
+ crate::coordinator::steps::abnormal::attempt_due_recoveries(
532
+ &case.root,
533
+ &crate::event_log::EventLog::new(&case.root),
534
+ &MockTransport::new(true),
535
+ );
536
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.36",
3
+ "version": "0.5.37",
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.36",
24
- "@team-agent/cli-darwin-x64": "0.5.36",
25
- "@team-agent/cli-linux-x64": "0.5.36"
23
+ "@team-agent/cli-darwin-arm64": "0.5.37",
24
+ "@team-agent/cli-darwin-x64": "0.5.37",
25
+ "@team-agent/cli-linux-x64": "0.5.37"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",