Skip to content

Don't mistake a failed decode thread for end-of-input#1052

Merged
happycube merged 1 commit into
happycube:mainfrom
skyeckstrom:fix/decodefield-exception-mistaken-for-eof
Jul 19, 2026
Merged

Don't mistake a failed decode thread for end-of-input#1052
happycube merged 1 commit into
happycube:mainfrom
skyeckstrom:fix/decodefield-exception-mistaken-for-eof

Conversation

@skyeckstrom

@skyeckstrom skyeckstrom commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Checklist

  • I have searched the open pull requests to confirm this change has not already been submitted.
  • My branch is up to date with the target branch.
  • I have tested my changes and all existing tests pass.
  • I have updated documentation where necessary.
  • My code follows the project's coding standards (see CONTRIBUTING.md).

Description

decodefield() sets rv['field'] = None / rv['offset'] = None before any code that can raise —
the same values the EOF path returns. On the worker thread, an exception leaves those untouched, so
readfield() reads (None, None) and treats a dead thread as end-of-input: the decode exits 0 with
a truncated .tbc. This records the exception on rv and re-raises it after the join.

Motivation

A decode-time exception on the worker thread silently truncates the output — exit 0, and a
.tbc.json reporting the short length as the whole tape. It hit me as 40546 of 98135 frames (41%)
reported as complete. Any exception a field's process() raises has the same effect, which is why
the fix belongs on the readfield/decodefield seam rather than on whatever raised.

Related Issues

Fixes #1051

Changes Made

  • decodefield(): init rv['exception'] = None with the other sentinels; set it in the existing
    except Exception handler (which was a no-op raise e) before re-raising.
  • readfield(): on the threaded path, re-raise a recorded exception after the join, before
    (None, None) is read as EOF.
  • tests/test_decodefield_errors.py (new).

The redo path is untouched — it runs decodefield() on the main thread (exceptions already
propagate) and discards the speculative result by design.

Testing

  • All existing tests pass (pytest)
  • New tests: tests/test_decodefield_errors.py — 5 tests; 4 fail on unpatched main (a dead
    thread reads as EOF), all 5 pass with the change.

Screenshots (if applicable)

N/A.

Additional Notes

Deliberately minimal and structure-free, so the DAG/controller rewrite noted at core.py:4126 can
delete it wholesale. On failure the traceback now prints twice (worker excepthook + the re-raise);
suppressing the first would mean overriding threading.excepthook, out of scope here. Written with
AI assistance (disclosed, as is normal in this repo).

decodefield() pre-populates its result dict with field/offset None sentinels
before any code that can raise. Those sentinels are also exactly what a genuine
end-of-input returns (the `rawdecode is None` early return), so when
decodefield() runs on a worker thread and raises, readfield() reads the
untouched sentinels back and cannot tell the two apart: the (None, None) flows
into the "EOF, probably" return, main.py sets done=True, prints "Completed:
saving JSON and exiting" and exits 0.

The traceback is still printed by the default threading excepthook, but the
decode is silent in exit code, control flow and the .tbc.json -- which records
the truncated field count as authoritative, so downstream tools treat a partial
capture as a complete one. main.py's bug-report handler never fires because no
exception ever crosses the main thread.

Record the exception on the shared dict and re-raise it in readfield() after the
join, before the sentinels can be read as EOF. The redo path is untouched: it
calls decodefield() directly on the main thread, where exceptions already
propagate, and it discards the speculative thread's result by design.

This is deliberately minimal and makes no structural change to the threading, so
the planned DAG/controller rewrite can drop it wholesale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@happycube
happycube merged commit 8af0932 into happycube:main Jul 19, 2026
6 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.

A failed decode thread is indistinguishable from EOF (decode truncates and exits 0)

2 participants