Problem
When you need this. An assistant should remember a user’s preferences after a restart without returning them to another user. Store the notes on disk and apply the user and task filters on every read.
The isolated contexts build kept each user’s, task’s and agent’s items apart. It kept them in a Python list. When the process stops, the list is gone: the next session starts with no memory of the user at all.
An assistant that remembers across sessions needs storage that outlives the process. The moment memories go to disk, two old risks come back in a new place. A query that forgets the user filter returns another user’s memories. A memory text that is pasted into an SQL string can change the query.
This build is the first part of a memory service. MemoryStore writes memories to one SQLite file and reads them back in a later process. Every read goes through a single query that filters by user and task. Memories are text with a source, at one of two levels: the user (every task) or the current task.
Success check: after the store is closed and reopened, Alice’s user-level memories are recalled in a new task, her task-level memory is not, and Bob never receives any of her memories.
You need: Python 3.11 or later, a terminal and basic SQL. The sqlite3 module is part of the standard library; nothing is downloaded.
Concepts
A memory is a stored fact with an owner and a source
A memory here is one short text the application decided to keep, such as “Alice is vegetarian.” It has an owner (a user), a level, a source that says where it came from, and a creation time. The source matters later: when a memory turns out to be wrong, it tells you which conversation turn produced it.
The application decides what to remember. This build does not extract memories from a conversation; the scenario supplies them. Build memory provenance and deletion covers correcting a stored memory.
Two levels of scope
| Level | Visible to | Example in the fixture |
|---|---|---|
user |
The same user, in every task | Alice is vegetarian. |
task |
The same user, in the same task | Book the Lisbon hotel near the river. |
A scope is the (user, task) pair of the request. The isolated contexts build also had app and agent scopes; this store keeps only the two a memory needs. A user-level memory follows Alice into a new task. A task-level memory stays where it was made.
One read path
Every read uses the same SQL:
SELECT id, task_id, text, source, created_at FROM memories
WHERE user_id = ? AND (task_id IS NULL OR task_id = ?) ORDER BY id
task_id IS NULL selects user-level memories. task_id = ? selects the current task’s memories. Both sit under user_id = ?, so no branch of the condition can return another user’s row. Having one read path means one place to review and one place to test.
The ? marks are placeholders. The sqlite3 module sends the values separately from the SQL text, so a memory that contains a quote or a semicolon stays data. The Python sqlite3 documentation says to always use placeholders instead of string formatting to avoid SQL injection.
A schema version guards the file
A database file outlives the code that wrote it. If a later version changes the table, old code reading the new file could misread it. SQLite stores an integer, user_version, in the file header for applications to use; SQLite itself does not use it. This store sets it to 1 when it creates a file and refuses any other value.
Architecture
The store is one Python object that holds one SQLite connection to one file. There is no server; the application imports the module. The demo writes 5 memories, closes the store, and opens a new store on the same file, as a restarted process would.
The saved result contains 4 recorded queries. The first two ask the same question in different tasks: in trip-planning, Alice gets 2 memories, the hotel and her diet; in conference-trip, she gets 1, because the hotel memory belongs to the other task. Bob asks the same question and receives only his own onboarding hotel. The demo also keeps the same writes in a Python list, as the isolated contexts build did; after the restart that list holds 0 memories.
Working code
Get the files
Download the complete lab, extract it and open a terminal in build-a-memory-service/. It includes the source, fixtures, recorded results and tests listed in the README.
python3.11 -m venv .venv
source .venv/bin/activateThe lab uses the Python standard library; there are no packages to install.
The store
"""A persistent memory service with explicit user and task scope.
Standard library only: sqlite3 for storage. Every query that reads or writes
memories carries the user's ID; task memories also carry the task's ID. The
caller authenticates the user before it builds a Scope.
"""
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
import re
import sqlite3
SCHEMA_VERSION = 1
SCHEMA = """
CREATE TABLE memories (
id INTEGER PRIMARY KEY,
user_id TEXT NOT NULL,
task_id TEXT,
text TEXT NOT NULL CHECK (length(trim(text)) > 0),
source TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE INDEX memories_by_scope ON memories (user_id, task_id);
"""
IDENTIFIER = re.compile(r'[a-z0-9][a-z0-9-]{0,63}')
MAX_TEXT = 2_000
WORD = re.compile(r'[a-z0-9]+')
STOPWORDS = frozenset('a an and are as at be by did do does for from how i in is it my of on or that the this '
'to was we what when where which who will with you your'.split())
class MemoryInputError(ValueError):
"""Invalid scope, memory text or source; nothing was written."""
class SchemaError(RuntimeError):
"""The database file has a schema this code does not understand."""
def _identifier(value, where):
if not isinstance(value, str) or not IDENTIFIER.fullmatch(value):
raise MemoryInputError(f'{where} must match {IDENTIFIER.pattern}')
return value
@dataclass(frozen=True)
class Scope:
"""Who is asking: one authenticated user, working on one task."""
user: str
task: str
def __post_init__(self):
_identifier(self.user, 'scope.user')
_identifier(self.task, 'scope.task')
def fixed_clock(start: str = '2026-10-20T09:00:00+00:00', step_seconds: int = 60) -> Callable[[], str]:
"""A deterministic clock for tests and demos: each call is one step later."""
moment = datetime.fromisoformat(start)
state = {'next': moment}
def now() -> str:
value = state['next']
state['next'] = value + timedelta(seconds=step_seconds)
return value.isoformat()
return now
def system_clock() -> str:
return datetime.now(timezone.utc).isoformat(timespec='seconds')
def words(text: str) -> set[str]:
return set(WORD.findall(text.lower())) - STOPWORDS
class MemoryStore:
def __init__(self, path: str, clock: Callable[[], str] = system_clock):
self.clock = clock
self.db = sqlite3.connect(path)
self.db.execute('PRAGMA foreign_keys = ON')
version = self.db.execute('PRAGMA user_version').fetchone()[0]
tables = self.db.execute("SELECT count(*) FROM sqlite_master WHERE type = 'table'").fetchone()[0]
if version == 0 and tables == 0:
# One explicit transaction: executescript() would commit on its own,
# and the sqlite3 module opens no transaction before CREATE TABLE.
try:
self.db.execute('BEGIN')
for statement in filter(str.strip, SCHEMA.split(';')):
self.db.execute(statement)
self.db.execute(f'PRAGMA user_version = {SCHEMA_VERSION}')
self.db.commit()
except BaseException:
self.db.rollback()
self.db.close()
raise
elif version != SCHEMA_VERSION:
self.db.close()
raise SchemaError(f'database schema version {version}; this code reads version {SCHEMA_VERSION}')
def close(self):
self.db.close()
def remember(self, scope: Scope, level: str, text: str, source: str) -> int:
"""Store one memory at user level (every task) or task level (this task only)."""
if level not in ('user', 'task'):
raise MemoryInputError('level must be user or task')
if not isinstance(text, str) or not text.strip() or len(text) > MAX_TEXT:
raise MemoryInputError(f'memory text must be nonblank and at most {MAX_TEXT} characters')
if not isinstance(source, str) or not source.strip():
raise MemoryInputError('every memory needs a source')
task = scope.task if level == 'task' else None
with self.db:
cursor = self.db.execute(
'INSERT INTO memories (user_id, task_id, text, source, created_at) VALUES (?, ?, ?, ?, ?)',
(scope.user, task, text, source, self.clock()))
return cursor.lastrowid
def _visible(self, scope: Scope) -> list[tuple]:
# The only read path: this user's user-level memories and this task's memories.
return self.db.execute(
'SELECT id, task_id, text, source, created_at FROM memories '
'WHERE user_id = ? AND (task_id IS NULL OR task_id = ?) ORDER BY id',
(scope.user, scope.task)).fetchall()
def recall(self, scope: Scope, query: str, limit: int = 3) -> list[dict]:
"""Return up to `limit` visible memories that share words with the query."""
if type(limit) is not int or not 1 <= limit <= 20:
raise MemoryInputError('limit must be an integer from 1 to 20')
wanted = words(query)
scored = []
for memory_id, task, text, source, created in self._visible(scope):
score = len(wanted & words(text))
if score:
scored.append({'id': memory_id, 'level': 'task' if task else 'user', 'text': text,
'source': source, 'created_at': created, 'score': score})
# More shared words first; on a tie, the newer memory first.
scored.sort(key=lambda m: (-m['score'], -m['id']))
return scored[:limit]
def count(self, scope: Scope) -> int:
return len(self._visible(scope))The constructor reads PRAGMA user_version and counts the tables. An empty file gets the schema and version 1 in one explicit transaction. The explicit BEGIN matters: in its default mode the sqlite3 module opens a transaction only before INSERT, UPDATE, DELETE and REPLACE, and executescript() commits on its own, so without it a failure halfway could leave tables with no version. A file at version 1 is used. Nonempty version-0 files and unsupported version numbers raise SchemaError and close the connection. The constructor trusts a version-1 stamp; it does not validate that every expected table and column exists. A caller importing a database from elsewhere needs that additional schema check.
remember validates before it writes. The level must be user or task, the text must be nonblank and at most 2,000 characters, and every memory needs a source. The insert runs inside with self.db:, which commits when the block succeeds and rolls back if it fails.
recall reads the scoped rows, counts the words each memory shares with the query, and drops memories with no shared word. It sorts by shared words, then by ID, newest first, so a later memory wins a tie. Word matching is deliberately simple: like and likes are different words, and synonyms do not match. Better retrieval is a separate problem from storage and scope; Build working, semantic, and episodic memory retrieval takes it up for this store. Exact vector search ranks by meaning instead of shared words, and a vector store applies a filter like this user and task condition before it ranks.
The clock is a parameter. Tests and the demo pass fixed_clock(), which advances one minute per write, so recorded timestamps are repeatable.
Run the scenario
{
"sessions": [
{
"writes": [
{"as": ["alice", "trip-planning"], "level": "user", "text": "Alice is vegetarian.", "source": "trip-planning/turn-3"},
{"as": ["alice", "trip-planning"], "level": "task", "text": "Book the Lisbon hotel near the river.", "source": "trip-planning/turn-5"},
{"as": ["alice", "trip-planning"], "level": "user", "text": "Alice prefers aisle seats on flights.", "source": "trip-planning/turn-6"},
{"as": ["bob", "onboarding"], "level": "user", "text": "Bob uses a screen reader, so describe images in text.", "source": "onboarding/turn-2"},
{"as": ["bob", "onboarding"], "level": "task", "text": "Bob's onboarding hotel is booked near the office.", "source": "onboarding/turn-4"}
]
},
{
"queries": [
{"as": ["alice", "trip-planning"], "query": "Which hotel should I book, and is my meal vegetarian?"},
{"as": ["alice", "conference-trip"], "query": "Which hotel should I book, and is my meal vegetarian?"},
{"as": ["alice", "conference-trip"], "query": "Which seats do I prefer on flights?"},
{"as": ["bob", "onboarding"], "query": "Which hotel should I book, and is my meal vegetarian?"}
]
}
]
}The first session writes 5 memories for two users. The second session opens the file again and runs 4 queries.
"""Write memories, close the store, reopen it from disk, then recall.
The baseline keeps the same memories in a Python list. The restart is simulated
in one process: the list is emptied where a new process would start empty.
"""
import json
from pathlib import Path
import tempfile
from memory import MemoryStore, Scope, fixed_clock, words
def run(scenario: dict) -> dict:
first, second = scenario['sessions']
with tempfile.TemporaryDirectory() as folder:
path = str(Path(folder) / 'memory.sqlite3')
store = MemoryStore(path, clock=fixed_clock())
baseline = [] # the in-process alternative: lost when the process exits
for write in first['writes']:
scope = Scope(*write['as'])
store.remember(scope, write['level'], write['text'], write['source'])
baseline.append(write)
store.close()
baseline = [] # process restart
store = MemoryStore(path, clock=fixed_clock()) # a new process opens the same file
results = []
for query in second['queries']:
scope = Scope(*query['as'])
recalled = store.recall(scope, query['query'])
results.append({
'scope': '/'.join(query['as']),
'query': query['query'],
'query_words': sorted(words(query['query'])),
'visible_memories': store.count(scope),
'recalled': recalled,
'baseline_recalled_after_restart': len(baseline),
})
store.close()
return {'stored': len(first['writes']), 'queries': results}
if __name__ == '__main__':
scenario = json.loads(Path(__file__).with_name('scenario.json').read_text())
print(json.dumps(run(scenario), indent=2))From the directory with the files, run:
python3 demo.pyThe demo creates the database in a temporary directory and removes it at the end, so repeated runs start clean. The recorded result for Alice’s question in the new task:
{
"scope": "alice/conference-trip",
"query": "Which hotel should I book, and is my meal vegetarian?",
"query_words": [
"book",
"hotel",
"meal",
"should",
"vegetarian"
],
"visible_memories": 2,
"recalled": [
{
"id": 1,
"level": "user",
"text": "Alice is vegetarian.",
"source": "trip-planning/turn-3",
"created_at": "2026-10-20T09:00:00+00:00",
"score": 1
}
],
"baseline_recalled_after_restart": 0
}Inspect the complete recorded output
{
"stored": 5,
"queries": [
{
"scope": "alice/trip-planning",
"query": "Which hotel should I book, and is my meal vegetarian?",
"query_words": [
"book",
"hotel",
"meal",
"should",
"vegetarian"
],
"visible_memories": 3,
"recalled": [
{
"id": 2,
"level": "task",
"text": "Book the Lisbon hotel near the river.",
"source": "trip-planning/turn-5",
"created_at": "2026-10-20T09:01:00+00:00",
"score": 2
},
{
"id": 1,
"level": "user",
"text": "Alice is vegetarian.",
"source": "trip-planning/turn-3",
"created_at": "2026-10-20T09:00:00+00:00",
"score": 1
}
],
"baseline_recalled_after_restart": 0
},
{
"scope": "alice/conference-trip",
"query": "Which hotel should I book, and is my meal vegetarian?",
"query_words": [
"book",
"hotel",
"meal",
"should",
"vegetarian"
],
"visible_memories": 2,
"recalled": [
{
"id": 1,
"level": "user",
"text": "Alice is vegetarian.",
"source": "trip-planning/turn-3",
"created_at": "2026-10-20T09:00:00+00:00",
"score": 1
}
],
"baseline_recalled_after_restart": 0
},
{
"scope": "alice/conference-trip",
"query": "Which seats do I prefer on flights?",
"query_words": [
"flights",
"prefer",
"seats"
],
"visible_memories": 2,
"recalled": [
{
"id": 3,
"level": "user",
"text": "Alice prefers aisle seats on flights.",
"source": "trip-planning/turn-6",
"created_at": "2026-10-20T09:02:00+00:00",
"score": 2
}
],
"baseline_recalled_after_restart": 0
},
{
"scope": "bob/onboarding",
"query": "Which hotel should I book, and is my meal vegetarian?",
"query_words": [
"book",
"hotel",
"meal",
"should",
"vegetarian"
],
"visible_memories": 2,
"recalled": [
{
"id": 5,
"level": "task",
"text": "Bob's onboarding hotel is booked near the office.",
"source": "onboarding/turn-4",
"created_at": "2026-10-20T09:04:00+00:00",
"score": 1
}
],
"baseline_recalled_after_restart": 0
}
]
}Check your understanding
Alice asks “Which seats do I prefer on flights?” in conference-trip. Before you look at the recording, decide which memory she gets and why the hotel memory is not even a candidate. Then compare your answer with the third query in memory-result.json.
Read the explanation
Scope is part of every read. A task memory answers only inside its task, so a new task starts with the user's facts and nothing else.
Failure cases
Run the tests:
python3 -m unittest -v test_memory.pyThe suite has 11 tests. The first runs demo.py and compares its output with the recording. The others cover these cases:
| Case | Expected behavior |
|---|---|
| The process restarts | A new store on the same file recalls the memory. |
| Another user’s memory matches the question | Never returned; another user’s scope counts zero memories. |
| A task-level memory in a different task | Not visible; user-level memories still are. |
| A query that forgets the user filter | Every executed SQL statement that reads memories contains the user filter. |
| Memory text that looks like SQL | Stored and recalled as text; the table is unchanged. |
| Creating the schema fails partway | Roll back; the file has no tables and version 0, and a later open succeeds. |
| A database with another schema version | Refuse to open it. |
| A nonempty file stamped version 0 | Refuse to open it; a version-1 stamp is trusted without a full schema check. |
| Ties between equally relevant memories | Newer memory first; memories with no shared word are not returned. |
| Invalid level, blank or oversized text, missing source or invalid scope | Reject before writing. |
The user-filter test uses set_trace_callback, which the sqlite3 module calls for each SQL statement the SQLite backend actually executes. The test checks every statement that touches memories, so a new query added later without the filter makes it fail.
To check that the tests catch real defects, we removed parts of the store one at a time. Without the user filter, the cross-user test, the trace test and the fixture test fail. Without the task filter, the task test and the fixture test fail. Without the version check, both refusal tests fail. Without the explicit BEGIN, the partial-creation test fails. With the insert built by string formatting, the SQL-text test fails.
Inspect the failure tests
import json
from pathlib import Path
import sqlite3
import subprocess
import sys
import tempfile
import unittest
import memory
from memory import MemoryInputError, MemoryStore, SchemaError, Scope, fixed_clock
HERE = Path(__file__).parent
ALICE = Scope('alice', 'trip-planning')
BOB = Scope('bob', 'onboarding')
class MemoryServiceTest(unittest.TestCase):
def setUp(self):
folder = tempfile.TemporaryDirectory()
self.addCleanup(folder.cleanup)
self.path = str(Path(folder.name) / 'memory.sqlite3')
def open(self):
store = MemoryStore(self.path, clock=fixed_clock())
self.addCleanup(store.close)
return store
def test_fixture_matches_recorded_result(self):
output = subprocess.run([sys.executable, 'demo.py'], cwd=HERE, check=True,
capture_output=True, text=True).stdout
self.assertEqual(json.loads(output), json.loads((HERE / 'memory-result.json').read_text()))
def test_memories_survive_a_restart(self):
store = MemoryStore(self.path, clock=fixed_clock())
store.remember(ALICE, 'user', 'Alice is vegetarian.', 'turn-3')
store.close()
reopened = self.open()
self.assertEqual([m['text'] for m in reopened.recall(ALICE, 'Is my meal vegetarian?')],
['Alice is vegetarian.'])
def test_another_users_memories_are_never_recalled(self):
store = self.open()
store.remember(ALICE, 'user', 'Book the Lisbon hotel near the river.', 'turn-5')
self.assertEqual(store.recall(Scope('bob', 'trip-planning'), 'Which Lisbon hotel?'), [])
self.assertEqual(store.count(BOB), 0)
def test_task_memories_stay_in_their_task(self):
store = self.open()
store.remember(ALICE, 'task', 'Book the Lisbon hotel near the river.', 'turn-5')
store.remember(ALICE, 'user', 'Alice prefers aisle seats on flights.', 'turn-6')
other_task = Scope('alice', 'conference-trip')
self.assertEqual(store.recall(other_task, 'Which Lisbon hotel?'), [])
self.assertEqual(len(store.recall(other_task, 'Which seats on flights?')), 1)
def test_every_memory_query_filters_by_user(self):
statements = []
store = self.open()
store.db.set_trace_callback(statements.append)
store.remember(ALICE, 'user', 'Alice is vegetarian.', 'turn-3')
store.recall(ALICE, 'vegetarian')
store.count(ALICE)
touching = [s for s in statements if 'memories' in s]
self.assertTrue(touching)
for statement in touching:
self.assertTrue(statement.startswith('INSERT') or 'WHERE user_id = ' in statement, statement)
def test_text_is_stored_as_data_not_sql(self):
store = self.open()
text = "Robert'); DROP TABLE memories;--"
store.remember(ALICE, 'user', text, 'turn-9')
self.assertEqual(store.recall(ALICE, 'Robert drop table')[0]['text'], text)
self.assertEqual(store.count(ALICE), 1)
def test_unknown_schema_version_is_refused(self):
self.open().close()
db = sqlite3.connect(self.path)
db.execute('PRAGMA user_version = 2')
db.close()
with self.assertRaisesRegex(SchemaError, 'version 2'):
MemoryStore(self.path)
def test_failed_creation_leaves_an_empty_file(self):
original = memory.SCHEMA
memory.SCHEMA = original + ';\nTHIS IS NOT SQL'
try:
with self.assertRaises(sqlite3.OperationalError):
MemoryStore(self.path)
finally:
memory.SCHEMA = original
db = sqlite3.connect(self.path)
self.assertEqual(db.execute("SELECT count(*) FROM sqlite_master WHERE type = 'table'").fetchone()[0], 0)
self.assertEqual(db.execute('PRAGMA user_version').fetchone()[0], 0)
db.close()
self.open().remember(ALICE, 'user', 'Alice is vegetarian.', 'turn-3')
def test_foreign_database_is_refused(self):
db = sqlite3.connect(self.path)
db.execute('CREATE TABLE notes (body TEXT)')
db.close()
with self.assertRaises(SchemaError):
MemoryStore(self.path)
def test_recall_orders_by_shared_words_then_newest(self):
store = self.open()
first = store.remember(ALICE, 'user', 'Alice likes window seats.', 'turn-1')
second = store.remember(ALICE, 'user', 'Alice likes aisle seats now.', 'turn-2')
store.remember(ALICE, 'user', 'Alice is vegetarian.', 'turn-3')
recalled = store.recall(ALICE, 'Which seats does Alice like?', limit=5)
self.assertEqual([m['id'] for m in recalled], [second, first, 3])
self.assertEqual([m['score'] for m in recalled], [2, 2, 1])
self.assertEqual(store.recall(ALICE, 'weather forecast'), [])
def test_invalid_input_is_rejected_before_writing(self):
store = self.open()
for args in ((ALICE, 'global', 'text', 'turn-1'), (ALICE, 'user', ' ', 'turn-1'),
(ALICE, 'user', 'x' * 2001, 'turn-1'), (ALICE, 'user', 'text', ' ')):
with self.subTest(args=args[1:3]), self.assertRaises(MemoryInputError):
store.remember(*args)
for user, task in (('', 'trip'), ('Alice', 'trip'), ('alice', None), ('alice', 'trip planning')):
with self.subTest(user=user, task=task), self.assertRaises(MemoryInputError):
Scope(user, task)
with self.assertRaises(MemoryInputError):
store.recall(ALICE, 'hotel', limit=0)
self.assertEqual(store.count(ALICE), 0)
if __name__ == '__main__':
unittest.main()Deployment
Where the file lives
Put the database file on a persistent volume that only the application process can read. It contains what users told the assistant, so treat it as user data: encrypt the volume, include it in backups, and restrict who can copy it. A backup also keeps memories that you later delete, so your retention policy has to cover backups too.
Create one MemoryStore per process and close it on shutdown. A sqlite3 connection by default raises an error if another thread uses it, so give each thread its own store or serialize access. This build is not a multi-process service; several processes writing the same file need a database designed for that, or one process that owns the file.
Changing the schema
When a later version changes the table, write a migration that moves a file from version 1 to version 2 inside one transaction, and test it on a copy of real data. The refusal in the constructor makes sure that old code never reads a new file by mistake. Keep the previous code and a backup of the file together for rollback.
Log memory IDs, levels and counts, not memory text. No server deployment was made for this build. The tests pass on Python 3.11 and 3.15.
Scaling
Where the work grows
recall reads every visible memory for the scope and scores each one in Python. The index on (user_id, task_id) keeps the scan to one user’s rows, but a user with many memories makes each recall slower in proportion. This build records no timing; measure recall time with your own memory counts before you rely on it.
Two changes reduce the scan: keep only the memories worth recalling (Build memory provenance and deletion adds correction, expiry and deletion), and move ranking into an index built for search. Either needs its own tests for the scope filter, because the filter must stay in every new query.
Key takeaways
- Persistent memory needs storage that outlives the process; a list in memory is lost on restart.
- Put the user and task filter in one read query, and test every executed statement against it.
- Bind values with placeholders so memory text stays data.
- Version the file and refuse versions the code does not understand.
Best practices
- Store a source with every memory, so a wrong memory can be traced to the turn that produced it.
- Inject the clock, so tests and recordings are repeatable.
- Treat the database file and its backups as user data.
- Log IDs and counts, never memory text.
References
- Python
sqlite3: placeholders against SQL injection, the connection as a context manager that commits or rolls back, which statements open a transaction implicitly,executescript()committing first,set_trace_callback, andcheck_same_thread. Checked October 20, 2026 (Python 3.15 documentation). - SQLite PRAGMA statements: user_version: an integer in the database header for applications; SQLite does not use it. Checked October 20, 2026.
- Build isolated contexts: the user and task scope rules this store keeps on disk.
The implementation, scenario, recorded output and tests belong to this build. The documentation describes the library and file format; it does not validate this store.