aamp-openclaw-plugin 0.1.29 → 0.1.31
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/README.md +8 -0
- package/dist/index.js +1001 -129
- package/dist/index.js.map +4 -4
- package/openclaw.plugin.json +6 -0
- package/package.json +16 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// ../sdk/src/jmap-push.
|
|
1
|
+
// ../sdk/src/jmap-push.js
|
|
2
2
|
import WebSocket from "ws";
|
|
3
3
|
|
|
4
|
-
// ../sdk/src/types.
|
|
5
|
-
var AAMP_PROTOCOL_VERSION = "1.
|
|
4
|
+
// ../sdk/src/types.js
|
|
5
|
+
var AAMP_PROTOCOL_VERSION = "1.1";
|
|
6
6
|
var AAMP_HEADER = {
|
|
7
7
|
VERSION: "X-AAMP-Version",
|
|
8
8
|
INTENT: "X-AAMP-Intent",
|
|
@@ -18,11 +18,12 @@ var AAMP_HEADER = {
|
|
|
18
18
|
QUESTION: "X-AAMP-Question",
|
|
19
19
|
BLOCKED_REASON: "X-AAMP-BlockedReason",
|
|
20
20
|
SUGGESTED_OPTIONS: "X-AAMP-SuggestedOptions",
|
|
21
|
+
STREAM_ID: "X-AAMP-Stream-Id",
|
|
21
22
|
PARENT_TASK_ID: "X-AAMP-ParentTaskId",
|
|
22
23
|
CARD_SUMMARY: "X-AAMP-Card-Summary"
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
// ../sdk/src/parser.
|
|
26
|
+
// ../sdk/src/parser.js
|
|
26
27
|
function normalizeBodyText(value) {
|
|
27
28
|
return value?.replace(/\r\n/g, "\n").trim() ?? "";
|
|
28
29
|
}
|
|
@@ -33,10 +34,7 @@ function extractBodySection(bodyText, label, nextLabels) {
|
|
|
33
34
|
if (!bodyText)
|
|
34
35
|
return "";
|
|
35
36
|
const nextPattern = nextLabels.length ? `(?=\\n(?:${nextLabels.map(escapeRegex).join("|")}):|$)` : "$";
|
|
36
|
-
const pattern = new RegExp(
|
|
37
|
-
`(?:^|\\n)${escapeRegex(label)}:\\s*([\\s\\S]*?)${nextPattern}`,
|
|
38
|
-
"i"
|
|
39
|
-
);
|
|
37
|
+
const pattern = new RegExp(`(?:^|\\n)${escapeRegex(label)}:\\s*([\\s\\S]*?)${nextPattern}`, "i");
|
|
40
38
|
const match = pattern.exec(bodyText);
|
|
41
39
|
return match?.[1]?.trim() ?? "";
|
|
42
40
|
}
|
|
@@ -63,9 +61,7 @@ function parseTaskHelpBody(bodyText) {
|
|
|
63
61
|
}
|
|
64
62
|
const question = extractBodySection(normalized, "Question", ["Blocked reason", "Suggested options"]);
|
|
65
63
|
const blockedReason = extractBodySection(normalized, "Blocked reason", ["Suggested options"]);
|
|
66
|
-
const suggestedOptions = parseSuggestedOptionsBlock(
|
|
67
|
-
extractBodySection(normalized, "Suggested options", [])
|
|
68
|
-
);
|
|
64
|
+
const suggestedOptions = parseSuggestedOptionsBlock(extractBodySection(normalized, "Suggested options", []));
|
|
69
65
|
if (question || blockedReason || suggestedOptions.length) {
|
|
70
66
|
return { question, blockedReason, suggestedOptions };
|
|
71
67
|
}
|
|
@@ -83,10 +79,7 @@ function decodeMimeEncodedWordSegment(segment) {
|
|
|
83
79
|
const buf = Buffer.from(body, "base64");
|
|
84
80
|
return buf.toString(charset === "utf-8" || charset === "utf8" ? "utf8" : "utf8");
|
|
85
81
|
}
|
|
86
|
-
const normalized = body.replace(/_/g, " ").replace(
|
|
87
|
-
/=([0-9A-Fa-f]{2})/g,
|
|
88
|
-
(_, hex) => String.fromCharCode(parseInt(hex, 16))
|
|
89
|
-
);
|
|
82
|
+
const normalized = body.replace(/_/g, " ").replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
90
83
|
const bytes = Buffer.from(normalized, "binary");
|
|
91
84
|
return bytes.toString(charset === "utf-8" || charset === "utf8" ? "utf8" : "utf8");
|
|
92
85
|
} catch {
|
|
@@ -97,19 +90,14 @@ function decodeMimeEncodedWords(value) {
|
|
|
97
90
|
if (!value || !value.includes("=?"))
|
|
98
91
|
return value ?? "";
|
|
99
92
|
const collapsed = value.replace(/\r?\n[ \t]+/g, " ");
|
|
100
|
-
const decoded = collapsed.replace(
|
|
101
|
-
/=\?[^?]+\?[bBqQ]\?[^?]*\?=/g,
|
|
102
|
-
(segment) => decodeMimeEncodedWordSegment(segment)
|
|
103
|
-
);
|
|
93
|
+
const decoded = collapsed.replace(/=\?[^?]+\?[bBqQ]\?[^?]*\?=/g, (segment) => decodeMimeEncodedWordSegment(segment));
|
|
104
94
|
return decoded.replace(/\s{2,}/g, " ").trim();
|
|
105
95
|
}
|
|
106
96
|
function normalizeHeaders(headers) {
|
|
107
|
-
return Object.fromEntries(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
])
|
|
112
|
-
);
|
|
97
|
+
return Object.fromEntries(Object.entries(headers).map(([k, v]) => [
|
|
98
|
+
k.toLowerCase(),
|
|
99
|
+
Array.isArray(v) ? v[0] : v
|
|
100
|
+
]));
|
|
113
101
|
}
|
|
114
102
|
function getAampHeader(headers, headerName) {
|
|
115
103
|
return headers[headerName.toLowerCase()];
|
|
@@ -182,9 +170,7 @@ function parseAampHeaders(meta) {
|
|
|
182
170
|
const decodedSubject = decodeMimeEncodedWords(meta.subject);
|
|
183
171
|
if (intent === "task.dispatch") {
|
|
184
172
|
const contextLinksStr = getAampHeader(headers, AAMP_HEADER.CONTEXT_LINKS) ?? "";
|
|
185
|
-
const dispatchContext = parseDispatchContextHeader(
|
|
186
|
-
getAampHeader(headers, AAMP_HEADER.DISPATCH_CONTEXT)
|
|
187
|
-
);
|
|
173
|
+
const dispatchContext = parseDispatchContextHeader(getAampHeader(headers, AAMP_HEADER.DISPATCH_CONTEXT));
|
|
188
174
|
const parentTaskId = getAampHeader(headers, AAMP_HEADER.PARENT_TASK_ID);
|
|
189
175
|
const priority = getAampHeader(headers, AAMP_HEADER.PRIORITY) ?? "normal";
|
|
190
176
|
const expiresAt = getAampHeader(headers, AAMP_HEADER.EXPIRES_AT);
|
|
@@ -225,9 +211,7 @@ function parseAampHeaders(meta) {
|
|
|
225
211
|
const status = getAampHeader(headers, AAMP_HEADER.STATUS) ?? "completed";
|
|
226
212
|
const output = getAampHeader(headers, AAMP_HEADER.OUTPUT) ?? parsedBody.output;
|
|
227
213
|
const errorMsg = getAampHeader(headers, AAMP_HEADER.ERROR_MSG) ?? parsedBody.errorMsg;
|
|
228
|
-
const structuredResult = decodeStructuredResult(
|
|
229
|
-
getAampHeader(headers, AAMP_HEADER.STRUCTURED_RESULT)
|
|
230
|
-
);
|
|
214
|
+
const structuredResult = decodeStructuredResult(getAampHeader(headers, AAMP_HEADER.STRUCTURED_RESULT));
|
|
231
215
|
const result = {
|
|
232
216
|
protocolVersion,
|
|
233
217
|
intent: "task.result",
|
|
@@ -271,6 +255,21 @@ function parseAampHeaders(meta) {
|
|
|
271
255
|
};
|
|
272
256
|
return ack;
|
|
273
257
|
}
|
|
258
|
+
if (intent === "task.stream.opened") {
|
|
259
|
+
const streamId = getAampHeader(headers, AAMP_HEADER.STREAM_ID) ?? "";
|
|
260
|
+
if (!streamId)
|
|
261
|
+
return null;
|
|
262
|
+
const streamOpened = {
|
|
263
|
+
protocolVersion,
|
|
264
|
+
intent: "task.stream.opened",
|
|
265
|
+
taskId,
|
|
266
|
+
streamId,
|
|
267
|
+
from,
|
|
268
|
+
to,
|
|
269
|
+
messageId: meta.messageId
|
|
270
|
+
};
|
|
271
|
+
return streamOpened;
|
|
272
|
+
}
|
|
274
273
|
if (intent === "card.query") {
|
|
275
274
|
const cardQuery = {
|
|
276
275
|
protocolVersion,
|
|
@@ -337,6 +336,14 @@ function buildAckHeaders(opts) {
|
|
|
337
336
|
[AAMP_HEADER.TASK_ID]: opts.taskId
|
|
338
337
|
};
|
|
339
338
|
}
|
|
339
|
+
function buildStreamOpenedHeaders(opts) {
|
|
340
|
+
return {
|
|
341
|
+
[AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
|
|
342
|
+
[AAMP_HEADER.INTENT]: "task.stream.opened",
|
|
343
|
+
[AAMP_HEADER.TASK_ID]: opts.taskId,
|
|
344
|
+
[AAMP_HEADER.STREAM_ID]: opts.streamId
|
|
345
|
+
};
|
|
346
|
+
}
|
|
340
347
|
function buildResultHeaders(params) {
|
|
341
348
|
const headers = {
|
|
342
349
|
[AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
|
|
@@ -374,7 +381,7 @@ function buildCardResponseHeaders(params) {
|
|
|
374
381
|
};
|
|
375
382
|
}
|
|
376
383
|
|
|
377
|
-
// ../sdk/src/tiny-emitter.
|
|
384
|
+
// ../sdk/src/tiny-emitter.js
|
|
378
385
|
var TinyEmitter = class {
|
|
379
386
|
listeners = /* @__PURE__ */ new Map();
|
|
380
387
|
onceWrappers = /* @__PURE__ */ new WeakMap();
|
|
@@ -416,7 +423,7 @@ var TinyEmitter = class {
|
|
|
416
423
|
}
|
|
417
424
|
};
|
|
418
425
|
|
|
419
|
-
// ../sdk/src/jmap-push.
|
|
426
|
+
// ../sdk/src/jmap-push.js
|
|
420
427
|
function describeError(err) {
|
|
421
428
|
if (!(err instanceof Error))
|
|
422
429
|
return String(err);
|
|
@@ -675,6 +682,9 @@ var JmapPushClient = class extends TinyEmitter {
|
|
|
675
682
|
case "task.ack":
|
|
676
683
|
this.emit("task.ack", aampMsg);
|
|
677
684
|
break;
|
|
685
|
+
case "task.stream.opened":
|
|
686
|
+
this.emit("task.stream.opened", aampMsg);
|
|
687
|
+
break;
|
|
678
688
|
case "card.query":
|
|
679
689
|
this.emit("card.query", aampMsg);
|
|
680
690
|
break;
|
|
@@ -773,12 +783,7 @@ var JmapPushClient = class extends TinyEmitter {
|
|
|
773
783
|
this.connecting = false;
|
|
774
784
|
const headerSummary = Object.entries(res.headers).map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(", ") : value ?? ""}`).join("; ");
|
|
775
785
|
this.startPolling(`websocket handshake failed: ${res.statusCode ?? "unknown"}`);
|
|
776
|
-
this.emit(
|
|
777
|
-
"error",
|
|
778
|
-
new Error(
|
|
779
|
-
`JMAP WebSocket handshake failed: ${res.statusCode ?? "unknown"} ${res.statusMessage ?? ""}${headerSummary ? ` | headers: ${headerSummary}` : ""}`
|
|
780
|
-
)
|
|
781
|
-
);
|
|
786
|
+
this.emit("error", new Error(`JMAP WebSocket handshake failed: ${res.statusCode ?? "unknown"} ${res.statusMessage ?? ""}${headerSummary ? ` | headers: ${headerSummary}` : ""}`));
|
|
782
787
|
this.scheduleReconnect();
|
|
783
788
|
});
|
|
784
789
|
this.ws.on("open", async () => {
|
|
@@ -790,13 +795,11 @@ var JmapPushClient = class extends TinyEmitter {
|
|
|
790
795
|
if (accountId && this.emailState === null) {
|
|
791
796
|
await this.initEmailState(accountId);
|
|
792
797
|
}
|
|
793
|
-
this.ws.send(
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
})
|
|
799
|
-
);
|
|
798
|
+
this.ws.send(JSON.stringify({
|
|
799
|
+
"@type": "WebSocketPushEnable",
|
|
800
|
+
dataTypes: ["Email"],
|
|
801
|
+
pushState: null
|
|
802
|
+
}));
|
|
800
803
|
this.emit("connected");
|
|
801
804
|
});
|
|
802
805
|
this.ws.on("pong", () => {
|
|
@@ -985,23 +988,15 @@ var JmapPushClient = class extends TinyEmitter {
|
|
|
985
988
|
return Buffer.from(arrayBuffer);
|
|
986
989
|
}
|
|
987
990
|
if (attempt < maxAttempts && (res.status === 404 || res.status === 429 || res.status === 503)) {
|
|
988
|
-
console.warn(
|
|
989
|
-
`[AAMP-SDK] blob download retry status=${res.status} attempt=${attempt}/${maxAttempts} url=${downloadUrl}`
|
|
990
|
-
);
|
|
991
|
+
console.warn(`[AAMP-SDK] blob download retry status=${res.status} attempt=${attempt}/${maxAttempts} url=${downloadUrl}`);
|
|
991
992
|
const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 15e3);
|
|
992
993
|
await new Promise((r) => setTimeout(r, delay));
|
|
993
994
|
continue;
|
|
994
995
|
}
|
|
995
|
-
console.error(
|
|
996
|
-
|
|
997
|
-
);
|
|
998
|
-
throw new Error(
|
|
999
|
-
`Blob download failed: status=${res.status} attempt=${attempt}/${maxAttempts} blobId=${blobId} filename=${filename ?? "attachment"} url=${downloadUrl}`
|
|
1000
|
-
);
|
|
996
|
+
console.error(`[AAMP-SDK] blob download failed status=${res.status} attempt=${attempt}/${maxAttempts} url=${downloadUrl}`);
|
|
997
|
+
throw new Error(`Blob download failed: status=${res.status} attempt=${attempt}/${maxAttempts} blobId=${blobId} filename=${filename ?? "attachment"} url=${downloadUrl}`);
|
|
1001
998
|
}
|
|
1002
|
-
throw new Error(
|
|
1003
|
-
`Blob download failed after retries: status=${lastStatus ?? "unknown"} attempt=${maxAttempts}/${maxAttempts} blobId=${blobId} filename=${filename ?? "attachment"} url=${downloadUrl}`
|
|
1004
|
-
);
|
|
999
|
+
throw new Error(`Blob download failed after retries: status=${lastStatus ?? "unknown"} attempt=${maxAttempts}/${maxAttempts} blobId=${blobId} filename=${filename ?? "attachment"} url=${downloadUrl}`);
|
|
1005
1000
|
}
|
|
1006
1001
|
/**
|
|
1007
1002
|
* Actively reconcile recent mailbox contents via JMAP HTTP.
|
|
@@ -1063,7 +1058,7 @@ var JmapPushClient = class extends TinyEmitter {
|
|
|
1063
1058
|
}
|
|
1064
1059
|
};
|
|
1065
1060
|
|
|
1066
|
-
// ../sdk/src/smtp-sender.
|
|
1061
|
+
// ../sdk/src/smtp-sender.js
|
|
1067
1062
|
import { createTransport } from "nodemailer";
|
|
1068
1063
|
import { randomUUID } from "crypto";
|
|
1069
1064
|
var sanitize = (s) => s.replace(/[\r\n]/g, " ").trim();
|
|
@@ -1077,23 +1072,11 @@ function deriveMailboxServiceDefaults(email, baseUrl2) {
|
|
|
1077
1072
|
};
|
|
1078
1073
|
}
|
|
1079
1074
|
var SmtpSender = class _SmtpSender {
|
|
1080
|
-
|
|
1081
|
-
this.config = config;
|
|
1082
|
-
this.transport = createTransport({
|
|
1083
|
-
host: config.host,
|
|
1084
|
-
port: config.port,
|
|
1085
|
-
secure: config.secure ?? false,
|
|
1086
|
-
auth: {
|
|
1087
|
-
user: config.user,
|
|
1088
|
-
pass: config.password
|
|
1089
|
-
},
|
|
1090
|
-
tls: {
|
|
1091
|
-
rejectUnauthorized: config.rejectUnauthorized ?? true
|
|
1092
|
-
}
|
|
1093
|
-
});
|
|
1094
|
-
}
|
|
1075
|
+
config;
|
|
1095
1076
|
transport;
|
|
1096
1077
|
discoveredApiUrlPromise = null;
|
|
1078
|
+
jmapSessionPromise = null;
|
|
1079
|
+
sentMailboxIdPromise = null;
|
|
1097
1080
|
static fromMailboxIdentity(config) {
|
|
1098
1081
|
const derived = deriveMailboxServiceDefaults(config.email, config.baseUrl);
|
|
1099
1082
|
return new _SmtpSender({
|
|
@@ -1107,6 +1090,21 @@ var SmtpSender = class _SmtpSender {
|
|
|
1107
1090
|
rejectUnauthorized: config.rejectUnauthorized
|
|
1108
1091
|
});
|
|
1109
1092
|
}
|
|
1093
|
+
constructor(config) {
|
|
1094
|
+
this.config = config;
|
|
1095
|
+
this.transport = createTransport({
|
|
1096
|
+
host: config.host,
|
|
1097
|
+
port: config.port,
|
|
1098
|
+
secure: config.secure ?? false,
|
|
1099
|
+
auth: {
|
|
1100
|
+
user: config.user,
|
|
1101
|
+
pass: config.password
|
|
1102
|
+
},
|
|
1103
|
+
tls: {
|
|
1104
|
+
rejectUnauthorized: config.rejectUnauthorized ?? true
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1110
1108
|
senderDomain() {
|
|
1111
1109
|
return this.config.user.split("@")[1]?.toLowerCase() ?? "";
|
|
1112
1110
|
}
|
|
@@ -1114,9 +1112,7 @@ var SmtpSender = class _SmtpSender {
|
|
|
1114
1112
|
return email.split("@")[1]?.toLowerCase() ?? "";
|
|
1115
1113
|
}
|
|
1116
1114
|
shouldUseHttpFallback(to) {
|
|
1117
|
-
return Boolean(
|
|
1118
|
-
this.config.httpBaseUrl && this.config.authToken && this.senderDomain() && this.senderDomain() === this.recipientDomain(to)
|
|
1119
|
-
);
|
|
1115
|
+
return Boolean(this.config.httpBaseUrl && this.config.authToken && this.senderDomain() && this.senderDomain() === this.recipientDomain(to));
|
|
1120
1116
|
}
|
|
1121
1117
|
async resolveAampApiUrl() {
|
|
1122
1118
|
const base = this.config.httpBaseUrl?.replace(/\/$/, "");
|
|
@@ -1173,6 +1169,134 @@ var SmtpSender = class _SmtpSender {
|
|
|
1173
1169
|
}
|
|
1174
1170
|
return { messageId: data.messageId };
|
|
1175
1171
|
}
|
|
1172
|
+
canPersistSentCopy() {
|
|
1173
|
+
return Boolean(this.config.httpBaseUrl && this.config.authToken);
|
|
1174
|
+
}
|
|
1175
|
+
getJmapAuthHeader() {
|
|
1176
|
+
if (!this.config.authToken) {
|
|
1177
|
+
throw new Error("JMAP auth token is not configured");
|
|
1178
|
+
}
|
|
1179
|
+
return `Basic ${this.config.authToken}`;
|
|
1180
|
+
}
|
|
1181
|
+
async resolveJmapSession() {
|
|
1182
|
+
const base = this.config.httpBaseUrl?.replace(/\/$/, "");
|
|
1183
|
+
if (!base) {
|
|
1184
|
+
throw new Error("JMAP base URL is not configured");
|
|
1185
|
+
}
|
|
1186
|
+
if (!this.jmapSessionPromise) {
|
|
1187
|
+
this.jmapSessionPromise = (async () => {
|
|
1188
|
+
const res = await fetch(`${base}/.well-known/jmap`, {
|
|
1189
|
+
headers: { Authorization: this.getJmapAuthHeader() }
|
|
1190
|
+
});
|
|
1191
|
+
if (!res.ok) {
|
|
1192
|
+
throw new Error(`JMAP session failed: ${res.status} ${res.statusText}`);
|
|
1193
|
+
}
|
|
1194
|
+
const session = await res.json();
|
|
1195
|
+
const accountId = session.primaryAccounts?.["urn:ietf:params:jmap:mail"] ?? Object.keys(session.accounts ?? {})[0];
|
|
1196
|
+
if (!accountId) {
|
|
1197
|
+
throw new Error("No JMAP mail account available");
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
accountId,
|
|
1201
|
+
apiUrl: `${base}/jmap/`
|
|
1202
|
+
};
|
|
1203
|
+
})();
|
|
1204
|
+
}
|
|
1205
|
+
try {
|
|
1206
|
+
return await this.jmapSessionPromise;
|
|
1207
|
+
} catch (err) {
|
|
1208
|
+
this.jmapSessionPromise = null;
|
|
1209
|
+
throw err;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
async jmapCall(methodCalls) {
|
|
1213
|
+
const session = await this.resolveJmapSession();
|
|
1214
|
+
const res = await fetch(session.apiUrl, {
|
|
1215
|
+
method: "POST",
|
|
1216
|
+
headers: {
|
|
1217
|
+
Authorization: this.getJmapAuthHeader(),
|
|
1218
|
+
"Content-Type": "application/json"
|
|
1219
|
+
},
|
|
1220
|
+
body: JSON.stringify({
|
|
1221
|
+
using: [
|
|
1222
|
+
"urn:ietf:params:jmap:core",
|
|
1223
|
+
"urn:ietf:params:jmap:mail"
|
|
1224
|
+
],
|
|
1225
|
+
methodCalls: methodCalls.map(([name, args, tag]) => [
|
|
1226
|
+
name,
|
|
1227
|
+
{ accountId: session.accountId, ...args },
|
|
1228
|
+
tag
|
|
1229
|
+
])
|
|
1230
|
+
})
|
|
1231
|
+
});
|
|
1232
|
+
if (!res.ok) {
|
|
1233
|
+
throw new Error(`JMAP API call failed: ${res.status}`);
|
|
1234
|
+
}
|
|
1235
|
+
const data = await res.json();
|
|
1236
|
+
return data.methodResponses ?? [];
|
|
1237
|
+
}
|
|
1238
|
+
async getSentMailboxId() {
|
|
1239
|
+
if (!this.sentMailboxIdPromise) {
|
|
1240
|
+
this.sentMailboxIdPromise = (async () => {
|
|
1241
|
+
const responses = await this.jmapCall([
|
|
1242
|
+
["Mailbox/get", { ids: null }, "mb1"]
|
|
1243
|
+
]);
|
|
1244
|
+
const result = responses.find(([name]) => name === "Mailbox/get")?.[1];
|
|
1245
|
+
const mailboxes = result?.list ?? [];
|
|
1246
|
+
return mailboxes.find((mailbox) => mailbox.role === "sent")?.id ?? mailboxes[0]?.id ?? null;
|
|
1247
|
+
})();
|
|
1248
|
+
}
|
|
1249
|
+
try {
|
|
1250
|
+
return await this.sentMailboxIdPromise;
|
|
1251
|
+
} catch (err) {
|
|
1252
|
+
this.sentMailboxIdPromise = null;
|
|
1253
|
+
throw err;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
async saveToSent(params) {
|
|
1257
|
+
if (!this.canPersistSentCopy())
|
|
1258
|
+
return;
|
|
1259
|
+
const sentMailboxId = await this.getSentMailboxId();
|
|
1260
|
+
if (!sentMailboxId)
|
|
1261
|
+
return;
|
|
1262
|
+
const emailCreate = {
|
|
1263
|
+
mailboxIds: { [sentMailboxId]: true },
|
|
1264
|
+
from: [{ email: params.from }],
|
|
1265
|
+
to: [{ email: params.to }],
|
|
1266
|
+
subject: params.subject,
|
|
1267
|
+
bodyValues: {
|
|
1268
|
+
body: {
|
|
1269
|
+
value: params.text,
|
|
1270
|
+
charset: "utf-8"
|
|
1271
|
+
}
|
|
1272
|
+
},
|
|
1273
|
+
textBody: [{ partId: "body", type: "text/plain" }],
|
|
1274
|
+
keywords: { "$seen": true }
|
|
1275
|
+
};
|
|
1276
|
+
if (params.inReplyTo) {
|
|
1277
|
+
emailCreate["header:In-Reply-To:asText"] = ` ${sanitize(params.inReplyTo)}`;
|
|
1278
|
+
}
|
|
1279
|
+
if (params.messageId) {
|
|
1280
|
+
emailCreate["header:Message-ID:asText"] = ` ${sanitize(params.messageId)}`;
|
|
1281
|
+
}
|
|
1282
|
+
if (params.references) {
|
|
1283
|
+
emailCreate["header:References:asText"] = ` ${sanitize(params.references)}`;
|
|
1284
|
+
}
|
|
1285
|
+
for (const [name, value] of Object.entries(params.aampHeaders)) {
|
|
1286
|
+
emailCreate[`header:${name}:asText`] = ` ${value}`;
|
|
1287
|
+
}
|
|
1288
|
+
await this.jmapCall([
|
|
1289
|
+
["Email/set", { create: { sent1: emailCreate } }, "sent1"]
|
|
1290
|
+
]);
|
|
1291
|
+
}
|
|
1292
|
+
async saveToSentBestEffort(params) {
|
|
1293
|
+
if (!this.canPersistSentCopy())
|
|
1294
|
+
return;
|
|
1295
|
+
try {
|
|
1296
|
+
await this.saveToSent(params);
|
|
1297
|
+
} catch {
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1176
1300
|
/**
|
|
1177
1301
|
* Send a task.dispatch email.
|
|
1178
1302
|
* Returns both the generated taskId and the SMTP Message-ID so callers can
|
|
@@ -1224,9 +1348,25 @@ ${opts.contextLinks.map((l) => ` ${l}`).join("\n")}` : "",
|
|
|
1224
1348
|
content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content
|
|
1225
1349
|
}))
|
|
1226
1350
|
});
|
|
1351
|
+
await this.saveToSentBestEffort({
|
|
1352
|
+
from: this.config.user,
|
|
1353
|
+
to: opts.to,
|
|
1354
|
+
subject: sendMailOpts.subject,
|
|
1355
|
+
text: sendMailOpts.text,
|
|
1356
|
+
aampHeaders,
|
|
1357
|
+
messageId: info2.messageId
|
|
1358
|
+
});
|
|
1227
1359
|
return { taskId, messageId: info2.messageId ?? "" };
|
|
1228
1360
|
}
|
|
1229
1361
|
const info = await this.transport.sendMail(sendMailOpts);
|
|
1362
|
+
await this.saveToSentBestEffort({
|
|
1363
|
+
from: this.config.user,
|
|
1364
|
+
to: opts.to,
|
|
1365
|
+
subject: sendMailOpts.subject,
|
|
1366
|
+
text: sendMailOpts.text,
|
|
1367
|
+
aampHeaders,
|
|
1368
|
+
messageId: info.messageId
|
|
1369
|
+
});
|
|
1230
1370
|
return { taskId, messageId: info.messageId ?? "" };
|
|
1231
1371
|
}
|
|
1232
1372
|
/**
|
|
@@ -1269,7 +1409,7 @@ Error: ${opts.errorMsg}` : ""
|
|
|
1269
1409
|
}));
|
|
1270
1410
|
}
|
|
1271
1411
|
if (this.shouldUseHttpFallback(opts.to)) {
|
|
1272
|
-
await this.sendViaHttp({
|
|
1412
|
+
const info2 = await this.sendViaHttp({
|
|
1273
1413
|
to: opts.to,
|
|
1274
1414
|
subject: mailOpts.subject,
|
|
1275
1415
|
text: mailOpts.text,
|
|
@@ -1280,9 +1420,29 @@ Error: ${opts.errorMsg}` : ""
|
|
|
1280
1420
|
content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content
|
|
1281
1421
|
}))
|
|
1282
1422
|
});
|
|
1423
|
+
await this.saveToSentBestEffort({
|
|
1424
|
+
from: this.config.user,
|
|
1425
|
+
to: opts.to,
|
|
1426
|
+
subject: mailOpts.subject,
|
|
1427
|
+
text: mailOpts.text,
|
|
1428
|
+
aampHeaders,
|
|
1429
|
+
messageId: info2.messageId,
|
|
1430
|
+
inReplyTo: opts.inReplyTo,
|
|
1431
|
+
references: opts.inReplyTo
|
|
1432
|
+
});
|
|
1283
1433
|
return;
|
|
1284
1434
|
}
|
|
1285
|
-
await this.transport.sendMail(mailOpts);
|
|
1435
|
+
const info = await this.transport.sendMail(mailOpts);
|
|
1436
|
+
await this.saveToSentBestEffort({
|
|
1437
|
+
from: this.config.user,
|
|
1438
|
+
to: opts.to,
|
|
1439
|
+
subject: mailOpts.subject,
|
|
1440
|
+
text: mailOpts.text,
|
|
1441
|
+
aampHeaders,
|
|
1442
|
+
messageId: info.messageId,
|
|
1443
|
+
inReplyTo: opts.inReplyTo,
|
|
1444
|
+
references: opts.inReplyTo
|
|
1445
|
+
});
|
|
1286
1446
|
}
|
|
1287
1447
|
/**
|
|
1288
1448
|
* Send a task.help_needed email when the agent is blocked
|
|
@@ -1324,7 +1484,7 @@ ${opts.suggestedOptions.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}` : ""
|
|
|
1324
1484
|
}));
|
|
1325
1485
|
}
|
|
1326
1486
|
if (this.shouldUseHttpFallback(opts.to)) {
|
|
1327
|
-
await this.sendViaHttp({
|
|
1487
|
+
const info2 = await this.sendViaHttp({
|
|
1328
1488
|
to: opts.to,
|
|
1329
1489
|
subject: helpMailOpts.subject,
|
|
1330
1490
|
text: helpMailOpts.text,
|
|
@@ -1335,9 +1495,29 @@ ${opts.suggestedOptions.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}` : ""
|
|
|
1335
1495
|
content: typeof a.content === "string" ? Buffer.from(a.content, "base64") : a.content
|
|
1336
1496
|
}))
|
|
1337
1497
|
});
|
|
1498
|
+
await this.saveToSentBestEffort({
|
|
1499
|
+
from: this.config.user,
|
|
1500
|
+
to: opts.to,
|
|
1501
|
+
subject: helpMailOpts.subject,
|
|
1502
|
+
text: helpMailOpts.text,
|
|
1503
|
+
aampHeaders,
|
|
1504
|
+
messageId: info2.messageId,
|
|
1505
|
+
inReplyTo: opts.inReplyTo,
|
|
1506
|
+
references: opts.inReplyTo
|
|
1507
|
+
});
|
|
1338
1508
|
return;
|
|
1339
1509
|
}
|
|
1340
|
-
await this.transport.sendMail(helpMailOpts);
|
|
1510
|
+
const info = await this.transport.sendMail(helpMailOpts);
|
|
1511
|
+
await this.saveToSentBestEffort({
|
|
1512
|
+
from: this.config.user,
|
|
1513
|
+
to: opts.to,
|
|
1514
|
+
subject: helpMailOpts.subject,
|
|
1515
|
+
text: helpMailOpts.text,
|
|
1516
|
+
aampHeaders,
|
|
1517
|
+
messageId: info.messageId,
|
|
1518
|
+
inReplyTo: opts.inReplyTo,
|
|
1519
|
+
references: opts.inReplyTo
|
|
1520
|
+
});
|
|
1341
1521
|
}
|
|
1342
1522
|
/**
|
|
1343
1523
|
* Send a task.cancel email to stop a previously dispatched task.
|
|
@@ -1358,15 +1538,35 @@ ${opts.suggestedOptions.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}` : ""
|
|
|
1358
1538
|
mailOpts.references = opts.inReplyTo;
|
|
1359
1539
|
}
|
|
1360
1540
|
if (this.shouldUseHttpFallback(opts.to)) {
|
|
1361
|
-
await this.sendViaHttp({
|
|
1541
|
+
const info2 = await this.sendViaHttp({
|
|
1362
1542
|
to: opts.to,
|
|
1363
1543
|
subject: mailOpts.subject,
|
|
1364
1544
|
text: mailOpts.text,
|
|
1365
1545
|
aampHeaders
|
|
1366
1546
|
});
|
|
1547
|
+
await this.saveToSentBestEffort({
|
|
1548
|
+
from: this.config.user,
|
|
1549
|
+
to: opts.to,
|
|
1550
|
+
subject: mailOpts.subject,
|
|
1551
|
+
text: mailOpts.text,
|
|
1552
|
+
aampHeaders,
|
|
1553
|
+
messageId: info2.messageId,
|
|
1554
|
+
inReplyTo: opts.inReplyTo,
|
|
1555
|
+
references: opts.inReplyTo
|
|
1556
|
+
});
|
|
1367
1557
|
return;
|
|
1368
1558
|
}
|
|
1369
|
-
await this.transport.sendMail(mailOpts);
|
|
1559
|
+
const info = await this.transport.sendMail(mailOpts);
|
|
1560
|
+
await this.saveToSentBestEffort({
|
|
1561
|
+
from: this.config.user,
|
|
1562
|
+
to: opts.to,
|
|
1563
|
+
subject: mailOpts.subject,
|
|
1564
|
+
text: mailOpts.text,
|
|
1565
|
+
aampHeaders,
|
|
1566
|
+
messageId: info.messageId,
|
|
1567
|
+
inReplyTo: opts.inReplyTo,
|
|
1568
|
+
references: opts.inReplyTo
|
|
1569
|
+
});
|
|
1370
1570
|
}
|
|
1371
1571
|
/**
|
|
1372
1572
|
* Send a task.ack email to confirm receipt of a dispatch
|
|
@@ -1385,15 +1585,85 @@ ${opts.suggestedOptions.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}` : ""
|
|
|
1385
1585
|
mailOpts.references = opts.inReplyTo;
|
|
1386
1586
|
}
|
|
1387
1587
|
if (this.shouldUseHttpFallback(opts.to)) {
|
|
1388
|
-
await this.sendViaHttp({
|
|
1588
|
+
const info2 = await this.sendViaHttp({
|
|
1389
1589
|
to: opts.to,
|
|
1390
1590
|
subject: mailOpts.subject,
|
|
1391
1591
|
text: mailOpts.text,
|
|
1392
1592
|
aampHeaders
|
|
1393
1593
|
});
|
|
1594
|
+
await this.saveToSentBestEffort({
|
|
1595
|
+
from: this.config.user,
|
|
1596
|
+
to: opts.to,
|
|
1597
|
+
subject: mailOpts.subject,
|
|
1598
|
+
text: mailOpts.text,
|
|
1599
|
+
aampHeaders,
|
|
1600
|
+
messageId: info2.messageId,
|
|
1601
|
+
inReplyTo: opts.inReplyTo,
|
|
1602
|
+
references: opts.inReplyTo
|
|
1603
|
+
});
|
|
1394
1604
|
return;
|
|
1395
1605
|
}
|
|
1396
|
-
await this.transport.sendMail(mailOpts);
|
|
1606
|
+
const info = await this.transport.sendMail(mailOpts);
|
|
1607
|
+
await this.saveToSentBestEffort({
|
|
1608
|
+
from: this.config.user,
|
|
1609
|
+
to: opts.to,
|
|
1610
|
+
subject: mailOpts.subject,
|
|
1611
|
+
text: mailOpts.text,
|
|
1612
|
+
aampHeaders,
|
|
1613
|
+
messageId: info.messageId,
|
|
1614
|
+
inReplyTo: opts.inReplyTo,
|
|
1615
|
+
references: opts.inReplyTo
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
async sendStreamOpened(opts) {
|
|
1619
|
+
const aampHeaders = buildStreamOpenedHeaders({
|
|
1620
|
+
taskId: opts.taskId,
|
|
1621
|
+
streamId: opts.streamId
|
|
1622
|
+
});
|
|
1623
|
+
const mailOpts = {
|
|
1624
|
+
from: this.config.user,
|
|
1625
|
+
to: opts.to,
|
|
1626
|
+
subject: `[AAMP Stream] Task ${opts.taskId}`,
|
|
1627
|
+
text: `AAMP task stream is ready.
|
|
1628
|
+
|
|
1629
|
+
Task ID: ${opts.taskId}
|
|
1630
|
+
Stream ID: ${opts.streamId}`,
|
|
1631
|
+
headers: aampHeaders
|
|
1632
|
+
};
|
|
1633
|
+
if (opts.inReplyTo) {
|
|
1634
|
+
mailOpts.inReplyTo = opts.inReplyTo;
|
|
1635
|
+
mailOpts.references = opts.inReplyTo;
|
|
1636
|
+
}
|
|
1637
|
+
if (this.shouldUseHttpFallback(opts.to)) {
|
|
1638
|
+
const info2 = await this.sendViaHttp({
|
|
1639
|
+
to: opts.to,
|
|
1640
|
+
subject: mailOpts.subject,
|
|
1641
|
+
text: mailOpts.text,
|
|
1642
|
+
aampHeaders
|
|
1643
|
+
});
|
|
1644
|
+
await this.saveToSentBestEffort({
|
|
1645
|
+
from: this.config.user,
|
|
1646
|
+
to: opts.to,
|
|
1647
|
+
subject: mailOpts.subject,
|
|
1648
|
+
text: mailOpts.text,
|
|
1649
|
+
aampHeaders,
|
|
1650
|
+
messageId: info2.messageId,
|
|
1651
|
+
inReplyTo: opts.inReplyTo,
|
|
1652
|
+
references: opts.inReplyTo
|
|
1653
|
+
});
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
const info = await this.transport.sendMail(mailOpts);
|
|
1657
|
+
await this.saveToSentBestEffort({
|
|
1658
|
+
from: this.config.user,
|
|
1659
|
+
to: opts.to,
|
|
1660
|
+
subject: mailOpts.subject,
|
|
1661
|
+
text: mailOpts.text,
|
|
1662
|
+
aampHeaders,
|
|
1663
|
+
messageId: info.messageId,
|
|
1664
|
+
inReplyTo: opts.inReplyTo,
|
|
1665
|
+
references: opts.inReplyTo
|
|
1666
|
+
});
|
|
1397
1667
|
}
|
|
1398
1668
|
async sendCardQuery(opts) {
|
|
1399
1669
|
const taskId = opts.taskId ?? randomUUID();
|
|
@@ -1416,9 +1686,29 @@ ${opts.suggestedOptions.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}` : ""
|
|
|
1416
1686
|
text: mailOpts.text,
|
|
1417
1687
|
aampHeaders
|
|
1418
1688
|
});
|
|
1689
|
+
await this.saveToSentBestEffort({
|
|
1690
|
+
from: this.config.user,
|
|
1691
|
+
to: opts.to,
|
|
1692
|
+
subject: mailOpts.subject,
|
|
1693
|
+
text: mailOpts.text,
|
|
1694
|
+
aampHeaders,
|
|
1695
|
+
messageId: info2.messageId,
|
|
1696
|
+
inReplyTo: opts.inReplyTo,
|
|
1697
|
+
references: opts.inReplyTo
|
|
1698
|
+
});
|
|
1419
1699
|
return { taskId, messageId: info2.messageId ?? "" };
|
|
1420
1700
|
}
|
|
1421
1701
|
const info = await this.transport.sendMail(mailOpts);
|
|
1702
|
+
await this.saveToSentBestEffort({
|
|
1703
|
+
from: this.config.user,
|
|
1704
|
+
to: opts.to,
|
|
1705
|
+
subject: mailOpts.subject,
|
|
1706
|
+
text: mailOpts.text,
|
|
1707
|
+
aampHeaders,
|
|
1708
|
+
messageId: info.messageId,
|
|
1709
|
+
inReplyTo: opts.inReplyTo,
|
|
1710
|
+
references: opts.inReplyTo
|
|
1711
|
+
});
|
|
1422
1712
|
return { taskId, messageId: info.messageId ?? "" };
|
|
1423
1713
|
}
|
|
1424
1714
|
async sendCardResponse(opts) {
|
|
@@ -1438,15 +1728,35 @@ ${opts.suggestedOptions.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}` : ""
|
|
|
1438
1728
|
mailOpts.references = opts.inReplyTo;
|
|
1439
1729
|
}
|
|
1440
1730
|
if (this.shouldUseHttpFallback(opts.to)) {
|
|
1441
|
-
await this.sendViaHttp({
|
|
1731
|
+
const info2 = await this.sendViaHttp({
|
|
1442
1732
|
to: opts.to,
|
|
1443
1733
|
subject: mailOpts.subject,
|
|
1444
1734
|
text: mailOpts.text,
|
|
1445
1735
|
aampHeaders
|
|
1446
1736
|
});
|
|
1737
|
+
await this.saveToSentBestEffort({
|
|
1738
|
+
from: this.config.user,
|
|
1739
|
+
to: opts.to,
|
|
1740
|
+
subject: mailOpts.subject,
|
|
1741
|
+
text: mailOpts.text,
|
|
1742
|
+
aampHeaders,
|
|
1743
|
+
messageId: info2.messageId,
|
|
1744
|
+
inReplyTo: opts.inReplyTo,
|
|
1745
|
+
references: opts.inReplyTo
|
|
1746
|
+
});
|
|
1447
1747
|
return;
|
|
1448
1748
|
}
|
|
1449
|
-
await this.transport.sendMail(mailOpts);
|
|
1749
|
+
const info = await this.transport.sendMail(mailOpts);
|
|
1750
|
+
await this.saveToSentBestEffort({
|
|
1751
|
+
from: this.config.user,
|
|
1752
|
+
to: opts.to,
|
|
1753
|
+
subject: mailOpts.subject,
|
|
1754
|
+
text: mailOpts.text,
|
|
1755
|
+
aampHeaders,
|
|
1756
|
+
messageId: info.messageId,
|
|
1757
|
+
inReplyTo: opts.inReplyTo,
|
|
1758
|
+
references: opts.inReplyTo
|
|
1759
|
+
});
|
|
1450
1760
|
}
|
|
1451
1761
|
/**
|
|
1452
1762
|
* Verify SMTP connection
|
|
@@ -1464,11 +1774,66 @@ ${opts.suggestedOptions.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}` : ""
|
|
|
1464
1774
|
}
|
|
1465
1775
|
};
|
|
1466
1776
|
|
|
1467
|
-
// ../sdk/src/
|
|
1777
|
+
// ../sdk/src/thread.js
|
|
1778
|
+
function singleLine(value, maxLength = 220) {
|
|
1779
|
+
const normalized = (value ?? "").replace(/\s+/g, " ").trim();
|
|
1780
|
+
if (!normalized)
|
|
1781
|
+
return "";
|
|
1782
|
+
if (normalized.length <= maxLength)
|
|
1783
|
+
return normalized;
|
|
1784
|
+
return `${normalized.slice(0, maxLength - 1)}\u2026`;
|
|
1785
|
+
}
|
|
1786
|
+
function formatTimestamp(value) {
|
|
1787
|
+
const date = new Date(value);
|
|
1788
|
+
if (Number.isNaN(date.getTime()))
|
|
1789
|
+
return value;
|
|
1790
|
+
return date.toISOString().slice(0, 16).replace("T", " ");
|
|
1791
|
+
}
|
|
1792
|
+
function renderEventLine(event) {
|
|
1793
|
+
const from = event.from.split("@")[0] || event.from;
|
|
1794
|
+
const timestamp = formatTimestamp(event.createdAt);
|
|
1795
|
+
if (event.intent === "task.dispatch") {
|
|
1796
|
+
const summary = singleLine(event.bodyText) || singleLine(event.title) || "Task dispatched";
|
|
1797
|
+
return `[${timestamp}] ${from} dispatched: ${summary}`;
|
|
1798
|
+
}
|
|
1799
|
+
if (event.intent === "task.help_needed") {
|
|
1800
|
+
const question = singleLine(event.question) || "Asked for help";
|
|
1801
|
+
const reason = singleLine(event.blockedReason);
|
|
1802
|
+
return `[${timestamp}] ${from} asked for help: ${question}${reason ? ` (reason: ${reason})` : ""}`;
|
|
1803
|
+
}
|
|
1804
|
+
if (event.intent === "task.result") {
|
|
1805
|
+
const output = singleLine(event.output) || singleLine(event.bodyText) || "Sent a result";
|
|
1806
|
+
return `[${timestamp}] ${from} replied: ${output}`;
|
|
1807
|
+
}
|
|
1808
|
+
if (event.intent === "task.cancel") {
|
|
1809
|
+
const body = singleLine(event.bodyText) || "Cancelled the task";
|
|
1810
|
+
return `[${timestamp}] ${from} cancelled the task: ${body}`;
|
|
1811
|
+
}
|
|
1812
|
+
if (event.intent === "task.ack") {
|
|
1813
|
+
return `[${timestamp}] ${from} acknowledged the task`;
|
|
1814
|
+
}
|
|
1815
|
+
return `[${timestamp}] ${from}: ${singleLine(event.bodyText) || event.intent}`;
|
|
1816
|
+
}
|
|
1817
|
+
function renderThreadHistoryForAgent(events, options = {}) {
|
|
1818
|
+
const filtered = events.filter((event) => event.intent !== "task.stream.opened");
|
|
1819
|
+
if (filtered.length === 0)
|
|
1820
|
+
return "";
|
|
1821
|
+
const maxEvents = Math.max(1, options.maxEvents ?? 8);
|
|
1822
|
+
const visible = filtered.slice(-maxEvents);
|
|
1823
|
+
const omitted = filtered.length - visible.length;
|
|
1824
|
+
return [
|
|
1825
|
+
"Prior thread context:",
|
|
1826
|
+
...omitted > 0 ? [`(${omitted} earlier event(s) omitted)`] : [],
|
|
1827
|
+
...visible.map((event) => `- ${renderEventLine(event)}`)
|
|
1828
|
+
].join("\n");
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// ../sdk/src/client.js
|
|
1468
1832
|
var AampClient = class _AampClient extends TinyEmitter {
|
|
1469
1833
|
jmapClient;
|
|
1470
1834
|
smtpSender;
|
|
1471
1835
|
config;
|
|
1836
|
+
streamAppendQueues = /* @__PURE__ */ new Map();
|
|
1472
1837
|
constructor(config) {
|
|
1473
1838
|
super();
|
|
1474
1839
|
this.config = config;
|
|
@@ -1521,6 +1886,9 @@ var AampClient = class _AampClient extends TinyEmitter {
|
|
|
1521
1886
|
this.jmapClient.on("task.ack", (ack) => {
|
|
1522
1887
|
this.emit("task.ack", ack);
|
|
1523
1888
|
});
|
|
1889
|
+
this.jmapClient.on("task.stream.opened", (stream) => {
|
|
1890
|
+
this.emit("task.stream.opened", stream);
|
|
1891
|
+
});
|
|
1524
1892
|
this.jmapClient.on("card.query", (query) => {
|
|
1525
1893
|
this.emit("card.query", query);
|
|
1526
1894
|
});
|
|
@@ -1679,6 +2047,9 @@ var AampClient = class _AampClient extends TinyEmitter {
|
|
|
1679
2047
|
async sendHelp(opts) {
|
|
1680
2048
|
return this.smtpSender.sendHelp(opts);
|
|
1681
2049
|
}
|
|
2050
|
+
async sendStreamOpened(opts) {
|
|
2051
|
+
return this.smtpSender.sendStreamOpened(opts);
|
|
2052
|
+
}
|
|
1682
2053
|
async sendCardQuery(opts) {
|
|
1683
2054
|
return this.smtpSender.sendCardQuery(opts);
|
|
1684
2055
|
}
|
|
@@ -1740,6 +2111,293 @@ var AampClient = class _AampClient extends TinyEmitter {
|
|
|
1740
2111
|
const data = await res.json();
|
|
1741
2112
|
return data.agents;
|
|
1742
2113
|
}
|
|
2114
|
+
async getThreadHistory(taskId, opts = {}) {
|
|
2115
|
+
const base = this.config.baseUrl;
|
|
2116
|
+
const mailboxToken = this.config.mailboxToken;
|
|
2117
|
+
const res = await _AampClient.callDiscoveredApi(base, {
|
|
2118
|
+
action: "aamp.mailbox.thread",
|
|
2119
|
+
authToken: mailboxToken,
|
|
2120
|
+
query: {
|
|
2121
|
+
taskId,
|
|
2122
|
+
includeStreamOpened: opts.includeStreamOpened
|
|
2123
|
+
}
|
|
2124
|
+
});
|
|
2125
|
+
if (!res.ok) {
|
|
2126
|
+
const body = await res.text().catch(() => "");
|
|
2127
|
+
throw new Error(`Thread history fetch failed: ${res.status} ${body || res.statusText}`);
|
|
2128
|
+
}
|
|
2129
|
+
const data = await res.json();
|
|
2130
|
+
return {
|
|
2131
|
+
taskId: data.taskId,
|
|
2132
|
+
events: Array.isArray(data.events) ? data.events : []
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
async hydrateTaskDispatch(task) {
|
|
2136
|
+
const history = await this.getThreadHistory(task.taskId);
|
|
2137
|
+
const priorEvents = history.events.filter((event) => event.messageId !== task.messageId);
|
|
2138
|
+
return {
|
|
2139
|
+
...task,
|
|
2140
|
+
threadHistory: priorEvents,
|
|
2141
|
+
threadContextText: renderThreadHistoryForAgent(priorEvents)
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
async resolveStreamCapability() {
|
|
2145
|
+
const discovery = await _AampClient.discoverAampService(this.config.baseUrl);
|
|
2146
|
+
const stream = discovery.capabilities?.stream;
|
|
2147
|
+
if (!stream?.transport) {
|
|
2148
|
+
throw new Error("AAMP stream capability is not available on this service");
|
|
2149
|
+
}
|
|
2150
|
+
return stream;
|
|
2151
|
+
}
|
|
2152
|
+
async createStream(opts) {
|
|
2153
|
+
const stream = await this.resolveStreamCapability();
|
|
2154
|
+
const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
|
|
2155
|
+
action: stream.createAction ?? "aamp.stream.create",
|
|
2156
|
+
method: "POST",
|
|
2157
|
+
authToken: this.config.mailboxToken,
|
|
2158
|
+
body: opts
|
|
2159
|
+
});
|
|
2160
|
+
if (!res.ok) {
|
|
2161
|
+
const body = await res.text().catch(() => "");
|
|
2162
|
+
throw new Error(`AAMP stream create failed: ${res.status} ${body || res.statusText}`);
|
|
2163
|
+
}
|
|
2164
|
+
return res.json();
|
|
2165
|
+
}
|
|
2166
|
+
getStreamAppendQueue(streamId) {
|
|
2167
|
+
let queue = this.streamAppendQueues.get(streamId);
|
|
2168
|
+
if (!queue) {
|
|
2169
|
+
queue = { running: false, operations: [] };
|
|
2170
|
+
this.streamAppendQueues.set(streamId, queue);
|
|
2171
|
+
}
|
|
2172
|
+
return queue;
|
|
2173
|
+
}
|
|
2174
|
+
async dispatchStreamAppend(opts) {
|
|
2175
|
+
const stream = await this.resolveStreamCapability();
|
|
2176
|
+
const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
|
|
2177
|
+
action: stream.appendAction ?? "aamp.stream.append",
|
|
2178
|
+
method: "POST",
|
|
2179
|
+
authToken: this.config.mailboxToken,
|
|
2180
|
+
body: opts
|
|
2181
|
+
});
|
|
2182
|
+
if (!res.ok) {
|
|
2183
|
+
const body = await res.text().catch(() => "");
|
|
2184
|
+
throw new Error(`AAMP stream append failed: ${res.status} ${body || res.statusText}`);
|
|
2185
|
+
}
|
|
2186
|
+
return res.json();
|
|
2187
|
+
}
|
|
2188
|
+
enqueueStreamAppend(streamId, operation) {
|
|
2189
|
+
const queue = this.getStreamAppendQueue(streamId);
|
|
2190
|
+
queue.operations.push(operation);
|
|
2191
|
+
void this.drainStreamAppendQueue(streamId);
|
|
2192
|
+
}
|
|
2193
|
+
async drainStreamAppendQueue(streamId) {
|
|
2194
|
+
const queue = this.streamAppendQueues.get(streamId);
|
|
2195
|
+
if (!queue || queue.running)
|
|
2196
|
+
return;
|
|
2197
|
+
queue.running = true;
|
|
2198
|
+
try {
|
|
2199
|
+
while (queue.operations.length) {
|
|
2200
|
+
const operation = queue.operations.shift();
|
|
2201
|
+
if (!operation)
|
|
2202
|
+
continue;
|
|
2203
|
+
if (operation.kind === "text-delta-batch") {
|
|
2204
|
+
try {
|
|
2205
|
+
const event = await this.dispatchStreamAppend({
|
|
2206
|
+
streamId,
|
|
2207
|
+
type: "text.delta",
|
|
2208
|
+
payload: {
|
|
2209
|
+
...operation.payload,
|
|
2210
|
+
text: operation.text
|
|
2211
|
+
}
|
|
2212
|
+
});
|
|
2213
|
+
for (const resolve of operation.resolvers)
|
|
2214
|
+
resolve(event);
|
|
2215
|
+
} catch (error) {
|
|
2216
|
+
for (const reject of operation.rejecters)
|
|
2217
|
+
reject(error);
|
|
2218
|
+
}
|
|
2219
|
+
continue;
|
|
2220
|
+
}
|
|
2221
|
+
try {
|
|
2222
|
+
const event = await this.dispatchStreamAppend(operation.opts);
|
|
2223
|
+
operation.resolve(event);
|
|
2224
|
+
} catch (error) {
|
|
2225
|
+
operation.reject(error);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
} finally {
|
|
2229
|
+
queue.running = false;
|
|
2230
|
+
if (queue.operations.length === 0) {
|
|
2231
|
+
this.streamAppendQueues.delete(streamId);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
async flushStreamAppendQueue(streamId) {
|
|
2236
|
+
while (true) {
|
|
2237
|
+
const queue = this.streamAppendQueues.get(streamId);
|
|
2238
|
+
if (!queue)
|
|
2239
|
+
return;
|
|
2240
|
+
if (!queue.running && queue.operations.length === 0) {
|
|
2241
|
+
this.streamAppendQueues.delete(streamId);
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
async appendStreamEvent(opts) {
|
|
2248
|
+
if (opts.type === "text.delta" && typeof opts.payload.text === "string") {
|
|
2249
|
+
return await new Promise((resolve, reject) => {
|
|
2250
|
+
const queue = this.getStreamAppendQueue(opts.streamId);
|
|
2251
|
+
const lastOperation = queue.operations.at(-1);
|
|
2252
|
+
if (lastOperation?.kind === "text-delta-batch") {
|
|
2253
|
+
lastOperation.text += String(opts.payload.text ?? "");
|
|
2254
|
+
lastOperation.resolvers.push(resolve);
|
|
2255
|
+
lastOperation.rejecters.push(reject);
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
this.enqueueStreamAppend(opts.streamId, {
|
|
2259
|
+
kind: "text-delta-batch",
|
|
2260
|
+
text: String(opts.payload.text ?? ""),
|
|
2261
|
+
payload: {
|
|
2262
|
+
...opts.payload
|
|
2263
|
+
},
|
|
2264
|
+
resolvers: [resolve],
|
|
2265
|
+
rejecters: [reject]
|
|
2266
|
+
});
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
return await new Promise((resolve, reject) => {
|
|
2270
|
+
this.enqueueStreamAppend(opts.streamId, {
|
|
2271
|
+
kind: "single-event",
|
|
2272
|
+
opts,
|
|
2273
|
+
resolve,
|
|
2274
|
+
reject
|
|
2275
|
+
});
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2278
|
+
async closeStream(opts) {
|
|
2279
|
+
await this.flushStreamAppendQueue(opts.streamId);
|
|
2280
|
+
const stream = await this.resolveStreamCapability();
|
|
2281
|
+
const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
|
|
2282
|
+
action: stream.closeAction ?? "aamp.stream.close",
|
|
2283
|
+
method: "POST",
|
|
2284
|
+
authToken: this.config.mailboxToken,
|
|
2285
|
+
body: opts
|
|
2286
|
+
});
|
|
2287
|
+
if (!res.ok) {
|
|
2288
|
+
const body = await res.text().catch(() => "");
|
|
2289
|
+
throw new Error(`AAMP stream close failed: ${res.status} ${body || res.statusText}`);
|
|
2290
|
+
}
|
|
2291
|
+
return res.json();
|
|
2292
|
+
}
|
|
2293
|
+
async getTaskStream(opts) {
|
|
2294
|
+
const stream = await this.resolveStreamCapability();
|
|
2295
|
+
const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
|
|
2296
|
+
action: stream.getAction ?? "aamp.stream.get",
|
|
2297
|
+
authToken: this.config.mailboxToken,
|
|
2298
|
+
query: {
|
|
2299
|
+
...opts.taskId ? { taskId: opts.taskId } : {},
|
|
2300
|
+
...opts.streamId ? { streamId: opts.streamId } : {}
|
|
2301
|
+
}
|
|
2302
|
+
});
|
|
2303
|
+
if (res.status === 404)
|
|
2304
|
+
return null;
|
|
2305
|
+
if (!res.ok) {
|
|
2306
|
+
const body = await res.text().catch(() => "");
|
|
2307
|
+
throw new Error(`AAMP stream get failed: ${res.status} ${body || res.statusText}`);
|
|
2308
|
+
}
|
|
2309
|
+
return res.json();
|
|
2310
|
+
}
|
|
2311
|
+
async subscribeStream(streamId, handlers, opts = {}) {
|
|
2312
|
+
const stream = await this.resolveStreamCapability();
|
|
2313
|
+
const template = stream.subscribeUrlTemplate;
|
|
2314
|
+
if (!template)
|
|
2315
|
+
throw new Error("AAMP stream subscribeUrlTemplate is missing");
|
|
2316
|
+
const url = new URL(template.replace("{streamId}", encodeURIComponent(streamId)), this.config.baseUrl);
|
|
2317
|
+
if (opts.lastEventId) {
|
|
2318
|
+
url.searchParams.set("lastEventId", opts.lastEventId);
|
|
2319
|
+
}
|
|
2320
|
+
const controller = new AbortController();
|
|
2321
|
+
if (opts.signal) {
|
|
2322
|
+
opts.signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
2323
|
+
}
|
|
2324
|
+
const res = await fetch(url, {
|
|
2325
|
+
headers: {
|
|
2326
|
+
Authorization: `Basic ${this.config.mailboxToken}`,
|
|
2327
|
+
Accept: "text/event-stream"
|
|
2328
|
+
},
|
|
2329
|
+
signal: controller.signal
|
|
2330
|
+
});
|
|
2331
|
+
if (!res.ok || !res.body) {
|
|
2332
|
+
throw new Error(`AAMP stream subscribe failed: ${res.status} ${res.statusText}`);
|
|
2333
|
+
}
|
|
2334
|
+
handlers.onOpen?.();
|
|
2335
|
+
const reader = res.body.getReader();
|
|
2336
|
+
const decoder = new TextDecoder();
|
|
2337
|
+
let buffer = "";
|
|
2338
|
+
let currentEvent = "message";
|
|
2339
|
+
let currentId = "";
|
|
2340
|
+
let currentData = [];
|
|
2341
|
+
const flush = () => {
|
|
2342
|
+
if (!currentData.length)
|
|
2343
|
+
return;
|
|
2344
|
+
try {
|
|
2345
|
+
const parsed = JSON.parse(currentData.join("\n"));
|
|
2346
|
+
handlers.onEvent({
|
|
2347
|
+
...parsed,
|
|
2348
|
+
...currentId ? { id: currentId } : {},
|
|
2349
|
+
type: parsed.type ?? currentEvent
|
|
2350
|
+
});
|
|
2351
|
+
} catch (err) {
|
|
2352
|
+
handlers.onError?.(err);
|
|
2353
|
+
} finally {
|
|
2354
|
+
currentEvent = "message";
|
|
2355
|
+
currentId = "";
|
|
2356
|
+
currentData = [];
|
|
2357
|
+
}
|
|
2358
|
+
};
|
|
2359
|
+
void (async () => {
|
|
2360
|
+
try {
|
|
2361
|
+
while (true) {
|
|
2362
|
+
const { value, done } = await reader.read();
|
|
2363
|
+
if (done)
|
|
2364
|
+
break;
|
|
2365
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2366
|
+
let index = buffer.indexOf("\n\n");
|
|
2367
|
+
while (index >= 0) {
|
|
2368
|
+
const frame = buffer.slice(0, index);
|
|
2369
|
+
buffer = buffer.slice(index + 2);
|
|
2370
|
+
for (const rawLine of frame.split("\n")) {
|
|
2371
|
+
const line = rawLine.replace(/\r$/, "");
|
|
2372
|
+
if (!line || line.startsWith(":"))
|
|
2373
|
+
continue;
|
|
2374
|
+
if (line.startsWith("event:")) {
|
|
2375
|
+
currentEvent = line.slice(6).trim();
|
|
2376
|
+
} else if (line.startsWith("id:")) {
|
|
2377
|
+
currentId = line.slice(3).trim();
|
|
2378
|
+
} else if (line.startsWith("data:")) {
|
|
2379
|
+
currentData.push(line.slice(5).trimStart());
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
flush();
|
|
2383
|
+
index = buffer.indexOf("\n\n");
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
} catch (err) {
|
|
2387
|
+
if (!controller.signal.aborted) {
|
|
2388
|
+
handlers.onError?.(err);
|
|
2389
|
+
}
|
|
2390
|
+
} finally {
|
|
2391
|
+
buffer += decoder.decode();
|
|
2392
|
+
controller.abort();
|
|
2393
|
+
}
|
|
2394
|
+
})();
|
|
2395
|
+
return {
|
|
2396
|
+
close() {
|
|
2397
|
+
controller.abort();
|
|
2398
|
+
}
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
1743
2401
|
/**
|
|
1744
2402
|
* Download a blob (attachment) by its JMAP blobId.
|
|
1745
2403
|
* Use this to retrieve attachment content from received TaskDispatch or TaskResult messages.
|
|
@@ -1767,6 +2425,9 @@ var AampClient = class _AampClient extends TinyEmitter {
|
|
|
1767
2425
|
}
|
|
1768
2426
|
};
|
|
1769
2427
|
|
|
2428
|
+
// src/index.ts
|
|
2429
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
2430
|
+
|
|
1770
2431
|
// src/file-store.ts
|
|
1771
2432
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1772
2433
|
import { dirname, join } from "node:path";
|
|
@@ -1873,7 +2534,9 @@ function baseUrl(aampHost) {
|
|
|
1873
2534
|
return `https://${aampHost}`;
|
|
1874
2535
|
}
|
|
1875
2536
|
var pendingTasks = /* @__PURE__ */ new Map();
|
|
2537
|
+
var activeTaskStreams = /* @__PURE__ */ new Map();
|
|
1876
2538
|
var terminalTaskIds = new Set(loadTaskState(defaultTaskStatePath()).terminalTaskIds ?? []);
|
|
2539
|
+
var AAMP_SESSION_PREFIX = "aamp:";
|
|
1877
2540
|
var dispatchedSubtasks = /* @__PURE__ */ new Map();
|
|
1878
2541
|
var waitingDispatches = /* @__PURE__ */ new Map();
|
|
1879
2542
|
var aampClient = null;
|
|
@@ -1884,9 +2547,57 @@ var lastTransportMode = "disconnected";
|
|
|
1884
2547
|
var lastLoggedTransportMode = "disconnected";
|
|
1885
2548
|
var reconcileTimer = null;
|
|
1886
2549
|
var transportMonitorTimer = null;
|
|
1887
|
-
var
|
|
2550
|
+
var historicalReconcileCompleted = false;
|
|
1888
2551
|
var channelRuntime = null;
|
|
1889
2552
|
var channelCfg = null;
|
|
2553
|
+
async function ensureTaskStream(task) {
|
|
2554
|
+
if (!aampClient?.isConnected())
|
|
2555
|
+
return null;
|
|
2556
|
+
const existing = activeTaskStreams.get(task.taskId);
|
|
2557
|
+
if (existing)
|
|
2558
|
+
return existing;
|
|
2559
|
+
const created = await aampClient.createStream({
|
|
2560
|
+
taskId: task.taskId,
|
|
2561
|
+
peerEmail: task.from
|
|
2562
|
+
});
|
|
2563
|
+
await aampClient.sendStreamOpened({
|
|
2564
|
+
to: task.from,
|
|
2565
|
+
taskId: task.taskId,
|
|
2566
|
+
streamId: created.streamId,
|
|
2567
|
+
inReplyTo: task.messageId || void 0
|
|
2568
|
+
});
|
|
2569
|
+
await aampClient.appendStreamEvent({
|
|
2570
|
+
streamId: created.streamId,
|
|
2571
|
+
type: "status",
|
|
2572
|
+
payload: { state: "running", label: "Task queued in OpenClaw" }
|
|
2573
|
+
});
|
|
2574
|
+
activeTaskStreams.set(task.taskId, created.streamId);
|
|
2575
|
+
return created.streamId;
|
|
2576
|
+
}
|
|
2577
|
+
async function appendTaskStream(taskId, type, payload) {
|
|
2578
|
+
if (!aampClient?.isConnected())
|
|
2579
|
+
return;
|
|
2580
|
+
const streamId = activeTaskStreams.get(taskId);
|
|
2581
|
+
if (!streamId)
|
|
2582
|
+
return;
|
|
2583
|
+
await aampClient.appendStreamEvent({
|
|
2584
|
+
streamId,
|
|
2585
|
+
type,
|
|
2586
|
+
payload
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
async function closeTaskStream(taskId, payload) {
|
|
2590
|
+
if (!aampClient?.isConnected())
|
|
2591
|
+
return;
|
|
2592
|
+
const streamId = activeTaskStreams.get(taskId);
|
|
2593
|
+
if (!streamId)
|
|
2594
|
+
return;
|
|
2595
|
+
activeTaskStreams.delete(taskId);
|
|
2596
|
+
await aampClient.closeStream({
|
|
2597
|
+
streamId,
|
|
2598
|
+
payload
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
1890
2601
|
function logTransportState(api, mode, email, previousMode) {
|
|
1891
2602
|
if (mode === previousMode)
|
|
1892
2603
|
return;
|
|
@@ -1903,6 +2614,12 @@ function logTransportState(api, mode, email, previousMode) {
|
|
|
1903
2614
|
function isSyntheticPendingKey(taskKey) {
|
|
1904
2615
|
return taskKey.startsWith("result:") || taskKey.startsWith("help:");
|
|
1905
2616
|
}
|
|
2617
|
+
function isAampSessionKey(sessionKey) {
|
|
2618
|
+
return typeof sessionKey === "string" && sessionKey.startsWith(AAMP_SESSION_PREFIX);
|
|
2619
|
+
}
|
|
2620
|
+
function buildAampWakeSessionKey(kind, id) {
|
|
2621
|
+
return `${AAMP_SESSION_PREFIX}wake:${kind}:${id}`;
|
|
2622
|
+
}
|
|
1906
2623
|
function saveTerminalTaskIds() {
|
|
1907
2624
|
saveTaskState({ terminalTaskIds: [...terminalTaskIds] }, defaultTaskStatePath());
|
|
1908
2625
|
}
|
|
@@ -1928,6 +2645,17 @@ function hasExpired(task) {
|
|
|
1928
2645
|
}
|
|
1929
2646
|
return false;
|
|
1930
2647
|
}
|
|
2648
|
+
function isTransientTransportError(message) {
|
|
2649
|
+
return [
|
|
2650
|
+
"ECONNRESET",
|
|
2651
|
+
"ETIMEDOUT",
|
|
2652
|
+
"ECONNREFUSED",
|
|
2653
|
+
"EPIPE",
|
|
2654
|
+
"UND_ERR_SOCKET",
|
|
2655
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
2656
|
+
"fetch failed"
|
|
2657
|
+
].some((needle) => message.includes(needle));
|
|
2658
|
+
}
|
|
1931
2659
|
function nextPendingEntry() {
|
|
1932
2660
|
const entries = [...pendingTasks.entries()];
|
|
1933
2661
|
const notifications = entries.filter(([key]) => key.startsWith("result:") || key.startsWith("help:"));
|
|
@@ -1950,6 +2678,8 @@ function queuePendingTask(task) {
|
|
|
1950
2678
|
from: task.from,
|
|
1951
2679
|
title: task.title,
|
|
1952
2680
|
bodyText: task.bodyText ?? "",
|
|
2681
|
+
threadHistory: task.threadHistory ?? [],
|
|
2682
|
+
threadContextText: task.threadContextText ?? "",
|
|
1953
2683
|
priority: task.priority ?? "normal",
|
|
1954
2684
|
...task.expiresAt ? { expiresAt: task.expiresAt } : {},
|
|
1955
2685
|
contextLinks: task.contextLinks ?? [],
|
|
@@ -2023,6 +2753,18 @@ var src_default = {
|
|
|
2023
2753
|
default: "openclaw-agent",
|
|
2024
2754
|
description: "Agent name prefix used in the mailbox address"
|
|
2025
2755
|
},
|
|
2756
|
+
summary: {
|
|
2757
|
+
type: "string",
|
|
2758
|
+
description: "Directory summary shown when other agents search for this agent."
|
|
2759
|
+
},
|
|
2760
|
+
cardText: {
|
|
2761
|
+
type: "string",
|
|
2762
|
+
description: "Inline card text used for automatic card.response replies."
|
|
2763
|
+
},
|
|
2764
|
+
cardFile: {
|
|
2765
|
+
type: "string",
|
|
2766
|
+
description: "Absolute path to a card text file. Used when cardText is not set."
|
|
2767
|
+
},
|
|
2026
2768
|
credentialsFile: {
|
|
2027
2769
|
type: "string",
|
|
2028
2770
|
description: "Absolute path to cache AAMP credentials between gateway restarts. Default: ~/.openclaw/extensions/aamp-openclaw-plugin/.credentials.json. Delete this file to force re-registration with a new mailbox."
|
|
@@ -2089,11 +2831,35 @@ var src_default = {
|
|
|
2089
2831
|
api.logger.warn(`[AAMP] Could not trigger heartbeat for ${label}: ${err.message}`);
|
|
2090
2832
|
}
|
|
2091
2833
|
}
|
|
2834
|
+
function getConfiguredCardText() {
|
|
2835
|
+
const inline = cfg.cardText?.trim();
|
|
2836
|
+
if (inline)
|
|
2837
|
+
return inline;
|
|
2838
|
+
const file = cfg.cardFile?.trim();
|
|
2839
|
+
if (!file)
|
|
2840
|
+
return void 0;
|
|
2841
|
+
const fromFile = readFileSync2(file, "utf-8").trim();
|
|
2842
|
+
return fromFile || void 0;
|
|
2843
|
+
}
|
|
2844
|
+
async function syncDirectoryProfile() {
|
|
2845
|
+
if (!aampClient)
|
|
2846
|
+
return;
|
|
2847
|
+
const summary = cfg.summary?.trim();
|
|
2848
|
+
const cardText = getConfiguredCardText();
|
|
2849
|
+
if (!summary && !cardText)
|
|
2850
|
+
return;
|
|
2851
|
+
await aampClient.updateDirectoryProfile({
|
|
2852
|
+
...summary ? { summary } : {},
|
|
2853
|
+
...cardText ? { cardText } : {}
|
|
2854
|
+
});
|
|
2855
|
+
api.logger.info(`[AAMP] Directory profile synced${cardText ? " (card text registered)" : ""}`);
|
|
2856
|
+
}
|
|
2092
2857
|
function wakeAgentForPendingTask(task) {
|
|
2093
|
-
const
|
|
2858
|
+
const fallbackSessionKey = buildAampWakeSessionKey("task", task.taskId);
|
|
2859
|
+
const fallback = () => triggerHeartbeatWake(fallbackSessionKey, `task ${task.taskId}`);
|
|
2094
2860
|
const dispatcher = channelRuntime?.reply?.dispatchReplyWithBufferedBlockDispatcher;
|
|
2095
2861
|
api.logger.info(
|
|
2096
|
-
`[AAMP] Wake requested for task ${task.taskId} \u2014 channelRuntime=${channelRuntime ? "yes" : "no"} channelCfg=${channelCfg ? "yes" : "no"} dispatcher=${typeof dispatcher === "function" ? "yes" : "no"}
|
|
2862
|
+
`[AAMP] Wake requested for task ${task.taskId} \u2014 channelRuntime=${channelRuntime ? "yes" : "no"} channelCfg=${channelCfg ? "yes" : "no"} dispatcher=${typeof dispatcher === "function" ? "yes" : "no"} fallbackSession=${fallbackSessionKey}`
|
|
2097
2863
|
);
|
|
2098
2864
|
if (!channelRuntime || !channelCfg || typeof dispatcher !== "function") {
|
|
2099
2865
|
fallback();
|
|
@@ -2145,6 +2911,16 @@ var src_default = {
|
|
|
2145
2911
|
fallback();
|
|
2146
2912
|
}
|
|
2147
2913
|
}
|
|
2914
|
+
async function reconcileMailbox(includeHistorical) {
|
|
2915
|
+
if (!aampClient)
|
|
2916
|
+
return;
|
|
2917
|
+
const opts = includeHistorical ? { includeHistorical: true } : void 0;
|
|
2918
|
+
const count = await aampClient.reconcileRecentEmails(100, opts);
|
|
2919
|
+
if (includeHistorical && !historicalReconcileCompleted) {
|
|
2920
|
+
historicalReconcileCompleted = true;
|
|
2921
|
+
api.logger.info(`[AAMP] Historical mailbox reconcile complete (${count} email(s) scanned)`);
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2148
2924
|
async function doConnect(identity) {
|
|
2149
2925
|
if (reconcileTimer) {
|
|
2150
2926
|
clearInterval(reconcileTimer);
|
|
@@ -2171,36 +2947,49 @@ var src_default = {
|
|
|
2171
2947
|
});
|
|
2172
2948
|
aampClient.on("task.dispatch", (task) => {
|
|
2173
2949
|
api.logger.info(`[AAMP] \u2190 task.dispatch ${task.taskId} "${task.title}" from=${task.from}`);
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2950
|
+
void (async () => {
|
|
2951
|
+
try {
|
|
2952
|
+
if (terminalTaskIds.has(task.taskId)) {
|
|
2953
|
+
api.logger.info(`[AAMP] Skipping already-terminal task ${task.taskId}`);
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
const decision = matchSenderPolicy(task, cfg.senderPolicies);
|
|
2957
|
+
if (!decision.allowed) {
|
|
2958
|
+
api.logger.warn(`[AAMP] \u2717 rejected by senderPolicies: ${task.from} task=${task.taskId} reason=${decision.reason}`);
|
|
2959
|
+
void aampClient.sendResult({
|
|
2960
|
+
to: task.from,
|
|
2961
|
+
taskId: task.taskId,
|
|
2962
|
+
status: "rejected",
|
|
2963
|
+
output: "",
|
|
2964
|
+
errorMsg: decision.reason ?? `Sender ${task.from} is not allowed.`
|
|
2965
|
+
}).catch((err) => {
|
|
2966
|
+
api.logger.error(`[AAMP] Failed to send rejection for task ${task.taskId}: ${err.message}`);
|
|
2967
|
+
});
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
const hydratedTask = await aampClient.hydrateTaskDispatch(task).catch((err) => {
|
|
2971
|
+
api.logger.warn(`[AAMP] Failed to load thread history for ${task.taskId}: ${err.message}`);
|
|
2972
|
+
return {
|
|
2973
|
+
...task,
|
|
2974
|
+
threadHistory: [],
|
|
2975
|
+
threadContextText: ""
|
|
2976
|
+
};
|
|
2190
2977
|
});
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2978
|
+
if (!queuePendingTask(hydratedTask)) {
|
|
2979
|
+
api.logger.info(`[AAMP] Ignoring already-terminal or expired task ${task.taskId}`);
|
|
2980
|
+
return;
|
|
2981
|
+
}
|
|
2982
|
+
void ensureTaskStream(pendingTasks.get(task.taskId)).catch((err) => {
|
|
2983
|
+
api.logger.warn(`[AAMP] Failed to open stream for task ${task.taskId}: ${err.message}`);
|
|
2984
|
+
});
|
|
2985
|
+
wakeAgentForPendingTask(pendingTasks.get(task.taskId));
|
|
2986
|
+
} catch (err) {
|
|
2987
|
+
api.logger.error(`[AAMP] task.dispatch handler failed for ${task.taskId}: ${err.message}`);
|
|
2988
|
+
if (pendingTasks.has(task.taskId)) {
|
|
2989
|
+
triggerHeartbeatWake(buildAampWakeSessionKey("task", task.taskId), `task ${task.taskId}`);
|
|
2990
|
+
}
|
|
2202
2991
|
}
|
|
2203
|
-
}
|
|
2992
|
+
})();
|
|
2204
2993
|
});
|
|
2205
2994
|
aampClient.on("task.cancel", (cancel) => {
|
|
2206
2995
|
api.logger.info(`[AAMP] \u2190 task.cancel ${cancel.taskId} from=${cancel.from}`);
|
|
@@ -2210,6 +2999,8 @@ var src_default = {
|
|
|
2210
2999
|
dispatchedSubtasks.delete(cancel.taskId);
|
|
2211
3000
|
waitingDispatches.delete(cancel.taskId);
|
|
2212
3001
|
rememberTerminalTask(cancel.taskId);
|
|
3002
|
+
void closeTaskStream(cancel.taskId, { reason: "task.cancel" }).catch(() => {
|
|
3003
|
+
});
|
|
2213
3004
|
if (removed) {
|
|
2214
3005
|
api.logger.info(`[AAMP] Cancelled task ${cancel.taskId} \u2014 removed from pending queue`);
|
|
2215
3006
|
}
|
|
@@ -2330,7 +3121,7 @@ ${notifyBody?.bodyText ?? "Sub-task completed."}${actionSection}`;
|
|
|
2330
3121
|
api.logger.error(`[AAMP] Channel dispatch failed: ${err.message}`);
|
|
2331
3122
|
});
|
|
2332
3123
|
} else {
|
|
2333
|
-
const notifySessionKey =
|
|
3124
|
+
const notifySessionKey = buildAampWakeSessionKey("result", result.taskId);
|
|
2334
3125
|
try {
|
|
2335
3126
|
api.runtime.system.requestHeartbeatNow({ reason: "wake", sessionKey: notifySessionKey });
|
|
2336
3127
|
api.logger.info(`[AAMP] Heartbeat for sub-task result ${result.taskId}`);
|
|
@@ -2406,7 +3197,7 @@ ${notifyBody?.bodyText ?? help.question}`;
|
|
|
2406
3197
|
api.logger.error(`[AAMP] Channel dispatch failed for help: ${err.message}`);
|
|
2407
3198
|
});
|
|
2408
3199
|
} else {
|
|
2409
|
-
const helpSessionKey =
|
|
3200
|
+
const helpSessionKey = buildAampWakeSessionKey("help", help.taskId);
|
|
2410
3201
|
try {
|
|
2411
3202
|
api.runtime.system.requestHeartbeatNow({ reason: "wake", sessionKey: helpSessionKey });
|
|
2412
3203
|
api.logger.info(`[AAMP] Heartbeat fallback for sub-task help ${help.taskId}`);
|
|
@@ -2441,9 +3232,16 @@ ${notifyBody?.bodyText ?? help.question}`;
|
|
|
2441
3232
|
}
|
|
2442
3233
|
return;
|
|
2443
3234
|
}
|
|
3235
|
+
if (err.message.startsWith("Safety reconcile failed:") && isTransientTransportError(err.message)) {
|
|
3236
|
+
api.logger.warn(`[AAMP] ${err.message}`);
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
2444
3239
|
api.logger.error(`[AAMP] ${err.message}`);
|
|
2445
3240
|
});
|
|
2446
3241
|
await aampClient.connect();
|
|
3242
|
+
await syncDirectoryProfile().catch((err) => {
|
|
3243
|
+
api.logger.warn(`[AAMP] Directory profile sync failed: ${err.message}`);
|
|
3244
|
+
});
|
|
2447
3245
|
api.logger.info(
|
|
2448
3246
|
`[AAMP] Transport after connect \u2014 ${aampClient.isUsingPollingFallback() ? "polling fallback" : "websocket"} as ${agentEmail}`
|
|
2449
3247
|
);
|
|
@@ -2466,9 +3264,13 @@ ${notifyBody?.bodyText ?? help.question}`;
|
|
|
2466
3264
|
lastTransportMode = mode;
|
|
2467
3265
|
lastLoggedTransportMode = mode;
|
|
2468
3266
|
}, 1e3);
|
|
2469
|
-
void
|
|
3267
|
+
void reconcileMailbox(!historicalReconcileCompleted).catch((err) => {
|
|
2470
3268
|
lastConnectionError = err.message;
|
|
2471
|
-
|
|
3269
|
+
if (!historicalReconcileCompleted) {
|
|
3270
|
+
api.logger.warn(`[AAMP] Startup mailbox reconcile failed: ${err.message} (will retry historical tasks)`);
|
|
3271
|
+
} else {
|
|
3272
|
+
api.logger.warn(`[AAMP] Startup mailbox reconcile failed: ${err.message}`);
|
|
3273
|
+
}
|
|
2472
3274
|
});
|
|
2473
3275
|
transportMonitorTimer = setInterval(() => {
|
|
2474
3276
|
if (!aampClient)
|
|
@@ -2487,9 +3289,14 @@ ${notifyBody?.bodyText ?? help.question}`;
|
|
|
2487
3289
|
reconcileTimer = setInterval(() => {
|
|
2488
3290
|
if (!aampClient)
|
|
2489
3291
|
return;
|
|
2490
|
-
|
|
3292
|
+
const includeHistorical = !historicalReconcileCompleted;
|
|
3293
|
+
void reconcileMailbox(includeHistorical).catch((err) => {
|
|
2491
3294
|
lastConnectionError = err.message;
|
|
2492
|
-
|
|
3295
|
+
if (includeHistorical) {
|
|
3296
|
+
api.logger.warn(`[AAMP] Mailbox reconcile failed while retrying historical tasks: ${err.message}`);
|
|
3297
|
+
} else {
|
|
3298
|
+
api.logger.warn(`[AAMP] Mailbox reconcile failed: ${err.message}`);
|
|
3299
|
+
}
|
|
2493
3300
|
});
|
|
2494
3301
|
}, 15e3);
|
|
2495
3302
|
}
|
|
@@ -2530,7 +3337,10 @@ ${notifyBody?.bodyText ?? help.question}`;
|
|
|
2530
3337
|
return;
|
|
2531
3338
|
api.logger.info(`[AAMP] gateway_start: re-triggering heartbeat for ${pendingTasks.size} pending task(s)`);
|
|
2532
3339
|
try {
|
|
2533
|
-
api.runtime.system.requestHeartbeatNow({
|
|
3340
|
+
api.runtime.system.requestHeartbeatNow({
|
|
3341
|
+
reason: "wake",
|
|
3342
|
+
sessionKey: buildAampWakeSessionKey("queue", "gateway-start")
|
|
3343
|
+
});
|
|
2534
3344
|
} catch (err) {
|
|
2535
3345
|
api.logger.warn(`[AAMP] gateway_start heartbeat failed: ${err.message}`);
|
|
2536
3346
|
}
|
|
@@ -2538,8 +3348,8 @@ ${notifyBody?.bodyText ?? help.question}`;
|
|
|
2538
3348
|
api.on(
|
|
2539
3349
|
"before_prompt_build",
|
|
2540
3350
|
(_event, ctx) => {
|
|
2541
|
-
if (
|
|
2542
|
-
|
|
3351
|
+
if (!isAampSessionKey(ctx?.sessionKey)) {
|
|
3352
|
+
return {};
|
|
2543
3353
|
}
|
|
2544
3354
|
for (const [id, t] of pendingTasks) {
|
|
2545
3355
|
if (hasExpired(t)) {
|
|
@@ -2642,10 +3452,12 @@ ${task.bodyText}` : "",
|
|
|
2642
3452
|
`### Sub-task dispatch rules:`,
|
|
2643
3453
|
`If you delegate work to another agent via aamp_dispatch_task, you MUST pass`,
|
|
2644
3454
|
`parentTaskId: "${task.taskId}" to establish the parent-child relationship.`,
|
|
3455
|
+
`If you need to find a suitable agent first, call aamp_directory_search.`,
|
|
2645
3456
|
``,
|
|
2646
3457
|
`Task ID: ${task.taskId}`,
|
|
2647
3458
|
`From: ${task.from}`,
|
|
2648
3459
|
`Title: ${task.title}`,
|
|
3460
|
+
task.threadContextText ? `${task.threadContextText}` : "",
|
|
2649
3461
|
task.bodyText ? `Description:
|
|
2650
3462
|
${task.bodyText}` : "",
|
|
2651
3463
|
task.contextLinks.length ? `Context Links:
|
|
@@ -2659,6 +3471,44 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
|
|
|
2659
3471
|
},
|
|
2660
3472
|
{ priority: 5 }
|
|
2661
3473
|
);
|
|
3474
|
+
api.registerTool({
|
|
3475
|
+
name: "aamp_directory_search",
|
|
3476
|
+
description: "Search the AAMP directory for agents by capability summary, card text, or email address.",
|
|
3477
|
+
parameters: {
|
|
3478
|
+
type: "object",
|
|
3479
|
+
required: ["query"],
|
|
3480
|
+
properties: {
|
|
3481
|
+
query: { type: "string", description: "Capability or keyword to search for" },
|
|
3482
|
+
limit: { type: "number", description: "Maximum number of matches to return (default: 10)" },
|
|
3483
|
+
includeSelf: { type: "boolean", description: "Whether to include the current agent in results" }
|
|
3484
|
+
}
|
|
3485
|
+
},
|
|
3486
|
+
execute: async (_id, params) => {
|
|
3487
|
+
if (!aampClient) {
|
|
3488
|
+
return { content: [{ type: "text", text: "Error: AAMP client is not connected." }] };
|
|
3489
|
+
}
|
|
3490
|
+
const query = String(params.query ?? "").trim();
|
|
3491
|
+
if (!query) {
|
|
3492
|
+
return { content: [{ type: "text", text: "Error: query is required." }] };
|
|
3493
|
+
}
|
|
3494
|
+
const agents = await aampClient.searchDirectory({
|
|
3495
|
+
query,
|
|
3496
|
+
limit: Number(params.limit ?? 10),
|
|
3497
|
+
includeSelf: Boolean(params.includeSelf)
|
|
3498
|
+
});
|
|
3499
|
+
if (!agents.length) {
|
|
3500
|
+
return { content: [{ type: "text", text: `No agents matched "${query}".` }] };
|
|
3501
|
+
}
|
|
3502
|
+
return {
|
|
3503
|
+
content: [{
|
|
3504
|
+
type: "text",
|
|
3505
|
+
text: agents.map(
|
|
3506
|
+
(agent, index) => `${index + 1}. ${agent.email}${agent.summary ? ` \u2014 ${agent.summary}` : ""}`
|
|
3507
|
+
).join("\n")
|
|
3508
|
+
}]
|
|
3509
|
+
};
|
|
3510
|
+
}
|
|
3511
|
+
}, { name: "aamp_directory_search" });
|
|
2662
3512
|
api.registerTool({
|
|
2663
3513
|
name: "aamp_send_result",
|
|
2664
3514
|
description: "Send the result of an AAMP task back to the dispatcher. Call this after you have finished processing the task.",
|
|
@@ -2758,6 +3608,18 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
|
|
|
2758
3608
|
content: readBinaryFile(a.path)
|
|
2759
3609
|
}));
|
|
2760
3610
|
}
|
|
3611
|
+
await appendTaskStream(task.taskId, "status", {
|
|
3612
|
+
state: "completing",
|
|
3613
|
+
label: `Sending ${p.status} result`
|
|
3614
|
+
});
|
|
3615
|
+
if (p.output) {
|
|
3616
|
+
await appendTaskStream(task.taskId, "text.delta", { text: p.output });
|
|
3617
|
+
}
|
|
3618
|
+
await closeTaskStream(task.taskId, {
|
|
3619
|
+
reason: "task.result",
|
|
3620
|
+
status: p.status,
|
|
3621
|
+
...p.errorMsg ? { error: p.errorMsg } : {}
|
|
3622
|
+
});
|
|
2761
3623
|
await aampClient.sendResult({
|
|
2762
3624
|
to: task.from,
|
|
2763
3625
|
taskId: task.taskId,
|
|
@@ -2773,7 +3635,10 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
|
|
|
2773
3635
|
api.logger.info(`[AAMP] \u2192 task.result ${task.taskId} ${p.status}`);
|
|
2774
3636
|
if (pendingTasks.size > 0) {
|
|
2775
3637
|
try {
|
|
2776
|
-
api.runtime.system.requestHeartbeatNow({
|
|
3638
|
+
api.runtime.system.requestHeartbeatNow({
|
|
3639
|
+
reason: "wake",
|
|
3640
|
+
sessionKey: buildAampWakeSessionKey("queue", "follow-up")
|
|
3641
|
+
});
|
|
2777
3642
|
} catch {
|
|
2778
3643
|
}
|
|
2779
3644
|
}
|
|
@@ -2824,6 +3689,13 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
|
|
|
2824
3689
|
if (!aampClient?.isConnected()) {
|
|
2825
3690
|
return { content: [{ type: "text", text: "Error: AAMP client is not connected." }] };
|
|
2826
3691
|
}
|
|
3692
|
+
await appendTaskStream(task.taskId, "status", {
|
|
3693
|
+
state: "help_needed",
|
|
3694
|
+
label: p.blockedReason
|
|
3695
|
+
});
|
|
3696
|
+
await closeTaskStream(task.taskId, {
|
|
3697
|
+
reason: "task.help_needed"
|
|
3698
|
+
});
|
|
2827
3699
|
await aampClient.sendHelp({
|
|
2828
3700
|
to: task.from,
|
|
2829
3701
|
taskId: task.taskId,
|