Skip to content

fix(msteams): persist conversation reference during DM pairing#60432

Merged
BradGroux merged 3 commits intoopenclaw:mainfrom
BradGroux:bgod/fix-43323-pairing-persist-convref
Apr 4, 2026
Merged

fix(msteams): persist conversation reference during DM pairing#60432
BradGroux merged 3 commits intoopenclaw:mainfrom
BradGroux:bgod/fix-43323-pairing-persist-convref

Conversation

@BradGroux
Copy link
Copy Markdown
Contributor

Fixes #43323

The first DM to the bot now persists the conversation reference BEFORE the pairing early-return, so openclaw pairing approve msteams <code> --notify works immediately after approval.

Changes:

  • Extracted buildStoredConversationReference() helper in message-handler.ts
  • Moved conversationStore.upsert() before the pairing branch return path
  • Added test verifying conversation reference is saved during pairing flow

Testing:

  • pnpm test -- extensions/msteams/src/monitor-handler/message-handler.authz.test.ts — 9/9 pass
  • pnpm check — clean

Copilot AI review requested due to automatic review settings April 3, 2026 16:58
@openclaw-barnacle openclaw-barnacle bot added channel: msteams Channel integration: msteams size: S maintainer Maintainer-authored PR labels Apr 3, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 3, 2026

Greptile Summary

This PR fixes the MS Teams DM pairing notification flow by persisting the StoredConversationReference before the pairing early-return in message-handler.ts, so that openclaw pairing approve msteams <code> --notify can look up the conversation reference immediately after approval. The fix is achieved by extracting a buildStoredConversationReference() helper at the top of the file and calling conversationStore.upsert (fire-and-forget, consistent with the rest of the file) inside the access.decision === "pairing" branch before the function returns.

  • message-handler.ts: buildStoredConversationReference extracted as a module-level helper; conversationStore.upsert moved before the pairing-path return; the inline conversationRef construction block in the normal path is removed (replaced by the shared helper). One minor leftover: activityClientInfo is re-extracted from activity.entities at line 662 even though the same value is already captured in conversationRef.timezone by the new helper — the || conversationRef.timezone fallback makes the two branches always identical.
  • message-handler.authz.test.ts: New assertion verifies the full StoredConversationReference shape (including tenantId, channelId, serviceUrl, locale, timezone) is written to conversationStore during the pairing flow, and negative assertions confirm that recordInboundSession and dispatchReplyFromConfigWithSettledDispatcher are not invoked on that path.
  • pnpm-lock.yaml: Unrelated addition of extensions/stepfun importer entry.

Confidence Score: 4/5

Safe to merge; the fix is well-scoped, tests cover the new path, and the only issue is a minor redundant lookup.

The behavioral change is correct and well-tested. The only finding is a P2 style issue: activityClientInfo?.timezone || conversationRef.timezone is always equivalent to conversationRef.timezone alone, since activityClientInfo reads the same activity.entities array that buildStoredConversationReference already consumed. No logic bugs or regressions found.

No files require special attention; the redundant lookup in message-handler.ts at line 662 is cosmetic.

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: 662-665

Comment:
**Redundant `clientInfo` extraction**

`buildStoredConversationReference` already reads `activity.entities` to populate `conversationRef.timezone`. Re-finding `clientInfo` here and then ORing it with `conversationRef.timezone` is always equivalent to just reading `conversationRef.timezone` directly, since both come from the same `activity.entities` array.

```suggestion
    const senderTimezone = conversationRef.timezone;
```

This removes the duplicate `entities.find` call and makes the intent—"use the timezone already captured in the conversation reference"—clearer.

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

Reviews (1): Last reviewed commit: "fix(msteams): persist conversation refer..." | Re-trigger Greptile

Comment on lines +662 to +665
const activityClientInfo = activity.entities?.find((e) => e.type === "clientInfo") as
| { timezone?: string }
| undefined;
const senderTimezone = activityClientInfo?.timezone || conversationRef.timezone;
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 Redundant clientInfo extraction

buildStoredConversationReference already reads activity.entities to populate conversationRef.timezone. Re-finding clientInfo here and then ORing it with conversationRef.timezone is always equivalent to just reading conversationRef.timezone directly, since both come from the same activity.entities array.

Suggested change
const activityClientInfo = activity.entities?.find((e) => e.type === "clientInfo") as
| { timezone?: string }
| undefined;
const senderTimezone = activityClientInfo?.timezone || conversationRef.timezone;
const senderTimezone = conversationRef.timezone;

This removes the duplicate entities.find call and makes the intent—"use the timezone already captured in the conversation reference"—clearer.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/monitor-handler/message-handler.ts
Line: 662-665

Comment:
**Redundant `clientInfo` extraction**

`buildStoredConversationReference` already reads `activity.entities` to populate `conversationRef.timezone`. Re-finding `clientInfo` here and then ORing it with `conversationRef.timezone` is always equivalent to just reading `conversationRef.timezone` directly, since both come from the same `activity.entities` array.

```suggestion
    const senderTimezone = conversationRef.timezone;
```

This removes the duplicate `entities.find` call and makes the intent—"use the timezone already captured in the conversation reference"—clearer.

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

@BradGroux BradGroux self-assigned this Apr 3, 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: 069536ee7f

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

allowNameMatching,
});
if (access.decision === "pairing") {
conversationStore.upsert(conversationId, conversationRef).catch((err) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Await pairing-path conversation upsert before returning

In the new DM pairing branch, conversationStore.upsert(...) is still fire-and-forget, so the handler can return before the reference is persisted. Because openclaw pairing approve msteams <code> --notify immediately resolves the recipient via findPreferredDmByUserId in the store, a fast approve/notify can race this write and still fail with "No conversation reference found" despite the new fix path. Awaiting the upsert here (or otherwise synchronizing persistence) is needed to guarantee the stated immediate-notify behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes the MS Teams DM pairing flow so the very first inbound DM persists a conversation reference before the allowlist/pairing early-return, enabling immediate proactive notifications on pairing approval (--notify).

Changes:

  • Extracts a buildStoredConversationReference() helper to centralize reference construction.
  • Moves conversationStore.upsert() into the DM pairing branch so references are saved before the drop/return path.
  • Adds/updates an authz test to assert the conversation reference is saved during pairing.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 3 comments.

File Description
pnpm-lock.yaml Adds a workspace importer entry for extensions/stepfun.
extensions/msteams/src/monitor-handler/message-handler.ts Refactors conversation reference building and persists it earlier during pairing.
extensions/msteams/src/monitor-handler/message-handler.authz.test.ts Adds an assertion that the conversation reference is saved in the DM pairing flow.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

conversationType,
tenantId: conversation?.tenantId,
},
teamId,
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

buildStoredConversationReference always includes teamId as an own property even when it’s undefined. Because mergeStoredConversationReference spreads incoming over existing, this can unintentionally clear a previously stored teamId (and it also makes exact-match tests brittle). Consider only adding teamId when it’s present (similar to the conditional timezone spread).

Suggested change
teamId,
...(teamId ? { teamId } : {}),

Copilot uses AI. Check for mistakes.
Comment on lines +607 to +608
extensions/stepfun: {}

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

pnpm-lock.yaml now adds an extensions/stepfun importer entry even though this PR is scoped to the MS Teams pairing flow. If this lockfile change isn’t intentional (e.g., from a workspace re-sync), consider reverting it to keep the PR focused and reduce merge conflicts.

Suggested change
extensions/stepfun: {}

Copilot uses AI. Check for mistakes.
Comment on lines +288 to +312
expect(conversationStore.upsert).toHaveBeenCalledWith("a:personal-chat", {
activityId: "msg-pairing",
user: {
id: "new-user-id",
aadObjectId: "new-user-aad",
name: "New User",
},
agent: {
id: "bot-id",
name: "Bot",
},
bot: {
id: "bot-id",
name: "Bot",
},
conversation: {
id: "a:personal-chat",
conversationType: "personal",
tenantId: "tenant-1",
},
channelId: "msteams",
serviceUrl: "https://smba.trafficmanager.net/amer/",
locale: "en-US",
timezone: "America/New_York",
});
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

This assertion uses a full deep-equality match for the conversation reference payload. buildStoredConversationReference() currently includes teamId as an own property even when it’s undefined for this input, which makes toHaveBeenCalledWith fail/brittle (missing vs undefined). Prefer expect.objectContaining(...) for the reference payload, or include teamId: undefined in the expected object.

Suggested change
expect(conversationStore.upsert).toHaveBeenCalledWith("a:personal-chat", {
activityId: "msg-pairing",
user: {
id: "new-user-id",
aadObjectId: "new-user-aad",
name: "New User",
},
agent: {
id: "bot-id",
name: "Bot",
},
bot: {
id: "bot-id",
name: "Bot",
},
conversation: {
id: "a:personal-chat",
conversationType: "personal",
tenantId: "tenant-1",
},
channelId: "msteams",
serviceUrl: "https://smba.trafficmanager.net/amer/",
locale: "en-US",
timezone: "America/New_York",
});
expect(conversationStore.upsert).toHaveBeenCalledWith(
"a:personal-chat",
expect.objectContaining({
activityId: "msg-pairing",
user: {
id: "new-user-id",
aadObjectId: "new-user-aad",
name: "New User",
},
agent: {
id: "bot-id",
name: "Bot",
},
bot: {
id: "bot-id",
name: "Bot",
},
conversation: {
id: "a:personal-chat",
conversationType: "personal",
tenantId: "tenant-1",
},
channelId: "msteams",
serviceUrl: "https://smba.trafficmanager.net/amer/",
locale: "en-US",
timezone: "America/New_York",
}),
);

Copilot uses AI. Check for mistakes.
@BradGroux BradGroux force-pushed the bgod/fix-43323-pairing-persist-convref branch from a2b939e to 7264d81 Compare April 3, 2026 17:44
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: 7264d81b63

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

allowNameMatching,
});
if (access.decision === "pairing") {
conversationStore.upsert(conversationId, conversationRef).catch((err) => {
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 Await conversation reference write in pairing branch

Persisting the DM conversation reference is still fire-and-forget in the pairing early-return path, so the handler can return before the filesystem-backed store finishes upsert. In that window, an immediate openclaw pairing approve msteams <code> --notify may still fail to resolve the recipient (findPreferredDmByUserId), which means the commit’s intended immediate-notify guarantee is not actually enforced under normal async I/O timing.

Useful? React with 👍 / 👎.

@BradGroux BradGroux force-pushed the bgod/fix-43323-pairing-persist-convref branch 2 times, most recently from 69d1990 to 7838ac6 Compare April 3, 2026 18:40
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: 7838ac6bd8

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

allowNameMatching,
});
if (access.decision === "pairing") {
conversationStore.upsert(conversationId, conversationRef).catch((err) => {
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 Await conversation upsert in pairing return path

In the DM pairing branch, conversationStore.upsert(...) is launched without await, and this handler returns on the same turn after pairing bookkeeping. The filesystem-backed store writes asynchronously (withFileLock + file write), so an operator who immediately runs openclaw pairing approve msteams <code> --notify can still race findPreferredDmByUserId(...) and get "No conversation reference found" even though the pairing DM just arrived. Awaiting the upsert in this branch is needed to make the new immediate-notify behavior deterministic.

Useful? React with 👍 / 👎.

@BradGroux BradGroux force-pushed the bgod/fix-43323-pairing-persist-convref branch from 7838ac6 to 9c9cbc3 Compare April 3, 2026 21:48
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: 9c9cbc3793

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

allowNameMatching,
});
if (access.decision === "pairing") {
conversationStore.upsert(conversationId, conversationRef).catch((err) => {
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 Await DM pairing conversation write before early return

The new pairing path still fires conversationStore.upsert(...) without awaiting it, then returns immediately; with the filesystem store this write is asynchronous (withFileLock + writeJsonFile), so a fast openclaw pairing approve msteams <code> --notify can race findPreferredDmByUserId(...) and still fail with "No conversation reference found." This undermines the commit’s stated immediate-notify guarantee for normal I/O timing.

Useful? React with 👍 / 👎.

@BradGroux BradGroux merged commit dd2faa3 into openclaw:main Apr 4, 2026
42 of 80 checks passed
KimGLee pushed a commit to KimGLee/openclaw that referenced this pull request Apr 4, 2026
…law#60432)

* fix(msteams): persist conversation reference during DM pairing (openclaw#43323)

* ci: retrigger checks

---------

Co-authored-by: Brad Groux <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: msteams Channel integration: msteams maintainer Maintainer-authored PR size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: MS Teams pairing drops first DM before saving conversation reference, so --notify always fails

2 participants