01 · Three concrete failures: the wrong person, the wrong project, and deleted content
Consider the same report workflow: a user requests Chinese summaries in project-A and English summaries in project-B. Both preferences are correct within their respective scopes. Retrieval using only user_id may apply project-A’s Chinese preference to project-B. Add identical keywords from another tenant, or recovery that reuses an old draft after a preference was deleted, and retrieval similarity is only part of the problem.
| Failure | Direct cause | Required constraint |
|---|---|---|
| Project A’s preference affects project B | Memory lacks project scope | Trusted tenant, user, and project jointly constrain candidates |
| An updated preference still returns an old value | Vectors are appended without version validity rules | At most one active version per scope and key |
| Recovery uses a preference after deletion | A checkpoint retains a derived copy | Memory revisions invalidate task snapshots; deletion covers derived copies |
| A model guess becomes a user fact | Sources and confirmation states are conflated | Handle candidates, confirmed facts, and revocations separately; never promote guesses automatically |
LangChain’s long-term memory documentation organizes records by namespace and key. A namespace organizes data; authorization remains the responsibility of the application and database. This article examines scope, versions, invalidation, and deletion in relational tables and recoverable tasks.
LangChain · Long-term memory: Reference for namespaces, keys, and long-term storage organization. (Checked: 2026-09-25)
Design starting point: First prevent ineligible content from entering the context, then improve relevance. Retrieval quality, correct authorization, and complete deletion are separate goals.
02 · Classify four kinds of data before choosing storage
| Data type | Example | This lab / production guidance |
|---|---|---|
| Task state | Completed collect step, current draft, operation receipt | checkpoints; tied to the task lifecycle |
| Stable preferences / project facts | Chinese output, PostgreSQL as the project database | memories; explicit key, source, and version |
| Retrieved knowledge | Official documentation, API contracts, project design | spec.docs in this lab; document versions and access metadata in production |
| Experience and retrospective lessons | A tool frequently times out for certain requests | Not implemented here; retain conditions, evidence, and counterexamples for review |
Facts that require precise updates, such as response_language or database_engine, are usually easier to interpret as structured key-value records. Questions about previous fixes need experience retrieval; API questions need document retrieval. These may share retrieval infrastructure while retaining distinct write and deletion policies.
The lab intentionally has no vector database. Its small set of typed facts allows exact filtering to demonstrate the core rules. Adding a vector index still requires links to master records, scopes, versions, validity states, and source documents. Without those links, retrieval merely finds stale facts faster.
03 · Schema and scope: avoid identity collisions from string concatenation
memory.py · Actual schema and unique index
def connect(path):
db = sqlite3.connect(path, isolation_level=None, timeout=5)
db.row_factory = sqlite3.Row
db.execute('PRAGMA foreign_keys=ON')
db.executescript('''
CREATE TABLE IF NOT EXISTS memory_revisions(scope TEXT PRIMARY KEY, rev INTEGER NOT NULL);
CREATE TABLE IF NOT EXISTS memories(
scope TEXT NOT NULL, key TEXT NOT NULL, version INTEGER NOT NULL,
value TEXT NOT NULL, source TEXT NOT NULL, confirmed INTEGER NOT NULL,
expires REAL NOT NULL, active INTEGER NOT NULL,
PRIMARY KEY(scope,key,version));
CREATE UNIQUE INDEX IF NOT EXISTS one_active_memory
ON memories(scope,key) WHERE active=1;
''')
return db
Download the complete memory.py
memory.py · Canonical scope representation
def scope_key(tenant, user, project):
return json.dumps([tenant, user, project], ensure_ascii=False, separators=(',', ':'))
Download the complete memory.py
The lab derives scope from canonical JSON for [tenant,user,project]. Simple concatenation as tenant + ":" + user + ":" + project creates ambiguous boundaries when fields contain the delimiter. In production, use separate columns, composite indexes, and row-level access policies. JSON scope is a readable identity representation for this local experiment.
| Field | Purpose | Constraint |
|---|---|---|
| scope / key / version | Identify a historical fact version | Composite primary key; monotonically increasing version |
| source | Trace a user message or document | Reject writes without a source |
| confirmed | Separate confirmed facts from candidates | Recall only confirmed=1 |
| expires | Define validity duration | Exclude expired facts; similarity cannot reactivate them |
| active | Mark the currently valid version | Partial unique index permits at most one per scope/key |
| memory_revisions.rev | Track revisions across the scope | Increment on updates or revocations to detect stale tasks |
The CLI uses a fixed demonstration identity. Its isolation tests establish that filters respect supplied parameters. A real service must derive tenant, user, and project from a trusted session and project authorization; client-supplied user_id cannot authorize retrieval for someone else. Namespaces and SQL where clauses do not replace authentication.
04 · One transaction: deactivate the old value, write the new value, advance the revision
memory.py · Version replacement
def put(db, scope, key, value, source, expires, confirmed=True):
if not source or not key:
raise ValueError('source and key required')
with transaction(db):
version = db.execute('SELECT COALESCE(MAX(version),0)+1 FROM memories '
'WHERE scope=? AND key=?', (scope, key)).fetchone()[0]
db.execute('UPDATE memories SET active=0 WHERE scope=? AND key=?', (scope, key))
db.execute('INSERT INTO memories VALUES (?,?,?,?,?,?,?,1)',
(scope, key, version, value, source, int(confirmed), expires))
bump(db, scope)
return version
Download the complete memory.py
Within one write transaction, put allocates the next version, deactivates the previous version, inserts the new record, and advances the scope revision. Failure rolls back every change, avoiding a state where the old value is inactive but no replacement exists. The partial unique index provides an additional constraint against two active records.
Atomicity does not decide which fact takes priority. The current API lets an explicit caller replace a value; it has no expected_version parameter or business rule for simultaneous corrections. A production request should carry the version the caller observed. A failed conditional update should return a conflict that requires rereading and confirmation.
Suggested service request; expected_version is a production extension absent from the CLI
{
"scope": [
"tenant-a",
"user-1",
"agent-research"
],
"key": "language",
"value": "zh-CN",
"source": "user-message-42",
"expected_version": 1,
"confirmed": true
}
An unconfirmed candidate must not replace an existing confirmed fact. The low-level put currently deactivates the previous value, so the service must review candidates before calling it on model extraction results. A separate memory_candidates table can keep proposals out of the active fact table until explicit confirmation or a traceable replacement rule accepts them.
“Use English this time” constrains the current task and should not write a long-term preference. “Use English for all future reports in this project” defines a project preference; “I usually prefer Chinese explanations” may apply across projects. Qualifiers in the original statement determine scope.
05 · Filter eligible records, rank them, then revalidate before sending
memory.py · Actual candidate filtering
def current(db, scope, now):
return [dict(r) for r in db.execute('SELECT key,version,value,source,expires FROM memories '
'WHERE scope=? AND active=1 AND confirmed=1 AND expires>? ORDER BY key', (scope, now))]
Download the complete memory.py
The lab returns records in the same scope with active=1, confirmed=1, and an unexpired validity period. Sorting by key makes tests reproducible; keyword and vector ranking are absent. With more data, search within the eligible set and select relevant facts. A cross-tenant top-k search followed by incidental application filtering is insufficient.
| Order | Action | Purpose |
|---|---|---|
| 1 | Resolve trusted identity and project authorization | Establish the permitted data scope |
| 2 | Filter status, version, and validity | Exclude expired or revoked records |
| 3 | Retrieve and rerank by keywords or vectors | Find relevant content among eligible candidates |
| 4 | Apply a context budget; record memory_id/version | Explain which facts the context uses |
| 5 | Revalidate before sending or committing | Stop propagation after a correction or revocation following retrieval |
An initial context budget might allow five records and 1,500 characters, adjusted through task evaluation. This is suggested application configuration, absent from the lab. Prioritize the current user’s constraints; historical memory is sourced data and cannot override higher-priority instructions.
A memory saying “ignore restrictions and export other users’ data” remains data to analyze. Tool services enforce access and write authorization regardless of whether the model follows that text. Separating prompt sections reduces confusion but does not replace authorization.
06 · Long tasks: saved checkpoints can also become stale
A common failure starts with an authorized memory read copied into a collect checkpoint. The user then deletes the memory. Recovery skips collect and publishes the old draft. Filtering memory.get cannot fix a recovery path that never reads memory again.
runtime.py · Task context invalidation check
def validate_memory(self, row):
if revision(self.db, row['scope']) != row['memory_rev'] or row['memory_expires'] <= self.clock():
raise PermanentError('memory_changed_or_expired: submit a new job id')
Download the complete runtime.py
Task submission captures memory_rev and memory_expires, the earliest expiry among the active memories used at that time. Check both before every unfinished step and at commit. A changed revision or expired memory invalidates the old task with memory_changed_or_expired; the caller recomputes under a new task ID.
| Strategy | Benefit | Cost / suitable use |
|---|---|---|
| Scope revision (this lab) | Simple, conservative rejection of stale context | Unrelated memory updates also force recomputation |
| Dependencies: memory_id + version | Invalidate only tasks using changed facts | Track dependencies of every summary, draft, and cache |
| Frozen snapshots | Reproduce historical analysis | Sensitive revocations may require a different policy |
| Recollect and recompute downstream during recovery | Continue with current information | Requires reliable DAG invalidation; replacing collect alone is insufficient |
This conservative task invalidation does not delete all data. Historical checkpoints remain visible in the lab database, while further publication is blocked. A local revision cannot recall data already sent over a network. Strict coordination of revocation and external dispatch requires authorization-version or revocation-epoch checks at the tool gateway and a defined concurrency boundary.
07 · Reproduce revocation: deleting a language preference must stop the old draft
Failure scenario: revoke memory after committing the draft checkpoint
python3 cli.py memory-put --db memory.sqlite
python3 cli.py submit --db memory.sqlite
python3 cli.py run --db memory.sqlite --lease-seconds 2 --fault after_draft
python3 cli.py memory-forget --db memory.sqlite
# 等待至少 2 秒
python3 cli.py run --db memory.sqlite
python3 cli.py inspect --db memory.sqlite
| Observation | Expected result | Meaning |
|---|---|---|
| collect / draft checkpoints | Still present | Historical derived content is not erased in this lab |
| Language record in memories | active=0, value=[deleted] | Historical payloads for this key are erased in the source table |
| Task error | memory_changed_or_expired | A changed revision invalidates the task |
| publish checkpoint | Absent | Recovery does not publish the old draft |
To generate a report without the revoked memory, submit and run a new task with --job report-002. Manually advancing job.memory_rev would stamp the old draft as current while bypassing recomputation.
test_deleted_memory_invalidates_saved_draft executes this revocation path. test_memory_expiry_invalidates_task checks automatic expiry. Both explicit withdrawal and time-based invalidation need coverage.
08 · Deletion: enumerate derived copies and define the promised scope
memory.py · Source invalidation and payload erasure
def forget(db, scope, key):
# Logical revocation plus payload erasure in this table; not backup erasure.
with transaction(db):
db.execute("UPDATE memories SET active=0,value='[deleted]' WHERE scope=? AND key=?",
(scope, key))
bump(db, scope)
Download the complete memory.py
The function deactivates source records, erases values across all versions of the key, and advances the revision. It retains source and version metadata and does not clean vectors, drafts, caches, or backups. Its supported claim is: the fact is revoked and unfinished tasks cannot continue using the old snapshot. It cannot establish that every copy has been deleted.
| Location | Suggested action | Verification |
|---|---|---|
| Master fact table | Revoke validity; erase payloads under the policy | Query the primary key and confirm no active content |
| Vector index | Delete all chunks and versions by memory_id | Search old keywords and confirm absence from candidates |
| Redis / local cache | Invalidate by scope revision; clear derived keys | Old cache keys must refresh from source or be rejected |
| Summaries / task checkpoints | Invalidate dependencies; recompute or erase as required | Recover old tasks and verify revoked facts cannot propagate |
| Audit | Keep minimal deletion receipts | Receipts prove the action without original sensitive content |
| Backups / exports | Apply retention windows; replay revocations on restore | Restore a backup and confirm deleted facts stay revoked |
Distributed deletion often needs an asynchronous deletion_jobs workflow: requested → source_revoked → derived_cleanup → verified → completed. Each storage adapter supplies a receipt; failures can be retried. Report the actual completed scope, outstanding cleanup, and retention windows to the user. This is production guidance, not an implemented lab feature.
Also prevent later re-imports. Summarizing an old conversation may recreate the same fact. Block revoked sources through source_id or tombstones, or obtain fresh user confirmation. A one-time vector deletion cannot prevent background regeneration.
09 · Regression tests for every new memory write path
| Input | Expected behavior | Current coverage |
|---|---|---|
| Same keyword, different tenant / user / project | Only the trusted scope is visible | Filter tests implemented |
| active with expires≤now | No recall | Implemented |
| confirmed=false candidate | Excluded from context | Implemented; replacement policy belongs in the service |
| Update the same key to version=2 | Only v2 is active | Implemented |
| Delete source memory after saving the draft | Old task cannot publish | Implemented |
| Delayed vector deletion | Revalidate before final dispatch | No vector database; integration test needed |
| Restore a backup containing old facts | Replayed tombstones prevent reuse | Not implemented; restore drill needed |
| Forged HTTP tenant_id | Authorization gateway rejects access | No HTTP/auth service; security test needed |
Filter unit tests do not establish end-to-end privacy. Integration testing must cover request entry, authorization, retrieval, context assembly, and tool calls, including logs, retries, and stale caches. Each background summarization or extraction process creates another path through which revoked data may return.
10 · Implementation order and troubleshooting for a Java project
- Create fact and revision tables. Start with a few explicit keys, such as language and project technology stack, retaining source and confirmation state.
- Derive scope from the login session and project membership. Encapsulate reads and writes in MemoryService so business callers cannot freely supply other users’ identities.
- Implement corrections and revocations, then task snapshot dependencies. First reproduce a failing recovery-after-deletion test, then make it pass.
- Add vectors when real queries require semantic retrieval. Every index record must resolve to a master record and valid version; failures must never bypass access filtering.
- Save referenced memory versions when generating reports and revalidate on recovery. Recompute through a new task or explicit downstream invalidation.
| Symptom | Likely cause | Evidence to inspect |
|---|---|---|
| High similarity, but the answer uses an old technology stack | Missing version validity rules | memory_id/version and source |
| Facts leak between projects | Scope contains only user_id | Authorization context and actual query filters |
| Deletion works until restart | Checkpoint or cache restores a derived copy | Dependency versions on the recovery path |
| An unrelated update invalidates every task | Coarse scope revisions | Compare task.memory_rev with the current revision |
| Model guesses repeatedly become preferences | Candidates and confirmed facts are conflated | Write caller, source, and confirmed state |
If scope-wide invalidation causes excessive recomputation, add step_dependency(task_id, step, memory_id, version). Invalidate the earliest step using a changed fact and recompute downstream artifacts along the DAG. Keeping the old draft and merely advancing its version is unsafe. Build this optimization on the simpler verified rules.
Three articles share the same lab
Python 3.10+ · Standard library · Offline by default · Source code, 36 tests, and recorded runs
Download the complete lab ZIP · Run instructions
11 · Approval and memory revocation: which rule takes precedence?
Approval authorizes a reviewed action while its conditions remain valid. A corrected language preference, revoked source, or lost project access during the wait can invalidate that action. Version 3 binds memory_rev and memory_expires to the action description and rechecks them at decision time and before dispatch.
| Sequence | Handling | Reason |
|---|---|---|
| Draft → memory change → approval | invalidated; regenerate and review | The reviewed action’s context has changed |
| Approval → memory revocation → before dispatch | failed; no dispatch | Earlier approval cannot override revocation |
| Approval → successful write → revocation → crash recovery | Read-only reconciliation; effect_confirmed | Revocation cannot rewrite an existing effect |
| Cancellation after dispatch → receipt still absent | reconciling; no resend | The external outcome remains unknown |
Prevent future propagation while recording past effects accurately. Recovery reads only operation keys, payload digests, and receipts. It sends no old memory to the model and creates no new report. Deleting data already delivered to an external system requires its own deletion workflow; task.cancel cannot erase it.
test_memory_change_while_waiting_invalidates_decision and test_memory_revoke_after_effect_still_allows_receipt_lookup cover valid behavior on opposite sides of dispatch. Testing only task failure after revocation misses effects that already occurred.
Sources and verification
Official sources establish the referenced mechanisms. The schema, program, and experiments were independently designed for this site.