Skip to content

feat(fleet): adaptive anti-stall + wider read-only shell grammar (defaults, not per-user config) #6015

Description

@7jrxt42BxFZo4iAnN4CX

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:

  1. 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.
  2. Rejection isn't treated as no-progress. The agent retries bash variants; stall not detected.
  3. 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.
  4. 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 Remove Normal mode and consolidate to Agent #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

  1. A: define "same action" — command-shape hash? tool-call + first-arg prefix? rejection signature? (avoid false positives on legit repeated calls).
  2. A: stall action default — force strategy-switch vs partial-report vs ask operator?
  3. B: allow | only when both sides are allowlisted read-only? $(...) in read-only context (e.g. cat $(git ls-files))?
  4. D: deny exec_shell entirely vs keep the classifier-bounded shell (executor.rs:245 design)?
  5. E: since it conflicts with FLEET.md:502 ("must not synthesize"), is changing that invariant acceptable — and what are the alert/kill semantics?
  6. Should the anti-stall detector also cover non-read-only roles (Worker/Builder) for "same error retry" loops?
  7. 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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationenhancementNew feature or request

    Projects

    • Status
      Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions