@camstack/server 1.2.610 → 1.2.612
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/THIRD_PARTY_MODELS.md +3 -1
- package/dist/api/trpc/generated-cap-routers.js +0 -9
- package/dist/core/addon/addon-registry.service.js +33 -13
- package/dist/core/agent/cluster-node-history-store.js +43 -145
- package/dist/manual-boot.js +5 -15
- package/package.json +14 -14
- package/dist/core/runner-health/update-preflight.js +0 -62
package/THIRD_PARTY_MODELS.md
CHANGED
|
@@ -32,6 +32,7 @@ under [Non-commercial models](#non-commercial-models).
|
|
|
32
32
|
|
|
33
33
|
- © Ultralytics, AGPL-3.0. Corresponding source: the upstream Ultralytics checkpoint and build-camstack-models.py (with convert_lib.py) published at https://huggingface.co/camstack/camstack-models/tree/main/source. https://github.com/ultralytics/ultralytics
|
|
34
34
|
- Coral Edge TPU models and AIY Vision Birds V1 © Google LLC, Apache-2.0. https://github.com/google-coral/test_data
|
|
35
|
+
- YOLOv9-s EdgeTPU weights © 2025 Dan Brown (fine-tuned to 17 COCO classes, INT8, compiled for the Edge TPU; https://github.com/dbro/frigate-detector-edgetpu-yolo9, repository MIT). Derived from the YOLOv9 code and yolov9-s checkpoint of Chien-Yao Wang, I-Hau Yeh, Hong-Yuan Mark Liao, GPL-3.0 (https://github.com/WongKinYiu/yolov9). Corresponding source: the fine-tuning checkpoint (yolov9-s-relu6-best.pt), the fine-tuning and conversion notebooks and the publisher LICENSE, mirrored at https://huggingface.co/camstack/camstack-models/tree/main/source/yolov9-edgetpu-dbro. https://github.com/dbro/frigate-detector-edgetpu-yolo9
|
|
35
36
|
- © Ultralytics, AGPL-3.0 (Ultralytics YOLOv9 weights). YOLOv9 architecture: Chien-Yao Wang, I-Hau Yeh, Hong-Yuan Mark Liao, GPL-3.0 (https://github.com/WongKinYiu/yolov9). Corresponding source: the upstream Ultralytics checkpoint and build-camstack-models.py (with convert_lib.py) published at https://huggingface.co/camstack/camstack-models/tree/main/source. https://github.com/ultralytics/ultralytics
|
|
36
37
|
- RF-DETR © Roboflow, Apache-2.0. https://github.com/roboflow/rf-detr
|
|
37
38
|
- YuNet weights Copyright (c) 2020 Shiqi Yu, MIT. Training code: ShiqiYu/libfacedetection.train, BSD-3-Clause. https://github.com/opencv/opencv_zoo/tree/main/models/face_detection_yunet
|
|
@@ -48,7 +49,7 @@ under [Non-commercial models](#non-commercial-models).
|
|
|
48
49
|
|
|
49
50
|
Training data under attribution terms:
|
|
50
51
|
|
|
51
|
-
- **COCO 2017** (training data of `yolo26n-seg` and
|
|
52
|
+
- **COCO 2017** (training data of `yolo26n-seg` and 52 more): annotations CC-BY-4.0; images under their Flickr owners' terms. https://cocodataset.org/#termsofuse
|
|
52
53
|
- **WIDER FACE** (training data of `yunet-2023mar`): research-oriented terms; images CC BY-NC-ND. http://shuoyang1213.me/WIDERFACE/
|
|
53
54
|
- **Open Images V7** (training data of `animal-classifier`): annotations CC-BY-4.0 (Google LLC); images listed as CC-BY-2.0, no warranty. https://storage.googleapis.com/openimages/web/factsfigures_v7.html
|
|
54
55
|
- **DrBimmer/vehicle-classification** (training data of `vehicle-type-v1`): CC-BY-NC-4.0 — treated as binding the trained weights (conservative reading). https://huggingface.co/datasets/DrBimmer/vehicle-classification
|
|
@@ -123,6 +124,7 @@ entry kept resolvable for a saved selection but no longer offered.
|
|
|
123
124
|
| `yolo26n-seg`, `yolo26s-seg`, `yolo26m-seg` | [Ultralytics](https://github.com/ultralytics/ultralytics) | AGPL-3.0 / AGPL-3.0 (`AGPL-3.0.txt`) | COCO 2017: annotations CC-BY-4.0; images under their Flickr owners' terms | exported to ONNX, OpenVINO IR and CoreML | mirror `segmentation/yolo26-seg` |
|
|
124
125
|
| `yolo26n` (default), `yolo26s`, `yolo26m`, `yolo26l`, `yolo26x` (hidden), `yolo26n-fp16` (hidden), `yolo26n-int8`, `yolo26s-fp16` (hidden), `yolo26s-int8`, `yolo26m-fp16` (hidden), `yolo26m-int8`, `yolo26l-fp16` (hidden), `yolo26l-int8`, `yolo26n-320`, `yolo26n-320-int8`, `yolo26n-256`, `yolo26n-256-int8`, `yolo26s-320`, `yolo26s-320-int8`, `yolo26s-256`, `yolo26s-256-int8`, `yolo26m-320`, `yolo26m-320-int8`, `yolo26m-256`, `yolo26m-256-int8`, `yolo26l-320`, `yolo26l-320-int8`, `yolo26l-256`, `yolo26l-256-int8`, `yolo26x-fp16` (hidden), `yolo26x-int8` (hidden) | [Ultralytics](https://github.com/ultralytics/ultralytics) | AGPL-3.0 / AGPL-3.0 (`AGPL-3.0.txt`) | COCO 2017: annotations CC-BY-4.0; images under their Flickr owners' terms | exported to ONNX, OpenVINO IR and CoreML (scripts/build-camstack-models.py); OpenVINO FP16 export; exported to OpenVINO IR, then OpenVINO NNCF INT8 post-training quantisation; re-exported at 320×320 to ONNX, OpenVINO IR and CoreML; re-exported at 320×320 to OpenVINO IR; OpenVINO NNCF INT8 post-training quantisation; re-exported at 256×256 to ONNX, OpenVINO IR and CoreML; re-exported at 256×256 to OpenVINO IR; OpenVINO NNCF INT8 post-training quantisation | mirror `objectDetection/yolo26` |
|
|
125
126
|
| `ssd-mobilenet-v2-coco-edgetpu` (default) | [Google Coral test_data](https://github.com/google-coral/test_data) | Apache-2.0 / Apache-2.0 (`Apache-2.0.txt`) | COCO 2017: annotations CC-BY-4.0; images under their Flickr owners' terms | none | https://github.com/google-coral/test_data |
|
|
127
|
+
| `yolov9s-320-edgetpu`, `yolov9s-512-edgetpu` | [YOLOv9-s EdgeTPU (Dan Brown)](https://github.com/dbro/frigate-detector-edgetpu-yolo9) | GPL-3.0 / GPL-3.0 (`GPL-3.0.txt`) | COCO 2017: annotations CC-BY-4.0; images under their Flickr owners' terms | none: the publisher release files are mirrored byte-identical | mirror `objectDetection/yolov9` |
|
|
126
128
|
| `yolov9t-320`, `yolov9t-320-int8`, `yolov9t-640`, `yolov9t-640-int8`, `yolov9s-320`, `yolov9s-320-int8`, `yolov9s-640`, `yolov9s-640-int8`, `yolov9m-320`, `yolov9m-320-int8`, `yolov9m-640`, `yolov9m-640-int8`, `yolov9c-320`, `yolov9c-320-int8`, `yolov9c-640`, `yolov9c-640-int8` | [Ultralytics](https://github.com/ultralytics/ultralytics) | AGPL-3.0 / AGPL-3.0 (`AGPL-3.0.txt`) | COCO 2017: annotations CC-BY-4.0; images under their Flickr owners' terms | exported at 320×320 to ONNX, OpenVINO IR FP16 and CoreML; exported at 320×320 to OpenVINO IR; OpenVINO NNCF INT8 post-training quantisation; exported at 640×640 to ONNX, OpenVINO IR FP16 and CoreML; exported at 640×640 to OpenVINO IR; OpenVINO NNCF INT8 post-training quantisation | mirror `objectDetection/yolov9` |
|
|
127
129
|
|
|
128
130
|
### Package detection
|
|
@@ -11068,15 +11068,6 @@ function createCapRouter_storageAdmin(getProvider, createRemoteProxy) {
|
|
|
11068
11068
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
11069
11069
|
return p.listTables(methodInput);
|
|
11070
11070
|
}),
|
|
11071
|
-
dropCollection: trpc_middleware_js_1.adminProcedure
|
|
11072
|
-
.input(types_125.storageAdminCapability.methods.dropCollection.input.loose())
|
|
11073
|
-
.output(types_125.storageAdminCapability.methods.dropCollection.output)
|
|
11074
|
-
.mutation(async ({ input, ctx }) => {
|
|
11075
|
-
const { nodeId, ...methodInput } = input;
|
|
11076
|
-
const p = resolveProvider('storage-admin', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
11077
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
11078
|
-
return p.dropCollection(methodInput);
|
|
11079
|
-
}),
|
|
11080
11071
|
listKinds: trpc_middleware_js_1.adminProcedure
|
|
11081
11072
|
.input(types_125.storageAdminCapability.methods.listKinds.input.loose())
|
|
11082
11073
|
.output(types_125.storageAdminCapability.methods.listKinds.output)
|
|
@@ -248,17 +248,30 @@ function isShareViewKind(value) {
|
|
|
248
248
|
// new `getAddonSettings / getGlobalSettings / getDeviceSettings`
|
|
249
249
|
// endpoints on the `addon-settings` singleton capability.
|
|
250
250
|
/**
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
251
|
+
* Capabilities whose provider OWNS its readiness protocol
|
|
252
|
+
* (`autoEmitReadiness=false`): it registers before it can answer, and emits
|
|
253
|
+
* `starting`/`ready`/`down` itself. The registration-time emit would flip
|
|
254
|
+
* them `ready` early.
|
|
255
|
+
*
|
|
256
|
+
* - `platform-probe` registers synchronously from `onInitialize` while the
|
|
257
|
+
* REAL probe (hardware + embedded-Python EP scoring) still runs async;
|
|
258
|
+
* probe-gated consumers (detection-pipeline engine auto-pick) would read
|
|
259
|
+
* accelerator-blind results.
|
|
260
|
+
* - `device-history` is `ready` when its STORE is open (`onHubReachable`),
|
|
261
|
+
* not when it registered; every producer that declares on `ready`
|
|
262
|
+
* (metrics-native, pipeline-analytics) declared into a closed store and
|
|
263
|
+
* logged a declare failure on each boot.
|
|
264
|
+
*
|
|
265
|
+
* Only single-provider caps belong here: the hook is per CAPABILITY, so a cap
|
|
266
|
+
* other addons also provide (`device-removal-hook`) would lose its ready.
|
|
259
267
|
*/
|
|
268
|
+
const SELF_READYING_CAPABILITIES = new Set([
|
|
269
|
+
'platform-probe',
|
|
270
|
+
'device-history',
|
|
271
|
+
]);
|
|
272
|
+
/** Whether the `capability:provider-registered` hook may emit `ready` for a capability. */
|
|
260
273
|
function shouldEmitProviderRegisteredReady(capability) {
|
|
261
|
-
return capability
|
|
274
|
+
return !SELF_READYING_CAPABILITIES.has(capability);
|
|
262
275
|
}
|
|
263
276
|
/**
|
|
264
277
|
* Is this capability device-scoped?
|
|
@@ -2378,6 +2391,17 @@ class AddonRegistryService {
|
|
|
2378
2391
|
// (the running instance) and `entry.initialized` untouched so
|
|
2379
2392
|
// `restartAddon` still shuts the live instance down cleanly.
|
|
2380
2393
|
const existing = this.addonEntries.get(registered.declaration.id);
|
|
2394
|
+
// Another PACKAGE claiming a running addon's id (a package since folded
|
|
2395
|
+
// into the framework, still on disk) never relabels it: its declaration's
|
|
2396
|
+
// entry would then be resolved against the owner's dir. Same rule as
|
|
2397
|
+
// `AddonLoader.loadDeclaration` at boot.
|
|
2398
|
+
if (existing && existing.packageName !== registered.packageName) {
|
|
2399
|
+
this.logger.warn('loadNewAddons: skipped a package claiming a running addon id', {
|
|
2400
|
+
tags: { addonId: registered.declaration.id },
|
|
2401
|
+
meta: { skippedPackage: registered.packageName, ownerPackage: existing.packageName },
|
|
2402
|
+
});
|
|
2403
|
+
continue;
|
|
2404
|
+
}
|
|
2381
2405
|
if (existing) {
|
|
2382
2406
|
existing.packageVersion = registered.packageVersion;
|
|
2383
2407
|
existing.packageName = registered.packageName;
|
|
@@ -3376,10 +3400,6 @@ class AddonRegistryService {
|
|
|
3376
3400
|
plannedRunnerIds() {
|
|
3377
3401
|
return [...this.buildAddonGroupPlan([...this.addonEntries.keys()]).keys()];
|
|
3378
3402
|
}
|
|
3379
|
-
/** The forked addon ids (the ones whose Init the D870 guard gates). */
|
|
3380
|
-
forkedAddonIds() {
|
|
3381
|
-
return [...this.buildAddonGroupPlan([...this.addonEntries.keys()]).values()].flatMap((roster) => roster.map((r) => r.addonId));
|
|
3382
|
-
}
|
|
3383
3403
|
/**
|
|
3384
3404
|
* Snapshot of the live runner subprocesses (`$process.list`), reduced to the
|
|
3385
3405
|
* `{ runnerId, addonIds }` shape the convergence planner needs. Returns `[]`
|
|
@@ -22,27 +22,11 @@ const DescriptorSchema = zod_1.z.object({
|
|
|
22
22
|
.readonly()
|
|
23
23
|
.optional(),
|
|
24
24
|
});
|
|
25
|
-
/**
|
|
25
|
+
/** A row as the store reads it: `at` as `lastActive`, plus the descriptor. */
|
|
26
26
|
const RowDataSchema = zod_1.z.object({
|
|
27
27
|
lastActive: zod_1.z.number(),
|
|
28
28
|
descriptor: DescriptorSchema,
|
|
29
29
|
});
|
|
30
|
-
/**
|
|
31
|
-
* @durable class=ledger owner=cluster-agent
|
|
32
|
-
* write="one row per node the hub has ever seen online, GATED BY A READ-BACK.
|
|
33
|
-
* `snapshot()` runs on every 30 s topology pass but commits only when there is no
|
|
34
|
-
* existing row, when the descriptor differs field-by-field, or when the stored
|
|
35
|
-
* `lastActive` has aged past LAST_ACTIVE_HEARTBEAT_MS (5 min) — an unchanged online
|
|
36
|
-
* node therefore writes 288 times a day instead of 2,880, ~864/day for three rows
|
|
37
|
-
* (D131). Off that gate: `touch()` on `$node.disconnected` (the offline edge, always
|
|
38
|
-
* written), `recordPackages` on a node's own declaration (grow-only merge) and
|
|
39
|
-
* `prunePackages` on an explicit undeploy"
|
|
40
|
-
* retention="none — nothing ages a row out; the only deletion is the operator's
|
|
41
|
-
* `forget(nodeId)` behind the inline Forget-node action. Losing the table makes every
|
|
42
|
-
* node that is not in the live Moleculer window vanish from the topology instead of
|
|
43
|
-
* rendering OFFLINE, and empties the package roster the addon back-fill converges to."
|
|
44
|
-
*/
|
|
45
|
-
const COLLECTION = 'cluster-node-history';
|
|
46
30
|
/**
|
|
47
31
|
* How stale `lastActive` may get on an ONLINE node before an otherwise
|
|
48
32
|
* unchanged snapshot is written anyway. Five minutes turns the 30 s topology
|
|
@@ -81,12 +65,9 @@ function samePackages(a, b) {
|
|
|
81
65
|
return (a.length === b.length && a.every((p, i) => p.name === b[i]?.name && p.version === b[i]?.version));
|
|
82
66
|
}
|
|
83
67
|
class ClusterNodeHistoryStore {
|
|
84
|
-
resolveSettingsStore;
|
|
85
68
|
logger;
|
|
86
|
-
declared = false;
|
|
87
69
|
resolveKind;
|
|
88
|
-
constructor(
|
|
89
|
-
this.resolveSettingsStore = resolveSettingsStore;
|
|
70
|
+
constructor(logger, resolveKind = () => null) {
|
|
90
71
|
this.logger = logger;
|
|
91
72
|
this.resolveKind = resolveKind;
|
|
92
73
|
}
|
|
@@ -94,38 +75,6 @@ class ClusterNodeHistoryStore {
|
|
|
94
75
|
bindRecordsKind(resolver) {
|
|
95
76
|
this.resolveKind = resolver;
|
|
96
77
|
}
|
|
97
|
-
/**
|
|
98
|
-
* Idempotently declare the typed collection. Returns the resolved provider
|
|
99
|
-
* or null if the settings-store isn't available yet (early boot / hub
|
|
100
|
-
* unreachable) — callers treat null as "skip, best-effort".
|
|
101
|
-
*/
|
|
102
|
-
async ensureDeclared() {
|
|
103
|
-
if (this.resolveKind() !== null)
|
|
104
|
-
return this.resolveSettingsStore();
|
|
105
|
-
const store = this.resolveSettingsStore();
|
|
106
|
-
if (!store)
|
|
107
|
-
return null;
|
|
108
|
-
if (this.declared)
|
|
109
|
-
return store;
|
|
110
|
-
try {
|
|
111
|
-
await store.declareCollection({
|
|
112
|
-
collection: COLLECTION,
|
|
113
|
-
columns: [
|
|
114
|
-
{ name: 'id', type: 'TEXT', primaryKey: true, notNull: true },
|
|
115
|
-
{ name: 'lastActive', type: 'INTEGER', notNull: true },
|
|
116
|
-
{ name: 'descriptor', type: 'JSON', notNull: true },
|
|
117
|
-
],
|
|
118
|
-
});
|
|
119
|
-
this.declared = true;
|
|
120
|
-
return store;
|
|
121
|
-
}
|
|
122
|
-
catch (err) {
|
|
123
|
-
this.logger.warn('declareCollection failed (best-effort)', {
|
|
124
|
-
meta: { error: (0, types_1.errMsg)(err) },
|
|
125
|
-
});
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
78
|
/**
|
|
130
79
|
* Upsert a node's descriptor + addon roster while it is ONLINE, stamping
|
|
131
80
|
* `lastActive = Date.now()`. Called on each `listNodes()` refresh for every
|
|
@@ -145,19 +94,19 @@ class ClusterNodeHistoryStore {
|
|
|
145
94
|
* `$node.disconnected`, and buys 288 writes/day/node instead of 2,880.
|
|
146
95
|
*/
|
|
147
96
|
async snapshot(descriptor) {
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
97
|
+
const kind = this.resolveKind();
|
|
98
|
+
if (kind === null)
|
|
150
99
|
return;
|
|
151
100
|
try {
|
|
152
101
|
// Carry the package roster forward. `listNodes()` does not resolve it
|
|
153
102
|
// (only the reconcile does), so writing this descriptor blind would erase
|
|
154
103
|
// the back-fill's intended set on the very next topology poll.
|
|
155
|
-
const existing = await this.readStored(descriptor.id);
|
|
104
|
+
const existing = await this.readStored(kind, descriptor.id);
|
|
156
105
|
const packages = descriptor.packages ?? existing?.descriptor.packages;
|
|
157
106
|
const next = packages === undefined ? descriptor : { ...descriptor, packages };
|
|
158
107
|
if (existing !== null && !this.worthWriting(existing, next))
|
|
159
108
|
return;
|
|
160
|
-
await this.writeStored(descriptor.id, Date.now(), next);
|
|
109
|
+
await this.writeStored(kind, descriptor.id, Date.now(), next);
|
|
161
110
|
}
|
|
162
111
|
catch (err) {
|
|
163
112
|
this.logger.warn('snapshot upsert failed (best-effort)', {
|
|
@@ -177,47 +126,21 @@ class ClusterNodeHistoryStore {
|
|
|
177
126
|
return !descriptorsEqual(existing.descriptor, next);
|
|
178
127
|
}
|
|
179
128
|
/** Read one node's row, validated. `null` when absent or malformed. */
|
|
180
|
-
async readStored(nodeId) {
|
|
181
|
-
const
|
|
182
|
-
if (
|
|
183
|
-
const row = await kind.get(nodeId);
|
|
184
|
-
if (row === null)
|
|
185
|
-
return null;
|
|
186
|
-
const parsed = RowDataSchema.safeParse({
|
|
187
|
-
lastActive: row.at ?? 0,
|
|
188
|
-
descriptor: row.data.descriptor,
|
|
189
|
-
});
|
|
190
|
-
return parsed.success ? parsed.data : null;
|
|
191
|
-
}
|
|
192
|
-
const store = this.resolveSettingsStore();
|
|
193
|
-
if (!store)
|
|
129
|
+
async readStored(kind, nodeId) {
|
|
130
|
+
const row = await kind.get(nodeId);
|
|
131
|
+
if (row === null)
|
|
194
132
|
return null;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (raw === null || raw === undefined)
|
|
200
|
-
return null;
|
|
201
|
-
const parsed = RowDataSchema.safeParse(raw);
|
|
133
|
+
const parsed = RowDataSchema.safeParse({
|
|
134
|
+
lastActive: row.at ?? 0,
|
|
135
|
+
descriptor: row.data.descriptor,
|
|
136
|
+
});
|
|
202
137
|
return parsed.success ? parsed.data : null;
|
|
203
138
|
}
|
|
204
|
-
async writeStored(nodeId, lastActive, descriptor) {
|
|
205
|
-
const
|
|
206
|
-
if (
|
|
207
|
-
const body = system_1.ClusterNodeHistoryBodySchema.safeParse({ descriptor });
|
|
208
|
-
if (!body.success)
|
|
209
|
-
return;
|
|
210
|
-
await kind.put({ id: nodeId, at: lastActive, data: body.data });
|
|
139
|
+
async writeStored(kind, nodeId, lastActive, descriptor) {
|
|
140
|
+
const body = system_1.ClusterNodeHistoryBodySchema.safeParse({ descriptor });
|
|
141
|
+
if (!body.success)
|
|
211
142
|
return;
|
|
212
|
-
}
|
|
213
|
-
const store = this.resolveSettingsStore();
|
|
214
|
-
if (!store)
|
|
215
|
-
return;
|
|
216
|
-
await store.set({
|
|
217
|
-
collection: COLLECTION,
|
|
218
|
-
key: nodeId,
|
|
219
|
-
value: { lastActive, descriptor },
|
|
220
|
-
});
|
|
143
|
+
await kind.put({ id: nodeId, at: lastActive, data: body.data });
|
|
221
144
|
}
|
|
222
145
|
/** A placeholder descriptor for a node the topology pass has not seen yet. */
|
|
223
146
|
static placeholderDescriptor(nodeId) {
|
|
@@ -242,11 +165,11 @@ class ClusterNodeHistoryStore {
|
|
|
242
165
|
* nothing is delivered rather than something guessed.
|
|
243
166
|
*/
|
|
244
167
|
async getPackages(nodeId) {
|
|
245
|
-
const
|
|
246
|
-
if (
|
|
168
|
+
const kind = this.resolveKind();
|
|
169
|
+
if (kind === null)
|
|
247
170
|
return [];
|
|
248
171
|
try {
|
|
249
|
-
return (await this.readStored(nodeId))?.descriptor.packages ?? [];
|
|
172
|
+
return (await this.readStored(kind, nodeId))?.descriptor.packages ?? [];
|
|
250
173
|
}
|
|
251
174
|
catch (err) {
|
|
252
175
|
this.logger.warn('getPackages failed (best-effort)', {
|
|
@@ -267,11 +190,11 @@ class ClusterNodeHistoryStore {
|
|
|
267
190
|
async recordPackages(nodeId, packages) {
|
|
268
191
|
if (packages.length === 0)
|
|
269
192
|
return;
|
|
270
|
-
const
|
|
271
|
-
if (
|
|
193
|
+
const kind = this.resolveKind();
|
|
194
|
+
if (kind === null)
|
|
272
195
|
return;
|
|
273
196
|
try {
|
|
274
|
-
const existing = await this.readStored(nodeId);
|
|
197
|
+
const existing = await this.readStored(kind, nodeId);
|
|
275
198
|
const merged = new Map((existing?.descriptor.packages ?? []).map((p) => [p.name, p]));
|
|
276
199
|
// The node's current declaration wins on version — it is the authority on
|
|
277
200
|
// what it is actually running.
|
|
@@ -281,7 +204,7 @@ class ClusterNodeHistoryStore {
|
|
|
281
204
|
...(existing?.descriptor ?? ClusterNodeHistoryStore.placeholderDescriptor(nodeId)),
|
|
282
205
|
packages: [...merged.values()],
|
|
283
206
|
};
|
|
284
|
-
await this.writeStored(nodeId, existing?.lastActive ?? Date.now(), descriptor);
|
|
207
|
+
await this.writeStored(kind, nodeId, existing?.lastActive ?? Date.now(), descriptor);
|
|
285
208
|
}
|
|
286
209
|
catch (err) {
|
|
287
210
|
this.logger.warn('recordPackages failed (best-effort)', {
|
|
@@ -298,11 +221,11 @@ class ClusterNodeHistoryStore {
|
|
|
298
221
|
async prunePackages(nodeId, names) {
|
|
299
222
|
if (names.length === 0)
|
|
300
223
|
return;
|
|
301
|
-
const
|
|
302
|
-
if (
|
|
224
|
+
const kind = this.resolveKind();
|
|
225
|
+
if (kind === null)
|
|
303
226
|
return;
|
|
304
227
|
try {
|
|
305
|
-
const existing = await this.readStored(nodeId);
|
|
228
|
+
const existing = await this.readStored(kind, nodeId);
|
|
306
229
|
if (existing === null)
|
|
307
230
|
return;
|
|
308
231
|
const current = existing.descriptor.packages;
|
|
@@ -312,7 +235,7 @@ class ClusterNodeHistoryStore {
|
|
|
312
235
|
const remaining = current.filter((p) => !drop.has(p.name));
|
|
313
236
|
if (remaining.length === current.length)
|
|
314
237
|
return;
|
|
315
|
-
await this.writeStored(nodeId, existing.lastActive, {
|
|
238
|
+
await this.writeStored(kind, nodeId, existing.lastActive, {
|
|
316
239
|
...existing.descriptor,
|
|
317
240
|
packages: remaining,
|
|
318
241
|
});
|
|
@@ -334,27 +257,14 @@ class ClusterNodeHistoryStore {
|
|
|
334
257
|
* time. A no-op if the node was never snapshotted (nothing to update).
|
|
335
258
|
*/
|
|
336
259
|
async touch(nodeId) {
|
|
337
|
-
const
|
|
338
|
-
if (
|
|
260
|
+
const kind = this.resolveKind();
|
|
261
|
+
if (kind === null)
|
|
339
262
|
return;
|
|
340
263
|
try {
|
|
341
|
-
const existing = await this.readStored(nodeId);
|
|
264
|
+
const existing = await this.readStored(kind, nodeId);
|
|
342
265
|
if (existing === null)
|
|
343
266
|
return;
|
|
344
|
-
|
|
345
|
-
if (kind !== null) {
|
|
346
|
-
await kind.put({
|
|
347
|
-
id: nodeId,
|
|
348
|
-
at: Date.now(),
|
|
349
|
-
data: { descriptor: existing.descriptor },
|
|
350
|
-
});
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
await store.update({
|
|
354
|
-
collection: COLLECTION,
|
|
355
|
-
id: nodeId,
|
|
356
|
-
data: { lastActive: Date.now() },
|
|
357
|
-
});
|
|
267
|
+
await this.writeStored(kind, nodeId, Date.now(), existing.descriptor);
|
|
358
268
|
}
|
|
359
269
|
catch (err) {
|
|
360
270
|
this.logger.warn('touch failed (best-effort)', {
|
|
@@ -369,27 +279,21 @@ class ClusterNodeHistoryStore {
|
|
|
369
279
|
}
|
|
370
280
|
/**
|
|
371
281
|
* Read the full persisted history, validated, or `null` when the store did
|
|
372
|
-
* not ANSWER (
|
|
282
|
+
* not ANSWER (the kind is not bound yet, or the read threw). A caller that must not claim
|
|
373
283
|
* a node absent from an unanswered read (D315) uses this; `getAll` folds the
|
|
374
284
|
* `null` into `[]` for the callers that only render what exists. Malformed
|
|
375
285
|
* rows are dropped (and logged) — the rest of the answer still stands.
|
|
376
286
|
*/
|
|
377
287
|
async tryGetAll() {
|
|
378
|
-
const
|
|
379
|
-
if (
|
|
288
|
+
const kind = this.resolveKind();
|
|
289
|
+
if (kind === null)
|
|
380
290
|
return null;
|
|
381
291
|
try {
|
|
382
|
-
const kind = this.resolveKind();
|
|
383
292
|
const result = [];
|
|
384
|
-
const rows = kind
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}))
|
|
389
|
-
: (await store.query({ collection: COLLECTION })).map((row) => ({
|
|
390
|
-
id: row.id,
|
|
391
|
-
data: row.data,
|
|
392
|
-
}));
|
|
293
|
+
const rows = (await (0, system_1.readAllRows)(kind)).map((row) => ({
|
|
294
|
+
id: row.id,
|
|
295
|
+
data: { lastActive: row.at ?? 0, descriptor: row.data.descriptor },
|
|
296
|
+
}));
|
|
393
297
|
for (const row of rows) {
|
|
394
298
|
const parsed = RowDataSchema.safeParse(row.data);
|
|
395
299
|
if (!parsed.success) {
|
|
@@ -416,21 +320,15 @@ class ClusterNodeHistoryStore {
|
|
|
416
320
|
* 2026-09-01 all three reached the UI as `{"success": true}`.
|
|
417
321
|
*/
|
|
418
322
|
async forget(nodeId) {
|
|
419
|
-
const
|
|
420
|
-
if (
|
|
323
|
+
const kind = this.resolveKind();
|
|
324
|
+
if (kind === null)
|
|
421
325
|
return 'store-unavailable';
|
|
422
326
|
try {
|
|
423
327
|
// Read first: `delete` on this provider is idempotent and cannot tell a
|
|
424
328
|
// removal from a no-op, and "there was nothing to remove" is the answer
|
|
425
329
|
// that stops an operator repeating a working action forever.
|
|
426
|
-
const existed = (await this.readStored(nodeId)) !== null;
|
|
427
|
-
|
|
428
|
-
if (kind !== null) {
|
|
429
|
-
await kind.delete(nodeId);
|
|
430
|
-
}
|
|
431
|
-
else {
|
|
432
|
-
await store.delete({ collection: COLLECTION, key: nodeId });
|
|
433
|
-
}
|
|
330
|
+
const existed = (await this.readStored(kind, nodeId)) !== null;
|
|
331
|
+
await kind.delete(nodeId);
|
|
434
332
|
if (!existed) {
|
|
435
333
|
this.logger.info('cluster node had no history row to forget', { tags: { nodeId } });
|
|
436
334
|
return 'not-found';
|
package/dist/manual-boot.js
CHANGED
|
@@ -76,7 +76,6 @@ const toast_wrapper_service_1 = require("./core/notification/toast-wrapper.servi
|
|
|
76
76
|
const repl_engine_service_1 = require("./core/repl/repl-engine.service");
|
|
77
77
|
const server_update_service_1 = require("./core/server-update/server-update.service");
|
|
78
78
|
const runner_boot_check_1 = require("./core/runner-health/runner-boot-check");
|
|
79
|
-
const update_preflight_1 = require("./core/runner-health/update-preflight");
|
|
80
79
|
const storage_service_1 = require("./core/storage/storage.service");
|
|
81
80
|
const stream_probe_service_1 = require("./core/streaming/stream-probe.service");
|
|
82
81
|
const topology_emitter_service_1 = require("./core/topology/topology-emitter.service");
|
|
@@ -163,11 +162,11 @@ async function bootManual(opts) {
|
|
|
163
162
|
// Same post-construction reason: the cluster-node lifecycle (forget, catalog
|
|
164
163
|
// removal) must leave a per-node line, and `console.log` is not a log store.
|
|
165
164
|
agentRegistryService.setLogger(loggingService.createLogger('ClusterNodes'));
|
|
166
|
-
// A1: durable, routing-blind offline-node history —
|
|
167
|
-
//
|
|
168
|
-
// Wired post-construction so the service's
|
|
169
|
-
|
|
170
|
-
|
|
165
|
+
// A1: durable, routing-blind offline-node history — the `cluster-agent`
|
|
166
|
+
// records kind, bound by the addon registry once the records provider is up
|
|
167
|
+
// (unavailable until then, D902). Wired post-construction so the service's
|
|
168
|
+
// 3-arg constructor stays intact.
|
|
169
|
+
const clusterNodeHistoryStore = new cluster_node_history_store_1.ClusterNodeHistoryStore(loggingService.createLogger('cluster-node-history'));
|
|
171
170
|
agentRegistryService.setClusterNodeHistoryStore(clusterNodeHistoryStore);
|
|
172
171
|
addonRegistryService.setClusterNodeHistoryStore(clusterNodeHistoryStore);
|
|
173
172
|
// ---- Layer 4: need AddonRegistry ---------------------------------------
|
|
@@ -287,16 +286,7 @@ async function bootManual(opts) {
|
|
|
287
286
|
});
|
|
288
287
|
},
|
|
289
288
|
});
|
|
290
|
-
const settingsStoreOrNull = () => capabilityService.getRegistry()?.getSingleton('settings-store') ?? null;
|
|
291
289
|
const serverUpdateService = new server_update_service_1.ServerUpdateService({
|
|
292
|
-
updatePreflight: (0, update_preflight_1.buildUpdatePreflight)({
|
|
293
|
-
logger: loggingService.createLogger('UpdatePreflight'),
|
|
294
|
-
forkedAddonIds: () => addonRegistryService.forkedAddonIds(),
|
|
295
|
-
store: () => {
|
|
296
|
-
const store = settingsStoreOrNull();
|
|
297
|
-
return store === null ? null : (0, update_preflight_1.legacyGuardClientOf)(store);
|
|
298
|
-
},
|
|
299
|
-
}),
|
|
300
290
|
runnerBootCheck: () => hubRunnerBootCheck.current(),
|
|
301
291
|
logger: loggingService.createLogger('ServerUpdate'),
|
|
302
292
|
restartServer: (requestedBy, update) => addonPackageService.restartServer(requestedBy, update),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.612",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"files": [
|
|
@@ -38,19 +38,19 @@
|
|
|
38
38
|
]
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@camstack/addon-admin-ui": "1.2.
|
|
42
|
-
"@camstack/addon-agent-ui": "1.2.
|
|
43
|
-
"@camstack/addon-auth": "1.2.
|
|
44
|
-
"@camstack/addon-decoder-nodeav": "1.2.
|
|
45
|
-
"@camstack/addon-notifiers": "1.2.
|
|
46
|
-
"@camstack/addon-pipeline": "1.2.
|
|
47
|
-
"@camstack/addon-pipeline-orchestrator": "1.2.
|
|
48
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
49
|
-
"@camstack/develop": "0.1.
|
|
50
|
-
"@camstack/sdk": "1.2.
|
|
51
|
-
"@camstack/system": "1.2.
|
|
52
|
-
"@camstack/types": "1.2.
|
|
53
|
-
"@camstack/ui-library": "1.2.
|
|
41
|
+
"@camstack/addon-admin-ui": "1.2.448",
|
|
42
|
+
"@camstack/addon-agent-ui": "1.2.254",
|
|
43
|
+
"@camstack/addon-auth": "1.2.159",
|
|
44
|
+
"@camstack/addon-decoder-nodeav": "1.2.134",
|
|
45
|
+
"@camstack/addon-notifiers": "1.2.612",
|
|
46
|
+
"@camstack/addon-pipeline": "1.2.612",
|
|
47
|
+
"@camstack/addon-pipeline-orchestrator": "1.2.614",
|
|
48
|
+
"@camstack/addon-post-analysis": "1.2.619",
|
|
49
|
+
"@camstack/develop": "0.1.24",
|
|
50
|
+
"@camstack/sdk": "1.2.144",
|
|
51
|
+
"@camstack/system": "1.2.462",
|
|
52
|
+
"@camstack/types": "1.2.394",
|
|
53
|
+
"@camstack/ui-library": "1.2.311",
|
|
54
54
|
"@fastify/compress": "^9.0.0",
|
|
55
55
|
"@fastify/cookie": "^11.0.2",
|
|
56
56
|
"@fastify/cors": "^11.2.0",
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LEGACY_BLOB_GUARD_SINCE = void 0;
|
|
4
|
-
exports.buildUpdatePreflight = buildUpdatePreflight;
|
|
5
|
-
exports.legacyGuardClientOf = legacyGuardClientOf;
|
|
6
|
-
/**
|
|
7
|
-
* The `applyServerUpdate` pre-flight (D877).
|
|
8
|
-
*
|
|
9
|
-
* Question: would the TARGET version refuse to start any addon on THIS node's
|
|
10
|
-
* live data? Today there is one deterministic Init refusal the framework can
|
|
11
|
-
* pre-check: the D870 legacy-blob guard (`assertLegacyBlobsMigrated`) - an addon
|
|
12
|
-
* whose settings still sit in its per-addon blob tables while the standard store
|
|
13
|
-
* holds none.
|
|
14
|
-
*
|
|
15
|
-
* The store-shape check is deliberately version-independent: the live store is
|
|
16
|
-
* what it is, so the only version question is "does the target contain the
|
|
17
|
-
* guard?" - answered by {@link LEGACY_BLOB_GUARD_SINCE}. The honest limit: it
|
|
18
|
-
* runs inside the version that ships it, so it protects updates FROM this
|
|
19
|
-
* version onwards; an update from an older hub never ran it. And it knows only
|
|
20
|
-
* the refusals listed here - a new deterministic Init refusal has to be added
|
|
21
|
-
* to this module to be pre-checked.
|
|
22
|
-
*/
|
|
23
|
-
const system_1 = require("@camstack/system");
|
|
24
|
-
const index_js_1 = require("../../server-root/index.js");
|
|
25
|
-
/** The first server version whose addons run `assertLegacyBlobsMigrated` at Init. */
|
|
26
|
-
exports.LEGACY_BLOB_GUARD_SINCE = '1.2.585';
|
|
27
|
-
function buildUpdatePreflight(deps) {
|
|
28
|
-
return async (targetVersion) => {
|
|
29
|
-
if ((0, index_js_1.compareSemver)(targetVersion, exports.LEGACY_BLOB_GUARD_SINCE) < 0)
|
|
30
|
-
return [];
|
|
31
|
-
const store = deps.store();
|
|
32
|
-
if (store === null) {
|
|
33
|
-
throw new Error('the settings store is not available to pre-check the update');
|
|
34
|
-
}
|
|
35
|
-
const blockers = [];
|
|
36
|
-
for (const addonId of deps.forkedAddonIds()) {
|
|
37
|
-
try {
|
|
38
|
-
await (0, system_1.assertLegacyBlobsMigrated)(store, addonId, deps.logger);
|
|
39
|
-
}
|
|
40
|
-
catch (err) {
|
|
41
|
-
// A refusal is the finding; any other error is "could not ask" and the
|
|
42
|
-
// caller treats a throw as exactly that (it never blocks on it).
|
|
43
|
-
if (!(err instanceof system_1.UnmigratedSettingsError))
|
|
44
|
-
throw err;
|
|
45
|
-
blockers.push({
|
|
46
|
-
addonId,
|
|
47
|
-
reason: err.message,
|
|
48
|
-
remedy: system_1.UNMIGRATED_SETTINGS_REMEDY,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return blockers;
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
/** The three store calls the guard makes, over the hub's `settings-store` provider. */
|
|
56
|
-
function legacyGuardClientOf(store) {
|
|
57
|
-
return {
|
|
58
|
-
query: { query: (input) => store.query(input) },
|
|
59
|
-
get: { query: (input) => store.get(input) },
|
|
60
|
-
count: { query: (input) => store.count(input) },
|
|
61
|
-
};
|
|
62
|
-
}
|