Skip to content

fix(image): one bad tile can no longer kill the whole canvas frame apply - #90

Merged
dorkmo merged 2 commits into
mainfrom
canvas-transcode-guard
Aug 2, 2026
Merged

fix(image): one bad tile can no longer kill the whole canvas frame apply#90
dorkmo merged 2 commits into
mainfrom
canvas-transcode-guard

Conversation

@dorkmo

@dorkmo dorkmo commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Fixes the pre-existing canvas bug surfaced during the F10 on-air acceptance (2026-08-01, RESULTS §6; found by PR #89's verification session).

The incident

With Pillow absent on the base station and the stream switched to mono_g4, the tile transcoder's lazy from PIL import Image raised ModuleNotFoundError — an exception type Canvas.apply's per-tile catch (CodecDecodeError only) did not cover. The exception unwound after _last_base_seq was adopted and before any tile applied, so the canvas silently froze while appearing to track the stream, and the request_keyframe path never ran (paho swallows handler exceptions above it). The only visible symptom was F10's stale-tile reports showing a canvas that never freshened.

The fix — two layers

  • codec_decode.transcode_to_webp: any transcoder exception other than CodecDecodeError is folded into CodecDecodeError (original exception preserved as __cause__) — every transcoder failure is a decode failure from the caller's perspective.
  • Canvas.apply: the per-tile catch broadens to Exception as the invariant's backstop — drop the tile, request a keyframe, keep applying the rest of the frame.

Tests

test_canvas_transcode_errors.py (4 new):

  • exact incident repro (mono_g4 delta + missing PIL): frame survives, seq adopted, request_keyframe set with the PIL reason, existing tiles untouched;
  • CodecDecodeError still passes through unwrapped;
  • arbitrary mid-frame transcoder death (RuntimeError on the 2nd of 3 tiles): tiles 1 and 3 still apply with fresh arrived_ms.

Full base-station suite (CI invocation): 1093 passed, 2446 subtests — including the pre-existing test_bad_codec_drops_tile_and_requests_keyframe, unchanged.

🤖 Generated with Claude Code

On air 2026-08-01 (F10 acceptance §6): with Pillow absent and the stream
switched to mono_g4, the lazy `from PIL import Image` raised
ModuleNotFoundError inside the tile transcode — an exception type
Canvas.apply's per-tile catch (CodecDecodeError only) did not cover. The
exception unwound AFTER _last_base_seq was adopted and BEFORE any tile
applied, so the canvas silently froze while appearing to track the
stream, and the request_keyframe path was skipped because the exception
unwound through web_ui's ingest (paho swallows handler exceptions).

Two layers now guarantee the invariant:
- transcode_to_webp folds any transcoder exception into CodecDecodeError
  (with the original as __cause__), keeping the caller-facing taxonomy;
- Canvas.apply's per-tile catch broadens to Exception as the backstop —
  drop the tile, request a keyframe, keep applying the rest.

Regression tests reproduce the exact incident (mono_g4 + missing PIL)
and an arbitrary mid-frame transcoder death: remaining tiles still
apply, arrived_ms refreshes, request_keyframe is set. Suite 1093/2446.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 2, 2026 00:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the base-station image pipeline so a single tile transcoder failure (including non-CodecDecodeError exceptions like missing Pillow) cannot abort an entire Canvas.apply() call and silently freeze the displayed canvas.

Changes:

  • Wrap unexpected transcoder exceptions in codec_decode.transcode_to_webp() into CodecDecodeError (preserving the original as __cause__).
  • Broaden Canvas.apply() per-tile exception handling to ensure a bad tile is dropped, a keyframe is requested, and the rest of the frame continues applying.
  • Add regression tests covering the reported on-air incident and mid-frame transcoder failure behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
LifeTrac-v25/DESIGN-CONTROLLER/base_station/tests/test_canvas_transcode_errors.py Adds targeted tests reproducing the incident and asserting frame-level survivability under transcoder exceptions.
LifeTrac-v25/DESIGN-CONTROLLER/base_station/image_pipeline/codec_decode.py Ensures any non-CodecDecodeError thrown by a transcoder is folded into CodecDecodeError for consistent caller handling.
LifeTrac-v25/DESIGN-CONTROLLER/base_station/image_pipeline/canvas.py Prevents per-tile exceptions from aborting frame application; requests a keyframe and continues processing remaining tiles.
Suppressed comments (1)

LifeTrac-v25/DESIGN-CONTROLLER/base_station/image_pipeline/canvas.py:160

  • The per-tile handler now catches all Exception types, but the update reason prefix still says "codec_decode_error" and the warning log drops the traceback. This makes non-codec failures harder to diagnose and the reason string misleading.
                LOG.warning("canvas: dropping tile %d codec=%d: %s",
                            tile.index, frame.codec, exc)
                update.request_keyframe = True
                update.reason = update.reason or f"codec_decode_error: {exc}"
                continue

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

self.assertEqual(upd.updated_indices, [])
# The keyframe-era tiles are untouched, not refreshed and not zeroed.
self.assertEqual(self.canvas._tiles[5].arrived_ms, 50)
self.assertEqual(self.canvas._tiles[7].arrived_ms, 50)
…label

Copilot's active comment: the incident-repro test asserted tiles 5 and 7
untouched but skipped tile 6, the other tile the failing frame carried.
Now all three are pinned.

Copilot's suppressed comment (valid): the broadened catch labelled every
failure "codec_decode_error" and dropped the traceback. Split back into
two handlers — CodecDecodeError keeps its label and warning; the
backstop uses LOG.exception (traceback preserved) and a distinct
"tile_apply_error: <Type>: <msg>" reason. Backstop test pins the label.

Suite 1093/2446 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dorkmo
dorkmo merged commit 02a84f2 into main Aug 2, 2026
10 of 22 checks passed
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.

2 participants