Memory

Memory (cold_memory internally) is Catentio's long-running, cross-session knowledge store — rows an agent's runner injects into context so it doesn't have to re-learn the same fact every run. Each row has a type (e.g. note, pattern, fact), optional agent/scope targeting, and a content field that's what actually gets embedded and recalled. There is no tags array, no markdown-title/body split, and no mem_... id — entry ids are plain integers.

For the runtime memory contract (recall ranking, the embedding path), see API → Memory.

Auth. Every route in this namespace requires the control plane's x-catentio-session header, which this SDK never sends. Neither apiKey mode nor Bearer-session mode authenticates here. See SDKs → Auth: what actually works.

Namespace

catentio.memory — five methods:

catentio.memory.stats()
catentio.memory.listEntries(query?)
catentio.memory.getEntry(id)
catentio.memory.createEntry(input)
catentio.memory.updateEntry(id, patch)

No delete, verify, supersede, links, bulk-delete, or graph — the control plane has all of them (POST /v1/memory/entries/{id}/verify, POST .../supersede, GET .../links, POST /v1/memory/entries/bulk-delete, GET /v1/memory/graph), but this SDK doesn't wrap any of them.

Methods

memory.stats

Signature. catentio.memory.stats(token?): Promise<MemoryStats>

import { CatentioSaasClient } from '@forjio/catentio-saas-node';

const catentio = new CatentioSaasClient({ session });

const stats = await catentio.memory.stats();
console.log(`${stats.total} entries`);

Real shape:

interface MemoryStats {
  total: number;
  by_agent: Array<{ key: string; count: number }>;
  by_type: Array<{ key: string; count: number }>;
  by_scope: Array<{ key: string; count: number }>;
}

There is no entryCount, tagCount, totalBytes, oldestAt, or newestAt field.

memory.listEntries

Signature. catentio.memory.listEntries(query?, token?): Promise<{ data: MemoryEntryListRow[]; meta: { limit: number; search: boolean; total?: number } }>

Real query parameters — these five, and only these five:

Field Type Notes
q string Full-text/semantic search. When set, rows are ranked by relevance and meta.search is true.
agent string Filter by agent.
type string Filter by entry type.
scope string Filter by scope.
limit integer 1–200, default 50.

There is no tag, since, or cursor parameter, and no offset either — when q is omitted, the route is ORDER BY created_at DESC LIMIT :limit with no way to page past the first limit rows except by narrowing agent/type/scope.

const { data, meta } = await catentio.memory.listEntries({ agent: 'gojo', limit: 20 });
for (const e of data) {
  console.log(e.id, e.type, e.slug, e.preview);
}

Real list-row shape (note: preview, not the full content):

interface MemoryEntryListRow {
  id: number;
  type: string;
  slug: string | null;
  name: string | null;
  description: string | null;
  preview: string;          // content, truncated to 240 chars
  agent: string | null;
  scope: string;
  source_attribution: string | null;
  status: string;
  always_load: boolean;
  last_verified_at: string | null;
  meta: Record<string, unknown> | null;
  created_at: string;
  score: number | null;     // only set when `q` was used
}

memory.getEntry

Signature. catentio.memory.getEntry(id, token?): Promise<MemoryEntry>

id is the entry's integer row id. Returns full detail (with content, not preview):

interface MemoryEntry {
  id: number;
  type: string;
  slug: string | null;
  name: string | null;
  description: string | null;
  content: string;
  agent: string | null;
  scope: string;
  source_attribution: string | null;
  status: string;
  always_load: boolean;
  last_verified_at: string | null;
  meta: Record<string, unknown> | null;
  created_at: string;
  updated_at: string;
  has_embedding: boolean;   // never the raw vector
  hit_count: number;
  impression_count: number;
  hit_rate: number;         // computed, 0.0 if no impressions
  last_hit_at: string | null;
}
const entry = await catentio.memory.getEntry(84213);
console.log(entry.content);

memory.createEntry

Signature. catentio.memory.createEntry(input, token?): Promise<MemoryEntry>

const entry = await catentio.memory.createEntry({
  type: 'note',
  content: 'Bang asked 2026-05-11 to keep emoji out of source-controlled files unless explicitly requested.',
  slug: 'feedback_no_emoji_in_files',
  name: 'No emoji in committed files',
  scope: 'shared',
});

Real input fields: content (required, non-empty) and type (required, e.g. note/pattern/fact) are the only two the runtime enforces; slug, name, description, source_attribution, agent, scope (defaults to "shared"), always_load (default false), and meta are all optional. There is no title, body, or tags field — those don't exist. Creation is subject to a cold_memory_entries tier cap.

memory.updateEntry

Signature. catentio.memory.updateEntry(id, patch, token?): Promise<MemoryEntry>

This route is OTP-gated. PATCH /v1/memory/entries/{id} requires x-catentio-otp-challenge and x-catentio-otp-code headers, which this SDK never sends. Without them the call fails with 403 { "otp_required": true, "challenge_id", "scope", "expires_at" } instead of applying the patch. To actually update an entry today, drive the OTP challenge/response yourself against the low-level client and pass the headers explicitly.

await catentio.memory.updateEntry(84213, { content: '...' }, /* still 403s without OTP headers */);

Common patterns

Health-check memory size by agent

const stats = await catentio.memory.stats();
console.log(`${stats.total} entries total`);
for (const row of stats.by_agent) {
  console.log(`${row.key}: ${row.count}`);
}

Search recall preview

const { data } = await catentio.memory.listEntries({ q: 'pawpado billing', limit: 10 });
console.log(data.map(e => `- ${e.name ?? e.slug} (score ${e.score?.toFixed(2) ?? 'n/a'})`).join('\n'));

Add a feedback entry from a script

async function recordFeedback(slug: string, lesson: string) {
  await catentio.memory.createEntry({
    type: 'feedback',
    slug: `feedback_${slug}`,
    content: `**Captured ${new Date().toISOString()}**\n\n${lesson}`,
  });
}

Errors

The real error body is FastAPI's {"detail": ...}. Because the wire response never has an error key, the SDK doesn't parse out a structured code/message — you get CatentioError with a generic HTTP_ERROR code and the HTTP reason phrase as the message.

Status Cause
401 Missing/invalid session — this SDK never sends x-catentio-session.
400 createEntry() missing content or type.
403 updateEntry() without OTP headers (see above), or past the cold_memory_entries tier cap on createEntry().
404 getEntry()/updateEntry() with an id that doesn't exist.

Next

  • Agents — the principal that recalls memory.
  • API → Memory — HTTP-level reference including recall semantics and the routes this SDK doesn't wrap.