aamp-openclaw-plugin 0.1.28 → 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/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- // ../sdk/src/jmap-push.ts
1
+ // ../sdk/src/jmap-push.js
2
2
  import WebSocket from "ws";
3
3
 
4
- // ../sdk/src/types.ts
5
- var AAMP_PROTOCOL_VERSION = "1.0";
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.ts
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
- Object.entries(headers).map(([k, v]) => [
109
- k.toLowerCase(),
110
- Array.isArray(v) ? v[0] : v
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.ts
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.ts
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
- JSON.stringify({
795
- "@type": "WebSocketPushEnable",
796
- dataTypes: ["Email"],
797
- pushState: null
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
- `[AAMP-SDK] blob download failed status=${res.status} attempt=${attempt}/${maxAttempts} url=${downloadUrl}`
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.ts
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
- constructor(config) {
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/client.ts
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
- try {
2175
- if (terminalTaskIds.has(task.taskId)) {
2176
- api.logger.info(`[AAMP] Skipping already-terminal task ${task.taskId}`);
2177
- return;
2178
- }
2179
- const decision = matchSenderPolicy(task, cfg.senderPolicies);
2180
- if (!decision.allowed) {
2181
- api.logger.warn(`[AAMP] \u2717 rejected by senderPolicies: ${task.from} task=${task.taskId} reason=${decision.reason}`);
2182
- void aampClient.sendResult({
2183
- to: task.from,
2184
- taskId: task.taskId,
2185
- status: "rejected",
2186
- output: "",
2187
- errorMsg: decision.reason ?? `Sender ${task.from} is not allowed.`
2188
- }).catch((err) => {
2189
- api.logger.error(`[AAMP] Failed to send rejection for task ${task.taskId}: ${err.message}`);
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
- return;
2192
- }
2193
- if (!queuePendingTask(task)) {
2194
- api.logger.info(`[AAMP] Ignoring already-terminal or expired task ${task.taskId}`);
2195
- return;
2196
- }
2197
- wakeAgentForPendingTask(pendingTasks.get(task.taskId));
2198
- } catch (err) {
2199
- api.logger.error(`[AAMP] task.dispatch handler failed for ${task.taskId}: ${err.message}`);
2200
- if (pendingTasks.has(task.taskId)) {
2201
- triggerHeartbeatWake(currentSessionKey, `task ${task.taskId}`);
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 aampClient.reconcileRecentEmails(100, { includeHistorical: true }).catch((err) => {
3160
+ void reconcileMailbox(!historicalReconcileCompleted).catch((err) => {
2470
3161
  lastConnectionError = err.message;
2471
- api.logger.warn(`[AAMP] Startup mailbox reconcile failed: ${err.message}`);
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
- void aampClient.reconcileRecentEmails(100).catch((err) => {
3185
+ const includeHistorical = !historicalReconcileCompleted;
3186
+ void reconcileMailbox(includeHistorical).catch((err) => {
2491
3187
  lastConnectionError = err.message;
2492
- api.logger.warn(`[AAMP] Mailbox reconcile failed: ${err.message}`);
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,