feat(chat): add Transcripts API + rename per-thread cache to threadHistory#448
Merged
Conversation
…History
Introduce `bot.transcripts` for cross-platform per-user message persistence.
When `ChatConfig.transcripts` and `ChatConfig.identity` are configured, every
inbound message has its `userKey` resolved during dispatch and the API exposes
`append` / `list` / `count` / `delete` keyed by that user. Backed by the
existing `StateAdapter.appendToList` primitive — every built-in state adapter
supports it with no contract changes.
Rename the existing per-thread history cache from `messageHistory` to
`threadHistory` (with backwards compat for `ChatConfig.messageHistory` and
`Adapter.persistMessageHistory`) so the two persistence layers don't share a
"messages" name. The state-adapter storage key prefix is unchanged so existing
data isn't orphaned.
`delete()` writes a tombstone via `appendToList(key, _, { maxLength: 1 })`
rather than `state.delete(key)`, because `state.delete` only addresses the
k/v namespace on every non-memory state adapter — `list()` and `count()`
filter the tombstone out so the API contract is preserved.
Add a Features-style "Conversation history" guide (`/docs/conversation-history`) walking through identity resolution, the LLM-context append/list pattern, filtering, and per-user deletion for DSR flows. Add an API reference page at `/docs/api/transcripts` with `<TypeTable>` blocks for `ChatConfig.transcripts`, `ChatConfig.identity`, every method on `bot.transcripts`, and the `TranscriptEntry` shape. Wire both into the corresponding `meta.json` files.
Replace the brittle `threadState.history` shim with `bot.transcripts.list({ ..., threadId, limit })` as the fallback context source for platforms without
`fetchMessages` (Telegram, WhatsApp). Drop the `history` field from
`ThreadState` accordingly.
Add a hardcoded `TEST_USER_KEY = "test-user"` so the API can be exercised
without juggling real user identities, plus "Show Transcripts" and
"Clear Transcripts" buttons in the welcome card so the store can be
inspected and reset from chat.
Polish on top of the Transcripts API + threadHistory rename, addressing review concerns before merge. Public surface (`types.ts`, `index.ts`): - Expose `transcripts` on the `ChatInstance` interface so callers typed against the public interface can reach `bot.transcripts`. - Promote the `count` argument to a named `CountQuery` interface, matching `DeleteTarget` / `ListQuery`. Exported from `index.ts`. - Document on `TranscriptsApi.list()` that pagination is intentionally out-of-scope — the store keeps at most `maxPerUser` entries per user. - Reconcile the `TranscriptEntry.id` JSDoc with the implementation: UUID assigned at append time, returned in append order, not lexicographically sortable; use `timestamp` for cross-store ordering. Wiring (`chat.ts`): - Include `threadId` in the identity-resolver failure log context so operators can correlate failures with the source thread. Stale-reference sweep: - Replace lingering "Messages API" / `chat.messages` / `messages.storeFormatted` strings in shipped JSDoc with the new `transcripts` names (these ride into `.d.ts` and are user-visible). - Fix the dead `[Messages API](./messages.ts)` link in the existing thread-history-rename changeset.
Fill gaps in the Transcripts API + threadHistory rename test suite: `chat.test.ts` (persistThreadHistory block): - top-level `config.messageHistory` (deprecated alias) flows through to the per-thread cache when `threadHistory` is unset - `threadHistory` takes precedence over `messageHistory` when both are set — pinned by asserting `appendToList` receives the new config's `maxLength` / `ttlMs` - both `persistThreadHistory` and `persistMessageHistory` set on the adapter still triggers persistence `transcripts-wiring.test.ts`: - sync resolver returning a plain string populates `message.userKey` - resolver returning `""` is treated as no userKey (truthy check at the dispatch hook would silently flip if a future change moved to `!== undefined`) `transcripts.test.ts`: - concurrent append/delete/append interleave preserves invariants: `count()` and `list()` agree (no tombstone leak), no pre-delete entry survives, and the post-delete result is bounded by the two concurrent appends
- `formatted` rows in the AppendInput / TranscriptEntry TypeTables now render `FormattedContent | undefined` (the alias actually exported from `chat`), instead of `Root | undefined` which would force readers to pull the type from `mdast` directly. - `count` signature uses the new `CountQuery` named type, with a one-liner describing its single field.
The action handler was relabelled to `transcripts` when it was wired to `bot.transcripts.list`, but the leading comment still read "Demonstrate fetchMessages and allMessages" from the previous iteration. Update it to describe the transcripts demo.
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
dancer
approved these changes
May 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a new
bot.transcriptsAPI for cross-platform per-user message persistence,and renames the existing per-thread cache from
messageHistorytothreadHistoryso the two persistence concepts don't share a "messages" name.
What's new
Transcripts API (
bot.transcripts, gated onChatConfig.transcripts+ChatConfig.identity):append,list,count,deletekeyed by a stable cross-platform user keyresolved by
ChatConfig.identityonce per inbound message during dispatch(cached on the
Messageinstance asmessage.userKey).StateAdapter.appendToListprimitive, so every built-instate adapter (
memory,redis,ioredis,pg) supports it without changes.deleteis implemented via a tombstone written withappendToList(key, _, { maxLength: 1 })because
state.deleteonly addresses the k/v namespace;list()/count()filter the tombstone out. This avoids extending the
StateAdaptercontract.the underlying
appendToListprimitive can't support them safely underconcurrent writes.
messageHistory→threadHistoryrename (with full backwards compatibility):ChatConfig.messageHistoryChatConfig.threadHistoryAdapter.persistMessageHistoryAdapter.persistThreadHistoryMessageHistoryCacheThreadHistoryCacheMessageHistoryConfigThreadHistoryConfigmessage-history.tsthread-history.tsChatConfig.threadHistoryandChatConfig.messageHistoryare read atconstruction;
threadHistorytakes precedence when both are set.Adapter.persistThreadHistoryandAdapter.persistMessageHistoryareread at the persistence sites; either flag enables persistence.
MessageHistoryCache/MessageHistoryConfigre-exported as@deprecatedaliases of the new names.
msg-history:) is intentionallyunchanged — renaming it would silently orphan existing user data.
@chat-adapter/telegramand@chat-adapter/whatsappupdated to usepersistThreadHistory. Custom adapters built againstpersistMessageHistorycontinue to work.
Public types added/exported
TranscriptsApi,TranscriptEntry,TranscriptRole,TranscriptsConfig,AppendInput,AppendOptions,ListQuery,CountQuery,DeleteTarget,IdentityResolver,IdentityContext,DurationString,ThreadHistoryCache,ThreadHistoryConfig.ChatInstanceinterface gains areadonly transcripts: TranscriptsApisocallers typed against the public interface can reach
bot.transcripts.Docs
covering both
threadHistoryand the Transcripts API together with usage patterns.meta.json.Example
examples/nextjs-chat— AI mode handler (onSubscribedMessage) now persistsinbound user messages and assistant replies via
bot.transcripts, and readscontext from
bot.transcripts.list({ threadId })when the adapter doesn'texpose
fetchMessages. Newtranscriptsandclear-transcriptsactionbuttons demonstrate
list/count/delete.TEST_USER_KEYis hardcodedand clearly labelled for demo purposes.
Test plan
pnpm validate(knip + check + typecheck + 914 tests + build) — all 16 tasks passpnpm --filter chat test— 914 tests, 21 files, all greenpnpm --filter docs build— 113 static pages generated, no broken linksMigration notes
For consumers on the previous
messageHistoryAPI, no code changesrequired — the deprecated names continue to work and read the same data.
Tests, types, and JSDoc all carry
@deprecatedhints toward the new names.Checklist
pnpm validatepasses.changeset/transcripts-api.mdminor forchat;.changeset/thread-history-rename.mdminor forchat, patch for@chat-adapter/telegram+@chat-adapter/whatsapp)