aamp-openclaw-plugin 0.1.29 → 0.1.30
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 +879 -120
- 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,7 +1774,61 @@ ${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;
|
|
@@ -1521,6 +1885,9 @@ var AampClient = class _AampClient extends TinyEmitter {
|
|
|
1521
1885
|
this.jmapClient.on("task.ack", (ack) => {
|
|
1522
1886
|
this.emit("task.ack", ack);
|
|
1523
1887
|
});
|
|
1888
|
+
this.jmapClient.on("task.stream.opened", (stream) => {
|
|
1889
|
+
this.emit("task.stream.opened", stream);
|
|
1890
|
+
});
|
|
1524
1891
|
this.jmapClient.on("card.query", (query) => {
|
|
1525
1892
|
this.emit("card.query", query);
|
|
1526
1893
|
});
|
|
@@ -1679,6 +2046,9 @@ var AampClient = class _AampClient extends TinyEmitter {
|
|
|
1679
2046
|
async sendHelp(opts) {
|
|
1680
2047
|
return this.smtpSender.sendHelp(opts);
|
|
1681
2048
|
}
|
|
2049
|
+
async sendStreamOpened(opts) {
|
|
2050
|
+
return this.smtpSender.sendStreamOpened(opts);
|
|
2051
|
+
}
|
|
1682
2052
|
async sendCardQuery(opts) {
|
|
1683
2053
|
return this.smtpSender.sendCardQuery(opts);
|
|
1684
2054
|
}
|
|
@@ -1740,6 +2110,194 @@ var AampClient = class _AampClient extends TinyEmitter {
|
|
|
1740
2110
|
const data = await res.json();
|
|
1741
2111
|
return data.agents;
|
|
1742
2112
|
}
|
|
2113
|
+
async getThreadHistory(taskId, opts = {}) {
|
|
2114
|
+
const base = this.config.baseUrl;
|
|
2115
|
+
const mailboxToken = this.config.mailboxToken;
|
|
2116
|
+
const res = await _AampClient.callDiscoveredApi(base, {
|
|
2117
|
+
action: "aamp.mailbox.thread",
|
|
2118
|
+
authToken: mailboxToken,
|
|
2119
|
+
query: {
|
|
2120
|
+
taskId,
|
|
2121
|
+
includeStreamOpened: opts.includeStreamOpened
|
|
2122
|
+
}
|
|
2123
|
+
});
|
|
2124
|
+
if (!res.ok) {
|
|
2125
|
+
const body = await res.text().catch(() => "");
|
|
2126
|
+
throw new Error(`Thread history fetch failed: ${res.status} ${body || res.statusText}`);
|
|
2127
|
+
}
|
|
2128
|
+
const data = await res.json();
|
|
2129
|
+
return {
|
|
2130
|
+
taskId: data.taskId,
|
|
2131
|
+
events: Array.isArray(data.events) ? data.events : []
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
async hydrateTaskDispatch(task) {
|
|
2135
|
+
const history = await this.getThreadHistory(task.taskId);
|
|
2136
|
+
const priorEvents = history.events.filter((event) => event.messageId !== task.messageId);
|
|
2137
|
+
return {
|
|
2138
|
+
...task,
|
|
2139
|
+
threadHistory: priorEvents,
|
|
2140
|
+
threadContextText: renderThreadHistoryForAgent(priorEvents)
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
async resolveStreamCapability() {
|
|
2144
|
+
const discovery = await _AampClient.discoverAampService(this.config.baseUrl);
|
|
2145
|
+
const stream = discovery.capabilities?.stream;
|
|
2146
|
+
if (!stream?.transport) {
|
|
2147
|
+
throw new Error("AAMP stream capability is not available on this service");
|
|
2148
|
+
}
|
|
2149
|
+
return stream;
|
|
2150
|
+
}
|
|
2151
|
+
async createStream(opts) {
|
|
2152
|
+
const stream = await this.resolveStreamCapability();
|
|
2153
|
+
const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
|
|
2154
|
+
action: stream.createAction ?? "aamp.stream.create",
|
|
2155
|
+
method: "POST",
|
|
2156
|
+
authToken: this.config.mailboxToken,
|
|
2157
|
+
body: opts
|
|
2158
|
+
});
|
|
2159
|
+
if (!res.ok) {
|
|
2160
|
+
const body = await res.text().catch(() => "");
|
|
2161
|
+
throw new Error(`AAMP stream create failed: ${res.status} ${body || res.statusText}`);
|
|
2162
|
+
}
|
|
2163
|
+
return res.json();
|
|
2164
|
+
}
|
|
2165
|
+
async appendStreamEvent(opts) {
|
|
2166
|
+
const stream = await this.resolveStreamCapability();
|
|
2167
|
+
const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
|
|
2168
|
+
action: stream.appendAction ?? "aamp.stream.append",
|
|
2169
|
+
method: "POST",
|
|
2170
|
+
authToken: this.config.mailboxToken,
|
|
2171
|
+
body: opts
|
|
2172
|
+
});
|
|
2173
|
+
if (!res.ok) {
|
|
2174
|
+
const body = await res.text().catch(() => "");
|
|
2175
|
+
throw new Error(`AAMP stream append failed: ${res.status} ${body || res.statusText}`);
|
|
2176
|
+
}
|
|
2177
|
+
return res.json();
|
|
2178
|
+
}
|
|
2179
|
+
async closeStream(opts) {
|
|
2180
|
+
const stream = await this.resolveStreamCapability();
|
|
2181
|
+
const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
|
|
2182
|
+
action: stream.closeAction ?? "aamp.stream.close",
|
|
2183
|
+
method: "POST",
|
|
2184
|
+
authToken: this.config.mailboxToken,
|
|
2185
|
+
body: opts
|
|
2186
|
+
});
|
|
2187
|
+
if (!res.ok) {
|
|
2188
|
+
const body = await res.text().catch(() => "");
|
|
2189
|
+
throw new Error(`AAMP stream close failed: ${res.status} ${body || res.statusText}`);
|
|
2190
|
+
}
|
|
2191
|
+
return res.json();
|
|
2192
|
+
}
|
|
2193
|
+
async getTaskStream(opts) {
|
|
2194
|
+
const stream = await this.resolveStreamCapability();
|
|
2195
|
+
const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
|
|
2196
|
+
action: stream.getAction ?? "aamp.stream.get",
|
|
2197
|
+
authToken: this.config.mailboxToken,
|
|
2198
|
+
query: {
|
|
2199
|
+
...opts.taskId ? { taskId: opts.taskId } : {},
|
|
2200
|
+
...opts.streamId ? { streamId: opts.streamId } : {}
|
|
2201
|
+
}
|
|
2202
|
+
});
|
|
2203
|
+
if (res.status === 404)
|
|
2204
|
+
return null;
|
|
2205
|
+
if (!res.ok) {
|
|
2206
|
+
const body = await res.text().catch(() => "");
|
|
2207
|
+
throw new Error(`AAMP stream get failed: ${res.status} ${body || res.statusText}`);
|
|
2208
|
+
}
|
|
2209
|
+
return res.json();
|
|
2210
|
+
}
|
|
2211
|
+
async subscribeStream(streamId, handlers, opts = {}) {
|
|
2212
|
+
const stream = await this.resolveStreamCapability();
|
|
2213
|
+
const template = stream.subscribeUrlTemplate;
|
|
2214
|
+
if (!template)
|
|
2215
|
+
throw new Error("AAMP stream subscribeUrlTemplate is missing");
|
|
2216
|
+
const url = new URL(template.replace("{streamId}", encodeURIComponent(streamId)), this.config.baseUrl);
|
|
2217
|
+
if (opts.lastEventId) {
|
|
2218
|
+
url.searchParams.set("lastEventId", opts.lastEventId);
|
|
2219
|
+
}
|
|
2220
|
+
const controller = new AbortController();
|
|
2221
|
+
if (opts.signal) {
|
|
2222
|
+
opts.signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
2223
|
+
}
|
|
2224
|
+
const res = await fetch(url, {
|
|
2225
|
+
headers: {
|
|
2226
|
+
Authorization: `Basic ${this.config.mailboxToken}`,
|
|
2227
|
+
Accept: "text/event-stream"
|
|
2228
|
+
},
|
|
2229
|
+
signal: controller.signal
|
|
2230
|
+
});
|
|
2231
|
+
if (!res.ok || !res.body) {
|
|
2232
|
+
throw new Error(`AAMP stream subscribe failed: ${res.status} ${res.statusText}`);
|
|
2233
|
+
}
|
|
2234
|
+
handlers.onOpen?.();
|
|
2235
|
+
const reader = res.body.getReader();
|
|
2236
|
+
const decoder = new TextDecoder();
|
|
2237
|
+
let buffer = "";
|
|
2238
|
+
let currentEvent = "message";
|
|
2239
|
+
let currentId = "";
|
|
2240
|
+
let currentData = [];
|
|
2241
|
+
const flush = () => {
|
|
2242
|
+
if (!currentData.length)
|
|
2243
|
+
return;
|
|
2244
|
+
try {
|
|
2245
|
+
const parsed = JSON.parse(currentData.join("\n"));
|
|
2246
|
+
handlers.onEvent({
|
|
2247
|
+
...parsed,
|
|
2248
|
+
...currentId ? { id: currentId } : {},
|
|
2249
|
+
type: parsed.type ?? currentEvent
|
|
2250
|
+
});
|
|
2251
|
+
} catch (err) {
|
|
2252
|
+
handlers.onError?.(err);
|
|
2253
|
+
} finally {
|
|
2254
|
+
currentEvent = "message";
|
|
2255
|
+
currentId = "";
|
|
2256
|
+
currentData = [];
|
|
2257
|
+
}
|
|
2258
|
+
};
|
|
2259
|
+
void (async () => {
|
|
2260
|
+
try {
|
|
2261
|
+
while (true) {
|
|
2262
|
+
const { value, done } = await reader.read();
|
|
2263
|
+
if (done)
|
|
2264
|
+
break;
|
|
2265
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2266
|
+
let index = buffer.indexOf("\n\n");
|
|
2267
|
+
while (index >= 0) {
|
|
2268
|
+
const frame = buffer.slice(0, index);
|
|
2269
|
+
buffer = buffer.slice(index + 2);
|
|
2270
|
+
for (const rawLine of frame.split("\n")) {
|
|
2271
|
+
const line = rawLine.replace(/\r$/, "");
|
|
2272
|
+
if (!line || line.startsWith(":"))
|
|
2273
|
+
continue;
|
|
2274
|
+
if (line.startsWith("event:")) {
|
|
2275
|
+
currentEvent = line.slice(6).trim();
|
|
2276
|
+
} else if (line.startsWith("id:")) {
|
|
2277
|
+
currentId = line.slice(3).trim();
|
|
2278
|
+
} else if (line.startsWith("data:")) {
|
|
2279
|
+
currentData.push(line.slice(5).trimStart());
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
flush();
|
|
2283
|
+
index = buffer.indexOf("\n\n");
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
} catch (err) {
|
|
2287
|
+
if (!controller.signal.aborted) {
|
|
2288
|
+
handlers.onError?.(err);
|
|
2289
|
+
}
|
|
2290
|
+
} finally {
|
|
2291
|
+
buffer += decoder.decode();
|
|
2292
|
+
controller.abort();
|
|
2293
|
+
}
|
|
2294
|
+
})();
|
|
2295
|
+
return {
|
|
2296
|
+
close() {
|
|
2297
|
+
controller.abort();
|
|
2298
|
+
}
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
1743
2301
|
/**
|
|
1744
2302
|
* Download a blob (attachment) by its JMAP blobId.
|
|
1745
2303
|
* Use this to retrieve attachment content from received TaskDispatch or TaskResult messages.
|
|
@@ -1767,6 +2325,9 @@ var AampClient = class _AampClient extends TinyEmitter {
|
|
|
1767
2325
|
}
|
|
1768
2326
|
};
|
|
1769
2327
|
|
|
2328
|
+
// src/index.ts
|
|
2329
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
2330
|
+
|
|
1770
2331
|
// src/file-store.ts
|
|
1771
2332
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1772
2333
|
import { dirname, join } from "node:path";
|
|
@@ -1873,6 +2434,7 @@ function baseUrl(aampHost) {
|
|
|
1873
2434
|
return `https://${aampHost}`;
|
|
1874
2435
|
}
|
|
1875
2436
|
var pendingTasks = /* @__PURE__ */ new Map();
|
|
2437
|
+
var activeTaskStreams = /* @__PURE__ */ new Map();
|
|
1876
2438
|
var terminalTaskIds = new Set(loadTaskState(defaultTaskStatePath()).terminalTaskIds ?? []);
|
|
1877
2439
|
var dispatchedSubtasks = /* @__PURE__ */ new Map();
|
|
1878
2440
|
var waitingDispatches = /* @__PURE__ */ new Map();
|
|
@@ -1884,9 +2446,58 @@ var lastTransportMode = "disconnected";
|
|
|
1884
2446
|
var lastLoggedTransportMode = "disconnected";
|
|
1885
2447
|
var reconcileTimer = null;
|
|
1886
2448
|
var transportMonitorTimer = null;
|
|
2449
|
+
var historicalReconcileCompleted = false;
|
|
1887
2450
|
var currentSessionKey = "agent:main:main";
|
|
1888
2451
|
var channelRuntime = null;
|
|
1889
2452
|
var channelCfg = null;
|
|
2453
|
+
async function ensureTaskStream(task) {
|
|
2454
|
+
if (!aampClient?.isConnected())
|
|
2455
|
+
return null;
|
|
2456
|
+
const existing = activeTaskStreams.get(task.taskId);
|
|
2457
|
+
if (existing)
|
|
2458
|
+
return existing;
|
|
2459
|
+
const created = await aampClient.createStream({
|
|
2460
|
+
taskId: task.taskId,
|
|
2461
|
+
peerEmail: task.from
|
|
2462
|
+
});
|
|
2463
|
+
await aampClient.sendStreamOpened({
|
|
2464
|
+
to: task.from,
|
|
2465
|
+
taskId: task.taskId,
|
|
2466
|
+
streamId: created.streamId,
|
|
2467
|
+
inReplyTo: task.messageId || void 0
|
|
2468
|
+
});
|
|
2469
|
+
await aampClient.appendStreamEvent({
|
|
2470
|
+
streamId: created.streamId,
|
|
2471
|
+
type: "status",
|
|
2472
|
+
payload: { state: "running", label: "Task queued in OpenClaw" }
|
|
2473
|
+
});
|
|
2474
|
+
activeTaskStreams.set(task.taskId, created.streamId);
|
|
2475
|
+
return created.streamId;
|
|
2476
|
+
}
|
|
2477
|
+
async function appendTaskStream(taskId, type, payload) {
|
|
2478
|
+
if (!aampClient?.isConnected())
|
|
2479
|
+
return;
|
|
2480
|
+
const streamId = activeTaskStreams.get(taskId);
|
|
2481
|
+
if (!streamId)
|
|
2482
|
+
return;
|
|
2483
|
+
await aampClient.appendStreamEvent({
|
|
2484
|
+
streamId,
|
|
2485
|
+
type,
|
|
2486
|
+
payload
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
async function closeTaskStream(taskId, payload) {
|
|
2490
|
+
if (!aampClient?.isConnected())
|
|
2491
|
+
return;
|
|
2492
|
+
const streamId = activeTaskStreams.get(taskId);
|
|
2493
|
+
if (!streamId)
|
|
2494
|
+
return;
|
|
2495
|
+
activeTaskStreams.delete(taskId);
|
|
2496
|
+
await aampClient.closeStream({
|
|
2497
|
+
streamId,
|
|
2498
|
+
payload
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
1890
2501
|
function logTransportState(api, mode, email, previousMode) {
|
|
1891
2502
|
if (mode === previousMode)
|
|
1892
2503
|
return;
|
|
@@ -1928,6 +2539,17 @@ function hasExpired(task) {
|
|
|
1928
2539
|
}
|
|
1929
2540
|
return false;
|
|
1930
2541
|
}
|
|
2542
|
+
function isTransientTransportError(message) {
|
|
2543
|
+
return [
|
|
2544
|
+
"ECONNRESET",
|
|
2545
|
+
"ETIMEDOUT",
|
|
2546
|
+
"ECONNREFUSED",
|
|
2547
|
+
"EPIPE",
|
|
2548
|
+
"UND_ERR_SOCKET",
|
|
2549
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
2550
|
+
"fetch failed"
|
|
2551
|
+
].some((needle) => message.includes(needle));
|
|
2552
|
+
}
|
|
1931
2553
|
function nextPendingEntry() {
|
|
1932
2554
|
const entries = [...pendingTasks.entries()];
|
|
1933
2555
|
const notifications = entries.filter(([key]) => key.startsWith("result:") || key.startsWith("help:"));
|
|
@@ -1950,6 +2572,8 @@ function queuePendingTask(task) {
|
|
|
1950
2572
|
from: task.from,
|
|
1951
2573
|
title: task.title,
|
|
1952
2574
|
bodyText: task.bodyText ?? "",
|
|
2575
|
+
threadHistory: task.threadHistory ?? [],
|
|
2576
|
+
threadContextText: task.threadContextText ?? "",
|
|
1953
2577
|
priority: task.priority ?? "normal",
|
|
1954
2578
|
...task.expiresAt ? { expiresAt: task.expiresAt } : {},
|
|
1955
2579
|
contextLinks: task.contextLinks ?? [],
|
|
@@ -2023,6 +2647,18 @@ var src_default = {
|
|
|
2023
2647
|
default: "openclaw-agent",
|
|
2024
2648
|
description: "Agent name prefix used in the mailbox address"
|
|
2025
2649
|
},
|
|
2650
|
+
summary: {
|
|
2651
|
+
type: "string",
|
|
2652
|
+
description: "Directory summary shown when other agents search for this agent."
|
|
2653
|
+
},
|
|
2654
|
+
cardText: {
|
|
2655
|
+
type: "string",
|
|
2656
|
+
description: "Inline card text used for automatic card.response replies."
|
|
2657
|
+
},
|
|
2658
|
+
cardFile: {
|
|
2659
|
+
type: "string",
|
|
2660
|
+
description: "Absolute path to a card text file. Used when cardText is not set."
|
|
2661
|
+
},
|
|
2026
2662
|
credentialsFile: {
|
|
2027
2663
|
type: "string",
|
|
2028
2664
|
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,6 +2725,29 @@ var src_default = {
|
|
|
2089
2725
|
api.logger.warn(`[AAMP] Could not trigger heartbeat for ${label}: ${err.message}`);
|
|
2090
2726
|
}
|
|
2091
2727
|
}
|
|
2728
|
+
function getConfiguredCardText() {
|
|
2729
|
+
const inline = cfg.cardText?.trim();
|
|
2730
|
+
if (inline)
|
|
2731
|
+
return inline;
|
|
2732
|
+
const file = cfg.cardFile?.trim();
|
|
2733
|
+
if (!file)
|
|
2734
|
+
return void 0;
|
|
2735
|
+
const fromFile = readFileSync2(file, "utf-8").trim();
|
|
2736
|
+
return fromFile || void 0;
|
|
2737
|
+
}
|
|
2738
|
+
async function syncDirectoryProfile() {
|
|
2739
|
+
if (!aampClient)
|
|
2740
|
+
return;
|
|
2741
|
+
const summary = cfg.summary?.trim();
|
|
2742
|
+
const cardText = getConfiguredCardText();
|
|
2743
|
+
if (!summary && !cardText)
|
|
2744
|
+
return;
|
|
2745
|
+
await aampClient.updateDirectoryProfile({
|
|
2746
|
+
...summary ? { summary } : {},
|
|
2747
|
+
...cardText ? { cardText } : {}
|
|
2748
|
+
});
|
|
2749
|
+
api.logger.info(`[AAMP] Directory profile synced${cardText ? " (card text registered)" : ""}`);
|
|
2750
|
+
}
|
|
2092
2751
|
function wakeAgentForPendingTask(task) {
|
|
2093
2752
|
const fallback = () => triggerHeartbeatWake(currentSessionKey, `task ${task.taskId}`);
|
|
2094
2753
|
const dispatcher = channelRuntime?.reply?.dispatchReplyWithBufferedBlockDispatcher;
|
|
@@ -2145,6 +2804,16 @@ var src_default = {
|
|
|
2145
2804
|
fallback();
|
|
2146
2805
|
}
|
|
2147
2806
|
}
|
|
2807
|
+
async function reconcileMailbox(includeHistorical) {
|
|
2808
|
+
if (!aampClient)
|
|
2809
|
+
return;
|
|
2810
|
+
const opts = includeHistorical ? { includeHistorical: true } : void 0;
|
|
2811
|
+
const count = await aampClient.reconcileRecentEmails(100, opts);
|
|
2812
|
+
if (includeHistorical && !historicalReconcileCompleted) {
|
|
2813
|
+
historicalReconcileCompleted = true;
|
|
2814
|
+
api.logger.info(`[AAMP] Historical mailbox reconcile complete (${count} email(s) scanned)`);
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2148
2817
|
async function doConnect(identity) {
|
|
2149
2818
|
if (reconcileTimer) {
|
|
2150
2819
|
clearInterval(reconcileTimer);
|
|
@@ -2171,36 +2840,49 @@ var src_default = {
|
|
|
2171
2840
|
});
|
|
2172
2841
|
aampClient.on("task.dispatch", (task) => {
|
|
2173
2842
|
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
|
-
|
|
2843
|
+
void (async () => {
|
|
2844
|
+
try {
|
|
2845
|
+
if (terminalTaskIds.has(task.taskId)) {
|
|
2846
|
+
api.logger.info(`[AAMP] Skipping already-terminal task ${task.taskId}`);
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
const decision = matchSenderPolicy(task, cfg.senderPolicies);
|
|
2850
|
+
if (!decision.allowed) {
|
|
2851
|
+
api.logger.warn(`[AAMP] \u2717 rejected by senderPolicies: ${task.from} task=${task.taskId} reason=${decision.reason}`);
|
|
2852
|
+
void aampClient.sendResult({
|
|
2853
|
+
to: task.from,
|
|
2854
|
+
taskId: task.taskId,
|
|
2855
|
+
status: "rejected",
|
|
2856
|
+
output: "",
|
|
2857
|
+
errorMsg: decision.reason ?? `Sender ${task.from} is not allowed.`
|
|
2858
|
+
}).catch((err) => {
|
|
2859
|
+
api.logger.error(`[AAMP] Failed to send rejection for task ${task.taskId}: ${err.message}`);
|
|
2860
|
+
});
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2863
|
+
const hydratedTask = await aampClient.hydrateTaskDispatch(task).catch((err) => {
|
|
2864
|
+
api.logger.warn(`[AAMP] Failed to load thread history for ${task.taskId}: ${err.message}`);
|
|
2865
|
+
return {
|
|
2866
|
+
...task,
|
|
2867
|
+
threadHistory: [],
|
|
2868
|
+
threadContextText: ""
|
|
2869
|
+
};
|
|
2190
2870
|
});
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2871
|
+
if (!queuePendingTask(hydratedTask)) {
|
|
2872
|
+
api.logger.info(`[AAMP] Ignoring already-terminal or expired task ${task.taskId}`);
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
void ensureTaskStream(pendingTasks.get(task.taskId)).catch((err) => {
|
|
2876
|
+
api.logger.warn(`[AAMP] Failed to open stream for task ${task.taskId}: ${err.message}`);
|
|
2877
|
+
});
|
|
2878
|
+
wakeAgentForPendingTask(pendingTasks.get(task.taskId));
|
|
2879
|
+
} catch (err) {
|
|
2880
|
+
api.logger.error(`[AAMP] task.dispatch handler failed for ${task.taskId}: ${err.message}`);
|
|
2881
|
+
if (pendingTasks.has(task.taskId)) {
|
|
2882
|
+
triggerHeartbeatWake(currentSessionKey, `task ${task.taskId}`);
|
|
2883
|
+
}
|
|
2202
2884
|
}
|
|
2203
|
-
}
|
|
2885
|
+
})();
|
|
2204
2886
|
});
|
|
2205
2887
|
aampClient.on("task.cancel", (cancel) => {
|
|
2206
2888
|
api.logger.info(`[AAMP] \u2190 task.cancel ${cancel.taskId} from=${cancel.from}`);
|
|
@@ -2210,6 +2892,8 @@ var src_default = {
|
|
|
2210
2892
|
dispatchedSubtasks.delete(cancel.taskId);
|
|
2211
2893
|
waitingDispatches.delete(cancel.taskId);
|
|
2212
2894
|
rememberTerminalTask(cancel.taskId);
|
|
2895
|
+
void closeTaskStream(cancel.taskId, { reason: "task.cancel" }).catch(() => {
|
|
2896
|
+
});
|
|
2213
2897
|
if (removed) {
|
|
2214
2898
|
api.logger.info(`[AAMP] Cancelled task ${cancel.taskId} \u2014 removed from pending queue`);
|
|
2215
2899
|
}
|
|
@@ -2441,9 +3125,16 @@ ${notifyBody?.bodyText ?? help.question}`;
|
|
|
2441
3125
|
}
|
|
2442
3126
|
return;
|
|
2443
3127
|
}
|
|
3128
|
+
if (err.message.startsWith("Safety reconcile failed:") && isTransientTransportError(err.message)) {
|
|
3129
|
+
api.logger.warn(`[AAMP] ${err.message}`);
|
|
3130
|
+
return;
|
|
3131
|
+
}
|
|
2444
3132
|
api.logger.error(`[AAMP] ${err.message}`);
|
|
2445
3133
|
});
|
|
2446
3134
|
await aampClient.connect();
|
|
3135
|
+
await syncDirectoryProfile().catch((err) => {
|
|
3136
|
+
api.logger.warn(`[AAMP] Directory profile sync failed: ${err.message}`);
|
|
3137
|
+
});
|
|
2447
3138
|
api.logger.info(
|
|
2448
3139
|
`[AAMP] Transport after connect \u2014 ${aampClient.isUsingPollingFallback() ? "polling fallback" : "websocket"} as ${agentEmail}`
|
|
2449
3140
|
);
|
|
@@ -2466,9 +3157,13 @@ ${notifyBody?.bodyText ?? help.question}`;
|
|
|
2466
3157
|
lastTransportMode = mode;
|
|
2467
3158
|
lastLoggedTransportMode = mode;
|
|
2468
3159
|
}, 1e3);
|
|
2469
|
-
void
|
|
3160
|
+
void reconcileMailbox(!historicalReconcileCompleted).catch((err) => {
|
|
2470
3161
|
lastConnectionError = err.message;
|
|
2471
|
-
|
|
3162
|
+
if (!historicalReconcileCompleted) {
|
|
3163
|
+
api.logger.warn(`[AAMP] Startup mailbox reconcile failed: ${err.message} (will retry historical tasks)`);
|
|
3164
|
+
} else {
|
|
3165
|
+
api.logger.warn(`[AAMP] Startup mailbox reconcile failed: ${err.message}`);
|
|
3166
|
+
}
|
|
2472
3167
|
});
|
|
2473
3168
|
transportMonitorTimer = setInterval(() => {
|
|
2474
3169
|
if (!aampClient)
|
|
@@ -2487,9 +3182,14 @@ ${notifyBody?.bodyText ?? help.question}`;
|
|
|
2487
3182
|
reconcileTimer = setInterval(() => {
|
|
2488
3183
|
if (!aampClient)
|
|
2489
3184
|
return;
|
|
2490
|
-
|
|
3185
|
+
const includeHistorical = !historicalReconcileCompleted;
|
|
3186
|
+
void reconcileMailbox(includeHistorical).catch((err) => {
|
|
2491
3187
|
lastConnectionError = err.message;
|
|
2492
|
-
|
|
3188
|
+
if (includeHistorical) {
|
|
3189
|
+
api.logger.warn(`[AAMP] Mailbox reconcile failed while retrying historical tasks: ${err.message}`);
|
|
3190
|
+
} else {
|
|
3191
|
+
api.logger.warn(`[AAMP] Mailbox reconcile failed: ${err.message}`);
|
|
3192
|
+
}
|
|
2493
3193
|
});
|
|
2494
3194
|
}, 15e3);
|
|
2495
3195
|
}
|
|
@@ -2642,10 +3342,12 @@ ${task.bodyText}` : "",
|
|
|
2642
3342
|
`### Sub-task dispatch rules:`,
|
|
2643
3343
|
`If you delegate work to another agent via aamp_dispatch_task, you MUST pass`,
|
|
2644
3344
|
`parentTaskId: "${task.taskId}" to establish the parent-child relationship.`,
|
|
3345
|
+
`If you need to find a suitable agent first, call aamp_directory_search.`,
|
|
2645
3346
|
``,
|
|
2646
3347
|
`Task ID: ${task.taskId}`,
|
|
2647
3348
|
`From: ${task.from}`,
|
|
2648
3349
|
`Title: ${task.title}`,
|
|
3350
|
+
task.threadContextText ? `${task.threadContextText}` : "",
|
|
2649
3351
|
task.bodyText ? `Description:
|
|
2650
3352
|
${task.bodyText}` : "",
|
|
2651
3353
|
task.contextLinks.length ? `Context Links:
|
|
@@ -2659,6 +3361,44 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
|
|
|
2659
3361
|
},
|
|
2660
3362
|
{ priority: 5 }
|
|
2661
3363
|
);
|
|
3364
|
+
api.registerTool({
|
|
3365
|
+
name: "aamp_directory_search",
|
|
3366
|
+
description: "Search the AAMP directory for agents by capability summary, card text, or email address.",
|
|
3367
|
+
parameters: {
|
|
3368
|
+
type: "object",
|
|
3369
|
+
required: ["query"],
|
|
3370
|
+
properties: {
|
|
3371
|
+
query: { type: "string", description: "Capability or keyword to search for" },
|
|
3372
|
+
limit: { type: "number", description: "Maximum number of matches to return (default: 10)" },
|
|
3373
|
+
includeSelf: { type: "boolean", description: "Whether to include the current agent in results" }
|
|
3374
|
+
}
|
|
3375
|
+
},
|
|
3376
|
+
execute: async (_id, params) => {
|
|
3377
|
+
if (!aampClient) {
|
|
3378
|
+
return { content: [{ type: "text", text: "Error: AAMP client is not connected." }] };
|
|
3379
|
+
}
|
|
3380
|
+
const query = String(params.query ?? "").trim();
|
|
3381
|
+
if (!query) {
|
|
3382
|
+
return { content: [{ type: "text", text: "Error: query is required." }] };
|
|
3383
|
+
}
|
|
3384
|
+
const agents = await aampClient.searchDirectory({
|
|
3385
|
+
query,
|
|
3386
|
+
limit: Number(params.limit ?? 10),
|
|
3387
|
+
includeSelf: Boolean(params.includeSelf)
|
|
3388
|
+
});
|
|
3389
|
+
if (!agents.length) {
|
|
3390
|
+
return { content: [{ type: "text", text: `No agents matched "${query}".` }] };
|
|
3391
|
+
}
|
|
3392
|
+
return {
|
|
3393
|
+
content: [{
|
|
3394
|
+
type: "text",
|
|
3395
|
+
text: agents.map(
|
|
3396
|
+
(agent, index) => `${index + 1}. ${agent.email}${agent.summary ? ` \u2014 ${agent.summary}` : ""}`
|
|
3397
|
+
).join("\n")
|
|
3398
|
+
}]
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
}, { name: "aamp_directory_search" });
|
|
2662
3402
|
api.registerTool({
|
|
2663
3403
|
name: "aamp_send_result",
|
|
2664
3404
|
description: "Send the result of an AAMP task back to the dispatcher. Call this after you have finished processing the task.",
|
|
@@ -2758,6 +3498,18 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
|
|
|
2758
3498
|
content: readBinaryFile(a.path)
|
|
2759
3499
|
}));
|
|
2760
3500
|
}
|
|
3501
|
+
await appendTaskStream(task.taskId, "status", {
|
|
3502
|
+
state: "completing",
|
|
3503
|
+
label: `Sending ${p.status} result`
|
|
3504
|
+
});
|
|
3505
|
+
if (p.output) {
|
|
3506
|
+
await appendTaskStream(task.taskId, "text.delta", { text: p.output });
|
|
3507
|
+
}
|
|
3508
|
+
await closeTaskStream(task.taskId, {
|
|
3509
|
+
reason: "task.result",
|
|
3510
|
+
status: p.status,
|
|
3511
|
+
...p.errorMsg ? { error: p.errorMsg } : {}
|
|
3512
|
+
});
|
|
2761
3513
|
await aampClient.sendResult({
|
|
2762
3514
|
to: task.from,
|
|
2763
3515
|
taskId: task.taskId,
|
|
@@ -2824,6 +3576,13 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
|
|
|
2824
3576
|
if (!aampClient?.isConnected()) {
|
|
2825
3577
|
return { content: [{ type: "text", text: "Error: AAMP client is not connected." }] };
|
|
2826
3578
|
}
|
|
3579
|
+
await appendTaskStream(task.taskId, "status", {
|
|
3580
|
+
state: "help_needed",
|
|
3581
|
+
label: p.blockedReason
|
|
3582
|
+
});
|
|
3583
|
+
await closeTaskStream(task.taskId, {
|
|
3584
|
+
reason: "task.help_needed"
|
|
3585
|
+
});
|
|
2827
3586
|
await aampClient.sendHelp({
|
|
2828
3587
|
to: task.from,
|
|
2829
3588
|
taskId: task.taskId,
|