Skip to content

heartbeat auto preflight mis-parses local HB_ID as UTC and keeps pending_heartbeat fresh for hours #117

Description

@elliot245

Summary

session_mode:auto heartbeat preflight can keep skipping on pending_heartbeat:<HB_ID> for hours because HB_ID is emitted with local wall-clock time but stale detection parses it as UTC.

Observed impact

On work-assistant/main (Asia/Shanghai, UTC+8):

  • heartbeat HB_ID=20260317-093001 was sent at 2026-03-17 09:30 CST
  • from 09:55 CST through 12:30 CST, every cron-triggered auto heartbeat skipped with the same reason:
    • state=busy, reason=pending_heartbeat:20260317-093001
  • dispatch only resumed at 12:35 CST via the starvation guard (32 consecutive preflight skips)

This swallowed the intended 30-minute proactive Slack updates for ~3 hours even though cron itself was still running.

Root cause

Current code path:

  • heartbeat generation: time.strftime('%Y%m%d-%H%M%S') → local wall-clock timestamp
  • stale check: datetime.strptime(hb_id, ...).replace(tzinfo=timezone.utc) → interpreted as UTC

For a host in UTC+8, HB_ID=20260317-093001 generated at 09:30 CST is later parsed as 09:30 UTC, i.e. ~8 hours in the future relative to actual UTC runtime. That makes stale age negative, so the 15-minute expiry never triggers on schedule.

Concrete example from the incident:

  • actual event time: 2026-03-17 09:30:01 CST = 2026-03-17 01:30:01 UTC
  • parsed by stale check as: 2026-03-17 09:30:01 UTC
  • at 2026-03-17 04:35:00 UTC, computed age is still negative (-17701s)

Minimal reproduction

  1. Run agent-manager on a non-UTC host (e.g. Asia/Shanghai / UTC+8).
  2. Let auto heartbeat send a heartbeat with HB_ID=<local time>.
  3. Leave the pane without a matching HEARTBEAT_OK.
  4. Wait past the 15-minute stale threshold.
  5. Observe preflight continues returning pending_heartbeat:<same id> instead of expiring it.

Evidence

Heartbeat log excerpt from the affected workspace:

⏭️  Agent is not idle (state=busy, reason=pending_heartbeat:20260317-093001); skipping heartbeat dispatch in auto mode to avoid batch accumulation
... repeated on every 5-minute heartbeat ...
⚠️  Auto-mode starvation guard triggered after 32 consecutive preflight skips; dispatching one heartbeat attempt anyway

Expected behavior

  • newly generated heartbeat ids should be timezone-safe
  • stale pending-heartbeat recovery should still work for legacy pane content emitted before the fix
  • after the stale threshold, auto mode should stop treating the old pending_heartbeat marker as fresh

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions