@elisym/cli 0.31.0 → 0.32.0

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
@@ -2,19 +2,19 @@
2
2
  import { ReadableStream } from 'node:stream/web';
3
3
  import { readFileSync, existsSync, readdirSync, statSync, renameSync, chmodSync, mkdirSync, writeFileSync, appendFileSync, unlinkSync, rmdirSync, realpathSync } from 'node:fs';
4
4
  import { dirname, join, resolve, basename, extname, relative, sep } from 'node:path';
5
- import { DEFAULTS, SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, splAssetsForNetwork, formatSol, formatAssetAmount, signerFromSecretKeyBase58, resolveUsdcAsset, ElisymClient, KIND_EXTERNAL_IDENTITIES, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, createBlossomTransport, generateSolanaWallet, verifyAgentIdentities, getProtocolConfig, getProtocolProgramId, calculateProtocolFee, LIMITS, makeCensor, DEFAULT_REDACT_PATHS, POLICY_T_TAG, SESSION_ID_REGEX, createSlidingWindowLimiter, utf8ByteLength, readAcceptedTransports, resolveDelegationAsset, MAX_PROOF_TTL_SECS, PROOF_CLOCK_SKEW_SECS, verifyDelegationAuthProof, deriveOwnerDelegationAta, getDelegation, buildDelegatedTransfer, buildSignedPull, sendConfirmToTerminal, parseDelegatedPayment, confirmPullToTerminal, isDefinitelyUnpaid, decodeJobPayload, BoundedSet, KIND_JOB_FEEDBACK, encodeSecretKeyBase58, GITHUB_USERNAME_REGEX, X_USERNAME_REGEX, normalizeNip05Identifier, splitNip05Identifier, NATIVE_SOL, GIST_ID_REGEX, TWEET_ID_REGEX } from '@elisym/sdk';
5
+ import { DEFAULTS, SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, splAssetsForNetwork, formatSol, formatAssetAmount, signerFromSecretKeyBase58, resolveUsdcAsset, ElisymClient, KIND_EXTERNAL_IDENTITIES, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, deleteControlCharacters, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, createBlossomTransport, generateSolanaWallet, verifyAgentIdentities, getProtocolConfig, getProtocolProgramId, calculateProtocolFee, LIMITS, makeCensor, DEFAULT_REDACT_PATHS, flattenForComparison, POLICY_T_TAG, SESSION_ID_REGEX, createSlidingWindowLimiter, excerptUntrusted, isScriptBillingExhaustedError, excerptUntrustedTail, scriptOutput, isScriptExecutionError, utf8ByteLength, readAcceptedTransports, resolveDelegationAsset, MAX_PROOF_TTL_SECS, PROOF_CLOCK_SKEW_SECS, verifyDelegationAuthProof, deriveOwnerDelegationAta, getDelegation, buildDelegatedTransfer, buildSignedPull, sendConfirmToTerminal, parseDelegatedPayment, confirmPullToTerminal, isDefinitelyUnpaid, decodeJobPayload, isLlmHealthError, BoundedSet, KIND_JOB_FEEDBACK, encodeSecretKeyBase58, GITHUB_USERNAME_REGEX, X_USERNAME_REGEX, normalizeNip05Identifier, splitNip05Identifier, clipToCodeUnits, PROVIDER_REFUSED_PREFIX, PROVIDER_FAILED_MESSAGE, NATIVE_SOL, GIST_ID_REGEX, TWEET_ID_REGEX } from '@elisym/sdk';
6
6
  import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, loadPoliciesFromDir, ensureGitignoreHasIrohEntry, ensureGitignoreHasX402Entries, ensureGitignoreHasSessionsEntry, ensureGitignoreHasDelegationNoncesEntry, readAgentPublic, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
7
7
  import { isAddress, createSolanaRpc, address, signature } from '@solana/kit';
8
8
  import { generateSecretKey, getPublicKey, nip19, verifyEvent } from 'nostr-tools';
9
9
  import YAML2 from 'yaml';
10
10
  import { Command } from 'commander';
11
- import { mkdir, writeFile, readFile, mkdtemp, rm, rename, stat } from 'node:fs/promises';
12
- import { parseSkillMd, validateSkillFrontmatter, DEFAULT_X402_MAX_INPUT_BYTES, resolveInsidePathReal, DEFAULT_SCRIPT_TIMEOUT_MS, OnchainCallSkill, DynamicScriptSkill as DynamicScriptSkill$1, StaticScriptSkill as StaticScriptSkill$1, StaticFileSkill as StaticFileSkill$1, ScriptSkill as ScriptSkill$1 } from '@elisym/sdk/skills';
11
+ import { mkdir, writeFile, mkdtemp, rm, readFile, rename, stat } from 'node:fs/promises';
12
+ import { parseSkillMd, validateSkillFrontmatter, isHostScratchError, isScriptRefusalError, startsWithRefusalHint, HostScratchError, DEFAULT_X402_MAX_INPUT_BYTES, resolveInsidePathReal, DEFAULT_SCRIPT_TIMEOUT_MS, OnchainCallSkill, DynamicScriptSkill as DynamicScriptSkill$1, StaticScriptSkill as StaticScriptSkill$1, StaticFileSkill as StaticFileSkill$1, ScriptSkill as ScriptSkill$1 } from '@elisym/sdk/skills';
13
13
  import chalk from 'chalk';
14
14
  import Decimal from 'decimal.js-light';
15
15
  import { decodePaymentRequiredHeader } from '@x402/core/http';
16
16
  import { createHash } from 'node:crypto';
17
- import { LlmHealthMonitor, startLlmRecovery, createFreeLlmLimiterSet, ScriptBillingExhaustedError, ScriptExecutionError, FREE_LLM_GLOBAL_KEY, freeLlmCustomerKey, LlmHealthError } from '@elisym/sdk/llm-health';
17
+ import { LlmHealthMonitor, startLlmRecovery, createFreeLlmLimiterSet, FREE_LLM_GLOBAL_KEY, freeLlmCustomerKey } from '@elisym/sdk/llm-health';
18
18
  import { createIrohTransport } from '@elisym/sdk/node';
19
19
  import { lookup } from 'node:dns/promises';
20
20
  import { Socket } from 'node:net';
@@ -3804,9 +3804,6 @@ function resolveProviderApiKey(input) {
3804
3804
  error: `Provider "${provider}" needs an API key (required by skill(s): ${skillList}). Set secrets.llm_api_keys.${provider} via 'npx @elisym/cli profile <agent>' or export ${descriptor.envVar}.`
3805
3805
  };
3806
3806
  }
3807
- function sanitizeForTerminal(value) {
3808
- return value.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, "");
3809
- }
3810
3807
  function resolveLevel(options) {
3811
3808
  if (options.level) {
3812
3809
  return options.level;
@@ -3845,7 +3842,7 @@ function createLogger(options = {}) {
3845
3842
  logger = pino(baseOptions, pino.destination(2));
3846
3843
  }
3847
3844
  function logWithIndent(line) {
3848
- process.stdout.write(` ${sanitizeForTerminal(line)}
3845
+ process.stdout.write(` ${deleteControlCharacters(line)}
3849
3846
  `);
3850
3847
  }
3851
3848
  return {
@@ -5324,24 +5321,106 @@ function resolveJobAsset(tags, skills) {
5324
5321
  const skill = skills.route(tags);
5325
5322
  return skill?.asset ?? NATIVE_SOL;
5326
5323
  }
5327
- var BILLING_BODY_MARKERS3 = ["credit balance", "billing", "insufficient", "insufficient_quota"];
5328
5324
  var SCRIPT_BILLING_INVALID_MARKERS = [
5329
- "credit balance",
5330
- "billing",
5331
- "insufficient",
5332
- "insufficient_quota",
5333
- "x-api-key",
5334
- "invalid api key",
5335
- "invalid_api_key",
5336
- "authentication_error",
5337
- "unauthorized",
5338
- "unauthenticated"
5325
+ // Three questions, one row each, so an edit cannot land half-applied: does
5326
+ // this phrase gate THIS pair at all, does it also take every model on the
5327
+ // operator's key offline (`cascades`), and is it about money rather than
5328
+ // credentials (`billing`, which picks the reason the operator is shown and
5329
+ // the recovery probe reads).
5330
+ //
5331
+ // `cascades` is the expensive one. Gating ONE pair on a false positive costs
5332
+ // the operator a capability until the recovery probe clears it; a cascade
5333
+ // costs them every capability on that key. So it needs a phrase an unrelated
5334
+ // failure does not produce: `insufficient` alone is a Solana builder saying
5335
+ // "insufficient funds for rent" and `billing` alone is a form field, while
5336
+ // nothing prints `invalid x-api-key` or `credit balance` except the provider
5337
+ // whose key it is. The bare status words carry `cascades: false` for the same
5338
+ // reason - `unauthorized` is what any proxy says when it did not like a
5339
+ // request. The LLM path next door demands an HTTP 401/402 before cascading;
5340
+ // this is the script path's version of that bar.
5341
+ { phrase: "credit balance", cascades: true, billing: true },
5342
+ { phrase: "billing", cascades: false, billing: true },
5343
+ { phrase: "insufficient", cascades: false, billing: true },
5344
+ { phrase: "insufficient_quota", cascades: true, billing: true },
5345
+ // Gates this pair, never cascades: `x-api-key` is the name of a REQUEST
5346
+ // HEADER, which `curl -v` and any client that prints its own headers emit on a
5347
+ // perfectly ordinary failure. Taking every model on the key offline for that
5348
+ // is the expensive direction; `invalid x-api-key` below is the provider
5349
+ // actually rejecting it.
5350
+ { phrase: "x-api-key", cascades: false, billing: false },
5351
+ // The provider REJECTING the key, which no request header says.
5352
+ { phrase: "invalid x-api-key", cascades: true, billing: false },
5353
+ { phrase: "invalid api key", cascades: true, billing: false },
5354
+ { phrase: "invalid_api_key", cascades: true, billing: false },
5355
+ { phrase: "authentication_error", cascades: true, billing: false },
5356
+ { phrase: "unauthorized", cascades: false, billing: false },
5357
+ { phrase: "unauthenticated", cascades: false, billing: false }
5339
5358
  ];
5340
- function scriptMessageLooksLikeBillingOrInvalid(message) {
5341
- const lower = message.toLowerCase();
5342
- return SCRIPT_BILLING_INVALID_MARKERS.some((marker) => lower.includes(marker));
5359
+ var BILLING_BODY_MARKERS3 = SCRIPT_BILLING_INVALID_MARKERS.filter((marker) => marker.billing).map(
5360
+ (marker) => marker.phrase
5361
+ );
5362
+ function classifyScriptSignal(message) {
5363
+ let looksBillingOrInvalid = false;
5364
+ let billing = false;
5365
+ let cascade = false;
5366
+ let signalAt = -1;
5367
+ for (const match of message.matchAll(ALL_MARKERS_RE)) {
5368
+ const marker = MARKER_BY_PHRASE.get(match[0].toLowerCase());
5369
+ looksBillingOrInvalid = true;
5370
+ billing = billing || marker?.billing === true;
5371
+ cascade = cascade || marker?.cascades === true;
5372
+ if (signalAt === -1) {
5373
+ signalAt = match.index;
5374
+ }
5375
+ }
5376
+ return { looksBillingOrInvalid, reason: billing ? "billing" : "invalid", cascade, signalAt };
5377
+ }
5378
+ var HOST_DISK_CODES = /* @__PURE__ */ new Set(["ENOSPC", "EROFS", "EDQUOT", "EMFILE", "ENFILE", "EIO"]);
5379
+ function asHostScratchFailure(error) {
5380
+ const code = error?.code;
5381
+ if (code === void 0 || !HOST_DISK_CODES.has(code)) {
5382
+ return void 0;
5383
+ }
5384
+ return new HostScratchError(error instanceof Error ? error.message : String(error));
5385
+ }
5386
+ var HEALTH_REASON_CHARS = 500;
5387
+ var SIGNAL_LEAD_UNITS = 80;
5388
+ function markerPattern(phrases, flags) {
5389
+ const longestFirst = [...phrases].sort((left, right) => right.length - left.length);
5390
+ return new RegExp(
5391
+ longestFirst.map((phrase) => phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"),
5392
+ flags
5393
+ );
5394
+ }
5395
+ var ALL_MARKERS_RE = markerPattern(
5396
+ SCRIPT_BILLING_INVALID_MARKERS.map((marker) => marker.phrase),
5397
+ "gi"
5398
+ );
5399
+ var MARKER_BY_PHRASE = new Map(
5400
+ SCRIPT_BILLING_INVALID_MARKERS.map((marker) => [marker.phrase, marker])
5401
+ );
5402
+ function signalIndex(text) {
5403
+ ALL_MARKERS_RE.lastIndex = 0;
5404
+ const found = ALL_MARKERS_RE.exec(text)?.index ?? -1;
5405
+ ALL_MARKERS_RE.lastIndex = 0;
5406
+ return found;
5407
+ }
5408
+ function operatorReason(diagnostic, known, budget = HEALTH_REASON_CHARS) {
5409
+ if (startsWithRefusalHint(diagnostic)) {
5410
+ return excerptUntrusted(diagnostic, budget);
5411
+ }
5412
+ const signalAt = known ?? signalIndex(diagnostic);
5413
+ if (signalAt === -1) {
5414
+ return excerptUntrustedTail(diagnostic, budget);
5415
+ }
5416
+ const from = Math.max(0, signalAt - SIGNAL_LEAD_UNITS);
5417
+ if (from === 0) {
5418
+ return excerptUntrusted(diagnostic, budget);
5419
+ }
5420
+ return `\u2026${excerptUntrusted(diagnostic.slice(from, from + budget * 8), budget - 1)}`;
5343
5421
  }
5344
5422
  var AGENT_UNAVAILABLE_MESSAGE = "Agent temporarily unavailable";
5423
+ var OPERATOR_EXCERPT_CHARS = 1500;
5345
5424
  var AgentUnavailableError = class extends Error {
5346
5425
  constructor() {
5347
5426
  super(AGENT_UNAVAILABLE_MESSAGE);
@@ -5395,16 +5474,49 @@ function customerSafeMessage(error) {
5395
5474
  if (error instanceof AgentUnavailableError || error instanceof ExecutionBudgetExceededError || error instanceof SeedFailedError) {
5396
5475
  return error.message;
5397
5476
  }
5398
- if (error instanceof ScriptExecutionError) {
5399
- return error.message;
5477
+ if (isScriptRefusalError(error)) {
5478
+ return `${PROVIDER_REFUSED_PREFIX}${error.message}`;
5400
5479
  }
5401
- if (error instanceof ScriptBillingExhaustedError) {
5480
+ if (isScriptExecutionError(error)) {
5481
+ return PROVIDER_FAILED_MESSAGE;
5482
+ }
5483
+ if (isScriptBillingExhaustedError(error) || isHostScratchError(error) || error instanceof X402TransientError) {
5402
5484
  return AGENT_UNAVAILABLE_MESSAGE;
5403
5485
  }
5404
5486
  if (error instanceof Error && CUSTOMER_SAFE_MESSAGE_PREFIXES.some((prefix) => error.message.startsWith(prefix))) {
5405
5487
  return error.message;
5406
5488
  }
5407
- return "Internal processing error";
5489
+ return PROVIDER_FAILED_MESSAGE;
5490
+ }
5491
+ function needsScratchSpace(mode, hasAttachment) {
5492
+ if (hasAttachment) {
5493
+ return true;
5494
+ }
5495
+ return mode !== void 0 && mode !== "llm" && mode !== "static-file" && mode !== "static-script";
5496
+ }
5497
+ function describeForOperator(error) {
5498
+ if (isHostScratchError(error)) {
5499
+ return `${error.message}: ${excerptUntrusted(error.detail, OPERATOR_EXCERPT_CHARS)}`;
5500
+ }
5501
+ if (isScriptBillingExhaustedError(error)) {
5502
+ return `script signalled billing exhausted: ${excerptUntrustedTail(scriptOutput(error), OPERATOR_EXCERPT_CHARS)}`;
5503
+ }
5504
+ if (isScriptRefusalError(error)) {
5505
+ return error.stderr === "" ? `refused: ${error.message}` : `refused: ${error.message} (stderr: ${error.stderr})`;
5506
+ }
5507
+ if (isScriptExecutionError(error)) {
5508
+ return `${error.message}: ${excerptUntrustedTail(error.detail, OPERATOR_EXCERPT_CHARS)}`;
5509
+ }
5510
+ if (typeof error === "string") {
5511
+ return excerptUntrusted(error, OPERATOR_EXCERPT_CHARS);
5512
+ }
5513
+ if (typeof error === "object" && error !== null && "message" in error) {
5514
+ const message = error.message;
5515
+ if (typeof message === "string" && message !== "") {
5516
+ return excerptUntrusted(message, OPERATOR_EXCERPT_CHARS);
5517
+ }
5518
+ }
5519
+ return "Unknown error";
5408
5520
  }
5409
5521
  function parseRawEventDelegated(rawEventJson) {
5410
5522
  if (rawEventJson === void 0) {
@@ -5691,12 +5803,51 @@ var AgentRuntime = class {
5691
5803
  }
5692
5804
  return ` (cascading to ${siblings} other model(s) for ${provider} sharing the same API key)`;
5693
5805
  }
5806
+ /**
5807
+ * Whether this agent can still make itself a scratch directory.
5808
+ *
5809
+ * A `HostScratchError` says the last job could not run because the temp
5810
+ * directory refused it. That is not the operator's API key, so it must not
5811
+ * gate the health pair - but SOMETHING has to stop the next customer paying
5812
+ * into an agent that will fail them the same way, which is what the health
5813
+ * gate used to do by accident. Probed before payment, and only once such a
5814
+ * failure has been seen - a working host pays nothing for this, and a broken
5815
+ * one is asked again on every job rather than after a timer: an agent whose
5816
+ * jobs arrive minutes apart would otherwise let every customer through.
5817
+ */
5818
+ scratchFailed = false;
5819
+ async scratchSpaceUsable() {
5820
+ if (!this.scratchFailed) {
5821
+ return true;
5822
+ }
5823
+ const probe = await mkdtemp(join(tmpdir(), "elisym-probe-")).catch(() => null);
5824
+ if (probe === null) {
5825
+ return false;
5826
+ }
5827
+ const wrote = await writeFile(join(probe, "probe"), "x").then(() => true).catch(() => false);
5828
+ await rm(probe, { recursive: true, force: true }).catch(() => {
5829
+ });
5830
+ if (!wrote) {
5831
+ return false;
5832
+ }
5833
+ this.scratchFailed = false;
5834
+ return true;
5835
+ }
5694
5836
  markHealthFromExecuteError(skill, err, log, jobId) {
5695
5837
  if (!this.healthMonitor) {
5696
5838
  return false;
5697
5839
  }
5698
5840
  const tag = `[${jobId.slice(0, 8)}]`;
5699
- if (err instanceof ScriptBillingExhaustedError) {
5841
+ if (isHostScratchError(err)) {
5842
+ log(
5843
+ `${tag} Skill "${skill.name}" could not be given scratch space by THIS AGENT: ${excerptUntrusted(err.detail, 300)} Health state unchanged; the job stays paid for recovery.`
5844
+ );
5845
+ return false;
5846
+ }
5847
+ if (isScriptRefusalError(err)) {
5848
+ return false;
5849
+ }
5850
+ if (isScriptBillingExhaustedError(err)) {
5700
5851
  const provider = skill.llmOverride?.provider;
5701
5852
  const model = skill.llmOverride?.model;
5702
5853
  if (!provider || !model) {
@@ -5708,7 +5859,12 @@ var AgentRuntime = class {
5708
5859
  log(
5709
5860
  `${tag} Script signaled billing-exhausted (exit ${err.exitCode}). Marking ${provider}/${model} unhealthy${this.cascadeSuffix(provider, model)}; future jobs against this pair will be refused until recovery probe succeeds.`
5710
5861
  );
5711
- this.healthMonitor.markUnhealthyFromJob(provider, model, "billing", err.message);
5862
+ this.healthMonitor.markUnhealthyFromJob(
5863
+ provider,
5864
+ model,
5865
+ "billing",
5866
+ excerptUntrustedTail(scriptOutput(err), 200)
5867
+ );
5712
5868
  return true;
5713
5869
  }
5714
5870
  if (skill.mode === "llm" && skill.resolvedTriple) {
@@ -5718,7 +5874,7 @@ var AgentRuntime = class {
5718
5874
  return false;
5719
5875
  }
5720
5876
  const status = Number(match[1]);
5721
- const body = (match[2] ?? "").slice(0, 200);
5877
+ const body = excerptUntrusted(match[2] ?? "", 200);
5722
5878
  const isBillingStatus = status === 402;
5723
5879
  const isAuthStatus = status === 401 || status === 403;
5724
5880
  const isBilling400 = status === 400 && bodyLooksLikeBilling3(body);
@@ -5737,33 +5893,43 @@ var AgentRuntime = class {
5737
5893
  }
5738
5894
  if (skill.mode !== "llm") {
5739
5895
  let message;
5740
- if (err instanceof ScriptExecutionError) {
5741
- message = err.detail;
5896
+ let scanned;
5897
+ let diagnostic;
5898
+ if (isScriptExecutionError(err)) {
5899
+ message = err.stderr ?? err.detail;
5900
+ diagnostic = startsWithRefusalHint(err.detail) || message.trim() === "" ? err.detail : message;
5901
+ scanned = err.stderr === void 0 ? "the skill error" : "stderr";
5742
5902
  } else if (err instanceof Error) {
5743
5903
  message = err.message;
5904
+ diagnostic = message;
5905
+ scanned = "the skill error";
5744
5906
  } else {
5745
5907
  message = String(err);
5908
+ diagnostic = message;
5909
+ scanned = "the skill error";
5746
5910
  }
5747
5911
  const provider = skill.llmOverride?.provider;
5748
5912
  const model = skill.llmOverride?.model;
5749
5913
  if (!provider || !model) {
5750
5914
  log(
5751
- `${tag} Script "${skill.name}" failed ("${message.slice(0, 120)}") but did not declare provider/model in SKILL.md - cannot gate future jobs.`
5915
+ `${tag} Script "${skill.name}" failed ("${operatorReason(diagnostic)}") but did not declare provider/model in SKILL.md - cannot gate future jobs.`
5752
5916
  );
5753
5917
  return false;
5754
5918
  }
5755
- const lower = message.toLowerCase();
5756
- const looksBillingOrInvalid = scriptMessageLooksLikeBillingOrInvalid(message);
5757
- const reason = looksBillingOrInvalid && (lower.includes("credit balance") || lower.includes("billing") || lower.includes("insufficient")) ? "billing" : "invalid";
5758
- const cascade = looksBillingOrInvalid;
5919
+ const { looksBillingOrInvalid, reason, cascade, signalAt } = classifyScriptSignal(message);
5759
5920
  const cascadeNote = cascade ? this.cascadeSuffix(provider, model) : " (no cascade)";
5760
- const signalNote = looksBillingOrInvalid ? `${reason} signal in stderr` : `generic exit (no billing/invalid markers, classified as ${reason}, skill-local)`;
5921
+ const signalNote = looksBillingOrInvalid ? `${reason} signal in ${scanned}` : `generic exit (no billing/invalid markers, classified as ${reason}, skill-local)`;
5761
5922
  log(
5762
5923
  `${tag} Script failure (${signalNote}). Marking ${provider}/${model} unhealthy${cascadeNote}; future jobs against this pair will be refused until recovery probe succeeds.`
5763
5924
  );
5764
- this.healthMonitor.markUnhealthyFromJob(provider, model, reason, message.slice(0, 200), {
5765
- cascade
5766
- });
5925
+ this.healthMonitor.markUnhealthyFromJob(
5926
+ provider,
5927
+ model,
5928
+ reason,
5929
+ // The index only when the quote is of the same string the scan read.
5930
+ operatorReason(diagnostic, diagnostic === message ? signalAt : void 0),
5931
+ { cascade }
5932
+ );
5767
5933
  return true;
5768
5934
  }
5769
5935
  return false;
@@ -5878,8 +6044,8 @@ var AgentRuntime = class {
5878
6044
  if (jobSession) {
5879
6045
  jobSession.store.admitLive(jobSession.customerId, jobSession.sessionId);
5880
6046
  }
5881
- this.limit(() => this.processJob(job)).catch((e) => {
5882
- this.callbacks.onJobError?.(job.jobId, e.message);
6047
+ this.limit(() => this.processJob(job)).catch((err) => {
6048
+ this.callbacks.onJobError?.(job.jobId, describeForOperator(err));
5883
6049
  }).finally(() => {
5884
6050
  this.inFlight.delete(job.jobId);
5885
6051
  this.pending--;
@@ -5964,18 +6130,31 @@ var AgentRuntime = class {
5964
6130
  this.jobAbortControllers.add(jobAbort);
5965
6131
  try {
5966
6132
  await this.executeJob(job, jobAbort.signal);
5967
- } catch (e) {
6133
+ } catch (raw) {
6134
+ const e = asHostScratchFailure(raw) ?? raw;
5968
6135
  const log = this.callbacks.onLog ?? console.log;
5969
- log(`[${job.jobId.slice(0, 8)}] Error: ${e.message}`);
6136
+ const operatorMessage = describeForOperator(e);
6137
+ log(`[${job.jobId.slice(0, 8)}] Error: ${operatorMessage}`);
6138
+ if (isHostScratchError(e)) {
6139
+ this.scratchFailed = true;
6140
+ }
5970
6141
  const currentStatus = this.ledger.getStatus(job.jobId);
5971
- const keepPaidForRecovery = (e instanceof AgentUnavailableError || e instanceof SeedFailedError || e instanceof PaymentTimeoutError || e instanceof X402TransientError || e instanceof ExecutionBudgetExceededError && this.skills.route(job.tags)?.mode === "x402") && currentStatus === "paid";
6142
+ const keepPaidForRecovery = (e instanceof AgentUnavailableError || // The exit-42 contract: the key is out of credits, not the job out of
6143
+ // sense, so the job waits for the operator to top up. Reached when the
6144
+ // gate did NOT flip - an agent with no health monitor, or a skill that
6145
+ // declares no pair - where the customer is told "temporarily
6146
+ // unavailable" and that has to stay true of their money.
6147
+ isScriptBillingExhaustedError(e) || // The agent's own disk, not the job: a tmpdir that is full or
6148
+ // read-only now may not be in five minutes, and the customer has
6149
+ // already paid. Terminating here would keep their money for a failure
6150
+ // that was never about their request.
6151
+ isHostScratchError(e) || e instanceof SeedFailedError || e instanceof PaymentTimeoutError || e instanceof X402TransientError || e instanceof ExecutionBudgetExceededError && this.skills.route(job.tags)?.mode === "x402") && currentStatus === "paid";
5972
6152
  if (currentStatus !== "executed" && !keepPaidForRecovery) {
5973
6153
  this.ledger.markFailed(job.jobId);
5974
6154
  }
5975
6155
  if (keepPaidForRecovery) {
5976
6156
  log(`[${job.jobId.slice(0, 8)}] Keeping status=paid; recovery will retry (24h cutoff).`);
5977
6157
  }
5978
- const operatorMessage = e instanceof ScriptExecutionError ? `${e.message}: ${e.detail}` : e.message ?? "Unknown error";
5979
6158
  this.callbacks.onJobError?.(job.jobId, operatorMessage);
5980
6159
  const safeMessage = customerSafeMessage(e);
5981
6160
  await this.transport.sendFeedback(job, { type: "error", message: safeMessage }).catch(() => {
@@ -6007,6 +6186,14 @@ var AgentRuntime = class {
6007
6186
  return;
6008
6187
  }
6009
6188
  }
6189
+ if (needsScratchSpace(matched?.mode, job.attachment !== void 0) && !await this.scratchSpaceUsable()) {
6190
+ log(
6191
+ `[${job.jobId.slice(0, 8)}] Refusing job before payment: this agent cannot create scratch space (check the temp directory).`
6192
+ );
6193
+ await this.transport.sendFeedback(job, { type: "error", message: AGENT_UNAVAILABLE_MESSAGE }).catch(() => {
6194
+ });
6195
+ return;
6196
+ }
6010
6197
  if (matched?.mode === "x402" && job.attachment !== void 0) {
6011
6198
  const method = matched.x402?.method ?? "POST";
6012
6199
  const maxInputBytes = matched.x402?.maxInputBytes ?? 0;
@@ -6893,7 +7080,9 @@ var AgentRuntime = class {
6893
7080
  });
6894
7081
  return this.materializeBytesInput(bytes, attachment.mime);
6895
7082
  }
6896
- const dir = await mkdtemp(join(tmpdir(), "elisym-job-"));
7083
+ const dir = await mkdtemp(join(tmpdir(), "elisym-job-")).catch((err) => {
7084
+ throw new HostScratchError(err instanceof Error ? err.message : String(err));
7085
+ });
6897
7086
  const filePath = join(dir, "input");
6898
7087
  try {
6899
7088
  await this.irohTransport.fetchToPath(irohMember.ticket, filePath, {
@@ -6945,14 +7134,16 @@ var AgentRuntime = class {
6945
7134
  return { inlineText: Buffer.from(bytes).toString("utf8"), cleanup: async () => {
6946
7135
  } };
6947
7136
  }
6948
- const dir = await mkdtemp(join(tmpdir(), "elisym-job-"));
7137
+ const dir = await mkdtemp(join(tmpdir(), "elisym-job-")).catch((err) => {
7138
+ throw new HostScratchError(err instanceof Error ? err.message : String(err));
7139
+ });
6949
7140
  const filePath = join(dir, "input");
6950
7141
  try {
6951
7142
  await writeFile(filePath, bytes);
6952
7143
  } catch (error) {
6953
7144
  await rm(dir, { recursive: true, force: true }).catch(() => {
6954
7145
  });
6955
- throw error;
7146
+ throw asHostScratchFailure(error) ?? error;
6956
7147
  }
6957
7148
  return {
6958
7149
  filePath,
@@ -7287,12 +7478,13 @@ var AgentRuntime = class {
7287
7478
  this.limit(async () => {
7288
7479
  try {
7289
7480
  await this.recoverSingleJob(entry, log);
7290
- } catch (e) {
7291
- log(`[${entry.job_id.slice(0, 8)}] Recovery: failed: ${e.message}`);
7481
+ } catch (err) {
7482
+ log(`[${entry.job_id.slice(0, 8)}] Recovery: failed: ${describeForOperator(err)}`);
7292
7483
  } finally {
7293
7484
  this.inFlight.delete(entry.job_id);
7294
7485
  this.pending--;
7295
7486
  }
7487
+ }).catch(() => {
7296
7488
  });
7297
7489
  }
7298
7490
  }
@@ -7398,7 +7590,7 @@ var AgentRuntime = class {
7398
7590
  try {
7399
7591
  await this.healthMonitor.assertReady(healthPair.provider, healthPair.model);
7400
7592
  } catch (err) {
7401
- if (err instanceof LlmHealthError) {
7593
+ if (isLlmHealthError(err)) {
7402
7594
  log(
7403
7595
  `[${entry.job_id.slice(0, 8)}] Recovery: pair ${healthPair.provider}/${healthPair.model} still unhealthy (${err.reason}); waiting for recovery probe.`
7404
7596
  );
@@ -7442,7 +7634,15 @@ var AgentRuntime = class {
7442
7634
  entry.customer_id,
7443
7635
  recoveryAbort.signal
7444
7636
  );
7445
- } catch {
7637
+ } catch (raw) {
7638
+ const err = asHostScratchFailure(raw) ?? raw;
7639
+ if (isHostScratchError(err)) {
7640
+ this.scratchFailed = true;
7641
+ log(
7642
+ `[${entry.job_id.slice(0, 8)}] Recovery: no scratch space for the input file; leaving the job paid.`
7643
+ );
7644
+ return;
7645
+ }
7446
7646
  log(`[${entry.job_id.slice(0, 8)}] Recovery: input file unavailable, marking failed`);
7447
7647
  await this.failRecoveredJob(entry, fakeJob, RECOVERY_INPUT_UNAVAILABLE_CUSTOMER_MESSAGE);
7448
7648
  return;
@@ -7453,6 +7653,7 @@ var AgentRuntime = class {
7453
7653
  skill
7454
7654
  );
7455
7655
  const runRecoveryExecution = async (history) => {
7656
+ let budgetExceeded = false;
7456
7657
  let budgetTimer;
7457
7658
  try {
7458
7659
  const execPromise = skill.execute(
@@ -7474,6 +7675,7 @@ var AgentRuntime = class {
7474
7675
  execPromise,
7475
7676
  new Promise((_resolve, reject) => {
7476
7677
  budgetTimer = setTimeout(() => {
7678
+ budgetExceeded = true;
7477
7679
  recoveryAbort.abort();
7478
7680
  reject(new ExecutionBudgetExceededError(recoveryBudgetMs));
7479
7681
  }, recoveryBudgetMs);
@@ -7482,6 +7684,12 @@ var AgentRuntime = class {
7482
7684
  }
7483
7685
  return await execPromise;
7484
7686
  } catch (err) {
7687
+ if (isHostScratchError(err)) {
7688
+ this.scratchFailed = true;
7689
+ }
7690
+ if (budgetExceeded) {
7691
+ throw new ExecutionBudgetExceededError(recoveryBudgetMs);
7692
+ }
7485
7693
  this.markHealthFromExecuteError(skill, err, log, entry.job_id);
7486
7694
  throw err;
7487
7695
  } finally {
@@ -7494,18 +7702,28 @@ var AgentRuntime = class {
7494
7702
  }
7495
7703
  }
7496
7704
  };
7497
- const output = recoveryJobSession === null ? await runRecoveryExecution() : await this.runSessionExchange({
7498
- session: recoveryJobSession,
7499
- jobId: entry.job_id,
7500
- userRecord: buildUserRecord(
7501
- recoveryInputFile?.inlineText ?? entry.input,
7502
- recoveryInputFile?.filePath !== void 0 ? "attachment" : void 0
7503
- ),
7504
- signal: recoveryAbort.signal,
7505
- log,
7506
- execute: runRecoveryExecution,
7507
- excludeOwnTurns: true
7508
- });
7705
+ let output;
7706
+ try {
7707
+ output = recoveryJobSession === null ? await runRecoveryExecution() : await this.runSessionExchange({
7708
+ session: recoveryJobSession,
7709
+ jobId: entry.job_id,
7710
+ userRecord: buildUserRecord(
7711
+ recoveryInputFile?.inlineText ?? entry.input,
7712
+ recoveryInputFile?.filePath !== void 0 ? "attachment" : void 0
7713
+ ),
7714
+ signal: recoveryAbort.signal,
7715
+ log,
7716
+ execute: runRecoveryExecution,
7717
+ excludeOwnTurns: true
7718
+ });
7719
+ } catch (error) {
7720
+ if (!isScriptRefusalError(error)) {
7721
+ throw error;
7722
+ }
7723
+ log(`[${entry.job_id.slice(0, 8)}] Recovery: ${describeForOperator(error)}`);
7724
+ await this.failRecoveredJob(entry, fakeJob, customerSafeMessage(error));
7725
+ return;
7726
+ }
7509
7727
  const { attachments: resultAttachments, deliveredContent } = await this.buildResultAttachment(
7510
7728
  entry.job_id,
7511
7729
  output,
@@ -8731,21 +8949,17 @@ function withPaymentIdentifier(headerValue, declaration, jobId) {
8731
8949
  return headerValue;
8732
8950
  }
8733
8951
  }
8734
- var UNICODE_FORMAT_MARKS = /\p{Cf}/gu;
8735
- function flattenForOperator(text) {
8736
- return sanitizeForTerminal(text).replace(UNICODE_FORMAT_MARKS, "").replace(/\s+/g, " ").trim();
8737
- }
8738
8952
  function printedPrefix(flattened) {
8739
- return flattened.length <= X402_ERROR_EXCERPT_CHARS ? flattened : flattened.slice(0, X402_ERROR_EXCERPT_CHARS).replace(/[\ud800-\udbff]$/, "");
8953
+ return clipToCodeUnits(flattened, X402_ERROR_EXCERPT_CHARS);
8740
8954
  }
8741
8955
  function clipForOperator(flattened) {
8742
8956
  return flattened.length <= X402_ERROR_EXCERPT_CHARS ? flattened : `${printedPrefix(flattened)}...`;
8743
8957
  }
8744
8958
  function quoteUpstream(text) {
8745
- return clipForOperator(flattenForOperator(text));
8959
+ return clipForOperator(flattenForComparison(text));
8746
8960
  }
8747
8961
  function quoteUpstreamWithInput(text, sentInput) {
8748
- return clipForOperator(maskCustomerInput(flattenForOperator(text), inputForms(sentInput)));
8962
+ return clipForOperator(maskCustomerInput(flattenForComparison(text), inputForms(sentInput)));
8749
8963
  }
8750
8964
  function wireForm(sentInput) {
8751
8965
  return new TextDecoder().decode(new TextEncoder().encode(sentInput));
@@ -8767,7 +8981,7 @@ function endsInsideInput(text, forms) {
8767
8981
  }
8768
8982
  function inputForms(sentInput) {
8769
8983
  return [wireForm(sentInput), jsonEscapedForm(sentInput), ...urlEncodedForms(sentInput)].map(
8770
- flattenForOperator
8984
+ flattenForComparison
8771
8985
  );
8772
8986
  }
8773
8987
  function maskCustomerInput(text, forms) {
@@ -8811,7 +9025,7 @@ var X402Driver = class {
8811
9025
  signerPromise = null;
8812
9026
  repriceHintLogged = /* @__PURE__ */ new Set();
8813
9027
  log(message) {
8814
- (this.options.log ?? console.log)(`[x402] ${flattenForOperator(message)}`);
9028
+ (this.options.log ?? console.log)(`[x402] ${flattenForComparison(message)}`);
8815
9029
  }
8816
9030
  getSigner() {
8817
9031
  const secret = this.options.solanaSecretKeyBase58;
@@ -9015,7 +9229,7 @@ var X402Driver = class {
9015
9229
  this.options.errorBodyReadMs ?? X402_ERROR_BODY_READ_MS
9016
9230
  );
9017
9231
  const forms = inputForms(sentInput);
9018
- const quoted = maskCustomerInput(flattenForOperator(body.text), forms);
9232
+ const quoted = maskCustomerInput(flattenForComparison(body.text), forms);
9019
9233
  if (quoted.length === 0) {
9020
9234
  return "";
9021
9235
  }
@@ -9667,7 +9881,7 @@ async function cmdStart(nameArg, options = {}) {
9667
9881
  }
9668
9882
  try {
9669
9883
  await client.policies.deletePolicy(identity2, type);
9670
- console.log(` Removed stale policy: ${sanitizeForTerminal(type)}`);
9884
+ console.log(` Removed stale policy: ${deleteControlCharacters(type)}`);
9671
9885
  } catch {
9672
9886
  }
9673
9887
  }
@@ -9778,7 +9992,7 @@ async function cmdStart(nameArg, options = {}) {
9778
9992
  const card = JSON.parse(ev.content);
9779
9993
  if (card.name && toDTag(card.name) === dTag) {
9780
9994
  await client.discovery.deleteCapability(identity2, card.name);
9781
- console.log(` Removed stale capability: ${sanitizeForTerminal(card.name)}`);
9995
+ console.log(` Removed stale capability: ${deleteControlCharacters(card.name)}`);
9782
9996
  }
9783
9997
  } catch {
9784
9998
  }
@@ -9871,7 +10085,7 @@ async function cmdStart(nameArg, options = {}) {
9871
10085
  ledger,
9872
10086
  {
9873
10087
  onJobReceived: (job) => {
9874
- const cap = sanitizeForTerminal(job.tags.find((t) => t !== "elisym") ?? "unknown");
10088
+ const cap = deleteControlCharacters(job.tags.find((t) => t !== "elisym") ?? "unknown");
9875
10089
  process.stdout.write(` [job] ${job.jobId.slice(0, 16)} | cap=${cap}
9876
10090
  `);
9877
10091
  logger.info({ event: "job_received", jobId: job.jobId, capability: cap });
@@ -9882,7 +10096,7 @@ async function cmdStart(nameArg, options = {}) {
9882
10096
  logger.info({ event: "job_delivered", jobId });
9883
10097
  },
9884
10098
  onJobError: (jobId, error) => {
9885
- const safeError = sanitizeForTerminal(error);
10099
+ const safeError = deleteControlCharacters(error);
9886
10100
  process.stderr.write(` [job] ${jobId.slice(0, 16)} | error: ${safeError}
9887
10101
  `);
9888
10102
  logger.error({ event: "job_error", jobId, error: safeError });