Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@

Sync wave from `chat@4.31.0` to `chat@4.41.1` (tracking #184). `UPSTREAM_PARITY` stays `4.31.0` until #203.

- **Slack inbound mrkdwn normalization, `channel.post()` thread ids, Socket Mode retries** (#283, part (b) of #209; ports vercel/chat `0b63791b` #667 (Slack half), `92530dd3` #720, `c3118279` #756, `e71bfead` #843 (Slack half) and `44423bdc` #960, chat@4.33.0–4.41.1). A live Slack-loop check is pending.
- **Breaking/consumer-visible (`message.text` is the plain text of `message.formatted`):** both parse paths now set `text = ast_to_plain_text(formatted)` (upstream `toPlainText(formatted)`), replacing the regex pass over the mrkdwn. Markup no longer leaks into `text`: inline-code backticks, list markers (`- a` → `a`, `1. one` → `one`), heading `#` and quote `>` prefixes are dropped, a trailing newline or a whitespace-only body is trimmed, and blocks are separated by a blank line. `SlackFormatConverter.extract_plain_text` drops its Python-only regex override for the base `to_ast` + `ast_to_plain_text` (upstream has none). *Migration:* match structure on `message.formatted`, or on `message.raw["text"]` for the original mrkdwn.
- **Breaking/consumer-visible (mentions and links read differently):** `<!here>` / `<!channel>` / `<!everyone>` become `@here` / `@channel` / `@everyone`, `<!subteam^S1|@eng>` becomes `@eng` (`<!subteam^S1>` → `@S1`), and a labelled channel keeps its id: `<#C123|general>` reads `#general (C123)` instead of `#general`. Tokens inside inline code or code blocks stay literal. A swapped `<label|https://…>` link becomes `[label](https://…)`. `&lt;` / `&gt;` / `&amp;` are now unescaped in `formatted` and `text` (they came through verbatim). The same applies to table cells and mrkdwn attachment parts (#210). *Migration:* update `on_message` regexes and anything matching `#channel-name` or raw `<!here>` / `<!subteam^…>`.
- **Breaking/consumer-visible (code blocks keep their first line):** Slack treats the text right after an opening ```` ``` ```` as code; it was read as an info string and dropped, so `` ```npm test``` `` gave an empty `formatted` (and `text` kept the backticks). Each paired fence now parses as a `code` node and `text` holds the code (`"npm test"`). Unpaired fences, fences in inline code or `<…>` tokens, and fences on `&gt;` quote lines stay literal, as in Slack.
- **Breaking/consumer-visible (`channel.post()` ids):** `post_channel_message` returns `slack:C123:<ts>` (a replyable thread rooted at the new message) instead of `slack:C123:` when Slack returns a string message `ts`, so `chat.thread(sent.thread_id).post(...)` replies in its thread. File-only posts keep `slack:C123:`.
- **Breaking/consumer-visible (Socket Mode retries are processed):** envelopes with `retry_attempt > 0` were acked and dropped; they are now routed like first deliveries and logged at info (`"Processing socket mode retry"` with `retry_attempt`, `retry_reason`, `type`). The event-id marker (#268) and core message-id dedupe (10 min TTL, #191) drop true duplicates, so an event missed during a restart or reconnect is no longer lost.
- Shared `parse_markdown` follows CommonMark's fence rule: a line like ```` ```npm test``` ```` (a backtick in the info string) is no longer a code-block opener, so a Slack quote starting with an inline fence keeps its text instead of becoming an empty code block. This affects every adapter that parses Markdown, only for such lines, and those lines now go through the (quadratic, #308) inline parser.
- Shared `parse_markdown` follows four more CommonMark rules that `message.text` exposed (every adapter that parses Markdown sees them): a paragraph drops its lines' leading spaces/tabs and its trailing ones (`run ```npm test``` please` reads `run`, `npm test`, `please`; a whitespace-padded message is trimmed); a thematic break repeats one marker (`-_-` is text, not an empty break); `_` emphasis is never intraword (`my_var` and `?utm_source=` keep their underscores); links never nest (`[[a](u) b](v)` is the inner link with literal brackets).
- **Python-specific (divergence from upstream):** `parse_markdown` nests at most 100 blockquotes; deeper `>` markers stay literal text. Slack `&gt;` now unescapes, and 1,100 of them raised `RecursionError` and dropped the message. See `docs/UPSTREAM_SYNC.md`.
- Shared `parse_markdown` links accept balanced brackets in the text and balanced parens in the URL (`[build [failed]](u)`, `…/Foo_(bar))`), as CommonMark does, so Slack labelled links read as upstream in `message.text`. Every adapter that parses Markdown sees this; an unbalanced `(` in a URL or a bare `[` in link text no longer forms a link.
- The mrkdwn scanners stay linear on untrusted text (results identical to upstream). Known shared-parser gaps (no multi-backtick code spans, no lazy blockquote continuation so `&gt; quoted` + a reply line reads as two blocks, quadratic time on some inputs, which the fence rule above reaches with new inputs) are tracked in #308; see `docs/UPSTREAM_SYNC.md`.
- **Telegram: multiple files or attachments go out as one media group** (#278; ports vercel/chat `8d7ccdb1` #605, chat@4.34.0).
- **Consumer-visible (Telegram):** `post_message` / `thread.post` with 2–10 `files` or 2–10 `attachments` now sends one `sendMediaGroup` album instead of raising `ValidationError("Telegram adapter supports a single file/attachment upload per message")`. Every sent message is cached and the last one is returned. The caption (with `parse_mode`) goes on the first item only, with the same MarkdownV2 → plain-text retry as single uploads. `thread.reply` threads the album to its target (`reply_parameters`). One file or attachment still uses `sendDocument` / `sendPhoto` / `sendVideo` / `sendAudio`.
- New `ValidationError`s: more than 10 items ("Telegram media groups support 2-10 files"); a card with buttons ("Telegram media groups do not support inline keyboards"); documents or audio mixed with another type (photos and videos may mix). Mixing `files` and `attachments` still raises.
Expand Down Expand Up @@ -89,7 +100,7 @@ Sync wave from `chat@4.31.0` to `chat@4.41.1` (tracking #184). `UPSTREAM_PARITY`
- **Consumer-visible (Slack):** `message.links` gains each non-unfurl attachment's `title_link`.
- **Routing:** attachment and table content now reaches `message.text`, so `on_message` regex patterns (and the core's text-based mention fallback when the bot id is unknown) can match it. Alert-bot messages in subscribed or pattern-matched channels may start matching; filter on `message.author.is_bot` if that is unwanted. LLM prompts built from `message.text` (`to_ai_messages`, history) get the extra content. Streaming is unaffected.
- The async parse path resolves `<@U…>` / `<#C…>` in table cells and attachment parts in one parallel lookup wave; the sync `parse_message` path does no lookups. `SlackEvent` gains typed `attachments` (`SlackAttachment`) and `SlackMessageBlock` (with `rows`).
- **Python-specific (temporary, until #283):** upstream derives all of `text` from `formatted`. That needs #283's inbound mrkdwn normalization; without it, code on a fence's opening line would be lost (`` ```npm test``` `` would give empty text). #283 switches to `ast_to_plain_text(formatted)`, which also drops list, `#`, `>` and backtick markers from body text. Until then, table cells and mrkdwn attachment parts render through the current converter: Slack's `&amp;` / `&lt;` / `&gt;` entities stay, a usergroup shows as a raw `<!subteam^…>` token, and a labelled channel shows as `#name` (upstream shows `#name (C…)`). Body `formatted` has the same gaps today. Live Slack-loop check: pending.
- **Python-specific (temporary, resolved by #283, see the #283 entry above):** upstream derives all of `text` from `formatted`. That needs #283's inbound mrkdwn normalization; without it, code on a fence's opening line would be lost (`` ```npm test``` `` would give empty text). #283 switches to `ast_to_plain_text(formatted)`, which also drops list, `#`, `>` and backtick markers from body text. Until then, table cells and mrkdwn attachment parts render through the current converter: Slack's `&amp;` / `&lt;` / `&gt;` entities stay, a usergroup shows as a raw `<!subteam^…>` token, and a labelled channel shows as `#name` (upstream shows `#name (C…)`). Body `formatted` has the same gaps today. Live Slack-loop check: pending.

- **Slack: Agent messaging experience (`agent_view`) and declarative agent config** (#214; Slack halves of vercel/chat `1721fa01` #684 and `0f743c9b` #698, plus `78021c09` #889 and the Slack part of `c21ccbc0` #943, chat@4.34.0–4.41.0). Slack will retire `assistant_view` in February 2027. Every feature is opt-in on `SlackAdapterConfig` and off by default. A live Slack-loop check is still pending.
- `agent_view=True`: `app_home_opened` fires for every tab and carries `tab` and, when Slack folds one in, `entities`. Each top-level DM message becomes its own thread (`slack:D…:{ts}` instead of `slack:D…:`), so replies thread under the user's message. If the conversation-scoped `slack:D…:` from `open_dm` is subscribed, top-level DMs still route there, so `on_subscribed_message` keeps working.
Expand Down Expand Up @@ -181,7 +192,7 @@ Sync wave from `chat@4.31.0` to `chat@4.41.1` (tracking #184). `UPSTREAM_PARITY`
- Tokens resolve only for the button that minted them (`actionId`) and in the conversation they were posted to. Thread posts, schedules and edits bind to the thread. Channel posts, schedules and channel `SentMessage.edit`s bind to the channel. A `post_ephemeral` DM fallback binds to the DM channel, and with neither native ephemeral nor a DM fallback no token is minted. A click whose action or conversation does not match leaves the record in place.
- The token swap now keeps every button field except `callback_url` (it used to copy a fixed whitelist), so new fields such as `tooltip` (#202) survive.
- **Python-specific (divergence from upstream):** after deleting a matched record, the resolver checks with `extend_lock` that its 10-second lease never lapsed, and returns `None` if it did. Upstream returns the record regardless, so a state call that stalls past the lease can let a second click also resolve and POST. See `docs/UPSTREAM_SYNC.md`.
- **Python-specific (divergence from upstream):** a channel `SentMessage.edit` binds its tokens to the channel (the scope the original `channel.post` used), not to `{reported thread id, "thread"}`. The thread id an adapter reports for a channel post often never equals a click's thread id: Teams and Google Chat report the channel id, Slack reports the synthetic `slack:C…:` (a click carries the message ts, and a Slack DM click carries no ts even once #283 makes the post report one), and a chained edit drops the reported id. Upstream's thread scope would leave those edited buttons never POSTing.
- **Python-specific (divergence from upstream):** a channel `SentMessage.edit` binds its tokens to the channel (the scope the original `channel.post` used), not to `{reported thread id, "thread"}`. The thread id an adapter reports for a channel post often never equals a click's thread id: Teams and Google Chat report the channel id, Slack reported the synthetic `slack:C…:` until #283 and still does without a string ts (a click carries the message ts, and a Slack DM click carries no ts even though the post now reports one), and a chained edit drops the reported id. Upstream's thread scope would leave those edited buttons never POSTing.
- Known gap (see `docs/UPSTREAM_SYNC.md`): Google Chat cards `thread.post`ed into a DM thread never POST (upstream parity: the card click omits the `:dm` suffix).
- API: `process_card_callback_urls(card, state, scope)` takes a required `CallbackScope`. `resolve_callback_url(token, state, context=None)` takes a `CallbackContext` (a `None` context never matches). `ResolvedCallback` gains keyword-only `action_id` and `scope`. New constant: `CALLBACK_LOCK_TTL_MS = 10_000`.
- **BREAKING (security) — Telegram: webhook verification is required by default; repeated updates are deduplicated** (#224; upstream vercel/chat#858, #799, #813).
Expand Down Expand Up @@ -447,7 +458,7 @@ Ports upstream `32687038` (vercel/chat#830, chat@4.38.1), the Google Chat part o
- Fidelity: `ai/index.test.ts` 26 → 0 missing, `ai/messages.test.ts` 10 → 9 at `chat@4.41.1`.
- **Slack: Enterprise Grid org-wide installs, `authorizations[]` routing, event retry marker** (#268, split from #213; ports the non-cache half of vercel/chat `907450d7` #724, chat@4.35.0).
- **Consumer-visible: org-wide OAuth installs now succeed.** `handle_oauth_callback` used to raise `missing access_token or team.id` for an org-wide install (`team: null`). It now stores it under `enterprise.id`, the key org-wide webhooks resolve by, and raises `missing access_token or enterprise.id` when that is absent. The result gains `enterprise_id` and `is_enterprise_install`; `team_id` is the storage key. `SlackInstallation` gains `enterprise_id` / `is_enterprise_install`.
- **Consumer-visible: retried events that were already dispatched are dropped.** Each dispatched event writes a `slack:event-delivered:{event_id}` state key (24 h TTL, fire-and-forget). A delivery with `x-slack-retry-num > 0` (or a forwarded socket event with `retryNum > 0`) whose key exists is acked and not processed; first deliveries never read state, and a retry whose original never arrived is still processed. Live Socket Mode retries are still skipped until #283.
- **Consumer-visible: retried events that were already dispatched are dropped.** Each dispatched event writes a `slack:event-delivered:{event_id}` state key (24 h TTL, fire-and-forget). A delivery with `x-slack-retry-num > 0` (or a forwarded socket event with `retryNum > 0`) whose key exists is acked and not processed; first deliveries never read state, and a retry whose original never arrived is still processed. Since #283, live Socket Mode retries (`retry_attempt > 0`) are routed and reach the marker as well.
- Multi-workspace events resolve their installation from `authorizations[0]` before the top-level `team_id` / `enterprise_id`, so Slack Connect events route to the receiving installation. Socket Mode `events_api`, slash commands and interactive payloads now resolve org-wide installs by enterprise ID, like HTTP.
- Under an org-wide install, the adapter's Web API calls send the event's workspace `team_id`, and calls to the event's channel echo a Slack Connect `context_team_id` as `client_context_team_id`. A `team_id` the caller passes wins. The #95 `chat_stream` `team_id` is unchanged.
- `W…` user ids count as raw user ids in outgoing `@mentions`. `with_bot_token` / `with_bot_token_async` accept keyword-only `installation_id=` to scope installation-owned caches outside webhooks. `RequestContext` gains `team_id`, `context_team_id` and `context_channel`.
Expand Down
Loading
Loading