Skip to content

cold-store-reaper: state.json and credentials.json are written in place with O_TRUNC — one torn write wedges every later run with a raw JSONDecodeError #3279

Closed

Description

@dezren39

Problem

ColdStore._write_private_json (pkgs/cold-store-reaper/cold_store.py:373-378) writes credentials.json and state.json in place: os.open(path, O_WRONLY|O_CREAT|O_TRUNC) followed by json.dump directly into the destination file. There is no temp file, no os.replace, and no fsync. A crash, power loss, OOM kill, or any exception mid-json.dump leaves the destination truncated to a partial JSON document.

Both readers then fail hard with a raw error, not the module's own error type:

  • load_state (cold_store.py:393-396) does a bare json.loads(self.state_path.read_text()) — a torn state.json raises json.decoder.JSONDecodeError uncaught. load_state is on the path of ensure_folders (:687) and _folder_id/_d1_endpoint (:709, :1242), so one torn write wedges every later run until a human hand-repairs the file.
  • load_credentials (cold_store.py:381-391) does the same bare json.loads — a torn credentials.json raises raw JSONDecodeError instead of the ColdStoreError("credentials.json is missing …")-style actionable errors the function otherwise raises, masking the real cause (the file exists; it is half-written).

save_state (cold_store.py:398-405) is called on routine paths (e.g. after folder discovery, :705), so the exposure window is every run, not just init.

Evidence (verified this run, 2026-10-31)

Code: cold_store.py:373-378 (in-place O_TRUNC write), :381-396 (bare json.loads in both loaders), :398-405 (save_state via the same writer).

Laptop probe (source sha-matched: cold_store.py sha256 fba42e08027514abae2de1602e755854ebfd6a3b578a5eece34b6c0becd39b95 on both sandbox main ed3a285e and the laptop copy; probe dir ~/git/fleet/probe-2026-10-31-laneB-coldstore-atomic/):

  • Control: save_state({"folders": {"a": "1"}}) then load_state() returns the dict.
  • Simulated crash mid-write (partial JSON flushed, then the write aborts): state.json is left as b'{"folders": {"a": "1"'.
  • After that, load_state() raises JSONDecodeError: Expecting ',' delimiter: line 1 column 22 — and isinstance(e, ColdStoreError) is False.
  • Same simulation against _write_private_json(credentials_path, …): load_credentials() raises the same raw JSONDecodeError.

Contrast within the same repo: pkgs/id's JSONL stores treat a crash-torn tail as a normal, repairable case (e.g. Petnames::open drops and truncates a torn final line). Cold-store's two most state-critical JSON files get no such protection.

Why it matters

Cold-store is the backup system: its state.json carries the Drive folder map every backup/restore run depends on, and credentials.json is hand-filled from Bitwarden — a torn credentials file destroyed by a crash during an unrelated save_state-adjacent write forces manual credential re-entry. A backup tool whose own state files are the least crash-safe part of the system will eventually wedge a nightly run with an inscrutable stdlib traceback instead of an actionable error.

Distinct from

Acceptance criteria

  • _write_private_json writes to a temp file in the same directory, fsyncs, and os.replaces onto the destination (preserving 0600), so a crash at any point leaves either the old or the new complete document.
  • load_state / load_credentials on a corrupt file raise ColdStoreError naming the file and the parse problem (or recover to a documented default), never a raw JSONDecodeError.
  • Regression test: simulate a mid-write failure (partial write + raise) and assert the previous complete document still loads, for both state and credentials paths.
  • Existing cold-store test suite passes.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    effort/sSmall: expected under 2 hours of work
    app/cold-store-reaperPackage/app: cold-store-reaper (pkgs/ or apps/ in this repo)
    on Nov 1, 2026
  2. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    Filed this run by new-issue-creator-code-b (Lane B). Found via: code read of pkgs/cold-store-reaper/cold_store.py (_write_private_json :373-378 in-place O_TRUNC write; bare json.loads in load_state :393-396 and load_credentials :381-391) plus a laptop probe on sha-matched source (cold_store.py sha256 fba42e08027514abae2de1602e755854ebfd6a3b578a5eece34b6c0becd39b95; probe ~/git/fleet/probe-2026-10-31-laneB-coldstore-atomic/): a simulated mid-write crash leaves state.json as partial JSON and load_state/load_credentials then raise raw JSONDecodeError (not ColdStoreError). Deduped against open+closed cold-store issues (#3240 is github-backup's files; #2550 is Drive/ledger crash-consistency; #940 was state contents/permissions). Enqueued as a triage packet on repo-health-code-pilot.

  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  4. 29 remaining items

  5. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — picked up
    Agent: repo-worker-code (OpenChamber scheduled lane)
    Task: repo-health-code-pilot task 5706 (small_fix)
    At: 2026-11-02T14:07:25Z
    State: claimed; fresh verification complete
    Found via: atomic claim from repo-health-code-pilot.
    Fresh state: #3279 OPEN, assigned to dezren39; canonical queue PR #4361 OPEN (non-draft, head d64a9e9), checks python/worker pending; no held/user-owned/do-not-merge/do-not-auto-edit restrictions; no competing PR.
    Next: resume branch queue/5706-3279-cold-store-atomic in /home/user/git/fleet/5706-3279-cold-store-atomic; re-verify tests; poll CI up to 5 min, park if still pending.
    Links: #3279 · #4361 · task 5706

  6. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 2, 2026
  7. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — parked
    Agent: repo-worker-code (OpenChamber scheduled lane)
    Task: repo-health-code-pilot task 5706
    At: 2026-11-02T14:14:42Z
    State: parked
    Fresh evidence: #3279 OPEN; canonical queue PR #4361 OPEN non-draft at d64a9e9; worktree suite green (120 tests OK, 1 skipped); CI poll 14:09–14:14Z still queued (python unittest suites + worker local proof, run 41448228868).
    Disposition: parked for 20 minutes for CI. work/assigned remains set.
    Resume: revalidate PR/issue/checks, disposition findings, merge if green, then complete fixed_merged.
    Links: #3279 · #4361 · task 5706

  8. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: picked up

  9. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: parked

  10. added a commit that references this issue on Nov 2, 2026
  11. removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    work/assignedQueue worker lifecycle state
    on Nov 2, 2026
  12. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/cold-store-reaperPackage/app: cold-store-reaper (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions