DoublewordDoubleword

BetterDB

Give your AI agents long-term memory that stays on your own infrastructure. BetterDB Agent Memory is an open-source library whose recall runs as a local vector query. It is fast, free to call, and never leaves your network. It also keeps memory current, so an agent that learns "the budget is 40k" and later "the budget is 55k" ends up with one correct memory instead of two contradictory ones.

The only part that costs anything is on the 'cold path' (in the background) which happens when new memories are embedded to make them searchable and when accumulated facts are reconciled. LLMs's can hold onto change in details and facts pretty well within a single session. However, between different chats, over a long time time with many changes, or in a large org/agent setup where context is changing often, memory is necessary to keep every participant on the same page.

This guide runs reconciliation on Doubleword's async (flex) tier at reduced rates, while reads and storage stay local. Flex guarantees 1 minute TTFT (time-to-first-token) saving 25%. For an even greater saving (90% on input tokens), you can enable prompt caching.

Hot path and cold path

Split the work by who is waiting for it.

  • Read (hot path): recall() runs a vector query against Valkey Search and returns the most relevant memories, ranked by similarity, recency, and importance. It is real-time and local.
  • Write and consolidate (cold path): embedding new memories and reconciling accumulated facts are background tasks. They run on Doubleword's async flex tier, which applies reduced-rate pricing and queues work server-side, so you need no backoff or retry logic.

Why the cold path belongs on flex

Consolidation is not a per-message cost. consolidate({ mode: 'facts' }) makes one LLM call per scope, reading a batch of a user's memories and returning the reconciled facts, so the extraction cost is amortized across the whole batch rather than charged per stored memory. Embedding on remember() is a single embedding call per memory, and neither operation runs while a user waits.

Because none of this is on the hot path, it does not need real-time pricing. Doubleword's flex tier applies async rates to exactly this kind of background work and queues it server-side, so a nightly reconciliation sweep runs at reduced cost with no rate-limit handling on your side. A background job should not pay real-time prices.

Supersession

Supersession (keeping only the latest version of a fact and dropping the outdated one) keeps memory current. consolidate({ mode: 'facts' }) groups a user's memories by subject and reconciles them to their current state. When a newer statement supersedes an older one for a subject, the newer statement becomes the current fact and the older one is retired. Resolution is driven by each memory's date, not by write order, and it reconciles against already-stored facts rather than starting from scratch each run.

For example, given a history where the Q3 budget changes and a migration is revised:

[2026-01-01] Migrating our backend from Node.js to Go.
[2026-01-02] Paused the Go migration, team pushed back.
[2026-01-03] Go migration back on, ingest service only.
[2026-01-05] Q3 tooling budget is 40k.
[2026-01-06] Finance bumped Q3 to 55k after the incident.

The stored facts after consolidation collapse to one current value per subject:

backend migration | 2026-01-03 | Go migration back on, ingest service only.
Q3 tooling budget  | 2026-01-06 | Finance bumped Q3 to 55k after the incident.

Each subject resolves to one current fact, and superseded statements are retired.

Prerequisites

  • Node.js 20 or newer. A Python package is available on PyPI; this guide uses the TypeScript library.
  • A Valkey server with the Valkey Search module loaded, for the FT.* commands used to create and query the vector index.
  • A Doubleword API key.

Sign in to the Doubleword Console, create a key under API Keys, and copy it, since it is only shown once.

Doubleword console login Generating a Doubleword API key in the Doubleword console

Then set it in your shell:

export DOUBLEWORD_API_KEY="sk-..."

Configure BetterDB for Doubleword's flex tier

AgentMemory takes an embedFn, and fact consolidation takes an extractor callback. Both are plain functions, so you point them at Doubleword through configuration.

Step 1: Install

npm install @betterdb/agent-memory iovalkey openai

Step 2: The Doubleword adapter

Save this as doubleword.ts. Pin the fact subjects to a canonical list in the prompt, since subject-keyed reconciliation needs stable subjects. And strip a Markdown code fence before parsing, since models sometimes wrap the JSON in one.

// doubleword.ts
import OpenAI from 'openai';
import type { Fact, MemoryItem } from '@betterdb/agent-memory';

const FLEX_TIMEOUT = 900_000;

export const client = new OpenAI({
  apiKey: process.env.DOUBLEWORD_API_KEY,
  baseURL: 'https://api.doubleword.ai/v1',
  timeout: FLEX_TIMEOUT,
});

export async function embedFn(text: string): Promise<number[]> {
  const res = await client.embeddings.create({
    model: 'Qwen/Qwen3-Embedding-8B', // 4096-dim
    input: text,
  });
  return res.data[0].embedding;
}

const CANONICAL_SUBJECTS = [
  'backend migration',
  'dashboard theme',
  'Q3 tooling budget',
  'incident',
  'cache technology',
]; // tailor to your domain

// Runs on the flex tier. `items` arrive date-stamped; returns reconciled facts.
export async function extractFacts(items: MemoryItem[]): Promise<Fact[]> {
  const res = await client.responses.create({
    model: 'deepseek-ai/DeepSeek-V4-Flash',
    service_tier: 'flex',
    max_output_tokens: 16384,
    input:
      'From these dated memories, return the current facts as JSON: an array of ' +
      '{subject, statement, date, tombstone}. Use ONLY these subjects: ' +
      CANONICAL_SUBJECTS.join(', ') + '. ' +
      'Dates are ISO YYYY-MM-DD. ' +
      'When a newer memory supersedes an older one for a subject, return the ' +
      'newer statement as the current fact. Set tombstone:true ONLY to retract a ' +
      'subject entirely; a newer value replacing an older one is NOT a tombstone. ' +
      'Return ONLY the JSON.\n\n' +
      items.map((i) => `[${i.date}] ${i.content}`).join('\n'),
  });

  // Models sometimes wrap the JSON in a Markdown fence; strip it before parsing.
  const raw = res.output_text.replace(/^```(?:json)?\s*|\s*```$/g, '').trim();
  return JSON.parse(raw) as Fact[];
}

deepseek-ai/DeepSeek-V4-Flash is the default but you can use deepseek-ai/DeepSeek-V4-Pro or another from the model catalog for long or complex contradictory histories.

Step 3: Ingest history

Save this as ingest.ts. Embeddings run on Doubleword; the batch loop caps in-flight requests and Doubleword queues the rest. The history below is the full set the next step reconciles.

// ingest.ts
import Valkey from 'iovalkey';
import { AgentMemory } from '@betterdb/agent-memory';
import { embedFn } from './doubleword';

const client = new Valkey('redis://localhost:6379');
const agent = new AgentMemory({ client, name: 'support_agent', embedFn });
await agent.initialize();

const history = [
  { userId: 'usr_123', date: '2026-01-01', text: 'Migrating our backend from Node.js to Go.' },
  { userId: 'usr_123', date: '2026-01-02', text: 'Paused the Go migration, team pushed back.' },
  { userId: 'usr_123', date: '2026-01-03', text: 'Go migration back on, ingest service only.' },
  { userId: 'usr_123', date: '2026-01-04', text: 'I hate dark mode, please keep my dashboards light.' },
  { userId: 'usr_123', date: '2026-01-05', text: 'Q3 tooling budget is 40k.' },
  { userId: 'usr_123', date: '2026-01-06', text: 'Finance bumped Q3 to 55k after the incident.' },
  { userId: 'usr_123', date: '2026-01-07', text: 'The incident was a cache stampede during the Black Friday load test.' },
  { userId: 'usr_123', date: '2026-01-08', text: "We're standardising on Valkey after the KeyDB licence change." },
];

const CONCURRENCY = 32;
for (let i = 0; i < history.length; i += CONCURRENCY) {
  const batch = history.slice(i, i + CONCURRENCY);
  await Promise.all(
    batch.map((h) =>
      agent.memory.remember(h.text, { agentId: h.userId, date: h.date, tags: ['history'] }),
    ),
  );
}
await agent.close();

Step 4: Consolidate on the flex tier

Save this as consolidate.ts. It is a complete, runnable file which it connects to Valkey, runs one consolidation for the user, and prints the resulting stored facts so you can verify the outcome.

// consolidate.ts
import Valkey from 'iovalkey';
import { AgentMemory } from '@betterdb/agent-memory';
import { embedFn, extractFacts } from './doubleword';

const client = new Valkey('redis://localhost:6379');
const agent = new AgentMemory({ client, name: 'support_agent', embedFn });
await agent.initialize();

// One extraction call per scope, reconciling this user's facts to their current state.  This runs on flex tier via extractFacts.
const result = await agent.memory.consolidate({
  mode: 'facts',
  agentId: 'usr_123',
  tags: ['history'],
  extractFacts,
});

console.log(`${result.candidates} memories in, ${result.facts} facts out.`);

// Verify - print the reconciled facts now stored in Valkey. Facts are stored as memories with source 'fact', so filter for them.
const { items } = await agent.memory.list({ agentId: 'usr_123' });
for (const f of items.filter((i) => i.source === 'fact')) {
  console.log(`${f.subject} | ${f.date} | ${f.content}`);
}

await agent.close();

Run it with a package runner tool for your node engine like npm or bun:

# Using npm
npx tsx ingest.ts
npx tsx consolidate.ts

# Using bun
bunx tsx ingest.ts
bunx tsx consolidate.ts

The expected output is one current fact per subject, with the superseded 40k budget and the earlier migration states already retired:

8 memories in, 5 facts out.
backend migration | 2026-01-03 | [2026-01-03] Go migration back on, ingest service only.
dashboard theme   | 2026-01-04 | [2026-01-04] I hate dark mode, please keep my dashboards light.
Q3 tooling budget | 2026-01-06 | [2026-01-06] Finance bumped Q3 to 55k after the incident.
incident          | 2026-01-07 | [2026-01-07] The incident was a cache stampede during the Black Friday load test.
cache technology  | 2026-01-08 | [2026-01-08] We're standardising on Valkey after the KeyDB licence change.

Eight raw memories went in and five current facts came out, with the contradictions resolved for you. Exact statement wording varies slightly run to run, since the summary is model-generated, but the subject, date, and resolved value are stable.

Observability

The memory store emits OpenTelemetry spans and Prometheus metrics for every operation including recall latency, hit rate, embedding calls, consolidations, and unmatched-tombstone events. The extraction and embedding calls sent to flex show up in your existing dashboards.

Model choice and latency

DeepSeek-V4-Flash and DeepSeek-V4-Pro both resolve supersession correctly. Flash is the lighter default and either should handle most use cases readily. Flex is an async tier, so extraction first returns in up to a minute. Keep it on a background cron, off the user-facing hot path.

Next steps

  • Nightly reconciliation. Run consolidation on a cron to keep memory current.
  • Canonical subjects. Maintain the subject list for your domain to keep supersession reliable.

Grab a Doubleword API key and point BetterDB's cold path at it, or head to betterdb.com to explore the full library.