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.