* docs: misc updates and fixes
* docs: update feature matrix to show Teams modal support
---------
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
* fix(chat): export standalone reviver for workflow-safe deserialization
Importing the Chat instance into Vercel Workflow files pulls in adapter
packages that depend on Node.js modules, which aren't available in the
workflow sandbox. This adds a standalone `reviver` function exported from
`chat` that deserializes Thread/Channel/Message objects without needing
a Chat instance or its adapter dependencies.
Also updates the durable chat sessions guide to use the standalone
reviver and dynamic bot imports inside step functions.
Closes#243
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(chat): preserve adapterName on deserialized toJSON and add standalone reviver docs
* fix: sort imports in index.ts
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: dancer <josh@afterima.ge>
Co-authored-by: Ben Sabic <27636870+bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
* feat(chat): Enable disabling code-fencing for streamed tables per-adapter
As Slack now natively supports markdown tables, the code-fencing for
tables is not required and in fact stops them from being rendered
natively.
This PR adds an option to configure code-fencing tables per adapter.
By default true, false only for Slack.
* Update StreamingMarkdownRendererOptions comments
Removed comment about default behavior for wrapTablesForAppend option.
* feat(adapter-teams): add Select and RadioSelect support in card Actions
Map Select and RadioSelect card elements to Teams Adaptive Card
ChoiceSetInput (compact and expanded styles). Auto-inject a submit
button when inputs exist without explicit buttons, and fan out the
auto-submit payload into individual onAction calls per input.
* [chat] adding plan/update plan blocks for threads
* refactor plan impl into its own file
* thread.post() instead of thread.postPlan()
* fixes
* lint fixes
* refactor postable objects
* knip
* Refactor PostableObject into dedicated module and add fallback support (#191)
* feat: refactor adapter-slack and types based on Malte's comments
Move PostableObject to dedicated file, update type exports, add fallback posts, and make PostableObject generic.
Slack-Thread: https://vercel.slack.com/archives/C0977L169MW/p1772755978706959?thread_ts=1772755978.706959&cid=C0977L169MW
Co-authored-by: Vishal Yathish <135551666+visyat@users.noreply.github.com>
* fix: fallback path for PostableObject and clean up exports
- Plan mutations (addTask, updateTask, complete) now work in fallback
mode by using editMessage with fallback text instead of no-oping
- Revert Slack adapter to throw on unsupported kinds (caller handles
fallback, so adapter-level fallback strings are unnecessary)
- Remove redundant re-exports of isPostableObject/PostableObject from
plan.ts — export from canonical postable-object.ts only
- Stop exporting POSTABLE_OBJECT symbol (internal implementation detail)
- Fix nested ternary lint error in getFallbackText
- Add tests for fallback post/edit path
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: v0 <v0[bot]@users.noreply.github.com>
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Fall back to plain text instead of throwing for unsupported postable object kinds
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(chat): deduplicate PostableObject handling, improve type safety and test coverage
---------
Co-authored-by: v0 <v0[bot]@users.noreply.github.com>
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: dancer <josh@afterima.ge>
* feat(github): add support for issue comment threads
Handle issue_comment webhooks on plain issues, not just PRs.
Issue threads use the format github:owner/repo:issue:42.
* fix(github): propagate threadType in GitHubRawMessage for issue threads
parseMessage and other methods now correctly produce issue-format thread
IDs when the raw message originated from a plain issue, not a PR.
---------
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
* feat(teams): add dialog (task module) support
Teams dialogs require modal content to be returned inline in the HTTP
response when a task/fetch invoke fires. This adds:
- `actionType: "modal"` on buttons to emit msteams task/fetch hint
- `onOpenModal` hook on WebhookOptions for inline modal interception
- dialog.open/dialog.submit handlers in Teams adapter with Promise.race
- Modal-to-AdaptiveCard converter (modals.ts)
- Bridge adapter sends empty body (not "{}") for dialog close responses
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(teams): use @microsoft/teams.cards builders for Adaptive Cards
Replace hand-rolled plain JSON objects and local type definitions with
typed builder classes from @microsoft/teams.cards. This gives compile-time
type safety and eliminates the local AdaptiveCard/AdaptiveCardElement/
AdaptiveCardAction interfaces.
Also fix ephemeral modal button missing actionType="modal", which
prevented the dialog from opening on Teams.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(teams): preserve contextId in modal update/push responses
Pass the original contextId through to re-rendered modals so subsequent
submissions can still retrieve the stored thread/message/channel context.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: address review feedback across dialog support
- Clean up timeout timer in handleDialogOpen to prevent resource leak
- Also race on actionPromise so errors surface instead of silently timing out
- Extract buildContinueResponse helper to deduplicate update/push cases
- Use typed TextInputOptions/ChoiceSetInputOptions instead of Record<string, unknown>
- Make processSlashCommand options parameter explicit (WebhookOptions | undefined)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(chat): await storeModalContext before opening modal
The state write was fire-and-forget, so a fast dialog.submit could
arrive before the context was persisted, causing retrieveModalContext
to return empty. Also adds changeset for the new public API surface.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(chat): add missing disabled prop to ButtonProps JSX interface
The ButtonElement and ButtonOptions already supported disabled, but the
JSX ButtonProps interface was missing it, causing <Button disabled> to
silently drop the prop.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR review feedback for dialog support
- Add unit tests for modals.ts (16 tests) and modal button actionType in cards.test.ts
- Make dialog open timeout configurable via dialogOpenTimeoutMs in TeamsAdapterConfig
- Fix ModalSubmitHandler type to accept Promise<void> returns, remove @ts-expect-error from example
- Delete stored modal context after retrieval to prevent state adapter leaks
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: lint fixes and add X-User-Agent header to Teams adapter
- Fix import ordering, formatting, and non-null assertions in modals.test.ts
- Sort interface members in TeamsAdapterConfig
- Add X-User-Agent: Vercel.ChatSDK header to Teams SDK App client
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(adapter-slack): replace empty table cells with single space
Slack's Block Kit API rejects cells with empty text fields. Fall back
to a single space in both mdastTableToSlackBlock (markdown.ts) and
convertTableToBlocks (cards.ts) to satisfy the API constraint.
* chore: add changeset for slack empty table cells fix
---------
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
* Migrate Teams adapter from botbuilder to @microsoft/teams.apps
Replace botbuilder/botframework-connector with @microsoft/teams.apps (TeamsSDK v2.0.6).
Breaking changes:
- Remove certificate auth (TeamsAuthCertificate) — use token or managedIdentityClientId instead
- Env vars changed: TEAMS_APP_ID → CLIENT_ID, TEAMS_APP_PASSWORD → CLIENT_SECRET, TEAMS_APP_TENANT_ID → TENANT_ID
- Reactions now work (addReaction/removeReaction) instead of throwing NotImplementedError
- Graph API calls use @microsoft/teams.graph-endpoints typed endpoints
Key changes:
- BridgeHttpAdapter captures TeamsSDK route handler for serverless dispatch
- Event handlers registered via app.on() (message, messageReaction, card.action, conversationUpdate, installationUpdate)
- Outbound calls use app.send() and app.api.conversations.activities()
- Stream support via post+edit (native HttpStream when SDK exports it)
- Graph API uses typed endpoints from @microsoft/teams.graph-endpoints
- isMention detection via activity entities instead of bot name matching
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Align TeamsAdapterConfig with AppOptions from TeamsSDK
- Replace custom config fields with Pick<AppOptions, ...>
clientId (was appId), clientSecret (was appPassword), tenantId (was appTenantId)
- Remove TeamsAuthFederated, appType — use managedIdentityClientId directly
- Constructor passes config through to App (App handles env var resolution)
- Use this.app.id instead of this.config.appId for bot identity checks
- 3 replay-fetch-messages tests have known failures (type shape mismatch)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Remove api() getter, add handler type safety
- Remove useless private api() getter, use this.app.api directly
- Type handlers with specific activity types:
handleMessageActivity(ctx: IActivityContext<IMessageActivity>)
handleAdaptiveCardAction(ctx: IActivityContext<IAdaptiveCardActionInvokeActivity>)
handleReactionFromContext(ctx: IActivityContext<IMessageReactionActivity>)
- Remove unnecessary casts now that ctx.activity is properly typed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Revert bot.tsx example changes from migration branch
Keep Azure OpenAI and DM handler changes as local-only, not part of the migration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Revert example package.json from migration branch
Keep only adapters.ts config changes, revert package.json dep changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Remove unrelated skill files from migration branch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix
* Update README for TeamsSDK migration
- New env vars: CLIENT_ID, CLIENT_SECRET, TENANT_ID
- New config: clientId, clientSecret, tenantId, token, managedIdentityClientId
- Remove certificate auth docs (dropped)
- Reactions now supported (add/remove)
- Typing indicator now supported
- Graph API uses @microsoft/teams.graph-endpoints
- Note streaming and modals status
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Revert "Update README for TeamsSDK migration"
This reverts commit 729fe463c1.
* update readme
* Refactor adapter-teams: extract types, errors, and graph-api modules
Split the 2,114-line index.ts into focused modules:
- types.ts: TeamsAdapterConfig, TeamsThreadId, TeamsChannelContext
- errors.ts: handleTeamsError as standalone pure function
- graph-api.ts: TeamsGraphReader class with all Graph API read methods
index.ts shrinks to ~1,133 lines, delegating graph reads to TeamsGraphReader
which receives dependencies via constructor injection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Clean up tests
* Do not break public facing config.
* Use previous .env vars
* Fix tests
* Fix webhook concurrency issue
* Remove support for reactions
* Fetch aadgroup id if it doesn't exist
* Fix lint and formatting issues in Teams adapter
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix README to match public API, fix 403 error mapping, correct NotImplementedError args, remove stale test aliases, and add changeset
* Fix remaining mockBotAdapter references and trailing whitespace
* fix graph pagination
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
* feat(docs): add inline logo chips to hero description
Add brand logos (Slack, Microsoft Teams, Google Chat, Discord) as
inline chips prefixed before each product name in the hero section.
- Import existing SVG logos from lib/logos.tsx
- Create LogoChip component with whitespace-nowrap to prevent
wrapping between logo and its corresponding product name
- Update Hero component description type from string to ReactNode
- Keep plain text description for metadata/SEO
* fix(docs): logo chips line-height, comma wrapping, and spacing
- Use absolute positioning inside a zero-height inline-block container
so the logo doesn't increase line height
- Move trailing commas inside the whitespace-nowrap span via a suffix
prop to prevent hanging commas at line start
- Add 2px extra spacing on the right side of logos before names
* fix(docs): vertically center logos and add 2px left spacing
- Use align-middle + top-1/2 -translate-y-1/2 for true vertical
centering relative to the text line
- Add ml-[2px] on the outer nowrap span for left-side spacing
* fix(docs): widen hero description max-width on desktop
Bump from max-w-3xl (48rem) to sm:max-w-[52rem] so the first
product (Slack) stays on the first line at typical desktop widths.
* fix(docs): resolve lint issues
- Fix import order (react type import after next)
- Replace namespace import with named imports for logos
- Sort CSS classes per biome nursery rule
- Fix formatter line wrapping
---------
Co-authored-by: jamesvclements <jamesvclements@users.noreply.github.com>
* Fix Redis state typings for url and client options
* Wait for injected Redis clients to become ready
* Simplify Redis readiness guard
* Stabilize memory state TTL test with fake timers
* Handle Redis reconnect errors until client is ready
* Handle injected Redis client reconnects correctly
* feat: add concurrency strategies for overlapping messages (queue, debounce, concurrent)
## Problem
When multiple messages arrive on the same thread while a handler is still
processing, the SDK has only one behavior: **lock-and-drop**. The incoming
message is silently discarded (or force-released, which creates uncontrolled
concurrency). This is insufficient for most real-world use cases:
- **AI chatbots** lose user follow-up messages sent while the model is streaming
- **Customer support bots** miss messages entirely, breaking conversation flow
- **Collaborative editing bots** need to coalesce rapid corrections into one action
## Solution
Introduce a new `concurrency` option on `ChatConfig` with four strategies:
### `'drop'` (default, backward-compatible)
Existing behavior. Lock acquired or `LockError` thrown. No changes.
### `'queue'`
Messages that arrive while a handler is running are enqueued in the state
adapter. When the current handler finishes, the queue is drained: **only the
latest message is dispatched**, with all intermediate messages provided as
`context.skipped`. This gives the handler full visibility into what happened
while it was busy, without forcing it to re-process every message sequentially.
```typescript
const chat = new Chat({
concurrency: 'queue',
// ...
});
chat.onNewMention(async (thread, message, context) => {
if (context && context.skipped.length > 0) {
// "You sent 4 messages while I was thinking. Responding to your latest."
const allMessages = [...context.skipped, message];
// Pass all messages to the LLM for full context
}
});
```
Flow:
```
A arrives → acquire lock → process A
B arrives → lock busy → enqueue B
C arrives → lock busy → enqueue C
D arrives → lock busy → enqueue D
A done → drain: [B, C, D] → handler(D, { skipped: [B, C] })
D done → queue empty → release lock
```
### `'debounce'`
Every message (including the first) starts or resets a debounce timer. Only the
**final message in a burst** is processed. The lock-holding function stays alive
through `waitUntil` during the debounce window.
```typescript
const chat = new Chat({
concurrency: { strategy: 'debounce', debounceMs: 1500 },
// ...
});
```
Flow:
```
A arrives → acquire lock → store A as pending → sleep(debounceMs)
B arrives → lock busy → overwrite pending with B (A dropped)
C arrives → lock busy → overwrite pending with C (B dropped)
... debounceMs elapses with no new message ...
→ process C → release lock
```
### `'concurrent'`
No locking at all. Every message is processed immediately in its own handler
invocation. Suitable for stateless handlers (lookups, translations) where
thread ordering doesn't matter.
```typescript
const chat = new Chat({
concurrency: 'concurrent',
// ...
});
```
## API Surface
### ChatConfig
```typescript
interface ChatConfig {
concurrency?: ConcurrencyStrategy | ConcurrencyConfig;
/** @deprecated Use `concurrency` instead */
onLockConflict?: 'force' | 'drop' | ((threadId, message) => ...);
}
type ConcurrencyStrategy = 'drop' | 'queue' | 'debounce' | 'concurrent';
interface ConcurrencyConfig {
strategy: ConcurrencyStrategy;
maxQueueSize?: number; // Default: 10
onQueueFull?: 'drop-oldest' | 'drop-newest'; // Default: 'drop-oldest'
queueEntryTtlMs?: number; // Default: 90_000 (90s)
debounceMs?: number; // Default: 1500
maxConcurrent?: number; // Default: Infinity
}
```
### MessageContext (new, passed to handlers)
```typescript
interface MessageContext {
skipped: Message[]; // Intermediate messages, chronological
totalSinceLastHandler: number; // skipped.length + 1
}
```
All handler types (`MentionHandler`, `MessageHandler`, `SubscribedMessageHandler`,
`DirectMessageHandler`) now accept an optional `MessageContext` as their last
parameter. Existing handlers that don't use it are unaffected.
### StateAdapter (new methods)
```typescript
interface StateAdapter {
enqueue(threadId: string, entry: QueueEntry, maxSize: number): Promise<number>;
dequeue(threadId: string): Promise<QueueEntry | null>;
queueDepth(threadId: string): Promise<number>;
}
```
Implemented across all four state adapters:
- **MemoryStateAdapter**: in-process array
- **RedisStateAdapter**: Lua script (RPUSH + LTRIM + PEXPIRE)
- **IoRedisStateAdapter**: same Lua approach
- **PostgresStateAdapter**: new `chat_state_queues` table with atomic dequeue
## Architecture
`handleIncomingMessage` was refactored into composable pieces:
- `dispatchToHandlers()` — shared handler dispatch logic (mention detection,
subscription routing, pattern matching). Extracted from the old monolithic
method so all strategies reuse it.
- `handleDrop()` — original lock-or-fail path (preserves `onLockConflict` compat)
- `handleQueueOrDebounce()` — enqueue if busy, drain or debounce after
- `handleConcurrent()` — skip locking entirely
- `drainQueue()` — collect all pending, dispatch latest with skipped context
- `debounceLoop()` — sleep/check/repeat until no new messages arrive
## Queue Entry TTL
Queued messages have a configurable TTL (`queueEntryTtlMs`, default 90s). Stale
entries are discarded on dequeue with a `message-expired` log event. This
prevents unbounded accumulation and ensures handlers don't process messages
that are no longer relevant.
## Observability
All strategies emit structured log events at `info` level:
| Event | Strategy | Data |
|-----------------------|------------------|---------------------------------------|
| `message-queued` | queue | threadId, messageId, queueDepth |
| `message-dequeued` | queue, debounce | threadId, messageId, skippedCount |
| `message-dropped` | drop, queue | threadId, messageId, reason |
| `message-expired` | queue, debounce | threadId, messageId |
| `message-superseded` | debounce | threadId, droppedId |
| `message-debouncing` | debounce | threadId, messageId, debounceMs |
| `message-debounce-reset` | debounce | threadId, messageId |
## Backward Compatibility
- Default remains `'drop'` — zero breaking changes for existing users
- `onLockConflict` continues to work but is marked `@deprecated`
- Handler signatures are backward-compatible (new `context` param is optional)
- Deduplication always runs regardless of strategy
## Files Changed
- `packages/chat/src/types.ts` — new types, updated handler signatures
- `packages/chat/src/chat.ts` — strategy routing, drain/debounce loops
- `packages/chat/src/index.ts` — export new types
- `packages/chat/src/mock-adapter.ts` — queue methods for test mock
- `packages/state-memory/src/index.ts` — in-memory queue
- `packages/state-redis/src/index.ts` — Redis queue (Lua)
- `packages/state-ioredis/src/index.ts` — ioredis queue (Lua)
- `packages/state-pg/src/index.ts` — Postgres queue table
- `packages/chat/src/chat.test.ts` — tests for queue, debounce, concurrent
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: comprehensive test coverage for concurrency strategies and queue methods
Add tests across all state adapters and the Chat class:
**MemoryStateAdapter** (8 new tests):
- enqueue/dequeue single entry
- dequeue from empty queue returns null
- dequeue from nonexistent thread returns null
- queueDepth returns 0 for empty queue
- FIFO ordering across multiple entries
- maxSize trimming (keeps newest)
- maxSize=1 debounce behavior (last-write-wins)
- queue isolation by thread
- queue cleared on disconnect
**PostgresStateAdapter** (8 new tests):
- INSERT query for enqueue
- overflow trimming query
- depth return value
- parsed entry from dequeue
- null from empty dequeue
- atomic DELETE-RETURNING for dequeue
- queueDepth return value
- zero depth for empty queue
**RedisStateAdapter / IoRedisStateAdapter** (3+3 existence checks):
- enqueue, dequeue, queueDepth method existence
**Chat concurrency** (5 new tests):
- drop-newest policy when queue is full
- drop-oldest policy evicts oldest entries
- expired entries skipped during drain
- onNewMessage pattern handlers receive context
- onSubscribedMessage handlers receive skipped context
Total new tests: 27 (780 chat + 33 memory + 59 pg)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address feedback
* Support a channel locking strategy, make it default for WhatsApp and Telegram
* docs: fix typo "Committment" → "Commitment" (#274)
Co-authored-by: Arif Kobel <arif.kobel@phorax.com>
* Add webhook verification to GChat (#287)
- Issues a warning if required env vars are not present (also for telegram)
- Makes telegram use a time-safe verifier
* Make adapters depend on `chat` as a real dep (#289)
Without this, changeset will make any dep change a major change
* Version Packages (#290)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* Fix serialization
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Arif Kobel <102538661+ArifKobel@users.noreply.github.com>
Co-authored-by: Arif Kobel <arif.kobel@phorax.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>