Python-only fixes on top of 4.31.0 (UPSTREAM_PARITY unchanged at 4.31.0). Two Slack adapter bug fixes, both divergences ahead of upstream (which shares the same gaps), documented in docs/UPSTREAM_SYNC.md.
- Slack: empty-DM
thread_tsfetch routing (#138). DM roots encode an emptythread_ts(slack:Dxxx:); the fetch paths (fetch_messages/fetch_message) calledconversations.replies(ts=''), which returns no replies and loses the DM root context. They now route an emptythread_tsto the existing channel-history path (conversations.history) instead — covering DM messages and the #137 DM block-action consumer uniformly. The non-empty (real thread) path is byte-identical. - Slack:
chat.startStreamteam_not_foundon Enterprise Grid (#95).chat.startStreamrequires an explicitteam_idfor Grid orgs, butstream()started the stream without one (while non-streamingchat.postMessageworked). The streaming call now threadsteam_id(from the already-plumbedrecipient_team_id, the per-workspaceT…id) through tochat.startStream. Verified end-to-end againstslack_sdkthat the value reaches thechat.startStreamAPI call;append/stopcorrectly do not need it. (Live Enterprise-Grid server-side acceptance is pending verification against a real Grid tenant.) - Docs: marked Linear
"agent-sessions"mode experimental inLinearAdapterConfig— its emit/fetch GraphQL is schema-hardened but unverified against a live Linear agent-session tenant (#151).
Synced to upstream vercel/chat@4.31.0. The mapped-core test files (packages/chat/src/*.test.ts) are byte-identical between the chat@4.30.0 and chat@4.31.0 tags, so the fidelity re-pin to chat@4.31.0 is string-only (732/732 mapped-core tests still pass, 0 missing); the core source delta is the LinkButton stable-id field (below). The headline is the Linear agent-sessions mode, plus the Teams SDK-free primitive subpaths, the Slack 4.31 changes, Telegram rich messages, and a Python-only opt-in ThinkingChunk stream type. Sets UPSTREAM_PARITY = "4.31.0".
Full port of upstream's Linear agent-sessions interaction model, delivered across five PRs (L1–L5). Because no official Linear Python SDK exists, every @linear/sdk call is reproduced as raw GraphQL over the existing _graphql_query helper and schema-hardened field-by-field against Linear's published GraphQL schema (linear/packages/sdk/src/schema.graphql).
- L1 — types (#168).
LinearAgentSessionThreadId, themode: "agent-sessions" | "comments"config (default"comments"), thekind-discriminated raw-message variants (comment/agent_session_comment), and theAgentSessionEventwebhook payload types. - L2 — thread-id (#170). Anchored
linear:{issue}:c:{comment}:s:{session}/linear:{issue}:s:{session}encode/decode with a strict decode order; existing thread-id forms stay byte-identical so cross-SDK state is preserved. - L3 — webhook parse + routing (#171). The
AgentSessionEventbranch with mutually-exclusive mode gating (agent-session events flow only inagent-sessionsmode, comment events only incommentsmode),_parse_message_from_agent_session_event(created/prompted actions + null-return/warn paths), app-ownership guard, andget_user_name_from_profile_url. - L4 — emit (#172).
post_message/start_typing/streamroute through rawagentActivityCreate/agentSessionUpdatemutations (lowercaseAgentActivityTypeenum,content: JSONObject!,ephemeralincluded only when set); streaming flushes markdown deltas asresponse/thoughtactivities and mapstask_update/plan_updatechunks toaction/erroractivities and session-plan updates. - L5 — fetch (#173).
fetch_messagesdispatches agent-session threads to_fetch_agent_session_messages(rawagentSession(id:)+comments(filter:{parent})with forward/backward pagination);edit_message/delete_messageraise append-only errors for session threads.
The schema-hardening caught two live-tenant-breaking selection bugs before they shipped (AgentActivity exposes the agentSession relation, not a scalar agentSessionId; AgentSession likewise has no scalar issueId). The mutations/queries are confirmed against the published schema but not yet exercised against a live Linear agent-session tenant — documented in docs/UPSTREAM_SYNC.md.
New runtime-free Teams subpaths mirroring upstream's @chat-adapter/teams/* exports: teams/api (Bot Connector — token grant, post/update/delete message, typing, create conversation), teams/graph (Microsoft Graph — channels, messages, pagination), teams/format (Teams text/mention/HTML↔Markdown), teams/webhook (read/parse, continuation/user/attachment extraction, mention detection), teams/cards + cards_input (card → Adaptive Card + input parsing), and teams/modals (modal → Adaptive Card + dialog-submit parsing). Each network-facing primitive carries an SSRF/token-leak host gate: call_teams_connector_api (serviceUrl Bot Framework allowlist) and call_teams_graph_api (host pinned to graph.microsoft.com, also guarding followed @odata.nextLink cursors).
@mention-inside-URL fix. Bare@handles insidehttp(s)URLs (paths, query strings, fragments) are no longer rewritten into<@handle>mentions (which corrupted the link), via a URL-span exclusion pass.web_client_optionsconfig — forwarded to both the default and per-tokenslack_sdkWebClients to tune the underlying HTTP client (timeout,retry_handlers, headers), with per-client header isolation. (Maps to slack_sdk kwargs rather than@slack/web-api's axios options — documented divergence.)- Stable link-button
action_id—LinkButton(id=…)now flows to the Slack blockaction_idinstead of always deriving it from the URL.
Character-for-character port of the new rich.ts (Telegram rich-message wire types → Markdown + plain text) as telegram/rich.py, plus the rich-message/media type family (TelegramRichText/RichBlock/RichMessage, animation/audio/location/video/voice), native sendRichMessage/sendRichMessageDraft threading through post/edit/stream with a rich→regular fallback, and a /slash-command router.
LinkButton(id=…)(chat@4.31, commit171657a). Optional action identifier for platforms that report link clicks (matches theButton/Selectidconvention). The JSX-runtime half of the same commit has no Python equivalent (no JSX runtime) and is documented as such.ThinkingChunk(Python-only, opt-in, default-off; supersedes PR #39, landed in #169). A separateThinkingChunk(type="thinking", content=str)stream-input type surfaces AI-SDKreasoning/reasoning-deltaparts.StreamChunkis not widened — it stays byte-identical to upstream's three variants, so consumers referencing it are unaffected;ThinkingChunkis accepted only at the stream boundaries via theStreamInput = str | StreamChunk | ThinkingChunkalias. Emitted only when a caller opts in (emit_thinking=True); the default stream and persistedMessageare byte-identical to upstream, so cross-SDK state stays compatible. Gives chinchill a first-class path to stream agent thinking to Slack/Teams without intercepting the model stream out-of-band.
chat/adaptersstatic catalog (new./adapterssubpath). Upstream's SDK-free adapter/env-var metadata registry is addressed by npm package names and includes ~13 vendor-official adapters this SDK doesn't ship, so it isn't meaningfully portable verbatim; a Python-native equivalent would be a new feature with no current consumer need. Documented indocs/UPSTREAM_SYNC.md; deferred demand-driven.
Synced to upstream vercel/chat@4.30.0. The mapped core (packages/chat/src) is content-identical between the chat@4.29.0 and chat@4.30.0 upstream tags, so this wave is all adapter work: a new Twilio adapter, a Telegram native-streaming port, a Slack primitives-subpath wave, a batch of WhatsApp / Slack / Google Chat fixes, and the headline — the Teams adapter migration to the official microsoft-teams-apps SDK (issue #93, delivered across four PRs). Sets UPSTREAM_PARITY = "4.30.0"; CI fidelity re-pinned to chat@4.30.0 (732/732 mapped-core tests still pass, 0 missing).
chat_sdk.adapters.twilio(vercel/chat#558; PR #142). Twilio Programmable Messaging adapter (10th platform): inbound message webhooks withX-Twilio-SignatureHMAC-SHA1 verification (hmac.compare_digest), outbound SMS/MMS through the Messages REST API (hand-rolled over an injectable transport — no officialtwilioSDK, mirroring upstream), 1:1 DM threads keyedtwilio:{sender}:{recipient}, plus standaloneapi/webhook/voicehelpers (TwiML builders, call + transcription parsing). New extra:chat-sdk-python[twilio]. Imports stay lazy so the package loads withoutaiohttpinstalled.
The hand-rolled Bot Framework REST + JWT stack is replaced by the official Microsoft Teams Python SDK (microsoft-teams-apps ≥ 2.0.13, added to the [teams] extra), mirroring upstream @chat-adapter/teams@4.30.0. Landed as four PRs:
- PR 1 — inbound + auth (#143). New
adapters/teams/bridge.py: aBridgeHttpAdapterimplementing the SDKHttpServerAdapterprotocol routes already-authenticated webhooks through the SDKApp. JWT validation now runs through the SDK'sTokenValidator(RS256 + audience + Bot Framework issuer via the live JWKS) in place of the hand-rolled_verify_bot_framework_tokenblock. Graph reads stay hand-rolled (nomsgraph-sdk/[graph]extra). - PR 2 — outbound (#144).
post_message/start_typingroute throughApp.send;edit_message/delete_messageroute throughApp.api.conversations.activities(...).update/.delete. Per-thread service-URL routing retargets the SDKApiClient's service-url chain (validated against the SSRF allow-list). The camelCase wire dict is still returned asRawMessage.raw, preserving the public contract (attachment shape, file delivery, returned id). - PR 3 — native streaming (#145). DM streaming uses the SDK's native
IStreamer(microsoft-teams-appsHttpStream) viaapp.activity_sender.create_stream(...)andstream.emit(...)per chunk, replacing the hand-rolled Bot Framework streaming wire format. The SDK owns the streamType/streamSequence framing, the inter-flush throttle (~500ms, 429-safe), and 429 retry. Atomically unwinds the two transitional public-type divergences (RawMessage.text,update_interval_ms) that PR 3 made unnecessary. - PR 4 — release cut (this entry). Version bump to
0.4.30, fidelity re-pin tochat@4.30.0, docs, and the@chat-adapter/teams@4.30.0version-label normalization.
The residual adapter-level divergences (we keep the SDK as the auth + transport layer but route the authenticated activity ourselves through a lenient CoreActivity; the streamer is closed in our own _handle_message_activity finally because our bridge owns dispatch) are documented in docs/UPSTREAM_SYNC.md.
- Telegram: native DM draft streaming (vercel/chat#340; PR #140). DMs stream via the
sendMessageDraftBot API method (the draft bubble updates in place, throttled toupdate_interval_ms, default 250ms), then a regularsendMessagepersists the final text; non-DM threads returnNonebefore consuming any chunks so the SDK's post+edit fallback handles groups/channels. Adds a sharedwith_telegram_markdown_fallback()retry-without-parse_modepath wrappingpost_message/edit_message/send_document/send_attachment. - Slack: webhook + primitives subpaths (vercel/chat#538, #547, #548, #555, #559; PR #139). New runtime-free
chat_sdk.adapters.slack.webhook(andslack.api) subpaths for lower-level Slack request verification, signed-body reading, and Events/slash/interactive payload parsing into typed dataclasses. The adapter now verifies through the sharedverify_slack_request/verify_slack_signatureprimitives (the inline_verify_signaturemethod is removed, matching upstream); the slack package__init__is now lazy (PEP 562) so importing a subpath does not pull in the full adapter runtime. The newslack/apiprimitives carry SSRF/token-leak guards (send_slack_response_url+fetch_slack_filehost allowlists).
- WhatsApp: typing-indicator support (vercel/chat#320; PR #141).
start_typingresolves the latest inbound message id from theThreadHistoryCacheand posts atyping_indicatorpayload (also marking the message read); Graph API default bumped v21.0 → v25.0;_graph_api_requestand the typing-indicator failure path raiseAdapterErrorinstead ofRuntimeError. - Slack / Google Chat: 4.30 rendering fixes (vercel/chat#523, #553, #573; PR #141). Includes collapsing redundant autolink formatting for Google Chat email/
mailto:links (port of upstream177735a).
- Twilio media-download SSRF guard.
rehydrate_attachmentvalidates the rehydrated media URL (https + Twilio-owned host) inside the download closure before forwarding the account SID / auth token as HTTP Basic, where upstreamfetchTwilioMediaGETs the URL blindly. Folded into the existingrehydrate_attachmentURL allowlist non-parity row (Slack / Teams / Google Chat / Twilio); enforcesCLAUDE.md's SSRF rule. Regression:tests/test_twilio_adapter.py::TestRehydrateAttachment::test_media_downloader_refuses_untrusted_hosts.
A pre-ship parity audit (the CI fidelity check covers only the mapped CORE packages/chat tests, never the adapters) surfaced 9 gaps present since 0.4.29 — the chat@4.29.0 and chat@4.30.0 tags are content-identical for the mapped core — and all but one (Linear agent sessions, deferred to 4.31) were closed in this wave, across four PRs:
- Google Chat: clear
cardsV2on edit-to-plain-text (PR #148). BUG — editing a card message down to plain text left the old card stranded on the message;edit_messagenow sends an explicit emptycardsV2so the card is dropped (streaming finalization is the common trigger). PlusSelect/RadioSelect→selectionInputwidgets: these card elements now render as Google ChatselectionInputwidgets and the selected option is read back fromformInputs. - Teams: ChoiceSet auto-submit fan-out (PR #149). BUG / contract break — an Adaptive Card
Action.Submitcarrying multiple input keys now fires oneprocess_actionper input key, so eachon_action(input_key)handler runs, instead of a single__auto_submitdispatch that no handler matched. Pluslist_threads/post_channel_message(were raisingChatNotImplementedError, now implemented) andapi_url/TEAMS_API_URLfor a custom Bot Framework endpoint (GCC-High / sovereign cloud). api_urlcustom-endpoint config across Slack / Discord / GitHub / Linear (PR #150). Custom base URLs for GovSlack / Enterprise Grid / GitHub Enterprise / self-hosted Linear; an empty string falls back to the default, matching upstream's truthy-spread default. Plus GitHub publicget_installation_id().- Core: root
chat_sdkre-exports the deprecatedchat/aitype aliases (PR #147).AiMessage,AiMessagePart,ToAiMessagesOptions, … resolve from the package root again (they had moved tochat_sdk.ai);chat_sdk.aistays the canonical home.
Documented exceptions. 0.4.30 matches chat@4.30.0 with documented exceptions. The Linear agent-sessions surface (#151) is the largest single gap from the audit and is deferred to the 4.31 wave. adapter-web, plus the GitHub / Linear native-client (octokit / linearClient) and message.subject halves, remain documented Known Non-Parity in docs/UPSTREAM_SYNC.md. The 4.31 wave is tracked in #152.
Synced to upstream vercel/chat@4.29.0 (release commit 6581d31, May 18 2026; upstream never tagged chat@4.27.0/chat@4.28.0). Headlines: Meta Messenger adapter (9th platform), chat/ai tool factories (create_chat_tools), callback_url on buttons and modals, Transcripts API + thread_history rename, burst concurrency strategy, a Slack feature wave (verifier precedence flip, external installation providers, native markdown_text, web_client), the upstream adapter-hardening security pass, and a Python floor bump to 3.12. Sets UPSTREAM_PARITY = "4.29.0"; CI fidelity re-pinned to chat@4.29.0.
callback_urlon buttons and modals (vercel/chat#454). Newsrc/chat_sdk/callback_url.pyplus plumbing through cards, modals, chat, channel, and thread: card buttons and modals can carry acallback_urlthat the SDK POSTs to when the action/submit fires, alongside regular handlers. Serialized wire keys stay camelCase (callbackUrl) for cross-SDK state compatibility.- Transcripts API + per-thread cache rename to
thread_history(vercel/chat#448).MessageHistoryCache→ThreadHistoryCache(modulemessage_history→thread_history) with back-compat shims at the old import path and config names (new name wins when both are set, matching upstream's?? config.messageHistoryfallback). The persisted state key prefixmsg-history:is unchanged (renaming would orphan existing data — upstream kept it too). New Transcripts surface for cross-platform per-user history;ChatConfig.transcriptsrequiresChatConfig.identityand raises on misconfiguration, matching upstream's guard. message.subject+fetch_subjectadapter hook (vercel/chat#459, PR #131).MessageSubjectdataclass, optionalBaseAdapter.fetch_subject(raw)hook, lazily-resolved cachedMessage.subjectaccessor, adapter bound at theChat._dispatch_to_handlersconvergence point. The GitHub/Linear adapter halves are blocked on native-client exposure (see Known Non-Parity rows).burstconcurrency strategy (vercel/chat#495, PR #114). Hybrid ofdebounceandqueue: idle threads coalesce a burst window into one dispatch with earlier messages incontext.skipped; busy threads drain likequeue. Note: upstream's PR title says "queue-debounce" but the shipped strategy string is"burst"— the Python port matches the string (cross-SDK config parity).process_messagereturns the handler task (core slice of vercel/chat#444). Streaming callers can await full handler completion and observe handler exceptions;wait_untilkeeps swallowed-error semantics; fire-and-forget callers are unaffected. The@chat-adapter/webpackage that motivated #444 is browser-only and not ported.chat/aisubpath (vercel/chat#492, design #109; PRs #116 + #122).chat_sdk.aiis now a package:ai/messages.py(to_ai_messages, moved with deprecation shims) andai/tools.py—create_chat_tools(chat, preset=, require_approval=, overrides=)returning the 17 upstream tool factories asChatTooldataclasses (JSON-Schemainput_schema, asyncexecute,needs_approval), keyed by upstream's camelCase tool ids. No new runtime dependencies.
chat_sdk.adapters.messenger(vercel/chat#461, design #110; PRs #118 + #124). Full Messenger Platform adapter: GET verification handshake +X-Hub-Signature-256HMAC verification, Graph API client with typed error mapping, text/Generic/Button-template sends with documented caps, buffered streaming (no edit API), postback/reaction/delivery/read handlers, attachment extraction with lazyfetch_data, local message cache backingfetch_messages. New extra:chat-sdk-python[messenger]. Capabilities matrix in the design issue;get_usertracked as #132.
- Slack:
webhook_verifiernow takes precedence oversigning_secret(vercel/chat#468, PR #113). Reverses the 0.4.27 precedence after upstream reversed itself; see the Known Non-Parity row update. Migration: if you relied onsigning_secretshadowing a configuredwebhook_verifier, remove one of the two. - Slack: native
markdown_textfor outgoing messages (vercel/chat#440). Outgoing posts use Slack's native markdown rendering instead of the legacy Block Kit conversion (deferred from 0.4.27). - Slack: external installation providers for bot token management (vercel/chat#467). Pluggable multi-workspace token source composing with the existing dynamic
bot_tokenresolver (per-request ContextVar caching preserved). - Slack:
web_clientproperty (vercel/chat#471/#476/#478, PR #127). Syncslack_sdk.WebClientbound to the request-context token;clientretained as a one-release deprecated alias. - Teams: outbound file delivery via data-URI activity attachments (PR #125, ports upstream
filesToAttachments). Execution artifacts now reach Teams;edit_messagedelivery is a documented deliberate superset. - Telegram:
video_noteextraction + typed attachment uploads (vercel/chat#457, #485; PR #119). - Discord: handle interactions in gateway-only mode (vercel/chat#490).
- Adapter hardening pass (slices of upstream
9824d33): Slack timing-safe socket-token comparison (PR #126), GitHub eager bot-user-ID auto-detection (PR #128), Linear OAuth tokens encrypted at rest with AES-256-GCM (PR #129), and Google Chat fail-closed webhook verification (PR #130 — breaking for gchat configs that previously constructed without any verification gate; setgoogle_chat_project_number,pubsub_audience, or the explicitdisable_signature_verification=Trueescape hatch).
- DM routing precedence docstrings (vercel/chat#491, PR #121).
on_direct_messageandThread.subscribedocstrings now state the DM > subscribed > mention > pattern precedence the runtime already implemented; regression test pins DM > pattern. - Streaming docstring refresh (vercel/chat#463, PR #123).
StreamingPlanOptions.update_interval_msandThreadImpl._handle_streamno longer imply a binary native-vs-fallback split.
- Slack
files_upload_v2confirmation surfaced throughpost()(PR #117; also shipped early as 0.4.27.1).SentMessage.rawcarriesuploaded_file_ids; history persistence nullsrawto avoid storage bloat/PII. - Slack: DM block-action responses no longer thread (PR #137, supersedes #133).
_handle_block_actionsmirrors_handle_message_event's DM handling so HITL button replies don't create phantom "1 reply" threads.
@chat-adapter/web(vercel/chat#444) — browser-only; no Python runtime.@chat-adapter/testskit (vercel/chat#470) —chat_sdk.testingalready covers the surface; row added.- GitHub
octokit/ Linearlinear_clientgetters (vercel/chat#459/#478 halves) — both Python adapters are hand-rolled overaiohttpwith no SDK object to expose; rows added with the revisit conditions (githubkitadoption / an official Linear Python SDK). - Teams migration to
microsoft-teams-apps(issue #93) — explicitly deferred to the 0.4.30 cycle; row added. The 3.12 floor prerequisite (#111) shipped in this release.
- Python floor bumped 3.10 → 3.12 (PR #111).
- Fidelity: CI re-pinned to
chat@4.29.0;MAPPINGupdated for the 4.29 layout (ai.test.tssplit intoai/messages.test.ts+ai/index.test.ts) and extended to the new core test files; converter-exact test renames intests/test_ai_tools.py; twochat.test.tssubject-rehydration ports areskipif-gated onBaseAdapter.fetch_subjectand activate automatically when PR #131 lands. - Next wave: upstream
chat@4.30.0(tagged 2026-06-01) is tracked in #135.
Python-only point release on the 0.4.27 line (branched from v0.4.27; shipped via tag v0.4.27.1 + merged PR #117). Backports the Slack files_upload_v2 confirmation fix (PR #117) so SentMessage.raw carries uploaded_file_ids, plus the history-persistence raw null-out. Shipped ahead of 0.4.29 to unblock chinchill-api's delivery-confirmation gating.
Python-only fix. No upstream version change.
- Slack now surfaces
files_upload_v2confirmation throughpost()—SlackAdapter._upload_filesalready computed the list of Slack-confirmed file IDs butpost_messagediscarded the return, andThreadImpl._create_sent_messagehardcodedraw=None, so the confirmation never reachedSentMessage.raw. Slack was the only file-capable adapter to drop this; discord/telegram upload inline and expose the platform response naturally.post_messagenow augmentsRawMessage.rawwith"uploaded_file_ids"on every return path that can carry files (file-only, card, table, text), andThreadImpl._create_sent_messageaccepts and propagates the adapter'srawintoSentMessage.raw.Nonemeans no upload occurred; an empty list signals Slack confirmed zero attachments. Therawpayload is augmented, not replaced, so existing consumers are unaffected. Unblocks chinchill gating UX on actual delivery success. An upstreamvercel/chatissue is filed in parallel for convergence.
- Added 4 tests: three in
tests/test_slack_api.py(file-only, text+files, and text-only-no-augment paths) and one end-to-endtests/test_thread_faithful.pytest verifyingpost()propagatesRawMessage.rawtoSentMessage.raw.
Alpha sync starter for upstream 4.29.0 (vercel/chat release commit
6581d31, May 18 2026). Upstream skipped tagging chat@4.27.0 and
chat@4.28.0 (only @chat-adapter/shared@4.27.0 and @chat-adapter/shared@4.28.0
got tags); chat@4.29.0 is the next real tag and the target of this
wave. No feature ports in this release — parity-bookkeeping bump
that sets UPSTREAM_PARITY = "4.29.0" and lays out the porting plan
below.
Each substantive commit lands as its own PR (matching the cadence used during the 4.27 sync: #83, #85, #86, #87, #88, #89, #90, #91, #92, #99, #101, #103, #104, #105). Tracking issue: #98.
webhook_verifiernow takes precedence oversigning_secret(vercel/chat#468, commit0f0c203). When a Slack adapter is constructed with bothwebhook_verifierandsigning_secret(or withwebhook_verifierwhileSLACK_SIGNING_SECRETis set in the env), the verifier wins and the signing-secret path is dropped entirely. This reverses the precedence the Python port shipped in 0.4.27 (PR #87), which preferredsigning_secretto match upstream's intent at that time. Upstream reversed itself in vercel/chat#468 (chat@4.29.0) so an env-configuredSLACK_SIGNING_SECRETcould not silently shadow a verifier the caller wired up; this port now follows. Migration: if you relied on a configuredsigning_secretoverridingwebhook_verifier, drop thewebhook_verifierfrom yourSlackAdapterConfig(or, if you wired the verifier in deliberately, your signing-secret path is now correctly inert and you can remove it). The built-in HMAC + 5-minute timestamp tolerance only applies on the signing-secret path; verifier implementers remain responsible for replay protection (slack/types.pySECURITY contract).
-
chat/aisubpath for AI SDK utilities (vercel/chat#492). New public API surface:createChatTools,toAiMessagesfor LLM/agent integration. Vercel AI SDK is TS-only; the Python equivalent needs a design call (see open question). Likely the biggest single PR in the wave. -
queue-debounceconcurrency strategy (vercel/chat#495). New strategy beyond the existingdrop/queue/debounce/concurrent. - Transcripts API + per-thread cache rename to
threadHistory(vercel/chat#448). New API surface; the cache rename has chinchill-api blast radius. -
callbackUrlon buttons and modals (vercel/chat#454). -
message.subject+ adapter client access (vercel/chat#459).
-
adapter.clientrename →adapter.octokit/adapter.linearClient/adapter.webClient(vercel/chat#478). Public API rename across all adapters; deprecation shims advisable for one release. -
private→protectedfor subclassing (vercel/chat#475). Already addressed — Python convention uses_underscore(de-facto protected); audit confirmed no__name_mangledinternals across all 8 adapters. No work needed.
- Native
markdown_textfor outgoing messages (vercel/chat#440). Was listed as "deferred" in 0.4.27. - External installation provider for bot token management (vercel/chat#467). Multi-workspace token mgmt extension.
- Flip
webhook_verifier > signing_secretprecedence (vercel/chat#468). Our 0.4.27 explicitly went the other direction ("match upstream" intent, with comment). Upstream has since reversed itself in #468. The comment onadapter.py:385is now stale; flip precedence + refresh comment + update tests. - Expose direct
WebClientviaadapter.client(vercel/chat#471, reverted in #472, reapplied in #476). Pairs with the #478 rename.
- Handle interactions in gateway-only mode (vercel/chat#490). Related to issue #57 (Discord native Gateway). Decide if Gateway support lands in this wave or stays on a separate track.
- Typed attachment uploads (vercel/chat#485). Bundled with related Telegram polish.
-
video_note(round video messages) inextractAttachments(vercel/chat#457). - MarkdownV2 entity safety trim to streaming chunks
(vercel/chat#446). Already addressed in our 0.4.27 — the
_trim_to_markdown_v2_safe_boundary/_find_unclosed_link_dest_open_bracket/_slice_to_utf16_unitshelpers from PR #89 cover this. No work needed.
- Migrate to
microsoft-teams-appsSDK (issue #93). Replaces our hand-rolled Bot Framework REST streaming withctx.stream.emit(). Requires Python 3.12 floor bump. Headline Teams change for this wave (or 0.4.29.1 if the migration slips).
-
@chat-adapter/messenger(vercel/chat#461). Brand-new Meta Messenger Platform adapter. Similar scope to porting WhatsApp or Telegram from scratch — own file tree undersrc/chat_sdk/adapters/messenger/, full webhook / message / attachment surface, ~1,500 LOC estimate. - [⏭️]
@chat-adapter/web(vercel/chat#444). Vue + Svelte browser UI for chat-sdk bots. Out of scope — no browser runtime in chat-sdk-python. -
@chat-adapter/teststest kit (vercel/chat#470). Test utilities for adapter authors. We already have an adapter-test pattern; evaluate whether to mirror.
@chat-adapter/webas above.- Documentation site changes —
apps/docs/, MDX refreshes, etc. - Vercel-specific release/CI automation (#465, #466, #511, #512, #520).
chat/aisubpath — Python design. Detailed scoping in design issue (see below). Recommended shape: shared SDK-agnostic core inchat_sdk/ai/tools.py+ thin per-SDK adapters (Anthropic, OpenAI) via optional extras (chat-sdk-python[ai-anthropic],chat-sdk-python[ai-openai]). 17 tool factories + the existingto_ai_messages(already inchat_sdk/ai.py). ~7 engineer-days. Three sub-questions: approval-flow contract, hand-written JSON Schema vs Pydantic v2, ship OpenAI extras in first cut?- Messenger adapter (vercel/chat#461) — Python port. Detailed
scoping in design issue (see below). Mirrors WhatsApp adapter
conventions; ~1,500 LOC prod + ~2,500 LOC tests; 2 PRs (scaffolding
then adapter); ~5–6 days. Three sub-questions: init-failure
semantics, postback
valuepassthrough, signature-failure HTTP status code (upstream returns 403, our other adapters return 401). - Cadence: ship as one wave (4.27 → 4.29) or split into 0.4.28 then 0.4.29?
- Python floor bump to 3.12 (required for Teams SDK migration — issue #93). Confirm chinchill-api compatibility before committing.
- Discord Gateway scope: ship Gateway support in this wave (issue #57) or keep gateway-only mode fix (vercel/chat#490) isolated?
adapter.clientrename: ship deprecation shim for one release, or hard cutover?
- This alpha PR establishes the sync. CI on this draft is intentionally
not invoked (lint.yml is gated on
!github.event.pull_request.draft). - Each item above lands as its own PR. Each port PR:
- Updates the relevant
MAPPING/ fidelity coverage and removes its entries fromscripts/fidelity_baseline.jsonif previously baselined. - Bumps lint.yml's pinned upstream ref to
chat@4.29.0(the new tag) once the first feature port lands. - Adds an entry under the next CHANGELOG heading (
0.4.29a2,0.4.29a3, …).
- Updates the relevant
- Once all items are ported (or explicitly documented as divergence in
docs/UPSTREAM_SYNC.md), the final PR cuts0.4.29and switches CI back to strict fidelity at the upstream tag.
Synced to upstream vercel/chat@4.27.0 (release commit f55378a, Apr 30 2026). Highlights: Slack Socket Mode + dynamic bot-token resolver, Teams native DM streaming, chat.get_user() across all 8 adapters, Telegram MarkdownV2 rendering, and a sweep of adapter bug fixes. Sets UPSTREAM_PARITY = "4.27.0".
Chat.get_user(adapter, user_id)for cross-platform user lookups (#90, vercel/chat#391). ReturnsUser | Nonewithemail,display_name,avatar_url,is_botpopulated from each platform's user-lookup API. Every adapter exposesasync def get_user(user_id); Telegram is best-effort (getChatonly), WhatsApp returns minimal user info (Cloud API has no separate lookup).ExternalSelect.initial_option+option_groups(#84, vercel/chat#410, #397). Type extensions onExternalSelect; Slack adapter serializesoption_groupsto Block Kit.concurrency.max_concurrenthonored inconcurrentstrategy (vercel/chat#419) — already enforced in the Python port viaasyncio.Semaphore; upstream has caught up. Divergence row indocs/UPSTREAM_SYNC.mddowngrades from "silent correctness bug upstream" to "behavior parity restored".
- Socket Mode transport (#86, vercel/chat#162). New
SlackAdapterConfig(mode="socket", app_token="xapp-...")opens a persistent WebSocket viaslack_sdk.socket_mode.aiohttp.SocketModeClient. Outer reconnect loop (1s → 30s exp backoff, 250ms shutdown poll) layered on top of the SDK's auto-reconnect. Forwarded-events receiver inhandle_webhookfor the serverless variant (x-slack-socket-token,hmac.compare_digest).ModalResponse(action="clear")lands too. New optional extra:chat-sdk-python[slack-socket]. Closes #68. - Dynamic
bot_tokenresolver + customwebhook_verifier(#87, vercel/chat#421).bot_tokennow acceptsstr | Callable[[], str | Awaitable[str]]; resolver is invoked per request and cached in a per-instance ContextVar so concurrent webhooks don't share tokens.webhook_verifierreplaces built-in HMAC + timestamp verification (returning astrsubstitutes the canonical body).signing_secretprecedence overwebhook_verifierpreserved.schedule_message().cancel()andAttachment.fetch_dataare rotation-safe. NewSlackAdapter.current_token_async()for cron-style callers outsidehandle_webhook. - Slack streaming team_id fix for interactive payloads (#85, vercel/chat#330).
recipient_team_idextraction now walksteam_id→team(string) →team.id(object) →user.team_idin order, returningNoneonly when no string ID is found. Previously the entireteamdict was forwarded forblock_actions, breaking streaming routing. - Link-preview unfurl enrichment (#89, vercel/chat#395).
message_changedevents are routed through a new_handle_message_changedhandler with a 2s poll window and per-event link cache (1h TTL), so the message handler sees enriched links. @mentionregex preserves email addresses (#91, vercel/chat#394). The@usermatcher now skips@characters inside email localparts.- Empty
thread_tsguard (#89, vercel/chat#292).stream()now degrades to a singlepost_messagefor emptythread_tsinstead of raising — top-level Slack DMs encode thread IDs with an emptythread_tsby design, and the oldValidationErrorsilently dropped the reply.
- Native streaming for DMs via emit (#88, vercel/chat#416). DM threads use the Bot Framework streaming protocol (
channelData.streamType=streaming+streamSequence, then a finalstreamType=finalmessage); group chats accumulate and post once (matches upstream's flicker-free behavior). NewTeamsAdapterConfig.native_stream_min_emit_interval_ms(default 1500ms) honors Teams' ~1 req/sec quota;StreamOptions.update_interval_msoverrides. Send-failure mid-stream cancels the session and re-raises soThread.streamhistory matches user-visible text. Migration tomicrosoft-teams-apps(Python SDK, GA 2026-05-01) tracked as #93 for 0.4.28. - DM conversation ID resolution for Graph API (#85, vercel/chat#403). Bot Framework opaque DM IDs are rejected by Graph's
/chats/{chat-id}/messagesendpoint; the adapter now caches the user'saadObjectIdfrom inbound activities into aTeamsDmContextkeyed by base conversation ID and resolves to the canonical19:{userAadId}_{botId}@unq.gbl.spacesform on Graph calls.
- MarkdownV2 rendering (#89, vercel/chat#407). Replaces the legacy
Markdownparse_mode withMarkdownV2. Three escape contexts (normal text, code blocks, inline-link URLs) handle the spec's 18-char escape set per region.
- Card text deduplication (#89, vercel/chat#256). Card posts omit
contenton create (Discord renders bothcontentand the embed otherwise); edits explicitly sendcontent: ""so leftover text from a previous edit is cleared.
- Markdown parser completeness (#101). GFM task lists (
- [ ]/- [x]→checked: bool), backslash-escaped delimiters (lookbehind(?<!\\)on inline regexes), inline math ($x$) preserved by_remendand the format converter. Sentinel-based escape protection prevents pathological backslash sequences from being eaten by emphasis/strikethrough regexes. - Streaming markdown list-marker awareness + table chunk-boundary (#99, issue #69).
_get_committable_prefixknows about list-marker positions so a chunk boundary lands cleanly; tables that span chunk boundaries are wrapped so the first chunk doesn't ship a half-table. SlackAdapter._upload_filesuseschannel=notchannel_id=forfiles_upload_v2(#103, issue #102). The underlyingfiles_completeUploadExternalforwardschannel_id=channelinternally, so caller-suppliedchannel_id=collided and raisedTypeErroron every Slack file upload.- Adapter dict-StreamChunk support (#105).
slack,github, andgoogle_chatstream loops now honor the dict-shaped{"type": "markdown_text", ...}chunks thatthread.py's_from_full_streamhas always forwarded (Teams already honored). Slacksend_structured_chunkrewritten with a_read()helper for dict/dataclass uniformity; fallback warning message rewritten to name the actual possible causes. - Google Chat card text rendering (#92).
GoogleChatFormatConverternow uses the full markdown parser for card text (was a regex stub that dropped formatting). - Adapter init logs + adapter-list in not-found errors (#104).
GoogleChatAdapter.initialize()andGitHubAdapter.initialize()now log on init (matching Slack/Teams).Chat.channel()/Chat.thread()"adapter not found" errors append(registered adapters: [...])so operators can disambiguate "never constructed" from "wrong lookup name".
- Review-loop discipline (
docs/UPSTREAM_SYNC.md,docs/SELF_REVIEW.md). Codifies the lessons learned from this wave: self-review before opening the PR (cheaper than bot rounds), trace fix cascades across overlapping PRs, prefer official SDKs over hand-rolled implementations, cap drafts to 3–4 in flight, divergence budget of ≤2 per sync PR.docs/SELF_REVIEW.mdadds adversarial check categories (input sweeps, emit/parse symmetry, pass-interaction, unforgeable sentinels, rebind/state coherence).
@chat-adapter/web(Vue + Svelte browser UI, vercel/chat#444) — no browser runtime in chat-sdk-python.- Teams SDK 2.0.8 +
User-Agentheader (vercel/chat#415) — JS-only. The Python Teams adapter uses rawaiohttp, notbotbuilder; tracked indocs/UPSTREAM_SYNC.mdnon-parity table as a deferred enhancement. - Bundled guide markdown + templates manifest (vercel/chat#423) — TS-monorepo authoring resources, not runtime behavior.
Upstream cut versions for the entire monorepo on Apr 30 2026 (commit f55378a), but only @chat-adapter/shared@4.27.0 got a git tag — no chat@4.27.0 tag was published. The fidelity workflow (scripts/verify_test_fidelity.py, .github/workflows/lint.yml) stays pinned to chat@4.26.0 for this release; it'll move to a 4.27 SHA pin (or a real tag if upstream publishes one) in the next sync.
Python-only fix. No upstream version change.
SlackFormatConverter.render_postablenow uses the AST path for all markdown inputs (issue #81). Previously,PostableMarkdownand{"markdown": ...}dict inputs were routed through a private regex helper (_markdown_to_mrkdwn) that truncated URLs containing parentheses and diverged silently from the TS SDK'sfromAst(parseMarkdown(text))behavior. Both branches now callfrom_markdown, which goes through the AST.strandrawbranches are unchanged.
- Deleted
_markdown_to_mrkdwn— a regex-based private method with no call sites after the fix above. The TS SDK has no equivalent; its presence was an undocumented divergence. Removes a confusing dead-code path and restores structural parity withadapter-slack/src/markdown.ts.
render_postablenow handles card and object-with-ast inputs — added{"card": ...}dict,{"type": "card", ...}CardElementdict,{"ast": ...}dict, and.card/.astattribute branches, plusstr(message)fallback for unrecognized types. Matches the full union ofAdapterPostableMessagevariants.
- Added 19 tests to
tests/test_slack_format.pycovering allrender_postablebranches, every_node_to_mrkdwnnode type (heading, blockquote, thematic break, image with/without alt), the remainingextract_plain_textpaths (strikethrough, bare URL, channel mentions), andto_blocks_with_tableedge cases (non-dict AST, standalone table, column alignment).
Parity catch-up with upstream 4.26.0. No upstream version change.
Thread.get_participants(): returns unique non-bot, non-self authors who've posted in the thread. Seeds fromcurrent_message.author(if eligible), then iteratesall_messages()and dedupes byuser_id. Mirrors upstream TSThread.getParticipants(). Issue #54.Chat.on_options_load(...)+Chat.process_options_load(...): port of upstreamonOptionsLoad/processOptionsLoadfor handling external-select option-load events. Specific action IDs run before catch-all handlers; errors are logged and skipped so later handlers still get a chance. New public types:OptionsLoadEvent,OptionsLoadHandler.- Slack
block_suggestiondispatch: the Slack adapter now routesblock_suggestioninteractive payloads throughprocess_options_loadand serializes the result to a Slack options JSON response. The handler is raced against a 2.5s budget (OPTIONS_LOAD_TIMEOUT_MS); on timeout the response is empty options and the orphaned task still logs errors viaasyncio.shield. Issue #50. IoRedisStateAdapter:RedisStateAdaptersubclass defaulting to theioredis_lock-token prefix used by upstream Vercel Chat'sioredis-backed state. Enables cross-runtime Redis sharing between TS and Python chat-sdk deployments during migrations. Closes #71. Note: the token shape after the prefix diverges intentionally — Python emitsioredis_{ms}_{hex32}(secrets.token_hex(16), CSPRNG) whereas upstream emitsioredis_{ms}_{base36<=13}(Math.random().toString(36), not CSPRNG). Lock-release still works across runtimes because each runtime generates its own token on acquire andrelease_lock/extendcompare the full token string — the divergence is observability-only (log lines, bytes-in-Redis), not a functional incompatibility. We will not regress toMath.random()for cosmetic byte-for-byte parity.RedisStateAdapter(token_prefix=...): newtoken_prefixkwarg (default"redis"). Parameterizes the lock-token prefix for observability and interop.StreamingPlan/StreamingPlanOptions(chat_sdk.plan): aPostableObjectwrapping an async iterable with platform-specific streaming options (group_tasks,end_with,update_interval_ms). Mirrors upstreamstreaming-plan.ts. Issue #56.Adapter.rehydrate_attachmenthook +Attachment.fetch_metadata: port of upstream'srehydrateAttachmenthook.Chat._rehydrate_messageinvokes the hook on every attachment that lost itsfetch_dataclosure during a JSON roundtrip (queue / debounce / persistent state). The new serializablefetch_metadata: dict[str, str] | Nonefield persists adapter-specific identifiers (Slackurl+teamId, Teamsurl, Google ChatresourceName+url, TelegramfileId, WhatsAppmediaId). Implementations land on Slack, Teams, Google Chat, Telegram, and WhatsApp. Each rehydrate closure validates the target URL against a per-adapter allowlist before forwarding the auth token (SSRF defense). Closes #52.
- Teams:
TeamsAuthCertificateconfig shape (Issue #58). Ports the upstreamTeamsAuthCertificateinterface (adapter-teams/src/types.ts:3-10) as a Python dataclass withcertificate_private_key,certificate_thumbprint, andx5cfields.TeamsAdapterConfig(certificate=...)is accepted and re-exported fromchat_sdk.adapters.teamsso consumers can code against the shape ahead of MS Teams SDK support. Passing a non-Nonevalue still throws at adapter startup — the error message is now verbatim withadapter-teams/src/config.ts:13-18("Certificate-based authentication is not yet supported by the Teams SDK adapter. Use appPassword (client secret) or federated (workload identity) authentication instead."). Not a functional implementation; upstream does not implement cert auth either.
- Ported the 4
[getParticipants]tests fromthread.test.tsand the 4[thread]factory tests fromchat.test.ts(existing-behavior coverage forChat.thread(id)). Closes 8 fidelity gaps. - Ported 19
[post with Plan]tests fromthread.test.ts— closes #55. - Ported 6
[Streaming]StreamingPlan option-variant tests from upstreamthread.test.ts— closes #56.
Plan.update_task(input)now honorsinput.id— previously only worked on the last in-progress task; withidset, targets that specific task and returnsNonefor unknown IDs. Matches upstreamUpdateTaskInputsemantics.Plan.add_task()/update_task()now propagateadapter.edit_objecterrors — previously swallowed and logged; upstream returns the chained promise so callers see failures.- Plan edit queue is now actually sequential under concurrency — previously racy under
asyncio.gather; rewrote_enqueue_editto build the chain synchronously before awaiting, matching upstream TS's.then-based chain. Fixes out-of-order edits when multipleadd_task/update_taskcalls interleave. StreamingPlanoptions now wired throughThread.post()— the Python port was missing theStreamingPlanclass entirely, sogroup_tasks/end_with/update_interval_mswere silently dropped (a plain async iterable was the only way to stream, and options went nowhere). Upstream already had thekind === "stream"branch that mapsgroupTasks → taskDisplayMode,endWith → stopBlocks, andupdateIntervalMs → updateIntervalMsontoStreamOptionsbefore invokingadapter.stream(...)or the fallbackpost+editpath. Issue #56.
- Sweep remaining
time.sleep→await asyncio.sleepin async tests (test_memory_state.py,test_state_postgres.py). Closes the same flaky-test hazard fixed for the Redis backend in PR #73.
verify_test_fidelity.pynow enforces against upstream on every PR (.github/workflows/lint.yml); fails when the upstream clone is missing or when any mapped TS file can't be found. Workflow runs--strictand the clone step no longer carriescontinue-on-error: true, so infra failures surface immediately at the job level. Baseline shipped empty (all previously-missing tests ported in this release) — strict fidelity for mapped core files (8 of 17packages/chat/src/*.test.tsfiles; see theMAPPINGdict inscripts/verify_test_fidelity.pyfor the authoritative scope list). Closes #53.
Python-only follow-up on 0.4.26. Still alpha — APIs may change.
- Slack native streaming:
SlackAdapter.stream()no longer callsAsyncWebClient.chat_stream(...)withoutawait. The unawaited coroutine returned a truthy object, and the firststreamer.append(...)raisedAttributeError, breaking native Slack streaming for any consumer using the default adapter. Issue #44. - Teams divider renders at non-zero height: empty
Containerwithseparator: Truerendered as zero-height in the Teams UI. Dividers between siblings now hoistseparator: Trueonto the following element; a trailing divider emits a minimal non-empty Container. Issue #45. ConcurrencyConfig.max_concurrentis now enforced: consumers settingconcurrency=ConcurrencyConfig(strategy="concurrent", max_concurrent=N)now actually get anasyncio.Semaphore(N)cap on in-flight handlers. Previously the field was accepted and ignored (upstream TS has the same gap).None/ unset keeps the unbounded default. Issue #51.
- Fallback streaming runtime robustness (cluster of fixes): framework-
agnostic
request.text()handling now tolerates sync Flask-style requests (was raisingTypeError: object is not awaitable). Handlers typedCallable[..., Awaitable[None] | None]may return sync (None) — the dispatcher nowawaits only wheninspect.isawaitable()confirms, preventing runtime crashes on sync handlers. max_concurrentenforcement (see above) — upstream accepts the config field but never enforces it; we do.
Chat.thread(thread_id, *, current_message=None): new worker- reconstruction factory mirroring TSchat.thread(threadId). Adapter is inferred from the thread-ID prefix; state and message history come from the Chat instance.current_messageis preserved so Slack native streaming still works post-reconstruction. Issue #46.SlackAdapter.current_token/current_client: public@propertyaccessors for the request-context-bound bot token and a preconfiguredAsyncWebClient. Replaces underscore access from consumer code making direct Slack Web API calls inside a handler (email resolution, user profile fetches, etc.). Issue #47.
- Pyrefly: 213 → 0 type errors; baseline file removed. CI now enforces
zero errors. Root causes fixed: 8-adapter
lock_scope: LockScope | Noneprotocol conformance;_ChatSingletonasProtocol; submodule-awarereplace-imports-with-any;NoReturnon error re-raisers;inspect.isawaitableguards for duck-typed request handling and sync-or-async handler dispatch. NoAnywidening, no new# type: ignorelines beyond 10 at adapter event-construction sites wherethread=None/channel=Noneget re-wrapped byChatbefore handler dispatch (matches upstream TS'sOmit<>partial-event pattern). - Test count: 3545 passed, 2 skipped.
onOptionsLoadhandler for dynamic select dropdowns — issue #50Thread.getParticipants()method — issue #54rehydrate_attachmentadapter hook for queue/debounce + attachments — issue #52- 40 upstream tests without Python equivalents (Options Load, Plan variants, StreamingPlan options, getParticipants) — issue #53
- Discord native Gateway WebSocket (HTTP-only today) — issue #57
- Teams certificate-based mTLS auth — issue #58
- Google Chat file uploads (TODO upstream too) — issue #59
- Global handler-dispatch bound across reactions/actions/slash/modals — issue #61
Synced to Vercel Chat 4.26.0.
- Standalone
reviver: new top-levelchat_sdk.reviverfunction for deserializingThread,Channel, andMessageobjects without importing aChatinstance. Designed for Vercel Workflow step functions and any environment where pulling adapter dependencies is undesirable. Use it asjson.loads(payload, object_hook=reviver). Lazy adapter resolution:chat.register_singleton()/chat.activate()must still be called before thread methods likepost()are invoked. - Workflow-safe
to_json():Thread.to_json()andChannel.to_json()now prefer the stored_adapter_nameoverself.adapter.name, so objects revived without a singleton can still be re-serialized.
- Fallback streaming no longer edits/posts empty content:
Thread.post(stream)on adapters without native streaming no longer sends{markdown: ""}during the LLM warm-up or when a chunk buffers to whitespace. Empty streams with placeholders disabled now post a single space rather than an empty string (a non-emptySentMessageis required by the stream contract). - Slack empty header cells: Markdown tables with an empty header cell now render as a single space in the Slack table block instead of being rejected by the Slack API. Replaces a truthiness-based fallback with an explicit length check, matching upstream.
- Google Chat custom link labels:
[Click here](https://example.com)now renders as<https://example.com|Click here>(Google Chat's supported custom-label syntax) instead ofClick here (https://example.com).
- Fallback streaming clears stranded placeholders: when a stream produces only whitespace with the default placeholder enabled, the final edit replaces
"..."with" "so the message doesn't render as permanently loading. Upstream 4.26 intentionally leaves the placeholder visible to avoid empty-edit API calls; we issue one final edit to" "instead. Documented under Known Non-Parity. - Google Chat
<url|text>round-trip: upstream 4.26 emits Google Chat's custom-label link syntax in the outgoing direction but doesn't parse it back into_ast()/extract_plain_text(). A[label](url)posted through the gchat adapter would round-trip back as raw"<url|label>"text with no link node, breaking downstream handlers. We added the inverse regex to close the round-trip. Documented under Known Non-Parity. from_json(data, adapter=X)syncs_adapter_name: upstream leaves_adapterNameat the payload value even when an explicit adapter is bound, soto_json()can emit a stale name that refers to a different adapter than what runtime calls use. We update_adapter_name = adapter.nameon explicit rebind so serialize and runtime stay consistent. Documented under Known Non-Parity.- Google Chat
<url|text>emit falls back totext (url)when it can't round-trip: the custom-label syntax is only safe when the label doesn't contain|/>/]/ newline, the label is non-empty, and the URL has an RFC 3986 scheme and no|or>. Upstream unconditionally emits<url|text>, producing malformed output for the edge cases. We fall back totext (url)(or bare URL for empty labels) so the content survives the round-trip and Google Chat's auto-link detection still fires for http(s) URLs. Documented under Known Non-Parity. - Google Chat headings render as bold:
#/##/ etc. emit as*text*for visual distinction. Upstream falls through to plain-text concatenation and loses the visual hierarchy entirely. Google Chat has no heading syntax, and bold is the closest approximation the platform supports. Documented under Known Non-Parity. - Google Chat images render as
{alt} ({url})(or bare URL): upstream has no image branch — the default fallback concatenates children only and silently drops the URL. We preserve the URL so the content isn't lost. Documented under Known Non-Parity. - Fallback streaming captures stream exceptions and flushes before re-raising: if the text stream iterator raises mid-flight (e.g. LLM connection drops),
_fallback_streamnow awaitspending_edit, flushes whatever partial content was rendered, clears the placeholder if appropriate, and THEN re-raises the original exception. Upstream propagates immediately, orphaningpendingEditas a background task and stranding"..."on the message. Documented under Known Non-Parity. - Fallback streaming final SentMessage carries repaired markdown: the returned
SentMessage.markdownisrenderer.finish()output (_remend'd — inline markers auto-closed). Upstream ships rawaccumulated. Narrow UX refinement — unobservable unless the stream ends mid-marker. Documented under Known Non-Parity.
Synced to Vercel Chat 4.25.0. New versioning: 0.{upstream_major}.{upstream_minor} embeds the upstream version directly.
- Plan blocks:
PlanPostableObject for structured task lists with live updates. Post a plan to a thread, thenadd_task(),update_task(), andcomplete()with automatic card rendering. - Streaming table option:
StreamingMarkdownRenderer(wrap_tables_for_append=False)disables code-fence wrapping for platforms with native table support. Slack adapter now uses this by default. - Teams Select/RadioSelect:
SelectandRadioSelectcard elements now render as Adaptive CardInput.ChoiceSetwith auto-injected submit button. - GitHub issue threads:
issue_commentwebhooks on plain issues (not just PRs) now create threads with formatgithub:owner/repo:issue:42. - Slack OAuth redirect fix:
handle_oauth_callbackcorrectly forwardsredirect_urioption.
- Version scheme changed from
0.0.1aXto0.{upstream_major}.{upstream_minor}[.patch] UPSTREAM_PARITYconstant inchat_sdk.__init__for programmatic access- Sync procedure documented in UPSTREAM_SYNC.md
Python 3.10 support, async-safe Chat resolver, and a large correctness audit.
Python 3.10 is now supported. CI tests 3.10 through 3.13.
Breaking changes (all alpha — no stable API guarantees yet):
- Serialization keys are now camelCase (
threadId,channelId,adapterName) to match the TS SDK.from_json()accepts both camelCase and snake_case, so existing stored data still loads. PermissionError→AdapterPermissionError: the old name shadowed Python's builtin. If you import it, update the name.StateNotConnectedErrorreplaces bareRuntimeErrorwhen calling state methods beforeconnect(). CatchStateNotConnectedErrorinstead ofRuntimeError.OnLockConflictcallbacks should return"force"or"drop"(strings). ReturningTruestill works for backward compat but is deprecated.reviver()no longer registers a global singleton. Each reviver is bound to the Chat that created it.
Thread and Channel deserialization now supports three resolution levels:
# 1. Explicit (best for library code, multi-tenant)
thread = ThreadImpl.from_json(data, chat=my_chat)
# 2. Context-local (best for tests, request scoping)
with chat.activate():
thread = ThreadImpl.from_json(data)
# 3. Global (existing pattern, unchanged)
chat.register_singleton()
thread = ThreadImpl.from_json(data)Concurrent async tasks using activate() are fully isolated — each task resolves its own Chat without interference.
- Fixed streaming: intermediate edits now use the markdown renderer (was sending raw text), paragraph separators between agent steps, 500ms latency on stream end eliminated
- Fixed all adapters: token refresh race conditions, HTTP session reuse (was creating one per request),
limit=0no longer silently replaced by defaults - Fixed serialization: Slack installations now interoperate with the TS SDK, card fallback text extracted properly, AI SDK field names corrected
- Fixed Teams: status code comparison, modal dialog buttons, table cell escaping
- Fixed shutdown: in-flight handler tasks are cancelled, fire-and-forget tasks tracked for GC safety
- 3,359 tests (up from 3,267), 0 warnings, 0 lint errors
- Automated test quality gate in CI (
audit_test_quality.py) - Comprehensive porting guide with 15 hazards and merge checklist
- Known non-parity documented in one place
Coverage and quality improvements.
- Teams adapter: 69% -> 79% line coverage (error handling, Graph API mapping, stream, card extraction, HTTP helpers)
- Telegram adapter: 68% -> 80% line coverage (webhook handling, reaction dispatch, emoji helpers, polling config, pagination, caching)
- Test fidelity: 100% test name alignment with TypeScript SDK (529/529 matched)
- Faithful line-by-line translations of chat/thread/channel test suites
MockAdapter.open_modalaccepts positional args (bug fix)
Test fidelity enforcement + process improvements.
- Added test fidelity verification script
- Aligned all markdown, serialization, and AI test names with TS source
- 100% test name fidelity across all 529 TypeScript tests
Faithful test translations and fidelity tooling.
- Faithful line-by-line translations of chat, thread, and channel tests
- Test fidelity verification infrastructure
Full test parity with TypeScript SDK.
- 3,106 tests, all passing
- Chat orchestrator: 96% of TS (concurrency, lock conflict, slash commands)
- Thread: 137% of TS (streaming, pagination, ephemeral, scheduling)
- Channel: 144% of TS (state, threads, metadata, serialization)
- Markdown: 126% of TS (node builders, round-trips, type guards)
- Integration: 94% of TS (recorded fixture replays for all platforms)
- All 8 adapters: 100%+ of TS test count
Coverage improvements + webhook fixtures.
Systematic port fidelity scan — 10 bugs fixed.
Port fidelity release — 10 critical/high bugs fixed.
Security hardening + launch documentation.
Initial alpha release.