fix(normalize): strip Claude Code <local-command-*> tags and ANSI escapes - #1334
fix(normalize): strip Claude Code <local-command-*> tags and ANSI escapes#1334lucagentile wants to merge 2 commits into
Conversation
…apes Claude Code wraps every slash-command invocation in a five-tag envelope (<local-command-caveat>, <command-name>, <command-message>, <command-args>, <local-command-stdout>). strip_noise() previously covered only command-name and command-message; the other three survived into stored drawers as tag remnants like "<command-args></command-args>" or whole stdout payloads. Bash tool_result content captured by Claude Code preserves ANSI color codes verbatim. These pass through strip_noise() untouched, bloating embeddings (each escape is several BPE tokens) and garbling semantic search. Extend _NOISE_TAGS with the missing three, and add CSI + OSC ANSI strippers applied after tag removal in strip_noise(). Each ANSI pattern is anchored on the literal ESC byte so user prose that mentions e.g. "[1m]" by name stays intact — verbatim-safety preserved per the existing design. Tests: add coverage for each new tag, the full slash-command envelope, the empty <command-args></command-args> shape, ANSI CSI / truecolor / cursor / OSC-title / OSC-hyperlink sequences, ANSI inside a noise tag (no double-strip needed), and two preservation tests for user prose that documents these constructs by name. Closes MemPalace#1333
|
Hi, This PR changes Severity: action required | Category: correctness How to fix: Bump NORMALIZE_VERSION to 3 Agent prompt to fix - you can give this to your LLM of choice:
Qodo code review - free for open-source. |
…hromadb version mismatch When an HNSW segment has a mtime gap > 7200s (2 h), quarantine it regardless of the metadata sniff-test result. ChromaDB flush-lag is measured in seconds; a 2+ hour gap means the segment was written by a different process/version. Observed: chromadb 0.6.x segfaults loading segments whose metadata passes the pickle format check but whose binary layout is incompatible with the current runtime. The original directory is renamed (not deleted), so manual recovery is still possible if the heuristic misfires. Includes cherry-pick of MemPalace#1334: fix(normalize): strip Claude Code local-command tags and ANSI escapes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous-mined drawers stamped with v2 won't be re-mined unless the schema version advances; the new tag/ANSI sweeps need this gate to flip so existing transcripts get rebuilt clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ion-safe Local integration of the fix for MemPalace#1333 (strip slash-command envelope remnants and ANSI escape sequences from Bash-tool output), informed by PR MemPalace#1909 upstream. Not opened as a competing PR -- MemPalace#1909 is already an active, close-to-mergeable submission for the same issue; this is for local use, with two things MemPalace#1909 doesn't have: 1. The tag-boundary fix MemPalace#1909's own review comment flagged: a bare `\b` after the tag name incorrectly matches the boundary between "s" and "-" inside a longer tag name (e.g. <command-args-extended>), causing <command-args>'s lazy body to stop early and misparse unrelated content. Fixed with a `[\s>]` lookahead instead. 2. A third ANSI pattern (_ANSI_SIMPLE_RE) for the "Fe minus CSI/OSC" and "Fs" escape sequence classes -- single ESC + one byte, no payload. Neither MemPalace#1334 nor MemPalace#1909 cover this; both only strip CSI and OSC. The motivating case: watch-mode dev tools (tsx watch, nodemon) emit ESC c (RIS, full terminal reset) before reprinting output on a file-change restart, confirmed by reproducing it directly against the real API dev server. A backgrounded process's redirected stdout/stderr can carry this into a captured Bash tool_result verbatim. Also fixes a real truncation-safety bug found during review of the CSI pattern (both MemPalace#1909's and an earlier draft of this one shared it): with `[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]` (both middle groups zero-or-more), a genuinely truncated `ESC[` immediately followed by ordinary real text is indistinguishable from a valid empty-parameter CSI sequence, since the final-byte class (0x40-0x7E) covers nearly every letter. Confirmed empirically: `"before \x1b[this has words after"` silently became `"before his has words after"` -- the leading "t" of "this" eaten as a false CSI terminator. Real captured tool output does get truncated mid-write (a background process's redirected stdout racing its reader), so this isn't a contrived edge case. Fixed by requiring >=1 parameter byte (`[0-9;]+`, not `*`) and dropping the intermediate-byte class entirely -- real SGR/cursor codes are overwhelmingly digits + `;`, and genuine use of ECMA-48 intermediate bytes in ordinary terminal color/cursor output is vanishingly rare. The accepted tradeoff: a truly bare, zero-param CSI (e.g. `ESC[H`, cursor-home with no row/col) is no longer stripped -- left as harmless unstripped noise rather than risking real-word corruption. The OSC pattern has a narrower, documented residual risk in the same family (a truncated OSC's greedy payload scan can still treat a later, genuine bare BEL in real prose as a false terminator) -- not closed, since BEL essentially never appears in real captured text unlike the letters/punctuation that made the CSI case common. Covered by an explicit regression test that documents current behavior so any future change to that tradeoff is a deliberate, visible diff. Test suite substantially expanded given this ships to an external repo: real-world CSI/OSC/simple-escape shapes, the truncation-safety regressions above (including the accepted OSC/simple-pattern residual risks), boundary conditions (empty string, escape at start/end, escapes only, unterminated at EOF), Unicode interaction (multi-byte characters and combining sequences adjacent to escapes never split), idempotency (stripping twice equals stripping once), ReDoS/performance checks against adversarial input sizes, and hypothesis property-based fuzzing for the core invariants (escape-free text is never touched; a well-formed SGR pair never touches its surrounding text; a truncated CSI never eats more than its own bytes). 26 tests -> 69 tests in this area. Full suite: 3268 passed, 20 skipped -- no regressions (same 2 pre-existing unrelated failures as always, tracked separately). ruff check / ruff format -- clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
What does this PR do?
Fixes #1333.
Extends
mempalace/normalize.py:strip_noise()to remove three Claude Code transcript artifacts that currently survive into stored drawers:<local-command-caveat>,<command-args>,<local-command-stdout>— that Claude Code injects on every slash-command invocation.strip_noise()previously covered only<command-name>and<command-message>; the other three slipped through, leaving drawers with bare<command-args></command-args>remnants and whole<local-command-stdout>payloads.Bashtool_result content. These bloat embeddings (each escape is several BPE tokens of garbage) and garble semantic search when colored CLI output (e.g./context,/help, anything piped through a modern tool) lands in a transcript.Code change (
mempalace/normalize.py)command-args,local-command-caveat,local-command-stdoutto_NOISE_TAGS. They flow through the existing_tag_patternbuilder, so they inherit the same line-anchoring + blank-line-bounded body guarantee — a stray unclosed tag in one message still cannot eat content from neighbouring messages._ANSI_CSI_REmatches the standard CSI shapeESC [ params interm finalcovering SGR colors, cursor moves, screen ops._ANSI_OSC_REmatchesESC ] ... BELorESC ] ... ESC \for terminal title (OSC 0/2) and hyperlinks (OSC 8).\x1b), so user prose that mentions[1m]or]8;;by name never matches.strip_noise()after tag/line/chrome stripping and before the existing blank-line collapse. Tag-wrapped ANSI is already gone via_NOISE_TAG_PATTERNS, so the ANSI sweep only has to cover standalone ANSI in tool output — no double-strip path needed.Test change (
tests/test_normalize.py)Eleven new test cases, all under the existing
TestStripNoiseRemovesSystemChromeandTestStripNoisePreservesUserContentclasses to keep the verbatim-safety boundary discipline visible:test_strips_each_known_noise_tagextended to iterate the new tags too.test_strips_full_claude_code_slash_command_envelope— integration: real five-tag envelope stripped, real prose after it preserved.test_strips_empty_command_args_pair— empty-body shape Claude Code emits when a command takes no args.test_strips_ansi_color_codes,test_strips_ansi_truecolor_codes,test_strips_ansi_cursor_moves— CSI coverage including 24-bit truecolor sequences from the/contextrenderer.test_strips_ansi_osc_terminal_title,test_strips_ansi_osc_hyperlink— OSC coverage with both terminator forms (BEL, ST).test_strips_ansi_inside_noise_tag_with_tag— proves no double-strip is needed; tag-wrapped ANSI exits with its tag.test_user_documents_ansi_escape_by_name— user prose mentioning[1m]/[22m]literally (no ESC byte) is preserved verbatim.test_user_mentions_local_command_inline— inline<local-command-caveat>mention in user prose stays intact (line-anchoring guarantee).How to test
Locally: 1499 passed, 1 skipped on the full suite (no new skips introduced); 127/127 in
test_normalize.py(was 117); ruff lint clean;mempalace/normalize.pyis format-clean. The pre-existingruff format --checkdrift intests/test_normalize.pyis left untouched — out of scope for this fix.Checklist
python -m pytest tests/ -v --ignore=tests/benchmarks)ruff check .)