ADR 0253: Remove the subagent turn limit
- Status: Accepted
- Date: 2026-09-15
- Deciders: PI-Desktop core
- Related: D423, ADR 0062, ADR 0063, ADR 0089, ADR 0119, ADR 0126, ADR 0166, ADR 0189, ADR 0210,
03-runtime/02-agent-runtime.md§5f,04-ux/06-settings-ia.md§7, E2E-155, E2E-SUBAGENT-legacy-turn-limit-frontmatter-is-ignored - Supersedes: the
maxTurnsclauses of ADR 0062, ADR 0063, ADR 0119, ADR 0126, ADR 0166, and ADR 0210. Historical ADR files are retained. This record removes the field those records described; it does not change their other clauses.
Context
ADR 0062 introduced subagents as bounded workers and gave a definition a maxTurns frontmatter cap. ADR 0119 made the key optional — omission, none and 0 meant unlimited, and an explicit value was clamped to 80. ADR 0126 and ADR 0063 carried the field into the parser and the Subagent editor. ADR 0166 then withdrew the idle and duration watchdogs but deliberately kept maxTurns as the remaining definition-level kill switch, and D328 recorded that "explicit maxTurns and the concurrency cap of 10 stay". ADR 0210 reused the same loose key-spelling rules for the sibling maxTokens cap.
The cap could not be reasoned about correctly, because the parent cannot see the delegate's live work. It cannot tell a delegate that is one turn from converging from one that never will, so the only honest cap would be "large enough", which is no cap at all. The shipped values were arbitrary (60, 50, 40, 80), the editor defaulted to unlimited anyway, and the failure mode was worse than the problem it solved: a delegate that reached the cap was killed mid-task and surfaced as truncated with a partial report — a state that reads as a failure to the user and to the parent model, and that neither can resume.
The kill paths that remain are all explicit and observable: the parent agent's TaskStop, the user's Stop, and a terminal parent error aborting leftovers (ADR 0189). A turn count is none of those.
Decision
Delete the mechanism. A delegate has no turn limit. It ends when it finishes, when the parent calls
TaskStop, when the user Stops, or when a terminal parent error aborts it. There is notruncatedoutcome and no turn-related termination path.Remove
maxTurnsfrom the contract. The field leavesSubagentDefinition, the frontmatter parser and its invalid/clamped warnings,MAX_SUBAGENT_MAX_TURNS,UserSubagentRecord/UserSubagentInput, the host-core registry (record, input, frontmatter parse, document render,MAX_TURNS_CEILING), the five built-in documents,SUBAGENT_PRESETS, and the Subagent editor's Advanced disclosure. The host-core input struct keeps ignoring unknown fields, so a renderer that still sendsmaxTurnsdoes not fail.Legacy documents keep loading.
maxTurns,max-turnsandmax_turnsbecome unrecognized frontmatter keys and are ignored exactly like every other unknown key: no error, no warning, the definition still resolves, and nothing rewrites the user's file. A definition that declared a cap therefore loses it silently.Remove the
truncatedstatus.SubagentRunStatusdrops it, and the renderer'sSubagentOutcomeunion, thechat.subagentStatuscatalog entry in every locale, and the delegation topology's "finished with warnings" count drop it with it.timed_outstays in the type even though the watchdogs that produced it are withdrawn (D328), because no other record withdraws it.No protocol and no schema change.
PROTOCOL_VERSIONstays 11 andSCHEMA_VERSIONstays 16. The removal is tolerated in both directions: an older renderer's extramaxTurnsfield is ignored by host-core (the input struct does not deny unknown fields), a newer renderer simply omits it, andtruncatedis a value the sidecar can no longer emit. Storage was never involved — the registry parses Markdown frontmatter and no column stores the cap.
Consequences
- A looping delegate now runs until the user Stops it, or the parent calls
TaskStop. That exposure already existed for every definition that omittedmaxTurns, which was the default and the editor's own starting state, and D328 shipped with it. - Existing user documents that relied on the cap lose it without a prompt. The migration path is the one this ADR documents: stop the delegate with Stop or
TaskStop, and put the stopping condition in the prompt body, where a delegate is already told when its work is done. - Settings no longer offers a "no limit" spelling for turns because there is no limit left to express. The Advanced disclosure keeps the model, thinking, output limit and scope fields.
- The built-in definitions lose one frontmatter line each and no longer carry a number that had no derivation.
- E2E-155 steps and the topology's warning count change with the status and the built-in backstops. The new ignore contract is covered by E2E-SUBAGENT-legacy-turn-limit-frontmatter-is-ignored and by unit tests in
packages/shared,packages/agent-runtime, andcrates/host-core.
Alternatives rejected
- Raise the ceiling, or make every builtin unlimited. Keeps a knob that cannot be set correctly, keeps the
truncatedstate and its user-visible copy, and still kills a delegate mid-task at an arbitrary point. - Keep parsing the key and warn on it. Unknown frontmatter keys are already ignored silently, so a warning would make
maxTurnsthe only key with a special diagnostic while the definition still ignored it. D328'sidle-timeout/max-durationprecedent is the opposite case: those keys still exist, they are simply not armed, so they keep their parse warnings. - Keep the field and never enforce it. A contract field that does nothing is worse than no field: the editor would keep offering it, the settings API would keep round-tripping it, and
truncatedwould stay in the status union as dead surface the UI must still render.