Skip to content

fix(msteams): resolve Graph chat ID for personal DM media downloads (#62219)#63063

Merged
BradGroux merged 2 commits intoopenclaw:mainfrom
feiskyer:fix/62219-teams-dm-graph-media
Apr 10, 2026
Merged

fix(msteams): resolve Graph chat ID for personal DM media downloads (#62219)#63063
BradGroux merged 2 commits intoopenclaw:mainfrom
feiskyer:fix/62219-teams-dm-graph-media

Conversation

@feiskyer
Copy link
Copy Markdown
Contributor

@feiskyer feiskyer commented Apr 8, 2026

Summary

  • Problem: Bot Framework personal DM conversation IDs (a:... format) are not valid Graph API chat IDs. When the direct Bot Framework attachment download fails and the code falls back to the Graph API path, buildMSTeamsGraphMessageUrls constructs URLs with the invalid a: ID, Graph returns 404 "Invalid ThreadId", and inbound media (images, documents) is silently dropped.
  • Why it matters: Users sending files or images in Teams personal DMs see <media:document> / <media:image> placeholders but the agent receives no file content.
  • What changed: Before calling resolveMSTeamsInboundMedia, resolve the real Graph chat ID via resolveGraphChatId() for personal DMs with a: conversation IDs. The resolved ID is cached in the conversation store so subsequent messages skip the API lookup.
  • What did NOT change (scope boundary): The direct Bot Framework download path (smba.trafficmanager.net), the channel message path, and the buildMSTeamsGraphMessageUrls function itself are unchanged.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause (if applicable)

  • Root cause: translateMSTeamsDmConversationIdForGraph synthesizes 19:{userId}_{appId}@unq.gbl.spaces which is not always accepted by the Graph /chats/{chatId}/messages endpoint. The resolveGraphChatId function (used in the send path for SharePoint uploads) was not used in the inbound media download path.
  • Missing detection / guardrail: No validation that the conversation ID passed to buildMSTeamsGraphMessageUrls is a valid Graph chat ID for the /chats/ endpoint.
  • Contributing context: The direct Bot Framework download (smba.trafficmanager.net) is the primary path and usually succeeds. The Graph fallback is reached when the direct download fails (e.g., auth issues, unsupported attachment types). The a: ID bug in the fallback path has been masked by the primary path working.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: extensions/msteams/src/attachments.helpers.test.ts
  • Scenario the test should lock in: buildMSTeamsGraphMessageUrls correctly uses a resolved Graph chat ID (not the a: format) for personal DMs.
  • Why this is the smallest reliable guardrail: The URL builder is a pure function — testing its output for different conversation ID formats directly validates the fix.
  • Existing test that already covers this (if any): None — existing tests only covered channel and groupChat URLs.

User-visible / Behavior Changes

  • Personal DM file/image attachments in Teams now reach the agent via the Graph API fallback path when the direct Bot Framework download fails.

Diagram (if applicable)

Before:
[Teams DM file] -> direct download (401) -> Graph fallback with a:xxx ID -> 404 Invalid ThreadId -> agent gets no file

After:
[Teams DM file] -> direct download (401) -> resolveGraphChatId(a:xxx) -> 19:real-id -> Graph fallback with real ID -> agent gets file

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No (reuses existing tokenProvider.getAccessToken and resolveGraphChatId)
  • New/changed network calls? Yes — one additional Graph API call (/me/chats?$filter=...) per personal DM conversation, cached after first resolution
  • Command/tool execution surface changed? No
  • Data access scope changed? No
  • The Graph /me/chats call uses the same token scope already used by the send path. The resolved chat ID is cached in the conversation store (same store used by send-context.ts).

Repro + Verification

Environment

  • OS: Ubuntu 24.04 (Linux 6.17.0-1008-azure, x64) — AKS pod
  • Runtime/container: OpenClaw 2026.3.13 (deployed), fix validated against upstream/main codebase
  • Model/provider: N/A (attachment handling, not model inference)
  • Integration/channel: MS Teams (Bot Framework), personal 1:1 DM
  • Relevant config: dmPolicy: "open", groupPolicy: "open"

Steps

  1. Configure MS Teams channel with bot registration
  2. Send a file or image to the bot in a personal 1:1 DM
  3. Observe: agent receives <media:image> or <media:document> placeholder but no file content

Expected

  • Agent receives the file content via the Graph API fallback path

Actual

  • Gateway log shows "graph media fetch empty" with hostedStatus: 403 / attachmentStatus: 404
  • Agent dispatched without media

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets

Before fix (live AKS pod):

08:28:07.327 received message
08:28:07.328 html attachment summary
08:28:08.100 graph media fetch empty
08:28:08.115 dispatching to agent      ← no media

After patching trafficmanager.net into auth allowlist (direct download fix, already on upstream/main):

Bot receives and processes image attachment successfully

Graph fallback path validated via instrumented logs:

convId=a:10OyQ1u7... type=personal isDM=true startsA=true

Confirmed the a: conversation ID reaches buildMSTeamsGraphMessageUrls unchanged when resolveGraphChatId is not called.

Human Verification (required)

  • Verified scenarios: Personal DM image upload on live AKS pod (aksclaw-peni namespace), instrumented with debug logs to trace conversation ID flow through resolveMSTeamsInboundMedia and buildMSTeamsGraphMessageUrls
  • Edge cases checked: Channel messages (verified 19: format passes through unchanged), a: prefix detection, conversation store caching path
  • What you did not verify: Graph /me/chats resolution with delegated auth tokens (tested environment uses app-only tokens where /me returns 400; the resolveGraphChatId function is already used and tested in the send path)

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Risks and Mitigations

  • Risk: resolveGraphChatId calls Graph /me/chats which requires delegated auth — may return null with app-only tokens
    • Mitigation: Falls back to the synthetic translateMSTeamsDmConversationIdForGraph ID (same behavior as before this fix). The primary download path (smba.trafficmanager.net) handles most cases; this fix improves the Graph fallback path.
  • Risk: Additional Graph API call per new DM conversation
    • Mitigation: Result is cached in the conversation store; subsequent messages use the cached value with zero API cost.

…penclaw#62219)

Bot Framework personal DM conversation IDs use an opaque `a:...` format
that the Graph `/chats/{chatId}/messages` endpoint rejects as "Invalid
ThreadId". When the direct Bot Framework attachment download fails and
the code falls back to the Graph API path, inbound media (images, files)
is silently dropped.

Resolve the real Graph chat ID via `resolveGraphChatId()` before
constructing Graph message URLs, with conversation-store caching so
subsequent messages skip the API lookup.
@openclaw-barnacle openclaw-barnacle Bot added channel: msteams Channel integration: msteams size: S labels Apr 8, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 8, 2026

Greptile Summary

This PR fixes a bug where Bot Framework personal DM conversation IDs in a: format were passed directly to the Graph API fallback path in resolveMSTeamsInboundMedia, causing 404 errors and silent media loss. The fix resolves the real Graph chat ID via resolveGraphChatId() before the media download call.

  • P1: The graphChatId caching doesn't work as designed — the unconditional conversationStore.upsert(conversationId, conversationRef) at line 390 runs before the cache read at line 517, and mergeStoredConversationReference overwrites graphChatId because only timezone gets the preserve-if-missing treatment. Every personal DM message will invoke resolveGraphChatId rather than just the first one.

Confidence Score: 4/5

Safe to merge after fixing the cache-invalidation bug in mergeStoredConversationReference; the core fix (passing a resolved Graph chat ID) is correct.

One P1 finding: the advertised per-conversation caching of graphChatId is broken because mergeStoredConversationReference does not preserve the field the way it does for timezone, meaning resolveGraphChatId (a Graph API call) fires on every personal-DM message instead of only once. The functional fix itself is sound, but the performance/rate-limit guarantee stated in the PR description does not hold until the merge helper is updated.

extensions/msteams/src/monitor-handler/message-handler.ts (caching logic) and extensions/msteams/src/conversation-store-helpers.ts (mergeStoredConversationReference needs a graphChatId preserve guard)

Vulnerabilities

No security concerns identified. The resolveGraphChatId call reuses the existing tokenProvider.getAccessToken flow already used on the send path; no new token scopes or secrets handling is introduced.

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: 517-537

Comment:
**`graphChatId` cache is silently erased before it is read**

The unconditional `conversationStore.upsert(conversationId, conversationRef)` at line 390 runs (and completes) before the `await conversationStore.get(conversationId)` cache check here. `mergeStoredConversationReference` spreads `incoming` last — it only explicitly preserves `timezone` from the existing entry; all other fields including `graphChatId` are overwritten. So on the second message the stored `graphChatId` is gone before the cache read, and `resolveGraphChatId` is called on every single personal-DM message rather than just once.

The fix mirrors the existing `timezone` guard:

```ts
// in conversation-store-helpers.ts  mergeStoredConversationReference
return {
  ...(existing?.timezone && !incoming.timezone ? { timezone: existing.timezone } : {}),
+ ...(existing?.graphChatId && !incoming.graphChatId ? { graphChatId: existing.graphChatId } : {}),
  ...incoming,
  lastSeenAt: nowIso,
};
```

Alternatively, populate `graphChatId` on `conversationRef` (or a local copy) before the first upsert fires so both writes carry the resolved value.

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

Reviews (1): Last reviewed commit: "fix(msteams): resolve Graph chat ID for ..." | Re-trigger Greptile

Comment thread extensions/msteams/src/monitor-handler/message-handler.ts
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: a3eafaa652

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

Comment thread extensions/msteams/src/monitor-handler/message-handler.ts
mergeStoredConversationReference only preserved timezone from the
existing entry — graphChatId was silently overwritten on every
activity-triggered upsert, defeating the cache and causing repeated
Graph API lookups on every DM turn.

Mirror the existing timezone guard so graphChatId survives upserts
that don't carry it.
@chatgpt-codex-connector
Copy link
Copy Markdown

💡 Codex Review

if (isDirectMessage && conversationId.startsWith("a:")) {
const cached = await conversationStore.get(conversationId);
if (cached?.graphChatId) {
graphConversationId = cached.graphChatId;
} else {

P2 Badge Gate Graph chat-ID lookup behind media-fallback need

Avoid resolving graphChatId for every personal DM turn here, because this block runs before we know whether Graph media fallback is needed and it executes even for text-only messages. In environments where /me/chats is unavailable (for example app-only tokens), resolveGraphChatId returns null, we do not cache that outcome, and the same failing network lookup is retried on each incoming DM, adding avoidable latency and Graph load to normal message handling.

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

@feiskyer
Copy link
Copy Markdown
Contributor Author

feiskyer commented Apr 9, 2026

@BradGroux could you help to have a look at this PR?

@BradGroux BradGroux merged commit 78389b1 into openclaw:main Apr 10, 2026
30 of 32 checks passed
@feiskyer feiskyer deleted the fix/62219-teams-dm-graph-media branch April 10, 2026 07:25
steipete pushed a commit that referenced this pull request Apr 10, 2026
…62219) (#63063)

* fix(msteams): resolve Graph chat ID for personal DM media downloads (#62219)

Bot Framework personal DM conversation IDs use an opaque `a:...` format
that the Graph `/chats/{chatId}/messages` endpoint rejects as "Invalid
ThreadId". When the direct Bot Framework attachment download fails and
the code falls back to the Graph API path, inbound media (images, files)
is silently dropped.

Resolve the real Graph chat ID via `resolveGraphChatId()` before
constructing Graph message URLs, with conversation-store caching so
subsequent messages skip the API lookup.

* fix(msteams): preserve graphChatId across conversation store upserts

mergeStoredConversationReference only preserved timezone from the
existing entry — graphChatId was silently overwritten on every
activity-triggered upsert, defeating the cache and causing repeated
Graph API lookups on every DM turn.

Mirror the existing timezone guard so graphChatId survives upserts
that don't carry it.
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
…penclaw#62219) (openclaw#63063)

* fix(msteams): resolve Graph chat ID for personal DM media downloads (openclaw#62219)

Bot Framework personal DM conversation IDs use an opaque `a:...` format
that the Graph `/chats/{chatId}/messages` endpoint rejects as "Invalid
ThreadId". When the direct Bot Framework attachment download fails and
the code falls back to the Graph API path, inbound media (images, files)
is silently dropped.

Resolve the real Graph chat ID via `resolveGraphChatId()` before
constructing Graph message URLs, with conversation-store caching so
subsequent messages skip the API lookup.

* fix(msteams): preserve graphChatId across conversation store upserts

mergeStoredConversationReference only preserved timezone from the
existing entry — graphChatId was silently overwritten on every
activity-triggered upsert, defeating the cache and causing repeated
Graph API lookups on every DM turn.

Mirror the existing timezone guard so graphChatId survives upserts
that don't carry it.
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…penclaw#62219) (openclaw#63063)

* fix(msteams): resolve Graph chat ID for personal DM media downloads (openclaw#62219)

Bot Framework personal DM conversation IDs use an opaque `a:...` format
that the Graph `/chats/{chatId}/messages` endpoint rejects as "Invalid
ThreadId". When the direct Bot Framework attachment download fails and
the code falls back to the Graph API path, inbound media (images, files)
is silently dropped.

Resolve the real Graph chat ID via `resolveGraphChatId()` before
constructing Graph message URLs, with conversation-store caching so
subsequent messages skip the API lookup.

* fix(msteams): preserve graphChatId across conversation store upserts

mergeStoredConversationReference only preserved timezone from the
existing entry — graphChatId was silently overwritten on every
activity-triggered upsert, defeating the cache and causing repeated
Graph API lookups on every DM turn.

Mirror the existing timezone guard so graphChatId survives upserts
that don't carry it.
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…penclaw#62219) (openclaw#63063)

* fix(msteams): resolve Graph chat ID for personal DM media downloads (openclaw#62219)

Bot Framework personal DM conversation IDs use an opaque `a:...` format
that the Graph `/chats/{chatId}/messages` endpoint rejects as "Invalid
ThreadId". When the direct Bot Framework attachment download fails and
the code falls back to the Graph API path, inbound media (images, files)
is silently dropped.

Resolve the real Graph chat ID via `resolveGraphChatId()` before
constructing Graph message URLs, with conversation-store caching so
subsequent messages skip the API lookup.

* fix(msteams): preserve graphChatId across conversation store upserts

mergeStoredConversationReference only preserved timezone from the
existing entry — graphChatId was silently overwritten on every
activity-triggered upsert, defeating the cache and causing repeated
Graph API lookups on every DM turn.

Mirror the existing timezone guard so graphChatId survives upserts
that don't carry it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: msteams Channel integration: msteams size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MS Teams: Graph API media fetch fails in personal DMs — Bot Framework conversation ID not recognized as Graph chat ID

2 participants