Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion REQUIREMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> **Note:** This document is automatically generated and verified against the live test suite by `scripts/generate_requirements.py` and `tests/backend/test_requirements_sync.py`.

**Test Verification Baseline:** **1115 Automated Tests** (757 Pytest Backend + 308 Vitest Frontend + 50 Playwright E2E).
**Test Verification Baseline:** **1120 Automated Tests** (762 Pytest Backend + 308 Vitest Frontend + 50 Playwright E2E).

---

Expand Down Expand Up @@ -1265,6 +1265,13 @@ over the PostgresConnectionAdapter using an initialized test engine._
- `test_storage_validation_and_errors`
- `test_ingestion_catalog_endpoint`

#### `tests/test_storage_vector_failures.py` (5 tests)
- `test_upsert_false_returns_500_rolls_back_disk_and_allows_retry`
- `test_delete_false_is_not_reported_as_success`
- `test_store_initialization_exception_rolls_back_upload`
- `test_failed_multichunk_upsert_removes_partial_vectors`
- `test_empty_replacement_succeeds_without_leaving_old_vectors`

#### `tests/test_summarizer.py` (12 tests)
- `test_db` - _Sets up an isolated SQLite database for summarizer testing._
- `test_generate_file_summary_success`
Expand Down
60 changes: 37 additions & 23 deletions app/services/local_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,28 +135,48 @@ def index_file(
category_override=cat
)

# Upsert vector points
if points:
try:
store = vs_service.get_vector_store()
store.delete_by_path(abs_path)
if hasattr(store, "upsert_documents"):
store.upsert_documents(points)
elif hasattr(store, "upsert_points"):
store.upsert_points(points)
except Exception as e:
logger.error(f"Failed to upsert vector points for {abs_path}: {e}")
try:
store = vs_service.get_vector_store()
# Deletion/upsert may partially succeed. Invalidate the old index first
# so a failed replacement remains retryable even after disk rollback.
with db_service.get_db_connection() as conn:
for table, col in (
("indexed_files", "filepath"), ("file_summaries", "filepath"),
("ast_symbols", "filepath"), ("pdf_extracted_cache", "filepath"),
("ast_relationships", "source_filepath"), ("api_routes", "filepath"),
("api_client_calls", "filepath"),
):
conn.execute(f"DELETE FROM {table} WHERE {col} = ?", (abs_path,))
conn.commit()
if store.delete_by_path(abs_path) is False:
raise RuntimeError("Vector store failed to delete existing file vectors")
if points:
try:
if hasattr(store, "upsert_documents"):
result = store.upsert_documents(points)
elif hasattr(store, "upsert_points"):
result = store.upsert_points(points)
else:
raise RuntimeError("Vector store does not support upserts")
if result is False:
raise RuntimeError("Vector store failed to upsert file vectors")
except Exception:
# A batch failure can leave some new vectors behind.
try:
if store.delete_by_path(abs_path) is False:
logger.error(f"Failed to clean up incomplete vectors for {abs_path}")
except Exception:
logger.exception(f"Failed to clean up incomplete vectors for {abs_path}")
raise
except Exception as e:
logger.error(f"Failed to index vector points for {abs_path}: {e}")
trigger_list_changed_notification()
raise RuntimeError(f"Failed to index vector points for {abs_path}") from e

# Update relational DB
mtime = os.path.getmtime(abs_path)
text_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()
with db_service.get_db_connection() as conn:
# Delete old relational records
for table in ("indexed_files", "file_summaries", "ast_symbols", "pdf_extracted_cache"):
try:
conn.execute(f"DELETE FROM {table} WHERE filepath = ?", (abs_path,))
except Exception:
pass
if doc_type == "pdf" and pdf_res is not None:
try:
pages_json = json.dumps([asdict(p) for p in pdf_res.pages])
Expand All @@ -178,12 +198,6 @@ def index_file(
)
except Exception as e:
logger.warning(f"Failed to cache PDF extraction for '{abs_path}': {e}")
for table, col in (("ast_relationships", "source_filepath"), ("api_routes", "filepath"), ("api_client_calls", "filepath")):
try:
conn.execute(f"DELETE FROM {table} WHERE {col} = ?", (abs_path,))
except Exception:
pass

# Insert indexed_file
conn.execute(
"INSERT INTO indexed_files (filepath, repo, doc_type, language, mtime, hash) VALUES (?, ?, ?, ?, ?, ?)",
Expand Down
16 changes: 16 additions & 0 deletions docs/guide/user-guide/local-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ When uploading PDF files, ContextCortex provides an interactive inspection modal

---

## Upload and Replacement Failures

Vector store initialization, deletion, or upsert failures return an error rather
than reporting the file as indexed. A failed new upload is removed from disk;
a failed replacement restores the previous file contents.

Once vector replacement starts, the previous index metadata is invalidated.
If indexing fails, the restored file is **not considered indexed**: retry the
upload or reindex the restored file after resolving the backend failure.
Incomplete vectors are cleaned up where possible; if the backend is unavailable,
the next successful retry deletes any remaining vectors before indexing.
Replacing a file with empty content succeeds with zero chunks and removes its
previous vectors.

---

## Large File Auto-Summarization

For large source code or documentation files exceeding the configured threshold (default: 500 KB):
Expand Down
Loading
Loading