homelab-codex-ws/scripts
oskar 71a7af5b3f fix(control-plane): unwedge incidents that never get service_healthy
_resolve_incident() only ever fires from process_event() on a
service_healthy/service_recovered event. A service that is removed,
renamed, or was only ever a one-off test never emits that event again,
so its incident stays "active" in world/incidents.json forever — this
is what left 5 incidents wedged on VPS until a manual on-node edit
during the 2026-08-26 recon session (docs/sessions/2026-08-26.md).

Two independent unwedging mechanisms, both in observer._prune_stale_world
(runs every cycle, so no new event is required to trigger either):

(a) Time-based fallback: any active incident with last_occurrence older
    than INCIDENT_STALE_RESOLVE_SECS (env, default 24h) auto-resolves
    with resolved_reason="auto_stale_no_events_24h". Unlike the existing
    orphan case (Case 2, 5-min guard, only unlinked incidents), this
    also clears a service's lingering incident_id link — that link is
    exactly what a decommissioned service's incident never gets a
    chance to clear via the normal event path.

(b) Manual path: an operator touches
    world/resolve-requests/<incident-id>; the observer consumes the
    flag file each cycle, force-resolves with resolved_reason=
    "manual_operator", and always removes the flag (even for an
    unknown/already-resolved id) so a mistyped flag can't sit forever
    looking unprocessed.

    Chose a flag file over adding a mutation endpoint to operator_ui.py:
    /action/mutate only knows actions/<status>/<id>.json, there is no
    incidents equivalent, and world/incidents.json is exclusively
    observer-owned (rewritten wholesale every cycle by _save_world) —
    a second writer (the HTTP handler thread) would race the observer's
    own writes. A flag file needs no new HTTP surface and reuses the
    same "operator drops a file, the owning process consumes it"
    pattern the actions pending/approved queue already uses. Smaller
    diff, no new attack surface on a server with no auth on writes.

Tests added to test_incident_lifecycle.py: stale-resolve past the
threshold (service still linked), negative case (fresh active incident
stays active), configurable threshold, manual-flag resolve + flag
removal, flag for an unknown incident, flag for an already-resolved
incident. Full control-plane suite: 179 passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017WDKj5LRY8vdQMx57dfNnu
2026-08-26 21:05:38 +02:00
..
bootstrap Implement VPS control-plane deployment profile 2026-05-12 20:19:05 +02:00
deploy fix(control-plane): redeploy wykonywalny — dispatch do host-side deploy-runnera 2026-08-03 18:26:30 +02:00
dev chore(agent.sh): podnies limit worktree 4->6 (regularnie 4 rownolegle watki: KB, HA, IR, infra) 2026-07-22 16:46:03 +02:00
ha fix(kb): przepiecie wszystkich odwolan wewnetrznych po migracji 2026-08-04 16:58:46 +02:00
kb feat(kb): skill i skrypt do pisania dokumentow kb/ (OKF authoring) 2026-08-26 17:05:04 +02:00
lib Implement filesystem-first runtime event system 2026-05-12 13:38:25 +02:00
maintenance fix(events): service_healthy tylko na transition + cleanup 358k backlog + retencja — glob zalewu paralizowal reconcile supervisora 2026-07-16 20:20:17 +02:00
monitor feat(node-agent): implement health monitor and safe cleanup policy 2026-05-27 13:15:06 +02:00
npm fix(kb): 13 pozostalych odwolan do sciezek sprzed migracji 2026-08-04 17:02:12 +02:00
observer fix(control-plane): unwedge incidents that never get service_healthy 2026-08-26 21:05:38 +02:00
onboard feat(kb): przenosiny type=runbook do kb/runbooks/ (5 plikow, bez SPLIT) 2026-08-04 16:58:04 +02:00
bootstrap.sh Initial homelab workspace structure 2026-05-07 20:17:27 +02:00