惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

量子位
F
Fortinet All Blogs
J
Java Code Geeks
Y
Y Combinator Blog
Stack Overflow Blog
Stack Overflow Blog
V
Visual Studio Blog
M
MIT News - Artificial intelligence
腾讯CDC
Last Week in AI
Last Week in AI
The Cloudflare Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
Jina AI
Jina AI
Microsoft Security Blog
Microsoft Security Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
P
Proofpoint News Feed
博客园 - 叶小钗
Recent Announcements
Recent Announcements
T
Tailwind CSS Blog
Engineering at Meta
Engineering at Meta
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
人人都是产品经理
人人都是产品经理
L
LangChain Blog
博客园 - 司徒正美
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻

Chat SDK Documentation

History | Chat SDK List a vendor-official adapter | Chat SDK Approvals | Chat SDK Vercel Connect | Chat SDK Teams Low-Level APIs | Chat SDK CLI | Chat SDK Platform Adapters | Chat SDK State Adapters | Chat SDK Cards | Chat SDK Getting Started | Chat SDK Introduction | Chat SDK Modals | Chat SDK Slack Low-Level APIs | Chat SDK Streaming | Chat SDK Testing | Chat SDK Overview | Chat SDK toAiMessages | Chat SDK Cards | Chat SDK Overview | Chat SDK Markdown | Chat SDK Modals | Chat SDK AI SDK Tools | Chat SDK Types | Chat SDK Message Subject | Chat SDK Conversation History | Chat SDK Transcripts | Chat SDK Slack bot with Next.js and Redis Actions | Chat SDK Direct Messages | Chat SDK Emoji | Chat SDK
History | Chat SDK
Vercel · 2026-08-28 · via Chat SDK Documentation

Store and retrieve message history across user, thread, and channel scopes.

The History API gives you three scopes for reading and persisting message history — keyed by user, thread, or channel. The user scope is stored in your state adapter; thread history uses platform APIs with an optional state-backed cache; channel history reads from the platform via adapter methods.

ScopeAccessKeyed byTypical use
Userbot.history.userCross-platform user keyLLM context, audit trail, GDPR
Threadbot.history.threadThread IDBackfill for adapters without server-side history
Channelbot.history.channelChannel IDChannel-level summaries, moderation

bot.history is the unified successor to bot.transcripts. The user scope is a drop-in replacement. See Migrating from bot.transcripts below.

All three scopes are available once you configure a state adapter and — for the user scope — an identity resolver:

import { Chat } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";
import { createDiscordAdapter } from "@chat-adapter/discord";
import { createRedisState } from "@chat-adapter/state-redis";

const bot = new Chat({
  userName: "mybot",
  adapters: {
    slack: createSlackAdapter(),
    discord: createDiscordAdapter(),
  },
  state: createRedisState({ url: process.env.REDIS_URL! }),

  history: {
    user: {
      // Required — maps each inbound message to a stable cross-platform key.
      // Return null to skip persistence for that message.
      identity: ({ author }) => author.email ?? null,

      // Storage tuning. retention is the list TTL, refreshed on every append.
      retention: "30d",
      maxPerUser: 200,
    },
  },
});

history.user requires an identity resolver — set history.user.identity, or keep the deprecated top-level identity field during migration. Omitting both when history.user (or legacy transcripts) is set throws at construction.

Thread and channel scopes are always available on bot.history. The thread cache (for adapters with persistThreadHistory: true, e.g. Telegram and WhatsApp) is tuned via history.thread:

const bot = new Chat({
  // ...
  history: {
    thread: {
      maxMessages: 200,
      ttlMs: 14 * 24 * 60 * 60 * 1000, // 14 days
    },
  },
});

User history

bot.history.user stores a per-user transcript keyed by a stable cross-platform identifier. The same user can talk to your bot on Slack and Discord and see the same accumulated history.

Building LLM context

The most common pattern: append the user's message, build a prompt from recent history, post the reply, then append the reply.

bot.onSubscribedMessage(async (thread, msg) => {
  await bot.history.user.append(thread, msg);

  const recent = await bot.history.user.list({
    userKey: msg.userKey!,
    limit: 20,
  });

  const reply = await generateReply(recent, msg);
  await thread.post(reply);

  await bot.history.user.append(
    thread,
    { role: "assistant", text: reply },
    { userKey: msg.userKey! }
  );
});

Key things to know:

  • msg.userKey is set automatically by the SDK from your identity resolver before your handler runs. If the resolver returned null, it remains undefined and append is a no-op.
  • Bot replies are explicit. The SDK does not auto-capture thread.post() output — you decide what gets persisted. This matters for retries and intermediate streaming chunks.
  • Order is chronological. list returns oldest-first, ready to feed into a model. Use limit to keep prompts bounded.

Filtering entries

// Recent 50 across all platforms (default)
await bot.history.user.list({ userKey: "mike@acme.com" });

// Newest 20 only
await bot.history.user.list({ userKey: "mike@acme.com", limit: 20 });

// Single platform
await bot.history.user.list({ userKey: "mike@acme.com", platforms: ["slack"] });

// Single thread
await bot.history.user.list({
  userKey: "mike@acme.com",
  threadId: "slack:C123:1234.5678",
});

// Only the user's own messages
await bot.history.user.list({ userKey: "mike@acme.com", roles: ["user"] });

Deleting a user's history

For GDPR data-subject requests or "forget me" flows:

await bot.history.user.delete({ userKey: "mike@acme.com" });
// → { deleted: 47 }

This wipes every entry stored under the key. Single-entry and time-range deletes are not supported — the underlying appendToList primitive cannot support them safely under concurrent writes.

Thread history

bot.history.thread caches messages per thread. It is used internally by adapters that lack server-side message history APIs and set persistThreadHistory: true (Telegram, WhatsApp). For those adapters, reads are served from the cache; for everything else, the platform response is authoritative.

bot.onSubscribedMessage(async (thread, message) => {
  // bot.history.thread.list() handles both cases — it delegates to
  // adapter.fetchMessages, and on adapters that persist history in the
  // SDK-maintained cache (persistThreadHistory) serves it from there.
  const { messages } = await bot.history.thread.list(thread.id, { limit: 20 });

  const reply = await generateReply(messages, message);
  await thread.post(reply);
});

Thread history is populated automatically when adapter.persistThreadHistory is true. You don't need to append manually. For adapters with server-side history (Slack, Teams, Discord), prefer thread.messages or adapter.fetchMessages.

Channel history

bot.history.channel provides channel-level reads: top-level messages and thread listings. It delegates to the adapter — methods throw when the adapter doesn't implement the underlying capability (for example, listThreads on WhatsApp).

bot.onSubscribedMessage(async (thread, message) => {
  const channelId = thread.channelId;

  const { messages } = await bot.history.channel.listMessages(channelId, {
    limit: 10,
  });

  const { threads } = await bot.history.channel.listThreads(channelId, {
    limit: 20,
  });

  const reply = await generateChannelSummary(messages, threads);
  await thread.post(reply);
});

The identity resolver can live on history.user.identity (preferred) or the deprecated top-level ChatConfig.identity field. It runs once per inbound message:

history: {
  user: {
    identity: async ({ adapter, author, message }) => {
      if (author.email) {
        return author.email;
      }
      // Map a platform user to an internal ID
      return await lookupUser(adapter, author.userId);
    },
    retention: "30d",
  },
}

Return null when you cannot resolve a key. The SDK will not fall back to a platform-specific ID — that would silently fragment a user's history across platforms.

If the resolver throws, the SDK logs a warning and dispatches the message without a userKey. Handlers still run; only the persistence is skipped.

bot.transcripts is deprecated in favour of bot.history.user. The API surface is identical — rename the config key and the call site:

// Before
const bot = new Chat({
  identity: ({ author }) => author.email ?? null,
  transcripts: { retention: "30d", maxPerUser: 200 },
});

await bot.transcripts.append(thread, msg);
const entries = await bot.transcripts.list({ userKey, limit: 20 });
await bot.transcripts.delete({ userKey });

// After — identity on history.user (top-level identity still works during migration)
const bot = new Chat({
  history: {
    user: {
      identity: ({ author }) => author.email ?? null,
      retention: "30d",
      maxPerUser: 200,
    },
  },
});

await bot.history.user.append(thread, msg);
const entries = await bot.history.user.list({ userKey, limit: 20 });
await bot.history.user.delete({ userKey });

bot.transcripts continues to work and will not be removed in the current major version.

You can migrate one field at a time: when both history.user and the legacy transcripts block are set, they merge, with history.user winning field by field. Settings like retention and maxPerUser left on transcripts keep applying until you move them.

User and thread cache scopes use StateAdapter.appendToList / getList. Channel scope reads from platform APIs and is not persisted in state by default.

ScopeStorage / sourceKey or API
UserState adaptertranscripts:user:{userKey}
Thread cacheState adapter (when persistThreadHistory: true)msg-history:{threadId}
Thread / channel readsPlatform adapteradapter.fetchMessages, fetchChannelMessages, listThreads

On channel-scoped platforms (WhatsApp, Telegram), inbound messages may also be appended under msg-history:{channelId} so channel-level iteration can fall back to the cache.

Appends to user/thread cache keys are atomic, so concurrent inbound messages on the same key don't race.

See History API reference for full type signatures, configuration options, and entry shapes.