Skip to content

Commit 53da71c

Browse files
phernandezclaude
andauthored
refactor(core): add shared runtime orchestration (#1002)
Extracts runtime, indexing, and index orchestration from the cloud platform into the local-first core: session-explicit stateless repositories, accepted-note mutation runners with optimistic concurrency (db_version CAS), note materialization with per-note worker routing and crash recovery, scan reconciliation with delete re-verification and unreadable-subtree carry-through, directory/project delete runners with structured errors and relation cleanup, and the relay-persistence contracts (base-checksum precondition, superseded-content live updates). Verified against the full adversarial review: all 28 confirmed findings fixed with regression tests, plausible findings adjudicated and fixed, all review threads resolved. Follow-up structure campaign: #1053. Signed-off-by: phernandez <paul@basicmachines.co> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 26846e3 commit 53da71c

334 files changed

Lines changed: 61555 additions & 15002 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ See the [README.md](README.md) file for a project overview.
2222
- Run unit tests (Postgres): `just test-unit-postgres`
2323
- Run integration tests (SQLite): `just test-int-sqlite`
2424
- Run integration tests (Postgres): `just test-int-postgres`
25-
- Run impacted tests: `just testmon` (pytest-testmon; only tests affected by changed code)
25+
- Run impacted tests: `just fast-test` or `just testmon` (pytest-testmon; only tests affected by changed code)
2626
- Run MCP smoke test: `just test-smoke`
27-
- Fast local loop: `just fast-check` (default iteration flow)
27+
- Fast static check: `just fast-check` (fix, format, typecheck)
28+
- Fast test loop: `just fast-test` (pytest-testmon impacted tests)
2829
- Local consistency check: `just doctor`
2930
- Run all consolidated agent package checks: `just package-check`
3031
- Run Claude Code plugin checks: `just package-check-claude-code`
@@ -39,7 +40,7 @@ See the [README.md](README.md) file for a project overview.
3940
- Type check: `just typecheck` or `uv run ty check src tests test-int`
4041
- Type check (pyright): `just typecheck-pyright` or `uv run pyright`
4142
- Format: `just format` or `uv run ruff format .`
42-
- Run all code checks: `just check` (runs lint, format, typecheck, test)
43+
- Run all static code checks: `just check` (runs lint, format, typecheck)
4344
- Create db migration: `just migration "Your migration message"`
4445
- Run development MCP Inspector: `just run-inspector`
4546

@@ -54,10 +55,11 @@ See the [README.md](README.md) file for a project overview.
5455
### Code/Test/Verify Loop (fast path)
5556

5657
1) **Code:** make changes.
57-
2) **Test:** `just fast-check` (lint/format/typecheck + pytest-testmon impacted tests for changed code).
58-
3) **Verify:** `just doctor` (end-to-end file ↔ DB loop in a temp project).
59-
4) **Package verify:** `just package-check` when changes touch `plugins/`, `skills/`, `integrations/`, package metadata, or release wiring.
60-
5) **Full gate (when needed):** `just test` or `just check` for SQLite + Postgres.
58+
2) **Check:** `just fast-check` (fix, format, typecheck; no tests).
59+
3) **Test:** `just fast-test` for pytest-testmon impacted tests, or a targeted `uv run pytest ...` command.
60+
4) **Verify:** `just doctor` (end-to-end file ↔ DB loop in a temp project).
61+
5) **Package verify:** `just package-check` when changes touch `plugins/`, `skills/`, `integrations/`, package metadata, or release wiring.
62+
6) **Full gate (when needed):** `just test` for SQLite + Postgres.
6163

6264
Run `just test-smoke` when you specifically need the MCP smoke flow.
6365

justfile

Lines changed: 276 additions & 35 deletions
Large diffs are not rendered by default.

src/basic_memory/api/app.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
synchronize_projects,
2828
)
2929
import logfire
30+
from basic_memory.cloud.note_content_materialization import drain_pending_materializations
3031
from basic_memory.config import init_api_logging
32+
from basic_memory.deps.services import drain_background_tasks
3133
from basic_memory.services.exceptions import EntityAlreadyExistsError
3234
from basic_memory.services.initialization import initialize_app
3335
from basic_memory.workspace_context import (
@@ -67,10 +69,10 @@ async def lifespan(app: FastAPI): # pragma: no cover
6769
app.state.session_maker = session_maker
6870
logger.info("Database connections cached in app state")
6971

70-
# Create and start sync coordinator (lifecycle centralized in coordinator)
71-
sync_coordinator = container.create_sync_coordinator()
72-
await sync_coordinator.start()
73-
app.state.sync_coordinator = sync_coordinator
72+
# Create and start local watch coordinator (lifecycle centralized in coordinator)
73+
watch_coordinator = container.create_watch_coordinator()
74+
await watch_coordinator.start()
75+
app.state.watch_coordinator = watch_coordinator
7476

7577
# Proceed with startup
7678
yield
@@ -82,7 +84,13 @@ async def lifespan(app: FastAPI): # pragma: no cover
8284
mode=container.mode.name.lower(),
8385
):
8486
logger.info("Shutting down Basic Memory API")
85-
await sync_coordinator.stop()
87+
await watch_coordinator.stop()
88+
# A local note write returns 202 before its markdown file is written;
89+
# SIGTERM can land while that materialization (and the vector sync /
90+
# relation resolution it schedules) is still queued. Drain both queues
91+
# before the engine closes so an accepted write is never lost.
92+
await drain_pending_materializations()
93+
await drain_background_tasks()
8694
await container.shutdown_database()
8795

8896

src/basic_memory/api/container.py

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717

1818
from basic_memory import db
1919
from basic_memory.config import BasicMemoryConfig, ConfigManager
20-
from basic_memory.runtime import RuntimeMode, resolve_runtime_mode
20+
from basic_memory.runtime.mode import RuntimeMode, resolve_runtime_mode
2121

2222
if TYPE_CHECKING: # pragma: no cover
23-
from basic_memory.sync import SyncCoordinator
23+
from basic_memory.index.watch_coordinator import WatchCoordinator
2424

2525

2626
@dataclass
@@ -54,40 +54,43 @@ def create(cls) -> "ApiContainer": # pragma: no cover
5454
# --- Runtime Mode Properties ---
5555

5656
@property
57-
def should_sync_files(self) -> bool:
58-
"""Whether file sync should be started.
57+
def should_watch_files(self) -> bool:
58+
"""Whether local file watching should be started.
5959
60-
Sync is enabled when:
61-
- sync_changes is True in config
62-
- Not in test mode (tests manage their own sync)
60+
Watching is enabled when:
61+
- index_changes is True in config
62+
- Not in test mode (tests manage their own watcher lifecycle)
63+
- Not in cloud mode (cloud handles storage events differently)
6364
"""
64-
return self.config.sync_changes and not self.mode.is_test
65+
return self.config.index_changes and not self.mode.is_test and not self.mode.is_cloud
6566

6667
@property
67-
def sync_skip_reason(self) -> str | None: # pragma: no cover
68-
"""Reason why sync is skipped, or None if sync should run.
68+
def watch_skip_reason(self) -> str | None: # pragma: no cover
69+
"""Reason why local watching is skipped, or None if it should run.
6970
70-
Useful for logging why sync was disabled.
71+
Useful for logging why local watching was disabled.
7172
"""
7273
if self.mode.is_test:
7374
return "Test environment detected"
74-
if not self.config.sync_changes:
75-
return "Sync changes disabled"
75+
if self.mode.is_cloud:
76+
return "Cloud mode enabled"
77+
if not self.config.index_changes:
78+
return "Local file watching disabled"
7679
return None
7780

78-
def create_sync_coordinator(self) -> "SyncCoordinator": # pragma: no cover
79-
"""Create a SyncCoordinator with this container's settings.
81+
def create_watch_coordinator(self) -> "WatchCoordinator": # pragma: no cover
82+
"""Create a WatchCoordinator with this container's settings.
8083
8184
Returns:
82-
SyncCoordinator configured for this runtime environment
85+
WatchCoordinator configured for this runtime environment
8386
"""
8487
# Deferred import to avoid circular dependency
85-
from basic_memory.sync import SyncCoordinator
88+
from basic_memory.index.watch_coordinator import WatchCoordinator
8689

87-
return SyncCoordinator(
90+
return WatchCoordinator(
8891
config=self.config,
89-
should_sync=self.should_sync_files,
90-
skip_reason=self.sync_skip_reason,
92+
should_watch=self.should_watch_files,
93+
skip_reason=self.watch_skip_reason,
9194
)
9295

9396
# --- Database Factory ---

0 commit comments

Comments
 (0)