Skip to content

Fix log/ring merge splicing non-contiguous bytes on log lag (BUG-076) - #919

Merged
anutron merged 1 commit into
masterfrom
argus/pty-garble-investigate
Jul 31, 2026
Merged

Fix log/ring merge splicing non-contiguous bytes on log lag (BUG-076)#919
anutron merged 1 commit into
masterfrom
argus/pty-garble-investigate

Conversation

@anutron

@anutron anutron commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

Same investigation as #912/#913/#916. After #916 (BUG-075) shipped, the coordinator reported a recurrence with a similar-looking but distinct signature: dropped letters mid-word and stray inserted characters (e.g. a "%" appearing where a letter should be), on an actively-streaming coordinator pane doing heavy git/tag operations, with no bind/resize/navigation event involved. Per the coordinator's explicit ask, I verified the running dogfood binary still contained #916's fix (confirmed via the binary's embedded VCS revision being a descendant of the BUG-075 commit, and by diffing the source at that exact revision) before looking for a second, distinct cause — grepping the codebase for other unsynchronized TotalWritten()/RecentOutput() call-site pairs.

Root cause

readLiveRebuildHistory's log/ring merge assumes the ring's overflow tail picks up exactly where the log-covered prefix ends. When the on-disk log lags the ring by more than the ring's own 256KB capacity — rare under normal operation (readLoop flushes log writes chunk-by-chunk), but reachable under a heavy output burst that outpaces a momentarily-slow disk write (matching the reported scenario: bulk tag/archive operations streaming a lot of output quickly) — the old code clamped the overflow and concatenated the log-covered prefix directly with the ring's overflow tail anyway. The two ranges are not contiguous in that case: the bytes in between were evicted from the ring before the log could catch up, and are unrecoverable from either source right now. The function's own comment already named this ("bytes... are unrecoverable") but the code spliced the two chunks together regardless, feeding x/vt content whose escape sequences and cursor state were never actually adjacent — producing exactly the reported signature (missing characters/words, garbled symbols at the seam).

This is a genuinely distinct root cause from #916 (BUG-075 was a plain incremental feed with no on-disk log involved at all) that happens to share the same "no bind event" surface signature, which is why it was easy to mistake for a #916 regression.

Fix

When the gap is unrecoverable, return just the log-covered prefix and its true total (logSize, not ringTotal). The caller records this as emuFedTotal, so understating it defers the unrecoverable range to the next Draw's ring-wrap check, which naturally retries the exact catch-up (readLogRangeForTask, #912/BUG-073) once the log has had a chance to catch up — instead of permanently losing content and mis-splicing what little was captured.

Test plan

  • TestReadLiveRebuildHistory_UnrecoverableGapDoesNotSpliceNonContiguousBytes — new test constructing exactly this scenario (log covers [0,4), ring currently holds only bytes [8,10), bytes [4,8) genuinely gone). Verified it fails on the pre-fix code (len(raw)=6, total=10 — the old code's raw/total mismatch, proving the caller would be lied to about what was actually fed).
  • Existing TestReadLiveRebuildHistory_LogTailOnly/OverflowMerge/NoLogFallback/NilSession pass unchanged (no behavior change for the non-clamped/normal cases).
  • go test ./internal/tui/terminal/... -race -count=1 green.
  • make pre-pr clean (same pre-existing vuln-gate exception as prior PRs in this series).
  • context/knowledge/gotchas/pty-terminal.md updated (BUG-076).
  • Rebased cleanly onto current master (this branch had the same squash-merge divergence issue as Fix TUI agent-pane live-stream TOCTOU race (BUG-075) #916 — reset to origin/master + cherry-picked just this commit, one trivial textual conflict in the shared gotcha-index table row, resolved manually).

🤖 Generated with Claude Code

Co-authored-by: Claude noreply@anthropic.com

readLiveRebuildHistory's merge assumes the ring's overflow tail picks up
exactly where the log-covered prefix ends. When the on-disk log lags
the ring by more than the ring's own 256KB capacity — rare under normal
operation, but reachable under a heavy output burst that outpaces a
momentarily-slow disk write — the old code clamped the overflow and
concatenated the log prefix directly with the ring's tail anyway. The
two ranges are NOT contiguous in that case: the bytes in between were
evicted from the ring before the log could catch up, and are
unrecoverable from either source right now. Splicing them together
feeds x/vt content whose escape sequences and cursor state were never
actually adjacent, producing unexplained missing characters/words and
garbled symbols at the seam — on an actively-streaming pane with no
bind/resize event at all, easy to mistake for BUG-075 (same "no bind
involved" signature, different mechanism: BUG-075 was a plain
incremental feed with no log involved).

Fix: when the gap is unrecoverable, return just the log-covered prefix
and its true total (logSize, not ringTotal). The caller records this as
emuFedTotal, so understating it defers the unrecoverable range to the
next Draw's ring-wrap check, which naturally retries the exact
catch-up (readLogRangeForTask, BUG-073) once the log has caught up,
instead of permanently losing content and mis-splicing what was
captured.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Merging this branch will increase overall coverage

Impacted Packages Coverage Δ 🤖
github.com/drn/argus/internal/tui/terminal 96.00% (+0.11%) 👍

Coverage by file

Changed files (no unit tests)

Changed File Coverage Δ Total Covered Missed 🤖
github.com/drn/argus/internal/tui/terminal/terminalpane.go 96.18% (+0.12%) 811 780 (+1) 31 (-1) 👍

Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code.

Changed unit test files

  • github.com/drn/argus/internal/tui/terminal/terminalpane_test.go

@anutron
anutron merged commit 5e35477 into master Jul 31, 2026
1 check passed
@anutron
anutron deleted the argus/pty-garble-investigate branch August 3, 2026 00:29
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.

1 participant