Webhook: assignee-change pivot creates duplicate implement dispatches #92
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks#92
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
User story
As the operator, I want to re-trigger
implementon an already-assigned issue without the previous-assignee's worker also picking up a stale dispatch — so that one ticket produces one PR, not two.What happened (concrete)
Operating pattern for re-dispatch: re-assign the issue to a different agent then back to the intended one ("assignee-pivot"). Forgejo fires two
issues.assignedevents — one per step — and the webhook dispatches both:charles/claude-hooks#73assigned todev→ dev's worker enqueuesimplement.boss→ boss's worker enqueuesimplement.Both agents run to completion, both push a branch, both open a PR — e.g. today:
This happened three separate times today. Pattern is reliable and has now cost non-trivial amounts of operator cleanup + agent compute.
Why today's workaround exists
It exists because the webhook only re-fires an implement dispatch on a fresh
issues.assignedevent. Re-posting the same assignee doesn't refire (Forgejo de-dupes). Re-opening the issue firesissues.reopened— not routed. Label-flipping only works forarea:design. So operators pivot to trigger a new event.Proposed shape
Pick one of:
A — honour the current assignee, not the event's
handleIssueAssigned(issue, agentFromEvent)today trusts the webhook's payload. Change it to refetchissue.assigneesafter dedupe and only dispatch if the current assignee still matches the event'sagent. Intermediate pivots clear before the second assignment actually fires (or the first firing gets skipped because the current assignee is no longerdev). Race-prone but small.B — short-lived dedupe on
(repo, issue, agent)assignmentInside
webhook-handlers.ts, remember the last-dispatched(repo, issue, agent)tuple for ~30 s. If the same tuple fires again within that window, drop it silently. If a different agent fires in the same window, drop the first-in-flight task's worker-queue entry (if still queued) and let the new one run. More robust than A; one extra Map entry plus a timer.C — explicit re-dispatch endpoint
POST /redispatch {repo, issue, agent, skill?}that bypasses the webhook. Operators use this instead of the pivot trick. Clean conceptually, but adds an endpoint and changes the operator's muscle memory.Recommend B: it fixes the observed bug without needing operator behaviour change, and the dedupe window is small enough that a real back-to-back reassignment (operator changes their mind twice) still lands the second agent.
Acceptance criteria
Core
webhook-handlers.tskeyed by${repo}#${issue}@${agent}; TTL 30 s; entry stamp is the last-dispatched timestamp.handleIssueAssignedrejects a dispatch if the tuple is already in the map within TTL.Tests
webhook-handlers.test.ts: twoissues.assignedevents for the same(repo, issue, agent)within 30 s → exactly one dispatch.webhook-handlers.test.ts:issues.assigned → dev,issues.assigned → bosswithin 30 s, dev's task still queued → dev's task is removed, boss dispatched.webhook-handlers.test.ts:issues.assigned → dev,issues.assigned → bosswithin 30 s, dev's task already running → both run (logged).Dashboard visibility
Out of scope
/redispatchexists; this ticket's scope is making the pivot safe.References
src/webhook-handlers.ts:handleIssueAssigned— where the guard lands.src/webhook.ts:172— comment already anticipates this class of double-firing for label routes; extend to assignee routes.Dependencies
main.