@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
package/Cargo.toml
CHANGED
|
@@ -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
|
-
|
|
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":
|
|
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.
|
|
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.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
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",
|