Summary
Follow-up to #934. The R2 Class A spike is resolved (#935 scoped the fan-out to the target's prefixes, and Class A operations have been flat since 2026-09-02). Phases 1 and 2 of the attachment-index plan are live: github_attachments is dual-written from every write path (#938, #945) and the shadow compare runs behind the attachment-index-shadow flag (#940, report script in #948).
The comment sync still lists R2, one or two ListObjects per sync. This issue tracks finishing the plan so it reads only D1. Low priority: cost is no longer the driver, this is for zero-R2 syncs and for retiring the shadow flag rather than leaving it running with no owner.
Remaining work
Before starting phase 3
Run node scripts/attachment-index-shadow-report.mjs --hours 72 (runbook in docs/ops.md) and confirm matches across more than one write path. As of 2026-09-06 the shadow is 100% match with no missing or extra rows, but every attachment-bearing sync came from a single private repo, so plain-prefix (public repo) targets, private-prefix rotation, and pre-#938 targets are still unverified. Workers Logs only reach back about three days, so windows longer than 72h return less, not more.
Full design, DDL, and write-path inventory are in the scoping comment on #934.
Summary
Follow-up to #934. The R2 Class A spike is resolved (#935 scoped the fan-out to the target's prefixes, and Class A operations have been flat since 2026-09-02). Phases 1 and 2 of the attachment-index plan are live:
github_attachmentsis dual-written from every write path (#938, #945) and the shadow compare runs behind theattachment-index-shadowflag (#940, report script in #948).The comment sync still lists R2, one or two
ListObjectsper sync. This issue tracks finishing the plan so it reads only D1. Low priority: cost is no longer the driver, this is for zero-R2 syncs and for retiring the shadow flag rather than leaving it running with no owner.Remaining work
gatherAttachments(apps/api/src/github-comment.ts) renders fromlistAttachmentsForTarget(apps/api/src/github-attachment-index.ts) when the flag is on, with list-on-miss fallback. Lane-aware URLs fromlane_idviaobjectPublicUrls; poster lane from the row instead ofcreateLaneResolver. Tests asserting zerolistObjectscalls under the flag.file_metadatagh.refrows (source='backfill'), then an offline R2 walk (apps/api/scripts/backfill-attachment-index.mjs, modelled onbackfill-gh-metadata.mjs) for anything the metadata pass missed.reconcileAttachmentIndexscoped to one target: diff R2 against the index, repair, emit a drift metric. Hook into the explicit resync path and the post-rotation resync.listActivePrefixIdsfor rotation and the CLI's local-ghfallback.GET /:workspace/github/attachmentsendpoint served from the index so the CLI's local-ghfallback stops fanning out client-side.Before starting phase 3
Run
node scripts/attachment-index-shadow-report.mjs --hours 72(runbook indocs/ops.md) and confirm matches across more than one write path. As of 2026-09-06 the shadow is 100% match with no missing or extra rows, but every attachment-bearing sync came from a single private repo, so plain-prefix (public repo) targets, private-prefix rotation, and pre-#938 targets are still unverified. Workers Logs only reach back about three days, so windows longer than 72h return less, not more.Full design, DDL, and write-path inventory are in the scoping comment on #934.