@@ -63,6 +63,7 @@ import {
6363import {
6464 assertSyncLeaseHeldInTx ,
6565 buildSyncLockAcquisition ,
66+ buildSyncUnscheduledUpdate ,
6667 createContentSyncLease ,
6768 holdsSyncLockToken ,
6869 LOCKABLE_CONNECTOR_STATUSES ,
@@ -352,6 +353,22 @@ export function isContentPassIncomplete(
352353 )
353354}
354355
356+ /** Live documents the connector owns: what the connector list shows as its document count. */
357+ async function countLiveConnectorDocuments ( connectorId : string ) : Promise < number > {
358+ const [ row ] = await db
359+ . select ( { count : sql < number > `count(*)::int` } )
360+ . from ( document )
361+ . where (
362+ and (
363+ eq ( document . connectorId , connectorId ) ,
364+ eq ( document . userExcluded , false ) ,
365+ isNull ( document . archivedAt ) ,
366+ isNull ( document . deletedAt )
367+ )
368+ )
369+ return row ?. count ?? 0
370+ }
371+
355372/**
356373 * Atomically publishes the completed log and connector terminal state.
357374 *
@@ -394,9 +411,11 @@ export async function completeSuccessfulSync(
394411 const completionNotice =
395412 [ directoryNotice , listingNotice , contentNotice ] . filter ( Boolean ) . join ( '\n' ) || null
396413 /**
397- * A display snapshot, taken before the completion transaction so a slow count neither holds
398- * the connector lock nor turns a sync whose documents already landed into a failure. When it
399- * cannot be read the previous count stands.
414+ * Read before the completion transaction so a slow count neither holds the connector lock
415+ * nor turns a sync whose documents already landed into a failure. It is also the previous
416+ * owned count the next pass's mass-deletion guard starts from, so a tally of this run's
417+ * adds and deletes would drift; when it cannot be read the previous count stands, which the
418+ * guard already treats as a floor.
400419 */
401420 const actualDocCount = await countLiveConnectorDocuments ( connectorId ) . catch ( ( error : unknown ) => {
402421 logger . warn ( 'Could not count connector documents; keeping the previous count' , {
@@ -569,14 +588,7 @@ async function releaseSyncLockOnDeletedConnector(
569588) : Promise < void > {
570589 await db
571590 . update ( knowledgeConnector )
572- . set ( {
573- status : 'error' ,
574- nextSyncAt : null ,
575- lastSyncError : 'Connector deleted during sync' ,
576- syncLockToken : null ,
577- syncLockLeaseAt : null ,
578- updatedAt : new Date ( ) ,
579- } )
591+ . set ( buildSyncUnscheduledUpdate ( new Date ( ) , 'Connector deleted during sync' ) )
580592 . where ( holdsSyncLockToken ( connectorId , syncLogId ) )
581593}
582594
@@ -684,13 +696,8 @@ export function buildSyncCapacityUpdate(
684696 errorMessage : string
685697) {
686698 return {
687- status : 'error' as const ,
688- lastSyncError : errorMessage ,
689- nextSyncAt : null ,
699+ ...buildSyncUnscheduledUpdate ( now , errorMessage ) ,
690700 consecutiveFailures : previousFailures ?? 0 ,
691- syncLockToken : null ,
692- syncLockLeaseAt : null ,
693- updatedAt : now ,
694701 }
695702}
696703
@@ -703,21 +710,6 @@ export function buildSyncCapacityUpdate(
703710 * `consecutiveFailures` still resets: a held pass is a healthy sync that declined
704711 * to delete, not a failure, and marking it broken would stop it syncing at all.
705712 */
706- /** Live documents the connector owns: what the connector list shows as its document count. */
707- async function countLiveConnectorDocuments ( connectorId : string ) : Promise < number > {
708- const [ row ] = await db
709- . select ( { count : sql < number > `count(*)::int` } )
710- . from ( document )
711- . where (
712- and (
713- eq ( document . connectorId , connectorId ) ,
714- eq ( document . userExcluded , false ) ,
715- isNull ( document . archivedAt ) ,
716- isNull ( document . deletedAt )
717- )
718- )
719- return row ?. count ?? 0
720- }
721713
722714export function buildSyncSuccessUpdate (
723715 now : Date ,
@@ -850,21 +842,13 @@ export async function executeSync(
850842 /**
851843 * A connector with no token source cannot succeed, and each attempt would only walk the
852844 * failure ladder and, at its end, disable a connector that merely needs reconnecting. Left
853- * unscheduled with the reconnect error instead; the same terminal transition as a deleted
854- * knowledge base, so a live run's own terminal write cannot revive the schedule.
845+ * unscheduled with the reconnect error instead, the same transition as a deleted knowledge base.
855846 */
856847 if ( ! connectorHasAuthSource ( connectorConfig . auth , connectorBeforeLock ) ) {
857848 logger . warn ( 'Skipping sync: connector has no credential to authenticate with' , { connectorId } )
858849 await db
859850 . update ( knowledgeConnector )
860- . set ( {
861- status : 'error' ,
862- nextSyncAt : null ,
863- lastSyncError : CREDENTIAL_REMOVED_SYNC_ERROR ,
864- syncLockToken : null ,
865- syncLockLeaseAt : null ,
866- updatedAt : new Date ( ) ,
867- } )
851+ . set ( buildSyncUnscheduledUpdate ( new Date ( ) , CREDENTIAL_REMOVED_SYNC_ERROR ) )
868852 . where ( eq ( knowledgeConnector . id , connectorId ) )
869853 return { ...result , skipReason : 'credential_missing' }
870854 }
@@ -890,25 +874,7 @@ export async function executeSync(
890874 )
891875 await db
892876 . update ( knowledgeConnector )
893- . set ( {
894- status : 'error' ,
895- nextSyncAt : null ,
896- lastSyncError : 'Knowledge base deleted' ,
897- /**
898- * Clears the lock alongside the status.
899- *
900- * This write runs BEFORE the lock is taken, but it is unconditional on
901- * status, so it can land on a row a previous run left `syncing` — a run
902- * that may still be alive. Flipping status without releasing the token
903- * left a row that was neither locked nor reclaimable: the reaper only
904- * looks at `syncing` rows, and the old run's terminal write could still
905- * match its own token and resurrect a state for a knowledge base that no
906- * longer exists. Releasing both makes the transition terminal.
907- */
908- syncLockToken : null ,
909- syncLockLeaseAt : null ,
910- updatedAt : new Date ( ) ,
911- } )
877+ . set ( buildSyncUnscheduledUpdate ( new Date ( ) , 'Knowledge base deleted' ) )
912878 . where ( eq ( knowledgeConnector . id , connectorId ) )
913879 return { ...result , skipReason : 'knowledge_base_deleted' }
914880 }
0 commit comments