Skip to content

fix(local): stop BashCommand when an automation run is cancelled #514

Open

Description

@neubig

Why

On the OSS Agent Canvas VM (Automation 1.13.3), I cancelled a running local automation through POST /api/automation/v1/runs/{id}/cancel. The run changed to CANCELLED, but its python3 worker.py child kept running and making GitHub requests for several minutes. I had to stop the child process separately. This makes cancellation unreliable as a resource-safety control.

The observed run was e7b70fc0-d599-4101-bd7b-f67e5058998a at 2026-09-23 04:24 UTC. The callback/status path worked; process termination did not. Related but separate from #283, which prevents overlapping cron dispatches.

Desired behavior

Cancelling a local automation run terminates its active Agent Server BashCommand and descendants, then releases the run's execution resources. A terminal CANCELLED run must not keep executing its entrypoint.

Actual behavior

cancel_run (openhands/automation/router.py) only performs process cleanup when run.sandbox_id is set. Local runs have no sandbox — LocalAgentServerBackend.get_execution_context returns sandbox_id=None — so nothing stops the entrypoint dispatched through POST /api/bash/start_bash_command. The run row flips to CANCELLED while its BashCommand process group keeps running until the command's own timeout.

Reproduce: dispatch a local run whose entrypoint is long-running (for example python3 worker.py sleeping well past the run timeout), then curl -X POST "$AUTOMATION_URL/api/automation/v1/runs/$RUN_ID/cancel". The run returns CANCELLED but pgrep -f 'python3 worker.py' still shows the process. The regression tests for this fix live in tests/test_cancel_run.py and run with uv run pytest tests/test_cancel_run.py.

Design decision

Use a targeted per-command stop operation on the Agent Server. The Agent Server (pinned openhands-sdk==1.49.4, and current main) currently exposes no way to stop a running BashCommand: bash_router offers only start, execute, event search/get, and DELETE /api/bash/bash_events (which deletes history, not the process), and the process group is signalled only when the Bash service closes or the command hits its own timeout.

  • Automation cancels the run's stored entrypoint bash_command_id through that API.
  • Stopping that command terminates its process group/descendants.
  • It must not stop unrelated BashCommands or the agent's TerminalTool activity on the shared local Agent Server.
  • Successful stop, already-finished, and not-found are all treated as idempotent cancellation.
  • The SDK endpoint is tracked in [Feature]: Agent Server operation to stop a single running BashCommand software-agent-sdk#5257; Automation depends on it.

Rejected alternatives: a host-level process signal issued by Automation, and deleting the shared conversation runtime. Cloud cancellation works only because deleting the sandbox kills the container — local mode cannot reuse that, and that mode-specific difference is documented below.

Acceptance Criteria

  • Cancelling a RUNNING local automation stops the run's entrypoint BashCommand (its recorded bash_command_id) through the Agent Server's per-command stop API ([Feature]: Agent Server operation to stop a single running BashCommand software-agent-sdk#5257), terminating the command's process group and descendants without waiting for the command's normal timeout.
  • Cancellation targets only that command id: other concurrently running BashCommands and the agent's TerminalTool activity on the shared local Agent Server are left running.
  • Cancellation is idempotent — a successful stop, an already-finished command, and an unknown/not-found command id all leave the run CANCELLED and do not surface an error to the caller.
  • A cancelled run stays CANCELLED: a late completion callback cannot revert it to COMPLETED or FAILED (the existing optimistic-locking guarantee is preserved).
  • The run's execution resources are released on cancel: the stopped command is observable as finished (its exit status is visible to the verifier, so a later watchdog scan does not report it as still running), and the shared Agent Server and its conversation runtime are left intact.
  • Cloud mode keeps its existing behavior (sandbox deletion kills the container); the mode-specific difference between cloud and local cancellation is documented in code or docs.
  • No host-level signal workaround is added, and the shared conversation runtime is not deleted.
  • Focused tests exercise a long-running entrypoint, cancel it, and verify the process exits and resources are released — including isolation from a concurrent command and idempotency for a finished or unknown command id.

Non-goals

Dependency


OpenHands AI triage

The following comments and acceptance criteria were added by the OpenHands AI agent.

Triage

Reconciled against the maintainer decision on this issue: the termination mechanism is a targeted per-command stop operation on the Agent Server/SDK (option (a)), called by cancel_run with the run's stored entrypoint bash_command_id, and the command scope is that single entrypoint command — not the agent's concurrent TerminalTool activity on the shared local Agent Server. The Agent Server currently has no such operation (bash_router offers start/execute/event search-get plus DELETE /api/bash/bash_events, which deletes history rather than the process, and BashEventService only signals a command's process group on service close or on that command's own timeout), so the endpoint is tracked in OpenHands/software-agent-sdk#5257 and Automation depends on it.

The bug label is added to match this repository's convention for fix issues (see #500, #498, #472, #470) and so the type-specific readiness criteria can be evaluated against this issue.

Desired Behavior

Cancelling a RUNNING local automation run stops its entrypoint BashCommand through the Agent Server's per-command stop API and leaves the run CANCELLED, without waiting for the command's normal timeout. The stop is scoped to the run's recorded bash_command_id so unrelated BashCommands and the agent's TerminalTool activity on the shared local Agent Server keep running. Cloud mode continues to rely on sandbox deletion, and the mode-specific difference is documented rather than worked around.

Acceptance Criteria

  • Cancelling a RUNNING local automation stops the run's entrypoint BashCommand (its recorded bash_command_id) through the Agent Server's per-command stop API ([Feature]: Agent Server operation to stop a single running BashCommand software-agent-sdk#5257), terminating the command's process group and descendants without waiting for the command's normal timeout.
  • Cancellation targets only that command id: other concurrently running BashCommands and the agent's TerminalTool activity on the shared local Agent Server are left running.
  • Cancellation is idempotent — a successful stop, an already-finished command, and an unknown/not-found command id all leave the run CANCELLED and do not surface an error to the caller.
  • A cancelled run stays CANCELLED: a late completion callback cannot revert it to COMPLETED or FAILED (the existing optimistic-locking guarantee in complete_run is preserved).
  • The run's execution resources are released on cancel: the stopped command is observable as finished (its exit status is visible to verify_run_on_agent_server, so a later watchdog scan does not report it as still running), and the shared Agent Server and its conversation runtime are left intact.
  • Cloud mode keeps its existing behavior (sandbox deletion kills the container); the mode-specific difference between cloud and local cancellation is documented in code or docs.
  • No host-level signal workaround is added, and the shared conversation runtime is not deleted.
  • Focused tests exercise a long-running entrypoint, cancel it, and verify the process exits and resources are released — including isolation from a concurrent command and idempotency for a finished or unknown command id.

Activity

  1. all-hands-bot commented on Sep 23, 2026

    @all-hands-bot
    Contributor

    The following comments were added by the OpenHands AI agent.

    Decision needed

    The reported gap is real and reproduced from the code: POST /api/automation/v1/runs/{id}/cancel only performs process cleanup when run.sandbox_id is set, and local runs have no sandbox (LocalAgentServerBackend returns sandbox_id=None), so nothing stops the entrypoint started via POST /api/bash/start_bash_command. Before this can be scoped, one design choice has to be settled, because the Agent Server (pinned openhands-sdk==1.49.4, and current main) exposes no way to stop a running BashCommand: bash_router offers only start, execute, search/get events, and DELETE /api/bash/bash_events (which deletes history, not the process), and the process group is signalled only when a Bash service closes. Cloud cancel works only because deleting the sandbox kills the container, which local mode cannot reuse.

    1. Which termination mechanism should this issue require? Today none exists, so the realistic options are (a) add a per-BashCommand stop/cancel endpoint to OpenHands/software-agent-sdk and call it from cancel_run, (b) reuse the conversation-scoped runtime lifecycle (DELETE /api/conversations/{id}/runtime) and make local cancel release the run's conversation runtime — which depends on the conversation-runtime dispatch work tracked in Run local automation bundles in isolated Docker conversation runtimes #448/Give local and Docker automation runs the same conversation execution contract #450 and would not cover the current global-bash dispatch, or (c) a host-level process signal issued by Automation. Please pick one (or state a different mechanism) so the criteria can name the supported API instead of assuming one.

    2. Should cancellation target only the run's entrypoint BashCommand (bash_command_id), or also any concurrent bash activity the run's agent started through TerminalTool on the shared local Agent Server? Local mode shares one server across runs, so the broader target has to be distinguished from unrelated commands.

    Once the mechanism and the command scope are settled, this issue can be rewritten with observable acceptance criteria and marked ready for development.

  2. neubig commented on Sep 23, 2026

    @neubig
    MemberAuthor

    Design decision: use option (a). Add a targeted Agent Server/SDK operation to stop one BashCommand and have Automation cancel the run's stored entrypoint bash_command_id through that API. Stopping that command should terminate its process group/descendants, but must not stop unrelated BashCommands or agent TerminalTool activity on the shared server. Treat successful stop and already-finished/not-found as idempotent cancellation. Please update the issue criteria accordingly and mark ready-for-dev when clear; the SDK endpoint should be tracked in a linked SDK issue, with Automation depending on it. Do not add a host-level signal workaround or delete the shared conversation runtime.

  3. neubig commented on Sep 23, 2026

    @neubig
    MemberAuthor

    @all-hands-bot Please reconcile the issue with the design decisions above and mark it ready-for-dev when its acceptance criteria are complete.

  4. added
    bugSomething isn't working
    ready-for-devScoped for contribution; managed by repository readiness checks.
    on Sep 23, 2026
  5. all-hands-bot commented on Sep 23, 2026

    @all-hands-bot
    Contributor

    Reconciled against your decision — thanks for settling the mechanism and scope.

    What changed

    The issue body is rewritten with observable criteria built on option (a) and marked ready-for-dev (bug added to match this repo's convention for fix issues, e.g. #500, #498, #472, #470).

    • Mechanism: cancel_run stops the run's entrypoint BashCommand through a new Agent Server per-command stop API, called with the run's stored bash_command_id. No host-level signal, no conversation-runtime deletion.
    • Scope: that single command id only. Other BashCommands and the agent's TerminalTool activity on the shared local Agent Server are explicitly out of scope.
    • Idempotency: successful stop, already-finished, and unknown/not-found all leave the run CANCELLED without erroring.
    • Cloud: unchanged (sandbox deletion kills the container); the mode-specific difference is now an explicit criterion to document.
    • Non-goals: subject release on cancel (Release conversation subjects when runs are cancelled #472), cloud lifecycle changes, watchdog timeout changes.

    Linked SDK issue

    The endpoint is tracked in OpenHands/software-agent-sdk#5257 (also ready-for-dev), and #514 carries it as an explicit dependency, so Automation's implementation waits on that endpoint shipping in a pinned SDK release.

    Two things worth flagging from the code, since they shape the criteria:

    1. BashEventService already has the machinery to reuse — subprocesses start with start_new_session=True, and _signal_process_group with SIGTERM→SIGKILL escalation is used on the timeout path. The gap is that it is only reachable on timeout or service close, so the SDK change is mostly exposing an on-demand stop per command id rather than new termination logic.
    2. "Resources released" is checkable rather than aspirational: after the stop, verify_run_on_agent_server must see a terminal exit status for that bash_command_id instead of STILL_RUNNING. Otherwise a later watchdog scan would still treat the run as live — which is the same class of bug as the original report.

    I did not push commits or open a PR; the SDK issue is the only artifact created.

    Generated by OpenHands AI on behalf of the user.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority:normalNormal-priority work itemready-for-devScoped for contribution; managed by repository readiness checks.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions