v0.4.0-gitea.1 polled /workspaces/:id/activity but never sent
/registry/heartbeat. The platform's healthsweep
(workspace-server/internal/registry/healthsweep.go) flipped any
runtime='external' workspace whose last_heartbeat_at was older than
90s back to status='awaiting_agent', so the canvas presence badge
stuck on awaiting_agent within 90s of plugin start even while A2A
traffic flowed fine over the long-poll loop.
Fix: per-workspace heartbeat ticker (default 30s, three ticks inside
the 90s stale window) POSTs the minimal HeartbeatPayload shape
(workspace_id only) — same path the Python runtime in
workspace/heartbeat.py uses when it has nothing else to report.
heartbeat.ts pure module + Bun.serve fixture test pin the wire
shape (POST + bearer + Origin + workspace_id body) so a future
refactor that drops any of those silently re-breaks the badge.
Bump 0.4.0-gitea.1 → 0.4.0-gitea.2 and document
MOLECULE_HEARTBEAT_INTERVAL_MS in README.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>