Conversation
… already ended
cancel_job and cancel_run kill the active generator's subprocess so
inference stops at once, but they did so whatever the job's status. For
a job that had already finished, failed or been cancelled, that
subprocess belongs to whatever is generating now, or holds the warm
model: cancelling a finished job (the CLI's `legacy cancel` /
`workflow-run cancel`, or pressing Cancel as a generation completes)
failed the other generation with "Subprocess died during generation" or
forced the next one to reload the model from scratch.
Only kill the subprocess when the job being cancelled was still pending
or running; cancelling an ended job keeps returning {"cancelled": true}.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Question: Im curious as if this also has an effect of the ability to free memory after a cancel/kill. Has anyone experience python separation from Modly processes when doing this? If so, that could be tied into this kind of fix. |
|
This change does not touch what happens on a real cancel. When the job is still pending or running, the worker is killed exactly as before, and the process exit is what returns its memory to the OS, so freeing memory after a cancel is unchanged. What changes is the case where the job has already ended: the cancel used to kill the worker anyway, which either broke a generation that was running for someone else or threw away the warm model. Now a cancel on a finished job only reports the job's state and leaves the worker, with the loaded model, in place. That is the same state the worker is in after any job completes normally, so the memory it holds is the warm-model memory that was already there, not something this PR keeps alive longer. If you have seen a worker's memory not being released after a kill, that would be a separate problem in the kill path itself, and worth its own issue with the platform and what |
What
Cancelling a generation job that has already ended still kills the model worker:
Subprocess died during generation.This is easy to hit from the headless surface:
python tools/modly-cli/agent.py legacy cancel <job_id>orworkflow-run cancel <run_id>on a job that finished, failed, or was already cancelled. It also happens in the app when Cancel is pressed just as a generation completes, because the poll loop posts the cancel on its next tick, after the backend has marked the jobdone.Why it triggers
cancel_job(/generate/cancel/{id}) and its siblingcancel_run(/workflow-runs/{id}/cancel) only update the status while the job is pending or running. The subprocess kill after that runs unconditionally:ExtensionProcesskeeps its subprocess alive between generations to hold the loaded model, sogen._proc.poll() is Noneis true for an idle worker too. For an ended job, that subprocess isn't running the job being cancelled.Fix
In both endpoints, return before the kill when the job has already ended. Cancelling a pending or running job is unchanged. The response stays
{"cancelled": true}in every case, so clients see no difference.Verification
New
CancelEndedJobTestsinapi/tests/test_workflow_runs_lifecycle.py, next to the existingcancel_runtest. The registry's active worker is a fake with a live_proc.test_cancelling_a_finished_job_leaves_the_active_worker_alone(cancel_job, statusdone): fails before, passes after.test_cancelling_a_failed_run_leaves_the_active_worker_alone(cancel_run, statuserror): fails before, passes after.test_cancelling_a_running_job_still_stops_the_workerandtest_cancelling_a_running_run_still_stops_the_worker: pass before and after. A running job is still markedcancelled, and its worker is still killed, detached and marked unloaded. That proves the change doesn't widen into leaving a real cancellation running.The ended-job tests also assert the job's status is unchanged (
done/error), which holds before and after.Fail-before output, with the two routers reverted and the test kept. It is verbatim except that the local checkout path prefix is replaced with
<repo>, and the registry's import-time path banner that follows it is omitted:After the fix,
python -m unittest tests.test_workflow_runs_lifecyclepasses all 6 tests (Ran 6 tests in 0.021s/OK).Whole suite,
python -m unittest discover -s testsinapi/(venv withfastapi+python-multipart+httpx):devRan 93 tests/OK (skipped=3)Ran 97 tests/OK (skipped=3)test_workflow_runs_lifecycle.pyThe 3 skips were already there on
dev.Lint: the repo configures no Python linter, and ESLint ignores
api/**. As a spot check,ruff check --isolatedreports the same counts ondevand this branch:generation.py10 → 10,workflow_runs.py8 → 8,test_workflow_runs_lifecycle.py4 → 4, so nothing new. No TypeScript touched.🤖 Generated with Claude Code