Skip to content

feat(chat): stop an in-flight Author or Plan and Execute run - #40

Merged
HackedRico merged 9 commits into
mitre:mainfrom
HackedRico:fix/stop-chat
Sep 1, 2026
Merged

feat(chat): stop an in-flight Author or Plan and Execute run#40
HackedRico merged 9 commits into
mitre:mainfrom
HackedRico:fix/stop-chat

Conversation

@HackedRico

Copy link
Copy Markdown
Collaborator

Closes #34.

Adds a Stop button for a running Author or Plan and Execute chat run, plus two defects found while building it.

Stop lives in the header status badge, which now reads Working / Stop. New POST /plugin/mcp/cancel stops the task owning a run id; the run reaches KILLED once the workflow unwinds, and History labels it STOPPED. Stop works before /execute has returned an id (the cancel is queued and sent when it arrives), and a boot sweep reconciles runs left RUNNING by a dead process.

Stop unwinds this plugin's agent loop. It does not roll back a CALDERA operation the agent already launched; that is halted from the Operations view. The stopped bubble says so.

Two fixes:

  • The stage line never moved. Workflows tag stages to MLflow, but /status serves the in-memory cache, which got a stage twice per run. The UI read "initializing" for the whole run. Stage tags are now mirrored into the snapshot, keyed by run id.
  • A stopped run was recorded as a crash. Cancelling tears down the MCP stdio transport, which anyio surfaces as an ExceptionGroup with no CancelledError leaf, so the workflow ran its failure path and wrote error and traceback params. Those are immutable, so History showed a deliberate stop as an error. Workflows now let the orchestrator classify a run it owns.

Known limitation: New chat mid-run followed by navigating away leaves a run the GUI cannot cancel. History has no cancel action yet.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

How Has This Been Tested?

451 tests pass, 29 of them new: test_run_cancellation.py (16), test_run_stage_progress.py (7), test_workflow_failure_ownership.py (6).

Every fix was checked by reverting it and confirming the relevant tests fail, so none pass tautologically.

Verified in a running CALDERA: Stop reaches the stopped bubble, the stage line shows real workflow stages, and the header count excludes killed runs. Confirmed in MLflow that runs stopped before the fix carry spurious error params and runs stopped after are clean.

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works

Keep each run's task handle so POST /plugin/mcp/cancel can name one. The
run reaches KILLED in the cache and in MLflow, which the poller treats as
terminal and History labels as stopped. Covers the anyio exception-group
shape and a cancel landing before the task body runs, both of which left
the run RUNNING forever.

Closes mitre#34
/execute mints the MLflow run before it answers, so the id can be
seconds away. Queue the cancel and send it once the id arrives.
The bubble heading already says the run stopped, so the note below it
only has to say what survives the cancel.
The run belongs to that bubble, so the control that ends it does too.
The header keeps the Working badge; ChatTranscript only relays state
and the stop request between the run and the message that owns it.
Workflows report progress via tracker.set_tag("stage", ...), which reaches
MLflow only, while /status serves the run cache. The cache got a stage
twice per run, so the UI read "initializing" throughout and ChatLoadingState
never had a second value to dwell on.

Observers are keyed by run id because a workflow binds its own tracker to
the run the service started.
The bubble button lived inside the only scroll container, and nothing
brought it back: the re-scroll gate needs 200px and the unconditional
watcher keys on messages.length, frozen for the whole run. Scrolling up to
re-read an answer lost the only Stop in the plugin, with the scrollbar
hidden so nothing hinted it was there.

One pinned element now carries both status and action, so they cannot
disagree, and it matches how magma halts an operation. Also stops counting
a killed run as a delivered response, and keeps a live run's badge after
New chat, which used to strand a run nothing in the GUI could cancel.
Stopping a run tears down the MCP stdio transport, and anyio surfaces that
as an ExceptionGroup wrapping BrokenResourceError with no CancelledError
leaf. The workflow's except Exception ran full failure bookkeeping before
re-raising: a traceback that read as a server fault, and error/traceback
params that are immutable, so the service's later KILLED tags could not
take them back and History showed a deliberate stop as an error.

A workflow handed a run by the orchestrator now re-raises and lets the
orchestrator classify; only one that minted its own run writes that run's
failure state. The traceback param moves to the service, the layer that
records cancel intent and can tell the two apart.

Also guards _recordRunHandle on its message still existing: keeping a live
run past New chat left the generation guard disarmed, so a POST resolving
after the wipe adopted the abandoned run's session_id into the fresh chat.
The header allow-listed FINISHED while ChatMessage rendered a result for
anything that was not RUNNING, FAILED or KILLED. A snapshot arriving with
no status becomes 'unknown', which drew an answer the header would not
count; a message with no role disagreed the same way.

Both now read isDeliveredResponse, and an assistant status with no branch
of its own falls to an explicit no-result case instead of silently
rendering as a delivered answer.
…ncel

Two findings from reviewing the branch.

Workflows stopped printing the traceback for a run the orchestrator owns,
and the service logged only a one-line summary. The MLflow traceback param
is best-effort, so a crash with a dead tracking store left an operator one
line and no stack. Log it with exc_info.

_sendCancel cleared the shared stopping flag from its catch without
checking the run was still current, so a cancel that rejected only after a
newer run started flipped that run's badge back to Working mid-stop.
Guarded like every other post-await write in the composable.
@HackedRico
HackedRico merged commit 01e160c into mitre:main Sep 1, 2026
3 checks passed
@HackedRico
HackedRico deleted the fix/stop-chat branch September 1, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add the ability to stop an in-flight workflow run

1 participant