Conversation
Not the CLI. PyPI `openhands` is OpenHands/openhands-cli, whose README opens with "This project is no longer actively maintained" and whose last release is 1.16.0 of 2026-05-08; this drives openhands-agent-server 1.49.2 (OpenHands/agent-sdk, released 2026-09-17) over its HTTP + WebSocket API, one server process per turn. Measured on the pinned version, and each decided a design choice: - cold start to a serving /alive is 3.3-4.1 s, and a conversation created by one server process is read back intact by a different process over the same on-disk store — which is what lets the one-process-per-turn contract hold. - the credential is never persisted (base_state.json carries the LLM spec with api_key: None), so it rides the driver's environment and the id is sent `openai/<id>` to stop litellm inferring a provider from the relay url. - the agent is frozen at the conversation's first creation, so the tool policy is part of the conversation id: a changed policy is a new conversation created with the tools it asks for. - tool_enforcement is hard, measured against a control. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The venv lives on the data volume and outlives the image, so an existence check cannot tell a current install from an old one. A volume first started by an earlier build of this branch held an openhands-venv whose python ran perfectly and whose contents were the deprecated CLI's stack (openhands 1.16.0 pinning openhands-sdk 1.21.0); the install was skipped and every turn died on `No module named 'openhands.sdk.marketplace.registration'`. Found by the first column. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The version guard ran `$(backend_bin openhands) -c …` before anything checked that the path exists. Under `set -euo pipefail` a missing interpreter is a 127 that kills the whole entrypoint: the container came up unhealthy and exited with no backends at all. Probed only when there is something to probe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four calls in the driver can raise `TimeoutError: timed out` with that exact message — three urlopen timeouts, the socket open — so the reason a turn failed did not say which one. Measured on vercel|openhands|claude-sonnet-4.6, whose recycle failed in 250 s saying only that. Each phase now names itself, and the reason reads `… (while sending the message)`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The terminal tool runs commands in tmux, and the server defaults TMUX_TMPDIR to a directory inside the working directory. A workspace here is /data/workspaces/hsess<32 hex>, so tmux's socket landed at 106 characters against the 108-byte sun_path limit and answered `LibTmuxException: new-session: error connecting to … (File name too long)`. The agent then retried the tool it could not start: the same three ids that failed at 235 s, and later at 2,377 s and 4,000 s, now run five scenarios each in 10-17 s. It took four wrong guesses to find, because the driver threw away the only thing that could say so: the server's output went to DEVNULL. It goes to a file under .harness/ now and its tail rides every failure this driver reports. A pipe would not do — one nobody drains fills and blocks the server mid-turn. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_build_openhands accepted mcp_servers and dropped it — the defect aider's bridge already taught this repo, in a different place. This base needs no bridge: the SDK takes the servers on the agent as `mcp_config` and dials them itself, and its MCPServer maps onto the harness's declaration one field at a time. The declared transport travels, as HarnessRouter#191's review settled for kimi, and a transport the SDK does not know is left out rather than guessed at: a config it rejects fails the whole turn rather than the one server. The servers are part of the conversation identity for the same reason the tool policy is — the agent is frozen at creation, so a server declared later cannot reach an agent already made. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every gemini follow-up on the google column failed after 145-212 s with no reason of any kind — eleven scenarios across five models, never a first turn. The cause is neither the provider nor the request shape. From litellm 1.95.0, PromptTokensDetailsWrapper mirrors an assignment between cache_write_tokens and cache_creation_tokens, putting BOTH names into model_fields_set, and then drops the unset attribute from __dict__; the SDK's telemetry uses `"cache_creation_tokens" in details.model_fields_set` as its existence test. Each side is self-consistent, together they are not, and 1.49.2 is the newest SDK. The SDK asks only for litellm>=1.93.0, so litellm joins the same single pip invocation pinned at 1.94.3, and the install asserts THE DEFECT IS ABSENT rather than asserting a version: a future bump fails the image instead of failing every follow-up on a provider that reports prompt caching. The guard compares both pins, because a volume built before this holds the right agent-server and the wrong litellm. The google column is 40 of 40 with the pin and was 29 of 40 without it. The defect is intermittent — Google populates prompt_tokens_details only sometimes, and a turn without it passes on the broken pin too — so the column, not the single-model A/B, is what carries the claim. Why the record said nothing: event_service publishes an error event only for an exception that is NOT a ConversationRunError, assuming run()/arun() already emitted its own, and an exception raised out of arun's error handling is exactly the case where nobody did. The driver now falls back to the server log, taking the exception lines rather than the file's last lines — a turn that dies in seconds and is retried to exhaustion ends with SIGTERM three minutes later, and a plain tail put that shutdown noise into the record, which reads like a reason and is worse than silence. COLUMNS is set so the sentence survives rich's wrapping in one piece. Five tests pin it. Also: the relay route is guarded on the key like every other builder's, and the catalog comment records that these ids are now measured rather than offered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… 6 gains the case with no reason at all Three things the final pass against the other harnesses found. ui/src/lib/harness.ts still carried "NOT ONE of these has been measured — no column has run", written before any column existed. The gateway's catalog comment was corrected when the columns finished and this copy was not; it now reads like kimi's, with the numbers and a pointer to where the record lives. docs/harness-verification.md point 6 tells the next backend to check how its CLI reports a provider failure rather than assume an error event. openhands found the case the point does not cover: a harness that reports a failure NOWHERE, and what to do about it (read the log, take the exception lines and not the last lines, because a turn retried to exhaustion ends with SIGTERM). Merged origin/main: b96d144, 0fcd90b and efd320b landed while this branch was out. Verified after the merge rather than trusting a clean auto-merge — openhands is intact at every registration point, the three commits' own additions are intact, tsc passes, and the python suite is 976 passed against origin/main's 944 with the same 23 environment failures on both, so no regression. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@ZixiaoL is attempting to deploy a commit to the Future HR Team on Vercel. A member of the Team first needs to authorize it. |
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.
OpenHands V1 as a backend, driven through
openhands-agent-server1.49.2 (MIT) — the REST/WebSocketinterface its vendor maintains, not the deprecated CLI. PyPI
openhandsis OpenHands/openhands-cli,whose README opens with "This project is no longer actively maintained" and whose last release is
1.16.0 of 2026-05-08; the agent server released 1.49.2 on 2026-09-17.
The shape
One server process per turn, so the one-process-per-turn contract every other backend keeps is kept
here: the runner reads NDJSON off a driver's stdout and cancel is a process-group kill. Cold start
to a serving
/aliveis 3.3–4.1 s, measured, and a conversation created by one process is read backintact by a different process over the same on-disk store — which is what makes the per-turn shape
work at all, and what this product needs, since sandboxes are recycled between turns.
runner/openhands_driver.pyfollowsdsh_driver.py: same header, same{"m", "p"}line protocol.Measured properties the design rests on, each pinned by a test:
over the tool policy and the declared MCP servers — a second create with a different tool list
leaves the persisted agent as it was
base_state.jsonholds the LLM spec withapi_key: None,so the key rides the environment
FinishActionrather than a trailing assistant messageColumns
Vercel's six: qwen3.8-27b missed a recall after a switch and after a recycle (the model). The other
four were mistral-medium-3.5, which Vercel refuses with
Assistant message must have either content or tool_callsabout a message whose content is right there — isolated against the live endpoint,the identical request answers 200 with
content: "M1"and 400 withcontent: [{"type":"text",…}].The relay stringifies assistant content for that route once the provider names it, the tool-role
twin one role over, and the re-run passed five of five.
The defect this PR spent the most on
The google column was 29 of 40 until the last commit. Every gemini follow-up failed after
145–212 s with no reason of any kind — eleven scenarios across five models, never a first turn:
Neither the provider nor the request shape. From litellm 1.95.0 the wrapper's
__setattr__mirrorsan assignment between
cache_write_tokensandcache_creation_tokens, putting both names intomodel_fields_set, and litellm then drops the unset attribute from__dict__as aconstruction-cost optimisation. The SDK's telemetry uses
"cache_creation_tokens" in details.model_fields_setas its existence test. Each side is self-consistent; together they arenot, and three lines reproduce it with no agent, no provider and no network:
1.49.2 is the newest SDK, so there is nothing to upgrade to; it asks only for
litellm>=1.93.0, solitellm joins the same single pip invocation pinned at 1.94.3, and the install asserts the
defect is absent rather than asserting a version — a future bump fails the image instead of
failing every follow-up on a provider that reports prompt caching. The guard compares both pins,
because a volume built before this holds the right agent-server and the wrong litellm.
Why Vercel never saw it: it reports
cache_creation_input_tokenson every response, so litellmsets
cache_creation_tokensrather than leaving it None and the attribute genuinely exists.Measured through litellm 1.101.0 on the real streaming path, vercel/mistral-medium-3.5 and
vercel/gpt-5.4 both answer
hasattr=True. That also rules the defect out as a second cause of themistral failures above.
The defect is intermittent — Google populates
prompt_tokens_detailsonly sometimes, and a turnwithout it passes on the broken pin too (captured from inside
_cache_buckets:Usage(prompt_tokens=7410, …, prompt_tokens_details=None), on a follow-up that passed). So thesingle-model A/B is supporting evidence and the column is what carries the claim: 29 of 40 before,
40 of 40 after, same ids, same image otherwise.
Why the record said nothing, and what now happens instead
event_service._run_and_publishpublishes an error event only for an exception that is not aConversationRunError, assuming the run already emitted its own — and an exception raised out ofthe run's error handling is exactly the case where nobody did. The status flips to error and the
WebSocket carries no reason at all; the sentence existed only in the server's log, and the record
said
the turn ended erroreleven times.The driver now falls back to that log, taking the exception lines rather than the file's last
lines: a turn that dies in seconds and is retried to exhaustion ends with
SIGTERM … Shutting down,and the first version of this fallback put exactly that into the record, which reads like a reason
and is worse than silence. It reports both ends of the chain, since the root cause names the defect
and the wrapper names the operation.
COLUMNSis set so the sentence survives rich's wrapping inone piece. Verified end to end against the broken pin: the record now reads
agent-server said: AttributeError: 'PromptTokensDetailsWrapper' object has no attribute 'cache_creation_tokens' … ConversationRunError: …. Five tests pin it, three of them for defectsthis fallback had.
docs/harness-verification.mdpoint 6 gains the case it did not cover: a harness that reports afailure nowhere.
Also here
tmuxin the image:openhands-toolsdeclareslibtmux, which drives the real binary; upstream'sown image installs it, and without it the server dies at start.
TMUX_TMPDIRis set short. The server defaults it inside the working directory, and a workspacehere is
/data/workspaces/hsess<32 hex>, so the socket landed at 106 characters against the108-byte
sun_pathlimit; tmux answeredFile name too long, the agent retried the tool it couldnot start, and a 10 s turn became 235 s and then 4,000 s. Four wrong guesses preceded it; what
ended the investigation was sending the server's stdout to a file instead of
DEVNULL.HR_BACKENDS: its environment is ~666 MB, the aiderprecedent and well above the 300 MB line.
Verification
All 17 registration points.
tscclean. Python suite 976 passed against origin/main's 944,with the same 23 environment failures on both, so no regression.
docs/support-matrix.mdcarries noopenhands rows, matching kimi, the most recent merged harness; the record is in
docs/support-matrix-notes.md.🤖 Generated with Claude Code