fix(msteams): persist conversation reference during DM pairing#60432
fix(msteams): persist conversation reference during DM pairing#60432BradGroux merged 3 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR fixes the MS Teams DM pairing notification flow by persisting the
Confidence Score: 4/5Safe 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: No files require special attention; the redundant lookup in Prompt To Fix All With AIThis 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 |
| const activityClientInfo = activity.entities?.find((e) => e.type === "clientInfo") as | ||
| | { timezone?: string } | ||
| | undefined; | ||
| const senderTimezone = activityClientInfo?.timezone || conversationRef.timezone; |
There was a problem hiding this 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.
| 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.There was a problem hiding this comment.
💡 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) => { |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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).
| teamId, | |
| ...(teamId ? { teamId } : {}), |
| extensions/stepfun: {} | ||
|
|
There was a problem hiding this comment.
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.
| extensions/stepfun: {} |
| 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", | ||
| }); |
There was a problem hiding this comment.
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.
| 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", | |
| }), | |
| ); |
a2b939e to
7264d81
Compare
There was a problem hiding this comment.
💡 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) => { |
There was a problem hiding this comment.
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 👍 / 👎.
69d1990 to
7838ac6
Compare
There was a problem hiding this comment.
💡 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) => { |
There was a problem hiding this comment.
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 👍 / 👎.
7838ac6 to
9c9cbc3
Compare
There was a problem hiding this comment.
💡 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) => { |
There was a problem hiding this comment.
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 👍 / 👎.
…law#60432) * fix(msteams): persist conversation reference during DM pairing (openclaw#43323) * ci: retrigger checks --------- Co-authored-by: Brad Groux <[email protected]>
Fixes #43323
The first DM to the bot now persists the conversation reference BEFORE the pairing early-return, so
openclaw pairing approve msteams <code> --notifyworks immediately after approval.Changes:
buildStoredConversationReference()helper inmessage-handler.tsconversationStore.upsert()before the pairing branch return pathTesting:
pnpm test -- extensions/msteams/src/monitor-handler/message-handler.authz.test.ts— 9/9 passpnpm check— clean