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
- Run agent-manager on a non-UTC host (e.g. Asia/Shanghai / UTC+8).
- Let auto heartbeat send a heartbeat with
HB_ID=<local time>.
- Leave the pane without a matching
HEARTBEAT_OK.
- Wait past the 15-minute stale threshold.
- 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
Summary
session_mode:autoheartbeat preflight can keep skipping onpending_heartbeat:<HB_ID>for hours becauseHB_IDis emitted with local wall-clock time but stale detection parses it as UTC.Observed impact
On
work-assistant/main(Asia/Shanghai, UTC+8):HB_ID=20260317-093001was sent at 2026-03-17 09:30 CSTstate=busy, reason=pending_heartbeat:20260317-09300132 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:
time.strftime('%Y%m%d-%H%M%S')→ local wall-clock timestampdatetime.strptime(hb_id, ...).replace(tzinfo=timezone.utc)→ interpreted as UTCFor a host in UTC+8,
HB_ID=20260317-093001generated 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:
2026-03-17 09:30:01 CST=2026-03-17 01:30:01 UTC2026-03-17 09:30:01 UTC2026-03-17 04:35:00 UTC, computed age is still negative (-17701s)Minimal reproduction
HB_ID=<local time>.HEARTBEAT_OK.pending_heartbeat:<same id>instead of expiring it.Evidence
Heartbeat log excerpt from the affected workspace:
Expected behavior
pending_heartbeatmarker as fresh