@camstack/system 1.2.441 → 1.2.443
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/addon-runner.js +72 -2
- package/dist/addon-runner.mjs +72 -2
- package/dist/addon-utils.js +1 -1
- package/dist/addon-utils.mjs +1 -1
- package/dist/auth/totp-manager.d.ts +46 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.d.ts +41 -1
- package/dist/builtins/alerts/alerts.addon.js +638 -26
- package/dist/builtins/alerts/alerts.addon.mjs +638 -26
- package/dist/builtins/alerts/critical-push.d.ts +86 -0
- package/dist/builtins/alerts/runner-alert.d.ts +71 -0
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composer.addon.js +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/block-package.d.ts +4 -2
- package/dist/builtins/core-blocks/block-settings-store.d.ts +65 -19
- package/dist/builtins/core-blocks/core-blocks.addon.js +365 -38
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +365 -38
- package/dist/builtins/device-manager/device-manager.addon.js +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +3 -3
- package/dist/builtins/keychain/index.mjs +3 -3
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/auth-schema.d.ts +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +226 -17
- package/dist/builtins/local-auth/local-auth.addon.mjs +226 -17
- package/dist/builtins/local-network/local-network.addon.js +2 -2
- package/dist/builtins/local-network/local-network.addon.mjs +2 -2
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +2 -2
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +2 -2
- package/dist/builtins/platform-probe/index.js +2 -2
- package/dist/builtins/platform-probe/index.mjs +2 -2
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +2 -2
- package/dist/builtins/system-config/system-config.addon.mjs +2 -2
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{composition-sources-BG-W2U0d.js → composition-sources-BUXX1e8j.js} +1 -1
- package/dist/{composition-sources-ByNISOnW.mjs → composition-sources-C8tUG2uL.mjs} +1 -1
- package/dist/{constants-DJILnUSb.mjs → constants-CGwVaAV3.mjs} +1 -1
- package/dist/{constants-UojhAJcm.js → constants-DWmm7_of.js} +1 -1
- package/dist/{device-ca-D8Ri9TVz.mjs → device-ca-DBfiJ_SB.mjs} +1 -1
- package/dist/{device-ca-196DAEZT.js → device-ca-DGayRmC0.js} +1 -1
- package/dist/{dist-BWvkuc31.mjs → dist-Bxt_S-9a.mjs} +180 -13
- package/dist/{dist-BiTCcaf-.js → dist-D9qT3jHr.js} +203 -12
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/{hub-mdns-ciao-DOBTq8qr.js → hub-mdns-ciao-B-wk9nzx.js} +1 -1
- package/dist/{hub-mdns-ciao-Cnd0bTB9.mjs → hub-mdns-ciao-DpPcQDg-.mjs} +1 -1
- package/dist/index.js +145 -16
- package/dist/index.mjs +140 -17
- package/dist/kernel/addon-refused-error.d.ts +30 -0
- package/dist/kernel/index.d.ts +3 -0
- package/dist/kernel/legacy-blob-guard.d.ts +6 -1
- package/dist/kernel/moleculer/process-service.d.ts +16 -1
- package/dist/kernel/moleculer/runner-refusal.d.ts +27 -0
- package/dist/{restore-archive-4ziluTU3.mjs → restore-archive-9CGgeBzD.mjs} +1 -1
- package/dist/{restore-archive-CfSSjKnA.js → restore-archive-BMNXwWWD.js} +1 -1
- package/dist/{wal-maintenance-CgZTNWAz.js → wal-maintenance-cRHR4rUY.js} +0 -0
- package/dist/{wal-maintenance-B57SAp4a.mjs → wal-maintenance-sIB32ONp.mjs} +0 -0
- package/package.json +1 -1
- package/dist/{custom-action-registry-BKmoaqG0.js → custom-action-registry-COyb49zm.js} +1 -1
- package/dist/{custom-action-registry-CyUuhATc.mjs → custom-action-registry-stpC3Zy-.mjs} +1 -1
|
@@ -1,4 +1,108 @@
|
|
|
1
|
-
import { Ar as errMsg,
|
|
1
|
+
import { Ar as errMsg, Ei as EventCategory, Jr as createEvent, i as AlertSchema, jr as BaseAddon, st as alertsCapability } from "../../dist-Bxt_S-9a.mjs";
|
|
2
|
+
//#region src/builtins/alerts/critical-push.ts
|
|
3
|
+
/** Alert categories that are critical system findings. */
|
|
4
|
+
var CRITICAL_CATEGORIES = new Set([
|
|
5
|
+
"addon.runner-failed",
|
|
6
|
+
"addon.runner-refused",
|
|
7
|
+
"system.runner-check"
|
|
8
|
+
]);
|
|
9
|
+
function isPushState(value) {
|
|
10
|
+
if (typeof value !== "object" || value === null) return false;
|
|
11
|
+
const rec = value;
|
|
12
|
+
return (rec["state"] === "pending" || rec["state"] === "delivered" || rec["state"] === "disabled") && typeof rec["key"] === "string" && typeof rec["attempts"] === "number";
|
|
13
|
+
}
|
|
14
|
+
function selectPushTargets(targets, policy) {
|
|
15
|
+
const enabled = targets.filter((t) => t.enabled);
|
|
16
|
+
if (policy.targetIds.length > 0) return enabled.filter((t) => policy.targetIds.includes(t.id));
|
|
17
|
+
return enabled.filter((t) => t.ownerUserId === void 0);
|
|
18
|
+
}
|
|
19
|
+
var MAX_DIGEST_LINES = 20;
|
|
20
|
+
/** The one message a target gets for a set of findings. */
|
|
21
|
+
function composeDigest(items) {
|
|
22
|
+
const first = items[0];
|
|
23
|
+
if (items.length === 1 && first !== void 0) return {
|
|
24
|
+
title: first.title,
|
|
25
|
+
body: first.line
|
|
26
|
+
};
|
|
27
|
+
const shown = items.slice(0, MAX_DIGEST_LINES).map((i) => i.line);
|
|
28
|
+
const more = items.length - shown.length;
|
|
29
|
+
return {
|
|
30
|
+
title: `${String(items.length)} addons need attention`,
|
|
31
|
+
body: [...shown, ...more > 0 ? [`... and ${String(more)} more`] : []].join("\n")
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Push every item that still needs it, as ONE message per target. Returns the new
|
|
36
|
+
* state of each item whose state changed (an item already delivered/disabled for
|
|
37
|
+
* its key is absent from the result).
|
|
38
|
+
*/
|
|
39
|
+
async function deliverDigest(input) {
|
|
40
|
+
const out = /* @__PURE__ */ new Map();
|
|
41
|
+
const work = input.items.filter((i) => !(i.previous !== null && i.previous.key === i.key && i.previous.state !== "pending"));
|
|
42
|
+
if (work.length === 0) return out;
|
|
43
|
+
const sameKey = (i) => i.previous !== null && i.previous.key === i.key ? i.previous : null;
|
|
44
|
+
const attemptsOf = (i) => (sameKey(i)?.attempts ?? 0) + 1;
|
|
45
|
+
const sentOf = (i) => sameKey(i)?.sentTo ?? [];
|
|
46
|
+
if (!input.policy.enabled) {
|
|
47
|
+
for (const i of work) out.set(i.alertId, {
|
|
48
|
+
state: "disabled",
|
|
49
|
+
key: i.key,
|
|
50
|
+
attempts: attemptsOf(i)
|
|
51
|
+
});
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
const fail = (error) => {
|
|
55
|
+
for (const i of work) out.set(i.alertId, {
|
|
56
|
+
state: "pending",
|
|
57
|
+
key: i.key,
|
|
58
|
+
attempts: attemptsOf(i),
|
|
59
|
+
lastError: error,
|
|
60
|
+
...sentOf(i).length > 0 ? { sentTo: sentOf(i) } : {}
|
|
61
|
+
});
|
|
62
|
+
return out;
|
|
63
|
+
};
|
|
64
|
+
let targets;
|
|
65
|
+
try {
|
|
66
|
+
targets = selectPushTargets(await input.ports.listTargets(), input.policy);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return fail(`notifiers unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
69
|
+
}
|
|
70
|
+
if (targets.length === 0) return fail("no eligible notifier target");
|
|
71
|
+
const sent = new Map(work.map((i) => [i.alertId, [...sentOf(i)]]));
|
|
72
|
+
const errors = new Map(work.map((i) => [i.alertId, []]));
|
|
73
|
+
for (const target of targets) {
|
|
74
|
+
const todo = work.filter((i) => !(sent.get(i.alertId) ?? []).includes(target.id));
|
|
75
|
+
if (todo.length === 0) continue;
|
|
76
|
+
let failure = null;
|
|
77
|
+
try {
|
|
78
|
+
const result = await input.ports.send(target, composeDigest(todo));
|
|
79
|
+
if (!result.success) failure = `${target.id}: ${result.error ?? "send failed"}`;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
failure = `${target.id}: ${err instanceof Error ? err.message : String(err)}`;
|
|
82
|
+
}
|
|
83
|
+
for (const i of todo) if (failure === null) sent.get(i.alertId)?.push(target.id);
|
|
84
|
+
else errors.get(i.alertId)?.push(failure);
|
|
85
|
+
}
|
|
86
|
+
for (const i of work) {
|
|
87
|
+
const sentTo = sent.get(i.alertId) ?? [];
|
|
88
|
+
const complete = targets.every((t) => sentTo.includes(t.id));
|
|
89
|
+
out.set(i.alertId, complete ? {
|
|
90
|
+
state: "delivered",
|
|
91
|
+
key: i.key,
|
|
92
|
+
at: input.now,
|
|
93
|
+
attempts: attemptsOf(i),
|
|
94
|
+
sentTo
|
|
95
|
+
} : {
|
|
96
|
+
state: "pending",
|
|
97
|
+
key: i.key,
|
|
98
|
+
attempts: attemptsOf(i),
|
|
99
|
+
lastError: (errors.get(i.alertId) ?? []).join("; "),
|
|
100
|
+
...sentTo.length > 0 ? { sentTo } : {}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
2
106
|
//#region src/builtins/alerts/device-label.ts
|
|
3
107
|
/**
|
|
4
108
|
* Operator-facing device label for alert messages.
|
|
@@ -42,6 +146,137 @@ function selectOverflow(alerts, maxAlerts) {
|
|
|
42
146
|
return [...byAge.filter((a) => a.read), ...byAge.filter((a) => !a.read)].slice(0, excess).map((a) => a.id);
|
|
43
147
|
}
|
|
44
148
|
//#endregion
|
|
149
|
+
//#region src/builtins/alerts/runner-alert.ts
|
|
150
|
+
var RUNNER_ALERT_CATEGORIES = [
|
|
151
|
+
"process.crashed",
|
|
152
|
+
"addon.crashed",
|
|
153
|
+
"addon.runner-failed",
|
|
154
|
+
"addon.runner-refused"
|
|
155
|
+
];
|
|
156
|
+
function isRunnerAlertCategory(category) {
|
|
157
|
+
return RUNNER_ALERT_CATEGORIES.includes(category);
|
|
158
|
+
}
|
|
159
|
+
/** Stable alert id of a runner on a node. */
|
|
160
|
+
function runnerAlertId(nodeId, runnerId) {
|
|
161
|
+
return `runner:${nodeId}:${runnerId}`;
|
|
162
|
+
}
|
|
163
|
+
function str(data, key) {
|
|
164
|
+
const value = data[key];
|
|
165
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
166
|
+
}
|
|
167
|
+
function strList(data, key) {
|
|
168
|
+
const value = data[key];
|
|
169
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
170
|
+
}
|
|
171
|
+
/** Which runner an event is about; `null` when it names none. */
|
|
172
|
+
function runnerKeyOf(category, data, source) {
|
|
173
|
+
const nodeId = str(data, "nodeId") ?? source?.nodeId ?? "hub";
|
|
174
|
+
const runnerId = category === "process.crashed" ? str(data, "processId") ?? source?.id ?? "unknown" : category === "addon.crashed" ? str(data, "addonId") ?? source?.id ?? "unknown" : str(data, "runnerId");
|
|
175
|
+
return runnerId === void 0 ? null : {
|
|
176
|
+
nodeId,
|
|
177
|
+
runnerId
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/** Fold one event into the runner's record. */
|
|
181
|
+
function foldRunnerEvent(previous, category, data, key, now) {
|
|
182
|
+
const base = previous ?? {
|
|
183
|
+
nodeId: key.nodeId,
|
|
184
|
+
runnerId: key.runnerId,
|
|
185
|
+
state: "crashed",
|
|
186
|
+
since: now,
|
|
187
|
+
count: 0,
|
|
188
|
+
addonIds: []
|
|
189
|
+
};
|
|
190
|
+
const addonIds = [...new Set([...base.addonIds, ...strList(data, "addonIds")])];
|
|
191
|
+
const count = base.count + 1;
|
|
192
|
+
switch (category) {
|
|
193
|
+
case "addon.runner-refused": return {
|
|
194
|
+
...base,
|
|
195
|
+
state: "refused",
|
|
196
|
+
count,
|
|
197
|
+
addonIds,
|
|
198
|
+
reason: str(data, "reason") ?? "refused to start",
|
|
199
|
+
...str(data, "remedy") !== void 0 ? { remedy: str(data, "remedy") } : {}
|
|
200
|
+
};
|
|
201
|
+
case "addon.runner-failed": {
|
|
202
|
+
if (base.state === "refused") return {
|
|
203
|
+
...base,
|
|
204
|
+
count,
|
|
205
|
+
addonIds
|
|
206
|
+
};
|
|
207
|
+
const crashes = data["crashesInWindow"];
|
|
208
|
+
return {
|
|
209
|
+
...base,
|
|
210
|
+
state: "failed",
|
|
211
|
+
count,
|
|
212
|
+
addonIds,
|
|
213
|
+
...typeof crashes === "number" ? { crashesInWindow: crashes } : {}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
case "addon.crashed": {
|
|
217
|
+
const error = str(data, "error");
|
|
218
|
+
const addonId = str(data, "addonId");
|
|
219
|
+
return {
|
|
220
|
+
...base,
|
|
221
|
+
count,
|
|
222
|
+
addonIds: addonId !== void 0 ? [...new Set([...addonIds, addonId])] : addonIds,
|
|
223
|
+
...error !== void 0 ? { lastError: error } : {}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
case "process.crashed": return {
|
|
227
|
+
...base,
|
|
228
|
+
count,
|
|
229
|
+
addonIds
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function renderRunnerAlert(record) {
|
|
234
|
+
const where = record.nodeId === "hub" ? record.runnerId : `${record.runnerId} on ${record.nodeId}`;
|
|
235
|
+
switch (record.state) {
|
|
236
|
+
case "refused": return {
|
|
237
|
+
severity: "error",
|
|
238
|
+
title: "Addon refused to start",
|
|
239
|
+
message: `${where}: refused - ${record.reason ?? "no reason reported"}${record.remedy !== void 0 ? ` ${record.remedy}` : ""}`
|
|
240
|
+
};
|
|
241
|
+
case "failed": return {
|
|
242
|
+
severity: "error",
|
|
243
|
+
title: "Addon crash loop",
|
|
244
|
+
message: `Runner "${where}" gave up${record.crashesInWindow !== void 0 ? ` after ${String(record.crashesInWindow)} crashes` : ""}${record.addonIds.length > 0 ? ` (addons: ${record.addonIds.join(", ")})` : ""} - ${String(record.count)} events`
|
|
245
|
+
};
|
|
246
|
+
case "crashed": return {
|
|
247
|
+
severity: "error",
|
|
248
|
+
title: "Addon crashed",
|
|
249
|
+
message: `Addon "${where}" crashed${record.lastError !== void 0 ? `: ${record.lastError}` : ""}${record.count > 1 ? ` (${String(record.count)} events)` : ""}`
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** Is the record in a state an operator must act on (terminal)? */
|
|
254
|
+
function isTerminalRunnerState(state) {
|
|
255
|
+
return state === "failed" || state === "refused";
|
|
256
|
+
}
|
|
257
|
+
function hash(text) {
|
|
258
|
+
let h = 5381;
|
|
259
|
+
for (let i = 0; i < text.length; i++) h = (h << 5) + h + text.charCodeAt(i) | 0;
|
|
260
|
+
return (h >>> 0).toString(36);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The push key of a record: its episode and its cause. The same refusal of the
|
|
264
|
+
* same episode is never pushed twice; a refusal with another reason, a breaker
|
|
265
|
+
* trip, or any failure after a recovery (a new episode) is a new push.
|
|
266
|
+
*/
|
|
267
|
+
function runnerPushKey(record) {
|
|
268
|
+
const cause = record.state === "refused" ? hash(`${record.reason ?? ""}|${record.remedy ?? ""}`) : "breaker";
|
|
269
|
+
return `runner:${record.state}:${String(record.since)}:${cause}`;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* The one line a push carries: runner, state and the remedy. Deliberately NOT
|
|
273
|
+
* the raw reason or error text - those stay on the alert.
|
|
274
|
+
*/
|
|
275
|
+
function runnerPushLine(record) {
|
|
276
|
+
const where = record.nodeId === "hub" ? record.runnerId : `${record.runnerId} on ${record.nodeId}`;
|
|
277
|
+
return record.state === "refused" ? `${where}: refused to start. ${record.remedy ?? "See the Addons page."}` : `${where}: stopped after repeated crashes. Restart it from the Addons page.`;
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
45
280
|
//#region src/builtins/alerts/alerts.addon.ts
|
|
46
281
|
var CATEGORY_MAPPINGS = {
|
|
47
282
|
"addon.crashed": {
|
|
@@ -52,6 +287,14 @@ var CATEGORY_MAPPINGS = {
|
|
|
52
287
|
severity: "error",
|
|
53
288
|
titleTemplate: "Addon crash loop"
|
|
54
289
|
},
|
|
290
|
+
"addon.runner-refused": {
|
|
291
|
+
severity: "error",
|
|
292
|
+
titleTemplate: "Addon refused to start"
|
|
293
|
+
},
|
|
294
|
+
"system.runner-check": {
|
|
295
|
+
severity: "error",
|
|
296
|
+
titleTemplate: "Runners degraded"
|
|
297
|
+
},
|
|
55
298
|
"addon.error": {
|
|
56
299
|
severity: "error",
|
|
57
300
|
titleTemplate: "Addon error"
|
|
@@ -135,6 +378,8 @@ var CATEGORY_MAPPINGS = {
|
|
|
135
378
|
* {@link CATEGORY_MAPPINGS}.
|
|
136
379
|
*/
|
|
137
380
|
var SILENT_TRACKED_CATEGORIES = [
|
|
381
|
+
"addon.started",
|
|
382
|
+
"process.state-changed",
|
|
138
383
|
"addon.load-recovered",
|
|
139
384
|
"system.liveness-recovered",
|
|
140
385
|
"stream.online"
|
|
@@ -156,6 +401,28 @@ var TRACKED_CATEGORIES = [...Object.keys(CATEGORY_MAPPINGS), ...SILENT_TRACKED_C
|
|
|
156
401
|
*/
|
|
157
402
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
158
403
|
var RATE_LIMIT_MAX_PER_SOURCE = 5;
|
|
404
|
+
/** The alert `category` a runner record carries - the state it is in. */
|
|
405
|
+
function runnerCategoryOf(state) {
|
|
406
|
+
return state === "refused" ? "addon.runner-refused" : state === "failed" ? "addon.runner-failed" : "addon.crashed";
|
|
407
|
+
}
|
|
408
|
+
function parseRunnerRecord(value) {
|
|
409
|
+
if (typeof value !== "object" || value === null) return null;
|
|
410
|
+
const rec = value;
|
|
411
|
+
const state = rec["state"];
|
|
412
|
+
if (typeof rec["nodeId"] !== "string" || typeof rec["runnerId"] !== "string" || state !== "crashed" && state !== "failed" && state !== "refused" || typeof rec["count"] !== "number" || !Array.isArray(rec["addonIds"])) return null;
|
|
413
|
+
return {
|
|
414
|
+
nodeId: rec["nodeId"],
|
|
415
|
+
runnerId: rec["runnerId"],
|
|
416
|
+
state,
|
|
417
|
+
since: typeof rec["since"] === "number" ? rec["since"] : 0,
|
|
418
|
+
count: rec["count"],
|
|
419
|
+
addonIds: rec["addonIds"].filter((x) => typeof x === "string"),
|
|
420
|
+
...typeof rec["lastError"] === "string" ? { lastError: rec["lastError"] } : {},
|
|
421
|
+
...typeof rec["reason"] === "string" ? { reason: rec["reason"] } : {},
|
|
422
|
+
...typeof rec["remedy"] === "string" ? { remedy: rec["remedy"] } : {},
|
|
423
|
+
...typeof rec["crashesInWindow"] === "number" ? { crashesInWindow: rec["crashesInWindow"] } : {}
|
|
424
|
+
};
|
|
425
|
+
}
|
|
159
426
|
/** Parse a raw settings record into a validated Alert. */
|
|
160
427
|
function parseAlert(record) {
|
|
161
428
|
return AlertSchema.parse({
|
|
@@ -165,8 +432,14 @@ function parseAlert(record) {
|
|
|
165
432
|
}
|
|
166
433
|
var DEFAULT_CONFIG = {
|
|
167
434
|
maxAlerts: 500,
|
|
168
|
-
retentionDays: 30
|
|
435
|
+
retentionDays: 30,
|
|
436
|
+
criticalPushEnabled: true,
|
|
437
|
+
criticalPushTargetIds: ""
|
|
169
438
|
};
|
|
439
|
+
/** One burst of critical findings (a whole boot of refused runners) is ONE digest push. */
|
|
440
|
+
var PUSH_COALESCE_MS = 2e3;
|
|
441
|
+
/** Addons whose (re)start is a reason to retry pending critical pushes. */
|
|
442
|
+
var PUSH_RECONCILE_ADDONS = new Set(["notifiers", "notification-center"]);
|
|
170
443
|
var ALERTS_COLLECTION = "alerts";
|
|
171
444
|
var EVENT_SOURCE = {
|
|
172
445
|
type: "addon",
|
|
@@ -189,8 +462,11 @@ function toPrunable(record) {
|
|
|
189
462
|
}
|
|
190
463
|
var AlertCenterAddon = class extends BaseAddon {
|
|
191
464
|
unsubscribers = [];
|
|
192
|
-
|
|
465
|
+
/** How long a burst of critical findings is gathered into one digest push. */
|
|
466
|
+
pushCoalesceMs;
|
|
467
|
+
constructor(options) {
|
|
193
468
|
super({ ...DEFAULT_CONFIG });
|
|
469
|
+
this.pushCoalesceMs = options?.pushCoalesceMs ?? PUSH_COALESCE_MS;
|
|
194
470
|
}
|
|
195
471
|
/**
|
|
196
472
|
* Per `(category, source.id)` counter used to rate-limit chatty
|
|
@@ -216,6 +492,7 @@ var AlertCenterAddon = class extends BaseAddon {
|
|
|
216
492
|
unsubs.push(unsub);
|
|
217
493
|
}
|
|
218
494
|
this.unsubscribers = unsubs;
|
|
495
|
+
this.reconcilePendingPushes();
|
|
219
496
|
this.ctx.logger.info("Initialized", { meta: { trackedCategoriesCount: TRACKED_CATEGORIES.length } });
|
|
220
497
|
return [{
|
|
221
498
|
capability: alertsCapability,
|
|
@@ -234,29 +511,46 @@ var AlertCenterAddon = class extends BaseAddon {
|
|
|
234
511
|
description: "Configure alert storage and retention.",
|
|
235
512
|
descriptionKey: "server:globalSettings.s_alerts.description",
|
|
236
513
|
columns: 1,
|
|
237
|
-
fields: [
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
514
|
+
fields: [
|
|
515
|
+
this.field({
|
|
516
|
+
type: "number",
|
|
517
|
+
key: "maxAlerts",
|
|
518
|
+
label: "Maximum stored alerts",
|
|
519
|
+
labelKey: "server:globalSettings.s_alerts.maxAlerts.label",
|
|
520
|
+
description: "Oldest read alerts are deleted when this limit is exceeded.",
|
|
521
|
+
descriptionKey: "server:globalSettings.s_alerts.maxAlerts.description",
|
|
522
|
+
min: 50,
|
|
523
|
+
max: 5e3,
|
|
524
|
+
step: 50,
|
|
525
|
+
default: 500
|
|
526
|
+
}),
|
|
527
|
+
this.field({
|
|
528
|
+
type: "number",
|
|
529
|
+
key: "retentionDays",
|
|
530
|
+
label: "Alert retention (days)",
|
|
531
|
+
labelKey: "server:globalSettings.s_alerts.retentionDays.label",
|
|
532
|
+
description: "Alerts older than this are automatically removed.",
|
|
533
|
+
descriptionKey: "server:globalSettings.s_alerts.retentionDays.description",
|
|
534
|
+
min: 1,
|
|
535
|
+
max: 365,
|
|
536
|
+
step: 1,
|
|
537
|
+
default: 30
|
|
538
|
+
}),
|
|
539
|
+
this.field({
|
|
540
|
+
type: "boolean",
|
|
541
|
+
key: "criticalPushEnabled",
|
|
542
|
+
label: "Push critical system alerts",
|
|
543
|
+
description: "A runner that failed or refused to start, or a node whose runners are degraded, is pushed straight to the notifier targets - even when notification-center is down.",
|
|
544
|
+
default: true
|
|
545
|
+
}),
|
|
546
|
+
this.field({
|
|
547
|
+
type: "text",
|
|
548
|
+
key: "criticalPushTargetIds",
|
|
549
|
+
label: "Critical push targets",
|
|
550
|
+
description: "Comma-separated notifier target ids. Empty = every enabled shared (not personal) target.",
|
|
551
|
+
default: ""
|
|
552
|
+
})
|
|
553
|
+
]
|
|
260
554
|
}] });
|
|
261
555
|
}
|
|
262
556
|
async listAlerts(filter) {
|
|
@@ -316,6 +610,23 @@ var AlertCenterAddon = class extends BaseAddon {
|
|
|
316
610
|
await this.handleModelDownload(event);
|
|
317
611
|
return;
|
|
318
612
|
}
|
|
613
|
+
if (isRunnerAlertCategory(category)) {
|
|
614
|
+
await this.handleRunnerEvent(category, event);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (category === EventCategory.ProcessStateChanged) {
|
|
618
|
+
await this.handleRunnerStateChanged(event);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (category === EventCategory.SystemRunnerCheck) {
|
|
622
|
+
await this.handleRunnerCheck(event);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (category === EventCategory.AddonStarted) {
|
|
626
|
+
const addonId = event.data["addonId"];
|
|
627
|
+
if (typeof addonId === "string" && PUSH_RECONCILE_ADDONS.has(addonId)) await this.reconcilePendingPushes();
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
319
630
|
if (category === EventCategory.AddonLoadFailed) {
|
|
320
631
|
await this.handleAddonLoadFailed(event);
|
|
321
632
|
return;
|
|
@@ -374,6 +685,307 @@ var AlertCenterAddon = class extends BaseAddon {
|
|
|
374
685
|
} });
|
|
375
686
|
}
|
|
376
687
|
}
|
|
688
|
+
async readRunnerRecord(alertId) {
|
|
689
|
+
const backend = this.ctx.api.settingsStore;
|
|
690
|
+
if (!backend) return null;
|
|
691
|
+
const raw = await backend.get.query({
|
|
692
|
+
collection: ALERTS_COLLECTION,
|
|
693
|
+
key: alertId
|
|
694
|
+
});
|
|
695
|
+
if (raw === null || raw === void 0 || typeof raw !== "object") return null;
|
|
696
|
+
const alert = AlertSchema.safeParse(raw);
|
|
697
|
+
if (!alert.success || alert.data.status === "dismissed") return null;
|
|
698
|
+
return parseRunnerRecord(alert.data.metadata?.["runner"]);
|
|
699
|
+
}
|
|
700
|
+
async handleRunnerEvent(category, event) {
|
|
701
|
+
if (!isRunnerAlertCategory(category)) return;
|
|
702
|
+
const key = runnerKeyOf(category, event.data, event.source ? {
|
|
703
|
+
id: String(event.source.id),
|
|
704
|
+
...typeof event.source.nodeId === "string" ? { nodeId: event.source.nodeId } : {}
|
|
705
|
+
} : void 0);
|
|
706
|
+
if (key === null) return;
|
|
707
|
+
const alertId = runnerAlertId(key.nodeId, key.runnerId);
|
|
708
|
+
const previous = await this.readRunnerRecord(alertId);
|
|
709
|
+
const record = foldRunnerEvent(previous, category, event.data, key, Date.now());
|
|
710
|
+
const rendered = renderRunnerAlert(record);
|
|
711
|
+
const existing = await this.readAlertRaw(alertId);
|
|
712
|
+
const now = Date.now();
|
|
713
|
+
const alert = {
|
|
714
|
+
id: alertId,
|
|
715
|
+
category: runnerCategoryOf(record.state),
|
|
716
|
+
severity: rendered.severity,
|
|
717
|
+
title: rendered.title,
|
|
718
|
+
message: rendered.message,
|
|
719
|
+
status: "active",
|
|
720
|
+
read: previous !== null && previous.state === record.state ? existing?.read === true : false,
|
|
721
|
+
createdAt: previous !== null ? existing?.createdAt ?? now : now,
|
|
722
|
+
updatedAt: now,
|
|
723
|
+
source: event.source ? {
|
|
724
|
+
type: event.source.type,
|
|
725
|
+
id: String(event.source.id)
|
|
726
|
+
} : {
|
|
727
|
+
type: "runner",
|
|
728
|
+
id: record.runnerId
|
|
729
|
+
},
|
|
730
|
+
metadata: {
|
|
731
|
+
runner: record,
|
|
732
|
+
pushLine: runnerPushLine(record),
|
|
733
|
+
...existing?.push !== void 0 ? { push: existing.push } : {}
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
await this.persistAlert(alert);
|
|
737
|
+
if (previous === null) this.emitAlertCreatedEvent(alert);
|
|
738
|
+
else this.emitAlertUpdatedEvent(alertId, {
|
|
739
|
+
message: alert.message,
|
|
740
|
+
title: alert.title
|
|
741
|
+
});
|
|
742
|
+
await this.enforceMaxAlerts();
|
|
743
|
+
if (isTerminalRunnerState(record.state)) this.queuePush({
|
|
744
|
+
alertId,
|
|
745
|
+
key: runnerPushKey(record),
|
|
746
|
+
title: rendered.title,
|
|
747
|
+
line: runnerPushLine(record)
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* A runner reached `running`: its episode is over. Close the runner alert so a
|
|
752
|
+
* later failure opens a NEW episode (new push key), instead of folding into a
|
|
753
|
+
* stale `refused` record that would never push again.
|
|
754
|
+
*/
|
|
755
|
+
async handleRunnerStateChanged(event) {
|
|
756
|
+
if (event.data["state"] !== "running") return;
|
|
757
|
+
const runnerId = typeof event.data["runnerId"] === "string" ? event.data["runnerId"] : null;
|
|
758
|
+
if (runnerId === null) return;
|
|
759
|
+
const alertId = runnerAlertId(typeof event.data["nodeId"] === "string" ? event.data["nodeId"] : "hub", runnerId);
|
|
760
|
+
if (await this.readRunnerRecord(alertId) === null) return;
|
|
761
|
+
await this.dismiss(alertId);
|
|
762
|
+
}
|
|
763
|
+
/** The node's one-shot runner boot check: one alert, dismissed when the node is clean. */
|
|
764
|
+
async handleRunnerCheck(event) {
|
|
765
|
+
const nodeId = typeof event.data["nodeId"] === "string" ? event.data["nodeId"] : "hub";
|
|
766
|
+
const alertId = `runner-check:${nodeId}`;
|
|
767
|
+
if (event.data["degraded"] !== true) {
|
|
768
|
+
await this.dismiss(alertId);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const summary = typeof event.data["summary"] === "string" ? event.data["summary"] : "degraded";
|
|
772
|
+
const ids = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
773
|
+
const refusedIds = Array.isArray(event.data["refused"]) ? event.data["refused"].flatMap((r) => {
|
|
774
|
+
const id = typeof r === "object" && r !== null ? r["runnerId"] : void 0;
|
|
775
|
+
return typeof id === "string" ? [id] : [];
|
|
776
|
+
}) : [];
|
|
777
|
+
const failedIds = ids(event.data["failed"]);
|
|
778
|
+
const missingIds = ids(event.data["missing"]);
|
|
779
|
+
const pushLine = `${nodeId}: runners degraded - ${[
|
|
780
|
+
...failedIds.length > 0 ? [`failed: ${failedIds.join(", ")}`] : [],
|
|
781
|
+
...refusedIds.length > 0 ? [`refused: ${refusedIds.join(", ")}`] : [],
|
|
782
|
+
...missingIds.length > 0 ? [`missing: ${missingIds.join(", ")}`] : []
|
|
783
|
+
].join("; ")}. See the Addons page.`;
|
|
784
|
+
const pushKey = `check:${[
|
|
785
|
+
...failedIds,
|
|
786
|
+
...refusedIds,
|
|
787
|
+
...missingIds
|
|
788
|
+
].sort().join(",")}`;
|
|
789
|
+
const now = Date.now();
|
|
790
|
+
const existing = await this.readAlertRaw(alertId);
|
|
791
|
+
const alert = {
|
|
792
|
+
id: alertId,
|
|
793
|
+
category: "system.runner-check",
|
|
794
|
+
severity: "error",
|
|
795
|
+
title: "Runners degraded",
|
|
796
|
+
message: `${nodeId}: ${summary}`,
|
|
797
|
+
status: "active",
|
|
798
|
+
read: false,
|
|
799
|
+
createdAt: existing?.createdAt ?? now,
|
|
800
|
+
updatedAt: now,
|
|
801
|
+
source: {
|
|
802
|
+
type: "node",
|
|
803
|
+
id: nodeId
|
|
804
|
+
},
|
|
805
|
+
metadata: {
|
|
806
|
+
nodeId,
|
|
807
|
+
failed: event.data["failed"],
|
|
808
|
+
refused: event.data["refused"],
|
|
809
|
+
missing: event.data["missing"],
|
|
810
|
+
pushLine,
|
|
811
|
+
...existing?.push !== void 0 ? { push: existing.push } : {}
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
await this.persistAlert(alert);
|
|
815
|
+
this.emitAlertCreatedEvent(alert);
|
|
816
|
+
this.queuePush({
|
|
817
|
+
alertId,
|
|
818
|
+
key: pushKey,
|
|
819
|
+
title: alert.title,
|
|
820
|
+
line: pushLine
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
async readAlertRaw(alertId) {
|
|
824
|
+
const backend = this.ctx.api.settingsStore;
|
|
825
|
+
if (!backend) return null;
|
|
826
|
+
const raw = await backend.get.query({
|
|
827
|
+
collection: ALERTS_COLLECTION,
|
|
828
|
+
key: alertId
|
|
829
|
+
});
|
|
830
|
+
if (raw === null || raw === void 0 || typeof raw !== "object") return null;
|
|
831
|
+
const rec = Object.fromEntries(Object.entries(raw));
|
|
832
|
+
const meta = rec["metadata"];
|
|
833
|
+
const push = typeof meta === "object" && meta !== null && !Array.isArray(meta) ? meta["push"] : void 0;
|
|
834
|
+
return {
|
|
835
|
+
...typeof rec["createdAt"] === "number" ? { createdAt: rec["createdAt"] } : {},
|
|
836
|
+
...typeof rec["read"] === "boolean" ? { read: rec["read"] } : {},
|
|
837
|
+
...isPushState(push) ? { push } : {}
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
/** Pushes run one at a time so a burst can never send the same state twice. */
|
|
841
|
+
pushChain = Promise.resolve();
|
|
842
|
+
/** Findings waiting for the next digest, by alert id. */
|
|
843
|
+
pushQueue = /* @__PURE__ */ new Map();
|
|
844
|
+
pushTimer = null;
|
|
845
|
+
pushPolicy() {
|
|
846
|
+
return {
|
|
847
|
+
enabled: this.config.criticalPushEnabled,
|
|
848
|
+
targetIds: this.config.criticalPushTargetIds.split(",").map((id) => id.trim()).filter((id) => id.length > 0)
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
pushPorts() {
|
|
852
|
+
const api = this.ctx.api;
|
|
853
|
+
return {
|
|
854
|
+
listTargets: async () => {
|
|
855
|
+
return (await api.notificationOutput.listTargets.query({})).map((t) => {
|
|
856
|
+
const owner = t.config["ownerUserId"];
|
|
857
|
+
return {
|
|
858
|
+
id: t.id,
|
|
859
|
+
addonId: t.addonId,
|
|
860
|
+
enabled: t.enabled,
|
|
861
|
+
...typeof owner === "string" && owner.length > 0 ? { ownerUserId: owner } : {}
|
|
862
|
+
};
|
|
863
|
+
});
|
|
864
|
+
},
|
|
865
|
+
send: async (target, message) => api.notificationOutput.send.mutate({
|
|
866
|
+
addonId: target.addonId,
|
|
867
|
+
targetId: target.id,
|
|
868
|
+
notification: {
|
|
869
|
+
title: message.title,
|
|
870
|
+
body: message.body,
|
|
871
|
+
format: "text",
|
|
872
|
+
priority: 5
|
|
873
|
+
}
|
|
874
|
+
})
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Queue a critical finding for the next digest. A burst (every runner of a boot)
|
|
879
|
+
* arrives within the coalesce window and leaves as ONE message per target.
|
|
880
|
+
*/
|
|
881
|
+
queuePush(item) {
|
|
882
|
+
this.pushQueue.set(item.alertId, item);
|
|
883
|
+
if (this.pushTimer !== null) return;
|
|
884
|
+
this.pushTimer = setTimeout(() => {
|
|
885
|
+
this.pushTimer = null;
|
|
886
|
+
this.flushPushes();
|
|
887
|
+
}, this.pushCoalesceMs);
|
|
888
|
+
this.pushTimer.unref?.();
|
|
889
|
+
}
|
|
890
|
+
/** Send the queued findings now (also the reconcile path). */
|
|
891
|
+
flushPushes() {
|
|
892
|
+
this.pushChain = this.pushChain.then(async () => {
|
|
893
|
+
const queued = [...this.pushQueue.values()];
|
|
894
|
+
this.pushQueue.clear();
|
|
895
|
+
if (queued.length === 0) return;
|
|
896
|
+
try {
|
|
897
|
+
const items = [];
|
|
898
|
+
for (const q of queued) {
|
|
899
|
+
const existing = await this.readAlertRaw(q.alertId);
|
|
900
|
+
items.push({
|
|
901
|
+
...q,
|
|
902
|
+
previous: existing?.push ?? null
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
const states = await deliverDigest({
|
|
906
|
+
items,
|
|
907
|
+
policy: this.pushPolicy(),
|
|
908
|
+
ports: this.pushPorts(),
|
|
909
|
+
now: Date.now()
|
|
910
|
+
});
|
|
911
|
+
for (const [alertId, state] of states) {
|
|
912
|
+
await this.storePushState(alertId, state);
|
|
913
|
+
this.ctx.logger?.info("critical alert push", { meta: {
|
|
914
|
+
alertId,
|
|
915
|
+
state: state.state,
|
|
916
|
+
attempts: state.attempts,
|
|
917
|
+
error: state.lastError
|
|
918
|
+
} });
|
|
919
|
+
}
|
|
920
|
+
} catch (err) {
|
|
921
|
+
this.ctx.logger?.warn("critical alert push failed unexpectedly", { meta: { error: errMsg(err) } });
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
return this.pushChain;
|
|
925
|
+
}
|
|
926
|
+
async storePushState(alertId, push) {
|
|
927
|
+
const backend = this.ctx.api.settingsStore;
|
|
928
|
+
if (!backend) return;
|
|
929
|
+
const raw = await backend.get.query({
|
|
930
|
+
collection: ALERTS_COLLECTION,
|
|
931
|
+
key: alertId
|
|
932
|
+
});
|
|
933
|
+
if (raw === null || raw === void 0 || typeof raw !== "object") return;
|
|
934
|
+
const existing = Object.fromEntries(Object.entries(raw));
|
|
935
|
+
const meta = existing["metadata"];
|
|
936
|
+
const metadata = typeof meta === "object" && meta !== null && !Array.isArray(meta) ? meta : {};
|
|
937
|
+
await backend.set.mutate({
|
|
938
|
+
collection: ALERTS_COLLECTION,
|
|
939
|
+
key: alertId,
|
|
940
|
+
value: {
|
|
941
|
+
...existing,
|
|
942
|
+
metadata: {
|
|
943
|
+
...metadata,
|
|
944
|
+
push
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Retry every critical alert whose push is still pending - at boot and when
|
|
951
|
+
* the notifiers or notification-center start. An alert whose state was already
|
|
952
|
+
* delivered is skipped by its key, so reconciling never sends twice.
|
|
953
|
+
*/
|
|
954
|
+
async reconcilePendingPushes() {
|
|
955
|
+
const backend = this.ctx.api.settingsStore;
|
|
956
|
+
if (!backend) return;
|
|
957
|
+
try {
|
|
958
|
+
const rows = await backend.query.query({
|
|
959
|
+
collection: ALERTS_COLLECTION,
|
|
960
|
+
filter: {
|
|
961
|
+
orderBy: {
|
|
962
|
+
field: "updatedAt",
|
|
963
|
+
direction: "desc"
|
|
964
|
+
},
|
|
965
|
+
limit: 500
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
for (const row of rows) {
|
|
969
|
+
const parsed = AlertSchema.safeParse({
|
|
970
|
+
id: row.id,
|
|
971
|
+
...row.data
|
|
972
|
+
});
|
|
973
|
+
if (!parsed.success || parsed.data.status !== "active") continue;
|
|
974
|
+
if (!CRITICAL_CATEGORIES.has(parsed.data.category)) continue;
|
|
975
|
+
const push = parsed.data.metadata?.["push"];
|
|
976
|
+
if (!isPushState(push) || push.state !== "pending") continue;
|
|
977
|
+
this.pushQueue.set(parsed.data.id, {
|
|
978
|
+
alertId: parsed.data.id,
|
|
979
|
+
key: push.key,
|
|
980
|
+
title: parsed.data.title,
|
|
981
|
+
line: String(parsed.data.metadata?.["pushLine"] ?? parsed.data.title)
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
await this.flushPushes();
|
|
985
|
+
} catch (err) {
|
|
986
|
+
this.ctx.logger?.warn("pending critical push reconcile failed", { meta: { error: errMsg(err) } });
|
|
987
|
+
}
|
|
988
|
+
}
|
|
377
989
|
async handleModelDownload(event) {
|
|
378
990
|
const data = event.data;
|
|
379
991
|
const modelId = data.modelId ?? "unknown";
|