gabriel / musehub public
push-422-root-cause.md markdown
111 lines 4.7 KB
Raw
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago

Push 422 Root Cause — Have-Anchor Object Gap

Symptom

muse push staging dev returns:

push rejected: 824 snapshot object(s) not in push bundle or storage —
missing: sha256:00145bb1bd38, sha256:002028f94a3b, sha256:0059ccbc0d19…

The same 824 OIDs are missing on every push attempt. The error is deterministic.

Smoking Gun

The push log says:

[stream] walk: … (683 commits, 4789 objects)
[stream] loaded 3965 objects …

4789 − 3965 = 824. Exactly the number of missing objects. The have computation excluded 824 objects from the wire bundle, and the server reports exactly those 824 as absent from its DB.

Root Cause

The push command computes have from the target remote's live branch heads (GET /refs), filtered to commits that exist locally:

candidate_have = list(remote_branch_heads.values())
have = [c for c in candidate_have
        if c != local_head and _is_valid_commit_id(c) and commit_exists(root, c)]

If staging has a main branch whose head commit exists in the local repo, it becomes a have anchor. The object walk stops at main's head, excluding the 824 objects reachable from main but not from the new dev commits.

The integrity check on the final batch requires ALL snapshot-referenced objects to be in the server DB. Those 824 excluded objects are referenced by dev snapshot manifests (shared with main), but main was never successfully pushed to staging — so the server DB has no rows for them.

Invariant

MB-6: A push of branch B that excludes objects via `have=[A_head]`
      SUCCEEDS only if the server already has those objects in its DB
      (from a prior push of branch A or any other path).

      If the server does NOT have those objects, the integrity check
      correctly rejects the push with 422.

This invariant is already enforced by the server (see MB-3). The gap is on the client side: the walk assumes the server has branch-A objects, but branch A may never have been pushed to that remote.

Immediate Fix

Push main to staging before pushing dev. This puts the 824 shared objects into the server DB, so the next dev push finds them.

muse push staging main
muse push staging dev

Long-Term Fix

The have anchor for object exclusion should only include remote branch heads whose objects are verifiably on the server — i.e., commits that were previously pushed to THIS specific remote, not just commits that exist locally and whose ID happens to match a remote branch head.

Options:

  • Track per-remote "object-coverage" in local tracking refs (expensive)
  • On first push to a remote or force push, widen the bundle to include all ancestors regardless of have anchors (safe default)
  • Server: expose an object-presence check endpoint so the client can verify before excluding objects from the bundle

Timeline

Session What happened
Previous session SSL BAD_RECORD_MAC on MWP POST after 216 R2 PUTs — retry loop not firing (wrong exception type: _httpx.TransportError vs muse TransportError)
Previous session Fixed retry exception type; added 0.5s cooldown after R2 phase
This session Scaled cooldown to min(10s, n_large_objects * 0.05s); bumped retries 3→5
This session presign_and_register_objects() written + 4 TDD tests (PI-1–PI-4)
This session MB-1–MB-5 written + passing — confirms non-final batch objects land in DB
This session Root cause identified: 4789−3965=824; main never pushed to staging

Tests

ID File What it proves
PI-1 test_wire_presign_integrity.py presign_and_register_objects inserts storage_uri=pending row
PI-2 test_wire_presign_integrity.py push succeeds when snapshot refs presigned (pre-registered) object
PI-3 test_wire_presign_integrity.py presign_and_register_objects is idempotent
PI-4 test_wire_presign_integrity.py creates musehub_object_refs row
MB-1 test_wire_multibatch_push.py non-final batch objects land in DB after request
MB-2 test_wire_multibatch_push.py final batch succeeds when earlier batches stored objects
MB-3 test_wire_multibatch_push.py final batch fails when referenced objects never sent (negative baseline)
MB-4 test_wire_multibatch_push.py presigned objects pass final batch integrity check
MB-5 test_wire_multibatch_push.py rolled-back earlier batch causes final failure (causal proof)
MB-6 test_wire_multibatch_push.py final batch fails when have-excluded objects are absent from DB
MB-7 test_wire_multibatch_push.py final batch succeeds when have-excluded objects are pre-pushed (push main → dev)
File History 1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago