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 +2 -0
- package/dist/index.js +146 -14
- package/dist/index.js.map +2 -2
- package/package.json +23 -1
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.
|
|
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 =
|
|
3034
|
-
const openClawSessionKey =
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
3606
|
-
|
|
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
|
-
`
|
|
3609
|
-
`
|
|
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
|
-
|
|
3725
|
+
dispatchContextLines,
|
|
3726
|
+
task.threadContextText ? `${task.threadContextText}` : "",
|
|
3727
|
+
task.bodyText ? `Latest user message:
|
|
3616
3728
|
${task.bodyText}` : "",
|
|
3617
|
-
|
|
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
|
-
]
|
|
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 }
|