Summary
The Microsoft Teams plugin (@openclaw/msteams) cannot read a file a user attaches to a channel message. The agent gets the message text but reports "no attachment." For thread replies it's worse — it silently fetches and answers about the thread root's file instead of the reply's actual file.
Reproduced on @openclaw/msteams 2026.5.7 and confirmed the same code exists on current main (2026-06-05). The bot has Graph application permission ChannelMessage.Read.All (admin-consented) and a manifest with RSC ChannelMessage.Read.Group, so this is not a permissions problem — it's three independent URL/flow bugs.
Root causes
1. Wrong team id in the Graph URL → HTTP 400
buildMSTeamsGraphMessageUrls (extensions/msteams/src/attachments/graph.ts) derives the team id as:
const teamId =
readNestedString(params.channelData, ["team", "id"]) ??
readNestedString(params.channelData, ["teamId"]);
channelData.team.id is the 19:[email protected] thread/conversation id, not the team's AAD group GUID. The Graph /teams/{team-id}/channels/... path requires the group GUID (channelData.team.aadGroupId). With team.id, every channel fetch returns 400.
Fix:
const teamId =
readNestedString(params.channelData, ["team", "aadGroupId"]) ??
readNestedString(params.channelData, ["aadGroupId"]) ??
readNestedString(params.channelData, ["team", "id"]) ??
readNestedString(params.channelData, ["teamId"]);
2. The Graph fetch is never attempted for channel @mentions
In resolveMSTeamsInboundMedia (extensions/msteams/src/monitor-handler/inbound-media.ts) the Graph-message fetch is gated on hasHtmlFileAttachment — an <attachment id> marker extracted from the message HTML. A channel @mention activity delivers only a text/html stub without that marker, so hasHtmlFileAttachment is false and the fetch never runs. The bot reports "no attachment" although the file is fetchable via Graph.
Fix: for channel (non-personal) conversations, attempt the Graph message fetch even when the HTML marker is absent (if no inline media was resolved).
3. Thread replies fetch the wrong message (relates to #89594)
For a reply, params.replyToId is empty, so the existing /replies/ branch is skipped. The fallback builds /messages/{candidate} for every candidate — including the thread root (carried in conversationMessageId). The reply's own id returns 404 (a reply id is not a top-level message), so it falls back to /messages/{root} (200) and returns the root message's attachment for every reply in the thread.
A reply is addressable only at /teams/{team}/channels/{channel}/messages/{rootId}/replies/{replyId}.
Fix: treat any candidate that isn't the current message id (params.messageId) as a potential parent, build /messages/{parent}/replies/{selfId} (tried first), and only fetch /messages/{selfId} for the current message — never the bare parent/root.
Reproduction
- In a channel, @mention the bot with a file attached on a top-level message → bot reports "no attachment" (bugs 1 + 2).
- Reply in a thread (whose root message has a different file) with a new file attached → bot answers about the root's file, not the reply's (bug 3).
Evidence (direct Graph calls)
GET /teams/{team.id == 19:…@thread.tacv2}/channels/{chan}/messages/{id} → 400
GET /teams/{aadGroupId GUID}/channels/{chan}/messages/{topLevelId} → 200 (+ attachment)
GET /teams/{aadGroupId}/channels/{chan}/messages/{replyId} → 404
GET /teams/{aadGroupId}/channels/{chan}/messages/{rootId}/replies/{replyId} → 200 (+ the reply's actual attachment)
Environment
@openclaw/msteams 2026.5.7 (same code on main).
- Graph app permission
ChannelMessage.Read.All admin-consented; RSC ChannelMessage.Read.Group in the manifest.
With all three fixes applied, channel file reading works for top-level and thread-reply messages, for documents and audio. Happy to open a PR.
Summary
The Microsoft Teams plugin (
@openclaw/msteams) cannot read a file a user attaches to a channel message. The agent gets the message text but reports "no attachment." For thread replies it's worse — it silently fetches and answers about the thread root's file instead of the reply's actual file.Reproduced on
@openclaw/msteams2026.5.7 and confirmed the same code exists on currentmain(2026-06-05). The bot has Graph application permissionChannelMessage.Read.All(admin-consented) and a manifest with RSCChannelMessage.Read.Group, so this is not a permissions problem — it's three independent URL/flow bugs.Root causes
1. Wrong team id in the Graph URL → HTTP 400
buildMSTeamsGraphMessageUrls(extensions/msteams/src/attachments/graph.ts) derives the team id as:channelData.team.idis the19:[email protected]thread/conversation id, not the team's AAD group GUID. The Graph/teams/{team-id}/channels/...path requires the group GUID (channelData.team.aadGroupId). Withteam.id, every channel fetch returns 400.Fix:
2. The Graph fetch is never attempted for channel @mentions
In
resolveMSTeamsInboundMedia(extensions/msteams/src/monitor-handler/inbound-media.ts) the Graph-message fetch is gated onhasHtmlFileAttachment— an<attachment id>marker extracted from the message HTML. A channel@mentionactivity delivers only atext/htmlstub without that marker, sohasHtmlFileAttachmentisfalseand the fetch never runs. The bot reports "no attachment" although the file is fetchable via Graph.Fix: for channel (non-personal) conversations, attempt the Graph message fetch even when the HTML marker is absent (if no inline media was resolved).
3. Thread replies fetch the wrong message (relates to #89594)
For a reply,
params.replyToIdis empty, so the existing/replies/branch is skipped. The fallback builds/messages/{candidate}for every candidate — including the thread root (carried inconversationMessageId). The reply's own id returns 404 (a reply id is not a top-level message), so it falls back to/messages/{root}(200) and returns the root message's attachment for every reply in the thread.A reply is addressable only at
/teams/{team}/channels/{channel}/messages/{rootId}/replies/{replyId}.Fix: treat any candidate that isn't the current message id (
params.messageId) as a potential parent, build/messages/{parent}/replies/{selfId}(tried first), and only fetch/messages/{selfId}for the current message — never the bare parent/root.Reproduction
Evidence (direct Graph calls)
GET /teams/{team.id == 19:…@thread.tacv2}/channels/{chan}/messages/{id}→ 400GET /teams/{aadGroupId GUID}/channels/{chan}/messages/{topLevelId}→ 200 (+ attachment)GET /teams/{aadGroupId}/channels/{chan}/messages/{replyId}→ 404GET /teams/{aadGroupId}/channels/{chan}/messages/{rootId}/replies/{replyId}→ 200 (+ the reply's actual attachment)Environment
@openclaw/msteams2026.5.7 (same code onmain).ChannelMessage.Read.Alladmin-consented; RSCChannelMessage.Read.Groupin the manifest.With all three fixes applied, channel file reading works for top-level and thread-reply messages, for documents and audio. Happy to open a PR.