Skip to content

refactor(core): add shared runtime orchestration - #1002

Merged
phernandez merged 497 commits into
mainfrom
codex/repository-explicit-sessions
Jul 14, 2026
Merged

refactor(core): add shared runtime orchestration#1002
phernandez merged 497 commits into
mainfrom
codex/repository-explicit-sessions

Conversation

@phernandez

@phernandez phernandez commented Jun 14, 2026

Copy link
Copy Markdown
Member

Why

This PR started as the explicit-session repository refactor and grew into the core half of the local/cloud runtime parity work. The goal is for basic-memory to own portable orchestration while cloud and local runtimes provide their own dependency adapters.

Paired cloud PR: basicmachines-co/basic-memory-cloud#1345.
Cloud parity document: basic-memory-cloud/docs/local-cloud-runtime-parity.md.

Closes #750.

What Changed

  • Convert repositories and call sites to explicit caller-owned sessions so cloud can provide tenant sessions without implicit session-maker state.
  • Add shared runtime contracts in basic_memory.runtime for storage events, note-content payloads, job identity/status, workflow metadata, materialization, and project/file indexing values.
  • Add basic_memory.cloud facades for note-content reads/writes, directory deletes, project deletes, and materialization so cloud/local APIs can share route-facing orchestration.
  • Add basic_memory.index as the event-based indexing orchestration layer for storage events, local filesystem watching, project fanout, and inline local processing.
  • Expand basic_memory.indexing with provider-neutral runners for accepted-note mutation/write flows, file indexing, project indexing, directory deletes, relation/vector follow-ups, and progress/result accounting.
  • Move legacy basic_memory.sync code and tests out of the active package/test tree so it is reference material only while the new index path becomes the clear file-indexing path.
  • Update API, MCP, CLI, dependency composition, and tests to use the shared runtime/index/gateway surfaces.

Public Surface

  • Local Basic Memory now has the same core note-content and event-indexing shapes that cloud uses through adapters.
  • Snapshots, file versions/history, tenant provisioning, Tigris bucket snapshots, and hosted admin behavior remain cloud-only and are not implemented in core.
  • The CLI/API route shapes have changed where needed to align with accepted note-content and async indexing behavior; the paired cloud branch consumes those new shapes.

Verification

Not rerun during PR publication. The branch has been validated incrementally during the refactor with focused core tests, just fast-check, just package-check, and paired cloud checks. Final broad core and cloud checks should run before merge.

Review Notes

This PR should be reviewed with the paired cloud PR because the ownership split is intentional: core owns portable orchestration; cloud owns hosted adapters and tenant/runtime wiring.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb65be817b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/services/entity_service.py Outdated
Comment thread src/basic_memory/sync/sync_service.py Outdated
@phernandez phernandez changed the title refactor(core): use explicit sessions in repositories refactor(core): add shared runtime orchestration Jun 23, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f338a6d382

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/indexing/project_index_workflow.py Outdated
Comment thread src/basic_memory/index/local_watch.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 703206a432

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/cloud/note_content_materialization.py Outdated
@phernandez
phernandez force-pushed the codex/repository-explicit-sessions branch from 703206a to 07a8882 Compare June 23, 2026 04:47
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

1 similar comment
@phernandez

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 58e93ca7c8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/runtime/contracts.py Outdated
Comment thread src/basic_memory/api/v2/routers/knowledge_router.py
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

3 similar comments
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@phernandez

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@phernandez
phernandez force-pushed the codex/repository-explicit-sessions branch from c3c5a7b to bd4b25c Compare June 23, 2026 14:40

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bd4b25ca3f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/basic_memory/runtime/contracts.py Outdated
Comment thread src/basic_memory/repository/accepted_note_search_repository.py Outdated
Comment thread src/basic_memory/indexing/project_index_workflow.py Outdated
Comment thread src/basic_memory/indexing/accepted_note_write_runner.py
Comment thread src/basic_memory/indexing/directory_delete_runner.py Outdated
@phernandez

Copy link
Copy Markdown
Member Author

Addressed the five latest Codex P2 findings in 07c25b2:

  • Deleted-note delete responses now fall back to file_path when permalink is missing, while external_id/title remain required.
  • SQLite accepted-note search inserts now store metadata as JSON text; Postgres keeps the JSONB cast.
  • Inline project indexing now consumes batch vector_targets through the existing embedding batch planner when semantic search is enabled.
  • Accepted-note deletes clear semantic vectors before deleting the entity.
  • Directory delete batches clear semantic vectors before deleting entity rows.

Regression coverage was added for each bug. Verification:

  • just pr-1002-feedback-test
  • just local-project-index-test
  • just fast-check
  • just doctor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 07c25b2144

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>
phernandez and others added 12 commits July 13, 2026 20:40
…objects

to_response_payload emitted errors as plain reason strings, but the
MCP/CLI client validates the payload as DirectoryDeleteResult whose
errors field is a list of DirectoryDeleteError ({path, error}) objects.
Any partial failure (guarded skip or enqueue failure) therefore raised
a Pydantic validation error client-side instead of reporting the failed
paths.

Carry structured DirectoryDeleteFileFailure (path, reason) pairs
through the runner's skip/failure accounting and serialize errors as
objects matching the client schema. Adds a client-shape regression that
validates the payload with DirectoryDeleteResult.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
accept_directory_delete captures the surviving outside-directory
sources whose relations pointed into the deleted set
(relation_cleanup_entity_ids), but DirectoryDeleteService dropped them,
leaving stale relation rows in the search index until an unrelated
rebuild. The project-index path already repairs these; the API route
path did not.

Add a DirectoryDeleteRelationCleanupRefresher capability to the
directory-delete runtime and reindex each surviving source after the
delete acceptance. Sources deleted in the meantime are skipped, not
fatal. The local runtime wires a repository/search-backed refresher;
queued runtimes still receive the ids on the accepted result.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
LocalProjectIndexScheduler spawned a bare asyncio task per request, so
every POST .../index and the startup scan started independent full
coordinator runs over the same rows; overlapping runs are also the
trigger for move/delete races.

Coalesce per project: while a run is in flight, a new request marks the
project dirty (keeping the strongest force_full) and exactly one
trailing rerun starts when the current run finishes — matching the
relation-resolution debounce in the same module. Test mode stays a
no-op without leaking the pending marker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Port the relay-owned note persistence concurrency guard (cloud issue
#1445) into the core accepted-note write path that PR #1002 moved out of
basic-memory-cloud.

Browser saves and the collaboration relay send the db_checksum they last
synced as the x-bm-cloud-note-base-checksum header on note PUTs. When the
accepted note_content row has advanced past that base, the update runner
now rejects the write with a structured 409 whose detail body is
{"message": "Note changed since your last sync", "db_checksum":
<current or null>} so clients rebase instead of clobbering the newer
write.

- AcceptedNoteUpdateMutation gains optional base_checksum; the replace
  branch compares it against the current accepted db_checksum, and the
  create branch rejects outright (db_checksum null) because a
  precondition with no surviving entity means the note was deleted after
  the caller's pre-read - creating it would silently resurrect it.
- Unlike cloud main's SELECT ... FOR UPDATE, core needs no row lock: the
  accept_write compare-and-set on db_version already prevents a write
  planned against a stale read from landing; a race between the check and
  the CAS trips the CAS and surfaces the concurrent-write 409.
- The rejection detail is typed (AcceptedNoteBaseChecksumConflict) in the
  runner and serialized to its wire dict at the service boundary;
  NoteContentMutationServiceError.detail widens to str | dict so routes
  keep passing it verbatim to HTTPException.
- The v2 PUT route reads the header (constant lives in the runtime
  note-content vocabulary) and threads it through
  NoteContentMutationService.update_note. An absent header keeps the
  existing upsert contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
…rch refresh

RepositoryProjectIndexMovedEntitySearchRefresher raised RuntimeError when
any moved entity id no longer had a database row at refresh time. Move
batches commit before the refresh runs, so a concurrent delete (file
removed, note deleted via API) between move-batch commit and refresh is a
benign race — but the raise aborted the whole coordinator run before
delete batches and file indexing executed, stalling the scan.

A missing entity at refresh time is now logged and skipped; the surviving
moved entities are still refreshed. The vanished entity's search rows were
already removed with its row, so there is nothing left to repair.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
…can reconciliation

os.walk's onerror only aborted the scan when the project root itself was
unreadable; a deeper unreadable subdirectory was pruned from the observed
snapshot with a warning. Delete reconciliation plans
all_db_paths - storage_paths, so every indexed row under the vanished
subtree was destroyed even though the files still exist.

Following the unobservable-file carry-through pattern (46d0011), the walk
now reports unreadable non-root directories in a LocalProjectIndexScan
result, and the observed-file source carries every indexed path under them
through the snapshot as present-with-unknown-checksum
(RuntimeObservedIndexFile with checksum None). Those paths classify as
modified, never deleted, and the batch planner re-checks them next pass.
The indexed-path source is the already-loaded indexed-stat snapshot; the
bare observed source (no stat source) has nothing to carry.

A walk error without a directory attribution now fails the scan outright:
it cannot be carried, and finishing the walk could still plan a mass
delete for an unknown subtree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
…n reconciliation

Scan delete plans compare a storage snapshot against a later DB read
(deleted = all_db_paths - storage_paths). A note accepted and materialized
between snapshot capture and the DB read was planned as deleted, and the
delete batch destroyed its entity, search, and vector rows with no
re-verification.

apply_project_index_delete_batch now re-confirms each planned path through
a ProjectIndexDeletePathVerifier capability before touching the database.
The local runtime injects LocalProjectIndexDeletePathVerifier, which
probes the filesystem: a path present again (or whose probe fails) is
skipped and logged — the next scan picks the file up as modified. Cloud/S3
runtimes keep current behavior through the explicit
TrustPlannedProjectIndexDeleteVerifier pass-through default, since their
storage listing is authoritative and they have no cheap per-path probe at
apply time. Skipped paths are reported on the batch result and delete run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
…nation entities

Scan change planning only pairs a move with a destination that had no DB
row at snapshot time, so an entity found at the destination during apply
was created concurrently (e.g. an accepted write_note whose content may
not be materialized yet). apply_project_index_move_batch deleted that
entity unconditionally and re-pointed the stale source entity onto the
path, destroying the accepted content.

Scan runtimes now opt into verify_replaced_move_targets on the maintenance
store: a replacement row is only deleted when its checksum equals the
moved entity's indexed checksum — the checksum the move was planned
against — which proves it merely indexes the moved bytes (a racing event
index of the new path, a safe dedupe). Any other replacement drops that
move from the batch with a warning; nothing is deleted or re-pointed, and
the next scan reconciles the stale source path as deleted-or-present.
Dropped source paths are reported on the batch result and move run.

The unconditional-replace branch stays the default because the watcher
flow legitimately moves onto an existing indexed file (mv over an indexed
path) where the pre-existing destination row must be replaced; cloud scan
runtimes also keep current behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
…ommits

apply_project_index_move_batch called the move content updater inside the
open db.scoped_session, and the local updater wrote the moved file's
permalink frontmatter to disk via update_frontmatter_with_result before
the transaction committed. If the batch rolled back (e.g. an intra-batch
permalink collision), the database reverted but the on-disk rewrites
persisted, leaving files ahead of their indexed state.

The ProjectIndexMoveContentUpdater capability is now split into
plan_moved_file_content — which runs inside the transaction, resolves the
permalink, and builds the updated markdown in memory without touching
storage — and write_moved_file_content, which the store calls only after a
successful commit. The database rows are stamped from the planned content,
whose checksum matches the exact bytes the post-commit write persists, so
DB and file agree when the write lands; a failed write (or formatter or
platform newline divergence) is logged and surfaces on the next scan as a
checksum-mismatch modified file, which re-indexes from disk.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
…ails

The trailing rerun sat after the try/finally, so an exception from
index_project skipped it and the request that coalesced behind the failed
run was silently dropped — exactly when it most needed a retry. Reschedule
inside finally, after the pending marker clears.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez
phernandez force-pushed the codex/repository-explicit-sessions branch from 5073c83 to c9980a6 Compare July 14, 2026 02:31
phernandez and others added 2 commits July 13, 2026 21:40
The refreshed ty dev dependency enforces mutability on protocol members:
a protocol that declares plain attributes (id: int, embeddings: bool, ...)
now rejects frozen-dataclass implementers, because their attributes are
read-only. The indexing test fakes were frozen dataclasses standing in for
such protocols (OrphanIndexedEntity, IndexedNoteContentEntity,
EmbeddingIndexBatchSummary, VectorSyncBatchSummary,
ProjectIndexRequestSource, ProjectDeleteAcceptedProjectSource,
UnresolvedRelation, ResolvedRelationTarget), which produced 15
invalid-argument-type diagnostics.

Drop frozen=True from those fakes so they are structurally assignable to
the protocols they implement, matching the writable shape of the real
implementations (SQLAlchemy models and queue payloads). No test relies on
the fakes being frozen or hashable, so runtime behavior is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 53863eb833

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +77 to +78
parts = self.key.split("/", 1)
return parts[0] if len(parts) == 2 else ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve multi-segment storage prefixes

When a project storage prefix contains a slash, this parser keeps only the first segment as the project prefix. For example, storage_object_key_from_project_path("/app/data/team-a/research", "notes/a.md") produces team-a/research/notes/a.md, but this code routes the event as project team-a with relative path research/notes/a.md; the repository resolver never sees the intended team-a/research prefix, so storage events for nested cloud/workspace prefixes are skipped or can route to the wrong project. Preserve the full configured project prefix before deriving the relative path.

Useful? React with 👍 / 👎.

Path.exists() folds PermissionError into False, so an unreadable file
counted as confirmed-absent and the planned index delete proceeded. The
verifier now stats the path directly: FileNotFoundError (or a vanished
parent) confirms absence, any other OSError skips the delete for the next
scan to reconcile. Also records the same-path-move fail-fast decision at
the rejection site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 96533ef0f9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

entity = entities_by_id.get(entity_id)
if entity is None:
continue
await self.entity_indexer.index_entity(entity)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh vectors after relation cleanup

When this refresher is invoked with relation_cleanup_entity_ids after a watcher/project-index delete removes a note that other notes link to, it rebuilds only the surviving source's FTS rows. Semantic chunks are stored separately and are only pruned by sync_entity_vectors_batch, so the old relation chunks for that surviving source remain in search_vector_chunks/search_vector_embeddings and semantic search can keep returning relations to the deleted note until a later full vector reindex. Include those cleanup ids in a vector sync path when semantic indexing is enabled.

Useful? React with 👍 / 👎.

@phernandez
phernandez merged commit 53da71c into main Jul 14, 2026
29 checks passed
@phernandez
phernandez deleted the codex/repository-explicit-sessions branch July 14, 2026 04:09
phernandez added a commit that referenced this pull request Jul 15, 2026
DELETE /v2/projects/{id}/knowledge/entities/{id} used to return 500 for
note_type="file" entities (issue #1033) because the delete path did
markdown-specific cleanup. The #1002/#1054 refactor fixed this by routing
single-entity delete through the shared accepted-note delete path, but no
test covered the non-markdown case.

Add a regression test that indexes a real .csv file through the local
project indexer (note_type="file", no permalink, no note_content row),
deletes it via the v2 endpoint, and asserts the entity row and its search
index rows are gone.

Refs #1033

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
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.

Refactor repositories to support caller-owned AsyncSession flows

1 participant