Skip to content
Merged
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
19 changes: 8 additions & 11 deletions src/basic_memory/indexing/note_content_reconciler.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
NoteContentSource,
NoteContentWriteStatus,
ObservedNoteContent,
plan_note_content_reconciliation,
bootstrap_note_content_plan,
plan_existing_note_content_reconciliation,
)
from basic_memory.models import NoteContent
from basic_memory.repository import NoteContentRepository
Expand Down Expand Up @@ -350,16 +351,14 @@ async def reconcile(
return NoteContentReconciliationResult.stale()

if note_content is None:
plan = plan_note_content_reconciliation(None, observed)
if not isinstance(plan, NoteContentBootstrap):
raise RuntimeError("Missing note_content must bootstrap reconciliation")
bootstrap = bootstrap_note_content_plan(observed=observed)

try:
await self._note_content_repository.create(
session,
note_content_from_bootstrap(entity.id, plan),
note_content_from_bootstrap(entity.id, bootstrap),
)
return NoteContentReconciliationResult.current(plan.db_version)
return NoteContentReconciliationResult.current(bootstrap.db_version)
except IntegrityError:
# Concurrent repair/index workers can both observe a missing row before
# one wins the insert. Reload the winner and let normal reconciliation
Expand All @@ -386,12 +385,10 @@ async def reconcile(
# the row between our read and write is not silently reverted.
expected_db_version = int(note_content.db_version)
current_state = note_content_state_from_model(note_content)
plan = plan_note_content_reconciliation(
current_state,
observed,
plan = plan_existing_note_content_reconciliation(
current=current_state,
observed=observed,
)
if isinstance(plan, NoteContentBootstrap):
raise RuntimeError("Existing note_content cannot bootstrap reconciliation")
if isinstance(plan, NoteContentReconciliationDeferred):
logger.debug(
"Deferred note_content file promotion for entity {}: "
Expand Down
44 changes: 23 additions & 21 deletions src/basic_memory/indexing/note_content_reconciliation.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,8 @@ class NoteContentPromoted:
last_materialization_attempt_at: datetime | None


type NoteContentReconciliationPlan = (
NoteContentBootstrap
| NoteContentFileSynced
type ExistingNoteContentReconciliationPlan = (
NoteContentFileSynced
| NoteContentFileObserved
| NoteContentReconciliationDeferred
| NoteContentPromoted
Expand All @@ -212,10 +211,28 @@ def note_content_matches_accepted_version(
return current.db_version == accepted.db_version and current.db_checksum == accepted.db_checksum


def plan_note_content_reconciliation(
current: NoteContentState | None,
def bootstrap_note_content_plan(*, observed: ObservedNoteContent) -> NoteContentBootstrap:
"""Seed db_version=1/file_version=1 synced state for a note with no stored content row."""
return NoteContentBootstrap(
markdown_content=observed.markdown_content,
db_version=1,
db_checksum=observed.checksum,
file_version=1,
file_checksum=observed.checksum,
file_write_status="synced",
last_source=observed.source,
updated_at=observed.observed_at,
file_updated_at=observed.observed_at,
last_materialization_error=None,
last_materialization_attempt_at=None,
)


def plan_existing_note_content_reconciliation(
*,
current: NoteContentState,
observed: ObservedNoteContent,
) -> NoteContentReconciliationPlan:
) -> ExistingNoteContentReconciliationPlan:
"""Choose the note_content write needed to converge one observed file.

The same rule must apply everywhere:
Expand All @@ -224,21 +241,6 @@ def plan_note_content_reconciliation(
- an unrelated observation can advance DB only from fully synchronized state
- otherwise: defer until the accepted DB/file lineage is stable
"""
if current is None:
return NoteContentBootstrap(
markdown_content=observed.markdown_content,
db_version=1,
db_checksum=observed.checksum,
file_version=1,
file_checksum=observed.checksum,
file_write_status="synced",
last_source=observed.source,
updated_at=observed.observed_at,
file_updated_at=observed.observed_at,
last_materialization_error=None,
last_materialization_attempt_at=None,
)

if observed.checksum == current.db_checksum:
return NoteContentFileSynced(
markdown_content=observed.markdown_content,
Expand Down
29 changes: 15 additions & 14 deletions tests/indexing/test_note_content_reconciliation.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
NoteContentReconciliationResult,
NoteContentState,
ObservedNoteContent,
bootstrap_note_content_plan,
plan_existing_note_content_reconciliation,
plan_note_content_materialization_publish,
plan_note_content_materialization_status,
plan_note_content_reconciliation,
)


Expand All @@ -45,7 +46,7 @@ def _observed(checksum: str = "observed-checksum") -> ObservedNoteContent:


def test_plan_bootstraps_missing_note_content() -> None:
plan = plan_note_content_reconciliation(None, _observed())
plan = bootstrap_note_content_plan(observed=_observed())

assert plan == NoteContentBootstrap(
markdown_content="# Observed\n",
Expand All @@ -63,14 +64,14 @@ def test_plan_bootstraps_missing_note_content() -> None:


def test_plan_marks_file_synced_when_observed_checksum_matches_db() -> None:
plan = plan_note_content_reconciliation(
NoteContentState(
plan = plan_existing_note_content_reconciliation(
current=NoteContentState(
db_version=7,
db_checksum="db-checksum",
file_version=6,
file_checksum="old-file-checksum",
),
_observed("db-checksum"),
observed=_observed("db-checksum"),
)

assert plan == NoteContentFileSynced(
Expand All @@ -85,14 +86,14 @@ def test_plan_marks_file_synced_when_observed_checksum_matches_db() -> None:


def test_plan_refreshes_file_observation_when_db_is_ahead() -> None:
plan = plan_note_content_reconciliation(
NoteContentState(
plan = plan_existing_note_content_reconciliation(
current=NoteContentState(
db_version=9,
db_checksum="new-db-checksum",
file_version=8,
file_checksum="old-file-checksum",
),
_observed("old-file-checksum"),
observed=_observed("old-file-checksum"),
)

assert plan == NoteContentFileObserved(
Expand All @@ -103,30 +104,30 @@ def test_plan_refreshes_file_observation_when_db_is_ahead() -> None:


def test_plan_defers_external_file_change_while_file_state_is_not_synced() -> None:
plan = plan_note_content_reconciliation(
NoteContentState(
plan = plan_existing_note_content_reconciliation(
current=NoteContentState(
db_version=3,
db_checksum="db-checksum",
file_version=5,
file_checksum="materialized-file-checksum",
file_write_status="pending",
),
_observed("external-change-checksum"),
observed=_observed("external-change-checksum"),
)

assert plan == NoteContentReconciliationDeferred()


def test_plan_promotes_external_file_change_from_synced_state() -> None:
plan = plan_note_content_reconciliation(
NoteContentState(
plan = plan_existing_note_content_reconciliation(
current=NoteContentState(
db_version=5,
db_checksum="db-checksum",
file_version=5,
file_checksum="db-checksum",
file_write_status="synced",
),
_observed("external-change-checksum"),
observed=_observed("external-change-checksum"),
)

assert plan == NoteContentPromoted(
Expand Down
Loading