Docs K  Search
Docs/Getting started/Production pattern
Production pattern

Chatbot & Agent Memory

The recommended end-to-end pattern for using Hebbrix as the memory layer for a conversational chatbot or an autonomous agent: provisioning, corrections, read-after-write consistency, low-latency search, and current-vs-retired facts.

01. Core principles

One collection per user

The collection is the isolation boundary; every read is hard-scoped to it. Keep one per user (or per agent / namespace).

Smart ingestion is queued by default

/memories/process normally returns 202 + job_id. Poll /memories/jobs/{job_id}; the completed result contains extraction events and affected memory IDs.

conflict_status == "none" is the current truth

Corrections retire the old fact (conflict_status = "superseded"). The default listing hides retired facts; search excludes them.

superseded_by_id traces the chain

A superseded memory points at the memory that replaced it. Use include_superseded=true for audit views only.

Use the production search policy

Omitting fast uses the current production default. Set it explicitly only when you need a stable policy contract, and inspect score_calibrated, query_confidence, and ranking_degraded.

Scope chat with collection_id

Memory chat without a scope searches the whole account and is slow. Always pass collection_id. learning:true adds no response-time cost; extraction runs in the background.

02. A reusable client

Node.js 18+ with built-in fetch. No SDK required.

hebbrix-memory.js
const BASE = "https://api.hebbrix.com/v1";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

class HebbrixMemory {
  constructor(apiKey) { this.key = apiKey; }

  async #req(path, { method = "GET", body } = {}) {
    const res = await fetch(BASE + path, {
      method,
      headers: {
        Authorization: `Bearer ${this.key}`,
        ...(body ? { "Content-Type": "application/json" } : {}),
      },
      body: body ? JSON.stringify(body) : undefined,
    });
    const text = await res.text();
    const json = text ? JSON.parse(text) : null;
    if (!res.ok) {
      // Structured errors carry an error_id you can quote to support.
      throw Object.assign(new Error(`HTTP ${res.status}`), { status: res.status, body: json });
    }
    return json;
  }

  // 1. One collection per user
  createUserCollection(userId) {
    return this.#req("/collections", {
      method: "POST",
      body: { name: `user:${userId}`, metadata: { user_id: userId } },
    }).then((c) => c.id);
  }

  // 2. Process a conversation turn / correction
  process(collectionId, messages) {
    return this.#req("/memories/process", {
      method: "POST",
      body: { collection_id: collectionId, messages },
    });
  }

  getProcessJob(id) {
    return this.#req(`/memories/jobs/${encodeURIComponent(id)}`);
  }

  // 3. Read-after-write: wait for extraction and indexing to finish
  async waitForProcessJob(jobId, { maxMs = 30000, intervalMs = 1000 } = {}) {
    const start = Date.now();
    while (Date.now() - start <= maxMs) {
      const job = await this.getProcessJob(jobId);
      if (job.status === "completed") return job.result;
      if (job.status === "failed") throw new Error(job.error || "Memory processing failed");
      await sleep(intervalMs);
    }
    return null; // still running; safe to continue polling later
  }

  // 4. Low-latency search
  search(collectionId, query, { fast = true, limit = 8, minScore } = {}) {
    return this.#req("/search", {
      method: "POST",
      body: {
        collection_id: collectionId, query, limit, fast,
        ...(minScore !== undefined ? { min_score: minScore } : {}),
      },
    });
  }

  // 6. Audit view: includes retired facts + supersede chain
  listMemories(collectionId, { includeSuperseded = false, limit = 50 } = {}) {
    const q = new URLSearchParams({ collection_id: collectionId, limit: String(limit) });
    if (includeSuperseded) q.set("include_superseded", "true");
    return this.#req(`/memories?${q.toString()}`);
  }

  // 7. Memory-aware chat (OpenAI-compatible)
  chat(collectionId, messages, { model = "gpt-5-nano", learning = false } = {}) {
    return this.#req("/chat/completions", {
      method: "POST",
      body: { model, collection_id: collectionId, messages, features: { memory: true, learning } },
    });
  }
}

03. End-to-end example

example.js
const mem = new HebbrixMemory(process.env.HEBBRIX_API_KEY);

// 1. One collection per user (do this once, then store the id).
const collectionId = await mem.createUserCollection("user-123");

// 2. Process a turn, then a correction.
await mem.process(collectionId, [
  { role: "user", content: "My preferred IDE is Cursor and my timezone is America/New_York." },
  { role: "assistant", content: "Noted." },
]);

const correctionJob = await mem.process(collectionId, [
  { role: "user", content: "Actually, my preferred IDE is VS Code, not Cursor." },
  { role: "assistant", content: "Updated." },
]);

// 3. Read-after-write: wait for the queued extraction result.
const correction = await mem.waitForProcessJob(correctionJob.job_id);
const updated = correction?.events?.find((e) => e.event === "UPDATE");

// 4. Search (fast path) returns the corrected fact, not the stale one.
const results = await mem.search(collectionId, "what is my preferred IDE");
//   -> [{ memory_id, content: "User's preferred IDE is VS Code.", score, conflict_status: "none" }]

// 5. Filter current facts (search already excludes retired facts; this is the
//    rule for any raw-memory reads).
const current = (results.results || []).filter((r) => (r.conflict_status ?? "none") === "none");

// 6. Audit view: see the supersede chain.
const audit = await mem.listMemories(collectionId, { includeSuperseded: true });
//   items include the retired fact:
//   { content: "User's preferred IDE is Cursor.", conflict_status: "superseded",
//     superseded_by_id: "<id of the VS Code memory>" }

// 7. Memory-aware chat, scoped to the user's collection.
const reply = await mem.chat(collectionId, [
  { role: "user", content: "Which IDE should I open?" },
], { learning: true });
//   reply.choices[0].message.content references the current fact (VS Code).
//   reply.memory_context.memories_used -> how many memories grounded the answer.

04. Read-after-write consistency

Smart ingestion is durable and queued on acknowledgment. Poll the returned job_id; a completed job carries the extraction result and affected memory IDs. Raw writes have their own indexing status.

read-after-write
POST /v1/memories/process        // default: HTTP 202 + job_id
GET  /v1/memories/jobs/{job_id}  // poll until completed; inspect result.events
POST /v1/search                  // query after the completed job

If you don't need strict read-after-write (e.g. background ingestion), skip the poll. The memory becomes searchable on its own.

05. Current vs. retired facts

  • conflict_status == "none": the current fact.
  • conflict_status == "superseded" / "duplicate": retired.
  • GET /v1/memories (default) and /v1/search exclude retired facts.
  • GET /v1/memories?include_superseded=true includes them, each with superseded_by_id pointing at the replacement.
  • superseded_by_id is populated in normal correction flows; it can be null only if the replacement memory was itself later deleted. Treat conflict_status as the authoritative "is this retired?" signal.

06. Latency expectations (warm, scoped)

Endpointp50p95Notes
POST /v1/searchmeasure your workload750ms p95 objectiveBest-effort engineering objective; inspect response timing and fallback metadata.
POST /v1/search (fast:false)workload-dependentworkload-dependentRequests the fuller ranking path; benchmark with your corpus and concurrency.
POST /v1/memories/processsub-second ack targetextraction is asyncDefault returns 202; job completion includes LLM extraction and conflict resolution.
POST /v1/search/reasonmodel-dependentmodel-dependentGrounded reasoning includes retrieval and managed-model generation.
POST /v1/chat/completions (memory, scoped)model-dependentmodel-dependentStream user-visible tokens and keep ingestion off the response path.

These are service objectives and workload categories, not guaranteed per-request timings. Measure p95/p99 under your corpus and concurrency. Memory-enabled chat without a collection_id searches the whole account and is much slower; always scope it.

07. Agent (policy / correction memory)

The same pattern works for an autonomous agent: store policies and corrections, and retrieve grounded policy before acting.

agent.js
// Store a policy / correction
await mem.process(agentCollectionId, [
  { role: "user", content: "For customer ACME, refunds above $250 require human approval." },
  { role: "assistant", content: "Recorded." },
]);

// Before acting, retrieve grounded policy (fast path)
const policy = await mem.search(agentCollectionId, "refund approval threshold for ACME", { fast: true });

// For a grounded natural-language decision, use reasoning search
const decision = await fetch("https://api.hebbrix.com/v1/search/reason", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.HEBBRIX_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    collection_id: agentCollectionId,
    query: "Can I approve a $275 refund for ACME without a human?",
  }),
}).then((r) => r.json());
//   decision.answer -> grounded answer citing the policy; non-2xx on internal failure.
Ask the docs
reading · this page

Hi! I'm the Hebbrix docs assistant. Ask me anything about this page: setup, code examples, endpoints, pricing, or integrations.