TypeScript SDK
twinmemory for JavaScript and TypeScript - no dependencies, typed results, retries, and a client that fails open when memory is down.
twinmemory is the JavaScript and TypeScript client. It is on npm Soon: this page describes version 0.1.0,
and until it is published, plain fetch does the same job.
npm install twinmemoryNode 20 or newer, or any runtime with fetch: Bun, Deno, edge runtimes. No dependencies; ESM and CommonJS,
with types.
Quickstart
import { Memory } from "twinmemory";
const memory = new Memory(); // reads TWIN_MEMORY_API_KEY
const dana = memory.user("dana"); // your own id for one of your end users
await dana.add("I moved to Lisbon for the job at Acme.", { role: "user", session: "onboarding" });
await dana.add({ id: "diet", kind: "note", text: "Vegetarian, allergic to peanuts." }, { wait: true });
const result = await dana.search("Where does Dana live?");
console.log(result.context()); // ready to paste into a promptMake a key in the console, under API keys. A key belongs to one project; each end user you name with
memory.user(...) has a memory of their own inside it. See End users.
| Setting | Option | Environment variable | Default |
|---|---|---|---|
| API key | apiKey | TWIN_MEMORY_API_KEY | required |
| Base URL | baseUrl | TWIN_MEMORY_BASE_URL | https://api.twinmemory.dev |
| Timeout | timeout | 60000 ms per attempt | |
| Retries | maxRetries | 2 | |
| Fail open | failOpen | false | |
| Integration | integration | none |
For a stack on your machine, set TWIN_MEMORY_BASE_URL=http://localhost:8210. Run it on your server: a key in
a browser bundle can be copied, and it speaks for every user of the project.
Adding
add takes a string, an item, or an array of either. The options fill in what an item leaves out.
await dana.add(
[
{ text: "Can you book the same hotel as last time?", role: "user" },
{ text: "Booked the Memmo Alfama again for 3-5 May.", role: "assistant" },
],
{ session: "trip-may" },
);
await dana.add("Moving house on 1 December.", { kind: "note", expires: "2026-12-31" });The fields are those of Add: text (required), kind, id, session, role, author,
title, about, at, expires and metadata. An add is accepted at once and searchable a moment later;
wait: true returns once it is searchable. Arrays longer than 500 items go in batches, and every add carries an
Idempotency-Key, so a retried request never saves an item twice.
Searching
const result = await dana.search("What should I avoid cooking for Dana?");
result.known; // what is true now, as lines
result.facts; // dated facts, each with the passage it came from
result.hits; // whole passages, with their addresses
result.held; // false when memory says it does not hold the answer
result.context(4000);fast: true skips the model filter: free and quicker, a little less precise. before and after (ISO dates)
and scope (a path prefix) narrow a search. See Search.
Everything else
| Method | Route |
|---|---|
memory.me() | Me |
user.history(subject, { attribute }) | History |
user.latest({ k, place, after, before }) | Latest |
user.grep(pattern, { scope, limit }) | Grep |
user.read(path) / user.ls(path) | Read / Ls |
user.around(passageId, { width }) | Around |
user.overview() | Overview |
user.forget({ items, sessions, threads, events }) | Forget |
user.correct(recordId, replacement?) | Correct |
user.versions(itemId) | Versions |
user.status(ids) | Status |
user.lineage(id) | Lineage |
Fields are camelCase; every result keeps the JSON the API sent as .raw.
Errors
Every error is a TwinMemoryError with status, the API's detail, requestId, and outage: whether it
means memory is unavailable rather than that the request or the key is wrong. See Errors.
| Error | When |
|---|---|
AuthenticationError | 401: no key, or one the service did not issue |
PermissionDeniedError | 403: the key reads, and the call writes |
NotFoundError | 404: nothing was ever added for this end user |
InvalidRequestError | 400, 413, 422: the request itself is wrong |
RateLimitError | 429, after retries |
ServiceUnavailableError | 503, after retries |
APIConnectionError | the service could not be reached, after retries |
APITimeoutError | the service did not answer in time, after retries |
429 and 503 are retried twice, waiting as the Retry-After header says; connection errors the same way.
Failing open
Memory being down should cost a reply its memory, not the reply. With new Memory({ failOpen: true }), an
outage - memory unreachable, timing out, or answering 429 or 5xx after its retries - does not throw:
searchanswers an empty result withfailed: true;addsaves nothing and answersfailed: true;- each logs a warning (
console.warn, or theloggeryou pass).
A wrong key or a wrong request (401, 403, 400) still throws: that is a mistake to fix, not an outage to ride
out. Pair it with a short timeout where a reply cannot wait.
new Memory({ integration: "langgraph" }) adds a word to the User-Agent, so the console's request logs show
where requests came from.
LangGraph.js
npx twinmemory-cli init langgraph writes memory into a LangGraph.js project as code you own, using this
client. See LangGraph.
Until it is on npm
The API is plain HTTP and JSON, so a few lines over fetch do the same job now:
const BASE = "https://api.twinmemory.dev/v1/memory";
export async function twin(user: string, route: string, body: object) {
const r = await fetch(`${BASE}/${route}`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TWIN_MEMORY_API_KEY}`,
"X-Twin-Account": user,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (r.status === 404 && route !== "correct") return null; // no memory for this end user yet
if (!r.ok) throw new Error(`${route}: ${r.status} ${await r.text()}`);
return r.json();
}
await twin("dana", "add", { items: [{ text: "I moved to Lisbon.", role: "user" }] });
const brief = await twin("dana", "search", { query: "Where does Dana live?" });Every route has the same shape: a POST to /v1/memory/<name> with a JSON body. See the API reference.