fix(alertmanager): don't silently assign new alert issues to an uninvokable agent - #1351
fix(alertmanager): don't silently assign new alert issues to an uninvokable agent#1351allyblockcast[bot] wants to merge 1 commit into
Conversation
…okable agent Alertmanager-origin issue creation resolves an assignee agentId via a label override, owner-map, or issue-route match and passed it to ctx.issues.create unconditionally. If that agent is paused, terminated, or otherwise uninvokable, the alert -- and every re-fire -- silently queues on a dead run path with no signal, and unlike an inert todo row a firing alert keeps retriggering while looking handled (BLO-26613, same class of gap as the Dependabot alert path fixed in #1344; confirmed live via BLO-26609/BLO-26610/BLO-26611). Add resolveInvokableAssigneeAgentId (mirrors server/src/services/dependabot-alert-issues.ts's resolveDependabotIssueAssigneeId): looks up the resolved agent via ctx.agents.get and @paperclipai/shared's isAgentStatusInvokable, falling back to unassigned (so allow_company_agent lets any agent pick it up) and logging distinctly when it isn't. Wired into handleFiring's issue-creation path only -- re-fires against an already-created issue are unaffected. New regression test in owner-resolver.test.ts confirmed red against unpatched HEAD, green after the fix. Full plugin suite (179 tests) passes; tsc clean. https://paperclip.blockcast.net/BLO/issues/BLO-26613 Co-Authored-By: Paperclip <noreply@paperclip.ing>
1 similar comment
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: 803a8a4
The core change is sound and the helper semantics are correct — isAgentStatusInvokable treats paused as non-invokable while idle/active/running/error pass (packages/shared/src/agent-eligibility.ts:59-69), which is exactly the BLO-26613 gap. Two issues below concern what happens around the new call rather than the check itself.
Critical Issues (0)
Important Issues (2)
-
[gstack/review, native-codex]
packages/plugins/paperclip-plugin-alertmanager/src/owner-resolver.ts:192— the newctx.agents.getcall is unguarded, adding a hard RPC dependency to the alert-issue creation path with no degraded fallback.- The sibling resolver 60 lines above does the opposite:
resolveOwnerUserIdwrapsctx.users.findByEmailintry/catchand degrades toundefined(owner-resolver.ts:129-137). The two owner-resolution helpers in the same file now have opposite failure semantics, and the new docstring doesn't say which is intended. - Concretely, if the agents RPC is unavailable, the throw is caught per-alert at
webhook-handler.ts:638, the fingerprint is pushed tofailedFingerprints, andAlertDeliveryIncompleteErrorfails the delivery so Alertmanager retries. That retry path is well-built, but if the RPC stays down through Alertmanager's retry budget the alert is lost with no issue row at all — strictly worse than the paused-assignee bug this PR fixes, where at least a visible issue existed. - Recommendation: wrap the lookup and return
undefinedon error. That degrades to unassigned — precisely the fallback this PR already argues for — instead of trading a silent-assignment failure for a silent-loss failure. If failing the delivery is the deliberate choice, say so in the docstring so the asymmetry withresolveOwnerUserIdis intentional rather than incidental.
- The sibling resolver 60 lines above does the opposite:
-
[pr-review-toolkit/code]
packages/plugins/paperclip-plugin-alertmanager/src/webhook-handler.ts:308—createAssigneeUserIdis derived from the pre-guardresolvedAssigneeAgentId, so when the guard drops a non-invokable agent it silently discards an already-resolved human owner too, and the issue is filed with no assignee of either kind.- Repro path:
ownerOverride === false, the issue-route supplies anassigneeAgentIdfor a paused agent, and the owner-map separately resolves an email to a realassigneeUserId. Line 305 makesresolvedAssigneeAgentIdtruthy → line 308 forcescreateAssigneeUserId = undefined→ line 317 nulls the agent → lines 356-357 spread neither key, so the issue lands fully unassigned. - The same applies when the route itself carries both
assigneeAgentIdandassigneeUserId: the route's own human fallback is dropped. - This isn't a regression (the old code assigned the paused agent and dropped the user as well), but it is an incomplete fix — a resolvable human owner is a better landing spot than
allow_company_agentpickup, and the PR's stated goal is getting the alert in front of someone who will act. - Recommendation: move the guard above the user derivation and key it off the post-guard id (
const createAssigneeUserId = createAssigneeAgentId ? undefined : …). TheownerOverridebranch is unaffected —resolveAssigneeUserIdalready returns email and agent mutually exclusively (owner-resolver.ts:155-168) — so the reorder only adds the fallback in the route case.
- Repro path:
Suggestions (3)
- [pr-review-toolkit/comments]
packages/plugins/paperclip-plugin-alertmanager/src/owner-resolver.ts:176-177— the docstring citesserver/src/services/dependabot-alert-issues.tsresolveDependabotIssueAssigneeIdas the mirrored source, but that symbol does not exist at this head; #1344 is still open, and the file's only exports arefindOpenDependabotAlertIssue,recordDependabotWebhookDiagnostic, andresolveStaleDependabotAlertWakeIssue. The PR body's "the sibling #1344 already fixed the analogous Dependabot alert path" reads the same way. Suggest "mirrors the approach proposed in #1344" until it merges, so someone grepping for the symbol isn't chasing a ghost. - [native-codex] The guard runs only on the first-seen-fingerprint branch, so an agent paused after its issue was created keeps collecting re-fires against the stored
existing.assigneeAgentId. The scope call is reasonable and stated in the PR body, but BLO-26613's symptom explicitly includes "keeps re-triggering while looking handled", so the re-fire path is worth a follow-up issue rather than leaving it implicit. - [pr-review-toolkit/tests]
packages/plugins/paperclip-plugin-alertmanager/src/__tests__/owner-resolver.test.ts:447— the six new cases cover status outcomes well, but there's no case whereagents.getrejects. Adding one would pin down whichever failure semantics you pick for the first Important finding.
Strengths
- The regression test was verified as a real one — stashing only the source changes and rerunning produced 6 failures. That's the check most PRs skip.
- Mutual exclusivity of email vs. agent resolution is preserved; the guard is inserted without disturbing the existing precedence chain.
agents.readwas genuinely already declared (src/manifest.ts:29), so the "no manifest change needed" claim checks out.- Pre-existing ctx fixtures were updated with an explanatory comment about why the default is invokable, rather than silently stubbed.
- Scope discipline: the generic
assertAssignableAgentpath is deliberately left alone, with the blast-radius reasoning stated.
CI note
Both red checks are infrastructure, not this change: General tests (workspaces-b) failed at the Checkout repository step, and Typecheck + Release Registry has no failed step at all — both logs end in The runner has received a shutdown signal / The operation was canceled. The pnpm "Failed to create bin … dev-cli.js" warnings are pre-existing plugin build-order noise across every plugin package, unrelated to this diff. These need a re-run, not a code fix.
Recommended Action
- No Critical issues — nothing blocks on correctness of the invokability check itself.
- Address both Important issues this cycle: guard the
agents.getcall so an RPC outage degrades to unassigned rather than risking alert loss, and reorder the user-fallback derivation so a resolved human owner survives the agent being dropped. - Consider the Suggestions opportunistically; the docstring reference to the unmerged #1344 symbol is the cheapest of them to fix.
- Re-run the two cancelled CI jobs.
Thinking Path
Linked Issues or Issue Description
Refs: BLO-26613 — "Issue creation performs no assignee-invokability check". That issue's AC calls out
paperclip-plugin-alertmanager's assignee resolution (webhook-handler.ts) as a confirmed-live instance of the same gap already fixed for Dependabot in #1344. Searched open/recent PRs foralertmanager+invokab+BLO-26613— no duplicate or overlapping PR found.What Changed
resolveInvokableAssigneeAgentIdinowner-resolver.ts— looks up the resolved agent viactx.agents.getand gates on@paperclipai/shared'sisAgentStatusInvokable(mirrorsserver/src/services/dependabot-alert-issues.ts'sresolveDependabotIssueAssigneeId). Falls back toundefined(unassigned) and logs a distinct warning when the agent is missing or not invokable.webhook-handler.ts'shandleFiringissue-creation path (first-time-seen-fingerprint branch only — re-fires against an already-created issue don't re-resolve or reassign).agents.readwas already a declared plugin capability; no manifest change needed.owner-resolver.test.ts(6 new cases: invokable pass-through, paused fallback, agent-not-found fallback, terminated/pending_approval fallback, no-agentId short-circuit).worker.test.tsandescalation.test.tsctx mocks to stubctx.agents.get(default: invokable), since those fixtures predate the guard and would otherwise throw on the new call.Verification
pnpm exec vitest runinpackages/plugins/paperclip-plugin-alertmanager— 179/179 pass.TypeError: ... is not a function/ assignment not gated), matching "must fail against current HEAD". Restored the fix, reran green.pnpm exec tsc --noEmitin the plugin package — clean.Risks
Low. Scoped to one plugin's issue-creation path; re-fire/resolve paths and every other assignee-resolution branch (users, routes without an agent target) are untouched. Worst case on a bug here is an alertmanager issue landing unassigned instead of on its configured owner — recoverable via
allow_company_agent, not a silent failure. Does not touch the genericassertAssignableAgent/issueService.create()check (deliberately out of scope, same as #1344 — tightening "assignable" company-wide needs a separate blast-radius review).Model Used
Claude Sonnet 5 (claude-sonnet-5), extended reasoning, via Claude Code — tool use (repo clone, vitest, tsc) to implement and verify.
Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue templatehttps://paperclip.blockcast.net/BLO/issues/BLO-26613