mcpField
T
telegram-bot-sessions

telegram-bot-sessions

github.com/agntdevDevelopment143 installsUpdated 17w ago
Repository

About telegram-bot-sessions

A skill for Telegram bot developers on managing conversation state with grammY. It covers in-memory sessions, SQLite persistence options, and isolation between tests.

Key features

  1. 01Explains grammY sessions and ctx.session
  2. 02Covers MemorySessionStorage
  3. 03Describes SQLite storage adapters
  4. 04Guides session shape design
  5. 05Explains test-level state isolation

Use cases

  1. 01Build multi-message conversation flows
  2. 02Choose development or production session storage
  3. 03Design session data and field changes
  4. 04Isolate bot state between test cases

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/agntdev/skills --skill telegram-bot-sessions

After installing, type/telegram-bot-sessionsin Claude Code to confirm it loaded.

Details

From the project README

telegram-bot-sessions Skill

How to persist user state in a Telegram bot — why it's needed, how grammY solves it, how the toolkit wraps it.

Built for the agntdev pipeline. See agnt-cli-builder for the discovery-and-claim loop. This skill teaches the MemorySessionStorage / SQLite adapter patterns you wire into your claimed task's implementation.


1. Why Sessions Are Needed (Bot API)

Telegram Bot API is stateless. Every Update arrives as a fresh HTTP request. The bot has no built-in memory of what a user did before.

User: "I want to book"
Bot:  "What service?"
User: "Haircut"
Bot:  "When?"
User:  "Tomorrow 2pm"
Bot:  "Booked!"

Without state, the bot can't know that "Haircut" answers the "What service?" question vs being a random message. It has no memory of the conversation step.

How Bot API handles it — there is no built-in mechanism

Bot API just delivers Updates. It's up to the bot to:

  1. Remember who is at what step
  2. Store partial data between messages
  3. Clear state when a flow completes

Option: Database. Write user state to SQLite/Redis on every message. Works but adds latency + complexity.

Option: In-memory Map. Fast but lost on restart. Fine for dev, bad for production.

Option: grammY sessions. The framework abstracts this away.


2. grammY Sessions — The Framework Solution

grammY provides a session() plugin that stores per-chat state and makes it available as ctx.session.

Basic setup
import { Bot, session } from "grammy";

interface SessionData {
  step: string;
  service?: string;
}

const bot = new Bot(token);

bot.use(session({
  initial: (): SessionData => ({ step: "idle" }),
  // storage: ...  // defaults to in-memory Map
}));

bot.command("book", async (ctx) => {
  ctx.session.step = "choosing_service";
  await ctx.reply("What service?");
});

bot.on("message:text", async (ctx) => {
  if (ctx.session.step === "choosing_service") {
    ctx.session.service = ctx.message.text;
    ctx.session.step = "choosing_time";
    await ctx.reply("What time?");
  }
});
Session key

grammY keys sessions by chatId_userId ("12345_67890"). This means:

  • Private chats: one session per user (chat and user are the same person)
  • Group chats: one session shared by all users in the chat
  • Session is per-chat, not per-user globally
Session flavor — typing ctx.session
import { session, type SessionFlavor } from "grammy";

type MyContext = Context & SessionFlavor<SessionData>;

const bot = new Bot<MyContext>(token);
bot.use(session({ initial: () => ({ step: "idle" }) }));

// ctx.session is now typed:
bot.command("book", async (ctx) => {
  ctx.session.step = "choosing";  // TypeScript knows this field
});
StorageAdapter interface

grammY sessions work with any storage backend that implements:

interface StorageAdapter<T> {
  read(key: string): T | undefined;
  write(key: string, value: T): void;
  delete(key: string): void;
  has(key: string): boolean;
  readAllKeys(): string[];
}

Built-in options:

  • Default — in-memory Map (fast, lost on restart, fine for dev)
  • SQLite — durable, survives restarts (add @grammyjs/storage-sqlite)
  • Redis — fast + durable (add @grammyjs/storage-redis)
  • Firebase, MongoDB, Supabase — community adapters available

3. The toolkit (src/toolkit/) — Session Defaults

MemorySessionStorage

The inlined toolkit (at src/toolkit/ in the bot-starter template) ships MemorySessionStorage — a grammY-compatible StorageAdapter backed by Map.

import { MemorySessionStorage } from "../src/toolkit/storage/memory.js";

// Implements StorageAdapter:
const store = new MemorySessionStorage<SessionData>();
store.write("123_456", { step: "idle" });
store.read("123_456");    // { step: "idle" }
store.has("123_456");     // true
store.delete("123_456");
store.readAllKeys();      // []

You rarely instantiate it directly. createBot() uses it by default:

const bot = createBot<Session>(token, {
  initial: () => ({ step: "idle" }),
  // storage omitted → MemorySessionStorage used automatically
});
Session shape design

Keep sessions flat and serializable — no functions, no class instances, no circular refs:

interface Session {
  // Dialog state
  step: string;

  // Flow data (optional until set)
  serviceId?: string;
  slotDate?: string;
  slotTime?: string;

  // Simple counters
  bookingsCount?: number;
}

Every session starts from initial():

initial: () => ({
  step: "idle",
  bookingsCount: 0,
})
Harness isolation

The test harness creates a fresh bot per spec via makeBot(). Each bot gets its own MemorySessionStorage:

Spec 1 ("booking flow"):
  makeBot() → fresh MemorySessionStorage → session starts from initial()

Spec 2 ("cancel flow"):
  makeBot() → another fresh MemorySessionStorage → session starts from initial()
  • No session leaks between specs
  • No cleanup needed between runs
  • Each spec sees exactly the state initial() defines
SQLite in production

For production bots, swap to SQLite (same StorageAdapter interface, same bot code):

import { SqliteSessionStorage } from "../src/toolkit/storage/sqlite.js"; // planned

const bot = createBot<Session>(token, {
  initial: () => ({ step: "idle" }),
  storage: new SqliteSessionStorage("./data/sessions.db"),
});

Until the toolkit ships SQLite adapter, use grammY's @grammyjs/storage-sqlite directly — same interface.

Migration

Adding fields to session:

// V1
interface Session { step: string; }

// V2 — add optional field (safe, no migration needed)
interface Session {
  step: string;
  theme?: "light" | "dark";  // optional — defaults to undefined
}

// Access with default:
const theme = ctx.session.theme ?? "light";
  • MemoryStorage: restarts wipe everything — no migration needed
  • SQLite: optional fields safe to add. Required new fields need migration step that fills defaults for existing rows

Quick Reference

WhatgrammYToolkit
Activate sessionsbot.use(session({...}))Auto via createBot()
Type ctx.sessionContext & SessionFlavor<S>BotContext<S> (pre-built)
Storage (dev)In-memory Map (default)MemorySessionStorage (default)
Storage (prod)@grammyjs/storage-sqliteSqliteSessionStorage (planned)
Session keychatId_userIdSame
Harness isolationManual setupAutomatic — fresh per spec

Common mistakes

  1. Storing non-serializable data — no functions, no class instances. Plain objects only.
  2. Not initializing fields in initial() — missing fields are undefined, not defaults.
  3. Relying on session across restarts (MemoryStorage) — ephemeral. Design flows restart-safe.
  4. Session is per-chat, not per-user — same user in different chats = different sessions.
  5. Session key = chatId_userId string — don't confuse with chat.id alone.

Comments

Markdown supported