Analyzed GitHub issue and verified root cause.
UnderstandImplemented fix to persist output on line truncation.
EditUpdated changelog and test files.
EditCommitted and pushed fix with detailed message.
VerifyAnalyze GitHub issue(s): https://github.com/badlogic/pi-mono/issues/2852
For each issue:
Add the inprogress label to the issue via GitHub CLI before analysis starts. If adding the label fails, report that explicitly and continue.
Read the issue in full, including all comments and linked issues/PRs.
Do not trust analysis written in the issue. Independently verify behavior and derive your own analysis from the code and execution path.
For bugs:
For feature requests:
Do NOT implement unless explicitly asked. Analyze and propose only.
Added inprogress to #2852 successfully.
Title: When bash tool truncates by line count (but not by byte count), temp file isn't written
gh issue view 2852 --json title,body,comments,labels,stateI did not rely on the issue analysis. I traced the actual code path and checked the truncation behavior directly.
Relevant files read in full:
packages/coding-agent/src/core/tools/bash.tspackages/coding-agent/src/core/tools/truncate.tspackages/coding-agent/src/core/bash-executor.tspackages/coding-agent/src/modes/interactive/components/bash-execution.tsI also verified that output like seq 3000 is well under the byte threshold but over the line threshold:
So this is a valid reproduction case for line-based truncation without byte-based overflow.
There are two independent truncation systems involved:
Temp file creation in bash execution
packages/coding-agent/src/core/tools/bash.tstotalBytes > DEFAULT_MAX_BYTESDisplayed output truncation
truncateTail(fullText)truncateTail() truncates on either:
maxLines default 2000maxBytes default 50KBThese are not aligned.
In createBashToolDefinition().execute():
handleData() increments totalBytestotalBytes > DEFAULT_MAX_BYTEStruncateTail(fullText) unconditionallytruncateTail(fullOutput) unconditionallyIf output is:
then:
truncateTail() returns truncated: true, truncatedBy: "lines"tempFilePath is still undefinedFull output: undefinedSo the real bug is:
Full-output persistence is gated only by raw byte count, while displayed truncation is gated by line count or byte count.
That mismatch exists even if the issue description were wrong.
The same design bug also exists in:
packages/coding-agent/src/core/bash-executor.tsThat path also:
totalBytes > DEFAULT_MAX_BYTEStruncateTail(fullOutput)So if this issue is fixed only in the tool implementation, the lower-level bash executor will still have the same line-truncation data loss behavior.
Create the temp file whenever output is known to be truncated and no temp file exists yet.
That means:
ensureTempFileFromBufferedOutput()truncateTail(fullText).truncated && !tempFilePath!tempFilePathThis fixes:
You could also track line count while streaming and create the temp file once lines exceed 2000. That works, but it is more error-prone because:
A final ensureTempFile... check is the minimal robust fix.
packages/coding-agent/src/core/tools/bash.ts
truncateTail(...) reports truncationFull output: undefinedpackages/coding-agent/src/core/bash-executor.ts
Search showed no focused tests for this behavior. Add regression tests around:
fullOutputPath presence when truncation occursLikely test targets:
packages/coding-agent/test/... for tool behaviorpackages/coding-agent/src/core/tools/bash.tsAdd a small internal helper that:
tempFilePathtempFileStream if neededUse it:
handleData() if truncateTail(fullText).truncateddetailspackages/coding-agent/src/core/bash-executor.tsSame idea:
outputChunkstruncateTail(fullOutput).truncated and no file existsFor commands like seq 3000:
fullOutputPath is always populatedIf you want, I can implement the fix and add regression tests.
i don't understand. is this not just a display issue then?
No. It is not just display.
The bash tool has two separate behaviors:
What it shows back to the model/UI
What it saves to a temp file
So for output like seq 3000:
If this were only a display issue, the full output would still exist somewhere and only the UI text would be wrong.
That is not the case here.
The tool returns only the truncated tail. The omitted lines are not persisted. So the agent cannot:
In other words:
seq 3000
That is data loss, not just presentation.
There is also a display/message bug layered on top:
bash.ts can produce a notice like:
Full output: undefinedSo yes, there is a display problem too. But the underlying problem is that the missing output was never saved.
If you want, I can break down the exact code path in 10 lines.
can we fix this nicely?
Yes.
A clean fix is to make "truncated output" and "full output persisted" use the same condition.
Instead of creating the temp file only when:
create it whenever:
That means:
Add a small helper in the bash code:
Then call that helper whenever truncateTail(...) says truncation happened.
packages/coding-agent/src/core/tools/bash.tsThis is the actual tool path.
Change behavior so that:
packages/coding-agent/src/core/bash-executor.tsThis lower-level path has the same bug.
It should get the same treatment so behavior stays consistent across:
Because it matches the user-facing contract:
Right now the code really implements:
Those are not the same thing.
A less nice fix would be:
That is possible, but not as robust:
Using truncateTail(...) as the source of truth is simpler and more correct.
For seq 3000:
fullOutputPath is populatedFor huge byte-heavy output:
Also suppress any message that would print:
Full output: undefinedThat should never appear.
If you want, I can implement this now.
truncateTail(...) i would assume that's also slow, no?
Potentially, yes. If you call truncateTail(fullText) on every chunk, it is not free.
truncateTail() currently does work like:
split("\n") the whole stringSo if you call it repeatedly during streaming:
In bash.ts, that already happens today for partial updates:
handleData() builds Buffer.concat(chunks)truncateTail(fullText)So the current code already pays that cost. The issue fix does not introduce that pattern. It only changes what happens when truncation is detected.
There are two possible fixes:
Use truncateTail(...) as the trigger for creating the temp file.
Pros:
Cons:
Track truncation state incrementally while streaming, then use truncateTail(...) only for rendering/final output.
That means tracking:
totalBytesThen:
Use an incremental trigger for persistence:
totalBytes > DEFAULT_MAX_BYTESlineCount > DEFAULT_MAX_LINESThis avoids needing truncateTail(...) as the persistence trigger.
Then keep truncateTail(...) only for:
That gives you:
Counting lines incrementally has a small wrinkle:
So you need a tiny bit of state, for example:
\n occurrences in each chunksawAnyOutput/endsWithNewline style stateStill straightforward.
truncateTail(...) is not especially cheap.truncateTail(...) to decide when to create the temp file.That is the nicest fix if you care about performance and design.
you are saying we alredy do truncateTail and don't need to all it a second time?
Yes.
That is exactly what I mean.
In packages/coding-agent/src/core/tools/bash.ts, inside handleData() we already do this on every chunk:
truncateTail(fullText)So if we changed the temp-file decision to also use that same truncation result, we would not need a second truncateTail(...) call in that path.
We could do:
const truncation = truncateTail(fullText)truncation.truncated && !tempFilePath, create temp file from buffered chunkstruncation for onUpdate(...)So there is no extra truncation pass there.
That only applies to the path where we are already computing truncateTail(...).
There are still two places to think about:
bash.tsNo second call needed. Reuse the existing result.
bash.tsThere is already one final truncateTail(fullOutput) call there too.
Same story. Reuse that result for:
Again, no second call needed.
Because even though we would not add a second truncateTail(...), the current approach is still somewhat expensive overall, since partial updates already recompute truncation repeatedly.
That is a separate optimization question.
For fixing this bug specifically:
truncateTail(...) resultIn bash.ts:
truncationtruncationIn bash-executor.ts:
truncateTail(fullOutput)So the bug can be fixed without introducing additional truncation calls.
If you want, I can now turn this into a concrete implementation plan file-by-file.
ok, fix, and write a regression test for it, sufficient to just instnatite and invoke the tool, don't need an agent
Implemented.
Changed files:
packages/coding-agent/src/core/tools/bash.tspackages/coding-agent/src/core/bash-executor.tspackages/coding-agent/test/tools.test.tsWhat changed:
bash tool now ensures a temp file exists whenever output is truncated, not only when raw bytes exceed 50KBexecuteBash path in bash-executor.tsseq 3000Regression covered:
fullOutputPath is populatedFull output: undefinedValidation:
cd packages/coding-agent && npx tsx ../../node_modules/vitest/dist/cli.js --run test/tools.test.tsnpm run checkBoth passed.
100% sure we are in a good place?
Not 100%, but close.
packages/coding-agent/src/core/tools/bash.tspackages/coding-agent/src/core/bash-executor.tsnpm run check passesFor the user-visible bug in the bash tool, yes, this is in a good place.
The new regression test covers the tool path, not executeBash() directly.
I did fix packages/coding-agent/src/core/bash-executor.ts too, but there is no dedicated regression test for that path yet.
So the honest answer is:
I would add one more small test for executeBash("seq 3000") asserting:
truncated === truefullOutputPath is definedThat would make me much more comfortable saying "yes, done".
do it
Added the extra coverage.
Now covered by tests:
bash tool invocationexecuteBash() invocationValidation rerun:
cd packages/coding-agent && npx tsx ../../node_modules/vitest/dist/cli.js --run test/tools.test.tsnpm run checkBoth passed.
So yes, this is now in a much better place. Still never "100% sure" in the absolute sense, but this is sufficient confidence for merge.
ok, how can i terst this manually?
From the repo root:
Use whatever local flow you normally use. For interactive TUI, the repo docs already suggest:
./pi-test.shOr in tmux if you want isolation:
tmux new-session -d -s pi-test -x 80 -y 24tmux send-keys -t pi-test "cd /Users/badlogic/workspaces/pi-mono && ./pi-test.sh" Entersleep 3tmux capture-pane -t pi-test -pAsk pi to run one of these:
run seq 3000or directly in slash/bash mode if you use that:
!seq 3000The output should be truncated, but the result should also include a temp file path, something like:
[Showing lines ... Full output: /var/folders/.../pi-bash-....log]The important part is:
Full output: undefinedCopy the path from the output and check it in another shell:
ls -l /path/from/output.logThen inspect it:
tail -5 /path/from/output.loghead -5 /path/from/output.logYou should see:
1, 2, 32998, 2999, 3000Also try something that exceeds bytes, not just lines:
run python - <<'PY'for i in range(400): print(f"{i} " + "x"*200)PYThat should also:
If you want to validate the underlying shell behavior without going through the full agent UI, the tests already cover it, but a quick manual check is:
cd packages/coding-agentnpx tsx ../../node_modules/vitest/dist/cli.js --run test/tools.test.tsIf you want, I can also give you a very small one-off node -e snippet to invoke executeBash() directly from source.
Wrap it.
Additional instructions:
Determine context from the conversation history first.
Rules for context detection:
/is or /pr, assume the issue or PR context is already known from the conversation and from the analysis work already done.Unless I explicitly override something in this request, do the following in order:
## [Unreleased] using the repo changelog rules.closes #<issue> in the commit message. If it is tied to multiple issues, stop and ask which one to use. If it is not tied to any issue, do not include closes # or fixes # in the commit message.main, stop and ask what to do. Do not push from another branch unless I explicitly say so.Constraints:
git add . or git add -A.Preview issue comment for #2852:
Fixed in main.
Root cause was that bash output persistence and bash output truncation used different thresholds. The tool only created a temp file after crossing the byte limit, but truncation also happens on line count. For outputs like seq 3000, the result was truncated while no temp file was written, so the omitted lines were lost.
Changes:
executeBash() pathexecuteBash() invocationManual check:
seq 3000Full output: /tmp/... path should be presentWrapped.
Done:
packages/coding-agent/CHANGELOG.md52d16d5amainCommit message:
fix(coding-agent): persist bash output on line truncation closes #2852Files in the commit:
packages/coding-agent/CHANGELOG.mdpackages/coding-agent/src/core/bash-executor.tspackages/coding-agent/src/core/tools/bash.tspackages/coding-agent/test/tools.test.ts./pi-test.shtmux new-session -d -s pi-test -x 80 -y 24tmux send-keys -t pi-test "cd /Users/badlogic/workspaces/pi-mono && ./pi-test.sh" Entersleep 3tmux capture-pane -t pi-test -prun seq 3000!seq 3000[Showing lines ... Full output: /var/folders/.../pi-bash-....log]ls -l /path/from/output.logtail -5 /path/from/output.loghead -5 /path/from/output.logrun python - <<'PY'for i in range(400): print(f"{i} " + "x"*200)PYcd packages/coding-agentnpx tsx ../../node_modules/vitest/dist/cli.js --run test/tools.test.ts