@elisym/cli 0.22.3 → 0.22.5

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
@@ -2115,6 +2115,12 @@ var SeedFailedError = class extends Error {
2115
2115
  this.name = "SeedFailedError";
2116
2116
  }
2117
2117
  };
2118
+ var PaymentTimeoutError = class extends Error {
2119
+ constructor() {
2120
+ super("Payment verification timed out; awaiting late confirmation.");
2121
+ this.name = "PaymentTimeoutError";
2122
+ }
2123
+ };
2118
2124
  var MIME_EXTENSIONS = {
2119
2125
  "image/png": ".png",
2120
2126
  "image/jpeg": ".jpg",
@@ -2544,7 +2550,7 @@ var AgentRuntime = class {
2544
2550
  const log = this.callbacks.onLog ?? console.log;
2545
2551
  log(`[${job.jobId.slice(0, 8)}] Error: ${e.message}`);
2546
2552
  const currentStatus = this.ledger.getStatus(job.jobId);
2547
- const keepPaidForRecovery = (e instanceof AgentUnavailableError || e instanceof SeedFailedError) && currentStatus === "paid";
2553
+ const keepPaidForRecovery = (e instanceof AgentUnavailableError || e instanceof SeedFailedError || e instanceof PaymentTimeoutError) && currentStatus === "paid";
2548
2554
  if (currentStatus !== "executed" && !keepPaidForRecovery) {
2549
2555
  this.ledger.markFailed(job.jobId);
2550
2556
  }
@@ -2617,7 +2623,7 @@ var AgentRuntime = class {
2617
2623
  );
2618
2624
  this.callbacks.onPaymentReceived?.(job.jobId, netAmount);
2619
2625
  }
2620
- const inputFile = await this.resolveInputFile(job.attachment, job.customerId);
2626
+ const inputFile = await this.resolveInputFile(job.attachment, job.customerId, signal);
2621
2627
  await this.transport.sendFeedback(job, { type: "processing" }).catch(() => {
2622
2628
  });
2623
2629
  const skill = this.skills.route(job.tags);
@@ -2760,40 +2766,55 @@ var AgentRuntime = class {
2760
2766
  )
2761
2767
  );
2762
2768
  }
2769
+ let deliveredContent = output.data;
2770
+ if (utf8ByteLength(output.data) > LIMITS.MAX_ENCRYPTED_INLINE_BYTES) {
2771
+ attachments.push(
2772
+ await this.spillTextAttachment(jobId, output.data, customerPubkey, wantBlossom)
2773
+ );
2774
+ deliveredContent = "";
2775
+ }
2763
2776
  this.ledger.recordAttachment(jobId, {
2764
2777
  resultAttachments: attachments.map((a) => JSON.stringify(a))
2765
2778
  });
2766
- return { attachments, deliveredContent: output.data };
2779
+ return { attachments, deliveredContent };
2767
2780
  } finally {
2768
2781
  await output.cleanup?.().catch(() => {
2769
2782
  });
2770
2783
  }
2771
2784
  }
2772
2785
  if (utf8ByteLength(output.data) > LIMITS.MAX_ENCRYPTED_INLINE_BYTES) {
2773
- if (!this.irohTransport) {
2774
- throw new Error("Result is too large to deliver inline and iroh transport is unavailable.");
2775
- }
2776
- const iroh = this.irohTransport;
2777
- const dataBytes = Buffer.from(output.data, "utf8");
2778
- const seeded = await this.seedGuarded(jobId, "large-text", () => iroh.seedBytes(dataBytes));
2779
- const transports = [{ kind: "iroh", ticket: seeded.ticket }];
2780
- if (wantBlossom) {
2781
- const blossomMember = await this.seedBlossomMember(dataBytes, customerPubkey);
2782
- if (blossomMember !== void 0) {
2783
- transports.unshift(blossomMember);
2784
- }
2785
- }
2786
- const attachment = {
2787
- name: "result.txt",
2788
- size: seeded.size,
2789
- mime: "text/plain",
2790
- transports
2791
- };
2786
+ const attachment = await this.spillTextAttachment(
2787
+ jobId,
2788
+ output.data,
2789
+ customerPubkey,
2790
+ wantBlossom
2791
+ );
2792
2792
  this.ledger.recordAttachment(jobId, { resultAttachments: [JSON.stringify(attachment)] });
2793
2793
  return { attachments: [attachment], deliveredContent: "" };
2794
2794
  }
2795
2795
  return { attachments: [], deliveredContent: output.data };
2796
2796
  }
2797
+ /**
2798
+ * Seed a text note too large for inline delivery to iroh (+ encrypted Blossom when
2799
+ * wanted) and return it as a `result.txt` FileAttachment. Shared by the large-text
2800
+ * result path and the file-result path (a file result may carry an oversized note).
2801
+ */
2802
+ async spillTextAttachment(jobId, text, customerPubkey, wantBlossom) {
2803
+ if (!this.irohTransport) {
2804
+ throw new Error("Result is too large to deliver inline and iroh transport is unavailable.");
2805
+ }
2806
+ const iroh = this.irohTransport;
2807
+ const dataBytes = Buffer.from(text, "utf8");
2808
+ const seeded = await this.seedGuarded(jobId, "large-text", () => iroh.seedBytes(dataBytes));
2809
+ const transports = [{ kind: "iroh", ticket: seeded.ticket }];
2810
+ if (wantBlossom) {
2811
+ const blossomMember = await this.seedBlossomMember(dataBytes, customerPubkey);
2812
+ if (blossomMember !== void 0) {
2813
+ transports.unshift(blossomMember);
2814
+ }
2815
+ }
2816
+ return { name: "result.txt", size: seeded.size, mime: "text/plain", transports };
2817
+ }
2797
2818
  /**
2798
2819
  * Seed ONE result file to iroh (+ encrypted Blossom when wanted) and build its
2799
2820
  * FileAttachment. A seed timeout throws SeedFailedError (job stays `paid` for
@@ -2906,51 +2927,77 @@ var AgentRuntime = class {
2906
2927
  * `fetchToBytes`/`fetchToPath` enforce the real cap on the BLAKE3-verified size, so
2907
2928
  * an incorrect declared `size` cannot exceed the in-memory ceiling.
2908
2929
  */
2909
- async resolveInputFile(attachment, senderPubkey) {
2930
+ async resolveInputFile(attachment, senderPubkey, signal) {
2910
2931
  if (attachment === void 0) {
2911
2932
  return void 0;
2912
2933
  }
2913
- const irohMember = attachment.transports.find(
2914
- (t) => t.kind === "iroh"
2915
- );
2916
- const blossomMember = attachment.transports.find(
2917
- (t) => t.kind === "blossom"
2918
- );
2919
- if (irohMember !== void 0 && this.irohTransport !== void 0) {
2920
- if (attachment.mime.startsWith("text/") && attachment.size <= LIMITS.MAX_REINLINE_TEXT_BYTES) {
2921
- const bytes = await this.irohTransport.fetchToBytes(irohMember.ticket, {
2922
- maxBytes: LIMITS.MAX_REINLINE_TEXT_BYTES
2934
+ const fetchTimeoutMs = this.config.executionTimeoutSecs && this.config.executionTimeoutSecs > 0 ? this.config.executionTimeoutSecs * 1e3 : void 0;
2935
+ const fetchAbort = new AbortController();
2936
+ const onParentAbort = () => fetchAbort.abort();
2937
+ if (signal) {
2938
+ if (signal.aborted) {
2939
+ fetchAbort.abort();
2940
+ } else {
2941
+ signal.addEventListener("abort", onParentAbort);
2942
+ }
2943
+ }
2944
+ const budgetTimer = fetchTimeoutMs !== void 0 ? setTimeout(() => fetchAbort.abort(), fetchTimeoutMs) : void 0;
2945
+ try {
2946
+ const irohMember = attachment.transports.find(
2947
+ (t) => t.kind === "iroh"
2948
+ );
2949
+ const blossomMember = attachment.transports.find(
2950
+ (t) => t.kind === "blossom"
2951
+ );
2952
+ if (irohMember !== void 0 && this.irohTransport !== void 0) {
2953
+ if (attachment.mime.startsWith("text/") && attachment.size <= LIMITS.MAX_REINLINE_TEXT_BYTES) {
2954
+ const bytes = await this.irohTransport.fetchToBytes(irohMember.ticket, {
2955
+ maxBytes: LIMITS.MAX_REINLINE_TEXT_BYTES,
2956
+ timeoutMs: fetchTimeoutMs,
2957
+ signal: fetchAbort.signal
2958
+ });
2959
+ return this.materializeBytesInput(bytes, attachment.mime);
2960
+ }
2961
+ const dir = await mkdtemp(join(tmpdir(), "elisym-job-"));
2962
+ const filePath = join(dir, "input");
2963
+ try {
2964
+ await this.irohTransport.fetchToPath(irohMember.ticket, filePath, {
2965
+ timeoutMs: fetchTimeoutMs,
2966
+ signal: fetchAbort.signal
2967
+ });
2968
+ } catch (error) {
2969
+ await rm(dir, { recursive: true, force: true }).catch(() => {
2970
+ });
2971
+ throw error;
2972
+ }
2973
+ return {
2974
+ filePath,
2975
+ cleanup: async () => {
2976
+ await rm(dir, { recursive: true, force: true });
2977
+ }
2978
+ };
2979
+ }
2980
+ if (blossomMember !== void 0 && this.blossomTransport !== void 0 && this.identity !== void 0) {
2981
+ if (attachment.size > LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES) {
2982
+ throw new Error("Blossom file input exceeds the encrypted size cap.");
2983
+ }
2984
+ const bytes = await this.blossomTransport.fetchToBytes({
2985
+ transport: blossomMember,
2986
+ senderPubkey,
2987
+ maxBytes: LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES,
2988
+ signal: fetchAbort.signal
2923
2989
  });
2924
2990
  return this.materializeBytesInput(bytes, attachment.mime);
2925
2991
  }
2926
- const dir = await mkdtemp(join(tmpdir(), "elisym-job-"));
2927
- const filePath = join(dir, "input");
2928
- try {
2929
- await this.irohTransport.fetchToPath(irohMember.ticket, filePath);
2930
- } catch (error) {
2931
- await rm(dir, { recursive: true, force: true }).catch(() => {
2932
- });
2933
- throw error;
2992
+ throw new Error("Job carries a file input but no supported transport is available.");
2993
+ } finally {
2994
+ if (budgetTimer !== void 0) {
2995
+ clearTimeout(budgetTimer);
2934
2996
  }
2935
- return {
2936
- filePath,
2937
- cleanup: async () => {
2938
- await rm(dir, { recursive: true, force: true });
2939
- }
2940
- };
2941
- }
2942
- if (blossomMember !== void 0 && this.blossomTransport !== void 0 && this.identity !== void 0) {
2943
- if (attachment.size > LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES) {
2944
- throw new Error("Blossom file input exceeds the encrypted size cap.");
2997
+ if (signal) {
2998
+ signal.removeEventListener("abort", onParentAbort);
2945
2999
  }
2946
- const bytes = await this.blossomTransport.fetchToBytes({
2947
- transport: blossomMember,
2948
- senderPubkey,
2949
- maxBytes: LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES
2950
- });
2951
- return this.materializeBytesInput(bytes, attachment.mime);
2952
3000
  }
2953
- throw new Error("Job carries a file input but no supported transport is available.");
2954
3001
  }
2955
3002
  /**
2956
3003
  * Turn fetched input bytes into a SkillInput: text within the re-inline ceiling becomes an
@@ -3147,7 +3194,10 @@ var AgentRuntime = class {
3147
3194
  }
3148
3195
  await this.transport.sendFeedback(job, { type: "error", message: "payment timeout" }).catch(() => {
3149
3196
  });
3150
- throw new Error("Payment timeout");
3197
+ if (customerAbandoned) {
3198
+ throw new Error("Payment timeout");
3199
+ }
3200
+ throw new PaymentTimeoutError();
3151
3201
  }
3152
3202
  async recoverPendingJobs() {
3153
3203
  const pending = this.ledger.pendingJobs().filter((e) => !this.inFlight.has(e.job_id));
@@ -3303,9 +3353,15 @@ var AgentRuntime = class {
3303
3353
  }
3304
3354
  let recoveryInputFile;
3305
3355
  try {
3356
+ const encrypted = rawEvent.tags?.some(
3357
+ (tag) => tag[0] === "encrypted" && tag[1] === "nip44"
3358
+ );
3359
+ const iTag = rawEvent.tags?.find((tag) => tag[0] === "i");
3360
+ const rawInput = encrypted ? rawEvent.content : iTag?.[1] ?? rawEvent.content;
3306
3361
  recoveryInputFile = await this.resolveInputFile(
3307
- decodeJobPayload(rawEvent.content).attachment,
3308
- entry.customer_id
3362
+ decodeJobPayload(rawInput).attachment,
3363
+ entry.customer_id,
3364
+ recoveryAbort.signal
3309
3365
  );
3310
3366
  } catch {
3311
3367
  log(`[${entry.job_id.slice(0, 8)}] Recovery: input file unavailable, marking failed`);
@@ -3341,6 +3397,9 @@ var AgentRuntime = class {
3341
3397
  } else {
3342
3398
  output = await execPromise;
3343
3399
  }
3400
+ } catch (err) {
3401
+ this.markHealthFromExecuteError(skill, err, log, entry.job_id);
3402
+ throw err;
3344
3403
  } finally {
3345
3404
  if (budgetTimer) {
3346
3405
  clearTimeout(budgetTimer);
@@ -4145,11 +4204,13 @@ async function cmdStart(nameArg, options = {}) {
4145
4204
  }
4146
4205
  console.log();
4147
4206
  } catch (e) {
4148
- console.warn(` ! Wallet error: ${e.message}
4207
+ const message = typeof e?.message === "string" ? e.message : String(e);
4208
+ console.warn(` ! Wallet error: ${redactRpcUrlsInText(message)}
4149
4209
  `);
4150
4210
  }
4151
4211
  }
4152
4212
  const scriptEnv = { ...process.env };
4213
+ delete scriptEnv.ELISYM_PASSPHRASE;
4153
4214
  const llmKeys = loaded.secrets.llm_api_keys ?? {};
4154
4215
  for (const descriptor of listLlmProviders()) {
4155
4216
  const secretValue = llmKeys[descriptor.id];
@@ -4703,6 +4764,9 @@ function stripRpcSecrets(raw) {
4703
4764
  return "[unparseable RPC URL]";
4704
4765
  }
4705
4766
  }
4767
+ function redactRpcUrlsInText(text) {
4768
+ return text.replace(/https?:\/\/[^\s)'"]+/g, (url) => stripRpcSecrets(url));
4769
+ }
4706
4770
  async function resolveMediaField(value, agentDir, cache, blossom, identity, onCacheUpdate) {
4707
4771
  if (!value) {
4708
4772
  return void 0;