fix(alertmanager): stop resolve auto-cancel from evicting a live execution lock (BLO-29908) - #1482
Conversation
…ution lock (BLO-29908) A resolved alert cancelled its issue unconditionally. `updateIssue` clears checkoutRunId / executionRunId / executionAgentNameKey / executionLockedAt on any transition out of `in_progress`, so the bridge silently dropped whatever run held the row — observed 46s after a live run wrote its findings document, and repeating roughly twice an hour per alert while a fingerprint flaps. The cancel now pins both execution-lock columns to null as write preconditions, which `updateIssue` evaluates inside its own transaction. A held row fails the precondition, nothing is written, and the plugin annotates the thread instead of cancelling. Both columns are pinned because an issue can be held via checkoutRunId with executionRunId still null (BLO-19749). Co-Authored-By: Paperclip <noreply@paperclip.ing>
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: ad7429a
Critical Issues (0)
Important Issues (1)
- [native-codex]
packages/plugins/paperclip-plugin-alertmanager/src/webhook-handler.ts:848-878— the holding-run diagnostic can become permanently keyed tounknown. If the initialissues.getsees no lock (or a lock that is released) and another run acquires the issue before the guarded update executes, the CAS correctly fails, butholderRunIdis stillnulland the code storescancelWithheldForRunId = "unknown". The next resolve sees theunknownmarker and suppresses its notification even if a different run now holds the issue, contradicting the stated per-holding-run behavior and hiding the actual owner. Re-read the issue after the CAS conflict to obtain the current lock owner, or avoid persisting/marker-deduping an unknown owner when the owner cannot be established.
Suggestions (1)
- [pr-review-toolkit]
packages/plugins/paperclip-plugin-alertmanager/src/webhook-handler.ts:791-804— the list-then-create marker check is not atomic, so concurrent duplicate resolved deliveries can still post two annotations for the same run. Consider an idempotent server-side comment marker or a state-store/CAS-based notification claim if exactly-once annotation is required.
Strengths
- The cancel uses transaction-level preconditions on both lock columns, covering checkout-only ownership as well as execution ownership.
- Non-precondition failures are rethrown, preserving Alertmanager retry semantics.
- The new tests cover both lock columns, withheld cancellation, repeated resolution, and transient failure handling.
Recommended Action
- Fix the Important issue before merge.
- Consider making the notification marker claim atomic.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
|
@allyblockcast Please re-review commit The CAS conflict path now re-reads the issue and uses the current checkout/execution holder when available; it no longer persists |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 32b6cae
Prior Findings Dispositioned (1)
- prior:ad7429a important 1 — fixed —
packages/plugins/paperclip-plugin-alertmanager/src/webhook-handler.ts:882-887— after the guarded update conflicts, the handler re-reads the issue and derives the diagnostic run from the current checkout/execution holder instead of persisting anunknownmarker.
Critical Issues (0)
Important Issues (1)
- [native-codex]
packages/plugins/paperclip-plugin-alertmanager/src/webhook-handler.ts:882-887— the fallback still reuses the stale pre-CAS holder when the post-conflict read finds no holder. If the initially observed run releases before the guarded update reaches the transaction, the CAS can fail due to the intervening lock, thencurrentIssuecan be unheld; the?? holderRunIdfallback records the released run as the current holder and creates a marker that can suppress notification for a later, different holder.- Only persist and deduplicate a run ID confirmed by the post-conflict read. If no holder is present after the conflict, avoid writing a holder-specific marker/comment (or use a separate non-holder diagnostic that cannot suppress a future holder).
Suggestions (1)
- [pr-review-toolkit]
packages/plugins/paperclip-plugin-alertmanager/src/webhook-handler.ts:790-804— the list-then-create marker check is not atomic, so concurrent duplicate resolved deliveries can still post two annotations for the same run. Consider an idempotent server-side marker or state-store compare-and-set if exactly-once annotation is required.
Strengths
- The cancel uses transaction-level preconditions on both lock columns, covering checkout-only ownership as well as execution ownership.
- Non-precondition failures are rethrown, preserving Alertmanager retry semantics.
- The regression coverage exercises both lock columns, withheld cancellation, repeated resolution, transient failures, and the post-conflict holder re-read.
Recommended Action
- Fix the Important issue before merge.
- Consider making the notification marker claim atomic.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
|
Addressed the important stale-holder fallback in commit d8e9de1.
Verification: |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: d8e9de1
Prior Findings Dispositioned (1)
- prior:32b6cae important 1 — fixed —
packages/plugins/paperclip-plugin-alertmanager/src/webhook-handler.ts:881-895— after a guarded cancel conflicts, the handler re-reads the issue and only uses a holder confirmed by that post-conflict read; an unheld row produces no holder-specific marker, so a released run cannot suppress notification for a later holder.
Critical Issues (0)
Important Issues (0)
Suggestions (1)
- [pr-review-toolkit]
packages/plugins/paperclip-plugin-alertmanager/src/webhook-handler.ts:790-804— the list-then-create marker check is not atomic, so concurrent duplicate resolved deliveries can still post duplicate annotations for one holding run. Consider an idempotent server-side marker or state-store compare-and-set if exactly-once annotation is required.
Strengths
- The cancel uses transaction-level preconditions on both lock columns, covering checkout-only ownership as well as execution ownership.
- The post-conflict re-read and regression tests close the prior stale and unknown-holder marker cases.
- Malformed preconditions are rejected, and non-precondition failures still propagate so Alertmanager retries.
Recommended Action
- Consider making the notification marker claim atomic.
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: d8e9de1
Prior Findings Dispositioned (1)
- prior:32b6cae important 1 — fixed —
packages/plugins/paperclip-plugin-alertmanager/src/webhook-handler.ts:881-895— after a guarded cancel conflicts, the handler re-reads the issue and only uses a holder confirmed by that post-conflict read; an unheld row produces no holder-specific marker, so a released run cannot suppress notification for a later holder.
Critical Issues (0)
Important Issues (0)
Suggestions (1)
- [pr-review-toolkit]
packages/plugins/paperclip-plugin-alertmanager/src/webhook-handler.ts:790-804— the list-then-create marker check is not atomic, so concurrent duplicate resolved deliveries can still post duplicate annotations for one holding run. Consider an idempotent server-side marker or state-store compare-and-set if exactly-once annotation is required.
Strengths
- The cancel uses transaction-level preconditions on both lock columns, covering checkout-only ownership as well as execution ownership.
- The post-conflict re-read and regression tests close the prior stale and unknown-holder marker cases.
- Malformed preconditions are rejected, and non-precondition failures still propagate so Alertmanager retries.
Recommended Action
- Consider making the notification marker claim atomic.
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: d8e9de1
Prior Findings Dispositioned (1)
- prior:32b6cae important 1 — fixed —
packages/plugins/paperclip-plugin-alertmanager/src/webhook-handler.ts:881-895— after a guarded cancel conflicts, the handler re-reads the issue and only uses a holder confirmed by that post-conflict read; an unheld row produces no holder-specific marker, so a released run cannot suppress notification for a later holder.
Critical Issues (0)
Important Issues (0)
Suggestions (1)
- [pr-review-toolkit]
packages/plugins/paperclip-plugin-alertmanager/src/webhook-handler.ts:790-804— the list-then-create marker check is not atomic, so concurrent duplicate resolved deliveries can still post duplicate annotations for one holding run. Consider an idempotent server-side marker or state-store compare-and-set if exactly-once annotation is required.
Strengths
- The cancel uses transaction-level preconditions on both lock columns, covering checkout-only ownership as well as execution ownership.
- The post-conflict re-read and regression tests close the prior stale and unknown-holder marker cases.
- Malformed preconditions are rejected, and non-precondition failures still propagate so Alertmanager retries.
Recommended Action
- Consider making the notification marker claim atomic.
|
Thanks for the review. I agree the list-then-create check is not atomic under concurrent resolved deliveries. I am leaving it unchanged in this PR because the marker is an operational notification and the acceptance criterion is observable notification of the holding run, not exactly-once annotation; duplicate comments are harmless and do not affect lock safety or state. Making this atomic would require extending the plugin SDK/host comment contract with an idempotency key or adding a state-store CAS, which is a separate cross-layer API change. The guarded cancel and post-conflict holder re-read remain the correctness boundary for BLO-29908. |
Thinking Path
Linked Issues or Issue Description
Paperclip issue: BLO-29908
Related, not fixed here:
keep_firing_foron the LLMProxy rules; reduces the frequency but not the defectDistinct from #923, which is intake-side (aggregate-safe issue creation). This is resolve-side.
What Changed
packages/plugins/paperclip-plugin-alertmanager/src/webhook-handler.ts— the resolve cancel now passesexpectedCurrentCheckoutRunId: nullandexpectedCurrentExecutionRunId: null. On a precondition failure the plugin posts a one-per-holding-run annotation naming the run, logs, and emitsalertmanager.resolved.cancel_withheld. Non-precondition errors still propagate so Alertmanager retries.checkoutRunIdwithexecutionRunIdstillnull(BLO-19749), which a one-column guard would miss."before the update could be applied"suffix — these are the only preconditions this call sets, so any of the three variants means exactly one thing.packages/plugins/paperclip-plugin-alertmanager/src/types.ts—AlertStateRecordgains diagnostic-onlycancelWithheldForRunId/cancelWithheldAt, so "open even though its alert cleared" is answerable from the state row.packages/plugins/sdk/src/types.ts— the two expectation fields are now part of the typedissues.updatesurface, documented as what any plugin transitioning an issue it does not hold should pass.server/src/services/plugin-host-services.ts— lifts the preconditions out ofpatch(so they are not logged as attempted column writes) and forwards them explicitly rather than by implicit spread, withnormalizeExpectedRunIdrejecting malformed values instead of degrading them toundefined.Deliberately not changed:
updateIssue's lock-clearing atissues.ts:9900. Making that conditional would alter semantics for every writer in the product, including the holding run's own legitimate release and the recovery paths that clear wedged locks. The defect is a third party writing a status it has no claim on, so the guard belongs at that call site.Verification
The new tests fail on
master. Reverting onlywebhook-handler.tsand re-running:The tests are the observed trace: a
resolvedwebhook delivered against a fingerprint whose issue isin_progresswith both lock columns naming run0657f242-….Server typecheck was run (
npx tsc --noEmit -p server/tsconfig.typecheck.json) — 834 pre-existing errors repo-wide under that config, none in any file this PR touches.Gap a reviewer should close: I could not run the server test suite (no Postgres in this environment), so the host→service forwarding of the two preconditions is verified by code reading plus the explicit forwarding above, not by execution. The end-to-end assertion worth adding in CI is BLO-29908's own: check out an alertmanager-origin issue as run R, deliver a
resolvedwebhook for that fingerprint, and assert the three lock columns still name R and status is notcancelled.Risks
Low-to-moderate, and biased toward safety.
getOrchestration().runs) derives run→issue fromheartbeatRuns.contextSnapshot ->> 'issueId'and caps at 100 rows — a holder whose snapshot lacksissueIdwould read as dead and be evicted anyway, which is the bug. The host's ownisTerminalOrMissingHeartbeatRunis the authoritative predicate; exposing it as a precondition (expectedNoLiveExecutionLock) is the precise follow-up and would let this guard narrow to live holders only.AlertStateRecordfields are optional; existing state rows read asundefined.escalation.ts:278cancels a cover issue through the same unguarded path. Not fixed here to keep the diff scoped to the filed defect; filed as follow-up.Model Used
Claude (Anthropic),
claude-opus-4-6, extended thinking, with tool use / code execution — running as the PlatformSREEngineer agent in Paperclip.Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template