aamp-openclaw-plugin 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7,9 +7,10 @@ var AAMP_HEADER = {
7
7
  VERSION: "X-AAMP-Version",
8
8
  INTENT: "X-AAMP-Intent",
9
9
  TASK_ID: "X-AAMP-TaskId",
10
- TIMEOUT: "X-AAMP-Timeout",
11
10
  CONTEXT_LINKS: "X-AAMP-ContextLinks",
12
11
  DISPATCH_CONTEXT: "X-AAMP-Dispatch-Context",
12
+ PRIORITY: "X-AAMP-Priority",
13
+ EXPIRES_AT: "X-AAMP-Expires-At",
13
14
  STATUS: "X-AAMP-Status",
14
15
  OUTPUT: "X-AAMP-Output",
15
16
  ERROR_MSG: "X-AAMP-ErrorMsg",
@@ -17,10 +18,59 @@ var AAMP_HEADER = {
17
18
  QUESTION: "X-AAMP-Question",
18
19
  BLOCKED_REASON: "X-AAMP-BlockedReason",
19
20
  SUGGESTED_OPTIONS: "X-AAMP-SuggestedOptions",
20
- PARENT_TASK_ID: "X-AAMP-ParentTaskId"
21
+ PARENT_TASK_ID: "X-AAMP-ParentTaskId",
22
+ CARD_SUMMARY: "X-AAMP-Card-Summary"
21
23
  };
22
24
 
23
25
  // ../sdk/src/parser.ts
26
+ function normalizeBodyText(value) {
27
+ return value?.replace(/\r\n/g, "\n").trim() ?? "";
28
+ }
29
+ function escapeRegex(value) {
30
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31
+ }
32
+ function extractBodySection(bodyText, label, nextLabels) {
33
+ if (!bodyText)
34
+ return "";
35
+ 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
+ );
40
+ const match = pattern.exec(bodyText);
41
+ return match?.[1]?.trim() ?? "";
42
+ }
43
+ function parseSuggestedOptionsBlock(block) {
44
+ if (!block.trim())
45
+ return [];
46
+ return block.split("\n").map((line) => line.replace(/^\s*(?:[-*]|\d+\.)\s*/, "").trim()).filter(Boolean);
47
+ }
48
+ function parseTaskResultBody(bodyText) {
49
+ const normalized = normalizeBodyText(bodyText);
50
+ if (!normalized)
51
+ return { output: "" };
52
+ const output = extractBodySection(normalized, "Output", ["Error"]);
53
+ const errorMsg = extractBodySection(normalized, "Error", []);
54
+ if (output || errorMsg) {
55
+ return { output, ...errorMsg ? { errorMsg } : {} };
56
+ }
57
+ return { output: normalized };
58
+ }
59
+ function parseTaskHelpBody(bodyText) {
60
+ const normalized = normalizeBodyText(bodyText);
61
+ if (!normalized) {
62
+ return { question: "", blockedReason: "", suggestedOptions: [] };
63
+ }
64
+ const question = extractBodySection(normalized, "Question", ["Blocked reason", "Suggested options"]);
65
+ const blockedReason = extractBodySection(normalized, "Blocked reason", ["Suggested options"]);
66
+ const suggestedOptions = parseSuggestedOptionsBlock(
67
+ extractBodySection(normalized, "Suggested options", [])
68
+ );
69
+ if (question || blockedReason || suggestedOptions.length) {
70
+ return { question, blockedReason, suggestedOptions };
71
+ }
72
+ return { question: normalized, blockedReason: "", suggestedOptions: [] };
73
+ }
24
74
  function decodeMimeEncodedWordSegment(segment) {
25
75
  const match = /^=\?([^?]+)\?([bBqQ])\?([^?]*)\?=$/.exec(segment);
26
76
  if (!match)
@@ -131,18 +181,20 @@ function parseAampHeaders(meta) {
131
181
  const to = meta.to.replace(/^<|>$/g, "");
132
182
  const decodedSubject = decodeMimeEncodedWords(meta.subject);
133
183
  if (intent === "task.dispatch") {
134
- const timeoutStr = getAampHeader(headers, AAMP_HEADER.TIMEOUT) ?? "300";
135
184
  const contextLinksStr = getAampHeader(headers, AAMP_HEADER.CONTEXT_LINKS) ?? "";
136
185
  const dispatchContext = parseDispatchContextHeader(
137
186
  getAampHeader(headers, AAMP_HEADER.DISPATCH_CONTEXT)
138
187
  );
139
188
  const parentTaskId = getAampHeader(headers, AAMP_HEADER.PARENT_TASK_ID);
189
+ const priority = getAampHeader(headers, AAMP_HEADER.PRIORITY) ?? "normal";
190
+ const expiresAt = getAampHeader(headers, AAMP_HEADER.EXPIRES_AT);
140
191
  const dispatch = {
141
192
  protocolVersion,
142
193
  intent: "task.dispatch",
143
194
  taskId,
144
195
  title: decodedSubject.replace(/^\[AAMP Task\]\s*/, "").trim() || "Untitled Task",
145
- timeoutSecs: parseInt(timeoutStr, 10) || 300,
196
+ priority: priority === "urgent" || priority === "high" ? priority : "normal",
197
+ ...expiresAt ? { expiresAt } : {},
146
198
  contextLinks: contextLinksStr ? contextLinksStr.split(",").map((s) => s.trim()).filter(Boolean) : [],
147
199
  ...dispatchContext ? { dispatchContext } : {},
148
200
  ...parentTaskId ? { parentTaskId } : {},
@@ -155,10 +207,24 @@ function parseAampHeaders(meta) {
155
207
  };
156
208
  return dispatch;
157
209
  }
210
+ if (intent === "task.cancel") {
211
+ const cancel = {
212
+ protocolVersion,
213
+ intent: "task.cancel",
214
+ taskId,
215
+ from,
216
+ to,
217
+ messageId: meta.messageId,
218
+ subject: meta.subject,
219
+ bodyText: ""
220
+ };
221
+ return cancel;
222
+ }
158
223
  if (intent === "task.result") {
224
+ const parsedBody = parseTaskResultBody(meta.bodyText);
159
225
  const status = getAampHeader(headers, AAMP_HEADER.STATUS) ?? "completed";
160
- const output = getAampHeader(headers, AAMP_HEADER.OUTPUT) ?? "";
161
- const errorMsg = getAampHeader(headers, AAMP_HEADER.ERROR_MSG);
226
+ const output = getAampHeader(headers, AAMP_HEADER.OUTPUT) ?? parsedBody.output;
227
+ const errorMsg = getAampHeader(headers, AAMP_HEADER.ERROR_MSG) ?? parsedBody.errorMsg;
162
228
  const structuredResult = decodeStructuredResult(
163
229
  getAampHeader(headers, AAMP_HEADER.STRUCTURED_RESULT)
164
230
  );
@@ -176,9 +242,10 @@ function parseAampHeaders(meta) {
176
242
  };
177
243
  return result;
178
244
  }
179
- if (intent === "task.help_needed" || intent === "task.help") {
180
- const question = getAampHeader(headers, AAMP_HEADER.QUESTION) ?? "";
181
- const blockedReason = getAampHeader(headers, AAMP_HEADER.BLOCKED_REASON) ?? "";
245
+ if (intent === "task.help_needed") {
246
+ const parsedBody = parseTaskHelpBody(meta.bodyText);
247
+ const question = getAampHeader(headers, AAMP_HEADER.QUESTION) ?? parsedBody.question;
248
+ const blockedReason = getAampHeader(headers, AAMP_HEADER.BLOCKED_REASON) ?? parsedBody.blockedReason;
182
249
  const suggestedOptionsStr = getAampHeader(headers, AAMP_HEADER.SUGGESTED_OPTIONS) ?? "";
183
250
  const help = {
184
251
  protocolVersion,
@@ -186,7 +253,7 @@ function parseAampHeaders(meta) {
186
253
  taskId,
187
254
  question: decodeMimeEncodedWords(question),
188
255
  blockedReason: decodeMimeEncodedWords(blockedReason),
189
- suggestedOptions: suggestedOptionsStr ? suggestedOptionsStr.split("|").map((s) => decodeMimeEncodedWords(s.trim())).filter(Boolean) : [],
256
+ suggestedOptions: suggestedOptionsStr ? suggestedOptionsStr.split("|").map((s) => decodeMimeEncodedWords(s.trim())).filter(Boolean) : parsedBody.suggestedOptions,
190
257
  from,
191
258
  to,
192
259
  messageId: meta.messageId
@@ -204,16 +271,45 @@ function parseAampHeaders(meta) {
204
271
  };
205
272
  return ack;
206
273
  }
274
+ if (intent === "card.query") {
275
+ const cardQuery = {
276
+ protocolVersion,
277
+ intent: "card.query",
278
+ taskId,
279
+ from,
280
+ to,
281
+ messageId: meta.messageId,
282
+ subject: meta.subject,
283
+ bodyText: ""
284
+ };
285
+ return cardQuery;
286
+ }
287
+ if (intent === "card.response") {
288
+ const summary = getAampHeader(headers, AAMP_HEADER.CARD_SUMMARY) ?? "";
289
+ const cardResponse = {
290
+ protocolVersion,
291
+ intent: "card.response",
292
+ taskId,
293
+ summary: decodeMimeEncodedWords(summary) || decodedSubject.replace(/^\[AAMP Card\]\s*/i, "").trim(),
294
+ from,
295
+ to,
296
+ messageId: meta.messageId,
297
+ subject: meta.subject,
298
+ bodyText: ""
299
+ };
300
+ return cardResponse;
301
+ }
207
302
  return null;
208
303
  }
209
304
  function buildDispatchHeaders(params) {
210
305
  const headers = {
211
306
  [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
212
307
  [AAMP_HEADER.INTENT]: "task.dispatch",
213
- [AAMP_HEADER.TASK_ID]: params.taskId
308
+ [AAMP_HEADER.TASK_ID]: params.taskId,
309
+ [AAMP_HEADER.PRIORITY]: params.priority ?? "normal"
214
310
  };
215
- if (params.timeoutSecs != null) {
216
- headers[AAMP_HEADER.TIMEOUT] = String(params.timeoutSecs);
311
+ if (params.expiresAt) {
312
+ headers[AAMP_HEADER.EXPIRES_AT] = params.expiresAt;
217
313
  }
218
314
  if (params.contextLinks.length > 0) {
219
315
  headers[AAMP_HEADER.CONTEXT_LINKS] = params.contextLinks.join(",");
@@ -227,6 +323,13 @@ function buildDispatchHeaders(params) {
227
323
  }
228
324
  return headers;
229
325
  }
326
+ function buildCancelHeaders(params) {
327
+ return {
328
+ [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
329
+ [AAMP_HEADER.INTENT]: "task.cancel",
330
+ [AAMP_HEADER.TASK_ID]: params.taskId
331
+ };
332
+ }
230
333
  function buildAckHeaders(opts) {
231
334
  return {
232
335
  [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
@@ -239,12 +342,8 @@ function buildResultHeaders(params) {
239
342
  [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
240
343
  [AAMP_HEADER.INTENT]: "task.result",
241
344
  [AAMP_HEADER.TASK_ID]: params.taskId,
242
- [AAMP_HEADER.STATUS]: params.status,
243
- [AAMP_HEADER.OUTPUT]: params.output
345
+ [AAMP_HEADER.STATUS]: params.status
244
346
  };
245
- if (params.errorMsg) {
246
- headers[AAMP_HEADER.ERROR_MSG] = params.errorMsg;
247
- }
248
347
  const structuredResult = encodeStructuredResult(params.structuredResult);
249
348
  if (structuredResult) {
250
349
  headers[AAMP_HEADER.STRUCTURED_RESULT] = structuredResult;
@@ -256,11 +355,24 @@ function buildHelpHeaders(params) {
256
355
  [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
257
356
  [AAMP_HEADER.INTENT]: "task.help_needed",
258
357
  [AAMP_HEADER.TASK_ID]: params.taskId,
259
- [AAMP_HEADER.QUESTION]: params.question,
260
- [AAMP_HEADER.BLOCKED_REASON]: params.blockedReason,
261
358
  [AAMP_HEADER.SUGGESTED_OPTIONS]: params.suggestedOptions.join("|")
262
359
  };
263
360
  }
361
+ function buildCardQueryHeaders(params) {
362
+ return {
363
+ [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
364
+ [AAMP_HEADER.INTENT]: "card.query",
365
+ [AAMP_HEADER.TASK_ID]: params.taskId
366
+ };
367
+ }
368
+ function buildCardResponseHeaders(params) {
369
+ return {
370
+ [AAMP_HEADER.VERSION]: AAMP_PROTOCOL_VERSION,
371
+ [AAMP_HEADER.INTENT]: "card.response",
372
+ [AAMP_HEADER.TASK_ID]: params.taskId,
373
+ [AAMP_HEADER.CARD_SUMMARY]: params.summary
374
+ };
375
+ }
264
376
 
265
377
  // ../sdk/src/tiny-emitter.ts
266
378
  var TinyEmitter = class {
@@ -504,7 +616,7 @@ var JmapPushClient = class extends TinyEmitter {
504
616
  * Process a received email.
505
617
  *
506
618
  * Priority:
507
- * 1. If X-AAMP-Intent is present → emit typed AAMP event (task.dispatch / task.result / task.help_needed)
619
+ * 1. If X-AAMP-Intent is present → emit typed AAMP event (task.dispatch / task.cancel / task.result / task.help_needed)
508
620
  * 2. If In-Reply-To is present → emit 'reply' event so the application layer can
509
621
  * resolve the thread (inReplyTo → taskId via Redis/DB) and handle human replies.
510
622
  * 3. Otherwise → ignore (not an AAMP-related email)
@@ -520,16 +632,18 @@ var JmapPushClient = class extends TinyEmitter {
520
632
  if (this.seenMessageIds.has(messageId))
521
633
  return;
522
634
  this.seenMessageIds.add(messageId);
635
+ const aampTextPartId = email.textBody?.[0]?.partId;
636
+ const aampBodyText = aampTextPartId ? (email.bodyValues?.[aampTextPartId]?.value ?? "").trim() : "";
523
637
  const msg = parseAampHeaders({
524
638
  from: fromAddr,
525
639
  to: toAddr,
526
640
  messageId,
527
641
  subject: email.subject ?? "",
528
- headers: headerMap
642
+ headers: headerMap,
643
+ bodyText: aampBodyText
529
644
  });
530
645
  if (msg && "intent" in msg) {
531
- const aampTextPartId = email.textBody?.[0]?.partId;
532
- const aampBodyText = aampTextPartId ? (email.bodyValues?.[aampTextPartId]?.value ?? "").trim() : "";
646
+ ;
533
647
  msg.bodyText = aampBodyText;
534
648
  const receivedAttachments = (email.attachments ?? []).map((a) => ({
535
649
  filename: a.name ?? "attachment",
@@ -549,16 +663,24 @@ var JmapPushClient = class extends TinyEmitter {
549
663
  case "task.dispatch":
550
664
  this.emit("task.dispatch", aampMsg);
551
665
  break;
666
+ case "task.cancel":
667
+ this.emit("task.cancel", aampMsg);
668
+ break;
552
669
  case "task.result":
553
670
  this.emit("task.result", aampMsg);
554
671
  break;
555
672
  case "task.help_needed":
556
673
  this.emit("task.help_needed", aampMsg);
557
- this.emit("task.help", aampMsg);
558
674
  break;
559
675
  case "task.ack":
560
676
  this.emit("task.ack", aampMsg);
561
677
  break;
678
+ case "card.query":
679
+ this.emit("card.query", aampMsg);
680
+ break;
681
+ case "card.response":
682
+ this.emit("card.response", aampMsg);
683
+ break;
562
684
  }
563
685
  return;
564
686
  }
@@ -886,7 +1008,7 @@ var JmapPushClient = class extends TinyEmitter {
886
1008
  * Useful as a safety net when the WebSocket stays "connected"
887
1009
  * but a notification is missed by an intermediate layer.
888
1010
  */
889
- async reconcileRecentEmails(limit = 20) {
1011
+ async reconcileRecentEmails(limit = 20, opts) {
890
1012
  if (!this.session) {
891
1013
  this.session = await this.fetchSession();
892
1014
  }
@@ -933,7 +1055,7 @@ var JmapPushClient = class extends TinyEmitter {
933
1055
  const bTs = new Date(b.receivedAt).getTime();
934
1056
  return aTs - bTs;
935
1057
  })) {
936
- if (!this.shouldProcessBootstrapEmail(email))
1058
+ if (!opts?.includeHistorical && !this.shouldProcessBootstrapEmail(email))
937
1059
  continue;
938
1060
  this.processEmail(email);
939
1061
  }
@@ -945,7 +1067,16 @@ var JmapPushClient = class extends TinyEmitter {
945
1067
  import { createTransport } from "nodemailer";
946
1068
  import { randomUUID } from "crypto";
947
1069
  var sanitize = (s) => s.replace(/[\r\n]/g, " ").trim();
948
- var SmtpSender = class {
1070
+ function deriveMailboxServiceDefaults(email, baseUrl2) {
1071
+ const domain = email.split("@")[1]?.trim();
1072
+ const resolvedBaseUrl = baseUrl2?.trim() || (domain ? `https://${domain}` : void 0);
1073
+ const smtpHost = domain || (resolvedBaseUrl ? new URL(resolvedBaseUrl).hostname : "localhost");
1074
+ return {
1075
+ smtpHost,
1076
+ httpBaseUrl: resolvedBaseUrl
1077
+ };
1078
+ }
1079
+ var SmtpSender = class _SmtpSender {
949
1080
  constructor(config) {
950
1081
  this.config = config;
951
1082
  this.transport = createTransport({
@@ -962,6 +1093,20 @@ var SmtpSender = class {
962
1093
  });
963
1094
  }
964
1095
  transport;
1096
+ discoveredApiUrlPromise = null;
1097
+ static fromMailboxIdentity(config) {
1098
+ const derived = deriveMailboxServiceDefaults(config.email, config.baseUrl);
1099
+ return new _SmtpSender({
1100
+ host: derived.smtpHost,
1101
+ port: config.smtpPort ?? 587,
1102
+ user: config.email,
1103
+ password: config.password,
1104
+ httpBaseUrl: derived.httpBaseUrl,
1105
+ authToken: Buffer.from(`${config.email}:${config.password}`).toString("base64"),
1106
+ secure: config.secure,
1107
+ rejectUnauthorized: config.rejectUnauthorized
1108
+ });
1109
+ }
965
1110
  senderDomain() {
966
1111
  return this.config.user.split("@")[1]?.toLowerCase() ?? "";
967
1112
  }
@@ -973,12 +1118,38 @@ var SmtpSender = class {
973
1118
  this.config.httpBaseUrl && this.config.authToken && this.senderDomain() && this.senderDomain() === this.recipientDomain(to)
974
1119
  );
975
1120
  }
976
- async sendViaHttp(opts) {
1121
+ async resolveAampApiUrl() {
977
1122
  const base = this.config.httpBaseUrl?.replace(/\/$/, "");
978
- if (!base || !this.config.authToken) {
1123
+ if (!base) {
1124
+ throw new Error("HTTP send fallback is not configured");
1125
+ }
1126
+ if (!this.discoveredApiUrlPromise) {
1127
+ this.discoveredApiUrlPromise = (async () => {
1128
+ const discoveryRes = await fetch(`${base}/.well-known/aamp`);
1129
+ if (!discoveryRes.ok) {
1130
+ throw new Error(`AAMP discovery failed: ${discoveryRes.status}`);
1131
+ }
1132
+ const discovery = await discoveryRes.json();
1133
+ if (!discovery.api?.url) {
1134
+ throw new Error("AAMP discovery did not return api.url");
1135
+ }
1136
+ return new URL(discovery.api.url, `${base}/`).toString();
1137
+ })();
1138
+ }
1139
+ try {
1140
+ return await this.discoveredApiUrlPromise;
1141
+ } catch (err) {
1142
+ this.discoveredApiUrlPromise = null;
1143
+ throw err;
1144
+ }
1145
+ }
1146
+ async sendViaHttp(opts) {
1147
+ if (!this.config.authToken) {
979
1148
  throw new Error("HTTP send fallback is not configured");
980
1149
  }
981
- const res = await fetch(`${base}/api/send`, {
1150
+ const apiUrl = new URL(await this.resolveAampApiUrl());
1151
+ apiUrl.searchParams.set("action", "aamp.mailbox.send");
1152
+ const res = await fetch(apiUrl, {
982
1153
  method: "POST",
983
1154
  headers: {
984
1155
  Authorization: `Basic ${this.config.authToken}`,
@@ -1008,11 +1179,13 @@ var SmtpSender = class {
1008
1179
  * store a reverse-index (messageId → taskId) for In-Reply-To thread routing.
1009
1180
  */
1010
1181
  async sendTask(opts) {
1011
- const taskId = randomUUID();
1182
+ const taskId = opts.taskId ?? randomUUID();
1012
1183
  const aampHeaders = buildDispatchHeaders({
1013
1184
  taskId,
1014
- timeoutSecs: opts.timeoutSecs,
1185
+ priority: opts.priority,
1186
+ expiresAt: opts.expiresAt,
1015
1187
  contextLinks: opts.contextLinks ?? [],
1188
+ dispatchContext: opts.dispatchContext,
1016
1189
  parentTaskId: opts.parentTaskId
1017
1190
  });
1018
1191
  const sendMailOpts = {
@@ -1022,7 +1195,8 @@ var SmtpSender = class {
1022
1195
  text: [
1023
1196
  `Task: ${opts.title}`,
1024
1197
  `Task ID: ${taskId}`,
1025
- opts.timeoutSecs ? `Deadline: ${opts.timeoutSecs}s` : `Deadline: none`,
1198
+ `Priority: ${opts.priority ?? "normal"}`,
1199
+ opts.expiresAt ? `Expires At: ${opts.expiresAt}` : `Expires At: none`,
1026
1200
  opts.contextLinks?.length ? `Context:
1027
1201
  ${opts.contextLinks.map((l) => ` ${l}`).join("\n")}` : "",
1028
1202
  opts.bodyText ?? "",
@@ -1111,7 +1285,7 @@ Error: ${opts.errorMsg}` : ""
1111
1285
  await this.transport.sendMail(mailOpts);
1112
1286
  }
1113
1287
  /**
1114
- * Send a task.help email when the agent is blocked
1288
+ * Send a task.help_needed email when the agent is blocked
1115
1289
  */
1116
1290
  async sendHelp(opts) {
1117
1291
  const aampHeaders = buildHelpHeaders({
@@ -1165,6 +1339,35 @@ ${opts.suggestedOptions.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}` : ""
1165
1339
  }
1166
1340
  await this.transport.sendMail(helpMailOpts);
1167
1341
  }
1342
+ /**
1343
+ * Send a task.cancel email to stop a previously dispatched task.
1344
+ */
1345
+ async sendCancel(opts) {
1346
+ const aampHeaders = buildCancelHeaders({
1347
+ taskId: opts.taskId
1348
+ });
1349
+ const mailOpts = {
1350
+ from: this.config.user,
1351
+ to: opts.to,
1352
+ subject: `[AAMP Cancel] Task ${opts.taskId}`,
1353
+ text: opts.bodyText ?? "The dispatcher cancelled this task.",
1354
+ headers: aampHeaders
1355
+ };
1356
+ if (opts.inReplyTo) {
1357
+ mailOpts.inReplyTo = opts.inReplyTo;
1358
+ mailOpts.references = opts.inReplyTo;
1359
+ }
1360
+ if (this.shouldUseHttpFallback(opts.to)) {
1361
+ await this.sendViaHttp({
1362
+ to: opts.to,
1363
+ subject: mailOpts.subject,
1364
+ text: mailOpts.text,
1365
+ aampHeaders
1366
+ });
1367
+ return;
1368
+ }
1369
+ await this.transport.sendMail(mailOpts);
1370
+ }
1168
1371
  /**
1169
1372
  * Send a task.ack email to confirm receipt of a dispatch
1170
1373
  */
@@ -1192,6 +1395,59 @@ ${opts.suggestedOptions.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}` : ""
1192
1395
  }
1193
1396
  await this.transport.sendMail(mailOpts);
1194
1397
  }
1398
+ async sendCardQuery(opts) {
1399
+ const taskId = opts.taskId ?? randomUUID();
1400
+ const aampHeaders = buildCardQueryHeaders({ taskId });
1401
+ const mailOpts = {
1402
+ from: this.config.user,
1403
+ to: opts.to,
1404
+ subject: `[AAMP Card Query] ${taskId}`,
1405
+ text: opts.bodyText?.trim() || "Please share your agent card and capability details.",
1406
+ headers: aampHeaders
1407
+ };
1408
+ if (opts.inReplyTo) {
1409
+ mailOpts.inReplyTo = opts.inReplyTo;
1410
+ mailOpts.references = opts.inReplyTo;
1411
+ }
1412
+ if (this.shouldUseHttpFallback(opts.to)) {
1413
+ const info2 = await this.sendViaHttp({
1414
+ to: opts.to,
1415
+ subject: mailOpts.subject,
1416
+ text: mailOpts.text,
1417
+ aampHeaders
1418
+ });
1419
+ return { taskId, messageId: info2.messageId ?? "" };
1420
+ }
1421
+ const info = await this.transport.sendMail(mailOpts);
1422
+ return { taskId, messageId: info.messageId ?? "" };
1423
+ }
1424
+ async sendCardResponse(opts) {
1425
+ const aampHeaders = buildCardResponseHeaders({
1426
+ taskId: opts.taskId,
1427
+ summary: opts.summary
1428
+ });
1429
+ const mailOpts = {
1430
+ from: this.config.user,
1431
+ to: opts.to,
1432
+ subject: `[AAMP Card] ${sanitize(opts.summary)}`,
1433
+ text: opts.bodyText,
1434
+ headers: aampHeaders
1435
+ };
1436
+ if (opts.inReplyTo) {
1437
+ mailOpts.inReplyTo = opts.inReplyTo;
1438
+ mailOpts.references = opts.inReplyTo;
1439
+ }
1440
+ if (this.shouldUseHttpFallback(opts.to)) {
1441
+ await this.sendViaHttp({
1442
+ to: opts.to,
1443
+ subject: mailOpts.subject,
1444
+ text: mailOpts.text,
1445
+ aampHeaders
1446
+ });
1447
+ return;
1448
+ }
1449
+ await this.transport.sendMail(mailOpts);
1450
+ }
1195
1451
  /**
1196
1452
  * Verify SMTP connection
1197
1453
  */
@@ -1209,56 +1465,68 @@ ${opts.suggestedOptions.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}` : ""
1209
1465
  };
1210
1466
 
1211
1467
  // ../sdk/src/client.ts
1212
- var AampClient = class extends TinyEmitter {
1468
+ var AampClient = class _AampClient extends TinyEmitter {
1213
1469
  jmapClient;
1214
1470
  smtpSender;
1215
1471
  config;
1216
1472
  constructor(config) {
1217
1473
  super();
1218
1474
  this.config = config;
1475
+ const mailboxToken = config.mailboxToken;
1476
+ const resolvedBaseUrl = config.baseUrl;
1477
+ const derived = deriveMailboxServiceDefaults(config.email, resolvedBaseUrl);
1478
+ const smtpHost = config.smtpHost ?? derived.smtpHost;
1219
1479
  let password;
1220
1480
  try {
1221
- const decoded = Buffer.from(config.jmapToken, "base64").toString("utf-8");
1481
+ const decoded = Buffer.from(mailboxToken, "base64").toString("utf-8");
1222
1482
  const colonIdx = decoded.indexOf(":");
1223
1483
  if (colonIdx < 0)
1224
- throw new Error("Invalid jmapToken format: expected base64(email:password)");
1484
+ throw new Error("Invalid mailboxToken format: expected base64(email:password)");
1225
1485
  password = decoded.slice(colonIdx + 1);
1226
1486
  if (!password)
1227
- throw new Error("Invalid jmapToken: empty password");
1487
+ throw new Error("Invalid mailboxToken: empty password");
1228
1488
  } catch (err) {
1229
- if (err instanceof Error && err.message.startsWith("Invalid jmapToken"))
1489
+ if (err instanceof Error && err.message.startsWith("Invalid mailboxToken"))
1230
1490
  throw err;
1231
- throw new Error(`Failed to decode jmapToken: ${err.message}`);
1491
+ throw new Error(`Failed to decode mailboxToken: ${err.message}`);
1232
1492
  }
1233
1493
  this.jmapClient = new JmapPushClient({
1234
1494
  email: config.email,
1235
1495
  password: password ?? config.smtpPassword,
1236
- jmapUrl: config.jmapUrl,
1496
+ jmapUrl: resolvedBaseUrl,
1237
1497
  reconnectInterval: config.reconnectInterval ?? 5e3,
1238
1498
  rejectUnauthorized: config.rejectUnauthorized
1239
1499
  });
1240
1500
  this.smtpSender = new SmtpSender({
1241
- host: config.smtpHost,
1501
+ host: smtpHost,
1242
1502
  port: config.smtpPort ?? 587,
1243
1503
  user: config.email,
1244
1504
  password: config.smtpPassword,
1245
- httpBaseUrl: config.httpSendBaseUrl ?? config.jmapUrl,
1246
- authToken: config.jmapToken,
1505
+ httpBaseUrl: config.httpSendBaseUrl ?? resolvedBaseUrl,
1506
+ authToken: mailboxToken,
1247
1507
  rejectUnauthorized: config.rejectUnauthorized
1248
1508
  });
1249
1509
  this.jmapClient.on("task.dispatch", (task) => {
1250
1510
  this.emit("task.dispatch", task);
1251
1511
  });
1512
+ this.jmapClient.on("task.cancel", (task) => {
1513
+ this.emit("task.cancel", task);
1514
+ });
1252
1515
  this.jmapClient.on("task.result", (result) => {
1253
1516
  this.emit("task.result", result);
1254
1517
  });
1255
1518
  this.jmapClient.on("task.help_needed", (help) => {
1256
1519
  this.emit("task.help_needed", help);
1257
- this.emit("task.help", help);
1258
1520
  });
1259
1521
  this.jmapClient.on("task.ack", (ack) => {
1260
1522
  this.emit("task.ack", ack);
1261
1523
  });
1524
+ this.jmapClient.on("card.query", (query) => {
1525
+ this.emit("card.query", query);
1526
+ });
1527
+ this.jmapClient.on("card.response", (response) => {
1528
+ this.emit("card.response", response);
1529
+ });
1262
1530
  this.jmapClient.on("_autoAck", async ({ to, taskId, messageId }) => {
1263
1531
  try {
1264
1532
  await this.smtpSender.sendAck({ to, taskId, inReplyTo: messageId });
@@ -1279,6 +1547,86 @@ var AampClient = class extends TinyEmitter {
1279
1547
  this.emit("error", err);
1280
1548
  });
1281
1549
  }
1550
+ static fromMailboxIdentity(config) {
1551
+ const derived = deriveMailboxServiceDefaults(config.email, config.baseUrl);
1552
+ return new _AampClient({
1553
+ email: config.email,
1554
+ mailboxToken: Buffer.from(`${config.email}:${config.smtpPassword}`).toString("base64"),
1555
+ baseUrl: derived.httpBaseUrl ?? `https://${config.email.split("@")[1] ?? "localhost"}`,
1556
+ smtpHost: derived.smtpHost,
1557
+ smtpPort: config.smtpPort ?? 587,
1558
+ smtpPassword: config.smtpPassword,
1559
+ reconnectInterval: config.reconnectInterval,
1560
+ rejectUnauthorized: config.rejectUnauthorized
1561
+ });
1562
+ }
1563
+ static async discoverAampService(aampHost) {
1564
+ const base = aampHost.replace(/\/$/, "");
1565
+ const res = await fetch(`${base}/.well-known/aamp`);
1566
+ if (!res.ok) {
1567
+ throw new Error(`AAMP discovery failed: ${res.status} ${res.statusText}`);
1568
+ }
1569
+ const discovery = await res.json();
1570
+ if (!discovery.api?.url) {
1571
+ throw new Error("AAMP discovery did not return api.url");
1572
+ }
1573
+ return discovery;
1574
+ }
1575
+ static async callDiscoveredApi(base, opts) {
1576
+ const discovery = await _AampClient.discoverAampService(base);
1577
+ const apiUrl = new URL(discovery.api.url, `${base}/`);
1578
+ apiUrl.searchParams.set("action", opts.action);
1579
+ for (const [key, value] of Object.entries(opts.query ?? {})) {
1580
+ if (value == null)
1581
+ continue;
1582
+ apiUrl.searchParams.set(key, String(value));
1583
+ }
1584
+ return fetch(apiUrl, {
1585
+ method: opts.method ?? "GET",
1586
+ headers: {
1587
+ ...opts.authToken ? { Authorization: `Basic ${opts.authToken}` } : {},
1588
+ ...opts.body ? { "Content-Type": "application/json" } : {}
1589
+ },
1590
+ ...opts.body ? { body: JSON.stringify(opts.body) } : {}
1591
+ });
1592
+ }
1593
+ static async registerMailbox(opts) {
1594
+ const base = opts.aampHost.replace(/\/$/, "");
1595
+ const registerRes = await _AampClient.callDiscoveredApi(base, {
1596
+ action: "aamp.mailbox.register",
1597
+ method: "POST",
1598
+ body: {
1599
+ slug: opts.slug,
1600
+ description: opts.description
1601
+ }
1602
+ });
1603
+ if (!registerRes.ok) {
1604
+ const body = await registerRes.text().catch(() => "");
1605
+ throw new Error(`Mailbox registration failed: ${registerRes.status} ${body || registerRes.statusText}`);
1606
+ }
1607
+ const registerData = await registerRes.json();
1608
+ if (!registerData.registrationCode) {
1609
+ throw new Error("Mailbox registration succeeded but no registrationCode was returned");
1610
+ }
1611
+ const credsRes = await _AampClient.callDiscoveredApi(base, {
1612
+ action: "aamp.mailbox.credentials",
1613
+ query: { code: registerData.registrationCode }
1614
+ });
1615
+ if (!credsRes.ok) {
1616
+ const body = await credsRes.text().catch(() => "");
1617
+ throw new Error(`Mailbox credential exchange failed: ${credsRes.status} ${body || credsRes.statusText}`);
1618
+ }
1619
+ const creds = await credsRes.json();
1620
+ if (!creds.email || !creds.mailbox?.token || !creds.smtp?.password) {
1621
+ throw new Error("Mailbox credential exchange returned an incomplete identity payload");
1622
+ }
1623
+ return {
1624
+ email: creds.email,
1625
+ mailboxToken: creds.mailbox.token,
1626
+ smtpPassword: creds.smtp.password,
1627
+ baseUrl: base
1628
+ };
1629
+ }
1282
1630
  // =====================================================
1283
1631
  // Lifecycle
1284
1632
  // =====================================================
@@ -1316,6 +1664,9 @@ var AampClient = class extends TinyEmitter {
1316
1664
  async sendTask(opts) {
1317
1665
  return this.smtpSender.sendTask(opts);
1318
1666
  }
1667
+ async sendCancel(opts) {
1668
+ return this.smtpSender.sendCancel(opts);
1669
+ }
1319
1670
  /**
1320
1671
  * Send a task.result email (agent → system/dispatcher)
1321
1672
  */
@@ -1323,11 +1674,72 @@ var AampClient = class extends TinyEmitter {
1323
1674
  return this.smtpSender.sendResult(opts);
1324
1675
  }
1325
1676
  /**
1326
- * Send a task.help email when the agent needs human assistance
1677
+ * Send a task.help_needed email when the agent needs human assistance
1327
1678
  */
1328
1679
  async sendHelp(opts) {
1329
1680
  return this.smtpSender.sendHelp(opts);
1330
1681
  }
1682
+ async sendCardQuery(opts) {
1683
+ return this.smtpSender.sendCardQuery(opts);
1684
+ }
1685
+ async sendCardResponse(opts) {
1686
+ return this.smtpSender.sendCardResponse(opts);
1687
+ }
1688
+ async updateDirectoryProfile(opts) {
1689
+ const base = this.config.baseUrl;
1690
+ const mailboxToken = this.config.mailboxToken;
1691
+ const res = await _AampClient.callDiscoveredApi(base, {
1692
+ action: "aamp.directory.upsert",
1693
+ method: "POST",
1694
+ authToken: mailboxToken,
1695
+ body: opts
1696
+ });
1697
+ if (!res.ok) {
1698
+ const body = await res.text().catch(() => "");
1699
+ throw new Error(`Directory profile update failed: ${res.status} ${body || res.statusText}`);
1700
+ }
1701
+ const data = await res.json();
1702
+ return data.profile;
1703
+ }
1704
+ async listDirectory(opts = {}) {
1705
+ const base = this.config.baseUrl;
1706
+ const mailboxToken = this.config.mailboxToken;
1707
+ const res = await _AampClient.callDiscoveredApi(base, {
1708
+ action: "aamp.directory.list",
1709
+ authToken: mailboxToken,
1710
+ query: {
1711
+ scope: opts.scope,
1712
+ includeSelf: opts.includeSelf,
1713
+ limit: opts.limit
1714
+ }
1715
+ });
1716
+ if (!res.ok) {
1717
+ const body = await res.text().catch(() => "");
1718
+ throw new Error(`Directory list failed: ${res.status} ${body || res.statusText}`);
1719
+ }
1720
+ const data = await res.json();
1721
+ return data.agents;
1722
+ }
1723
+ async searchDirectory(opts) {
1724
+ const base = this.config.baseUrl;
1725
+ const mailboxToken = this.config.mailboxToken;
1726
+ const res = await _AampClient.callDiscoveredApi(base, {
1727
+ action: "aamp.directory.search",
1728
+ authToken: mailboxToken,
1729
+ query: {
1730
+ q: opts.query,
1731
+ scope: opts.scope,
1732
+ includeSelf: opts.includeSelf,
1733
+ limit: opts.limit
1734
+ }
1735
+ });
1736
+ if (!res.ok) {
1737
+ const body = await res.text().catch(() => "");
1738
+ throw new Error(`Directory search failed: ${res.status} ${body || res.statusText}`);
1739
+ }
1740
+ const data = await res.json();
1741
+ return data.agents;
1742
+ }
1331
1743
  /**
1332
1744
  * Download a blob (attachment) by its JMAP blobId.
1333
1745
  * Use this to retrieve attachment content from received TaskDispatch or TaskResult messages.
@@ -1341,8 +1753,8 @@ var AampClient = class extends TinyEmitter {
1341
1753
  * a flaky WebSocket path. Safe to call periodically; duplicate processing is
1342
1754
  * suppressed by the JMAP push client.
1343
1755
  */
1344
- async reconcileRecentEmails(limit) {
1345
- return this.jmapClient.reconcileRecentEmails(limit);
1756
+ async reconcileRecentEmails(limit, opts) {
1757
+ return this.jmapClient.reconcileRecentEmails(limit, opts);
1346
1758
  }
1347
1759
  /**
1348
1760
  * Verify SMTP connectivity
@@ -1362,15 +1774,22 @@ import { homedir } from "node:os";
1362
1774
  function defaultCredentialsPath() {
1363
1775
  return join(homedir(), ".openclaw", "extensions", "aamp-openclaw-plugin", ".credentials.json");
1364
1776
  }
1777
+ function defaultTaskStatePath() {
1778
+ return join(homedir(), ".openclaw", "extensions", "aamp-openclaw-plugin", ".task-state.json");
1779
+ }
1365
1780
  function loadCachedIdentity(file) {
1366
1781
  const resolved = file ?? defaultCredentialsPath();
1367
1782
  if (!existsSync(resolved))
1368
1783
  return null;
1369
1784
  try {
1370
1785
  const parsed = JSON.parse(readFileSync(resolved, "utf-8"));
1371
- if (!parsed.email || !parsed.jmapToken || !parsed.smtpPassword)
1786
+ if (!parsed.email || !parsed.mailboxToken || !parsed.smtpPassword)
1372
1787
  return null;
1373
- return parsed;
1788
+ return {
1789
+ email: parsed.email,
1790
+ mailboxToken: parsed.mailboxToken,
1791
+ smtpPassword: parsed.smtpPassword
1792
+ };
1374
1793
  } catch {
1375
1794
  return null;
1376
1795
  }
@@ -1378,7 +1797,31 @@ function loadCachedIdentity(file) {
1378
1797
  function saveCachedIdentity(identity, file) {
1379
1798
  const resolved = file ?? defaultCredentialsPath();
1380
1799
  mkdirSync(dirname(resolved), { recursive: true });
1381
- writeFileSync(resolved, JSON.stringify(identity, null, 2), "utf-8");
1800
+ writeFileSync(resolved, JSON.stringify({
1801
+ email: identity.email,
1802
+ mailboxToken: identity.mailboxToken,
1803
+ smtpPassword: identity.smtpPassword
1804
+ }, null, 2), "utf-8");
1805
+ }
1806
+ function loadTaskState(file) {
1807
+ const resolved = file ?? defaultTaskStatePath();
1808
+ if (!existsSync(resolved))
1809
+ return { terminalTaskIds: [] };
1810
+ try {
1811
+ const parsed = JSON.parse(readFileSync(resolved, "utf-8"));
1812
+ return {
1813
+ terminalTaskIds: Array.isArray(parsed.terminalTaskIds) ? parsed.terminalTaskIds.filter(Boolean) : []
1814
+ };
1815
+ } catch {
1816
+ return { terminalTaskIds: [] };
1817
+ }
1818
+ }
1819
+ function saveTaskState(state, file) {
1820
+ const resolved = file ?? defaultTaskStatePath();
1821
+ mkdirSync(dirname(resolved), { recursive: true });
1822
+ writeFileSync(resolved, JSON.stringify({
1823
+ terminalTaskIds: state.terminalTaskIds ?? []
1824
+ }, null, 2), "utf-8");
1382
1825
  }
1383
1826
  function ensureDir(dir) {
1384
1827
  mkdirSync(dir, { recursive: true });
@@ -1430,6 +1873,7 @@ function baseUrl(aampHost) {
1430
1873
  return `https://${aampHost}`;
1431
1874
  }
1432
1875
  var pendingTasks = /* @__PURE__ */ new Map();
1876
+ var terminalTaskIds = new Set(loadTaskState(defaultTaskStatePath()).terminalTaskIds ?? []);
1433
1877
  var dispatchedSubtasks = /* @__PURE__ */ new Map();
1434
1878
  var waitingDispatches = /* @__PURE__ */ new Map();
1435
1879
  var aampClient = null;
@@ -1456,10 +1900,83 @@ function logTransportState(api, mode, email, previousMode) {
1456
1900
  }
1457
1901
  api.logger.info(`[AAMP] Connected \u2014 listening as ${email}`);
1458
1902
  }
1903
+ function isSyntheticPendingKey(taskKey) {
1904
+ return taskKey.startsWith("result:") || taskKey.startsWith("help:");
1905
+ }
1906
+ function saveTerminalTaskIds() {
1907
+ saveTaskState({ terminalTaskIds: [...terminalTaskIds] }, defaultTaskStatePath());
1908
+ }
1909
+ function rememberTerminalTask(taskId) {
1910
+ terminalTaskIds.add(taskId);
1911
+ saveTerminalTaskIds();
1912
+ }
1913
+ function priorityRank(priority) {
1914
+ switch (priority) {
1915
+ case "urgent":
1916
+ return 0;
1917
+ case "high":
1918
+ return 1;
1919
+ default:
1920
+ return 2;
1921
+ }
1922
+ }
1923
+ function hasExpired(task) {
1924
+ if (task.expiresAt) {
1925
+ const expiresAtMs = new Date(task.expiresAt).getTime();
1926
+ if (Number.isFinite(expiresAtMs) && Date.now() >= expiresAtMs)
1927
+ return true;
1928
+ }
1929
+ return false;
1930
+ }
1931
+ function nextPendingEntry() {
1932
+ const entries = [...pendingTasks.entries()];
1933
+ const notifications = entries.filter(([key]) => key.startsWith("result:") || key.startsWith("help:"));
1934
+ if (notifications.length > 0) {
1935
+ return notifications.sort((a, b) => new Date(a[1].receivedAt).getTime() - new Date(b[1].receivedAt).getTime())[0];
1936
+ }
1937
+ return entries.filter(([key]) => !key.startsWith("result:") && !key.startsWith("help:")).sort((a, b) => {
1938
+ const rankDiff = priorityRank(a[1].priority) - priorityRank(b[1].priority);
1939
+ if (rankDiff !== 0)
1940
+ return rankDiff;
1941
+ return new Date(a[1].receivedAt).getTime() - new Date(b[1].receivedAt).getTime();
1942
+ })[0];
1943
+ }
1944
+ function queuePendingTask(task) {
1945
+ if (terminalTaskIds.has(task.taskId)) {
1946
+ return false;
1947
+ }
1948
+ pendingTasks.set(task.taskId, {
1949
+ taskId: task.taskId,
1950
+ from: task.from,
1951
+ title: task.title,
1952
+ bodyText: task.bodyText ?? "",
1953
+ priority: task.priority ?? "normal",
1954
+ ...task.expiresAt ? { expiresAt: task.expiresAt } : {},
1955
+ contextLinks: task.contextLinks ?? [],
1956
+ messageId: task.messageId ?? "",
1957
+ receivedAt: (/* @__PURE__ */ new Date()).toISOString()
1958
+ });
1959
+ if (hasExpired(pendingTasks.get(task.taskId))) {
1960
+ pendingTasks.delete(task.taskId);
1961
+ rememberTerminalTask(task.taskId);
1962
+ return false;
1963
+ }
1964
+ return true;
1965
+ }
1459
1966
  async function registerNode(cfg) {
1460
1967
  const slug = (cfg.slug ?? "openclaw-agent").toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "");
1461
1968
  const base = baseUrl(cfg.aampHost);
1462
- const res = await fetch(`${base}/api/nodes/self-register`, {
1969
+ const discoveryRes = await fetch(`${base}/.well-known/aamp`);
1970
+ if (!discoveryRes.ok) {
1971
+ throw new Error(`AAMP discovery failed (${discoveryRes.status}): ${discoveryRes.statusText}`);
1972
+ }
1973
+ const discovery = await discoveryRes.json();
1974
+ const apiUrl = discovery.api?.url;
1975
+ if (!apiUrl) {
1976
+ throw new Error("AAMP discovery did not return api.url");
1977
+ }
1978
+ const apiBase = new URL(apiUrl, `${base}/`).toString();
1979
+ const res = await fetch(`${apiBase}?action=aamp.mailbox.register`, {
1463
1980
  method: "POST",
1464
1981
  headers: { "Content-Type": "application/json" },
1465
1982
  body: JSON.stringify({ slug, description: "OpenClaw AAMP agent node" })
@@ -1470,7 +1987,7 @@ async function registerNode(cfg) {
1470
1987
  }
1471
1988
  const regData = await res.json();
1472
1989
  const credRes = await fetch(
1473
- `${base}/api/nodes/credentials?code=${encodeURIComponent(regData.registrationCode)}`
1990
+ `${apiBase}?action=aamp.mailbox.credentials&code=${encodeURIComponent(regData.registrationCode)}`
1474
1991
  );
1475
1992
  if (!credRes.ok) {
1476
1993
  const err = await credRes.json().catch(() => ({}));
@@ -1479,7 +1996,7 @@ async function registerNode(cfg) {
1479
1996
  const credData = await credRes.json();
1480
1997
  return {
1481
1998
  email: credData.email,
1482
- jmapToken: credData.jmap.token,
1999
+ mailboxToken: credData.mailbox.token,
1483
2000
  smtpPassword: credData.smtp.password
1484
2001
  };
1485
2002
  }
@@ -1644,13 +2161,10 @@ var src_default = {
1644
2161
  lastLoggedTransportMode = "disconnected";
1645
2162
  api.logger.info(`[AAMP] Mailbox identity ready \u2014 ${agentEmail}`);
1646
2163
  const base = baseUrl(cfg.aampHost);
1647
- aampClient = new AampClient({
2164
+ aampClient = AampClient.fromMailboxIdentity({
1648
2165
  email: identity.email,
1649
- jmapToken: identity.jmapToken,
1650
- jmapUrl: base,
1651
- smtpHost: new URL(base).hostname,
1652
- smtpPort: 587,
1653
2166
  smtpPassword: identity.smtpPassword,
2167
+ baseUrl: base,
1654
2168
  // Local/dev: management-service proxy uses plain HTTP, no TLS cert to verify.
1655
2169
  // Production: set to true when using wss:// with valid certs.
1656
2170
  rejectUnauthorized: false
@@ -1658,6 +2172,10 @@ var src_default = {
1658
2172
  aampClient.on("task.dispatch", (task) => {
1659
2173
  api.logger.info(`[AAMP] \u2190 task.dispatch ${task.taskId} "${task.title}" from=${task.from}`);
1660
2174
  try {
2175
+ if (terminalTaskIds.has(task.taskId)) {
2176
+ api.logger.info(`[AAMP] Skipping already-terminal task ${task.taskId}`);
2177
+ return;
2178
+ }
1661
2179
  const decision = matchSenderPolicy(task, cfg.senderPolicies);
1662
2180
  if (!decision.allowed) {
1663
2181
  api.logger.warn(`[AAMP] \u2717 rejected by senderPolicies: ${task.from} task=${task.taskId} reason=${decision.reason}`);
@@ -1672,16 +2190,10 @@ var src_default = {
1672
2190
  });
1673
2191
  return;
1674
2192
  }
1675
- pendingTasks.set(task.taskId, {
1676
- taskId: task.taskId,
1677
- from: task.from,
1678
- title: task.title,
1679
- bodyText: task.bodyText ?? "",
1680
- contextLinks: task.contextLinks,
1681
- timeoutSecs: task.timeoutSecs,
1682
- messageId: task.messageId ?? "",
1683
- receivedAt: (/* @__PURE__ */ new Date()).toISOString()
1684
- });
2193
+ if (!queuePendingTask(task)) {
2194
+ api.logger.info(`[AAMP] Ignoring already-terminal or expired task ${task.taskId}`);
2195
+ return;
2196
+ }
1685
2197
  wakeAgentForPendingTask(pendingTasks.get(task.taskId));
1686
2198
  } catch (err) {
1687
2199
  api.logger.error(`[AAMP] task.dispatch handler failed for ${task.taskId}: ${err.message}`);
@@ -1690,7 +2202,21 @@ var src_default = {
1690
2202
  }
1691
2203
  }
1692
2204
  });
2205
+ aampClient.on("task.cancel", (cancel) => {
2206
+ api.logger.info(`[AAMP] \u2190 task.cancel ${cancel.taskId} from=${cancel.from}`);
2207
+ const removed = pendingTasks.delete(cancel.taskId);
2208
+ pendingTasks.delete(`result:${cancel.taskId}`);
2209
+ pendingTasks.delete(`help:${cancel.taskId}`);
2210
+ dispatchedSubtasks.delete(cancel.taskId);
2211
+ waitingDispatches.delete(cancel.taskId);
2212
+ rememberTerminalTask(cancel.taskId);
2213
+ if (removed) {
2214
+ api.logger.info(`[AAMP] Cancelled task ${cancel.taskId} \u2014 removed from pending queue`);
2215
+ }
2216
+ });
1693
2217
  aampClient.on("task.result", (result) => {
2218
+ if (result.from.toLowerCase() === agentEmail.toLowerCase())
2219
+ return;
1694
2220
  api.logger.info(`[AAMP] \u2190 task.result ${result.taskId} status=${result.status} from=${result.from}`);
1695
2221
  const sub = dispatchedSubtasks.get(result.taskId);
1696
2222
  dispatchedSubtasks.delete(result.taskId);
@@ -1754,8 +2280,8 @@ Output:
1754
2280
  ${truncatedOutput}${attachmentInfo}` : `Agent ${result.from} rejected the sub-task.
1755
2281
 
1756
2282
  Reason: ${result.errorMsg ?? "unknown"}`,
2283
+ priority: "urgent",
1757
2284
  contextLinks: [],
1758
- timeoutSecs: 0,
1759
2285
  messageId: "",
1760
2286
  receivedAt: (/* @__PURE__ */ new Date()).toISOString()
1761
2287
  });
@@ -1817,6 +2343,8 @@ ${notifyBody?.bodyText ?? "Sub-task completed."}${actionSection}`;
1817
2343
  });
1818
2344
  });
1819
2345
  aampClient.on("task.help_needed", (help) => {
2346
+ if (help.from.toLowerCase() === agentEmail.toLowerCase())
2347
+ return;
1820
2348
  api.logger.info(`[AAMP] \u2190 task.help_needed ${help.taskId} question="${help.question}" from=${help.from}`);
1821
2349
  const waiter = waitingDispatches.get(help.taskId);
1822
2350
  if (waiter) {
@@ -1835,8 +2363,8 @@ ${notifyBody?.bodyText ?? "Sub-task completed."}${actionSection}`;
1835
2363
  Question: ${help.question}
1836
2364
  Blocked reason: ${help.blockedReason}${help.suggestedOptions?.length ? `
1837
2365
  Suggested options: ${help.suggestedOptions.join(", ")}` : ""}`,
2366
+ priority: "urgent",
1838
2367
  contextLinks: [],
1839
- timeoutSecs: 0,
1840
2368
  messageId: "",
1841
2369
  receivedAt: (/* @__PURE__ */ new Date()).toISOString()
1842
2370
  });
@@ -1938,6 +2466,10 @@ ${notifyBody?.bodyText ?? help.question}`;
1938
2466
  lastTransportMode = mode;
1939
2467
  lastLoggedTransportMode = mode;
1940
2468
  }, 1e3);
2469
+ void aampClient.reconcileRecentEmails(100, { includeHistorical: true }).catch((err) => {
2470
+ lastConnectionError = err.message;
2471
+ api.logger.warn(`[AAMP] Startup mailbox reconcile failed: ${err.message}`);
2472
+ });
1941
2473
  transportMonitorTimer = setInterval(() => {
1942
2474
  if (!aampClient)
1943
2475
  return;
@@ -1955,7 +2487,7 @@ ${notifyBody?.bodyText ?? help.question}`;
1955
2487
  reconcileTimer = setInterval(() => {
1956
2488
  if (!aampClient)
1957
2489
  return;
1958
- void aampClient.reconcileRecentEmails(20).catch((err) => {
2490
+ void aampClient.reconcileRecentEmails(100).catch((err) => {
1959
2491
  lastConnectionError = err.message;
1960
2492
  api.logger.warn(`[AAMP] Mailbox reconcile failed: ${err.message}`);
1961
2493
  });
@@ -2009,24 +2541,45 @@ ${notifyBody?.bodyText ?? help.question}`;
2009
2541
  if (ctx?.sessionKey && !String(ctx.sessionKey).startsWith("aamp:")) {
2010
2542
  currentSessionKey = ctx.sessionKey;
2011
2543
  }
2012
- const now = Date.now();
2013
2544
  for (const [id, t] of pendingTasks) {
2014
- if (t.timeoutSecs && now - new Date(t.receivedAt).getTime() > t.timeoutSecs * 1e3) {
2015
- api.logger.warn(`[AAMP] Task ${id} timed out \u2014 removing from queue`);
2545
+ if (hasExpired(t)) {
2546
+ if (!isSyntheticPendingKey(id) && aampClient?.isConnected()) {
2547
+ void aampClient.sendResult({
2548
+ to: t.from,
2549
+ taskId: t.taskId,
2550
+ status: "rejected",
2551
+ output: "",
2552
+ errorMsg: t.expiresAt ? "Task expired before the agent could complete it." : "Task timed out while waiting for agent completion or follow-up input.",
2553
+ inReplyTo: t.messageId || void 0
2554
+ }).then(() => {
2555
+ rememberTerminalTask(t.taskId);
2556
+ api.logger.warn(`[AAMP] Task ${id} expired \u2014 sent rejected result to dispatcher`);
2557
+ }).catch((err) => {
2558
+ api.logger.error(`[AAMP] Task ${id} expired \u2014 failed to notify dispatcher: ${err.message}`);
2559
+ });
2560
+ } else {
2561
+ rememberTerminalTask(t.taskId);
2562
+ api.logger.warn(`[AAMP] Task ${id} expired \u2014 removing from queue`);
2563
+ }
2016
2564
  pendingTasks.delete(id);
2017
2565
  }
2018
2566
  }
2019
2567
  if (pendingTasks.size === 0)
2020
2568
  return {};
2021
- const allEntries = [...pendingTasks.entries()];
2022
- const notifications = allEntries.filter(([key]) => key.startsWith("result:") || key.startsWith("help:"));
2023
- const actionable = allEntries.filter(([key]) => !key.startsWith("result:") && !key.startsWith("help:"));
2024
- const [taskKey, task] = notifications.length > 0 ? notifications.sort((a, b) => new Date(a[1].receivedAt).getTime() - new Date(b[1].receivedAt).getTime())[0] : actionable.sort((a, b) => new Date(a[1].receivedAt).getTime() - new Date(b[1].receivedAt).getTime())[0];
2569
+ const nextEntry = nextPendingEntry();
2570
+ if (!nextEntry)
2571
+ return {};
2572
+ const [taskKey, task] = nextEntry;
2025
2573
  const isNotification = taskKey.startsWith("result:") || taskKey.startsWith("help:");
2026
2574
  if (isNotification && taskKey) {
2027
2575
  pendingTasks.delete(taskKey);
2028
2576
  }
2029
- const actionableTasks = [...pendingTasks.entries()].filter(([key]) => !key.startsWith("result:") && !key.startsWith("help:")).map(([, t]) => t);
2577
+ const actionableTasks = [...pendingTasks.entries()].filter(([key]) => !key.startsWith("result:") && !key.startsWith("help:")).map(([, t]) => t).sort((a, b) => {
2578
+ const rankDiff = priorityRank(a.priority) - priorityRank(b.priority);
2579
+ if (rankDiff !== 0)
2580
+ return rankDiff;
2581
+ return new Date(a.receivedAt).getTime() - new Date(b.receivedAt).getTime();
2582
+ });
2030
2583
  const hasAttachmentInfo = isNotification && (task.bodyText?.includes("aamp_download_attachment") ?? false);
2031
2584
  const actionRequiredSection = isNotification && actionableTasks.length > 0 ? [
2032
2585
  ``,
@@ -2036,7 +2589,7 @@ ${notifyBody?.bodyText ?? help.question}`;
2036
2589
  `Use the sub-task result above to complete them by calling aamp_send_result.`,
2037
2590
  ``,
2038
2591
  ...actionableTasks.map(
2039
- (t) => `- Task ID: ${t.taskId} | From: ${t.from} | Title: "${t.title}"`
2592
+ (t) => `- [${t.priority}] Task ID: ${t.taskId} | From: ${t.from} | Title: "${t.title}"`
2040
2593
  ),
2041
2594
  ...hasAttachmentInfo ? [
2042
2595
  ``,
@@ -2054,6 +2607,7 @@ ${notifyBody?.bodyText ?? help.question}`;
2054
2607
  `If the sub-task included attachments, use aamp_download_attachment to fetch them.`,
2055
2608
  ``,
2056
2609
  `Task ID: ${task.taskId}`,
2610
+ `Priority: ${task.priority}`,
2057
2611
  `From: ${task.from}`,
2058
2612
  `Title: ${task.title}`,
2059
2613
  task.bodyText ? `
@@ -2096,7 +2650,7 @@ ${task.bodyText}` : "",
2096
2650
  ${task.bodyText}` : "",
2097
2651
  task.contextLinks.length ? `Context Links:
2098
2652
  ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
2099
- task.timeoutSecs ? `Deadline: ${task.timeoutSecs}s from dispatch` : `Deadline: none`,
2653
+ task.expiresAt ? `Expires: ${task.expiresAt}` : `Expires: none`,
2100
2654
  `Received: ${task.receivedAt}`,
2101
2655
  pendingTasks.size > 1 ? `
2102
2656
  (+${pendingTasks.size - 1} more tasks queued)` : ""
@@ -2215,6 +2769,7 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
2215
2769
  attachments
2216
2770
  });
2217
2771
  pendingTasks.delete(task.taskId);
2772
+ rememberTerminalTask(task.taskId);
2218
2773
  api.logger.info(`[AAMP] \u2192 task.result ${task.taskId} ${p.status}`);
2219
2774
  if (pendingTasks.size > 0) {
2220
2775
  try {
@@ -2296,8 +2851,13 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
2296
2851
  if (pendingTasks.size === 0) {
2297
2852
  return { content: [{ type: "text", text: "No pending AAMP tasks." }] };
2298
2853
  }
2299
- const lines = [...pendingTasks.values()].sort((a, b) => new Date(a.receivedAt).getTime() - new Date(b.receivedAt).getTime()).map(
2300
- (t, i) => `${i + 1}. [${t.taskId}] "${t.title}"${t.bodyText ? `
2854
+ const lines = [...pendingTasks.values()].sort((a, b) => {
2855
+ const rankDiff = priorityRank(a.priority) - priorityRank(b.priority);
2856
+ if (rankDiff !== 0)
2857
+ return rankDiff;
2858
+ return new Date(a.receivedAt).getTime() - new Date(b.receivedAt).getTime();
2859
+ }).map(
2860
+ (t, i) => `${i + 1}. [${t.priority}] [${t.taskId}] "${t.title}"${t.bodyText ? `
2301
2861
  Description: ${t.bodyText}` : ""} \u2014 from ${t.from} (received ${t.receivedAt})`
2302
2862
  );
2303
2863
  return {
@@ -2311,6 +2871,40 @@ ${lines.join("\n")}`
2311
2871
  };
2312
2872
  }
2313
2873
  }, { name: "aamp_pending_tasks" });
2874
+ api.registerTool({
2875
+ name: "aamp_cancel_task",
2876
+ description: "Cancel a pending AAMP task and notify the dispatcher.",
2877
+ parameters: {
2878
+ type: "object",
2879
+ required: ["taskId"],
2880
+ properties: {
2881
+ taskId: { type: "string", description: "The AAMP task ID to cancel." },
2882
+ bodyText: { type: "string", description: "Optional cancellation note sent in the email body." }
2883
+ }
2884
+ },
2885
+ execute: async (_id, params) => {
2886
+ const p = params;
2887
+ const task = pendingTasks.get(p.taskId);
2888
+ if (!task) {
2889
+ return { content: [{ type: "text", text: `Error: task ${p.taskId} not found in pending queue.` }] };
2890
+ }
2891
+ if (!aampClient?.isConnected()) {
2892
+ return { content: [{ type: "text", text: "Error: AAMP client is not connected." }] };
2893
+ }
2894
+ await aampClient.sendCancel({
2895
+ to: task.from,
2896
+ taskId: task.taskId,
2897
+ bodyText: p.bodyText,
2898
+ inReplyTo: task.messageId || void 0
2899
+ });
2900
+ pendingTasks.delete(task.taskId);
2901
+ rememberTerminalTask(task.taskId);
2902
+ api.logger.info(`[AAMP] \u2192 task.cancel ${task.taskId}`);
2903
+ return {
2904
+ content: [{ type: "text", text: `Cancellation sent for task ${task.taskId}.` }]
2905
+ };
2906
+ }
2907
+ }, { name: "aamp_cancel_task" });
2314
2908
  api.registerTool({
2315
2909
  name: "aamp_dispatch_task",
2316
2910
  description: "Send a task to another AAMP agent and WAIT for the result. This tool blocks until the sub-agent replies (typically 5-60s). The sub-agent's output and any attachment file paths are returned directly.",
@@ -2322,7 +2916,8 @@ ${lines.join("\n")}`
2322
2916
  title: { type: "string", description: "Task title (concise summary)" },
2323
2917
  bodyText: { type: "string", description: "Detailed task description" },
2324
2918
  parentTaskId: { type: "string", description: "If you are processing a pending AAMP task, pass its Task ID here to establish parent-child nesting. Omit for top-level tasks." },
2325
- timeoutSecs: { type: "number", description: "Timeout in seconds (optional)" },
2919
+ priority: { type: "string", enum: ["urgent", "high", "normal"], description: "Task priority (optional)" },
2920
+ expiresAt: { type: "string", description: "Absolute expiry time in ISO 8601 format (optional)" },
2326
2921
  contextLinks: {
2327
2922
  type: "array",
2328
2923
  items: { type: "string" },
@@ -2360,7 +2955,8 @@ ${lines.join("\n")}`
2360
2955
  to: params.to,
2361
2956
  title: params.title,
2362
2957
  parentTaskId: params.parentTaskId,
2363
- timeoutSecs: params.timeoutSecs,
2958
+ priority: params.priority,
2959
+ expiresAt: params.expiresAt,
2364
2960
  contextLinks: params.contextLinks,
2365
2961
  attachments
2366
2962
  });
@@ -2371,12 +2967,14 @@ ${lines.join("\n")}`
2371
2967
  parentTaskId: params.parentTaskId
2372
2968
  });
2373
2969
  api.logger.info(`[AAMP] \u2192 task.dispatch ${result.taskId} to=${params.to} parent=${params.parentTaskId ?? "none"} (waiting for reply\u2026)`);
2374
- const timeoutMs = (params.timeoutSecs ?? 300) * 1e3;
2970
+ const timeoutMs = params.expiresAt ? Math.max(new Date(params.expiresAt).getTime() - Date.now(), 1) : 300 * 1e3;
2375
2971
  const reply = await new Promise((resolve, reject) => {
2376
2972
  waitingDispatches.set(result.taskId, resolve);
2377
2973
  setTimeout(() => {
2378
2974
  if (waitingDispatches.delete(result.taskId)) {
2379
- reject(new Error(`Sub-task ${result.taskId} timed out after ${params.timeoutSecs ?? 300}s`));
2975
+ reject(new Error(
2976
+ params.expiresAt ? `Sub-task ${result.taskId} expired before a reply was received` : `Sub-task ${result.taskId} timed out after 300s`
2977
+ ));
2380
2978
  }
2381
2979
  }, timeoutMs);
2382
2980
  });
@@ -2478,7 +3076,8 @@ Question: ${h.question}`,
2478
3076
  const apiUrl = discovery.api?.url;
2479
3077
  if (!apiUrl)
2480
3078
  throw new Error("AAMP discovery did not return api.url");
2481
- const res = await fetch(`${base}${apiUrl}?action=aamp.mailbox.check&email=${encodeURIComponent(email)}`);
3079
+ const apiBase = new URL(apiUrl, `${base}/`).toString();
3080
+ const res = await fetch(`${apiBase}?action=aamp.mailbox.check&email=${encodeURIComponent(email)}`);
2482
3081
  if (!res.ok)
2483
3082
  throw new Error(`HTTP ${res.status}`);
2484
3083
  const data = await res.json();
@@ -2558,6 +3157,11 @@ Question: ${h.question}`,
2558
3157
  }
2559
3158
  };
2560
3159
  export {
2561
- src_default as default
3160
+ baseUrl,
3161
+ src_default as default,
3162
+ matchSenderPolicy,
3163
+ queuePendingTask,
3164
+ registerNode,
3165
+ resolveIdentity
2562
3166
  };
2563
3167
  //# sourceMappingURL=index.js.map