fix(msteams): handle Adaptive Card Action.Submit invoke activities#60431
Conversation
Greptile SummaryThis PR adds handling for
Confidence Score: 3/5Not safe to merge — two logic bugs in the new handler path that cause incorrect behavior for edge-case payloads. Two P1 bugs: null payloads are silently forwarded as the string "null", and the no-value path unintentionally re-invokes the normal handler chain instead of being dropped. extensions/msteams/src/monitor-handler.ts — both bugs are in the new serializeAdaptiveCardActionValue function and the adaptiveCard/action block in registerMSTeamsHandlers. Prompt To Fix All With AIThis is a comment left during a code review.
Path: extensions/msteams/src/monitor-handler.ts
Line: 56-58
Comment:
**`null` value passes through as the string `"null"`**
`null` is not `undefined`, so the `null` guard does not fire — `typeof null === "object"`, which falls through to `JSON.stringify(null)` and returns the truthy string `"null"`. That string is then forwarded to the agent as the message body, which is almost certainly not intended.
```suggestion
if (value == null) {
return null;
}
```
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.ts
Line: 420
Comment:
**No-value path falls through to `originalRun`**
When `text` is falsy the code logs "skipping" but then falls through to `return originalRun.call(handler, context)`, passing the `adaptiveCard/action` invoke to the normal handler chain. That chain dispatches the activity as a message, which contradicts the intent of "skipping". A missing `return` here means the invoke is still processed.
```suggestion
deps.log.debug?.("skipping adaptive card action invoke without value payload");
return;
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(msteams): handle Adaptive Card Actio..." | Re-trigger Greptile |
| if (value === undefined) { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
null value passes through as the string "null"
null is not undefined, so the null guard does not fire — typeof null === "object", which falls through to JSON.stringify(null) and returns the truthy string "null". That string is then forwarded to the agent as the message body, which is almost certainly not intended.
| if (value === undefined) { | |
| return null; | |
| } | |
| if (value == null) { | |
| return null; | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/monitor-handler.ts
Line: 56-58
Comment:
**`null` value passes through as the string `"null"`**
`null` is not `undefined`, so the `null` guard does not fire — `typeof null === "object"`, which falls through to `JSON.stringify(null)` and returns the truthy string `"null"`. That string is then forwarded to the agent as the message body, which is almost certainly not intended.
```suggestion
if (value == null) {
return null;
}
```
How can I resolve this? If you propose a fix, please make it concise.| }); | ||
| return; | ||
| } | ||
| deps.log.debug?.("skipping adaptive card action invoke without value payload"); |
There was a problem hiding this comment.
No-value path falls through to
originalRun
When text is falsy the code logs "skipping" but then falls through to return originalRun.call(handler, context), passing the adaptiveCard/action invoke to the normal handler chain. That chain dispatches the activity as a message, which contradicts the intent of "skipping". A missing return here means the invoke is still processed.
| deps.log.debug?.("skipping adaptive card action invoke without value payload"); | |
| deps.log.debug?.("skipping adaptive card action invoke without value payload"); | |
| return; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/monitor-handler.ts
Line: 420
Comment:
**No-value path falls through to `originalRun`**
When `text` is falsy the code logs "skipping" but then falls through to `return originalRun.call(handler, context)`, passing the `adaptiveCard/action` invoke to the normal handler chain. That chain dispatches the activity as a message, which contradicts the intent of "skipping". A missing `return` here means the invoke is still processed.
```suggestion
deps.log.debug?.("skipping adaptive card action invoke without value payload");
return;
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Pull request overview
This PR fixes Microsoft Teams Adaptive Card Action.Submit interactions being dropped by detecting adaptiveCard/action invoke activities and forwarding their value payload to the existing message handler, so the agent receives the button-click data.
Changes:
- Added
serializeAdaptiveCardActionValue()to serializeinvoke.valuefor Adaptive Card actions. - Intercepted
adaptiveCard/actioninvoke activities inregisterMSTeamsHandlersand re-routed them through the message pipeline. - Added a Vitest regression test covering the forwarding behavior.
Reviewed changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| pnpm-lock.yaml | Updates the workspace importers list (includes extensions/stepfun). |
| extensions/msteams/src/monitor-handler.ts | Detects adaptiveCard/action invokes and forwards serialized value to the message handler. |
| extensions/msteams/src/monitor-handler.adaptive-card.test.ts | Adds a regression test ensuring Adaptive Card action payloads reach the agent. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| if (value === undefined) { | ||
| return null; | ||
| } | ||
| try { | ||
| return JSON.stringify(value); |
There was a problem hiding this comment.
serializeAdaptiveCardActionValue() will forward null payloads as the literal string "null" (since JSON.stringify(null) returns "null"), which can result in a synthetic message being dispatched for Action.Submit clicks that carry no usable data. Consider treating null the same as undefined, and also guard against JSON.stringify(...) returning undefined (e.g., for values like functions/symbols) by normalizing that to null as well.
| if (value === undefined) { | |
| return null; | |
| } | |
| try { | |
| return JSON.stringify(value); | |
| if (value == null) { | |
| return null; | |
| } | |
| try { | |
| const serialized = JSON.stringify(value); | |
| return typeof serialized === "string" ? serialized : null; |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9ad63a41ce
ℹ️ 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".
| if (ctx.activity?.type === "invoke" && ctx.activity?.name === "adaptiveCard/action") { | ||
| const text = serializeAdaptiveCardActionValue(ctx.activity?.value); | ||
| if (text) { | ||
| await handleTeamsMessage({ |
There was a problem hiding this comment.
Wrap adaptive invoke forwarding in existing error guard
The new adaptiveCard/action branch calls handleTeamsMessage(...) directly, but unlike the normal onMessage path it is not inside the try/catch that reports failures via runtime.error. If this call throws (for example from debounce/runtime wiring failures), the exception bubbles out of registerMSTeamsHandlers, and createMSTeamsAdapter.process suppresses invoke errors for type === "invoke", so the action is silently dropped with no diagnostic. Reusing the same guarded wrapper as onMessage avoids losing observability for this new path.
Useful? React with 👍 / 👎.
906afb5 to
568c540
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 568c54018b
ℹ️ 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".
| if (ctx.activity?.type === "invoke" && ctx.activity?.name === "adaptiveCard/action") { | ||
| const text = serializeAdaptiveCardActionValue(ctx.activity?.value); | ||
| if (text) { | ||
| await handleTeamsMessage({ |
There was a problem hiding this comment.
Mark adaptive invokes as mentioned before message forwarding
Forwarding adaptiveCard/action invokes through handleTeamsMessage without any mention override means channel submissions still fail under default policy: resolveMSTeamsReplyPolicy defaults requireMention to true, and wasMSTeamsBotMentioned only checks mention entities. Adaptive invoke payloads typically lack mention entities, so the message handler’s mention gate drops these synthetic messages and the button click is ignored in channels. Please set a mention/bypass signal for this converted activity path (or skip mention gating for this invoke type).
Useful? React with 👍 / 👎.
568c540 to
7f31e42
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7f31e42582
ℹ️ 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".
| activity: { | ||
| ...ctx.activity, | ||
| type: "message", | ||
| text, |
There was a problem hiding this comment.
Preserve thread anchor when converting adaptive invokes
This conversion only changes type/text, so the synthetic message keeps the invoke activity ID. Downstream Teams reply threading uses activity.id as the anchor (for example via conversation references and default reply target logic), but adaptive-card clicks in channel threads typically identify the clicked card message via replyToId, not the invoke ID itself. In that case replies can be posted out-of-thread or fail to thread correctly. When constructing this message-shaped activity, carry the thread anchor from replyToId (or explicitly set the reply target) instead of leaving the invoke ID as the message ID.
Useful? React with 👍 / 👎.
7f31e42 to
a38ea0f
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a38ea0ff30
ℹ️ 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".
| await handleTeamsMessage({ | ||
| ...ctx, | ||
| activity: { | ||
| ...ctx.activity, | ||
| type: "message", |
There was a problem hiding this comment.
Preserve adaptive action thread target in converted message
This invoke-to-message conversion reuses ctx.sendActivity, but that sender was created from the original invoke context where replyToActivityId is bound to the invoke activity.id; as a result, replies from this path can target the invoke ID instead of the clicked card message (replyToId) in channel threads. In practice this can post out-of-thread (or fail to thread) for adaptive card submits. Ensure this path explicitly preserves the thread anchor (for example by routing with a context/proactive reference keyed to replyToId, or by setting outbound replyToId explicitly).
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 93d5a92276
ℹ️ 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".
| if (ctx.activity?.type === "invoke" && ctx.activity?.name === "adaptiveCard/action") { | ||
| const text = serializeAdaptiveCardActionValue(ctx.activity?.value); | ||
| if (text) { | ||
| await handleTeamsMessage({ |
There was a problem hiding this comment.
Bypass debounce for adaptive card submit events
Forwarding adaptiveCard/action through handleTeamsMessage(...) routes these synthetic submits into the normal inbound debouncer, and when messages.inbound.debounceMs is non-zero the handler can merge multiple rapid button clicks from the same sender/conversation into one combined payload (the flush path concatenates texts and keeps only the last context in extensions/msteams/src/monitor-handler/message-handler.ts). For adaptive actions this changes semantics (distinct submits become one turn), so quick multi-click workflows can execute the wrong action or lose submissions.
Useful? React with 👍 / 👎.
…penclaw#60431) * fix(msteams): handle Adaptive Card Action.Submit invoke activities (openclaw#55384) * ci: retrigger checks --------- Co-authored-by: Brad Groux <[email protected]>
Fixes #55384
When a user clicks an Action.Submit button on an Adaptive Card, the invoke activity is no longer silently dropped. The handler now detects
adaptiveCard/actioninvoke activities and forwards thevaluepayload to the agent as a message.Changes:
serializeAdaptiveCardActionValue()helper to extract/serialize the action payloadadaptiveCard/actioninvoke detection inregisterMSTeamsHandlersmonitor-handler.adaptive-card.test.tsTesting:
pnpm test -- extensions/msteams/src/monitor-handler.adaptive-card.test.ts— 1/1 passpnpm check— clean