feat: broadcast Timmy state changes via WS relay (#380)

Co-authored-by: Kimi Agent <kimi@timmy.local>
Co-committed-by: Kimi Agent <kimi@timmy.local>
This commit is contained in:
2026-03-19 00:25:11 -04:00
committed by hermes
parent aa4f1de138
commit da43421d4e
5 changed files with 171 additions and 16 deletions

View File

@@ -11,6 +11,7 @@ import asyncio
import hashlib
import json
import logging
from collections.abc import Awaitable, Callable
from datetime import UTC, datetime
from pathlib import Path
@@ -91,12 +92,14 @@ class WorkshopHeartbeat:
self,
interval: int = HEARTBEAT_INTERVAL,
path: Path | None = None,
on_change: Callable[[dict], Awaitable[None]] | None = None,
) -> None:
self._interval = interval
self._path = path or PRESENCE_FILE
self._last_hash: str | None = None
self._task: asyncio.Task | None = None
self._trigger = asyncio.Event()
self._on_change = on_change
async def start(self) -> None:
"""Start the heartbeat background loop."""
@@ -129,13 +132,13 @@ class WorkshopHeartbeat:
except TimeoutError:
pass # Normal periodic tick
self._write_if_changed()
await self._write_if_changed()
except asyncio.CancelledError:
raise
except Exception as exc:
logger.error("Workshop heartbeat error: %s", exc)
def _write_if_changed(self) -> None:
async def _write_if_changed(self) -> None:
"""Build state, compare hash, write only if changed."""
state_dict = get_state_dict()
current_hash = _state_hash(state_dict)
@@ -143,6 +146,11 @@ class WorkshopHeartbeat:
return
self._last_hash = current_hash
write_state(state_dict, self._path)
if self._on_change:
try:
await self._on_change(state_dict)
except Exception as exc:
logger.warning("on_change callback failed: %s", exc)
def _subscribe_to_events(self) -> None:
"""Subscribe to cognitive state change events on the sensory bus."""