@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.
- package/dist/deploy/orchestrator.d.ts +14 -0
- package/dist/deploy/orchestrator.d.ts.map +1 -1
- package/dist/deploy/orchestrator.js +33 -1
- package/dist/deploy/orchestrator.js.map +1 -1
- package/dist/deploy/register.d.ts +7 -0
- package/dist/deploy/register.d.ts.map +1 -1
- package/dist/deploy/register.js +1 -0
- package/dist/deploy/register.js.map +1 -1
- package/dist/dsl/events/handlers.d.ts.map +1 -1
- package/dist/dsl/events/handlers.js +4 -0
- package/dist/dsl/events/handlers.js.map +1 -1
- package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
- package/dist/dsl/monitor/process-one-position.js +6 -2
- package/dist/dsl/monitor/process-one-position.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +114 -14
- package/dist/index.js.map +1 -1
- package/dist/runtime/boot-skip.d.ts +3 -0
- package/dist/runtime/boot-skip.d.ts.map +1 -1
- package/dist/runtime/boot-skip.js +7 -3
- package/dist/runtime/boot-skip.js.map +1 -1
- package/dist/runtime/lifecycle-events.d.ts +6 -0
- package/dist/runtime/lifecycle-events.d.ts.map +1 -1
- package/dist/runtime/lifecycle-events.js +1 -0
- package/dist/runtime/lifecycle-events.js.map +1 -1
- package/dist/runtime/reconcile-state.d.ts.map +1 -1
- package/dist/runtime/reconcile-state.js +4 -0
- package/dist/runtime/reconcile-state.js.map +1 -1
- package/dist/runtime/run.d.ts +2 -0
- package/dist/runtime/run.d.ts.map +1 -1
- package/dist/runtime/run.js +1 -0
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/runtime-schema.d.ts +100 -100
- package/dist/runtime/runtime.d.ts +8 -3
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +14 -6
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/scanners/engine/engine.d.ts +2 -0
- package/dist/scanners/engine/engine.d.ts.map +1 -1
- package/dist/scanners/engine/engine.js +6 -0
- package/dist/scanners/engine/engine.js.map +1 -1
- package/dist/scanners/runtime-module.d.ts +2 -0
- package/dist/scanners/runtime-module.d.ts.map +1 -1
- package/dist/scanners/runtime-module.js +3 -0
- package/dist/scanners/runtime-module.js.map +1 -1
- package/dist/senpi/client.d.ts +9 -1
- package/dist/senpi/client.d.ts.map +1 -1
- package/dist/senpi/client.js +25 -0
- package/dist/senpi/client.js.map +1 -1
- package/dist/senpi/types.d.ts +19 -0
- package/dist/senpi/types.d.ts.map +1 -1
- package/dist/senpi/types.js +0 -1
- package/dist/senpi/types.js.map +1 -1
- package/dist/skills-manager/bootstrap.d.ts +5 -2
- package/dist/skills-manager/bootstrap.d.ts.map +1 -1
- package/dist/skills-manager/bootstrap.js +24 -4
- package/dist/skills-manager/bootstrap.js.map +1 -1
- package/dist/skills-manager/coordinator.d.ts +32 -3
- package/dist/skills-manager/coordinator.d.ts.map +1 -1
- package/dist/skills-manager/coordinator.js +317 -29
- package/dist/skills-manager/coordinator.js.map +1 -1
- package/dist/skills-manager/install-error.d.ts +34 -0
- package/dist/skills-manager/install-error.d.ts.map +1 -0
- package/dist/skills-manager/install-error.js +31 -0
- package/dist/skills-manager/install-error.js.map +1 -0
- package/dist/skills-manager/types.d.ts +55 -7
- package/dist/skills-manager/types.d.ts.map +1 -1
- package/dist/skills-manager/utils.d.ts +8 -0
- package/dist/skills-manager/utils.d.ts.map +1 -1
- package/dist/skills-manager/utils.js +15 -0
- package/dist/skills-manager/utils.js.map +1 -1
- package/dist/skills-manager/writer.d.ts.map +1 -1
- package/dist/skills-manager/writer.js +35 -14
- package/dist/skills-manager/writer.js.map +1 -1
- package/dist/strategy/__tests__/fixtures/hawk-facts.d.ts +4 -0
- package/dist/strategy/__tests__/fixtures/hawk-facts.d.ts.map +1 -0
- package/dist/strategy/__tests__/fixtures/hawk-facts.js +29 -0
- package/dist/strategy/__tests__/fixtures/hawk-facts.js.map +1 -0
- package/dist/strategy/facts-sync.d.ts +79 -0
- package/dist/strategy/facts-sync.d.ts.map +1 -0
- package/dist/strategy/facts-sync.js +184 -0
- package/dist/strategy/facts-sync.js.map +1 -0
- package/dist/strategy/facts.d.ts +79 -0
- package/dist/strategy/facts.d.ts.map +1 -0
- package/dist/strategy/facts.js +184 -0
- package/dist/strategy/facts.js.map +1 -0
- package/dist/strategy/index.d.ts +1 -0
- package/dist/strategy/index.d.ts.map +1 -1
- package/dist/strategy/index.js +1 -0
- package/dist/strategy/index.js.map +1 -1
- package/dist/utils/event-catalog.d.ts +143 -0
- package/dist/utils/event-catalog.d.ts.map +1 -1
- package/dist/utils/event-catalog.js +12 -0
- package/dist/utils/event-catalog.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- 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
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
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,
|
|
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
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
575
|
+
runSummary.toInstall.push(skillName);
|
|
513
576
|
this.state.skills[skillName] = skillState;
|
|
514
577
|
}
|
|
515
|
-
logger.info(`[senpi-skills-manager] tick — triage | upToDate=${
|
|
516
|
-
if (
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
626
|
+
failed.push(skillName);
|
|
550
627
|
}
|
|
551
628
|
}
|
|
552
|
-
logger.info(`[senpi-skills-manager] applyUpdates complete | installed=${
|
|
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, {
|
|
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, {
|
|
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) ?? {
|
|
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 {
|
|
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,
|
|
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);
|