How do I give an AI agent memory that survives a new session?
A runnable Python example retrieves a current instruction and rejects changed content, broken correction links, cycles, due reviews, and expiration.

An AI agent gets durable memory when you put an instruction in a file that will still exist tomorrow, give later sessions a small pointer to that file, and refuse to use the instruction until the pointer, record, source, freshness dates, and content hashes all check out. The model does not carry the instruction across the session boundary. Your storage and retrieval path do.
That distinction matters the first time a corrected instruction comes back wrong. Saving more chat history does not solve it. You need a durable source, a retrieval cue, a supersession trail, a review date, an expiration date, and a test that a fresh process can run without trusting the previous session. This is the implementation companion to *Why did my AI forget what I taught it yesterday?* The business piece explains the record-keeping problem. Here we will build the mechanism from an empty directory.
Problem: What problem are we actually solving?
Suppose you correct an agent on Monday: customer-facing copy must use the full public product name. On Tuesday, a new session uses an internal abbreviation again. The Monday session may have understood perfectly. The Tuesday session did not receive a trustworthy route back to the correction.
I treat memory as retrieval infrastructure, not as a personality trait of the model. The system must answer six questions before it applies a remembered instruction:
- Where is the durable source that owns the instruction?
- Which small pointer tells this session when to read it?
- Does that pointer still resolve to the record it was bound to?
- Has a newer record superseded it?
- Does the source still match the hash recorded when it was verified?
- Is the record inside its review and expiration window?
If any answer is missing, retrieval failed. An agent saying that it remembers is not a seventh check.
Value: Why is a layered design worth the extra files?
A single giant memory file looks simple until every session has to load it. It mixes hot routing information with cold detail, makes conflicting instructions hard to spot, and changes so often that the startup prefix becomes expensive to reuse. At the other extreme, a directory full of accurate records is useless when nothing tells the next session which one to open.
The useful split is small and deliberate:
- The durable source owns the instruction in the place responsible for it.
- The index stays small and maps a key plus a
read_whentrigger to one current record. It hash-binds that record so a changed wrapper cannot pass. - The record points to the source and carries its hash, status, verification date, review date, expiration date, and reciprocal supersession links. The old record names its replacement; the replacement names the old record.
- The retriever reads the instruction from the source and fails closed when any check breaks. It never trusts a duplicate copy in the record.
- Old records remain as a correction trail, but the index never points to them.
Retrieval rejects a missing forward or reverse link, disagreement between the two links, a link back to the same record, or a cycle with no current endpoint.
This is the routing shape shown by the maintained public memory templates: a compact entry routes to detail loaded only when relevant. I add record hashes, source hashes, supersession, and freshness controls because a pointer that resolves to changed content is a successful lookup of the wrong answer.
Need help turning this pattern into a dependable AI system?
Just In Time AI can help your team design and implement the smallest useful workflow with the security, compliance, and operational controls it needs, then prove it before expanding. Explore AI Systems Setup and Coaching.
Solution: How do you build memory that survives a new session?
What do you need before you start?
Plan on about 20 minutes. The example uses Python 3.9 or newer and a macOS, Linux, Windows Subsystem for Linux, or Git Bash shell. You need permission to create files in one working directory. You do not need an API key, database, vector store, model account, or background service.
The example stores a harmless naming instruction. Do not put passwords, tokens, private customer data, or regulated records in an agent memory directory. A retrieval system makes information easier to find; it does not make sensitive information safe to store.
How should the memory layers fit together?
We will build this directory:
ai-memory/
index.json
memory_check.py
records/
customer-copy-v1.json
customer-copy-v2.json
sources/
customer-copy-policy-v1.md
customer-copy-policy-v2.md
Version 1 is intentionally retained and marked superseded. Version 2 is the current instruction. index.json points only to version 2 and records the exact hash of that wrapper. The wrapper records the exact source hash. The retriever reads the instruction from the source itself.
The durable principle outlasts these filenames: keep the canonical instruction separate from the small routing surface, and validate the complete route before use. In a larger system, the source may be a policy repository or configuration service, and the index may be generated. The trust checks stay the same.
How do you build the example from true step one?
1. How do you prove the environment is ready before writing memory?
Start below the headline task. Create the empty structure, confirm your shell is in the expected directory, and confirm Python responds:
mkdir -p ai-memory/records ai-memory/sources
cd ai-memory
pwd
python3 --version
test -d records && test -d sources && echo "PASS: empty memory workspace is ready"
You should see the absolute path to ai-memory, a Python version, and the PASS line. If python3 is not found on Windows, try py -3 --version; then replace python3 with py -3 in later commands. Do not continue until the interpreter and both directories are visible.
2. How do you create the durable sources before the pointers?
Write the old instruction and its corrected replacement as separate sources:
cat > sources/customer-copy-policy-v1.md <<'EOF'
# Customer-facing copy policy (superseded)
Use the internal short name in every customer-facing document.
This example source is retained only to show the supersession trail. The
current index must never point here.
EOF
cat > sources/customer-copy-policy-v2.md <<'EOF'
# Customer-facing copy policy
Use the full public product name in customer-facing copy; reserve internal short names for private operational notes.
Review this instruction when the public product name or audience changes.
EOF
Calculate the exact SHA-256 for each file:
python3 - <<'PY'
from hashlib import sha256
from pathlib import Path
for name in ("customer-copy-policy-v1.md", "customer-copy-policy-v2.md"):
path = Path("sources") / name
print(name, sha256(path.read_bytes()).hexdigest())
PY
You should see these values:
customer-copy-policy-v1.md 91fa74109f2936c70e874184930086fad3aa3e6b77be4f55fd1a68e21b3e95fc
customer-copy-policy-v2.md f5239cabd9a222a37ed03ed388c77a8ce48c8234f2e42052dbed0ed5cf6239ec
If your values differ, compare whitespace and line endings before continuing. The record must bind the bytes you actually wrote.
3. How do you record a correction without erasing history?
Save this as records/customer-copy-v1.json:
{
"id": "customer-copy-v1",
"status": "superseded",
"recorded_on": "2026-07-01",
"last_verified": "2026-07-01",
"review_on": "2026-08-01",
"expires_on": "2026-10-01",
"durable_source": "sources/customer-copy-policy-v1.md",
"source_sha256": "91fa74109f2936c70e874184930086fad3aa3e6b77be4f55fd1a68e21b3e95fc",
"superseded_by": "customer-copy-v2",
"change_reason": "The public naming rule requires the full public product name in customer-facing copy."
}
Save the current record as records/customer-copy-v2.json:
{
"id": "customer-copy-v2",
"status": "current",
"recorded_on": "2026-09-14",
"last_verified": "2026-09-14",
"review_on": "2026-12-01",
"expires_on": "2027-03-01",
"durable_source": "sources/customer-copy-policy-v2.md",
"source_sha256": "f5239cabd9a222a37ed03ed388c77a8ce48c8234f2e42052dbed0ed5cf6239ec",
"supersedes": "customer-copy-v1",
"change_reason": "Separate public naming from private operational shorthand."
}
The two records form one reciprocal correction edge: customer-copy-v1.superseded_by names customer-copy-v2, and customer-copy-v2.supersedes names customer-copy-v1. The validator requires both fields and exact agreement. A one-way link can make a replacement look current while the old record tells a different story.
The dates are controls. review_on is the day a person or governing process must compare the record with reality again. expires_on is the hard stop after which retrieval must refuse the record. Choose a shorter window for volatile instructions and a longer one for stable policy. Never move a date forward merely to make the test green.
Verify the JSON and calculate the current record hash:
python3 -m json.tool records/customer-copy-v1.json >/dev/null
python3 -m json.tool records/customer-copy-v2.json >/dev/null
python3 - <<'PY'
from hashlib import sha256
from pathlib import Path
path = Path("records/customer-copy-v2.json")
print(sha256(path.read_bytes()).hexdigest())
PY
The current record hash should be fba4d6a5307d7e4922041382e238b3bb02c8f0842150df66b4f995be593052a7.
4. How do you add the smallest useful retrieval pointer?
Create index.json with one current pointer:
{
"schema_version": "1.0",
"entries": [
{
"key": "customer-copy",
"read_when": "Before writing customer-facing product copy",
"record_id": "customer-copy-v2",
"target": "records/customer-copy-v2.json",
"record_sha256": "fba4d6a5307d7e4922041382e238b3bb02c8f0842150df66b4f995be593052a7"
}
]
}
The key is for deterministic lookup. read_when tells the agent when the lookup applies. The target is a pointer, not another copy of the instruction. When you correct a fact, write the new source and record first, mark the prior record superseded, then move this pointer last. That order keeps the index from pointing at a file that does not exist.
5. How do you retrieve only verified, current memory?
Save the following as memory_check.py:
from datetime import date
from hashlib import sha256
import json
from pathlib import Path
import sys
root = Path(__file__).parent.resolve()
key = sys.argv[1]
as_of = date.fromisoformat(sys.argv[2]) if len(sys.argv) > 2 else date.today()
def load(path):
value = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(value, dict):
raise SystemExit(f"FAIL: {path} must contain a JSON object")
return value
def local(relative):
path = (root / relative).resolve()
if not path.is_relative_to(root) or not path.is_file():
raise SystemExit(f"FAIL: unsafe or missing path: {relative}")
return path
def day(record, field):
try:
return date.fromisoformat(record[field])
except (KeyError, TypeError, ValueError):
raise SystemExit(f"FAIL: {record.get('id')}: {field} must be YYYY-MM-DD")
records = {}
for record_path in sorted((root / "records").glob("*.json")):
record = load(record_path)
record_id = record.get("id")
if not isinstance(record_id, str) or not record_id or record_id in records:
raise SystemExit("FAIL: every record needs a unique non-empty id")
record["_path"] = record_path.resolve()
records[record_id] = record
for record_id, record in records.items():
if record.get("status") not in {"current", "superseded"}:
raise SystemExit(f"FAIL: {record_id}: invalid status")
source = local(record.get("durable_source", ""))
if sha256(source.read_bytes()).hexdigest() != record.get("source_sha256"):
raise SystemExit(f"FAIL: {record_id}: durable source hash changed")
if day(record, "review_on") >= day(record, "expires_on"):
raise SystemExit(f"FAIL: {record_id}: review_on must precede expires_on")
replacement = record.get("superseded_by")
predecessor = record.get("supersedes")
if record["status"] == "superseded":
if replacement == record_id:
raise SystemExit(f"FAIL: {record_id}: superseded_by cannot be a self-link")
if replacement not in records:
raise SystemExit(f"FAIL: {record_id}: superseded_by does not resolve")
elif replacement is not None:
raise SystemExit(f"FAIL: {record_id}: current record cannot declare superseded_by")
if predecessor is not None:
if predecessor == record_id:
raise SystemExit(f"FAIL: {record_id}: supersedes cannot be a self-link")
if predecessor not in records:
raise SystemExit(f"FAIL: {record_id}: supersedes does not resolve")
for start_id in records:
seen = set()
cursor = start_id
while records[cursor]["status"] == "superseded":
if cursor in seen:
raise SystemExit(f"FAIL: {start_id}: supersession cycle detected")
seen.add(cursor)
cursor = records[cursor]["superseded_by"]
for record_id, record in records.items():
if record["status"] == "superseded":
replacement_id = record["superseded_by"]
if records[replacement_id].get("supersedes") != record_id:
raise SystemExit(
f"FAIL: {record_id} -> {replacement_id}: reverse link does not agree"
)
predecessor_id = record.get("supersedes")
if predecessor_id is not None:
predecessor = records[predecessor_id]
if predecessor["status"] != "superseded":
raise SystemExit(f"FAIL: {record_id}: supersedes must identify a superseded record")
if predecessor.get("superseded_by") != record_id:
raise SystemExit(
f"FAIL: {record_id} -> {predecessor_id}: forward link does not agree"
)
index = load(local("index.json"))
entry = next((item for item in index["entries"] if item["key"] == key), None)
if entry is None:
raise SystemExit(f"FAIL: no pointer matches {key}")
record_path = local(entry["target"])
if sha256(record_path.read_bytes()).hexdigest() != entry["record_sha256"]:
raise SystemExit("FAIL: record hash changed")
record = records.get(entry["record_id"])
if record is None or record_path != record["_path"] or record["status"] != "current":
raise SystemExit("FAIL: pointer does not resolve to the current record")
review_on = day(record, "review_on")
expires_on = day(record, "expires_on")
if as_of >= expires_on:
raise SystemExit(f"FAIL: record expired on {expires_on}")
if as_of >= review_on:
raise SystemExit(f"FAIL: review due on {review_on}")
source = local(record["durable_source"])
instruction = next(
line.strip() for line in source.read_text(encoding="utf-8").splitlines()
if line.strip() and not line.lstrip().startswith("#")
)
print(f"PASS key={key}")
print("PASS supersession links are reciprocal, non-self, and acyclic")
print(f"source={record['durable_source']}")
print(f"instruction={instruction}")
print(f"verified={record['last_verified']}")
print(f"review_on={record['review_on']}")
print(f"expires_on={record['expires_on']}")
Run retrieval with an explicit date so the result is reproducible:
python3 memory_check.py customer-copy 2026-09-14
You should see PASS key=customer-copy, confirmation that the supersession links are reciprocal, non-self, and acyclic, the version 2 source, the full-public-name instruction, and both freshness dates. Run the same command from a fresh shell. The output should be identical because the new process read the files; it did not inherit state from your current shell.
Prove the guards work. Change the test date to 2026-12-01; retrieval must stop with FAIL: review due on 2026-12-01. Change it to 2027-03-01; it must stop with FAIL: record expired on 2027-03-01. Restore the original date, edit one character in the version 2 source, and run again; the source hash must fail. Undo that edit, change one character in the version 2 record, and run again; the record hash must fail. Then remove or change either supersession field, point either field back to its own record, and make the two records point at each other as replacements. Retrieval must reject the missing or disagreeing edge, self-link, and cycle before returning the instruction.
The supplied run-memory-demo.sh performs those checks from a temporary copy, compares the successful run byte-for-byte with expected-output.txt, and exits nonzero if any negative fixture is accepted. Invoke it from any directory with sh /path/to/run-memory-demo.sh. A clean run ends with these checks:
PASS expected output matches
PASS source tamper rejected
PASS record tamper rejected
PASS pointer tamper rejected
PASS malformed superseded_by link rejected
PASS malformed supersedes link rejected
PASS supersession self-link rejected
PASS supersession cycle rejected
PASS pointer to superseded record rejected
PASS due review rejected
PASS expired record rejected
Details Matter: What fails first and how do you recover?
If the key is missing, do not create a second memory store. Search the existing index and the system responsible for the subject. Add a pointer only after a durable owner exists.
If the target file is missing, restore it from version control or point the index at a verified replacement. Do not use the old superseded record merely because it still exists.
If either hash changed, compare the changed file with its governing source. An intentional correction needs a new dated record, reciprocal supersession links, and an index update. An unexplained change is a failed integrity check.
If a supersession field is missing or the two fields disagree, inspect both records and restore the one edge they should describe. Do not guess direction from the dates. If either field points to its own record or the forward links form a cycle, retrieval stays blocked until the chain reaches one current record.
If review is due, recheck the instruction against reality and set a new date only after it passes. If the record expired, retrieval stays blocked until a current replacement exists. Expiration without refusal is only a comment.
A weak implementation may duplicate the instruction in the record and retrieve that copy. Changing the duplicate would not disturb the source hash, so wrong text could still print. Keep the instruction only in the durable source, hash-bind the record from the index, and retrieve governing content from the source. The source, record, pointer, and correction-chain negatives each protect a different hop.
Artifacts
- Current public templates: inspect the default-branch JitNeuro memory template and detail-index template. These establish the compact route and on-demand detail pattern; the integrity and lifecycle controls in this tutorial extend that pattern.
- Runnable package: download
run-memory-demo.sh,memory_cli.py,expected-output.txt, and the hash-bounddemo-run.receipt.json. - Sample memory store: inspect
index.json, the superseded record, the current record, the superseded source, and the current source. - Copy-along files: use
setup.sh,create-sources.sh,hash-sources.sh,source-hashes.txt,verify-records.sh,memory_check.py,retrieve.sh, and the exactdemo-run-output.txt.
For the business decision and value behind this method, read Why Did My AI Forget What I Taught It Yesterday?.
Bottom Line: How can you prove the memory survived a new session?
Put the instruction in its durable source, point one small index entry at its current record, and make a fresh process verify the record hash, source hash, reciprocal supersession chain, status, review date, and expiration date before returning the instruction. Then test a superseded record, a changed source, a changed record, a missing or disagreeing supersession link, a self-link, a cycle, a due review, and an expired record. A memory system earns trust by refusing those cases.
Your next action is small: choose one instruction that matters tomorrow and run the example with that instruction today. The shareable rule is: **If a fresh session cannot retrieve and verify the source, the agent does not remember it.** If your retrieval check fails in a way this example does not cover, use the owned Dan Stolts contact route and include the failure plus command output.
Frequently Asked Questions
Does a vector database give an AI agent durable memory?
It can help find similar text, but similarity does not establish that the result is current, authoritative, or safe to use. Keep the source, status, supersession, review, expiration, and verification checks even when vector search proposes the candidate record.
Should I store the full instruction in the index or record?
Keep the index to a key, a read trigger, an exact record pointer, and the record hash. Keep the record to routing and lifecycle metadata. Read the instruction from the hash-bound durable source so no duplicate text can drift.
What should happen when two current records conflict?
Stop retrieval and resolve ownership before the agent acts. Mark the losing record superseded, link both directions, update the durable source if needed, and move the index only after the replacement passes validation.
How often should an AI memory record be reviewed?
Set the interval from how quickly the underlying fact can change and how costly a stale answer would be. Review volatile product, availability, access, and policy facts sooner than stable background. Use an expiration date as the hard stop, not as a reminder you are free to ignore.
Who reads the memory index, and when does the system evaluate its retrieval trigger?
The session bootstrap or task router loads the compact index before substantive work, then compares the current task and context with each read_when trigger. One clear match loads the pointed record, validates its hash and lifecycle fields, and opens the authoritative source. The key-based command in this tutorial makes that path deterministic; a production orchestrator must supply and test the trigger evaluator.
What should happen when no trigger matches or several triggers match?
With no match, load no detail and use the approved router or accountable owner if the decision is required. With several plausible matches, stop and resolve the ambiguity instead of guessing which instruction governs. A stale, inaccessible, hash-mismatched, or conflicting result is also a failed retrieval and must not authorize consequential work.
Update History
- 2026-09-14 - What changed: Added reciprocal forward-and-reverse supersession validation, explicit self-link and cycle rejection, portable negative fixtures, current-default public template links, and downloadable local artifacts. Why: A one-way correction trail can disagree with its replacement and still return an apparently current instruction, while an unlinked runnable example leaves the reader unable to inspect or execute the taught method.
This entry records development before publication authorization; it does not describe a prior public version or claim that the article had already been published at that time.
- 2026-09-14 - Reader path and implementation help: Added the property-specific Just In Time AI service callout within the first third of the article and verified the planned paired reader path. The article and companion routes are now bound for release; deployment and anonymous readback remain pending.
Evidence and provenance: the public repository is https://github.com/dstolts/jitneuro. The exact sources reviewed on 2026-09-14 are the memory template and detail-index template. The runnable example and its hash-bound execution receipt demonstrate retrieval plus source, record, pointer, reciprocal supersession, self-link, cycle, review, and expiration checks. They do not claim a current deployment, automatic model recall, measured savings, or successful customer use. The article and downloadable asset routes are bound for release; deployment and anonymous readback remain pending.
Update history
What changed: Replaced an unapproved email address with the owned Dan Stolts contact route and changed one nonessential commercial term to operational wording.
Why it matters: Preserves the durable-memory method, runnable evidence, FAQ intent, and pair relation while removing release-blocking reader-visible copy.