`claude --print --output-format json` reports its own failures (API error, usage limit, error_max_turns) as the result envelope on stdout and leaves stderr empty, so ask_pm()'s stderr-only message logged "claude exited 1: " with nothing after it, twice on 2026-10-31. describe_cli_failure() now logs the envelope's diagnostic fields plus stdout/stderr tails, redacted (sk-ant-, Bearer/token, URL userinfo). PM_DECISION_MAX_TURNS (default 1, unchanged) makes the max-turns remedy a ConfigMap change if the envelope turns out to say error_max_turns. Co-Authored-By: Claude Opus 5.5 <[email protected]>
4.9 KiB
4.9 KiB
date, branch, title
| date | branch | title |
|---|---|---|
| 2026-10-31 | fix/pm-agent-triage-exit | pm-agent triage fails with "claude exited 1:" and no message; log the CLI's stdout envelope so the cause is visible |
What changed
infra/agent/pm-dispatch.py,ask_pm(): on a non-zero exit the error now comes from a newdescribe_cli_failure(), not fromproc.stderralone. With--output-format jsonthe CLI reports its own failures as the result envelope on stdout and writes nothing to stderr, which is why both failures today loggedclaude exited 1:with an empty message. The new line carries the envelope'ssubtype,is_error,api_error_status,num_turns,duration_ms,total_cost_usd,stop_reason, itsresult/errortext, and the tails of stdout and stderr. It names silence explicitly when both streams are empty.redact(): everything in that message, and inextract_decision()'s "no decision object" error, passes through it. It coverssk-ant-…,Bearer/tokenvalues, anduser:pass@in bothhttp://andhttps://URLs.PM_DECISION_MAX_TURNS: a new knob. It defaults to 1, today's value, so behaviour is unchanged, and it is bounded to 1–5. If the newly visible envelope sayserror_max_turns, the remedy is a ConfigMap change rather than another image build. Documented inplatform/pm-agent/README.md.- Selftest: four new assertions. They cover an empty stderr with the cause on stdout, a stderr-only failure, total silence, and redaction.
What I verified
- Timeline (Loki,
{namespace="pm-agent"}, andkubectl logs):- 16:37:38 to 16:38:00: pod
pm-agent-898fd8f5f-dp5hs, image c4eba5b3, config-generation 2, dry run. It triaged #97 in 22 s, loggedPM says comment, and posted the 16:38:00Z comment. The thread then held only the issue body. - 16:43:09 to 16:44:00: pod
pm-agent-f7c9b7fcf-j8mzn, image df36d9b7, config-generation 3, still dry run. It loggedclaude exited 1:with an empty message after 51 s. - 16:55:42 to 16:56:36: pod
pm-agent-7796db8bcb-p5dsm, df36d9b7, config-generation 4, live. The same empty failure, after 54 s. - The only successful triage ever was the 16:38 one. Every earlier
898fd8f5f/5db55d558clog line is a startup crash (GiteaGET /user401), not a triage.
- 16:37:38 to 16:38:00: pod
- Going live is NOT the cause. The first failure happened in dry run.
ask_pm()has no dry-run branch: the prompt, flags, tools (--tools "") and cwd are identical in both modes.PM_DRY_RUNgates onlycreate_job(), which runs after the model has answered. - Ruled out, with evidence:
- Timeout: 51 to 54 s against a 900 s limit, and a timeout raises
TimeoutExpired, not rc 1. - Budget cap: the PM call passes no
--max-budget-usd. - Prompt size: the thread is the issue body (293 chars) plus three comments (1871 + 404 + 77 chars), and the persona is 8 KB.
- Writable HOME: HOME=
/workspace(emptyDir), andCLAUDE_CONFIG_DIR=/workspace/.claudewas written at 16:56. - Egress: a TLS 1.3 handshake to
api.anthropic.com:443from the dispatcher pod succeeds in 0.06 s. - Model name and CLI version: the same
claude-opus-5and CLI 2.1.285 succeeded at 16:38. Between c4eba5b and df36d9b,infra/agent/differs only by a template selftest assertion and the toolbox base digest.
- Timeout: 51 to 54 s against a 900 s limit, and a timeout raises
- A hint, not proof:
/workspace/.claude/.claude.jsonin the live pod hascachedExtraUsageDisabledReason: 'out_of_credits'. No token was read or printed. - Offline selftest:
PM_SELFTEST=1against the worktree's template and persona printsselftest OK, including the 4 new assertions. - Toolbox gate: see the PR description for the rc and the stages that were skipped.
What I did NOT verify
- The root cause. UNVERIFIED. The CLI's own message was discarded,
--no-session-persistenceleaves no transcript, and I did not runclaudein the pod. The candidates still open:- Usage or rate limit on the subscription seat. The seat is shared with the operator's
interactive use. The
out_of_creditsextra-usage flag fits this. So does a ~50 s run, which matches the CLI exhausting its 429/529 retries. error_max_turns. With--max-turns 1and--json-schema, an answer that fails the schema cannot be retried. The failing thread now invitesimplement(a branch plus a long brief), where the 16:38 one produced a shortcomment. ~50 s is about two generations of the 22 s success.
- Usage or rate limit on the subscription seat. The seat is shared with the operator's
interactive use. The
- The 17:11 retry will NOT reveal the cause. It runs the old image, which still discards
stdout. The cause becomes visible only after this PR merges, CI re-publishes agent-runner,
and a follow-up PR bumps the dispatcher digest (
deployment.yamland thePM_WORKER_IMAGEinconfigmap.yaml, plus aconfig-generationbump). - That the CLI's error envelope carries exactly these key names. The parser takes whichever keys are present and falls back to raw tails.