Critical defect from Task 3 review: context_from_dict rebuilt
DifyBuilderContext field-by-field and never read repair_attempts or
last_repair_error, even though context_to_dict (asdict) wrote them.
BUILD_AWAIT_REPAIR is a waiting state, so every approve_repair is a
fresh turn that reloads the context via context_from_dict on
submit_action -> repair_attempts silently reset to 0 every turn,
incremented to 1, and could never reach _MAX_REPEATED_REPAIRS. The
_repair_is_repeating guard added in Task 3 was therefore structurally
unreachable in production (ESQ1-285/290's dead loop, still alive).
Adds both fields to the context_from_dict constructor call, following
the existing per-field coercion idioms (int() for the counter, matching
checkpoint_seq; bare .get with a "" default for the string, matching
its string-field neighbors).
Also adds:
- A round-trip + defaults-when-absent test pair in test_serde.py,
following the file's existing convention, reproducing the exact
reload-drops-fields regression.
- An integration test in test_handlers_build.py that drives the
"Repeated failure" branch through handle_test_and_repair itself
(StubAgent + FakeBuildDifyPort, not a direct _repair_is_repeating
call), so a future regression that re-disconnects serde from the
handler would be caught even if the pure guard-function unit tests
still pass in isolation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>