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
Outgoing Slack text turns @word into mentions in two places: the markdown→Slack payload converter (@name → <@name>) and the adapter's cached-name resolver (@name → <@U…>). Neither pass skips code, and the resolver doesn't skip URLs either. When an agent prints npm i @scope/pkg or `@vercel/postgres`, the snippet is corrupted into a ping. Meanwhile the native streaming path skips name resolution entirely, so a streamed @alice stays plain text while the same text posted normally would ping. This issue moves both passes onto the shared code- and URL-aware scanner from #193, and resolves mentions incrementally on the native stream.
a8c4af74 fix(slack): skip urls during mention resolution (#619) — chat@4.32.0 — replaces the regex passes in resolveOutgoingMentions and SlackFormatConverter (finalize, text nodes) with a char-by-char replaceBareMentions(text, replacer) that skips code, URLs (schemed and schemeless) and <…> tokens.
6f0d2f02 fix(slack): resolve outgoing mentions on the native streaming path (#755) — chat@4.37.0 — before each append, runs the committed renderer text line by line through resolveOutgoingMentions, tracking fence state (fence lines and fenced content stay literal). Deltas are computed against the resolved buffer.
Current Python behavior
src/chat_sdk/adapters/slack/format_converter.py:40BARE_MENTION_REGEX = (?<![<\w/])@(\w+) (ASCII) and :49-67_link_bare_mentions_outside_urls skip only http(s):// spans. They are used at :209 (_finalize) and :228 (mrkdwn text nodes). Code spans are not skipped, so install `@vercel/postgres` becomes `<@vercel>/postgres`.
src/chat_sdk/adapters/slack/adapter.py:3006-3060_resolve_outgoing_mentions uses re.compile(r"(?<![\w<])@(\w+)"). It is Unicode \w (no re.ASCII) and skips neither code nor URLs. It is called only via _resolve_message_mentions (:3062-3076) from post/edit/ephemeral/schedule (:3620, :3733, :4079, :4141).
adapter.py:3966-3969flush_markdown_delta appends raw renderer text (streamer.append(markdown_text=delta, ...)). Nothing on the stream path calls _resolve_outgoing_mentions.
format_converter.py: delete BARE_MENTION_REGEX, URL_REGEX and _link_bare_mentions_outside_urls. Add _link_bare_mention_names(text) = replace_bare_mentions(text, lambda _m, name: f"<@{name}>") and use it in _finalize and the mrkdwn text-node branch.
adapter.py_resolve_outgoing_mentions: collect names with replace_bare_mentions (the replacer returns the mention unchanged). Keep the early return when no names are found. Replace with a second replace_bare_mentions pass that keeps the existing unique / participant-disambiguation / SLACK_USER_ID_EXACT_PATTERN logic.
adapter.pystream(): add resolved_committed, resolved_source_done and inside_resolved_fence state plus async def resolve_committed(committable), a line-by-line port of upstream resolveCommitted. Compute every text delta (push_text_and_flush, send_structured_chunk's pre-flush, and the final flush after renderer.finish()) as resolved_committed[len(last_appended):], and set last_appended = resolved_committed.
Fence detection on the stream path: a line whose lstrip() starts with ``` or ~~~ toggles fence state, applied only once the line's newline is committed (6f0d2f02 semantics). [4.41/SL3] Slack: rotate long native streams before expiry #208 later replaces this with a marker-matching fence tracker.
The resolver is async and runs once per newly committed line, so a long stream issues one state read per line with a bare mention. Keep the early return in _resolve_outgoing_mentions so lines without @ never touch state. Do not cache resolutions across lines: the participant list can change mid-stream.
Chunk safety comes from the renderer. It commits partial lines only inside fences (literal) or at inline-marker holdback cuts, which never split a bare @name. Pass the whole line segment to the resolver, never a character-level delta.
last_appended must track the resolved buffer. Mixing coordinate spaces duplicates or drops text as soon as a replacement changes length (@alice → <@U123>).
Behavior changes to call out:
Streamed @name for a cached user becomes a real ping, which is visible to streaming consumers.
@a/@b now links both mentions. The old / lookbehind skipped @b. See upstream "rewrites slash-separated mentions".
Handles inside inline code and fences are never rewritten.
Tests
None of these files are fidelity-mapped (scripts/verify_test_fidelity.py MAPPING covers packages/chat/src only).
packages/adapter-slack/src/index.test.ts → tests/test_slack_api.py (resolver): "skips mentions inside inline code (backticks)", "skips mentions inside code blocks (triple backticks)", "resolves mentions outside code but skips those inside", "handles multiple inline code spans with mentions", "resolves the same name outside code while skipping it inside code", "resolves a mention immediately following an inline code span", "resolves a mention immediately preceding an inline code span", "resolves mentions surrounding a multiline fenced code block", "does not skip a mention after an unbalanced single backtick", "skips a mention inside inline code at the start of the text", "does not resolve @Handles inside URLs".
Same file, describe "native streaming outgoing mention resolution": "resolves cached @name mentions on the native streaming path", "resolves mentions that span source chunks", "resolves mentions on lines committed mid-stream", "leaves ambiguous mentions as plain text", "disambiguates ambiguous mentions using thread participants", "keeps mentions literal inside code fences". Use an AsyncMock streamer and assert on the concatenated markdown_text of the append calls.
packages/adapter-slack/src/markdown.test.ts → tests/test_slack_format.py: "does not mangle @Handles inside Slack links", "does not mangle @Handles inside Markdown links", "does not link @Handles inside inline code", "does not link @Handles inside fenced code", "rewrites slash-separated mentions", "rewrites mentions after schemeless host punctuation", "preserves schemeless URL handles in response URL markdown", "handles malformed angle text without rescanning it". The last one uses 20,000 < characters; assert the output, not a timing.
tests/test_slack_format.py:237-316 already covers the URL cases from a8bf99a. Keep them and delete any that become exact duplicates of the ported names (CLAUDE.md principle 3).
Python-specific: a stream whose resolved text grows (@al + ice → <@U1>) produces no duplicated or dropped characters across appends.
Acceptance criteria
Full validation command from CLAUDE.md passes.
docs/UPSTREAM_SYNC.md updated if anything diverges, such as a Python-only guard in the stream resolver.
CHANGELOG entry under "Unreleased (4.41 wave)".
Consumer-visible changes called out: code/URL handles are no longer pinged; streamed @name now pings; slash-separated mentions link.
Live-loop check: a streamed reply containing `npm i @scope/pkg` and @<cached user> renders the code literally and pings the user.
Dependencies
Blocked by #193 (chat_sdk.shared.mentions.replace_bare_mentions(text, replacer), replacer (mention, name) -> str). Blocks #208 (rotation is written against the resolved buffer). #207 is not blocked but rebases over the resolved-buffer change in stream(); landing SL1 first is simplest.
Metadata
Effort: M (~450 LOC incl. tests)
Consumer impact: high for streaming consumers. Agent output with package names or shell snippets stops producing spurious pings, and streamed @name starts pinging.
Summary
Outgoing Slack text turns
@wordinto mentions in two places: the markdown→Slack payload converter (@name→<@name>) and the adapter's cached-name resolver (@name→<@U…>). Neither pass skips code, and the resolver doesn't skip URLs either. When an agent printsnpm i @scope/pkgor`@vercel/postgres`, the snippet is corrupted into a ping. Meanwhile the native streaming path skips name resolution entirely, so a streamed@alicestays plain text while the same text posted normally would ping. This issue moves both passes onto the shared code- and URL-aware scanner from #193, and resolves mentions incrementally on the native stream.Upstream changes
07c11129[slack] skip mention resolution inside code blocks (#629) — chat@4.32.0 —resolveOutgoingMentionsskips@nameinside inline code and fenced blocks.a8c4af74fix(slack): skip urls during mention resolution (#619) — chat@4.32.0 — replaces the regex passes inresolveOutgoingMentionsandSlackFormatConverter(finalize, text nodes) with a char-by-charreplaceBareMentions(text, replacer)that skips code, URLs (schemed and schemeless) and<…>tokens.d4c52cadrefactor(shared): share the bare-mention scanner across Discord, Teams, and Slack (#652) — chat@4.33.0 — moves the scanner to@chat-adapter/shared. The Python port of the shared helper is [4.41/C2b] Shared text utilities: bare-mention scanner, code-fence helpers, to_plain_text structural whitespace #193; this issue only adopts it in Slack.6f0d2f02fix(slack): resolve outgoing mentions on the native streaming path (#755) — chat@4.37.0 — before eachappend, runs the committed renderer text line by line throughresolveOutgoingMentions, tracking fence state (fence lines and fenced content stay literal). Deltas are computed against the resolved buffer.Current Python behavior
src/chat_sdk/adapters/slack/format_converter.py:40BARE_MENTION_REGEX = (?<![<\w/])@(\w+)(ASCII) and:49-67_link_bare_mentions_outside_urlsskip onlyhttp(s)://spans. They are used at:209(_finalize) and:228(mrkdwn text nodes). Code spans are not skipped, soinstall `@vercel/postgres`becomes`<@vercel>/postgres`.src/chat_sdk/adapters/slack/adapter.py:3006-3060_resolve_outgoing_mentionsusesre.compile(r"(?<![\w<])@(\w+)"). It is Unicode\w(nore.ASCII) and skips neither code nor URLs. It is called only via_resolve_message_mentions(:3062-3076) from post/edit/ephemeral/schedule (:3620,:3733,:4079,:4141).adapter.py:3966-3969flush_markdown_deltaappends raw renderer text (streamer.append(markdown_text=delta, ...)). Nothing on the stream path calls_resolve_outgoing_mentions.grep -rn "replace_bare_mentions" src/finds nothing until [4.41/C2b] Shared text utilities: bare-mention scanner, code-fence helpers, to_plain_text structural whitespace #193 lands.Scope
format_converter.py: deleteBARE_MENTION_REGEX,URL_REGEXand_link_bare_mentions_outside_urls. Add_link_bare_mention_names(text) = replace_bare_mentions(text, lambda _m, name: f"<@{name}>")and use it in_finalizeand the mrkdwn text-node branch.adapter.py_resolve_outgoing_mentions: collect names withreplace_bare_mentions(the replacer returns the mention unchanged). Keep the early return when no names are found. Replace with a secondreplace_bare_mentionspass that keeps the existing unique / participant-disambiguation /SLACK_USER_ID_EXACT_PATTERNlogic.adapter.pystream(): addresolved_committed,resolved_source_doneandinside_resolved_fencestate plusasync def resolve_committed(committable), a line-by-line port of upstreamresolveCommitted. Compute every text delta (push_text_and_flush,send_structured_chunk's pre-flush, and the final flush afterrenderer.finish()) asresolved_committed[len(last_appended):], and setlast_appended = resolved_committed.lstrip()starts with```or~~~toggles fence state, applied only once the line's newline is committed (6f0d2f02 semantics). [4.41/SL3] Slack: rotate long native streams before expiry #208 later replaces this with a marker-matching fence tracker.Out of scope
chat_sdk/shared/mentions.py) and its unit tests belong to [4.41/C2b] Shared text utilities: bare-mention scanner, code-fence helpers, to_plain_text structural whitespace #193.native_streamingconfig andstream()returningNonebelong to [4.41/SL2] Slack streaming: stream() returns None without context, native_streaming config with post+edit fallback #207. In fallback mode, post/edit already resolves mentions.SLACK_USER_ID_EXACT_PATTERNbelong to [4.41/SL8] Slack: guarded downloads, Enterprise Grid org-wide installs, egress proxy #213.slack:user-by-name:keys belong to [4.41/SL0] Slack: installation-scoped caches, drop unresolved installs, strict response_url, bounded regexes #205. Merge whichever lands second on top of the other.Porting notes
\wmust stay ASCII-only everywhere (upstream JS semantics). If [4.41/C2b] Shared text utilities: bare-mention scanner, code-fence helpers, to_plain_text structural whitespace #193's scanner exposes a name pattern, reuse it; don't reintroduce a Unicode\wregex in the adapter._resolve_outgoing_mentionsso lines without@never touch state. Do not cache resolutions across lines: the participant list can change mid-stream.@name. Pass the whole line segment to the resolver, never a character-level delta.last_appendedmust track the resolved buffer. Mixing coordinate spaces duplicates or drops text as soon as a replacement changes length (@alice→<@U123>).@namefor a cached user becomes a real ping, which is visible to streaming consumers.@a/@bnow links both mentions. The old/lookbehind skipped@b. See upstream "rewrites slash-separated mentions".Tests
None of these files are fidelity-mapped (
scripts/verify_test_fidelity.pyMAPPING coverspackages/chat/srconly).packages/adapter-slack/src/index.test.ts→tests/test_slack_api.py(resolver): "skips mentions inside inline code (backticks)", "skips mentions inside code blocks (triple backticks)", "resolves mentions outside code but skips those inside", "handles multiple inline code spans with mentions", "resolves the same name outside code while skipping it inside code", "resolves a mention immediately following an inline code span", "resolves a mention immediately preceding an inline code span", "resolves mentions surrounding a multiline fenced code block", "does not skip a mention after an unbalanced single backtick", "skips a mention inside inline code at the start of the text", "does not resolve @Handles inside URLs".markdown_textof theappendcalls.packages/adapter-slack/src/markdown.test.ts→tests/test_slack_format.py: "does not mangle @Handles inside Slack links", "does not mangle @Handles inside Markdown links", "does not link @Handles inside inline code", "does not link @Handles inside fenced code", "rewrites slash-separated mentions", "rewrites mentions after schemeless host punctuation", "preserves schemeless URL handles in response URL markdown", "handles malformed angle text without rescanning it". The last one uses 20,000<characters; assert the output, not a timing.tests/test_slack_format.py:237-316already covers the URL cases from a8bf99a. Keep them and delete any that become exact duplicates of the ported names (CLAUDE.md principle 3).@al+ice→<@U1>) produces no duplicated or dropped characters across appends.Acceptance criteria
docs/UPSTREAM_SYNC.mdupdated if anything diverges, such as a Python-only guard in the stream resolver.@namenow pings; slash-separated mentions link.`npm i @scope/pkg`and@<cached user>renders the code literally and pings the user.Dependencies
Blocked by #193 (
chat_sdk.shared.mentions.replace_bare_mentions(text, replacer), replacer(mention, name) -> str). Blocks #208 (rotation is written against the resolved buffer). #207 is not blocked but rebases over the resolved-buffer change instream(); landing SL1 first is simplest.Metadata
@namestarts pinging.sync/4.41-sl1Part of #184.