fix(webhook): handle issues.unassigned — cancel running + drop queued tasks for removed assignee #301

Closed
opened 2026-04-23 23:39:52 +00:00 by claude-desktop · 0 comments
Collaborator

User story

As an operator, I want unassigning an agent from a Forgejo issue to cancel that agent's running task and drop its queued tasks for that issue, so that stale work does not keep executing after I've reassigned the issue to someone else.

Current behaviour

apps/server/src/http/webhook.ts:160-212 handles issues.assigned, labeled, label_updated, closed — but not unassigned. Today:

  1. Operator unassigns agent A from issue #N on Forgejo.
  2. A's queued task for #N keeps sitting in A's queue.
  3. Reassign to agent B → B also dispatches for #N.
  4. Both A and B run (or A finishes, then B starts) on the same issue — duplicate work, wasted quota.

Running tasks have the same problem: A keeps executing even after unassignment, because /cancel is never called.

Acceptance criteria

Webhook

  • Add issues.unassigned branch alongside assigned in webhook.ts. Payload carries the removed assignee at payload.assignee.login (Forgejo v15 fires one event per removed user, same shape as assigned).
  • New handleIssueUnassigned(repo, issue, removedAssignee) in webhook-handlers.ts:
    • Resolve the removed Forgejo login to an agent-type (reuse the routing that already maps forgejo_user → type).
    • For every running worker of that type whose currentTask.request.issue_number === issue.number && .repo === repo: call the existing abort path (same one /cancel uses).
    • For every queued task in that type's workers whose request.issue_number === issue.number && .repo === repo: drop from the queue and mark the TaskRecord as cancelled with finished_at = Date.now().
    • Emit a single task_cancelled SSE event per cancelled/dropped task so the dashboard refreshes.

Worker

  • worker.ts exposes a dropQueued(predicate: (task) => boolean): TaskRecord[] method (returns dropped tasks for the handler to persist + SSE-broadcast). Keep it narrow — one predicate shape so tests don't explode.

Tests

  • webhook-handlers.test.ts: issues.unassigned with a running task on that issue → abort called, record status = cancelled.
  • webhook-handlers.test.ts: issues.unassigned with queued tasks only → queue shrinks, records persist as cancelled.
  • webhook-handlers.test.ts: unassigning an agent that has no running/queued task for that issue → silent no-op, no errors, no SSE noise.
  • worker.test.ts: dropQueued(predicate) removes matching entries, leaves non-matching intact, returns dropped tasks.

Out of scope

  • A UI "unassign" button — this ticket only closes the webhook hole. Manual cancel-from-UI is tracked separately.
  • Propagation to dependent issues (if the unassigned agent was also implicit on sibling tickets). Out of scope — one event, one scope.
  • pull_request.unassigned — PR flows use review-request, not assignee, for dispatch. Unless a follow-up proves otherwise.

References

  • apps/server/src/http/webhook.ts:160-212 — dispatch switch to extend
  • apps/server/src/http/webhook-handlers.tshandleIssueAssigned for the mirror-image pattern
  • apps/server/src/main.ts:685/cancel handler; share its abort path
  • apps/server/src/background/worker.ts — queue internals
  • Related: cancel-queued-from-UI ticket (sibling)
## User story As an operator, I want unassigning an agent from a Forgejo issue to cancel that agent's running task **and** drop its queued tasks for that issue, so that stale work does not keep executing after I've reassigned the issue to someone else. ## Current behaviour `apps/server/src/http/webhook.ts:160-212` handles `issues.assigned`, `labeled`, `label_updated`, `closed` — but **not** `unassigned`. Today: 1. Operator unassigns agent A from issue #N on Forgejo. 2. A's queued task for #N keeps sitting in A's queue. 3. Reassign to agent B → B also dispatches for #N. 4. Both A and B run (or A finishes, then B starts) on the same issue — duplicate work, wasted quota. Running tasks have the same problem: A keeps executing even after unassignment, because `/cancel` is never called. ## Acceptance criteria ### Webhook - [ ] Add `issues.unassigned` branch alongside `assigned` in `webhook.ts`. Payload carries the removed assignee at `payload.assignee.login` (Forgejo v15 fires one event per removed user, same shape as `assigned`). - [ ] New `handleIssueUnassigned(repo, issue, removedAssignee)` in `webhook-handlers.ts`: - Resolve the removed Forgejo login to an agent-type (reuse the routing that already maps `forgejo_user` → type). - For every running worker of that type whose `currentTask.request.issue_number === issue.number && .repo === repo`: call the existing abort path (same one `/cancel` uses). - For every queued task in that type's workers whose `request.issue_number === issue.number && .repo === repo`: drop from the queue and mark the `TaskRecord` as `cancelled` with `finished_at = Date.now()`. - Emit a single `task_cancelled` SSE event per cancelled/dropped task so the dashboard refreshes. ### Worker - [ ] `worker.ts` exposes a `dropQueued(predicate: (task) => boolean): TaskRecord[]` method (returns dropped tasks for the handler to persist + SSE-broadcast). Keep it narrow — one predicate shape so tests don't explode. ### Tests - [ ] `webhook-handlers.test.ts`: `issues.unassigned` with a running task on that issue → abort called, record status = `cancelled`. - [ ] `webhook-handlers.test.ts`: `issues.unassigned` with queued tasks only → queue shrinks, records persist as `cancelled`. - [ ] `webhook-handlers.test.ts`: unassigning an agent that has no running/queued task for that issue → silent no-op, no errors, no SSE noise. - [ ] `worker.test.ts`: `dropQueued(predicate)` removes matching entries, leaves non-matching intact, returns dropped tasks. ## Out of scope - A UI "unassign" button — this ticket only closes the webhook hole. Manual cancel-from-UI is tracked separately. - Propagation to dependent issues (if the unassigned agent was also implicit on sibling tickets). Out of scope — one event, one scope. - `pull_request.unassigned` — PR flows use review-request, not assignee, for dispatch. Unless a follow-up proves otherwise. ## References - `apps/server/src/http/webhook.ts:160-212` — dispatch switch to extend - `apps/server/src/http/webhook-handlers.ts` — `handleIssueAssigned` for the mirror-image pattern - `apps/server/src/main.ts:685` — `/cancel` handler; share its abort path - `apps/server/src/background/worker.ts` — queue internals - Related: cancel-queued-from-UI ticket (sibling)
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks#301
No description provided.