Skip to content

pendingFinalDelivery heartbeat replay loops forever — no attempt cap or TTL on orphan sessions #85743

Description

@techware33-dev

pendingFinalDelivery heartbeat replay loops forever — no attempt cap or TTL on orphan sessions

Summary

A session entry whose pendingFinalDelivery: true flag is set but never cleared (orphan / dead session, channel-state mismatch, etc.) causes the heartbeat replay branch in src/auto-reply/reply/get-reply.ts to re-send the cached pendingFinalDeliveryText indefinitely. The same alert text is delivered to the user once per heartbeat tick, day after day. Observed in a 2026.5.18 install with pendingFinalDeliveryAttemptCount: 189 and pendingFinalDeliveryCreatedAt four days old.

This is complementary to (not duplicated by) #83184 / #83187 (clear-after-heartbeat-success) and #84679 (target === "none" clearing). Those fix the most common cause; this issue is about the fail-safe for everything they don't catch.

Reproduction

  1. End a session while pendingFinalDelivery: true and pendingFinalDeliveryText are set (e.g. process killed, channel disabled, telegram bot token rotated mid-delivery).
  2. Allow heartbeat ticks to fire against the same sessionKey.
  3. Inspect ~/.openclaw/agents/main/sessions/sessions.json → the entry's pendingFinalDeliveryAttemptCount climbs monotonically, pendingFinalDeliveryLastAttemptAt advances, pendingFinalDeliveryText is re-emitted verbatim each tick.

In production we saw 189 attempts over 4 days, surfacing the same stale "CRITICAL ALERT" message to the user repeatedly.

Root cause

src/auto-reply/reply/get-reply.ts (lines ~432–458, heartbeat replay branch, inside if (sessionEntry?.pendingFinalDelivery && sessionEntry.pendingFinalDeliveryText) { if (opts?.isHeartbeat) { ... else { ... } } }):

const attemptCount = (sessionEntry.pendingFinalDeliveryAttemptCount ?? 0) + 1;
sessionEntry.pendingFinalDeliveryLastAttemptAt = updatedAt;
sessionEntry.pendingFinalDeliveryAttemptCount = attemptCount;
sessionEntry.pendingFinalDeliveryLastError = null;
const replayText = sanitizePendingFinalDeliveryText(heartbeatPending.replayText);
sessionEntry.pendingFinalDeliveryText = replayText;
// ...persists, returns { text: replayText }

No bound check against attemptCount or pendingFinalDeliveryCreatedAt. Clear only fires via the sibling heartbeatPending.shouldClear branch (text stripped to HEARTBEAT_OK) or successful dispatch in dispatch-from-config.ts:clearPendingFinalDeliveryAfterSuccess — neither triggers for stale orphan buffers.

Proposed fix

Mirror the existing suspendPendingFinalDelivery({ reason: "retry-limit" | "expiry" }) vocabulary from src/agents/subagent-registry-lifecycle.ts:500-548. Before incrementing at line ~434, check:

  • attemptCount > PENDING_FINAL_DELIVERY_MAX_HEARTBEAT_ATTEMPTS (suggest 10), or
  • Date.now() - pendingFinalDeliveryCreatedAt > PENDING_FINAL_DELIVERY_TTL_MS (suggest 24h)

…and if either holds, route through the existing clear block (lines 406–431) with a warn log carrying the attempt count and age, then continue as if no pending delivery existed. This preserves the legitimate replay-after-restart behavior the buffer is designed for, while bounding the failure mode.

PR ready to follow — keeping the constants local so they can graduate to config if needed.

Workaround for users hit by this today

Until the upstream fix lands, a small janitor script that scans sessions.json and clears pendingFinalDelivery entries exceeding the same thresholds works. (Happy to share if useful.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High-priority user-facing bug, regression, or broken workflow.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:needs-product-decisionClawSweeper marked this issue as needing a product or behavior decision.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions