npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

xmemory

v3.13.0

Published

xmemory

Readme

xmemory

TypeScript/JavaScript client library for the xmemory API.

Installation

npm install xmemory

The package ships both ESM and CommonJS builds. import and require both work, and TypeScript resolves the matching declarations either way.

Quick start

import { XmemoryClient } from "xmemory";

const xm = new XmemoryClient({
  url: "https://api.xmemory.ai",  // or set XMEM_API_URL env var
  apiKey: "<your-api-key>",       // or set XMEM_API_KEY env var
});

// Write and read from an existing instance
const inst = xm.instance("<your-instance-id>");

await inst.write("Alice is a software engineer who loves TypeScript.");
const result = await inst.read("What does Alice do?");
console.log(result.reader_result);

Configuration

| Parameter | Env var | Default | Description | |-------------|------------------|---------------------------|-----------------------------------------| | url | XMEM_API_URL | https://api.xmemory.ai | Base URL of the xmemory API | | apiKey | XMEM_API_KEY | undefined | Bearer API key for authentication | | timeoutMs | — | 60000 | Default request timeout in milliseconds |

The legacy token option and XMEM_AUTH_TOKEN env var are still accepted for backwards compatibility but are deprecated and will be removed in a future release. Using them prints a deprecation warning. If both the new and legacy values are set, the new ones win.

Creating a client

import { XmemoryClient, xmemoryInstance } from "xmemory";

// Option 1: constructor (no health check)
const xm1 = new XmemoryClient({ apiKey: "..." });

// Option 2: factory with health check
const xm2 = await XmemoryClient.create({ apiKey: "..." });

// Option 3: convenience function (same as Option 2)
const xm3 = await xmemoryInstance({ apiKey: "..." });

Admin operations

All cluster and instance management lives under client.admin.

Clusters

const clusters = await xm.admin.listClusters();
const cluster = await xm.admin.getCluster(clusterId);

Create an instance

import { SchemaType } from "xmemory";

const inst = await xm.admin.createInstance(
  clusterId,
  "my-instance",
  schemaYml,
  SchemaType.YML,
  { description: "User profiles" },
);

// inst is an InstanceHandle — use it directly for data operations
await inst.write("Alice joined the team.");

List and get instances

const instances = await xm.admin.listInstances();
const info = await xm.admin.getInstance(instanceId);

Schema operations

const schema = await xm.admin.getInstanceSchema(instanceId);
await xm.admin.updateInstanceSchema(instanceId, newYml, SchemaType.YML);

Generate schema

const result = await xm.admin.generateSchema(clusterId, "Track user profiles and preferences");
console.log(result.data_schema);

Update metadata and delete

await xm.admin.updateInstanceMetadata(instanceId, "new-name", "new description");

// Change one field and leave the rest alone
await xm.admin.patchInstanceMetadata(instanceId, { description: "a new description" });

const deletedIds = await xm.admin.deleteInstance(instanceId);

Agent-facing instance metadata

An instance can carry metadata that shapes how agents connect to it and what they do with it. patchInstanceMetadata is the way to set the advisory hints: every option is independent, omitting one leaves the stored value untouched, and passing null clears it.

import { AgentSurface, BindingTier } from "xmemory";

await xm.admin.patchInstanceMetadata(instanceId, {
  // Advisory hints — they seed what a connect flow proposes, and grant nothing.
  agentSurfaces: [AgentSurface.CLAUDE_CODE, AgentSurface.CODEX],
  agentDefaultBindingTier: BindingTier.AUTOLOAD,
  agentEngagementHints: ["a convention is learned or corrected"],
});

Concurrent edits to these three are last-writer-wins by design: they only seed what a connect flow proposes, so the loser of a race re-applies a suggestion. agentOwnerInstructions is not like that — see below.

Reading it back:

const info = await xm.admin.getInstance(instanceId);
info.agent_owner_instructions;
info.agent_surfaces;                  // e.g. ["claude_code", "codex"]
info.agent_default_binding_tier;      // e.g. "autoload"
info.agent_engagement_hints;

These read as plain strings rather than a narrow union, so a value your server knows and this release does not is returned rather than making the instance unreadable.

Setting the standing instructions. Use updateInstanceMetadata for agentOwnerInstructions, not patchInstanceMetadata. The field is rendered to agents verbatim, and a second writer edits it from the same screen, so a silently lost edit is a rule that stops being enforced. Only updateInstanceMetadata carries expectedOwnerInstructionsEpoch: pass the epoch you read the value at and the server refuses the losing save instead of applying it:

const info = await xm.admin.getInstance(instanceId);
await xm.admin.updateInstanceMetadata(instanceId, info.name, info.description ?? "", {
  agentOwnerInstructions: `${info.agent_owner_instructions ?? ""}\nAlso: never paraphrase a rule.`,
  expectedOwnerInstructionsEpoch: info.agent_owner_instructions_epoch,
});

patchInstanceMetadata also accepts the field — it is the only way to set it without restating the name — but it can carry no guard, so an edit composed from stale data overwrites a newer one silently. Reach for it only when you are seeding a value nobody else is editing.

Instance data operations

Get a handle to an instance and use it for reads, writes, and extractions.

const inst = xm.instance("<instance-id>");

inst.write(text | mutations, options?) → WriteResult

Extract and store structured objects from text:

const result = await inst.write("Bob is a designer based in Berlin.");
console.log(result.write_id, result.trace_id);
console.log(result.changes); // what the write created / updated / deleted
console.log(result.console_url); // this operation's trace in the console, or null

Every data operation — read, write, writeAsync, writeStatus, extract — carries console_url, the deep link to that call's trace. It is per operation rather than per record, and null when the server has no console configured.

Options: { extractionLogic?, diffEngine?, scope?, timeoutMs? } — extractionLogic defaults to "fast"; see Scoped writes for scope.

Or pass a WriteMutation[] for structured writes — deterministic, LLM-free create/update/delete mutations applied in array order (later mutations may reference objects created earlier in the batch; a null value in values clears that field):

const result = await inst.write([
  {
    object_mutation: {
      object_type: "person",
      create: { key: { name: "Bob" }, values: { role: "designer" } },
    },
  },
  {
    object_mutation: {
      object_type: "person",
      update: { key: { name: "Bob" }, values: { role: null } }, // null clears
    },
  },
]);

Options for the mutations form: { timeoutMs? } (extraction options don't apply).

inst.writeAsync(text | mutations, options?) → AsyncWriteResult

Start an asynchronous write (same text / WriteMutation[] dual input and the same options as inst.write, scope included). Returns a write_id for tracking; a scope violation is reported by inst.writeStatus as a failed write.

const { write_id } = await inst.writeAsync("Carol manages the London office.");

inst.writeStatus(writeId, options?) → WriteStatusResult

Poll the status of an async write.

const status = await inst.writeStatus(write_id);
console.log(status.write_status); // "queued" | "processing" | "completed" | "failed" | "not_found"

inst.read(query, options?) → ReadResult

Query the instance.

const result = await inst.read("Who is on the team?");
console.log(result.reader_result);

Options: { readMode?, scope?, includeRelatedTypes?, relatedTypesDepth?, skipSuggestionCapture?, traceId?, timeoutMs? } — readMode defaults to "single-answer".

What comes back

In "single-answer" mode reader_result is always the prose answer. In "raw-tables" and "xresponse" mode its value says which of four answers the read gave:

| reader_result | Meaning | What to do | | --- | --- | --- | | rows | Answered. | Use them. | | exactly { columns: [], rows: [] } / { objects: [], relations: [] } | The query executed and matched nothing — every table and column it used exists, so the data is absent. | Trust the empty result. | | null | The schema provably cannot represent the concept. An answer, not a variant of the empty one. | Try a better-matching instance; this memory cannot hold it. | | (throws) | Every sub-query's SQL failed, so nothing was answered. | Catch XmemoryAPIError with status 422 and code "INVALID_INPUT". It is the same answer for the same input, so do not retry it. |

const result = await inst.read("Which invoices are overdue?", { readMode: "raw-tables" });
if (result.reader_result === null) {
  // Not a concept this memory holds — try another instance.
} else {
  const { rows } = result.reader_result as { columns: string[]; rows: unknown[][] };
  // rows.length === 0 means the data is absent, not that the read failed.
}

Composite queries

When a query bundles several independent questions, the server may decompose it into sub-queries and answer each one. reader_result is still the combined answer (for "single-answer" mode, a labelled multi-part string); reader_results holds one TaggedReaderResult ({ sub_query, reader_result, error }) per sub-query so you can read each answer unambiguously — a single-intent query decomposes to one entry. reader_results is always an array: a server without question decomposition omits the field on the wire and the client normalizes it to [], so it is empty regardless of how many questions the query held.

const result = await inst.read("Who leads sales, and where is HQ?");
for (const part of result.reader_results) {
  console.log(part.sub_query, "→", part.error ?? part.reader_result);
}

Each part's reader_result uses the same four answers as above, with one more case: a sub-query whose SQL failed carries the empty result and a user-safe error, while the others are answered regardless. Its bytes then equal those of a sub-query that matched nothing, so read error before reader_result, as the loop above does. The combined reader_result folds the parts: rows if any sub-query answered; else the empty result if any executed and matched nothing; else null. In the tabular modes a read where every sub-query failed throws the 422 above instead.

Scoped reads

By default a read may draw on the whole instance. Pass a scope to restrict it to a set of concrete objects — useful for grounding an answer in exactly the records you care about, or for keeping a per-user / per-entity read from leaking into unrelated data.

Each object in the scope is identified by its type (the PascalCase class name or snake_case table name) plus its user-defined primary key, a mapping of primary-key field name to value. Only objects of a type that has a user-defined primary key can be scoped:

const result = await inst.read("What do we know about these people?", {
  scope: {
    objects: [
      { type: "Person", key: { full_name: "Alice Smith" } },
      { type: "Person", key: { full_name: "Bob Jones" } },
    ],
    relationsScope: "all_relations", // default: "no_relations"
  },
});

relationsScope controls relation traversal: "no_relations" (the default) restricts the read to the listed objects only, while "all_relations" also exposes the relations among the in-scope objects.

Related types

A read can also say what else the memory could answer about. Pass includeRelatedTypes: "types" and related_types on the result names the object types the read touched, each with the fields it did not return and the object types a declared relation links it to (the relation, both roles, and its cardinality seen from the touched type), plus a catalog that describes every named type (description, primary key, field names) and relation once. It is derived from the instance schema and the statements the read executed — no extra rows, no model call — so an agent can ask a deliberate follow-up instead of guessing what the store holds.

const result = await inst.read("Which courses require an English test?", {
  includeRelatedTypes: "types",
});
for (const touched of result.related_types?.touched ?? []) {
  console.log(touched.object_type, "did not return", touched.fields_not_returned);
  for (const link of touched.related) {
    const neighbour = result.related_types!.objects[link.object_type];
    console.log("  linked to", link.object_type, "via", link.relation, neighbour.fields);
  }
}

related_types is null unless the read asked for it. A requested read that executed nothing arrives with touched: []. The server caps the payload; truncated says when it did, and omitted_touched and each entry's omitted_related count what was dropped.

To follow the relations further, add relatedTypesDepth: 2 (up to 3). Every entry under related_types.objects then carries its distance from the touched types (0 for a touched type) and, between the touched types and the last level, its own related edges, so the walk can be continued from the catalog:

const result = await inst.read("Which courses require an English test?", {
  includeRelatedTypes: "types",
  relatedTypesDepth: 2,
});
for (const [name, entry] of Object.entries(result.related_types?.objects ?? {})) {
  console.log(name, "at distance", entry.distance);
  for (const link of entry.related) {
    console.log("  linked to", link.object_type, "via", link.relation);
  }
}

related_types.depth echoes the depth asked for. distance counts listed edges from the nearest touched type, and omitted_objects counts the types within that many relation levels that the server's budgets kept out of the catalog; truncated says something was cut. Left unset, nothing is sent and the server serves one level in the shape it always had: depth, omitted_objects and the catalog entries' distance, related and omitted_related are then absent. A value outside 1 to 3 is a 422 whose code is VALIDATION_ERROR. Asking for related types needs the instance.get_own permission on the API key, the same one the schema endpoints need, on top of data.read: a key without it gets a 403 whose message names the permission, and the plain read is unaffected.

Keeping a read out of schema suggestions

Every read feeds the suggestion engine: after answering, the server asks a model whether the schema could fully answer the question, and a gap becomes a proposed schema change. For programmatic reads — high-volume polling, read-your-write checks — whose question does not reflect what a person wants from the memory, pass skipSuggestionCapture: true. The answer is the same; the read proposes nothing and costs no judge call.

const result = await inst.read("Does invoice INV-42 exist?", { skipSuggestionCapture: true });

Scoped writes

A write is normally free to touch anything in the instance: the extractor sees the text alone, and whatever it produces is reconciled against the whole instance. Pass a scope to anchor a text write to a set of concrete existing objects instead — the same ScopeObject shape as a scoped read:

const result = await inst.write(
  "After her promotion she is a surgeon, and her desk phone is +1-555-0100.",
  { scope: { objects: [{ type: "Person", key: { full_name: "Alice Smith" } }] } },
);

This does two things at once. The scoped objects' current values are shown to the extractor, so the new information is folded into them instead of producing a near-duplicate record. And the write is then confined to the scope: it may only modify or delete the scoped objects, and create new objects and relations anchored to them. A write that would touch any other existing object fails with a validation error rather than applying partially — that confinement is checked against the resulting plan, so it holds regardless of what the extractor produced.

A write scope takes the same ScopeObjects as a read scope, identified the same way. Unlike a read scope there is no relationsScope: the relations among the scoped objects always accompany the extraction hint.

Things to know before reaching for it:

  • Scope applies to text writes only. The WriteMutation[] overload takes { timeoutMs? }, so passing a scope alongside structured mutations does not compile — those bypass extraction entirely and leave a scope nothing to anchor to.
  • Only objects of a type with a user-defined primary key can be scoped. A scope names records by that key, so a type declared primary_key: [] has nothing to name its records by.
  • The server currently accepts a scope with fast extraction only, and caps the number of scoped objects per write. Both are server-side rules, so they surface as an XmemoryAPIError.
  • A scoped write additionally requires read permission on the instance, because the scoped objects' current values are shown to the extractor. An API key with write access alone is refused.

inst.extract(text, options?) → ExtractResult

Extract objects from text without storing them.

const result = await inst.extract("Dave is an engineer in Tokyo.");
console.log(result.objects_extracted);

inst.getSchema(options?) → InstanceSchemaInfo

const schema = await inst.getSchema();
console.log(schema.data_schema);

inst.describe(options?) → DescribeResult

Get the agent-facing tool descriptions for an instance, with its schema in schemaSummary.

const desc = await inst.describe();
console.log(desc.asText());              // plain text for system prompts
const tools = desc.asAnthropicTools();   // Anthropic tool-use format
const tools = desc.asOpenaiTools();      // OpenAI function-calling format

desc.purpose;                            // what the memory is for (the instance description)
desc.ownerInstructions;                  // the standing preference set for it, verbatim
desc.usageBrief;                         // generated from the schema; null until generated

asText() includes purpose and ownerInstructions when the instance has them. usageBrief is left out of it — it restates the schema summary that is already there — so read the property if you want it.

Both fields are free text set by anyone holding edit permission on the instance, so asText() labels each with where it came from rather than presenting it as the library's own words. Those labels state provenance; they are not a security boundary. If you inject this into a system prompt you are still handling text you do not control.

Results are cached for 5 minutes. Call inst.clearDescribeCache() to force a refresh.

Schema evolution

Schemas can change after creation. xmemory supports safe, data-preserving migrations (rename / remove / type change) driven by structured migration ops, plus a suggestion engine that proposes improvements from real read traffic. This is purely additive — existing methods are unchanged.

See the Schema evolution section of the API reference for the conceptual model, and the TypeScript guide for full walkthroughs.

Suggestion-engine flow (review → decide → apply)

The engine surfaces a single rolling proposal per instance. The minimum flow is three calls — review, decide (in bulk), apply:

import { XmemoryClient, type DecisionInput } from "xmemory";

const xm = new XmemoryClient({ apiKey: "..." });
const inst = xm.instance("<instance-id>");

// 1. Review — get the proposal + its concurrency token.
const review = await inst.reviewSuggestions();
if (review.status === "evolution_in_progress") {
  console.log(`A migration is in flight; retry in ${review.retry_after_seconds}s`);
} else if (review.proposal) {
  const proposal = review.proposal;
  for (const item of proposal.items) {
    console.log(item.item_fingerprint, item.rationale, item.op);
  }

  // 2. Decide — accept / reject / defer per item, in one batch.
  const decisions: DecisionInput[] = proposal.items.map((item) => ({
    item_fingerprint: item.item_fingerprint,
    decision: "accept",
  }));
  const decided = await inst.decideSuggestions(proposal.proposal_version, decisions);

  // 3. Apply — commit accepted decisions as one migration.
  const applied = await inst.applyPendingDecisions(decided.next_proposal_version);
  console.log(applied.status, applied.summary); // e.g. "ok" "added 1 field"
}

When status === "evolution_in_progress", back off for retry_after_seconds and retry instead of blocking.

Direct migration flow (enhance → dry-run → update)

Drive a migration yourself — ask the server to enhance the current schema, preview the DDL, then apply it:

import { XmemoryClient, SchemaType } from "xmemory";
import yaml from "js-yaml";

const xm = new XmemoryClient({ apiKey: "..." });
const current = (await xm.admin.getInstanceSchema("<instance-id>")).data_schema;

// 1. Enhance — new schema + an executor-ready migration plan.
const enhanced = await xm.admin.enhanceSchema(
  "<cluster-id>",
  "Rename Person.mail to Person.email.",
  yaml.dump(current),
);
console.log(enhanced.summary, enhanced.migration_plan?.ops);

const newYaml = yaml.dump(enhanced.data_schema);

// 2. Dry-run — preview the DDL without applying anything.
const preview = await xm.admin.dryRunMigration("<instance-id>", newYaml, SchemaType.YML, {
  migrationPlan: enhanced.migration_plan ?? undefined,
});
console.log(preview.statements);

// 3. Update — apply. confirmDestructive is required for ops that drop data.
const info = await xm.admin.updateInstanceSchema("<instance-id>", newYaml, SchemaType.YML, {
  migrationPlan: enhanced.migration_plan ?? undefined,
  confirmDestructive: false,
});
console.log(info.migration_id, info.prior_version, "->", info.new_version);

Migration history

const page = await xm.admin.listMigrations("<instance-id>", { limit: 20 });
for (const record of page.items) {
  console.log(record.id, record.source, record.prior_version, "->", record.new_version);
}

const detail = await xm.admin.getMigration("<instance-id>", "<migration-id>", { includeYaml: true });
console.log(detail.yaml_before, detail.yaml_after);

Migration ops are exported as discriminated-union types (MigrationPlan, MigrationOp, AddField, RenameField, RemoveObject, …) keyed on op_type. ProposalItem.op and MigrationRecord.ops are raw dicts for forward compatibility — narrow them to MigrationOp when needed.

Runnable end-to-end examples live in examples/.

Connecting an instance elsewhere

admin.getSetupInstructions(instanceId) and instance.setupInstructions() both return an AgentSetupResult: how to reach the same memory from another agent surface, ordered most-likely-first. Available on either handle, because the MCP instance connection serves the same tool.

const setup = await inst.setupInstructions();
for (const surface of setup.surfaces) {
  console.log(surface.label);
  for (const step of surface.steps) console.log(" ", step.description, step.command ?? "");
}

Two formats. The default, SetupFormat.AGENT, answers what do I run right now, here. SetupFormat.PROJECT also returns the files a team commits once, so nobody sets the instance up by hand:

const setup = await inst.setupInstructions({ format: SetupFormat.PROJECT });
if (setup.format === SetupFormat.PROJECT) {
  for (const f of setup.project?.fragments ?? []) {
    console.log(f.path, f.merge); // a merge, never a file to overwrite
  }
}

Read setup.format rather than assuming. A server older than that parameter ignores it, answers 200, and names no format at all — so undefined means this deployment predates the project rendering. That is a deliberate difference from the Python client, whose model applies an AGENT default; there is no runtime normalization point here, and inventing one would report a format the server never claimed.

Nothing returned carries a credential: the steps tell a reader to sign in themselves, out of band, so an instance id stays an identifier rather than a key.

Advisory values — step.kind, fragment.merge, format — are widened to accept a value added to the server after this release, so an additive change does not become a breaking one. A step.kind you do not recognise is not something to execute.

Request identification

Every request this client issues carries an X-Xmemory-Client header naming the package and its release, followed by the host — for example xmemory-node/3.9.0 (node v24.5.0; darwin). The parenthetical reports process.version and process.platform, says unknown for either one a host does not supply, and never carries a hostname. The API uses it to tell its own clients apart.

The identity travels in a dedicated header rather than in User-Agent, which belongs to the runtime: your User-Agent is neither read nor written, and every other header on the request is left alone. A proxy that strips unknown X- headers costs the attribution but not the request — the call still goes through, counted as a generic caller.

Error handling

All errors throw XmemoryAPIError. Health check failures throw XmemoryHealthCheckError (a subclass).

import { XmemoryClient, XmemoryAPIError, XmemoryHealthCheckError } from "xmemory";

try {
  const xm = await XmemoryClient.create({ apiKey: "..." });
} catch (e) {
  if (e instanceof XmemoryHealthCheckError) {
    console.error("Server unreachable:", e.message);
  }
}

try {
  await inst.read("query");
} catch (e) {
  if (e instanceof XmemoryAPIError) {
    console.error(`API error (HTTP ${e.status}): ${e.message}`);
  }
}

XmemoryAPIError carries status (HTTP status), code (structured error code, when the server returned one), details (an optional structured payload), and retryAfter (the Retry-After header in seconds, when the server sent one).

Branch on code, not on the bare HTTP status — the same status can mean different things:

| HTTP | code | Meaning | Retryable? | | ---- | ----------------- | ---------------------------------------------------- | ---------------------------------- | | 402 | QUOTA_EXCEEDED | Tenant exhausted its plan/usage allowance (a daily or monthly token quota). | No. Wait for the window to reset. | | 429 | RATE_LIMITED | Genuine velocity / rate limit. | Yes, with backoff. | | 422 | INVALID_INPUT | A read that answered nothing: in "raw-tables" / "xresponse" mode every sub-query's SQL failed, or the model provider declined the input. See What comes back. | No. Same input, same answer. | | 403 | FORBIDDEN | The API key lacks a permission the call needs — for a read with includeRelatedTypes: "types", instance.get_own; the message names it. See Related types. | No. Repeat without the option, or use a key that holds it. |

For QUOTA_EXCEEDED, details carries kind ("daily_quota_exceeded" | "monthly_quota_exceeded") and retry_after_seconds (number | null); when the window is resettable the server also sends a Retry-After header, surfaced as retryAfter (seconds). RATE_LIMITED is retryable — honour retryAfter (or Retry-After) for backoff. The client never retries on its own; it only surfaces the value.

try {
  await inst.write("...");
} catch (e) {
  if (!(e instanceof XmemoryAPIError)) throw e;
  switch (e.code) {
    case "QUOTA_EXCEEDED": {
      // Non-retryable: plan/usage allowance exhausted.
      const kind = (e.details as { kind?: string } | null)?.kind; // daily_ | monthly_quota_exceeded
      console.error(`Quota exhausted (${kind}); resets in ${e.retryAfter ?? "?"}s`);
      break;
    }
    case "RATE_LIMITED":
      // Retryable: back off and retry, honouring Retry-After.
      console.error(`Rate limited; retry after ${e.retryAfter ?? "a short delay"}s`);
      break;
    default:
      console.error(`API error (HTTP ${e.status}, code ${e.code}): ${e.message}`);
  }
}

The schema-evolution endpoints return codes you can pattern match on via .code the same way — for example stale_proposal_version, dependency_closure_failed, destructive_confirmation_required, non_additive_change_requires_plan, stale_schema_version, migration_not_found, instance_not_initialised:

try {
  await inst.applyPendingDecisions(token);
} catch (e) {
  if (e instanceof XmemoryAPIError && e.code === "stale_proposal_version") {
    const review = await inst.reviewSuggestions(); // re-review and retry
  }
}

All timeouts are per-request

Every method accepts an optional timeoutMs in its options bag, overriding the client default.

const result = await inst.read("query", { timeoutMs: 120_000 });

Mastra integration

You can also use xmemory as an MCP server within Mastra.ai.

First, create a local Mastra instance:

npm create mastra@latest mastra-with-xmemory

From within this example mastra-with-xmemory directory, first give it some LLM key:

echo "export ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY_FOR_MASTRA" >>.env

Then you may want to add MCP support to Mastra first, for its hot reload to pick up xmemory right away.

npm i @mastra/mcp

Then fire up the AI-assisted IDE of your choice and give it this prompt.

We need to integrate the `xmemory` MCP server with the Mastra instance running from this directory.

To do this we need to add the `xmemory` Agent, alongside the Weather Agent, and the `xmemory` MCP server to use the Tools from it.

The `xmemory` Agent setup is straightforward, just clone what the Weather Agent has, with `xmemory`-specific instructions. Use the following instructions:

~ ~ ~

> You are the xmemory assistant. You help users manage and query their xmemory instance:
> - Create and configure new instances, generate or enhance schemas, connect and disconnect from instances.
> - Use the xmemory_admin_* tools to perform administrative and schema operations as requested.
> - Be concise and confirm what you did after each action.

~ ~ ~

For the MCP server, you need to add `@mastra/mcp` into `package.json` if it's not already there.

And then make changes along these lines:

new file mode 100644
--- /dev/null
+++ b/src/mastra/mcp-clients.ts
@@ -0,0 +1,19 @@
+import { MCPClient } from '@mastra/mcp';
+
+if (!process.env.XMEM_MCP_BEARER_TOKEN) {
+  throw new Error('XMEM_MCP_BEARER_TOKEN environment variable is required');
+}
+
+export const xmemoryMcp = new MCPClient({
+  id: 'xmemory',
+  servers: {
+    xmemory: {
+      url: new URL('https://dk-mcp.xmemory.ai'),
+      requestInit: {
+        headers: {
+          Authorization: `Bearer ${process.env.XMEM_MCP_BEARER_TOKEN}`,
+        },
+      },
+    },
+  },
+});


new file mode 100644
--- /dev/null
+++ b/src/mastra/xmemory-tools.ts
@@ -0,0 +1,10 @@
+import { xmemoryMcp } from './mcp-clients';
+
+let xmemoryTools: Record<string, any> = {};
+try {
+  xmemoryTools = await xmemoryMcp.listTools();
+} catch (err) {
+  console.error('Failed to load xmemory MCP tools:', err);
+}
+
+export { xmemoryTools };

~ ~ ~

Commit the above as "Added `xmemory` to Mastra."