An interrupted blob upload was silent data loss: the packer writes a blob's chunks, blob_chunks, and blobs rows to the local index before the upload is attempted (internal/blob/packer.go). If the upload failed those rows survived, and the next run deduplicated against chunks whose data never reached storage, then exited 0 with a snapshot that failed to restore (chunk missing from blob map).
Fix (issue option b):
Deduplicate only against chunks held by a blob whose uploaded_ts is set (Chunks.ListInUploadedBlobs, used by loadKnownChunks).
At the start of every scan, drop blob rows with uploaded_ts IS NULL (their blob_chunks cascade) and any chunks left unreferenced, so the affected data is re-chunked and re-uploaded and a previously interrupted index is repaired.
The startup repair is load-bearing, not just hygiene: CompleteSnapshot attaches any blob holding a referenced chunk, so a lingering un-uploaded blob would be exported and then be missing at restore.
The storage-less path now marks its blobs uploaded in the same transaction that attaches them, so the "a snapshot's blobs are uploaded" invariant holds uniformly.
Test: TestBackupRetryAfterInterruptedUploadIsRestorable fails a blob upload through a storer wrapper, retries on the same index with a working backend, then restores and byte-compares. It fails without the change at plan build and passes with it.
The skipped test named in the issue lives in the unmerged #72 branch and is not present here, so this adds its own reproduction instead of depending on it.
Local make check is green. model: claude-opus-4-8
An interrupted blob upload was silent data loss: the packer writes a blob's `chunks`, `blob_chunks`, and `blobs` rows to the local index before the upload is attempted (`internal/blob/packer.go`). If the upload failed those rows survived, and the next run deduplicated against chunks whose data never reached storage, then exited 0 with a snapshot that failed to restore (`chunk missing from blob map`).
Fix (issue option b):
- Deduplicate only against chunks held by a blob whose `uploaded_ts` is set (`Chunks.ListInUploadedBlobs`, used by `loadKnownChunks`).
- At the start of every scan, drop blob rows with `uploaded_ts IS NULL` (their `blob_chunks` cascade) and any chunks left unreferenced, so the affected data is re-chunked and re-uploaded and a previously interrupted index is repaired.
The startup repair is load-bearing, not just hygiene: `CompleteSnapshot` attaches any blob holding a referenced chunk, so a lingering un-uploaded blob would be exported and then be missing at restore.
The storage-less path now marks its blobs uploaded in the same transaction that attaches them, so the "a snapshot's blobs are uploaded" invariant holds uniformly.
Test: `TestBackupRetryAfterInterruptedUploadIsRestorable` fails a blob upload through a storer wrapper, retries on the same index with a working backend, then restores and byte-compares. It fails without the change at plan build and passes with it.
The skipped test named in the issue lives in the unmerged #72 branch and is not present here, so this adds its own reproduction instead of depending on it.
Local `make check` is green. model: claude-opus-4-8
An interrupted blob upload left the blob's chunks, blob_chunks, and
blobs rows committed before the upload was attempted, so a later run
deduplicated against data that never reached storage and produced a
snapshot that reported success but could not be restored.
Chosen fix (issue option b): a chunk counts as known only when a blob
holding it has uploaded_ts set, and each run drops un-uploaded blob
rows and the chunks they orphan at startup, so the affected data is
re-chunked and re-uploaded. Option a (withholding all row writes until
the upload succeeds) would entangle the packer's writes with upload
ordering; gated reads plus a startup repair is smaller and self-healing.
Blobs recorded with no remote backend are marked uploaded so the
invariant holds uniformly.
model: claude-opus-4-8
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
An interrupted blob upload was silent data loss: the packer writes a blob's
chunks,blob_chunks, andblobsrows to the local index before the upload is attempted (internal/blob/packer.go). If the upload failed those rows survived, and the next run deduplicated against chunks whose data never reached storage, then exited 0 with a snapshot that failed to restore (chunk missing from blob map).Fix (issue option b):
uploaded_tsis set (Chunks.ListInUploadedBlobs, used byloadKnownChunks).uploaded_ts IS NULL(theirblob_chunkscascade) and any chunks left unreferenced, so the affected data is re-chunked and re-uploaded and a previously interrupted index is repaired.The startup repair is load-bearing, not just hygiene:
CompleteSnapshotattaches any blob holding a referenced chunk, so a lingering un-uploaded blob would be exported and then be missing at restore.The storage-less path now marks its blobs uploaded in the same transaction that attaches them, so the "a snapshot's blobs are uploaded" invariant holds uniformly.
Test:
TestBackupRetryAfterInterruptedUploadIsRestorablefails a blob upload through a storer wrapper, retries on the same index with a working backend, then restores and byte-compares. It fails without the change at plan build and passes with it.The skipped test named in the issue lives in the unmerged #72 branch and is not present here, so this adds its own reproduction instead of depending on it.
Local
make checkis green. model: claude-opus-4-8View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.