Summary
On current main, an accepted write_note mutation can trigger relation resolution before its Markdown file has been materialized. The MCP call correctly returns 202/Created, but the debounced background relation-resolution pass may reindex an affected source entity while that entity is still in the valid accepted-but-file-pending state.
The reindex path supplies no preloaded content, falls back to reading Markdown from disk, and logs FileNotFoundError. Materialization and the watcher later recover, so the write eventually becomes searchable, but the expected pending window is currently treated as a background task failure.
Observed on main commit 60408ad7 while rerunning the release write-load harness from #1021 against published v0.22.1.
Execution path
knowledge_router.create_entity() persists the accepted NoteContent mutation.
LocalNoteContentMaterializationProvider.materialize_write_change() schedules Markdown materialization off the acceptance path and returns immediately.
_schedule_post_write_followups() schedules debounced relation resolution for the project.
RepositoryRelationResolutionRuntime.resolve_relations() resolves inbound references and reindexes affected source entities through entity_indexer.index_entities(...).
SearchService.index_entities() calls index_entity_data(entity) without content.
index_entity_markdown() sees content is None and calls file_service.read_entity_content(entity).
- If materialization is still pending, the Markdown path does not exist and the background task raises
FileNotFoundError.
Representative stack:
LocalRelationResolutionScheduler._resolve_after_debounce
-> resolve_project_relations
-> RepositoryRelationResolutionRuntime.resolve_relations
-> SearchService.index_entities
-> SearchService.index_entity_data(entity, content=None)
-> SearchService.index_entity_markdown
-> FileService.read_entity_content
-> Path.read_text
-> FileNotFoundError
Relevant code:
src/basic_memory/api/v2/routers/knowledge_router.py::_schedule_post_write_followups
src/basic_memory/index/note_content_materialization.py::LocalNoteContentMaterializationProvider.materialize_write_change
src/basic_memory/indexing/relation_resolution.py::RepositoryRelationResolutionRuntime.resolve_relations
src/basic_memory/services/search_service.py::SearchService.index_entities
src/basic_memory/services/search_service.py::SearchService.index_entity_markdown
Benchmark evidence
The release benchmark used three fresh-runtime repeats per backend, 100 notes per concurrency level, concurrency 1/8/32/64, and eight warmups.
All 36 valid measurement rows had zero caller-visible write errors and every measured level eventually became searchable. However, every candidate run logged transient background search-index FileNotFoundErrors:
- SQLite: 27, 25, and 26 failures across the three repeats
- Postgres: 15, 15, and 10 failures across the three repeats
- Published v0.22.1 SQLite: zero
This is therefore reproducible under bursty write load. It adds error noise and redundant recovery work, and it makes genuine missing-file defects harder to distinguish from a valid pending-materialization state.
Performance constraint
Do not move Markdown materialization back onto the request/acceptance path.
The asynchronous path is producing the intended caller-facing improvement:
- SQLite acceptance throughput improved by approximately 31% at concurrency 8
- approximately 41% at concurrency 32
- approximately 54% at concurrency 64
The complete 100-note batch reached Markdown materialization approximately 24–30% later. That is an acceptable tradeoff: fast durable acceptance is the desired boundary, and file/search projections may catch up asynchronously.
The defect is the follow-up job's treatment of the valid pending state, not deferred materialization itself.
Existing recovery path is the inverse case
edit_note._resolve_after_disk_recovery() and POST /knowledge/index-file recover a Markdown file that already exists on disk but has not been indexed yet. This race is the opposite state: accepted NoteContent and the entity exist, while the Markdown projection is still pending. /index-file correctly returns 404 when the physical file does not exist, so that recovery path does not apply here.
Suggested direction
Prefer one of these state-aware approaches:
- Reindex affected sources from their accepted
NoteContent.markdown_content while materialization is pending; or
- Explicitly defer/requeue the affected source reindex until materialization completes.
Avoid broadly swallowing FileNotFoundError, because a genuinely missing materialized file remains a correctness problem that should surface.
Cloud/local lifecycle parity should be preserved. Cloud already schedules accepted-note relation repair after materialization; local should not require the file before its corresponding materialization has completed.
Acceptance criteria
Related
Summary
On current
main, an acceptedwrite_notemutation can trigger relation resolution before its Markdown file has been materialized. The MCP call correctly returns202/Created, but the debounced background relation-resolution pass may reindex an affected source entity while that entity is still in the valid accepted-but-file-pending state.The reindex path supplies no preloaded content, falls back to reading Markdown from disk, and logs
FileNotFoundError. Materialization and the watcher later recover, so the write eventually becomes searchable, but the expected pending window is currently treated as a background task failure.Observed on
maincommit60408ad7while rerunning the release write-load harness from #1021 against published v0.22.1.Execution path
knowledge_router.create_entity()persists the acceptedNoteContentmutation.LocalNoteContentMaterializationProvider.materialize_write_change()schedules Markdown materialization off the acceptance path and returns immediately._schedule_post_write_followups()schedules debounced relation resolution for the project.RepositoryRelationResolutionRuntime.resolve_relations()resolves inbound references and reindexes affected source entities throughentity_indexer.index_entities(...).SearchService.index_entities()callsindex_entity_data(entity)without content.index_entity_markdown()seescontent is Noneand callsfile_service.read_entity_content(entity).FileNotFoundError.Representative stack:
Relevant code:
src/basic_memory/api/v2/routers/knowledge_router.py::_schedule_post_write_followupssrc/basic_memory/index/note_content_materialization.py::LocalNoteContentMaterializationProvider.materialize_write_changesrc/basic_memory/indexing/relation_resolution.py::RepositoryRelationResolutionRuntime.resolve_relationssrc/basic_memory/services/search_service.py::SearchService.index_entitiessrc/basic_memory/services/search_service.py::SearchService.index_entity_markdownBenchmark evidence
The release benchmark used three fresh-runtime repeats per backend, 100 notes per concurrency level, concurrency 1/8/32/64, and eight warmups.
All 36 valid measurement rows had zero caller-visible write errors and every measured level eventually became searchable. However, every candidate run logged transient background search-index
FileNotFoundErrors:This is therefore reproducible under bursty write load. It adds error noise and redundant recovery work, and it makes genuine missing-file defects harder to distinguish from a valid pending-materialization state.
Performance constraint
Do not move Markdown materialization back onto the request/acceptance path.
The asynchronous path is producing the intended caller-facing improvement:
The complete 100-note batch reached Markdown materialization approximately 24–30% later. That is an acceptable tradeoff: fast durable acceptance is the desired boundary, and file/search projections may catch up asynchronously.
The defect is the follow-up job's treatment of the valid pending state, not deferred materialization itself.
Existing recovery path is the inverse case
edit_note._resolve_after_disk_recovery()andPOST /knowledge/index-filerecover a Markdown file that already exists on disk but has not been indexed yet. This race is the opposite state: acceptedNoteContentand the entity exist, while the Markdown projection is still pending./index-filecorrectly returns 404 when the physical file does not exist, so that recovery path does not apply here.Suggested direction
Prefer one of these state-aware approaches:
NoteContent.markdown_contentwhile materialization is pending; orAvoid broadly swallowing
FileNotFoundError, because a genuinely missing materialized file remains a correctness problem that should surface.Cloud/local lifecycle parity should be preserved. Cloud already schedules accepted-note relation repair after materialization; local should not require the file before its corresponding materialization has completed.
Acceptance criteria
write_notecontinues to acknowledge accepted mutations without awaiting Markdown materialization.FileNotFoundErrorduring relation resolution.Related
write_notecould back-resolve inbound forward references. This issue tracks ordering that follow-up correctly around asynchronous materialization; it does not reopen the original missing-follow-up bug.