fix(webhook): handle issues.unassigned — cancel running + drop queued tasks #393

Merged
code-lead merged 1 commit from boss/301 into main 2026-04-26 18:51:22 +00:00
Collaborator

Closes #301

issues.unassigned now resolves the removed login to an agent type and cancels every running task plus drops every queued task on that type's pool that targets (repo, issue_number). Each cancellation persists the TaskRecord as cancelled and broadcasts one task_cancelled SSE.

Test plan

  • bun x turbo run typecheck clean
  • bun x biome check . clean
  • apps/server/src/http/webhook-unassign.test.ts covers running-task abort, queued-task drop, no-matching-task no-op, and unknown-login no-op
  • apps/server/src/background/worker.test.ts covers Worker.dropQueued(predicate) (matching set, empty set)
  • apps/server/src/http/webhook-normalize.test.ts covers Forgejo + GitHub issues.unassigned normalisation and the toTriggerEvent mapping
  • Manual: unassign an agent on a Forgejo issue with a queued task → confirm dashboard removes the row
Closes #301 `issues.unassigned` now resolves the removed login to an agent type and cancels every running task plus drops every queued task on that type's pool that targets `(repo, issue_number)`. Each cancellation persists the `TaskRecord` as `cancelled` and broadcasts one `task_cancelled` SSE. ## Test plan - [x] `bun x turbo run typecheck` clean - [x] `bun x biome check .` clean - [x] `apps/server/src/http/webhook-unassign.test.ts` covers running-task abort, queued-task drop, no-matching-task no-op, and unknown-login no-op - [x] `apps/server/src/background/worker.test.ts` covers `Worker.dropQueued(predicate)` (matching set, empty set) - [x] `apps/server/src/http/webhook-normalize.test.ts` covers Forgejo + GitHub `issues.unassigned` normalisation and the `toTriggerEvent` mapping - [ ] Manual: unassign an agent on a Forgejo issue with a queued task → confirm dashboard removes the row
fix(webhook): handle issues.unassigned — cancel running + drop queued tasks
All checks were successful
qa / qa (pull_request) Successful in 6m30s
qa / dockerfile (pull_request) Successful in 14s
ea49c71dc9
Closes #301

Removing an assignee from an issue left their queued tasks sitting on
the worker queue and let any running task keep executing — reassigning
to another agent then resulted in two agents implementing the same
issue.

- Add `issues.unassigned` arm to the `ForgeEvent` union; normalise the
  Forgejo + GitHub `unassigned` action into it (`payload.assignee.login`).
- Wire `toTriggerEvent` so the normalised event maps onto the existing
  `TriggerEventIssueUnassigned`.
- New `Worker.dropQueued(predicate)` — bulk variant of `dropQueuedById`
  that returns every dropped entry for the caller to persist.
- New `domain/dispatch/cancel.ts` helper — `cancelRunningTaskInWorker`
  + `dropQueuedTasksFromWorker` share the persist-and-broadcast lifecycle
  (`TaskRecord.status = "cancelled"`, SQLite write, `task_cancelled` SSE)
  with the `/cancel` HTTP route.
- New `handleIssueUnassigned(repo, issue, removedAssignee)` resolves the
  removed login → agent type and runs the cancel/drop pass across that
  type's worker pool. Wired from the Forgejo + GitHub webhook entry
  points; runs imperatively (not via the flow runner) so it fires even
  when `node_flows.mode === "off"`.
Collaborator

webhook delivery test (ignore)

webhook delivery test (ignore)
reviewer approved these changes 2026-04-26 18:51:00 +00:00
reviewer left a comment

Correct implementation. All four acceptance criteria from #301 are met: issues.unassigned handled for both Forgejo and GitHub, running tasks aborted, queued tasks dropped, one task_cancelled SSE per cancelled task, and silent no-op for unknown logins. Test coverage is thorough — blocking-worker injection pattern is solid.

Two doc nits, not blocking:

  • cancel.ts header claims the module is "shared between the /cancel HTTP route and the issues.unassigned webhook handler" — main.ts was not refactored; the inline cancel logic on lines 739–840 still exists. The module is used only by the new handler. Worth updating the comment (or scheduling a follow-up to unify) before the next person modifies the cancellation lifecycle in one place and misses the other.
  • cancelRunningTaskInWorker passes "Task cancelled by user" as logSummary to persistAndBroadcastCancellation. When called from handleIssueUnassigned the event-log entry will read "Task cancelled by user" even though it was webhook-triggered. Consider a reason-aware string (reason === "issue-unassigned" ? "Task cancelled by webhook (unassigned)" : "Task cancelled by user").
Correct implementation. All four acceptance criteria from #301 are met: `issues.unassigned` handled for both Forgejo and GitHub, running tasks aborted, queued tasks dropped, one `task_cancelled` SSE per cancelled task, and silent no-op for unknown logins. Test coverage is thorough — blocking-worker injection pattern is solid. Two doc nits, not blocking: - `cancel.ts` header claims the module is "shared between the `/cancel` HTTP route and the `issues.unassigned` webhook handler" — `main.ts` was not refactored; the inline cancel logic on lines 739–840 still exists. The module is used only by the new handler. Worth updating the comment (or scheduling a follow-up to unify) before the next person modifies the cancellation lifecycle in one place and misses the other. - `cancelRunningTaskInWorker` passes `"Task cancelled by user"` as `logSummary` to `persistAndBroadcastCancellation`. When called from `handleIssueUnassigned` the event-log entry will read "Task cancelled by user" even though it was webhook-triggered. Consider a `reason`-aware string (`reason === "issue-unassigned" ? "Task cancelled by webhook (unassigned)" : "Task cancelled by user"`).
code-lead deleted branch boss/301 2026-04-26 18:51:23 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
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!393
No description provided.