aamp-openclaw-plugin 0.1.29 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,11 +1774,66 @@ ${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;
1471
1835
  config;
1836
+ streamAppendQueues = /* @__PURE__ */ new Map();
1472
1837
  constructor(config) {
1473
1838
  super();
1474
1839
  this.config = config;
@@ -1521,6 +1886,9 @@ var AampClient = class _AampClient extends TinyEmitter {
1521
1886
  this.jmapClient.on("task.ack", (ack) => {
1522
1887
  this.emit("task.ack", ack);
1523
1888
  });
1889
+ this.jmapClient.on("task.stream.opened", (stream) => {
1890
+ this.emit("task.stream.opened", stream);
1891
+ });
1524
1892
  this.jmapClient.on("card.query", (query) => {
1525
1893
  this.emit("card.query", query);
1526
1894
  });
@@ -1679,6 +2047,9 @@ var AampClient = class _AampClient extends TinyEmitter {
1679
2047
  async sendHelp(opts) {
1680
2048
  return this.smtpSender.sendHelp(opts);
1681
2049
  }
2050
+ async sendStreamOpened(opts) {
2051
+ return this.smtpSender.sendStreamOpened(opts);
2052
+ }
1682
2053
  async sendCardQuery(opts) {
1683
2054
  return this.smtpSender.sendCardQuery(opts);
1684
2055
  }
@@ -1740,6 +2111,293 @@ var AampClient = class _AampClient extends TinyEmitter {
1740
2111
  const data = await res.json();
1741
2112
  return data.agents;
1742
2113
  }
2114
+ async getThreadHistory(taskId, opts = {}) {
2115
+ const base = this.config.baseUrl;
2116
+ const mailboxToken = this.config.mailboxToken;
2117
+ const res = await _AampClient.callDiscoveredApi(base, {
2118
+ action: "aamp.mailbox.thread",
2119
+ authToken: mailboxToken,
2120
+ query: {
2121
+ taskId,
2122
+ includeStreamOpened: opts.includeStreamOpened
2123
+ }
2124
+ });
2125
+ if (!res.ok) {
2126
+ const body = await res.text().catch(() => "");
2127
+ throw new Error(`Thread history fetch failed: ${res.status} ${body || res.statusText}`);
2128
+ }
2129
+ const data = await res.json();
2130
+ return {
2131
+ taskId: data.taskId,
2132
+ events: Array.isArray(data.events) ? data.events : []
2133
+ };
2134
+ }
2135
+ async hydrateTaskDispatch(task) {
2136
+ const history = await this.getThreadHistory(task.taskId);
2137
+ const priorEvents = history.events.filter((event) => event.messageId !== task.messageId);
2138
+ return {
2139
+ ...task,
2140
+ threadHistory: priorEvents,
2141
+ threadContextText: renderThreadHistoryForAgent(priorEvents)
2142
+ };
2143
+ }
2144
+ async resolveStreamCapability() {
2145
+ const discovery = await _AampClient.discoverAampService(this.config.baseUrl);
2146
+ const stream = discovery.capabilities?.stream;
2147
+ if (!stream?.transport) {
2148
+ throw new Error("AAMP stream capability is not available on this service");
2149
+ }
2150
+ return stream;
2151
+ }
2152
+ async createStream(opts) {
2153
+ const stream = await this.resolveStreamCapability();
2154
+ const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
2155
+ action: stream.createAction ?? "aamp.stream.create",
2156
+ method: "POST",
2157
+ authToken: this.config.mailboxToken,
2158
+ body: opts
2159
+ });
2160
+ if (!res.ok) {
2161
+ const body = await res.text().catch(() => "");
2162
+ throw new Error(`AAMP stream create failed: ${res.status} ${body || res.statusText}`);
2163
+ }
2164
+ return res.json();
2165
+ }
2166
+ getStreamAppendQueue(streamId) {
2167
+ let queue = this.streamAppendQueues.get(streamId);
2168
+ if (!queue) {
2169
+ queue = { running: false, operations: [] };
2170
+ this.streamAppendQueues.set(streamId, queue);
2171
+ }
2172
+ return queue;
2173
+ }
2174
+ async dispatchStreamAppend(opts) {
2175
+ const stream = await this.resolveStreamCapability();
2176
+ const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
2177
+ action: stream.appendAction ?? "aamp.stream.append",
2178
+ method: "POST",
2179
+ authToken: this.config.mailboxToken,
2180
+ body: opts
2181
+ });
2182
+ if (!res.ok) {
2183
+ const body = await res.text().catch(() => "");
2184
+ throw new Error(`AAMP stream append failed: ${res.status} ${body || res.statusText}`);
2185
+ }
2186
+ return res.json();
2187
+ }
2188
+ enqueueStreamAppend(streamId, operation) {
2189
+ const queue = this.getStreamAppendQueue(streamId);
2190
+ queue.operations.push(operation);
2191
+ void this.drainStreamAppendQueue(streamId);
2192
+ }
2193
+ async drainStreamAppendQueue(streamId) {
2194
+ const queue = this.streamAppendQueues.get(streamId);
2195
+ if (!queue || queue.running)
2196
+ return;
2197
+ queue.running = true;
2198
+ try {
2199
+ while (queue.operations.length) {
2200
+ const operation = queue.operations.shift();
2201
+ if (!operation)
2202
+ continue;
2203
+ if (operation.kind === "text-delta-batch") {
2204
+ try {
2205
+ const event = await this.dispatchStreamAppend({
2206
+ streamId,
2207
+ type: "text.delta",
2208
+ payload: {
2209
+ ...operation.payload,
2210
+ text: operation.text
2211
+ }
2212
+ });
2213
+ for (const resolve of operation.resolvers)
2214
+ resolve(event);
2215
+ } catch (error) {
2216
+ for (const reject of operation.rejecters)
2217
+ reject(error);
2218
+ }
2219
+ continue;
2220
+ }
2221
+ try {
2222
+ const event = await this.dispatchStreamAppend(operation.opts);
2223
+ operation.resolve(event);
2224
+ } catch (error) {
2225
+ operation.reject(error);
2226
+ }
2227
+ }
2228
+ } finally {
2229
+ queue.running = false;
2230
+ if (queue.operations.length === 0) {
2231
+ this.streamAppendQueues.delete(streamId);
2232
+ }
2233
+ }
2234
+ }
2235
+ async flushStreamAppendQueue(streamId) {
2236
+ while (true) {
2237
+ const queue = this.streamAppendQueues.get(streamId);
2238
+ if (!queue)
2239
+ return;
2240
+ if (!queue.running && queue.operations.length === 0) {
2241
+ this.streamAppendQueues.delete(streamId);
2242
+ return;
2243
+ }
2244
+ await new Promise((resolve) => setTimeout(resolve, 0));
2245
+ }
2246
+ }
2247
+ async appendStreamEvent(opts) {
2248
+ if (opts.type === "text.delta" && typeof opts.payload.text === "string") {
2249
+ return await new Promise((resolve, reject) => {
2250
+ const queue = this.getStreamAppendQueue(opts.streamId);
2251
+ const lastOperation = queue.operations.at(-1);
2252
+ if (lastOperation?.kind === "text-delta-batch") {
2253
+ lastOperation.text += String(opts.payload.text ?? "");
2254
+ lastOperation.resolvers.push(resolve);
2255
+ lastOperation.rejecters.push(reject);
2256
+ return;
2257
+ }
2258
+ this.enqueueStreamAppend(opts.streamId, {
2259
+ kind: "text-delta-batch",
2260
+ text: String(opts.payload.text ?? ""),
2261
+ payload: {
2262
+ ...opts.payload
2263
+ },
2264
+ resolvers: [resolve],
2265
+ rejecters: [reject]
2266
+ });
2267
+ });
2268
+ }
2269
+ return await new Promise((resolve, reject) => {
2270
+ this.enqueueStreamAppend(opts.streamId, {
2271
+ kind: "single-event",
2272
+ opts,
2273
+ resolve,
2274
+ reject
2275
+ });
2276
+ });
2277
+ }
2278
+ async closeStream(opts) {
2279
+ await this.flushStreamAppendQueue(opts.streamId);
2280
+ const stream = await this.resolveStreamCapability();
2281
+ const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
2282
+ action: stream.closeAction ?? "aamp.stream.close",
2283
+ method: "POST",
2284
+ authToken: this.config.mailboxToken,
2285
+ body: opts
2286
+ });
2287
+ if (!res.ok) {
2288
+ const body = await res.text().catch(() => "");
2289
+ throw new Error(`AAMP stream close failed: ${res.status} ${body || res.statusText}`);
2290
+ }
2291
+ return res.json();
2292
+ }
2293
+ async getTaskStream(opts) {
2294
+ const stream = await this.resolveStreamCapability();
2295
+ const res = await _AampClient.callDiscoveredApi(this.config.baseUrl, {
2296
+ action: stream.getAction ?? "aamp.stream.get",
2297
+ authToken: this.config.mailboxToken,
2298
+ query: {
2299
+ ...opts.taskId ? { taskId: opts.taskId } : {},
2300
+ ...opts.streamId ? { streamId: opts.streamId } : {}
2301
+ }
2302
+ });
2303
+ if (res.status === 404)
2304
+ return null;
2305
+ if (!res.ok) {
2306
+ const body = await res.text().catch(() => "");
2307
+ throw new Error(`AAMP stream get failed: ${res.status} ${body || res.statusText}`);
2308
+ }
2309
+ return res.json();
2310
+ }
2311
+ async subscribeStream(streamId, handlers, opts = {}) {
2312
+ const stream = await this.resolveStreamCapability();
2313
+ const template = stream.subscribeUrlTemplate;
2314
+ if (!template)
2315
+ throw new Error("AAMP stream subscribeUrlTemplate is missing");
2316
+ const url = new URL(template.replace("{streamId}", encodeURIComponent(streamId)), this.config.baseUrl);
2317
+ if (opts.lastEventId) {
2318
+ url.searchParams.set("lastEventId", opts.lastEventId);
2319
+ }
2320
+ const controller = new AbortController();
2321
+ if (opts.signal) {
2322
+ opts.signal.addEventListener("abort", () => controller.abort(), { once: true });
2323
+ }
2324
+ const res = await fetch(url, {
2325
+ headers: {
2326
+ Authorization: `Basic ${this.config.mailboxToken}`,
2327
+ Accept: "text/event-stream"
2328
+ },
2329
+ signal: controller.signal
2330
+ });
2331
+ if (!res.ok || !res.body) {
2332
+ throw new Error(`AAMP stream subscribe failed: ${res.status} ${res.statusText}`);
2333
+ }
2334
+ handlers.onOpen?.();
2335
+ const reader = res.body.getReader();
2336
+ const decoder = new TextDecoder();
2337
+ let buffer = "";
2338
+ let currentEvent = "message";
2339
+ let currentId = "";
2340
+ let currentData = [];
2341
+ const flush = () => {
2342
+ if (!currentData.length)
2343
+ return;
2344
+ try {
2345
+ const parsed = JSON.parse(currentData.join("\n"));
2346
+ handlers.onEvent({
2347
+ ...parsed,
2348
+ ...currentId ? { id: currentId } : {},
2349
+ type: parsed.type ?? currentEvent
2350
+ });
2351
+ } catch (err) {
2352
+ handlers.onError?.(err);
2353
+ } finally {
2354
+ currentEvent = "message";
2355
+ currentId = "";
2356
+ currentData = [];
2357
+ }
2358
+ };
2359
+ void (async () => {
2360
+ try {
2361
+ while (true) {
2362
+ const { value, done } = await reader.read();
2363
+ if (done)
2364
+ break;
2365
+ buffer += decoder.decode(value, { stream: true });
2366
+ let index = buffer.indexOf("\n\n");
2367
+ while (index >= 0) {
2368
+ const frame = buffer.slice(0, index);
2369
+ buffer = buffer.slice(index + 2);
2370
+ for (const rawLine of frame.split("\n")) {
2371
+ const line = rawLine.replace(/\r$/, "");
2372
+ if (!line || line.startsWith(":"))
2373
+ continue;
2374
+ if (line.startsWith("event:")) {
2375
+ currentEvent = line.slice(6).trim();
2376
+ } else if (line.startsWith("id:")) {
2377
+ currentId = line.slice(3).trim();
2378
+ } else if (line.startsWith("data:")) {
2379
+ currentData.push(line.slice(5).trimStart());
2380
+ }
2381
+ }
2382
+ flush();
2383
+ index = buffer.indexOf("\n\n");
2384
+ }
2385
+ }
2386
+ } catch (err) {
2387
+ if (!controller.signal.aborted) {
2388
+ handlers.onError?.(err);
2389
+ }
2390
+ } finally {
2391
+ buffer += decoder.decode();
2392
+ controller.abort();
2393
+ }
2394
+ })();
2395
+ return {
2396
+ close() {
2397
+ controller.abort();
2398
+ }
2399
+ };
2400
+ }
1743
2401
  /**
1744
2402
  * Download a blob (attachment) by its JMAP blobId.
1745
2403
  * Use this to retrieve attachment content from received TaskDispatch or TaskResult messages.
@@ -1767,6 +2425,9 @@ var AampClient = class _AampClient extends TinyEmitter {
1767
2425
  }
1768
2426
  };
1769
2427
 
2428
+ // src/index.ts
2429
+ import { readFileSync as readFileSync2 } from "node:fs";
2430
+
1770
2431
  // src/file-store.ts
1771
2432
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1772
2433
  import { dirname, join } from "node:path";
@@ -1873,7 +2534,9 @@ function baseUrl(aampHost) {
1873
2534
  return `https://${aampHost}`;
1874
2535
  }
1875
2536
  var pendingTasks = /* @__PURE__ */ new Map();
2537
+ var activeTaskStreams = /* @__PURE__ */ new Map();
1876
2538
  var terminalTaskIds = new Set(loadTaskState(defaultTaskStatePath()).terminalTaskIds ?? []);
2539
+ var AAMP_SESSION_PREFIX = "aamp:";
1877
2540
  var dispatchedSubtasks = /* @__PURE__ */ new Map();
1878
2541
  var waitingDispatches = /* @__PURE__ */ new Map();
1879
2542
  var aampClient = null;
@@ -1884,9 +2547,57 @@ var lastTransportMode = "disconnected";
1884
2547
  var lastLoggedTransportMode = "disconnected";
1885
2548
  var reconcileTimer = null;
1886
2549
  var transportMonitorTimer = null;
1887
- var currentSessionKey = "agent:main:main";
2550
+ var historicalReconcileCompleted = false;
1888
2551
  var channelRuntime = null;
1889
2552
  var channelCfg = null;
2553
+ async function ensureTaskStream(task) {
2554
+ if (!aampClient?.isConnected())
2555
+ return null;
2556
+ const existing = activeTaskStreams.get(task.taskId);
2557
+ if (existing)
2558
+ return existing;
2559
+ const created = await aampClient.createStream({
2560
+ taskId: task.taskId,
2561
+ peerEmail: task.from
2562
+ });
2563
+ await aampClient.sendStreamOpened({
2564
+ to: task.from,
2565
+ taskId: task.taskId,
2566
+ streamId: created.streamId,
2567
+ inReplyTo: task.messageId || void 0
2568
+ });
2569
+ await aampClient.appendStreamEvent({
2570
+ streamId: created.streamId,
2571
+ type: "status",
2572
+ payload: { state: "running", label: "Task queued in OpenClaw" }
2573
+ });
2574
+ activeTaskStreams.set(task.taskId, created.streamId);
2575
+ return created.streamId;
2576
+ }
2577
+ async function appendTaskStream(taskId, type, payload) {
2578
+ if (!aampClient?.isConnected())
2579
+ return;
2580
+ const streamId = activeTaskStreams.get(taskId);
2581
+ if (!streamId)
2582
+ return;
2583
+ await aampClient.appendStreamEvent({
2584
+ streamId,
2585
+ type,
2586
+ payload
2587
+ });
2588
+ }
2589
+ async function closeTaskStream(taskId, payload) {
2590
+ if (!aampClient?.isConnected())
2591
+ return;
2592
+ const streamId = activeTaskStreams.get(taskId);
2593
+ if (!streamId)
2594
+ return;
2595
+ activeTaskStreams.delete(taskId);
2596
+ await aampClient.closeStream({
2597
+ streamId,
2598
+ payload
2599
+ });
2600
+ }
1890
2601
  function logTransportState(api, mode, email, previousMode) {
1891
2602
  if (mode === previousMode)
1892
2603
  return;
@@ -1903,6 +2614,12 @@ function logTransportState(api, mode, email, previousMode) {
1903
2614
  function isSyntheticPendingKey(taskKey) {
1904
2615
  return taskKey.startsWith("result:") || taskKey.startsWith("help:");
1905
2616
  }
2617
+ function isAampSessionKey(sessionKey) {
2618
+ return typeof sessionKey === "string" && sessionKey.startsWith(AAMP_SESSION_PREFIX);
2619
+ }
2620
+ function buildAampWakeSessionKey(kind, id) {
2621
+ return `${AAMP_SESSION_PREFIX}wake:${kind}:${id}`;
2622
+ }
1906
2623
  function saveTerminalTaskIds() {
1907
2624
  saveTaskState({ terminalTaskIds: [...terminalTaskIds] }, defaultTaskStatePath());
1908
2625
  }
@@ -1928,6 +2645,17 @@ function hasExpired(task) {
1928
2645
  }
1929
2646
  return false;
1930
2647
  }
2648
+ function isTransientTransportError(message) {
2649
+ return [
2650
+ "ECONNRESET",
2651
+ "ETIMEDOUT",
2652
+ "ECONNREFUSED",
2653
+ "EPIPE",
2654
+ "UND_ERR_SOCKET",
2655
+ "UND_ERR_CONNECT_TIMEOUT",
2656
+ "fetch failed"
2657
+ ].some((needle) => message.includes(needle));
2658
+ }
1931
2659
  function nextPendingEntry() {
1932
2660
  const entries = [...pendingTasks.entries()];
1933
2661
  const notifications = entries.filter(([key]) => key.startsWith("result:") || key.startsWith("help:"));
@@ -1950,6 +2678,8 @@ function queuePendingTask(task) {
1950
2678
  from: task.from,
1951
2679
  title: task.title,
1952
2680
  bodyText: task.bodyText ?? "",
2681
+ threadHistory: task.threadHistory ?? [],
2682
+ threadContextText: task.threadContextText ?? "",
1953
2683
  priority: task.priority ?? "normal",
1954
2684
  ...task.expiresAt ? { expiresAt: task.expiresAt } : {},
1955
2685
  contextLinks: task.contextLinks ?? [],
@@ -2023,6 +2753,18 @@ var src_default = {
2023
2753
  default: "openclaw-agent",
2024
2754
  description: "Agent name prefix used in the mailbox address"
2025
2755
  },
2756
+ summary: {
2757
+ type: "string",
2758
+ description: "Directory summary shown when other agents search for this agent."
2759
+ },
2760
+ cardText: {
2761
+ type: "string",
2762
+ description: "Inline card text used for automatic card.response replies."
2763
+ },
2764
+ cardFile: {
2765
+ type: "string",
2766
+ description: "Absolute path to a card text file. Used when cardText is not set."
2767
+ },
2026
2768
  credentialsFile: {
2027
2769
  type: "string",
2028
2770
  description: "Absolute path to cache AAMP credentials between gateway restarts. Default: ~/.openclaw/extensions/aamp-openclaw-plugin/.credentials.json. Delete this file to force re-registration with a new mailbox."
@@ -2089,11 +2831,35 @@ var src_default = {
2089
2831
  api.logger.warn(`[AAMP] Could not trigger heartbeat for ${label}: ${err.message}`);
2090
2832
  }
2091
2833
  }
2834
+ function getConfiguredCardText() {
2835
+ const inline = cfg.cardText?.trim();
2836
+ if (inline)
2837
+ return inline;
2838
+ const file = cfg.cardFile?.trim();
2839
+ if (!file)
2840
+ return void 0;
2841
+ const fromFile = readFileSync2(file, "utf-8").trim();
2842
+ return fromFile || void 0;
2843
+ }
2844
+ async function syncDirectoryProfile() {
2845
+ if (!aampClient)
2846
+ return;
2847
+ const summary = cfg.summary?.trim();
2848
+ const cardText = getConfiguredCardText();
2849
+ if (!summary && !cardText)
2850
+ return;
2851
+ await aampClient.updateDirectoryProfile({
2852
+ ...summary ? { summary } : {},
2853
+ ...cardText ? { cardText } : {}
2854
+ });
2855
+ api.logger.info(`[AAMP] Directory profile synced${cardText ? " (card text registered)" : ""}`);
2856
+ }
2092
2857
  function wakeAgentForPendingTask(task) {
2093
- const fallback = () => triggerHeartbeatWake(currentSessionKey, `task ${task.taskId}`);
2858
+ const fallbackSessionKey = buildAampWakeSessionKey("task", task.taskId);
2859
+ const fallback = () => triggerHeartbeatWake(fallbackSessionKey, `task ${task.taskId}`);
2094
2860
  const dispatcher = channelRuntime?.reply?.dispatchReplyWithBufferedBlockDispatcher;
2095
2861
  api.logger.info(
2096
- `[AAMP] Wake requested for task ${task.taskId} \u2014 channelRuntime=${channelRuntime ? "yes" : "no"} channelCfg=${channelCfg ? "yes" : "no"} dispatcher=${typeof dispatcher === "function" ? "yes" : "no"} session=${currentSessionKey}`
2862
+ `[AAMP] Wake requested for task ${task.taskId} \u2014 channelRuntime=${channelRuntime ? "yes" : "no"} channelCfg=${channelCfg ? "yes" : "no"} dispatcher=${typeof dispatcher === "function" ? "yes" : "no"} fallbackSession=${fallbackSessionKey}`
2097
2863
  );
2098
2864
  if (!channelRuntime || !channelCfg || typeof dispatcher !== "function") {
2099
2865
  fallback();
@@ -2145,6 +2911,16 @@ var src_default = {
2145
2911
  fallback();
2146
2912
  }
2147
2913
  }
2914
+ async function reconcileMailbox(includeHistorical) {
2915
+ if (!aampClient)
2916
+ return;
2917
+ const opts = includeHistorical ? { includeHistorical: true } : void 0;
2918
+ const count = await aampClient.reconcileRecentEmails(100, opts);
2919
+ if (includeHistorical && !historicalReconcileCompleted) {
2920
+ historicalReconcileCompleted = true;
2921
+ api.logger.info(`[AAMP] Historical mailbox reconcile complete (${count} email(s) scanned)`);
2922
+ }
2923
+ }
2148
2924
  async function doConnect(identity) {
2149
2925
  if (reconcileTimer) {
2150
2926
  clearInterval(reconcileTimer);
@@ -2171,36 +2947,49 @@ var src_default = {
2171
2947
  });
2172
2948
  aampClient.on("task.dispatch", (task) => {
2173
2949
  api.logger.info(`[AAMP] \u2190 task.dispatch ${task.taskId} "${task.title}" from=${task.from}`);
2174
- 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}`);
2950
+ void (async () => {
2951
+ try {
2952
+ if (terminalTaskIds.has(task.taskId)) {
2953
+ api.logger.info(`[AAMP] Skipping already-terminal task ${task.taskId}`);
2954
+ return;
2955
+ }
2956
+ const decision = matchSenderPolicy(task, cfg.senderPolicies);
2957
+ if (!decision.allowed) {
2958
+ api.logger.warn(`[AAMP] \u2717 rejected by senderPolicies: ${task.from} task=${task.taskId} reason=${decision.reason}`);
2959
+ void aampClient.sendResult({
2960
+ to: task.from,
2961
+ taskId: task.taskId,
2962
+ status: "rejected",
2963
+ output: "",
2964
+ errorMsg: decision.reason ?? `Sender ${task.from} is not allowed.`
2965
+ }).catch((err) => {
2966
+ api.logger.error(`[AAMP] Failed to send rejection for task ${task.taskId}: ${err.message}`);
2967
+ });
2968
+ return;
2969
+ }
2970
+ const hydratedTask = await aampClient.hydrateTaskDispatch(task).catch((err) => {
2971
+ api.logger.warn(`[AAMP] Failed to load thread history for ${task.taskId}: ${err.message}`);
2972
+ return {
2973
+ ...task,
2974
+ threadHistory: [],
2975
+ threadContextText: ""
2976
+ };
2190
2977
  });
2191
- 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}`);
2978
+ if (!queuePendingTask(hydratedTask)) {
2979
+ api.logger.info(`[AAMP] Ignoring already-terminal or expired task ${task.taskId}`);
2980
+ return;
2981
+ }
2982
+ void ensureTaskStream(pendingTasks.get(task.taskId)).catch((err) => {
2983
+ api.logger.warn(`[AAMP] Failed to open stream for task ${task.taskId}: ${err.message}`);
2984
+ });
2985
+ wakeAgentForPendingTask(pendingTasks.get(task.taskId));
2986
+ } catch (err) {
2987
+ api.logger.error(`[AAMP] task.dispatch handler failed for ${task.taskId}: ${err.message}`);
2988
+ if (pendingTasks.has(task.taskId)) {
2989
+ triggerHeartbeatWake(buildAampWakeSessionKey("task", task.taskId), `task ${task.taskId}`);
2990
+ }
2202
2991
  }
2203
- }
2992
+ })();
2204
2993
  });
2205
2994
  aampClient.on("task.cancel", (cancel) => {
2206
2995
  api.logger.info(`[AAMP] \u2190 task.cancel ${cancel.taskId} from=${cancel.from}`);
@@ -2210,6 +2999,8 @@ var src_default = {
2210
2999
  dispatchedSubtasks.delete(cancel.taskId);
2211
3000
  waitingDispatches.delete(cancel.taskId);
2212
3001
  rememberTerminalTask(cancel.taskId);
3002
+ void closeTaskStream(cancel.taskId, { reason: "task.cancel" }).catch(() => {
3003
+ });
2213
3004
  if (removed) {
2214
3005
  api.logger.info(`[AAMP] Cancelled task ${cancel.taskId} \u2014 removed from pending queue`);
2215
3006
  }
@@ -2330,7 +3121,7 @@ ${notifyBody?.bodyText ?? "Sub-task completed."}${actionSection}`;
2330
3121
  api.logger.error(`[AAMP] Channel dispatch failed: ${err.message}`);
2331
3122
  });
2332
3123
  } else {
2333
- const notifySessionKey = `agent:main:aamp-notify-${Date.now()}`;
3124
+ const notifySessionKey = buildAampWakeSessionKey("result", result.taskId);
2334
3125
  try {
2335
3126
  api.runtime.system.requestHeartbeatNow({ reason: "wake", sessionKey: notifySessionKey });
2336
3127
  api.logger.info(`[AAMP] Heartbeat for sub-task result ${result.taskId}`);
@@ -2406,7 +3197,7 @@ ${notifyBody?.bodyText ?? help.question}`;
2406
3197
  api.logger.error(`[AAMP] Channel dispatch failed for help: ${err.message}`);
2407
3198
  });
2408
3199
  } else {
2409
- const helpSessionKey = `agent:main:aamp-notify-${Date.now()}`;
3200
+ const helpSessionKey = buildAampWakeSessionKey("help", help.taskId);
2410
3201
  try {
2411
3202
  api.runtime.system.requestHeartbeatNow({ reason: "wake", sessionKey: helpSessionKey });
2412
3203
  api.logger.info(`[AAMP] Heartbeat fallback for sub-task help ${help.taskId}`);
@@ -2441,9 +3232,16 @@ ${notifyBody?.bodyText ?? help.question}`;
2441
3232
  }
2442
3233
  return;
2443
3234
  }
3235
+ if (err.message.startsWith("Safety reconcile failed:") && isTransientTransportError(err.message)) {
3236
+ api.logger.warn(`[AAMP] ${err.message}`);
3237
+ return;
3238
+ }
2444
3239
  api.logger.error(`[AAMP] ${err.message}`);
2445
3240
  });
2446
3241
  await aampClient.connect();
3242
+ await syncDirectoryProfile().catch((err) => {
3243
+ api.logger.warn(`[AAMP] Directory profile sync failed: ${err.message}`);
3244
+ });
2447
3245
  api.logger.info(
2448
3246
  `[AAMP] Transport after connect \u2014 ${aampClient.isUsingPollingFallback() ? "polling fallback" : "websocket"} as ${agentEmail}`
2449
3247
  );
@@ -2466,9 +3264,13 @@ ${notifyBody?.bodyText ?? help.question}`;
2466
3264
  lastTransportMode = mode;
2467
3265
  lastLoggedTransportMode = mode;
2468
3266
  }, 1e3);
2469
- void aampClient.reconcileRecentEmails(100, { includeHistorical: true }).catch((err) => {
3267
+ void reconcileMailbox(!historicalReconcileCompleted).catch((err) => {
2470
3268
  lastConnectionError = err.message;
2471
- api.logger.warn(`[AAMP] Startup mailbox reconcile failed: ${err.message}`);
3269
+ if (!historicalReconcileCompleted) {
3270
+ api.logger.warn(`[AAMP] Startup mailbox reconcile failed: ${err.message} (will retry historical tasks)`);
3271
+ } else {
3272
+ api.logger.warn(`[AAMP] Startup mailbox reconcile failed: ${err.message}`);
3273
+ }
2472
3274
  });
2473
3275
  transportMonitorTimer = setInterval(() => {
2474
3276
  if (!aampClient)
@@ -2487,9 +3289,14 @@ ${notifyBody?.bodyText ?? help.question}`;
2487
3289
  reconcileTimer = setInterval(() => {
2488
3290
  if (!aampClient)
2489
3291
  return;
2490
- void aampClient.reconcileRecentEmails(100).catch((err) => {
3292
+ const includeHistorical = !historicalReconcileCompleted;
3293
+ void reconcileMailbox(includeHistorical).catch((err) => {
2491
3294
  lastConnectionError = err.message;
2492
- api.logger.warn(`[AAMP] Mailbox reconcile failed: ${err.message}`);
3295
+ if (includeHistorical) {
3296
+ api.logger.warn(`[AAMP] Mailbox reconcile failed while retrying historical tasks: ${err.message}`);
3297
+ } else {
3298
+ api.logger.warn(`[AAMP] Mailbox reconcile failed: ${err.message}`);
3299
+ }
2493
3300
  });
2494
3301
  }, 15e3);
2495
3302
  }
@@ -2530,7 +3337,10 @@ ${notifyBody?.bodyText ?? help.question}`;
2530
3337
  return;
2531
3338
  api.logger.info(`[AAMP] gateway_start: re-triggering heartbeat for ${pendingTasks.size} pending task(s)`);
2532
3339
  try {
2533
- api.runtime.system.requestHeartbeatNow({ reason: "wake", sessionKey: currentSessionKey });
3340
+ api.runtime.system.requestHeartbeatNow({
3341
+ reason: "wake",
3342
+ sessionKey: buildAampWakeSessionKey("queue", "gateway-start")
3343
+ });
2534
3344
  } catch (err) {
2535
3345
  api.logger.warn(`[AAMP] gateway_start heartbeat failed: ${err.message}`);
2536
3346
  }
@@ -2538,8 +3348,8 @@ ${notifyBody?.bodyText ?? help.question}`;
2538
3348
  api.on(
2539
3349
  "before_prompt_build",
2540
3350
  (_event, ctx) => {
2541
- if (ctx?.sessionKey && !String(ctx.sessionKey).startsWith("aamp:")) {
2542
- currentSessionKey = ctx.sessionKey;
3351
+ if (!isAampSessionKey(ctx?.sessionKey)) {
3352
+ return {};
2543
3353
  }
2544
3354
  for (const [id, t] of pendingTasks) {
2545
3355
  if (hasExpired(t)) {
@@ -2642,10 +3452,12 @@ ${task.bodyText}` : "",
2642
3452
  `### Sub-task dispatch rules:`,
2643
3453
  `If you delegate work to another agent via aamp_dispatch_task, you MUST pass`,
2644
3454
  `parentTaskId: "${task.taskId}" to establish the parent-child relationship.`,
3455
+ `If you need to find a suitable agent first, call aamp_directory_search.`,
2645
3456
  ``,
2646
3457
  `Task ID: ${task.taskId}`,
2647
3458
  `From: ${task.from}`,
2648
3459
  `Title: ${task.title}`,
3460
+ task.threadContextText ? `${task.threadContextText}` : "",
2649
3461
  task.bodyText ? `Description:
2650
3462
  ${task.bodyText}` : "",
2651
3463
  task.contextLinks.length ? `Context Links:
@@ -2659,6 +3471,44 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
2659
3471
  },
2660
3472
  { priority: 5 }
2661
3473
  );
3474
+ api.registerTool({
3475
+ name: "aamp_directory_search",
3476
+ description: "Search the AAMP directory for agents by capability summary, card text, or email address.",
3477
+ parameters: {
3478
+ type: "object",
3479
+ required: ["query"],
3480
+ properties: {
3481
+ query: { type: "string", description: "Capability or keyword to search for" },
3482
+ limit: { type: "number", description: "Maximum number of matches to return (default: 10)" },
3483
+ includeSelf: { type: "boolean", description: "Whether to include the current agent in results" }
3484
+ }
3485
+ },
3486
+ execute: async (_id, params) => {
3487
+ if (!aampClient) {
3488
+ return { content: [{ type: "text", text: "Error: AAMP client is not connected." }] };
3489
+ }
3490
+ const query = String(params.query ?? "").trim();
3491
+ if (!query) {
3492
+ return { content: [{ type: "text", text: "Error: query is required." }] };
3493
+ }
3494
+ const agents = await aampClient.searchDirectory({
3495
+ query,
3496
+ limit: Number(params.limit ?? 10),
3497
+ includeSelf: Boolean(params.includeSelf)
3498
+ });
3499
+ if (!agents.length) {
3500
+ return { content: [{ type: "text", text: `No agents matched "${query}".` }] };
3501
+ }
3502
+ return {
3503
+ content: [{
3504
+ type: "text",
3505
+ text: agents.map(
3506
+ (agent, index) => `${index + 1}. ${agent.email}${agent.summary ? ` \u2014 ${agent.summary}` : ""}`
3507
+ ).join("\n")
3508
+ }]
3509
+ };
3510
+ }
3511
+ }, { name: "aamp_directory_search" });
2662
3512
  api.registerTool({
2663
3513
  name: "aamp_send_result",
2664
3514
  description: "Send the result of an AAMP task back to the dispatcher. Call this after you have finished processing the task.",
@@ -2758,6 +3608,18 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
2758
3608
  content: readBinaryFile(a.path)
2759
3609
  }));
2760
3610
  }
3611
+ await appendTaskStream(task.taskId, "status", {
3612
+ state: "completing",
3613
+ label: `Sending ${p.status} result`
3614
+ });
3615
+ if (p.output) {
3616
+ await appendTaskStream(task.taskId, "text.delta", { text: p.output });
3617
+ }
3618
+ await closeTaskStream(task.taskId, {
3619
+ reason: "task.result",
3620
+ status: p.status,
3621
+ ...p.errorMsg ? { error: p.errorMsg } : {}
3622
+ });
2761
3623
  await aampClient.sendResult({
2762
3624
  to: task.from,
2763
3625
  taskId: task.taskId,
@@ -2773,7 +3635,10 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
2773
3635
  api.logger.info(`[AAMP] \u2192 task.result ${task.taskId} ${p.status}`);
2774
3636
  if (pendingTasks.size > 0) {
2775
3637
  try {
2776
- api.runtime.system.requestHeartbeatNow({ reason: "wake", sessionKey: currentSessionKey });
3638
+ api.runtime.system.requestHeartbeatNow({
3639
+ reason: "wake",
3640
+ sessionKey: buildAampWakeSessionKey("queue", "follow-up")
3641
+ });
2777
3642
  } catch {
2778
3643
  }
2779
3644
  }
@@ -2824,6 +3689,13 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
2824
3689
  if (!aampClient?.isConnected()) {
2825
3690
  return { content: [{ type: "text", text: "Error: AAMP client is not connected." }] };
2826
3691
  }
3692
+ await appendTaskStream(task.taskId, "status", {
3693
+ state: "help_needed",
3694
+ label: p.blockedReason
3695
+ });
3696
+ await closeTaskStream(task.taskId, {
3697
+ reason: "task.help_needed"
3698
+ });
2827
3699
  await aampClient.sendHelp({
2828
3700
  to: task.from,
2829
3701
  taskId: task.taskId,