Skip to content

fix(msteams): preserve channel reply threading in proactive fallback#55198

Merged
BradGroux merged 3 commits intoopenclaw:mainfrom
hyojin:fix/msteams-reply-threading
Apr 2, 2026
Merged

fix(msteams): preserve channel reply threading in proactive fallback#55198
BradGroux merged 3 commits intoopenclaw:mainfrom
hyojin:fix/msteams-reply-threading

Conversation

@hyojin
Copy link
Copy Markdown
Contributor

@hyojin hyojin commented Mar 26, 2026

Summary

Describe the problem and fix in 2–5 bullets:

  • Problem: When the Bot Framework turn context is revoked (e.g. due to inbound message debouncing), proactive messaging fallback posts replies as new top-level messages in Teams channels instead of threading
    them under the original message.
  • Why it matters: Users see bot replies as disconnected new messages instead of threaded replies, breaking conversation flow in Teams channels.
  • What changed: sendProactively in messenger.ts now reconstructs the threaded conversation ID ({channelId};messageid={activityId}) for channel messages when the reply style is "thread", so the proactive
    fallback delivers replies into the correct thread. Two new unit tests added.
  • What did NOT change (scope boundary): Non-channel conversations (DMs, group chats) are unaffected. The buildActivity, sendMessageInContext, and sendMessageBatchInContext functions are unchanged. No
    config or schema changes.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause / Regression History (if applicable)

  • Root cause: normalizeConversationId strips the ;messageid=... thread suffix from the conversation ID when storing the conversation reference. When sendProactively builds the proactive reference, it uses
    this normalized ID, so continueConversation sends the message to the channel root instead of the original thread.
  • Missing detection / guardrail: No test covered the proactive fallback path with channel-specific threading. The existing revoke fallback test used a conversation reference without conversationType: "channel".
  • Prior context: PR fix(msteams): add proactive fallback for revoked turn context #27224 added withRevokedProxyFallback to fix the revoked proxy error ([Bug]: MS Teams replies fail with 'Cannot perform set on a proxy that has been revoked #27189), which solved the delivery failure but introduced this threading regression — replies were delivered but as new
    top-level messages.
  • Why this regressed now: The proactive fallback path was added to handle revoked contexts, but the conversation ID reconstruction for channel threading was not considered at that time.
  • If unknown, what was ruled out: N/A — root cause confirmed.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: extensions/msteams/src/messenger.test.ts
  • Scenario the test should lock in: When context is revoked for a channel message (conversationType: "channel"), the proactive reference's conversation ID must include ;messageid={activityId}. For group chat
    (conversationType: "groupChat"), it must NOT add the suffix.
  • Why this is the smallest reliable guardrail: Unit test on sendMSTeamsMessages directly verifies the conversation reference passed to continueConversation, covering the exact bug surface.
  • Existing test that already covers this (if any): None — existing revoke fallback test did not assert on conversation ID.
  • If no new test is added, why not: Two new tests added.

User-visible / Behavior Changes

When the bot's turn context is revoked and the proactive fallback is used in a Teams channel, replies now appear as threaded replies under the original message instead of new top-level posts.

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: Ubuntu 24.04 LTS
  • Runtime/container: Node.js v22.22.0
  • Model/provider: Amazon Bedrock / Claude Sonnet 4.6
  • Integration/channel: Microsoft Teams (channel)
  • Relevant config: channels.msteams.enabled: true, messages.inbound.debounceMs: 100 (for testing)

Steps

  1. Configure MS Teams channel with the bot
  2. Force a turn context revoke (either via debouncing or simulated 20s delay + TypeError throw in the run callback)
  3. Send a message to the bot in a Teams channel via @mention

Expected

  • Bot reply appears as a threaded reply under the original message

Actual (before fix)

  • Bot reply appears as a new top-level message in the channel

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets

Before fix (revoke, no fix):
sendProactively: baseRef.activityId=1774527433372 proactiveRef.activityId=undefined conversationId=19:[email protected]
sendMessageInContext: activity.replyToId=undefined activity.type=message
→ New top-level message posted

After fix (revoke, with fix):
sendProactively: baseRef.activityId=1774527601946 replyToId=1774527601946 isChannel=true conversationId=19:[email protected];messageid=1774527601946
sendMessageInContext: activity.replyToId=1774527601946 activity.type=message
→ Reply correctly threaded under original message

Human Verification (required)

  • Verified scenarios: Forced revoke on live Teams channel (with and without fix), normal non-revoked path, DM path
  • Edge cases checked: Group chat (no thread suffix added), channel (thread suffix reconstructed), missing activityId (no suffix added)
  • What you did not verify: Natural revoke via actual debouncing (current CloudAdapter version does not revoke proxy; tested with simulated revoke instead)

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: Revert the single commit; only messenger.ts changed
  • Files/config to restore: extensions/msteams/src/messenger.ts
  • Known bad symptoms reviewers should watch for: Replies landing in wrong thread or failing to send in Teams channels

Risks and Mitigations

  • Risk: The reconstructed conversation ID format ({channelId};messageid={activityId}) depends on the Teams Bot Framework convention for thread addressing.
    • Mitigation: This is the documented Teams threading format, and was verified to work on a live Teams channel. The reconstruction only activates for conversationType: "channel" with a valid activityId,
      falling back to the original behavior otherwise.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) [email protected]

When the live turn context is revoked (e.g. due to inbound message
debouncing), the proactive messaging fallback posted replies as new
top-level messages in Teams channels.

The root cause is that normalizeConversationId strips the
";messageid=..." thread suffix from the conversation ID, and the
proactive reference uses this normalized ID — so the Bot Framework
sends the message to the channel root instead of the original thread.

Fix by reconstructing the threaded conversation ID
("{channelId};messageid={activityId}") when the reply style is "thread"
and the conversation type is "channel", ensuring proactive fallback
replies land in the correct thread.

Add tests for channel thread reconstruction and group chat (no suffix).

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@openclaw-barnacle openclaw-barnacle bot added channel: msteams Channel integration: msteams size: S labels Mar 26, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 26, 2026

Greptile Summary

This PR fixes a threading regression in the MS Teams proactive messaging fallback: when a Bot Framework turn context is revoked (e.g. due to inbound message debouncing), replies were delivered to the channel root as new top-level posts instead of as threaded replies. The fix reconstructs the {channelId};messageid={activityId} conversation ID inside sendProactively for conversationType: "channel" messages when the revoked-context fallback fires, and two new unit tests lock in the behavior.\n\nKey changes:\n- sendProactively gains an optional threadActivityId parameter; when isChannel && threadActivityId, it appends ;messageid=${threadActivityId} to the normalized base conversation ID before calling continueConversation.\n- normalizeConversationId already strips any existing ;messageid=... suffix from stored references, so double-suffixing cannot occur.\n- The thread suffix is intentionally gated on replyStyle === "thread" — the "top-level" path calls sendProactively without a threadActivityId and is unaffected.\n- Non-channel conversations (DMs, group chats) are correctly excluded by the isChannel guard.\n- Two regression tests were added covering channel (suffix expected) and groupChat (suffix must not appear) scenarios.

Confidence Score: 5/5

Safe to merge — minimal, well-scoped change with two targeted regression tests and no impact on non-channel or non-thread paths.

The fix is a small, isolated addition that only activates for the specific edge case (replyStyle: "thread", conversationType: "channel", revoked context, valid activityId). The normalizeConversationId call in buildConversationReference prevents double-suffixing. The only finding is a trivial redundant ?? undefined that has no runtime effect. Both new tests are well-structured and directly verify the bug surface.

No files require special attention.

Important Files Changed

Filename Overview
extensions/msteams/src/messenger.ts Adds optional threadActivityId parameter to sendProactively and reconstructs the ;messageid= thread suffix for channel messages when the proactive fallback fires. Logic is correct: normalizeConversationId already strips any existing suffix before the new one is appended, so double-suffixing cannot occur. Minor: ?? undefined on the activityId extraction is redundant.
extensions/msteams/src/messenger.test.ts Adds two targeted regression tests: one asserts the ;messageid= suffix is appended for conversationType: "channel", the other asserts it is NOT appended for conversationType: "groupChat". Tests capture the continueConversation reference directly, which is the exact bug surface. Good coverage for the fix.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/msteams/src/messenger.ts
Line: 526

Comment:
**Redundant `?? undefined` nullish coalescing**

`activityId` is typed as `string | undefined` (an optional property on `StoredConversationReference`), so coalescing to `undefined` is a no-op — it can never be `null`. The expression can be simplified:

```suggestion
    const threadActivityId = params.conversationRef.activityId;
```

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

Reviews (1): Last reviewed commit: "fix(msteams): preserve channel reply thr..." | Re-trigger Greptile

hyojin and others added 2 commits March 26, 2026 23:12
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@BradGroux BradGroux merged commit 739ed1b into openclaw:main Apr 2, 2026
40 checks passed
@BradGroux BradGroux self-assigned this Apr 2, 2026
ngutman pushed a commit that referenced this pull request Apr 3, 2026
…55198)

When a thread reply's turn context is revoked and falls back to proactive messaging, the normalized conversation ID lost the thread suffix, causing replies to land in the channel root instead of the original thread.

Reconstructs the threaded conversation ID (`;messageid=<activityId>`) for channel conversations in the proactive fallback path, while correctly leaving group chat conversations flat.

Fixes #27189

Thanks @hyojin
steipete pushed a commit to duncanita/openclaw that referenced this pull request Apr 4, 2026
…penclaw#55198)

When a thread reply's turn context is revoked and falls back to proactive messaging, the normalized conversation ID lost the thread suffix, causing replies to land in the channel root instead of the original thread.

Reconstructs the threaded conversation ID (`;messageid=<activityId>`) for channel conversations in the proactive fallback path, while correctly leaving group chat conversations flat.

Fixes openclaw#27189

Thanks @hyojin
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: msteams Channel integration: msteams size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants