aamp-openclaw-plugin 0.1.36 → 0.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,6 +38,7 @@ npm run build
38
38
  "enabled": true,
39
39
  "config": {
40
40
  "aampHost": "https://meshmail.ai",
41
+ "taskDispatchConcurrency": 10,
41
42
  "slug": "openclaw-agent",
42
43
  "credentialsFile": "~/.openclaw/extensions/aamp-openclaw-plugin/.credentials.json",
43
44
  "senderPolicies": [
@@ -57,6 +58,7 @@ npm run build
57
58
  ```
58
59
 
59
60
  If `senderPolicies` is omitted, all senders are accepted. If set, the dispatch sender must match one policy and all configured dispatch-context rules for that sender must pass.
61
+ `taskDispatchConcurrency` is optional and defaults to `10`.
60
62
 
61
63
  The plugin also understands:
62
64
 
@@ -8,6 +8,7 @@ import { stdin as input, stdout as output, stderr } from 'node:process'
8
8
  import { spawnSync } from 'node:child_process'
9
9
  import { createRequire } from 'node:module'
10
10
  import { fileURLToPath } from 'node:url'
11
+ import { AampClient } from 'aamp-sdk'
11
12
 
12
13
  const PLUGIN_ID = 'aamp-openclaw-plugin'
13
14
  const DEFAULT_AAMP_HOST = 'https://meshmail.ai'
@@ -444,50 +445,15 @@ export async function ensureMailboxIdentity({ aampHost, slug, credentialsFile })
444
445
  }
445
446
  }
446
447
 
447
- const base = normalizeBaseUrl(aampHost)
448
- const discoveryRes = await fetch(`${base}/.well-known/aamp`)
449
- if (!discoveryRes.ok) {
450
- const text = await discoveryRes.text().catch(() => '')
451
- throw new Error(`AAMP discovery failed (${discoveryRes.status}): ${text || discoveryRes.statusText}`)
452
- }
453
- const discovery = await discoveryRes.json()
454
- const apiUrl = discovery?.api?.url
455
- if (!apiUrl) {
456
- throw new Error('AAMP discovery did not return api.url')
457
- }
458
- const apiBase = new URL(apiUrl, `${base}/`).toString()
459
-
460
- const registerRes = await fetch(`${apiBase}?action=aamp.mailbox.register`, {
461
- method: 'POST',
462
- headers: { 'Content-Type': 'application/json' },
463
- body: JSON.stringify({
464
- slug,
465
- description: 'OpenClaw AAMP agent node',
466
- }),
448
+ const credData = await AampClient.registerMailbox({
449
+ aampHost: normalizeBaseUrl(aampHost),
450
+ slug,
451
+ description: 'OpenClaw AAMP agent node',
467
452
  })
468
-
469
- if (!registerRes.ok) {
470
- const text = await registerRes.text().catch(() => '')
471
- throw new Error(`AAMP self-register failed (${registerRes.status}): ${text || registerRes.statusText}`)
472
- }
473
-
474
- const registerData = await registerRes.json()
475
- const code = registerData?.registrationCode
476
- if (!code) {
477
- throw new Error('AAMP self-register succeeded but no registrationCode was returned')
478
- }
479
-
480
- const credRes = await fetch(`${apiBase}?action=aamp.mailbox.credentials&code=${encodeURIComponent(code)}`)
481
- if (!credRes.ok) {
482
- const text = await credRes.text().catch(() => '')
483
- throw new Error(`AAMP credential exchange failed (${credRes.status}): ${text || credRes.statusText}`)
484
- }
485
-
486
- const credData = await credRes.json()
487
453
  const identity = {
488
454
  email: credData?.email,
489
- mailboxToken: credData?.mailbox?.token ?? credData?.jmap?.token,
490
- smtpPassword: credData?.smtp?.password,
455
+ mailboxToken: credData?.mailboxToken,
456
+ smtpPassword: credData?.smtpPassword,
491
457
  }
492
458
 
493
459
  if (!identity.email || !identity.mailboxToken || !identity.smtpPassword) {
package/dist/index.js CHANGED
@@ -1,12 +1,13 @@
1
- // ../sdk/src/jmap-push.js
1
+ // ../sdk/dist/jmap-push.js
2
2
  import WebSocket from "ws";
3
3
 
4
- // ../sdk/src/types.js
4
+ // ../sdk/dist/types.js
5
5
  var AAMP_PROTOCOL_VERSION = "1.1";
6
6
  var AAMP_HEADER = {
7
7
  VERSION: "X-AAMP-Version",
8
8
  INTENT: "X-AAMP-Intent",
9
9
  TASK_ID: "X-AAMP-TaskId",
10
+ SESSION_KEY: "X-AAMP-Session-Key",
10
11
  CONTEXT_LINKS: "X-AAMP-ContextLinks",
11
12
  DISPATCH_CONTEXT: "X-AAMP-Dispatch-Context",
12
13
  PRIORITY: "X-AAMP-Priority",
@@ -23,7 +24,7 @@ var AAMP_HEADER = {
23
24
  CARD_SUMMARY: "X-AAMP-Card-Summary"
24
25
  };
25
26
 
26
- // ../sdk/src/parser.js
27
+ // ../sdk/dist/parser.js
27
28
  function normalizeBodyText(value) {
28
29
  return value?.replace(/\r\n/g, "\n").trim() ?? "";
29
30
  }
@@ -171,6 +172,7 @@ function parseAampHeaders(meta) {
171
172
  if (intent === "task.dispatch") {
172
173
  const contextLinksStr = getAampHeader(headers, AAMP_HEADER.CONTEXT_LINKS) ?? "";
173
174
  const dispatchContext = parseDispatchContextHeader(getAampHeader(headers, AAMP_HEADER.DISPATCH_CONTEXT));
175
+ const sessionKey = getAampHeader(headers, AAMP_HEADER.SESSION_KEY);
174
176
  const parentTaskId = getAampHeader(headers, AAMP_HEADER.PARENT_TASK_ID);
175
177
  const priority = getAampHeader(headers, AAMP_HEADER.PRIORITY) ?? "normal";
176
178
  const expiresAt = getAampHeader(headers, AAMP_HEADER.EXPIRES_AT);
@@ -178,6 +180,7 @@ function parseAampHeaders(meta) {
178
180
  protocolVersion,
179
181
  intent: "task.dispatch",
180
182
  taskId,
183
+ ...sessionKey ? { sessionKey } : {},
181
184
  title: decodedSubject.replace(/^\[AAMP Task\]\s*/, "").trim() || "Untitled Task",
182
185
  priority: priority === "urgent" || priority === "high" ? priority : "normal",
183
186
  ...expiresAt ? { expiresAt } : {},
@@ -310,6 +313,9 @@ function buildDispatchHeaders(params) {
310
313
  if (params.expiresAt) {
311
314
  headers[AAMP_HEADER.EXPIRES_AT] = params.expiresAt;
312
315
  }
316
+ if (params.sessionKey?.trim()) {
317
+ headers[AAMP_HEADER.SESSION_KEY] = params.sessionKey.trim();
318
+ }
313
319
  if (params.contextLinks.length > 0) {
314
320
  headers[AAMP_HEADER.CONTEXT_LINKS] = params.contextLinks.join(",");
315
321
  }
@@ -381,7 +387,7 @@ function buildCardResponseHeaders(params) {
381
387
  };
382
388
  }
383
389
 
384
- // ../sdk/src/tiny-emitter.js
390
+ // ../sdk/dist/tiny-emitter.js
385
391
  var TinyEmitter = class {
386
392
  listeners = /* @__PURE__ */ new Map();
387
393
  onceWrappers = /* @__PURE__ */ new WeakMap();
@@ -421,9 +427,20 @@ var TinyEmitter = class {
421
427
  }
422
428
  return true;
423
429
  }
430
+ async emitAsync(event, ...args) {
431
+ const bucket = this.listeners.get(event);
432
+ if (!bucket || bucket.size === 0)
433
+ return false;
434
+ const settled = await Promise.allSettled([...bucket].map((listener) => Promise.resolve(listener(...args))));
435
+ const rejected = settled.find((result) => result.status === "rejected");
436
+ if (rejected) {
437
+ throw rejected.reason;
438
+ }
439
+ return true;
440
+ }
424
441
  };
425
442
 
426
- // ../sdk/src/jmap-push.js
443
+ // ../sdk/dist/jmap-push.js
427
444
  function describeError(err) {
428
445
  if (!(err instanceof Error))
429
446
  return String(err);
@@ -1103,7 +1120,7 @@ var JmapPushClient = class extends TinyEmitter {
1103
1120
  }
1104
1121
  };
1105
1122
 
1106
- // ../sdk/src/smtp-sender.js
1123
+ // ../sdk/dist/smtp-sender.js
1107
1124
  import { createTransport } from "nodemailer";
1108
1125
  import { randomUUID } from "crypto";
1109
1126
  var sanitize = (s) => s.replace(/[\r\n]/g, " ").trim();
@@ -1361,7 +1378,7 @@ var SmtpSender = class _SmtpSender {
1361
1378
  from: this.config.user,
1362
1379
  to: opts.to,
1363
1380
  subject: `[AAMP Task] ${sanitize(opts.title)}`,
1364
- text: [
1381
+ text: opts.rawBodyText ?? [
1365
1382
  `Task: ${opts.title}`,
1366
1383
  `Task ID: ${taskId}`,
1367
1384
  `Priority: ${opts.priority ?? "normal"}`,
@@ -1429,7 +1446,7 @@ ${opts.contextLinks.map((l) => ` ${l}`).join("\n")}` : "",
1429
1446
  from: this.config.user,
1430
1447
  to: opts.to,
1431
1448
  subject: `[AAMP Result] Task ${opts.taskId} \u2014 ${opts.status}`,
1432
- text: [
1449
+ text: opts.rawBodyText ?? [
1433
1450
  `AAMP Task Result`,
1434
1451
  ``,
1435
1452
  `Task ID: ${opts.taskId}`,
@@ -1503,7 +1520,7 @@ Error: ${opts.errorMsg}` : ""
1503
1520
  from: this.config.user,
1504
1521
  to: opts.to,
1505
1522
  subject: `[AAMP Help] Task ${opts.taskId} needs assistance`,
1506
- text: [
1523
+ text: opts.rawBodyText ?? [
1507
1524
  `AAMP Task Help Request`,
1508
1525
  ``,
1509
1526
  `Task ID: ${opts.taskId}`,
@@ -1819,7 +1836,7 @@ Stream ID: ${opts.streamId}`,
1819
1836
  }
1820
1837
  };
1821
1838
 
1822
- // ../sdk/src/thread.js
1839
+ // ../sdk/dist/thread.js
1823
1840
  function singleLine(value, maxLength = 220) {
1824
1841
  const normalized = (value ?? "").replace(/\s+/g, " ").trim();
1825
1842
  if (!normalized)
@@ -1873,7 +1890,7 @@ function renderThreadHistoryForAgent(events, options = {}) {
1873
1890
  ].join("\n");
1874
1891
  }
1875
1892
 
1876
- // ../sdk/src/client.js
1893
+ // ../sdk/dist/client.js
1877
1894
  function buildRegisteredCommandDispatchPayload(opts) {
1878
1895
  const command = opts.command.trim();
1879
1896
  if (!command) {
@@ -1897,14 +1914,27 @@ function buildRegisteredCommandDispatchPayload(opts) {
1897
1914
  stream: { mode: opts.streamMode ?? "full" }
1898
1915
  };
1899
1916
  }
1917
+ var DEFAULT_TASK_DISPATCH_CONCURRENCY = 10;
1918
+ function normalizeTaskDispatchConcurrency(value) {
1919
+ if (value == null)
1920
+ return DEFAULT_TASK_DISPATCH_CONCURRENCY;
1921
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
1922
+ throw new Error("taskDispatchConcurrency must be a positive integer");
1923
+ }
1924
+ return value;
1925
+ }
1900
1926
  var AampClient = class _AampClient extends TinyEmitter {
1901
1927
  jmapClient;
1902
1928
  smtpSender;
1903
1929
  config;
1930
+ taskDispatchConcurrency;
1931
+ pendingTaskDispatches = [];
1932
+ activeTaskDispatchCount = 0;
1904
1933
  streamAppendQueues = /* @__PURE__ */ new Map();
1905
1934
  constructor(config) {
1906
1935
  super();
1907
1936
  this.config = config;
1937
+ this.taskDispatchConcurrency = normalizeTaskDispatchConcurrency(config.taskDispatchConcurrency);
1908
1938
  const mailboxToken = config.mailboxToken;
1909
1939
  const resolvedBaseUrl = config.baseUrl;
1910
1940
  const derived = deriveMailboxServiceDefaults(config.email, resolvedBaseUrl);
@@ -1940,7 +1970,7 @@ var AampClient = class _AampClient extends TinyEmitter {
1940
1970
  rejectUnauthorized: config.rejectUnauthorized
1941
1971
  });
1942
1972
  this.jmapClient.on("task.dispatch", (task) => {
1943
- this.emit("task.dispatch", task);
1973
+ this.enqueueTaskDispatch(task);
1944
1974
  });
1945
1975
  this.jmapClient.on("task.cancel", (task) => {
1946
1976
  this.emit("task.cancel", task);
@@ -1993,6 +2023,7 @@ var AampClient = class _AampClient extends TinyEmitter {
1993
2023
  smtpPort: config.smtpPort ?? 587,
1994
2024
  smtpPassword: config.smtpPassword,
1995
2025
  reconnectInterval: config.reconnectInterval,
2026
+ taskDispatchConcurrency: config.taskDispatchConcurrency,
1996
2027
  rejectUnauthorized: config.rejectUnauthorized
1997
2028
  });
1998
2029
  }
@@ -2063,6 +2094,22 @@ var AampClient = class _AampClient extends TinyEmitter {
2063
2094
  baseUrl: base
2064
2095
  };
2065
2096
  }
2097
+ static async checkMailbox(opts) {
2098
+ const base = opts.aampHost.replace(/\/$/, "");
2099
+ const res = await _AampClient.callDiscoveredApi(base, {
2100
+ action: "aamp.mailbox.check",
2101
+ query: { email: opts.email }
2102
+ });
2103
+ if (!res.ok) {
2104
+ const body = await res.text().catch(() => "");
2105
+ throw new Error(`Mailbox check failed: ${res.status} ${body || res.statusText}`);
2106
+ }
2107
+ const payload = await res.json();
2108
+ return {
2109
+ aamp: Boolean(payload.aamp),
2110
+ ...payload.domain ? { domain: payload.domain } : {}
2111
+ };
2112
+ }
2066
2113
  // =====================================================
2067
2114
  // Lifecycle
2068
2115
  // =====================================================
@@ -2109,6 +2156,7 @@ var AampClient = class _AampClient extends TinyEmitter {
2109
2156
  rawBodyText: JSON.stringify(payload, null, 2),
2110
2157
  priority: opts.priority,
2111
2158
  expiresAt: opts.expiresAt,
2159
+ sessionKey: opts.sessionKey,
2112
2160
  contextLinks: opts.contextLinks,
2113
2161
  dispatchContext: opts.dispatchContext,
2114
2162
  parentTaskId: opts.parentTaskId,
@@ -2246,6 +2294,30 @@ var AampClient = class _AampClient extends TinyEmitter {
2246
2294
  }
2247
2295
  return res.json();
2248
2296
  }
2297
+ enqueueTaskDispatch(task) {
2298
+ this.pendingTaskDispatches.push(task);
2299
+ this.drainTaskDispatchQueue();
2300
+ }
2301
+ drainTaskDispatchQueue() {
2302
+ while (this.activeTaskDispatchCount < this.taskDispatchConcurrency && this.pendingTaskDispatches.length > 0) {
2303
+ const nextTask = this.pendingTaskDispatches.shift();
2304
+ if (!nextTask)
2305
+ return;
2306
+ this.activeTaskDispatchCount += 1;
2307
+ void this.runTaskDispatch(nextTask);
2308
+ }
2309
+ }
2310
+ async runTaskDispatch(task) {
2311
+ try {
2312
+ await this.emitAsync("task.dispatch", task);
2313
+ } catch (err) {
2314
+ const error = err instanceof Error ? err : new Error(String(err));
2315
+ this.emit("error", error);
2316
+ } finally {
2317
+ this.activeTaskDispatchCount = Math.max(0, this.activeTaskDispatchCount - 1);
2318
+ this.drainTaskDispatchQueue();
2319
+ }
2320
+ }
2249
2321
  getStreamAppendQueue(streamId) {
2250
2322
  let queue = this.streamAppendQueues.get(streamId);
2251
2323
  if (!queue) {
@@ -2706,6 +2778,10 @@ function isSyntheticPendingKey(taskKey) {
2706
2778
  function isTaskAwaitingHelpReply(task) {
2707
2779
  return task.awaitingHelpReply === true;
2708
2780
  }
2781
+ function isConversationalTask(task) {
2782
+ const source = task.dispatchContext?.source?.trim().toLowerCase();
2783
+ return source === "feishu" || source === "wechat";
2784
+ }
2709
2785
  function threadAlreadyTerminal(events) {
2710
2786
  return (events ?? []).some(
2711
2787
  (event) => event.intent === "task.result" || event.intent === "task.cancel"
@@ -2752,12 +2828,36 @@ function buildOpenClawMainSessionKey(mainKey, config) {
2752
2828
  function buildAampConversationSessionKey(value, config) {
2753
2829
  return buildOpenClawMainSessionKey(`${AAMP_SESSION_PREFIX}default:${value}`, config);
2754
2830
  }
2831
+ function buildAampStickySessionKey(sessionKey, config) {
2832
+ const stickyValue = sessionKey?.trim();
2833
+ if (!stickyValue)
2834
+ return void 0;
2835
+ return buildAampConversationSessionKey(`session:${stickyValue}`, config);
2836
+ }
2755
2837
  function buildAampTaskSessionKey(taskId, config) {
2756
2838
  return buildAampConversationSessionKey(`task:${taskId}`, config);
2757
2839
  }
2758
2840
  function buildAampWakeSessionKey(kind, id) {
2759
2841
  return `${AAMP_SESSION_PREFIX}wake:${kind}:${id}`;
2760
2842
  }
2843
+ function buildSessionKeyForPendingTask(task, config) {
2844
+ return buildAampStickySessionKey(task.sessionKey, config) ?? buildAampTaskSessionKey(task.taskId, config);
2845
+ }
2846
+ function buildWakeSessionKeyForPendingTask(task, config) {
2847
+ return buildAampStickySessionKey(task.sessionKey, config) ?? buildAampWakeSessionKey("task", task.taskId);
2848
+ }
2849
+ function findPendingEntryForSession(sessionKey, config) {
2850
+ if (typeof sessionKey !== "string" || !isAampSessionKey(sessionKey))
2851
+ return void 0;
2852
+ const requested = buildOpenClawMainSessionKey(stripOpenClawAgentScope(sessionKey), config);
2853
+ const entries = [...pendingTasks.entries()].filter(([key, task]) => isActionablePendingTask(key, task)).filter(([, task]) => buildSessionKeyForPendingTask(task, config) === requested).sort((a, b) => {
2854
+ const rankDiff = priorityRank(a[1].priority) - priorityRank(b[1].priority);
2855
+ if (rankDiff !== 0)
2856
+ return rankDiff;
2857
+ return new Date(a[1].receivedAt).getTime() - new Date(b[1].receivedAt).getTime();
2858
+ });
2859
+ return entries[0];
2860
+ }
2761
2861
  function resolvePendingKeyFromSessionKey(sessionKey) {
2762
2862
  if (typeof sessionKey !== "string")
2763
2863
  return void 0;
@@ -2848,6 +2948,7 @@ function queuePendingTask(task) {
2848
2948
  from: task.from,
2849
2949
  title: task.title,
2850
2950
  bodyText: task.bodyText ?? "",
2951
+ dispatchContext: task.dispatchContext,
2851
2952
  threadHistory: task.threadHistory ?? [],
2852
2953
  threadContextText: task.threadContextText ?? "",
2853
2954
  priority: task.priority ?? "normal",
@@ -2865,39 +2966,15 @@ function queuePendingTask(task) {
2865
2966
  }
2866
2967
  async function registerNode(cfg) {
2867
2968
  const slug = (cfg.slug ?? "openclaw-agent").toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "");
2868
- const base = baseUrl(cfg.aampHost);
2869
- const discoveryRes = await fetch(`${base}/.well-known/aamp`);
2870
- if (!discoveryRes.ok) {
2871
- throw new Error(`AAMP discovery failed (${discoveryRes.status}): ${discoveryRes.statusText}`);
2872
- }
2873
- const discovery = await discoveryRes.json();
2874
- const apiUrl = discovery.api?.url;
2875
- if (!apiUrl) {
2876
- throw new Error("AAMP discovery did not return api.url");
2877
- }
2878
- const apiBase = new URL(apiUrl, `${base}/`).toString();
2879
- const res = await fetch(`${apiBase}?action=aamp.mailbox.register`, {
2880
- method: "POST",
2881
- headers: { "Content-Type": "application/json" },
2882
- body: JSON.stringify({ slug, description: "OpenClaw AAMP agent node" })
2969
+ const credData = await AampClient.registerMailbox({
2970
+ aampHost: cfg.aampHost,
2971
+ slug,
2972
+ description: "OpenClaw AAMP agent node"
2883
2973
  });
2884
- if (!res.ok) {
2885
- const err = await res.json().catch(() => ({}));
2886
- throw new Error(`AAMP registration failed (${res.status}): ${err.error ?? res.statusText}`);
2887
- }
2888
- const regData = await res.json();
2889
- const credRes = await fetch(
2890
- `${apiBase}?action=aamp.mailbox.credentials&code=${encodeURIComponent(regData.registrationCode)}`
2891
- );
2892
- if (!credRes.ok) {
2893
- const err = await credRes.json().catch(() => ({}));
2894
- throw new Error(`AAMP credential exchange failed (${credRes.status}): ${err.error ?? credRes.statusText}`);
2895
- }
2896
- const credData = await credRes.json();
2897
2974
  return {
2898
2975
  email: credData.email,
2899
- mailboxToken: credData.mailbox.token,
2900
- smtpPassword: credData.smtp.password
2976
+ mailboxToken: credData.mailboxToken,
2977
+ smtpPassword: credData.smtpPassword
2901
2978
  };
2902
2979
  }
2903
2980
  async function resolveIdentity(cfg) {
@@ -3030,8 +3107,8 @@ var src_default = {
3030
3107
  api.logger.info(`[AAMP] Directory profile synced${cardText ? " (card text registered)" : ""}`);
3031
3108
  }
3032
3109
  function wakeAgentForPendingTask(task) {
3033
- const fallbackSessionKey = buildAampWakeSessionKey("task", task.taskId);
3034
- const openClawSessionKey = buildAampTaskSessionKey(task.taskId, api.config);
3110
+ const fallbackSessionKey = buildWakeSessionKeyForPendingTask(task, api.config);
3111
+ const openClawSessionKey = buildSessionKeyForPendingTask(task, api.config);
3035
3112
  const fallback = () => triggerHeartbeatWake(fallbackSessionKey, `task ${task.taskId}`);
3036
3113
  const dispatcher = channelRuntime?.reply?.dispatchReplyWithBufferedBlockDispatcher;
3037
3114
  api.logger.info(
@@ -3117,13 +3194,14 @@ var src_default = {
3117
3194
  email: identity.email,
3118
3195
  smtpPassword: identity.smtpPassword,
3119
3196
  baseUrl: base,
3197
+ taskDispatchConcurrency: cfg.taskDispatchConcurrency,
3120
3198
  // Local/dev: management-service proxy uses plain HTTP, no TLS cert to verify.
3121
3199
  // Production: set to true when using wss:// with valid certs.
3122
3200
  rejectUnauthorized: false
3123
3201
  });
3124
3202
  aampClient.on("task.dispatch", (task) => {
3125
3203
  api.logger.info(`[AAMP] \u2190 task.dispatch ${task.taskId} "${task.title}" from=${task.from}`);
3126
- void (async () => {
3204
+ return (async () => {
3127
3205
  try {
3128
3206
  if (terminalTaskIds.has(task.taskId)) {
3129
3207
  api.logger.info(`[AAMP] Skipping already-terminal task ${task.taskId}`);
@@ -3167,7 +3245,7 @@ var src_default = {
3167
3245
  } catch (err) {
3168
3246
  api.logger.error(`[AAMP] task.dispatch handler failed for ${task.taskId}: ${err.message}`);
3169
3247
  if (pendingTasks.has(task.taskId)) {
3170
- triggerHeartbeatWake(buildAampWakeSessionKey("task", task.taskId), `task ${task.taskId}`);
3248
+ triggerHeartbeatWake(buildWakeSessionKeyForPendingTask(pendingTasks.get(task.taskId), api.config), `task ${task.taskId}`);
3171
3249
  }
3172
3250
  }
3173
3251
  })();
@@ -3567,7 +3645,8 @@ ${notifyBody?.bodyText ?? help.question}`;
3567
3645
  }
3568
3646
  return [targetedPendingKey, targetedTask];
3569
3647
  })() : void 0;
3570
- const nextEntry = targetedPendingKey ? targetedEntry : nextPendingEntry();
3648
+ const sessionScopedEntry = targetedPendingKey ? void 0 : findPendingEntryForSession(ctx?.sessionKey, api.config);
3649
+ const nextEntry = targetedPendingKey ? targetedEntry : sessionScopedEntry ?? nextPendingEntry();
3571
3650
  if (!nextEntry)
3572
3651
  return {};
3573
3652
  const [taskKey, task] = nextEntry;
@@ -3602,19 +3681,45 @@ ${notifyBody?.bodyText ?? help.question}`;
3602
3681
  ` Example: attachments: [{ filename: "file.html", path: "/tmp/aamp-files/file.html" }]`
3603
3682
  ] : []
3604
3683
  ].join("\n") : "";
3605
- const lines = isNotification ? [
3606
- `## Sub-task Update`,
3684
+ const dispatchContextLines = task.dispatchContext && Object.keys(task.dispatchContext).length > 0 ? `Dispatch Context:
3685
+ ${Object.entries(task.dispatchContext).map(([key, value]) => ` - ${key}: ${value}`).join("\n")}` : "";
3686
+ const taskPromptLines = isConversationalTask(task) ? [
3687
+ `## Pending AAMP Conversation Turn`,
3607
3688
  ``,
3608
- `A sub-task you dispatched has returned a result. Review the information below.`,
3609
- `If the sub-task included attachments, use aamp_download_attachment to fetch them.`,
3689
+ `This AAMP task came from a chat surface (${task.dispatchContext?.source ?? "unknown"}).`,
3690
+ `Treat it as an ongoing conversation turn, not a one-off work order.`,
3691
+ `Your job is to reply naturally to the user's latest message and keep the conversation moving.`,
3692
+ ``,
3693
+ `### Tool selection rules for chat turns:`,
3694
+ ``,
3695
+ `Use aamp_send_result for normal conversation replies, including:`,
3696
+ ` - greetings, acknowledgements, and small talk ("hi", "hello", "thanks", "got it")`,
3697
+ ` - short follow-up questions that help narrow the user's intent`,
3698
+ ` - direct answers, suggestions, or next-step guidance`,
3699
+ ``,
3700
+ `Use aamp_send_help ONLY when you are truly blocked and cannot produce a meaningful`,
3701
+ `reply without waiting for specific missing information from the human.`,
3702
+ `Do NOT use aamp_send_help just because the message is brief or casual.`,
3703
+ ``,
3704
+ `IMPORTANT: For conversational traffic, replying to "hi" with a natural greeting and an`,
3705
+ `offer to help is CORRECT. Do not reject greetings as invalid tasks.`,
3706
+ ``,
3707
+ `### Sub-task dispatch rules:`,
3708
+ `If you delegate work to another agent via aamp_dispatch_task, you MUST pass`,
3709
+ `parentTaskId: "${task.taskId}" to establish the parent-child relationship.`,
3710
+ `If you need to find a suitable agent first, call aamp_directory_search.`,
3610
3711
  ``,
3611
3712
  `Task ID: ${task.taskId}`,
3612
- `Priority: ${task.priority}`,
3613
3713
  `From: ${task.from}`,
3614
3714
  `Title: ${task.title}`,
3615
- task.bodyText ? `
3715
+ dispatchContextLines,
3716
+ task.threadContextText ? `${task.threadContextText}` : "",
3717
+ task.bodyText ? `Latest user message:
3616
3718
  ${task.bodyText}` : "",
3617
- actionRequiredSection,
3719
+ task.contextLinks.length ? `Context Links:
3720
+ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
3721
+ task.expiresAt ? `Expires: ${task.expiresAt}` : `Expires: none`,
3722
+ `Received: ${task.receivedAt}`,
3618
3723
  otherActionableTasks.length > 0 ? `
3619
3724
  (+${otherActionableTasks.length} more tasks queued)` : ""
3620
3725
  ] : [
@@ -3649,6 +3754,7 @@ ${task.bodyText}` : "",
3649
3754
  `Task ID: ${task.taskId}`,
3650
3755
  `From: ${task.from}`,
3651
3756
  `Title: ${task.title}`,
3757
+ dispatchContextLines,
3652
3758
  task.threadContextText ? `${task.threadContextText}` : "",
3653
3759
  task.bodyText ? `Description:
3654
3760
  ${task.bodyText}` : "",
@@ -3658,7 +3764,23 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
3658
3764
  `Received: ${task.receivedAt}`,
3659
3765
  otherActionableTasks.length > 0 ? `
3660
3766
  (+${otherActionableTasks.length} more tasks queued)` : ""
3661
- ].filter(Boolean).join("\n");
3767
+ ];
3768
+ const lines = isNotification ? [
3769
+ `## Sub-task Update`,
3770
+ ``,
3771
+ `A sub-task you dispatched has returned a result. Review the information below.`,
3772
+ `If the sub-task included attachments, use aamp_download_attachment to fetch them.`,
3773
+ ``,
3774
+ `Task ID: ${task.taskId}`,
3775
+ `Priority: ${task.priority}`,
3776
+ `From: ${task.from}`,
3777
+ `Title: ${task.title}`,
3778
+ task.bodyText ? `
3779
+ ${task.bodyText}` : "",
3780
+ actionRequiredSection,
3781
+ otherActionableTasks.length > 0 ? `
3782
+ (+${otherActionableTasks.length} more tasks queued)` : ""
3783
+ ] : taskPromptLines.filter(Boolean).join("\n");
3662
3784
  return { prependContext: lines };
3663
3785
  },
3664
3786
  { priority: 5 }
@@ -4075,17 +4197,9 @@ ${lines.join("\n")}`
4075
4197
  const dir = "/tmp/aamp-files";
4076
4198
  ensureDir(dir);
4077
4199
  const downloaded = [];
4078
- const base = baseUrl(cfg.aampHost);
4079
- const identity = loadCachedIdentity(cfg.credentialsFile ?? defaultCredentialsPath());
4080
- const authHeader = identity ? `Basic ${Buffer.from(identity.email + ":" + identity.smtpPassword).toString("base64")}` : "";
4081
4200
  for (const att of r.attachments) {
4082
4201
  try {
4083
- const dlUrl = `${base}/jmap/download/n/${encodeURIComponent(att.blobId)}/${encodeURIComponent(att.filename)}?accept=application/octet-stream`;
4084
- api.logger.info(`[AAMP] Fetching ${dlUrl}`);
4085
- const dlRes = await fetch(dlUrl, { headers: { Authorization: authHeader } });
4086
- if (!dlRes.ok)
4087
- throw new Error(`HTTP ${dlRes.status}`);
4088
- const buffer = Buffer.from(await dlRes.arrayBuffer());
4202
+ const buffer = await aampClient.downloadBlob(att.blobId, att.filename);
4089
4203
  const filepath = `${dir}/${att.filename}`;
4090
4204
  writeBinaryFile(filepath, buffer);
4091
4205
  downloaded.push(`${att.filename} (${(buffer.length / 1024).toFixed(1)} KB) \u2192 ${filepath}`);
@@ -4157,18 +4271,10 @@ Question: ${h.question}`,
4157
4271
  return { content: [{ type: "text", text: "Error: email parameter is required" }] };
4158
4272
  }
4159
4273
  try {
4160
- const discoveryRes = await fetch(`${base}/.well-known/aamp`);
4161
- if (!discoveryRes.ok)
4162
- throw new Error(`HTTP ${discoveryRes.status}`);
4163
- const discovery = await discoveryRes.json();
4164
- const apiUrl = discovery.api?.url;
4165
- if (!apiUrl)
4166
- throw new Error("AAMP discovery did not return api.url");
4167
- const apiBase = new URL(apiUrl, `${base}/`).toString();
4168
- const res = await fetch(`${apiBase}?action=aamp.mailbox.check&email=${encodeURIComponent(email)}`);
4169
- if (!res.ok)
4170
- throw new Error(`HTTP ${res.status}`);
4171
- const data = await res.json();
4274
+ const data = await AampClient.checkMailbox({
4275
+ aampHost: base,
4276
+ email
4277
+ });
4172
4278
  return {
4173
4279
  content: [{
4174
4280
  type: "text",