Skip to content

feat(memory): implement independent atomic memory - #1857

Open
frf12 wants to merge 11 commits into
oceanbase:masterfrom
frf12:codex/atomic-memory
Open

frf12 wants to merge 11 commits into
oceanbase:masterfrom
frf12:codex/atomic-memory

Conversation

@frf12

@frf12 frf12 commented Oct 6, 2026

Copy link
Copy Markdown
Member

Which issue or RFC does this PR close?

Implements RFC #1809 and follows the retrieval contract in RFC #1803.

Rationale for this change

Memory collections couple unrelated entries to one revision and write boundary. This change makes each fact, preference or decision an independent Artifact, so content revision, lifecycle, authorization and exact evidence can be managed per memory.

What changes are included in this PR?

  • Introduce atomic-memory content with immutable revisions and separate active, forgotten, merged and retired state. Add explicit writes, exact historical reads, merge, forgetting, restoration preview, restoration and merge undo.
  • Replace collection generation with Source extraction and reconciliation (create, revise, merge, noop). Recheck evidence, authorization, read dependencies and cursor generation before publishing memory changes and Source progress together.
  • Separate authoritative history/head/state from the rebuildable current search projection. Apply Scope, access, kind and tag eligibility before ranking and limits; use consistent snapshots for short reads. Support text, vector and hybrid retrieval, including full-text retrieval without embeddings.
  • Expose Atomic operations through HTTP, MCP and the Python client; update host adapters, generated contracts, dashboard, benchmarks, tests and bilingual workflow/migration documentation. Add offline migration and projection maintenance commands.

Are there any user-facing changes?

This is a breaking Memory upgrade.

  • Retained remember/search/list routes return Atomic records and exact Artifact references. Flush retains progress/counts with memory: null. Route retention does not preserve the old response models. Collection CAS, citation revise/retire, capacity/compaction, collection Create/Replace, continuous changes and collection rollback are unsupported; HTTP rejects unsupported operations with 422 legacy_memory_operation_unsupported before writes.
  • Existing databases require a backup, stopped writers and explicit atomic-memory-migrate --action plan/apply/verify; startup verifies readiness instead of converting data automatically. Migration maps legacy entries to deterministic new Artifact IDs while retaining their version chains, historical evidence, lifecycle, ownership, tags, grants/receipts and Source/task progress. Invalid history/ownership, unsupported grants, unresolved candidates or task/cursor formats can block conversion.
  • Custom memory.extract Prompts require explicit migration to atomic_memory.extract and atomic_memory.reconcile. Legacy CandidatePipeline and MemoryWriteGate injection are incompatible with the Atomic runtime.
  • Ordinary vector search and related-memory enumeration currently use exact L2 over eligible rows. Related memories are compared in bounded model batches, but enumeration is uncapped. This PR makes no ANN or production-scale performance claim. Merge/restoration do not provide a durable idempotency receipt.

How was this change tested?

Submission checks: uv run --locked prek run -a passed all 11 hooks, including Ruff and ty; git diff --check passed. The seven follow-up files match the recorded validation material.

Recorded validation used isolated, locked environments:

  • DSH: pnpm --dir integrations/dsh/plugins/powercontext test and test:e2e — 266 unit tests and 9 E2E tests passed on each of macOS and Linux. Formal builds reproduced the checked-in bundles. OpenCode test — 72 passed; typecheck and build passed.
  • Selected pytest coverage: 84 prompt/Atomic checks passed; 49 distinct SeekDB cases and 37 supported SQLite Dream cases passed; the seven host dependency checks passed. Host coverage includes adapter fixtures, native discovery and one real CLI hook, rather than complete acceptance of every host.
  • Real-model Tags/Prompt acceptance: six distinct SQLite/OceanBase nodes passed with GPT-5.6 Luna and text-embedding-3-small. SQLite's three raw XML reports were checked locally. OceanBase's three complete launcher records exited 0; their raw XML/events could not be exported after connectivity was lost. Across all model attempts, results were 9 passed / 8 failed over 17 executions of those six nodes: two provider errors, three model-output/decision failures and three configuration failures. These earlier failures remain recorded.
  • Earlier full default suite at 2b903293: 3,798 passed / 152 skipped / 4 deselected. Across recorded implementation commits, all 90 distinct official OceanBase nodes have passing records after targeted fixes/replays. This is not a full-suite run of the submission's seven follow-up files; that full suite was not rerun.

A historical Zcode Stop unknown and one OceanBase FULLTEXT initialization lock timeout were not reproduced in bounded diagnostics; their causes remain unknown. Windows/macOS-specific cases, isolated native service checks and four controlled/live Zcode acceptance nodes remain deferred. Real-model OceanBase raw evidence export and the new validation container's stop operation remain pending connectivity. Migration evidence covers the tested schemas/scenarios, not production deployment acceptance.

AI usage statement

GPT-6.1 Sol Ultra subagents performed implementation, validation and PR preparation; the Codex root agent reviewed the result. GPT-5.6 Luna was used as the generation/reranking model in the final real-model acceptance run. Validation scope and evidence limits are stated above.

@Teingi Teingi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Four correctness issues need fixing before merge; details and reproductions are inline. I also exercised migration and HTTP/MCP lifecycle flows against real OceanBase 4.3.5.4 with live model calls. Provider account errors and timeouts required supplementary model configurations, so this does not establish an all-green run with the original model configuration. The migration finding reproduces on real OceanBase; the other findings use the controlled timing or model responses described in their comments.

raise InvalidBaseAccessRequestError("mode", "must be auto, text, vector, or hybrid")
if kind is not None:
AtomicMemoryContent(kind=kind, text="validation")
filters = await application.security.filters(self.scope_id, self._context(context), tags=tag_filter)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] Bind search authorization to the data snapshot

filters caches scope_read before query embedding, but retrieval uses a later database snapshot without rechecking that permission. In a SQLite HTTP reproduction, I paused a reader's vector query during embedding, revoked its scope.viewer grant, then created a new memory. Resuming the query returned that new body; a fresh query returned no hits and direct GET returned 403. This reproduces with both built-in and Casbin access providers. The returned memory did not exist while the reader was authorized. Resolve permission and eligible data within the same snapshot after embedding, rather than carrying the earlier scope-wide allow decision into retrieval.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in a6d1c379. Query embedding completes before the read transaction; authorization now uses access.with_connection(connection) inside the same consistent snapshot as retrieval and returned-hit validation. Decision audit writes flush after that transaction closes. No Scope row lock was added.

Added HTTP race regressions for both Builtin and Casbin providers: revocation during embedding denies the subsequent search, and revocation plus creation during an established read snapshot can commit without blocking while the reader sees only its original snapshot. The new cases passed on SQLite and real SeekDB. The new and existing snapshot suites also passed on real OceanBase 4.3.5.6: 15 passed, 0 failed, 0 skipped.


def _imported_refs(entry: _Entry, row: Mapping[str, Any]) -> tuple[dict[str, Any], ...]:
values = [
{"family": "memory", "artifact_id": entry.memory_id, "revision": row["created_in_revision"]},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Preserve per-entry evidence when importing legacy memory

Adding the whole collection revision to every imported entry's lineage makes EvidenceResolver traverse that collection's Sources too. I migrated a collection written with Sources A and B where entry A cited only Source A: resolving the imported atomic memory produced roots A+B and included B's unrelated body in the evidence projection used by Dream, while its legacy MemoryCitation still resolved only A. This reproduces on SQLite and real OceanBase 4.3.5.4, even though apply, verify and restart report ready. Preserve the selected entry's exact evidence; collection-level migration provenance must not become evidence for every entry. The reproduction exercised evidence resolution, not a Dream model call.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in a6d1c379. The stored collection reference remains migration provenance. A shared reader now identifies the imported entry through its exact retained manifest/version and deterministic Atomic identity, then expands only that entry's Source/Artifact evidence and same-entry predecessor. Dream evidence resolution and automatic extraction both use it.

The provenance anchor is excluded from evidence-graph traversal, including when another selected root explicitly references the same collection. Ordinary explicit collection evidence retains its existing meaning. This also fixes already-imported databases without rewriting immutable content or lineage.

Added 13 SQLite regressions covering per-entry Sources and Artifacts, mixed-root selection, history, merge/restore, automatic extraction, and verify/repeated apply with unchanged imported rows.

model = self.application.embedding_model
if model is None or profile is None or model.profile != profile:
raise AtomicMemoryIndexError("embedding-profile", "Related-memory vector profile is unavailable")
result = await model.embed((query,))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Use query embeddings for related-memory retrieval

_query_vector() calls model.embed(), while ordinary memory search uses embed_query(). These differ for supported asymmetric embedding providers. With the MiniMax adapter and mocked HTTP responses containing asymmetric vectors, ordinary vector search emits type=query and retrieves the existing memory, but Source flush emits type=db, supplies an empty related set to reconciliation, and creates a second active identity instead of revising the existing one. Use the query embedding boundary here as well so related-memory retrieval has the same query semantics as ordinary search.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in a6d1c379. Related-memory retrieval now calls the existing embed_query helper. Document writes still use document embeddings, and symmetric providers retain the helper's fallback to embed.

Added a Source-flush regression using the real MiniMax adapter with mocked asymmetric HTTP responses. It verifies type=query for recall and that an equivalent Source retains the existing memory identity instead of creating a duplicate. A symmetric-provider regression also passes.

Comment on lines +487 to +490
except AtomicMemoryIndexError:
if not self.config.related_fts_fallback:
raise
mode = "fts"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Apply the opt-in FTS fallback to transient embedding failures

The documented ATOMIC_MEMORY_RELATED_FTS_FALLBACK=true option permits full-text retrieval when the query vector is unavailable, but this handler only catches AtomicMemoryIndexError. A provider's InferenceUnavailableError therefore escapes before reconciliation. In a SQLite probe with an existing vectorized memory and a duplicate Source, injecting this temporary embedding failure leaves the cursor at 0 in hybrid mode with fallback enabled; switching to FTS performs a noop, advances the cursor to 2, and retains one active memory without needing new document embeddings. Handle the supported transient query-embedding errors when this fallback is enabled.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in a6d1c379. The query-vector boundary now handles InferenceUnavailableError and InferenceTimeoutError when the FTS fallback is explicitly enabled. With fallback disabled, the error propagates and the cursor stays unchanged. Document-embedding failures still fail the write; programming errors and cancellation also propagate.

Source-flush regressions cover unavailable/timeout responses, duplicate noops, create/revise failures, and recovery: 12 passed.

Overall verification: all 25 CI checks pass; the full local E2E rerun has 407 passed, 0 failed, 96 skipped (4 deselected). The full local unit rerun retains one failure: 3,432 passed, 1 failed, 68 skipped. test_destructured_assignments_downgrade_outer_identifiers_and_members[js] exhausted the existing 5-second query deadline during repository revalidation. That case passed in isolation and in all four CI Python versions; the cause of the local timeout remains unconfirmed. No timeout or assertion was weakened.

This branch has not been deployed

No deployments
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.

2 participants