basic-memory 0.22.1__py3-none-any.whl → 0.23.1.dev2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- basic_memory/__init__.py +2 -2
- basic_memory/alembic/env.py +35 -25
- basic_memory/alembic/versions/2d26b287813b_dedupe_observations_and_purge_stale_.py +69 -0
- basic_memory/alembic/versions/o8j9k0l1m2n3_add_note_file_vacate_table.py +52 -0
- basic_memory/alembic/versions/p9k0l1m2n3o4_add_vector_index_manifest_state.py +132 -0
- basic_memory/alembic/versions/q0l1m2n3o4p5_add_relation_search_refresh_table.py +57 -0
- basic_memory/alembic/versions/r1m2n3o4p5q6_add_relation_generation.py +95 -0
- basic_memory/api/app.py +15 -19
- basic_memory/api/container.py +59 -20
- basic_memory/api/template_loader.py +16 -4
- basic_memory/api/v2/routers/__init__.py +2 -0
- basic_memory/api/v2/routers/directory_router.py +146 -25
- basic_memory/api/v2/routers/importer_router.py +95 -9
- basic_memory/api/v2/routers/inspect_router.py +205 -0
- basic_memory/api/v2/routers/knowledge_router.py +601 -248
- basic_memory/api/v2/routers/memory_router.py +37 -12
- basic_memory/api/v2/routers/project_router.py +77 -55
- basic_memory/api/v2/routers/prompt_router.py +11 -4
- basic_memory/api/v2/routers/resource_router.py +134 -284
- basic_memory/api/v2/routers/schema_router.py +211 -52
- basic_memory/api/v2/routers/search_router.py +154 -71
- basic_memory/api/v2/utils.py +28 -5
- basic_memory/cli/analytics.py +2 -2
- basic_memory/cli/app.py +43 -1
- basic_memory/cli/auth.py +8 -8
- basic_memory/cli/auto_update.py +87 -13
- basic_memory/cli/commands/__init__.py +4 -0
- basic_memory/cli/commands/cloud/__init__.py +2 -0
- basic_memory/cli/commands/cloud/api_client.py +8 -5
- basic_memory/cli/commands/cloud/bisync_commands.py +72 -4
- basic_memory/cli/commands/cloud/cloud_utils.py +6 -6
- basic_memory/cli/commands/cloud/project_sync.py +329 -63
- basic_memory/cli/commands/cloud/rclone_commands.py +178 -58
- basic_memory/cli/commands/cloud/rclone_installer.py +1 -1
- basic_memory/cli/commands/cloud/shares.py +533 -0
- basic_memory/cli/commands/cloud/transfer.py +58 -0
- basic_memory/cli/commands/cloud/upload.py +20 -4
- basic_memory/cli/commands/cloud/upload_command.py +10 -10
- basic_memory/cli/commands/cloud/webdav.py +388 -0
- basic_memory/cli/commands/cloud/webdav_transfer.py +600 -0
- basic_memory/cli/commands/command_utils.py +26 -16
- basic_memory/cli/commands/config.py +251 -0
- basic_memory/cli/commands/db.py +120 -83
- basic_memory/cli/commands/doctor.py +82 -17
- basic_memory/cli/commands/format.py +2 -1
- basic_memory/cli/commands/hook.py +1508 -0
- basic_memory/cli/commands/inspect.py +670 -0
- basic_memory/cli/commands/mcp.py +2 -2
- basic_memory/cli/commands/orphans.py +2 -2
- basic_memory/cli/commands/project.py +46 -25
- basic_memory/cli/commands/schema.py +4 -4
- basic_memory/cli/commands/status.py +63 -161
- basic_memory/cli/commands/tool.py +631 -8
- basic_memory/cli/container.py +1 -1
- basic_memory/cli/main.py +2 -0
- basic_memory/config.py +97 -985
- basic_memory/config_logging.py +67 -0
- basic_memory/config_migrations.py +90 -0
- basic_memory/config_models.py +1060 -0
- basic_memory/db.py +139 -38
- basic_memory/deps/__init__.py +62 -162
- basic_memory/deps/config.py +18 -12
- basic_memory/deps/db.py +21 -4
- basic_memory/deps/importers.py +21 -148
- basic_memory/deps/projects.py +24 -161
- basic_memory/deps/read_cache.py +48 -0
- basic_memory/deps/repositories.py +8 -106
- basic_memory/deps/services.py +360 -348
- basic_memory/file_utils.py +24 -3
- basic_memory/hooks/__init__.py +16 -0
- basic_memory/hooks/_uuid7.py +43 -0
- basic_memory/hooks/adapters/__init__.py +33 -0
- basic_memory/hooks/adapters/base.py +36 -0
- basic_memory/hooks/adapters/claude.py +39 -0
- basic_memory/hooks/adapters/codex.py +38 -0
- basic_memory/hooks/archive.py +97 -0
- basic_memory/hooks/envelope.py +170 -0
- basic_memory/hooks/inbox.py +205 -0
- basic_memory/hooks/project_ref.py +16 -0
- basic_memory/ignore_utils.py +2 -2
- basic_memory/importers/__init__.py +12 -1
- basic_memory/importers/base.py +27 -2
- basic_memory/importers/chatgpt_importer.py +52 -9
- basic_memory/importers/claude_conversations_importer.py +3 -1
- basic_memory/importers/claude_projects_importer.py +3 -1
- basic_memory/importers/memory_json_importer.py +3 -1
- basic_memory/importers/project_zip_import.py +181 -0
- basic_memory/index/__init__.py +5 -0
- basic_memory/index/event_operations.py +134 -0
- basic_memory/index/filesystem.py +215 -0
- basic_memory/index/inline_operations.py +103 -0
- basic_memory/index/local_dependencies.py +772 -0
- basic_memory/index/local_moves.py +424 -0
- basic_memory/index/local_notes.py +246 -0
- basic_memory/index/local_project.py +844 -0
- basic_memory/index/local_runtime.py +442 -0
- basic_memory/index/local_schedulers.py +268 -0
- basic_memory/index/local_watch.py +320 -0
- basic_memory/index/note_content_materialization.py +744 -0
- basic_memory/index/project_indexing.py +48 -0
- basic_memory/index/repository_project_resolution.py +98 -0
- basic_memory/index/schedulers.py +19 -0
- basic_memory/index/storage_events.py +165 -0
- basic_memory/index/watch_coordinator.py +139 -0
- basic_memory/index/watch_service.py +350 -0
- basic_memory/indexing/__init__.py +4 -30
- basic_memory/indexing/accepted_note_enqueue_runner.py +210 -0
- basic_memory/indexing/accepted_note_mutation_runner.py +1128 -0
- basic_memory/indexing/accepted_note_search.py +134 -0
- basic_memory/indexing/accepted_note_write_runner.py +793 -0
- basic_memory/indexing/batch_indexer.py +384 -139
- basic_memory/indexing/change_detector.py +184 -0
- basic_memory/indexing/change_planning.py +176 -0
- basic_memory/indexing/directory_delete_runner.py +647 -0
- basic_memory/indexing/embedding_index_planning.py +460 -0
- basic_memory/indexing/external_file_delete_runner.py +228 -0
- basic_memory/indexing/file_batch_runner.py +220 -0
- basic_memory/indexing/file_index_checking.py +463 -0
- basic_memory/indexing/file_index_planning.py +200 -0
- basic_memory/indexing/file_indexer.py +274 -0
- basic_memory/indexing/forward_reference_resolution.py +339 -0
- basic_memory/indexing/index_batch_runtime.py +188 -0
- basic_memory/indexing/index_file_runner.py +304 -0
- basic_memory/indexing/index_file_runtime.py +142 -0
- basic_memory/indexing/input_file_adaptation.py +56 -0
- basic_memory/indexing/job_payloads.py +491 -0
- basic_memory/indexing/link_resolution.py +112 -0
- basic_memory/indexing/models.py +795 -2
- basic_memory/indexing/note_content_batch_reconciliation.py +306 -0
- basic_memory/indexing/note_content_read_repair_runner.py +275 -0
- basic_memory/indexing/note_content_reconciler.py +456 -0
- basic_memory/indexing/note_content_reconciliation.py +342 -0
- basic_memory/indexing/note_file_delete_runner.py +124 -0
- basic_memory/indexing/note_materialization_runner.py +862 -0
- basic_memory/indexing/orphan_cleanup.py +118 -0
- basic_memory/indexing/progress.py +316 -0
- basic_memory/indexing/project_delete_acceptance.py +60 -0
- basic_memory/indexing/project_delete_runner.py +312 -0
- basic_memory/indexing/project_index_coordinator.py +369 -0
- basic_memory/indexing/project_index_maintenance.py +1384 -0
- basic_memory/indexing/project_index_progress.py +465 -0
- basic_memory/indexing/project_index_runtime.py +211 -0
- basic_memory/indexing/project_index_workflow.py +41 -0
- basic_memory/indexing/project_index_workflow_models.py +262 -0
- basic_memory/indexing/project_index_workflow_stale.py +105 -0
- basic_memory/indexing/project_index_workflow_updates.py +279 -0
- basic_memory/indexing/relation_persistence.py +191 -0
- basic_memory/indexing/relation_resolution.py +542 -0
- basic_memory/man/bm.1 +2 -2
- basic_memory/markdown/entity_parser.py +47 -5
- basic_memory/markdown/plugins.py +88 -26
- basic_memory/markdown/schemas.py +3 -1
- basic_memory/mcp/async_client.py +0 -22
- basic_memory/mcp/client_info.py +112 -0
- basic_memory/mcp/clients/__init__.py +2 -0
- basic_memory/mcp/clients/directory.py +21 -5
- basic_memory/mcp/clients/inspect.py +66 -0
- basic_memory/mcp/clients/knowledge.py +45 -24
- basic_memory/mcp/clients/memory.py +12 -8
- basic_memory/mcp/clients/project.py +10 -10
- basic_memory/mcp/clients/search.py +13 -4
- basic_memory/mcp/container.py +25 -22
- basic_memory/mcp/note_reads.py +106 -0
- basic_memory/mcp/project_context.py +92 -642
- basic_memory/mcp/project_context_identifiers.py +213 -0
- basic_memory/mcp/prompts/__init__.py +2 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +8 -55
- basic_memory/mcp/prompts/continue_conversation.py +2 -2
- basic_memory/mcp/prompts/getting_started.py +252 -0
- basic_memory/mcp/prompts/search.py +2 -2
- basic_memory/mcp/prompts/utils.py +3 -3
- basic_memory/mcp/resources/__init__.py +2 -24
- basic_memory/mcp/resources/ai_assistant_guide.md +28 -312
- basic_memory/mcp/resources/project_info.py +33 -41
- basic_memory/mcp/server.py +166 -79
- basic_memory/mcp/tools/__init__.py +2 -6
- basic_memory/mcp/tools/basic_memory_diagnostics.py +88 -0
- basic_memory/mcp/tools/build_context.py +46 -10
- basic_memory/mcp/tools/chatgpt_tools.py +74 -8
- basic_memory/mcp/tools/delete_note.py +23 -6
- basic_memory/mcp/tools/edit_note.py +186 -42
- basic_memory/mcp/tools/list_directory.py +105 -11
- basic_memory/mcp/tools/move_note.py +134 -100
- basic_memory/mcp/tools/project_management.py +204 -43
- basic_memory/mcp/tools/read_content.py +13 -5
- basic_memory/mcp/tools/read_note.py +91 -77
- basic_memory/mcp/tools/recent_activity.py +76 -15
- basic_memory/mcp/tools/schema.py +80 -13
- basic_memory/mcp/tools/search.py +100 -12
- basic_memory/mcp/tools/ui_sdk.py +12 -2
- basic_memory/mcp/tools/utils.py +146 -2
- basic_memory/mcp/tools/view_note.py +6 -1
- basic_memory/mcp/tools/workspaces.py +8 -3
- basic_memory/mcp/tools/write_note.py +10 -4
- basic_memory/mcp/workspace_project_index.py +358 -0
- basic_memory/migration_loop.py +34 -0
- basic_memory/models/__init__.py +10 -1
- basic_memory/models/knowledge.py +78 -5
- basic_memory/models/project.py +2 -1
- basic_memory/models/relation_search_refresh.py +45 -0
- basic_memory/models/search.py +8 -1
- basic_memory/{schema → picoschema}/__init__.py +5 -5
- basic_memory/{schema → picoschema}/diff.py +2 -2
- basic_memory/{schema → picoschema}/inference.py +4 -3
- basic_memory/{schema → picoschema}/parser.py +29 -5
- basic_memory/{schema → picoschema}/resolver.py +14 -5
- basic_memory/{schema → picoschema}/validator.py +25 -24
- basic_memory/read_cache/__init__.py +38 -0
- basic_memory/read_cache/contract.py +118 -0
- basic_memory/read_cache/invalidation.py +94 -0
- basic_memory/read_cache/keys.py +90 -0
- basic_memory/read_cache/lifecycle.py +46 -0
- basic_memory/read_cache/policy.py +6 -0
- basic_memory/read_cache/read_through.py +188 -0
- basic_memory/read_cache/redis.py +225 -0
- basic_memory/redaction.py +132 -0
- basic_memory/repository/__init__.py +11 -3
- basic_memory/repository/accepted_note_repositories.py +47 -0
- basic_memory/repository/accepted_note_search_repository.py +171 -0
- basic_memory/repository/accepted_note_search_row.py +22 -0
- basic_memory/repository/accepted_note_vector_cleanup.py +183 -0
- basic_memory/repository/embedding_provider.py +22 -3
- basic_memory/repository/embedding_provider_factory.py +117 -13
- basic_memory/repository/entity_repository.py +286 -145
- basic_memory/repository/fastembed_provider.py +3 -4
- basic_memory/repository/fastembed_rerank_provider.py +161 -0
- basic_memory/repository/litellm_provider.py +33 -12
- basic_memory/repository/litellm_rerank_provider.py +121 -0
- basic_memory/repository/milvus_config.py +81 -0
- basic_memory/repository/milvus_index.py +236 -0
- basic_memory/repository/milvus_repository.py +422 -0
- basic_memory/repository/note_content_repository.py +258 -73
- basic_memory/repository/note_file_vacate_repository.py +260 -0
- basic_memory/repository/observation_repository.py +87 -20
- basic_memory/repository/openai_provider.py +1 -2
- basic_memory/repository/pgvector_index.py +378 -0
- basic_memory/repository/postgres_search_repository.py +311 -242
- basic_memory/repository/prefixing_provider.py +97 -0
- basic_memory/repository/project_info_repository.py +3 -3
- basic_memory/repository/project_repository.py +112 -107
- basic_memory/repository/relation_repository.py +972 -78
- basic_memory/repository/repository.py +216 -220
- basic_memory/repository/rerank_provider.py +86 -0
- basic_memory/repository/rerank_provider_factory.py +113 -0
- basic_memory/repository/search_index_row.py +42 -2
- basic_memory/repository/search_query.py +302 -0
- basic_memory/repository/search_repository.py +107 -13
- basic_memory/repository/search_repository_base.py +1768 -1339
- basic_memory/repository/search_trace.py +630 -0
- basic_memory/repository/semantic_chunking.py +261 -0
- basic_memory/repository/semantic_errors.py +17 -0
- basic_memory/repository/semantic_vector_index.py +128 -0
- basic_memory/repository/semantic_vector_index_factory.py +132 -0
- basic_memory/repository/semantic_vector_sync.py +1441 -0
- basic_memory/repository/sqlite_search_repository.py +307 -199
- basic_memory/repository/sqlite_vec_index.py +375 -0
- basic_memory/runtime/__init__.py +5 -0
- basic_memory/runtime/accepted_note_changes.py +329 -0
- basic_memory/runtime/cleanup.py +400 -0
- basic_memory/runtime/job_payloads.py +161 -0
- basic_memory/runtime/jobs.py +308 -0
- basic_memory/{runtime.py → runtime/mode.py} +6 -6
- basic_memory/runtime/note_content.py +156 -0
- basic_memory/runtime/note_content_deletes.py +270 -0
- basic_memory/runtime/note_content_reads.py +142 -0
- basic_memory/runtime/note_content_responses.py +320 -0
- basic_memory/runtime/note_file_guards.py +101 -0
- basic_memory/runtime/note_materialization.py +135 -0
- basic_memory/runtime/note_materialization_planning.py +204 -0
- basic_memory/runtime/note_move.py +36 -0
- basic_memory/runtime/note_object_metadata.py +267 -0
- basic_memory/runtime/observed_index_files.py +42 -0
- basic_memory/runtime/process.py +43 -0
- basic_memory/runtime/projects.py +80 -0
- basic_memory/runtime/sample_notes.py +52 -0
- basic_memory/runtime/storage.py +373 -0
- basic_memory/runtime/storage_events.py +116 -0
- basic_memory/runtime/storage_project_resolution.py +140 -0
- basic_memory/runtime/vector_sync.py +36 -0
- basic_memory/runtime/workflows.py +289 -0
- basic_memory/schemas/__init__.py +9 -6
- basic_memory/schemas/base.py +9 -8
- basic_memory/schemas/cloud.py +5 -3
- basic_memory/schemas/directory.py +21 -1
- basic_memory/schemas/inspect.py +883 -0
- basic_memory/schemas/memory.py +6 -0
- basic_memory/schemas/project_index.py +61 -0
- basic_memory/schemas/project_info.py +13 -2
- basic_memory/schemas/request.py +9 -1
- basic_memory/schemas/schema.py +18 -2
- basic_memory/schemas/search.py +24 -4
- basic_memory/schemas/v2/__init__.py +11 -9
- basic_memory/schemas/v2/entity.py +62 -5
- basic_memory/schemas/v2/project_index.py +20 -0
- basic_memory/services/bulk_link_resolver.py +346 -0
- basic_memory/services/composition.py +192 -0
- basic_memory/services/context_service.py +21 -6
- basic_memory/services/directory_deletes.py +134 -0
- basic_memory/services/directory_service.py +121 -27
- basic_memory/services/entity_service.py +539 -1014
- basic_memory/services/exceptions.py +22 -0
- basic_memory/services/file_service.py +45 -28
- basic_memory/services/initialization.py +207 -55
- basic_memory/services/link_resolver.py +269 -108
- basic_memory/services/note_content_reads.py +222 -0
- basic_memory/services/note_content_writes.py +523 -0
- basic_memory/services/note_preparation.py +1032 -0
- basic_memory/services/project_deletes.py +167 -0
- basic_memory/services/project_service.py +766 -599
- basic_memory/services/retrieval_inspect.py +607 -0
- basic_memory/services/search_service.py +268 -276
- basic_memory/utils.py +11 -2
- {basic_memory-0.22.1.dist-info → basic_memory-0.23.1.dev2.dist-info}/METADATA +90 -20
- basic_memory-0.23.1.dev2.dist-info/RECORD +397 -0
- {basic_memory-0.22.1.dist-info → basic_memory-0.23.1.dev2.dist-info}/WHEEL +1 -1
- basic_memory/deps.py +0 -16
- basic_memory/mcp/formatting.py +0 -162
- basic_memory/mcp/resources/cloud_info.md +0 -16
- basic_memory/mcp/resources/discovery.py +0 -17
- basic_memory/mcp/resources/release_notes.md +0 -16
- basic_memory/mcp/resources/ui.py +0 -89
- basic_memory/mcp/tools/canvas.py +0 -164
- basic_memory/mcp/tools/cloud_info.py +0 -15
- basic_memory/mcp/tools/release_notes.py +0 -15
- basic_memory/schemas/sync_report.py +0 -72
- basic_memory/schemas/v2/resource.py +0 -47
- basic_memory/sync/__init__.py +0 -7
- basic_memory/sync/background_sync.py +0 -26
- basic_memory/sync/coordinator.py +0 -161
- basic_memory/sync/sync_service.py +0 -1778
- basic_memory/sync/watch_service.py +0 -606
- basic_memory-0.22.1.dist-info/RECORD +0 -254
- {basic_memory-0.22.1.dist-info → basic_memory-0.23.1.dev2.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.22.1.dist-info → basic_memory-0.23.1.dev2.dist-info}/licenses/LICENSE +0 -0
basic_memory/__init__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
|
|
2
2
|
|
|
3
3
|
# Package version - updated by release automation
|
|
4
|
-
__version__ = "0.
|
|
4
|
+
__version__ = "0.23.0"
|
|
5
5
|
|
|
6
6
|
# API version for FastAPI - independent of package version
|
|
7
|
-
__api_version__ = "
|
|
7
|
+
__api_version__ = "v2"
|
basic_memory/alembic/env.py
CHANGED
|
@@ -2,37 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import os
|
|
5
|
+
import sys
|
|
5
6
|
from contextlib import suppress
|
|
6
7
|
from logging.config import fileConfig
|
|
7
8
|
|
|
8
9
|
from loguru import logger
|
|
10
|
+
from sqlalchemy import engine_from_config, pool
|
|
11
|
+
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import
|
|
13
|
+
from alembic import context
|
|
14
|
+
|
|
15
|
+
from basic_memory.config import ConfigManager
|
|
16
|
+
from basic_memory.migration_loop import running_on_uvloop
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
# Allow nested event loops (needed for pytest-asyncio and other async contexts).
|
|
19
|
+
# nest_asyncio cannot patch a uvloop loop or Python 3.14+; in those cases we skip
|
|
20
|
+
# it and rely on the thread-based fallback in run_migrations_online() instead
|
|
21
|
+
# (see basic_memory.migration_loop for why uvloop must be detected up front).
|
|
22
|
+
if sys.version_info < (3, 14) and not running_on_uvloop():
|
|
15
23
|
try:
|
|
16
24
|
import nest_asyncio
|
|
17
25
|
|
|
18
26
|
nest_asyncio.apply()
|
|
19
27
|
except (ImportError, ValueError) as exc:
|
|
20
28
|
# Trigger: nest_asyncio is absent (ImportError) or refuses to patch the
|
|
21
|
-
# running loop (ValueError
|
|
22
|
-
# Postgres backend - #831/#877).
|
|
23
|
-
# Why: the uvloop ValueError is now an *expected* path on every Postgres
|
|
24
|
-
# startup, so swallowing it silently hides a routine branch.
|
|
29
|
+
# running loop (ValueError).
|
|
25
30
|
# Outcome: log at DEBUG (observable, not noisy) and fall through to the
|
|
26
31
|
# thread-based migration fallback.
|
|
27
32
|
logger.debug(f"nest_asyncio not applied ({exc!r}); using thread-based migration fallback")
|
|
28
|
-
# For Python 3.14+, we rely on the thread-based fallback in run_migrations_online()
|
|
29
|
-
|
|
30
|
-
from sqlalchemy import engine_from_config, pool
|
|
31
|
-
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
|
32
|
-
|
|
33
|
-
from alembic import context
|
|
34
|
-
|
|
35
|
-
from basic_memory.config import ConfigManager
|
|
36
33
|
|
|
37
34
|
# Trigger: only set test env when actually running under pytest
|
|
38
35
|
# Why: alembic/env.py is imported during normal operations (MCP server startup, migrations)
|
|
@@ -170,17 +167,30 @@ def _run_async_migrations_in_thread(connectable) -> None:
|
|
|
170
167
|
future.result() # Wait for completion and re-raise any exceptions
|
|
171
168
|
|
|
172
169
|
|
|
173
|
-
def
|
|
174
|
-
"""
|
|
170
|
+
def _loop_is_running() -> bool:
|
|
171
|
+
"""Check if an event loop is currently running."""
|
|
175
172
|
try:
|
|
173
|
+
asyncio.get_running_loop()
|
|
174
|
+
except RuntimeError:
|
|
175
|
+
# Trigger: no running loop (get_running_loop raises RuntimeError)
|
|
176
|
+
# Outcome: return False to indicate no loop is running
|
|
177
|
+
return False
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _run_async_engine_migrations(connectable) -> None:
|
|
182
|
+
"""Run async migrations, adapting to whether an event loop is already running."""
|
|
183
|
+
|
|
184
|
+
# Trigger: caller may be inside a running event loop (e.g. pytest-asyncio, uvicorn).
|
|
185
|
+
# Why: asyncio.run() raises RuntimeError when nested inside a running loop, so we
|
|
186
|
+
# detect the condition up front via _loop_is_running() instead of catching the error.
|
|
187
|
+
# Outcome: running-loop context -> thread fallback; no loop -> asyncio.run() directly.
|
|
188
|
+
if _loop_is_running():
|
|
189
|
+
# Can't nest asyncio.run() inside a running loop -> offload to a thread.
|
|
190
|
+
_run_async_migrations_in_thread(connectable)
|
|
191
|
+
else:
|
|
192
|
+
# No running loop: asyncio.run() is safe; let any unexpected errors propagate.
|
|
176
193
|
_run_async_migrations_with_asyncio_run(connectable)
|
|
177
|
-
except RuntimeError as e:
|
|
178
|
-
if "cannot be called from a running event loop" in str(e):
|
|
179
|
-
# We're in a running event loop (likely uvloop or Python 3.14+ tests).
|
|
180
|
-
# Switch to a dedicated thread so Alembic can finish without nesting loops.
|
|
181
|
-
_run_async_migrations_in_thread(connectable)
|
|
182
|
-
else:
|
|
183
|
-
raise
|
|
184
194
|
|
|
185
195
|
|
|
186
196
|
def run_migrations_online() -> None:
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Dedupe observations and purge stale observation search rows.
|
|
2
|
+
|
|
3
|
+
Revision ID: 2d26b287813b
|
|
4
|
+
Revises: r1m2n3o4p5q6
|
|
5
|
+
Create Date: 2026-08-10 19:22:17.344875
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Sequence, Union
|
|
10
|
+
|
|
11
|
+
from alembic import op
|
|
12
|
+
from sqlalchemy import inspect
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# revision identifiers, used by Alembic.
|
|
16
|
+
revision: str = "2d26b287813b"
|
|
17
|
+
down_revision: Union[str, None] = "r1m2n3o4p5q6"
|
|
18
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
19
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _table_exists(connection, table_name: str) -> bool:
|
|
23
|
+
"""Return whether a runtime-managed table is present for this database."""
|
|
24
|
+
return table_name in inspect(connection).get_table_names()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def upgrade() -> None:
|
|
28
|
+
"""Repair duplicate observation projections and their orphaned search rows."""
|
|
29
|
+
connection = op.get_bind()
|
|
30
|
+
if connection.dialect.name == "postgresql":
|
|
31
|
+
# PostgreSQL JSON has no equality operator, so its textual representation is the
|
|
32
|
+
# grouping key. GROUP BY treats NULL context and tags values consistently.
|
|
33
|
+
op.execute(
|
|
34
|
+
"""
|
|
35
|
+
DELETE FROM observation
|
|
36
|
+
WHERE id NOT IN (
|
|
37
|
+
SELECT MIN(id)
|
|
38
|
+
FROM observation
|
|
39
|
+
GROUP BY entity_id, category, content, context, tags::text
|
|
40
|
+
)
|
|
41
|
+
"""
|
|
42
|
+
)
|
|
43
|
+
else:
|
|
44
|
+
op.execute(
|
|
45
|
+
"""
|
|
46
|
+
DELETE FROM observation
|
|
47
|
+
WHERE id NOT IN (
|
|
48
|
+
SELECT MIN(id)
|
|
49
|
+
FROM observation
|
|
50
|
+
GROUP BY entity_id, category, content, context, tags
|
|
51
|
+
)
|
|
52
|
+
"""
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# SQLite creates search_index at runtime rather than through Alembic. A fresh database
|
|
56
|
+
# therefore has no table or stale rows to purge during this migration.
|
|
57
|
+
if _table_exists(connection, "search_index"):
|
|
58
|
+
op.execute(
|
|
59
|
+
"""
|
|
60
|
+
DELETE FROM search_index
|
|
61
|
+
WHERE type = 'observation'
|
|
62
|
+
AND id NOT IN (SELECT id FROM observation)
|
|
63
|
+
"""
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def downgrade() -> None:
|
|
68
|
+
"""No-op: removed duplicate and orphaned projection rows cannot be reconstructed."""
|
|
69
|
+
pass
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Add note_file_vacate table
|
|
2
|
+
|
|
3
|
+
Revision ID: o8j9k0l1m2n3
|
|
4
|
+
Revises: n7i8j9k0l1m2
|
|
5
|
+
Create Date: 2026-07-26 12:00:00.000000
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Sequence, Union
|
|
10
|
+
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
from alembic import op
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# revision identifiers, used by Alembic.
|
|
16
|
+
revision: str = "o8j9k0l1m2n3"
|
|
17
|
+
down_revision: Union[str, None] = "n7i8j9k0l1m2"
|
|
18
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
19
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
"""Create note_file_vacate: durable proof a source path was vacated by a move.
|
|
24
|
+
|
|
25
|
+
The indexer keys on (project_id, file_path) to tell a move's lingering source object (skip
|
|
26
|
+
re-import) from a legitimate byte-identical copy (index as new) — see
|
|
27
|
+
basic-memory-cloud#1601.
|
|
28
|
+
"""
|
|
29
|
+
op.create_table(
|
|
30
|
+
"note_file_vacate",
|
|
31
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
|
32
|
+
sa.Column("project_id", sa.Integer(), nullable=False),
|
|
33
|
+
sa.Column("entity_id", sa.Integer(), nullable=True),
|
|
34
|
+
sa.Column("file_path", sa.String(), nullable=False),
|
|
35
|
+
sa.Column("file_checksum", sa.String(), nullable=True),
|
|
36
|
+
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
|
37
|
+
sa.ForeignKeyConstraint(["project_id"], ["project.id"], ondelete="CASCADE"),
|
|
38
|
+
sa.ForeignKeyConstraint(["entity_id"], ["entity.id"], ondelete="SET NULL"),
|
|
39
|
+
sa.PrimaryKeyConstraint("id"),
|
|
40
|
+
sa.UniqueConstraint(
|
|
41
|
+
"project_id", "file_path", name="uix_note_file_vacate_project_file_path"
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
op.create_index(
|
|
45
|
+
"ix_note_file_vacate_project_id", "note_file_vacate", ["project_id"], unique=False
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def downgrade() -> None:
|
|
50
|
+
"""Drop note_file_vacate and its supporting index."""
|
|
51
|
+
op.drop_index("ix_note_file_vacate_project_id", table_name="note_file_vacate")
|
|
52
|
+
op.drop_table("note_file_vacate")
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Add vector index identity and readiness to the semantic manifest.
|
|
2
|
+
|
|
3
|
+
Revision ID: p9k0l1m2n3o4
|
|
4
|
+
Revises: o8j9k0l1m2n3
|
|
5
|
+
Create Date: 2026-07-21 00:00:00.000000
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Sequence, Union
|
|
10
|
+
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
from alembic import op
|
|
13
|
+
from sqlalchemy import inspect
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
revision: str = "p9k0l1m2n3o4"
|
|
17
|
+
down_revision: Union[str, None] = "o8j9k0l1m2n3"
|
|
18
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
19
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _create_note_file_vacate_table() -> None:
|
|
23
|
+
"""Backfill the canonical o8 schema when the duplicate vector revision ran."""
|
|
24
|
+
op.create_table(
|
|
25
|
+
"note_file_vacate",
|
|
26
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
|
27
|
+
sa.Column("project_id", sa.Integer(), nullable=False),
|
|
28
|
+
sa.Column("entity_id", sa.Integer(), nullable=True),
|
|
29
|
+
sa.Column("file_path", sa.String(), nullable=False),
|
|
30
|
+
sa.Column("file_checksum", sa.String(), nullable=True),
|
|
31
|
+
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
|
32
|
+
sa.ForeignKeyConstraint(["project_id"], ["project.id"], ondelete="CASCADE"),
|
|
33
|
+
sa.ForeignKeyConstraint(["entity_id"], ["entity.id"], ondelete="SET NULL"),
|
|
34
|
+
sa.PrimaryKeyConstraint("id"),
|
|
35
|
+
sa.UniqueConstraint(
|
|
36
|
+
"project_id",
|
|
37
|
+
"file_path",
|
|
38
|
+
name="uix_note_file_vacate_project_file_path",
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
op.create_index(
|
|
42
|
+
"ix_note_file_vacate_project_id",
|
|
43
|
+
"note_file_vacate",
|
|
44
|
+
["project_id"],
|
|
45
|
+
unique=False,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def upgrade() -> None:
|
|
50
|
+
"""Reconcile both historical o8 schemas, then add vector manifest state.
|
|
51
|
+
|
|
52
|
+
For a short period, note-file vacates and vector manifest state shared the
|
|
53
|
+
o8 revision ID. A database can therefore arrive here with either schema
|
|
54
|
+
while Alembic reports the same predecessor. Restore the canonical o8
|
|
55
|
+
note-file table first, then idempotently apply the PostgreSQL vector state.
|
|
56
|
+
SQLite creates its derived vector table lazily at runtime, so it only needs
|
|
57
|
+
the missing canonical table repaired.
|
|
58
|
+
"""
|
|
59
|
+
connection = op.get_bind()
|
|
60
|
+
inspector = inspect(connection)
|
|
61
|
+
tables = set(inspector.get_table_names())
|
|
62
|
+
|
|
63
|
+
if "note_file_vacate" not in tables:
|
|
64
|
+
_create_note_file_vacate_table()
|
|
65
|
+
|
|
66
|
+
if connection.dialect.name != "postgresql":
|
|
67
|
+
return
|
|
68
|
+
if "search_vector_chunks" not in tables:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
op.execute("ALTER TABLE search_vector_chunks ADD COLUMN IF NOT EXISTS vector_index TEXT")
|
|
72
|
+
op.execute("ALTER TABLE search_vector_chunks ADD COLUMN IF NOT EXISTS embedding_status TEXT")
|
|
73
|
+
op.execute(
|
|
74
|
+
"""
|
|
75
|
+
UPDATE search_vector_chunks
|
|
76
|
+
SET vector_index = 'pgvector'
|
|
77
|
+
WHERE vector_index IS NULL
|
|
78
|
+
"""
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if "search_vector_embeddings" in tables:
|
|
82
|
+
op.execute(
|
|
83
|
+
"""
|
|
84
|
+
UPDATE search_vector_chunks AS chunks
|
|
85
|
+
SET embedding_status = CASE
|
|
86
|
+
WHEN EXISTS (
|
|
87
|
+
SELECT 1 FROM search_vector_embeddings AS embeddings
|
|
88
|
+
WHERE embeddings.chunk_id = chunks.id
|
|
89
|
+
) THEN 'ready'
|
|
90
|
+
ELSE 'pending'
|
|
91
|
+
END
|
|
92
|
+
WHERE embedding_status IS NULL
|
|
93
|
+
"""
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
op.execute(
|
|
97
|
+
"""
|
|
98
|
+
UPDATE search_vector_chunks
|
|
99
|
+
SET embedding_status = 'pending'
|
|
100
|
+
WHERE embedding_status IS NULL
|
|
101
|
+
"""
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
op.execute("ALTER TABLE search_vector_chunks ALTER COLUMN vector_index SET NOT NULL")
|
|
105
|
+
op.execute("ALTER TABLE search_vector_chunks ALTER COLUMN embedding_status SET NOT NULL")
|
|
106
|
+
constraint_names = {
|
|
107
|
+
constraint.get("name")
|
|
108
|
+
for constraint in inspector.get_check_constraints("search_vector_chunks")
|
|
109
|
+
}
|
|
110
|
+
if "ck_search_vector_chunks_embedding_status" not in constraint_names:
|
|
111
|
+
op.create_check_constraint(
|
|
112
|
+
"ck_search_vector_chunks_embedding_status",
|
|
113
|
+
"search_vector_chunks",
|
|
114
|
+
"embedding_status IN ('pending', 'ready')",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def downgrade() -> None:
|
|
119
|
+
"""Remove vector index manifest state from PostgreSQL."""
|
|
120
|
+
connection = op.get_bind()
|
|
121
|
+
if connection.dialect.name != "postgresql":
|
|
122
|
+
return
|
|
123
|
+
if "search_vector_chunks" not in inspect(connection).get_table_names():
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
op.drop_constraint(
|
|
127
|
+
"ck_search_vector_chunks_embedding_status",
|
|
128
|
+
"search_vector_chunks",
|
|
129
|
+
type_="check",
|
|
130
|
+
)
|
|
131
|
+
op.execute("ALTER TABLE search_vector_chunks DROP COLUMN IF EXISTS embedding_status")
|
|
132
|
+
op.execute("ALTER TABLE search_vector_chunks DROP COLUMN IF EXISTS vector_index")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Add durable relation search refresh work.
|
|
2
|
+
|
|
3
|
+
Revision ID: q0l1m2n3o4p5
|
|
4
|
+
Revises: p9k0l1m2n3o4
|
|
5
|
+
Create Date: 2026-07-27 18:00:00.000000
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Sequence, Union
|
|
10
|
+
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
from alembic import op
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
revision: str = "q0l1m2n3o4p5"
|
|
16
|
+
down_revision: Union[str, None] = "p9k0l1m2n3o4"
|
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
18
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
"""Create durable work items for relation-derived search refreshes."""
|
|
23
|
+
op.create_table(
|
|
24
|
+
"relation_search_refresh",
|
|
25
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
|
26
|
+
sa.Column("project_id", sa.Integer(), nullable=False),
|
|
27
|
+
sa.Column("entity_id", sa.Integer(), nullable=False),
|
|
28
|
+
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
|
29
|
+
sa.ForeignKeyConstraint(["project_id"], ["project.id"], ondelete="CASCADE"),
|
|
30
|
+
sa.ForeignKeyConstraint(["entity_id"], ["entity.id"], ondelete="CASCADE"),
|
|
31
|
+
sa.PrimaryKeyConstraint("id"),
|
|
32
|
+
)
|
|
33
|
+
op.create_index(
|
|
34
|
+
"ix_relation_search_refresh_project_id",
|
|
35
|
+
"relation_search_refresh",
|
|
36
|
+
["project_id"],
|
|
37
|
+
unique=False,
|
|
38
|
+
)
|
|
39
|
+
op.create_index(
|
|
40
|
+
"ix_relation_search_refresh_entity_id",
|
|
41
|
+
"relation_search_refresh",
|
|
42
|
+
["entity_id"],
|
|
43
|
+
unique=False,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def downgrade() -> None:
|
|
48
|
+
"""Drop durable relation-search refresh work."""
|
|
49
|
+
op.drop_index(
|
|
50
|
+
"ix_relation_search_refresh_entity_id",
|
|
51
|
+
table_name="relation_search_refresh",
|
|
52
|
+
)
|
|
53
|
+
op.drop_index(
|
|
54
|
+
"ix_relation_search_refresh_project_id",
|
|
55
|
+
table_name="relation_search_refresh",
|
|
56
|
+
)
|
|
57
|
+
op.drop_table("relation_search_refresh")
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Add generation ownership to relation projections.
|
|
2
|
+
|
|
3
|
+
Revision ID: r1m2n3o4p5q6
|
|
4
|
+
Revises: q0l1m2n3o4p5
|
|
5
|
+
Create Date: 2026-08-09 17:00:00.000000
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Sequence, Union
|
|
10
|
+
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
from alembic import op
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
revision: str = "r1m2n3o4p5q6"
|
|
16
|
+
down_revision: Union[str, None] = "q0l1m2n3o4p5"
|
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
18
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
"""Backfill relation rows with their source note's accepted generation."""
|
|
23
|
+
op.add_column(
|
|
24
|
+
"relation_search_refresh",
|
|
25
|
+
sa.Column("publication_generation", sa.BigInteger(), nullable=True),
|
|
26
|
+
)
|
|
27
|
+
op.create_index(
|
|
28
|
+
"ix_relation_search_refresh_project_publication_generation",
|
|
29
|
+
"relation_search_refresh",
|
|
30
|
+
["project_id", "publication_generation"],
|
|
31
|
+
unique=False,
|
|
32
|
+
)
|
|
33
|
+
op.add_column(
|
|
34
|
+
"relation",
|
|
35
|
+
sa.Column(
|
|
36
|
+
"generation",
|
|
37
|
+
sa.BigInteger(),
|
|
38
|
+
nullable=False,
|
|
39
|
+
server_default=sa.text("0"),
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
connection = op.get_bind()
|
|
44
|
+
if connection.dialect.name == "postgresql":
|
|
45
|
+
op.execute(
|
|
46
|
+
"""
|
|
47
|
+
UPDATE relation
|
|
48
|
+
SET generation = note_content.db_version
|
|
49
|
+
FROM note_content
|
|
50
|
+
WHERE relation.from_id = note_content.entity_id
|
|
51
|
+
AND relation.project_id = note_content.project_id
|
|
52
|
+
"""
|
|
53
|
+
)
|
|
54
|
+
else:
|
|
55
|
+
op.execute(
|
|
56
|
+
"""
|
|
57
|
+
UPDATE relation
|
|
58
|
+
SET generation = COALESCE(
|
|
59
|
+
(
|
|
60
|
+
SELECT note_content.db_version
|
|
61
|
+
FROM note_content
|
|
62
|
+
WHERE note_content.entity_id = relation.from_id
|
|
63
|
+
AND note_content.project_id = relation.project_id
|
|
64
|
+
),
|
|
65
|
+
0
|
|
66
|
+
)
|
|
67
|
+
"""
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
op.create_index(
|
|
71
|
+
"ix_relation_project_from_generation",
|
|
72
|
+
"relation",
|
|
73
|
+
["project_id", "from_id", "generation"],
|
|
74
|
+
unique=False,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def downgrade() -> None:
|
|
79
|
+
"""Remove relation generation ownership."""
|
|
80
|
+
op.drop_index("ix_relation_project_from_generation", table_name="relation")
|
|
81
|
+
if op.get_bind().dialect.name == "postgresql":
|
|
82
|
+
op.drop_column("relation", "generation")
|
|
83
|
+
else:
|
|
84
|
+
with op.batch_alter_table("relation") as batch_op:
|
|
85
|
+
batch_op.drop_column("generation")
|
|
86
|
+
|
|
87
|
+
op.drop_index(
|
|
88
|
+
"ix_relation_search_refresh_project_publication_generation",
|
|
89
|
+
table_name="relation_search_refresh",
|
|
90
|
+
)
|
|
91
|
+
if op.get_bind().dialect.name == "postgresql":
|
|
92
|
+
op.drop_column("relation_search_refresh", "publication_generation")
|
|
93
|
+
else:
|
|
94
|
+
with op.batch_alter_table("relation_search_refresh") as batch_op:
|
|
95
|
+
batch_op.drop_column("publication_generation")
|
basic_memory/api/app.py
CHANGED
|
@@ -5,7 +5,6 @@ from contextlib import asynccontextmanager
|
|
|
5
5
|
from fastapi import FastAPI, HTTPException, Request
|
|
6
6
|
from fastapi.exception_handlers import http_exception_handler
|
|
7
7
|
from fastapi.responses import JSONResponse
|
|
8
|
-
from fastapi.routing import APIRouter
|
|
9
8
|
from loguru import logger
|
|
10
9
|
|
|
11
10
|
from basic_memory import __version__ as version
|
|
@@ -20,14 +19,12 @@ from basic_memory.api.v2.routers import (
|
|
|
20
19
|
prompt_router as v2_prompt,
|
|
21
20
|
importer_router as v2_importer,
|
|
22
21
|
schema_router as v2_schema,
|
|
23
|
-
|
|
24
|
-
from basic_memory.api.v2.routers.project_router import (
|
|
25
|
-
add_project,
|
|
26
|
-
list_projects,
|
|
27
|
-
synchronize_projects,
|
|
22
|
+
inspect_router as v2_inspect,
|
|
28
23
|
)
|
|
29
24
|
import logfire
|
|
25
|
+
from basic_memory.index.note_content_materialization import drain_pending_materializations
|
|
30
26
|
from basic_memory.config import init_api_logging
|
|
27
|
+
from basic_memory.index.local_schedulers import drain_background_tasks
|
|
31
28
|
from basic_memory.services.exceptions import EntityAlreadyExistsError
|
|
32
29
|
from basic_memory.services.initialization import initialize_app
|
|
33
30
|
from basic_memory.workspace_context import (
|
|
@@ -67,10 +64,10 @@ async def lifespan(app: FastAPI): # pragma: no cover
|
|
|
67
64
|
app.state.session_maker = session_maker
|
|
68
65
|
logger.info("Database connections cached in app state")
|
|
69
66
|
|
|
70
|
-
# Create and start
|
|
71
|
-
|
|
72
|
-
await
|
|
73
|
-
app.state.
|
|
67
|
+
# Create and start local watch coordinator (lifecycle centralized in coordinator)
|
|
68
|
+
watch_coordinator = container.create_watch_coordinator()
|
|
69
|
+
await watch_coordinator.start()
|
|
70
|
+
app.state.watch_coordinator = watch_coordinator
|
|
74
71
|
|
|
75
72
|
# Proceed with startup
|
|
76
73
|
yield
|
|
@@ -82,7 +79,13 @@ async def lifespan(app: FastAPI): # pragma: no cover
|
|
|
82
79
|
mode=container.mode.name.lower(),
|
|
83
80
|
):
|
|
84
81
|
logger.info("Shutting down Basic Memory API")
|
|
85
|
-
await
|
|
82
|
+
await watch_coordinator.stop()
|
|
83
|
+
# A local note write returns 202 before its markdown file is written;
|
|
84
|
+
# SIGTERM can land while that materialization (and the vector sync /
|
|
85
|
+
# relation resolution it schedules) is still queued. Drain both queues
|
|
86
|
+
# before the engine closes so an accepted write is never lost.
|
|
87
|
+
await drain_pending_materializations()
|
|
88
|
+
await drain_background_tasks()
|
|
86
89
|
await container.shutdown_database()
|
|
87
90
|
|
|
88
91
|
|
|
@@ -129,19 +132,12 @@ app.include_router(v2_directory, prefix="/v2/projects/{project_id}")
|
|
|
129
132
|
app.include_router(v2_prompt, prefix="/v2/projects/{project_id}")
|
|
130
133
|
app.include_router(v2_importer, prefix="/v2/projects/{project_id}")
|
|
131
134
|
app.include_router(v2_schema, prefix="/v2/projects/{project_id}")
|
|
135
|
+
app.include_router(v2_inspect, prefix="/v2/projects/{project_id}")
|
|
132
136
|
app.include_router(v2_project, prefix="/v2")
|
|
133
137
|
|
|
134
138
|
# Legacy web app proxy paths (compat with /proxy/projects/projects)
|
|
135
139
|
app.include_router(v2_project, prefix="/proxy/projects")
|
|
136
140
|
|
|
137
|
-
# Legacy v1 compat: older CLI versions (v0.18.0 and earlier) call /projects/...
|
|
138
|
-
# Using router mount causes 307 redirect which proxy doesn't follow, so add explicit routes
|
|
139
|
-
legacy_router = APIRouter(tags=["legacy"])
|
|
140
|
-
legacy_router.add_api_route("/projects/projects", list_projects, methods=["GET"])
|
|
141
|
-
legacy_router.add_api_route("/projects/projects", add_project, methods=["POST"])
|
|
142
|
-
legacy_router.add_api_route("/projects/config/sync", synchronize_projects, methods=["POST"])
|
|
143
|
-
app.include_router(legacy_router)
|
|
144
|
-
|
|
145
141
|
# V2 routers are the only public API surface
|
|
146
142
|
|
|
147
143
|
|