Skip to content

feat(msteams): auto-inject parent message context for thread replies (#54932)#63945

Merged
BradGroux merged 3 commits intoopenclaw:mainfrom
sudie-codes:feat/msteams-thread-context-injection-54932
Apr 10, 2026
Merged

feat(msteams): auto-inject parent message context for thread replies (#54932)#63945
BradGroux merged 3 commits intoopenclaw:mainfrom
sudie-codes:feat/msteams-thread-context-injection-54932

Conversation

@sudie-codes
Copy link
Copy Markdown
Contributor

Summary

When a user replies to a message in a Teams channel thread, the bot now fetches the parent message via Graph and prepends it as context for the agent. This makes thread replies coherent — the agent knows what is being responded to.

What changed

  • On inbound channel thread reply, fetch parent message via Graph /teams/{teamId}/channels/{channelId}/messages/{replyToId}
  • Prepend a system event: Replying to @{sender}: {parentText}
  • LRU cache (5 min, 100 entries) to avoid repeated fetches in active threads
  • Per-session dedupe so consecutive replies to the same parent do not re-inject identical context
  • Graceful fallback on Graph failure (no crash, no event emitted)

Builds on PR #62713 (per-thread session isolation): when a new thread session has no history, the parent context gives the agent immediate grounding.

Test plan

  • Parent fetched on first thread reply
  • Cache hit on subsequent fetches
  • Graph failure handled gracefully
  • DMs unaffected (no parent fetch)
  • Top-level channel messages unaffected
  • Dedupe prevents re-injection within a session
  • LRU eviction works at the 100-entry cap
  • TTL expiry re-fetches after 5 min
  • Manual: reply to a message in a Teams channel thread

Closes #54932

@openclaw-barnacle openclaw-barnacle Bot added channel: msteams Channel integration: msteams size: L labels Apr 9, 2026
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: 193eed8031

ℹ️ 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".

Comment on lines +578 to +582
if (parentSummary && shouldInjectParentContext(route.sessionKey, activity.replyToId)) {
core.system.enqueueSystemEvent(formatParentContextEvent(parentSummary), {
sessionKey: route.sessionKey,
contextKey: `msteams:thread-parent:${conversationId}:${activity.replyToId}`,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Gate parent-context injection by allowlist visibility

This new parent-context event is emitted as soon as parentSummary exists, but it does not apply the same sender visibility checks used for thread/quote context (contextVisibilityMode + allowlist via filterSupplementalContextItems). In channels configured with contextVisibility: "allowlist", a blocked parent author can now still inject Replying to @... text into system events, so unallowlisted content reaches the next prompt even though BodyForAgent correctly filters it out.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 9, 2026

Greptile Summary

Adds parent-message context injection for Teams channel thread replies: a new thread-parent-context.ts module implements an LRU+TTL cache and per-session dedupe, and message-handler.ts fetches the parent concurrently with thread replies before enqueuing a Replying to @sender: … system event.

  • P1 — Promise.all couples failure domains: wrapping both fetchParentMessageCached and fetchThreadReplies in Promise.all (lines 573–576 of message-handler.ts) means a transient Graph error on the parent fetch also silently discards the thread reply context. Using Promise.allSettled keeps the two operations independently resilient.

Confidence Score: 4/5

Safe to merge after addressing the Promise.all coupling; the feature logic and test coverage are otherwise solid.

One P1 defect: a transient Graph error on the parent-message fetch will also drop thread reply context due to Promise.all coupling. The remaining findings are P2 style issues. Score is 4 rather than lower because the failure mode is bounded (graceful degradation still applies, no data loss or crash) and is easily fixed.

extensions/msteams/src/monitor-handler/message-handler.ts — the Promise.all block at lines 573–576

Comments Outside Diff (1)

  1. extensions/msteams/src/monitor-handler/message-handler.authz.test.ts, line 154-162 (link)

    P2 Cache reset is not guaranteed before every test

    _resetThreadParentContextCachesForTest() is called inside resetThreadMocks(), which is only invoked from mockThreadContext(). Any authz test that exercises a channel thread reply path without calling mockThreadContext() could see stale LRU state from a prior test. Moving the reset into a beforeEach would be safer and make the dependency explicit.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/msteams/src/monitor-handler/message-handler.authz.test.ts
    Line: 154-162
    
    Comment:
    **Cache reset is not guaranteed before every test**
    
    `_resetThreadParentContextCachesForTest()` is called inside `resetThreadMocks()`, which is only invoked from `mockThreadContext()`. Any authz test that exercises a channel thread reply path without calling `mockThreadContext()` could see stale LRU state from a prior test. Moving the reset into a `beforeEach` would be safer and make the dependency explicit.
    
    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/message-handler.ts
Line: 573-576

Comment:
**`Promise.all` couples parent-fetch failures to thread-context loss**

If `fetchParentMessageCached` throws (transient Graph error, 429, etc.), `Promise.all` rejects immediately and the `catch` block discards both `parentMsg` and `replies`. Any thread history that `fetchThreadReplies` would have returned is silently lost, degrading the agent more than necessary. The two fetches are independent and should fail independently.

```suggestion
        const [parentResult, repliesResult] = await Promise.allSettled([
          fetchParentMessageCached(graphToken, groupId, conversationId, activity.replyToId),
          fetchThreadReplies(graphToken, groupId, conversationId, activity.replyToId),
        ]);
        const parentMsg =
          parentResult.status === "fulfilled" ? parentResult.value : undefined;
        const replies =
          repliesResult.status === "fulfilled" ? repliesResult.value : [];
```

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/thread-parent-context.ts
Line: 64-66

Comment:
**`buildInjectedKey` is a no-op identity function**

This helper just returns `sessionKey` unchanged and adds unnecessary indirection. Every call site should use `sessionKey` directly.

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/monitor-handler/message-handler.authz.test.ts
Line: 154-162

Comment:
**Cache reset is not guaranteed before every test**

`_resetThreadParentContextCachesForTest()` is called inside `resetThreadMocks()`, which is only invoked from `mockThreadContext()`. Any authz test that exercises a channel thread reply path without calling `mockThreadContext()` could see stale LRU state from a prior test. Moving the reset into a `beforeEach` would be safer and make the dependency explicit.

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

Reviews (1): Last reviewed commit: "feat(msteams): auto-inject parent messag..." | Re-trigger Greptile

Comment on lines 573 to 576
const [parentMsg, replies] = await Promise.all([
fetchChannelMessage(graphToken, groupId, conversationId, activity.replyToId),
fetchParentMessageCached(graphToken, groupId, conversationId, activity.replyToId),
fetchThreadReplies(graphToken, groupId, conversationId, activity.replyToId),
]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Promise.all couples parent-fetch failures to thread-context loss

If fetchParentMessageCached throws (transient Graph error, 429, etc.), Promise.all rejects immediately and the catch block discards both parentMsg and replies. Any thread history that fetchThreadReplies would have returned is silently lost, degrading the agent more than necessary. The two fetches are independent and should fail independently.

Suggested change
const [parentMsg, replies] = await Promise.all([
fetchChannelMessage(graphToken, groupId, conversationId, activity.replyToId),
fetchParentMessageCached(graphToken, groupId, conversationId, activity.replyToId),
fetchThreadReplies(graphToken, groupId, conversationId, activity.replyToId),
]);
const [parentResult, repliesResult] = await Promise.allSettled([
fetchParentMessageCached(graphToken, groupId, conversationId, activity.replyToId),
fetchThreadReplies(graphToken, groupId, conversationId, activity.replyToId),
]);
const parentMsg =
parentResult.status === "fulfilled" ? parentResult.value : undefined;
const replies =
repliesResult.status === "fulfilled" ? repliesResult.value : [];
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/monitor-handler/message-handler.ts
Line: 573-576

Comment:
**`Promise.all` couples parent-fetch failures to thread-context loss**

If `fetchParentMessageCached` throws (transient Graph error, 429, etc.), `Promise.all` rejects immediately and the `catch` block discards both `parentMsg` and `replies`. Any thread history that `fetchThreadReplies` would have returned is silently lost, degrading the agent more than necessary. The two fetches are independent and should fail independently.

```suggestion
        const [parentResult, repliesResult] = await Promise.allSettled([
          fetchParentMessageCached(graphToken, groupId, conversationId, activity.replyToId),
          fetchThreadReplies(graphToken, groupId, conversationId, activity.replyToId),
        ]);
        const parentMsg =
          parentResult.status === "fulfilled" ? parentResult.value : undefined;
        const replies =
          repliesResult.status === "fulfilled" ? repliesResult.value : [];
```

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

Comment on lines +64 to +66
function buildInjectedKey(sessionKey: string): string {
return sessionKey;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 buildInjectedKey is a no-op identity function

This helper just returns sessionKey unchanged and adds unnecessary indirection. Every call site should use sessionKey directly.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/thread-parent-context.ts
Line: 64-66

Comment:
**`buildInjectedKey` is a no-op identity function**

This helper just returns `sessionKey` unchanged and adds unnecessary indirection. Every call site should use `sessionKey` directly.

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

@BradGroux BradGroux merged commit 01ea7e4 into openclaw:main Apr 10, 2026
69 of 73 checks passed
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
…penclaw#54932) (openclaw#63945)

* feat(msteams): auto-inject parent message context for thread replies (openclaw#54932)

* msteams: use Promise.allSettled for thread context, remove no-op buildInjectedKey

* fix(msteams): gate thread parent context by visibility

---------

Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…penclaw#54932) (openclaw#63945)

* feat(msteams): auto-inject parent message context for thread replies (openclaw#54932)

* msteams: use Promise.allSettled for thread context, remove no-op buildInjectedKey

* fix(msteams): gate thread parent context by visibility

---------

Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…penclaw#54932) (openclaw#63945)

* feat(msteams): auto-inject parent message context for thread replies (openclaw#54932)

* msteams: use Promise.allSettled for thread context, remove no-op buildInjectedKey

* fix(msteams): gate thread parent context by visibility

---------

Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
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: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(msteams): Auto-inject thread context when message arrives as thread reply

2 participants