343 Commits

Author SHA1 Message Date
Ben Sabic 6adca3617e feat(slack): support egress proxies (#916)
Adds proxy configuration for Slack connections that previously bypassed
`webClientOptions.agent`. Socket Mode now uses that agent for HTTP and
WebSocket connections, and new `fetch` and `fileTransport` options cover
response URLs, webhook forwarding, and attachment downloads.

```ts
createSlackAdapter({
  webClientOptions: { agent },
  fetch: proxyFetch,
  fileTransport,
});
```

Includes a setup example and guidance on the destination restrictions
custom download transports need to enforce.

Closes #596

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-09-09 17:46:53 +00:00
Ben Sabic f893470e19 fix(telegram): preserve links during truncation (#915)
Stops Telegram MarkdownV2 messages from being cut at a backtick inside a
link destination. Rendered text that fits the length limit now ships
exactly as it was rendered. The safe-boundary trimmer only runs on the
over-limit slice, where it removes an incomplete link, code span, or
underline before the ellipsis.

This covers message posts, edits, and attachment captions. The original
underscore URL case was already fixed; this closes the remaining
backtick and truncation cases.

Changes:

- Return text that fits the length limit unchanged instead of trimming
it
- Replace the per-marker scans with a single pass that groups delimiter
positions by marker and counts closed links
- Treat two backticks as a cut fence only at the end of the slice, so
empty inline code spans keep their trailing content
- Count a bare `]` as a link closer unless it ends the slice
- Pair `__` underline separately from `_` italic
- Test the trimmer directly, covering escaped `*`, `_`, `~`, empty code
spans, bare brackets, and underline
- Import the production helpers in the adapter tests instead of local
copies
- Update the changeset and docs paragraph

Closes #866

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-09-09 18:39:41 +01:00
Ben Sabic 2e2426d17e feat(teams): add installation lifecycle events (#914)
## summary

- add `onInstalled` and `onUninstalled` handlers for Teams personal,
group chat, and team installations, including upgrades that add or
remove the bot from the app manifest
- expose a persistable `event.channelId` for proactive messages through
`bot.channel(channelId).post()`, alongside raw platform metadata for
application-owned persistence
- route outbound operations through the service URL encoded in each
thread, preserving the explicit `apiUrl` override
- document stable bot, tenant, and team installation keys separately
from selected-channel destinations, and clean up for both `remove` and
`remove-upgrade`
- exercise the documented persistence example with regressions for
upgrade cleanup, team-scoped removal, reinstall replacement, tenant
isolation, and personal/group conversations

closes #847

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: dancer <josh@afterima.ge>
2026-09-09 18:30:10 +01:00
Ben Sabic ea025af7ac feat(postgres): allow migration-managed schemas (#913)
Adds `autoCreateSchema: false` so applications can manage PostgreSQL
tables and indexes through migrations and run the bot with restricted
database permissions.

```ts
createPostgresState({
  url: process.env.POSTGRES_URL,
  autoCreateSchema: false,
});
```

Automatic schema creation remains enabled by default. The docs include
the migration SQL and required grants. The expired-cache behavior
reported in the issue is already fixed in the base branch.

Closes #722

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-09-09 17:18:11 +00:00
Max 056d8830b2 feat(history): support uncapped per-user retention (#904)
## Summary

Allow `history.user.maxPerUser: false` and legacy
`transcripts.maxPerUser: false` to disable count-based eviction. Apps
that retain complete user history can keep entries beyond the default
200. Numeric limits still trim older entries, and retention TTL remains
independent.

## Test plan

Added coverage for retaining 205 entries with no cap, the default
200-entry cap, and legacy configuration merging. The existing
numeric-limit test remains. `pnpm validate` passed.

## Checklist

- [x] All commits are signed and verified
- [x] All commits are signed off for the DCO (`git commit -s`)
- [x] `pnpm validate` passes
- [x] Changeset added (or N/A — see
[CONTRIBUTING.md](./CONTRIBUTING.md))
- [x] Documentation updated (or N/A)

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-09-06 15:27:47 +10:00
‌ 31bce0a7a0 feat(whatsapp): expose typed API errors (#896)
- export `WhatsAppApiError` so consumers can handle Meta error codes
without parsing error messages
- expose HTTP status, provider details, optional subcode and trace ID,
and the raw response
- cover message requests, media uploads, and media metadata failures
while preserving existing error messages and `AdapterError`
compatibility
- add regression coverage and document error handling

closes #712

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-09-05 05:53:12 +00:00
‌ 7062c395d0 fix(teams): preserve outgoing mention text (#898)
- keep outgoing `@names` as plain text instead of generating `<at>`
markup without the mention entities Teams requires
- preserve multi-word names across plain text, raw, markdown, and AST
messages
- keep incoming mention decoding and explicit raw markup unchanged
- add formatter and send/edit regression tests and document that
plain-text names do not notify users

closes #853
2026-09-05 15:23:02 +10:00
‌ aaeede70be feat(teams): dispatch bot join events (#899)
- dispatch `onMemberJoinedChannel` when the bot joins a Teams channel or
group chat
- expose `botUserId` from the configured app identity
- preserve channel routing, inviter identity, and webhook `waitUntil`
tracking
- add regression tests and document the bot-only scope

addresses the bot-join portion of #847; personal install/uninstall hooks
remain separate

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-09-05 05:21:54 +00:00
Hiroki Osame 2cc8cc3f80 fix(slack): surface custom status text in the Agent messaging experience (#897)
## summary

- restore custom loading labels for `startTyping` and
`setAssistantStatus` under `agentView`, which stopped displaying after
#862 moved status updates to the native sessions API
- send custom labels through `assistant.threads.setStatus` with
`loading_messages`; `setAssistantStatus` preserves explicit arrays, then
configured defaults, then falls back to the custom status
- keep native `processing` and initiator attribution when
`startTyping()` has no custom status, and use native `active` when
clearing
- add regression coverage for message precedence, native routing,
clearing, and native API failures
- verify custom labels in DMs and channels, streamed completion, and
real native stop-button cancellation against locally built packages

## limitations

custom labels and native session state are not equivalent: in the test
workspace, the custom-label path displayed the requested text but did
not create a native processing session or stop button, even with
`agent_session_stopped` enabled

use `startTyping()` without custom text when native processing and stop
behavior are required; an existing native processing indicator can also
take precedence over a custom label

Slack's [native sessions API](https://docs.slack.dev/ai/agent-sessions/)
does not accept custom loading text, so this restores labels through the
[legacy status
endpoint](https://docs.slack.dev/reference/methods/assistant.threads.setStatus/)
without promising identical lifecycle behavior

---------

Co-authored-by: dancer <josh@afterima.ge>
2026-09-04 21:40:02 +00:00
Mohammed Mansoor Ahmed 4a0b5c0c3f feat(cards): add button tooltips and a card width hint (#895)
Buttons can now show hover text, and a card can ask to be rendered wider
than usual. Both are small hints: Teams renders them, and every other
adapter leaves the card exactly as it was before.

### Button tooltips

`Button` and `LinkButton` take an optional `tooltip`. On Teams it
appears when someone hovers over the button.

```tsx
<Card title="Deploy request">
  <Actions>
    <Button id="approve" style="primary" tooltip="Ships this build to production">
      Approve
    </Button>
    <LinkButton url="https://example.com/build/1234" tooltip="Opens the build log in your browser">
      View build
    </LinkButton>
  </Actions>
</Card>
```

The same option is available on the plain builder functions:

```ts
Button({ id: "approve", label: "Approve", tooltip: "Ships this build to production" })
```

Tooltips survive the `callbackUrl` flow too. When a button's callback
URL is swapped for a token before the card is sent, every other field on
the button is kept, so a tooltip on a callback button shows up just like
one on a regular button.

### Full-width cards

`Card` takes an optional `width`, either `"default"` or `"full"`. Teams
draws a `"full"` card wider than its usual size, which suits tables and
digests. It does not stretch the card across the whole chat pane, that
is how Teams defines full width.

```tsx
<Card title="Weekly digest" width="full">
  <Table headers={["Service", "Uptime"]} rows={[["api", "99.98%"], ["web", "99.95%"]]} />
</Card>
```

### What Teams receives

- `tooltip` becomes the `tooltip` on the Adaptive Card action, for both
submit and open-URL buttons.
- `width="full"` becomes `msteams: { width: "full" }` on the card.
- The card now declares Adaptive Card version 1.5, which is the version
that introduced action tooltips. Teams accepts cards up to 1.6 for bots,
so nothing changes for existing cards beyond the version number.
- The runtime-free `@chat-adapter/teams/cards` helpers understand both
new fields as well, so apps that build Teams cards without the full
adapter get the same result.

### Why only Teams

Slack and Google Chat have no hover text for buttons. They do have
screen-reader labels, but those replace the button text for assistive
technology rather than adding to it, so mapping a tooltip onto them
would change what a screen reader announces. The fields are documented
as Teams-only for that reason.

### Small cleanup along the way

The JSX runtime used to decide whether a set of props belonged to a
`Card` by checking for a fixed list of prop names. Any new `Card` prop
that was not on that list was silently dropped. Since `Card` is the only
component left once every other one has been matched, the props are now
used directly and the list is gone.

Docs for both props are on the cards page and in the API reference.

---------

Signed-off-by: Mohammed Mansoor Ahmed <mansoorahmed.mohammed@gmail.com>
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-09-04 22:08:15 +10:00
psychomet 043386b52c feat(telegram): add Business mode support (#888)
- Adds Telegram Business mode support to `@chat-adapter/telegram`
- Handles `business_connection`, `business_message`, and
`edited_business_message` updates
- Passes `business_connection_id` on outbound sends, edits, typing, and
file uploads
- Opt-in via `businessMode: true` (default off, backward compatible)

Closes #887

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-09-04 04:03:01 +00:00
dcbuilder.eth d4a1f03afc fix(slack): rotate long native streams before expiry (#884)
Slack expires native streams after roughly five minutes. Finalize
long-running streams after four minutes by default and continue in a
fresh segment so late appends do not fail with
message_not_in_streaming_state. Preserve open fenced code blocks by
closing and reopening them across the segment boundary. The threshold is
configurable with streamSegmentMaxAgeMs.

---------

Signed-off-by: dcbuild3r <dcbuilder@pm.me>
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-09-04 10:54:00 +10:00
Ben Sabic f485255bcf fix(adapters): harden webhook tenant isolation (#877)
Multi-workspace Slack now ignores commands and interactions when their
installation cannot be found, and channel names stay isolated per
workspace.

Google Chat no longer learns its identity from incoming mentions, and
forward history reads use bounded native pagination.

Webhook logs avoid message content. The example app protects preview
routing, records only successful verified deliveries, and caps recording
size and retention.

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Signed-off-by: dancer <josh@afterima.ge>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: dancer <josh@afterima.ge>
2026-09-03 17:30:32 +01:00
Ben Sabic b7c9316bfd fix(chat): tighten conversation boundaries (#875)
Agent tools now keep user profile lookups behind approval and apply
conversation scope to typing indicators.

Discord thread targets are checked against their parent channel, and
private slash-command follow-ups stay private. Twilio direct messages no
longer share history or scope across recipients.

Queued messages restore their own conversation context before handlers
run. Callback tokens are bound to their action and conversation, expire
sooner, and can only be used once.

Link preview metadata is clearly marked as untrusted and bounded before
it reaches an AI prompt.

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Signed-off-by: dancer <josh@afterima.ge>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: dancer <josh@afterima.ge>
2026-09-03 17:10:58 +01:00
Max a8de95bcc4 fix(teams): infer missing conversation types (#879)
## Summary

Teams can omit `conversationType` while still sending
`conversation.isGroup`. Use `isGroup` and team context as a fallback so
`a:`-prefixed group chats are not treated as DMs. An explicit
`conversationType` still takes precedence.

## Test plan

- `pnpm validate`
- `pnpm --filter @chat-adapter/teams test`, 276 tests passed

## Checklist

- [x] All commits are signed and verified
- [x] All commits are signed off for the DCO with `git commit -s`
- [x] `pnpm validate` passes
- [x] Changeset added
- [x] Documentation updated

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-09-04 00:25:29 +10:00
Ben Sabic 7609d8f60e fix(adapters): validate external request targets (#876)
Adapters now reject untrusted destinations before sending credentials,
message content, or attachment requests.

Teams Connector and Graph calls stay on known Microsoft hosts, Instagram
downloads stay on trusted Meta hosts, and Slack response URLs are
checked before use.

XChat now handles CRC challenges itself and rejects tokens that could be
reused to forge webhook signatures.

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-31 17:13:03 +00:00
Noppakorn Kaewsalabnil f691ad5848 docs: add LINE community adapter (#873)
## Summary

- Add `chat-adapter-line` to the community adapter catalog.
- Add a hand-authored LINE adapter docs page with configuration,
webhook, messaging, and feature-matrix details.
- Register `chat-adapter-line` as a valid docs code-example import.

## Validation

- `pnpm --filter @chat-adapter/integration-tests test --
src/docs-adapters.test.ts --coverage=false` — 467 tests passed
- `pnpm --filter @chat-adapter/integration-tests test --
src/docs-content.test.ts --coverage=false` — 91 tests passed
- `pnpm --filter @chat-adapter/integration-tests test --
src/docs-llms.test.ts --coverage=false` — 145 tests passed
- `pnpm exec biome check apps/docs/content/adapters/community/line.mdx
apps/docs/content/adapters/community/meta.json apps/docs/adapters.json
packages/integration-tests/src/documentation-test-utils.ts` — passed

Signed-off-by: PunGrumpy <108584943+PunGrumpy@users.noreply.github.com>
2026-08-30 18:42:17 +10:00
Ben Sabic 894fc7c7b3 docs: redirect conversation-history and update Vercel Connect page (#870)
Redirects /docs/conversation-history to /docs/history, since the History
guide already covers the old transcripts content. Removes the Vercel
Connect beta callout and adds related links to the Chat SDK docs and The
Complete Guide to Vercel Connect.

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 10:31:31 -07:00
OSS Polar Bear 75cadbf9aa feat(twilio): add RCS support for interactive inbound and rich outbound (#590)
Extend the Twilio adapter with RCS webhook parsing, Content API
integration, and card-to-template mapping with SMS fallback.

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 19:57:44 +10:00
OSS Polar Bear 169788b65a feat(chat): introduce unified History API with user, thread, and chan… (#592)
Adds `bot.history` as the canonical entry point for message history,
with three scopes: `user`, `thread`, and `channel`. `bot.transcripts`
stays as a deprecated alias, so nothing breaks.

## Why

History access was spread across `bot.transcripts`, `thread.messages` /
`thread.allMessages`, and per-adapter calls. `bot.history` puts the
promise-based read paths in one place, and the AI tools
(`fetchMessages`, `fetchChannelMessages`, `listThreads`) now route
through it.

## User scope

Cross-platform per-user persistence, identical in surface to
`bot.transcripts`:

```typescript
const bot = new Chat({
  adapters: { slack, telegram },
  state,
  history: {
    user: {
      identity: ({ author }) => author.email ?? null,
      retention: "30d",
      maxPerUser: 200,
    },
  },
});

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

The new `toPromptEntries` helper turns those entries into `{ role,
content }` messages for an LLM call:

```typescript
import { toPromptEntries } from "chat";

const entries = await bot.history.user.list({ userKey });
const { text } = await generateText({
  model,
  messages: toPromptEntries(entries),
});
```

## Thread scope

Single-page reads and an auto-paginating generator:

```typescript
// One page, newest messages by default
const { messages, nextCursor } = await bot.history.thread.list(thread.id, {
  limit: 20,
});

// Everything, oldest first, pagination handled for you
for await (const msg of bot.history.thread.collect(thread.id, { limit: 50 })) {
  console.log(msg.text);
}
```

## Channel scope

```typescript
// Top-level channel messages (not thread replies)
const { messages } = await bot.history.channel.listMessages("slack:C123", {
  limit: 20,
});

// Thread listings
const { threads } = await bot.history.channel.listThreads("slack:C123");

// Threads together with a page of messages each
const result = await bot.history.channel.listThreadsWithMessages("slack:C123", {
  maxThreads: 5,
  messagesPerThread: 10,
});
```

## Semantics

The read paths are strict about where data comes from:

- The adapter named in the ID prefix must be registered. A typo'd or
unknown prefix throws instead of reading as an empty conversation.
- The SDK-side `ThreadHistoryCache` only serves adapters that persist
history there (`persistThreadHistory: true`, e.g. Telegram, WhatsApp).
For every other adapter the platform response is authoritative, so an
empty page is a real empty page, and a `cursor` always returns the
adapter's response as-is.
- Cache reads honor the same windows as adapter reads: backward
(default) gives the newest N, forward the oldest N, and `collect()`
yields the oldest N on both paths.
- `channel.listMessages` throws a capability error on adapters without
`fetchChannelMessages` (persisting adapters are served from the
channel-keyed cache instead), and `listThreadsWithMessages` fetches
per-thread pages through `history.thread.list` a few threads at a time
to stay inside platform rate limits.

## Migration

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

// After
const bot = new Chat({
  history: {
    user: {
      identity: ({ author }) => author.email ?? null,
      retention: "30d",
      maxPerUser: 200,
    },
  },
});
await bot.history.user.append(thread, msg);
```

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, so settings left on `transcripts` keep applying
until you move them. `TranscriptEntry` is deprecated in favour of
`HistoryEntry` (also exported as `UserHistoryEntry`); all deprecated
names keep working in the current major version.

## Included

- New `packages/chat/src/history/` module with unit tests for every
scope
- AI tools rewired to `bot.history`, keeping their scope guards
- The nextjs example uses the new APIs throughout, with Thread History
and Channel History test buttons that exercise every scope
- Docs: `/docs/history` guide, `/docs/api/history` reference,
deprecation callouts on the transcripts pages
- Changeset (`minor` for `chat`)

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 18:47:26 +10:00
dependabot[bot] 4fa1c2bcf9 build(deps-dev): bump postcss from 8.5.25 to 8.5.26 (#795)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.25 to
8.5.26.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/releases">postcss's
releases</a>.</em></p>
<blockquote>
<h2>8.5.26</h2>
<ul>
<li>Fixed <code>list.split()</code> regression (by <a
href="https://github.com/lazerg"><code>@​lazerg</code></a>).</li>
<li>Track symlinks in path protection in source map loading (by <a
href="https://github.com/drengir1"><code>@​drengir1</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/blob/main/CHANGELOG.md">postcss's
changelog</a>.</em></p>
<blockquote>
<h2>8.5.26</h2>
<ul>
<li>Fixed <code>list.split()</code> regression (by <a
href="https://github.com/lazerg"><code>@​lazerg</code></a>).</li>
<li>Track symlinks in path protection in source map loading (by <a
href="https://github.com/drengir1"><code>@​drengir1</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/postcss/postcss/commit/07b25773f38f77919f2af02ae3e8896b0deb5988"><code>07b2577</code></a>
Release 8.5.26 version</li>
<li><a
href="https://github.com/postcss/postcss/commit/47de6b9d7c55674cb326c5de7a734a740916defc"><code>47de6b9</code></a>
Update CI</li>
<li><a
href="https://github.com/postcss/postcss/commit/1493a83db7830912316512f55ab6064e7b7dd68e"><code>1493a83</code></a>
Fix Rule#selectors losing the empty selector (<a
href="https://redirect.github.com/postcss/postcss/issues/2129">#2129</a>)</li>
<li><a
href="https://github.com/postcss/postcss/commit/180db166e250d20e6761b224ae8d8134c9ba3e40"><code>180db16</code></a>
Typo</li>
<li><a
href="https://github.com/postcss/postcss/commit/29e9e00f132c96e46e1de295b816fe88a05354e7"><code>29e9e00</code></a>
Resolve symlinks before the previous-source-map containment check (<a
href="https://redirect.github.com/postcss/postcss/issues/2125">#2125</a>)</li>
<li><a
href="https://github.com/postcss/postcss/commit/3ba8f84703a884329b58abea579c3615684e0b7e"><code>3ba8f84</code></a>
Update dependencies</li>
<li><a
href="https://github.com/postcss/postcss/commit/87e72f671fd0d401c52822b5226c656632d92ec0"><code>87e72f6</code></a>
Update lock file</li>
<li><a
href="https://github.com/postcss/postcss/commit/caaeeb907e4a816c44a23b00b151882bd02325a1"><code>caaeeb9</code></a>
Upgrade nanoid to fix infinite loop on zero size (<a
href="https://redirect.github.com/postcss/postcss/issues/2124">#2124</a>)</li>
<li><a
href="https://github.com/postcss/postcss/commit/3609b6f4296952d0b5b9ddae42c8d73ee460c041"><code>3609b6f</code></a>
Explain how to type plugin options</li>
<li><a
href="https://github.com/postcss/postcss/commit/fbad419cbd01cd7a9a1a46413447f2cd9b3fce4a"><code>fbad419</code></a>
docs: show ESM and TypeScript plugin declaration (<a
href="https://redirect.github.com/postcss/postcss/issues/2118">#2118</a>)</li>
<li>See full diff in <a
href="https://github.com/postcss/postcss/compare/8.5.25...8.5.26">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 17:53:22 +10:00
Yevanchen 5b538f6f21 fix(chat): keep thread locks alive during long handlers (#821)
- renew a held thread or channel lock every 10 seconds while a locking
concurrency strategy is running
- stop the heartbeat before releasing the lock, and handle extension
failures without unhandled rejections
- add regression coverage proving `queue`, `burst`, and `debounce`
remain serialized when a handler exceeds the 30-second lock TTL
- keep the existing short TTL, so a crashed process still releases its
lock automatically

Mosoo Agents hit this with Chat SDK's Telegram adapter while waiting on
long-running Codex Agent handlers. Once a handler crossed 30 seconds, a
later Telegram message could acquire an expired channel lock and run
concurrently on the same conversation.

Fixes #685.

---------

Signed-off-by: Yevanchen <cyefan2@gmail.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 17:39:26 +10:00
I'm Groot 🌳 26a06ca51d feat(telegram): treat a reply to the bot as a mention (#834)
Based on #833.

In a group a bot only sees messages that address it, and people address
a bot by replying to it as often as by typing its handle. The adapter
reported `isMention` for the handle but not for the reply, so a bot went
quiet the moment the conversation moved to replies.

`mentionOnReply` turns that on. **Off by default** — the flag changes
which messages report `isMention`, and a bot that deliberately answers
only explicit mentions should keep the stricter behaviour. It also reads
`TELEGRAM_MENTION_ON_REPLY`, so a deployment can set it without code,
and the key is declared in the adapters catalog.

The check runs before the empty-text guard, so a reply carrying only a
photo or a document counts too.

---------

Signed-off-by: grootbro <vadim@ravefox.dev>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 12:15:32 +10:00
I'm Groot 🌳 d5ebec127b feat(telegram): implement native message replies (#833)
`Thread.reply()` throws `NotImplementedError` on Telegram: the adapter
has no `reply` method, even though the Bot API threads an answer to its
question with `reply_parameters`.

`postMessage` takes an optional reply target and passes it to every send
path — text, rich messages, documents, attachments and both media group
variants — and `reply()` delegates to it, the same shape the WhatsApp
adapter uses for this contract. The target is decoded through the
existing `decodeCompositeMessageId`, so a target from another chat is
rejected exactly as an edit would be.

`allow_sending_without_reply` is set: a deleted target degrades to an
unthreaded message instead of failing the send.

Three tests cover it: the reference lands on a reply, a plain
`postMessage` stays unthreaded, and a target from another chat is
refused.

---------

Signed-off-by: grootbro <vadim@ravefox.dev>
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 01:16:28 +10:00
Mahdi Jaafar 500b7e6d2c fix(web): prevent tool approval bypass via client-supplied messages array (#857)
Hardens two trust boundaries reported against the framework: the web
adapter derived conversation state from the client-supplied
`body.messages` array, and the AI SDK write tools skipped the
conversation scope check that read tools already enforced.

## Web adapter: client-supplied messages

`handleWebhook` previously accepted the full `useChat` `messages` array
from the browser. A client could forge tool-call and approval parts in
it, and handlers reading `message.raw` would see that forged state as if
the server had produced it.

The adapter now:

- consumes only the latest user message and ignores the rest of the
array
- strips tool parts from that message, so forged tool-call or approval
state never reaches handlers; text, file, and custom `data-*` parts pass
through to `message.raw` unchanged
- returns 400 when nothing usable remains after stripping
- no longer passes `originalMessages` to `createUIMessageStream`
(nothing registers `onFinish`, so it was never consumed; prior turns
come from the state adapter via `persistMessageHistory`, never from the
request body)

## AI SDK tools: scope on writes

`createChatTools` now runs the same scope guard on write tools that read
tools already used. A thread or channel id the model supplies that
resolves outside the scoped conversation is rejected before the write
executes. The guard is threaded through each tool factory
(`ToolOptions.guard`) rather than wrapped around `execute`, so it is
typed against each tool's input schema and a future tool can't ship
unguarded.

`sendDirectMessage` targets a user id rather than a conversation, so the
guard has nothing to check it against; it stays gated by approval, and
the docs now say so explicitly.

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 00:01:50 +10:00
Ben Sabic b6fa24c68f fix(adapters): guard attachment downloads across slack, discord, telegram, and whatsapp (#865)
Follows up on #850, #856, and #859 by adopting the shared guarded
downloader (`downloadAttachment` in `@chat-adapter/shared`) in the
remaining adapters that fetch attachment bytes from event-supplied URLs.

- Slack, Discord, and WhatsApp attachment downloads now refuse private
and internal addresses (as URL literals, through DNS resolution, and
after redirects), cap responses at 25 MB, and time out after 30 seconds.
- Slack sends the bot token only on hops to trusted Slack origins, so a
redirect can never carry it to another host, and keeps the
HTML-login-page detection. A protected `createFileTransport()` override
routes downloads through a proxy.
- WhatsApp keeps its access token on Meta's media hosts, and the
configured Graph origin via the hosts allowlist; `downloadMedia()`
accepts a custom transport.
- Telegram keeps downloads on the Web Fetch API because a downstream
Cloudflare Workers consumer depends on portability (#828), enforcing the
same 25 MB cap and 30-second timeout with web streams.
- `downloadAttachment` now resolves `headers` per hop (function form
decides what each redirect target receives), forwards the resolved
headers to custom transports, and accepts an `onResponse` hook that can
reject a final response before its body is read.
- Adds "Inbound attachments" docs sections for all four adapters.

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-27 13:05:06 +10:00
Ben Sabic 2ce2be008f feat(slack): add Agent Sessions lifecycle and native stop (#862)
Migrates Slack's `agent_view` integration to the Agent Sessions
lifecycle while preserving the legacy `assistant_view` compatibility
path.

- Adds `agents.sessions.setStatus` and `agents.sessions.rename` support
for processing, active, suspended, and closed sessions.
- Handles `agent_session_stopped` without taking the message lock,
clears Slack's processing state, and dispatches `onAgentSessionStopped`.
- Adds cross-process turn cancellation through the configured state
adapter and exposes the active turn as `thread.signal`.
- Handles `agent_session_title_changed` and automatically titles new
agent conversations from their root message, with a configurable
resolver.
- Propagates `session_status` through native stream completion and
supports suspended human-in-the-loop turns.
- Updates Slack manifests, examples, API docs, fixtures, and migration
guidance for the February 2027 `assistant_view` retirement.

Configure the Agent messaging experience and optional title resolver:

```ts
const slack = createSlackAdapter({
  agentView: true,
  sessionTitle: ({ text }) => text.split("\n", 1)[0]?.slice(0, 80) ?? null,
});
```

Pass the thread signal into model generation so Slack's native stop
button cancels upstream work as well as message delivery:

```ts
bot.onDirectMessage(async (thread, message) => {
  await thread.startTyping();

  const result = await agent.stream({
    prompt: message.text,
    abortSignal: thread.signal,
  });

  await thread.post(result.fullStream);
});
```

React to session lifecycle events:

```ts
bot.onAgentSessionStopped(async (event) => {
  await releaseExternalResources(event.threadId);
});

bot.onAgentSessionTitleChanged(async (event) => {
  await syncTitle(event.threadId, event.title);
});
```

Leave a stream suspended when the agent needs user input or approval:

```ts
await thread.post(
  new StreamingPlan(result.fullStream, {
    sessionStatus: "suspended",
  })
);
```

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-27 10:03:14 +10:00
christopherkindl 50af1605d5 chore(docs): use geistdocs 1.23.1 (#864)
Uses `@vercel/geistdocs@1.23.1`, which includes the desktop navbar fix:
clicking an open navigation trigger closes its menu.

Release:
https://github.com/vercel/geistdocs/releases/tag/%40vercel%2Fgeistdocs%401.23.1

## Validation
- `pnpm install --lockfile-only --ignore-scripts`
- `git diff --check`
2026-08-25 21:30:51 +10:00
josh 153bd9640d fix(messenger): guard attachment downloads (#856)
## summary

- restrict Messenger attachment downloads to Meta's `fbsbx.com` and
`fbcdn.net` hosts while preserving external URLs on `attachment.url`
- reject untrusted URLs before connecting using HTTPS validation,
connection-bound DNS checks, manual redirect validation, timeouts, and
streamed size limits
- move the guarded downloader into `@chat-adapter/shared` and keep the
Teams implementation behaviorally equivalent
- normalize malformed redirect locations and other download failures as
typed `NetworkError` values
- document the inbound attachment policy for Messenger
- stacked on #850 and should merge after it

## test plan

- verified valid Meta image, audio, video, and file CDN hosts remain
downloadable
- verified external hosts, private addresses, malformed URLs, unsafe
ports, trailing dots, and suffix attacks are rejected
- verified mixed private and public DNS results fail closed
- verified redirects are revalidated and malformed or external
destinations are rejected
- verified declared and streamed size limits and stalled body timeouts
- ran workspace build, affected package tests and typechecks,
integration checks, Knip, Ultracite, and diff validation

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-25 21:30:21 +10:00
josh bb926884a2 fix(teams): secure attachment downloads (#850)
## summary

- restrict anonymous attachment downloads to current Microsoft 365
SharePoint and OneDrive for Business hosts
- reject internal addresses using connection-bound DNS validation
- revalidate every redirect and disable connection reuse outside the
guarded transport
- enforce a 25 MB streaming response limit and a 15 second request
timeout
- preserve connector-origin bot authentication and the protected custom
fetch override
- document the default anonymous download policy

## test plan

- verify trusted Microsoft 365 attachment hosts remain supported
- verify HTTP, custom ports, lookalike domains, trailing-dot hosts, and
generic off-origin URLs are rejected
- verify private IPv4, encoded IPv4, bracketed IPv6, and mixed DNS
results are rejected
- verify redirects are revalidated before another request
- verify oversized streamed responses are stopped
- verify activity parsing and attachment rehydration use the guarded
transport
- run Teams tests, typecheck, formatting, and production builds

---------

Signed-off-by: dancer <josh@afterima.ge>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-25 21:09:08 +10:00
Max eddcd7e46b fix(telegram): return portable file data (#828)
## Summary

Telegram file downloads already receive their bytes from the Web Fetch
API as an `ArrayBuffer`, but the adapter immediately converts them with
`Buffer.from(...)` before returning. That conversion is unnecessary for
consumers that accept web-standard binary data, and it throws when the
Node `Buffer` global is unavailable. The [Fetch
standard](https://fetch.spec.whatwg.org/#dom-body-arraybuffer) defines
`Response.arrayBuffer()` as returning an `ArrayBuffer`; Cloudflare
Workers exposes the [Fetch API
natively](https://developers.cloudflare.com/workers/runtime-apis/fetch/),
while `Buffer` belongs to its [Node.js compatibility
surface](https://developers.cloudflare.com/workers/runtime-apis/nodejs/buffer/).

This change returns the fetched `ArrayBuffer` directly from Telegram.
The shared `Attachment.fetchData` and protected Telegram method use
`Buffer | ArrayBuffer` so existing adapters and subclasses that return
`Buffer` remain source-compatible. The two consumers of that contract
now accept the portable value: `chat/ai` passes `ArrayBuffer` directly
to the AI SDK, and the X adapter normalizes either type at its
Buffer-based upload boundary. The public file documentation and patch
changesets are updated with the same contract.

The downstream evidence is a pnpm patch in the private Calories
Cloudflare Workers consumer at
`patches/@chat-adapter__telegram@4.36.0.patch`. Its portability hunk
changes `downloadFile` from `Promise<Buffer>` to `Promise<ArrayBuffer>`
and changes `Buffer.from(await response.arrayBuffer())` to
`response.arrayBuffer()`; the other Telegram hunks in that patch are
already upstream and are intentionally excluded here.

## Test plan

- `pnpm validate`
- `pnpm --filter @chat-adapter/telegram test` (269 tests)
- `pnpm --filter @chat-adapter/telegram typecheck`
- `pnpm --filter chat test` (1,131 tests)
- `pnpm --filter chat typecheck`
- `pnpm --filter @chat-adapter/x test` (222 tests)
- `pnpm --filter @chat-adapter/x typecheck`
- Added a regression test that removes the global `Buffer`, exercises
Telegram's mocked `getFile` and file-fetch path, and asserts the
returned bytes are an `ArrayBuffer`.

The runtime proof is limited to the isolated download seam under Node
with `Buffer` removed. This PR does not claim a deployed
no-compatibility Cloudflare Worker or a live Telegram end-to-end
request.

## Checklist

- [x] All commits are signed and verified
- [x] All commits are signed off for the DCO (`git commit -s`)
- [x] `pnpm validate` passes
- [x] Changeset added (or N/A — see
[CONTRIBUTING.md](./CONTRIBUTING.md))
- [x] Documentation updated (or N/A)

---------

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-08-25 21:03:46 +10:00
Max 63997acaa8 fix(teams): hydrate incoming users without Graph (#860)
## Summary

Changes live incoming Teams author hydration to
`ctx.api.conversations.getMemberById`, so the normal path no longer
requires Microsoft Graph's `User.Read.All` permission or tenant admin
consent. Explicit `getUser()` lookups remain Graph-backed.

## Test Plan

- `pnpm --filter @chat-adapter/teams test` (264 passed)
- `pnpm --filter @chat-adapter/teams exec vitest run src/index.test.ts
-t 'incoming sender email'` (8 passed)
- `pnpm --filter @chat-adapter/teams typecheck`
- `pnpm --filter @chat-adapter/teams... build`
- `pnpm check`
- `git diff --check`
- built and packed `@chat-adapter/teams`; inspected the artifact for
both the Connector lookup and preserved Graph lookup

The regression tests assert the exact activity conversation and sender
IDs, Graph isolation on Connector success and failure, cache behavior,
the missing-AAD fallback, and the DM path. A live Microsoft Teams tenant
was not available for runtime verification.

## Checklist

- [x] All commits are signed and verified
- [x] All commits are signed off for the DCO (`git commit -s`)
- [ ] `pnpm validate` passes
- [x] Changeset added (or N/A — see
[CONTRIBUTING.md](./CONTRIBUTING.md))
- [x] Documentation updated (or N/A)

---------

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-08-25 20:45:27 +10:00
christopherkindl ea716568fa chore(docs): update geistdocs to 1.24.0 (#863)
Updates the docs app to `@vercel/geistdocs@1.24.0` and refreshes the
pnpm lockfile.

Includes improved agent recovery and discovery from
https://github.com/vercel/geistdocs/pull/255.

## Validation
- `pnpm install --lockfile-only --ignore-scripts`
- `git diff --check`
2026-08-25 20:16:22 +10:00
christopherkindl a0084cb6e2 chore(docs): update geistdocs to 1.23.1 (#861)
Updates the docs app to `@vercel/geistdocs@1.23.1` and refreshes the
pnpm lockfile.

## Validation
- `pnpm install --lockfile-only --ignore-scripts`
- `git diff --check`
2026-08-25 20:02:38 +10:00
josh c4a359e7e9 fix(telegram): require webhook verification by default (#858)
## summary

- require `secretToken` when Telegram resolves to webhook mode
- reject unverified messages and callback queries before dispatch
- add `allowUnverifiedWebhooks` as an explicit escape hatch for local
fixtures or trusted upstream verification
- preserve polling without requiring webhook credentials
- deduplicate every accepted webhook update
- update adapter docs, configuration metadata, and integration fixtures

---------

Signed-off-by: dancer <josh@afterima.ge>
2026-08-25 20:02:02 +10:00
Rich Haines b7e4bbfbb0 chore(docs): upgrade Geistdocs to 1.22.0 (#855)
## Summary

Upgrades the chat-sdk.dev docs app from `@vercel/geistdocs` **1.20.4 →
1.22.0** (published 2026-08-21, Apache-2.0) and `next` **16.2.11 →
16.3.1**, following the bundled 1.22.0 template as the source of truth.

The target release includes all of the behavior-changing PRs for this
round:

- vercel/geistdocs#245 — require Next.js 16.3, scaffold 16.3.1, drop the
dev filesystem-cache flag (shipped in 1.21.1)
- vercel/geistdocs#246 — Cache Components across Geistdocs
- vercel/geistdocs#249 — Partial Prefetching + instant docs navigation
- vercel/geistdocs#250 — stable Next 16.3 APIs, retryable page/Ask AI
error boundaries, full prefetch of package links, no generic page shell
- vercel/geistdocs#251 — tree sidebar preserves scroll position on
folder toggles

## Adapter and configuration changes

- `next.config.ts`: `cacheComponents: true`, `partialPrefetching: true`;
removed `experimental.turbopackFileSystemCacheForDev` (default in 16.3).
Redirects, `/sitemap.xml` rewrite, and image config unchanged.
- New `lib/geistdocs/root-params.ts`; all layouts read `[lang]` via
`next/root-params` instead of `params`. Route handlers keep
route-context `params`.
- Root layout gains `generateStaticParams` returning every configured
language (`en`) — home (`/`), `/adapters`, and `/resources` now
prerender statically (previously dynamic).
- Route adapters no longer re-export `revalidate`/`dynamic` from package
factories (`agents.md`, `sitemap.md`, `llms.mdx`), and custom routes
drop their own `revalidate` exports (`llms.txt`, `llms-full.txt`,
`adapters.mdx`, `rss.xml`, `resources`).
- `llms.mdx` adopts the template form: `sources: [geistdocsSource]` +
`notFound: {}`, enabling smart agent-readable 404/410 responses with
real HTTP statuses.
- `rss.xml` migrated to the template's `"use cache"` +
`cacheLife("max")` form with `getPublicPath` base-path handling.
- `resources` page: `revalidate = 86400` → `"use cache"` +
`cacheLife("days")` (same 1-day lifetime).
- App-owned data fetching moved off `next: { revalidate }` (unsupported
under Cache Components): GitHub README fetches and homepage OSS stats
now use `"use cache"` + `cacheLife("hours")`.
- Homepage Shiki highlighting (`Demo`, `CodePreview`, `highlightCode`)
runs inside `"use cache"` scopes — Shiki reads `Date.now()` internally,
which otherwise fails prerendering.
- App-owned links to statically generated docs pages get
`prefetch={true}` (platform grid, feature matrix, adapter slug list,
"Visit Documentation") per the template's agent guidance; package-owned
sidebar/prev-next links already prefetch fully in 1.22.0.
- `Analytics`/`SpeedInsights` moved into
`components/geistdocs/provider.tsx` per the template.
- CSS: `styles/geistdocs.css` now imports `@vercel/geistdocs/theme.css`
(self-sourcing package dist/streamdown) instead of layering on
`styles.css` from `global.css`; updated the mobile breadcrumb selector
for the new package DOM; kept the site-specific shadcn tokens, dark
background-scale override, prose, TOC, and streamdown fixes.
- Added `apps/docs/AGENTS.md` capturing the packaged-architecture
conventions (cache-components rules, root-params, markdown contract,
proxy mappings).

## PR #251 (tree sidebar scroll) verification

The fix is package-internal (`manualToggleRef` in `SidebarTree`); no
consumer change is needed. This site's sidebars render no collapsible
folder rows (content uses spread folders, `...api` etc.), so I verified
the shipped behavior against the bundled 1.22.0 template with
`sidebarMode="tree"` enabled locally: expanding/collapsing a folder
preserves the exact sidebar scroll position (772 → 772), and a route
change into a collapsed folder still scrolls the active item into view.
8/8 checks pass.

## Static-generation coverage

Production build: 290/290 static pages generated. Every intended
parameter tuple is prerendered with complete content (verified H1/body
in emitted HTML):

- `/en` home, `/en/adapters` listing, `/en/resources` — now fully static
(were `ƒ` on main)
- `/en/docs/*` — 45 pages, complete static HTML + one generic
`[[...slug]]` fallback entry (allowed)
- `/en/adapters/{official,community,vendor-official}/*` — 44 detail
pages + `/en/adapters.mdx/*` markdown for all 44
- `/en/sitemap.md` — SSG

Intentional contract differences (match the 1.22.0 template's own build
output):

- OG image routes (`/og/[...slug]`, adapter `*/og`) render on demand
under Cache Components instead of build-time SSG; Next 16.3 caches the
rendered image per route. URLs and content types verified unchanged.
- `llms.txt`, `llms-full.txt`, `llms.mdx`, `rss.xml`, `agents.md` remain
on-demand route handlers (same as main); `agents.md` reads the request
origin by package design.
- Unknown HTML routes: browsers receive the docs shell with 200 before
not-found UI resolves; crawlers get a real 404. Machine-readable unknown
routes return the new smart 404 body with real 404 status and
`X-Robots-Tag: noindex`.

## Lockfile

`pnpm-lock.yaml` delta: the `docs` importer's `@vercel/geistdocs`
(1.20.4 → 1.22.0) and `next` (16.2.11 → 16.3.1) bumps, their
peer-context re-resolutions, and one mechanical re-keying of `@swc/core`
peer contexts to include `@swc/helpers` across existing entries (no
version changes outside the docs app). Verified with `pnpm install
--frozen-lockfile`.

## Test results

- `pnpm install --frozen-lockfile` ✓
- `pnpm check` ✓ (1 pre-existing warning in untouched
`lib/read-more.ts`)
- `pnpm typecheck` — 43/43 ✓
- `pnpm knip` ✓, `pnpm konsistent` ✓
- `pnpm test` — 47/47 turbo tasks ✓
- Clean production build (removed `.next`/`.source`) ✓ 290/290
- Production-server (`next start`) contract checks: HTML docs,
`.md`/`.mdx`, `Accept: text/markdown`, agent-UA negotiation, `llms.txt`,
`llms-full.txt`, `sitemap.md`, `agents.md`, `/.well-known/mcp.json`
(intentional 404), `rss.xml`, `robots.txt`, `/sitemap.xml` rewrite, OG
images, `AGENTS.md`, all redirects (308s), `/api/search` JSON and not
rewritten as markdown, `/docs.md` section root ✓
- Browser checks (Playwright, Chrome, `next start`) — 16/16: instant
sidebar + prev/next client navigation with complete content and no
loading shell, single visible H1 (Activity-preserved routes stay
hidden), Copy Page, page actions menu, search → result navigation, Ask
AI panel with suggestions, theme switch (dark applies the site's
background scale), mobile navbar menu and docs sheet navigation, unknown
page shows not-found UI, zero console/page errors and failed requests
- Ask AI scoped failure: with no local AI Gateway credentials the chat
surfaces the error, the surrounding page stays intact, and resubmission
retries cleanly
- Visual parity screenshots vs production (home/docs/adapters, light +
dark) match

## Preview

- Preview:
https://chat-git-richardhaines-geistdocs-122-upgrade.vercel.sh —
deployment **Ready**, build passed on Vercel.
- The preview sits behind Vercel SSO (Fork Protection), so automated
route checks aren't possible without a bypass token; please spot-check
through SSO: `/docs/getting-started`, `/docs/getting-started.md`,
`/adapters`, `/llms.txt`, and a client navigation between docs pages.

Signed-off-by: molebox <rich@vercel.com>
2026-08-24 12:22:40 +02:00
Rich Haines 4c18ec98e8 chore(docs): update Geistdocs to 1.20.4 (#845)
Updates Geistdocs so link-preview bots receive HTML and preserve rich
unfurls instead of being served Markdown.
2026-08-20 10:47:53 +10:00
Ben Sabic 294b595da1 docs: require non-static credential support for vendor-official adapters (#844)
Adds a qualification to the vendor-official listing guide: credential
fields must accept resolver functions alongside static strings, resolved
per outbound call, so short-lived tokens from tools like Vercel Connect
work. Also adds the matching reviewer check and links the Vercel Connect
docs.

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-19 11:09:57 +10:00
josh 3e6e866a0c fix(whatsapp): support business-scoped user ids (#818)
- support phone-based IDs, BSUIDs, parent BSUIDs, and username-only
webhook payloads
- preserve existing thread IDs by storing identity aliases and outbound
routing details in the configured state adapter
- send replies using `to`, `recipient`, or both according to the
identifiers available
- preserve thread continuity across `user_changed_number` and
`user_changed_user_id` system messages
- update WhatsApp types and documentation for the new identity fields
and authentication-template limitation
- closes #794

---------

Signed-off-by: dancer <josh@afterima.ge>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Pablo Botta <886512+p4bl1t0@users.noreply.github.com>
2026-08-19 01:11:16 +10:00
josh d8103a103c fix(twilio): restrict authenticated media downloads (#831)
## summary

- validate media URLs against the configured Twilio API origin before
resolving credentials
- reject protocol, hostname, and port mismatches without making a
network request
- preserve support for configured regional Twilio API origins
- document that `apiUrl` defines the trusted origin for media downloads

## test plan

- added API-level coverage for trusted regional origins and untrusted
URL variants
- added adapter-level coverage for rehydrated attachments from untrusted
origins
- ran the Twilio build, tests, typecheck, integration tests, and
formatting checks

Signed-off-by: dancer <josh@afterima.ge>
2026-08-17 21:01:49 +01:00
josh 745fdf5a97 fix(adapters): harden Telegram streaming and XChat read receipts (#826)
## summary

- pace Telegram post-and-edit streams for private and non-private chat
limits, including the final edit
- respect Telegram `retry_after` cooldowns and reject when the complete
response cannot be delivered
- prevent explicit XChat read receipts from advancing past an unresolved
message
- preserve latest-event fallback for delivered XChat messages without a
sequence id
- update adapter documentation and regression coverage

---------

Signed-off-by: dancer <josh@afterima.ge>
2026-08-14 19:34:05 +01:00
josh 3bbf3ff542 fix(telegram): make native draft streaming opt-in (#822)
- use post-and-edit streaming by default to avoid leaked draft previews
in Telegram clients
- add `nativeStreaming: true` for explicitly enabling native draft
previews in private chats
- preserve existing native streaming behavior when enabled
- document the client compatibility tradeoff
- closes #782

before: private chat streams used native Telegram drafts by default,
which could remain visible over the final message on Telegram macOS

after: streams use post-and-edit by default across Telegram clients,
while native drafts remain available as an opt-in

---------

Signed-off-by: dancer <josh@afterima.ge>
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-14 15:21:48 +10:00
josh 83ede7eab2 feat(chat): add message reply support (#819)
- add `thread.reply()` for sending messages with native references to
existing messages
- accept either a message object from the same thread or a message id as
the reply target
- support text, markdown, AST, cards, files, and buffered streams
- add WhatsApp contextual replies using the Cloud API
`context.message_id` field
- apply reply context only to the first outgoing message when content is
split across multiple sends
- preserve the target message through sent message edits and thread
history
- throw `NotImplementedError` for adapters without native reply support
- document the API and add message replies to the adapter feature matrix

fixes #786

---------

Signed-off-by: dancer <josh@afterima.ge>
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Aradhya C P <135510032+aradhyacp@users.noreply.github.com>
2026-08-14 14:32:27 +10:00
josh 18d4a230d7 feat(chat): add mark as read support (#820)
- add `thread.markAsRead()` for the current message, an explicit
`Message`, or a message ID
- expose read receipts as an optional adapter capability with explicit
unsupported and thread mismatch errors
- support WhatsApp read acknowledgements, Messenger `mark_seen`, and
XChat read watermarks
- preserve automatic XChat receipts while allowing manual timing and
surfacing explicit failures
- document provider-specific behavior and capability support
- closes #785

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Aradhya C P <135510032+aradhyacp@users.noreply.github.com>
2026-08-14 14:09:23 +10:00
Ben Sabic 7a1150ce23 Add Vercel Connect support to Telegram (#813)
Adds function-backed Telegram bot-token resolution so the adapter can
use short-lived Vercel Connect credentials for every Bot API and
file-download request. Static tokens retain their existing synchronous
behavior, while native Telegram webhook verification or polling remains
unchanged.

```ts
import { createTelegramAdapter } from "@chat-adapter/telegram";
import { connectTelegramAdapter } from "@vercel/connect/chat";

createTelegramAdapter({
  ...connectTelegramAdapter("telegram/acme-telegram"),
  secretToken: process.env.TELEGRAM_WEBHOOK_SECRET_TOKEN,
});
```

`create-chat-sdk` now recognizes Telegram as Connect-capable, preserves
`TELEGRAM_WEBHOOK_SECRET_TOKEN`, and emits native-webhook guidance:

```bash
npm create chat-sdk@latest -- my-bot --adapter telegram memory --connect -y
```

This PR is stacked on the Notion Connect work in #812. Validated with
the Telegram adapter suite (251 tests), create-chat-sdk suite (211
tests), package type checks/builds, and repository lint/format checks.

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-12 11:49:29 +10:00
Ben Sabic 06b04ac4d9 Add Vercel Connect support to Notion (#812)
Adds function-backed Notion access-token resolution so the adapter can
use short-lived Vercel Connect credentials for every API request, retry,
and multipart upload. Direct Notion webhooks continue to use
`NOTION_VERIFICATION_TOKEN` and native HMAC verification because Connect
does not forward Notion triggers.

```ts
import { createNotionAdapter } from "@chat-adapter/notion";
import { connectNotionAdapter } from "@vercel/connect/chat";

createNotionAdapter({
  ...connectNotionAdapter("notion/acme-notion"),
  verificationToken: process.env.NOTION_VERIFICATION_TOKEN,
});
```

`create-chat-sdk` now recognizes Notion as Connect-capable, preserves
the native webhook verification token, and emits direct-webhook
guidance:

```bash
npm create chat-sdk@latest -- my-bot --adapter notion memory --connect -y
```

Validated with the Notion adapter suite (71 tests), create-chat-sdk
suite (209 tests), package type checks/builds, and repository
lint/format checks.

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-12 11:00:21 +10:00
Max 0f24cc3062 feat(chat): preserve replied-to message context (#802)
## Summary

- add optional, normalized `Message.replyTo` context that survives JSON
and workflow serialization, queue rehydration, thread history, and
`SentMessage` reconstruction
- populate it from Telegram's `reply_to_message`, including combined
media groups, so handlers don't need raw Telegram payloads
- keep the core contract adapter-neutral while Telegram owns only its
platform mapping, allowing other adapters to populate it when they
receive full replied-to messages


Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-08-11 17:56:40 +01:00
Max 1d2b78d933 Deduplicate repeated Telegram webhook updates (#799)
## Summary

Telegram retries webhook deliveries after non-2xx responses, and its
`update_id` field is explicitly intended for ignoring repeated updates.
The Telegram adapter previously routed every webhook delivery
independently.

This change atomically claims each integer `update_id` through the
configured `StateAdapter` before routing the update. Repeated deliveries
return 200 without reaching bot handlers, while state failures return
503 without dispatching so Telegram can retry. Updates without an
integer `update_id` keep their existing behavior, and polling remains
unchanged.

Claims expire after 24 hours because Telegram retains incoming updates
for no longer than 24 hours. This is a bounded retention choice, not a
documented retry timeout. Cross-instance deduplication requires shared
durable state; in-memory state only protects one process. The change
provides webhook-delivery idempotency, not end-to-end exactly-once
handler completion.

Telegram contract: [Update](https://core.telegram.org/bots/api#update)
and [setWebhook](https://core.telegram.org/bots/api#setwebhook).

## Test plan

- `pnpm --filter @chat-adapter/telegram test`
- `pnpm --filter @chat-adapter/telegram typecheck`
- `pnpm check`
- `pnpm konsistent`
- `TURBO_CONCURRENCY=1 pnpm validate`

Regression coverage verifies sequential and concurrent repeated
deliveries, distinct update IDs, missing update IDs, duplicate 200
responses, and state-failure retry behavior. GitHub CI also passes on
Node 22 and Node 24.

## Checklist

- [x] All commits are signed and verified
- [x] All commits are signed off for the DCO (`git commit -s`)
- [x] `pnpm validate` passes
- [x] Changeset added (or N/A — see
[CONTRIBUTING.md](./CONTRIBUTING.md))
- [x] Documentation updated (or N/A)

---------

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
Signed-off-by: dancer <josh@afterima.ge>
Co-authored-by: dancer <josh@afterima.ge>
2026-08-11 17:37:50 +01:00
Ben Sabic 927d0dbd7d docs: add cross-link card sections and page-level SEO metadata (#804)
Many docs pages are orphaned: nothing links to them apart from the
sidebar, so readers and crawlers rarely find them. This PR gives every
docs page a Read more section with four cards at the bottom of the
article, above the prev/next footer.

Cards are picked deterministically in lib/read-more.ts: the page's
related frontmatter first, then prerequisites, then siblings from the
same sidebar section, then the rest of the page tree, so every page
always fills all four slots. Card titles and descriptions come from the
target page's own frontmatter, nothing is duplicated. The section is
injected through the MDX wrapper slot in the docs route, so it applies
to all pages without touching content.

To make the links topical rather than positional, 26 pages get related
frontmatter additions. The 20 pages that no other page referenced (all
ten api/ pages among them) now each have at least one inbound link,
generally pairing guides with their API reference and back. The bundled
copy of create-chat-sdk.mdx is synced to keep the byte-match test green.

Official adapter pages get the same treatment with a More adapters
section: same-type adapters first (platform or state, using the catalog
order), topped up from the other official group. Vendor-official and
community adapters are never shown, and their pages don't render the
section. It reuses AdapterCard, so logos and package names match the
listing page.

Two small SEO fixes ride along. JSON-LD was allowlisted to three docs
pages; the allowlist is gone, so all 45 now emit HowTo or TechArticle
plus a BreadcrumbList. Docs and adapter detail pages also emit canonical
URLs now, resolved against the existing metadataBase.

Verified against the production build: all 45 docs pages and all 19
official adapter pages render exactly four cards, no page is left
unreferenced, canonicals and JSON-LD are present everywhere, and pnpm
validate passes. Docs-only, so no changeset.

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-11 08:12:05 +10:00
Ben Sabic a0cba0288a Add Vercel Connect support to Discord (#808)
Adds function-backed Discord bot token and application ID resolvers,
plus custom webhook verification for Vercel Connect trigger-forwarded
interactions. Native Discord Ed25519 verification remains the default
when no custom verifier is configured.

```ts
import { createDiscordAdapter } from "@chat-adapter/discord";
import { connectDiscordAdapter } from "@vercel/connect/chat";

createDiscordAdapter({
  ...connectDiscordAdapter("discord/acme-discord"),
});
```

`create-chat-sdk` now recognizes Discord as Connect-capable, generates
`DISCORD_CONNECTOR` instead of native credential variables, and
preserves `CRON_SECRET` for Gateway forwarding:

```bash
npm create chat-sdk@latest -- my-bot --adapter discord memory --connect -y
```

Validated with the Discord adapter suite (284 tests), create-chat-sdk
suite (206 tests), package type checks/builds, and repository
lint/format checks.

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-11 08:11:32 +10:00