| … | … | @@ -503,11 +503,26 @@ export class ReleaseManager { |
| 503 | 503 | // _index.json says what exists, the overlay's FileIndex says what can be |
| 504 | 504 | // served, and only registerFile writes the second one. |
| 505 | 505 | const known = new Set() |
| 506 | | const seeded = await this._overlay.seedFilePathsFromIndex((overlayPath) => { |
| 506 | let unhashed = 0 |
| 507 | const seeded = await this._overlay.seedFilePathsFromIndex((overlayPath, entry) => { |
| 507 | 508 | if (!overlayPath || !overlayPath.startsWith(RELEASE_OVERLAY_PREFIX)) return null |
| 508 | | known.add(overlayPath) |
| 509 | // ONLY an entry that actually carries a contentHash counts as known. |
| 510 | // |
| 511 | // The overlay persists `contentHash: null` for a file indexed while still |
| 512 | // hashing, and such an entry seeds the PATH map but is unservable BY OID. |
| 513 | // Adding it to `known` on path alone made the repair below skip it, so the |
| 514 | // repair written for exactly this failure could not see it -- and since the |
| 515 | // hash never changes, nothing ever retried. That is the "no holder for a |
| 516 | // file that is on disk with a matching hash" bug: 2026-08-08 (136 MiB APK), |
| 517 | // 2026-08-25 (471 MB zip), 2026-08-29 (168 MB APK). |
| 518 | if (entry && entry.contentHash) known.add(overlayPath) |
| 519 | else if (entry) unhashed++ |
| 509 | 520 | return join(this._releaseDir, overlayPath.slice(RELEASE_OVERLAY_PREFIX.length)) |
| 510 | 521 | }) |
| 522 | if (unhashed) { |
| 523 | console.log('[releases] overlay index holds ' + unhashed + |
| 524 | ' entry(s) with no contentHash - unservable by oid; repairing from the release index') |
| 525 | } |
| 511 | 526 | await this._repairOverlayRegistrations(known) |
| 512 | 527 | return seeded |
| 513 | 528 | } |