fix(msteams): wire CLI --media path into pending upload store (#49784)#64646
Conversation
Greptile SummaryThis PR fixes a silent failure in Confidence Score: 5/5Safe to merge; the only finding is a harmless redundant TTL guard that dead-code-replicates what 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 AIThis 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 |
| if (Date.now() - record.createdAt > ttlMs) { | ||
| return undefined; | ||
| } |
There was a problem hiding this 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:
| 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.There was a problem hiding this comment.
💡 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); |
There was a problem hiding this comment.
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); | |||
There was a problem hiding this comment.
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 👍 / 👎.
30be2c1 to
09c4738
Compare
There was a problem hiding this comment.
💡 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({ |
There was a problem hiding this comment.
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 👍 / 👎.
09c4738 to
0de22a9
Compare
There was a problem hiding this comment.
💡 Codex Review
https://github.com/openclaw/openclaw/blob/0de22a927dcb702cef821d0845c6d7623ba563f8/extensions/msteams/src/send.ts#L170-L174
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".
| candidate.version === 1 && | ||
| typeof candidate.uploads === "object" && | ||
| candidate.uploads !== null && | ||
| !Array.isArray(candidate.uploads) |
There was a problem hiding this comment.
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 👍 / 👎.
|
CI status notes: All failing checks are pre-existing issues on
This PR's touched surface is all green:
|
There was a problem hiding this comment.
💡 Codex Review
https://github.com/openclaw/openclaw/blob/157bdd7e1023a28d9e921cec27e99ccde84ec38f/extensions/msteams/src/send.ts#L170-L174
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".
157bdd7 to
e98ce0a
Compare
There was a problem hiding this comment.
💡 Codex Review
openclaw/extensions/msteams/src/send.ts
Lines 170 to 174 in e98ce0a
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".
| if (Date.now() - record.createdAt > ttlMs) { | ||
| return undefined; | ||
| } |
There was a problem hiding this comment.
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 👍 / 👎.
…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>
…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>
…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>
…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>
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 thefileConsent/invokecallback 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 topending-uploads.ts(in-memory). That store cannot span the CLI and gateway processes, so the invoke handler'sgetPendingUpload(uploadId)returnedundefinedand the upload silently dropped.Fix
extensions/msteams/src/pending-uploads-fs.ts(base64-encoded buffers, TTL-pruned, file-locked, bounded cap)prepareFileConsentActivityFshelper infile-consent-helpers.tsthat mirrors the pending upload into the FS store under the sameuploadIdalready placed in the card contextsend.ts(CLI--mediapath) now calls the FS helper and also persists the consent card activity id viasetPendingUploadActivityIdFsmonitor-handler.tsfalls back to the FS store when the in-memory lookup misses and cleans up both stores on upload/declinemessenger.ts) is unchanged: it keeps using the in-memory fast pathTest plan
pending-uploads-fs.test.ts: store/get/remove round-trip, TTL expiry, malformed file tolerance, cross-process read simulation,prepareFileConsentActivityFsend-to-end against a tmp state dirmonitor-handler.file-consent.test.ts: FS-fallback invoke handler uploads when in-memory store is empty, cleans up FS entry on accept and declineopenclaw message send --channel msteams --media file.pdfto a Teams personal DM, click Allow, verify file info card replaces consent cardFixes #49784
🤖 Generated with Claude Code