fix(webhook): dedupe assign-events to prevent pivot double-dispatch #93
No reviewers
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!93
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "dev/92"
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?
Summary
handleIssueAssignedkeyed by${repo}#${issue}@${agent}to drop duplicate assign events for the same tuplewebhook-assign-dedup.test.tscovers all three acceptance-criteria scenarios: same-agent dedup, cross-agent queue cancellation, and cross-agent running-task pass-throughCloses #92
Introduces a 30 s dedup guard in handleIssueAssigned keyed by `${repo}#${issue}@${agent}`. Duplicate fires for the same tuple are dropped silently. When a different agent fires for the same issue within the TTL window, the previous agent's still-queued task is removed from the queue; if that task is already running, both agents continue (logged). Closes #92 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>Review
CI is green ✅. Logic is correct and all three acceptance-criteria scenarios are covered by the new tests. One real issue before merge:
_lastIssueDispatchis an unboundedMap— memory leakFile:
src/webhook-handlers.tsThis plain
Maphas no eviction: every issue that ever triggershandleIssueAssignedadds an entry that is never removed in production (resetAssignDedup()only clears it in tests). In a long-running service this leaks memory without bound.Everywhere else in this file the same pattern uses
BoundedMap—_assignDedupuses it,_postMergeRebaseDispatcheduses it. This map should too.Fix: replace the plain
Mapwith aBoundedMapusing the same TTL:No other changes needed — the TTL guard inside
cancelStaleIssueTaskremains correct (BoundedMap may not evict on reads, so the manual check is good belt-and-suspenders).Note (non-blocking)
The acceptance criteria in issue #92 names
webhook-handlers.test.tsas the target test file, but the tests landed inwebhook-assign-dedup.test.ts. The scenarios are all covered and Bun picks up both files, so this is functionally fine — just a minor deviation from the spec wording.Fixed in
96d9769. Replaced the plainMapwithBoundedMap<string, LastDispatch>(500, ASSIGN_DEDUP_TTL_MS)— consistent with_assignDedupand_postMergeRebaseDispatched. Also updatedresetAssignDedup()to use the iterate-and-delete pattern instead of.clear()sinceBoundedMapdoesn't expose that method.Non-blocking note acknowledged — test file name deviates from the spec wording but all scenarios are covered and Bun picks up both files.
Review: APPROVED ✅
CI is green (run #1623, 2m47s,
success). All three acceptance criteria from issue #92 are met. Code is correct and clean.Acceptance criteria verification
webhook-handlers.tskeyed by${repo}#${issue}@${agent}; TTL 30 s_assignDedupBoundedMap inwebhook-handlers.tshandleIssueAssignedrejects dispatch if tuple already in map within TTLisDupAssign()guard, returnsnullcancelStaleIssueTask()splices fromprevWorker.queue; else logsTest coverage
All five tests pass and cover every scenario specified in the issue: same-agent dedup, cross-agent queue cancellation, cross-agent running-task pass-through, single clean assignment, and unconfigured-agent no-op. Tests are in
webhook-assign-dedup.test.ts(a dedicated file is a fine choice; the issue reference towebhook-handlers.test.tswas directional, not prescriptive).Memory safety
The final commit switches both
_assignDedupand_lastIssueDispatchfrom plainMaptoBoundedMap(500, TTL), which caps entries at 500 and auto-evicts stale ones on every write. The manual TTL checks inisDupAssignandcancelStaleIssueTaskare correct belt-and-suspenders guards for lazy-eviction scenarios.Minor observation (non-blocking)
resetAssignDedup()clears both maps by iterating withfor...ofand calling.delete(k). This is safe in JS, but ifBoundedMapexposes a.clear()method it would be simpler. Not blocking — test-only code, CI is green.No logic bugs, no safety issues, no scope creep. Ship it.