Implements the 4-phase documentation reorganization master plan. - Consolidation: Merged brief/, handoff/, planning/, and decisions/ into briefs/, handoffs/, plans/, and adr/ respectively (101 ADRs relocated) - Root Cleanup: Relocated HANDOFF-gpt55-*.md and key top-level docs (runtime_contracts.md, etc.) to canonical folders. Added superseded alerts. - Indices & Navigation: Created docs/README.md navigation document, docs/sessions/README.md index, docs/adr/README.md index - Note: Also includes prior commit adding ADR-0200+ corpus hygiene governance (ADR-0225, dependency map, backfilled cross-references)
3.4 KiB
ADR-0156 — Atomic engine-state checkpoint writes (W-022 / L10b.1)
Status: accepted Date: 2026-05-25
Context
ADR-0146 §"File Operations and Invariants" specified:
"Checkpointing must be atomic (e.g., write to temporary file and rename) to prevent corruption if the process is terminated mid-write."
The W-008 implementation used Path.write_text directly on all three
checkpoint files (manifest.json, recognizers.jsonl,
discovery_candidates.jsonl). write_text opens the target with
O_TRUNC, so the existing file is truncated before the new
content is written. SIGINT, SIGKILL, hardware reset, or even an
exception in serialization between truncate and write leaves a
partial / empty file on disk. The next process's load then either
fails or — worse — silently restores from a half-written checkpoint.
L10's "reboot is recovery, not control flow" invariant requires that reboot find a consistent prior state. Mid-write corruption violates that invariant.
Decision
Introduce engine_state._atomic_write_text(target, content):
tempfile.NamedTemporaryFile(dir=target.parent, delete=False)— keeps the temp on the same filesystem as the target soos.replaceis atomic.- Write content,
fh.flush(),os.fsync(fh.fileno())— content is on the disk's write queue before rename. os.replace(tmp_path, target)— atomic same-directory rename.- On any exception before or during rename, unlink the temp file (best-effort) and re-raise.
All three EngineStateStore.save_* methods route through this
helper. The directory-create step moves from each caller into the
helper.
Invariants pinned by tests
tests/test_adr_0156_atomic_checkpoint.py (9 tests):
- Atomic write creates target / overwrites existing / creates parent dir
os.replacefailure preserves the prior target file byte-identicallyos.replacefailure cleans up the temp file- Temp file lives in the target's directory (same-FS requirement)
- Store-level:
save_manifest,save_recognizersfailure preserves prior - Round-trip content unchanged after the atomic refactor (regression guard)
Determinism
No payload bytes change. The on-disk content is byte-identical to pre-W-022 for the same input. Only the failure-mode contract improves: prior valid checkpoint stays visible, never a partial new one.
Out of scope
reboot_eventaudit trail entry (L10 scope §Sub-question 3) — L10b.3 / W-024.- Revision-mismatch warning on load (ADR-0146 §Risks line 127) — L10b.2 / W-023.
- fsync of the parent directory after rename. POSIX strictly requires this for crash-safety of the rename metadata itself. Defers to a future ADR if a real-world corruption is observed; the same-FS rename + content fsync we ship today is sufficient for the SIGINT/SIGKILL failure modes ADR-0146 specifically named.
- Cross-process locking. Shape B is single-process per ADR-0146; concurrent writers are out of scope.
Validation
tests/test_adr_0156_atomic_checkpoint.py(9 passed)tests/test_adr_0146_engine_state.py(8 passed — round-trip regression guard)core test --suite smoke(67 passed)core test --suite cognition(120 passed, 1 skipped)
Closure
L10b.1 closes the highest-leverage Shape-B correctness gap: a checkpoint is now either fully-prior or fully-new on disk, never partial. Reboot recovery is no longer one signal away from silent corruption.