Repository navigation
add: a large tool result is saved to a file and read as a preview, on Claude Code's measured terms - #517
Merged
Conversation
… Claude Code's measured terms A result over its tool's limit (bash 30,000 characters, other tools 50,000) is written whole to .entoli-output/<call id>.txt in the chat's workspace and worded for the model as Claude Code's <persisted-output> notice: the size, the path and a 2,000-character preview. The path rides the result as ToolResult.outputFile; the log keeps the whole output. read returns 2,000 lines by default and is held to 25,000 tokens: a whole-file read over the cap is the lines that fit, with Claude Code's PARTIAL view notice, and a range over it is refused in Claude Code's words. Foreground commands get two minutes by default and ten at most. A timed-out command keeps its output and is pointed at background jobs. The descriptions warn against piping through tail. A job that ends during a turn with no later tool call now wakes the agent when the turn is over.
…e cut - saveIfLarge is the one length test; the ! command reads BashTool.outputLimit rather than a template constant. - ChatJobs keeps the unheard ends (ChatShellJobs.unheard), and the room asks at settle instead of holding a second list. - _RunState.timedOut, one switch in observation, safeCut in core/formatting, charsPerToken beside estimateTokens, fileStemOf for job logs and saved results alike. - A job's log is named to the model by the same absolute path a saved result is (shellPath). - readPage numbers lines only up to the cap.
# Conflicts: # lib/domain/shell.dart
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.
A long DeepSeek V4 Pro chat filled its context with raw command and file output and lost output to the 3-minute command cap. This PR makes Entoli handle tool output the way Claude Code does. The behaviour was measured, not recalled: I probed Claude Code 2.1.321's own
BashandReadtools from a live session and checked each result in the JS bundled in its binary.What Claude Code does (measured)
Persisted output (every tool, in its tool-result layer)
maxResultSizeChars, capped at a global 50000, is persisted. For Bash the limit is 30000 chars (BASH_MAX_OUTPUT_LENGTHdefault 30000). Grep's is 20000, most tools' 50000. Read's isInfinity, since Read bounds itself by tokens. Probes of 30001 and 30118 chars were persisted..0dropped (Qt). The...line appears only when there is more.lAe). With 51-char lines it showed 39 lines (1988 chars).Timeouts
BASH_DEFAULT_TIMEOUT_MS,BASH_MAX_TIMEOUT_MS).Command did not complete within its 4s timeout and was moved to the background (ID: …). Output is being written to: <tasks>/<id>.output. You will be notified when it completes. …. It was not killed: it finished later and a completion notification arrived. Where a command cannot be backgrounded it is killed, and the result endsCommand timed out after <duration>.Background
run_in_backgroundanswersCommand running in background with ID: <id>. Output is being written to: <path>. You will be notified when it completes — do not poll that file or sleep while waiting.<task-notification>arrives as its own event: task id, output file, status, andBackground command "<cmd>" completed (exit code N).Exit code 3\nout\nerr.Read
[Truncated: PARTIAL view — <path>: showing lines 1-253 of 501 total (42006 tokens, cap 25000). Call Read with offset=254 limit=253 for the next page, or Grep to find a specific section. … Do NOT answer from this page alone if the answer may be further in the file.]File content (33606 tokens) exceeds maximum allowed tokens (25000). Use offset and limit parameters to read specific portions of the file, or search for specific content instead of reading the whole file.What this changes in Entoli
BuiltinAgent._savingLargewraps each call's events, batched calls included, andsaveIfLargeis the single size test (the!command uses it too, withBashTool.outputLimit). A result over the tool'sinlineLimitis written to.entoli-output/<call id>.txtin the chat's workspace: bash 30000, other tools 50000,readandask_usernone. The path rides the result as a fact,ToolResult.outputFile, and the projection words Claude Code's notice from it, so a reload words what the live turn saw. The log keeps the whole output, and the chat shows all of it..entoli-jobs/, soreadandtailreach them from every shell. The model is given an absolute path (shellPath), the guest's inside a guest, so atailafter acdstill works. Background-job logs are now named the same way; they used to be achat:-relative name that onlyreadunderstood. Both are named by one sanitiser,fileStemOf. The files go when the chat does, and a sub-agent's land in the parent chat's workspace. They are not in.entoli-tmp, which is the model's own scratch and may be cleared. Names never collide: providers that send no ids getcall_$i, so a repeat id saves ascall_0-2.txt.readhas a 2000-line default window and a 25000-token cap (estimated at 4 chars per token, asestimateTokensdoes everywhere else). It gives Claude Code's partial page for a whole-file read and its error for an explicit range. One line longer than the cap is cut by characters.commandTimeoutis 2 min (the default) and the newmaxCommandTimeoutis 10 min (the cap). The!command gets the 10.ShellResult.timedOutrecords why a command stopped, andbashadds that a command that long belongs inbackground: true.commandsays the whole output comes back, is saved past 30000, and that| tail/| headloses everything when the timeout kills the pipeline (14 of the chat's 21 empty timeouts were like that).timeoutandbackgroundsay long builds go to a job.ChatJobskeeps each end until a tool result carries it or a wake tells it (ChatShellJobs.unheard), and once a turn is over and the room settled, the room asks for that list and wakes the agent for the first one. The room keeps no list of its own. Stopped and failed turns still wake nothing.!command saves on bash's terms (ShellRunPart.outputFile).Deliberate differences
outputvalue, keeping Entoli's JSON result shape.Sync and mirrors
A mirror's turns run on the home device ("Driving a turn from the other end"), and the mirror reads the log, which keeps the whole output. So the mirror's chat shows everything, and the path is only ever given to a model on the device that has the file. Older peers ignore the new field.
Tests
builtin_agent_test: a 51000-char result is saved, and the model reads exactly Claude Code's notice. 30000 chars is worded whole and 30001 is saved. The default and maximum timeouts reach the shell.projection_test: a result withoutputFilewords as the notice on a reload. A long result with no file is cut past 50000 and worded whole below it.background_jobs_test: a timed-out command keeps its output and is pointed at the background.read_page_test: the partial page, the range refusal, the 2000-line window, and a very long line.saved_output_test: a repeated call id, guest paths, and a failed save.local_shell_test:timedOuton a real timeout, and not on a stop.chat_room_test: a job ending mid-turn with no later call wakes the agent after the turn, and an end a later call heard does not.background_jobs_test: an end stays unheard until a call carries it or a wake tells it.formatting_test:safeCutnever splits a surrogate pair.A
/simplifyreview was applied in a follow-up commit: one size test, the jobs as the only record of unheard ends, atimedOutgetter on the run, oneswitchinobservation,safeCut,charsPerTokenbesideestimateTokens, andreadPagenumbering lines only up to the cap.flutter analyzeis clean. Only the files touched were run locally; CI runs the rest.