@team-agent/installer 0.5.61 → 0.5.63
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/adapters.rs +5 -2
- package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
- package/crates/team-agent/src/cli/diagnose.rs +1 -0
- package/crates/team-agent/src/cli/emit.rs +41 -7
- package/crates/team-agent/src/cli/helpers.rs +1 -0
- package/crates/team-agent/src/cli/leader.rs +6 -1
- package/crates/team-agent/src/cli/leaders.rs +2 -0
- package/crates/team-agent/src/cli/mod.rs +33 -3
- package/crates/team-agent/src/cli/named_address.rs +8 -1
- package/crates/team-agent/src/cli/profile.rs +2 -1
- package/crates/team-agent/src/cli/send/coordinator.rs +1 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +3 -0
- package/crates/team-agent/src/cli/send/persist.rs +1 -0
- package/crates/team-agent/src/cli/send/presentation.rs +1 -0
- package/crates/team-agent/src/cli/send/resolve.rs +206 -1
- package/crates/team-agent/src/cli/send.rs +2 -1
- package/crates/team-agent/src/cli/spec.rs +1 -0
- package/crates/team-agent/src/cli/status.rs +2 -1
- package/crates/team-agent/src/cli/status_port/compact.rs +1 -0
- package/crates/team-agent/src/cli/status_port/format.rs +1 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +1 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
- package/crates/team-agent/src/cli/types.rs +3 -2
- package/crates/team-agent/src/communication_mode/mod.rs +1 -0
- package/crates/team-agent/src/compiler.rs +1 -0
- package/crates/team-agent/src/conpty/backend.rs +1 -0
- package/crates/team-agent/src/conpty/mod.rs +1 -0
- package/crates/team-agent/src/coordinator/backoff.rs +5 -8
- package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
- package/crates/team-agent/src/coordinator/health.rs +1 -0
- package/crates/team-agent/src/coordinator/mod.rs +2 -2
- package/crates/team-agent/src/coordinator/orphan.rs +4 -116
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +15 -866
- package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
- package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
- package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +139 -375
- package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
- package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -49
- package/crates/team-agent/src/coordinator/tick.rs +23 -40
- package/crates/team-agent/src/coordinator/types.rs +3 -176
- package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
- package/crates/team-agent/src/db/message_store.rs +2 -1
- package/crates/team-agent/src/db/migration.rs +5 -4
- package/crates/team-agent/src/db/mod.rs +1 -0
- package/crates/team-agent/src/db/schema.rs +1 -0
- package/crates/team-agent/src/diagnose/comms.rs +2 -1
- package/crates/team-agent/src/diagnose/mod.rs +1 -0
- package/crates/team-agent/src/diagnose/orphans.rs +1 -0
- package/crates/team-agent/src/layout/manager.rs +3 -7
- package/crates/team-agent/src/layout/mod.rs +2 -2
- package/crates/team-agent/src/layout/overlay.rs +6 -1
- package/crates/team-agent/src/layout/placement.rs +1 -0
- package/crates/team-agent/src/layout/recovery.rs +3 -0
- package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
- package/crates/team-agent/src/layout/sessions.rs +8 -1
- package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
- package/crates/team-agent/src/layout/worker_env.rs +3 -0
- package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
- package/crates/team-agent/src/leader/helpers.rs +1 -0
- package/crates/team-agent/src/leader/incident.rs +1 -0
- package/crates/team-agent/src/leader/lease.rs +27 -22
- package/crates/team-agent/src/leader/mod.rs +2 -3
- package/crates/team-agent/src/leader/owner_bind.rs +13 -3
- package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
- package/crates/team-agent/src/leader/rediscover.rs +4 -17
- package/crates/team-agent/src/leader/registry.rs +3 -15
- package/crates/team-agent/src/leader/start.rs +373 -125
- package/crates/team-agent/src/leader/takeover.rs +10 -232
- package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
- package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
- package/crates/team-agent/src/leader/tests/identity.rs +7 -2
- package/crates/team-agent/src/leader/tests/idle.rs +0 -193
- package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
- package/crates/team-agent/src/leader/tests/mod.rs +4 -41
- package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
- package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
- package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
- package/crates/team-agent/src/leader/types.rs +1 -28
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/display.rs +7 -1
- package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -27
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch.rs +11 -12
- package/crates/team-agent/src/lifecycle/mod.rs +2 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +3 -66
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
- package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
- package/crates/team-agent/src/lifecycle/restart.rs +13 -15
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
- package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
- package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
- package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
- package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
- package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
- package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
- package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
- package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
- package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
- package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
- package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
- package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
- package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
- package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
- package/crates/team-agent/src/lifecycle/tests.rs +59 -0
- package/crates/team-agent/src/lifecycle/types.rs +0 -9
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
- package/crates/team-agent/src/mcp_server/mod.rs +1 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +4 -3
- package/crates/team-agent/src/mcp_server/tools.rs +1 -0
- package/crates/team-agent/src/mcp_server/types.rs +1 -18
- package/crates/team-agent/src/mcp_server/wire.rs +5 -5
- package/crates/team-agent/src/messaging/activity.rs +1 -0
- package/crates/team-agent/src/messaging/address.rs +1 -0
- package/crates/team-agent/src/messaging/delivery.rs +4 -3
- package/crates/team-agent/src/messaging/helpers.rs +2 -1
- package/crates/team-agent/src/messaging/leader_channel.rs +1 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +2 -1
- package/crates/team-agent/src/messaging/mod.rs +10 -14
- package/crates/team-agent/src/messaging/peers.rs +2 -0
- package/crates/team-agent/src/messaging/persist.rs +3 -1
- package/crates/team-agent/src/messaging/presentation.rs +2 -1
- package/crates/team-agent/src/messaging/results.rs +1 -0
- package/crates/team-agent/src/messaging/scheduler.rs +1 -0
- package/crates/team-agent/src/messaging/selftest.rs +1 -1
- package/crates/team-agent/src/messaging/send.rs +41 -8
- package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
- package/crates/team-agent/src/messaging/types.rs +2 -2
- package/crates/team-agent/src/messaging/watchers.rs +19 -13
- package/crates/team-agent/src/model/enums.rs +1 -0
- package/crates/team-agent/src/model/errors.rs +1 -0
- package/crates/team-agent/src/model/ids.rs +1 -0
- package/crates/team-agent/src/model/mod.rs +1 -0
- package/crates/team-agent/src/model/name_similarity.rs +1 -0
- package/crates/team-agent/src/model/pane_authority_refusal.rs +1 -0
- package/crates/team-agent/src/model/paths.rs +1 -0
- package/crates/team-agent/src/model/permissions.rs +3 -2
- package/crates/team-agent/src/model/routing.rs +1 -0
- package/crates/team-agent/src/model/spec.rs +1 -0
- package/crates/team-agent/src/model/task_graph.rs +1 -0
- package/crates/team-agent/src/model/yaml/tests.rs +1 -0
- package/crates/team-agent/src/model/yaml.rs +1 -0
- package/crates/team-agent/src/packaging/install.rs +9 -147
- package/crates/team-agent/src/packaging/migrate.rs +2 -0
- package/crates/team-agent/src/packaging/mod.rs +1 -0
- package/crates/team-agent/src/packaging/repair.rs +2 -0
- package/crates/team-agent/src/packaging/tests.rs +29 -218
- package/crates/team-agent/src/packaging/types.rs +8 -15
- package/crates/team-agent/src/platform/argv.rs +4 -0
- package/crates/team-agent/src/platform/errors.rs +10 -1
- package/crates/team-agent/src/platform/file_lock.rs +4 -157
- package/crates/team-agent/src/platform/mod.rs +0 -61
- package/crates/team-agent/src/platform/process.rs +1 -0
- package/crates/team-agent/src/provider/adapter.rs +1 -0
- package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
- package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
- package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
- package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
- package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
- package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
- package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
- package/crates/team-agent/src/provider/classify.rs +1 -0
- package/crates/team-agent/src/provider/faults.rs +1 -26
- package/crates/team-agent/src/provider/helpers.rs +1 -0
- package/crates/team-agent/src/provider/mod.rs +1 -1
- package/crates/team-agent/src/provider/session/capture.rs +1 -4
- package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
- package/crates/team-agent/src/provider/session/mod.rs +2 -4
- package/crates/team-agent/src/provider/session/resume.rs +2 -209
- package/crates/team-agent/src/provider/session_scan/claude.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
- package/crates/team-agent/src/provider/session_scan.rs +1 -42
- package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
- package/crates/team-agent/src/provider/types.rs +1 -0
- package/crates/team-agent/src/provider/wire.rs +1 -0
- package/crates/team-agent/src/state/identity.rs +16 -5
- package/crates/team-agent/src/state/identity_keys.rs +1 -0
- package/crates/team-agent/src/state/mod.rs +2 -0
- package/crates/team-agent/src/state/owner_gate.rs +5 -0
- package/crates/team-agent/src/state/ownership.rs +10 -4
- package/crates/team-agent/src/state/paths.rs +12 -0
- package/crates/team-agent/src/state/persist.rs +9 -3
- package/crates/team-agent/src/state/projection.rs +15 -2
- package/crates/team-agent/src/state/repository/intent.rs +1 -0
- package/crates/team-agent/src/state/repository/tests.rs +1 -0
- package/crates/team-agent/src/state/repository.rs +7 -13
- package/crates/team-agent/src/state/selector.rs +1 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
- package/crates/team-agent/src/tmux_backend.rs +63 -0
- package/crates/team-agent/src/transport.rs +15 -0
- package/package.json +4 -4
- package/crates/team-agent/src/leader/inject.rs +0 -33
- package/crates/team-agent/src/provider/command.rs +0 -80
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
//!
|
|
1
2
|
//! unit-11 (Stage 4) — coordinator tick `abnormal` step group.
|
|
2
3
|
//!
|
|
3
4
|
//! Abnormal-exit detection + classification step extracted from
|
|
@@ -15,6 +16,7 @@ use super::super::health::pid_is_running;
|
|
|
15
16
|
use super::super::tick::TickError;
|
|
16
17
|
use super::super::types::Pid;
|
|
17
18
|
|
|
19
|
+
///
|
|
18
20
|
/// #236 `worker.abnormal_exit` watcher.
|
|
19
21
|
///
|
|
20
22
|
/// Notify only when the bounded transcript/rollout tail contains a latest explicit
|
|
@@ -176,34 +178,19 @@ pub(crate) fn detect_abnormal_exits(
|
|
|
176
178
|
{
|
|
177
179
|
continue;
|
|
178
180
|
}
|
|
179
|
-
// 0.5.36 (`.team/artifacts/supermarket-api-error-recovery-locate.md`
|
|
180
|
-
// §7.1/§7.6/§7.5): classify, update backpressure, record recovery
|
|
181
|
-
// intent (retryable only), then compose the policy-aware tail. This
|
|
182
|
-
// is INTENT WRITE only — actual lifecycle work happens post-save
|
|
183
|
-
// via `attempt_due_recoveries` (§7.3, R6 guard).
|
|
184
181
|
let manual_command =
|
|
185
182
|
recovery_manual_command(agent.agent_id.as_str(), workspace, team.as_str());
|
|
186
|
-
let
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
&agent,
|
|
193
|
-
&fact,
|
|
194
|
-
&manual_command,
|
|
195
|
-
error_observation_key_string.as_str(),
|
|
196
|
-
)?;
|
|
197
|
-
let recovery_tail = recovery_notification_tail(
|
|
198
|
-
recovery_class,
|
|
199
|
-
recovery_schedule.as_ref(),
|
|
200
|
-
recovery_backpressure_until,
|
|
201
|
-
&recovery_cohort_key,
|
|
183
|
+
let manual_recovery_hint = format!(
|
|
184
|
+
"No automatic restart was performed.\nTo recover manually, run: {manual_command}"
|
|
185
|
+
);
|
|
186
|
+
let content = format_abnormal_exit_message(
|
|
187
|
+
&team,
|
|
188
|
+
&agent,
|
|
202
189
|
&fact,
|
|
203
|
-
&
|
|
190
|
+
&liveness,
|
|
191
|
+
size,
|
|
192
|
+
&manual_recovery_hint,
|
|
204
193
|
);
|
|
205
|
-
let content =
|
|
206
|
-
format_abnormal_exit_message(&team, &agent, &fact, &liveness, size, &recovery_tail);
|
|
207
194
|
let outcome = crate::messaging::send_to_leader_receiver(
|
|
208
195
|
workspace,
|
|
209
196
|
state,
|
|
@@ -967,6 +954,7 @@ fn abnormal_last_error_observation_cohort(state: &Value, agent_id: &str) -> Opti
|
|
|
967
954
|
/// P1: Python `_TAIL_BYTES` parity (idle_takeover_wiring.py:13) — RS must not read less.
|
|
968
955
|
const ABNORMAL_TAIL_BYTES: u64 = 131_072;
|
|
969
956
|
|
|
957
|
+
///
|
|
970
958
|
/// P1: bounded tail read; a partial first line is harmless (the consumer only parses
|
|
971
959
|
/// the latest complete JSONL record) and lossy UTF-8 keeps a mid-codepoint seek safe.
|
|
972
960
|
pub(crate) fn read_tail_text(path: &Path, max_bytes: u64) -> std::io::Result<String> {
|
|
@@ -1297,7 +1285,7 @@ fn format_abnormal_exit_message(
|
|
|
1297
1285
|
fact: &crate::provider::FaultFact,
|
|
1298
1286
|
liveness: &ProcessCheck,
|
|
1299
1287
|
size: u64,
|
|
1300
|
-
|
|
1288
|
+
manual_recovery_hint: &str,
|
|
1301
1289
|
) -> String {
|
|
1302
1290
|
let turn_id = fact.turn_id.as_ref().map(|id| id.as_str()).unwrap_or("-");
|
|
1303
1291
|
format!(
|
|
@@ -1311,7 +1299,7 @@ turn_id: {turn_id}\n\
|
|
|
1311
1299
|
transcript: {path}\n\
|
|
1312
1300
|
last_offset: {size}\n\
|
|
1313
1301
|
pid_status: {pid_status}\n\n\
|
|
1314
|
-
{
|
|
1302
|
+
{manual_recovery_hint}",
|
|
1315
1303
|
node = agent.agent_id.as_str(),
|
|
1316
1304
|
provider = provider_wire(agent.provider),
|
|
1317
1305
|
signature = fact.signature.as_str(),
|
|
@@ -1320,106 +1308,6 @@ pid_status: {pid_status}\n\n\
|
|
|
1320
1308
|
)
|
|
1321
1309
|
}
|
|
1322
1310
|
|
|
1323
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1324
|
-
// 0.5.36 (`.team/artifacts/supermarket-api-error-recovery-locate.md` §7-§8):
|
|
1325
|
-
// api_error recovery classifier + policy + state I/O + notification tail.
|
|
1326
|
-
// Recovery EXECUTION lives post-atomic_save in `attempt_due_recoveries`
|
|
1327
|
-
// below; `detect_abnormal_exits` only RECORDS intent (§7.3, R6 guard).
|
|
1328
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1329
|
-
|
|
1330
|
-
/// 0.5.36 §7.1 classifier. Retryable = transient provider outage; recovery
|
|
1331
|
-
/// may be scheduled. NonRetryable = configuration / auth issue; guide only.
|
|
1332
|
-
/// Unknown = neither confirmed retryable nor known-bad; guide only.
|
|
1333
|
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
1334
|
-
pub(crate) enum ApiErrorRecoveryClass {
|
|
1335
|
-
Retryable,
|
|
1336
|
-
NonRetryable,
|
|
1337
|
-
Unknown,
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
impl ApiErrorRecoveryClass {
|
|
1341
|
-
fn as_str(self) -> &'static str {
|
|
1342
|
-
match self {
|
|
1343
|
-
Self::Retryable => "retryable",
|
|
1344
|
-
Self::NonRetryable => "non_retryable",
|
|
1345
|
-
Self::Unknown => "unknown",
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
/// 0.5.36 §7.1: rules keyed on status/error text.
|
|
1351
|
-
pub(crate) fn classify_api_error_recovery(
|
|
1352
|
-
signature: &str,
|
|
1353
|
-
api_error_status: Option<i64>,
|
|
1354
|
-
error: Option<&str>,
|
|
1355
|
-
) -> ApiErrorRecoveryClass {
|
|
1356
|
-
if signature != "api_error" {
|
|
1357
|
-
return ApiErrorRecoveryClass::Unknown;
|
|
1358
|
-
}
|
|
1359
|
-
if let Some(status) = api_error_status {
|
|
1360
|
-
return match status {
|
|
1361
|
-
408 | 429 => ApiErrorRecoveryClass::Retryable,
|
|
1362
|
-
s if (500..600).contains(&s) => ApiErrorRecoveryClass::Retryable,
|
|
1363
|
-
400 | 401 | 403 | 404 => ApiErrorRecoveryClass::NonRetryable,
|
|
1364
|
-
_ => classify_error_text(error),
|
|
1365
|
-
};
|
|
1366
|
-
}
|
|
1367
|
-
classify_error_text(error)
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
fn classify_error_text(error: Option<&str>) -> ApiErrorRecoveryClass {
|
|
1371
|
-
match error {
|
|
1372
|
-
Some(text) => {
|
|
1373
|
-
let lower = text.to_ascii_lowercase();
|
|
1374
|
-
if ["rate_limit", "overloaded", "timeout"]
|
|
1375
|
-
.iter()
|
|
1376
|
-
.any(|needle| lower.contains(needle))
|
|
1377
|
-
{
|
|
1378
|
-
ApiErrorRecoveryClass::Retryable
|
|
1379
|
-
} else if [
|
|
1380
|
-
"model_not_found",
|
|
1381
|
-
"auth",
|
|
1382
|
-
"invalid_request",
|
|
1383
|
-
"not_found_error",
|
|
1384
|
-
]
|
|
1385
|
-
.iter()
|
|
1386
|
-
.any(|needle| lower.contains(needle))
|
|
1387
|
-
{
|
|
1388
|
-
ApiErrorRecoveryClass::NonRetryable
|
|
1389
|
-
} else {
|
|
1390
|
-
ApiErrorRecoveryClass::Unknown
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
None => ApiErrorRecoveryClass::Unknown,
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
// 0.5.36 §7.2 defaults — code constants; a later config slice may expose
|
|
1398
|
-
// them without a new CLI flag.
|
|
1399
|
-
pub(crate) const RECOVERY_MAX_ATTEMPTS: u64 = 2;
|
|
1400
|
-
pub(crate) const RECOVERY_BACKOFF_SECS: [i64; 2] = [30, 120];
|
|
1401
|
-
pub(crate) const BACKPRESSURE_THRESHOLD: usize = 3;
|
|
1402
|
-
pub(crate) const BACKPRESSURE_WINDOW_SECS: i64 = 120;
|
|
1403
|
-
pub(crate) const BACKPRESSURE_COOLDOWN_SECS: i64 = 300;
|
|
1404
|
-
|
|
1405
|
-
fn now_utc() -> chrono::DateTime<chrono::Utc> {
|
|
1406
|
-
chrono::Utc::now()
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
fn cohort_key_for(team: &str, provider: &str, fact: &crate::provider::FaultFact) -> String {
|
|
1410
|
-
let status = fact
|
|
1411
|
-
.api_error_status
|
|
1412
|
-
.map(|s| s.to_string())
|
|
1413
|
-
.unwrap_or_else(|| "-".to_string());
|
|
1414
|
-
let error = fact.error.as_deref().unwrap_or("-");
|
|
1415
|
-
format!(
|
|
1416
|
-
"{team}:{provider}:{signature}:{status}:{error}",
|
|
1417
|
-
signature = fact.signature.as_str()
|
|
1418
|
-
)
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
/// 0.5.36 §7.2 manual command line. shell-single-quoted workspace so paths
|
|
1422
|
-
/// with spaces survive copy/paste (§7.5).
|
|
1423
1311
|
fn recovery_manual_command(agent_id: &str, workspace: &Path, team: &str) -> String {
|
|
1424
1312
|
let workspace_str = workspace.to_string_lossy();
|
|
1425
1313
|
let shell_workspace = workspace_str.replace('\'', "'\\''");
|
|
@@ -1428,747 +1316,12 @@ fn recovery_manual_command(agent_id: &str, workspace: &Path, team: &str) -> Stri
|
|
|
1428
1316
|
)
|
|
1429
1317
|
}
|
|
1430
1318
|
|
|
1431
|
-
fn recovery_intent_root<'a>(
|
|
1432
|
-
state: &'a mut Value,
|
|
1433
|
-
) -> Option<&'a mut serde_json::Map<String, Value>> {
|
|
1434
|
-
coordinator_child_object(state, "abnormal_api_error_recovery")
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
fn recovery_intent_agents<'a>(
|
|
1438
|
-
state: &'a mut Value,
|
|
1439
|
-
) -> Option<&'a mut serde_json::Map<String, Value>> {
|
|
1440
|
-
let root = recovery_intent_root(state)?;
|
|
1441
|
-
let agents = root
|
|
1442
|
-
.entry("agents".to_string())
|
|
1443
|
-
.or_insert_with(|| serde_json::json!({}));
|
|
1444
|
-
if !agents.is_object() {
|
|
1445
|
-
*agents = serde_json::json!({});
|
|
1446
|
-
}
|
|
1447
|
-
agents.as_object_mut()
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
fn recovery_backpressure<'a>(
|
|
1451
|
-
state: &'a mut Value,
|
|
1452
|
-
) -> Option<&'a mut serde_json::Map<String, Value>> {
|
|
1453
|
-
let root = recovery_intent_root(state)?;
|
|
1454
|
-
let bp = root
|
|
1455
|
-
.entry("backpressure".to_string())
|
|
1456
|
-
.or_insert_with(|| serde_json::json!({}));
|
|
1457
|
-
if !bp.is_object() {
|
|
1458
|
-
*bp = serde_json::json!({});
|
|
1459
|
-
}
|
|
1460
|
-
bp.as_object_mut()
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
/// 0.5.36 §7.6: increment the cohort counter, prune stale events outside the
|
|
1464
|
-
/// sliding window, and return the current active-cooldown state (if any) so
|
|
1465
|
-
/// the caller can decide canary vs deferral. Only fresh retryable errors get
|
|
1466
|
-
/// here — non-retryable/unknown never touch backpressure.
|
|
1467
|
-
fn record_backpressure_event(
|
|
1468
|
-
state: &mut Value,
|
|
1469
|
-
team: &str,
|
|
1470
|
-
provider: &str,
|
|
1471
|
-
fact: &crate::provider::FaultFact,
|
|
1472
|
-
agent_id: &str,
|
|
1473
|
-
now: chrono::DateTime<chrono::Utc>,
|
|
1474
|
-
) -> BackpressureDecision {
|
|
1475
|
-
let cohort = cohort_key_for(team, provider, fact);
|
|
1476
|
-
let now_str = now.to_rfc3339();
|
|
1477
|
-
let window = chrono::Duration::seconds(BACKPRESSURE_WINDOW_SECS);
|
|
1478
|
-
let cooldown = chrono::Duration::seconds(BACKPRESSURE_COOLDOWN_SECS);
|
|
1479
|
-
let Some(bp) = recovery_backpressure(state) else {
|
|
1480
|
-
return BackpressureDecision {
|
|
1481
|
-
cooldown_until: None,
|
|
1482
|
-
just_activated: false,
|
|
1483
|
-
cohort_key: cohort,
|
|
1484
|
-
};
|
|
1485
|
-
};
|
|
1486
|
-
let entry = bp
|
|
1487
|
-
.entry(cohort.clone())
|
|
1488
|
-
.or_insert_with(|| serde_json::json!({}));
|
|
1489
|
-
let obj = match entry.as_object_mut() {
|
|
1490
|
-
Some(obj) => obj,
|
|
1491
|
-
None => {
|
|
1492
|
-
*entry = serde_json::json!({});
|
|
1493
|
-
entry.as_object_mut().expect("just replaced with object")
|
|
1494
|
-
}
|
|
1495
|
-
};
|
|
1496
|
-
let window_started = obj
|
|
1497
|
-
.get("window_started_at")
|
|
1498
|
-
.and_then(Value::as_str)
|
|
1499
|
-
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
|
|
1500
|
-
.map(|dt| dt.with_timezone(&chrono::Utc));
|
|
1501
|
-
let reset_window = window_started
|
|
1502
|
-
.map(|start| now.signed_duration_since(start) > window)
|
|
1503
|
-
.unwrap_or(true);
|
|
1504
|
-
if reset_window {
|
|
1505
|
-
obj.insert("window_started_at".to_string(), serde_json::json!(now_str));
|
|
1506
|
-
obj.insert("count".to_string(), serde_json::json!(0u64));
|
|
1507
|
-
obj.insert(
|
|
1508
|
-
"agents".to_string(),
|
|
1509
|
-
serde_json::json!(Vec::<String>::new()),
|
|
1510
|
-
);
|
|
1511
|
-
}
|
|
1512
|
-
obj.insert("last_seen_at".to_string(), serde_json::json!(now_str));
|
|
1513
|
-
let count = obj
|
|
1514
|
-
.get("count")
|
|
1515
|
-
.and_then(Value::as_u64)
|
|
1516
|
-
.unwrap_or(0)
|
|
1517
|
-
.saturating_add(1);
|
|
1518
|
-
obj.insert("count".to_string(), serde_json::json!(count));
|
|
1519
|
-
let agents_arr = obj
|
|
1520
|
-
.entry("agents".to_string())
|
|
1521
|
-
.or_insert_with(|| serde_json::json!([]));
|
|
1522
|
-
if let Some(list) = agents_arr.as_array_mut() {
|
|
1523
|
-
if !list.iter().any(|v| v.as_str() == Some(agent_id)) {
|
|
1524
|
-
list.push(serde_json::json!(agent_id));
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
let previously_active = obj
|
|
1528
|
-
.get("cooldown_until")
|
|
1529
|
-
.and_then(Value::as_str)
|
|
1530
|
-
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
|
|
1531
|
-
.map(|dt| dt.with_timezone(&chrono::Utc) > now)
|
|
1532
|
-
.unwrap_or(false);
|
|
1533
|
-
let mut just_activated = false;
|
|
1534
|
-
let mut cooldown_until: Option<chrono::DateTime<chrono::Utc>> = None;
|
|
1535
|
-
if previously_active {
|
|
1536
|
-
cooldown_until = obj
|
|
1537
|
-
.get("cooldown_until")
|
|
1538
|
-
.and_then(Value::as_str)
|
|
1539
|
-
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
|
|
1540
|
-
.map(|dt| dt.with_timezone(&chrono::Utc));
|
|
1541
|
-
} else if (count as usize) >= BACKPRESSURE_THRESHOLD {
|
|
1542
|
-
let until = now + cooldown;
|
|
1543
|
-
obj.insert(
|
|
1544
|
-
"cooldown_until".to_string(),
|
|
1545
|
-
serde_json::json!(until.to_rfc3339()),
|
|
1546
|
-
);
|
|
1547
|
-
obj.insert("status".to_string(), serde_json::json!("active"));
|
|
1548
|
-
cooldown_until = Some(until);
|
|
1549
|
-
just_activated = true;
|
|
1550
|
-
}
|
|
1551
|
-
BackpressureDecision {
|
|
1552
|
-
cooldown_until,
|
|
1553
|
-
just_activated,
|
|
1554
|
-
cohort_key: cohort,
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
struct BackpressureDecision {
|
|
1559
|
-
cooldown_until: Option<chrono::DateTime<chrono::Utc>>,
|
|
1560
|
-
just_activated: bool,
|
|
1561
|
-
cohort_key: String,
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
/// 0.5.36 §7.2: reserve or update the per-agent recovery intent. Returns the
|
|
1565
|
-
/// scheduled `next_retry_at` for the notification/event.
|
|
1566
|
-
fn schedule_recovery_intent(
|
|
1567
|
-
state: &mut Value,
|
|
1568
|
-
agent_id: &str,
|
|
1569
|
-
cohort_key: &str,
|
|
1570
|
-
error_key: &str,
|
|
1571
|
-
manual_command: &str,
|
|
1572
|
-
now: chrono::DateTime<chrono::Utc>,
|
|
1573
|
-
cooldown_until: Option<chrono::DateTime<chrono::Utc>>,
|
|
1574
|
-
) -> Option<RecoveryIntentSchedule> {
|
|
1575
|
-
let Some(agents) = recovery_intent_agents(state) else {
|
|
1576
|
-
return None;
|
|
1577
|
-
};
|
|
1578
|
-
let existing = agents.get(agent_id).cloned();
|
|
1579
|
-
let attempts = existing
|
|
1580
|
-
.as_ref()
|
|
1581
|
-
.and_then(|v| v.get("attempts").and_then(Value::as_u64))
|
|
1582
|
-
.unwrap_or(0);
|
|
1583
|
-
if attempts >= RECOVERY_MAX_ATTEMPTS {
|
|
1584
|
-
return None;
|
|
1585
|
-
}
|
|
1586
|
-
let backoff = RECOVERY_BACKOFF_SECS
|
|
1587
|
-
.get(attempts as usize)
|
|
1588
|
-
.copied()
|
|
1589
|
-
.unwrap_or(*RECOVERY_BACKOFF_SECS.last().unwrap_or(&120));
|
|
1590
|
-
let mut next_retry = now + chrono::Duration::seconds(backoff);
|
|
1591
|
-
let mut backpressured = false;
|
|
1592
|
-
if let Some(until) = cooldown_until {
|
|
1593
|
-
if until > next_retry {
|
|
1594
|
-
next_retry = until;
|
|
1595
|
-
backpressured = true;
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
let status = if backpressured {
|
|
1599
|
-
"backpressured"
|
|
1600
|
-
} else {
|
|
1601
|
-
"scheduled"
|
|
1602
|
-
};
|
|
1603
|
-
let payload = serde_json::json!({
|
|
1604
|
-
"error_key": error_key,
|
|
1605
|
-
"cohort_key": cohort_key,
|
|
1606
|
-
"status": status,
|
|
1607
|
-
"attempts": attempts,
|
|
1608
|
-
"max_attempts": RECOVERY_MAX_ATTEMPTS,
|
|
1609
|
-
"next_retry_at": next_retry.to_rfc3339(),
|
|
1610
|
-
"last_attempt_at": Value::Null,
|
|
1611
|
-
"last_error": Value::Null,
|
|
1612
|
-
"backoff_seconds": backoff,
|
|
1613
|
-
"manual_command": manual_command,
|
|
1614
|
-
});
|
|
1615
|
-
agents.insert(agent_id.to_string(), payload);
|
|
1616
|
-
Some(RecoveryIntentSchedule {
|
|
1617
|
-
next_retry_at: next_retry.to_rfc3339(),
|
|
1618
|
-
attempt: attempts,
|
|
1619
|
-
backoff_seconds: backoff,
|
|
1620
|
-
backpressured,
|
|
1621
|
-
})
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
struct RecoveryIntentSchedule {
|
|
1625
|
-
next_retry_at: String,
|
|
1626
|
-
attempt: u64,
|
|
1627
|
-
backoff_seconds: i64,
|
|
1628
|
-
backpressured: bool,
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
/// 0.5.36 §7.6: returns true when any agent already holds a `scheduled`
|
|
1632
|
-
/// recovery intent for the given cohort.
|
|
1633
|
-
fn has_active_canary_in_cohort(state: &Value, cohort_key: &str) -> bool {
|
|
1634
|
-
let Some(agents) = state
|
|
1635
|
-
.pointer("/coordinator/abnormal_api_error_recovery/agents")
|
|
1636
|
-
.and_then(Value::as_object)
|
|
1637
|
-
else {
|
|
1638
|
-
return false;
|
|
1639
|
-
};
|
|
1640
|
-
agents.values().any(|entry| {
|
|
1641
|
-
entry.get("status").and_then(Value::as_str) == Some("scheduled")
|
|
1642
|
-
&& entry.get("cohort_key").and_then(Value::as_str) == Some(cohort_key)
|
|
1643
|
-
})
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
fn recovery_intent_status(state: &Value, agent_id: &str) -> Option<String> {
|
|
1647
|
-
state
|
|
1648
|
-
.pointer(&format!(
|
|
1649
|
-
"/coordinator/abnormal_api_error_recovery/agents/{agent_id}/status"
|
|
1650
|
-
))
|
|
1651
|
-
.and_then(Value::as_str)
|
|
1652
|
-
.map(str::to_string)
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
fn recovery_intent_field(state: &Value, agent_id: &str, field: &str) -> Option<Value> {
|
|
1656
|
-
state
|
|
1657
|
-
.pointer(&format!(
|
|
1658
|
-
"/coordinator/abnormal_api_error_recovery/agents/{agent_id}/{field}"
|
|
1659
|
-
))
|
|
1660
|
-
.cloned()
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
/// 0.5.36 §7.5 policy-aware notification tail.
|
|
1664
|
-
fn recovery_notification_tail(
|
|
1665
|
-
class: ApiErrorRecoveryClass,
|
|
1666
|
-
schedule: Option<&RecoveryIntentSchedule>,
|
|
1667
|
-
backpressure_until: Option<chrono::DateTime<chrono::Utc>>,
|
|
1668
|
-
cohort_key: &str,
|
|
1669
|
-
fact: &crate::provider::FaultFact,
|
|
1670
|
-
manual_command: &str,
|
|
1671
|
-
) -> String {
|
|
1672
|
-
let manual_line = format!("manual_recovery: {manual_command}");
|
|
1673
|
-
let backpressure_line = match backpressure_until {
|
|
1674
|
-
Some(until) => {
|
|
1675
|
-
let status = fact
|
|
1676
|
-
.api_error_status
|
|
1677
|
-
.map(|s| s.to_string())
|
|
1678
|
-
.unwrap_or_else(|| "-".to_string());
|
|
1679
|
-
format!(
|
|
1680
|
-
"backpressure: active cohort={cohort_key} status={status} until={until}",
|
|
1681
|
-
until = until.to_rfc3339()
|
|
1682
|
-
)
|
|
1683
|
-
}
|
|
1684
|
-
None => "backpressure: inactive".to_string(),
|
|
1685
|
-
};
|
|
1686
|
-
let auto_line = match class {
|
|
1687
|
-
ApiErrorRecoveryClass::Retryable => match schedule {
|
|
1688
|
-
Some(sched) if sched.backpressured => format!(
|
|
1689
|
-
"auto_recovery: delayed by provider backpressure until {}",
|
|
1690
|
-
sched.next_retry_at
|
|
1691
|
-
),
|
|
1692
|
-
Some(sched) => format!(
|
|
1693
|
-
"auto_recovery: scheduled attempt {attempt}/{max} in {backoff}s (due {due})",
|
|
1694
|
-
attempt = sched.attempt + 1,
|
|
1695
|
-
max = RECOVERY_MAX_ATTEMPTS,
|
|
1696
|
-
backoff = sched.backoff_seconds,
|
|
1697
|
-
due = sched.next_retry_at,
|
|
1698
|
-
),
|
|
1699
|
-
None => "auto_recovery: not scheduled (recovery_exhausted); use manual command below"
|
|
1700
|
-
.to_string(),
|
|
1701
|
-
},
|
|
1702
|
-
ApiErrorRecoveryClass::NonRetryable => {
|
|
1703
|
-
let error = fact.error.as_deref().unwrap_or("non_retryable");
|
|
1704
|
-
format!("auto_recovery: not scheduled (non_retryable_api_error: {error})")
|
|
1705
|
-
}
|
|
1706
|
-
ApiErrorRecoveryClass::Unknown => {
|
|
1707
|
-
"auto_recovery: not scheduled (unknown_api_error_class)".to_string()
|
|
1708
|
-
}
|
|
1709
|
-
};
|
|
1710
|
-
format!("{auto_line}\n{manual_line}\n{backpressure_line}")
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
/// 0.5.36 §7 orchestration for a single fresh abnormal notification. Runs
|
|
1714
|
-
/// inside `detect_abnormal_exits`; only classifies + records intent +
|
|
1715
|
-
/// emits recovery-family events. Returns the classifier verdict, the
|
|
1716
|
-
/// scheduled intent (if any), the cohort key, and the active backpressure
|
|
1717
|
-
/// window (if any) so the caller can build the notification tail.
|
|
1718
|
-
///
|
|
1719
|
-
/// R6 guard: this function must not call any lifecycle helpers. Actual
|
|
1720
|
-
/// lifecycle work runs post-atomic_save in `attempt_due_recoveries`.
|
|
1721
|
-
fn process_api_error_recovery_intent(
|
|
1722
|
-
state: &mut Value,
|
|
1723
|
-
event_log: &EventLog,
|
|
1724
|
-
team: &str,
|
|
1725
|
-
agent: &AbnormalWatchAgent,
|
|
1726
|
-
fact: &crate::provider::FaultFact,
|
|
1727
|
-
manual_command: &str,
|
|
1728
|
-
error_key: &str,
|
|
1729
|
-
) -> Result<
|
|
1730
|
-
(
|
|
1731
|
-
ApiErrorRecoveryClass,
|
|
1732
|
-
Option<RecoveryIntentSchedule>,
|
|
1733
|
-
String,
|
|
1734
|
-
Option<chrono::DateTime<chrono::Utc>>,
|
|
1735
|
-
),
|
|
1736
|
-
TickError,
|
|
1737
|
-
> {
|
|
1738
|
-
let class = classify_api_error_recovery(
|
|
1739
|
-
fact.signature.as_str(),
|
|
1740
|
-
fact.api_error_status,
|
|
1741
|
-
fact.error.as_deref(),
|
|
1742
|
-
);
|
|
1743
|
-
let provider_str = provider_wire(agent.provider);
|
|
1744
|
-
let cohort_key = cohort_key_for(team, provider_str, fact);
|
|
1745
|
-
if !matches!(class, ApiErrorRecoveryClass::Retryable) {
|
|
1746
|
-
return Ok((class, None, cohort_key, None));
|
|
1747
|
-
}
|
|
1748
|
-
let now = now_utc();
|
|
1749
|
-
let cohort_hint = cohort_key_for(team, provider_str, fact);
|
|
1750
|
-
let canary_active = has_active_canary_in_cohort(state, &cohort_hint);
|
|
1751
|
-
let mut bp_decision = record_backpressure_event(
|
|
1752
|
-
state,
|
|
1753
|
-
team,
|
|
1754
|
-
provider_str,
|
|
1755
|
-
fact,
|
|
1756
|
-
agent.agent_id.as_str(),
|
|
1757
|
-
now,
|
|
1758
|
-
);
|
|
1759
|
-
// 0.5.36 §7.6: "at most one canary". If another agent in the same
|
|
1760
|
-
// cohort already holds a scheduled intent, defer the new arrival to
|
|
1761
|
-
// the cohort cooldown (or a synthetic short cooldown if none yet).
|
|
1762
|
-
if canary_active {
|
|
1763
|
-
let synthetic = bp_decision
|
|
1764
|
-
.cooldown_until
|
|
1765
|
-
.unwrap_or_else(|| now + chrono::Duration::seconds(BACKPRESSURE_COOLDOWN_SECS));
|
|
1766
|
-
bp_decision.cooldown_until = Some(synthetic);
|
|
1767
|
-
}
|
|
1768
|
-
if bp_decision.just_activated {
|
|
1769
|
-
let bp_agents = state
|
|
1770
|
-
.pointer(&format!(
|
|
1771
|
-
"/coordinator/abnormal_api_error_recovery/backpressure/{}/agents",
|
|
1772
|
-
bp_decision.cohort_key
|
|
1773
|
-
))
|
|
1774
|
-
.cloned()
|
|
1775
|
-
.unwrap_or_else(|| Value::Array(Vec::new()));
|
|
1776
|
-
let cooldown_until_str = bp_decision
|
|
1777
|
-
.cooldown_until
|
|
1778
|
-
.map(|dt| dt.to_rfc3339())
|
|
1779
|
-
.unwrap_or_default();
|
|
1780
|
-
event_log.write(
|
|
1781
|
-
"worker.abnormal_exit.backpressure_started",
|
|
1782
|
-
serde_json::json!({
|
|
1783
|
-
"team_id": team,
|
|
1784
|
-
"provider": provider_str,
|
|
1785
|
-
"signature": fact.signature.as_str(),
|
|
1786
|
-
"apiErrorStatus": fact.api_error_status,
|
|
1787
|
-
"error": fact.error.as_deref(),
|
|
1788
|
-
"cohort_key": bp_decision.cohort_key,
|
|
1789
|
-
"threshold": BACKPRESSURE_THRESHOLD,
|
|
1790
|
-
"window_seconds": BACKPRESSURE_WINDOW_SECS,
|
|
1791
|
-
"cooldown_until": cooldown_until_str,
|
|
1792
|
-
"agents": bp_agents,
|
|
1793
|
-
}),
|
|
1794
|
-
)?;
|
|
1795
|
-
}
|
|
1796
|
-
let schedule = schedule_recovery_intent(
|
|
1797
|
-
state,
|
|
1798
|
-
agent.agent_id.as_str(),
|
|
1799
|
-
&bp_decision.cohort_key,
|
|
1800
|
-
error_key,
|
|
1801
|
-
manual_command,
|
|
1802
|
-
now,
|
|
1803
|
-
bp_decision.cooldown_until,
|
|
1804
|
-
);
|
|
1805
|
-
if let Some(sched) = schedule.as_ref() {
|
|
1806
|
-
if sched.backpressured {
|
|
1807
|
-
// Backpressured workers are not scheduled for the canary window;
|
|
1808
|
-
// emit a distinct `backpressure_active` event so the leader
|
|
1809
|
-
// notification tail can trace which cohort deferred which agent.
|
|
1810
|
-
event_log.write(
|
|
1811
|
-
"worker.abnormal_exit.backpressure_active",
|
|
1812
|
-
serde_json::json!({
|
|
1813
|
-
"team_id": team,
|
|
1814
|
-
"agent_id": agent.agent_id.as_str(),
|
|
1815
|
-
"cohort_key": bp_decision.cohort_key,
|
|
1816
|
-
"cooldown_until": bp_decision
|
|
1817
|
-
.cooldown_until
|
|
1818
|
-
.map(|dt| dt.to_rfc3339())
|
|
1819
|
-
.unwrap_or_default(),
|
|
1820
|
-
"action": "deferred_recovery",
|
|
1821
|
-
"manual_command": manual_command,
|
|
1822
|
-
}),
|
|
1823
|
-
)?;
|
|
1824
|
-
} else {
|
|
1825
|
-
event_log.write(
|
|
1826
|
-
"worker.abnormal_exit.recovery_scheduled",
|
|
1827
|
-
serde_json::json!({
|
|
1828
|
-
"team_id": team,
|
|
1829
|
-
"agent_id": agent.agent_id.as_str(),
|
|
1830
|
-
"provider": provider_str,
|
|
1831
|
-
"signature": fact.signature.as_str(),
|
|
1832
|
-
"apiErrorStatus": fact.api_error_status,
|
|
1833
|
-
"error": fact.error.as_deref(),
|
|
1834
|
-
"attempt": sched.attempt,
|
|
1835
|
-
"max_attempts": RECOVERY_MAX_ATTEMPTS,
|
|
1836
|
-
"due_at": sched.next_retry_at,
|
|
1837
|
-
"backoff_seconds": sched.backoff_seconds,
|
|
1838
|
-
"error_key": error_key,
|
|
1839
|
-
"cohort_key": bp_decision.cohort_key,
|
|
1840
|
-
"backpressured": false,
|
|
1841
|
-
"manual_command": manual_command,
|
|
1842
|
-
}),
|
|
1843
|
-
)?;
|
|
1844
|
-
}
|
|
1845
|
-
} else {
|
|
1846
|
-
// schedule_recovery_intent returns None ONLY when max_attempts hit.
|
|
1847
|
-
event_log.write(
|
|
1848
|
-
"worker.abnormal_exit.recovery_exhausted",
|
|
1849
|
-
serde_json::json!({
|
|
1850
|
-
"team_id": team,
|
|
1851
|
-
"agent_id": agent.agent_id.as_str(),
|
|
1852
|
-
"attempts": RECOVERY_MAX_ATTEMPTS,
|
|
1853
|
-
"last_error": fact.error.as_deref(),
|
|
1854
|
-
"manual_command": manual_command,
|
|
1855
|
-
}),
|
|
1856
|
-
)?;
|
|
1857
|
-
}
|
|
1858
|
-
Ok((
|
|
1859
|
-
class,
|
|
1860
|
-
schedule,
|
|
1861
|
-
bp_decision.cohort_key,
|
|
1862
|
-
bp_decision.cooldown_until,
|
|
1863
|
-
))
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1867
|
-
// 0.5.36 §7.3 recovery execution — runs post-atomic_save, reloads fresh
|
|
1868
|
-
// state, consumes due intents, invokes the lifecycle `start_agent_at_paths`
|
|
1869
|
-
// with force=true (stop-before-start semantics for live panes, so noop is
|
|
1870
|
-
// impossible), and writes the outcome back to state via the lifecycle
|
|
1871
|
-
// path (which owns its own save).
|
|
1872
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1873
|
-
|
|
1874
|
-
/// 0.5.36 §7.3: process all due recovery intents. Called from tick.rs
|
|
1875
|
-
/// AFTER atomic_save has flushed the detector-written intent. Reloads a
|
|
1876
|
-
/// fresh state each call so the caller's stale in-memory state cannot
|
|
1877
|
-
/// clobber lifecycle writes. Best-effort: recovery failure produces
|
|
1878
|
-
/// events + updated intent, never a tick failure.
|
|
1879
|
-
pub(crate) fn attempt_due_recoveries(
|
|
1880
|
-
workspace: &Path,
|
|
1881
|
-
event_log: &EventLog,
|
|
1882
|
-
transport: &dyn crate::transport::Transport,
|
|
1883
|
-
) {
|
|
1884
|
-
// 0.5.43 debt-sweep (§5 D-j): single fresh post-save load. Do NOT
|
|
1885
|
-
// re-use the tick's pre-save Value — that would reopen the 0.5.36
|
|
1886
|
-
// R6 lifecycle-save-clobbered-by-tick window (highest risk item in
|
|
1887
|
-
// this slice per locate). We own our load here, mutate in memory
|
|
1888
|
-
// (`clear_stale_terminal_next_retry_at` is now a pure
|
|
1889
|
-
// `(&mut Value) -> bool` scrub), persist through the existing
|
|
1890
|
-
// allowlisted root save when the scrub actually changed a row, and
|
|
1891
|
-
// then collect due agents from the SAME post-scrub Value so
|
|
1892
|
-
// terminal intents newly stripped of `next_retry_at` cannot double-
|
|
1893
|
-
// fire below.
|
|
1894
|
-
let Ok(mut state) = crate::state::persist::load_runtime_state(workspace) else {
|
|
1895
|
-
return;
|
|
1896
|
-
};
|
|
1897
|
-
if clear_stale_terminal_next_retry_at(&mut state) {
|
|
1898
|
-
let _ = crate::state::repository::StateRepository::new(workspace).save(
|
|
1899
|
-
crate::state::repository::StateWriteIntent::CoordinatorApiErrorRecovery {
|
|
1900
|
-
team_key: state.get("active_team_key").and_then(Value::as_str),
|
|
1901
|
-
agent_id: None,
|
|
1902
|
-
},
|
|
1903
|
-
&state,
|
|
1904
|
-
);
|
|
1905
|
-
}
|
|
1906
|
-
let due_agents = collect_due_recovery_agents(&state);
|
|
1907
|
-
for agent_id in due_agents {
|
|
1908
|
-
run_single_recovery(workspace, event_log, transport, &agent_id);
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
|
|
1912
|
-
fn collect_due_recovery_agents(state: &Value) -> Vec<String> {
|
|
1913
|
-
let now = now_utc();
|
|
1914
|
-
let Some(agents) = state
|
|
1915
|
-
.pointer("/coordinator/abnormal_api_error_recovery/agents")
|
|
1916
|
-
.and_then(Value::as_object)
|
|
1917
|
-
else {
|
|
1918
|
-
return Vec::new();
|
|
1919
|
-
};
|
|
1920
|
-
let mut due = Vec::new();
|
|
1921
|
-
for (agent_id, intent) in agents {
|
|
1922
|
-
let status = intent.get("status").and_then(Value::as_str).unwrap_or("");
|
|
1923
|
-
// 0.5.37 R8: dispatch only for non-terminal states. `succeeded`,
|
|
1924
|
-
// `blocked`, `exhausted`, `backpressured` are terminal / awaiting
|
|
1925
|
-
// manual action — a lifecycle dispatch here would be double-fire
|
|
1926
|
-
// or wasted work. `scheduled` / `running` remain dispatchable so
|
|
1927
|
-
// an interrupted tick can resume.
|
|
1928
|
-
if !matches!(status, "scheduled" | "running") {
|
|
1929
|
-
continue;
|
|
1930
|
-
}
|
|
1931
|
-
let Some(next_retry_at) = intent
|
|
1932
|
-
.get("next_retry_at")
|
|
1933
|
-
.and_then(Value::as_str)
|
|
1934
|
-
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
|
|
1935
|
-
.map(|dt| dt.with_timezone(&chrono::Utc))
|
|
1936
|
-
else {
|
|
1937
|
-
continue;
|
|
1938
|
-
};
|
|
1939
|
-
if next_retry_at <= now {
|
|
1940
|
-
due.push(agent_id.clone());
|
|
1941
|
-
}
|
|
1942
|
-
}
|
|
1943
|
-
due
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
/// 0.5.37 R8 + 0.5.43 debt-sweep §5 D-j: pure scrub helper. Terminal
|
|
1947
|
-
/// recovery intents (`succeeded`/`blocked`/`exhausted`) must not carry
|
|
1948
|
-
/// a stale `next_retry_at`. Returns true when at least one row was
|
|
1949
|
-
/// stripped, so the caller can save through its own load/write cycle
|
|
1950
|
-
/// (no double-load, no double-save). No I/O in this function — the
|
|
1951
|
-
/// only responsibility is mutation on a caller-owned `Value`.
|
|
1952
|
-
fn clear_stale_terminal_next_retry_at(state: &mut Value) -> bool {
|
|
1953
|
-
let mut mutated = false;
|
|
1954
|
-
if let Some(agents) = recovery_intent_agents(state) {
|
|
1955
|
-
for (_agent_id, entry) in agents.iter_mut() {
|
|
1956
|
-
let Some(obj) = entry.as_object_mut() else {
|
|
1957
|
-
continue;
|
|
1958
|
-
};
|
|
1959
|
-
let status = obj
|
|
1960
|
-
.get("status")
|
|
1961
|
-
.and_then(Value::as_str)
|
|
1962
|
-
.unwrap_or("")
|
|
1963
|
-
.to_string();
|
|
1964
|
-
if !matches!(status.as_str(), "succeeded" | "blocked" | "exhausted") {
|
|
1965
|
-
continue;
|
|
1966
|
-
}
|
|
1967
|
-
let had_stale = obj.get("next_retry_at").filter(|v| !v.is_null()).is_some();
|
|
1968
|
-
if had_stale {
|
|
1969
|
-
obj.remove("next_retry_at");
|
|
1970
|
-
mutated = true;
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
mutated
|
|
1975
|
-
}
|
|
1976
|
-
|
|
1977
|
-
fn run_single_recovery(
|
|
1978
|
-
workspace: &Path,
|
|
1979
|
-
event_log: &EventLog,
|
|
1980
|
-
transport: &dyn crate::transport::Transport,
|
|
1981
|
-
agent_id: &str,
|
|
1982
|
-
) {
|
|
1983
|
-
// Read the current intent so we can bump attempts / write result fields
|
|
1984
|
-
// through the shared writer (`write_recovery_intent_field`); the actual
|
|
1985
|
-
// lifecycle helper below owns its own state save.
|
|
1986
|
-
let Ok(state_before) = crate::state::persist::load_runtime_state(workspace) else {
|
|
1987
|
-
return;
|
|
1988
|
-
};
|
|
1989
|
-
let team_key = crate::state::projection::team_state_key(&state_before);
|
|
1990
|
-
let attempt = state_before
|
|
1991
|
-
.pointer(&format!(
|
|
1992
|
-
"/coordinator/abnormal_api_error_recovery/agents/{agent_id}/attempts"
|
|
1993
|
-
))
|
|
1994
|
-
.and_then(Value::as_u64)
|
|
1995
|
-
.unwrap_or(0);
|
|
1996
|
-
let manual_command = state_before
|
|
1997
|
-
.pointer(&format!(
|
|
1998
|
-
"/coordinator/abnormal_api_error_recovery/agents/{agent_id}/manual_command"
|
|
1999
|
-
))
|
|
2000
|
-
.and_then(Value::as_str)
|
|
2001
|
-
.map(str::to_string)
|
|
2002
|
-
.unwrap_or_else(|| recovery_manual_command(agent_id, workspace, team_key.as_str()));
|
|
2003
|
-
let _ = event_log.write(
|
|
2004
|
-
"worker.abnormal_exit.recovery_started",
|
|
2005
|
-
serde_json::json!({
|
|
2006
|
-
"team_id": team_key.as_str(),
|
|
2007
|
-
"agent_id": agent_id,
|
|
2008
|
-
"attempt": attempt,
|
|
2009
|
-
"mode": "start_agent_force",
|
|
2010
|
-
"allow_fresh": false,
|
|
2011
|
-
}),
|
|
2012
|
-
);
|
|
2013
|
-
let agent_id_typed = crate::model::ids::AgentId::new(agent_id.to_string());
|
|
2014
|
-
let outcome = crate::lifecycle::restart::start_agent_at_paths_for_recovery(
|
|
2015
|
-
workspace,
|
|
2016
|
-
&agent_id_typed,
|
|
2017
|
-
Some(team_key.as_str()),
|
|
2018
|
-
transport,
|
|
2019
|
-
);
|
|
2020
|
-
let now = now_utc();
|
|
2021
|
-
match outcome {
|
|
2022
|
-
Ok(recovery_outcome) => {
|
|
2023
|
-
let _ = event_log.write(
|
|
2024
|
-
"worker.abnormal_exit.recovery_succeeded",
|
|
2025
|
-
serde_json::json!({
|
|
2026
|
-
"team_id": team_key.as_str(),
|
|
2027
|
-
"agent_id": agent_id,
|
|
2028
|
-
"start_mode": recovery_outcome.start_mode,
|
|
2029
|
-
"target": recovery_outcome.target,
|
|
2030
|
-
"coordinator_started": recovery_outcome.coordinator_started,
|
|
2031
|
-
}),
|
|
2032
|
-
);
|
|
2033
|
-
write_recovery_intent_result(
|
|
2034
|
-
workspace,
|
|
2035
|
-
agent_id,
|
|
2036
|
-
RecoveryIntentUpdate {
|
|
2037
|
-
status: "succeeded",
|
|
2038
|
-
attempts: attempt + 1,
|
|
2039
|
-
last_attempt_at: now.to_rfc3339(),
|
|
2040
|
-
last_error: None,
|
|
2041
|
-
blocked_reason: None,
|
|
2042
|
-
},
|
|
2043
|
-
);
|
|
2044
|
-
}
|
|
2045
|
-
Err(RecoveryError::NoopBlocked) => {
|
|
2046
|
-
let _ = event_log.write(
|
|
2047
|
-
"worker.abnormal_exit.recovery_blocked",
|
|
2048
|
-
serde_json::json!({
|
|
2049
|
-
"team_id": team_key.as_str(),
|
|
2050
|
-
"agent_id": agent_id,
|
|
2051
|
-
"reason": "noop_not_recovery",
|
|
2052
|
-
"manual_command": manual_command,
|
|
2053
|
-
}),
|
|
2054
|
-
);
|
|
2055
|
-
write_recovery_intent_result(
|
|
2056
|
-
workspace,
|
|
2057
|
-
agent_id,
|
|
2058
|
-
RecoveryIntentUpdate {
|
|
2059
|
-
status: "blocked",
|
|
2060
|
-
attempts: attempt + 1,
|
|
2061
|
-
last_attempt_at: now.to_rfc3339(),
|
|
2062
|
-
last_error: None,
|
|
2063
|
-
blocked_reason: Some("noop_not_recovery"),
|
|
2064
|
-
},
|
|
2065
|
-
);
|
|
2066
|
-
}
|
|
2067
|
-
Err(RecoveryError::Lifecycle(reason)) => {
|
|
2068
|
-
let _ = event_log.write(
|
|
2069
|
-
"worker.abnormal_exit.recovery_blocked",
|
|
2070
|
-
serde_json::json!({
|
|
2071
|
-
"team_id": team_key.as_str(),
|
|
2072
|
-
"agent_id": agent_id,
|
|
2073
|
-
"reason": "lifecycle_error",
|
|
2074
|
-
"detail": reason,
|
|
2075
|
-
"manual_command": manual_command,
|
|
2076
|
-
}),
|
|
2077
|
-
);
|
|
2078
|
-
write_recovery_intent_result(
|
|
2079
|
-
workspace,
|
|
2080
|
-
agent_id,
|
|
2081
|
-
RecoveryIntentUpdate {
|
|
2082
|
-
status: "blocked",
|
|
2083
|
-
attempts: attempt + 1,
|
|
2084
|
-
last_attempt_at: now.to_rfc3339(),
|
|
2085
|
-
last_error: Some(reason),
|
|
2086
|
-
blocked_reason: Some("lifecycle_error"),
|
|
2087
|
-
},
|
|
2088
|
-
);
|
|
2089
|
-
}
|
|
2090
|
-
}
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
pub(crate) enum RecoveryError {
|
|
2094
|
-
NoopBlocked,
|
|
2095
|
-
Lifecycle(String),
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
/// 0.5.36 §7.3 typed outcome returned from
|
|
2099
|
-
/// `lifecycle::restart::start_agent_at_paths_for_recovery` to the post-save
|
|
2100
|
-
/// step. Small on purpose — just the fields the `recovery_succeeded` event
|
|
2101
|
-
/// needs.
|
|
2102
|
-
pub(crate) struct RecoveryLifecycleOutcome {
|
|
2103
|
-
pub start_mode: String,
|
|
2104
|
-
pub target: String,
|
|
2105
|
-
pub coordinator_started: bool,
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
struct RecoveryIntentUpdate {
|
|
2109
|
-
status: &'static str,
|
|
2110
|
-
attempts: u64,
|
|
2111
|
-
last_attempt_at: String,
|
|
2112
|
-
last_error: Option<String>,
|
|
2113
|
-
blocked_reason: Option<&'static str>,
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
fn write_recovery_intent_result(workspace: &Path, agent_id: &str, update: RecoveryIntentUpdate) {
|
|
2117
|
-
let Ok(mut state) = crate::state::persist::load_runtime_state(workspace) else {
|
|
2118
|
-
return;
|
|
2119
|
-
};
|
|
2120
|
-
if let Some(agents) = recovery_intent_agents(&mut state) {
|
|
2121
|
-
let entry = agents
|
|
2122
|
-
.entry(agent_id.to_string())
|
|
2123
|
-
.or_insert_with(|| serde_json::json!({}));
|
|
2124
|
-
if !entry.is_object() {
|
|
2125
|
-
*entry = serde_json::json!({});
|
|
2126
|
-
}
|
|
2127
|
-
if let Some(obj) = entry.as_object_mut() {
|
|
2128
|
-
obj.insert("status".to_string(), serde_json::json!(update.status));
|
|
2129
|
-
obj.insert("attempts".to_string(), serde_json::json!(update.attempts));
|
|
2130
|
-
obj.insert(
|
|
2131
|
-
"last_attempt_at".to_string(),
|
|
2132
|
-
serde_json::json!(update.last_attempt_at),
|
|
2133
|
-
);
|
|
2134
|
-
obj.insert(
|
|
2135
|
-
"last_error".to_string(),
|
|
2136
|
-
match update.last_error.as_ref() {
|
|
2137
|
-
Some(text) => serde_json::json!(text),
|
|
2138
|
-
None => Value::Null,
|
|
2139
|
-
},
|
|
2140
|
-
);
|
|
2141
|
-
match update.blocked_reason {
|
|
2142
|
-
Some(reason) => {
|
|
2143
|
-
obj.insert("blocked_reason".to_string(), serde_json::json!(reason));
|
|
2144
|
-
}
|
|
2145
|
-
None => {
|
|
2146
|
-
obj.remove("blocked_reason");
|
|
2147
|
-
}
|
|
2148
|
-
}
|
|
2149
|
-
// 0.5.37 R8: transitioning into a terminal state clears the
|
|
2150
|
-
// stale `next_retry_at`; a future retry earns a fresh due
|
|
2151
|
-
// time when it re-enters `scheduled` through the detector.
|
|
2152
|
-
if matches!(update.status, "succeeded" | "blocked" | "exhausted") {
|
|
2153
|
-
obj.remove("next_retry_at");
|
|
2154
|
-
}
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2157
|
-
let _ = crate::state::repository::StateRepository::new(workspace).save(
|
|
2158
|
-
crate::state::repository::StateWriteIntent::CoordinatorApiErrorRecovery {
|
|
2159
|
-
team_key: state.get("active_team_key").and_then(Value::as_str),
|
|
2160
|
-
agent_id: Some(agent_id),
|
|
2161
|
-
},
|
|
2162
|
-
&state,
|
|
2163
|
-
);
|
|
2164
|
-
}
|
|
2165
|
-
|
|
2166
1319
|
#[cfg(test)]
|
|
2167
1320
|
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
2168
1321
|
mod tests {
|
|
2169
1322
|
use super::*;
|
|
2170
1323
|
use crate::coordinator::tick::Coordinator;
|
|
2171
|
-
use crate::coordinator::types::{
|
|
1324
|
+
use crate::coordinator::types::{ProviderRegistry, WorkspacePath};
|
|
2172
1325
|
use std::io::Write as _;
|
|
2173
1326
|
|
|
2174
1327
|
struct NormalRegistry;
|
|
@@ -2180,10 +1333,6 @@ mod tests {
|
|
|
2180
1333
|
) -> Box<dyn crate::provider::ProviderAdapter> {
|
|
2181
1334
|
crate::provider::get_adapter(provider)
|
|
2182
1335
|
}
|
|
2183
|
-
|
|
2184
|
-
fn error_lists(&self, _provider: crate::provider::Provider) -> ErrorLists {
|
|
2185
|
-
ErrorLists::default()
|
|
2186
|
-
}
|
|
2187
1336
|
}
|
|
2188
1337
|
#[test]
|
|
2189
1338
|
fn abnormal_stale_error_baselines_then_fresh_alive_error_notifies() {
|