Skip to content

Agent-ergonomics fixes from CLI transcript mining: py3.12 sandbox, list_instances, usage-on-error, JSON query-raw errors - #328

Open
qu0b wants to merge 9 commits into
masterfrom
qu0b/cli-agent-ergonomics
Open

Agent-ergonomics fixes from CLI transcript mining: py3.12 sandbox, list_instances, usage-on-error, JSON query-raw errors#328
qu0b wants to merge 9 commits into
masterfrom
qu0b/cli-agent-ergonomics

Conversation

@qu0b

@qu0b qu0b commented Jul 24, 2026

Copy link
Copy Markdown
Member

Mined every Claude Code session that drove the panda CLI (June 12 – July 15: 777 invocations, 43 hard failures) and fixed the failure classes that repeatedly burned real devnet investigations. Each fix is verified against the exact command that failed in the transcripts.

sandbox: Python 3.11 → 3.12 (PEP 701)

LLM-generated code routinely nests same-quote strings and backslashes inside f-string expressions — a SyntaxError on 3.11, legal on 3.12. Base image bumped to pinned python:3.12-slim; the hash lock was recompiled with the same pinned uv (0.11.17) and resolution is unchanged (header-only diff). Verified: image builds, ethpandaops/pandas import, transcript-failing f-strings parse on 3.12.13.

dora + ethnode: get_clients / list_instances

Models guess ethnode.list_instances(network) after list_networks() and hit an ImportError; instance labels were documented as non-enumerable. They are enumerable: Dora's /api/v1/clients/consensus client_name is exactly the ethnode instance label (verified against live hoodi). New dora.get_clients server op + Python wrapper, and ethnode.list_instances(network) returns sorted {name, status} — status distinguishes online/offline nodes for free. Covered by two new server tests.

cli: stop making agents guess syntax

SilenceUsage left flag typos as a bare Error: unknown flag: --network with no correction path — transcripts show 6+ guessed-flag failures (--network, --like, --database, --datasource, -c, arg-count misses). Now:

  • unknown-flag errors append the command's usage line + --help pointer (SetFlagErrorFunc)
  • arg-count errors carry the usage line too (Args validators wrapped tree-wide)
  • panda execute -c works as shorthand for --code

cli: machine output mode — JSON in, JSON out

The single most frequent wasted-iteration pattern (~15 occurrences): panda clickhouse query-raw ... 2>&1 | python3 json.load fails, the JSONDecodeError masks the real ClickHouse error, and the agent retries blind. The general condition is stdout is a JSON document and the consumer merges stderr into the pipe — which the transcripts also do with -o json on other commands.

So the rule is per-run, not per-command: when output is JSON (-o json/--json, or an always-JSON command like query-raw), the CLI owns error rendering — stdout carries exactly one JSON document, stderr stays empty, and the exit code conveys failure. Update notices are suppressed in that mode too (they broke even successful pipelines). Human text mode is untouched: cobra still prints Error: ... to stderr.

This also fixes two pre-existing machine-honesty bugs that the mode change makes safe to correct: panda execute --json and panda build --json exited 0 when the execution or build had actually failed, unlike their text paths.

server: JSON-RPC rejections are not gateway failures

A JSON-RPC error object rides back on a healthy node connection — the node processed and rejected the request. Returning 502 (with the CLI's "temporarily unreachable — retry" hint) made deterministic failures look transient: transcripts show an agent retrying debug_getBadBlocks (-32601) four hours after first learning the client doesn't support it, and revert reasons ("stack underflow", "invalid opcode") arriving triple-wrapped as gateway errors during EIP testing. JSON-RPC errors now return 400 with the error's data payload appended (it often carries the answer, e.g. a revert reason), and the CLI adds a do-not-retry hint keyed on the message — so it also improves output against older servers still wrapping these in 502.

embedding + search: outages are bounded and honest

On 2026-07-08 the embedding upstream (OpenRouter) hung and three consecutive panda search calls each blocked for the full 2-minute batch timeout during a live devnet investigation. Root cause: a single remoteEmbedTimeout shared by 500-document index batches and single-query embeds, with server and proxy timeouts identical so the server always timed out first with a generic error.

  • single-item embeds (the interactive search shape) get a 15s deadline; batches get 130s, deliberately outlasting the proxy's 2-minute upstream timeout so its specific error surfaces
  • search failures from an unreachable embedding upstream, a warming index, or an index that never activated return 503 instead of 400 — retryable service conditions, not query errors. Filter-value rejections ("unknown category") stay 400
  • CLI adds retry-shortly hints for both conditions

Test plan

  • go test -race ./pkg/... ./modules/... clean, make lint 0 issues
  • make docker-sandbox builds; PEP 701 smoke test + new sandbox functions import in the image
  • End-to-end against a running server: flag/arg errors show usage, -c executes, query-raw success and failure both parse through 2>&1 | json.load
  • New unit tests: dora.get_clients (bare list + non-JSON body), ethnode execution_rpc JSON-RPC error → 400 with data, searchErrorStatus classification, serverErrorHint for JSON-RPC/-32601 across old and new server statuses

qu0b added 3 commits July 24, 2026 14:37
LLM-generated code routinely nests same-quote strings and backslashes
inside f-string expressions, which is a SyntaxError on 3.11. Lock
recompiled with the same pinned uv; resolution is unchanged.
Instance labels were not enumerable from the sandbox: models guess
ethnode.list_instances(network) and fail. Dora already tracks every
node, and its client_name is exactly the ethnode instance label, so
expose /api/v1/clients/consensus as dora.get_clients and derive
ethnode.list_instances (name + online/offline status) from it.
- unknown-flag and arg-count errors now carry the command's usage line
  (SilenceUsage previously left them with no correction path)
- panda execute gains -c as --code shorthand
- query-raw failures emit {"error": ...} on stdout and suppress the
  stderr duplicate, so 2>&1 JSON pipelines surface the real error
  instead of a JSONDecodeError; its update notice is skipped for the
  same reason. Error printing moves from cobra into Execute, which
  knows which failures were already reported.
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

🐼 Smoke eval — c0129bc: ✅ 8/8 pass

📊 Interactive report — tokens p50 17,174 · tokens/solve 19,913.

Reference points: qu0b/cli-agent-ergonomics@862f602 100% · qu0b/cli-agent-ergonomics@189cd99 100% · qu0b/cli-agent-ergonomics@361a627 100%.

question result tokens tools
forky_node_coverage 16,491 7
tracoor_node_coverage 16,661 6
mainnet_block_arrival_p50 15,725 7
list_datasources 13,139 2
block_count_24h 20,410 11
missed_slots_24h 25,047 20
chartkit_default_arrival_distribution 34,143 19
storage_upload_session_scoped 17,687 11
🔭 Langfuse traces (8 runs; ⚠️ = failed)

The report walks this branch's commits against the master baseline and the most recent release. A self-contained copy is in the run's eval-smoke-* artifact.

qu0b added 6 commits July 24, 2026 18:00
A JSON-RPC error object rides back on a healthy node connection: the
node processed and rejected the request (unsupported method, bad
params, node-side limits, reverts). Returning 502 made deterministic
failures look transient — transcripts show agents retrying -32601
method-not-found hours later and misreading capability gaps as
outages. Return 400 and append the error's data payload, which often
carries the actual answer (e.g. a revert reason).
On 2026-07-08 the embedding upstream hung and every panda search
blocked for the full 2-minute batch timeout before failing. Single-item
embeds (the interactive search shape) now get a 15s deadline; batch
calls get 130s, deliberately outlasting the proxy's 2-minute upstream
timeout so its specific error surfaces instead of a generic client
timeout. Search failures caused by an unreachable embedding upstream or
a warming index now return 503 instead of 400 — they are retryable
service conditions, not query errors.
JSON-RPC rejections get a do-not-retry hint (method-not-found is a
client capability gap, keyed on the message so old servers' 502
wrapping benefits too); embed-upstream and index-warming search
failures get an explicit retry-shortly hint instead of the generic
status hint.
Root-level SilenceErrors inverted error printing for the whole tree to
serve one command: every other entry point executing rootCmd would
lose output, and Execute had to mirror cobra's printing forever (it
already dropped the unknown-command usage line). Instead, query-raw's
RunE sets SilenceErrors on itself only after the stdout JSON error is
written — flag and arg errors return before RunE and stay loud, and
cobra's own printing is restored everywhere else.
…potent usage wrap

- embed timeouts came from item count, so a warm index rebuild embedding
  exactly one new document was clamped to the 15s query deadline. The
  deadline is now handed down from the public entry point (Embed = query,
  EmbedBatch/EmbedQueryBatch = build); a regression test pins that a
  one-item build honors the deadline it was given.
- cap the JSON-RPC error data payload at 512 bytes; some clients return
  whole traces and an error message is not a data channel.
- attachUsageToArgErrors is not idempotent, so guard it with sync.Once
  against a second Execute() in-process.
- ethnode.list_instances explains the Dora dependency when the explorer
  is unavailable instead of surfacing a bare Dora error.
Two leftovers fitted to the sampled transcripts rather than the failure:

- error-as-JSON and update-notice suppression keyed on the query-raw
  command name, but the failure is 'stdout is JSON and stderr gets
  merged into the pipe' — true of every command run with -o json, which
  the same transcripts also do. Replaced with one machine-output mode
  (isJSON() or an always-JSON command): stdout carries exactly one JSON
  document, stderr stays empty, exit code conveys failure. query-raw's
  per-command printing and SilenceErrors hack are gone.
- searchErrorStatus matched the two error strings seen in the incident
  and missed their sibling, '<x> search index not available' (index
  never activated), which kept returning 400. Filter-value rejections
  stay 400.

Also fixes two pre-existing machine-honesty bugs the mode change makes
safe: 'panda execute --json' and 'panda build --json' exited 0 when the
execution or build had failed, unlike their text paths.
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.

1 participant