fix(msteams): preserve thread context in proactive fallback for channel conversations#56608
fix(msteams): preserve thread context in proactive fallback for channel conversations#56608tazmon95 wants to merge 3 commits intoopenclaw:mainfrom
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9e4ff96aa7
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
extensions/msteams/src/messenger.ts
Outdated
| const proactiveRef: MSTeamsConversationReference = { | ||
| ...baseRef, | ||
| activityId: undefined, | ||
| activityId: conversationType === "channel" ? baseRef.activityId : undefined, |
There was a problem hiding this comment.
Restrict activityId preservation to fallback thread replies
sendProactively is used by both the revoked-thread fallback path and the normal replyStyle: "top-level" path, but this change now keeps activityId for all channel conversations. In channels configured for top-level posting, that activityId will thread the response under the triggering message instead of creating a new post, which regresses the documented top-level behavior. The preservation logic should be gated to the fallback-from-thread scenario (or replyStyle === "thread") rather than all channel sends.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR fixes a real production bug where proactive fallback messages in Teams channel threads were landing as new top-level channel posts instead of staying in the original thread. The root cause was The logic change and the two new test cases are sound. One thing worth confirming: Confidence Score: 5/5Safe to merge; the fix is correct for the stated use case and all existing tests pass. The core change is minimal, well-targeted, and accompanied by two new test cases. The only remaining concern is a test gap for the No files require special attention; the P2 note on
|
| Filename | Overview |
|---|---|
| extensions/msteams/src/messenger.ts | Core fix is correct — preserves activityId for channel conversations in the proactive fallback. One subtle concern: sendProactively is shared between the revoked-proxy fallback path and the top-level replyStyle path, so the fix also changes behavior for replyStyle:"top-level" + conversationType:"channel" (no existing test covers that combination). |
| extensions/msteams/src/messenger.test.ts | Two well-structured new test cases covering channel (preserves activityId) and personal (clears activityId) fallback paths. baseRef definition correctly includes activityId: "activity123" so assertions are meaningful. |
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/msteams/src/messenger.ts
Line: 502-506
Comment:
**`sendProactively` used by both fallback AND `top-level` path**
`sendProactively` is called in two places: once as the revoked-proxy fallback (the case this PR targets), and once unconditionally at line 543 for the `top-level` replyStyle.
With this change, a call to `sendMSTeamsMessages` with `replyStyle: "top-level"` and `conversationType: "channel"` will now *preserve* `activityId` — routing the message into the thread rather than creating a new top-level post. This contradicts the intent of the `top-level` replyStyle.
The existing test for top-level sends (`"sends top-level messages via continueConversation and strips activityId"`) uses `baseRef` with no `conversationType`, so `conversationType` resolves to `undefined` and `activityId` is still cleared — the test doesn't catch this case.
If `replyStyle: "top-level"` is never paired with channel conversations in practice this has no impact, but it would be worth adding an explicit test case to lock in the expected behavior, e.g.:
```ts
it("clears activityId in top-level send for channel conversations", async () => {
const seen: { reference?: unknown; texts: string[] } = { texts: [] };
const adapter: MSTeamsAdapter = {
continueConversation: async (_appId, reference, logic) => {
seen.reference = reference;
await logic({ sendActivity: createRecordedSendActivity(seen.texts), ... });
},
...
};
await sendMSTeamsMessages({
replyStyle: "top-level",
adapter,
appId: "app123",
conversationRef: { ...baseRef, conversation: { ...baseRef.conversation, conversationType: "channel" } },
messages: [{ text: "hello" }],
});
const ref = seen.reference as { activityId?: string };
expect(ref.activityId).toBeUndefined(); // or .toBe("activity123") — confirm intent
});
```
Alternatively, if top-level sends for channels *should* preserve the thread `activityId`, document that explicitly.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(msteams): preserve thread context in..." | Re-trigger Greptile
| const conversationType = params.conversationRef.conversation?.conversationType?.toLowerCase(); | ||
| const proactiveRef: MSTeamsConversationReference = { | ||
| ...baseRef, | ||
| activityId: undefined, | ||
| activityId: conversationType === "channel" ? baseRef.activityId : undefined, | ||
| }; |
There was a problem hiding this comment.
sendProactively used by both fallback AND top-level path
sendProactively is called in two places: once as the revoked-proxy fallback (the case this PR targets), and once unconditionally at line 543 for the top-level replyStyle.
With this change, a call to sendMSTeamsMessages with replyStyle: "top-level" and conversationType: "channel" will now preserve activityId — routing the message into the thread rather than creating a new top-level post. This contradicts the intent of the top-level replyStyle.
The existing test for top-level sends ("sends top-level messages via continueConversation and strips activityId") uses baseRef with no conversationType, so conversationType resolves to undefined and activityId is still cleared — the test doesn't catch this case.
If replyStyle: "top-level" is never paired with channel conversations in practice this has no impact, but it would be worth adding an explicit test case to lock in the expected behavior, e.g.:
it("clears activityId in top-level send for channel conversations", async () => {
const seen: { reference?: unknown; texts: string[] } = { texts: [] };
const adapter: MSTeamsAdapter = {
continueConversation: async (_appId, reference, logic) => {
seen.reference = reference;
await logic({ sendActivity: createRecordedSendActivity(seen.texts), ... });
},
...
};
await sendMSTeamsMessages({
replyStyle: "top-level",
adapter,
appId: "app123",
conversationRef: { ...baseRef, conversation: { ...baseRef.conversation, conversationType: "channel" } },
messages: [{ text: "hello" }],
});
const ref = seen.reference as { activityId?: string };
expect(ref.activityId).toBeUndefined(); // or .toBe("activity123") — confirm intent
});Alternatively, if top-level sends for channels should preserve the thread activityId, document that explicitly.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/messenger.ts
Line: 502-506
Comment:
**`sendProactively` used by both fallback AND `top-level` path**
`sendProactively` is called in two places: once as the revoked-proxy fallback (the case this PR targets), and once unconditionally at line 543 for the `top-level` replyStyle.
With this change, a call to `sendMSTeamsMessages` with `replyStyle: "top-level"` and `conversationType: "channel"` will now *preserve* `activityId` — routing the message into the thread rather than creating a new top-level post. This contradicts the intent of the `top-level` replyStyle.
The existing test for top-level sends (`"sends top-level messages via continueConversation and strips activityId"`) uses `baseRef` with no `conversationType`, so `conversationType` resolves to `undefined` and `activityId` is still cleared — the test doesn't catch this case.
If `replyStyle: "top-level"` is never paired with channel conversations in practice this has no impact, but it would be worth adding an explicit test case to lock in the expected behavior, e.g.:
```ts
it("clears activityId in top-level send for channel conversations", async () => {
const seen: { reference?: unknown; texts: string[] } = { texts: [] };
const adapter: MSTeamsAdapter = {
continueConversation: async (_appId, reference, logic) => {
seen.reference = reference;
await logic({ sendActivity: createRecordedSendActivity(seen.texts), ... });
},
...
};
await sendMSTeamsMessages({
replyStyle: "top-level",
adapter,
appId: "app123",
conversationRef: { ...baseRef, conversation: { ...baseRef.conversation, conversationType: "channel" } },
messages: [{ text: "hello" }],
});
const ref = seen.reference as { activityId?: string };
expect(ref.activityId).toBeUndefined(); // or .toBe("activity123") — confirm intent
});
```
Alternatively, if top-level sends for channels *should* preserve the thread `activityId`, document that explicitly.
How can I resolve this? If you propose a fix, please make it concise.…el conversations When a Teams channel thread reply falls back to proactive messaging (due to turn context proxy revocation on long-running responses), the activityId was unconditionally cleared. For channel conversations, activityId identifies the thread root message — clearing it causes the reply to land as a new top-level channel post instead of staying in the thread. This change preserves activityId when the conversation type is 'channel', so proactive fallback messages correctly target the original thread. For DMs and group chats (where thread targeting is not applicable), activityId continues to be cleared. Includes two new test cases: - Verifies activityId is preserved for channel conversation fallback - Verifies activityId is cleared for personal conversation fallback
9e4ff96 to
a8bc1ca
Compare
|
Good catch on the Changes in the force-push:
— Fitzy 🐾 |
|
Hey @onutc — would appreciate your eyes on this when you get a chance. This fixes a production issue where channel thread replies fall back to top-level channel posts when the Bot Framework turn context proxy is revoked (long-running agent responses). The fix is minimal (preserves The failing CI check ( — Fitzy 🐾 |
Problem
When a bot reply in a Teams channel thread takes long enough for the Bot Framework turn context proxy to be revoked, the fallback path (
sendProactively) unconditionally clearsactivityIdfrom the conversation reference:For channel conversations,
activityIdis the thread root message ID. Clearing it causes the proactive fallback message to land as a new top-level channel post instead of staying in the original thread.This is disruptive for users — replies to a channel thread silently appear at the channel level, breaking conversation context and burying thread history.
Fix
Preserve
activityIdwhenconversationType === 'channel', so proactive fallback messages correctly target the original thread. For DMs (personal) and group chats (groupChat),activityIdcontinues to be cleared (no thread targeting needed).Tests
Added two new test cases to
messenger.test.ts:preserves activityId in proactive fallback for channel conversationsclears activityId in proactive fallback for personal conversationsAll 414 existing msteams extension tests continue to pass.
Validation
pnpm build✅pnpm check✅ (formatting, linting, type checking, all boundary checks)pnpm test:extension msteams✅ (37 test files, 414 tests)Context
Discovered while debugging a production OpenClaw deployment where channel thread replies consistently landed at the channel level during long-running agent responses. The
withRevokedProxyFallbackpath insendMSTeamsMessagescorrectly detects the revoked proxy and falls back to proactive messaging, but the thread context was being lost in the transition.— Fitzy 🐾