aamp-openclaw-plugin 0.1.36 → 0.1.37

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
 
package/dist/index.js CHANGED
@@ -421,6 +421,17 @@ var TinyEmitter = class {
421
421
  }
422
422
  return true;
423
423
  }
424
+ async emitAsync(event, ...args) {
425
+ const bucket = this.listeners.get(event);
426
+ if (!bucket || bucket.size === 0)
427
+ return false;
428
+ const settled = await Promise.allSettled([...bucket].map((listener) => Promise.resolve(listener(...args))));
429
+ const rejected = settled.find((result) => result.status === "rejected");
430
+ if (rejected) {
431
+ throw rejected.reason;
432
+ }
433
+ return true;
434
+ }
424
435
  };
425
436
 
426
437
  // ../sdk/src/jmap-push.js
@@ -1897,14 +1908,27 @@ function buildRegisteredCommandDispatchPayload(opts) {
1897
1908
  stream: { mode: opts.streamMode ?? "full" }
1898
1909
  };
1899
1910
  }
1911
+ var DEFAULT_TASK_DISPATCH_CONCURRENCY = 10;
1912
+ function normalizeTaskDispatchConcurrency(value) {
1913
+ if (value == null)
1914
+ return DEFAULT_TASK_DISPATCH_CONCURRENCY;
1915
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
1916
+ throw new Error("taskDispatchConcurrency must be a positive integer");
1917
+ }
1918
+ return value;
1919
+ }
1900
1920
  var AampClient = class _AampClient extends TinyEmitter {
1901
1921
  jmapClient;
1902
1922
  smtpSender;
1903
1923
  config;
1924
+ taskDispatchConcurrency;
1925
+ pendingTaskDispatches = [];
1926
+ activeTaskDispatchCount = 0;
1904
1927
  streamAppendQueues = /* @__PURE__ */ new Map();
1905
1928
  constructor(config) {
1906
1929
  super();
1907
1930
  this.config = config;
1931
+ this.taskDispatchConcurrency = normalizeTaskDispatchConcurrency(config.taskDispatchConcurrency);
1908
1932
  const mailboxToken = config.mailboxToken;
1909
1933
  const resolvedBaseUrl = config.baseUrl;
1910
1934
  const derived = deriveMailboxServiceDefaults(config.email, resolvedBaseUrl);
@@ -1940,7 +1964,7 @@ var AampClient = class _AampClient extends TinyEmitter {
1940
1964
  rejectUnauthorized: config.rejectUnauthorized
1941
1965
  });
1942
1966
  this.jmapClient.on("task.dispatch", (task) => {
1943
- this.emit("task.dispatch", task);
1967
+ this.enqueueTaskDispatch(task);
1944
1968
  });
1945
1969
  this.jmapClient.on("task.cancel", (task) => {
1946
1970
  this.emit("task.cancel", task);
@@ -1993,6 +2017,7 @@ var AampClient = class _AampClient extends TinyEmitter {
1993
2017
  smtpPort: config.smtpPort ?? 587,
1994
2018
  smtpPassword: config.smtpPassword,
1995
2019
  reconnectInterval: config.reconnectInterval,
2020
+ taskDispatchConcurrency: config.taskDispatchConcurrency,
1996
2021
  rejectUnauthorized: config.rejectUnauthorized
1997
2022
  });
1998
2023
  }
@@ -2246,6 +2271,30 @@ var AampClient = class _AampClient extends TinyEmitter {
2246
2271
  }
2247
2272
  return res.json();
2248
2273
  }
2274
+ enqueueTaskDispatch(task) {
2275
+ this.pendingTaskDispatches.push(task);
2276
+ this.drainTaskDispatchQueue();
2277
+ }
2278
+ drainTaskDispatchQueue() {
2279
+ while (this.activeTaskDispatchCount < this.taskDispatchConcurrency && this.pendingTaskDispatches.length > 0) {
2280
+ const nextTask = this.pendingTaskDispatches.shift();
2281
+ if (!nextTask)
2282
+ return;
2283
+ this.activeTaskDispatchCount += 1;
2284
+ void this.runTaskDispatch(nextTask);
2285
+ }
2286
+ }
2287
+ async runTaskDispatch(task) {
2288
+ try {
2289
+ await this.emitAsync("task.dispatch", task);
2290
+ } catch (err) {
2291
+ const error = err instanceof Error ? err : new Error(String(err));
2292
+ this.emit("error", error);
2293
+ } finally {
2294
+ this.activeTaskDispatchCount = Math.max(0, this.activeTaskDispatchCount - 1);
2295
+ this.drainTaskDispatchQueue();
2296
+ }
2297
+ }
2249
2298
  getStreamAppendQueue(streamId) {
2250
2299
  let queue = this.streamAppendQueues.get(streamId);
2251
2300
  if (!queue) {
@@ -2706,6 +2755,19 @@ function isSyntheticPendingKey(taskKey) {
2706
2755
  function isTaskAwaitingHelpReply(task) {
2707
2756
  return task.awaitingHelpReply === true;
2708
2757
  }
2758
+ function isConversationalTask(task) {
2759
+ return task.dispatchContext?.source === "feishu";
2760
+ }
2761
+ function firstDispatchContextValue(context, keys) {
2762
+ if (!context)
2763
+ return void 0;
2764
+ for (const key of keys) {
2765
+ const value = context[key]?.trim();
2766
+ if (value)
2767
+ return value;
2768
+ }
2769
+ return void 0;
2770
+ }
2709
2771
  function threadAlreadyTerminal(events) {
2710
2772
  return (events ?? []).some(
2711
2773
  (event) => event.intent === "task.result" || event.intent === "task.cancel"
@@ -2752,12 +2814,36 @@ function buildOpenClawMainSessionKey(mainKey, config) {
2752
2814
  function buildAampConversationSessionKey(value, config) {
2753
2815
  return buildOpenClawMainSessionKey(`${AAMP_SESSION_PREFIX}default:${value}`, config);
2754
2816
  }
2817
+ function buildAampStickySessionKey(dispatchContext, config) {
2818
+ const stickyValue = firstDispatchContextValue(dispatchContext, ["session_key", "conversation_key", "thread_key"]);
2819
+ if (!stickyValue)
2820
+ return void 0;
2821
+ return buildAampConversationSessionKey(`session:${stickyValue}`, config);
2822
+ }
2755
2823
  function buildAampTaskSessionKey(taskId, config) {
2756
2824
  return buildAampConversationSessionKey(`task:${taskId}`, config);
2757
2825
  }
2758
2826
  function buildAampWakeSessionKey(kind, id) {
2759
2827
  return `${AAMP_SESSION_PREFIX}wake:${kind}:${id}`;
2760
2828
  }
2829
+ function buildSessionKeyForPendingTask(task, config) {
2830
+ return buildAampStickySessionKey(task.dispatchContext, config) ?? buildAampTaskSessionKey(task.taskId, config);
2831
+ }
2832
+ function buildWakeSessionKeyForPendingTask(task, config) {
2833
+ return buildAampStickySessionKey(task.dispatchContext, config) ?? buildAampWakeSessionKey("task", task.taskId);
2834
+ }
2835
+ function findPendingEntryForSession(sessionKey, config) {
2836
+ if (typeof sessionKey !== "string" || !isAampSessionKey(sessionKey))
2837
+ return void 0;
2838
+ const requested = buildOpenClawMainSessionKey(stripOpenClawAgentScope(sessionKey), config);
2839
+ const entries = [...pendingTasks.entries()].filter(([key, task]) => isActionablePendingTask(key, task)).filter(([, task]) => buildSessionKeyForPendingTask(task, config) === requested).sort((a, b) => {
2840
+ const rankDiff = priorityRank(a[1].priority) - priorityRank(b[1].priority);
2841
+ if (rankDiff !== 0)
2842
+ return rankDiff;
2843
+ return new Date(a[1].receivedAt).getTime() - new Date(b[1].receivedAt).getTime();
2844
+ });
2845
+ return entries[0];
2846
+ }
2761
2847
  function resolvePendingKeyFromSessionKey(sessionKey) {
2762
2848
  if (typeof sessionKey !== "string")
2763
2849
  return void 0;
@@ -2848,6 +2934,7 @@ function queuePendingTask(task) {
2848
2934
  from: task.from,
2849
2935
  title: task.title,
2850
2936
  bodyText: task.bodyText ?? "",
2937
+ dispatchContext: task.dispatchContext,
2851
2938
  threadHistory: task.threadHistory ?? [],
2852
2939
  threadContextText: task.threadContextText ?? "",
2853
2940
  priority: task.priority ?? "normal",
@@ -3030,8 +3117,8 @@ var src_default = {
3030
3117
  api.logger.info(`[AAMP] Directory profile synced${cardText ? " (card text registered)" : ""}`);
3031
3118
  }
3032
3119
  function wakeAgentForPendingTask(task) {
3033
- const fallbackSessionKey = buildAampWakeSessionKey("task", task.taskId);
3034
- const openClawSessionKey = buildAampTaskSessionKey(task.taskId, api.config);
3120
+ const fallbackSessionKey = buildWakeSessionKeyForPendingTask(task, api.config);
3121
+ const openClawSessionKey = buildSessionKeyForPendingTask(task, api.config);
3035
3122
  const fallback = () => triggerHeartbeatWake(fallbackSessionKey, `task ${task.taskId}`);
3036
3123
  const dispatcher = channelRuntime?.reply?.dispatchReplyWithBufferedBlockDispatcher;
3037
3124
  api.logger.info(
@@ -3117,13 +3204,14 @@ var src_default = {
3117
3204
  email: identity.email,
3118
3205
  smtpPassword: identity.smtpPassword,
3119
3206
  baseUrl: base,
3207
+ taskDispatchConcurrency: cfg.taskDispatchConcurrency,
3120
3208
  // Local/dev: management-service proxy uses plain HTTP, no TLS cert to verify.
3121
3209
  // Production: set to true when using wss:// with valid certs.
3122
3210
  rejectUnauthorized: false
3123
3211
  });
3124
3212
  aampClient.on("task.dispatch", (task) => {
3125
3213
  api.logger.info(`[AAMP] \u2190 task.dispatch ${task.taskId} "${task.title}" from=${task.from}`);
3126
- void (async () => {
3214
+ return (async () => {
3127
3215
  try {
3128
3216
  if (terminalTaskIds.has(task.taskId)) {
3129
3217
  api.logger.info(`[AAMP] Skipping already-terminal task ${task.taskId}`);
@@ -3167,7 +3255,7 @@ var src_default = {
3167
3255
  } catch (err) {
3168
3256
  api.logger.error(`[AAMP] task.dispatch handler failed for ${task.taskId}: ${err.message}`);
3169
3257
  if (pendingTasks.has(task.taskId)) {
3170
- triggerHeartbeatWake(buildAampWakeSessionKey("task", task.taskId), `task ${task.taskId}`);
3258
+ triggerHeartbeatWake(buildWakeSessionKeyForPendingTask(pendingTasks.get(task.taskId), api.config), `task ${task.taskId}`);
3171
3259
  }
3172
3260
  }
3173
3261
  })();
@@ -3567,7 +3655,8 @@ ${notifyBody?.bodyText ?? help.question}`;
3567
3655
  }
3568
3656
  return [targetedPendingKey, targetedTask];
3569
3657
  })() : void 0;
3570
- const nextEntry = targetedPendingKey ? targetedEntry : nextPendingEntry();
3658
+ const sessionScopedEntry = targetedPendingKey ? void 0 : findPendingEntryForSession(ctx?.sessionKey, api.config);
3659
+ const nextEntry = targetedPendingKey ? targetedEntry : sessionScopedEntry ?? nextPendingEntry();
3571
3660
  if (!nextEntry)
3572
3661
  return {};
3573
3662
  const [taskKey, task] = nextEntry;
@@ -3602,19 +3691,45 @@ ${notifyBody?.bodyText ?? help.question}`;
3602
3691
  ` Example: attachments: [{ filename: "file.html", path: "/tmp/aamp-files/file.html" }]`
3603
3692
  ] : []
3604
3693
  ].join("\n") : "";
3605
- const lines = isNotification ? [
3606
- `## Sub-task Update`,
3694
+ const dispatchContextLines = task.dispatchContext && Object.keys(task.dispatchContext).length > 0 ? `Dispatch Context:
3695
+ ${Object.entries(task.dispatchContext).map(([key, value]) => ` - ${key}: ${value}`).join("\n")}` : "";
3696
+ const taskPromptLines = isConversationalTask(task) ? [
3697
+ `## Pending AAMP Conversation Turn`,
3607
3698
  ``,
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.`,
3699
+ `This AAMP task came from a chat surface (${task.dispatchContext?.source ?? "unknown"}).`,
3700
+ `Treat it as an ongoing conversation turn, not a one-off work order.`,
3701
+ `Your job is to reply naturally to the user's latest message and keep the conversation moving.`,
3702
+ ``,
3703
+ `### Tool selection rules for chat turns:`,
3704
+ ``,
3705
+ `Use aamp_send_result for normal conversation replies, including:`,
3706
+ ` - greetings, acknowledgements, and small talk ("hi", "hello", "thanks", "got it")`,
3707
+ ` - short follow-up questions that help narrow the user's intent`,
3708
+ ` - direct answers, suggestions, or next-step guidance`,
3709
+ ``,
3710
+ `Use aamp_send_help ONLY when you are truly blocked and cannot produce a meaningful`,
3711
+ `reply without waiting for specific missing information from the human.`,
3712
+ `Do NOT use aamp_send_help just because the message is brief or casual.`,
3713
+ ``,
3714
+ `IMPORTANT: For conversational traffic, replying to "hi" with a natural greeting and an`,
3715
+ `offer to help is CORRECT. Do not reject greetings as invalid tasks.`,
3716
+ ``,
3717
+ `### Sub-task dispatch rules:`,
3718
+ `If you delegate work to another agent via aamp_dispatch_task, you MUST pass`,
3719
+ `parentTaskId: "${task.taskId}" to establish the parent-child relationship.`,
3720
+ `If you need to find a suitable agent first, call aamp_directory_search.`,
3610
3721
  ``,
3611
3722
  `Task ID: ${task.taskId}`,
3612
- `Priority: ${task.priority}`,
3613
3723
  `From: ${task.from}`,
3614
3724
  `Title: ${task.title}`,
3615
- task.bodyText ? `
3725
+ dispatchContextLines,
3726
+ task.threadContextText ? `${task.threadContextText}` : "",
3727
+ task.bodyText ? `Latest user message:
3616
3728
  ${task.bodyText}` : "",
3617
- actionRequiredSection,
3729
+ task.contextLinks.length ? `Context Links:
3730
+ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
3731
+ task.expiresAt ? `Expires: ${task.expiresAt}` : `Expires: none`,
3732
+ `Received: ${task.receivedAt}`,
3618
3733
  otherActionableTasks.length > 0 ? `
3619
3734
  (+${otherActionableTasks.length} more tasks queued)` : ""
3620
3735
  ] : [
@@ -3649,6 +3764,7 @@ ${task.bodyText}` : "",
3649
3764
  `Task ID: ${task.taskId}`,
3650
3765
  `From: ${task.from}`,
3651
3766
  `Title: ${task.title}`,
3767
+ dispatchContextLines,
3652
3768
  task.threadContextText ? `${task.threadContextText}` : "",
3653
3769
  task.bodyText ? `Description:
3654
3770
  ${task.bodyText}` : "",
@@ -3658,7 +3774,23 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
3658
3774
  `Received: ${task.receivedAt}`,
3659
3775
  otherActionableTasks.length > 0 ? `
3660
3776
  (+${otherActionableTasks.length} more tasks queued)` : ""
3661
- ].filter(Boolean).join("\n");
3777
+ ];
3778
+ const lines = isNotification ? [
3779
+ `## Sub-task Update`,
3780
+ ``,
3781
+ `A sub-task you dispatched has returned a result. Review the information below.`,
3782
+ `If the sub-task included attachments, use aamp_download_attachment to fetch them.`,
3783
+ ``,
3784
+ `Task ID: ${task.taskId}`,
3785
+ `Priority: ${task.priority}`,
3786
+ `From: ${task.from}`,
3787
+ `Title: ${task.title}`,
3788
+ task.bodyText ? `
3789
+ ${task.bodyText}` : "",
3790
+ actionRequiredSection,
3791
+ otherActionableTasks.length > 0 ? `
3792
+ (+${otherActionableTasks.length} more tasks queued)` : ""
3793
+ ] : taskPromptLines.filter(Boolean).join("\n");
3662
3794
  return { prependContext: lines };
3663
3795
  },
3664
3796
  { priority: 5 }