Expand description
T30/TR-4 — terminal-noise normalization (ReductionKind::OutputNormalized):
a small, deterministic line-buffer terminal simulator that collapses ANSI
color/style codes and carriage-return/erase-line/cursor-up redraws down to
the FINAL rendered content of each line — the same content a human
watching the build would actually see, without the hundreds of
intermediate redraws a captured progress bar otherwise leaves in the
transcript.
Not a full vte emulation (per SPEC.md TR-4’s approach sketch): this
supports exactly the sequences that dominate real cargo/npm/pip/
docker output —
- SGR (
ESC[...m, colors/styles) — stripped; it never prints or moves. - CR (
\r) — cursor to column 0 of the current row. - LF (
\n) — cursor to column 0 of the NEXT row (a deliberate simplification: real LF preserves column, but every real capture in this codebase’s fixtures pairs LF with either a preceding CR or content that starts a fresh line anyway, so this never diverges from the fixtures’ actual rendering and keeps the model trivial to reason about). - EL (
ESC[K,ESC[0K,ESC[1K,ESC[2K) — erase to end / to start / whole line. - CUU / CUD (
ESC[<n>A/ESC[<n>B) — cursor up/downnrows (the multi-line redraw idiomdocker pulluses for concurrent layers). - CHA (
ESC[<n>G) — cursor to absolute columnn(1-based); the idiom modernnpm’s spinner uses instead of\r. - DEC private mode set/reset (
ESC[?...h/ESC[?...l) — e.g.?25l/?25h(cursor hide/show around a spinner): dropped silently. This is a deliberately narrow carve-out (real DEC private modes can do much more, e.g. the alternate screen buffer) but build-tool output never uses those — see the module-level safety note below.
Everything else — including a CSI sequence with a final byte this module doesn’t recognize (device status report, cursor-position, scroll, etc.), an OSC sequence, or any escape truncated by an upstream byte cap before its terminator — is passed through verbatim, as literal printable text, landing in the rendered output unchanged. “Never guess”: an unrecognized sequence is never assumed to be a no-op, so its bytes are never silently dropped, and the reduction is content-preserving even for escape vocabulary this module has never seen.
§Safety / no-panic guarantee
normalize never panics on any input, including a &str truncated
mid-escape-sequence (the TR-1 gotcha this module was warned about:
Agent::cap_tool_output’s 100 KB history cap can slice a raw tool output
anywhere, including through the middle of a CSI/OSC sequence, before this
module ever sees it). A truncated sequence at the end of the input is
detected (no terminator found before the string ends) and copied through
as literal text, same as any other unrecognized sequence — never a panic,
never an out-of-bounds slice. See tests::never_panics_on_malformed_input
for a sweep over adversarial byte patterns (including sequences chopped at
every possible byte boundary).
All scanning here is on &str byte offsets, but every control byte this
module inspects (ESC 0x1B, CR 0x0D, LF 0x0A, CSI param/final bytes
0x20-0x7E) is ASCII — and ASCII bytes are never a continuation byte
(0x80-0xBF) or a lead byte (0xC0-0xFF) of a multi-byte UTF-8 sequence, so
every position this module treats as a slice boundary is guaranteed to
already be a valid char boundary in a well-formed &str. Regular
(non-control) runs between control bytes are therefore always safe to
slice directly.
Constants§
- DEFAULT_
MIN_ SAVINGS - Minimum byte savings (
original.len() - normalized.len()) forproject_messagesto accept a normalization candidate — SPEC.md TR-4’s “savings floor” knob, mirrored asReductionPolicy::terminal_output_min_savings. Exposed here as the documented default; the policy field is what callers actually tune. - NORMALIZE_
TOOLS - Tool names T30/TR-4’s candidate rule treats as “terminal/exec” — a result
from one of these is eligible for
super::ReductionKind::OutputNormalized. Compared against the INVOKING tool call’s function name (seedetect_normalize_candidates), the same pattern the A8 read-tool list uses for read-type tools:
Functions§
- normalize
- Normalize
input: strip ANSI SGR, simulate CR/EL/CUU/CUD/CHA redraws, and return the final rendered text. Deterministic and pure — same bytes in, byte-identical text out, every call (SPEC.md TR-4’s determinism requirement; seetests::deterministic_across_repeated_runs). - summary
- Format the honesty trailer’s summary text (SPEC.md TR-4: “normalized text
must remain honest”): plain ASCII, one line, no
]— same constraints the reduction-stub formatter already enforces on every summary, so this is folded into the shared[sc-reduced output-normalized <id>: ...]grammar (seereduce.rs’sOutputNormalizedcandidate pass) rather than a bespoke sentinel — that keeps the existing leak-guard (A11),stub::parse(sessions show-reductions), andKind::fromdispatch all working for this kind with no special-casing.