Repository navigation
fix(transforms): log the compression failures that silently become "no savings" - #4092
chopratejas wants to merge 37 commits into
Conversation
…o savings" Five places caught every exception from a compressor and returned the input unchanged with no log line, so a compression bug looked exactly like content that did not compress: - compression_batches.compress_batch_with_router: router.compress failure - cross_turn_dedup.dedup_blocks: its error stat is never read by the caller - lossless_compaction.compact_lossless: any fold that raises - kompress_compressor.store_kompress_in_ccr: CCR write failure (output ships without a retrieval marker) - smart_crusher CCR mirror: store write failure was logged at debug Each now logs a warning with counts and the exception, matching the existing _persist_to_python_ccr warnings. Return values are unchanged. Adds a test that compact_lossless logs a raising fold and stays quiet on a working one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR governanceThis PR follows the template and is marked ready for human review. |
Compressor and store exceptions can quote tool output, so each WARNING now names only the exception type and counts; the full detail moves to a DEBUG line with exc_info. Adds a parametrized test over all five failure paths: one WARNING without the exception text, and the detail only at DEBUG. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assisted-by: claude-code:claude-opus-5-5 Headline-Session: 6e77a807-8dd4-4a77-a2fa-ddce6dc2515e Headline-AI-Share: 78% strict / 78% weighted
JerrettDavis
left a comment
There was a problem hiding this comment.
Reviewed head f255772, including the comments and your replies. The warning sanitization and failure-path coverage are useful improvements, but moving arbitrary exception details to DEBUG leaves a privacy issue in all five changed paths.
I invoked the real lossless-compaction and Kompress CCR-store entry points with injected failures echoing the original tool output. Both correctly preserved their fallback behavior, and WARNING omitted the canary. Formatting the DEBUG records, however, exposed the tool-output canary through exc_info=True. An opt-in diagnostic level can still put private prompts, source content or credentials into retained/shared logs.
Please keep failure diagnostics payload-safe at every enabled level across batch compression, cross-turn dedup, lossless compaction, Kompress storage and SmartCrusher mirroring. Preserve exception class/count/hash metadata and existing fallback behavior; exclude arbitrary exception messages/chains that can echo content. Extend the existing five-path regression to format captured DEBUG and WARNING records and assert content/credential canaries are absent from every record. Checking WARNING alone misses the rendered traceback leak.
All 32 selected failure-logging/lossless tests passed (exit 0). The independent two-path formatted-log reproduction passed its assertions (exit 0). Main a9815bb is an ancestor, diff check passes, and GitHub reports conflict-free. Qualification stops at this concrete diagnostic issue. No contributor source edits or merge performed.
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…repeat warnings Log records are kept and shared, so no level may carry request content, tool output or credentials. headroom/log_safety.py describes an exception by its types and code locations without the message (full traceback only with HEADROOM_DEBUG_DUMP=full), redacts URL userinfo and query, bounds ids, and gives a warn-once guard that stays bounded instead of resetting. The request debug dump now uses the shared URL redactor and opt-in check. Refs #4136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cleanup/transforms-log-silent-failures
…at every level The five failure paths logged the exception at DEBUG with exc_info=True, so a formatted DEBUG record carried the traceback and with it any tool output or credential the exception quoted. The DEBUG line now uses describe_exception, which gives the exception types and code locations only. The WARNING metadata and the fallbacks are unchanged. The regression formats every captured DEBUG and WARNING record and asserts a content canary and a credential canary are absent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ilures' into cleanup/transforms-log-silent-failures
… overflow describe_exception printed OSError.strerror, which some subclasses fill with free text; it now prints os.strerror(errno). After its overflow notice, WarnOnce no longer lets forget() reopen the cap for new keys. Refs #4136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cleanup/transforms-log-silent-failures
describe_exception no longer needs a catch-all: frame data is read through dict's own methods with exact type checks, so the odd inputs it guarded against cannot raise (their tests still pass). safe_id catches only the errors repr raises in practice. Refs #4136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cleanup/transforms-log-silent-failures
… in URLs Every text a frame carries (module, file, function name) can be set by code compiled at runtime, so describe_exception now names a frame only when it is Headroom's own code: registered module namespace, plain headroom.* name, the module's own file, and an identifier function name. Library frames are skipped and only the innermost three Headroom frames are kept, in bounded memory. redact_url accepts hostless file/sqlite/unix URLs and a host only when it is a DNS name or IP address. Refs #4136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cleanup/transforms-log-silent-failures
describe_exception reads __cause__, __context__, __traceback__, errno and the class name through the base classes' own descriptors, so a subclass property that raises never runs. safe_id uses the built-in repr of str, bytes and numbers and shows any other type by name, so no __repr__ of the caller's object runs. No blind except is needed for either. Refs #4136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cleanup/transforms-log-silent-failures
…n safe_id - redact_url drops an IPv6 zone id (free text), accepts "_" in host labels (container service names) and gives non-ASCII hosts in IDNA form; a hostless URL must have ":///" right after its scheme. - safe_id checks the real type, not a fakeable __class__, and respects the process's int-to-text digit limit. - Class names are logged only when shaped like source identifiers. Refs #4136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cleanup/transforms-log-silent-failures
- A host that is not an IP address or a strict DNS name (no "_") may be a pasted key, so it is shown as <host>; scheme and port are kept. - The int digit-limit getter only exists from 3.10.7; earlier releases have no limit, so safe_id no longer raises AttributeError there. - The hostless check cleans the URL the same way urlsplit does. Refs #4136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cleanup/transforms-log-silent-failures
Description
Five places in the compression path caught every exception and returned the input unchanged, with no log line. A compression bug therefore looked exactly like content that did not compress. Each now logs a WARNING that names the exception type and counts, and a DEBUG line that names the exception types and code locations (
describe_exception). No level logs the exception text or a traceback, because compressor and store errors can quote tool output or credentials. Return values do not change.Refs #4136
Depends on #4138 (
headroom/log_safety.py); its files show in this diff until it merges.Type of Change
Changes Made
compression_batches.compress_batch_with_router: warn whenrouter.compressraises (the batch still passes through).cross_turn_dedup.dedup_blocks: warn on failure. The"error": Truestat it returned was never read.lossless_compaction.compact_lossless: warn when a fold raises, with the kind and length. It still never raises.kompress_compressor.store_kompress_in_ccr: warn on a CCR store failure. The output then has no retrieval marker.smart_crusherCCR mirror: a store write failure is now WARNING, not DEBUG.type(exc).__name__plus counts or the hash, and never the exception text. The detail is a separate DEBUG line built withdescribe_exception(e), which gives types and code locations and never the message or traceback (unless the operator setsHEADROOM_DEBUG_DUMP=full). It only runs when DEBUG is enabled.tests/test_transforms_failure_logging.py: one parametrized test over all five paths. It checks pass-through, one WARNING and one DEBUG detail, and formats every captured record withlogging.Formatterto assert a content canary and a credential canary (raised in the exception and its cause) are absent. All five fail on main, and all five fail on the earlierexc_info=Trueversion of this PR. A second parametrized test checksdescribe_exceptionis not called when DEBUG is off. A third runs the realcompact_losslessfallback with DEBUG on and anOSErrorwhose errno is out of the C int range (2**31, and10**10000built at runtime), with the integer digit limit on and off. It checks the original content comes back and the DEBUG line stays bounded.Testing
pytest)ruff check .)mypy headroom)Test Output
Real Behavior Proof
.venv, this branch's worktree with the prebuiltheadroom._core. TempHOME,HEADROOM_WORKSPACE_DIRandCODEX_HOME; nothing touched the real home.smoke.sh <worktree>); (2) forced failure:compact_losslesswithcollapse_runspatched to raise an error whose text contains a fake token, logging at INFO.Runtime Rollout Safety
Review Readiness
Checklist
CHANGELOG.md— it is generated by release-please from my Conventional Commit PR title (a CI guard enforces this)Screenshots (if applicable)
Not applicable.
Additional Notes
Part of maintainer-requested cleanup #4136 (dead code, duplication, debuggability), split into small PRs for review.
Documentation changes are not in this PR. They are recorded in the cleanup docs ledger for the single follow-up docs PR.
The image tile-optimizer item from the audit was dropped. A non-decodable image makes PIL raise
UnidentifiedImageErrorthere, so a warning would fire on ordinary bad input. There is no once-guard. Each warning fires only when a compressor raises or the CCR store fails, the same idiom as the existing CCR write-through warnings.🤖 Generated with Claude Code