Skip to content

feat(agent-server): allow bash timeout=None to run until stopped - #6052

Open
Jacob-Lasky wants to merge 2 commits into
OpenHands:mainfrom
Jacob-Lasky:bash-timeout-none
Open

Jacob-Lasky wants to merge 2 commits into
OpenHands:mainfrom
Jacob-Lasky:bash-timeout-none

Conversation

@Jacob-Lasky

@Jacob-Lasky Jacob-Lasky commented Oct 17, 2026 •

Copy link
Copy Markdown

HUMAN:

Allow for a CLI command to run without a timeout. Many of my commands take a few minutes, especially rust builds and test suites.


AGENT:

Why

Long build commands are often supervised by the caller, which decides when they should end and uses POST /api/bash/bash_commands/{command_id}/stop (added in #5348) to end them. For those commands an arbitrary server-side timer is the wrong control: there is no correct number. Today the only way to start a command with no deadline is to replace the private BashEventService._execute_bash_command.

Summary

  • ExecuteBashRequest.timeout is now int | None, with the default unchanged at 300. null on start_bash_command and the bash websocket means the command runs until it exits or is stopped.
  • The blocking execute_bash_command route returns 422 for null, because a command that never exits would otherwise hang the request with no id to stop it. The 422 points callers at start_bash_command plus the stop route.
  • SDK. RemoteWorkspace.start_command and AsyncRemoteWorkspace.start_command accept timeout=None and send null. execute_command rejects None with a ValueError before any request, sync and async.
  • TypeScript client. startCommand forwards null unchanged, executeCommand rejects it before sending, and ExecuteBashRequest.timeout is number | null.
  • Feature map. The F13.timeout-none sub-feature and its recipe, which includes the 422 check.
  • Config wording. The bash_events_retention_seconds description now says "run time" instead of "timeout".

Issue Number

None opened. The motivation comes from a downstream deployment that currently patches _execute_bash_command.

How to Test

All new tests are additions; no existing test is removed or renamed.

  • tests/agent_server/test_bash_service.py
    • test_execute_bash_request_timeout_defaults_to_300_and_accepts_null
    • test_bash_command_without_timeout_runs_until_stopped: the wait receives None, the command is still running after 1.5s, and stop ends it with -15.
    • test_bash_command_default_timeout_bounds_the_wait: the wait receives exactly 300.
    • test_execute_bash_command_rejects_null_timeout
  • tests/sdk/workspace/remote/test_remote_workspace_mixin.py
    • test_start_command_generator_sends_null_timeout
    • test_execute_command_generator_rejects_none_timeout_before_starting
  • tests/sdk/workspace/remote/test_runtime_scope.py: test_execute_command_rejects_none_timeout_before_any_request, sync and async.
  • tests/cross/test_remote_conversation_live_server.py: test_start_command_without_timeout_runs_until_stopped_with_live_server, a real server.
  • TypeScript
    • BashClient.startCommand forwards a null timeout unchanged
    • BashClient.executeCommand rejects a null timeout before sending

Red-then-green. With the source change reverted and the new tests kept, the model, service, SDK-generator and live-server tests fail, as does the map recipe. The default-300 test passes either way; it guards the default.

Gates:

Check Result
uv run pre-commit run on the changed files pass
Relevant Python set (bash service, api, sockets, init, file router, remote workspace, OpenAPI contract, verify-agent-server skill) 482 passed, 1 skipped, three runs
Live-server bash tests 3 passed
Feature map: map check ok
Feature map: map run --file F13 --only F13.timeout-none --fresh on a real server pass: 2, fail: 0
OpenAPI type-quality check pass
TypeScript lint, prettier, tsc, vitest pass; vitest 389 passed, public type budget unchanged

Video/Screenshots

Not applicable. There are no visual changes.

Design Doc

Skipped. The change is additive: one field accepts null.

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

Commands started on the process-global bash service. A command started with timeout: null on the process-global bash service, through /api/bash/start_bash_command or the bash websocket, is not stopped when an in-process app lifespan exits.

  • Pre-existing. The lifespan has never closed the global bash service. A command with a numeric timeout was only ever bounded by that timer, and null removes that bound.
  • What callers should do. A caller that starts a command without a timeout owns its end. It should stop the command through POST /api/bash/bash_commands/{command_id}/stop (or RemoteWorkspace.stop_command / BashClient.stopCommand) before shutting down.
  • Conversation-scoped commands are unaffected. Their service is closed with the conversation.

Making the lifespan own the global service's lifetime takes more than closing it on exit:

  • overlapping lifespans share the service;
  • a failing shutdown step must not skip cleanup;
  • the sockets fallback must use the current service;
  • a start that races a close needs handling.

I'm happy to send that as a separate PR.

Expect a release-note-required label: the BashCommand response's timeout now also allows null.

ExecuteBashRequest.timeout accepts null (default stays 300). A command
started through start_bash_command or the bash websocket with a null
timeout runs until it exits or is stopped through
POST .../bash_commands/{command_id}/stop. The blocking
execute_bash_command answers 422 for a null timeout, since it would
hold the request open with no id to stop the command.

SDK: RemoteWorkspace/AsyncRemoteWorkspace.start_command accept
timeout=None and send it as null, leaving the start request on the
client's HTTP timeout; execute_command rejects None with ValueError
before sending anything.

TypeScript client: BashClient.startCommand forwards a null timeout
unchanged (Math.floor(null) used to send 0), executeCommand rejects
null before sending, and ExecuteBashRequest.timeout is number | null.

Adds F13.timeout-none to the verify-agent-server feature map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Jacob-Lasky
Jacob-Lasky marked this pull request as ready for review October 17, 2026 16:48
Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions

Copy link
Copy Markdown
Contributor

📁 PR Artifacts Notice

This PR contains a .pr/ directory with temporary PR-specific documents. The directory will be automatically removed after this PR merges to main through a cleanup PR.

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.

2 participants