aamp-openclaw-plugin 0.1.34 → 0.1.35

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
@@ -446,6 +446,27 @@ function describeError(err) {
446
446
  }
447
447
  return parts.join(" | ");
448
448
  }
449
+ function sleep(ms) {
450
+ return new Promise((resolve) => setTimeout(resolve, ms));
451
+ }
452
+ function shouldRetrySessionFetch(status) {
453
+ return status === 429 || status >= 500;
454
+ }
455
+ function shouldRetryBlobDownload(status) {
456
+ return status === 404 || status === 429 || status === 503;
457
+ }
458
+ function rewriteUrlToConfiguredOrigin(rawUrl, configuredBaseUrl) {
459
+ const parsed = new URL(rawUrl);
460
+ const configured = new URL(configuredBaseUrl);
461
+ parsed.protocol = configured.protocol;
462
+ parsed.username = configured.username;
463
+ parsed.password = configured.password;
464
+ parsed.hostname = configured.hostname;
465
+ parsed.port = configured.port;
466
+ return parsed.toString();
467
+ }
468
+ var SESSION_FETCH_MAX_ATTEMPTS = 3;
469
+ var SESSION_FETCH_RETRY_BASE_DELAY_MS = 250;
449
470
  var JmapPushClient = class extends TinyEmitter {
450
471
  ws = null;
451
472
  session = null;
@@ -522,18 +543,30 @@ var JmapPushClient = class extends TinyEmitter {
522
543
  */
523
544
  async fetchSession() {
524
545
  const url = `${this.jmapUrl}/.well-known/jmap`;
525
- let res;
526
- try {
527
- res = await fetch(url, {
528
- headers: { Authorization: this.getAuthHeader() }
529
- });
530
- } catch (err) {
531
- throw new Error(`fetchSession ${url} failed: ${describeError(err)}`);
532
- }
533
- if (!res.ok) {
534
- throw new Error(`Failed to fetch JMAP session: ${res.status} ${res.statusText}`);
546
+ let lastError = null;
547
+ for (let attempt = 1; attempt <= SESSION_FETCH_MAX_ATTEMPTS; attempt += 1) {
548
+ let res;
549
+ try {
550
+ res = await fetch(url, {
551
+ headers: { Authorization: this.getAuthHeader() }
552
+ });
553
+ } catch (err) {
554
+ lastError = new Error(`fetchSession ${url} failed: ${describeError(err)}`);
555
+ if (attempt >= SESSION_FETCH_MAX_ATTEMPTS)
556
+ throw lastError;
557
+ await sleep(SESSION_FETCH_RETRY_BASE_DELAY_MS * attempt);
558
+ continue;
559
+ }
560
+ if (res.ok) {
561
+ return res.json();
562
+ }
563
+ lastError = new Error(attempt >= SESSION_FETCH_MAX_ATTEMPTS || !shouldRetrySessionFetch(res.status) ? `Failed to fetch JMAP session: ${res.status} ${res.statusText}` : `Failed to fetch JMAP session after ${attempt} attempt(s): ${res.status} ${res.statusText}`);
564
+ if (attempt >= SESSION_FETCH_MAX_ATTEMPTS || !shouldRetrySessionFetch(res.status)) {
565
+ throw lastError;
566
+ }
567
+ await sleep(SESSION_FETCH_RETRY_BASE_DELAY_MS * attempt);
535
568
  }
536
- return res.json();
569
+ throw lastError ?? new Error("Failed to fetch JMAP session");
537
570
  }
538
571
  /**
539
572
  * Perform a JMAP API call
@@ -967,27 +1000,37 @@ var JmapPushClient = class extends TinyEmitter {
967
1000
  const accountId = this.session.primaryAccounts["urn:ietf:params:jmap:mail"] ?? Object.keys(this.session.accounts)[0];
968
1001
  let downloadUrl = this.session.downloadUrl ?? `${this.jmapUrl}/jmap/download/{accountId}/{blobId}/{name}`;
969
1002
  try {
970
- const parsed = new URL(downloadUrl);
971
- const configured = new URL(this.jmapUrl);
972
- parsed.protocol = configured.protocol;
973
- parsed.host = configured.host;
974
- downloadUrl = parsed.toString();
1003
+ downloadUrl = rewriteUrlToConfiguredOrigin(downloadUrl, this.jmapUrl);
975
1004
  } catch {
976
1005
  }
977
1006
  const safeFilename = filename ?? "attachment";
978
1007
  downloadUrl = downloadUrl.replace(/\{accountId\}|%7BaccountId%7D/gi, encodeURIComponent(accountId)).replace(/\{blobId\}|%7BblobId%7D/gi, encodeURIComponent(blobId)).replace(/\{name\}|%7Bname%7D/gi, encodeURIComponent(safeFilename)).replace(/\{type\}|%7Btype%7D/gi, "application/octet-stream");
979
1008
  const maxAttempts = 8;
980
1009
  let lastStatus = null;
1010
+ let lastError = null;
981
1011
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
982
- const res = await fetch(downloadUrl, {
983
- headers: { Authorization: this.getAuthHeader() }
984
- });
1012
+ let res;
1013
+ try {
1014
+ res = await fetch(downloadUrl, {
1015
+ headers: { Authorization: this.getAuthHeader() }
1016
+ });
1017
+ } catch (err) {
1018
+ lastError = new Error(`Blob download fetch failed: attempt=${attempt}/${maxAttempts} blobId=${blobId} filename=${filename ?? "attachment"} url=${downloadUrl} error=${describeError(err)}`);
1019
+ if (attempt < maxAttempts) {
1020
+ console.warn(`[AAMP-SDK] blob download retry fetch-error attempt=${attempt}/${maxAttempts} url=${downloadUrl} error=${describeError(err)}`);
1021
+ const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 15e3);
1022
+ await new Promise((r) => setTimeout(r, delay));
1023
+ continue;
1024
+ }
1025
+ console.error(`[AAMP-SDK] blob download fetch-error attempt=${attempt}/${maxAttempts} url=${downloadUrl} error=${describeError(err)}`);
1026
+ throw lastError;
1027
+ }
985
1028
  lastStatus = res.status;
986
1029
  if (res.ok) {
987
1030
  const arrayBuffer = await res.arrayBuffer();
988
1031
  return Buffer.from(arrayBuffer);
989
1032
  }
990
- if (attempt < maxAttempts && (res.status === 404 || res.status === 429 || res.status === 503)) {
1033
+ if (attempt < maxAttempts && shouldRetryBlobDownload(res.status)) {
991
1034
  console.warn(`[AAMP-SDK] blob download retry status=${res.status} attempt=${attempt}/${maxAttempts} url=${downloadUrl}`);
992
1035
  const delay = Math.min(1e3 * Math.pow(2, attempt - 1), 15e3);
993
1036
  await new Promise((r) => setTimeout(r, delay));
@@ -996,6 +1039,8 @@ var JmapPushClient = class extends TinyEmitter {
996
1039
  console.error(`[AAMP-SDK] blob download failed status=${res.status} attempt=${attempt}/${maxAttempts} url=${downloadUrl}`);
997
1040
  throw new Error(`Blob download failed: status=${res.status} attempt=${attempt}/${maxAttempts} blobId=${blobId} filename=${filename ?? "attachment"} url=${downloadUrl}`);
998
1041
  }
1042
+ if (lastError)
1043
+ throw lastError;
999
1044
  throw new Error(`Blob download failed after retries: status=${lastStatus ?? "unknown"} attempt=${maxAttempts}/${maxAttempts} blobId=${blobId} filename=${filename ?? "attachment"} url=${downloadUrl}`);
1000
1045
  }
1001
1046
  /**
@@ -1829,6 +1874,29 @@ function renderThreadHistoryForAgent(events, options = {}) {
1829
1874
  }
1830
1875
 
1831
1876
  // ../sdk/src/client.js
1877
+ function buildRegisteredCommandDispatchPayload(opts) {
1878
+ const command = opts.command.trim();
1879
+ if (!command) {
1880
+ throw new Error("Registered command name cannot be empty.");
1881
+ }
1882
+ if (opts.args != null && (typeof opts.args !== "object" || Array.isArray(opts.args))) {
1883
+ throw new Error("Registered command args must be an object when provided.");
1884
+ }
1885
+ if (opts.inputs) {
1886
+ for (const input of opts.inputs) {
1887
+ if (!input.slot?.trim() || !input.attachmentName?.trim()) {
1888
+ throw new Error("Each registered command input must include slot and attachmentName.");
1889
+ }
1890
+ }
1891
+ }
1892
+ return {
1893
+ kind: "registered-command/v1",
1894
+ command,
1895
+ ...opts.args && Object.keys(opts.args).length > 0 ? { args: opts.args } : {},
1896
+ ...opts.inputs?.length ? { inputs: opts.inputs } : {},
1897
+ stream: { mode: opts.streamMode ?? "full" }
1898
+ };
1899
+ }
1832
1900
  var AampClient = class _AampClient extends TinyEmitter {
1833
1901
  jmapClient;
1834
1902
  smtpSender;
@@ -2032,6 +2100,21 @@ var AampClient = class _AampClient extends TinyEmitter {
2032
2100
  async sendTask(opts) {
2033
2101
  return this.smtpSender.sendTask(opts);
2034
2102
  }
2103
+ async sendRegisteredCommand(opts) {
2104
+ const payload = buildRegisteredCommandDispatchPayload(opts);
2105
+ return this.smtpSender.sendTask({
2106
+ to: opts.to,
2107
+ taskId: opts.taskId,
2108
+ title: opts.title?.trim() || `Registered command: ${payload.command}`,
2109
+ rawBodyText: JSON.stringify(payload, null, 2),
2110
+ priority: opts.priority,
2111
+ expiresAt: opts.expiresAt,
2112
+ contextLinks: opts.contextLinks,
2113
+ dispatchContext: opts.dispatchContext,
2114
+ parentTaskId: opts.parentTaskId,
2115
+ attachments: opts.attachments
2116
+ });
2117
+ }
2035
2118
  async sendCancel(opts) {
2036
2119
  return this.smtpSender.sendCancel(opts);
2037
2120
  }
@@ -2535,8 +2618,6 @@ function baseUrl(aampHost) {
2535
2618
  }
2536
2619
  var pendingTasks = /* @__PURE__ */ new Map();
2537
2620
  var activeTaskStreams = /* @__PURE__ */ new Map();
2538
- var sessionTaskBindings = /* @__PURE__ */ new Map();
2539
- var taskVisibleStreamState = /* @__PURE__ */ new Map();
2540
2621
  var terminalTaskIds = new Set(loadTaskState(defaultTaskStatePath()).terminalTaskIds ?? []);
2541
2622
  var AAMP_SESSION_PREFIX = "aamp:";
2542
2623
  var DEFAULT_OPENCLAW_AGENT_ID = "main";
@@ -2556,7 +2637,6 @@ var lastLoggedTransportMode = "disconnected";
2556
2637
  var reconcileTimer = null;
2557
2638
  var transportMonitorTimer = null;
2558
2639
  var historicalReconcileCompleted = false;
2559
- var stopAgentEventSubscription = null;
2560
2640
  var channelRuntime = null;
2561
2641
  var channelCfg = null;
2562
2642
  async function ensureTaskStream(task) {
@@ -2705,80 +2785,6 @@ function resolvePendingKeyFromSessionKey(sessionKey) {
2705
2785
  }
2706
2786
  return void 0;
2707
2787
  }
2708
- function normalizeBoundSessionKey(sessionKey) {
2709
- if (typeof sessionKey !== "string")
2710
- return void 0;
2711
- const normalized = stripOpenClawAgentScope(sessionKey).trim();
2712
- return normalized || void 0;
2713
- }
2714
- function bindSessionToTask(sessionKey, taskId) {
2715
- const normalized = normalizeBoundSessionKey(sessionKey);
2716
- if (!normalized)
2717
- return;
2718
- if (!taskId) {
2719
- sessionTaskBindings.delete(normalized);
2720
- return;
2721
- }
2722
- sessionTaskBindings.set(normalized, taskId);
2723
- }
2724
- function resolveStreamTaskId(sessionKey) {
2725
- const pendingKey = resolvePendingKeyFromSessionKey(sessionKey);
2726
- if (pendingKey && !isSyntheticPendingKey(pendingKey))
2727
- return pendingKey;
2728
- const normalized = normalizeBoundSessionKey(sessionKey);
2729
- if (!normalized)
2730
- return void 0;
2731
- const taskId = sessionTaskBindings.get(normalized);
2732
- if (!taskId)
2733
- return void 0;
2734
- if (!pendingTasks.has(taskId) && !activeTaskStreams.has(taskId)) {
2735
- sessionTaskBindings.delete(normalized);
2736
- return void 0;
2737
- }
2738
- return taskId;
2739
- }
2740
- function forgetTaskStreamContext(taskId) {
2741
- taskVisibleStreamState.delete(taskId);
2742
- for (const [sessionKey, boundTaskId] of sessionTaskBindings) {
2743
- if (boundTaskId === taskId) {
2744
- sessionTaskBindings.delete(sessionKey);
2745
- }
2746
- }
2747
- }
2748
- async function handleAgentVisibleTextEvent(event) {
2749
- if (event.stream !== "assistant")
2750
- return;
2751
- const taskId = resolveStreamTaskId(event.sessionKey);
2752
- if (!taskId || !activeTaskStreams.has(taskId))
2753
- return;
2754
- const payload = event.data && typeof event.data === "object" ? event.data : {};
2755
- const fullText = typeof payload.text === "string" ? payload.text : void 0;
2756
- const deltaText = typeof payload.delta === "string" ? payload.delta : void 0;
2757
- if (!fullText && !deltaText)
2758
- return;
2759
- const runId = typeof event.runId === "string" && event.runId.trim() ? event.runId : "__default__";
2760
- const previousState = taskVisibleStreamState.get(taskId);
2761
- const previousText = previousState?.runId === runId ? previousState.text : "";
2762
- let nextText = previousText;
2763
- let nextDelta = "";
2764
- if (typeof fullText === "string") {
2765
- nextText = fullText;
2766
- if (fullText.startsWith(previousText)) {
2767
- nextDelta = fullText.slice(previousText.length);
2768
- } else if (!previousText) {
2769
- nextDelta = fullText;
2770
- } else if (deltaText) {
2771
- nextDelta = deltaText;
2772
- }
2773
- } else if (deltaText) {
2774
- nextText = `${previousText}${deltaText}`;
2775
- nextDelta = deltaText;
2776
- }
2777
- taskVisibleStreamState.set(taskId, { runId, text: nextText });
2778
- if (!nextDelta)
2779
- return;
2780
- await appendTaskStream(taskId, "text.delta", { text: nextDelta });
2781
- }
2782
2788
  function saveTerminalTaskIds() {
2783
2789
  saveTaskState({ terminalTaskIds: [...terminalTaskIds] }, defaultTaskStatePath());
2784
2790
  }
@@ -2957,7 +2963,12 @@ var src_default = {
2957
2963
  const cfg = api.config?.channels?.aamp ?? api.pluginConfig ?? {};
2958
2964
  api.registerChannel({
2959
2965
  id: "aamp",
2960
- meta: { label: "AAMP" },
2966
+ meta: {
2967
+ label: "AAMP",
2968
+ selectionLabel: "AAMP",
2969
+ docsPath: "/channels/aamp",
2970
+ blurb: "AAMP mailbox channel for receiving and replying to tasks over email."
2971
+ },
2961
2972
  capabilities: { chatTypes: ["dm"] },
2962
2973
  config: {
2963
2974
  listAccountIds: () => cfg.aampHost ? ["default"] : [],
@@ -2990,22 +3001,6 @@ var src_default = {
2990
3001
  api.logger.warn(`[AAMP] Could not trigger heartbeat for ${label}: ${err.message}`);
2991
3002
  }
2992
3003
  }
2993
- if (stopAgentEventSubscription) {
2994
- try {
2995
- stopAgentEventSubscription();
2996
- } catch {
2997
- }
2998
- stopAgentEventSubscription = null;
2999
- }
3000
- const onAgentEvent = api.runtime?.events?.onAgentEvent;
3001
- if (typeof onAgentEvent === "function") {
3002
- const unsubscribe = onAgentEvent((event) => {
3003
- void handleAgentVisibleTextEvent(event).catch((err) => {
3004
- api.logger.warn(`[AAMP] Failed to mirror assistant stream: ${err.message}`);
3005
- });
3006
- });
3007
- stopAgentEventSubscription = typeof unsubscribe === "function" ? unsubscribe : null;
3008
- }
3009
3004
  function getConfiguredCardText() {
3010
3005
  const inline = cfg.cardText?.trim();
3011
3006
  if (inline)
@@ -3174,7 +3169,6 @@ var src_default = {
3174
3169
  pendingTasks.delete(`help:${cancel.taskId}`);
3175
3170
  dispatchedSubtasks.delete(cancel.taskId);
3176
3171
  waitingDispatches.delete(cancel.taskId);
3177
- forgetTaskStreamContext(cancel.taskId);
3178
3172
  rememberTerminalTask(cancel.taskId);
3179
3173
  void closeTaskStream(cancel.taskId, { reason: "task.cancel" }).catch(() => {
3180
3174
  });
@@ -3507,13 +3501,6 @@ ${notifyBody?.bodyText ?? help.question}`;
3507
3501
  } catch {
3508
3502
  }
3509
3503
  }
3510
- if (stopAgentEventSubscription) {
3511
- try {
3512
- stopAgentEventSubscription();
3513
- } catch {
3514
- }
3515
- stopAgentEventSubscription = null;
3516
- }
3517
3504
  }
3518
3505
  });
3519
3506
  api.on("gateway_start", () => {
@@ -3584,10 +3571,6 @@ ${notifyBody?.bodyText ?? help.question}`;
3584
3571
  return rankDiff;
3585
3572
  return new Date(a.receivedAt).getTime() - new Date(b.receivedAt).getTime();
3586
3573
  });
3587
- bindSessionToTask(
3588
- ctx?.sessionKey,
3589
- isNotification ? actionableTasks.length === 1 ? actionableTasks[0]?.taskId : void 0 : task.taskId
3590
- );
3591
3574
  const otherActionableTasks = actionableTasks.filter((pendingTask) => pendingTask.taskId !== task.taskId);
3592
3575
  const hasAttachmentInfo = isNotification && (task.bodyText?.includes("aamp_download_attachment") ?? false);
3593
3576
  const actionRequiredSection = isNotification && actionableTasks.length > 0 ? [
@@ -3811,17 +3794,14 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
3811
3794
  state: "completing",
3812
3795
  label: `Sending ${p.status} result`
3813
3796
  });
3814
- const streamedText = taskVisibleStreamState.get(task.taskId)?.text ?? "";
3815
- const finalOutputDelta = streamedText && p.output.startsWith(streamedText) ? p.output.slice(streamedText.length) : p.output;
3816
- if (finalOutputDelta) {
3817
- await appendTaskStream(task.taskId, "text.delta", { text: finalOutputDelta });
3797
+ if (p.output) {
3798
+ await appendTaskStream(task.taskId, "text.delta", { text: p.output });
3818
3799
  }
3819
3800
  await closeTaskStream(task.taskId, {
3820
3801
  reason: "task.result",
3821
3802
  status: p.status,
3822
3803
  ...p.errorMsg ? { error: p.errorMsg } : {}
3823
3804
  });
3824
- forgetTaskStreamContext(task.taskId);
3825
3805
  await aampClient.sendResult({
3826
3806
  to: task.from,
3827
3807
  taskId: task.taskId,
@@ -3895,18 +3875,37 @@ ${task.contextLinks.map((l) => ` - ${l}`).join("\n")}` : "",
3895
3875
  state: "help_needed",
3896
3876
  label: p.blockedReason
3897
3877
  });
3878
+ try {
3879
+ await aampClient.sendHelp({
3880
+ to: task.from,
3881
+ taskId: task.taskId,
3882
+ question: p.question,
3883
+ blockedReason: p.blockedReason,
3884
+ suggestedOptions: p.suggestedOptions ?? [],
3885
+ inReplyTo: task.messageId || void 0
3886
+ });
3887
+ } catch (err) {
3888
+ const message = err instanceof Error ? err.message : String(err);
3889
+ await appendTaskStream(task.taskId, "error", {
3890
+ message: `Failed to send help request: ${message}`
3891
+ });
3892
+ await appendTaskStream(task.taskId, "status", {
3893
+ state: "running",
3894
+ label: "Help request failed; task still needs a reply"
3895
+ });
3896
+ api.logger.error(`[AAMP] aamp_send_help failed for ${task.taskId}: ${message}`);
3897
+ return {
3898
+ content: [
3899
+ {
3900
+ type: "text",
3901
+ text: `Error: failed to send help request for task ${task.taskId}: ${message}`
3902
+ }
3903
+ ]
3904
+ };
3905
+ }
3898
3906
  await closeTaskStream(task.taskId, {
3899
3907
  reason: "task.help_needed"
3900
3908
  });
3901
- forgetTaskStreamContext(task.taskId);
3902
- await aampClient.sendHelp({
3903
- to: task.from,
3904
- taskId: task.taskId,
3905
- question: p.question,
3906
- blockedReason: p.blockedReason,
3907
- suggestedOptions: p.suggestedOptions ?? [],
3908
- inReplyTo: task.messageId || void 0
3909
- });
3910
3909
  pendingTasks.set(task.taskId, {
3911
3910
  ...task,
3912
3911
  awaitingHelpReply: true