Summary
Images sent in personal (1:1) bot DM chats are never downloaded. Three bugs in the msteams extension's media download pipeline cause all image downloads to fail silently.
Environment
- OpenClaw 2026.2.22
- Microsoft 365 Business
- Bot Framework SDK (via
@microsoft/agents-hosting)
Bug 1: Wrong conversation ID format for Graph API
File: extensions/msteams/src/attachments/graph.ts → buildMSTeamsGraphMessageUrls
Bot Framework uses a:xxx conversation IDs for personal chats, but the Graph API requires 19:{userId}_{botAppId}@unq.gbl.spaces. The Graph request returns "Invalid ThreadId".
Fix: Translate the Bot Framework conversation ID before passing to resolveMSTeamsInboundMedia:
if (isDirectMessage && conversationId.startsWith("a:") && from.aadObjectId && appId) {
graphConversationId = `19:${from.aadObjectId}_${appId}@unq.gbl.spaces`;
}
Bug 2: Graph fallback blocked by non-HTML attachments
File: extensions/msteams/src/monitor-handler/inbound-media.ts
The Graph fallback only triggers when ALL attachments are text/html (attachments.every(...)). Image messages include non-HTML metadata attachments alongside the HTML content, which blocks the fallback path entirely.
Fix: Change every() to some() — trigger Graph fallback when ANY HTML attachment is present.
Bug 3: Hosted content bytes not returned inline
File: extensions/msteams/src/attachments/graph.ts → downloadGraphHostedContent
The /hostedContents list endpoint returns metadata but contentBytes is null for bot DM chats. The code skips items without inline bytes (if (!contentBytes) { continue; }). The actual binary image data IS available at the individual $value endpoint: /hostedContents/{id}/$value.
Fix: When contentBytes is empty, fetch the binary content from the $value endpoint:
if (!buffer && item.id) {
const valueUrl = `${messageUrl}/hostedContents/${encodeURIComponent(item.id)}/$value`;
const valueRes = await fetchFn(valueUrl, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (valueRes.ok) {
buffer = Buffer.from(await valueRes.arrayBuffer());
}
}
Diagnosis method
- Gateway logs showed
"graph media fetch empty" for every image message
- Manual Graph API calls confirmed the conversation ID format mismatch (404 with
a: ID, success with 19: ID)
- Manual fetch of
/hostedContents/{id}/$value returned the full image (32KB JPEG)
- All three patches together resolve the issue completely
Labels
bug, msteams
Summary
Images sent in personal (1:1) bot DM chats are never downloaded. Three bugs in the msteams extension's media download pipeline cause all image downloads to fail silently.
Environment
@microsoft/agents-hosting)Bug 1: Wrong conversation ID format for Graph API
File:
extensions/msteams/src/attachments/graph.ts→buildMSTeamsGraphMessageUrlsBot Framework uses
a:xxxconversation IDs for personal chats, but the Graph API requires19:{userId}_{botAppId}@unq.gbl.spaces. The Graph request returns"Invalid ThreadId".Fix: Translate the Bot Framework conversation ID before passing to
resolveMSTeamsInboundMedia:Bug 2: Graph fallback blocked by non-HTML attachments
File:
extensions/msteams/src/monitor-handler/inbound-media.tsThe Graph fallback only triggers when ALL attachments are
text/html(attachments.every(...)). Image messages include non-HTML metadata attachments alongside the HTML content, which blocks the fallback path entirely.Fix: Change
every()tosome()— trigger Graph fallback when ANY HTML attachment is present.Bug 3: Hosted content bytes not returned inline
File:
extensions/msteams/src/attachments/graph.ts→downloadGraphHostedContentThe
/hostedContentslist endpoint returns metadata butcontentBytesisnullfor bot DM chats. The code skips items without inline bytes (if (!contentBytes) { continue; }). The actual binary image data IS available at the individual$valueendpoint:/hostedContents/{id}/$value.Fix: When
contentBytesis empty, fetch the binary content from the$valueendpoint:Diagnosis method
"graph media fetch empty"for every image messagea:ID, success with19:ID)/hostedContents/{id}/$valuereturned the full image (32KB JPEG)Labels
bug, msteams