<- blog

Chat Adapters Normalize Messages, Not Meaning

Fresh Chat SDK and Copilot releases show why shared handlers still require channel-aware identity, retries, context, capabilities, and approvals.

#ai-agents#developer-tools#operations#reliability

One agent can now appear in more places without becoming the same agent experience in each place.

Vercel released three Chat SDK changes on 25 August. A Notion adapter maps pages to channels and comment discussions to threads. The Slack adapter now supports Enterprise Grid, including organisation-wide installations, installation-scoped caches, Slack Connect routing, and 24-hour retry deduplication. A new XChat adapter handles encrypted conversations, key management, signature verification, and platform-specific text fallbacks.

Four days earlier, GitHub put Copilot's cloud agent into Slack conversations and dedicated code channels and Microsoft Teams channels, threads, meeting chats, and direct messages. Work can begin where people discuss it, continue asynchronously in a sandbox, and finish as a pull request.

The portability is real. A shared handler can receive a normalized message and post a reply across several services. The equivalence is not. A Notion page, Slack Connect thread, Teams meeting chat, and encrypted XChat group carry different identity evidence, history, triggers, controls, retries, formatting, and approval mechanisms.

The repeated angle to avoid

The ten most recent posts here covered agent compute placement, security treatment states, soft-navigation measurement, agent authorization loops, credential isolation, payment routing, MCP detection, encrypted handshakes, deployment authentication, and checkout experiments. Older overlapping posts covered plugin portability, comment-triggered intent, work intake, cross-session context copies, and agent audit trails.

The weak version of this article is the old X needs Y formula: multi-channel agents need governance. That would merely repeat the existing advice to type agent intake, constrain authority, and keep run evidence.

The sharper thesis is that a shared programming interface can hide incompatible evidence contracts. Chat SDK can normalize payloads into messages, threads, and channels. It cannot make a platform user the same business principal everywhere, turn a comment into the same kind of instruction, make closed history available, add an approval control a channel does not have, or guarantee that one delivered event produces one business action. Builders must keep transport normalization and workflow normalization separate.

Fresh evidence and background context

The source map separates this week's releases from the current implementation documentation that explains the mechanics:

Source Freshness What it contributes
Chat SDK's Notion adapter 25 August 2026 Page-to-channel and comment-to-thread mapping, mention fallbacks, three-file limit, open-comment history, and missing buttons, modals, and reactions
Slack Enterprise Grid support 25 August 2026 Enterprise installation identity, tenant-scoped caches, Slack Connect routing, workspace-aware API calls, and 24-hour retry deduplication
Chat SDK's XChat adapter 25 August 2026 Encrypted group and direct conversations, bot-initiated message conditions, signature handling, text degradation, and edit-based streaming
Copilot in Slack 21 August 2026 Shared agent sessions, dedicated code channels, asynchronous work, app attribution, repository controls, and optional extra pull-request approval
Copilot in Microsoft Teams 21 August 2026 Meeting-to-agent handoff, participant steering, repository write-access boundary, default-repository behaviour, budgets, and extra approval
Chat SDK platform-adapter documentation Current primary documentation Webhook verification, normalized messages, shared handlers, outgoing conversion, and the explicit feature matrix
Chat SDK event-routing documentation Current primary documentation Precedence among direct messages, subscribed threads, mentions, pattern matches, reactions, commands, actions, and modals
Chat SDK concurrency documentation Current primary documentation Drop, queue, burst, debounce, and concurrent strategies, plus lock scope and skipped-message evidence
Chat SDK conversation-history documentation Current primary documentation Cross-platform identity resolution, transcript retention, explicit capture, and deletion behaviour

The new information surplus is the operating contract between the sources. The releases demonstrate that adapters can absorb substantial platform complexity. The documentation exposes what remains an application decision. Joined together, they imply a design rule: the canonical unit is not a chat message; it is a channel-qualified intake event that may create one idempotent business job.

One message object can represent different claims

A normalized message is valuable because application code can use fields such as text, author, thread ID, attachments, mention status, and the original payload without parsing every provider's webhook format.

That object still describes transport evidence, not business meaning.

platform webhook
  -> signature verified
  -> platform payload parsed
  -> normalized message
  -> workflow interprets identity, intent, scope, and authority
  -> canonical job created or request declined
  -> result posted through a channel-specific renderer

The adapter can establish that Slack sent a signed event containing user U123, or that a Notion integration received a comment in a particular discussion. The application must decide whether that platform identity is linked to a customer, employee, repository contributor, or service account—and what that principal may do.

The distinction is especially important across organisations and shared spaces. Slack Enterprise Grid adds enterprise and workspace identity. Slack Connect puts participants from different organisations in one channel. Microsoft Teams permits participants with repository write access to trigger Copilot changes. A display name, email-like username, or membership in the conversation is not enough to merge those identities automatically.

Use two identities:

Identity Source Purpose
Transport actor Adapter, installation, enterprise/workspace, platform user ID Verify and route this event inside the originating platform
Business principal Explicit account link or owned directory mapping Authorize access to repositories, customer records, deployments, orders, or internal tools

Do not fall back from an unresolved business principal to a platform display name. For read-only public help, an anonymous or channel-local identity may be acceptable. For writes, exports, code changes, payments, bookings, CRM updates, or publication, unresolved identity should stop the action path.

A thread is a routing key, not a universal conversation

Chat SDK presents a Thread abstraction across platforms. That gives builders one place to post, subscribe, fetch history, and store state. The backing conversation boundaries still differ.

The Notion release makes the mapping explicit: a page becomes a channel and a comment discussion becomes a thread. Discussions can be attached to a page or block, but not an inline text range. History fetches return only open comments. A resolved discussion can therefore disappear from the history available to a later handler even though it remains relevant to the work.

Slack and Teams distinguish channels, threads, and direct messages in other ways. GitHub's Teams integration can use a configured default repository in public channels, while direct messages do not use that default. GitHub's Slack integration can move work from an ordinary thread into a dedicated code channel. The conversation that supplied intent and the surface that displays execution can consequently be different objects.

XChat adds another shape: encrypted one-to-one and group conversations, with proactive bot messages allowed only when the user has encrypted chat configured and follows the bot.

A safe workflow therefore records both source and continuation:

intake source:     adapter + installation + channel + thread + message
canonical job:     stable application-owned job ID
continuations:     zero or more platform threads, PRs, issues, records, or alerts

The canonical job must not be keyed only by a thread ID. One task can move from a Teams meeting chat to a sandbox and pull request. One Slack code channel can outlive the originating discussion. One user can contact the bot in another platform tomorrow. Preserve links between those objects without pretending they are one transcript.

Triggers carry different levels of intent

The same handler can fire from a mention on several platforms, but mention semantics are not the same as permission.

The Notion adapter replies on @-mention by default. Where mentions are unavailable, it can trigger on a keyword or every comment. Chat SDK's documented routing order gives direct messages precedence, then subscribed threads, then mentions, then regex pattern matches; reactions, slash commands, actions, and modals use separate paths.

These routes carry different ambiguity:

Trigger Evidence about intent Safe default
Explicit command or structured action User chose a named operation and supplied structured fields Validate identity, resource, and current approval before creating a job
Direct @-mention with task text User deliberately addressed the bot, but scope may still be vague Classify intent; diagnose or draft before mutating
Message in a subscribed thread Message belongs to an ongoing conversation, not necessarily a new instruction Append context; do not create another job without an explicit transition
Keyword match Text matched a pattern; intent may be incidental Triage or ask for a structured command
Every-comment mode Presence in the discussion is the only trigger evidence Read-only assistance unless separately promoted
Reaction Lightweight signal attached to a message Use only where the channel supports it and the approver's identity is verified

A unified handler should receive a trigger_class, not infer equal authority from the fact that it ran. Moving from mention-only to every-comment mode is a policy change even if no business logic changes.

Delivery retries and human overlap are separate duplicate problems

Slack Grid support includes 24-hour deduplication for retried event deliveries. That closes an important transport failure mode: the provider can redeliver the same event after a delay, and one webhook should not create two responses or two actions.

Human conversation creates another form of overlap. A person may send three short messages while the agent is thinking. Two participants may issue conflicting instructions in one shared thread. A correction can arrive after the first message has already started a side effect.

Chat SDK documents several concurrency strategies:

  • drop is the default; an overlapping message is discarded when a handler already holds the thread lock;
  • queue processes the latest queued message and exposes intermediate messages as skipped context;
  • burst waits briefly and combines a multi-message turn;
  • debounce keeps only the final message in a rapid sequence;
  • concurrent runs messages without ordering.

Those are conversation-processing policies. None is a substitute for business idempotency.

Use three different keys:

transport dedup key = adapter + installation + provider event ID
conversation lock   = adapter + installation + channel or thread
business action key = principal + action + resource + authoritative version

If Slack retries one create_refund event, transport dedup should suppress the duplicate. If a user sends “refund order 482” and then “wait, not that order,” the conversation strategy must preserve the correction. If the handler restarts after the refund API succeeded but before the reply posted, the business action key must return the existing refund result rather than perform it again.

The SDK's state documentation also warns that force-releasing a lock does not cancel the first handler. Two handlers can briefly run on the same thread. Cancellation intent and business-operation status must live in the application, not only in a chat lock.

Capability differences change what the workflow can prove

Cross-platform rendering is not merely cosmetic when interface controls carry approval or evidence.

Notion currently has no buttons, modals, or reactions in the adapter, and cards render as Markdown. XChat has no Markdown rendering; links and mentions stay tappable, tables become ASCII code blocks, cards become text with a preview, and streaming uses message edits. The shared Thread interface also documents methods that are no-ops or unsupported on some platforms.

That produces a dangerous lowest-common-denominator shortcut: a workflow designed around a Slack approval button becomes “reply yes” in a channel without buttons. The visible action looks similar while the evidence becomes weaker.

Use capability negotiation instead:

Required interaction Native capability present Native capability absent
Read-only answer Render in the best supported format Fall back to plain text with stable links
Select one bounded option Use a signed structured action Link to an authenticated web form or issue command
Approve a reversible draft Record approver, object version, and action ID Require an explicit structured command with the same fields
Approve an irreversible or high-impact action Use a separately authenticated approval surface Do not downgrade to free-text chat approval
Inspect tabular evidence Render native table or card Post a short summary plus link to canonical evidence
Cancel running work Use native control tied to job ID Accept a structured cancel command and confirm canonical job state

The decision rule is simple: degrade presentation freely; do not degrade authority silently. If a channel cannot produce the evidence a sensitive transition requires, move that transition to a surface that can.

Cross-platform memory requires an explicit identity join

A channel's history and an application's memory are different stores.

Chat SDK's conversation-history feature can keep a transcript under a stable user key across platforms. The identity resolver can map a platform actor to an internal ID; if it returns null, the SDK deliberately does not fall back to the platform user ID. The application also chooses which user and bot messages to append, the retention period, the maximum entries, and when to delete the transcript.

That is the right boundary because cross-platform continuity is an opt-in data operation:

Slack message under workspace identity
  + verified account link
  -> internal principal
  + retention and purpose rule
  -> cross-platform transcript entry

Without the verified link, merging by email can join the wrong accounts, especially with guests, changed employers, aliases, shared mailboxes, or platforms that do not expose a verified address. With the link, the workflow still has to decide whether moving a Notion comment into context for an XChat reply matches the user's expectation and the organisation's data policy.

Keep thread-local state separate from cross-platform memory. A subscription, typing mode, queued message, or open-comment cursor belongs to the channel thread. A durable customer preference or approved project fact may belong to a business principal. A live order, repository permission, consent status, or incident state should be fetched from its authoritative system rather than remembered from chat.

A channel-qualified intake envelope

A compact envelope can preserve meaning after the adapter normalizes transport:

channel_intake:
  intake_id: ci_2026_08_25_01
  received_at: 2026-08-25T15:04:00Z
  transport:
    adapter: slack
    installation_id: enterprise_E012
    enterprise_id: E012
    workspace_id: T482
    shared_channel: true
    channel_id: C774
    thread_id: slack:C774:1787670240.0142
    message_id: 1787670245.0331
    provider_event_id: Ev09ABC
    delivery_attempt: 2
    signature_verified: true
  actor:
    platform_user_id: U992
    business_principal_id: employee_184
    linkage: verified_account_connection
  trigger:
    class: explicit_mention
    text_version: original_message_v1
    subscribed_thread: false
  requested_work:
    class: code_investigation
    target: github:example/storefront#842
    mutation_allowed: false
  channel_capabilities:
    structured_actions: true
    reactions: true
    message_editing: true
    closed_history_visible: true
  context:
    source_messages:
      - 1787670245.0331
    external_refs:
      - actions_run_55391
    transcript_merge_allowed: false
  control:
    transport_dedup_key: slack:enterprise_E012:Ev09ABC
    conversation_strategy: queue
    business_idempotency_key: investigate:storefront:actions_run_55391
    approval_state: diagnosis_only
  continuation:
    canonical_job_id: job_481
    reply_thread_id: slack:C774:1787670240.0142
    durable_result_target: github:example/storefront#842

The envelope keeps provider payloads available by reference without copying every message into every downstream log. More importantly, it prevents a later worker from seeing only text: investigate this and reconstructing authority from prose.

For Notion, the envelope would record page and discussion identity, whether the comment history is open, and the configured mention fallback. For Teams, it would record channel versus direct message, repository selection, participant identity, and write-access result. For XChat, it would record encryption verification, direct or group scope, and the capabilities lost during rendering.

A rollout pattern that preserves portability

A small team can keep one codebase without flattening the channels:

  1. Normalize only transport facts in adapters. Verify signatures, parse events, create namespaced IDs, and convert outgoing content.
  2. Build an explicit principal linker. Require a verified account connection or directory mapping before action-capable tools are available.
  3. Classify trigger evidence. Distinguish commands, structured actions, mentions, subscriptions, keywords, all-comment modes, and reactions.
  4. Create a canonical job before side effects. Return the job ID to the channel and use business idempotency outside the webhook process.
  5. Select concurrency by conversation style. Queue or burst multi-message agent conversations; reserve concurrent processing for truly independent lookups.
  6. Negotiate capabilities per channel. Keep a feature matrix in code and refuse authority downgrades when buttons, modals, reactions, or durable history are absent.
  7. Separate thread state, transcript memory, and business state. Give each its own identity, retention, and deletion rule.
  8. Post receipts, not only prose. Include job ID, interpreted action, target, status, evidence link, and next required approval in every action-capable result.
  9. Test provider retries and human corrections. Replay the same event, delay it, send overlapping messages, edit the request, resolve a Notion discussion, and move work into another channel.
  10. Review channel-specific exceptions after adapter upgrades. A newly supported capability can change which evidence the workflow can collect and which fallback paths should be retired.

Failure modes worth testing

One Slack Connect participant resolves through another tenant's cache

A user or mention lookup is keyed too broadly, so a shared-channel event resolves against the wrong workspace. Scope installations, tokens, user caches, and deduplication keys by enterprise or team identity exactly as the adapter expects; test equal-looking IDs across tenants.

A retry repeats a business action after transport dedup expires

The provider redelivers an old event, or the handler succeeds remotely and fails before recording its response. Keep business idempotency for at least the action's retry and reconciliation window, independently of the adapter's 24-hour delivery deduplication.

The default drop strategy loses the correction

The agent is working on “update order 482” when the user sends “stop” or corrects the order number. A thread lock discards the later message. Choose queue or burst where corrections matter, and make cancellation update canonical job state rather than merely release a chat lock.

A resolved Notion discussion removes decision history

A later handler fetches only open comments and misses the discussion that approved or rejected the work. Store the canonical decision and evidence reference when the transition occurs; do not use live comment history as the approval database.

A reaction-based approval silently becomes free text

The workflow moves to a channel without reactions or structured controls and accepts “yes” as equivalent approval. Require a structured command or authenticated approval surface carrying the same principal, object version, action, and expiry.

Cross-platform memory joins the wrong person

Two platform profiles share a display name or unverified address, so one person's transcript appears in another channel. Refuse the merge until account linkage is verified, and expose a deletion path by internal principal.

Formatting degradation hides the decision

A rich card with alternatives, warnings, and evidence becomes a clipped text block or ASCII table. Test the rendered output in every supported channel and link to canonical evidence when the channel cannot display it reliably.

Shared conversation becomes shared authority

Everyone can see and steer an agent session, so the application treats every participant as authorized to change the repository or business record. Recheck the acting participant's current permission at each sensitive transition; visibility and participation are not authorization.

The practical conclusion

The new adapters and chat integrations remove valuable plumbing. Builders no longer need a separate webhook parser, message model, formatter, and conversation wrapper for every platform. Teams can start agent work in the discussion where intent already exists and continue it in a durable development or business system.

That convenience stops at the evidence boundary. Notion supplies page and discussion semantics but limited history and controls. Slack Grid introduces enterprise, workspace, shared-channel, cache, and retry identities. Teams changes repository selection and who may trigger code work. XChat changes encryption, initiation conditions, rendering, and interaction capabilities. A shared handler does not erase any of those facts.

Keep the adapter interface. Preserve the channel qualifier. Link transport actors to business principals explicitly, type the trigger, separate delivery deduplication from action idempotency, negotiate capabilities, and anchor side effects to a canonical job with durable evidence. Then one agent can operate across many chat surfaces without turning transport portability into accidental equivalence.

Need technical help?

I'm a software engineer who builds web apps, APIs, and AI tooling. If you've got a project or a problem to talk through, book a free 30-minute call.

Book time with me ->