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

Comment thread extensions/msteams/src/inbound.ts
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".

Comment thread extensions/msteams/src/messenger.ts Outdated
Comment thread extensions/msteams/src/monitor-handler/message-handler.ts
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".

Comment thread extensions/msteams/src/inbound.ts
Comment thread extensions/msteams/src/monitor-handler.ts
@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
@vicboon
Copy link
Copy Markdown

vicboon commented Apr 9, 2026

@BradGroux @steipete @vincentkoc PTAL 🙏
This PR should be ready.

@openclaw-barnacle
Copy link
Copy Markdown

Please don’t spam-ping multiple maintainers at once. Be patient, or join our community Discord for help: https://discord.gg/clawd

@hay7a4jhgqjw4
Copy link
Copy Markdown

I am linking a newly opened follow-up issue here because I think it may be related but not fully covered by #58615.

Related issue:

Why I think this PR is relevant but may not be the whole story:

  • [Bug]: msteams channel threads share the same session key (cross-thread context bleed) #58615 is the simpler failure mode where multiple Teams channel threads collapse to the same base session key because thread qualification is missing.
  • In my newer investigation, I observed a malformed key of the form:
    ...:thread:OLD_THREAD_ID:thread:NEW_THREAD_ID
  • That suggests a different or later-stage failure mode: an old session path may be reselected first, and then a new thread id is appended afterward.

So if this PR fixes:

  • per-thread key derivation
  • outbound targeting
  • thread-scoped history

that is excellent and likely necessary.

But I would still recommend verifying one additional negative case before considering this whole class closed:

when a previous thread in the same Teams channel already has persisted session state, does a new thread ever produce a malformed doubly-threaded key like ...:thread:old:thread:new?

If the answer is no after this PR, then great, this PR may implicitly cover more than #58615. If not, #66771 may remain as a follow-up bug around old-session reselection / binding / target-session resolution.

@omarshahine omarshahine removed the channel: bluebubbles Channel integration: bluebubbles label Apr 19, 2026
@grafoteka
Copy link
Copy Markdown

Adding production confirmation for this PR.

Running OpenClaw v2026.4.21 in a multi-channel MS Teams deployment (11 channels, mix of DMs and threads). Without the thread session isolation this PR introduces, we observe two consistent symptoms:

  1. Thread session key not qualified → cross-thread context bleed when multiple threads are active in the same channel
  2. Outbound reply ignores replyToId → related to fix(msteams): preserve channel thread isolation during proactive fallback #59314, proactive fallback posts to channel top-level

The combined fix (per-thread session + replyToId in buildActivity) addresses both. PR #59314 covers the specific proactive-path sub-case; this PR is the broader correctness fix.

Happy to test against our deployment if that helps land this.

Thanks @atharva-getboon for the work.

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented May 1, 2026

Codex review: needs changes before merge.

Summary
The PR changes the Microsoft Teams plugin to add thread-aware session/history/debounce routing, preserve non-bot mentions, improve outbound thread targeting, broaden file attachment fallback handling, and add related tests.

Reproducibility: yes. #58615 gives a concrete two-thread Teams reproduction, and current main still has a code-level reproduction for rapid same-sender messages because debounce and pending-history keys omit the Teams thread id.

Next step before merge
A focused automated replacement is plausible because the remaining repair is localized to the MS Teams plugin and changelog.

Security
Cleared: The PR touches channel plugin TypeScript/tests and formatting-only files, with no dependency, workflow, package metadata, secrets, or new third-party execution surface changes.

Review findings

  • [P2] Honor the effective thread binding policy — extensions/msteams/src/monitor-handler/message-handler.ts:391-396
  • [P3] Add the required changelog entry — extensions/msteams/src/monitor-handler/message-handler.ts:391
Review details

Best possible solution:

Rebase or replace this PR with a narrow MS Teams-only change on current main that preserves shipped threadId and proactive ;messageid= routing while adding thread-aware debounce/history keys, effective thread-binding policy handling, focused attachment/mention decisions, tests, and a changelog entry.

Do we have a high-confidence way to reproduce the issue?

Yes. #58615 gives a concrete two-thread Teams reproduction, and current main still has a code-level reproduction for rapid same-sender messages because debounce and pending-history keys omit the Teams thread id.

Is this the best way to solve the issue?

No. The direction is useful, but the patch is stale and its thread-binding gate is not the narrowest maintainable fix because OpenClaw's effective policy supports channel/account overrides and default-enabled behavior.

Full review comments:

  • [P2] Honor the effective thread binding policy — extensions/msteams/src/monitor-handler/message-handler.ts:391-396
    The new gate only checks cfg.session?.threadBindings?.enabled === true, but OpenClaw also supports channel/account threadBindings.enabled overrides and default-enabled behavior. A Teams install relying on those supported settings would still take the unthreaded session/history/debounce path, leaving the cross-thread bleed this PR is meant to fix.
    Confidence: 0.86
  • [P3] Add the required changelog entry — extensions/msteams/src/monitor-handler/message-handler.ts:391
    This is a user-facing Microsoft Teams fix, but the PR file list does not include CHANGELOG.md. Repo policy requires a single-line changelog entry for user-facing fix changes before merge.
    Confidence: 0.94

Overall correctness: patch is incorrect
Overall confidence: 0.78

Acceptance criteria:

  • pnpm test extensions/msteams/src/monitor-handler/message-handler.thread-session.test.ts extensions/msteams/src/monitor-handler/inbound-media.test.ts extensions/msteams/src/messenger.test.ts extensions/msteams/src/inbound.test.ts
  • pnpm exec oxfmt --check --threads=1 extensions/msteams/src/monitor-handler/message-handler.ts extensions/msteams/src/monitor-handler/thread-session.ts extensions/msteams/src/monitor-handler/inbound-media.ts extensions/msteams/src/inbound.ts extensions/msteams/src/messenger.ts CHANGELOG.md
  • pnpm check:changed in Testbox before handoff if runtime code changes remain

What I checked:

Likely related people:

  • sudie-codes: Authored the current main commits for MS Teams channel thread session isolation, proactive thread routing, and later thread parent context injection. (role: introduced current Teams thread behavior; confidence: high; commits: 38aa1edf76da, 9edfefedf7bf, 01ea7e49212a; files: extensions/msteams/src/monitor-handler/message-handler.ts, extensions/msteams/src/monitor-handler/thread-session.ts, extensions/msteams/src/messenger.ts)
  • Peter Steinberger: Dominant recent contributor in the msteams handler/messenger area and author of the shared pending-history and plugin SDK channel helper refactors that this fix needs to preserve. (role: shared routing/history maintainer; confidence: medium; commits: 521ea4ae5bcb, c70837f07d1f, 40f2bf395059; files: extensions/msteams/src/monitor-handler/message-handler.ts, src/plugin-sdk/reply-history.ts, src/channels/thread-bindings-policy.ts)
  • Vincent Koc: Recently maintained the msteams handler tests and shared thread/binding helper surfaces adjacent to this PR's expected validation path. (role: recent msteams test/refactor maintainer; confidence: medium; commits: d00ab0604838, 8c88fb68b772, 74e7b8d47b18; files: extensions/msteams/src/monitor-handler/message-handler.ts, extensions/msteams/src/monitor-handler/message-handler.thread-session.test.ts, src/channels/thread-bindings-policy.ts)
  • BradGroux: Maintains the Microsoft issue tracker context, has recent MS Teams plugin fixes, and co-authored the current thread parent context work. (role: adjacent Microsoft/Teams maintainer; confidence: medium; commits: fce81fccd859, dd2faa376492, 01ea7e49212a; files: extensions/msteams/src/monitor-handler/message-handler.ts, extensions/msteams/src/monitor-handler/thread-parent-context.ts, extensions/msteams/src/conversation-store.ts)

Remaining risk / open question:

  • No live Teams run was performed in this read-only review, so Graph attachment fallback and connector thread delivery still need targeted and ideally manual validation.
  • The branch is stale against current main and includes incidental formatting churn across unrelated plugins, so a repair may need a narrow replacement branch rather than a direct update.
  • [Bug]: MSTeams malformed mixed thread session key from old-session reselection #66771 may represent a distinct old-session reselection path that should be tested if the session-key path is touched.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 40f2bf395059.

BradGroux pushed a commit to harrisali0101/openclaw that referenced this pull request May 8, 2026
…re-suffixed bases (openclaw#66771)

When the inbound message handler reuses a `route.sessionKey` that was already
thread-qualified by a prior turn (e.g. cached resolve-route hit, or the
cache-miss return at src/routing/resolve-route.ts:696 whose object reference
is then mutated in-place by message-handler.ts:489), naively appending the
current thread id compounds into `…:thread:OLD:thread:NEW` and routes the
turn to a malformed lane that splits same-thread context across turns.

Strip any trailing `:thread:<id>+` segments from the base before delegating
to resolveThreadSessionKeys. Thread ids never contain ':' so the segment
boundary is unambiguous; the '+' covers pathological doubly-suffixed bases
already in caches/persisted sessions from prior runs.

This is defense-in-depth at the helper level, complementary to the larger
handler refactor in openclaw#59294 (still open since 2026-04-01). Even after that
or a routing-cache fix lands, making the helper itself idempotent prevents
any future caller hygiene regression from re-introducing the bug.

Adds four regression tests under a new "idempotency against pre-suffixed
bases" describe block in message-handler.thread-session.test.ts:
  - already-thread-suffixed base + new thread → single clean suffix
  - doubly-thread-suffixed (already-malformed) base → collapsed to one
  - same-thread re-application is a fixed point
  - stale thread suffix + top-level inbound → bare base

Closes openclaw#66771.
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: 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)

5 participants