Memory pressure — 6 live patchů a bathroom audio reset

Druhá půle dne 14. května. Ráno proběhl úklid RPi/HAOS infrastruktury (sandbox 24/7, kiosk Phase E, dashboard cache retry — samostatný post). Odpoledne se ale začalo vrtat do jednoho konkrétního symptomu: koupelna občas zůstala svítit a hrát rádio dlouho po tom, co jsme z ní odešli. Reálně viděný problém, opakující se, ne edge case.

Tady je jak se z toho stalo 6 živých patchů do Homey v jednom dni. Bez chaosu. Sandbox-first, drift check, rollback ready, SHA bit-perfect verify na každém PUT.

Phase R0 — co reálně bolí

Read-only preflight přes sandbox runner + Homey REST + watchdog log + EventLog. Tři číselné nálezy hned dali kontext:

  • HomeyScript app stav running, ale mem oscilovala 99-154 MB přes 24h, threshold 125 MB
  • 38 restart_triggered ve watchdog logu za 24h. Průměrně jeden restart každých 38 min, ale clusterově každých 5-10 min při high-mem fázích
  • HomeyScript cumulative crashed counter 1906

Sandbox runner gate ale ukázal DEPLOY_GATE_OPEN, testy 20/20 PASS. Sandbox infra OK. Problém byl jinde — v reálné runtime stabilitě Homey.

Phase R1 — root cause classification, ne hádání

R1 prompt měl explicit enum 11 možných root cause tagů (FLOW_MISSING, WRONG_FLOW_CARD, STATE_NOT_WRITTEN, SCRIPT_CRASH_INTERRUPTED, VACANCY_GUARDIAN_NOT_ENFORCING, atd.). Cíl: zařadit do jedné nebo více kategorií, ne mít fluffy diagnózu.

Diagnostika ukázala:

  • sh_vacancy_guardian_last_run_ts = '2026-05-14T06:25:02Z' — guardian naposledy doběhl před 67 minutami
  • sh_audio_current_mode stuck na 'radio_bathroom' od ranní rutiny (~03:30Z)
  • sh_audio_active_speaker stuck na 'Speaker koupelna'
  • 5 vacancy decisions / 24h v EventLog (očekáváno ~288 = 5min × 24h)
  • Motion ages ve vacancy decisions: 13 686 s, 9 456 s, 4 685 s, 4 056 s, 2 286 s — světlo/audio běželo až 3.8 hodiny po odchodu z koupelny

Klasifikace: VACANCY_GUARDIAN_NOT_ENFORCING primary + STATE_NOT_RESET secondary (audio mode neresetuje sám sebe).

Phase R1 FLOW VERIFY — eliminace špatné hypotézy

Hypotéza #1 byla: cron Flow je rozbitý. Klasická intuice, často správná. Sandbox-first ale chce empirický důkaz, ne intuici. Pull všech 233 advanced flows přes REST, najít flow co volá guardian skript, inspekce karet:

Flow a996ffa1 — "SH – Vacancy – Guardian Cron 5min"
  enabled: true
  broken:  false
  trigger card:
    id:   homey:manager:cron:every_nth         ✅ správný cron format
    args: { n: 5, type: "minute" }              ✅ správné argumenty (T6+T15 valid)
  action card:
    id:   homey:app:com.athom.homeyscript:run
    args.script.id: 718292bb-...                 ✅ matches sh_room_vacancy_guardian_v1

Flow je strukturálně 100% správně. Eliminované hypotézy: FLOW_MISSING, FLOW_DISABLED, FLOW_BROKEN_CARD, FLOW_WRONG_SCRIPT_TARGET.

Ne flow je rozbitý, ale runtime kill mid-flight. Watchdog log to potvrdil: cluster restartů přesně okolo posledního guardian run-u (06:20, 06:25, 06:30 — guardian completed at 06:25, killed cluster at 06:30).

Heartbeat patch — důkaz CRASH_MID_RUN bez hádání

Klíčový problém pre-deploy: měli jsme jen jeden timestamp (sh_vacancy_guardian_last_run_ts psaný na konci run()). Když byl stale, nebylo možné rozlišit:

  1. "cron Flow je rozbitý a vůbec nefire-uje guardian"
  2. "cron Flow fire-uje, ale guardian se zabije mid-flight"

Patch: přidat druhý timestamp na začátku run()sh_vacancy_guardian_heartbeat_ts. Když cron fire-uje a script alespoň jednou volá setValue, heartbeat se zapíše. Pokud pak script doběhne, zapíše se i last_run. Pair logika:

both fresh        → OK_completing
heartbeat fresh,
last_run stale    → CRASH_MID_RUN  ← runtime kill během exekuce
both stale        → CRON_NOT_FIRING ← cron flow / app dead
mixed             → WARN

Sandbox test T26 5/5 PASS. Replay scénář guardian_heartbeat_crash_mid_run.json simuloval přesnou produkční situaci.

Dvě malé live změny — var a guardian v1.1

Před guardian v1.1 deploye potřeba nová Logic proměnná:

POST /api/manager/logic/variable
body: { "name": "sh_vacancy_guardian_heartbeat_ts",
        "type": "string", "value": "" }
→ HTTP 200
→ id: 1514bec1-7a87-4d22-b3c7-16ce185a2e79

Idempotent guard pre-check potvrdil, že var neexistuje. POST. Verify post-GET. Audit JSON do rollback/var_create_sh_vacancy_guardian_heartbeat_ts.json. Žádný script ji ještě nepoužíval — bezpečné izolované přidání.

Potom guardian v1.0 → v1.1: 273 řádků → 309 řádků, dvě malé editace — heartbeat write at top of run() + bathroom-specific stuck-state reset when vacancy confirmed. Backup pre-deploy live source pulled, SHA porovnáno s pre-staging backup-em (drift check), PUT, re-GET, SHA bit-perfect verify.

První cron po deployi — sandbox-first vindication

PUT proběhl v 08:01:13Z. Next 5-min cron at 08:05:00Z. Sample at 08:05:29Z:

sh_vacancy_guardian_heartbeat_ts = '2026-05-14T08:05:00.138Z'  ← ✅ fresh
sh_vacancy_guardian_last_run_ts  = '2026-05-14T07:55:02.351Z'  ← ❌ stuck 10 min

CRASH_MID_RUN diagnóza okamžitě prokázána. Heartbeat fire-d (cron OK, script started), last_run NEpsán (script killed mid-flight). Stejný pattern pozorován i v 08:10, 08:15 cron firings.

Tím se hypotéza R1 FLOW VERIFY confirmed: ne flow, ale runtime. Pivot na R2.

Phase R2 — memory pressure audit

R2 read-only audit pull všech 132 scriptů zdroje + cron inventory. Číslo, které zarazí: 1 295.7 cron fires per hour napříč 67 cron flows. 21 fires per minute average. Worst offender:

  • sh_tts_orchestrator_v1: 240/h = every 15 s (TTS queue processor, fires regardless of queue state)
  • sh_shelly3em_poller_v1: 120/h = every 30 s
  • sh_pracka_done_detector_v1: 60/h
  • sh_morning_kaffe_check_v1: 60/h
  • sh_wake_confirm_v1: 60/h
  • sh_time_sync_guard_v1: 60/h
  • sh_intent_engine_v1: 60/h
  • … 60 dalších cron flows ve frekvencích 30/h-1/h

Každý fire = fresh V8 sandbox spawn v HomeyScript app. I když je work-load script byl no-op, lib eval + var load + JSON parse zabere 50-300 KB allocations. 1295 fires/h × průměr 200 KB = ~260 MB/h V8 heap churn. GC reclaim některé, ale net mem grows monotonically until threshold 125 MB → watchdog kill.

Quick Win Throttle — 5 patchů s jasným pravidlem

Strategie: minimální možný early-return guard na top of main entry každého worst offender scriptu. Žádný redesign, žádné funkce přepisované. Single if (...) return; blok per script:

  • TTS Orchestrator: skip if sh_tts_queue empty AND sh_tts_queue_trigger idle. Saves ~180/h.
  • Kaffe Check: skip outside Prague 04:00-11:00 window. Saves ~1 020 fires/day (70 % of daily).
  • Wake Confirm: skip if user not sleeping AND hour ≥ 12. Saves ~45/h.
  • Wake Scorer: same logic. Saves ~20/h.
  • Morning Score: same logic. Saves ~45/h.

Per patch: pull live source, drift check vs pre-staging backup (catch case kdy mezitím někdo edituje skript přes UI), apply minimal patch via Python binary-mode (preserve CRLF), staging file with manifest + SHA256. Sandbox regression 26/26 PASS před každým PUT. Bit-perfect SHA re-GET verify po PUT. Audit JSON. 5 minut work per patch.

Sequential deploy — 21 minut, 5 PUTů, žádný restart

Per SAFE_REPAIR_MODE: jedna změna, ověření, další změna. Timeline (UTC):

08:26:46  PUT sh_tts_orchestrator_v1 v1.1     ✅ SHA bit-perfect
08:43:58  PUT sh_morning_kaffe_check_v1 v1.1   ✅ SHA bit-perfect
08:47:16  PUT sh_wake_confirm_v1 v1.1          ✅ SHA bit-perfect
08:47:27  PUT sh_wake_scorer_v1 v1.1           ✅ SHA bit-perfect
08:47:34  PUT sh_morning_score_v1 v1.1         ✅ SHA bit-perfect

App state běhěm všech 5 PUTů zůstala running. Žádné nové crashy (crashedCount stable 1920 přes 21 min). Bit-perfect SHA verify na re-GET každého patche. Backup intact pro per-script rollback (~10s single PUT).

9 minut po prvním deploy — bathroom audio konečně idle

Hlavní win nečekal 24-hodinový soak. Sample 12 minut po TTS deploy:

Pre-deploy 08:26Z:
  sh_vacancy_guardian_heartbeat_ts = 08:25:00Z
  sh_vacancy_guardian_last_run_ts  = 07:55:02Z  ← stuck 31 min

Post-deploy 08:40Z (12 min after deploy):
  sh_vacancy_guardian_heartbeat_ts = 08:35:00.156Z
  sh_vacancy_guardian_last_run_ts  = 08:35:08.417Z  ← ✅ +8s = COMPLETED
  sh_audio_current_mode             = 'idle'         ← ✅ reset from 'radio_bathroom'

První successful guardian run za 40+ minut. Stuck-audio-reset patch v1.1 se v té iteraci dostal k kódu a vyresetoval sh_audio_current_mode na 'idle'. Reálně viditelná oprava primárního user-reported problému — bathroom radio už nebude resume po TTS notifikaci, protože mode už není pinned na radio_bathroom.

Co tenhle den NEbyl

  • Nebyl to flow edit. Sandbox-first eliminace flow problému uvolnila ruce pro správnou opravu (script-level throttle + heartbeat).
  • Nebyl to chaos. Šest live změn během dne, ale každá byla single PUT s pre-flight + drift check + audit + verify.
  • Nebyl to PC export. Phase E kiosk byl rebuilt fresh z RPi/SSH bez jakéhokoli čtení z C:\. Heartbeat var + guardian v1.1 + 5 throttle patches všechny vznikly na HAOS staging dir, nikoli na PC.
  • Není to hotovo. Watchdog restart rate ještě plně nepoklesl při baseline monitoringu (+30 min mark). Throttle savings akumulují pomalu. Doporučený 2-6h soak před dalším rozhodnutím (Shelly poller offload, log buffer, atd.).

Pět hard rules co tohle všechno drží

RuleJak se v R2 projevila
SANDBOX-FIRST 26 sandbox testů + 19 replay scenarios PASS před každým PUT
AUTO-REGISTRY Nový var sh_vacancy_guardian_heartbeat_ts přidán s audit JSON; sandbox runner registry baseline preserved
SAFE_REPAIR_MODE Per-script live backup pre-PUT, SHA drift check, bit-perfect verify, rollback dokumentován jako single PUT ~10s
REUSE FIRST Žádný nový HomeyScript. 6 patchů = update existing scripts. Sandbox tests T22-T26 vznikly v existing framework.
RPi/HAOS infra rule Vše pulled přes Homey REST + HAOS sandbox staging dir. Žádný PC read v R2.

Co bude soak ukázat

Sandbox runner běží každou hodinu jako 24/7 brána. Pair-logic diagnóza sh_vacancy_guardian_heartbeat_ts vs sh_vacancy_guardian_last_run_ts poskytne kontinuální čítač:

  • OK_completing rate (kolik z 288 fires/day skutečně dokončí)
  • CRASH_MID_RUN rate (kolik startů killed)
  • Watchdog restart trend per hour
  • Bathroom audio_mode reset frequency (kolikrát guardian executes patch B)

Pokud po 2-6 hodinách restart rate spadne pod 20/h, R2 Quick Win considered success. Pokud zůstane 20-30/h, další kandidát je Shelly poller offload na RPi Python (eliminuje 120/h fires). Pokud je stále >30/h, deeper problém — log buffer offload, cache patches, možná HomeyScript app version upgrade.

Takeaway

Šest živých zásahů do produkčního Homey v jednom dni je hodně. Bez sandbox-first by to bylo riskantní. S 26 sandbox testy + 19 replay scenarios + per-script SHA verify + audit JSON za každý PUT, ale rychlost neznamená chaos.

A reálný uživatelský win — bathroom radio už nebude resume z prázdné koupelny — byl měřitelný 9 minut po prvním deploy. Ne za týden. Ne s "ono se to časem srovná". Měřitelný timestamp, prokazatelná oprava, sandbox replay scenario, který tu situaci reprodukoval předem.