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.

4 min read
session persistence agent SQLite Bun agent thread session map Slack bot memory session store SDK

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

Keep Going

Ready to Start Building?

Pick the next step that matches where you are right now.

Tutorial
Claude Code Basics

Start with the terminal basics. A hands-on, step-by-step guide to your first 10 minutes with Claude Code.

Start the Tutorial
Guide
AI-Powered Workflows

Automate your client work. Learn how to connect AI tools into workflows that handle repetitive tasks for you.

Read the Guide
Community
Join the Community

Connect with other fractional leaders building with AI. Share workflows, get feedback, and learn from operators who are ahead of you.

Apply to Join