@elisym/cli 0.11.5 → 0.13.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/README.md CHANGED
@@ -127,6 +127,8 @@ my-project/
127
127
 
128
128
  Skills are defined in `SKILL.md` files inside `./skills/<skill-name>/`. Each file has YAML frontmatter (between `---` delimiters) that describes the skill, followed by a markdown body that becomes the LLM system prompt.
129
129
 
130
+ > Canonical reference for every frontmatter field, execution mode, the LLM health-monitor contract, and the script exit-code convention lives in [`SKILLS.md`](./SKILLS.md). The summary below covers the common cases; consult `SKILLS.md` for the full schema.
131
+
130
132
  ```markdown
131
133
  ---
132
134
  name: youtube-summary
package/dist/index.js CHANGED
@@ -2,13 +2,13 @@
2
2
  import { readFileSync, readdirSync, statSync, renameSync, mkdirSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, join, resolve, basename, relative, sep } from 'node:path';
4
4
  import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, ElisymClient, MediaService, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, makeCensor, DEFAULT_REDACT_PATHS, createSlidingWindowLimiter, getProtocolProgramId, getProtocolConfig, LIMITS, calculateProtocolFee, BoundedSet, KIND_JOB_FEEDBACK, NATIVE_SOL } from '@elisym/sdk';
5
- import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
5
+ import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
6
6
  import { isAddress, createSolanaRpc, address } from '@solana/kit';
7
7
  import { generateSecretKey, getPublicKey, nip19, verifyEvent } from 'nostr-tools';
8
8
  import YAML from 'yaml';
9
9
  import { Command } from 'commander';
10
10
  import { createHash } from 'node:crypto';
11
- import { LlmHealthMonitor, startLlmHeartbeat, createFreeLlmLimiterSet, FREE_LLM_GLOBAL_KEY, freeLlmCustomerKey } from '@elisym/sdk/llm-health';
11
+ import { LlmHealthMonitor, startLlmRecovery, createFreeLlmLimiterSet, ScriptBillingExhaustedError, FREE_LLM_GLOBAL_KEY, freeLlmCustomerKey } from '@elisym/sdk/llm-health';
12
12
  import { lookup } from 'node:dns/promises';
13
13
  import { Socket } from 'node:net';
14
14
  import pino from 'pino';
@@ -341,7 +341,7 @@ var init_anthropic = __esm({
341
341
  // src/llm/providers/openai-compatible.ts
342
342
  function createOpenAICompatibleProvider(config) {
343
343
  const billingMarkers = [...DEFAULT_BILLING_MARKERS, ...config.extraBillingMarkers ?? []];
344
- function bodyLooksLikeBilling3(body) {
344
+ function bodyLooksLikeBilling4(body) {
345
345
  const lower = body.toLowerCase();
346
346
  return billingMarkers.some((marker) => lower.includes(marker));
347
347
  }
@@ -423,7 +423,7 @@ function createOpenAICompatibleProvider(config) {
423
423
  if (response.status === 402) {
424
424
  return { ok: false, reason: "billing", status: response.status, body };
425
425
  }
426
- if ((response.status === 400 || response.status === 429) && bodyLooksLikeBilling3(body)) {
426
+ if ((response.status === 400 || response.status === 429) && bodyLooksLikeBilling4(body)) {
427
427
  return { ok: false, reason: "billing", status: response.status, body };
428
428
  }
429
429
  return {
@@ -1163,6 +1163,7 @@ async function cmdInit(nameArg, options = {}) {
1163
1163
  }
1164
1164
  const llmApiKeys = Object.fromEntries(collectedKeys);
1165
1165
  await writeYamlInitial(created.dir, yaml);
1166
+ await writeExampleSkillTemplate(created.dir);
1166
1167
  await writeSecrets(
1167
1168
  created.dir,
1168
1169
  {
@@ -1967,6 +1968,36 @@ function resolveJobAsset(tags, skills) {
1967
1968
  const skill = skills.route(tags);
1968
1969
  return skill?.asset ?? NATIVE_SOL;
1969
1970
  }
1971
+ var BILLING_BODY_MARKERS3 = ["credit balance", "billing", "insufficient", "insufficient_quota"];
1972
+ var AGENT_UNAVAILABLE_MESSAGE = "Agent temporarily unavailable";
1973
+ var AgentUnavailableError = class extends Error {
1974
+ constructor() {
1975
+ super(AGENT_UNAVAILABLE_MESSAGE);
1976
+ this.name = "AgentUnavailableError";
1977
+ }
1978
+ };
1979
+ function bodyLooksLikeBilling3(body) {
1980
+ const lower = body.toLowerCase();
1981
+ return BILLING_BODY_MARKERS3.some((marker) => lower.includes(marker));
1982
+ }
1983
+ function resolveHealthPair(skill) {
1984
+ if (!skill) {
1985
+ return null;
1986
+ }
1987
+ if (skill.mode === "llm" && skill.resolvedTriple) {
1988
+ return {
1989
+ provider: skill.resolvedTriple.provider,
1990
+ model: skill.resolvedTriple.model
1991
+ };
1992
+ }
1993
+ if (skill.mode !== "llm" && skill.llmOverride?.provider && skill.llmOverride?.model) {
1994
+ return {
1995
+ provider: skill.llmOverride.provider,
1996
+ model: skill.llmOverride.model
1997
+ };
1998
+ }
1999
+ return null;
2000
+ }
1970
2001
  var RATE_LIMIT_WINDOW_MS = 10 * 60 * 1e3;
1971
2002
  var MAX_JOBS_PER_CUSTOMER = 20;
1972
2003
  var GLOBAL_MAX_JOBS_PER_WINDOW = 200;
@@ -2012,6 +2043,78 @@ var AgentRuntime = class {
2012
2043
  * the operator's API key.
2013
2044
  */
2014
2045
  freeLlmLimiters = createFreeLlmLimiterSet();
2046
+ /**
2047
+ * Inspect an error thrown by `skill.execute()` and, when it carries a
2048
+ * billing/invalid signal from the LLM provider, flip the matching
2049
+ * (provider, model) pair to unhealthy via the health monitor. The next
2050
+ * job hitting the same pair is then refused at the preflight gate
2051
+ * before payment, so customers don't keep paying for jobs that will
2052
+ * fail. Recovery happens through the lazy recovery loop.
2053
+ *
2054
+ * Two error shapes are recognized:
2055
+ *
2056
+ * - `ScriptBillingExhaustedError` - thrown by SDK script skills when
2057
+ * the spawned process exits with `SCRIPT_EXIT_BILLING_EXHAUSTED`.
2058
+ * Pair comes from `skill.llmOverride` (operator-declared).
2059
+ *
2060
+ * - LLM provider HTTP error from `mode: 'llm'` - bare `Error` whose
2061
+ * message starts with "<Provider> API error: <status> <body>" (the
2062
+ * format every CLI provider currently uses). We classify on status:
2063
+ * 402 / 401 / 403 -> mark unhealthy. Body markers (`credit
2064
+ * balance`, `billing`, `insufficient`) catch the 400-on-billing case
2065
+ * Anthropic returns and the 429+`insufficient_quota` case OpenAI
2066
+ * and the openai-compatible providers (xAI/Google/DeepSeek) return.
2067
+ * Pair comes from `skill.resolvedTriple`.
2068
+ *
2069
+ * Anything else is a transient/skill error and does NOT touch health
2070
+ * state - the recovery loop should not be poisoned by skill bugs.
2071
+ */
2072
+ markHealthFromExecuteError(skill, err, log, jobId) {
2073
+ if (!this.healthMonitor) {
2074
+ return false;
2075
+ }
2076
+ const tag = `[${jobId.slice(0, 8)}]`;
2077
+ if (err instanceof ScriptBillingExhaustedError) {
2078
+ const provider = skill.llmOverride?.provider;
2079
+ const model = skill.llmOverride?.model;
2080
+ if (!provider || !model) {
2081
+ log(
2082
+ `${tag} Script returned exit ${err.exitCode} (billing-exhausted) but skill "${skill.name}" did not declare provider/model in SKILL.md - cannot gate future jobs.`
2083
+ );
2084
+ return false;
2085
+ }
2086
+ log(
2087
+ `${tag} Script signaled billing-exhausted (exit ${err.exitCode}). Marking ${provider}/${model} unhealthy; future jobs against this pair will be refused until recovery probe succeeds.`
2088
+ );
2089
+ this.healthMonitor.markUnhealthyFromJob(provider, model, "billing", err.message);
2090
+ return true;
2091
+ }
2092
+ if (skill.mode === "llm" && skill.resolvedTriple) {
2093
+ const message = err instanceof Error ? err.message : String(err);
2094
+ const match = /API error:\s*(\d{3})\b\s*(.*)/i.exec(message);
2095
+ if (!match) {
2096
+ return false;
2097
+ }
2098
+ const status = Number(match[1]);
2099
+ const body = (match[2] ?? "").slice(0, 200);
2100
+ const isBillingStatus = status === 402;
2101
+ const isAuthStatus = status === 401 || status === 403;
2102
+ const isBilling400 = status === 400 && bodyLooksLikeBilling3(body);
2103
+ const isBilling429 = status === 429 && bodyLooksLikeBilling3(body);
2104
+ if (!isBillingStatus && !isAuthStatus && !isBilling400 && !isBilling429) {
2105
+ return false;
2106
+ }
2107
+ const reason = isAuthStatus ? "invalid" : "billing";
2108
+ const provider = skill.resolvedTriple.provider;
2109
+ const model = skill.resolvedTriple.model;
2110
+ log(
2111
+ `${tag} LLM provider returned HTTP ${status} (${reason}). Marking ${provider}/${model} unhealthy; future jobs against this pair will be refused until recovery probe succeeds.`
2112
+ );
2113
+ this.healthMonitor.markUnhealthyFromJob(provider, model, reason, body);
2114
+ return true;
2115
+ }
2116
+ return false;
2117
+ }
2015
2118
  /** Fetch on-chain protocol config (fee, treasury). Always fetches fresh to avoid stale treasury. */
2016
2119
  async fetchProtocolConfig() {
2017
2120
  if (this.config.network !== "devnet") {
@@ -2177,7 +2280,14 @@ var AgentRuntime = class {
2177
2280
  this.ledger.markFailed(job.jobId);
2178
2281
  }
2179
2282
  this.callbacks.onJobError?.(job.jobId, e.message);
2180
- const safeMessage = e.message?.includes("API") ? "Internal processing error" : e.message ?? "Unknown error";
2283
+ let safeMessage;
2284
+ if (e instanceof AgentUnavailableError) {
2285
+ safeMessage = e.message;
2286
+ } else if (e.message?.includes("API")) {
2287
+ safeMessage = "Internal processing error";
2288
+ } else {
2289
+ safeMessage = e.message ?? "Unknown error";
2290
+ }
2181
2291
  await this.transport.sendFeedback(job, { type: "error", message: safeMessage }).catch(() => {
2182
2292
  });
2183
2293
  } finally {
@@ -2192,18 +2302,16 @@ var AgentRuntime = class {
2192
2302
  throw new Error(`Input too long: ${job.input.length} chars (max ${LIMITS.MAX_INPUT_LENGTH})`);
2193
2303
  }
2194
2304
  const matched = this.skills.route(job.tags);
2195
- if (this.healthMonitor && matched && matched.mode === "llm" && matched.resolvedTriple) {
2305
+ const healthPair = resolveHealthPair(matched);
2306
+ if (this.healthMonitor && healthPair) {
2196
2307
  try {
2197
- await this.healthMonitor.assertReady(
2198
- matched.resolvedTriple.provider,
2199
- matched.resolvedTriple.model
2200
- );
2308
+ await this.healthMonitor.assertReady(healthPair.provider, healthPair.model);
2201
2309
  } catch (err) {
2202
2310
  const detail = err instanceof Error ? err.message : String(err);
2203
2311
  log(`[${job.jobId.slice(0, 8)}] LLM health gate refused job: ${detail}`);
2204
2312
  await this.transport.sendFeedback(job, {
2205
2313
  type: "error",
2206
- message: "Service temporarily unavailable, try again later"
2314
+ message: AGENT_UNAVAILABLE_MESSAGE
2207
2315
  }).catch(() => {
2208
2316
  });
2209
2317
  return;
@@ -2244,15 +2352,24 @@ var AgentRuntime = class {
2244
2352
  throw new Error("No skill matched for tags: " + job.tags.join(", "));
2245
2353
  }
2246
2354
  log(`[${job.jobId.slice(0, 8)}] Executing skill: ${skill.name}`);
2247
- const output = await skill.execute(
2248
- {
2249
- data: job.input,
2250
- inputType: job.inputType,
2251
- tags: job.tags,
2252
- jobId: job.jobId
2253
- },
2254
- { ...this.skillCtx, signal }
2255
- );
2355
+ let output;
2356
+ try {
2357
+ output = await skill.execute(
2358
+ {
2359
+ data: job.input,
2360
+ inputType: job.inputType,
2361
+ tags: job.tags,
2362
+ jobId: job.jobId
2363
+ },
2364
+ { ...this.skillCtx, signal }
2365
+ );
2366
+ } catch (err) {
2367
+ const flippedToUnhealthy = this.markHealthFromExecuteError(skill, err, log, job.jobId);
2368
+ if (flippedToUnhealthy) {
2369
+ throw new AgentUnavailableError();
2370
+ }
2371
+ throw err;
2372
+ }
2256
2373
  this.ledger.markExecuted(job.jobId, output.data);
2257
2374
  log(`[${job.jobId.slice(0, 8)}] Skill completed, delivering result`);
2258
2375
  const eventId = await this.transport.deliverResult(job, output.data, netAmount);
@@ -2638,6 +2755,7 @@ var StaticFileSkill = class {
2638
2755
  image;
2639
2756
  imageFile;
2640
2757
  dir;
2758
+ llmOverride;
2641
2759
  inner;
2642
2760
  constructor(params) {
2643
2761
  this.name = params.name;
@@ -2648,6 +2766,7 @@ var StaticFileSkill = class {
2648
2766
  this.image = params.image;
2649
2767
  this.imageFile = params.imageFile;
2650
2768
  this.dir = params.dir;
2769
+ this.llmOverride = params.llmOverride;
2651
2770
  this.inner = new StaticFileSkill$1({
2652
2771
  name: params.name,
2653
2772
  description: params.description,
@@ -2673,6 +2792,7 @@ var StaticScriptSkill = class {
2673
2792
  image;
2674
2793
  imageFile;
2675
2794
  dir;
2795
+ llmOverride;
2676
2796
  inner;
2677
2797
  constructor(params) {
2678
2798
  this.name = params.name;
@@ -2683,6 +2803,7 @@ var StaticScriptSkill = class {
2683
2803
  this.image = params.image;
2684
2804
  this.imageFile = params.imageFile;
2685
2805
  this.dir = params.dir;
2806
+ this.llmOverride = params.llmOverride;
2686
2807
  this.inner = new StaticScriptSkill$1({
2687
2808
  name: params.name,
2688
2809
  description: params.description,
@@ -2711,6 +2832,7 @@ var DynamicScriptSkill = class {
2711
2832
  image;
2712
2833
  imageFile;
2713
2834
  dir;
2835
+ llmOverride;
2714
2836
  inner;
2715
2837
  constructor(params) {
2716
2838
  this.name = params.name;
@@ -2721,6 +2843,7 @@ var DynamicScriptSkill = class {
2721
2843
  this.image = params.image;
2722
2844
  this.imageFile = params.imageFile;
2723
2845
  this.dir = params.dir;
2846
+ this.llmOverride = params.llmOverride;
2724
2847
  this.inner = new DynamicScriptSkill$1({
2725
2848
  name: params.name,
2726
2849
  description: params.description,
@@ -2846,7 +2969,8 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
2846
2969
  outputFilePath,
2847
2970
  image: parsed.image,
2848
2971
  imageFile: parsed.imageFile,
2849
- dir: entryPath
2972
+ dir: entryPath,
2973
+ llmOverride: parsed.llmOverride
2850
2974
  });
2851
2975
  break;
2852
2976
  }
@@ -2874,7 +2998,8 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
2874
2998
  scriptEnv,
2875
2999
  image: parsed.image,
2876
3000
  imageFile: parsed.imageFile,
2877
- dir: entryPath
3001
+ dir: entryPath,
3002
+ llmOverride: parsed.llmOverride
2878
3003
  });
2879
3004
  break;
2880
3005
  }
@@ -3292,12 +3417,16 @@ async function cmdStart(nameArg, options = {}) {
3292
3417
  process.exit(1);
3293
3418
  }
3294
3419
  const llmSkills = allSkills.filter((skill) => skill.mode === "llm");
3420
+ const scriptDepSkills = allSkills.filter(
3421
+ (skill) => skill.mode !== "llm" && skill.llmOverride?.provider && skill.llmOverride?.model
3422
+ );
3423
+ const monitorPairs = /* @__PURE__ */ new Map();
3295
3424
  const triplesByKey = /* @__PURE__ */ new Map();
3296
3425
  const dependentSkillsByProvider = /* @__PURE__ */ new Map();
3297
3426
  let agentDefaultCacheKey;
3298
3427
  const llmClientCache = /* @__PURE__ */ new Map();
3299
3428
  const healthMonitor = new LlmHealthMonitor();
3300
- if (llmSkills.length > 0) {
3429
+ if (llmSkills.length > 0 || scriptDepSkills.length > 0) {
3301
3430
  const resolutionErrors = [];
3302
3431
  for (const skill of llmSkills) {
3303
3432
  const skillMdPath = join(skillsDir, skill.name, "SKILL.md");
@@ -3311,6 +3440,10 @@ async function cmdStart(nameArg, options = {}) {
3311
3440
  }
3312
3441
  const cacheKey = cacheKeyFor(result);
3313
3442
  triplesByKey.set(cacheKey, result);
3443
+ monitorPairs.set(`${result.provider}::${result.model}`, {
3444
+ provider: result.provider,
3445
+ model: result.model
3446
+ });
3314
3447
  skill.resolvedTriple = {
3315
3448
  provider: result.provider,
3316
3449
  model: result.model,
@@ -3320,6 +3453,16 @@ async function cmdStart(nameArg, options = {}) {
3320
3453
  list.push(skill.name);
3321
3454
  dependentSkillsByProvider.set(result.provider, list);
3322
3455
  }
3456
+ for (const skill of scriptDepSkills) {
3457
+ const provider = skill.llmOverride?.provider;
3458
+ const model = skill.llmOverride?.model;
3459
+ monitorPairs.set(`${provider}::${model}`, { provider, model });
3460
+ const list = dependentSkillsByProvider.get(provider) ?? [];
3461
+ if (!list.includes(skill.name)) {
3462
+ list.push(skill.name);
3463
+ }
3464
+ dependentSkillsByProvider.set(provider, list);
3465
+ }
3323
3466
  if (resolutionErrors.length > 0) {
3324
3467
  for (const message of resolutionErrors) {
3325
3468
  console.error(` ! ${message}`);
@@ -3359,27 +3502,27 @@ async function cmdStart(nameArg, options = {}) {
3359
3502
  console.error("");
3360
3503
  process.exit(1);
3361
3504
  }
3362
- for (const [, triple] of triplesByKey) {
3363
- const apiKey = keyByProvider.get(triple.provider);
3505
+ for (const [, pair] of monitorPairs) {
3506
+ const apiKey = keyByProvider.get(pair.provider);
3364
3507
  if (!apiKey) {
3365
3508
  continue;
3366
3509
  }
3367
- const envVar = getLlmProvider(triple.provider)?.envVar ?? `${triple.provider.toUpperCase()}_API_KEY`;
3368
- process.stdout.write(` Verifying ${triple.provider} ${triple.model}... `);
3369
- const verification = await verifyLlmApiKeyDeep(triple.provider, apiKey, triple.model);
3370
- const descriptor = getLlmProvider(triple.provider);
3371
- const verifyFn = async (signal) => descriptor ? descriptor.verifyKeyDeep(apiKey, triple.model, signal) : { ok: false, reason: "unavailable", error: "no descriptor" };
3510
+ const envVar = getLlmProvider(pair.provider)?.envVar ?? `${pair.provider.toUpperCase()}_API_KEY`;
3511
+ process.stdout.write(` Verifying ${pair.provider} ${pair.model}... `);
3512
+ const verification = await verifyLlmApiKeyDeep(pair.provider, apiKey, pair.model);
3513
+ const descriptor = getLlmProvider(pair.provider);
3514
+ const verifyFn = async (signal) => descriptor ? descriptor.verifyKeyDeep(apiKey, pair.model, signal) : { ok: false, reason: "unavailable", error: "no descriptor" };
3372
3515
  healthMonitor.register({
3373
- provider: triple.provider,
3374
- model: triple.model,
3516
+ provider: pair.provider,
3517
+ model: pair.model,
3375
3518
  verifyFn
3376
3519
  });
3377
- healthMonitor.seed(triple.provider, triple.model, verification);
3520
+ healthMonitor.seed(pair.provider, pair.model, verification);
3378
3521
  if (verification.ok) {
3379
3522
  console.log("ok");
3380
3523
  } else if (verification.reason === "invalid") {
3381
3524
  console.log("INVALID");
3382
- console.error(` ! ${triple.provider} rejected the API key (HTTP ${verification.status}).`);
3525
+ console.error(` ! ${pair.provider} rejected the API key (HTTP ${verification.status}).`);
3383
3526
  console.error(
3384
3527
  ` Update it via \`npx @elisym/cli profile ${agentName}\` or set ${envVar} to a valid key.
3385
3528
  `
@@ -3389,7 +3532,7 @@ async function cmdStart(nameArg, options = {}) {
3389
3532
  console.log("BILLING");
3390
3533
  const detail = verification.body ? ` ${verification.body.slice(0, 200)}` : "";
3391
3534
  console.error(
3392
- ` ! ${triple.provider} reports a billing/quota issue for ${triple.model}.${detail}`
3535
+ ` ! ${pair.provider} reports a billing/quota issue for ${pair.model}.${detail}`
3393
3536
  );
3394
3537
  console.error(
3395
3538
  ` Top up the account at the provider console, or set ${envVar} to a key on a funded org.
@@ -3399,7 +3542,7 @@ async function cmdStart(nameArg, options = {}) {
3399
3542
  } else {
3400
3543
  console.log("unavailable");
3401
3544
  console.warn(
3402
- ` ! Could not verify ${triple.provider} ${triple.model} (${verification.error}). Continuing - jobs will fail if the key is invalid.
3545
+ ` ! Could not verify ${pair.provider} ${pair.model} (${verification.error}). Continuing - jobs will fail if the key is invalid.
3403
3546
  `
3404
3547
  );
3405
3548
  }
@@ -3421,7 +3564,7 @@ async function cmdStart(nameArg, options = {}) {
3421
3564
  llmClientCache.set(cacheKey, createLlmClient(config));
3422
3565
  }
3423
3566
  } else {
3424
- console.log(" No LLM skills loaded; skipping LLM key check.\n");
3567
+ console.log(" No LLM-dependent skills loaded; skipping LLM key check.\n");
3425
3568
  }
3426
3569
  const agentDefaultClient = agentDefaultCacheKey !== void 0 ? llmClientCache.get(agentDefaultCacheKey) : void 0;
3427
3570
  const getLlm = (override) => {
@@ -3618,12 +3761,12 @@ async function cmdStart(nameArg, options = {}) {
3618
3761
  logger
3619
3762
  });
3620
3763
  let llmHeartbeat;
3621
- if (llmSkills.length > 0) {
3622
- llmHeartbeat = startLlmHeartbeat({
3764
+ if (monitorPairs.size > 0) {
3765
+ llmHeartbeat = startLlmRecovery({
3623
3766
  monitor: healthMonitor,
3624
3767
  log: diagLog
3625
3768
  });
3626
- diagLog("LLM health monitor armed (10min TTL, 10min heartbeat).");
3769
+ diagLog("LLM health monitor armed (lazy recovery, 5min interval).");
3627
3770
  }
3628
3771
  const runtime = new AgentRuntime(
3629
3772
  transport,