Skip to main content
Version: 1.0.0

Retain conversation history with SQLite

Goal: retain an agent's exchanges after process exit and inspect their size without printing private messages. SQLite ships with Node.js 24; no separate system installation or SQLite package is needed. It is not enabled automatically: Touba uses RAM unless configured otherwise.

1. Prepare the project​

Follow the SDK installation and prerequisites in the native calls tutorial, create its native-calculator.tb program and compile it. Keep its contracts and commands; only the provider changes here. The same settings work with decisionInterface: "structured".

Add .private-state/ to the project's .gitignore. This directory contains private data, not resources to publish.

2. Configure a persistent provider​

Replace the provider module with:

providers/calculator.provider.mjs
import { mkdirSync, appendFileSync } from "node:fs";
import { createOpenAiDecisionProvider } from "@touba/sdk";
import { createSqliteDecisionHistoryStore } from "@touba/sdk/history/sqlite";

mkdirSync(".private-state", { recursive: true, mode: 0o700 });
const store = createSqliteDecisionHistoryStore({
filename: ".private-state/conversations.sqlite",
maxBytes: 32 * 1024 * 1024,
ttlMs: 24 * 60 * 60 * 1000,
});

export const decideAgentCapability = createOpenAiDecisionProvider({
model: "gpt-4.1-mini",
decisionInterface: "native",
maxOutputTokens: 1000,
history: {
store,
namespace: "calculator-local",
maxMessages: 128,
maxBytes: 262144,
maxRetainedBytes: 8 * 1024 * 1024,
onEvent(event) {
appendFileSync(".private-state/history-events.jsonl",
JSON.stringify(event) + "\n", { mode: 0o600 });
},
},
});

Check permissions of an existing directory: mkdirSync does not fix them. Events contain counters and a hashed session key, not messages. The event file belongs to the application; configure log rotation for long-running use.

3. Run, then inspect after the process exits​

Run the native tutorial's touba run command with the key in your environment. This contacts the actual provider and may incur charges. The two Double calls should remain distinct operations.

Create this reader, then run node inspect-history.mjs in the same directory:

inspect-history.mjs
import { readFileSync } from "node:fs";
import { createSqliteDecisionHistoryStore } from "@touba/sdk/history/sqlite";

const events = readFileSync(".private-state/history-events.jsonl", "utf8")
.trim().split("\n").map(line => JSON.parse(line));
const sessionKey = events.at(-1).sessionKey;
const store = createSqliteDecisionHistoryStore({
filename: ".private-state/conversations.sqlite",
});
try {
const record = store.read(sessionKey);
if (!record) throw new Error("History expired or absent.");
console.log({
retainedExchanges: record.turns.length,
omittedFromContext: record.contextStart,
pendingCall: record.pending !== null,
closed: record.closed,
});
} finally {
store.close();
}

This second process reads a retained snapshot without network access. A completed calculation should have closed: true and no pending call. read does not renew the TTL. Detailed contents are private; avoid printing them in logs.

4. Understand resume​

Running the strategy again creates a new run and invocation, with separate history. The database does not replay a previous run. An actual approval resume uses the checkpoint, the same store, namespace, model and interface; the runtime preserves invocation identity. See the human-wait guide.

After a crash, DecisionHistoryBusy may indicate a persistent reservation. Verify that the old worker has stopped and reconcile effects before calling store.recover(sessionKey, { workerStopped: true }). This is neither human approval nor permission to repeat an effect.

5. Check the limits​

  • Sent context and retained exchanges have separate limits.
  • Compaction increases contextStart without reducing turns.length.
  • TTL and store.delete(sessionKey) can remove an inactive conversation.
  • A full store refuses; it does not automatically delete older exchanges.
  • Identity and namespaces come from the host, never from the model.

Continue with the context and compaction tutorial for token limits.