Hmbown / Hmbown/Codewhale

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

Open
#6,015 8 comments 0 reactions 1 assignee Claimed by @Hmbown View on GitHub
documentation enhancement
Dominant language
Rust
Stars
41k
Forks
3.6k
Avg merge
13h 59m
Merged PRs (30d)
299

Description

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:**

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 [#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)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.