@senpi/runtime 3.0.162 → 3.0.168

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.
Files changed (96) hide show
  1. package/dist/deploy/orchestrator.d.ts +14 -0
  2. package/dist/deploy/orchestrator.d.ts.map +1 -1
  3. package/dist/deploy/orchestrator.js +33 -1
  4. package/dist/deploy/orchestrator.js.map +1 -1
  5. package/dist/deploy/register.d.ts +7 -0
  6. package/dist/deploy/register.d.ts.map +1 -1
  7. package/dist/deploy/register.js +1 -0
  8. package/dist/deploy/register.js.map +1 -1
  9. package/dist/dsl/events/handlers.d.ts.map +1 -1
  10. package/dist/dsl/events/handlers.js +4 -0
  11. package/dist/dsl/events/handlers.js.map +1 -1
  12. package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
  13. package/dist/dsl/monitor/process-one-position.js +6 -2
  14. package/dist/dsl/monitor/process-one-position.js.map +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +114 -14
  17. package/dist/index.js.map +1 -1
  18. package/dist/runtime/boot-skip.d.ts +3 -0
  19. package/dist/runtime/boot-skip.d.ts.map +1 -1
  20. package/dist/runtime/boot-skip.js +7 -3
  21. package/dist/runtime/boot-skip.js.map +1 -1
  22. package/dist/runtime/lifecycle-events.d.ts +6 -0
  23. package/dist/runtime/lifecycle-events.d.ts.map +1 -1
  24. package/dist/runtime/lifecycle-events.js +1 -0
  25. package/dist/runtime/lifecycle-events.js.map +1 -1
  26. package/dist/runtime/reconcile-state.d.ts.map +1 -1
  27. package/dist/runtime/reconcile-state.js +4 -0
  28. package/dist/runtime/reconcile-state.js.map +1 -1
  29. package/dist/runtime/run.d.ts +2 -0
  30. package/dist/runtime/run.d.ts.map +1 -1
  31. package/dist/runtime/run.js +1 -0
  32. package/dist/runtime/run.js.map +1 -1
  33. package/dist/runtime/runtime-schema.d.ts +100 -100
  34. package/dist/runtime/runtime.d.ts +8 -3
  35. package/dist/runtime/runtime.d.ts.map +1 -1
  36. package/dist/runtime/runtime.js +14 -6
  37. package/dist/runtime/runtime.js.map +1 -1
  38. package/dist/scanners/engine/engine.d.ts +2 -0
  39. package/dist/scanners/engine/engine.d.ts.map +1 -1
  40. package/dist/scanners/engine/engine.js +6 -0
  41. package/dist/scanners/engine/engine.js.map +1 -1
  42. package/dist/scanners/runtime-module.d.ts +2 -0
  43. package/dist/scanners/runtime-module.d.ts.map +1 -1
  44. package/dist/scanners/runtime-module.js +3 -0
  45. package/dist/scanners/runtime-module.js.map +1 -1
  46. package/dist/senpi/client.d.ts +9 -1
  47. package/dist/senpi/client.d.ts.map +1 -1
  48. package/dist/senpi/client.js +25 -0
  49. package/dist/senpi/client.js.map +1 -1
  50. package/dist/senpi/types.d.ts +19 -0
  51. package/dist/senpi/types.d.ts.map +1 -1
  52. package/dist/senpi/types.js +0 -1
  53. package/dist/senpi/types.js.map +1 -1
  54. package/dist/skills-manager/bootstrap.d.ts +5 -2
  55. package/dist/skills-manager/bootstrap.d.ts.map +1 -1
  56. package/dist/skills-manager/bootstrap.js +24 -4
  57. package/dist/skills-manager/bootstrap.js.map +1 -1
  58. package/dist/skills-manager/coordinator.d.ts +32 -3
  59. package/dist/skills-manager/coordinator.d.ts.map +1 -1
  60. package/dist/skills-manager/coordinator.js +317 -29
  61. package/dist/skills-manager/coordinator.js.map +1 -1
  62. package/dist/skills-manager/install-error.d.ts +34 -0
  63. package/dist/skills-manager/install-error.d.ts.map +1 -0
  64. package/dist/skills-manager/install-error.js +31 -0
  65. package/dist/skills-manager/install-error.js.map +1 -0
  66. package/dist/skills-manager/types.d.ts +55 -7
  67. package/dist/skills-manager/types.d.ts.map +1 -1
  68. package/dist/skills-manager/utils.d.ts +8 -0
  69. package/dist/skills-manager/utils.d.ts.map +1 -1
  70. package/dist/skills-manager/utils.js +15 -0
  71. package/dist/skills-manager/utils.js.map +1 -1
  72. package/dist/skills-manager/writer.d.ts.map +1 -1
  73. package/dist/skills-manager/writer.js +35 -14
  74. package/dist/skills-manager/writer.js.map +1 -1
  75. package/dist/strategy/__tests__/fixtures/hawk-facts.d.ts +4 -0
  76. package/dist/strategy/__tests__/fixtures/hawk-facts.d.ts.map +1 -0
  77. package/dist/strategy/__tests__/fixtures/hawk-facts.js +29 -0
  78. package/dist/strategy/__tests__/fixtures/hawk-facts.js.map +1 -0
  79. package/dist/strategy/facts-sync.d.ts +79 -0
  80. package/dist/strategy/facts-sync.d.ts.map +1 -0
  81. package/dist/strategy/facts-sync.js +184 -0
  82. package/dist/strategy/facts-sync.js.map +1 -0
  83. package/dist/strategy/facts.d.ts +79 -0
  84. package/dist/strategy/facts.d.ts.map +1 -0
  85. package/dist/strategy/facts.js +184 -0
  86. package/dist/strategy/facts.js.map +1 -0
  87. package/dist/strategy/index.d.ts +1 -0
  88. package/dist/strategy/index.d.ts.map +1 -1
  89. package/dist/strategy/index.js +1 -0
  90. package/dist/strategy/index.js.map +1 -1
  91. package/dist/utils/event-catalog.d.ts +143 -0
  92. package/dist/utils/event-catalog.d.ts.map +1 -1
  93. package/dist/utils/event-catalog.js +12 -0
  94. package/dist/utils/event-catalog.js.map +1 -1
  95. package/openclaw.plugin.json +1 -1
  96. package/package.json +1 -1
@@ -138,9 +138,10 @@ export declare class SkillsUpdateCoordinator {
138
138
  * Per-skill errors are recorded in state without aborting the others;
139
139
  * outcomes are logged (no notification).
140
140
  *
141
- * If the clone itself fails we log and return without touching state — a
142
- * full-clone failure is almost always a transient network/git issue that
143
- * affects every skill equally. The next tick retries.
141
+ * A failed clone comes back from `withClonedGroups` as a per-skill `clone`
142
+ * outcome and is recorded like any other install failure. Only a throw from
143
+ * `cloneAndInstallMany` itself (the injected `cloneAndInstall` in tests)
144
+ * returns without touching state. The next tick retries either way.
144
145
  */
145
146
  private applyUpdates;
146
147
  /**
@@ -248,6 +249,34 @@ export declare class SkillsUpdateCoordinator {
248
249
  * state rather than stale pre-action data.
249
250
  */
250
251
  private buildSnapshot;
252
+ /**
253
+ * Skills whose failed install could not put the previous install back (`rolled_back=false`), so
254
+ * the skill dir is gone. Filled by `emitInstallFailed`, read and cleared by the next
255
+ * `emitCheckCompleted`. Runs are serialized on `operationChain`, so one run never sees another's.
256
+ */
257
+ private readonly skillDirsLost;
258
+ /**
259
+ * What the last check run in this process saw on disk, per skill (`null` = nothing there). A run
260
+ * that checks only some skills (`updateSkill`) reports the rest from here, not from persisted
261
+ * state, which keeps the last non-null version.
262
+ */
263
+ private readonly lastOnDisk;
264
+ /**
265
+ * Emit a catalogued `skills.*` event, guarded so a throwing sink or a bad attribute shape can
266
+ * never disturb a tick, a manual update or bootstrap.
267
+ */
268
+ private emitEvent;
269
+ /** `skills.check_completed`: one summary per check run, with the whole managed set's versions. */
270
+ private emitCheckCompleted;
271
+ /** `skills.installed` or `skills.install_failed` for one processed install outcome. */
272
+ private emitInstallOutcome;
273
+ private emitInstalled;
274
+ /** `skills.install_failed`. The original thrown value rides the `error` slot, stack and all. */
275
+ private emitInstallFailed;
276
+ /** `skills.bootstrap_completed` plus one `skills.installed` / `skills.install_failed` per attempt. */
277
+ private emitBootstrapEvents;
278
+ /** Source for event attrs only; a resolve failure drops the source attrs instead of throwing. */
279
+ private resolveSourceForTelemetry;
251
280
  private persistState;
252
281
  }
253
282
  //# sourceMappingURL=coordinator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"coordinator.d.ts","sourceRoot":"","sources":["../../src/skills-manager/coordinator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAUH,OAAO,KAAK,EAMV,qBAAqB,EAGrB,8BAA8B,EAC/B,MAAM,YAAY,CAAC;AAGpB,qBAAa,iCAAkC,SAAQ,KAAK;gBAC9C,OAAO,EAAE,MAAM;CAI5B;AAkCD,qBAAa,uBAAuB;IAatB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAZpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0B;IAChD,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,KAAK,CAAyD;IACtE,OAAO,CAAC,QAAQ,CAAwB;IACxC;;;OAGG;IACH,OAAO,CAAC,cAAc,CAAoC;gBAE7B,OAAO,EAAE,8BAA8B;IAKpE;;;;;;;;;OASG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA4B5B;;;;;;;;;;OAUG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAW3B;;;;;;;;;;;;;;;;;;;OAmBG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBlC,iEAAiE;IACjE,WAAW,IAAI,qBAAqB;IAMpC;;;;;;;;;;OAUG;IACG,WAAW,CACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAkGxD;;;;;;;;;OASG;IACG,SAAS,IAAI,OAAO,CACxB,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAClF;IA0ID;;;OAGG;YACW,aAAa;IAwB3B;;;;;OAKG;IACH,OAAO,CAAC,4BAA4B;IAqBpC;;;OAGG;YACW,OAAO;IAuGrB;;;;;;;;OAQG;YACW,YAAY;IAmC1B;;;;;;;;;;;OAWG;YACW,mBAAmB;IAwFjC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,aAAa;IAOrB;;;;;;;OAOG;IACH,OAAO,CAAC,kBAAkB;IAU1B;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAmB7B;;;;;;;;OAQG;YACW,WAAW;IAmDzB;;;;;;;;OAQG;IACH,OAAO,CAAC,cAAc;IAatB;;;;;OAKG;IACH,OAAO,CAAC,aAAa;IAIrB;;;;;;;;;OASG;IACH,OAAO,CAAC,iBAAiB;IAIzB;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,aAAa;YA4CP,YAAY;CAS3B"}
1
+ {"version":3,"file":"coordinator.d.ts","sourceRoot":"","sources":["../../src/skills-manager/coordinator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAUH,OAAO,KAAK,EAQV,qBAAqB,EAIrB,8BAA8B,EAC/B,MAAM,YAAY,CAAC;AA6CpB,qBAAa,iCAAkC,SAAQ,KAAK;gBAC9C,OAAO,EAAE,MAAM;CAI5B;AAkCD,qBAAa,uBAAuB;IAatB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAZpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0B;IAChD,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,KAAK,CAAyD;IACtE,OAAO,CAAC,QAAQ,CAAwB;IACxC;;;OAGG;IACH,OAAO,CAAC,cAAc,CAAoC;gBAE7B,OAAO,EAAE,8BAA8B;IAKpE;;;;;;;;;OASG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAoC5B;;;;;;;;;;OAUG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAW3B;;;;;;;;;;;;;;;;;;;OAmBG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IA0BlC,iEAAiE;IACjE,WAAW,IAAI,qBAAqB;IAMpC;;;;;;;;;;OAUG;IACG,WAAW,CACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAiHxD;;;;;;;;;OASG;IACG,SAAS,IAAI,OAAO,CACxB,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAClF;IAyJD;;;OAGG;YACW,aAAa;IAwB3B;;;;;OAKG;IACH,OAAO,CAAC,4BAA4B;IAqBpC;;;OAGG;YACW,OAAO;IAgHrB;;;;;;;;;OASG;YACW,YAAY;IA+C1B;;;;;;;;;;;OAWG;YACW,mBAAmB;IAmGjC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,aAAa;IAOrB;;;;;;;OAOG;IACH,OAAO,CAAC,kBAAkB;IAU1B;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IA8B7B;;;;;;;;OAQG;YACW,WAAW;IAsDzB;;;;;;;;OAQG;IACH,OAAO,CAAC,cAAc;IAatB;;;;;OAKG;IACH,OAAO,CAAC,aAAa;IAIrB;;;;;;;;;OASG;IACH,OAAO,CAAC,iBAAiB;IAIzB;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,aAAa;IA+CrB;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IAEnD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAoC;IAE/D;;;OAGG;IACH,OAAO,CAAC,SAAS;IAQjB,kGAAkG;IAClG,OAAO,CAAC,kBAAkB;IA4E1B,uFAAuF;IACvF,OAAO,CAAC,kBAAkB;IAiB1B,OAAO,CAAC,aAAa;IAoBrB,gGAAgG;IAChG,OAAO,CAAC,iBAAiB;IA6BzB,sGAAsG;IACtG,OAAO,CAAC,mBAAmB;IAwB3B,iGAAiG;IACjG,OAAO,CAAC,yBAAyB;YAQnB,YAAY;CAS3B"}
@@ -29,7 +29,21 @@ import { parseSemver } from "../runtime/auto-update/policy.js";
29
29
  import { SkillsManagerStateStore } from "./state-store.js";
30
30
  import { checkSkillVersion, normalizeVersion } from "./version-manager.js";
31
31
  import { installSkill } from "./writer.js";
32
- import { createDefaultSkillState, createDefaultSkillsManagerState, formatErr } from "./utils.js";
32
+ import { createDefaultSkillState, createDefaultSkillsManagerState, formatErr, formatSkillVersions, joinSkillNames, } from "./utils.js";
33
+ import { classifyInstallFailure, installRolledBack } from "./install-error.js";
34
+ import { EVENT_NAME, eventAttrs } from "../utils/event-catalog.js";
35
+ function emptyCheckRunSummary() {
36
+ return {
37
+ checked: 0,
38
+ upToDate: 0,
39
+ toInstall: [],
40
+ majorPending: [],
41
+ checkFailed: [],
42
+ installed: [],
43
+ installFailed: [],
44
+ onDisk: new Map(),
45
+ };
46
+ }
33
47
  export class SkillsCoordinatorUnavailableError extends Error {
34
48
  constructor(message) {
35
49
  super(message);
@@ -109,10 +123,19 @@ export class SkillsUpdateCoordinator {
109
123
  this.state = await this.store.load();
110
124
  const bootstrapFn = this.options.deps?.bootstrapSkillsIfNeeded ?? bootstrapSkillsIfNeeded;
111
125
  try {
112
- await bootstrapFn(config, logger);
126
+ const result = await bootstrapFn(config, logger);
127
+ if (result)
128
+ this.emitBootstrapEvents(result);
113
129
  }
114
130
  catch (err) {
115
131
  logger.warn("[senpi-skills-manager] bootstrap error (non-fatal)", err);
132
+ this.emitEvent(EVENT_NAME.skillsBootstrapCompleted, "warn", {
133
+ body: "Skills bootstrap errored before finishing",
134
+ attributes: eventAttrs({
135
+ "senpi.skills.errored": true,
136
+ }),
137
+ error: err,
138
+ });
116
139
  }
117
140
  this.snapshot = this.buildSnapshot("healthy", "started");
118
141
  }
@@ -172,6 +195,13 @@ export class SkillsUpdateCoordinator {
172
195
  }
173
196
  catch (err) {
174
197
  this.options.logger.warn("[senpi-skills-manager] tick error", err);
198
+ this.emitEvent(EVENT_NAME.skillsDegraded, "warn", {
199
+ body: "Skills tick failed",
200
+ attributes: eventAttrs({
201
+ "senpi.skills.trigger": "tick",
202
+ }),
203
+ error: err,
204
+ });
175
205
  this.snapshot = this.buildSnapshot("degraded", "tick-error", formatErr(err));
176
206
  // No internal backoff — the next external tick (from auto-update's
177
207
  // own poll loop / backoff) will retry on its own cadence.
@@ -217,6 +247,7 @@ export class SkillsUpdateCoordinator {
217
247
  logger.warn(`[senpi-skills-manager] updateSkill — version check failed | skill=${skillName} err=${errorMessage}`);
218
248
  this.recordCheckFailure(skillName, errorMessage);
219
249
  await this.persistState();
250
+ this.emitCheckCompleted("update_skill", { ...emptyCheckRunSummary(), checked: 1, checkFailed: [skillName] });
220
251
  this.snapshot = this.buildSnapshot(this.snapshot.health, "manual-update-failed", errorMessage);
221
252
  throw err;
222
253
  }
@@ -249,13 +280,26 @@ export class SkillsUpdateCoordinator {
249
280
  // Persist so the `lastCheckedAt` timestamp survives a gateway restart
250
281
  // even on skip paths (otherwise the next load() would see a stale null).
251
282
  await this.persistState();
283
+ this.emitCheckCompleted("update_skill", {
284
+ ...emptyCheckRunSummary(),
285
+ checked: 1,
286
+ upToDate: 1,
287
+ onDisk: new Map([[skillName, versionInfo.localVersion]]),
288
+ });
252
289
  // Refresh snapshot so the status RPC doesn't return pre-check data.
253
290
  this.snapshot = this.buildSnapshot(this.snapshot.health, "manual-update-skipped");
254
291
  return { updated: false, version: versionInfo.localVersion };
255
292
  }
293
+ const runSummary = {
294
+ ...emptyCheckRunSummary(),
295
+ checked: 1,
296
+ toInstall: [skillName],
297
+ onDisk: new Map([[skillName, versionInfo.localVersion]]),
298
+ };
256
299
  try {
257
- const installedVersion = await this.applyUpdate(skillName, versionInfo.remoteVersion);
300
+ const installedVersion = await this.applyUpdate(skillName, versionInfo);
258
301
  logger.info(`[senpi-skills-manager] updateSkill complete | skill=${skillName} version=${installedVersion ?? "unknown"}`);
302
+ this.emitCheckCompleted("update_skill", { ...runSummary, installed: [skillName] });
259
303
  // Refresh snapshot so the status RPC reflects the freshly-installed version
260
304
  // immediately, rather than waiting for the next poll tick (up to 60 min).
261
305
  this.snapshot = this.buildSnapshot("healthy", "manual-update-complete");
@@ -269,6 +313,7 @@ export class SkillsUpdateCoordinator {
269
313
  // persist silently failed, and also so the updated lastCheckedAt we set
270
314
  // earlier in this method is guaranteed to reach disk on the error path.
271
315
  await this.persistState();
316
+ this.emitCheckCompleted("update_skill", { ...runSummary, installFailed: [skillName] });
272
317
  this.snapshot = this.buildSnapshot(this.snapshot.health, "manual-update-failed", errorMessage);
273
318
  throw err;
274
319
  }
@@ -307,6 +352,8 @@ export class SkillsUpdateCoordinator {
307
352
  }));
308
353
  const results = [];
309
354
  const skillsToInstall = [];
355
+ const runSummary = { ...emptyCheckRunSummary(), checked: skillNames.length };
356
+ const checkedInfo = new Map();
310
357
  for (let index = 0; index < versionChecks.length; index++) {
311
358
  const skillName = skillNames[index];
312
359
  const checkResult = versionChecks[index];
@@ -319,10 +366,13 @@ export class SkillsUpdateCoordinator {
319
366
  // the caller sees `error` in the returned array, but the snapshot
320
367
  // and persisted state never reflect it.
321
368
  this.recordCheckFailure(skillName, errorMessage);
369
+ runSummary.checkFailed.push(skillName);
322
370
  results.push({ name: skillName, updated: false, version: null, error: errorMessage });
323
371
  continue;
324
372
  }
325
373
  const { localVersion, remoteVersion, updateKind } = checkResult.value;
374
+ checkedInfo.set(skillName, { localVersion, remoteVersion, updateKind });
375
+ runSummary.onDisk.set(skillName, localVersion);
326
376
  // Mirror runTick: every successfully-checked skill gets `lastCheckedAt`
327
377
  // updated and `installedVersion` refreshed from the local read, whether
328
378
  // or not it proceeds to install. Otherwise skills touched only by
@@ -342,15 +392,18 @@ export class SkillsUpdateCoordinator {
342
392
  if (updateKind === "none" || updateKind === "unknown") {
343
393
  logger.info(`[senpi-skills-manager] updateAll — skipping | skill=${skillName} reason=${updateKind}`);
344
394
  results.push({ name: skillName, updated: false, version: localVersion });
395
+ runSummary.upToDate += 1;
345
396
  continue;
346
397
  }
347
398
  skillsToInstall.push(skillName);
348
399
  }
400
+ runSummary.toInstall = [...skillsToInstall];
349
401
  if (skillsToInstall.length === 0) {
350
402
  logger.info(`[senpi-skills-manager] updateAll complete — no installs needed | checked=${skillNames.length}`);
351
403
  // Persist so the `lastCheckedAt` timestamps we just set on every
352
404
  // successfully-checked skill survive a gateway restart.
353
405
  await this.persistState();
406
+ this.emitCheckCompleted("update_all", runSummary);
354
407
  // No installs, but we may have learned new remote versions from the
355
408
  // parallel checks above — refresh the snapshot so getSnapshot() doesn't
356
409
  // return pre-check data.
@@ -368,11 +421,14 @@ export class SkillsUpdateCoordinator {
368
421
  logger.warn("[senpi-skills-manager] updateAll — clone failed", err);
369
422
  for (const skillName of skillsToInstall) {
370
423
  results.push({ name: skillName, updated: false, version: null, error: errorMessage });
424
+ this.emitInstallFailed(skillName, "update_all", { failureKind: "clone", cause: err }, checkedInfo.get(skillName));
425
+ runSummary.installFailed.push(skillName);
371
426
  }
372
427
  // Clone failure doesn't produce install outcomes per-skill, but every
373
428
  // checked skill already had `lastCheckedAt` updated in-memory above.
374
429
  // Persist so those timestamps survive a gateway restart.
375
430
  await this.persistState();
431
+ this.emitCheckCompleted("update_all", runSummary);
376
432
  this.snapshot = this.buildSnapshot(this.snapshot.health, "manual-update-clone-failed");
377
433
  return results;
378
434
  }
@@ -380,16 +436,20 @@ export class SkillsUpdateCoordinator {
380
436
  let failureCount = 0;
381
437
  for (const skillName of skillsToInstall) {
382
438
  const outcome = this.processInstallOutcome(skillName, installOutcomes);
439
+ this.emitInstallOutcome(skillName, "update_all", outcome, checkedInfo.get(skillName));
383
440
  if (outcome.success) {
384
441
  results.push({ name: skillName, updated: true, version: outcome.version });
442
+ runSummary.installed.push(skillName);
385
443
  successCount += 1;
386
444
  }
387
445
  else {
388
446
  results.push({ name: skillName, updated: false, version: null, error: outcome.error });
447
+ runSummary.installFailed.push(skillName);
389
448
  failureCount += 1;
390
449
  }
391
450
  }
392
451
  await this.persistState();
452
+ this.emitCheckCompleted("update_all", runSummary);
393
453
  logger.info(`[senpi-skills-manager] updateAll complete | installed=${successCount} failed=${failureCount} checked=${skillNames.length}`);
394
454
  // Refresh snapshot so the status RPC reflects the freshly-installed
395
455
  // versions immediately instead of waiting for the next poll tick.
@@ -459,19 +519,22 @@ export class SkillsUpdateCoordinator {
459
519
  // a major bump is authorized without re-resolving the source.
460
520
  return { name: skillName, ...versionInfo, maxMajor: source.maxMajor };
461
521
  }));
462
- const skillsToInstall = [];
463
- let majorPendingCount = 0;
464
- let upToDateCount = 0;
522
+ // Triage results (up to date / held-back major / to install) accumulate on the summary.
523
+ const runSummary = { ...emptyCheckRunSummary(), checked: skillNames.length };
524
+ const checkedInfo = new Map();
465
525
  for (let index = 0; index < versionChecks.length; index++) {
466
526
  const skillName = skillNames[index];
467
527
  const checkResult = versionChecks[index];
468
528
  if (checkResult.status === "rejected") {
469
529
  const errorMessage = formatErr(checkResult.reason);
470
530
  this.recordCheckFailure(skillName, errorMessage);
531
+ runSummary.checkFailed.push(skillName);
471
532
  logger.warn(`[senpi-skills-manager] tick — version check failed | skill=${skillName} err=${errorMessage}`);
472
533
  continue;
473
534
  }
474
535
  const { localVersion, remoteVersion, updateKind, maxMajor } = checkResult.value;
536
+ checkedInfo.set(skillName, { localVersion, remoteVersion, updateKind });
537
+ runSummary.onDisk.set(skillName, localVersion);
475
538
  const skillState = this.state.skills[skillName] ?? createDefaultSkillState(skillName);
476
539
  skillState.lastCheckedAt = new Date().toISOString();
477
540
  if (localVersion !== null)
@@ -487,7 +550,7 @@ export class SkillsUpdateCoordinator {
487
550
  logger.info(`[senpi-skills-manager] tick — skill=${skillName} local=${localVersion ?? "none"} remote=${remoteVersion ?? "unknown"} kind=${updateKind}`);
488
551
  if (updateKind === "none" || updateKind === "unknown") {
489
552
  this.state.skills[skillName] = skillState;
490
- upToDateCount += 1;
553
+ runSummary.upToDate += 1;
491
554
  continue;
492
555
  }
493
556
  // Enforce the `maxMajor` ceiling on the **remote major regardless of how
@@ -502,54 +565,69 @@ export class SkillsUpdateCoordinator {
502
565
  // `senpi skills update <name>` or by bumping `maxMajor` in the manifest.
503
566
  logger.info(`[senpi-skills-manager] tick — major available, not auto-applied | skill=${skillName} local=${localVersion ?? "none"} remote=${remoteVersion ?? "unknown"} maxMajor=${maxMajor ?? "none"}`);
504
567
  this.state.skills[skillName] = skillState;
505
- majorPendingCount += 1;
568
+ runSummary.majorPending.push(skillName);
506
569
  continue;
507
570
  }
508
571
  if (updateKind === "major") {
509
572
  logger.info(`[senpi-skills-manager] tick — major within maxMajor ceiling, auto-applying | skill=${skillName} local=${localVersion ?? "none"} remote=${remoteVersion ?? "unknown"} maxMajor=${maxMajor}`);
510
573
  }
511
574
  // patch / minor / authorized-major — queue for install
512
- skillsToInstall.push(skillName);
575
+ runSummary.toInstall.push(skillName);
513
576
  this.state.skills[skillName] = skillState;
514
577
  }
515
- logger.info(`[senpi-skills-manager] tick — triage | upToDate=${upToDateCount} majorPending=${majorPendingCount} toInstall=${skillsToInstall.length}`);
516
- if (skillsToInstall.length === 0)
517
- return;
518
- await this.applyUpdates(skillsToInstall);
578
+ logger.info(`[senpi-skills-manager] tick — triage | upToDate=${runSummary.upToDate} majorPending=${runSummary.majorPending.length} toInstall=${runSummary.toInstall.length}`);
579
+ if (runSummary.toInstall.length > 0) {
580
+ const applied = await this.applyUpdates([...runSummary.toInstall], checkedInfo);
581
+ runSummary.installed = applied.installed;
582
+ runSummary.installFailed = applied.failed;
583
+ }
584
+ // One summary per tick, after installs, so `installed_versions` is what the box has now. This is
585
+ // the regularly-emitted fact that answers "which version of each skill does this box run".
586
+ this.emitCheckCompleted("tick", runSummary);
519
587
  }
520
588
  /**
521
589
  * Clone the repo once and install every skill in `skillNames`.
522
590
  * Per-skill errors are recorded in state without aborting the others;
523
591
  * outcomes are logged (no notification).
524
592
  *
525
- * If the clone itself fails we log and return without touching state — a
526
- * full-clone failure is almost always a transient network/git issue that
527
- * affects every skill equally. The next tick retries.
593
+ * A failed clone comes back from `withClonedGroups` as a per-skill `clone`
594
+ * outcome and is recorded like any other install failure. Only a throw from
595
+ * `cloneAndInstallMany` itself (the injected `cloneAndInstall` in tests)
596
+ * returns without touching state. The next tick retries either way.
528
597
  */
529
- async applyUpdates(skillNames) {
598
+ async applyUpdates(skillNames, checkedInfo) {
530
599
  const { logger } = this.options;
531
600
  logger.info(`[senpi-skills-manager] applyUpdates — batch install starting | count=${skillNames.length} skills=${skillNames.join(",")}`);
601
+ const installed = [];
602
+ const failed = [];
532
603
  let installOutcomes;
533
604
  try {
534
605
  installOutcomes = await this.cloneAndInstallMany(skillNames);
535
606
  }
536
607
  catch (err) {
537
608
  logger.warn(`[senpi-skills-manager] applyUpdates — git clone failed, skipping batch: ${formatErr(err)}`);
538
- return;
609
+ // Reached only when `cloneAndInstallMany` itself throws, which in tests means the injected
610
+ // `cloneAndInstall` dep. A real clone failure is caught per group in `withClonedGroups` and
611
+ // comes back as a `clone` outcome, which counts toward `consecutiveFailures` below.
612
+ for (const skillName of skillNames) {
613
+ this.emitInstallFailed(skillName, "tick", { failureKind: "clone", cause: err }, checkedInfo.get(skillName));
614
+ failed.push(skillName);
615
+ }
616
+ return { installed, failed };
539
617
  }
540
- let succeeded = 0;
541
- let failed = 0;
542
618
  for (const skillName of skillNames) {
543
619
  const outcome = this.processInstallOutcome(skillName, installOutcomes);
620
+ this.emitInstallOutcome(skillName, "tick", outcome, checkedInfo.get(skillName));
544
621
  if (outcome.success) {
545
- succeeded += 1;
622
+ installed.push(skillName);
546
623
  logger.info(`[senpi-skills-manager] applyUpdates — installed | skill=${skillName} version=${outcome.version ?? "unknown"}`);
547
624
  }
548
625
  else {
549
- failed += 1;
626
+ failed.push(skillName);
550
627
  }
551
628
  }
552
- logger.info(`[senpi-skills-manager] applyUpdates complete | installed=${succeeded} failed=${failed}`);
629
+ logger.info(`[senpi-skills-manager] applyUpdates complete | installed=${installed.length} failed=${failed.length}`);
630
+ return { installed, failed };
553
631
  }
554
632
  /**
555
633
  * Clone the remote skills repo exactly once and install every skill in
@@ -580,7 +658,12 @@ export class SkillsUpdateCoordinator {
580
658
  });
581
659
  }
582
660
  else {
583
- outcomes.set(skillName, { version: null, error: "install failed" });
661
+ outcomes.set(skillName, {
662
+ version: null,
663
+ error: "install failed",
664
+ failureKind: "install",
665
+ cause: new Error("install failed"),
666
+ });
584
667
  }
585
668
  }
586
669
  return outcomes;
@@ -617,7 +700,13 @@ export class SkillsUpdateCoordinator {
617
700
  }
618
701
  catch (err) {
619
702
  const errorMessage = formatErr(err);
620
- outcomes.set(skillName, { version: null, error: errorMessage });
703
+ outcomes.set(skillName, {
704
+ version: null,
705
+ error: errorMessage,
706
+ failureKind: classifyInstallFailure(err),
707
+ rolledBack: installRolledBack(err),
708
+ cause: err,
709
+ });
621
710
  logger.warn(`[senpi-skills-manager] cloneAndInstallMany — install failed | skill=${skillName} err=${errorMessage}`);
622
711
  }
623
712
  }
@@ -626,7 +715,7 @@ export class SkillsUpdateCoordinator {
626
715
  // and continues — earlier groups' installs are preserved.
627
716
  const errorMessage = `clone failed: ${formatErr(err)}`;
628
717
  for (const source of groupSources) {
629
- outcomes.set(source.name, { version: null, error: errorMessage });
718
+ outcomes.set(source.name, { version: null, error: errorMessage, failureKind: "clone", cause: err });
630
719
  }
631
720
  logger.warn(`[senpi-skills-manager] cloneAndInstallMany — clone failed, skipping group | skills=${groupSources.map((s) => s.name).join(",")} err=${formatErr(err)}`);
632
721
  });
@@ -680,14 +769,25 @@ export class SkillsUpdateCoordinator {
680
769
  * success/failure result for the caller's bookkeeping.
681
770
  */
682
771
  processInstallOutcome(skillName, installOutcomes) {
683
- const outcome = installOutcomes.get(skillName) ?? { version: null, error: "install result missing" };
772
+ const outcome = installOutcomes.get(skillName) ?? {
773
+ version: null,
774
+ error: "install result missing",
775
+ failureKind: "install",
776
+ cause: new Error("install result missing"),
777
+ };
684
778
  const skillState = this.state.skills[skillName] ?? createDefaultSkillState(skillName);
685
779
  if (outcome.error) {
686
780
  skillState.consecutiveFailures += 1;
687
781
  skillState.lastFailureAt = new Date().toISOString();
688
782
  skillState.lastFailureReason = outcome.error;
689
783
  this.state.skills[skillName] = skillState;
690
- return { success: false, error: outcome.error };
784
+ return {
785
+ success: false,
786
+ error: outcome.error,
787
+ failureKind: outcome.failureKind,
788
+ cause: outcome.cause,
789
+ rolledBack: outcome.rolledBack,
790
+ };
691
791
  }
692
792
  this.recordSuccess(skillState, outcome.version);
693
793
  this.state.skills[skillName] = skillState;
@@ -702,8 +802,9 @@ export class SkillsUpdateCoordinator {
702
802
  * error is re-thrown here so `updateSkill` can surface it to the RPC
703
803
  * caller via a rejected promise (batch path captures it instead).
704
804
  */
705
- async applyUpdate(skillName, remoteVersion) {
805
+ async applyUpdate(skillName, versionInfo) {
706
806
  const { logger } = this.options;
807
+ const { remoteVersion } = versionInfo;
707
808
  logger.info(`[senpi-skills-manager] applyUpdate — starting | skill=${skillName} targetVersion=${remoteVersion ?? "unknown"}`);
708
809
  // Clone can throw (full repo fetch failed). Unlike the batch path — which
709
810
  // intentionally doesn't attribute a shared clone failure to individual
@@ -724,6 +825,7 @@ export class SkillsUpdateCoordinator {
724
825
  this.state.skills[skillName] = skillState;
725
826
  await this.persistState();
726
827
  logger.warn(`[senpi-skills-manager] applyUpdate — ${errorMessage} | skill=${skillName}`);
828
+ this.emitInstallFailed(skillName, "update_skill", { failureKind: "clone", cause: err }, versionInfo);
727
829
  throw err;
728
830
  }
729
831
  // Delegate outcome bookkeeping (success or install-failure) to the shared
@@ -733,6 +835,7 @@ export class SkillsUpdateCoordinator {
733
835
  // updateSkill install failures were silently invisible to the snapshot.
734
836
  const processed = this.processInstallOutcome(skillName, installOutcomes);
735
837
  await this.persistState();
838
+ this.emitInstallOutcome(skillName, "update_skill", processed, versionInfo);
736
839
  if (!processed.success) {
737
840
  logger.warn(`[senpi-skills-manager] applyUpdate — install failed | skill=${skillName} err=${processed.error}`);
738
841
  throw new Error(processed.error);
@@ -827,6 +930,191 @@ export class SkillsUpdateCoordinator {
827
930
  skills: skillSnapshots,
828
931
  };
829
932
  }
933
+ // ── Telemetry (`skills.*` events) ─────────────────────────────────────────
934
+ // Emit-only: none of these read back into decisions or state.
935
+ /**
936
+ * Skills whose failed install could not put the previous install back (`rolled_back=false`), so
937
+ * the skill dir is gone. Filled by `emitInstallFailed`, read and cleared by the next
938
+ * `emitCheckCompleted`. Runs are serialized on `operationChain`, so one run never sees another's.
939
+ */
940
+ skillDirsLost = new Set();
941
+ /**
942
+ * What the last check run in this process saw on disk, per skill (`null` = nothing there). A run
943
+ * that checks only some skills (`updateSkill`) reports the rest from here, not from persisted
944
+ * state, which keeps the last non-null version.
945
+ */
946
+ lastOnDisk = new Map();
947
+ /**
948
+ * Emit a catalogued `skills.*` event, guarded so a throwing sink or a bad attribute shape can
949
+ * never disturb a tick, a manual update or bootstrap.
950
+ */
951
+ emitEvent(name, level, opts) {
952
+ try {
953
+ this.options.logger.event(name, level, opts);
954
+ }
955
+ catch {
956
+ // A telemetry-side failure must never disturb the skills-manager.
957
+ }
958
+ }
959
+ /** `skills.check_completed`: one summary per check run, with the whole managed set's versions. */
960
+ emitCheckCompleted(trigger, summary) {
961
+ let managed;
962
+ try {
963
+ managed = this.resolveSkillNames();
964
+ }
965
+ catch {
966
+ // Telemetry must not throw. The check itself already resolved the set, so this does not happen.
967
+ managed = Object.keys(this.state.skills);
968
+ }
969
+ // A skill not checked in this run reads as the last run saw it on disk. Then record what this
970
+ // run leaves on disk, for the next run.
971
+ for (const name of managed) {
972
+ if (!summary.onDisk.has(name) && this.lastOnDisk.has(name)) {
973
+ summary.onDisk.set(name, this.lastOnDisk.get(name) ?? null);
974
+ }
975
+ }
976
+ for (const name of managed) {
977
+ if (summary.installed.includes(name)) {
978
+ this.lastOnDisk.set(name, this.state.skills[name]?.installedVersion ?? null);
979
+ }
980
+ else if (summary.installFailed.includes(name) && this.skillDirsLost.has(name)) {
981
+ this.lastOnDisk.set(name, null);
982
+ }
983
+ else if (summary.onDisk.has(name)) {
984
+ this.lastOnDisk.set(name, summary.onDisk.get(name) ?? null);
985
+ }
986
+ }
987
+ // What is on disk now, per skill: the version installed in this run; else nothing, when this
988
+ // run's install failed and could not put the previous install back; else the version read from
989
+ // disk in this run (null = nothing readable); else, for a skill not checked in this run, the
990
+ // persisted version. Persisted state is not cleared on a null read, so it alone would report a
991
+ // vanished skill dir at its old version.
992
+ const lostNow = (name) => summary.installFailed.includes(name) && this.skillDirsLost.has(name);
993
+ const installedNow = (name) => {
994
+ if (summary.installed.includes(name))
995
+ return this.state.skills[name]?.installedVersion ?? null;
996
+ if (lostNow(name))
997
+ return null;
998
+ if (summary.onDisk.has(name))
999
+ return summary.onDisk.get(name) ?? null;
1000
+ return this.state.skills[name]?.installedVersion ?? null;
1001
+ };
1002
+ const missing = managed.filter((name) => !summary.installed.includes(name) &&
1003
+ (lostNow(name) || (summary.onDisk.has(name) && summary.onDisk.get(name) === null)));
1004
+ const installedVersions = formatSkillVersions(managed.map((name) => [name, installedNow(name)]));
1005
+ this.skillDirsLost.clear();
1006
+ const latestVersions = formatSkillVersions(managed.map((name) => [
1007
+ name,
1008
+ this.latestVersions.get(name) ?? this.state.skills[name]?.latestVersion,
1009
+ ]));
1010
+ this.emitEvent(EVENT_NAME.skillsCheckCompleted, "info", {
1011
+ body: `Skills check (${trigger}): checked=${summary.checked} upToDate=${summary.upToDate} ` +
1012
+ `toInstall=${summary.toInstall.length} majorPending=${summary.majorPending.length} ` +
1013
+ `checkFailed=${summary.checkFailed.length} missing=${missing.length} ` +
1014
+ `installed=${summary.installed.length} installFailed=${summary.installFailed.length}`,
1015
+ attributes: eventAttrs({
1016
+ "senpi.skills.trigger": trigger,
1017
+ "senpi.skills.checked_count": summary.checked,
1018
+ "senpi.skills.up_to_date_count": summary.upToDate,
1019
+ "senpi.skills.to_install_count": summary.toInstall.length,
1020
+ "senpi.skills.major_pending_count": summary.majorPending.length,
1021
+ "senpi.skills.check_failed_count": summary.checkFailed.length,
1022
+ "senpi.skills.missing_count": missing.length,
1023
+ "senpi.skills.installed_count": summary.installed.length,
1024
+ "senpi.skills.install_failed_count": summary.installFailed.length,
1025
+ "senpi.skills.installed_versions": installedVersions,
1026
+ "senpi.skills.latest_versions": latestVersions,
1027
+ "senpi.skills.to_install": joinSkillNames(summary.toInstall),
1028
+ "senpi.skills.major_pending": joinSkillNames(summary.majorPending),
1029
+ "senpi.skills.check_failed": joinSkillNames(summary.checkFailed),
1030
+ "senpi.skills.missing": joinSkillNames(missing),
1031
+ }),
1032
+ });
1033
+ }
1034
+ /** `skills.installed` or `skills.install_failed` for one processed install outcome. */
1035
+ emitInstallOutcome(skillName, trigger, outcome, info) {
1036
+ if (!outcome.success) {
1037
+ this.emitInstallFailed(skillName, trigger, outcome, info);
1038
+ return;
1039
+ }
1040
+ this.emitInstalled(skillName, trigger, {
1041
+ fromVersion: info?.localVersion ?? null,
1042
+ toVersion: outcome.version ?? info?.remoteVersion ?? null,
1043
+ updateKind: info?.updateKind ?? null,
1044
+ });
1045
+ }
1046
+ emitInstalled(skillName, trigger, versions) {
1047
+ const source = this.resolveSourceForTelemetry(skillName);
1048
+ this.emitEvent(EVENT_NAME.skillsInstalled, "info", {
1049
+ body: `Skill ${skillName} installed (${trigger}): ${versions.fromVersion ?? "none"} -> ${versions.toVersion ?? "unknown"}`,
1050
+ attributes: eventAttrs({
1051
+ "senpi.skills.skill": skillName,
1052
+ "senpi.skills.trigger": trigger,
1053
+ "senpi.skills.from_version": versions.fromVersion ?? undefined,
1054
+ "senpi.skills.to_version": versions.toVersion ?? undefined,
1055
+ "senpi.skills.update_kind": versions.updateKind ?? undefined,
1056
+ "senpi.skills.source_branch": source?.branch,
1057
+ "senpi.skills.preserve_paths_count": source?.preserve.length ?? 0,
1058
+ }),
1059
+ });
1060
+ }
1061
+ /** `skills.install_failed`. The original thrown value rides the `error` slot, stack and all. */
1062
+ emitInstallFailed(skillName, trigger, failure, info) {
1063
+ if (failure.rolledBack === false)
1064
+ this.skillDirsLost.add(skillName);
1065
+ else
1066
+ this.skillDirsLost.delete(skillName);
1067
+ const source = this.resolveSourceForTelemetry(skillName);
1068
+ // Bootstrap keeps no per-skill failure state, so it has no running count to report.
1069
+ const consecutiveFailures = trigger === "bootstrap" ? undefined : this.state.skills[skillName]?.consecutiveFailures;
1070
+ this.emitEvent(EVENT_NAME.skillsInstallFailed, "error", {
1071
+ body: `Skill ${skillName} install failed (${trigger}, ${failure.failureKind})`,
1072
+ attributes: eventAttrs({
1073
+ "senpi.skills.skill": skillName,
1074
+ "senpi.skills.trigger": trigger,
1075
+ "senpi.skills.failure_kind": failure.failureKind,
1076
+ "senpi.skills.rolled_back": failure.rolledBack,
1077
+ "senpi.skills.consecutive_failures": consecutiveFailures,
1078
+ "senpi.skills.from_version": info?.localVersion ?? undefined,
1079
+ "senpi.skills.to_version": info?.remoteVersion ?? undefined,
1080
+ "senpi.skills.update_kind": info?.updateKind,
1081
+ "senpi.skills.source_branch": source?.branch,
1082
+ }),
1083
+ error: failure.cause,
1084
+ });
1085
+ }
1086
+ /** `skills.bootstrap_completed` plus one `skills.installed` / `skills.install_failed` per attempt. */
1087
+ emitBootstrapEvents(result) {
1088
+ for (const name of result.installed) {
1089
+ this.emitInstalled(name, "bootstrap", { fromVersion: null, toVersion: null, updateKind: null });
1090
+ }
1091
+ for (const failure of result.failed) {
1092
+ this.emitInstallFailed(failure.name, "bootstrap", failure, undefined);
1093
+ }
1094
+ this.emitEvent(EVENT_NAME.skillsBootstrapCompleted, result.failed.length > 0 ? "warn" : "info", {
1095
+ body: `Skills bootstrap: manifest=${result.manifestCount} missing=${result.missing.length} ` +
1096
+ `installed=${result.installed.length} failed=${result.failed.length}`,
1097
+ attributes: eventAttrs({
1098
+ "senpi.skills.errored": false,
1099
+ "senpi.skills.manifest_count": result.manifestCount,
1100
+ "senpi.skills.missing_count": result.missing.length,
1101
+ "senpi.skills.installed_count": result.installed.length,
1102
+ "senpi.skills.install_failed_count": result.failed.length,
1103
+ "senpi.skills.missing": joinSkillNames(result.missing),
1104
+ "senpi.skills.installed": joinSkillNames(result.installed),
1105
+ "senpi.skills.install_failed": joinSkillNames(result.failed.map((f) => f.name)),
1106
+ }),
1107
+ });
1108
+ }
1109
+ /** Source for event attrs only; a resolve failure drops the source attrs instead of throwing. */
1110
+ resolveSourceForTelemetry(skillName) {
1111
+ try {
1112
+ return this.resolveSource(skillName);
1113
+ }
1114
+ catch {
1115
+ return null;
1116
+ }
1117
+ }
830
1118
  async persistState() {
831
1119
  try {
832
1120
  await this.store.save(this.state);