Skip to content

fix(stella-tools): cut read_file's payload cap from 76% of the context budget to 12% (#1842) - #1888

Merged
macanderson merged 2 commits into
mainfrom
fix/1842-read-payload-cap
Aug 6, 2026
Merged

fix(stella-tools): cut read_file's payload cap from 76% of the context budget to 12% (#1842)#1888
macanderson merged 2 commits into
mainfrom
fix/1842-read-payload-cap

Conversation

@macanderson

@macanderson macanderson commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Problem

MAX_RENDER_BYTES was 400 KB — about 114k estimated tokens, or 76% of the whole 150k compaction budget in one tool result.

That is worse than the number alone suggests, because a single large result does not trigger the compaction that would reclaim it. With the rest of the transcript small, compact_measured returns early (under budget), and the retention horizon (tool_result_horizon_steps: Some(8)) then keeps the result verbatim for the next eight tool-bearing steps. One read of a lockfile, a schema dump or a bundled JS file cost roughly 900k input tokens.

Change

Option (a) of the two the issue offered — the per-result cap, not a retention change — because it is the simpler mechanism and matches how comparable tools bound a read.

before after
MAX_RENDER_BYTES 400 KB 64 KB
≈ tokens ~114k ~18k
share of the 150k budget 76% 12%

12% is a bound a turn can carry eight times over — which is exactly the horizon that used to multiply it.

The trade-off, stated rather than buried

A full 2000-line read of ordinary source (~45 bytes a line, ~90 KB) now stops around line 1400 and the model pages once. That cost is the fix, not a side effect. It lives in the constant's doc comment so the next reader sees the reasoning and not just the number, and moving it is one edit if you want a different point on the curve.

The half that makes paging cheap

The footer said:

stopped at the 400 KB payload cap — re-read with offset/limit to continue

…and never said from where. The answer is not the line count the model can see, because start may be non-zero and clipped lines still count as shown. So the note cost a guess, and a wrong guess costs another read. It now names the line:

stopped at the 64 KB payload cap — continue with offset=1403

Witness

the_total_payload_cap_stops_the_render_and_reports_it gains the paging assertions the issue asks for:

  1. the footer names a resume offset;
  2. that line is not already in this render — an off-by-one silently skips a line or repeats one, and the model cannot tell which;
  3. re-reading at it begins exactly there, asserted against a second real execute.

Its existing cap assertion also stops hard-coding "400 KB" and derives the number from the constant. That assertion would have gone on passing for a cap that moved — the shape a size guard can least afford, and the reason this PR had to touch it at all.

$ cargo test -p stella-tools
test result: ok. 737 passed; 0 failed

cargo clippy -p stella-tools --all-targets -- -D warnings and cargo fmt --check clean.

Not in scope

bash.rs and custom.rs carry the same shape at MAX_OUTPUT_BYTES = 100 KB (~28k tokens). Smaller magnitude, same argument — left for a follow-up rather than folded in, since each deserves its own trade-off discussion and its own witness.

Closes #1842

Summary by Sourcery

Reduce the read_file tool’s maximum rendered payload size to limit per-call context usage and improve paging behavior for large files.

New Features:

  • Include an explicit resume offset in read_file footers when payloads are capped, enabling precise paging via offset/limit.

Enhancements:

  • Lower the MAX_RENDER_BYTES cap from 400 KB to 64 KB and update related documentation to align with compaction budget constraints.
  • Derive cap-related test expectations from the MAX_RENDER_BYTES constant to keep tests accurate when the cap changes.
  • Extend tests to verify that the reported resume offset is present, advances beyond the last shown line, and that a follow-up read resumes exactly at that line.

Tests:

  • Augment read_file cap tests to cover the new resume offset footer behavior and to assert continuity when re-reading from the suggested offset.

…t budget to 12%

`MAX_RENDER_BYTES` was 400 KB — about 114k estimated tokens, or 76% of the
whole 150k compaction budget in ONE tool result.

That is worse than the number alone suggests, because a single large result
does not trigger the compaction that would reclaim it. With the rest of the
transcript small, `compact_measured` returns early (under budget) and the
retention horizon (`tool_result_horizon_steps: Some(8)`) then keeps the result
verbatim for the next eight tool-bearing steps. One read of a lockfile, a
schema dump or a bundled JS file cost roughly 900k input tokens.

Option (a) of the two the issue offered — the per-result cap, not a retention
change — because it is the simpler mechanism and the one that matches how
comparable tools bound a read. 64 KB is ~18k tokens, about 12% of the budget:
a bound a turn can carry eight times over.

The trade-off is real and is stated in the constant's doc comment rather than
buried: a full 2000-line read of ordinary source (~45 bytes a line, ~90 KB)
now stops around line 1400 and the model pages once. That cost IS the fix.
`read_file` already supports `offset`/`limit`, and moving the number is one
edit if a maintainer wants a different point on the curve.

The second half is what makes paging cheap. The footer said "re-read with
offset/limit to continue" and never said FROM WHERE — and the answer is not
the line count the model can see, because `start` may be non-zero and clipped
lines still count as shown. It now names the line: "continue with offset=N".

Witness: `the_total_payload_cap_stops_the_render_and_reports_it` gains the
paging assertions — the footer names a resume offset, that line is NOT already
in the render (an off-by-one silently skips or repeats a line and the model
cannot tell), and re-reading at it begins exactly there. Its existing
cap assertion also stops hard-coding "400 KB" and derives the number from the
constant, since an assertion that would go on passing for a cap that moved is
the shape a size guard can least afford.

`cargo test -p stella-tools` — 737 passed, 0 failed. Clippy and fmt clean.

Closes #1842

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @macanderson, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
stella-cli-docs Ignored Ignored Preview Aug 6, 2026 6:37pm

@sourcery-ai

sourcery-ai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Reduces read_file’s maximum rendered payload size and makes paged reads precise and test-backed so large file reads don’t monopolize the compaction budget and the footer provides an exact resume offset.

Sequence diagram for read_file paged reads with precise resume offset

sequenceDiagram
    actor Model
    participant ReadFile

    Model->>ReadFile: execute({path, offset=0}, workdir)
    ReadFile-->>Model: ToolOutput::Ok(content with footer, stopped at 64 KB payload cap, continue with offset=start+shown+1)

    Model->>ReadFile: execute({path, offset=start+shown+1}, workdir)
    ReadFile-->>Model: ToolOutput::Ok(content starting at resume line)
Loading

File-Level Changes

Change Details Files
Lower the maximum rendered payload cap and update related documentation/comments to reflect the new trade-offs.
  • Reduce MAX_RENDER_BYTES from 400 KB to 64 KB to cap any single read_file result at ~18k tokens (~12% of the 150k compaction budget).
  • Expand the MAX_RENDER_BYTES doc comment to explain the compaction interaction, retention horizon, and deliberate paging trade-off when reading long files.
  • Adjust comments describing MAX_FILE_BYTES so that references to the payload cap size stay accurate.
crates/stella-tools/src/read.rs
Improve the footer message for capped reads to provide an exact resume offset and enforce correctness via tests.
  • Change the capped-read footer text to say "continue with offset={line}" instead of a generic re-read instruction, computing the offset as start + shown + 1.
  • Update the existing cap assertion to derive the KB value from MAX_RENDER_BYTES instead of hard-coding the number in the test.
  • Add test logic that parses the resume offset from the footer, asserts that the named line is not already in the render, and verifies that a subsequent execute with that offset begins exactly at that line.
crates/stella-tools/src/read.rs

Assessment against linked issues

Issue Objective Addressed Explanation
#1842 Reduce read_file's per-result payload cap (MAX_RENDER_BYTES) from 400 KB to a substantially smaller fraction of the compaction budget so that a single tool result cannot dominate the context for the 8-step retention horizon.
#1842 Update read_file's truncation/footer message to explicitly include the offset from which to continue reading when the payload cap is hit, and add tests that witness this paging behavior.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@macanderson
macanderson merged commit fa392a3 into main Aug 6, 2026
4 checks passed
@macanderson
macanderson deleted the fix/1842-read-payload-cap branch August 6, 2026 19:07
macanderson added a commit that referenced this pull request Aug 6, 2026
…put instead of dropping the tail (#1889) (#1900)

## Problem

`bash.rs` and `custom.rs` capped tool output at `MAX_OUTPUT_BYTES = 100
KB` — ~28k estimated tokens, **~19% of the 150k compaction budget in one
result**. A single large result does not trigger the compaction that
would reclaim it (`compact_measured` returns early when the rest of the
transcript is small), and the 8-step retention horizon then keeps it
verbatim: one `cargo test --workspace` or `npm ci` printing 100 KB cost
~224k input tokens, not 28k (#1889, the deliberate residue of #1842).

On top of the budget shape, the crate had grown **three independent
elision spellings**: `exec::truncate_middle` (50/50 split, one marker
format), `bash.rs`'s inline copy (50/50, a second marker format), and
`custom.rs::truncate_middle_out` (40/60, the second marker format
again). Three copies is the shape that lets one drift — and they already
had, on both the split and the marker.

## Decision

**Head + tail elision through one shared helper, and a 64 KB cap.**

- **One spelling:** `exec::truncate_middle_capped(s, max_bytes)` is now
the crate's single model-facing elision primitive.
`exec::truncate_middle`, `bash`, and `custom` all cut through it;
`custom.rs`'s private copy (and its boundary helpers) are deleted. It
sits in `exec.rs` because that module already owns the output-cap policy
for every other runner (`truncate_middle`, `CappedStream`,
`truncate_preview`), and `exec.rs` is not a god file.
- **Split: 40% head / 60% tail.** Tail-biased because a failing
command's densest signal is at the end — the final test summary, the
last error, the exit status. This is lesson L-S3, already ratified in
`custom.rs`; `bash` moves from 50/50 to match rather than the reverse,
and the shared function makes future divergence structurally impossible.
Both cuts land on UTF-8 char boundaries (the existing discipline of
`truncate_preview` / the old inline code).
- **Marker names the elided byte count *and* the cap:** `[… N bytes
truncated: output exceeded the 65536-byte cap; the head and tail are
kept …]` — the model can account for every byte and knows the bound it
is working under, mirroring `CappedStream`'s "say which cap did this"
convention.
- **Cap: 100 KB → 64 KB (~12% of the compaction budget)** — the exact
point #1842 ratified for `read_file` (400 KB → 64 KB, PR #1888), for the
same multiplier argument. It stays the same order of magnitude and
preserves #616's ratio argument: still 2.2x `exec::MAX_OUTPUT_BYTES`
(30k), so the shell remains the agent's wide sensory channel while
`read_output` pages stay cheap. `custom.rs` now **aliases** `bash`'s
constant (`pub(crate) use`) instead of carrying a copy, so the two
cannot drift.
- **Retention-aging is deliberately not here.** The issue's option (b) —
aging an oversized result out of retention early — belongs to the
engine's retention fold, tracked by #1819 (reclaimable-bytes gate) and
the #1438 file-budget umbrella. This PR bounds the cost at the source;
those bound what survives the horizon.

Exemplar for the consolidation shape: the crate's own `shell_quote`,
which collapsed five drifting copies into one `exec.rs` primitive with a
"a new one must be too" contract; this PR does the same for elision.

## Witness

`over_cap_output_keeps_first_and_last_lines_with_a_named_elision`, once
per surface (`bash::tests`, `custom::tests`): a command/script emitting
a first sentinel line, `MAX_OUTPUT_BYTES` of filler, and a last sentinel
line yields a result containing **both sentinel lines**, a marker naming
the **elided byte count** and the **cap**, bounded by the cap plus the
marker. Every size is derived from the constant — nothing hard-codes a
human-readable size, the assertion shape #1842 caught going stale.

Verified failing on the old code the artisanal way — both tests spliced
onto the parent commit:

```
test bash::tests::over_cap_output_keeps_first_and_last_lines_with_a_named_elision ... FAILED
test custom::tests::over_cap_output_keeps_first_and_last_lines_with_a_named_elision ... FAILED
  panicked: 'the marker names the cap it enforced'
```

…and passing on this branch. `exec::tests` additionally pin the helper
itself: the marker's elided count is arithmetically exact (derived, not
hard-coded), tail budget ≥ head budget (L-S3), both cuts survive landing
mid-multibyte-char, and at-or-below-cap input is byte-identical.

## Verification

- `cargo test -p stella-tools`: **723 passed** (lib) + all integration
suites green, 0 failed.
- `cargo clippy -p stella-tools --all-targets -- -D warnings` clean;
`cargo fmt -p stella-tools --check` clean.
- Workspace-wide validation left to CI per build economy (no public API
changed; every touched item is `pub(crate)`).

Closes #1889

Refs #1842 #1819 #1438

## Summary by Sourcery

Unify and tighten stdout/stderr truncation across exec, bash, and custom
tools to keep both the head and tail of oversized outputs under a
smaller shared cap.

Bug Fixes:
- Ensure over-cap bash and custom tool output retains both the first and
last lines instead of dropping the tail under some sizes.

Enhancements:
- Introduce a shared truncate_middle_capped helper in exec that performs
UTF-8-safe, tail-biased head+tail elision with an explicit marker naming
the elided byte count and cap.
- Reduce the bash/custom output cap from 100 KB to 64 KB to better bound
transcript and compaction costs, while preserving the intended ratio to
exec output limits.
- Alias custom tool output caps and elision behavior to bash so their
output budgets and truncation semantics cannot drift apart.

Tests:
- Add unit tests for truncate_middle_capped to validate exact elided
byte accounting, UTF-8 boundary safety, and no-op behavior at or below
the cap.
- Add integration tests for bash and custom tools that verify over-cap
outputs keep both sentinel lines, include a cap- and byte-count-bearing
elision marker, and remain within the bounded size.

Co-authored-by: Stella Test <test@stella.local>
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.

tools: one read_file can inject ~114k tokens (400KB cap) that retention then protects for 8 steps

1 participant