Repository navigation
Description
Refs: #7644
Summary
Run 39089680523 (work-resolve-issue, issue #7644) ended with:
work-resolve-issue exited with: failure class=max-turns credential=CLAUDE_CODE_OAUTH_TOKEN turns=140 max-turns=75 models=claude-sonnet-5-5 (infrastructure error, not a code-quality signal — e.g. usage limit, OIDC failure, SDK crash, is_error result, or max-turns exhaustion).
Two problems:
- The run went to 140 turns under
--max-turns 75, and nothing in the docs explains how. - A max-turns result is reported as an infrastructure error. It means the work is bigger than one run allows, so it is a sizing stop.
Where turns= comes from
turns= is the SDK result message's num_turns. Nothing in this code counts assistant messages or subagent transcripts.
- ci-workflows
report-lane-outcome(pinned at v0.43.0,64fd515; the action directory is unchanged at v0.45.0),.github/actions/report-lane-outcome/classify.cjs:206:const turns = result?.num_turns;read from the lasttype == "result"message, printed atclassify.cjs:233asturns=${turns}. max-turns=is the caller's input (pr-run-activity-write.yml:1119,max-turns: ${{ steps.config.outputs.max-turns }}), 75 from thestandardprofile (docs/conventions/pr-pipeline.yaml:259). The run log showsclaude_args: ... --max-turns 75.classify.cjs:131-137assignsmax-turnswhensubtype === "error_max_turns"or whensubtype === "success"andnum_turns > maxTurns. This run hit the second branch. The log shows{"subtype":"success","is_error":false,"num_turns":140,...}.- The failure itself comes from claude-code-action at
a26b07d(v1.0.250),base-action/src/run-claude-sdk.ts:241-250. That code throwsClaude reported a successful result after 140 turns, exceeding the configured maximum of 75on a success result whosenum_turnsis overmaxTurns. The CLI did not stop at 75. It finished, and the action failed the step afterwards.
What the docs say
--max-turns(https://code.claude.com/docs/en/cli-reference): "Limit the number of agentic turns (print mode only). Exits with an error when the limit is reached. No limit by default."maxTurns(https://code.claude.com/docs/en/agent-sdk/agent-loop): "You can cap the loop withmax_turns/maxTurns, which counts tool-use turns only." The limits table gives it as "Maximum tool-use round trips". When the limit is hit, "the SDK returns aResultMessagewith a corresponding error subtype (error_max_turns...)".num_turns(https://code.claude.com/docs/en/agent-sdk/typescript,SDKResultMessage): the field is listed asnum_turns: numberwith no definition. The same page does define its neighbours:usageis "main agent loop only. Excludes subagent and auxiliary model calls", andmodelUsage"includ[es] the main loop, subagents, and internal calls". It does not say which of these scopesnum_turnsuses.
Is 140 > 75 explained?
No. The docs say a run that reaches the cap stops with error_max_turns. This run returned success at 140. One possible cause is that num_turns counts subagent or non-tool-use turns while the cap counts only main-loop tool-use turns. That is UNVERIFIED: the docs do not say it, and the lane does not keep the execution file (only reply.txt and act-turns.json are uploaded, pr-run-activity-write.yml:1121-1131), so main-loop and subagent turns for this run cannot be counted afterwards. A one-turn difference (the final non-tool turn) cannot explain a gap of 65.
How the outcome is reported
- ci-workflows
.github/actions/report-lane-outcome/action.yml:237-245: any class other thanrate-limitgets the text(infrastructure error, not a ${signal} signal — e.g. usage limit, OIDC failure, SDK crash, is_error result, or max-turns exhaustion). That puts max-turns in the same group as OIDC failures and SDK crashes. pr-run-activity-write.yml:1671and1679-1680write the class intolane.jsonas{branch, class, resets}..github/scripts/work-resolve-issue/report-outcome.mjs:629-641(readLimit) reads onlyclass == "rate-limit". Every other class,max-turnsincluded, gets no special handling.report-outcome.mjs:434-(decide): an unread outcome becomesstoppedand goes tohandBack(report-outcome.mjs:267-302). That function posts<!-- work-resolve-issue outcome=stopped -->with the generic text "stopped partway through this issue and needs a person", and adds the human-gated label (needs-humanby default,report-outcome.mjs:442). Neither the comment nor the labels mention turns.- In this run the skill had already written a
doneoutcome and committed, so report-outcome opened #7896 ("report-outcome: opened PR refactor(hooks): remove the unused --skip-if-all-false flag from exec-bash.mjs #7896 for issue refactor(hooks): remove the unused --skip-if-all-false flag from exec-bash.mjs and its 25 copies #7644"). Theresolve-issue / runcheck is still red and labelled an infrastructure error, even though the work completed.
Fix (proposed)
- ci-workflows
report-lane-outcome: drop "max-turns exhaustion" from the infrastructure-error text. Giveclass=max-turnsits own message, for example<lane> stopped: turn budget used (turns=N max-turns=M); the work is too big for one run, split it. Keepclass=max-turnsas the bare token. report-outcome.mjs: readclass == "max-turns"fromlane.json(next toreadLimit).- When the run left no readable outcome, post
outcome=stoppedwith a note like "too big for one run (turns=N of M); split it into smaller issues". Addneeds-triageinstead of a bare hand-back. Removeagent-ready, so the lane does not retry the same oversized issue. - When the run left a
doneoutcome with commits, as in this run, keep opening the PR. Put a line in the body saying the run went past its turn budget.
- When the run left no readable outcome, post
- Keep the execution file, or a projection of main-loop and subagent turn counts from it, as a run artifact, so the next 140-of-75 case can be explained from evidence.
- Separately, find out why
num_turnswas 140 under--max-turns 75: check the CLI or SDK source for whatnum_turnscounts, and file upstream if the cap and the count disagree.
Verification (how to test)
- ci-workflows: add a
classify.test.cjs/outcome-step.test.cjscase for asuccessresult withnum_turnsovermaxTurns. Assert the annotation hasclass=max-turnsand does not contain "infrastructure error". Repeat for subtypeerror_max_turns. report-outcome.test.mjs: withlane.json{class: "max-turns"}and nowork-outcome.json, assertdecidereturnsexit: "stopped", a comment with the markeroutcome=stoppedand the split note,addLabelswith the triage label, andremoveLabelswithagent-ready. With adoneoutcome and a branch, assert it still returnsopen-pr.- Re-run the lane on an issue sized to exceed 75 turns. Check the issue comment, the labels, and the check-run text.
Related
- Evidence run: https://github.com/melodic-software/claude-code-plugins/actions/runs/39089680523
- PR the run opened: refactor(hooks): remove the unused --skip-if-all-false flag from exec-bash.mjs #7896
- ci-workflows
report-lane-outcomeaction (classifier and annotation text) .github/workflows/pr-run-activity-write.yml,.github/scripts/work-resolve-issue/report-outcome.mjs
🤖 Generated with Claude Code
Activity
github-actions commentedon Oct 18, 2026
Proposed work class: work-class: scoped
Basis: automated intake triage; the lanes App applied this class.
Size: Medium
Complexity: Medium
Uncertainty: Knowable
Flags: none
Blocked by: none
#7901 (comment)
That decision settles the open questions, so nothing is asked again. I understand the work as: give
class=max-turnsits own report text, flag over-cap runs in the PR body, split the act-turns counts into main-loop and subagent counts in a shared script, and bump the pins. The one host-run step, readingact-turns.jsonfrom an over-cap run, is already recorded in the decision.Automated triage, applied by the lanes App.
kyle-sexton commentedon Oct 18, 2026
Adding a progress heartbeat to this issue's scope.
The lane reports turns only when a run ends, so a run that hits the job timeout leaves no turn count and no diagnosis. On 2026-10-18:
For comparison, the 40 most recent successful resolve-issue jobs took p50 3.5 min, p90 10 min and max 15.4 min. The timeout is being cut to 25 minutes in a separate PR.
Proposal: alongside uploading the execution file, write the running turn count and the last tool call to a small file every N turns, and upload it with
if: always(). A timed-out or cancelled run then shows how far it got and what it was stuck on.feat(pr-pipeline): budget issue runs by issue fields and resume cappe…
kyle-sexton commentedon Oct 20, 2026
Remaining scope after #8253 and #8281, checked criterion by criterion against main at 419f77d.
Already done: the turn-cap marker, the hand-back and
agent-readyremoval, sizing by issue fields, and resume (#8281); an over-cap success no longer fails the check (#8253). One deviation from the original brief: a turn-cap stop adds needs-human rather than needs-triage (report-outcome.mjs:387-416).Remaining:
report-lane-outcome: drop "max-turns exhaustion" from the infrastructure-error annotation (action.yml~268-269 on main) and giveclass=max-turnsits own "stopped: turn budget used (turns=N max-turns=M)" text, withclassify.test.cjs/outcome-step.test.cjscases for an over-capsuccessand forerror_max_turns. Then bump this repo's pin (pr-run-activity-write.yml:1422, still v0.43.0).donewith commits but over its cap (now recorded as a plain success by fix(pr-pipeline): report an over-cap successful run as success #8253), say so in the PR body. That needs the act-result step to pass an over-cap flag through to report-outcome.if: always()), so a cancelled or timed-out run shows how far it got.num_turnscounts compared with--max-turns(CLI/SDK source), and file upstream if they disagree.kyle-sexton commentedon Oct 25, 2026
Released for re-triage under the owner's 2026-10-24 rule: an issue stops for a person only for an intent or vision question, a security call, a host or attended run, or spend outside prepaid credit. The hold label came off and intake triage runs again now; it re-applies a hold, with a numbered question, when one of those applies.
To hold this issue again, add
needs-human(orstatus: needs-decision) and say why in a comment.9 remaining items
kyle-sexton commentedon Oct 31, 2026
Decision recorded.
This answers the four open questions in intake's refinement (comment 6392103943), read against origin/main
0ad858ec05. It replaces the 2026-10-26 record (comment 6407928079), which quoted only general standing direction and moved item 1 to a ci-workflows issue that was never filed. No branch and no PR exists for this issue in either repository:fix/max-turns-annotation-7901andfix/max-turns-sizing-stop-7901both return 404. The PRs that did #7901 work are #8253 and #8281, already counted done in the 2026-10-20 comment. #8280 also references it; it made triage set the issue fields. Agents decided this on evidence under standing direction §1.report-lane-outcomeat ci-workflows2476d5d(pr-run-activity-write.yml:1821). At that commit, every class but rate-limit still gets the infrastructure-error text ending "is_error result, or max-turns exhaustion" (ci-workflows.github/actions/report-lane-outcome/action.yml:375-376).pr-run-activity-read.yml:1529). It is related, not a replacement: it has no main-loop and subagent split, and item 3 produces that split here.num_turnsis listed with no definition (TypeScript reference,SDKResultMessage). That does not explain 140 turns under a cap of 75, so the item stays open. Item 3's split is the evidence it needs.act-resultstep. The issue lane's skill runs in the read runner on run-claude-cli (pr-run-activity-read.yml:1529,:1581,:1627), and the write runner refuses an issue skill (pr-run-activity-write.yml:790-797). The read runner has noact-resultstep; the write runner's returns at once on a successful skill step (pr-run-activity-write.yml:1635-1638).act-turnsstep, which runs when whichever leg ran succeeded or failed (pr-run-activity-read.yml:1723-1730) and already sets aturnsoutput fromnum_turns(:1799). The step that buildslane.jsonfrom step outputs writesmax-turnsbut noturns(:2015,:2034-2037); it addsturns, and the flag isturnsovermax-turns.report-outcome.mjsreads the flag next to its turn-cap reader (.github/scripts/work-resolve-issue/report-outcome.mjs:1528-1542).donerun's PR body says nothing about the cap. The run's turns and cap reach only the check run (.github/actions/report-check-run/report-check-run.mjs:106).act-turns.json: assistant messages in the execution file whoseparent_tool_use_idis null (main loop), and those where it is set (subagents). This repository already uses that field to mark subagent events (plugins/harness-ops/skills/behavior-probes/scripts/probe.py:410).pr-run-activity-write.yml:1759-1764).pr-run-activity-read.yml:1723-1809,pr-run-activity-write.yml:1699-1796). A fixture test therefore means moving its logic into a script under.github/scripts/that both runners call.lane.cjs:776-792). That comment also says a job cancel or step timeout still loses the turn record.pr-run-activity-read.yml:1529), so the leg-end write applies to it: a leg timeout keeps the turn record, and a job cancel, as in fix(planning): collapse archived cards in the interview page's Groups rail #7750's run, does not.Acceptance criteria:
donewith commits andnum_turnsover its--max-turnsopens its PR with a body line saying the run went past its turn budget (turns=N max-turns=M). Areport-outcome.test.mjscase asserts that line and thatdecidestill returnsopen-pr, and it passes in CI..github/scripts/that both the read and write runners call.act-turns.jsoncarries the count of assistant messages with a nullparent_tool_use_idand the count with one set, under the same 32768-byte cap. A test with a fixture holding both kinds asserts both counts, and it passes in CI.report-lane-outcomegivesclass=max-turnsits own text naming the turns and the cap, and its infrastructure-error text no longer lists max-turns exhaustion.classify.test.cjsoroutcome-step.test.cjscases cover an over-capsuccessand anerror_max_turnsresult, and they pass in ci-workflows CI.report-lane-outcomepin in this repository moves to a ci-workflows release that carries that text.Host-run step, outside the criteria: once item 3 ships, read
act-turns.jsonfrom one over-cap run and state in this issue the main-loop and subagent counts against its cap. That answers item 4 from evidence. No upstream report follows: the owner ruled "no upstream issues to 3rd party / external repos" (2026-10-30), so the result is recorded in this issue only. Until that step is done, a PR for these criteria writesRefs: #7901.Route: builder session. The act-turns move and the over-cap flag edit workflow files, which the lanes App cannot push, and the annotation text is a ci-workflows change. Labels: add
host-run.status: confirmedstays. Noagent-ready.kyle-sexton commentedon Nov 1, 2026
Claimed by the Backlog HITL host session; work in progress.
fix(pr-pipeline): put the setup-env comment back over its own step
kyle-sexton commentedon Nov 1, 2026
Host run. The Backlog HITL host session built this repository's part of the 2026-10-31 decision: the PR-body note for a done run past its cap, and the main-loop and subagent turn counts. The ci-workflows part is written up below for Automation Lanes.
What ran (Claude Code 2.1.316)
fix/max-turns-sizing-stop-7901was 1282 commits behind and kept the logic inline, so it was used only as a reference.scripts/check-affected.sh --testson the committed head: 11 static checks passed. The 68 selected suites are left to CI because the workflow files changed. The directly affected suites, run locally, all passed:report-outcome.test.mjs,callers.test.mjsand.github/scripts/lib/act-turns.test.mjs: 572 pass.scripts/act-turns-denied.test.sh: 63 pass, 0 fail.Acceptance criteria
report-outcome.mjsreadsturnsbesidemax-turnsfromlane.json(readOverCap). The read runner now writesturns. A done run past its cap gets the Summary lineThe run went past its turn budget (turns=N max-turns=M), anddecidestill returnsopen-pr; a test asserts both..github/scripts/lib/act-turns.mjs, which both runners call..basebefore the head checkout and records its SHA-256. The step loads only bytes that match.act-turns.jsonnow carriesturn_counts:main_loop: assistant messages with a nullparent_tool_use_id.subagent: assistant messages with one set.report-lane-outcometext. Not done: this belongs to Automation Lanes, so no ci-workflows PR was opened. The change needed is already drafted on ci-workflows branchfix/max-turns-annotation-7901(local commit587c78b, never pushed; it is 35 commits behind main):.github/actions/report-lane-outcome/action.yml, aclass=max-turnsfailure reads<lane> stopped: turn budget used (<lane> exited with: <outcome> class=max-turns turns=N max-turns=M ...); the work is too big for one run, split it.class=max-turnstoken and theexited with:phrase stay.outcome-step.test.cjsgets cases for an over-capsuccessresult and anerror_max_turnsresult, plus updated snapshots.report-lane-outcomepin moves to it: 2 inpr-run-activity-read.ymland-write.yml, and 7 inpr-review-main.yml, all at2476d5dtoday.Host-run step. Not run yet. Once this PR merges, read
act-turns.jsonfrom one over-cap run and state its main-loop and subagent counts against its cap.Process note. The claim comment on this issue was posted before the write gate cleared: the breaker had tripped at the time. Every later write waited on the gate.
PR: #11839 (
Refs: #7901), opened as a draft and then marked ready through the ready gates. Main is merged in, the verify gate passed on the merged head, and the code and security reviews were left to the CI lanes. Both review lanes passed onef5b122447and opened one thread. It was valid: the setup-env step's comment had ended up over the new act-turns copy step in the write runner. The fix is2cc2bdd102; the thread has a reply and is resolved.kyle-sexton commentedon Nov 1, 2026
Host-run step. I read
act-turns.jsonfrom one over-cap run after #11839 merged (2026-11-01T06:03Z).The run: 41313113933, issue lane on #11881. It opened #11934, whose body carries the new "went past its turn budget" line.
lane.jsonmax-turnslane.jsonturnslane.jsonclassact-turns.jsonsubtypesuccessact-turns.jsonnum_turnsact-turns.jsonturn_countsmain_loop/subagent(assistant messages)act-turns.jsonturn_countsmain_loop_turns/subagent_turns(distinct model turns)act-turns.jsondenied_totalAgainst the cap:
num_turns121 against a cap of 113 and still endedsuccess, noterror_max_turns.num_turnsadds other turns on top. It is a single run, so this is UNVERIFIED. Main-loop plus subagent model turns (110) do not add up to 121.Criteria 4 and 5 (the ci-workflows
report-lane-outcometext and the pin bump) belong to Automation Lanes, on ci-workflows587c78b. With this step done, no host-run work is left, sohost-runcomes off.Target labels: add none; remove
host-run.kyle-sexton commentedon Nov 2, 2026
Closing as completed. Each acceptance criterion of the 2026-10-31 decision record, which replaced the earlier records, is met. Checked against origin/main
a33d0b315fand ci-workflowsc41e345on 2026-11-01.1. A done run over its cap gets a PR-body line, with a passing test. Met.
.github/scripts/work-resolve-issue/report-outcome.mjs:1386addsThe run went past its turn budget (turns=N max-turns=M)to the Summary whenreadOverCap(:1707) findsturnsovermax-turnsinlane.json.report-outcome.test.mjs:2392-2399asserts that line, its place in the Summary, and thatdecidestill returnsopen-pr.ci-statuspassed on head2cc2bdd102.2. A run within its cap gets no line, with a passing test. Met.
report-outcome.test.mjs:2401-2405assertsopen-prand that the body has no turn-budget text. It shipped in #11839 under the sameci-status.3. The act-turns script is shared, and
act-turns.jsoncarries the two counts. Met..github/scripts/lib/act-turns.mjsholds the logic..baseand load it:pr-run-activity-read.yml:1291,:1758andpr-run-activity-write.yml:1439,:1757.turnCounts(act-turns.mjs:23-33) splits assistant messages by a null or setparent_tool_use_id. The cap stays at 32768 bytes (CAP,:13).act-turns.test.mjs:33asserts both counts on a fixture that holds both kinds of message, and:59checks that they survive the cap.4. ci-workflows
report-lane-outcometext. Met.c41e345.class=max-turnsnow reads<lane> stopped: turn budget used (... class=max-turns turns=N max-turns=M); the work is too big for one run, split it..github/actions/report-lane-outcome/action.yml:369-385).outcome-step.test.cjscovers an over-capsuccessresult (the test at about line 424) and anerror_max_turnsresult (about line 443). Both assert that "infrastructure error" is absent.test-contractsjob (pr-require-checks.yml:552). That job andci-statuspassed on fix(guardrails): restore path-detection prefilter fail-open fix (0.9.8) #946 (run 41278770645).5. Every
report-lane-outcomepin moves to a ci-workflows version that carries the text. Met, at a main commit rather than a tagged release.ci-statuspassing, moved all 57 pins toc41e3457e01c9ff6ca2c3d10e5bf470551383e11.git grep "report-lane-outcome@"on origin/main finds nouses:line at any other SHA.c41e345; the newest release is v0.69.0, from 2026-10-31.2476d5d, was also an untagged main commit, and so are the other ci-workflows pins. ci: pin report-lane-outcome at c41e345 for the max-turns sizing stop #11964's "Pin choice" section gives that practice as the reason. This repository's runs therefore already use the new text.Host-run step (outside the criteria). Done. The 2026-11-01 record gives one over-cap run's counts against its cap:
num_turns: 121, against a cap of 113.Under the decision record, this step answers body item 4, and no upstream report follows. Two gaps remain open as notes, not criteria:
num_turnscounts.This was generated by AI.