Design: MemMachine server redesign (tenants, event memory API, configuration, lifecycle, schema management) by edwinyyyu · Pull Request #1579 · MemMachine/MemMachine · GitHub
Skip to content

Design: MemMachine server redesign (tenants, event memory API, configuration, lifecycle, schema management) - #1579

Draft
edwinyyyu wants to merge 63 commits into
MemMachine:speedkickfrom
edwinyyyu:design/tenant-lifecycle
Draft

Design: MemMachine server redesign (tenants, event memory API, configuration, lifecycle, schema management)#1579
edwinyyyu wants to merge 63 commits into
MemMachine:speedkickfrom
edwinyyyu:design/tenant-lifecycle

Conversation

@edwinyyyu

@edwinyyyu edwinyyyu commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

A design proposal, no code: design/server_redesign.md. It started as a tenant lifecycle design and now covers the server, because the lifecycle cannot be fixed inside the current API, configuration and wiring. Against speedkick, which carries the segment store overhaul it refers to (#1548, the merged copy of #1545). Tracking: #1574. Line references are to speedkick at 7752e4c. Settings are named, never given numeric defaults. The premise since the last revision: the first deployment of this design carries no data forward.

Decisions the document records

  • Tenant replaces org/project/session: an isolated memory with its own configuration and lifecycle. Name (arbitrary string, unique among live tenants, renamable) lives in one SQL table; the id is a UUID minted per lifetime and is the physical key in every store (no incarnation anywhere); the row stays as a tombstone after deletion, refusing a reused id at mint and driving the tombstone sweep, until a sweep finds nothing under the id and a retention of the order of a day has passed on the database clock; every remote client must have a request timeout, which is what makes that release safe.
  • Events are the caller's ingestion type, with a source_id beside timestamp (a bounded application-owned identity, indexed and filtered by source_ids) and a Context that is a mapping from part kind to one registered Pydantic part (composed by merging, read by kind, never filtered; first kinds author with the name as recorded and time_ranges from Add temporal scoring #1436; library users register kinds; replaces Add temporal scoring #1436's ordered CompositeContext; every hit returns source_id and context as data, so an application renders current names itself and the server keeps no directory); producer, produced_for and roles are gone. Blocks are a registered family of kinds like context parts (text built in, a kind discriminator, a render hook, unknown kinds survive decoding, and a segmenter or deriver with no policy for a kind passes it through as one segment with no derivatives); a segment is one block, so a block's kind is a system field of the segment and never of the event. Event and Context are reshaped for one reason: to prohibit dynamic index creation, which makes a deployment too difficult to maintain and its performance unpredictable; every kind of data gets a place that is not an index. An event store, its own component and the system of record, keeps them and a per-tenant log of additions and deletions. Ingest and deletion respond 202 once durable; processing is a replay job per tenant and subsystem, one consumer each and so in commit order, executed by reconcilers or inline, with ?wait= polling the watermarks. EventMemory is renamed EpisodicMemory; its segment and vector stores are derived. No rebuild of derived data: a full reprocessing is a new ingestion into a new tenant.
  • Control plane and data plane are separate objects. The tenant service creates, renames, configures and deletes tenants and records jobs, and is the only reader of the tenant table. Components own their per-tenant state and are known to it only as a name, a tenant configuration model and three hooks: provision (idempotent by provenance), delete (logical unlink), purge (one bounded batch of physical removal, DONE or MORE; a sweep calls it until done or a time budget is spent). The tombstone sweep is the only scheduled duty. Create, delete and configuration updates respond 202 and are polled or waited on (?wait=); a duplicate name is 409 tenant_exists and nothing else; configuration updates keep the tenant active, an option being immutable exactly when changing it would touch existing data. Data operations carry the tenant id in the path; there are no handles at the API. Inside the server a consumer of a store's data holds a stateless handle (SegmentPartition, VectorCollection): the store's operations bound to one key and container, built without I/O, fenced per operation by the store, nothing in it to go stale or evict; past its construction nothing can route to another tenant. The incarnation-bound, opened-and-closed handles of the current stores are not these.
  • Keys and fencing. Stores take a UUID key and nothing else and fence on their own registry row, which also holds what addresses the tenant. SQL-backed stores keep the shipped in-statement fence and pins. Stores whose data is elsewhere read the row, perform the remote write acknowledged as applied, and read the row again, raising if the key died; no lock or lease spans remote I/O, since neither closes the window a dead session with a request in flight leaves, and the tombstone sweep collects that residual in any ordering. Reconciler job claims are held row locks. Store creates are strict; idempotency is the component's provision, which knows the key's provenance (agrees with Feat: Remove open-or-create semantics from VectorStore and SegmentStore (collection registry stack 3/5) #1530).
  • Key registry. One implementation of per-key bookkeeping for non-SQL stores, injected as a view scoped to one store name so a store structurally cannot touch another's rows; shared code and database, not shared rows. SQL stores keep their registry table beside their data.
  • Vector store: registry rows in the key registry, written before any record can carry a key; containers are deployment configuration (one per embedder), retired by the schema command once undeclared and unreferenced; the store rejects undeclared property keys on upsert and query. A table surveys Qdrant, Milvus, pgvector, Pinecone, S3 Vectors, Weaviate, Chroma and the two SQLite stores at the tier that scales; none needs an object opened per tenant and kept. The SQLite stores move to shared tables; no store creates a table per tenant.
  • Concurrency scope (process < host < cluster; Feat: Declare concurrency scope and data-plane contracts on storage ABCs (collection registry stack 4/5) #1531 is the reference): every resource computes its scope from its constructor arguments, the deployment declares the scope it runs at, and startup refuses anything narrower. The reconciler is a role.
  • Composition and settings, five rules. Fixed topology with pluggable slots chosen by kind in settings (a table per slot family, entry points for out-of-tree kinds, optional extras imported lazily, one image); composition is one Python function, type-checked, with the standard composition shipped and another loadable by module path; settings are data (per-kind Pydantic models from environment and an optional file, schema and example generated, no wiring expressible); scoped views wherever a shared resource serves several holders; and three scopes told apart by identity: resources (built by the composition, with lifecycle), configured objects (built by a factory from resources and a tenant's structural configuration, interchangeable, cached by configuration), call arguments. Factories are resources and their products are not. EpisodicMemoryManager is the resource; it builds one EpisodicMemory per request in one constructor call from the tenant's two handles, its embedder and cached segmenter and deriver objects, so past that call nothing can name another tenant or another embedder's container, and does dispatch, defaults and validation only. EpisodicMemory operations name no key. Per-request options (reranker, limits) are call arguments with tenant defaults, and the separation is enforced by signature.
  • Properties and filtering. System fields (since, before, session_ids, source_ids, block_kinds; a system field is one the server gives semantics beyond filtering: session_id bounds the order expansion walks, a block's kind selects its processing; the other candidates are ruled on in the text: a channel is a session, a workspace a set of sessions managed above the server, a user any of source, session, workspace or tenant, importance and language user properties; anything else efficient is a deployment-declared indexed property) are first-class typed parameters stored under a reserved memmachine_ key namespace, so stores filter them with the same machinery as user properties. User properties are scalar, bounded, immutable, copied verbatim to derived data; filter indexes are declared once per vector store, never dynamically; undeclared keys are filtered in the segment store. A filter is a constructed closed-union tree, a JSON object under a generated schema at the API and in MCP, no string language. Routing by backend capability and selectivity: each store declares the filter nodes it evaluates during a search (a table covers the major providers; Chroma lacks $ne, $not and $exists, sqlite-vec takes comparisons joined by AND only), and any other predicate is routed to the segment store, so the language stays one. Every count is a maximum. Readable structured metadata as a json block kind is a stated suggestion with pros and cons; there is no separate opaque field.
  • API /v1/tenants, /v1/tenants/{id}/events, /v1/tenants/{id}/episodic-memory/*, one error handler, closed error-code set, no tracebacks in responses.
  • Schema management: Alembic per component with its own version table; memmachine schema upgrade (an operator's command under a database lock, serving nothing) is the only thing that runs DDL that is not tenant-specific and also provisions vector containers; serve verifies and fails on mismatch; expand/contract for rolling deployments. Tenant-specific DDL is the only DDL allowed elsewhere, and no store in this design uses it.
  • Not wired in: short-term memory, semantic memory, declarative memory and the graph stores, the retrieval agent.

Component specifications

design/components/: one file per component (tenant service with registry, jobs, reconciler and sweep; key registry; event store; segment store; vector store; EpisodicMemory; EpisodicMemoryManager; context; blocks; ingest service; filters and properties; server and settings), each with its API, storage, fencing, settings and the changes required of an existing component; identifiers are typed UUID throughout. Lifecycle transitions and job steps are serialized on the tenant row's lock, and race matrices tabulate every concurrent pair on a tenant and on the data path. Every SQL-backed component's schema is given with SQLAlchemy types mapped to PostgreSQL and SQLite, constraints and indexes. The event store is a per-tenant log of additions and deletions with commit-ordered positions that subsystems replay, which is what makes every acknowledged client write reach every subsystem at least once; the job table and the log are the two queues, and neither is a message broker. Jobs record last_run_at, last_outcome and attempts, and eligibility is computed at claim from the settings rather than stored as a next_run_at prescription. Every contract is an ABC, and every store is two of them: the store, the only place a key is named, for lifecycle and for constructing handles; and the handle (EventPartition, SegmentPartition, VectorCollection), the data surface, bound to one key at construction with no method taking a key, so a data consumer can reach neither lifecycle nor a wrong key. EpisodicMemory gains expand, the neighbourhood of a segment or event in the tenant's one total order. Scores are cosine similarity; SimilarityMetric goes. Job kinds are four (provision; delete, the unlink, one call; sweep, purging batch by batch until nothing remains, reset by the tombstone pass for late garbage; replay), all defined by the tenant service.

One naming scheme for values: settings (deployment), tenant configuration (per tenant, with options), templates, overrides, defaults, partition configuration (per key), request parameters, job arguments; in identifiers Settings and Config, Python's conventions; conf, params, args and payload are not identifiers in new code.

What must be built first

A section names what the first deployment freezes and so must precede the first tenant (the key scheme, including the physical key inside each store, and the tenant table with tombstones; the event store with positions; stored-record conventions; a registry row per vector key; the public surface; Alembic from the first migration), what must land before churn, what can follow at any time, and the narrowest first deployment.

Relation to issues

Listed in the document's "Relation to open issues": #1548, #1530, #1531, #1571, #1575, #1576, #1577, #1572, #1573, #1564, #1565, #1537, #1563, #1535, #1570, #1542, and #1574 as the tracking issue.

Open questions

Hierarchy (flat proposed); synchronous in-request processing with catch_up as repair versus a queue; event size limits; readable metadata as a json block kind or a designed field; retention as a job kind.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5

edwinyyyu added a commit to edwinyyyu/MemMachine that referenced this pull request Sep 2, 2026
…laims

purge_partition existed to serve a promise the delete path can no
longer make: prompt physical erasure is not something the store can
keep on every dialect (on SQLite any writer past the busy timeout
fails), and the inline drain that used it, first of the global queue
and then scoped to the deleted key, added a second purger, a polling
loop while an entry was held, and a slow-purge warning, all to shorten
a window that no contract requires to be short.

drop_session_partition now deletes the collection and the partition,
nulls its handles, and returns; the partition is unreachable at once
and its rows are reclaimed by the resource manager's sweeper within its
interval. The ABC keeps one purge method, the sweeper, whose contract
already says a deployment must run it. The composite queue index, the
held-entry pause, the existence read, the slow-purge warning and their
tests go with the method; the sweeper's same-tick ordering wording and
the queue-row docstring's forensic key stay.

A per-tenant reclaim step returns with the tenant lifecycle layer
(MemMachine#1579), where single-use keys make it job-like: progress, retry and
failure per tenant, which the global sweeper cannot attribute.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
@edwinyyyu
edwinyyyu force-pushed the design/tenant-lifecycle branch from 2d315ac to 7e33dc2 Compare September 2, 2026 19:07
@edwinyyyu
edwinyyyu marked this pull request as draft September 2, 2026 19:15
edwinyyyu added a commit to edwinyyyu/MemMachine that referenced this pull request Sep 2, 2026
…laims

purge_partition existed to serve a promise the delete path can no
longer make: prompt physical erasure is not something the store can
keep on every dialect (on SQLite any writer past the busy timeout
fails), and the inline drain that used it, first of the global queue
and then scoped to the deleted key, added a second purger, a polling
loop while an entry was held, and a slow-purge warning, all to shorten
a window that no contract requires to be short.

drop_session_partition now deletes the collection and the partition,
nulls its handles, and returns; the partition is unreachable at once
and its rows are reclaimed by the resource manager's sweeper within its
interval. The ABC keeps one purge method, the sweeper, whose contract
already says a deployment must run it. The composite queue index, the
held-entry pause, the existence read, the slow-purge warning and their
tests go with the method; the sweeper's same-tick ordering wording and
the queue-row docstring's forensic key stay.

A per-tenant reclaim step returns with the tenant lifecycle layer
(MemMachine#1579), where single-use keys make it job-like: progress, retry and
failure per tenant, which the global sweeper cannot attribute.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
Design proposal for tenant lifecycle management above the stores: a
tenant record with step rows and a reconciler, UUID-only store keys
minted per tenant lifetime, a single tenant handle, and the resource
contracts the segment and vector stores expose to that layer. Rebuilt on
speedkick, which carries the segment store overhaul (MemMachine#1548) the document
refers to. Tracking: MemMachine#1574.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
@edwinyyyu
edwinyyyu force-pushed the design/tenant-lifecycle branch from 7e33dc2 to 32c813f Compare September 2, 2026 20:32
@edwinyyyu
edwinyyyu changed the base branch from main to speedkick September 2, 2026 20:32
The lifecycle design cannot be fixed inside the current API,
configuration and wiring, so the document now covers the server:
tenants (name + UUID id, renamable, name in one SQL table, UUID keys in
every store), a control plane (tenant service, job table, reconciler)
separate from the data plane (subsystems serving data operations by
tenant id in the path, no routing handle), the subsystem registration
contract (schema with mutable/immutable options, provision and delete
hooks), event memory with the segment store as system of record and no
episode store, store contracts including a SQL ledger for vector
collections with a derived reclaim grace period, a declarative
configuration document with providers and tenant templates, eager
startup by constructor injection, the v1 HTTP API with one error
handler, and schema management (Alembic per component, boot modes,
container provisioning). Short-term, semantic and declarative memory are
not wired in. File renamed from tenant_lifecycle.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
@edwinyyyu edwinyyyu changed the title Design: tenant lifecycle for the server (UUID-only store keys, step table, reconciler) Design: MemMachine server redesign (tenants, event memory API, configuration, lifecycle, schema management) Sep 2, 2026
edwinyyyu and others added 12 commits September 2, 2026 16:25
… config

Every store now rejects operations on a deleted key by itself, with its
registry row in SQL: writes pin it FOR SHARE, the logical delete takes
it FOR UPDATE and so waits out in-flight writes, reads verify it. The
vector store adopts this through a SQL ledger, and dead keys stay as
tombstones swept at a bounded rate, so no clock is compared anywhere and
no key is forgotten while a record could exist. The segment store no
longer fences on behalf of the subsystem. Subsystems own their per-tenant
state; only the tenant service reads the tenant table. The reconciler is
a role a deployment runs in as many or as few processes as it needs.
An event store returns as the system of record, designed new, so derived
data can be rebuilt. Configuration is the components' own parameter
models with typed references resolved by the loader; nothing receives a
catalog. Schema is upgraded only by an operator's command and verified
at startup. The SQLite vector stores keep per-collection tables, as
their docstring's reason for avoiding vec0 partition keys stands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
The SQLite stores are not for large deployments; they are fine as long
as they work and obey the contracts. State the rule as one about cost at
scale in the requirements, the schema principle, and both places that
mention the SQLite stores' per-collection tables.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
State the rule as two halves: component schema runs only in the setup
command, which serves nothing and cannot race, and serving and
reconciler processes verify it; tenant-specific DDL is the only DDL
allowed elsewhere, avoided where it would be expensive at scale.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
…name to episodic memory

Stores fence on the caller's UUID alone: the registry row keyed by it is
the whole fence, and the segment store's incarnation goes; replacement
is the subsystem's, by minting a new key per generation and recording
it in its own per-tenant row. The fence section now defines the write
step as a FOR SHARE row lock released by the database at transaction
end, with the two settings that bound a live session holding it. The
event store is its own component, the system of record, with positions
that subsystems process by; EventMemory is renamed episodic memory, so
"event" is the caller's ingestion type. The uuid5-derived segment and
derivative ids are withdrawn; idempotency is per event, by forgetting
an event's derived rows before reprocessing. A table surveys Qdrant,
Milvus, pgvector, Pinecone, S3 Vectors, Weaviate and Chroma at the tier
that scales, for per-tenant objects, rejection, listing and reclaim.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
A full rebuild costs what an ingestion costs, so it is one, into a new
tenant. The rebuild job, its endpoint, and the per-generation keys it
needed go; the tenant id is the key in every store again, and the
subsystem's per-tenant row holds only the watermark and the applied
configuration. The event store keeps its two uses: repair of partial
processing and processing history for a subsystem enabled later.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
Chroma's own multi-tenancy write-up warns that metadata filtering slows
as users and documents grow, so its row now reads collection per tenant.
Its per-tenant object is a Collection handle: get_collection is one
round trip resolving the name to the collection's UUID (verified in
chromadb/api/fastapi.py), after which every data operation addresses
the UUID. The paragraph under the table says what that costs, that the
instance cache pays it once per open, and that storing the UUID in the
ledger removes the call. Per-collection cost and Chroma Cloud's
collection count are marked unverified.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
Every store operation takes the key. The registry row that fences it
also holds what addresses the tenant on the backend (codec
configuration, container, collection UUID), so nothing is opened or
closed per tenant, a process holds no per-tenant state, and a
configuration update takes effect on the next request. Chroma's
collection UUID is recorded in the ledger at creation and operations go
to its HTTP API by that UUID; Weaviate's wrapper is built per call.
The per-tenant instance cache, its TTL, and MemMachine#1548's partition handle go.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
Replace the "verified before an implementation" note for S3 Vectors
with the documented figures: 10,000 indexes per bucket, top-K up to
10,000 per query, DeleteVectors by key at 500 per call and no delete by
filter, filterable metadata 2 KB per vector, filters evaluated during
the search, numeric-only range comparisons.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
Adopt MemMachine#1531's ConcurrencyScope: every component computes its scope
from its params, a composition's scope is the minimum of its parts',
the deployment declares the scope it runs at, and startup refuses any
component narrower than that. The horizontal scaling requirement is
stated at cluster scope; the SQLite stores declare process or machine
and are held to every contract within it. Scope declarations are
tabulated, the file lock is named as what gives SQLite-backed stores
machine scope, and "large deployment" wording is replaced by scope.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
…as reference

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
From the registry session's review of the draft. Store creates are
strict and raise on any row under the key; idempotency is the
component's ensure, which knows the key's provenance, and a row in a
non-live state is a reused key that raises. A table states what every
operation does with a non-live key. Tombstones are kept by default;
pruning is an operator's trade gated on a clean sweep, with what it
gives up stated. The fence's cost is stated for sizing: a pooled
connection held across each remote write, one row read per query.
Containers are retired by the schema command once undeclared and
unreferenced. maintain runs without exclusion and says why that is
safe. MemMachine#1530 is recorded as agreeing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
@edwinyyyu

Copy link
Copy Markdown
Contributor Author

edwinyyyu and others added 7 commits September 3, 2026 09:54
Where a backend's tenant is a native object (a Chroma collection, a
Weaviate tenant, a SQLite table), create is two steps, so the ledger
gains a creating state that ensure resumes, and the row carries the
object's address. The Chroma row and the paragraph under the table take
the registry session's chromadb 1.5.9 findings from the MemMachine#1579 comment:
a stale UUID raises NotFoundError, so the UUID is the fence; duplicate
create is rejected but untyped, so already-exists is told by message;
concurrent creates yield one winner; metadata values are scalars. The
per-collection cost and Cloud collection counts stay marked unverified.
schema status and prune's dry run report prunable versus awaiting
tombstones.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
Same signature and reason; MemMachine#1530's names are reused and separated by
incarnations, so its create raising means exists, while here a row
under a never-reused key is a violated invariant and kept tombstones
are load-bearing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
…d-poll, constructor-derived configuration

Tombstones move to the tenant service: a deleted tenant's row stays as
the one detector of a reused id, refusing a duplicate at mint before
any store is touched, and drives a periodic re-sweep through every
component's reclaim, so no store keeps tombstones and the stores cannot
disagree about a reused key. The reconciler's lease becomes a held row
lock, and a paragraph says why locks rather than leases everywhere.
Creation, deletion and configuration updates respond 202 and are
polled or waited on; no lifecycle request fails for a job the
reconciler will retry; the get-or-create flag goes, the 409 carrying
the existing tenant. Configuration updates keep the tenant active and
reach every process through the component's per-tenant row; an option
is immutable exactly when changing it would touch existing data.
Configuration is the resources' constructor arguments: plain classes
with typed constructors, one flat resources map with unique ids and a
kind table as the only registration point, dependencies reflected from
annotations, third-party clients as factory kinds; the relation to the
earlier resource_initializer proposal is stated. Concurrency scope
levels are process, host, cluster. Both SQLite vector stores move to
shared tables, and no store creates a table per tenant. All numeric
defaults are removed; settings are named.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
…llables

A duplicate-name create is 409 tenant_exists and nothing else; a caller
wanting the existing tenant looks it up. The sqlite-vec paragraph
records that a vec0 partition key prunes the KNN to the tenant's own
chunks (two orders of magnitude on 0.1.9 with 400 tenants) and that the
per-partition cost is a chunk allocation both layouts paid identically,
with chunk_size as the knob. A kind names a callable, class or factory,
ours or third-party; Params models are optional grouping; the loader
reflects the signature, validates scalars, and instance-checks resolved
dependencies against annotations via validate_call, which is stated as
exactly what a runtime check can and cannot prove.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
System fields are first-class typed parameters (since, before,
producers) stored under a reserved memmachine_ key namespace so stores
filter them with the same machinery as user properties. User
properties are scalar, bounded, immutable, copied verbatim to derived
data; an opaque payload is proposed for unfilterable metadata. Filter
indexes are declared once per vector store in configuration and never
created dynamically; undeclared keys are filtered in the segment store.
A filter is a constructed closed-union tree, a JSON object under a
generated schema at the API and in MCP, never a string language.
Routing is per backend capability and selectivity: declared keys
during the search, undeclared keys by a selectivity probe choosing an
allowlist or bounded post-filter. Every count is a maximum; nothing is
called top k. Reference: the default branch of edwinyyyu/MemMachine.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
edwinyyyu and others added 22 commits September 4, 2026 13:12
The method transforms events into their derived representation, which
EventMemory already called encode_events; process was overloaded.
replay applies encode to added entries and forget to deleted ones.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
…budget

reclaim becomes purge throughout, matching the shipped
purge_deleted_partitions and the storage-engine sense of purging
delete-marked rows. A sweep is the run that calls purge repeatedly; a
delete job's step sweeps one tenant, the tombstone pass sweeps deleted
tenants, both within reconciler.step_duration.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
The delete job calls the component's delete once and the tenant is
deleted within seconds; a sweep job per tenant and component then
calls purge batch by batch until nothing remains or its time budget
is spent, and the tombstone pass resets sweeps so late garbage is
purged too. Four job kinds: provision, delete, sweep, replay.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
… arguments

Settings are deployment values, tenant configuration is per tenant with
options as its fields, templates and overrides and defaults are named
as such, a store records a partition configuration per key, requests
carry request parameters, a job row carries arguments, and config,
params, args and payload are not identifiers in new code. The stale
enqueue bullet in the reconciler section becomes the reset bullet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
Python's conventions: Settings for values read from the environment
and files, as pydantic_settings names them; Config for tenant and
per-key configuration, as logging.config, configparser and Pydantic's
ConfigDict do. The prose keeps the word configuration.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
The entity responsible for an event's content is its author, {id,
name}: the id a bounded string the application owns and filters by,
the name display data recorded as it was at the event and never
filtered, so no id-to-name directory is needed. It is an optional
field beside timestamp, so filtering is uniform over every event; the
Context union goes with it, and produced_for and the producer roles
are not carried over. Segments carry the author's id and name in
place of the context column.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
Identity and rendering are separate channels. source_id is a system
field beside timestamp, a bounded application-owned string, indexed and
filtered by source_ids, uniform over every event. Context stays, with
rendering as its only job: an extensible typed union, codec-encoded,
never filtered, copied to segments, whose first member is
AuthorContext with the readable name as it was at the event; a caller
that knows a source's current name passes FormatOptions.names, and
show_source_id prints the id beside the name, so no server-side
directory exists and deadnaming is the caller's to correct at render
time. produced_for and the producer roles are not carried over.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
…ptions

Every hit returns source_id and context as data; an application that
knows current names renders them itself. The server's text rendering
prints what was recorded, and FormatOptions stays dates, times, locale
and timezone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
A context is a mapping from part kind to one Pydantic part registered
under its kind, so parts compose by merging and a step reads the one
it needs by kind; no order, no nesting, no edit to a core union to add
one, which is what MemMachine#1436's CompositeContext needed. First kinds:
author (the readable name) and time_ranges (the temporal signal of
MemMachine#1436, read by scoring). No context is the empty mapping; a source
with no good name has a source_id and no author part, and names are a
part's field, so source and name need not be bijective. A part is
never filtered on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
A context is a mapping from part kind to one registered Pydantic
part, never None; the empty mapping is no context and the answer to a
source with no name. The spec lists what was wanted of Context and
answers each: registration through a kind table and an entry-point
group, composition by merging with no order or nesting, propagation to
segments and derive-time derivatives, rendering by per-kind
contribution with placement the renderer's, never filtered, codec
round-trip with unknown kinds preserved, additive evolution, API
validation against a generated schema, and what MemMachine#1436's types become.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
…elds

A session id is the stream an event belongs to; the one total order
that expansion and context windows walk is within a session, so a
tenant holding many interleaved conversations never expands a hit
into another conversation. The criterion for a system field is stated:
the server gives it semantics beyond filtering (timestamp orders,
bounds and scores; session_id bounds the order; source_id is kept for
universality), and everything else efficient is a deployment-declared
indexed property, which closes the system set without starving it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
- blocks.md: block kinds registered like context parts
  (memmachine.block_kinds), `kind` as the discriminator, a render hook,
  UnknownBlock on decode, an unhandled-kind clause for segmenter and
  deriver. The kind is a system field of the segment, never of the
  event: `block_kinds` on search and expansion, `memmachine_block_kind`
  on vector records, `block_kind` column on segments.
- Properties and filtering: state why Event and Context are reshaped
  (to prohibit dynamic index creation, which makes the deployment too
  difficult to maintain and its performance unpredictable); rule on
  channel, workspace, user, kind, importance and language against the
  criterion.
- Segment store reads take the system filters as typed parameters
  (get_segment_contexts: source_ids, block_kinds; find_segments: all).
- Event store types: session_id was missing from the Types paragraph.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
…o key

Restores handles to the segment and vector stores as stateless bindings
of a store's data operations to one key (and container): built without
I/O, fenced per operation by the store, nothing to open, close, evict
or go stale. `SegmentPartition` and `VectorCollection` are the two,
one final class per store beside the key-taking data ABC.
`EpisodicMemory` takes them as dependencies and its operations take no
key, so past construction nothing can route to another tenant; the
manager builds it per request and caches segmenters and derivers by
options instead of `EpisodicMemory` objects by configuration.
`for_container` is subsumed by `collection(key, container)`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
The two ABCs per store are the store (lifecycle, and constructing
handles; the only place a key is named) and the handle
(`EventPartition`, `SegmentPartition`, `VectorCollection`), the data
surface bound to one key at construction with no method taking a key.
The key-taking data ABC and the delegating final class are removed, so
a wrong key is unrepresentable past construction instead of checked per
call. Each backend implements both, as the current code does with
`SegmentStorePartition` and `VectorStoreCollection`, minus open, close
and stale state. Applied to the event store as well for uniformity.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MbYdqGZsuws6Z2WHYfCCR5
@edwinyyyu

edwinyyyu commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

5f7264acb8b12b36c3e7bf6078f4afb5729b6927

Four systematic problems account for most of the list. The design describes two registry models, a three-state key registry and the SQL stores' row-plus-purge-queue, in the vocabulary of the first, so several general statements are unimplementable for the event and segment stores. The reconciler's locking is stated three incompatible ways, and the design's own "no lock across remote I/O" rule is broken by the reconciler itself. The garbage story has open ends the design's own requirement forbids. And the main document was not re-swept after the handle and replay reshapes, so its store sections, wiring and API tables lag the component files.

Design-level: decisions needed

  • A1 Two registry models. The SQL stores' registry rows have no state column and their logical delete removes the row. So the fence predicate with AND live (main 1012), the "row present but not live" state for SQL stores (1134), and provision's live/creating/dropping branch with KeyReusedError (798, 1123; manager 67; tenant_service 39) cannot exist for the event and segment stores, where create raises KeyExistsError on a purge-queue entry. The create table's "no row: creates" (1141) is false while that entry stands. purge on a live key raises in the vector store and is undefined in the SQL stores.
  • A2 Tombstone prune is not computable. "Purge found nothing on its first batch" (465; tenant_service 255) is recorded in no column: success writes nothing, resets clear nothing. The pass also resets the sweeps and evaluates the predicate in one pass with no stated order. Read literally, no tombstone is ever pruned. ORDER BY swept_at puts never-swept tombstones last on PostgreSQL.
  • A3 Reconciler locking. "Every job step holds the tenant row's lock" (535) versus "replay holds only its job row" (701; tenant_service 218), with sweep unlisted, and matrix row 296 calls replay/delete "serialized" by a lock replay never takes. A LIMIT n claim in one transaction cannot coexist with "lock held for the step" and "one connection per executing job" (507, 559). Locking tenant rows in claim order across tenants can deadlock two reconcilers. The requirement that no lock spans remote I/O (48, 284) is violated by the hook running under th3, 539). On SQLite, BEGIN IMMEDIATE for the wholestep (517) holds the file lock past every store's busy timeout, and the shipped code documents BEGIN IMMEDIATE as impossible on a shared engine while the sketch shares one engine; t_store 138 and tenant_service 221.
  • A4 Watermark before vectors. The manager advances the watermark in the segment-write transaction (94), but vectors are writtafter segments (episodic_memory 132). A crash betwch's derivatives permanently, since there is norebuild. A5 Replay job gaps. Nothing in the design inserts vice 128 says provision, but hooks cannot write thejob table). The event store registers as a component with an abstract replay hook and no semantics, and every ingest resets job. purge_interval after any more outcome delays . Reset clears neither the last outcome nor attempts, and backoff to the power of attempts is uncapped and never reset on success, so a recovered job waits out its whole failure history. Reset as update or upsert decides whetheresurrected row whose foreign key blocks the prune.Replay after delete removed the per-tenant row is undefined; provision's "insert or update" of that row can reset the watermark to zero. "Each batch is processed by its own procects the single consumer.
  • A6 Provision's version. "The section at the job's version" (520; tenant_service 232) cannot be retrieved: only the current
    configuration and a counter exist. A provision stes itself done and then trips the unguarded "lastprovision done, set active" transition (526).
  • A7 Compaction and bootstrap. Nothing calls compactever. A subsystem enabled on an existing tenant getsno provision jobs and its routes answer 409 forever. The switch between bootstrapping from events and replaying the log is
    unstated. "Contiguous" positions are false after cad_log resolves an added entry by uuid, so adeleted-and-re-ingested id makes an old entry carry new content.
  • A8 Removed component. Delete and sweep jobs are pe rows under a component dropped from the deploymentare never purged; only provider ids are startup-guarded.
  • A9 Retention premise. "A stale write is always issits" (396) is wrong: the read precedes, the issue can follow. The bound is read-to-issue delay plus timeout.
  • A10 include_events. Manager 42 says SearchDefaults including it, checked at import; query has no suchparameter, and no object holds an event store to serve it.
  • A11 encode(format_options). A per-call value that which rule 5 makes structural; it is in no tenantsection and no API body.
  • A12 Containers and types. The vector store requireh dimensions; the settings example declares none andputs dimensions on the embedder. A datetime-typed indexed property has no sqlite-vec representation; sqlite-vec's epoch seconds
    break the half-open range boundary. vector_store_p
  • A13 Expansion and the order. session_ids on expand contradicts "stays in the anchor's session" and is absent from the API body.
    unit=events has no store support. Cursors are promnull session id under tuple comparison gives theungrouped stream empty neighbourhoods. Ties break on a random uuid rather than the commit-ordered position. "The index is per
    session so a window never leaves it" needs an equare lacks. The segment's timezone offset has no source column in the event row, so it cannot be replayed.
  • A14 Search semantics. Allowlist path mechanics, ovker, min_score before or after reranking, hitordering, seed marking, cross-hit duplicates, the extent of include_events, and whether the user filter bounds context windows
    are all unstated.
  • A15 Schema management. The "at head" startup check defeats the expand-and-contract rollout it sits beside. A deployment adding an
    index online is non-tenant DDL outside the setup sURRENTLY cannot run under the advisory transactionlock. Autogenerate against partial metadata drops unknown tables unless filtered; chain order within one database is unstated.
  • A16 Ingest identity and bounds. Optional id with pretry without an id duplicates. No way to filter byevent id or select the ungrouped stream. Session and source values "bounded by the property naming contract" means 32 bytes and
    the key charset. The naming bound, the tenant names have unstated or duplicated enforcement points.
  • A17 Chroma. The per-tenant collection name is unspecified and the address is written only at live, so a crash between create and
    live loses the UUID and the resume and no-row purgsolve by.
  • A18 Undefined types and hooks. MemorySubsystem, StoredEvent, AddResult, DeleteResult, Cursor, Page, Expansion, the request and
    response models, Tenant, TenantState; two differentResult.position when every id is skipped;list_events order and cursor; no hook reports the applied version or the watermark that Tenant, replay_position and
    wait_processed need; validate(section) is not in t
  • A19 Settings and states. ServerSettings is both the root model and s.server; its field list omits ingest, filter and context
    bounds; request_timeout exists at two levels. PATCning tenant are undefined. Which vector backends areimplemented is never said.
  • A20 Migration paragraph. It contradicts the clean-s the current code: rows are keyed by incarnationtoday, vector names are content-addressed, and the session key is hashed only when it fails validation.

Mechanical: contradictions and stale text

  • B1 Principle 3 lists the job kinds as provision, delete, purge, replay (269); elsewhere the four are provision, delete, sweep,
    replay.
  • B2 Main 594 still has the subsystem exposing encode(tenant_id, events) to the ingest service, and 600 still has a cache keyed by
    configuration with "no per-tenant object".
  • B3 The ingest service is built before the tenant service (313), holds "the list of subsystems" (333), and the sketch passes them
    (1426); ingest_service.md takes the tenant service
  • B4 The sketch omits the manager's engine and metrics factory, the tenant service's settings, and the engine for SQL-backed vector
    stores; two spellings of the segment store class; a service they are passed to.
  • B5 Inline create runs only with ?wait= in the main document and unconditionally in tenant_service.md.
  • B6 Chroma "has no $ne" (970) versus the table; "evership" versus sqlite-vec; Pinecone and Chromamissing from the filters-during-search list.
  • B7 The main document's vector store section is sti_ids; "key-taking operations behind a statelesshandle" (1740); create_partition(key) without its config; the event-store operation list omits read_events_after and returns a
    tuple.
  • B8 What the delete request does to replay rows is stated three ways, twice with the stale term "component-defined job".
  • B9 Delete's ?wait= is defined "as for create", whirequests take ?wait=, not three; timeout status foringest is unstated.
  • B10 The search body lacks min_score; the expand bosegments lack the uuid expand needs; four spellingsof the event id; the tenant section omits include_events.
  • B11 KeyRegistry is a Protocol in its code block an column tables disagree; the main document's methodlist is incomplete.
  • B12 The README error list lacks five raised errors InvalidContextError, InvalidBlockError,UnsupportedFilterError, so a caller's bad block is a 500; no code for KeyReusedError or AttemptsExhaustedError; matrix 298 gives
    409 while the tombstone row exists.
  • B13 Progress "returned by every bounded step" versus purge_deleted_partitions returning a bool.
  • B14 Smaller items, listed in the file: "Four rulesrule 5" naming two different lists;context.get(Author); session id missing from the Event vocabulary; "both derivers" where one exists; "tombstone sweep" beside "tombstone pass"; the filter example's kind properization"; two typos; two code references off by a few lines.
  • B15 The PR body's open questions still name catch_ng versus a queue.

@edwinyyyu

Copy link
Copy Markdown
Contributor Author

Reducing scope.

@edwinyyyu edwinyyyu closed this Sep 5, 2026
@edwinyyyu edwinyyyu reopened this Sep 5, 2026
@edwinyyyu

Copy link
Copy Markdown
Contributor Author

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.

1 participant