@openclaw/msteams 2026.5.2 → 2026.5.3-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +3 -0
- package/dist/channel-D7hdreTh.js +984 -0
- package/dist/channel-config-api.js +2 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.runtime-BC1ruIfN.js +573 -0
- package/dist/config-schema-B8QezH6t.js +15 -0
- package/dist/contract-api.js +2 -0
- package/dist/graph-users-9uQJepqr.js +1354 -0
- package/dist/index.js +22 -0
- package/dist/oauth-BWJyilR1.js +114 -0
- package/dist/oauth.token-xxpoLWy5.js +115 -0
- package/dist/policy-DTnU2GR7.js +142 -0
- package/dist/probe-D_H8yFps.js +2194 -0
- package/dist/resolve-allowlist-D41JSziq.js +219 -0
- package/dist/runtime-api-DV1iVMn1.js +28 -0
- package/dist/runtime-api.js +2 -0
- package/dist/secret-contract-BuoEXmPS.js +35 -0
- package/dist/secret-contract-api.js +2 -0
- package/dist/setup-entry.js +15 -0
- package/dist/setup-plugin-api.js +64 -0
- package/dist/setup-surface-BLkFQYIQ.js +313 -0
- package/dist/src-CFp1QpFd.js +4064 -0
- package/dist/test-api.js +2 -0
- package/package.json +14 -6
- package/api.ts +0 -3
- package/channel-config-api.ts +0 -1
- package/channel-plugin-api.ts +0 -2
- package/config-api.ts +0 -4
- package/contract-api.ts +0 -4
- package/index.ts +0 -20
- package/runtime-api.ts +0 -73
- package/secret-contract-api.ts +0 -5
- package/setup-entry.ts +0 -13
- package/setup-plugin-api.ts +0 -3
- package/src/ai-entity.ts +0 -7
- package/src/approval-auth.ts +0 -44
- package/src/attachments/bot-framework.test.ts +0 -461
- package/src/attachments/bot-framework.ts +0 -362
- package/src/attachments/download.ts +0 -311
- package/src/attachments/graph.test.ts +0 -416
- package/src/attachments/graph.ts +0 -484
- package/src/attachments/html.ts +0 -122
- package/src/attachments/payload.ts +0 -14
- package/src/attachments/remote-media.test.ts +0 -137
- package/src/attachments/remote-media.ts +0 -112
- package/src/attachments/shared.test.ts +0 -530
- package/src/attachments/shared.ts +0 -626
- package/src/attachments/types.ts +0 -47
- package/src/attachments.graph.test.ts +0 -342
- package/src/attachments.helpers.test.ts +0 -246
- package/src/attachments.test-helpers.ts +0 -17
- package/src/attachments.test.ts +0 -687
- package/src/attachments.ts +0 -18
- package/src/block-streaming-config.test.ts +0 -61
- package/src/channel-api.ts +0 -1
- package/src/channel.actions.test.ts +0 -742
- package/src/channel.directory.test.ts +0 -200
- package/src/channel.runtime.ts +0 -56
- package/src/channel.setup.ts +0 -77
- package/src/channel.test.ts +0 -128
- package/src/channel.ts +0 -1136
- package/src/config-schema.ts +0 -6
- package/src/config-ui-hints.ts +0 -12
- package/src/conversation-store-fs.test.ts +0 -74
- package/src/conversation-store-fs.ts +0 -149
- package/src/conversation-store-helpers.test.ts +0 -202
- package/src/conversation-store-helpers.ts +0 -105
- package/src/conversation-store-memory.ts +0 -51
- package/src/conversation-store.shared.test.ts +0 -225
- package/src/conversation-store.ts +0 -71
- package/src/directory-live.test.ts +0 -156
- package/src/directory-live.ts +0 -111
- package/src/doctor.ts +0 -27
- package/src/errors.test.ts +0 -133
- package/src/errors.ts +0 -246
- package/src/feedback-reflection-prompt.ts +0 -117
- package/src/feedback-reflection-store.ts +0 -114
- package/src/feedback-reflection.test.ts +0 -237
- package/src/feedback-reflection.ts +0 -283
- package/src/file-consent-helpers.test.ts +0 -326
- package/src/file-consent-helpers.ts +0 -126
- package/src/file-consent-invoke.ts +0 -150
- package/src/file-consent.test.ts +0 -363
- package/src/file-consent.ts +0 -287
- package/src/graph-chat.ts +0 -55
- package/src/graph-group-management.test.ts +0 -318
- package/src/graph-group-management.ts +0 -168
- package/src/graph-members.test.ts +0 -89
- package/src/graph-members.ts +0 -48
- package/src/graph-messages.actions.test.ts +0 -243
- package/src/graph-messages.read.test.ts +0 -391
- package/src/graph-messages.search.test.ts +0 -213
- package/src/graph-messages.test-helpers.ts +0 -50
- package/src/graph-messages.ts +0 -534
- package/src/graph-teams.test.ts +0 -215
- package/src/graph-teams.ts +0 -114
- package/src/graph-thread.test.ts +0 -246
- package/src/graph-thread.ts +0 -146
- package/src/graph-upload.test.ts +0 -258
- package/src/graph-upload.ts +0 -531
- package/src/graph-users.ts +0 -29
- package/src/graph.test.ts +0 -516
- package/src/graph.ts +0 -293
- package/src/inbound.test.ts +0 -221
- package/src/inbound.ts +0 -148
- package/src/index.ts +0 -4
- package/src/media-helpers.test.ts +0 -202
- package/src/media-helpers.ts +0 -105
- package/src/mentions.test.ts +0 -244
- package/src/mentions.ts +0 -114
- package/src/messenger.test.ts +0 -865
- package/src/messenger.ts +0 -605
- package/src/monitor-handler/access.ts +0 -125
- package/src/monitor-handler/inbound-media.test.ts +0 -289
- package/src/monitor-handler/inbound-media.ts +0 -180
- package/src/monitor-handler/message-handler-mock-support.test-support.ts +0 -28
- package/src/monitor-handler/message-handler.authz.test.ts +0 -669
- package/src/monitor-handler/message-handler.dm-media.test.ts +0 -54
- package/src/monitor-handler/message-handler.test-support.ts +0 -100
- package/src/monitor-handler/message-handler.thread-parent.test.ts +0 -223
- package/src/monitor-handler/message-handler.thread-session.test.ts +0 -77
- package/src/monitor-handler/message-handler.ts +0 -1000
- package/src/monitor-handler/reaction-handler.test.ts +0 -267
- package/src/monitor-handler/reaction-handler.ts +0 -210
- package/src/monitor-handler/thread-session.ts +0 -17
- package/src/monitor-handler.adaptive-card.test.ts +0 -162
- package/src/monitor-handler.feedback-authz.test.ts +0 -314
- package/src/monitor-handler.file-consent.test.ts +0 -423
- package/src/monitor-handler.sso.test.ts +0 -563
- package/src/monitor-handler.test-helpers.ts +0 -180
- package/src/monitor-handler.ts +0 -534
- package/src/monitor-handler.types.ts +0 -27
- package/src/monitor-types.ts +0 -6
- package/src/monitor.lifecycle.test.ts +0 -278
- package/src/monitor.test.ts +0 -119
- package/src/monitor.ts +0 -442
- package/src/oauth.flow.ts +0 -77
- package/src/oauth.shared.ts +0 -37
- package/src/oauth.test.ts +0 -305
- package/src/oauth.token.ts +0 -158
- package/src/oauth.ts +0 -130
- package/src/outbound.test.ts +0 -130
- package/src/outbound.ts +0 -71
- package/src/pending-uploads-fs.test.ts +0 -246
- package/src/pending-uploads-fs.ts +0 -235
- package/src/pending-uploads.test.ts +0 -173
- package/src/pending-uploads.ts +0 -121
- package/src/policy.test.ts +0 -240
- package/src/policy.ts +0 -262
- package/src/polls-store-memory.ts +0 -32
- package/src/polls.test.ts +0 -160
- package/src/polls.ts +0 -323
- package/src/presentation.ts +0 -68
- package/src/probe.test.ts +0 -77
- package/src/probe.ts +0 -132
- package/src/reply-dispatcher.test.ts +0 -437
- package/src/reply-dispatcher.ts +0 -346
- package/src/reply-stream-controller.test.ts +0 -235
- package/src/reply-stream-controller.ts +0 -147
- package/src/resolve-allowlist.test.ts +0 -250
- package/src/resolve-allowlist.ts +0 -309
- package/src/revoked-context.ts +0 -17
- package/src/runtime.ts +0 -9
- package/src/sdk-types.ts +0 -59
- package/src/sdk.test.ts +0 -666
- package/src/sdk.ts +0 -884
- package/src/secret-contract.ts +0 -49
- package/src/secret-input.ts +0 -7
- package/src/send-context.ts +0 -231
- package/src/send.test.ts +0 -493
- package/src/send.ts +0 -637
- package/src/sent-message-cache.test.ts +0 -15
- package/src/sent-message-cache.ts +0 -56
- package/src/session-route.ts +0 -40
- package/src/setup-core.ts +0 -160
- package/src/setup-surface.test.ts +0 -202
- package/src/setup-surface.ts +0 -320
- package/src/sso-token-store.test.ts +0 -72
- package/src/sso-token-store.ts +0 -166
- package/src/sso.ts +0 -300
- package/src/storage.ts +0 -25
- package/src/store-fs.ts +0 -44
- package/src/streaming-message.test.ts +0 -262
- package/src/streaming-message.ts +0 -297
- package/src/test-runtime.ts +0 -16
- package/src/thread-parent-context.test.ts +0 -224
- package/src/thread-parent-context.ts +0 -159
- package/src/token-response.ts +0 -11
- package/src/token.test.ts +0 -259
- package/src/token.ts +0 -195
- package/src/user-agent.test.ts +0 -86
- package/src/user-agent.ts +0 -53
- package/src/webhook-timeouts.ts +0 -27
- package/src/welcome-card.test.ts +0 -81
- package/src/welcome-card.ts +0 -57
- package/test-api.ts +0 -1
- package/tsconfig.json +0 -16
package/src/errors.test.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
classifyMSTeamsSendError,
|
|
4
|
-
formatMSTeamsSendErrorHint,
|
|
5
|
-
formatUnknownError,
|
|
6
|
-
isRevokedProxyError,
|
|
7
|
-
} from "./errors.js";
|
|
8
|
-
import { withRevokedProxyFallback } from "./revoked-context.js";
|
|
9
|
-
|
|
10
|
-
describe("msteams errors", () => {
|
|
11
|
-
it("formats unknown errors", () => {
|
|
12
|
-
expect(formatUnknownError("oops")).toBe("oops");
|
|
13
|
-
expect(formatUnknownError(null)).toBe("null");
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("classifies auth errors", () => {
|
|
17
|
-
expect(classifyMSTeamsSendError({ statusCode: 401 }).kind).toBe("auth");
|
|
18
|
-
expect(classifyMSTeamsSendError({ statusCode: 403 }).kind).toBe("auth");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("classifies ContentStreamNotAllowed as permanent instead of auth", () => {
|
|
22
|
-
expect(
|
|
23
|
-
classifyMSTeamsSendError({
|
|
24
|
-
statusCode: 403,
|
|
25
|
-
response: {
|
|
26
|
-
body: {
|
|
27
|
-
error: {
|
|
28
|
-
code: "ContentStreamNotAllowed",
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
}),
|
|
33
|
-
).toMatchObject({
|
|
34
|
-
kind: "permanent",
|
|
35
|
-
statusCode: 403,
|
|
36
|
-
errorCode: "ContentStreamNotAllowed",
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("classifies throttling errors and parses retry-after", () => {
|
|
41
|
-
expect(classifyMSTeamsSendError({ statusCode: 429, retryAfter: "1.5" })).toMatchObject({
|
|
42
|
-
kind: "throttled",
|
|
43
|
-
statusCode: 429,
|
|
44
|
-
retryAfterMs: 1500,
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("classifies transient errors", () => {
|
|
49
|
-
expect(classifyMSTeamsSendError({ statusCode: 503 })).toMatchObject({
|
|
50
|
-
kind: "transient",
|
|
51
|
-
statusCode: 503,
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("classifies permanent 4xx errors", () => {
|
|
56
|
-
expect(classifyMSTeamsSendError({ statusCode: 400 })).toMatchObject({
|
|
57
|
-
kind: "permanent",
|
|
58
|
-
statusCode: 400,
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("provides actionable hints for common cases", () => {
|
|
63
|
-
expect(formatMSTeamsSendErrorHint({ kind: "auth" })).toContain("msteams");
|
|
64
|
-
expect(formatMSTeamsSendErrorHint({ kind: "throttled" })).toContain("throttled");
|
|
65
|
-
expect(
|
|
66
|
-
formatMSTeamsSendErrorHint({
|
|
67
|
-
kind: "permanent",
|
|
68
|
-
errorCode: "ContentStreamNotAllowed",
|
|
69
|
-
}),
|
|
70
|
-
).toContain("expired the content stream");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe("isRevokedProxyError", () => {
|
|
74
|
-
it("returns true for revoked proxy TypeError", () => {
|
|
75
|
-
expect(
|
|
76
|
-
isRevokedProxyError(new TypeError("Cannot perform 'set' on a proxy that has been revoked")),
|
|
77
|
-
).toBe(true);
|
|
78
|
-
expect(
|
|
79
|
-
isRevokedProxyError(new TypeError("Cannot perform 'get' on a proxy that has been revoked")),
|
|
80
|
-
).toBe(true);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("returns false for non-TypeError errors", () => {
|
|
84
|
-
expect(isRevokedProxyError(new Error("proxy that has been revoked"))).toBe(false);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("returns false for unrelated TypeErrors", () => {
|
|
88
|
-
expect(isRevokedProxyError(new TypeError("undefined is not a function"))).toBe(false);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("returns false for non-error values", () => {
|
|
92
|
-
expect(isRevokedProxyError(null)).toBe(false);
|
|
93
|
-
expect(isRevokedProxyError("proxy that has been revoked")).toBe(false);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe("withRevokedProxyFallback", () => {
|
|
98
|
-
it("returns primary result when no error occurs", async () => {
|
|
99
|
-
await expect(
|
|
100
|
-
withRevokedProxyFallback({
|
|
101
|
-
run: async () => "ok",
|
|
102
|
-
onRevoked: async () => "fallback",
|
|
103
|
-
}),
|
|
104
|
-
).resolves.toBe("ok");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("uses fallback when proxy-revoked TypeError is thrown", async () => {
|
|
108
|
-
const onRevokedLog = vi.fn();
|
|
109
|
-
await expect(
|
|
110
|
-
withRevokedProxyFallback({
|
|
111
|
-
run: async () => {
|
|
112
|
-
throw new TypeError("Cannot perform 'get' on a proxy that has been revoked");
|
|
113
|
-
},
|
|
114
|
-
onRevoked: async () => "fallback",
|
|
115
|
-
onRevokedLog,
|
|
116
|
-
}),
|
|
117
|
-
).resolves.toBe("fallback");
|
|
118
|
-
expect(onRevokedLog).toHaveBeenCalledOnce();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("rethrows non-revoked errors", async () => {
|
|
122
|
-
const err = Object.assign(new Error("boom"), { statusCode: 500 });
|
|
123
|
-
await expect(
|
|
124
|
-
withRevokedProxyFallback({
|
|
125
|
-
run: async () => {
|
|
126
|
-
throw err;
|
|
127
|
-
},
|
|
128
|
-
onRevoked: async () => "fallback",
|
|
129
|
-
}),
|
|
130
|
-
).rejects.toBe(err);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
});
|
package/src/errors.ts
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
2
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
export function formatUnknownError(err: unknown): string {
|
|
6
|
-
if (err instanceof Error) {
|
|
7
|
-
return err.message;
|
|
8
|
-
}
|
|
9
|
-
if (typeof err === "string") {
|
|
10
|
-
return err;
|
|
11
|
-
}
|
|
12
|
-
if (err === null) {
|
|
13
|
-
return "null";
|
|
14
|
-
}
|
|
15
|
-
if (err === undefined) {
|
|
16
|
-
return "undefined";
|
|
17
|
-
}
|
|
18
|
-
if (typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") {
|
|
19
|
-
return String(err);
|
|
20
|
-
}
|
|
21
|
-
if (typeof err === "symbol") {
|
|
22
|
-
return err.description ?? err.toString();
|
|
23
|
-
}
|
|
24
|
-
if (typeof err === "function") {
|
|
25
|
-
return err.name ? `[function ${err.name}]` : "[function]";
|
|
26
|
-
}
|
|
27
|
-
try {
|
|
28
|
-
return JSON.stringify(err) ?? "unknown error";
|
|
29
|
-
} catch {
|
|
30
|
-
return "unknown error";
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function extractStatusCode(err: unknown): number | null {
|
|
35
|
-
if (!isRecord(err)) {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
const direct = err.statusCode ?? err.status;
|
|
39
|
-
if (typeof direct === "number" && Number.isFinite(direct)) {
|
|
40
|
-
return direct;
|
|
41
|
-
}
|
|
42
|
-
if (typeof direct === "string") {
|
|
43
|
-
const parsed = Number.parseInt(direct, 10);
|
|
44
|
-
if (Number.isFinite(parsed)) {
|
|
45
|
-
return parsed;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const response = err.response;
|
|
50
|
-
if (isRecord(response)) {
|
|
51
|
-
const status = response.status;
|
|
52
|
-
if (typeof status === "number" && Number.isFinite(status)) {
|
|
53
|
-
return status;
|
|
54
|
-
}
|
|
55
|
-
if (typeof status === "string") {
|
|
56
|
-
const parsed = Number.parseInt(status, 10);
|
|
57
|
-
if (Number.isFinite(parsed)) {
|
|
58
|
-
return parsed;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function extractErrorCode(err: unknown): string | null {
|
|
67
|
-
if (!isRecord(err)) {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const direct = err.code;
|
|
72
|
-
if (typeof direct === "string" && direct.trim()) {
|
|
73
|
-
return direct;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const response = err.response;
|
|
77
|
-
if (!isRecord(response)) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const body = response.body;
|
|
82
|
-
if (isRecord(body)) {
|
|
83
|
-
const error = body.error;
|
|
84
|
-
if (isRecord(error) && typeof error.code === "string" && error.code.trim()) {
|
|
85
|
-
return error.code;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function extractRetryAfterMs(err: unknown): number | null {
|
|
93
|
-
if (!isRecord(err)) {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const direct = err.retryAfterMs ?? err.retry_after_ms;
|
|
98
|
-
if (typeof direct === "number" && Number.isFinite(direct) && direct >= 0) {
|
|
99
|
-
return direct;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const retryAfter = err.retryAfter ?? err.retry_after;
|
|
103
|
-
if (typeof retryAfter === "number" && Number.isFinite(retryAfter)) {
|
|
104
|
-
return retryAfter >= 0 ? retryAfter * 1000 : null;
|
|
105
|
-
}
|
|
106
|
-
if (typeof retryAfter === "string") {
|
|
107
|
-
const parsed = Number.parseFloat(retryAfter);
|
|
108
|
-
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
109
|
-
return parsed * 1000;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const response = err.response;
|
|
114
|
-
if (!isRecord(response)) {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const headers = response.headers;
|
|
119
|
-
if (!headers) {
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (isRecord(headers)) {
|
|
124
|
-
const raw = headers["retry-after"] ?? headers["Retry-After"];
|
|
125
|
-
if (typeof raw === "string") {
|
|
126
|
-
const parsed = Number.parseFloat(raw);
|
|
127
|
-
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
128
|
-
return parsed * 1000;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Fetch Headers-like interface
|
|
134
|
-
if (
|
|
135
|
-
typeof headers === "object" &&
|
|
136
|
-
headers !== null &&
|
|
137
|
-
"get" in headers &&
|
|
138
|
-
typeof (headers as { get?: unknown }).get === "function"
|
|
139
|
-
) {
|
|
140
|
-
const raw = (headers as { get: (name: string) => string | null }).get("retry-after");
|
|
141
|
-
if (raw) {
|
|
142
|
-
const parsed = Number.parseFloat(raw);
|
|
143
|
-
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
144
|
-
return parsed * 1000;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
type MSTeamsSendErrorKind = "auth" | "throttled" | "transient" | "permanent" | "unknown";
|
|
153
|
-
|
|
154
|
-
type MSTeamsSendErrorClassification = {
|
|
155
|
-
kind: MSTeamsSendErrorKind;
|
|
156
|
-
statusCode?: number;
|
|
157
|
-
retryAfterMs?: number;
|
|
158
|
-
errorCode?: string;
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Classify outbound send errors for safe retries and actionable logs.
|
|
163
|
-
*
|
|
164
|
-
* Important: We only mark errors as retryable when we have an explicit HTTP
|
|
165
|
-
* status code that indicates the message was not accepted (e.g. 429, 5xx).
|
|
166
|
-
* For transport-level errors where delivery is ambiguous, we prefer to avoid
|
|
167
|
-
* retries to reduce the chance of duplicate posts.
|
|
168
|
-
*/
|
|
169
|
-
export function classifyMSTeamsSendError(err: unknown): MSTeamsSendErrorClassification {
|
|
170
|
-
const statusCode = extractStatusCode(err);
|
|
171
|
-
const retryAfterMs = extractRetryAfterMs(err);
|
|
172
|
-
const errorCode = extractErrorCode(err) ?? undefined;
|
|
173
|
-
|
|
174
|
-
if (statusCode === 401) {
|
|
175
|
-
return { kind: "auth", statusCode, errorCode };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (statusCode === 403) {
|
|
179
|
-
if (errorCode === "ContentStreamNotAllowed") {
|
|
180
|
-
return { kind: "permanent", statusCode, errorCode };
|
|
181
|
-
}
|
|
182
|
-
return { kind: "auth", statusCode, errorCode };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (statusCode === 429) {
|
|
186
|
-
return {
|
|
187
|
-
kind: "throttled",
|
|
188
|
-
statusCode,
|
|
189
|
-
retryAfterMs: retryAfterMs ?? undefined,
|
|
190
|
-
errorCode,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (statusCode === 408 || (statusCode != null && statusCode >= 500)) {
|
|
195
|
-
return {
|
|
196
|
-
kind: "transient",
|
|
197
|
-
statusCode,
|
|
198
|
-
retryAfterMs: retryAfterMs ?? undefined,
|
|
199
|
-
errorCode,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (statusCode != null && statusCode >= 400) {
|
|
204
|
-
return { kind: "permanent", statusCode, errorCode };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return {
|
|
208
|
-
kind: "unknown",
|
|
209
|
-
statusCode: statusCode ?? undefined,
|
|
210
|
-
retryAfterMs: retryAfterMs ?? undefined,
|
|
211
|
-
errorCode,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Detect whether an error is caused by a revoked Proxy.
|
|
217
|
-
*
|
|
218
|
-
* The Bot Framework SDK wraps TurnContext in a Proxy that is revoked once the
|
|
219
|
-
* turn handler returns. Any later access (e.g. from a debounced callback)
|
|
220
|
-
* throws a TypeError whose message contains the distinctive "proxy that has
|
|
221
|
-
* been revoked" string.
|
|
222
|
-
*/
|
|
223
|
-
export function isRevokedProxyError(err: unknown): boolean {
|
|
224
|
-
if (!(err instanceof TypeError)) {
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
return /proxy that has been revoked/i.test(err.message);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export function formatMSTeamsSendErrorHint(
|
|
231
|
-
classification: MSTeamsSendErrorClassification,
|
|
232
|
-
): string | undefined {
|
|
233
|
-
if (classification.kind === "auth") {
|
|
234
|
-
return "check msteams appId/appPassword/tenantId (or env vars MSTEAMS_APP_ID/MSTEAMS_APP_PASSWORD/MSTEAMS_TENANT_ID)";
|
|
235
|
-
}
|
|
236
|
-
if (classification.errorCode === "ContentStreamNotAllowed") {
|
|
237
|
-
return "Teams expired the content stream; stop streaming earlier and fall back to normal message delivery";
|
|
238
|
-
}
|
|
239
|
-
if (classification.kind === "throttled") {
|
|
240
|
-
return "Teams throttled the bot; backing off may help";
|
|
241
|
-
}
|
|
242
|
-
if (classification.kind === "transient") {
|
|
243
|
-
return "transient Teams/Bot Framework error; retry may succeed";
|
|
244
|
-
}
|
|
245
|
-
return undefined;
|
|
246
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
|
|
2
|
-
|
|
3
|
-
/** Max chars of the thumbed-down response to include in the reflection prompt. */
|
|
4
|
-
const MAX_RESPONSE_CHARS = 500;
|
|
5
|
-
|
|
6
|
-
type ParsedReflectionResponse = {
|
|
7
|
-
learning: string;
|
|
8
|
-
followUp: boolean;
|
|
9
|
-
userMessage?: string;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export function buildReflectionPrompt(params: {
|
|
13
|
-
thumbedDownResponse?: string;
|
|
14
|
-
userComment?: string;
|
|
15
|
-
}): string {
|
|
16
|
-
const parts: string[] = ["A user indicated your previous response wasn't helpful."];
|
|
17
|
-
|
|
18
|
-
if (params.thumbedDownResponse) {
|
|
19
|
-
const truncated =
|
|
20
|
-
params.thumbedDownResponse.length > MAX_RESPONSE_CHARS
|
|
21
|
-
? `${params.thumbedDownResponse.slice(0, MAX_RESPONSE_CHARS)}...`
|
|
22
|
-
: params.thumbedDownResponse;
|
|
23
|
-
parts.push(`\nYour response was:\n> ${truncated}`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (params.userComment) {
|
|
27
|
-
parts.push(`\nUser's comment: "${params.userComment}"`);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
parts.push(
|
|
31
|
-
"\nBriefly reflect: what could you improve? Consider tone, length, " +
|
|
32
|
-
"accuracy, relevance, and specificity. Reply with a single JSON object " +
|
|
33
|
-
'only, no markdown or prose, using this exact shape:\n{"learning":"...",' +
|
|
34
|
-
'"followUp":false,"userMessage":""}\n' +
|
|
35
|
-
"- learning: a short internal adjustment note (1-2 sentences) for your " +
|
|
36
|
-
"future behavior in this conversation.\n" +
|
|
37
|
-
"- followUp: true only if the user needs a direct follow-up message.\n" +
|
|
38
|
-
"- userMessage: only the exact user-facing message to send; empty string " +
|
|
39
|
-
"when followUp is false.",
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
return parts.join("\n");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function parseBooleanLike(value: unknown): boolean | undefined {
|
|
46
|
-
if (typeof value === "boolean") {
|
|
47
|
-
return value;
|
|
48
|
-
}
|
|
49
|
-
if (typeof value === "string") {
|
|
50
|
-
const normalized = normalizeOptionalLowercaseString(value);
|
|
51
|
-
if (normalized === "true" || normalized === "yes") {
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
if (normalized === "false" || normalized === "no") {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function parseStructuredReflectionValue(value: unknown): ParsedReflectionResponse | null {
|
|
62
|
-
if (value == null || typeof value !== "object" || Array.isArray(value)) {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const candidate = value as {
|
|
67
|
-
learning?: unknown;
|
|
68
|
-
followUp?: unknown;
|
|
69
|
-
userMessage?: unknown;
|
|
70
|
-
};
|
|
71
|
-
const learning = typeof candidate.learning === "string" ? candidate.learning.trim() : undefined;
|
|
72
|
-
if (!learning) {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
learning,
|
|
78
|
-
followUp: parseBooleanLike(candidate.followUp) ?? false,
|
|
79
|
-
userMessage:
|
|
80
|
-
typeof candidate.userMessage === "string" && candidate.userMessage.trim()
|
|
81
|
-
? candidate.userMessage.trim()
|
|
82
|
-
: undefined,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function parseReflectionResponse(text: string): ParsedReflectionResponse | null {
|
|
87
|
-
const trimmed = text.trim();
|
|
88
|
-
if (!trimmed) {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const candidates = [
|
|
93
|
-
trimmed,
|
|
94
|
-
...(trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i)?.slice(1, 2) ?? []),
|
|
95
|
-
];
|
|
96
|
-
|
|
97
|
-
for (const candidateText of candidates) {
|
|
98
|
-
const candidate = candidateText.trim();
|
|
99
|
-
if (!candidate) {
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
try {
|
|
103
|
-
const parsed = parseStructuredReflectionValue(JSON.parse(candidate));
|
|
104
|
-
if (parsed) {
|
|
105
|
-
return parsed;
|
|
106
|
-
}
|
|
107
|
-
} catch {
|
|
108
|
-
// Fall through to the next parse strategy.
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Safe fallback: keep the internal learning, but never auto-message the user.
|
|
113
|
-
return {
|
|
114
|
-
learning: trimmed,
|
|
115
|
-
followUp: false,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
/** Default cooldown between reflections per session (5 minutes). */
|
|
5
|
-
export const DEFAULT_COOLDOWN_MS = 300_000;
|
|
6
|
-
|
|
7
|
-
/** Tracks last reflection time per session to enforce cooldown. */
|
|
8
|
-
const lastReflectionBySession = new Map<string, number>();
|
|
9
|
-
|
|
10
|
-
/** Maximum cooldown entries before pruning expired ones. */
|
|
11
|
-
const MAX_COOLDOWN_ENTRIES = 500;
|
|
12
|
-
|
|
13
|
-
function legacySanitizeSessionKey(sessionKey: string): string {
|
|
14
|
-
return sessionKey.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function encodeSessionKey(sessionKey: string): string {
|
|
18
|
-
return Buffer.from(sessionKey, "utf8").toString("base64url");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function resolveLearningsFilePath(storePath: string, sessionKey: string): string {
|
|
22
|
-
return `${storePath}/${encodeSessionKey(sessionKey)}.learnings.json`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function resolveLegacyLearningsFilePath(storePath: string, sessionKey: string): string {
|
|
26
|
-
return `${storePath}/${legacySanitizeSessionKey(sessionKey)}.learnings.json`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function readLearningsFile(
|
|
30
|
-
filePath: string,
|
|
31
|
-
): Promise<{ exists: boolean; learnings: string[] }> {
|
|
32
|
-
try {
|
|
33
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
34
|
-
const parsed = JSON.parse(content);
|
|
35
|
-
return { exists: true, learnings: Array.isArray(parsed) ? parsed : [] };
|
|
36
|
-
} catch {
|
|
37
|
-
return { exists: false, learnings: [] };
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Prune expired cooldown entries to prevent unbounded memory growth. */
|
|
42
|
-
function pruneExpiredCooldowns(cooldownMs: number): void {
|
|
43
|
-
if (lastReflectionBySession.size <= MAX_COOLDOWN_ENTRIES) {
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
const now = Date.now();
|
|
47
|
-
for (const [key, time] of lastReflectionBySession) {
|
|
48
|
-
if (now - time >= cooldownMs) {
|
|
49
|
-
lastReflectionBySession.delete(key);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Check if a reflection is allowed (cooldown not active). */
|
|
55
|
-
export function isReflectionAllowed(sessionKey: string, cooldownMs?: number): boolean {
|
|
56
|
-
const cooldown = cooldownMs ?? DEFAULT_COOLDOWN_MS;
|
|
57
|
-
const lastTime = lastReflectionBySession.get(sessionKey);
|
|
58
|
-
if (lastTime == null) {
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
return Date.now() - lastTime >= cooldown;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Record that a reflection was run for a session. */
|
|
65
|
-
export function recordReflectionTime(sessionKey: string, cooldownMs?: number): void {
|
|
66
|
-
lastReflectionBySession.set(sessionKey, Date.now());
|
|
67
|
-
pruneExpiredCooldowns(cooldownMs ?? DEFAULT_COOLDOWN_MS);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Clear reflection cooldown tracking (for tests). */
|
|
71
|
-
export function clearReflectionCooldowns(): void {
|
|
72
|
-
lastReflectionBySession.clear();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Store a learning derived from feedback reflection in a session companion file. */
|
|
76
|
-
export async function storeSessionLearning(params: {
|
|
77
|
-
storePath: string;
|
|
78
|
-
sessionKey: string;
|
|
79
|
-
learning: string;
|
|
80
|
-
}): Promise<void> {
|
|
81
|
-
const learningsFile = resolveLearningsFilePath(params.storePath, params.sessionKey);
|
|
82
|
-
const legacyLearningsFile = resolveLegacyLearningsFilePath(params.storePath, params.sessionKey);
|
|
83
|
-
const { exists, learnings: existingLearnings } = await readLearningsFile(learningsFile);
|
|
84
|
-
const { learnings: legacyLearnings } =
|
|
85
|
-
exists || legacyLearningsFile === learningsFile
|
|
86
|
-
? { learnings: [] as string[] }
|
|
87
|
-
: await readLearningsFile(legacyLearningsFile);
|
|
88
|
-
|
|
89
|
-
let learnings = exists ? existingLearnings : legacyLearnings;
|
|
90
|
-
|
|
91
|
-
learnings.push(params.learning);
|
|
92
|
-
if (learnings.length > 10) {
|
|
93
|
-
learnings = learnings.slice(-10);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
await fs.mkdir(path.dirname(learningsFile), { recursive: true });
|
|
97
|
-
await fs.writeFile(learningsFile, JSON.stringify(learnings, null, 2), "utf-8");
|
|
98
|
-
if (!exists && legacyLearningsFile !== learningsFile) {
|
|
99
|
-
await fs.rm(legacyLearningsFile, { force: true }).catch(() => undefined);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Load session learnings for injection into extraSystemPrompt. */
|
|
104
|
-
export async function loadSessionLearnings(
|
|
105
|
-
storePath: string,
|
|
106
|
-
sessionKey: string,
|
|
107
|
-
): Promise<string[]> {
|
|
108
|
-
const learningsFile = resolveLearningsFilePath(storePath, sessionKey);
|
|
109
|
-
const { exists, learnings } = await readLearningsFile(learningsFile);
|
|
110
|
-
if (exists) {
|
|
111
|
-
return learnings;
|
|
112
|
-
}
|
|
113
|
-
return (await readLearningsFile(resolveLegacyLearningsFilePath(storePath, sessionKey))).learnings;
|
|
114
|
-
}
|