§13. Incidents and the diagnoser (crates incident, diagnose)
planarian @ a924e09 · Spec v4.6 · snapshot main · all versions
Incident lifecycle
- Open when a unit is expected (desired condition holds, no override) and its derived status is Failed, Degraded, Stopped-but-desired-Running, Detected-but-desired-Bound, or Absent-but-desired-present, with fresh evidence for two consecutive ticks.
- Defer to systemd first. If the unit has
Restart=and systemd is within its ownStartLimitBurst, the incident waits for systemd’s attempt (up to the unit’sRestartSecplus settle) before Planarian acts, so two controllers don’t restart the same service. - Attempts are transactions; each outcome is appended. Budget 3 per incident, persisted in the journal (survives daemon restart). Backoff between attempts: 10 s, 60 s, 300 s.
- Closed when the target satisfies its desired predicate (Running, Bound, Stopped, or Absent as desired) with fresh evidence for a stabilization window (5 min).
- Regressed: if the same target diverges again within 30 min of Close, the incident reopens with
count+1and a longer backoff; atcount = 3it goes straight to Escalated (report to user; cooldown 1 h; no autonomous action on that target until a human acts or the cooldown ends). - Cooldown after budget exhaustion: 1 h, then one more attempt, then 6 h.
- An override on the target closes the incident (I13).
- A person acting on the target (
release,override,stop, a manual repair) reopens the incident with a fresh attempt and no cooldown — but only when the action was authorized and succeeded. A request that is refused, parked, or fails leaves the incident record exactly as it was: budget, cooldown, escalation and next attempt unchanged. Rejection grants no indirect control over the repair loop. Gym case: an unauthorizedplanarian release sshdduring sshd’s cooldown changes nothing about the incident. - Quiet mode. While
Foregroundholds, incidents on units the foreground work does not depend on wait (§12f); their budgets and backoff do not advance while they wait.
Diagnoser
- From the open incidents, walk
Requiresedges (andDiagnosticedges with confidence ≥ 0.7) to the deepest divergent unit; that is the target.OrderingandWantsedges are never followed: a failed wanted unit is its own incident and is never assumed to be the cause of the wanting unit’s failure. Cycles: the walked subgraph is first collapsed into strongly connected components; “deepest” is defined on the resulting DAG; within a component, the target is the unit with the earliest definiteObserved::Failedtimestamp, else the one with the most failed Readiness probes, else lexical order. Gym case: an optional wanted service fails while its wanting service stays healthy (two incidents at most, no false root cause). - Enumerate candidate transitions, depth ≤ 2, whose precondition holds and whose prediction closes the divergence. Exclude exhausted and cooling targets.
- Order by: tier ascending, then disruption ascending, then recovery (Idempotent < Revertible), then cost.
Rollbacklast. - Submit one at a time as
Proposal { source: Diagnoser }. Stop at the firstCommittedwhose expectation held. - Exhausted →
DiagnoserExhausted→ model (B) or user report (A).
Phase A ApplyChange candidates are limited to catalog entries tagged repair: (re-enable a predicted option; reapply a previously-committed change for the same target — the latter is in the catalog from v4.6; it was specified and not shipped in v4.5).