Research: Corrupt per-mission state file crashes (#4642)
Consolidated from a 3-lens research squad (structural root-cause, architecture-scout census, remediation design) run 2026-09-18/19 against skupstream/main @ 29507866f3.
Decision: mirror the MissionMetaReadError precedent, no new primitive
- Rationale:
meta.jsonalready has the correct shape — a typedMissionMetaReadError(RuntimeError)(core/paths.py:544) raised byload_meta_fail_closed, routing through the kernel decoderkernel.meta_decode.decode_meta. Mirroring it is the smallest safe fix for a release blocker. - Alternatives considered: a generic
read_json_state()kernel primitive + a single CLI-boundary exception hook. Rejected for the P0 (spans ~8 subsystems with divergent typed-error contracts; risks the graceful siblings). Deferred to a hardening mission that subsumes #4600 + #4642.
Root cause: unenforced two-half convention
Fail-closed handling requires (A) the reader to wrap raw decode failures into a typed domain error AND (B) the command boundary to catch it. Nothing enforces either half. The two instances fail on opposite halves:
next(Half B):MissionMetaReadErroris raised correctly but escapes because the parse happens downstream of_resolve_mission_slug(viaquery_current_state→runtime_bridge→load_meta_fail_closed), outside thenext_cmd.py:195-217try. Adding it to that except-list does NOT catch the crash — verified on current main by three independent traces.- decisions (Half A):
decisions/store.py:load_indexnever wrapsjson.loads(read_text)/model_validate; rawJSONDecodeError/UnicodeDecodeError/ValidationErrorescape.ownership.py:531already re-implements the guard locally — proof the source reader lacks it.
Recurrence evidence (adversarial finding, accepted)
core/agent_config.py:78 (the #4600 fix) comments "same defect class as ledger SK-16, charter status --json's leaked-exception bug" — this is at least the 4th occurrence of the class. Disposition: accepted → recorded as the driver for the deferred hardening mission (C-003), not folded into this P0.
Scope finding: wps_manifest.py is OUT (adversarial contested → changed)
paula-patterns flagged core/wps_manifest.py as same-tier and argued for P0 inclusion. Independent trace: its next-reachable call site (runtime_bridge.py:1019) is already wrapped in a broad except Exception→warning, so it does NOT crash next. Real exposure is finalize-tasks (a command not named in #4642). Disposition: changed — excluded from P0, moved to the deferred mission (where the broad except Exception is itself a smell to replace with the typed seam).
Partial-guard tail (deferred)
decisions/service.py, merge/state.py, review/baseline.py catch JSONDecodeError but not UnicodeDecodeError (still crash on non-UTF-8); review/lock.py, review/artifacts.py, status/validate.py fully unguarded. All deferred to the hardening mission.