@@ -477,11 +477,25 @@ export async function ingestPackageProfile(
477477 updated_at = excluded.updated_at` ,
478478 )
479479 . bind ( job . did , slug , verified . cid , nowIso ) ;
480+ const retainReleaseHistory = db
481+ . prepare (
482+ `INSERT INTO package_release_history
483+ (did, package, release_history_complete, first_observed_at, first_observed_source)
484+ VALUES (?, ?, ?, ?, ?)
485+ ON CONFLICT(did, package) DO NOTHING` ,
486+ )
487+ . bind (
488+ job . did ,
489+ slug ,
490+ job . source === "jetstream" && job . operation === "create" ? 1 : 0 ,
491+ nowIso ,
492+ job . source ?? "unknown" ,
493+ ) ;
480494
481495 // The revision must exist before the current pointer moves. D1 batches are
482496 // transactional, so a failure leaves both the old pointer and old mutable
483497 // compatibility row intact.
484- await db . batch ( [ retainRevision , updateCurrentPackage , moveCurrentPointer ] ) ;
498+ await db . batch ( [ retainRevision , updateCurrentPackage , retainReleaseHistory , moveCurrentPointer ] ) ;
485499}
486500
487501export async function ingestPackageRelease (
@@ -618,10 +632,21 @@ export async function ingestPackageRelease(
618632 // roll back together and the message retries to a clean state. Without
619633 // the batch, an insert-success / refresh-failure could leave
620634 // `packages.latest_version` permanently stale.
621- const batchResults = await db . batch ( [
622- insertStmt ,
623- refreshPackageLatestStmt ( db , job . did , record . package ) ,
624- ] ) ;
635+ const batchStatements = [ insertStmt , refreshPackageLatestStmt ( db , job . did , record . package ) ] ;
636+ if ( job . source !== "jetstream" ) {
637+ // A release first encountered outside the cursor-backed stream proves
638+ // that the aggregator cannot claim continuous history for this package.
639+ batchStatements . push (
640+ db
641+ . prepare (
642+ `UPDATE package_release_history
643+ SET release_history_complete = 0
644+ WHERE did = ? AND package = ?` ,
645+ )
646+ . bind ( job . did , record . package ) ,
647+ ) ;
648+ }
649+ const batchResults = await db . batch ( batchStatements ) ;
625650 const insertResult = batchResults [ 0 ] ;
626651 if ( ! insertResult ) {
627652 // Defensive: D1.batch() guarantees one result per statement; if it
@@ -1053,14 +1078,29 @@ async function writeDeadLetter(
10531078 // envelope of operation+cid so the row is still inspectable.
10541079 const payload = JSON . stringify ( job . jetstreamRecord ?? { operation : job . operation , cid : job . cid } ) ;
10551080 const payloadBytes = new TextEncoder ( ) . encode ( payload ) ;
1056- await db
1081+ const retainDeadLetter = db
10571082 . prepare (
10581083 `INSERT INTO dead_letters
10591084 (did, collection, rkey, reason, detail, payload, received_at)
10601085 VALUES (?, ?, ?, ?, ?, ?, ?)` ,
10611086 )
1062- . bind ( job . did , job . collection , job . rkey , reason , detail , payloadBytes , now . toISOString ( ) )
1063- . run ( ) ;
1087+ . bind ( job . did , job . collection , job . rkey , reason , detail , payloadBytes , now . toISOString ( ) ) ;
1088+ const releaseIdentity =
1089+ job . collection === NSID . packageRelease ? parseReleaseRkey ( job . rkey ) : null ;
1090+ if ( ! releaseIdentity ) {
1091+ await retainDeadLetter . run ( ) ;
1092+ return ;
1093+ }
1094+ const markHistoryIncomplete = db
1095+ . prepare (
1096+ `INSERT INTO package_release_history
1097+ (did, package, release_history_complete, first_observed_at, first_observed_source)
1098+ VALUES (?, ?, 0, ?, ?)
1099+ ON CONFLICT(did, package) DO UPDATE SET
1100+ release_history_complete = 0` ,
1101+ )
1102+ . bind ( job . did , releaseIdentity . pkg , now . toISOString ( ) , job . source ?? "unknown" ) ;
1103+ await db . batch ( [ retainDeadLetter , markHistoryIncomplete ] ) ;
10641104}
10651105
10661106// ─── Production wiring ─────────────────────────────────────────────────────
0 commit comments