fix(heartbeat): promote parked scheduled retries when a human comment wake coalesces into them#9732
Conversation
…alesces into them Comment wakes are enqueued with source "automation", so when the target run is parked in scheduled_retry the wake is absorbed into the run's context without pulling its schedule forward. The person who commented gets no response until the bounded backoff expires, which can be hours after the underlying failure (for example a provider session limit) has cleared. Promote the parked retry through the standard gate whenever the coalesced wake was requested by a user, mirroring retryScheduledRetryNow. Agent- and system-initiated wakes keep the existing behavior so bounded retry backoff still protects against wake storms.
Greptile SummaryThis PR fixes an agent responsiveness bug: when a human comments on an issue whose agent run is parked in
Confidence Score: 5/5Safe to merge — the promotion still goes through the full gate (budget, invokability, issue state, pause holds), attempt counting is unchanged, and agent/system wakes are unaffected; only human-attributed wakes trigger the pull-forward. Both race-condition and observability gaps from the first review are closed: the snapshot merge and scheduledRetryAt pull-forward are now a single guarded UPDATE, and the original wakeup request payload is stamped in the same transaction. The fallback path on a lost race still writes an unguarded snapshot update, but that window only opens after the scheduler has already promoted the run, making a conflict there benign. Test coverage directly exercises the incident-shaped scenario and the payload-stamp regression. No files require special attention. Important Files Changed
Reviews (3): Last reviewed commit: "fix(heartbeat): make human-wake retry pr..." | Re-trigger Greptile |
…wakeup payload Address two review findings on the human-wake promotion path: 1. The same-scope coalesce path wrote the merged context snapshot and then promoted the retry in a second unguarded update that spread the in-memory snapshot, so a concurrent snapshot write landing between the two could be silently clobbered. The promotion now receives the merged snapshot and folds the merge and the scheduledRetryAt pull-forward into a single UPDATE guarded by status = scheduled_retry. When the guarded update loses the race the merged snapshot is still persisted so the coalesced wake is not dropped. 2. Mirror retryScheduledRetryNow by stamping the retry run's original agentWakeupRequests.payload with scheduledRetryAt and retryNowRequestedAt inside the same transaction, so support tooling reading the wakeup request can see the retry was pulled forward.
|
@greptileai review Both P2 findings from the first review are addressed in 5f38458 (single guarded UPDATE for the merge plus pull-forward, and the |
Thinking Path
Linked Issues or Issue Description
Refs #9730 (this PR fixes the second half of that report: "commenting on the issue does not wake the agent while a run sits in scheduled_retry"; the first half, backoff ignoring the provider-reported reset time, is addressed by #9626 and #8692)
Related PRs found while searching for duplicates:
source === "on_demand"wakes (the "Run Heartbeat" button); comment wakes arrive withsource: "automation"and are not covered by it. This PR follows the same implementation pattern so the two compose cleanly if both land; the promotion is guarded by run status, so double promotion is a no-op.Incident evidence (from a production instance on
@paperclipai/server 2026.707.0, full timeline in #9730): an agent hit a provider session limit and its retry was parked for 2h13m. A user comment posted 9 seconds after the limit reset was coalesced into the parked run (itscontextSnapshot.wakeCommentIdwas updated) but the run kept its schedule, more than two hours away. Clicking "Run Heartbeat" one minute later dispatched a run instantly, proving capacity was back. The issue was inin_reviewat the time, so the existing route-level path that cancels a scheduled retry when a human comments (shouldHumanCommentResumeInProgressScheduledRetry) did not apply; that path only coversin_progressissues, and the wake was swallowed at the heartbeat layer instead.What Changed
server/src/services/heartbeat.ts: addedpromoteScheduledRetryForHumanWakeinsideenqueueWakeup. When a wake withrequestedByActorType: "user"coalesces into ascheduled_retryrun (either coalesce path: the issue-execution transaction path or the same-scope active-run path), the retry'sscheduledRetryAtis pulled forward to now, a lifecycle run event is recorded, and the run is promoted through the standardpromoteScheduledRetryRungate (budget, invokability, issue state), then dispatched viastartNextQueuedRunForAgent.promoteScheduledRetryForHumanWakenow receives the merged snapshot and folds the merge plus thescheduledRetryAtpull-forward into a singleUPDATEguarded bystatus = 'scheduled_retry', so a concurrent snapshot write can no longer land between the two writes and be clobbered. If the guarded update loses the race, the merged snapshot is still persisted so the coalesced wake is not dropped.retryScheduledRetryNow, the promotion now also stamps the retry run's originalagentWakeupRequests.payloadwithscheduledRetryAtandretryNowRequestedAtinside the same transaction, closing the observability gap for support tooling that reads the wakeup request to explain a promotion.server/src/__tests__/heartbeat-human-comment-wake-promotes-retry.test.ts: embedded-postgres coverage for the new behavior: an agent comment wake keeps the retry parked; a human comment wake on anin_reviewissue (the incident shape) promotes and executes the parked run with the comment merged into its context; a human wake promotes an agent-scoped parked retry; an already-promoted (queued) run is not re-promoted.Verification
pnpm exec vitest run src/__tests__/heartbeat-human-comment-wake-promotes-retry.test.tsinserver/: 4 tests pass (run twice to confirm stability)pnpm exec vitest run src/__tests__/heartbeat-retry-scheduling.test.ts src/__tests__/heartbeat-comment-wake-batching.test.tsinserver/: 39 tests pass, no regressions in the adjacent retry and comment-wake suitestsc --noEmitinserver/passesagentWakeupRequests.payloadstamp; that assertion fails against the pre-fix code and passes after the fix, shown below.Full suite passing at 5f38458 (after the review fixes):
Same test file run against the pre-fix
heartbeat.ts(8c31865): the new payload-stamp assertion fails withretryNowRequestedAtmissing, proving the regression coverage is real:Risks
evaluateScheduledRetryGate, so budget blocks, agent invokability, terminal issue states, pause holds, and review-participant checks all still suppress it. Attempt counting is unchanged, so a promoted retry that fails again re-parks with the next bounded backoff step and retries stay finite.requestedByActorType: "user". Worst case they pull a retry forward that would have fired later anyway, still bounded by max attempts.Model Used
Claude Fable 5 (Anthropic, model ID
claude-fable-5), running in Claude Code / Claude Agent SDK with extended thinking and tool use. The change and tests were authored by the model and reviewed by the account owner.Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template#NNN/github.com/paperclipai/paperclipURLs)docs/...,fix/...) and contains no internal Paperclip ticket id or instance-derived details