{"record":{"id":"71510add388b32c2","repo":"unslothai/unsloth","slug":"linked-folder-is-still-being-removed","errorCode":null,"errorMessage":"Linked folder is still being removed","messagePattern":"Linked folder is still being removed","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"warning","filePath":"studio/backend/core/rag/folder_sync.py","lineNumber":256,"sourceCode":"    folder_id = str(uuid.uuid4())\n    now = _now()\n    with _scope_lock(scope):\n        conn = rag_db.get_connection()\n        try:\n            conn.execute(\"BEGIN IMMEDIATE\")\n            if conn.execute(\n                \"SELECT 1 FROM linked_folder_retired_scopes WHERE scope=?\", (scope,)\n            ).fetchone():\n                raise ValueError(\"The linked-folder scope no longer exists\")\n            normalized_key = _path_key(normalized)\n            existing = conn.execute(\n                \"SELECT * FROM linked_folders WHERE scope=?\", (scope,)\n            ).fetchall()\n            for row in existing:\n                existing_key = _path_key(row[\"path\"])\n                if existing_key == normalized_key or _same_file(row[\"path\"], normalized):\n                    if row[\"status\"] == \"retired\" or row[\"delete_remove_index\"] is not None:\n                        raise ValueError(\"Linked folder is still being removed\")\n                    conn.rollback()\n                    return _reauthorize_folder(row[\"id\"], normalized, expected_identity)\n                if _paths_overlap(existing_key, normalized_key):\n                    raise ValueError(\"Linked folders in the same scope cannot overlap\")\n            try:\n                current_identity = _root_identity(normalized)\n            except RuntimeError as exc:\n                raise ValueError(str(exc)) from exc\n            if current_identity != (root_device, root_inode) or (\n                expected_identity is not None and current_identity != expected_identity\n            ):\n                raise ValueError(\"Linked folder changed after it was selected\")\n            conn.execute(\n                \"INSERT INTO linked_folders(id, scope_type, scope_id, scope, path, name, \"\n                \"root_device, root_inode, auto_sync, status, created_at, updated_at) \"\n                \"VALUES(?,?,?,?,?,?,?,?,?,?,?,?)\",\n                (\n                    folder_id,","sourceCodeStart":238,"sourceCodeEnd":274,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/core/rag/folder_sync.py#L238-L274","documentation":"Inside create_folder's duplicate-detection loop: when the new path matches an existing linked_folders row (same path key or os.path.samefile) but that row has status='retired' or a pending delete_remove_index, the folder is mid-teardown and cannot be re-authorized, so creation aborts instead of resurrecting a half-deleted row.","triggerScenarios":"Re-linking the same directory while a previous unlink/delete job (which may still be removing indexed chunks) is in flight; retrying a create immediately after a remove_folder call; crash-recovery reruns where the retire job hasn't reached terminal state yet.","commonSituations":"User removes a folder and instantly re-adds it; background delete worker is slow because the RAG index is large; the delete job is queued behind another sync for the same folder.","solutions":["Wait for the removal job to reach terminal state (status in {'completed','failed'}) and the row to disappear, then retry create_folder.","Poll the folder status/jobs API for the same path before retrying instead of retrying immediately.","If a row is stuck retired with no running job (e.g. after a crash), run the reconciliation/cleanup path so the retire job completes."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"# No public helper exists; approximate by checking the row state directly.\nimport sqlite3\nfrom storage import rag_db\n\ndef folder_ready_for_relink(path_key: str, scope: str) -> bool:\n    with closing(rag_db.get_connection()) as conn:\n        rows = conn.execute(\"SELECT status, delete_remove_index FROM linked_folders WHERE scope=?\", (scope,)).fetchall()\n    return not any(r[\"status\"] == \"retired\" or r[\"delete_remove_index\"] is not None for r in rows)","typeGuard":null,"tryCatchPattern":"try:\n    create_folder(...)\nexcept ValueError as e:\n    if \"still being removed\" in str(e):\n        schedule_retry_after_removal_completes(path)  # backoff until terminal\n    else:\n        raise","preventionTips":["Wait for remove_folder's job to reach completed/failed before offering re-add in the UI.","Never fire add and remove for the same path concurrently.","Monitor retire-job progress and only then re-enable the link button."],"tags":["rag","folder-sync","lifecycle","race-condition","async-cleanup"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}