Session Persistence with SQLite and Bun
Map Slack threads to Agent SDK session IDs using Bun's native SQLite. Build a session store with get, set, and delete methods that gives your Slack agent conversation memory.
The problem with stateless agents
Without persistence, every message to your Slack bot starts a new session. A user sends "remember my name is Alex." The bot responds. The user asks "what's my name?" The bot has no idea. Every conversation resets.
For a personal assistant, this is unusable. For a team tool, it is worse — multiple users in the same channel all talking to an agent with no memory.
Bun ships with SQLite built in. No external database needed. A lightweight session store solves this in under 50 lines of code.
How thread-to-session mapping works
Each Slack thread is identified by two values: the channel ID and the thread timestamp. Combine them into a key. Map that key to the Agent SDK session ID. When a message arrives in that thread, look up the session ID and pass it to the agent via resume.
thread_key = channel_id + "::" + thread_timestamp
session_map: thread_key → session_id
Building the session store
Create src/session-store.ts:
import { Database } from "bun:sqlite";
export class SessionStore {
private db: Database;
constructor(dbPath: string) {
this.db = new Database(dbPath);
this.db.run(`
CREATE TABLE IF NOT EXISTS sessions (
thread_key TEXT PRIMARY KEY,
session_id TEXT NOT NULL
)
`);
}
get(threadKey: string): string | undefined {
const row = this.db
.query("SELECT session_id FROM sessions WHERE thread_key = ?")
.get(threadKey) as { session_id: string } | null;
return row?.session_id;
}
set(threadKey: string, sessionId: string): void {
this.db.run(
"INSERT OR REPLACE INTO sessions (thread_key, session_id) VALUES (?, ?)",
[threadKey, sessionId]
);
}
delete(threadKey: string): void {
this.db.run("DELETE FROM sessions WHERE thread_key = ?", [threadKey]);
}
}
Three methods. get retrieves the session ID for a thread. set saves it. delete cleans up broken sessions.
Wiring the session store into the bot
In src/bot.ts, initialize the store and update both event handlers:
import { SessionStore } from "./session-store";
const sessionStore = new SessionStore(
process.env.SESSION_DB_PATH ?? "sessions.db"
);
app.event("app_mention", async ({ event, client }) => {
const threadTs = event.thread_ts ?? event.ts;
const threadKey = `${event.channel}::${threadTs}`;
const existingSessionId = sessionStore.get(threadKey);
const userMessage = stripMentionText(event.text ?? "");
const thinkingMessage = await client.chat.postMessage({
token: process.env.SLACK_BOT_TOKEN,
channel: event.channel,
thread_ts: threadTs,
text: "_Thinking..._",
});
try {
const { response, sessionId } = await agentChat(userMessage, existingSessionId);
sessionStore.set(threadKey, sessionId);
await client.chat.update({
token: process.env.SLACK_BOT_TOKEN,
channel: event.channel,
ts: thinkingMessage.ts!,
text: markdownToSlack(response),
});
} catch (error) {
if (existingSessionId) {
sessionStore.delete(threadKey);
}
await client.chat.update({
token: process.env.SLACK_BOT_TOKEN,
channel: event.channel,
ts: thinkingMessage.ts!,
text: "Something went wrong.",
});
}
});
The same pattern applies to the DM handler. The only difference is how the thread key is constructed from the DM event values.
The error recovery pattern
When an agent session fails, the session ID in the store may be corrupt or expired. The delete call in the catch block cleans it up so the next message starts a fresh session rather than repeatedly trying to resume a broken one.
Testing session persistence
1. Send a message: "My name is Alex." 2. Receive a response acknowledging the name. 3. Send a follow-up in the same thread: "What's my name?" 4. The agent should answer correctly, referencing Alex.
Start a new thread and ask the same question. The agent should not know the name — it is a different thread with its own session.
> Cloud deployment note: When you deploy to Railway, the SQLite file needs to live on a persistent volume — otherwise it gets deleted on every redeploy. The SESSIONDBPATH environment variable pointing to /data/sessions.db handles this. The deployment guide covers the full volume setup.
---
Author: FractionalSkill