Current Core scope (September 9): adaptive anti-stall and safe read-only shell grammar are admitted in C05/C06 of the Core plan. The earlier postrelease-only/no-broader-grammar restriction is superseded. Preserve one tool/permission authority, test actual stall/recovery behavior, and use model-led effort/resource decisions rather than a new keyword classifier. C20 separately decides the release cut.
Problem
Read-only fleet sub-agents (Scout/Reviewer/Planner) stall and burn tokens on today's defaults — no per-user [subagents] edit should be required to fix this.
Rôles recap: 8 roles (role.rs:64-98): Worker (full), Scout/Planner/Reviewer (read-only, role.rs:712-720 — spawn narrows them to a read-only shell), Builder (full), Verifier (dedicated Run surface), Consultant (shell-less even among read-only, role.rs:88, executor.rs:245), Custom (inherits parent). Agent surface maps through a separate alias layer (identity.rs:355-362: explore→scout, implement→builder, general→worker). Per-role deny lists exist (role.rs:14-320); inherit_disallowed_tools: false can never drop a posture denial (SUBAGENTS.md:100, pinned by fleet/exact.rs test) — read-only stays read-only through delegation.
Four root problems:
- Read-only shell grammar too narrow.
command_safety.rs:361 allowlists 21 prefixes + gh … view/list; charset gate (:465+) rejects any command containing ; & | > < $ * ? [ ] { }or newline.sed, awk, find, pipes, cd &&impossible.sed -n '300,400p' f` → rejection → retries → stalls.
- Rejection isn't treated as no-progress. The agent retries bash variants; stall not detected.
- Model-step defaults unbounded.
[subagents] default_max_steps = 0 = unbounded (SUBAGENTS.md:423-433; FLEET.md:502-503 "must not synthesize"); DEFAULT_MAX_MODEL_STEPS=200 exists only for the exec path (turn_budget.rs:40), fleet workers get None (executor.rs:202). Wall-clock does have a 1800s default — model steps do not.
- Allow-vs-reject contradiction. Read-only role surface permits
exec_shell (executor.rs:325 allow/deny lists empty by default), the grammar then rejects it → model confused, keeps trying.
Real case (2026-09-08, bug-hunt):
- "errors" lens, ~18 files → 85 model steps, ~6M provider input-tokens, repeated context compaction, no report (kept retrying grammar-rejected bash; steps 80-84 alternated read_file/bash/bash/bash).
- "bugs" lens, same task → 30 steps, ~1.4M, via
read_file, clean report.
- Why 6M: quadratic growth — each step resends the full growing transcript (169 msgs), compactions re-enter as fresh context; provider
input_tokens sums all resends. Real generation was ~35k tokens — the cost is stall + resend, not "the model thought a lot".
Proposed solutions (options for maintainers)
- A. Adaptive anti-stall detector (core). Same action / same rejection N× consecutively (same command shape or error signature) → stalled → force strategy-switch, or stop-with-partial-report / ask operator. No fixed budget → doesn't chop agents making real progress. Needs precise "same action" definition; false positives route to strategy-switch, not kill.
- B. Widen read-only grammar. Add
sed -n (never -i), awk (no write), find, sort, uniq, diff; relax | only for allowlisted read-only pipelines; keep banning ; && $() > < and globs. Every addition must be proven mutation-proof.
- C. Rejection-aware role prompt. Read-only roles: "grammar rejection = hard no → use
read_file/grep_files/fd". Cheapest; behavior-only.
- D. Truth the surface. Deny
exec_shell for these roles (a posture denial — cannot be dropped by inheritance) → removes contradiction #4.
- E. Fallback budgets — NOTE: conflicts with a stated invariant.
FLEET.md:502-503 ("must not synthesize a default step budget"). Alert+checkpoint defaults would change that invariant deliberately; flag for explicit decision. Only if A is rejected.
- F. Combination. A+B strongest (mechanism + tooling); C+D cheap first wave; E only if A rejected (and the invariant consciously changed).
- G. Text-review path. Text review (issues, docs, design proposals) is a different job from code review; the fleet
reviewer role is code-oriented and read-only-shell-constrained. Route document/issue reviews to a general/custom sub-agent with explicit bounds ("no bash, no code verification", step/output limits, strict output template), or add a dedicated text-review role. Removes the same stall causes from document work.
Use case
Bug-hunt reviewers, same task: 85 steps / 6M / no report (retrying rejected bash) vs 30 steps / 1.4M via read_file. Defaults should catch the first case for everyone — without per-user config, and without chopping agents that are doing real work.
Alternatives considered
- Per-user
[subagents] limits: local patch; asks everyone to fix defaults; hard budgets chop working agents.
- Grammar-only (B): removes tool stalls, not "re-reading same files" stalls.
- Prompt-only (C): not a mechanism.
- Do nothing: ~6M tokens per stall, no report.
Impact
Anyone running read-only fleet review/exploration (bug-hunt, security-review, research). Today: unbounded cost per stall (observed 6M tokens, no report). Compatibility: explicit positive budgets must keep winning; the stall detector stops on no-progress, not on budget — preserving "omitted = unbounded".
Open questions
- A: define "same action" — command-shape hash? tool-call + first-arg prefix? rejection signature? (avoid false positives on legit repeated calls).
- A: stall action default — force strategy-switch vs partial-report vs ask operator?
- B: allow
| only when both sides are allowlisted read-only? $(...) in read-only context (e.g. cat $(git ls-files))?
- D: deny
exec_shell entirely vs keep the classifier-bounded shell (executor.rs:245 design)?
- E: since it conflicts with
FLEET.md:502 ("must not synthesize"), is changing that invariant acceptable — and what are the alert/kill semantics?
- Should the anti-stall detector also cover non-read-only roles (Worker/Builder) for "same error retry" loops?
- Text review: dedicated text-review role vs documented recommendation to use
general/custom with explicit "no bash/no code" bounds — which default?
References (verified in fork, upstream/main)
crates/tui/src/command_safety.rs:361 (allowlist), :465+ (charset gate)
crates/tui/src/fleet/role.rs:64-98 (roles), :88 (Consultant shell-less), :712-720 (read-only), :14-320 (deny lists)
crates/tui/src/fleet/identity.rs:355-362 (surface aliases)
crates/tui/src/fleet/executor.rs:202 (max_turns None), :245 (classifier-bounded shell; consultants shell-less), :325 (allow/deny)
crates/tui/src/core/engine/turn_budget.rs:40 (DEFAULT_MAX_MODEL_STEPS=200, exec only)
docs/SUBAGENTS.md:100 (posture denial not droppable), :423-433 (zero = unbounded; 1800s wall)
docs/FLEET.md:502-503 ("must not synthesize a default step budget")
- Real case: 85 steps / ~6M / no report vs 30 steps / ~1.4M (2026-09-08)
Current Core scope (September 9): adaptive anti-stall and safe read-only shell grammar are admitted in C05/C06 of the Core plan. The earlier postrelease-only/no-broader-grammar restriction is superseded. Preserve one tool/permission authority, test actual stall/recovery behavior, and use model-led effort/resource decisions rather than a new keyword classifier. C20 separately decides the release cut.
Problem
Read-only fleet sub-agents (Scout/Reviewer/Planner) stall and burn tokens on today's defaults — no per-user
[subagents]edit should be required to fix this.Rôles recap: 8 roles (
role.rs:64-98): Worker (full), Scout/Planner/Reviewer (read-only,role.rs:712-720— spawn narrows them to a read-only shell), Builder (full), Verifier (dedicated Run surface), Consultant (shell-less even among read-only,role.rs:88,executor.rs:245), Custom (inherits parent). Agent surface maps through a separate alias layer (identity.rs:355-362:explore→scout,implement→builder,general→worker). Per-role deny lists exist (role.rs:14-320);inherit_disallowed_tools: falsecan never drop a posture denial (SUBAGENTS.md:100, pinned byfleet/exact.rstest) — read-only stays read-only through delegation.Four root problems:
command_safety.rs:361allowlists 21 prefixes +gh … view/list; charset gate (:465+) rejects any command containing; & | > <$ * ? [ ] { }or newline.sed,awk,find, pipes,cd &&impossible.sed -n '300,400p' f` → rejection → retries → stalls.[subagents] default_max_steps= 0 = unbounded (SUBAGENTS.md:423-433;FLEET.md:502-503"must not synthesize");DEFAULT_MAX_MODEL_STEPS=200exists only for the exec path (turn_budget.rs:40), fleet workers getNone(executor.rs:202). Wall-clock does have a 1800s default — model steps do not.exec_shell(executor.rs:325allow/deny lists empty by default), the grammar then rejects it → model confused, keeps trying.Real case (2026-09-08, bug-hunt):
read_file, clean report.input_tokenssums all resends. Real generation was ~35k tokens — the cost is stall + resend, not "the model thought a lot".Proposed solutions (options for maintainers)
sed -n(never-i),awk(no write),find,sort,uniq,diff; relax|only for allowlisted read-only pipelines; keep banning; && $() > <and globs. Every addition must be proven mutation-proof.read_file/grep_files/fd". Cheapest; behavior-only.exec_shellfor these roles (a posture denial — cannot be dropped by inheritance) → removes contradiction #4.FLEET.md:502-503("must not synthesize a default step budget"). Alert+checkpoint defaults would change that invariant deliberately; flag for explicit decision. Only if A is rejected.reviewerrole is code-oriented and read-only-shell-constrained. Route document/issue reviews to ageneral/customsub-agent with explicit bounds ("no bash, no code verification", step/output limits, strict output template), or add a dedicated text-review role. Removes the same stall causes from document work.Use case
Bug-hunt reviewers, same task: 85 steps / 6M / no report (retrying rejected bash) vs 30 steps / 1.4M via
read_file. Defaults should catch the first case for everyone — without per-user config, and without chopping agents that are doing real work.Alternatives considered
[subagents]limits: local patch; asks everyone to fix defaults; hard budgets chop working agents.Impact
Anyone running read-only fleet review/exploration (bug-hunt, security-review, research). Today: unbounded cost per stall (observed 6M tokens, no report). Compatibility: explicit positive budgets must keep winning; the stall detector stops on no-progress, not on budget — preserving "omitted = unbounded".
Open questions
|only when both sides are allowlisted read-only?$(...)in read-only context (e.g.cat $(git ls-files))?exec_shellentirely vs keep the classifier-bounded shell (executor.rs:245design)?FLEET.md:502("must not synthesize"), is changing that invariant acceptable — and what are the alert/kill semantics?general/customwith explicit "no bash/no code" bounds — which default?References (verified in fork, upstream/main)
crates/tui/src/command_safety.rs:361(allowlist),:465+(charset gate)crates/tui/src/fleet/role.rs:64-98(roles),:88(Consultant shell-less),:712-720(read-only),:14-320(deny lists)crates/tui/src/fleet/identity.rs:355-362(surface aliases)crates/tui/src/fleet/executor.rs:202(max_turns None),:245(classifier-bounded shell; consultants shell-less),:325(allow/deny)crates/tui/src/core/engine/turn_budget.rs:40(DEFAULT_MAX_MODEL_STEPS=200, exec only)docs/SUBAGENTS.md:100(posture denial not droppable),:423-433(zero = unbounded; 1800s wall)docs/FLEET.md:502-503("must not synthesize a default step budget")