Skip to content

deferredwork: a lone surrogate defeats _one_line's never-raises contract and crashes the sweep #329

Description

@pbean

Summary

deferredwork._one_line (added in #305) guards line breaks and documents a stronger contract
than it delivers:

Sanitizes; never raises. The close paths call these writers bare (sweep._close_resolved,
decisions.apply_pre_answer), so a ValueError here would end the sweep as crashed.

A lone surrogate defeats that without ever being a line break. Verified chain:

  1. json.loads on a session result.json containing "\ud800" yields the surrogate.
  2. validate_triage returns errors == [] and carries it into ResolvedEntry.evidence.
  3. LINE_BREAK_RE.search is False, so _one_line passes it through untouched.
  4. The write raises UnicodeEncodeError — a ValueError subclass — at sweep.py:942
    (_close_resolved), sweep.py:1071, or decisions.py:147.

That is precisely the outcome the sanitizer exists to prevent: a bare call on a close path taking
down the run. It compounds with the bare write_text issue, where the same call both raises and
truncates the ledger to zero bytes.

Pre-existing — the writers always encoded UTF-8 — but #305 introduced the "never raises" claim, so
either the claim or the code should change.

Fix shape

Either narrow the docstring to "never raises on a line break", or make it true:
value.encode("utf-8", "replace").decode("utf-8") before the break collapse. The latter keeps the
close paths genuinely crash-free, which is the doctrine #305 settled on.

Also worth checking: tui/app.py:_record_decision now catches ValueError, so the TUI degrades —
but the sweep close path has no such guard.

Found during the #305 review.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:deferred-workdeferred-work.md ledger and sweepbugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions