You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Slack messages often carry their real content outside event["text"]. Pasted spreadsheets arrive as table / data_table blocks (top-level or inside attachments), and alerting integrations (Sentry, PagerDuty, GitHub) put their payload in legacy attachment pretext / title / text / fields. The Python adapter builds Message.text / Message.formatted from event["text"] only, so handlers, thread.messages, to_ai_messages and the AI tools see a one-line summary. This issue ports the Slack halves of upstream #817 and #846 so both parse paths fold that content into the message AST and plain text.
Upstream changes
764e4759 fix(slack): preserve pasted tables in message content (#817) — chat@4.38.1 — parses table/data_table blocks (top-level and non-unfurl attachment blocks) into mdast table nodes. Cells are flattened to mrkdwn (blocktext: link/emoji/user/broadcast/channel/usergroup/date/color/team/raw_* values). Headerless pasted tables get an empty header row. Tables before the first non-table block stay above the text. text becomes toPlainText(formatted). The core markdown.ts half belongs to [4.41/C2b] Shared text utilities: bare-mention scanner, code-fence helpers, to_plain_text structural whitespace #193.
864d9222 fix(slack): keep alert attachment content on normalized messages (#846) — chat@4.39.0 — non-unfurl attachments contribute pretext, title (linked to title_link, whose URL is also added to message.links), text and fields, literal unless named in mrkdwn_in. fallback is used only when nothing else renders. Blocks win over legacy fields; each attachment's tables stay next to its text. Mention IDs across body, cells and attachments are looked up in one parallel wave (collectMentionIds / lookupMentionNames / applyMentionNames). Its previous_message hunk in the message_changed path belongs to [4.41/SL6] Slack: dispatch message_changed / message_deleted to lifecycle handlers #211.
src/chat_sdk/adapters/slack/adapter.py:3306-3387_parse_slack_message: raw_text = event.get("text", "") (:3315). At :3343 only that string goes through _resolve_inline_mentions, and :3361-3362 build text/formatted from it via extract_plain_text / to_ast.
adapter.py:3389-3431_parse_slack_message_sync (used by parse_message at :4673) does the same with no lookups.
adapter.py:3082-3131_extract_links reads attachments only for unfurl metadata (from_url/original_url, :3106-3118). title_link is never surfaced.
adapter.py:2923-2989_resolve_inline_mentions does collect, lookup and apply in one method; there is no reusable collect/apply split.
src/chat_sdk/adapters/slack/types.py:224-246SlackEvent TypedDict has no attachments field, and blocks is typed as list[SlackRichTextBlock] only.
Module-level helpers in adapters/slack/adapter.py (or a new adapters/slack/content.py): _TABLE_BLOCK_TYPES, _block_text, _has_bold_text, _table_data, _event_tables (leading/trailing split), _is_foreign_attachment, _author_attachments, _attachment_content (parts plus tables), _literal_phrasing.
Split _resolve_inline_mentions into _collect_mention_ids(text, user_ids, channel_ids), async _lookup_mention_names(...) and _apply_mention_names(text, names). Keep the split-on-< parsing; no new regex over user text.
_content(event, text, attachments) (sync) and async _resolved_content(...): collect IDs from body, cells and attachment parts, one lookup wave, then apply. Plus _assemble_content, _attachment_nodes (literal lines share one paragraph joined by break; each mrkdwn part parsed in isolation), _table_node, _cell_children.
_parse_slack_message and _parse_slack_message_sync: formatted = <content>, text = ast_to_plain_text(formatted) (the C2b-updated helper). parse_message inherits the sync path.
_extract_links: add title_link of non-foreign attachments.
Payloads are untrusted: guard with isinstance(x, dict) / isinstance(x, str) everywhere (upstream's isRecord/str). A cell value may be 0, False or ""; test type / is not None, never truthiness. Match JS String(): check bool before int (bool subclasses int) and emit "true"/"false"; str(3) → "3", str(1.5) → "1.5".
Date cells without fallback: datetime.fromtimestamp(ts, tz=timezone.utc).date().isoformat() (upstream toISOString().slice(0, 10)), never a naive local date.
headerless applies only to type == "table" with no bold cell in row 0; data_table row 0 is always the header.
Attachment title with title_link: f"<{title_link}|{escape_slack_text(title)}>" (adapters/slack/format.py:46), treated as literal.
fallback is used only when parts is empty and the attachment produced no tables.
Routing change: attachment content now reaches message.text, so on_message regex patterns and anything reading text see it. Document that handlers can filter on message.author.is_bot.
No background tasks; everything runs inside the existing message factory.
Tests
Upstream packages/adapter-slack/src/index.test.ts is not fidelity-mapped (MAPPING covers packages/chat/src only). Port into tests/test_slack_adapter.py or a new tests/test_slack_inbound_content.py:
#817: "preserves pasted table attachments as message content", "preserves table-only messages and ignores malformed table blocks", "preserves inline rich text within table cells", "preserves rich text metadata within table cells", "formats date cells from the timestamp when no fallback is present", "preserves empty and raw value cells so columns stay aligned", "parses data_table blocks with their header row intact", "ignores tables in unfurl and app attachments", "keeps tables pasted above the message text above it", "does not leave raw bot mention tokens in table cells".
#846: "preserves alert attachment content as message content", "falls back to attachment fallback only when nothing else carries content", "ignores content in unfurl and app attachments", "keeps attachment formatting characters literal unless mrkdwn_in enables them", "keeps attachment content out of an unclosed code fence in the body", "uses the fallback when attachment blocks carry nothing renderable", "prefers attachment blocks over legacy fields, matching Slack rendering", "links the attachment title to title_link and surfaces the URL", "keeps each attachment's tables adjacent to its text", "resolves mentions in attachment content with a single lookup per user" (assert the users_info AsyncMock await_count == 1).
Python-specific: raw_booleanFalse renders as false; a non-dict attachment or row is skipped without raising; sync and async paths give equal formatted for an event with no mentions; an on_message pattern matching only attachment text now fires. Use the Ev0ATABLE001 payload from packages/adapter-slack/sample-messages.md as the table fixture.
Acceptance criteria
Full validation command from CLAUDE.md passes.
docs/UPSTREAM_SYNC.md updated for any divergence (e.g. a separate content.py module).
CHANGELOG entry under "Unreleased (4.41 wave)".
Consumer-visible changes called out: message.text/formatted now include pasted tables (tab-separated) and alert attachment content; message.links gains attachment title_links; on_message patterns can match attachment text.
Consumer impact: low. Additive, but LLM prompts built from message.text get more content, and alert-bot messages in subscribed channels can now match pattern handlers. Streaming is unaffected.
Summary
Slack messages often carry their real content outside
event["text"]. Pasted spreadsheets arrive astable/data_tableblocks (top-level or inside attachments), and alerting integrations (Sentry, PagerDuty, GitHub) put their payload in legacy attachmentpretext/title/text/fields. The Python adapter buildsMessage.text/Message.formattedfromevent["text"]only, so handlers,thread.messages,to_ai_messagesand the AI tools see a one-line summary. This issue ports the Slack halves of upstream #817 and #846 so both parse paths fold that content into the message AST and plain text.Upstream changes
764e4759fix(slack): preserve pasted tables in message content (#817) — chat@4.38.1 — parsestable/data_tableblocks (top-level and non-unfurl attachment blocks) into mdasttablenodes. Cells are flattened to mrkdwn (blocktext: link/emoji/user/broadcast/channel/usergroup/date/color/team/raw_* values). Headerless pasted tables get an empty header row. Tables before the first non-table block stay above the text.textbecomestoPlainText(formatted). The coremarkdown.tshalf belongs to [4.41/C2b] Shared text utilities: bare-mention scanner, code-fence helpers, to_plain_text structural whitespace #193.864d9222fix(slack): keep alert attachment content on normalized messages (#846) — chat@4.39.0 — non-unfurl attachments contributepretext,title(linked totitle_link, whose URL is also added tomessage.links),textandfields, literal unless named inmrkdwn_in.fallbackis used only when nothing else renders. Blocks win over legacy fields; each attachment's tables stay next to its text. Mention IDs across body, cells and attachments are looked up in one parallel wave (collectMentionIds/lookupMentionNames/applyMentionNames). Itsprevious_messagehunk in themessage_changedpath belongs to [4.41/SL6] Slack: dispatch message_changed / message_deleted to lifecycle handlers #211.683eadc1fix(slack): read mentions from message content (#947) — chat@4.41.0 — classifies mentions in attachment blocks/parts (classifyAttachmentPart). [4.41/SL4] Slack inbound normalization: self-mention decode, content is_mention, mrkdwn fixes, bot author ids, socket retries #209 ports it in full (including its attachment tests) with a minimal private attachment extractor; this issue only makes that extractor reuse the new rendering helpers.Current Python behavior
src/chat_sdk/adapters/slack/adapter.py:3306-3387_parse_slack_message:raw_text = event.get("text", "")(:3315). At:3343only that string goes through_resolve_inline_mentions, and:3361-3362buildtext/formattedfrom it viaextract_plain_text/to_ast.adapter.py:3389-3431_parse_slack_message_sync(used byparse_messageat:4673) does the same with no lookups.adapter.py:3082-3131_extract_linksreadsattachmentsonly for unfurl metadata (from_url/original_url,:3106-3118).title_linkis never surfaced.adapter.py:2923-2989_resolve_inline_mentionsdoes collect, lookup and apply in one method; there is no reusable collect/apply split.src/chat_sdk/adapters/slack/types.py:224-246SlackEventTypedDict has noattachmentsfield, andblocksis typed aslist[SlackRichTextBlock]only.grep -n 'pretext\|mrkdwn_in\|data_table\|title_link\|raw_number' src/chat_sdk/adapters/slack/adapter.pyreturns nothing.src/chat_sdk/shared/markdown_parser.py:1083ast_to_plain_texthas no table/tableRow handling; [4.41/C2b] Shared text utilities: bare-mention scanner, code-fence helpers, to_plain_text structural whitespace #193 fixes that.Scope
adapters/slack/adapter.py(or a newadapters/slack/content.py):_TABLE_BLOCK_TYPES,_block_text,_has_bold_text,_table_data,_event_tables(leading/trailing split),_is_foreign_attachment,_author_attachments,_attachment_content(parts plus tables),_literal_phrasing._resolve_inline_mentionsinto_collect_mention_ids(text, user_ids, channel_ids),async _lookup_mention_names(...)and_apply_mention_names(text, names). Keep the split-on-<parsing; no new regex over user text._content(event, text, attachments)(sync) andasync _resolved_content(...): collect IDs from body, cells and attachment parts, one lookup wave, then apply. Plus_assemble_content,_attachment_nodes(literal lines share one paragraph joined bybreak; each mrkdwn part parsed in isolation),_table_node,_cell_children._parse_slack_messageand_parse_slack_message_sync:formatted = <content>,text = ast_to_plain_text(formatted)(the C2b-updated helper).parse_messageinherits the sync path._extract_links: addtitle_linkof non-foreign attachments._detect_self_mentionattachment extractor at_author_attachments/_attachment_contentso mention classification and rendering read the same parts. No behavior change; SL4's attachment mention tests must still pass untouched.SlackEvent:attachments(blocks,fallback,fields,is_app_unfurl,is_msg_unfurl,mrkdwn_in,pretext,title,title_link, …) and aSlackMessageBlockwithrows.Out of scope
to_plain_texttable rules (keep empty cells, drop empty rows): [4.41/C2b] Shared text utilities: bare-mention scanner, code-fence helpers, to_plain_text structural whitespace #193.is_mentionclassification (body and attachments) and the mrkdwn fence/special-mention normalization into_ast: [4.41/SL4] Slack inbound normalization: self-mention decode, content is_mention, mrkdwn fixes, bot author ids, socket retries #209.previous_messageparsing with a sync fallback on edits: [4.41/SL6] Slack: dispatch message_changed / message_deleted to lifecycle handlers #211.data_tablerendering from cards: [4.41/SL7] Slack cards & modals: data_table / data_visualization blocks, datepicker, number_input, selection change events #212.Porting notes
isinstance(x, dict)/isinstance(x, str)everywhere (upstream'sisRecord/str). A cellvaluemay be0,Falseor""; test type /is not None, never truthiness. Match JSString(): checkboolbeforeint(boolsubclassesint) and emit"true"/"false";str(3)→"3",str(1.5)→"1.5".fallback:datetime.fromtimestamp(ts, tz=timezone.utc).date().isoformat()(upstreamtoISOString().slice(0, 10)), never a naive local date.headerlessapplies only totype == "table"with no bold cell in row 0;data_tablerow 0 is always the header.title_link:f"<{title_link}|{escape_slack_text(title)}>"(adapters/slack/format.py:46), treated as literal.fallbackis used only whenpartsis empty and the attachment produced no tables.message.text, soon_messageregex patterns and anything readingtextsee it. Document that handlers can filter onmessage.author.is_bot.Tests
Upstream
packages/adapter-slack/src/index.test.tsis not fidelity-mapped (MAPPING coverspackages/chat/srconly). Port intotests/test_slack_adapter.pyor a newtests/test_slack_inbound_content.py:users_infoAsyncMockawait_count == 1).raw_booleanFalserenders asfalse; a non-dict attachment or row is skipped without raising; sync and async paths give equalformattedfor an event with no mentions; anon_messagepattern matching only attachment text now fires. Use theEv0ATABLE001payload frompackages/adapter-slack/sample-messages.mdas the table fixture.Acceptance criteria
docs/UPSTREAM_SYNC.mdupdated for any divergence (e.g. a separatecontent.pymodule).message.text/formattednow include pasted tables (tab-separated) and alert attachment content;message.linksgains attachmenttitle_links;on_messagepatterns can match attachment text.Dependencies
Blocked by #209 (content
is_mention, attachment extractor,to_astnormalization; SL4 itself needs #193'sto_plain_texttable rules).Verify first
ast_to_plain_textafter [4.41/C2b] Shared text utilities: bare-mention scanner, code-fence helpers, to_plain_text structural whitespace #193 emits tab-joined cells and drops empty rows; the expectedmessage.textin the upstream tests depends on it.Metadata
message.textget more content, and alert-bot messages in subscribed channels can now match pattern handlers. Streaming is unaffected.sync/4.41-sl5Part of #184.