@@ -231,12 +231,27 @@ async def list_owned_or_visible(
231231 return list (result .all ())
232232
233233
234- async def backfill_all_users_from_disk (* , kb_root : Path | None = None ) -> int :
235- """Backfill missing KB rows for every existing user.
234+ async def backfill_all_users_from_disk (
235+ * ,
236+ kb_root : Path | None = None ,
237+ username : str | None = None ,
238+ dry_run : bool = False ,
239+ ) -> int :
240+ """Adopt KB directories that have no ``knowledge_base`` row.
241+
242+ Legacy-recovery only. The ``knowledge_base`` row is the sole authority for KB
243+ metadata and nothing in the steady-state code path reads the on-disk
244+ ``embedding_metadata.json`` sidecar any more, so this exists purely to adopt
245+ directories written by a Langflow version that predates that change. It is
246+ opt-in at startup (``LANGFLOW_KB_DISK_RECONCILE_ENABLED``) and available on
247+ demand as ``langflow reconcile-kb-from-disk``.
236248
237- Runs during application startup so list/detail endpoints can stay
238- read-only. Returns the total number of inserted rows across all
239- users and never raises for per-user failures.
249+ ``username`` narrows the scan to a single account — the usual shape of a
250+ real recovery ("this user's KBs vanished after the upgrade"). ``dry_run``
251+ reports what would be adopted without writing.
252+
253+ Returns the number of rows inserted (or that would be inserted, under
254+ ``dry_run``). Never raises for per-user failures.
240255 """
241256 from langflow .api .utils .kb_helpers import KBStorageHelper
242257 from langflow .services .database .models .user .model import User
@@ -246,18 +261,25 @@ async def backfill_all_users_from_disk(*, kb_root: Path | None = None) -> int:
246261 return 0
247262
248263 async with session_scope () as session :
249- users = list ((await session .exec (select (User ))).all ())
264+ stmt = select (User )
265+ if username :
266+ stmt = stmt .where (User .username == username )
267+ users = list ((await session .exec (stmt )).all ())
250268
251269 inserted = 0
252270 for user in users :
253271 kb_user_root = effective_root / user .username
254272 if not kb_user_root .exists ():
255273 continue
256274 try :
257- inserted += await backfill_from_disk (user_id = user .id , kb_user_root = kb_user_root )
275+ inserted += await backfill_from_disk (
276+ user_id = user .id ,
277+ kb_user_root = kb_user_root ,
278+ dry_run = dry_run ,
279+ )
258280 except Exception as exc : # noqa: BLE001
259281 await logger .awarning (
260- "knowledge-base startup reconciliation failed for user %s: %s" ,
282+ "knowledge-base disk reconciliation failed for user %s: %s" ,
261283 user .username ,
262284 exc ,
263285 )
@@ -440,9 +462,8 @@ async def read_metadata(
440462def record_to_metadata_dict (record : KnowledgeBaseRecord ) -> dict [str , Any ]:
441463 """Serialize a row into the legacy JSON-file shape.
442464
443- Matches the keys ``KBAnalysisHelper.get_metadata`` and the API
444- routes expect so a DB-first migration doesn't need a parallel
445- consumer refactor.
465+ Matches the key set the API routes and the frontend expect, so the
466+ row can back the response shape without a parallel consumer refactor.
446467 """
447468 status = record .status
448469 if status == KnowledgeBaseStatus .READY .value and record .chunks <= 0 :
@@ -497,13 +518,15 @@ async def backfill_from_disk(
497518 * ,
498519 user_id : UUID ,
499520 kb_user_root : Path ,
521+ dry_run : bool = False ,
500522) -> int :
501523 """Create missing ``knowledge_base`` rows for existing KB directories.
502524
503- Called on first boot after the Phase 1.5 migration lands so every
504- pre-existing KB gains a row. Also serves as an idempotent
505- fallback: if a user drops an exported KB directory on disk, this
506- upserts the corresponding row on next access.
525+ Legacy-recovery only — see :func:`backfill_all_users_from_disk`. This is the
526+ one remaining reader of the on-disk ``embedding_metadata.json`` sidecar, and
527+ it runs only when an operator asks for it.
528+
529+ ``dry_run`` counts what would be adopted without writing any rows.
507530
508531 Returns the number of rows inserted. Never raises — failures are
509532 logged and skipped so one malformed KB directory doesn't block the
@@ -538,9 +561,6 @@ async def backfill_from_disk(
538561 continue
539562
540563 try :
541- from langflow .api .utils .kb_helpers import KBAnalysisHelper
542-
543- metadata = KBAnalysisHelper .get_metadata (kb_dir , fast = False ) or metadata
544564 model_selection = _normalize_model_selection (metadata .get ("model_selection" ))
545565 record_id = _coerce_uuid (metadata .get ("id" )) or uuid4 ()
546566 # ``backend_type``/``backend_config`` are persisted by
@@ -569,23 +589,24 @@ async def backfill_from_disk(
569589 "provider" : provider_raw ,
570590 }
571591
572- await create_record (
573- user_id = user_id ,
574- name = name ,
575- model_selection = normalized_selection ,
576- chunk_size = int (metadata .get ("chunk_size" ) or 1000 ),
577- chunk_overlap = int (metadata .get ("chunk_overlap" ) or 200 ),
578- separator = metadata .get ("separator" ),
579- column_config = metadata .get ("column_config" ) or [],
580- backend_type = backend_type ,
581- backend_config = backend_config ,
582- chunks = _coerce_int (metadata .get ("chunks" ), default = 0 ),
583- words = _coerce_int (metadata .get ("words" ), default = 0 ),
584- characters = _coerce_int (metadata .get ("characters" ), default = 0 ),
585- size_bytes = _coerce_int (metadata .get ("size_bytes" , metadata .get ("size" )), default = 0 ),
586- source_types = _coerce_source_types (metadata .get ("source_types" )),
587- record_id = record_id ,
588- )
592+ if not dry_run :
593+ await create_record (
594+ user_id = user_id ,
595+ name = name ,
596+ model_selection = normalized_selection ,
597+ chunk_size = int (metadata .get ("chunk_size" ) or 1000 ),
598+ chunk_overlap = int (metadata .get ("chunk_overlap" ) or 200 ),
599+ separator = metadata .get ("separator" ),
600+ column_config = metadata .get ("column_config" ) or [],
601+ backend_type = backend_type ,
602+ backend_config = backend_config ,
603+ chunks = _coerce_int (metadata .get ("chunks" ), default = 0 ),
604+ words = _coerce_int (metadata .get ("words" ), default = 0 ),
605+ characters = _coerce_int (metadata .get ("characters" ), default = 0 ),
606+ size_bytes = _coerce_int (metadata .get ("size_bytes" , metadata .get ("size" )), default = 0 ),
607+ source_types = _coerce_source_types (metadata .get ("source_types" )),
608+ record_id = record_id ,
609+ )
589610 inserted += 1
590611 except Exception as exc : # noqa: BLE001
591612 await logger .aerror ("backfill: failed to upsert KB %s/%s: %s" , user_id , name , exc )
0 commit comments