perf(msteams): reuse prepared activity on retries (#128329)

Amp-Thread-ID: https://ampcode.com/threads/T-01a021f5-984a-7628-a30c-491c166ff247

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Peter Steinberger
2026-08-23 11:50:09 -07:00
committed by GitHub
parent b191296a62
commit 7c7e10ab85
2 changed files with 20 additions and 11 deletions
+14 -5
View File
@@ -508,13 +508,14 @@ describe("msteams messenger", () => {
expect(retryEvents).toEqual([{ nextAttempt: 2, delayMs: 0 }]);
});
it("retries full activity preparation when media upload fails transiently", async () => {
it("retries media preparation but reuses it after provider dispatch starts", async () => {
const tmpDir = await mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "msteams-retry-"));
const localFile = path.join(tmpDir, "retry.txt");
await writeFile(localFile, "hello");
try {
const attempts: string[] = [];
const providerPayloads: string[] = [];
const retryEvents: Array<{ nextAttempt: number; delayMs: number }> = [];
let uploadAttempts = 0;
graphUploadMockState.uploadAndShareSharePoint.mockImplementation(async () => {
@@ -535,8 +536,12 @@ describe("msteams messenger", () => {
name: "retry.txt",
});
const sendActivity = createRecordedSendActivity(attempts, 429);
const ctx = {
sendActivity: createRecordedSendActivity(attempts),
sendActivity: async (activity: unknown) => {
providerPayloads.push(JSON.stringify(activity));
return await sendActivity(activity);
},
};
const ids = await sendMSTeamsMessages({
replyStyle: "thread",
@@ -555,14 +560,18 @@ describe("msteams messenger", () => {
getAccessToken: async () => "token",
},
sharePointSiteId: "site-123",
retry: { maxAttempts: 2, baseDelayMs: 0, maxDelayMs: 0 },
retry: { maxAttempts: 3, baseDelayMs: 0, maxDelayMs: 0 },
onRetry: (e) => retryEvents.push({ nextAttempt: e.nextAttempt, delayMs: e.delayMs }),
});
expect(uploadAttempts).toBe(2);
expect(attempts).toEqual(["one"]);
expect(attempts).toEqual(["one", "one"]);
expect(providerPayloads[1]).toBe(providerPayloads[0]);
expect(ids).toEqual(["id:one"]);
expect(retryEvents).toEqual([{ nextAttempt: 2, delayMs: 0 }]);
expect(retryEvents).toEqual([
{ nextAttempt: 2, delayMs: 0 },
{ nextAttempt: 3, delayMs: 0 },
]);
} finally {
await rm(tmpDir, { recursive: true, force: true });
}
+6 -6
View File
@@ -452,12 +452,15 @@ export async function sendMSTeamsMessages(params: {
message: MSTeamsRenderedMessage,
messageIndex: number,
): Promise<string> => {
let activity: Record<string, unknown> | undefined;
let pendingUploadId: string | undefined;
let response: unknown;
try {
response = await sendWithRetry(
async () => {
const activity = await buildActivity(
// Retry failed preparation, but keep its successful I/O and SharePoint work
// out of subsequent provider retries.
activity ??= await buildActivity(
message,
params.conversationRef,
params.tokenProvider,
@@ -466,14 +469,11 @@ export async function sendMSTeamsMessages(params: {
{ feedbackLoopEnabled: params.feedbackLoopEnabled },
);
// Extract and strip the internal-only pending upload tag before sending.
pendingUploadId =
pendingUploadId ??=
typeof activity["_pendingUploadId"] === "string"
? activity["_pendingUploadId"]
: undefined;
if (pendingUploadId) {
delete activity["_pendingUploadId"];
}
delete activity["_pendingUploadId"];
providerDispatchStarted = true;
return await sendFn(activity);