Three issues raised in review on #1217, all real:
- The STRIX_MAX_AGENTS check read agent_count() and then spawned, so two
parents racing for the last slot both passed before either child
registered and the graph overshot the cap. The coordinator now hands out
slots atomically (try_reserve_agent_slot / release_agent_slot), counting
outstanding reservations alongside live agents under the same lock. The
slot is released once the spawner has registered the child or failed.
Depth stays a plain check — a parent's depth cannot change mid-spawn.
- _trim_parent_history kept the newest item whole when that item alone
exceeded the budget ("and kept"), so the cap bounded nothing on the
child's first request and could overflow the provider context window. An
oversized newest item is now rendered as a truncated text message. No
tool-call pairing can break: nothing else survives that trim.
- --reasoning-effort exported STRIX_REASONING_EFFORT, which persist_current()
then wrote into cli-config.json, turning a documented per-run flag into
the default for every later run. Env vars can now be marked run-scoped and
are skipped when persisting.
Tests cover the concurrent reservation, slot release, the oversized-item
trim, and the persist exemption.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Autonomous scans can spawn unbounded agents, each re-paying the full
system prompt on every turn and (by default) inheriting a full copy of
the parent's history — a large token-cost driver on a single target.
This adds knobs to bound it, all OFF by default so out-of-the-box
behavior is unchanged.
New (opt-in via env, default 0 = disabled):
- STRIX_MAX_AGENTS — cap total agents in the graph; create_agent refuses
past the cap with a model-facing message to reuse/wait/self-serve.
- STRIX_MAX_AGENT_DEPTH — cap spawn depth (root = 1).
- STRIX_INHERIT_CONTEXT_MAX_TOKENS — trim inherited parent history to the
most-recent tail within a token budget.
Also:
- New --reasoning-effort CLI flag (overrides STRIX_REASONING_EFFORT per
run); pure addition, no default change.
Coordinator gains agent_count()/depth_of() helpers. Tests cover the caps
and the history trim. Docs updated. No default behavior changes.
* add extra-files plumbing so orchestrators can drop single files into the sandbox workspace
* reject extra-file paths that collide with a local source tree
* add --workspace-file so CLI users can place files in the sandbox workspace
* reject repeated and control-character workspace paths
* revalidate persisted workspace files when resuming a run
* drop the workspace-file size limit
The Exit Codes table in the CLI reference only listed 0 and 2, but the
CLI also exits with 1 on fatal errors (missing environment variables,
Docker unavailable, invalid config file, diff-scope resolution failure,
or an unhandled exception). It also implied exit 0 means no
vulnerabilities were found, which is only true in headless mode -
interactive runs always exit 0 regardless of findings.
Document exit code 1 and clarify the two cases for exit 0.
* fix: resolve pre-commit check failures
- Change RuntimeError to TypeError for type validation in report/writer.py
- Update pyupgrade to v3.21.2 for Python 3.14 compatibility
* chore: add pytest test infrastructure
Mirror the layout introduced on feature/438-token_budget: pytest +
pytest-asyncio dev deps, asyncio_mode auto, a tests.* mypy override, and
pytest in the mypy pre-commit hook deps so the tests/ package type-checks.
* feat: add --mount and large-target pre-flight for local repos (#492)
Large local targets were copied into the sandbox file-by-file via the SDK
LocalDir entry, which stalls on big repos and could leave /workspace empty.
- --mount <path> bind-mounts a host directory read-only at /workspace/<subdir>
instead of copying it, bypassing the per-file stream.
- A size pre-flight (STRIX_MAX_LOCAL_COPY_MB, default 1024) fails fast with a
clear message suggesting --mount when a non-mounted local target is too big.
* fix: reject empty --mount paths
An empty or whitespace-only --mount value resolves to the current working
directory and would silently bind-mount it into the sandbox. Reject it.
* fix: dedupe local targets so a dir is never both copied and mounted
If the same directory is passed via --target and --mount (or as duplicate
values), it previously produced two targets — copied AND bind-mounted, and
the copied one could trip the size pre-flight. Dedupe by resolved path,
preferring the bind mount.
* fix: treat non-positive STRIX_MAX_LOCAL_COPY_MB as disabled
Previously a value of 0 (or negative) made every local target count as
oversized, aborting all local scans. Now <= 0 disables the pre-flight.
* fix: log unreadable subtrees during size pre-flight
os.walk silently swallowed directory-listing errors, so a permission-denied
subtree could make a large repo under-count and slip past the pre-flight.
Surface such omissions via an onerror warning.
* docs: document --mount and STRIX_MAX_LOCAL_COPY_MB
Add CLI reference + example for --mount, document the size pre-flight env var,
note the read-only-is-not-a-hard-boundary caveat and that remote repos are not
size-checked, and clarify the backends docstring on when bind mounts apply.
* Update strix/interface/main.py
* Update strix/runtime/docker_client.py
---------
* fix: resolve pre-commit check failures
- Change RuntimeError to TypeError for type validation in report/writer.py
- Update pyupgrade to v3.21.2 for Python 3.14 compatibility
* feat(cli): add --max-budget-usd flag
Raises BudgetExceededError in ReportUsageHooks after each LLM call when
accumulated cost reaches the limit, with clean "stopped" status and
child-agent cancellation in non-interactive mode.
* test: add budget enforcement unit tests
7 tests covering no-budget, under-budget, at-limit, over-limit, error
message content, None report state, and exception hierarchy.
Also adds pytest/pytest-asyncio to dev deps and a mypy override for tests.
* fix(budget): validate positive budget and check the live cost ledger
Two hardening fixes for --max-budget-usd enforcement:
- Reject non-positive budgets. ReportUsageHooks now raises ValueError for
max_budget_usd <= 0, and the CLI validates the flag via a custom argparse
type so '--max-budget-usd 0' fails fast with a friendly message instead of
silently killing the scan on the first model response.
- Read the live cost. The budget check now reads ReportState.get_total_llm_cost()
(the live ledger) instead of the persisted run-record snapshot, so it stays
accurate even when a usage save fails after a model call.
* fix(budget): stop the entire scan deterministically when the limit is hit
Previously a BudgetExceededError was handled per-agent: it was swallowed in
interactive mode (the loop kept waiting), a child's error escaped its detached
task as an unretrieved-exception warning, the parent was never released from
wait_for_message, and the stop was logged at ERROR with a traceback as if the
agent had failed.
Replace that with a single scan-wide signal on the coordinator:
- AgentCoordinator.trigger_budget_stop() sets a flag and wakes every parked
agent; wait_for_message returns as soon as the flag is set.
- The run loops check coordinator.budget_stopped and raise to exit cleanly,
marking themselves 'stopped'. The root's exception reaches run_strix_scan's
handler, which cancels descendants and tears the scan down once; child
exceptions are swallowed in their detached task.
- The budget stop is logged at INFO, not as a failure.
This is deterministic regardless of tree depth or which agent first sees the
limit, fixing the interactive/TUI hang where a deep agent's stop never reached
a parked root. Also re-raises BudgetExceededError explicitly in the stream
handler so it can't be mistaken for the LiteLLM 'after shutdown' race.
* fix(budget): treat a budget stop as a clean stop in the TUI
Add an explicit BudgetExceededError handler in the TUI scan thread so that, if
the error ever reaches it, the budget stop is logged as a graceful stop rather
than surfaced as a red scan error by the broad 'except Exception'. The runner
normally absorbs the error and returns cleanly, so this is defensive depth for
a money-spending feature.
* docs(cli): document --max-budget-usd behavior and limitations
Clarify that the budget is cumulative across all agents, checked after each
model response, that the scan stops cleanly (not as a failure), that the value
must be > 0, and that spend can slightly overshoot due to in-flight calls and
best-effort cost estimation.
* Apply suggestions from code review
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>