Recall conversation memory after a restart (TypeScript)
We will store a fictional running-club name and shoe size, exit the process, then ask a new process to recall them. The state file retains the actual conversation ID, so recall cannot silently select another conversation or repeat teaching.
userId is ownership metadata; creating another conversation with the same user
id creates a different conversation. The middleware reads the supplied
conversation’s context. It does not search other conversations or call the
bridge-only preference APIs. Workspace credentials define access to the data.
Step 1: Build the SDK and install the lesson project
You need a workspace-scoped MEMORY_API_KEY for a dedicated test workspace on
NAMS. Model calls also need OPENAI_API_KEY.
Use Git, Node.js 22.9 or later with npm, and a POSIX shell. The lesson
commands load .env with Node’s --env-file-if-exists flag, added in
Node.js 22.9; the SDK itself supports Node.js 22. These lessons
share one repository checkout. The example-based lessons build the SDK from
source so they run against current source; application code, including the
MCP lesson’s standalone my-memory-mcp project, installs the published
@neo4j-labs/[email protected] package from npm. Keep this checkout for the
remaining TypeScript lessons.
For your first lesson, clone and build the SDK:
git clone https://github.com/neo4j-labs/agent-memory.git
cd agent-memory
export AGENT_MEMORY_TUTORIAL_ROOT="$PWD"
cd typescript
npm ci
npm run build
cd "$AGENT_MEMORY_TUTORIAL_ROOT"
Expected: the build finishes without errors and typescript/dist/index.js
exists. The example-based lessons use this built SDK; installing an example
does not build it. The MCP lesson’s project uses the npm package instead, so
it does not depend on this build.
Continuing from another lesson, keep your existing checkout and skip the clone/build block above. From any directory inside that checkout, run:
cd "$(git rev-parse --show-toplevel)"
export AGENT_MEMORY_TUTORIAL_ROOT="$PWD"
Both entry paths finish at the repository root. If you changed the SDK source since the preceding lesson, rebuild it before continuing with an example-based lesson.
Enter the shared lesson project — its package.json pins the SDK with
file:../.. so it always runs against the checkout you just built. On its
first use, install its locked dependencies; subsequent lessons reuse them.
Create the tutorial environment file only if it does not already exist:
cd "$AGENT_MEMORY_TUTORIAL_ROOT/typescript/examples/vercel-ai"
if [ ! -d node_modules ]; then npm ci; fi
if [ ! -e .env ]; then
(umask 077; set -C; cat .env.tutorial.example > .env)
fi
chmod 600 .env
npm run lint
Expected: typechecking finishes without errors. It does not authenticate or
write to NAMS. Edit the private .env with the keys required by this lesson;
keep it out of version control. An existing .env is preserved: add missing
tutorial settings without replacing its other values.
For the two model lessons, set OPENAI_MODEL=gpt-4o-mini and supply
OPENAI_API_KEY; an existing demo configuration may select a different model.
If an earlier install was interrupted, or the lockfile changed, run npm ci
again from this project directory, then npm run lint. The directory check
above only avoids an unnecessary reinstall; it does not prove the dependencies
are complete. npm ci replaces node_modules from the lockfile and preserves
your .env and .tutorial-state/ files.
Keep a private note of the selected NAMS workspace name/ID and its key owner.
The optional MEMORY_WORKSPACE_ID is a selector, not proof of ownership. A
workspace-scoped key may not need it. Keep the same endpoint, key and selector
for the run’s authenticated verification and cleanup; do not print the key or
use a different workspace to work around a failed check.
The tutorial template uses gpt-4o-mini for model lessons. DEMO_USER_ID,
newest-conversation reuse and the gpt-5-mini default in .env.example belong
to the separate npm start demo. These lessons create or explicitly resume
their own conversations instead.
All remaining commands run in typescript/examples/vercel-ai/.
--env-file-if-exists=.env loads that file; exported shell variables take
precedence, so keep them consistent with the selected workspace. The project
sets ESM and supplies the compiler configuration; no global TypeScript install
is needed.
Step 2: Read the teach, recall and cleanup program
Open src/tutorials/conversation.ts. This complete program has separate modes so
the recall run cannot accidentally repeat the teaching message.
import type { LanguageModelV4 } from "@ai-sdk/provider";
import { openai } from "@ai-sdk/openai";
import { generateText, wrapLanguageModel } from "ai";
import { agentMemoryMiddleware } from "@neo4j-labs/agent-memory/middleware/vercel-ai";
import { TutorialRun, isTutorialEntryPoint } from "../../../shared/tutorial-state.js";
import { runTutorialCommand } from "../../../shared/tutorial-cleanup.js";
export async function teach(run: TutorialRun) {
const conversation = await run.createConversation();
await run.addMessage("user", `My fictional running club is called ${run.name}. I wear size 10 shoes.`);
await run.verify();
console.log(`CONVERSATION_ID=${conversation.id}`);
console.log(`Teaching message verified. Stop this process and keep ${run.path}.`);
return conversation.id;
}
export async function recall(run: TutorialRun, baseModel: LanguageModelV4) {
await run.verify();
const before = await run.client.shortTerm.getConversation(run.conversationId);
console.log(`Stored messages verified before this question: ${before.messages.length}`);
const model = wrapLanguageModel({ model: baseModel,
middleware: agentMemoryMiddleware(run.client, { conversationId: run.conversationId }) });
// Neither the synthetic name nor the shoe size appears in this question.
const prompt = "What is the exact full running club name I told you, including its identifier, and what shoe size did I tell you?";
const restore = run.trackMiddlewareWrites();
try {
const { text } = await generateText({ model,
system: "Answer from the supplied memory. Say when a requested fact is missing.", prompt });
console.log(`Model returned: ${text}`);
await run.verifyTurn(before.messages.map(m => m.id), prompt, text);
console.log("New user and assistant messages verified in storage.");
if (!text.includes(run.name) || !/\b10\b/.test(text)) throw new Error("Storage passed, but recall did not include the run-specific club and size.");
console.log("Recall verified against the run-specific club and shoe size.");
return text;
} finally { restore(); }
}
if (isTutorialEntryPoint(import.meta.url)) {
runTutorialCommand("conversation", process.argv.slice(2), "teach", ["teach", "recall"], (mode, run) => {
if (mode === "teach") return teach(run);
return recall(run, openai(process.env.OPENAI_MODEL ?? "gpt-4o-mini"));
}, { preflight: mode => {
if (mode === "recall" && !process.env.OPENAI_API_KEY?.trim()) throw new Error("Set OPENAI_API_KEY for recall; teach, inspect, verify and cleanup do not need it.");
} }).catch(error => { console.error(error); process.exitCode = 1; });
}
The maintained state and cleanup helpers are in typescript/examples/shared/.
Keep the private .tutorial-state/ directory with this checkout. Each run binds
its saved IDs to the endpoint, workspace selector and credential; a changed
identity stops authenticated use of those IDs; local inspect remains available. Reseeding an existing state is refused.
TutorialRun and runTutorialCommand are local teaching helpers, not exports
from the published SDK. They keep a private ledger around these public APIs:
| In this lesson | Public SDK operation underneath |
|---|---|
|
Constructs |
|
|
|
|
|
Reads |
|
|
|
Tutorial-only tracking around middleware |
The helper also retains uncertain operations and scopes cleanup to this run.
When adapting the lesson, use run.client to see the actual SDK calls, and keep
an equivalent ownership and failure policy before removing the teaching
helpers. See the TypeScript API reference
for public methods and the middleware guide
for agentMemoryMiddleware and its best-effort storage behavior.
Step 3: Store a memory and exit
npx tsx --env-file-if-exists=.env \
src/tutorials/conversation.ts teach .tutorial-state/conversation.json
Expect CONVERSATION_ID= and Teaching message verified.; the state also
retains the returned teaching-message ID. This command makes no model call. Keep .tutorial-state/conversation.json for every later phase;
the saved conversation ID replaces guessing by user name or selecting the newest
conversation.
Step 4: Recall from a new process
The teaching command has exited. From the same lesson directory, run:
npx tsx --env-file-if-exists=.env \
src/tutorials/conversation.ts recall .tutorial-state/conversation.json
Expect Stored messages verified before this question: 1, then a
Model returned: answer recalling the seeded full club name, including its
unique identifier, and size 10. The question asks for that precision without
supplying either fact. Surrounding wording varies; inspect recall separately
from the storage milestone.
Expect New user and assistant messages verified in storage. and
Recall verified against the run-specific club and shoe size.
Middleware writes are best effort. This program independently reads the new
user and assistant messages and requires their new IDs, correct roles and exact
question/answer content. A model response alone cannot produce a persistence
pass. If recall or readback fails, keep the state and inspect the recorded IDs;
reusing a user ID does not resume the same conversation.
Verify in a fresh process
Run these commands from the same directory, using the state created above:
npx tsx src/tutorials/conversation.ts \
inspect .tutorial-state/conversation.json
npx tsx --env-file-if-exists=.env \
src/tutorials/conversation.ts verify .tutorial-state/conversation.json
inspect shows local saved IDs and operation status without loading .env
or contacting NAMS. verify opens another authenticated client and reads the
recorded conversation and exact message IDs, roles and content digests. Expect
Verified stored messages: with the conversation ID and message count.
Verification makes no model calls and creates no records. It establishes
persistence across client processes; it does not imply that the hosted service
restarted.
Clean up and inspect the disposition
From the same lesson directory, clean up this run using its saved state:
npx tsx --env-file-if-exists=.env \
src/tutorials/conversation.ts cleanup .tutorial-state/conversation.json
npx tsx src/tutorials/conversation.ts \
inspect .tutorial-state/conversation.json
The cleanup command waits for the recorded messages' extraction to finish, checks the entities derived from those messages and any explicitly created entities, and deletes only records with proven exclusive ownership. It verifies deleted records are absent. Shared, merged or uncertain records remain listed; inspect their disposition instead of treating partial cleanup as a complete reset. A timeout or failed readback stops cleanup and preserves the state.
Keep the state until every resource has a verified disposition. Closing the client alone does not delete data. Do not remove the state file to bypass an unfinished run or bulk-delete workspace entities.
Read the cleanup result before starting another run. These are illustrative shapes; your IDs and counts differ:
Cleanup: {"complete":true,"retainedIds":[],"residualIds":[]}
Cleanup: {"complete":false,"retainedIds":["<entity-id>"],"residualIds":[]}
| Result | Next action |
|---|---|
|
Keep the original ledger as the completed record. A new run uses a new state path. |
|
Inspect each retained resource’s reason. Ask the workspace owner to review its ownership; do not delete it by name or force a workspace reset. |
Error, exit status |
Preserve the ledger and error. A timeout can be retried against the same state; an uncertain write or residual ID needs review before further writes or deletion. |
inspect reads only the local ledger and works without .env, a current key,
or a service connection. It reports IDs, operation status and cleanup
dispositions, omitting credentials, credential fingerprints, workspace
selectors and message digests. It does not establish the current remote state.
After key rotation, verify, record and cleanup still refuse a changed
identity. Do not edit the ledger’s identity to bypass that guard.
For an uncertain or retained result, privately give the workspace owner the run ID, recorded resource IDs, operation statuses, selected workspace note and the failing command. Retain any original MCP create receipt. Do not send keys or the raw ledger. The owner must resolve the exact records before a cleanup claim; a new key or a repeated write is not evidence that the earlier attempt failed.
Only after a completed run, start the next exercise with a distinct filename:
npx tsx --env-file-if-exists=.env \
src/tutorials/conversation.ts teach .tutorial-state/conversation-2.json
Use that new filename for all subsequent commands for the new run. Preserve
the original ledger and any receipts; do not remove them to make seed,
teach or init accept a used path.