From c035f50f690890f3201a7e1410c59494a90ab104 Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Sun, 9 Aug 2026 16:49:39 -0500 Subject: [PATCH] appview: delete tag records with the encoded rkey on manifest delete DeleteManifestHandler built the tag rkey as "repo:tag" while the write path uses RepositoryTagToRKey, which is "repo_tag" with "/" encoded as "~". For a nested repo like stream/cache the two never match, so the cascade leaves the tag record on the PDS while removing the local cache row, and the tag reappears on the next backfill. Depending on the variant it either no-ops (deleteRecord is idempotent) or fails outright on an rkey containing a slash. Every other io.atcr.tag call site already routes through the helper. This was the last hand-built one. Co-Authored-By: Claude Opus 5 (1M context) --- pkg/appview/handlers/images.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/appview/handlers/images.go b/pkg/appview/handlers/images.go index 7b6f1a5..9fb766f 100644 --- a/pkg/appview/handlers/images.go +++ b/pkg/appview/handlers/images.go @@ -178,8 +178,11 @@ func (h *DeleteManifestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request // Delete each tag from PDS and database for _, tag := range tags { - // Delete from PDS - tagRKey := fmt.Sprintf("%s:%s", repo, tag) + // Delete from PDS. Must go through RepositoryTagToRKey: the write + // path encodes repository slashes as "~", so hand-building the + // rkey here targets a record that doesn't exist (a silent no-op, + // since deleteRecord is idempotent) for any repo with a "/" in it. + tagRKey := atproto.RepositoryTagToRKey(repo, tag) if err := pdsClient.DeleteRecord(r.Context(), atproto.TagCollection, tagRKey); err != nil { // Check if OAuth error - if so, invalidate sessions and return 401 if handleOAuthError(r.Context(), h.Refresher, user.DID, err) {