feat(monitor): one-click re-dispatch for failed / cancelled / interrupted tasks #222

Closed
opened 2026-04-21 12:02:43 +00:00 by claude-desktop · 0 comments
Collaborator

User story

As an operator, I want a Re-dispatch button on every task-history row whose status is terminal-but-not-successful (failed, cancelled, interrupted), so that recovering from an aborted run is a single click instead of the Forgejo unassign → reassign dance I had to do for #209 / #210 on 2026-04-21.

Acceptance criteria

Server

  • POST /task/:id/redispatch (auth-gated via the M18-8 middleware). Reads the original task_history row, reuses repo + issue_number + agent_type + user, generates a fresh task_id, and enqueues it through the same pool scheduler path an issues.assigned webhook would use. Returns 202 { task_id } on success; 409 Conflict if the original is still running; 404 if the task_id is unknown.
  • The new task chain-resumes the original session if sessions.json still has an entry for the <type>:<repo>:<issue> key — same resume semantics as a natural re-dispatch.

UI

  • On /app/monitor/tasks (the Tasks table) — a ↻ Re-dispatch icon button appears on rows with status ∈ {failed, cancelled, interrupted}. Button is disabled (with a tooltip "task is running") when the row's latest status is running.
  • On /app/monitor/task/:taskId (the task detail view) — same button in the top-right actions bar.
  • Click fires POST /task/:id/redispatch, shows a toast on success with a link to the new task detail, rolls back the optimistic state on non-2xx.
  • After success, the row visually updates to reflect the new dispatch (the new task becomes the "active" entry for that issue).

Safety rails

  • Re-dispatching a task whose issue has since been closed returns 410 Gone with a body hint to reopen the issue first. Avoids spawning a worker on stale state.
  • Re-dispatching an interrupted task that still has uncommitted worktree content is fine — the worker's existing "worktree has uncommitted changes from a previous dispatch" warning covers it.

Verification

  • Unit test in apps/server/src/main.test.ts for the new endpoint.
  • Playwright smoke in apps/web/e2e/monitor.spec.ts — mock a failed task row, click the re-dispatch button, assert POST fires and a toast appears.
  • Manual: kill a task via /cancel, click Re-dispatch from Monitor, confirm a new dispatch fires (check service logs for [dev-*] starting <new task id>).

Out of scope

  • Bulk re-dispatch (select multiple, re-dispatch all) — nice later, file a follow-up if it comes up.
  • Re-dispatching to a different agent type than the original — this is the "reroute" use case, stays a Forgejo unassign + reassign action.
  • Mid-flight steering of a still-running task — covered by the log-page redesign ticket.

References

  • apps/server/src/task-store.tsTaskRecord shape; listTasksForIssue already fetches by issue.
  • apps/server/src/pool.ts::dispatchByType — the path a new dispatch should take.
  • apps/server/src/webhook-handlers.ts::handleIssueAssigned — template for how to build the task envelope from an issue + type.
  • 2026-04-21 incident: #209 / #210 had to be recovered via manual unassign + reassign via the Forgejo API. This button makes that two clicks.
## User story As an operator, I want a **Re-dispatch** button on every task-history row whose status is terminal-but-not-successful (`failed`, `cancelled`, `interrupted`), so that recovering from an aborted run is a single click instead of the Forgejo unassign → reassign dance I had to do for #209 / #210 on 2026-04-21. ## Acceptance criteria ### Server - [ ] `POST /task/:id/redispatch` (auth-gated via the M18-8 middleware). Reads the original `task_history` row, reuses `repo` + `issue_number` + `agent_type` + `user`, generates a fresh `task_id`, and enqueues it through the same pool scheduler path an `issues.assigned` webhook would use. Returns `202 { task_id }` on success; `409 Conflict` if the original is still `running`; `404` if the `task_id` is unknown. - [ ] The new task chain-resumes the original session if `sessions.json` still has an entry for the `<type>:<repo>:<issue>` key — same resume semantics as a natural re-dispatch. ### UI - [ ] On `/app/monitor/tasks` (the Tasks table) — a **↻ Re-dispatch** icon button appears on rows with `status` ∈ {`failed`, `cancelled`, `interrupted`}. Button is disabled (with a tooltip "task is running") when the row's latest status is `running`. - [ ] On `/app/monitor/task/:taskId` (the task detail view) — same button in the top-right actions bar. - [ ] Click fires `POST /task/:id/redispatch`, shows a toast on success with a link to the new task detail, rolls back the optimistic state on non-2xx. - [ ] After success, the row visually updates to reflect the new dispatch (the new task becomes the "active" entry for that issue). ### Safety rails - [ ] Re-dispatching a task whose issue has since been closed returns `410 Gone` with a body hint to reopen the issue first. Avoids spawning a worker on stale state. - [ ] Re-dispatching an `interrupted` task that still has uncommitted worktree content is fine — the worker's existing "worktree has uncommitted changes from a previous dispatch" warning covers it. ### Verification - [ ] Unit test in `apps/server/src/main.test.ts` for the new endpoint. - [ ] Playwright smoke in `apps/web/e2e/monitor.spec.ts` — mock a `failed` task row, click the re-dispatch button, assert POST fires and a toast appears. - [ ] Manual: kill a task via `/cancel`, click Re-dispatch from Monitor, confirm a new dispatch fires (check service logs for `[dev-*] starting <new task id>`). ## Out of scope - Bulk re-dispatch (select multiple, re-dispatch all) — nice later, file a follow-up if it comes up. - Re-dispatching to a different agent type than the original — this is the "reroute" use case, stays a Forgejo unassign + reassign action. - Mid-flight steering of a still-running task — covered by the log-page redesign ticket. ## References - `apps/server/src/task-store.ts` — `TaskRecord` shape; `listTasksForIssue` already fetches by issue. - `apps/server/src/pool.ts::dispatchByType` — the path a new dispatch should take. - `apps/server/src/webhook-handlers.ts::handleIssueAssigned` — template for how to build the task envelope from an issue + type. - 2026-04-21 incident: #209 / #210 had to be recovered via manual unassign + reassign via the Forgejo API. This button makes that two clicks.
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#222
No description provided.