feat(context): spill oversized tool output into the sandbox workspace#882
Merged
Conversation
Contributor
Author
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Contributor
Greptile SummaryReplaces host-side paginated tool-output retrieval with sandbox-local spill files.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Reviews (15): Last reviewed commit: "refactor(context): workspace spill only;..." | Re-trigger Greptile |
devin-ai-integration
Bot
force-pushed
the
agent/context-compaction
branch
from
July 25, 2026 22:35
f6e2827 to
5433bfc
Compare
devin-ai-integration
Bot
force-pushed
the
agent/tool-output-spill
branch
from
July 25, 2026 22:41
ed423a8 to
72041ab
Compare
Contributor
Author
devin-ai-integration
Bot
force-pushed
the
agent/context-compaction
branch
from
July 25, 2026 22:51
5433bfc to
bae1f9a
Compare
devin-ai-integration
Bot
force-pushed
the
agent/tool-output-spill
branch
from
July 25, 2026 22:52
72041ab to
029d8b2
Compare
Contributor
Author
devin-ai-integration
Bot
force-pushed
the
agent/context-compaction
branch
from
July 25, 2026 23:06
bae1f9a to
a3e2e3f
Compare
devin-ai-integration
Bot
force-pushed
the
agent/tool-output-spill
branch
2 times, most recently
from
July 25, 2026 23:13
83659b4 to
97919a4
Compare
Contributor
Author
devin-ai-integration
Bot
force-pushed
the
agent/tool-output-spill
branch
from
July 25, 2026 23:25
97919a4 to
d2797af
Compare
Contributor
Author
devin-ai-integration
Bot
force-pushed
the
agent/context-compaction
branch
from
July 25, 2026 23:49
771284e to
16cbd51
Compare
devin-ai-integration
Bot
force-pushed
the
agent/tool-output-spill
branch
from
July 25, 2026 23:51
2c83e88 to
189e375
Compare
Contributor
Author
devin-ai-integration
Bot
force-pushed
the
agent/tool-output-spill
branch
from
July 26, 2026 00:05
189e375 to
028a854
Compare
Contributor
Author
devin-ai-integration
Bot
force-pushed
the
agent/tool-output-spill
branch
from
July 26, 2026 00:16
028a854 to
e5a46b9
Compare
Contributor
Author
Contributor
Author
Contributor
Author
1 similar comment
Contributor
Author
Contributor
Author
2 similar comments
Contributor
Author
Contributor
Author
devin-ai-integration
Bot
force-pushed
the
agent/context-compaction
branch
from
July 26, 2026 19:29
4eedfc6 to
d4f72e6
Compare
devin-ai-integration
Bot
force-pushed
the
agent/tool-output-spill
branch
3 times, most recently
from
July 26, 2026 21:09
0123e3b to
c91908a
Compare
Truncating a large tool result to a head+tail preview loses the middle, which may hold the one line that matters (a buried match, a stack frame, a credential). Instead of dropping it, persist the full output and let the agent page back to it on demand. - output_store.py: bound_and_store() writes the complete output to a per-scan store and embeds an output_id in the truncation notice; read_stored_output() serves it back in validated, paginated chunks (output_id is a 32-char hex token, guarding against path traversal). - read_tool_output tool: lets the agent retrieve any elided output by id, paging via offset/limit. - factory.py: bounded tool results now spill via bound_and_store; the new tool is registered for every scan agent. - runner.py: point the store at the run's .state/tool-output directory so spilled output lives beside the rest of the scan state. Falls back to a plain head+tail preview if the spill write fails.
Exempt read_tool_output from result-bounding so paging a large stored output isn't re-spilled under a new id, byte-cap each page in place, and stream the requested window with islice instead of loading the whole file per page.
Bounding a retrieval page with a destructive head+tail preview and then advancing the offset past those lines could permanently lose the elided output the store exists to preserve. Instead honour the page byte budget by returning fewer whole lines and advancing the offset only by lines actually returned, so paging forward reconstructs the full output.
bound_and_store's notice carries the output_id and is longer than the plain truncation notice, so reserve for it explicitly and cover that the spilled preview stays within max_bytes.
…rflows The line-based pager always kept at least one whole line, so a stored line larger than the page byte ceiling was returned in full and could overflow history (retrieval bypasses the result-bounding wrapper). Page by byte offset instead: every page is bounded by the byte budget even inside one oversized line, a partial UTF-8 char at the window edge is dropped and re-read on the next page, and paging forward reconstructs the output byte-for-byte.
read_tool_output accepts arbitrary byte offsets; one landing inside a multi-byte character previously decoded to a replacement character. Trim the orphaned leading continuation bytes (their lead byte is on an earlier page) so every accepted offset returns valid UTF-8, without affecting forward paging offsets.
…ge ceiling A full non-final retrieval page consumed the entire byte budget and then appended the pagination hint, so the returned result (which bypasses the result-bounding wrapper) exceeded the ceiling. Reserve the hint's bytes out of the page budget so content plus hint always fits.
An arbitrary offset landing inside a multi-byte character is now advanced to the next character boundary before reading, instead of trimming leading continuation bytes after the read. This keeps every page on a real boundary (no replacement characters) and guarantees forward progress, so an offset inside the final character can't yield an empty page with an unchanged continuation offset.
…imit The continuation hint was reserved only against the global page cap, so a small caller-supplied limit bounded content alone and the appended hint pushed the complete response past the requested maximum. Reserve the hint out of the effective ceiling (min of limit and the global cap) so content plus hint always honours the caller's limit, flooring content so pages still make progress.
A final page carries no continuation hint, so it uses the whole budget and honors any limit exactly. A non-final page's content plus hint must fit the limit; a limit too small to hold a progressing page and its hint is now rejected with a clear message rather than silently exceeding the documented maximum.
Prefer writing an oversized tool result into the agent's own sandbox at /workspace/.strix/tool-output/<id>.txt so the agent reads it back with its normal file tools (exec_command etc.) instead of a dedicated retrieval tool. The host-side store + read_tool_output remain as a fallback when no sandbox writer is configured (unit tests, chat mode) or a workspace write fails. Only sandbox-origin tools (shell/filesystem capabilities) may spill into the workspace; orchestrator-side tool output is never written into the hostile sandbox and always uses the host-side store.
…pace Drop the workspace_spill toggle and the orchestrator-vs-sandbox origin guard. Every oversized tool result now prefers the sandbox-workspace writer; the host-side store + read_tool_output remains only as the fallback when no writer is configured (unit tests, chat mode) or the workspace write fails.
…output Every agent run always has a sandbox, so the host-side output store and the read_tool_output retrieval tool were only ever a redundant fallback. Remove them entirely: oversized tool output always spills to /workspace/.strix/tool-output/<id>.txt and the agent reads it with its own file tools. If the sandbox write fails, degrade to a plain head+tail preview.
devin-ai-integration
Bot
force-pushed
the
agent/tool-output-spill
branch
from
July 26, 2026 21:40
c91908a to
f957a32
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Completes the per-tool-output story from #880. Today an oversized tool result is truncated to a head+tail preview and the middle is discarded — which can drop the one line that matters (a buried grep hit, a stack frame, a leaked credential). This spills the full output durably into the agent's own sandbox and gives it a bounded preview pointing at the file, so history stays bounded but nothing is lost.
Workspace spill only
Every agent run always has a sandbox (SandboxAgent's shell/filesystem require one; the runner always creates it), so the full output is written inside the agent's own sandbox at
/workspace/.strix/tool-output/<id>.txtand the agent reads it back with its normal file tools (exec_command/sed/grep/cat) — no dedicated retrieval tool, no host-side store.Wiring
strix/core/runner.py: once the sandbox exists, installs a writer viaconfigure_spill_writer(_spill_to_workspace)that doessandbox_session.write(/workspace/.strix/tool-output/<id>.txt, ...), cleared infinallyso a later scan can't use a stale session.read_tool_outputtool (strix/tools/tool_output/) and the host-side output store (store_full_output/read_stored_output/configure_output_store) are removed entirely._head_tailreserves bytes for the longest possible notice (workspace path or plain) so the joined preview always fitstool_output_max_bytes, and recomputes dropped-line counts after the byte-trim pass.Test plan
tests/test_output_store.py: small output not spilled; oversized output spilled to the workspace (full text to the writer, notice points at the workspace path, a buried line absent from the preview is present in the spill); multibyte-safe preview within the byte ceiling; writer-failure / no-writer degrade to a plain preview; dropped-line count accounts for byte trimming. Validated live in a real Kali Docker sandbox (215KB output → spill file, agentgrep/catrecovers it byte-identical) and in the full integration scan.uv run pytestgreen; ruff + mypy + bandit pass.Link to Devin session: https://app.devin.ai/sessions/3461e7dce476402aa2091e4af3f55c00
Requested by: @0xallam