twin memory

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 twinmemory

Node 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 prompt

Make 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.

SettingOptionEnvironment variableDefault
API keyapiKeyTWIN_MEMORY_API_KEYrequired
Base URLbaseUrlTWIN_MEMORY_BASE_URLhttps://api.twinmemory.dev
Timeouttimeout60000 ms per attempt
RetriesmaxRetries2
Fail openfailOpenfalse
Integrationintegrationnone

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

MethodRoute
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.

ErrorWhen
AuthenticationError401: no key, or one the service did not issue
PermissionDeniedError403: the key reads, and the call writes
NotFoundError404: nothing was ever added for this end user
InvalidRequestError400, 413, 422: the request itself is wrong
RateLimitError429, after retries
ServiceUnavailableError503, after retries
APIConnectionErrorthe service could not be reached, after retries
APITimeoutErrorthe 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:

  • search answers an empty result with failed: true;
  • add saves nothing and answers failed: true;
  • each logs a warning (console.warn, or the logger you 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:

twin.ts
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.

On this page