Skip to content

fix(msteams): wire CLI --media path into pending upload store (#49784)#64646

Merged
BradGroux merged 3 commits intoopenclaw:mainfrom
sudie-codes:fix/msteams-cli-media-upload-49784
Apr 11, 2026
Merged

fix(msteams): wire CLI --media path into pending upload store (#49784)#64646
BradGroux merged 3 commits intoopenclaw:mainfrom
sudie-codes:fix/msteams-cli-media-upload-49784

Conversation

@sudie-codes
Copy link
Copy Markdown
Contributor

Summary

openclaw message send --media <file> targeting a Teams DM silently failed after the user clicked "Allow" on the FileConsentCard. The CLI send path stored the pending upload in an in-memory map and then exited — when Teams delivered the fileConsent/invoke callback to the long-lived gateway monitor process, its in-memory lookup missed and the user saw "This card action is not supported".

Root cause

The outbound CLI path went through prepareFileConsentActivity, which only wrote to pending-uploads.ts (in-memory). That store cannot span the CLI and gateway processes, so the invoke handler's getPendingUpload(uploadId) returned undefined and the upload silently dropped.

Fix

  • New filesystem-backed pending upload store at extensions/msteams/src/pending-uploads-fs.ts (base64-encoded buffers, TTL-pruned, file-locked, bounded cap)
  • New prepareFileConsentActivityFs helper in file-consent-helpers.ts that mirrors the pending upload into the FS store under the same uploadId already placed in the card context
  • send.ts (CLI --media path) now calls the FS helper and also persists the consent card activity id via setPendingUploadActivityIdFs
  • monitor-handler.ts falls back to the FS store when the in-memory lookup misses and cleans up both stores on upload/decline
  • Same-process reply path (messenger.ts) is unchanged: it keeps using the in-memory fast path

Test plan

  • pending-uploads-fs.test.ts: store/get/remove round-trip, TTL expiry, malformed file tolerance, cross-process read simulation, prepareFileConsentActivityFs end-to-end against a tmp state dir
  • monitor-handler.file-consent.test.ts: FS-fallback invoke handler uploads when in-memory store is empty, cleans up FS entry on accept and decline
  • Existing monitor-handler file-consent tests still pass (in-memory path preserved)
  • Full msteams suite: 777 tests passing
  • Manual: openclaw message send --channel msteams --media file.pdf to a Teams personal DM, click Allow, verify file info card replaces consent card

Fixes #49784

🤖 Generated with Claude Code

@openclaw-barnacle openclaw-barnacle Bot added channel: msteams Channel integration: msteams size: L labels Apr 11, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 11, 2026

Greptile Summary

This PR fixes a silent failure in openclaw message send --media targeting a Teams personal DM: the CLI process stored the pending upload in-memory, then exited before Teams delivered the fileConsent/invoke callback to the long-lived gateway monitor process, causing a lookup miss and "card action not supported". The fix introduces a filesystem-backed store (pending-uploads-fs.ts) that persists the upload buffer (base64-encoded, file-locked, TTL-pruned) and updates send.ts to write through it; the monitor handler falls back to the FS store when the in-memory lookup misses. Same-process reply path is unchanged and keeps using the fast in-memory path.

Confidence Score: 5/5

Safe to merge; the only finding is a harmless redundant TTL guard that dead-code-replicates what readStore already does.

All remaining findings are P2 (dead code / style). The cross-process fix is correctly implemented: file-locked read-modify-write for writes, atomic file writes make unlocked reads safe, both stores are cleaned up on accept/decline, and the new FS path is well-covered by unit and integration tests.

No files require special attention.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/msteams/src/pending-uploads-fs.ts
Line: 186-188

Comment:
**Redundant TTL check after `readStore` already prunes expired entries**

`readStore` calls `pruneExpired(value.uploads, Date.now(), ttlMs)` before returning, which removes any entry where `nowMs - record.createdAt > ttlMs`. Since all records returned in `store.uploads` have already passed that filter, this subsequent check (`Date.now() - record.createdAt > ttlMs`) at lines 186–188 can never be `true` in practice — the only scenario where it would fire is if the TTL expires in the nanoseconds between `readStore`'s `Date.now()` call and this one, which requires a TTL of ~0ms. The code can be simplified:

```suggestion
  return recordToUpload(record);
```

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

Reviews (1): Last reviewed commit: "fix(msteams): wire CLI --media path into..." | Re-trigger Greptile

Comment on lines +186 to +188
if (Date.now() - record.createdAt > ttlMs) {
return undefined;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant TTL check after readStore already prunes expired entries

readStore calls pruneExpired(value.uploads, Date.now(), ttlMs) before returning, which removes any entry where nowMs - record.createdAt > ttlMs. Since all records returned in store.uploads have already passed that filter, this subsequent check (Date.now() - record.createdAt > ttlMs) at lines 186–188 can never be true in practice — the only scenario where it would fire is if the TTL expires in the nanoseconds between readStore's Date.now() call and this one, which requires a TTL of ~0ms. The code can be simplified:

Suggested change
if (Date.now() - record.createdAt > ttlMs) {
return undefined;
}
return recordToUpload(record);
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/pending-uploads-fs.ts
Line: 186-188

Comment:
**Redundant TTL check after `readStore` already prunes expired entries**

`readStore` calls `pruneExpired(value.uploads, Date.now(), ttlMs)` before returning, which removes any entry where `nowMs - record.createdAt > ttlMs`. Since all records returned in `store.uploads` have already passed that filter, this subsequent check (`Date.now() - record.createdAt > ttlMs`) at lines 186–188 can never be `true` in practice — the only scenario where it would fire is if the TTL expires in the nanoseconds between `readStore`'s `Date.now()` call and this one, which requires a TTL of ~0ms. The code can be simplified:

```suggestion
  return recordToUpload(record);
```

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

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: 30be2c1ba7

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

setPendingUploadActivityId(uploadId, messageId);
await setPendingUploadActivityIdFs(uploadId, messageId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Treat FS activity-id persistence as best effort

This call runs after the FileConsent card has already been sent, so an I/O failure here (e.g., disk full, permission issue, transient lock error) turns a successful send into a thrown error. In that scenario users see the command fail and may retry, creating duplicate consent cards, even though the pending upload record already exists and the accept flow can still work. The FS activity-id update should be non-fatal (log and continue) rather than failing sendMessageMSTeams.

Useful? React with 👍 / 👎.

@@ -200,6 +214,7 @@ async function handleFileConsentInvoke(
await context.sendActivity("File upload failed. Please try again.");
} finally {
removePendingUpload(uploadId);
await removePendingUploadFs(uploadId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make FS pending-upload cleanup non-fatal in invoke handler

removePendingUploadFs is awaited inside finally, so a cleanup write failure can reject the invoke handler even after the file upload already succeeded. In environments with intermittent FS issues this can surface as a failed invoke/possible retry path and duplicate side effects, despite successful upload work. Cleanup should be best-effort (catch/log) so storage errors do not break the user-facing consent completion flow.

Useful? React with 👍 / 👎.

@sudie-codes sudie-codes force-pushed the fix/msteams-cli-media-upload-49784 branch from 30be2c1 to 09c4738 Compare April 11, 2026 05:11
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: 09c4738d4b

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

type: "message",
attachments: [consentCard],
};
await storePendingUploadFs({
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Roll back in-memory pending upload on FS write failure

prepareFileConsentActivityFs creates the in-memory pending entry before awaiting the filesystem mirror write, so any storePendingUploadFs error (disk full, permissions, lock/write failure) throws after the large buffer has already been retained in pending-uploads.ts. In long-lived paths that call sendMessageMSTeams (for example outbound/action uploads), repeated failures can accumulate multiple large buffers for the full TTL and cause avoidable memory pressure; this should remove the in-memory entry on FS failure (or write FS first with a generated id).

Useful? React with 👍 / 👎.

@sudie-codes sudie-codes force-pushed the fix/msteams-cli-media-upload-49784 branch from 09c4738 to 0de22a9 Compare April 11, 2026 05:22
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

https://github.com/openclaw/openclaw/blob/0de22a927dcb702cef821d0845c6d7623ba563f8/extensions/msteams/src/send.ts#L170-L174
P2 Badge Remove persisted upload when consent-card send fails

prepareFileConsentActivityFs stores the full file payload before the card is sent, but if sendProactiveActivity throws (for example transient Graph/Bot Framework failures), this branch exits without removing the newly persisted entry. In CLI retry scenarios, those orphaned records keep large base64 payloads on disk (and in-memory for the current process) until TTL/cap pruning, which can create avoidable state bloat and evict still-valid pending uploads under load.

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +119 to +122
candidate.version === 1 &&
typeof candidate.uploads === "object" &&
candidate.uploads !== null &&
!Array.isArray(candidate.uploads)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate upload records before pruning the FS store

isValidStore only verifies that uploads is an object, but pruneExpired assumes each entry has a record shape with createdAt; a malformed entry like {"uploads":{"x":null}} will throw when record.createdAt is accessed and break consent-store reads/writes until manual cleanup. Since this module is intended to tolerate malformed store files, it should also validate or defensively skip invalid per-upload entries.

Useful? React with 👍 / 👎.

@sudie-codes
Copy link
Copy Markdown
Contributor Author

CI status notes:

All failing checks are pre-existing issues on origin/main, unrelated to this PR:

  • checks-fast-contracts-protocol and checks-node-core-test-core-support-boundary: boundary test failures for src/config/config.pruning-defaults.test.ts which imports ../../extensions/anthropic/provider-policy-api.js. This file is untouched by this PR and the failures reproduce on latest origin/main locally.
  • checks-node-extensions-shard-6: memory-wiki test extensions/memory-wiki/index.test.ts expecting only 15 gateway methods but the module now registers 18 (added wiki.importRuns, wiki.importInsights, wiki.palace). This PR does not touch extensions/memory-wiki/**.

This PR's touched surface is all green:

  • check, check-additional, build-smoke, checks-fast-bundled, extension-fast (msteams), all six extension shards (1-5), all core unit shards, windows test, checks-node-channels
  • Local pnpm test extensions/msteams: 777/777 passing (61 test files)

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

https://github.com/openclaw/openclaw/blob/157bdd7e1023a28d9e921cec27e99ccde84ec38f/extensions/msteams/src/send.ts#L170-L174
P2 Badge Clean up pending upload when consent card send fails

prepareFileConsentActivityFs writes the full upload payload to the FS-backed pending store before this send call, but there is no failure-path cleanup if sendProactiveActivity throws (for example transient Bot Framework/API errors). In the CLI process that means orphaned large base64 blobs stay on disk until TTL/cap pruning, and repeated send failures can consume the 100-entry cap and evict still-valid pending uploads from other sends, causing later fileConsent/invoke accepts to look expired. Wrap this path with cleanup (removePendingUpload + removePendingUploadFs) before rethrowing.

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@BradGroux BradGroux force-pushed the fix/msteams-cli-media-upload-49784 branch from 157bdd7 to e98ce0a Compare April 11, 2026 13:44
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

const messageId = await sendProactiveActivity({
adapter,
appId,
ref,
activity,

P2 Badge Clean up persisted upload when consent-card send throws

prepareFileConsentActivityFs has already written the full media payload to the FS pending-upload store before this sendProactiveActivity call, but if the send fails (for example token/conversation/network errors), the function exits without removing that record. In this failure path no consent card exists for users to act on, so retries can accumulate orphaned large blobs on disk and retain file contents unexpectedly; add rollback cleanup for both in-memory and FS stores when proactive send fails.

ℹ️ 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 on lines +186 to +188
if (Date.now() - record.createdAt > ttlMs) {
return undefined;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist TTL pruning when expired FS upload is read

When an entry is expired, getPendingUploadFs returns undefined but never writes the deletion back, so expired base64 file payloads can remain in msteams-pending-uploads.json indefinitely if no later write happens. That breaks the intended 5-minute retention behavior for abandoned consent cards and can keep sensitive file bytes on disk much longer than expected; expired reads should trigger a locked rewrite/removal.

Useful? React with 👍 / 👎.

@BradGroux BradGroux merged commit ba1b842 into openclaw:main Apr 11, 2026
41 checks passed
trudbot pushed a commit to trudbot/openclaw that referenced this pull request Apr 12, 2026
…aw#49784) (openclaw#64646)

* fix(msteams): wire CLI --media path into FS-backed pending upload store (openclaw#49784)

* test(msteams): clean up temp dirs in pending-uploads-fs.test.ts

* test(msteams): satisfy pending upload fs lint

---------

Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
…aw#49784) (openclaw#64646)

* fix(msteams): wire CLI --media path into FS-backed pending upload store (openclaw#49784)

* test(msteams): clean up temp dirs in pending-uploads-fs.test.ts

* test(msteams): satisfy pending upload fs lint

---------

Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…aw#49784) (openclaw#64646)

* fix(msteams): wire CLI --media path into FS-backed pending upload store (openclaw#49784)

* test(msteams): clean up temp dirs in pending-uploads-fs.test.ts

* test(msteams): satisfy pending upload fs lint

---------

Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…aw#49784) (openclaw#64646)

* fix(msteams): wire CLI --media path into FS-backed pending upload store (openclaw#49784)

* test(msteams): clean up temp dirs in pending-uploads-fs.test.ts

* test(msteams): satisfy pending upload fs lint

---------

Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
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: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: MS Teams File Upload via CLI

2 participants