Skip to content

fix(msteams): isolate thread sessions, outbound targeting, and attachment resolution#59294

Open
atharva-getboon wants to merge 12 commits intoopenclaw:mainfrom
atharva-getboon:fix/msteams-thread-isolation-58615
Open

fix(msteams): isolate thread sessions, outbound targeting, and attachment resolution#59294
atharva-getboon wants to merge 12 commits intoopenclaw:mainfrom
atharva-getboon:fix/msteams-thread-isolation-58615

Conversation

@atharva-getboon
Copy link
Copy Markdown

@atharva-getboon atharva-getboon commented Apr 1, 2026

Summary

Fixes #58615 — msteams channel threads share the same session key, causing cross-thread context bleed.

  • Thread session isolation: Each Teams channel thread gets its own session key via resolveThreadSessionKeys(), gated by session.threadBindings.enabled config
  • Thread-scoped history: historyKey is now thread-aware so buildPendingHistoryContextFromMap doesn't mix channel-level and thread-level history
  • Outbound thread targeting: buildActivity() sets activity.replyToId before any early returns so proactive sends (text + file attachments via SharePoint/OneDrive) land in the correct thread
  • User mention preservation: New stripMSTeamsBotMentionTag() strips only the bot's <at> tag — other user mentions are preserved as @Name in agent context. A separate commandText (all mentions stripped) is used for slash command detection to avoid regressions.
  • Key normalization: Consistent toLowerCase() across session key, history key, and conversation store key derivations
  • Thread file attachment fix: Graph API fallback now triggers for mixed attachment types (text/html + file.download.info without downloadUrl), fixing file attachments not being visible in thread replies. Narrowed to only file-like content types to avoid unnecessary Graph calls for Adaptive Cards.
  • Thread-aware debounce: Debounce key includes replyToId when thread bindings enabled, preventing cross-thread message merging

AI-Assisted PR 🤖

  • Marked as AI-assisted
  • Fully tested (487 msteams tests pass, type-check clean, lint clean, build clean)
  • Session logs available on request
  • I understand what the code does
  • Bot review conversations resolved with commit references

Test plan

  • 487/487 msteams extension tests pass (44 test files)
  • Type-check (pnpm tsgo) clean
  • Lint/format (pnpm check) clean
  • Build (pnpm build) clean
  • New unit tests for stripMSTeamsBotMentionTag (10 cases)
  • New unit tests for buildActivity replyToId thread targeting (3 cases)
  • New integration tests for thread isolation logic (12 cases)
  • New unit tests for Graph API fallback on mixed attachment types (6 cases)
  • Manual verification: proactive sends with activity.replyToId land in correct Teams thread
  • Manual verification: file attachments in thread replies are visible to the bot

🤖 Generated with Claude Code

@openclaw-barnacle openclaw-barnacle bot added channel: msteams Channel integration: msteams size: M labels Apr 1, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 1, 2026

Greptile Summary

This PR fixes cross-thread context bleed in the MS Teams channel by introducing per-thread session keys, thread-scoped history, and outbound replyToId targeting. It also improves mention handling (preserving non-bot @mentions in agent context) and addresses file attachment visibility in thread replies via Graph API fallback. The changes are well-scoped and accompanied by solid unit and integration test coverage.

Key changes:

  • Thread session isolation (message-handler.ts): resolveThreadSessionKeys() derives a per-thread session key when session.threadBindings.enabled is set; session key, history key, and store key all use a consistently lowercased threadId.
  • Outbound thread targeting (messenger.ts, conversation-store.ts): buildActivity sets activity.replyToId from the stored conversationRef.replyToId so proactive sends land in the correct thread.
  • Bot-mention isolation (inbound.ts): stripMSTeamsBotMentionTag replaces stripMSTeamsMentionTags; non-bot mentions are preserved as @Name instead of being silently dropped.
  • Graph API fallback broadening (inbound-media.ts): Fallback now triggers when any attachments are present and direct download yields nothing — fixing thread reply file attachments but potentially causing unnecessary Graph calls for non-downloadable attachment types (see inline comment).
  • Known gap documented: Feedback invoke events still use the channel-level session key; tracked in a TODO for a follow-up fix.

Confidence Score: 3/5

  • Safe to merge with minor concerns; the Graph fallback broadening may add unnecessary API calls for non-media attachment types.
  • The thread isolation logic is well-implemented and consistent. The two flagged issues are: (1) the Graph API fallback now triggers for all attachment types (not just HTML/file-download), which could cause spurious Graph calls for Adaptive Card messages — this is a P1 correctness concern for throughput/cost in channels with card-heavy traffic; (2) </> are not escaped in the bot-name regex, which is a safe-but-minor defensive gap. The core feature changes (session keys, replyToId, mention handling) are solid and well-tested.
  • extensions/msteams/src/monitor-handler/inbound-media.ts — the broadened Graph fallback condition warrants a closer look before merging in environments with Adaptive Card usage.

Comments Outside Diff (1)

  1. extensions/msteams/src/monitor-handler/inbound-media.ts, line 57-66 (link)

    P1 Graph fallback now triggers for all attachment types, including non-downloadable ones

    The condition was narrowed from onlyHtmlAttachments (all attachments are text/html) to hasAttachments (any attachment exists). While this correctly fixes the thread reply scenario (mixed text/html + file.download.info without downloadUrl), it now triggers a Graph API call for any message where direct download returns nothing — including messages with Adaptive Cards (application/vnd.microsoft.card.adaptive), Hero Cards, or other metadata-only content types that by design never yield downloadable media.

    For every such message, downloadMSTeamsAttachments legitimately returns [] (nothing to download), but now an unnecessary Graph API call is issued, adding latency and consuming Graph quota.

    A more targeted fix would limit the fallback to attachment types that are expected to have downloadable content but may be missing their URL:

    This preserves the existing text/html behavior, adds the new file.download.info-without-downloadUrl case, and avoids spurious Graph calls for card attachment types.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/msteams/src/monitor-handler/inbound-media.ts
    Line: 57-66
    
    Comment:
    **Graph fallback now triggers for all attachment types, including non-downloadable ones**
    
    The condition was narrowed from `onlyHtmlAttachments` (all attachments are `text/html`) to `hasAttachments` (any attachment exists). While this correctly fixes the thread reply scenario (mixed `text/html` + `file.download.info` without `downloadUrl`), it now triggers a Graph API call for *any* message where direct download returns nothing — including messages with Adaptive Cards (`application/vnd.microsoft.card.adaptive`), Hero Cards, or other metadata-only content types that by design never yield downloadable media.
    
    For every such message, `downloadMSTeamsAttachments` legitimately returns `[]` (nothing to download), but now an unnecessary Graph API call is issued, adding latency and consuming Graph quota.
    
    A more targeted fix would limit the fallback to attachment types that are *expected* to have downloadable content but may be missing their URL:
    
    
    
    This preserves the existing `text/html` behavior, adds the new `file.download.info`-without-`downloadUrl` case, and avoids spurious Graph calls for card attachment types.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/msteams/src/monitor-handler/inbound-media.ts
Line: 57-66

Comment:
**Graph fallback now triggers for all attachment types, including non-downloadable ones**

The condition was narrowed from `onlyHtmlAttachments` (all attachments are `text/html`) to `hasAttachments` (any attachment exists). While this correctly fixes the thread reply scenario (mixed `text/html` + `file.download.info` without `downloadUrl`), it now triggers a Graph API call for *any* message where direct download returns nothing — including messages with Adaptive Cards (`application/vnd.microsoft.card.adaptive`), Hero Cards, or other metadata-only content types that by design never yield downloadable media.

For every such message, `downloadMSTeamsAttachments` legitimately returns `[]` (nothing to download), but now an unnecessary Graph API call is issued, adding latency and consuming Graph quota.

A more targeted fix would limit the fallback to attachment types that are *expected* to have downloadable content but may be missing their URL:

```suggestion
    const shouldFallbackToGraph =
      attachments.length > 0 &&
      attachments.some(
        (att) =>
          String(att.contentType ?? "").startsWith("text/html") ||
          String(att.contentType ?? "").includes("file.download.info"),
      );

    if (shouldFallbackToGraph) {
```

This preserves the existing `text/html` behavior, adds the new `file.download.info`-without-`downloadUrl` case, and avoids spurious Graph calls for card attachment types.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/msteams/src/inbound.ts
Line: 122-123

Comment:
**Regex escaping omits `<` and `>` — may break if bot name contains those characters**

The escaping function covers regex metacharacters (`.*+?^${}()|[\]`) but does not escape `<` or `>`. While `<` is not a regex metacharacter, `>` appears in the surrounding pattern `<at[^>]*>…</at>`, so a bot name containing `>` would prematurely close the `[^>]*` alternation and potentially cause the regex to match/corrupt unintended content.

Example: if `botMentionName = "Bot >v2"`, the compiled regex becomes  
`<at[^>]*>Bot >v2</at>` where the `>` in `Bot >v2` terminates the `[^>]*` character class.

Bot display names containing `<`/`>` are unusual in Teams, but defensive escaping costs nothing:

```suggestion
  const escaped = botMentionName.replace(/[.*+?^${}()|[\]\\<>]/g, "\\$&");
```

(Note: `<` and `>` are not special in JS regex, so escaping them is harmless and produces the expected literal match.)

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "msteams: fix Graph API fallback not trig..." | Re-trigger Greptile

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9213f05201

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eaee7238d8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@atharva-getboon atharva-getboon force-pushed the fix/msteams-thread-isolation-58615 branch from bd5109a to 35d331d Compare April 2, 2026 18:16
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

return !core.channel.text.hasControlCommand(entry.text, cfg);

P2 Badge Use mention-stripped text for command debounce checks

After this change, entry.text can start with preserved mentions (for example @Alice /status), so hasControlCommand(entry.text, cfg) returns false and the message is incorrectly debounced. That lets mention-prefixed slash commands be coalesced with subsequent messages in the debounce window, which can delay or alter command handling compared to the previous non-debounced path. The debounce gate should check entry.commandText (or another mention-stripped form) so command messages still bypass debounce reliably.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@atharva-getboon
Copy link
Copy Markdown
Author

@onutc @osolmaz Could you review this when you get a chance? This fixes thread session isolation, outbound reply targeting, file attachment visibility, and mention handling in the msteams plugin. All bot review conversations have been resolved with commit references.

…olation-58615

# Conflicts:
#	extensions/msteams/src/inbound.ts
#	extensions/msteams/src/monitor-handler/inbound-media.test.ts
#	extensions/msteams/src/monitor-handler/inbound-media.ts
#	extensions/msteams/src/monitor-handler/message-handler.ts
@openclaw-barnacle openclaw-barnacle bot added channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: line Channel integration: line channel: matrix Channel integration: matrix channel: nextcloud-talk Channel integration: nextcloud-talk channel: signal Channel integration: signal channel: slack Channel integration: slack labels Apr 3, 2026
@openclaw-barnacle openclaw-barnacle bot added channel: telegram Channel integration: telegram channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser commands Command implementations agents Agent runtime and tooling channel: feishu Channel integration: feishu size: L and removed size: M labels Apr 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: feishu Channel integration: feishu channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: line Channel integration: line channel: matrix Channel integration: matrix channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: signal Channel integration: signal channel: slack Channel integration: slack channel: telegram Channel integration: telegram channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser commands Command implementations size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: msteams channel threads share the same session key (cross-thread context bleed)

1 participant