Audited all 143 plan files across the 10 plans/ folders. Each plan now carries a Status header (shipped | in-progress | queued | abandoned) backed by codebase evidence, and every folder has a README.md index grouping plans by status. Shipped plans were moved into a per-folder plans/archive/ (via git mv, history preserved) so each plans/ folder surfaces only live work: shipped (archived): 122 in-progress: 8 queued: 7 abandoned: 4 docs/plans/ is the frozen legacy folder; its plans were stamped and indexed in place (48 of 52 archived) but it remains closed to new plans. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016hpUivtmq4pgzqRbY6MYrA
3.4 KiB
TODO: Deletion Requests (kind 5) for Gift Wraps
Status: queued — none of the four steps landed:
HostStubstill lacks arecipientfield andDeletionIndexhas no gift-wrap recipient-authored handling; header itself reads "Open — not implemented". Audited 2026-06-30.
Date: 2026-06-12
Status: Open — design agreed, not implemented
Modules: quartz (DeletionIndex), amethyst (LocalCache)
The special case
Gift wraps (kind 1059) are signed by a discarded throwaway key, so the
normal NIP-09 rule — a deletion only applies when its author equals the
target's author — can never match a wrap. The intended rule: a kind-5
authored by the wrap's p tag (the recipient) may delete it. A
recipient deleting their own received wrap is also the only deletion a
client can express without leaking the private rumor id (the rumor id
must never appear in a public kind-5).
Current behavior (verified 2026-06-12)
DeletionIndex is strictly author-keyed (DeletionRequest(targetId, deleterPubkey)), the live-delete path requires deleteNote.author == deletion.pubKey, and no downward cascade (wrap → seal → rumor) exists —
deleteEnvelopes only walks upward via Note.rumorHost.
| Deletion e-tags | authored by | live message deleted? | blocks later insert? |
|---|---|---|---|
| wrap id | recipient (p tag) | no (key mismatch + no cascade + wrap note usually GC'd) | no (tombstone keyed (wrapId, recipient), check uses (wrapId, throwawayKey)) |
| seal id | sender | seal note only; message survives (no cascade) | yes — accidental: seals are sender-signed, and GiftWrapEventHandler gates the unwrap on justConsume(seal) |
| rumor id | sender | yes (private un-react path; deleteEnvelopes cascades upward) |
yes |
Only the rumor-id direction works; the wrap-id direction — the one the special case describes — does nothing.
Implementation sketch
-
Insertion blocking (quartz, small): in
DeletionIndex.hasBeenDeleted, when the event is aGiftWrapEvent, additionally checkDeletionRequest(event.id, event.recipientPubKey()). A recipient-authored tombstone then blocks the wrap injustConsumebefore it is ever unwrapped, which blocks the message. -
Recipient on the stub (commons, tiny): add
recipient: HexKey?toHostStub(one shared-string reference per rumor), populated fromGiftWrapEvent.recipientPubKey()at unseal time, so the validation below works after the wrap note is GC'd. -
Live cascade (amethyst
LocalCache.consume(DeletionEvent)): the wrap note that knew itsinnerEventIdis GC'd by the time a deletion arrives, so find the rumor by reverse lookup: scan notes fornote.rumorHost?.id == deletedId(precedent: the addressable pass in the same function already does a fullnotes.forEach; deletions are rare). Validatedeletion.pubKey == note.rumorHost.recipient, thendeleteNote(rumor)— envelope cleanup and chatroom removal already follow from the existing deleted-notes pipeline. -
Tests: block-before-unwrap (tombstone first, wrap second → message never materializes) and delete-after-unwrap (message in a chatroom, recipient-authored kind-5 for the wrap id → rumor and envelopes gone).
Note: only wraps addressed to the local user are ever in the cache, so the live-cascade case in practice means "another of my devices retracted a DM" — rare, not a hot path.