[bug] [presence] poll-mode workspaces show awaiting_agent — platform-side presence tracker should count activity polls #24

Closed
opened 2026-05-07 13:18:12 +00:00 by claude-ceo-assistant · 1 comment

Symptom

External user reported v0.4.0-gitea.1 channel plugin (delivery_mode=poll) workspaces show status: awaiting_agent in canvas instead of online, even though plugin is actively polling /workspaces/:id/activity and auth is valid.

Filed as companion to molecule-mcp-claude-channel#? (presence ping on plugin side).

Platform-side question

Should the platform's presence tracker count /workspaces/:id/activity GETs from delivery_mode=poll workspaces as keepalive ticks?

Today the tracker likely only counts explicit heartbeat POSTs (which the legacy server-mode plugin makes). Poll-mode plugins skip that POST since they're already long-polling activity. Net result: poll-mode workspaces look offline even when actively running.

Proposed fix paths

  1. Platform-only: any /workspaces/:id/activity GET from a registered workspace counts as last_seen_at update. Fixes ALL future poll-mode plugins without per-plugin changes.
  2. Plugin-only: each poll-mode plugin adds an explicit heartbeat POST per tick (filed on molecule-mcp-claude-channel side).
  3. Combined: platform accepts both; plugin opt-in to explicit POST for backward compat.

Recommendation

Path 1 is cleaner — single source of truth for presence; plugins don't have to know about it. But platform team should evaluate based on existing presence-tracker semantics.

Investigation handoff

  • Find presence-tracker code: probably workspace-server's /workspaces/:id GET handler or a separate presence reconciler.
  • Identify field: last_seen_at vs last_heartbeat_at vs agent_attached_at.
  • Decide whether activity GET should update it.

Reporter

airenostars (Reno-Stars external workspace user) via Hongming chat 2026-05-07.

Out of scope

  • Plugin-side fix (separate issue on molecule-mcp-claude-channel)
  • General presence-tracker architecture refactor
## Symptom External user reported v0.4.0-gitea.1 channel plugin (delivery_mode=poll) workspaces show `status: awaiting_agent` in canvas instead of `online`, even though plugin is actively polling `/workspaces/:id/activity` and auth is valid. Filed as companion to molecule-mcp-claude-channel#? (presence ping on plugin side). ## Platform-side question Should the platform's presence tracker count `/workspaces/:id/activity` GETs from delivery_mode=poll workspaces as keepalive ticks? Today the tracker likely only counts explicit heartbeat POSTs (which the legacy server-mode plugin makes). Poll-mode plugins skip that POST since they're already long-polling activity. Net result: poll-mode workspaces look offline even when actively running. ## Proposed fix paths 1. **Platform-only**: any `/workspaces/:id/activity` GET from a registered workspace counts as last_seen_at update. Fixes ALL future poll-mode plugins without per-plugin changes. 2. **Plugin-only**: each poll-mode plugin adds an explicit heartbeat POST per tick (filed on molecule-mcp-claude-channel side). 3. **Combined**: platform accepts both; plugin opt-in to explicit POST for backward compat. ## Recommendation Path 1 is cleaner — single source of truth for presence; plugins don't have to know about it. But platform team should evaluate based on existing presence-tracker semantics. ## Investigation handoff - Find presence-tracker code: probably workspace-server's `/workspaces/:id` GET handler or a separate presence reconciler. - Identify field: `last_seen_at` vs `last_heartbeat_at` vs `agent_attached_at`. - Decide whether activity GET should update it. ## Reporter airenostars (Reno-Stars external workspace user) via Hongming chat 2026-05-07. ## Out of scope - Plugin-side fix (separate issue on molecule-mcp-claude-channel) - General presence-tracker architecture refactor
Author
Owner

Closing as won't-fix — plugin-side fix landed in molecule-mcp-claude-channel#7 (tag v0.4.0-gitea.2).

Why platform-side change skipped

Path 1 (count /workspaces/:id/activity GETs as keepalives) is interesting but a deeper semantic change to a hot read endpoint. Risks:

  • Would mask actual bugs: e.g. a poll-mode workspace whose agent silently died but a stale tail process keeps polling activity would falsely look online.
  • last_heartbeat_at becomes ambiguous between "agent alive" and "someone read activity" — the column documentation in models/workspace.go would need to change.
  • The activity endpoint is per-call hot; adding an UPDATE workspaces SET last_heartbeat_at = now() per GET would multiply DB writes by the poll cadence (5s default) across every poll-mode workspace.

The plugin-side fix is the smaller blast radius and matches the existing Python runtime + legacy server-mode plugin contract: a real /registry/heartbeat POST is the source of truth for presence. The platform contract stays unchanged.

Plugin fix details

  • Per-workspace setInterval posting {workspace_id: id} to /registry/heartbeat every 30s (three ticks inside the platform's 90s REMOTE_LIVENESS_STALE_AFTER stale window).
  • Closes the gap surfaced in internal/registry/healthsweep.go:166 where external runtime workspaces with stale heartbeat get flipped to awaiting_agent.
  • Live verification on Hongming tenant pending; tests pinned via Bun.serve fixture (heartbeat.test.ts).

If future poll-mode plugins ship without their own heartbeat path, we can revisit Path 1 with proper guard rails (e.g. only count GETs as keepalives within a narrower window). Until then, closing this issue.

Closing as won't-fix — plugin-side fix landed in molecule-mcp-claude-channel#7 (tag v0.4.0-gitea.2). ## Why platform-side change skipped Path 1 (count `/workspaces/:id/activity` GETs as keepalives) is interesting but a deeper semantic change to a hot read endpoint. Risks: - Would mask actual bugs: e.g. a poll-mode workspace whose agent silently died but a stale tail process keeps polling activity would falsely look online. - `last_heartbeat_at` becomes ambiguous between "agent alive" and "someone read activity" — the column documentation in models/workspace.go would need to change. - The activity endpoint is per-call hot; adding an `UPDATE workspaces SET last_heartbeat_at = now()` per GET would multiply DB writes by the poll cadence (5s default) across every poll-mode workspace. The plugin-side fix is the smaller blast radius and matches the existing Python runtime + legacy server-mode plugin contract: a real `/registry/heartbeat` POST is the source of truth for presence. The platform contract stays unchanged. ## Plugin fix details - Per-workspace `setInterval` posting `{workspace_id: id}` to `/registry/heartbeat` every 30s (three ticks inside the platform's 90s `REMOTE_LIVENESS_STALE_AFTER` stale window). - Closes the gap surfaced in `internal/registry/healthsweep.go:166` where `external` runtime workspaces with stale heartbeat get flipped to `awaiting_agent`. - Live verification on Hongming tenant pending; tests pinned via Bun.serve fixture (heartbeat.test.ts). If future poll-mode plugins ship without their own heartbeat path, we can revisit Path 1 with proper guard rails (e.g. only count GETs as keepalives within a narrower window). Until then, closing this issue.
Sign in to join this conversation.
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: molecule-ai/molecule-core#24
No description provided.