Skip to content

Handle large exec_command output and surface scan errors as TUI toasts#583

Open
mhspektr wants to merge 513 commits into
usestrix:mainfrom
mhspektr:feature/issue-579
Open

Handle large exec_command output and surface scan errors as TUI toasts#583
mhspektr wants to merge 513 commits into
usestrix:mainfrom
mhspektr:feature/issue-579

Conversation

@mhspektr

@mhspektr mhspektr commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

NB: this builds on #577 and should be merged after

  • Large output handlingexec_command results are now capped at STRIX_MAX_TOOL_OUTPUT_CHARS (default 65 536 chars). JSON arrays are trimmed to the first 50 records (valid JSON preserved); plain text keeps the first 300 lines. Both truncation paths include a header with total size and count so the agent retains full context awareness. Set STRIX_MAX_TOOL_OUTPUT_CHARS=0 to disable.
  • MapReduce LLM compression — when a single chunk still overflows after truncation, a MapReduce pipeline compresses it via the LLM before it reaches the context window, preventing hard overflows.
  • Scan error toasts — scan thread failures now post a persistent Textual toast immediately rather than surfacing as a post-exit traceback. A timeout=0 bug that caused the toast to expire on mount was also fixed (Textual's default ~5 sis now used).

Closes #579

0xallam and others added 30 commits January 22, 2026 13:09
Updates LiteLLM from ~1.80.7 to ~1.81.1 which includes
full support for z.ai (Zhipu AI) provider using the 'zai/model-name'
format. This enables Strix to work with z.ai subscription
credentials by setting STRIX_LLM="zai/glm-4.7" with appropriate
LLM_API_KEY and LLM_API_BASE environment variables.

Changes:
- Updated litellm version constraint in pyproject.toml
- No breaking changes to Strix API or configuration

Closes #ISSUE_ID (to be linked if applicable)

Signed-off-by: legendevent <legendevent@users.noreply.github.com>
The memory compressor was calling litellm.completion() without passing
the api_key and api_base parameters, causing authentication errors when
LLM_API_KEY is set but provider-specific env vars (OPENAI_API_KEY, etc.)
are not. This matches the pattern used in dedupe.py.
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.6.0 to 6.6.2.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](py-pdf/pypdf@6.6.0...6.6.2)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 6.6.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Remove hardcoded github.com/gitlab.com/bitbucket.org host lists from
infer_target_type. Instead, detect git repositories on any host by
querying the standard /info/refs?service=git-upload-pack endpoint.

Works for any self-hosted git instance.
Improve the finish_scan tool schema to produce more professional
pentest reports: expand parameter descriptions with structural
guidance, rewrite recommendations example with proper urgency tiers
instead of Priority 0/1/2, fix duplicated section titles, and clean
up informal language.
Enable native text selection across tool components and agent messages
with automatic clipboard copy, toast notification, and decorative icon
stripping. Replace Padding wrappers with Text to support selection
across multiple renderables.
Bumps [pillow](https://github.com/python-pillow/Pillow) from 11.3.0 to 12.1.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](python-pillow/Pillow@11.3.0...12.1.1)

---
updated-dependencies:
- dependency-name: pillow
  dependency-version: 12.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [cryptography](https://github.com/pyca/cryptography) from 44.0.1 to 46.0.5.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](pyca/cryptography@44.0.1...46.0.5)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 46.0.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [protobuf](https://github.com/protocolbuffers/protobuf) from 6.33.4 to 6.33.5.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: protobuf
  dependency-version: 6.33.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
… and CVSS

Replace 12 flat parameters (code_file, code_before, code_after, code_diff,
and 8 CVSS fields) with structured nested XML fields: code_locations with
co-located fix_before/fix_after per location, cvss_breakdown, and cwe.

This enables multi-file vulnerability locations, per-location fixes with
precise line numbers, data flow representation (source/sink), CWE
classification, and compatibility with GitHub/GitLab PR review APIs.
- Specify encoding="utf-8" in registry.py _load_xml_schema()
- Specify encoding="utf-8" in skills/__init__.py load_skills()
- Prevents cp949/shift_jis/cp1252 decoding errors on non-English Windows
… multi-part suggestions

Rewrote the code_locations parameter description to make fix_before/fix_after
semantics explicit: they are literal block-level replacements mapped directly
to GitHub/GitLab PR suggestion blocks. Added guidance for multi-part fixes
(separate locations for non-contiguous changes like imports + code), common
mistakes to avoid, and updated all examples to demonstrate multi-line ranges.
* feat: add to readme new keys

* feat: shoutout strix models, docs

* fix: mypy error

* fix: base api

* docs: update quickstart and models

* fixes: changes to docs

uniform api_key variable naming

* test: git commit hook

* nevermind it was nothing

* docs: Update default model to claude-sonnet-4.6 and improve Strix Router docs

- Replace gpt-5 and opus-4.6 defaults with claude-sonnet-4.6 across all docs and code
- Rewrite Strix Router (models.mdx) page with clearer structure and messaging
- Add Strix Router as recommended option in overview.mdx and quickstart prerequisites
- Update stale Claude 4.5 references to 4.6 in anthropic.mdx, openrouter.mdx, bug_report.md
- Fix install.sh links to point to models.strix.ai and correct docs URLs
- Update error message examples in main.py to use claude-sonnet-4-6

---------

Co-authored-by: 0xallam <ahmed39652003@gmail.com>
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 6.6.2 to 6.7.1.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](py-pdf/pypdf@6.6.2...6.7.1)

---
updated-dependencies:
- dependency-name: pypdf
  dependency-version: 6.7.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
0xallam and others added 20 commits June 9, 2026 02:48
- Change RuntimeError to TypeError for type validation in report/writer.py
- Update pyupgrade to v3.21.2 for Python 3.14 compatibility
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.
…ix#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.
An empty or whitespace-only --mount value resolves to the current working
directory and would silently bind-mount it into the sandbox. Reject it.
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.
Previously a value of 0 (or negative) made every local target count as
oversized, aborting all local scans. Now <= 0 disables the 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.
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.
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>
ContextWindowExceededError carries status_code=400, which matched the
_INPUT_REJECTION_CODES guard and triggered image-strip retry logic.
Image stripping cannot reduce token count, so the agent wasted up to
3 retry cycles before parking as failed.

Add an explicit isinstance check before the status-code guard to detect
context-window overflow and park the agent as 'failed' immediately.
Scan errors were stored in _scan_error and re-raised only after the
user closed the TUI, making the app appear stuck while producing a
confusing post-exit traceback.

Extract _notify_scan_error and call it from all three error branches
in _start_scan_thread so a persistent Textual toast is shown
immediately when the scan thread fails.
…missal

Textual's Notification expires when raised_at + timeout - time() <= 0.
With timeout=0 the toast expired immediately on mount, making the error
notification invisible to users. Drop the timeout argument to use
Textual's default display duration (~5 s).
exec_command output is now capped at STRIX_MAX_TOOL_OUTPUT_CHARS (default
65536). JSON arrays are parsed and trimmed to the first 50 records (valid
JSON preserved); plain text keeps the first 300 lines. Both include a
header showing the total size and record/line count so the agent retains
full awareness of what was produced.

A secondary character cap prevents single very long lines from bypassing
the line limit. Set STRIX_MAX_TOOL_OUTPUT_CHARS=0 to disable.
Replaces plain truncation with a parallel summarisation pipeline when
exec_command output exceeds STRIX_MAX_TOOL_OUTPUT_CHARS (default 65536).
Output is split at JSON record or line boundaries; each chunk is
summarised via litellm.acompletion; summaries are consolidated into a
single result that fits the context window.

Falls back to truncate_exec_result on any compression error.

Closes usestrix#579
Code review findings:

- factory._wrap_exec_command: add post-compression backstop that calls
  truncate_exec_result when compress_exec_result returns an oversized
  result (happens when _split produces 1 chunk, e.g. a single very
  large JSON record — compress_large_output correctly returns it
  unchanged, but the result was never guarded against threshold after).

- tests/agents/test_factory_helpers.py (new): unit tests for
  _resolve_model (run_config vs settings fallback, None/whitespace
  error cases, non-string Model object guard) and _extract_task_hint
  (valid cmd, missing cmd, non-string cmd, invalid JSON, non-dict JSON).
@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds three self-contained features on top of PR #577: structure-aware truncation and MapReduce LLM compression for oversized exec_command outputs, a ContextWindowExceededError fast-path in the agent run loop that bypasses the (unhelpful) image-strip retry, and cross-thread TUI toasts for scan-thread failures. It also adds a --mount CLI option that bind-mounts large local directories read-only instead of streaming them file-by-file.

  • Large output pipeline — outputs exceeding STRIX_MAX_TOOL_OUTPUT_CHARS are first compressed via a parallel MapReduce LLM fan-out; single-chunk outputs and compression failures fall back to structure-aware truncation (JSON arrays kept as valid JSON, plain text line-capped), with a final character-count backstop in _wrap_exec_command.
  • Context window bypassContextWindowExceededError (status 400) is now intercepted before the image-strip retry loop, parks the agent as "failed", and propagates correctly for root agents while returning None for child agents.
  • --mount bind-mount supportbuild_mount_targets_info / build_session_entries / StrixDockerSandboxClient are wired together to pass host directories to Docker at container-create time; a pre-flight size check rejects oversized --target directories and directs users to --mount.

Confidence Score: 5/5

Safe to merge; all three feature areas are well-isolated with dedicated tests, and the fallback chains preserve session correctness.

The truncation/compression pipeline has a solid backstop so oversized outputs cannot bypass the character cap. The ContextWindowExceededError handler is tested end-to-end including the non-retry assertion. The bind-mount wiring is straightforward. The two issues noted are a misleading log message in non-interactive mode and a deprecated asyncio.wait_for(coroutine) call pattern — neither affects correctness.

No files require special attention; strix/core/execution.py has a misleading log message and strix/core/mapreduce_output.py has the deprecated asyncio.wait_for(coroutine) call, but neither introduces wrong behavior.

Important Files Changed

Filename Overview
strix/core/large_output.py New module: structure-aware truncation for oversized tool outputs. Halving loop, SDK header preservation, and text/JSON branching all look correct. Single-oversized-record edge case (noted in previous review) is handled by caller backstop.
strix/core/mapreduce_output.py New MapReduce LLM compression module. Fan-out to parallel litellm calls is sound; single-chunk fast-path correctly delegates truncation to caller. asyncio.wait_for receives a bare coroutine instead of a Task (deprecated in Python 3.12+). Broad except Exception in _summarise is intentional for chunk-level resilience.
strix/agents/factory.py Added MapReduce compression + truncation backstop inside _wrap_exec_command. Fallback chain (compress → backstop truncate) is correct; load_settings() called inside the hot tool-invocation path, which is fine if settings are cached by the config layer.
strix/core/execution.py ContextWindowExceededError handler correctly bypasses image-strip retry. Log message "parking as failed" fires before the if not interactive: raise guard and is factually wrong in non-interactive mode.
strix/interface/tui/app.py Scan error toast via call_from_thread with contextlib.suppress is the correct cross-thread notification pattern. Removed explicit timeout=0 (which caused immediate expiration) and uses Textual's default timeout instead.
strix/interface/utils.py New helpers: build_mount_targets_info, dedupe_local_targets, find_oversized_local_targets, directory_size_bytes. Deduplication prefers bind-mounted entries; path resolution in build_mount_targets_info is consistent. Size walk is stat-only and best-effort as documented.
strix/runtime/session_manager.py Refactored to build_session_entries which splits local sources into copied entries and bind-mount specs. Logic is correct; mounted paths are excluded from SDK manifest (preventing file-by-file copy).
strix/runtime/docker_client.py Added strix_bind_mounts class attribute (now None not [] per prior review) and Docker mount injection in _create_container. Pyright suppression comments added for private SDK imports.
strix/runtime/backends.py Passes bind_mounts through from session manager to Docker backend. Straightforward wiring change.
strix/config/settings.py Added max_local_copy_mb and max_tool_output_chars settings with documented env-var aliases and defaults. Zero-disables convention is consistent across both fields.
strix/interface/main.py Added --mount CLI argument with correct action="append", error messages updated, pre-flight size check wired in. dedupe_local_targets called after both --target and --mount lists are assembled.

Reviews (2): Last reviewed commit: "Fix review comment" | Re-trigger Greptile

Comment thread strix/runtime/docker_client.py Outdated
@mhspektr

Copy link
Copy Markdown
Contributor Author

@greptile

@mhspektr

Copy link
Copy Markdown
Contributor Author

I suggest merging #577, updating this branch, and rerunning greptile before reviewing.

mhspektr and others added 3 commits June 22, 2026 11:17
* 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>
* 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 (usestrix#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


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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Running strix on a large repo crashed with a ContextWindowExceededError