Skip to content

[Fix] One reliable path for git-native document ingest & indexing - #1715

Merged
CREDO23 merged 13 commits into
MODSetter:devfrom
CREDO23:fix/kb-ingest-index-atomicity
Aug 25, 2026
Merged

[Fix] One reliable path for git-native document ingest & indexing#1715
CREDO23 merged 13 commits into
MODSetter:devfrom
CREDO23:fix/kb-ingest-index-atomicity

Conversation

@CREDO23

@CREDO23 CREDO23 commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Makes the git-native ingest→index path one reliable writer keyed on the durable documents.path column, closing the drift sources found in production.

Changes

  • Path ownership & resolution keyed on documents.path; stop stamping the legacy virtual_path marker; backfill the column (migration 189).
  • Live writers re-attach a recordless save/ingest to its existing git file instead of forking a (2).
  • Pin ingested rows to their path at commit time.
  • Isolate per-document indexing so one failure can't sink the batch.
  • Signup: born default workspace honors KNOWLEDGE_STORE_ENABLED.
  • Reconciler skips unrecorded pending/failed rows (no false drift).
  • Drift monitor emits OTLP traces: a sweep span plus a per-workspace knowledge_store.drift.check span carrying the missing/extra/mismatched magnitude, so an alert opens a trace instead of the worker logs.
  • Tests: force KNOWLEDGE_STORE_ROOT to a tmp dir (guarded); pin the upload suite to legacy indexing.

Test plan

  • Ingest→index verbs (upload/note/rename/move/edit/delete) git↔PG consistent
  • Connector re-sync (skip + delta + failed): deleted=0, parity clean
  • Podcast artifact create → indexed ready, parity clean
  • Drift span carries the per-workspace magnitude (integration)
  • Backend integration suite

High-level PR Summary

This PR establishes a single, reliable write path for git-native document ingestion and indexing by making documents.path the authoritative source of truth, eliminating git↔Postgres drift that previously occurred from multiple concurrent writers and transaction boundaries. Key changes include: pinning document paths at ingest time rather than waiting for convergence, re-attaching recordless saves to their existing git files instead of forking duplicates, isolating per-document indexing failures to prevent batch rollback, migrating from the legacy virtual_path marker to the durable path column (with backfill via migration 189), ensuring signup-created workspaces honor the KNOWLEDGE_STORE_ENABLED flag, and updating the drift reconciler to skip unrecorded pending/failed rows. The changes consolidate what were previously three independent durability domains (git content, Postgres at ingest, Postgres projection) into a single convergent model where git is the source of truth and Postgres is a self-healing projection.

⏱️ Estimated Review Time: 1-3 hours

💡 Review Order Suggestion
Order File Path
1 plans/git-native-kb/10-ingest-index-atomicity.md
2 surfsense_backend/alembic/versions/189_backfill_document_path.py
3 surfsense_backend/app/knowledge_store/paths/store_path.py
4 surfsense_backend/app/knowledge_store/paths/__init__.py
5 surfsense_backend/app/knowledge_store/paths/legacy.py
6 surfsense_backend/app/knowledge_store/paths/resolve.py
7 surfsense_backend/app/knowledge_store/service.py
8 surfsense_backend/app/knowledge_store/index/rows.py
9 surfsense_backend/app/knowledge_store/index/converge.py
10 surfsense_backend/app/knowledge_store/migrate.py
11 surfsense_backend/app/users.py
12 surfsense_backend/app/agents/chat/multi_agent_chat/main_agent/middleware/knowledge_tree/middleware.py
13 surfsense_backend/app/agents/chat/runtime/mention_resolver.py
14 surfsense_backend/app/agents/chat/runtime/references/documents/resolver.py
15 surfsense_backend/tests/integration/conftest.py
16 surfsense_backend/tests/integration/knowledge_store/conftest.py
17 surfsense_backend/tests/integration/artifacts/test_git_adoption.py
18 surfsense_backend/tests/integration/document_upload/conftest.py
19 surfsense_backend/tests/integration/knowledge_store/test_backfill_document_path.py
20 surfsense_backend/tests/integration/knowledge_store/index/test_converge.py
21 surfsense_backend/tests/integration/knowledge_store/index/test_drift_monitor.py
22 surfsense_backend/tests/integration/knowledge_store/index/test_project.py
23 surfsense_backend/tests/integration/knowledge_store/test_folders.py
24 surfsense_backend/tests/integration/knowledge_store/test_migrate_placement.py
25 surfsense_backend/tests/integration/knowledge_store/test_path_column.py
26 surfsense_backend/tests/integration/knowledge_store/test_service.py
27 surfsense_backend/tests/integration/test_workspace_born_git_native.py
28 surfsense_backend/tests/integration/test_store_isolation.py
29 surfsense_backend/tests/unit/knowledge_store/test_recorded_virtual_path.py
30 surfsense_backend/tests/unit/agents/new_chat/test_mention_resolver.py
31 surfsense_backend/tests/unit/agents/new_chat/test_path_resolver.py
32 surfsense_backend/tests/unit/middleware/test_knowledge_tree.py

Need help? Join our Discord

CREDO23 added 12 commits August 24, 2026 17:42
ingest_documents committed the git revision but left Document.path and
PATH_MARKER unset, deferring the pin to the async converge. For a
pending/failed doc converge never runs, so the row stayed unmarked and
the next connector re-sync re-authored a fresh path against the git
tree, forking the file into "name (2).md" and stranding the original as
an orphan. Mark each row the moment the batch lands, mirroring
save_document and move_documents.
on_after_register inlined the default-workspace INSERT without setting
knowledge_store_enabled, so signup workspaces defaulted to legacy even
when the global switch was on -- the source of the growing unflipped
backlog. Extract create_default_workspace(session, user), set the flag
from config as POST /workspaces does, and cover it with a real
INSERT round-trip test.
…f forking

When a row loses both its PATH_MARKER and path column -- the crash window
between the git commit and the mark, and the legacy unmarked-orphan rows
in production -- a re-ingest re-authored the canonical name against the
git tree and forked the file into "name (2).md", stranding the original.

_place_unmarked now asks virtual_path_to_doc whether git already holds
the canonical name for this same row (its _resolve_by_title matches an
unlocated row) and, if so, reuses that path to overwrite in place. A
fresh name is authored only when the canonical name is free or owned by
a different row, so intra-batch distinctness is preserved.
…n't sink the batch

index() rolls the shared session back on failure, which discarded every
upsert in the same converge run and could crash it with MissingGreenlet.
Run each document's index() in its own session; a failure rolls back only
that connection.
…urable column

Indexer ownership (load_owned/prune/delete_row), path->doc resolution
(virtual_path_to_doc), and path display (virtual_path_of) now read
Document.path instead of the document_metadata virtual_path marker. Writers
still dual-write both, and recorded_virtual_path stays marker-first, so a
legacy row whose column is still NULL keeps resolving until the backfill
populates it. Folds the two private recorded-path readers into one shared
helper.
…l_path marker

Ownership and resolution now key on documents.path, but rows written before the
column existed carry the path only on their metadata marker. Migration 189 copies
it into the column, guarded to fill only an empty column from a /documents marker
so an authored path is never overwritten. Integration test proves a legacy moved
note flips from unresolvable to column-resolvable across the real resolver.
Writers now record a document's location only on the durable documents.path
column: upsert, save, ingest, move, and the Phase-5 seeder no longer write the
virtual_path metadata marker. recorded_virtual_path reads the column first and
falls back to the marker only for rows written before migration 189 backfills
it, so a re-sync that drops the marker can no longer fork a document.

Tests assert on the path column instead of the marker, add coverage that a
reindex preserves a row's metadata (no marker is resurrected), and bind
converge's per-document session in the artifacts adoption test.
… suite

These tests exercise the upload pipeline (ETL, dedup, credits, limits, search)
on the Postgres-inline indexer. They inherited the process flag, so a dev .env
with KNOWLEDGE_STORE_ENABLED=TRUE born-flipped the test workspace and routed
uploads through the git store, whose async convergence no in-process broker
runs — hanging the suite locally while CI (flag off) stayed green. Pin the flag
off per test so the suite is hermetic; git-native indexing is covered by the
knowledge_store and artifacts suites.
…orking

The two live writers decided a document's path two different ways. A sync
ingest re-attached an unmarked row to the file git already holds for it —
the crash window between the commit and the path write-back — while an
editor save authored a fresh `name (2).md` and stranded the first. That is
a fork, and a steady drift source on flipped workspaces.

Move that decision into one place both writers call:
`_reattach_or_author_path` re-attaches by the row's identity when git holds
the canonical name, else authors. Recorded-path reuse stays in each caller
(the record-world check). The seeder, artifacts and `_relocation_of` are
left as-is: only an incremental writer of an existing row can reach the
recordless-but-file-still-in-git fork.

Proven by test_a_resave_of_an_unmarked_row_reattaches_instead_of_forking,
the editor twin of the existing ingest re-attach test.
migrate_workspace built its desired set from any body-bearing row without
looking at status, so a row with a body but no recorded git path was counted
missing whatever its state. A still-pending or failed row therefore alarmed the
hourly drift check every run and drew a reindex (git->Postgres) that cannot make
a file out of a Postgres row -- a perpetual no-op repair.

Skip an unrecorded row whose status is pending/processing/failed: the store was
never asked to hold it yet, and the live writer records it once it is ready. A
row that already records a path keeps its file whatever its status, so nothing
in git is dropped.
Integration tests wrote git repos to the real KNOWLEDGE_STORE_ROOT, a shared
on-disk path keyed by workspace id. Because the test schema is recreated per
session, test workspaces reuse low ids and overwrote dev workspace repos with
the same id, silently clobbering local data.

A session-autouse fixture redirects the root to a pytest tmp dir for the whole
integration session; a guard test asserts it took.
@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

@CREDO23 is attempting to deploy a commit to the Rohan Verma's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 863c7578-76fa-49ca-bc27-39c26bcfec51

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…space magnitude

The scheduled parity check emitted a status-only metric (ok/drift/error), which
alarms but cannot say how far git and Postgres diverged — that lived only in a
log line. Wrap the sweep in a parent span and each workspace check in a
`knowledge_store.drift.check` span carrying the missing/extra/mismatched path
counts, so a drift alert opens a trace that already quantifies the gap instead
of sending someone to grep the worker logs.
@CREDO23
CREDO23 merged commit aa7d351 into MODSetter:dev Aug 25, 2026
9 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant