What happens. applyWrites checks the whole batch first, then writes its ops one at a time. The upfront check does not cover everything the per-op step can refuse. Two examples are a delete into a collection already holding 256 tombstones, and the log budget filling up mid-batch. When a later op is refused, the call returns an error, but the ops before it are already in memory and in the log, and they survive a restart. The registry logs "batch refused; nothing was written". It also skips the commit, the firehose event and blob reference marking for the ops that were applied.
How to reproduce.
crates/didbot-pds/src/provision/registry.rs:930-990checks delete targets, record room and blob holds. It does not check tombstone room.crates/didbot-pds/src/durable.rs:1720-1772applies each create at once. A later delete can return early fromguard_tombstone_room(...)?(durable.rs:1758) orwal.append(...)?(durable.rs:1766). The doc comment atdurable.rs:1716-1719says nothing can fail once planning has succeeded.registry.rs:996-1001logs the refusal and returns beforecommit_writeandannounce_batchrun.- A local test against b8bab73cf put and deleted 256 keys in a collection, put a key
victim, then applied[Create "fresh", Delete "victim"]. The batch returnedTombstonesFull, yetfreshwas readable afterwards, and again after reopening the store.
Why it matters. A client told its batch failed will retry it and create duplicates. The stray records are served by getRecord but sit outside the signed tree until a rebuild, and no relay ever receives an op for them. Blobs they name stay unreferenced, so the blob collector can delete them while records still point at them.
Fixed by https://tangled.org/permadeath.com/didbot/pulls/1244, now on main.