@@ -1054,6 +1054,58 @@ export async function copyForkResourceContent(params: {
10541054 } )
10551055 }
10561056 }
1057+ /**
1058+ * Find the placeholders a worker from before this exclusion (a rolling deploy) planned for
1059+ * connector-managed documents, drop their persisted identities, and return the child ids to
1060+ * report as failed documents - the page query no longer returns their sources, so nothing
1061+ * would ever fill them, leaving archived empty rows that a mapping and a remapped
1062+ * `document-selector` still resolve to.
1063+ *
1064+ * Keyed on the SOURCE being connector-managed, which is deterministic: such a document can
1065+ * never become copyable, so this cannot race a concurrent attempt sitting between
1066+ * {@link ensureKbDocumentPlaceholder} and {@link finalizeKbDocument} (a "planned but unfilled"
1067+ * sweep would).
1068+ *
1069+ * Best-effort, like the count above: this probe runs on EVERY copied KB that has referenced
1070+ * documents, while the state it repairs exists only inside a rollout window. Letting a
1071+ * transient failure reach the KB's catch would delete an otherwise-complete copy and clear
1072+ * every reference to it - far worse, and far more likely, than the dangling placeholder it
1073+ * guards against. A failure is logged loudly and leaves that pre-existing state in place.
1074+ */
1075+ const reconcileStalePlannedDocuments = async ( kb : ForkContentKbEntry ) : Promise < string [ ] > => {
1076+ const plannedSourceIds = Object . keys ( kb . documentIdMap )
1077+ if ( plannedSourceIds . length === 0 ) return [ ]
1078+ try {
1079+ const stalePlanned = await db
1080+ . select ( { id : document . id } )
1081+ . from ( document )
1082+ . where ( and ( inArray ( document . id , plannedSourceIds ) , isNotNull ( document . connectorId ) ) )
1083+ const staleChildIds : string [ ] = [ ]
1084+ for ( const { id } of stalePlanned ) {
1085+ const childDocumentId = kb . documentIdMap [ id ]
1086+ if ( ! childDocumentId ) continue
1087+ // Left in `documentIdMap` deliberately: if the KB itself later fails, its failure lists
1088+ // the same child id again, and the cleanup keys failed ids by kind in a Set.
1089+ await dropCopiedDocumentMapping ( childDocumentId )
1090+ staleChildIds . push ( childDocumentId )
1091+ logger . warn (
1092+ `[${ requestId } ] Dropping a fork placeholder planned for a connector-managed document` ,
1093+ { sourceDocumentId : id , childDocumentId, childKnowledgeBaseId : kb . childId }
1094+ )
1095+ }
1096+ return staleChildIds
1097+ } catch ( error ) {
1098+ logger . error (
1099+ `[${ requestId } ] Failed to reconcile fork placeholders planned for connector-managed documents` ,
1100+ {
1101+ sourceKnowledgeBaseId : kb . sourceId ,
1102+ childKnowledgeBaseId : kb . childId ,
1103+ error : getErrorMessage ( error ) ,
1104+ }
1105+ )
1106+ return [ ]
1107+ }
1108+ }
10571109 /**
10581110 * Report the connector-managed documents a copied KB leaves behind, since a fully
10591111 * connector-synced base lands in the child with no documents at all. Strictly observability,
@@ -1196,34 +1248,9 @@ export async function copyForkResourceContent(params: {
11961248 for ( const kb of contentPlan . knowledgeBases ) {
11971249 try {
11981250 await logSkippedConnectorDocuments ( kb )
1199- // A worker from before this exclusion (a rolling deploy) could have planned a placeholder
1200- // for a connector-managed document. The page query below no longer returns its source, so
1201- // nothing would ever fill it - leaving an archived empty row that a persisted mapping and
1202- // a remapped `document-selector` still resolve to. Report those child ids as failed
1203- // documents instead, so the shared cleanup clears the references and drops the rows.
1204- //
1205- // Keyed on the SOURCE being connector-managed, which is deterministic: such a document can
1206- // never become copyable, so this can never race a concurrent attempt mid-fill (unlike a
1207- // "source is gone" check, which could).
1208- const plannedSourceIds = Object . keys ( kb . documentIdMap )
1209- if ( plannedSourceIds . length > 0 ) {
1210- const stalePlanned = await db
1211- . select ( { id : document . id } )
1212- . from ( document )
1213- . where ( and ( inArray ( document . id , plannedSourceIds ) , isNotNull ( document . connectorId ) ) )
1214- for ( const { id } of stalePlanned ) {
1215- const childDocumentId = kb . documentIdMap [ id ]
1216- if ( ! childDocumentId ) continue
1217- // Left in `documentIdMap` deliberately: if the KB itself later fails, its failure
1218- // lists the same child id again, and the cleanup keys failed ids by kind in a Set.
1219- await dropCopiedDocumentMapping ( childDocumentId )
1220- failedResources += 1
1221- failures . push ( { kind : 'knowledge-document' , childId : childDocumentId } )
1222- logger . warn (
1223- `[${ requestId } ] Dropping a fork placeholder planned for a connector-managed document` ,
1224- { sourceDocumentId : id , childDocumentId, childKnowledgeBaseId : kb . childId }
1225- )
1226- }
1251+ for ( const childDocumentId of await reconcileStalePlannedDocuments ( kb ) ) {
1252+ failedResources += 1
1253+ failures . push ( { kind : 'knowledge-document' , childId : childDocumentId } )
12271254 }
12281255 let afterDocId : string | null = null
12291256 for ( ; ; ) {
0 commit comments