Skip to main content
← Back to list
01Issue
BugShippedSwamp CLIPublic
Assigneesstack72

Relationships

#1738 Cancel endpoint reports status: cancelled for runs it did not actually cancel

Opened by hammz · 8/19/2026· Shipped 8/20/2026

Problem

POST /api/v1/cancel/(workflow-run|method-run)/<id> returns {"status":"cancelled"} as soon as an AbortController is aborted. Aborting is not cancelling: for in-process runs the abort is purely cooperative, so a hung or stuck run keeps executing while the caller is told it stopped. There is no later correction — the response is the only signal the client ever gets.

swamp workflow cancel --run <id> --server then prints Cancelled run <id> on server for a run that is still going.

Why the abort does not stop a stuck run

  • RunCancelRegistry.cancel and ActiveRunRegistry.cancel only call controller.abort(...) (src/serve/run_cancel_registry.ts:63-72, src/serve/active_run_registry.ts:141-146).
  • The workflow engine only tests options.signal.aborted at step boundaries (src/domain/workflows/execution_service.ts:1918, 2317, 2361). A step that is already stuck never reaches one.
  • Extension methods execute in-process via InProcessExecutor — same process as swamp serve. The signal is handed to the method context (src/libswamp/models/run.ts:744) but there is no subprocess to kill and no race against the signal, so a method that ignores ctx.signal (infinite loop, blocking call, fetch without the signal) runs to completion.

Consequences beyond the wrong status: the run stays in the registry, and because the finally that calls flushLocks() never runs, it keeps holding its model locks. The existing backstops do not help — --max-run-duration fires the same cooperative abort (src/serve/active_run_registry.ts:100-108), and stale-run reaping only rewrites tracker records. Restarting serve is the only way out.

Contrast with the two paths that do work:

  • Local swamp workflow cancel / swamp model cancel use killProcessTree — SIGTERM, 2s poll, then SIGKILL to the process and its children (src/infrastructure/process/process_kill.ts:120-142).
  • Worker-dispatched steps send runner.cancel and then unconditionally child.kill() after RUNNER_CANCEL_GRACE_MS (src/worker/dispatch_handler.ts:258-274), which works because the runner is a separate process.

Steps to reproduce

  1. Start swamp serve.
  2. Run a workflow whose step calls a model method that ignores ctx.signal (e.g. a loop with a plain await new Promise(r => setTimeout(r, 1000))).
  3. POST /api/v1/cancel/workflow-run/<runId> (or swamp workflow cancel --run <id> --server).

Actual: HTTP 200 {"status":"cancelled"}; the run keeps emitting events, stays in GET /api/v1/health activeRuns, and holds its locks. Expected: the response distinguishes "abort delivered" from "run stopped".

Suggested direction

Return a status that means what happened — e.g. cancellation_requested — and let the caller confirm termination (poll the active-run list, or have the endpoint wait a bounded grace period for the run to leave the registry before answering). A force path matching the worker's grace-then-kill behaviour would need out-of-process execution for in-process runs, so it is a larger change; at minimum the reported status should stop claiming a cancellation that did not happen.

Found while triaging #1536 (adding --server support to swamp model cancel), which would inherit this same misreporting.

02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 2 MOREREVIEW+ 4 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

8/20/2026, 1:07:10 AM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack728/19/2026, 11:07:26 PM
Editable. Press Enter to edit.

stack72 commented 8/20/2026, 1:07:18 AM

Thanks @hammz for reporting this! The fix has been merged and a release is on its way. We appreciate your contribution to swamp.

Sign in to post a ripple.