feat(cli): report the real exit code and really stop what uipath server runs - #1842
Draft
robert-ursu wants to merge 2 commits into
Draft
robert-ursu wants to merge 2 commits into
robert-ursu wants to merge 2 commits into
Conversation
robert-ursu
force-pushed
the
feat/python-job-cancellation
branch
2 times, most recently
from
August 4, 2026 11:09
ffcf810 to
bca74f9
Compare
robert-ursu
force-pushed
the
feat/async-job-dispatch-and-result-push
branch
from
August 4, 2026 11:13
114a204 to
71c81e9
Compare
robert-ursu
force-pushed
the
feat/python-job-cancellation
branch
from
August 4, 2026 11:18
bca74f9 to
b05d195
Compare
robert-ursu
force-pushed
the
feat/python-job-cancellation
branch
from
August 4, 2026 16:00
b05d195 to
1df44eb
Compare
robert-ursu
force-pushed
the
feat/async-job-dispatch-and-result-push
branch
from
August 19, 2026 19:13
6c8af15 to
e509941
Compare
robert-ursu
force-pushed
the
feat/python-job-cancellation
branch
from
August 19, 2026 19:13
1df44eb to
e14f4b2
Compare
robert-ursu
force-pushed
the
feat/async-job-dispatch-and-result-push
branch
from
September 24, 2026 15:12
e509941 to
db4653c
Compare
robert-ursu
force-pushed
the
feat/python-job-cancellation
branch
from
September 24, 2026 15:26
e14f4b2 to
50e0cc7
Compare
robert-ursu
force-pushed
the
feat/python-job-cancellation
branch
from
September 28, 2026 08:18
50e0cc7 to
0654cec
Compare
robert-ursu
changed the base branch from
feat/async-job-dispatch-and-result-push
to
main
September 28, 2026 08:19
robert-ursu
force-pushed
the
feat/python-job-cancellation
branch
from
September 28, 2026 08:29
0654cec to
7e4af34
Compare
robert-ursu
force-pushed
the
feat/python-job-cancellation
branch
from
September 28, 2026 11:33
7e4af34 to
5c4ea34
Compare
robert-ursu
force-pushed
the
feat/python-job-cancellation
branch
from
September 28, 2026 11:46
5c4ea34 to
37cc492
Compare
🚨 Heads up:
|
robert-ursu
force-pushed
the
feat/python-job-cancellation
branch
2 times, most recently
from
September 28, 2026 13:42
b3df754 to
195d328
Compare
Under standalone_mode=False click returns ctx.exit(N)'s code instead of raising, so every ConsoleLogger.error path came back as a result and _run_command_isolated reported ExitCode 0. Map a returned int to the exit code. The HTTP start reply now carries exitCode, the field the .NET HTTP client already reads (it defaulted a missing one to 0, so every 200 was a success). StopJob returns False: this server cannot stop what it runs, and the handler must not be told it did. A result status the IPC sink does not know is reported as Faulted instead of Successful, and "stopped" maps to the wire's Stopped. Entrypoint discovery failures in `uipath run` and `uipath eval` exit 1. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
run/debug/eval drive their own event loop on the server's worker thread. They
now publish it through run_execution_loop to an ExecutionControl, so a stop
cancels the execution's root task and the runtime unwinds cooperatively,
still writing its result.
stop_execution backs both IPC StopJob and the new POST /jobs/{key}/stop. It
cancels the root task, waits a grace period, cancels every task on the
execution's loop, and answers False if it is still running, because it is
blocked in a call that only ending the process can interrupt. forceStop
shortens the waits. A queued execution is dropped before it runs. A stop that
targets another resume version, or an unknown key, answers True: that run is
not running.
The lock, env and cwd are no longer handed on while the execution's thread
still runs. A cancelled caller (a dropped IPC connection, a shutdown) stops
the execution and re-raises only once the thread has exited, and the job
scope's teardown completes before the env is restored.
A stopped execution ends with exit code 143 and "Stopped on request"; a
CancelledError it raised on its own is reported as an unexpected failure.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
robert-ursu
force-pushed
the
feat/python-job-cancellation
branch
from
September 28, 2026 15:05
195d328 to
3c195d6
Compare
|
🚨 Heads up:
|
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Pairs with UiPath/hdens#7868, which makes the handler send the stop, act on the answer, and kill and restart the server when a job cannot be stopped.
Why
Exit codes
A failed execution inside
uipath serverwas reported as a success, on both transports:ConsoleLogger.errorends inclick.get_current_context().exit(1). Understandalone_mode=Falseclick returns that code instead of raising (click/core.py,except Exit)._run_command_isolatedtook the return value as the result and hard-codedExitCode: 0. Everyconsole.errorpath (runtime errors, unexpected exceptions, bad--simulation) came back as exit code 0./jobs/{key}/startreply had noexitCode. The .NETPythonRuntimeClientreadsexitCodeinto anintand treats a missing one as 0, so every HTTP 200 counted as a success, even when the body saidsuccess: false.The handler usually overrides the exit code with the status in
output.jsonor the IPC result. The exit code decides the outcome when neither exists: failures before or outsideUiPathRuntimeContext, and crashes.Stop
uipath servercould not stop what it was running:StopJobwas a no-op that answeredTrue.uipath_ipc/client/connection.py,_teardown).await asyncio.to_thread(...)raised, the lock was released and env/cwd were restored while the execution's thread kept running. The next execution then ran concurrently in a mutated process.What: exit codes
_server_core: anintreturned bycmd.mainis the exit code.run,debugandevalnever return an int of their own.exitCodeis added to every response. An unchanged handler becomes correct against this runtime with no .NET change._job_api: a result status the sink does not know is reported as Faulted, not Successful."stopped"maps to the wire'sStopped(4), which the handler already accepts.uipath run/uipath eval: entrypoint or eval-set discovery failure exits 1 instead of 0.What: how an execution is stopped
run,debugandevaleach drive their own event loop on the worker thread, so an execution is an event loop. They now callrun_execution_loopinstead ofasyncio.run. It publishes the loop and its root task to theExecutionControlcarried on aContextVar. Outside the server (uipath runon a terminal) it isasyncio.rununchanged.stop_execution(job_key, resume_version, force)is shared by IPCStopJoband the newPOST /jobs/{key}/stop(200 {"stopped": bool}).Both a stop and a kill are cooperative and run the same sequence; a kill (
forceStop: true) only has tighter windows:UiPathRuntimeContext.__exit__still writes the result. Wait 30 s, or 5 s for a kill.Falseif the execution still runs. It is typically blocked inside a call that cancellation cannot reach (a sync socket read, a lock wait, a busy loop with noawait), and only ending the process stops it. Whether to do that is the handler's call, since the process and the execution instance belong to it.So a stop answers within about 40 s and a kill within about 10 s, both within the handler's 60 s
StopJobTimeout.Cases that answer
True:Falsethere would invite the handler to kill a resumed run for a stop aimed at the previous one.The lock waits for the thread
_run_command_isolatednow returns or raises only once the worker thread has exited. Cancelling the caller, for example on a dropped connection, stops the execution. TheCancelledErroris re-raised only after that, so the lock, env and cwd are never handed on while the job still runs.The job-scope teardown also completes before the env is restored, so the
xfailintest_server_job_core.pyis removed.Outcomes
ExitCodeError143Stopped on requestCancelledErrorescape1(unexpected)The execution cancelled itselfExit code: NA stopped execution's result document still says Faulted/
ERROR_CancelledError; that is written by uipath-runtime. The handler reports Stopped for an Orchestrator stop regardless of what the runtime wrote.Worth a careful look
run_in_executorfuture, not a task. A task re-raises the execution'sSystemExitinto the server loop, and_run_command_isolatedturnsSystemExitinto an exit code.to_threadwork (sync LangGraph nodes, sync HTTP clients) runs itsfinallyat once. But the execution'sasyncio.Runnerdoes not close until that call returns.run_execution_loopwithdraws the loop before the runner closes, so the sweep in step 2 cannot cancel the runner's wait on those threads, which would leak them. Such an execution answersFalseif the call outlives the grace.ExecutionControl.cancel()delivers at most once. A stop followed by a force stop is ordinary, and a second delivery would land inside the cleanup that writesoutput.json.Testing
uv run pytest tests/cli: 1521 passed, 1 skipped, with coverage on as in CI.ruff check,ruff format --checkandmypyare clean.tests/cli/test_server_cancellation.py(18). They use click commands shaped like the real ones and cover:Falseand keeps the lock until its thread exits;StopJobstops a runningRunJob;/startand validates its body.ConsoleLogger.errorreports exit code 1 through_run_command_isolated;success: false, exitCode: 1;"stopped"to Stopped;🤖 Generated with Claude Code