Skip to content

fix(alertmanager): stop routing the member resolve write through ctx.db.query (BLO-31035) - #1579

Merged
allyblockcast[bot] merged 2 commits into
masterfrom
fix/blo-31035-aggregate-member-resolve-select-only
Sep 1, 2026
Merged

fix(alertmanager): stop routing the member resolve write through ctx.db.query (BLO-31035)#1579
allyblockcast[bot] merged 2 commits into
masterfrom
fix/blo-31035-aggregate-member-resolve-select-only

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Sep 1, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work
  • The Alertmanager plugin is the intake path that turns firing Prometheus alerts into Paperclip issues with an owner, priority, and metadata
  • Since ~2026-08-29 that intake has been 100% down: every webhook delivery 502s, 4,400 failures/hour against a ≤500/h baseline, and zero alert-origin issues filed in ~26.5h
  • Alerts firing with nothing being filed is indistinguishable from a quiet estate, which is precisely the failure mode alert intake exists to prevent
  • The plugin host exposes two DB calls — ctx.db.query (SELECT-only) and ctx.db.execute (INSERT/UPDATE/DELETE, returns only a row count) — and resolveAggregateMember sent an UPDATE … RETURNING through the SELECT-only one
  • This pull request routes that write through execute and makes the test double enforce the host's contract, so the bug class cannot ship green again
  • The benefit is that alert intake resumes, and every existing test in the file now covers this class of mistake

Linked Issues or Issue Description

  • Refs BLO-31035Alertmanager intake is 100% down ~26.5h: plugin worker 502s on every webhook

Bug summary (Paperclip-internal tracker, no GitHub issue):

  • What happened: POST /plugins/paperclip-plugin-alertmanager/webhooks/alertmanager returns 502 for every delivery containing a resolved, aggregate-tracked alert.
  • Expected: 2xx, member marked resolved, issue closed or commented.
  • Actual: host rejects the statement with ctx.db.query only allows SELECT statements; handleWebhook collects the per-alert failure and throws AlertDeliveryIncompleteError, so the whole batch 502s and Alertmanager redelivers it forever.
  • Evidence: 206 occurrences of that error in a single 13k-line paperclip-0 window; sum(increase(alertmanager_notification_requests_failed_total{integration="webhook"}[1h]))4,400; 0 alert-origin issues in ~26.5h.

What Changed

  • webhook-handler.tsresolveAggregateMember no longer issues UPDATE … RETURNING issue_id through ctx.db.query. The membership read is now a SELECT via query, and the resolve write is an UPDATE via execute.
  • __tests__/worker.test.ts — the ctx.db.query test double previously accepted any SQL. An afterEach now asserts the host's SELECT-only contract (validatePluginRuntimeQuery) over every recorded db.query call, across every test in the file.
  • __tests__/worker.test.ts — four mock branches that keyed on the old UPDATE … aggregate_members SQL now key on the new SELECT, so they keep exercising the membership path instead of silently falling through to "no membership".

Verification

pnpm --filter @paperclipai/plugin-sdk build
cd packages/plugins/paperclip-plugin-alertmanager
npx vitest run     # 258 passed
npx tsc --noEmit -p tsconfig.json   # clean
  • 258 tests pass. The one failing file, job-company-scope.test.ts, fails identically on unmodified master (pre-existing collection failure, embedded-postgres) — confirmed by a baseline run via git stash before the change, which reported the same 1 failed | 7 passed.
  • The new guard was verified to actually bite. Reintroducing the exact original statement makes 8 tests fail with the host's own message: ctx.db.query only allows SELECT statements, got: UPDATE alertmanager.alertmanager_aggregate_members. A regression test that cannot fail is worthless, so this was checked rather than assumed.
  • Schema claims verified against the live database, not inferred: alertmanager_aggregate_members_pkey PRIMARY KEY (company_id, aggregate_key, fingerprint).

Risks

Low. The single statement becomes two, so the atomicity question is the only real one — and it is closed rather than hand-waved:

  • Both statements key on the members primary key (company_id, aggregate_key, fingerprint), so the read matches at most the single row the write targets.
  • Nothing in the plugin deletes member rows (only ..._creation_claims rows are deleted), so the row cannot vanish between the two statements.
  • The write is idempotent (COALESCE(resolved_at, now())), so a concurrent delivery resolving the same member in between lands on the same terminal state and keeps the earlier resolved_at.

Return values are unchanged: no-membership is still returned exactly when no member row matches that predicate, and resolvedIssueId still falls back to the passed issueId.

Out of scope, deliberately. The 19 aggregates wedged in phase firing are a separate defect. The firing fence is claimed at :852, the try opens at :863, and the finally releases at :1347 with nothing throwable in between — so an exception cannot wedge it; only process death between claim and release can, which is what a rollout does. Fixing that needs restart-resumable fences and is tracked separately. Live DB shows 19 firing / 92 active / 0 finalizing, confirming the two are independent.

Model Used

  • Claude Opus 4.5 (claude-opus-5[1m]), 1M context, extended thinking, with tool use and code execution (repo checkout, vitest/tsc, kubectl, live Prometheus and PostgreSQL reads).

Checklist

…db.query (BLO-31035)

`resolveAggregateMember` marked an aggregate member resolved with a single
`UPDATE ... RETURNING issue_id` issued through `ctx.db.query`. That call path is
SELECT-only (`validatePluginRuntimeQuery`), so the host rejected it with
"ctx.db.query only allows SELECT statements" on every resolve delivery for an
aggregate-tracked fingerprint. `handleWebhook` collects per-alert failures and
then throws `AlertDeliveryIncompleteError`, so one rejected statement 502s the
whole batch and Alertmanager redelivers it forever.

Measured on the live estate before this change: ~4,400 webhook delivery failures
per hour against a <=500/h baseline, 206 of these rejections in one 13k-line
`paperclip-0` window, and zero alert-origin issues filed in ~26.5h.

There is no host call that both writes and returns a column — `ctx.db.execute`
reports only a row count — so the statement is split in two. The split is exact,
not approximate: both statements are keyed on the members primary key
(company_id, aggregate_key, fingerprint), so the read matches at most the single
row the write targets; nothing deletes member rows, so it cannot vanish in
between; and the write is idempotent via COALESCE, so a concurrent delivery
resolving the same member lands on the same terminal state.

The test double for `ctx.db.query` accepted any SQL, which is why this shipped
green. It now asserts the host's SELECT-only contract over every recorded call,
so each existing test in the file covers the bug class rather than just this one
call site. Verified by reintroducing the defect: 8 tests fail with the host's
exact message.

Not fixed here, and deliberately separate: the 19 aggregates wedged in phase
'firing' are a distinct defect. The firing fence is released in a `finally`, so
an exception cannot wedge it — only process death between claim and release can,
which is what a rollout does. That needs restart-resumable fences, not this.
@allyblockcast

allyblockcast Bot commented Sep 1, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-31035

1 similar comment
@allyblockcast

allyblockcast Bot commented Sep 1, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-31035

@allyblockcast

allyblockcast Bot commented Sep 1, 2026

Copy link
Copy Markdown
Author

Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Verification
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • Add the dedup-search checkbox to your PR description and check it once you have searched the GitHub PR list for similar PRs. See the PR template at .github/PULL_REQUEST_TEMPLATE.md and CONTRIBUTING.md → "Before You Start: Search First".

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 4b7a6c5

Critical Issues (0)

Important Issues (0)

Suggestions (0)

Strengths

  • The aggregate-member resolution now uses ctx.db.query only for SELECT statements and routes the mutation through ctx.db.execute.
  • The membership lookup, fallback behavior, and sibling-resolution checks remain keyed by the aggregate membership identity.
  • Tests now enforce the host query contract across contexts, including tests that install custom query implementations.

Recommended Action

  1. No Critical or Important issues found. This App-authored PR is reviewed as a formal comment.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: af0c315

Critical Issues (0)

Important Issues (0)

Suggestions (0)

Strengths

  • The aggregate-member resolution now uses ctx.db.query only for SELECT statements and routes the mutation through ctx.db.execute.
  • The membership lookup, fallback behavior, and sibling-resolution checks remain keyed by the aggregate membership identity.
  • Tests now enforce the host query contract across contexts, including tests that install custom query implementations.

Recommended Action

  1. No Critical or Important issues found. This App-authored PR is reviewed as a formal comment.

@allyblockcast
allyblockcast Bot added this pull request to the merge queue Sep 1, 2026
Merged via the queue into master with commit eae0646 Sep 1, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants