@trillboards/edge-sdk 0.2.81 → 0.2.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/deploy/detector/README.md +108 -21
  2. package/deploy/detector/bench/verify_against_reference.py +6 -1
  3. package/deploy/detector/pyproject.toml +1 -1
  4. package/deploy/detector/tests/test_loader.py +188 -0
  5. package/deploy/detector/tests/test_modelpath.py +48 -3
  6. package/deploy/detector/tests/test_registry.py +880 -0
  7. package/deploy/detector/tests/test_server.py +163 -7
  8. package/deploy/detector/tests/test_session.py +29 -10
  9. package/deploy/detector/trillboards_detector/__init__.py +1 -1
  10. package/deploy/detector/trillboards_detector/cli.py +49 -2
  11. package/deploy/detector/trillboards_detector/detections.py +98 -0
  12. package/deploy/detector/trillboards_detector/errors.py +38 -0
  13. package/deploy/detector/trillboards_detector/labels.py +239 -24
  14. package/deploy/detector/trillboards_detector/loader.py +186 -148
  15. package/deploy/detector/trillboards_detector/modelpath.py +32 -0
  16. package/deploy/detector/trillboards_detector/registry.py +836 -0
  17. package/deploy/detector/trillboards_detector/server.py +59 -40
  18. package/deploy/detector/trillboards_detector/session.py +84 -55
  19. package/dist/EdgeAgent.js +1 -1
  20. package/dist/SensingProfileClient.d.ts +1 -1
  21. package/dist/SensingProfileClient.d.ts.map +1 -1
  22. package/dist/SensingProfileClient.js +20 -0
  23. package/dist/SensingProfileClient.js.map +1 -1
  24. package/dist/autoSense.d.ts +6 -5
  25. package/dist/autoSense.d.ts.map +1 -1
  26. package/dist/autoSense.js +12 -2
  27. package/dist/autoSense.js.map +1 -1
  28. package/dist/cli.d.ts.map +1 -1
  29. package/dist/cli.js +11 -6
  30. package/dist/cli.js.map +1 -1
  31. package/dist/diagnose.d.ts +1 -1
  32. package/dist/diagnose.js +1 -1
  33. package/dist/modelOta.d.ts +274 -0
  34. package/dist/modelOta.d.ts.map +1 -0
  35. package/dist/modelOta.js +492 -0
  36. package/dist/modelOta.js.map +1 -0
  37. package/dist/sensingRunMode.d.ts +21 -0
  38. package/dist/sensingRunMode.d.ts.map +1 -1
  39. package/dist/sensingRunMode.js +32 -7
  40. package/dist/sensingRunMode.js.map +1 -1
  41. package/package.json +1 -1
@@ -0,0 +1,492 @@
1
+ "use strict";
2
+ /**
3
+ * modelOta — deliver EVERY model the server granted this connector, not just one.
4
+ *
5
+ * ── The gap this closes ────────────────────────────────────────────────────
6
+ *
7
+ * The connector's whole model-delivery surface was ONE hardcoded `modelType`:
8
+ * `cli.ts` called `startGemmaModelOta(config)` and nothing else. No writer of
9
+ * `models/<id>/model.onnx` existed anywhere — `modelpath.py` only READS that
10
+ * path. So `rfdetr_medium` could be registry-active with a live 10% canary,
11
+ * three installers reporting healthy and `devices_succeeded = 0`, while the
12
+ * detector service 503'd forever waiting for an artifact nothing was sending.
13
+ * That is the `yolov8_nano` shape (offered everywhere, loadable nowhere) with
14
+ * the delivery leg missing instead of the producer leg.
15
+ *
16
+ * The SERVER side was already generic — `getGlobalVlmModelDescriptor` resolves
17
+ * any `modelType` through `resolveGlobalOtaRegistryId`, which accepts a registry
18
+ * `model_id` directly and gates non-alias types through the device manifest,
19
+ * failing closed. Nothing there needed changing. The missing half was here.
20
+ *
21
+ * ── Shape ──────────────────────────────────────────────────────────────────
22
+ *
23
+ * `startModelOta(config, models)` composes two INDEPENDENT watchers:
24
+ *
25
+ * 1. the VLM watcher — `startGemmaModelOta`, called unchanged. It keeps its
26
+ * own gating (`modelOtaEnabled`, the refusal to overwrite a custom
27
+ * litert-lm model), its own litert-lm STORE path, its own XNNPACK cache
28
+ * clear and its own `litert-lm serve` restart. This module adds nothing to
29
+ * it and takes nothing away: `gemma-vlm` behaviour is byte-identical, and
30
+ * a test asserts that against a fake manager rather than trusting it.
31
+ *
32
+ * 2. the SPECIALIST watcher — its own `ModelManager`, its own models dir and
33
+ * therefore its own `models_manifest.json`.
34
+ *
35
+ * TWO managers and not one, for a reason that is not tidiness: the VLM watcher
36
+ * declines to run at all when the operator pointed `localSensing.model` at a
37
+ * custom litert-lm model, and a specialist must still be delivered in that
38
+ * state. A shared manager built inside `startGemmaModelOta` cannot express
39
+ * that. Separate managers also make the byte-identical claim structural — the
40
+ * VLM's manifest, poll loop and event stream are untouched by anything here.
41
+ *
42
+ * ── What may be delivered, and what is refused OUT LOUD ────────────────────
43
+ *
44
+ * A capability is offered only where a producer will write its answer down. The
45
+ * connector delivers a granted model only when it has a DESTINATION for its
46
+ * format — today `onnx`, whose destination is the path `modelpath.py` computes
47
+ * and the detector service reads. Every other format is SKIPPED WITH ITS REASON
48
+ * (once per model, not once per poll): `tflite`/`mlkit`/`mediapipe` are Android
49
+ * loaders with no runtime on this box, and `litert-lm` is the VLM watcher's own
50
+ * business. A format we cannot place is never guessed at — a near-miss path is
51
+ * a silent no-delivery that reports success, which is the failure this file
52
+ * exists to end.
53
+ *
54
+ * ── Non-fatal, always ──────────────────────────────────────────────────────
55
+ *
56
+ * Every failure here means the connector keeps serving what it already has. A
57
+ * specialist that cannot download must not stop the VLM, and must not stop
58
+ * capture. `ModelManager`'s atomic `.tmp` + rename + SHA-256 verify means a
59
+ * failed download leaves NO artifact rather than a truncated one, so the
60
+ * detector service keeps answering 503 *with the reason* instead of silently
61
+ * serving no detections. Down is never empty.
62
+ *
63
+ * Node built-ins only (survives bundling into the self-contained connector;
64
+ * trivially mockable in tests).
65
+ */
66
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
67
+ if (k2 === undefined) k2 = k;
68
+ var desc = Object.getOwnPropertyDescriptor(m, k);
69
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
70
+ desc = { enumerable: true, get: function() { return m[k]; } };
71
+ }
72
+ Object.defineProperty(o, k2, desc);
73
+ }) : (function(o, m, k, k2) {
74
+ if (k2 === undefined) k2 = k;
75
+ o[k2] = m[k];
76
+ }));
77
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
78
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
79
+ }) : function(o, v) {
80
+ o["default"] = v;
81
+ });
82
+ var __importStar = (this && this.__importStar) || (function () {
83
+ var ownKeys = function(o) {
84
+ ownKeys = Object.getOwnPropertyNames || function (o) {
85
+ var ar = [];
86
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
87
+ return ar;
88
+ };
89
+ return ownKeys(o);
90
+ };
91
+ return function (mod) {
92
+ if (mod && mod.__esModule) return mod;
93
+ var result = {};
94
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
95
+ __setModuleDefault(result, mod);
96
+ return result;
97
+ };
98
+ })();
99
+ Object.defineProperty(exports, "__esModule", { value: true });
100
+ exports.CONNECTOR_LOADABLE_MODEL_FORMATS = void 0;
101
+ exports.resolveOnnxModelPath = resolveOnnxModelPath;
102
+ exports.destinationForGrantedModel = destinationForGrantedModel;
103
+ exports.buildGrantedModelSource = buildGrantedModelSource;
104
+ exports.startModelOta = startModelOta;
105
+ const os = __importStar(require("os"));
106
+ const path = __importStar(require("path"));
107
+ const edge_core_1 = require("@trillboards/edge-core");
108
+ const gemmaModelOta_1 = require("./gemmaModelOta");
109
+ /**
110
+ * Formats this connector has a DESTINATION for — i.e. a place to write the
111
+ * artifact where something on this box will read it.
112
+ *
113
+ * `onnx` only, and that is a statement about what exists rather than a
114
+ * limitation to apologise for: `deploy/detector` reads
115
+ * `${TRILLBOARDS_DATA_DIR:-~/.trillboards}/models/<id>/model.onnx` and nothing
116
+ * else on the connector loads a model file at all. Widening this list is an edit
117
+ * that must arrive WITH its reader, in the same commit — the same discipline
118
+ * `advertisedClasses` applies to the class vocabulary.
119
+ */
120
+ exports.CONNECTOR_LOADABLE_MODEL_FORMATS = Object.freeze(['onnx']);
121
+ /** Fixed file name inside the per-model directory — `modelpath.py`'s `MODEL_FILE_NAME`. */
122
+ const ONNX_MODEL_FILE_NAME = 'model.onnx';
123
+ // ─── Destination resolution ─────────────────────────────────────────────────
124
+ /**
125
+ * Where an ONNX artifact must land, mirroring
126
+ * `deploy/detector/trillboards_detector/modelpath.py` EXACTLY:
127
+ *
128
+ * ${TRILLBOARDS_DATA_DIR:-${HOME:-${USERPROFILE:-/tmp}}/.trillboards}
129
+ * /models/<model-id>/model.onnx
130
+ *
131
+ * THE ENVIRONMENT, NOT `config.dataDir`. The reader is a separate PROCESS — a
132
+ * LaunchAgent, a systemd unit or a Windows Scheduled Task — that resolves this
133
+ * path from its own environment and has never heard of the connector's config
134
+ * file. Writing to `config.dataDir` would put the artifact somewhere the service
135
+ * does not look, which is a permanently-503 detector on a box whose model is
136
+ * sitting right there. `trillboards-detector.service` pins `HOME=/opt/trillboards`
137
+ * for precisely this reason and says so.
138
+ *
139
+ * `TRILLBOARDS_DETECTOR_MODEL` is NOT honoured here. It is `modelpath.py`'s bench
140
+ * escape hatch that points the SERVICE at an arbitrary file; an OTA that followed
141
+ * it would overwrite whatever a bench run was pointed at.
142
+ *
143
+ * One directory per model NAME, never per version — `ModelManager` rewrites the
144
+ * one file in place, so an update reclaims the old bytes by definition. A
145
+ * version-named directory would add a second leak on a path that already orphans
146
+ * ~752 MiB per Gemma update and has no GC.
147
+ */
148
+ function resolveOnnxModelPath(opts) {
149
+ const env = opts.env ?? process.env;
150
+ const override = (env.TRILLBOARDS_DATA_DIR || '').trim();
151
+ const dataDir = override
152
+ || path.join((env.HOME || '').trim() || (env.USERPROFILE || '').trim() || '/tmp', '.trillboards');
153
+ return path.join(dataDir, 'models', opts.modelId, ONNX_MODEL_FILE_NAME);
154
+ }
155
+ /**
156
+ * Decide a granted model's fate. A `skip` ALWAYS carries a reason — the whole
157
+ * point of this surface is that an exclusion is a stated fact rather than a
158
+ * silent drop, so there is no branch that returns nothing.
159
+ */
160
+ function destinationForGrantedModel(model, opts = {}) {
161
+ const modelId = typeof model?.modelId === 'string' ? model.modelId.trim() : '';
162
+ if (!modelId) {
163
+ return { kind: 'skip', modelId: String(model?.modelId ?? ''), reason: 'no model id' };
164
+ }
165
+ // A path traversal in a server-supplied id would let a compromised or buggy
166
+ // manifest write outside the model store. The ids are `sensing_model_registry`
167
+ // primary keys — snake_case identifiers — so anything else is refused rather
168
+ // than sanitised: silently rewriting an id would put the artifact at a path
169
+ // the reader does not compute, which is the no-delivery case again.
170
+ if (!/^[A-Za-z0-9._-]+$/.test(modelId) || modelId === '.' || modelId === '..') {
171
+ return { kind: 'skip', modelId, reason: `model id is not a bare registry identifier` };
172
+ }
173
+ const format = typeof model.modelFormat === 'string' ? model.modelFormat.trim().toLowerCase() : '';
174
+ if (!format) {
175
+ return { kind: 'skip', modelId, reason: 'the server declared no model_format for it' };
176
+ }
177
+ if (format === 'litert-lm') {
178
+ return {
179
+ kind: 'skip',
180
+ modelId,
181
+ reason: 'litert-lm artifacts are the VLM watcher\'s own business (store path + serve restart)',
182
+ };
183
+ }
184
+ if (!exports.CONNECTOR_LOADABLE_MODEL_FORMATS.includes(format)) {
185
+ return {
186
+ kind: 'skip',
187
+ modelId,
188
+ reason: `format '${format}' has no reader on this connector `
189
+ + `(loadable: ${exports.CONNECTOR_LOADABLE_MODEL_FORMATS.join(', ')})`,
190
+ };
191
+ }
192
+ return { kind: 'download', modelId, filePath: resolveOnnxModelPath({ modelId, env: opts.env }) };
193
+ }
194
+ /**
195
+ * Union the granted lists of every feed's screen.
196
+ *
197
+ * A connector is ONE box with ONE model store but N feeds on N screens, whose
198
+ * manifests can differ. The union is the right answer: a model granted to ANY of
199
+ * this box's screens has to be on this box's disk for that screen to be served.
200
+ * A screen that fails to answer simply does not contribute, and a total failure
201
+ * returns null so the caller keeps what it already had — the same
202
+ * failure-is-silence-not-a-reset rule `deployedCadenceSource` uses, and for the
203
+ * same reason: snapping back on a blip is worse than being briefly stale.
204
+ */
205
+ function buildGrantedModelSource(options) {
206
+ const feeds = [
207
+ ...(options.feeds || []).filter((f) => f && f.client),
208
+ ...(options.profileClients || []).filter(Boolean).map((client) => ({ screenId: null, client })),
209
+ ];
210
+ if (feeds.length === 0)
211
+ return undefined;
212
+ return {
213
+ async resolve() {
214
+ const results = await Promise.all(feeds.map(async (f) => ({
215
+ screenId: f.screenId,
216
+ profile: await f.client.fetchProfile().catch(() => null),
217
+ })));
218
+ let anyAnswered = false;
219
+ const byId = new Map();
220
+ for (const { screenId, profile } of results) {
221
+ if (!profile)
222
+ continue;
223
+ anyAnswered = true;
224
+ for (const m of Array.isArray(profile.models) ? profile.models : []) {
225
+ if (!m || typeof m.modelId !== 'string' || !m.modelId)
226
+ continue;
227
+ const existing = byId.get(m.modelId);
228
+ // First non-null format wins; a screen that reports null for a model
229
+ // another screen typed must not erase the type. Same rule for the
230
+ // granting screen: the FIRST screen that could name itself owns the
231
+ // download identity, so a later screen answering with none cannot
232
+ // demote a model back to the host fingerprint.
233
+ if (!existing) {
234
+ byId.set(m.modelId, {
235
+ modelId: m.modelId,
236
+ modelFormat: typeof m.modelFormat === 'string' ? m.modelFormat : null,
237
+ grantedByScreenId: screenId || null,
238
+ });
239
+ }
240
+ else {
241
+ if (existing.modelFormat === null && typeof m.modelFormat === 'string') {
242
+ existing.modelFormat = m.modelFormat;
243
+ }
244
+ if (!existing.grantedByScreenId && screenId)
245
+ existing.grantedByScreenId = screenId;
246
+ }
247
+ }
248
+ }
249
+ if (!anyAnswered)
250
+ return null;
251
+ return [...byId.values()].sort((a, b) => a.modelId.localeCompare(b.modelId));
252
+ },
253
+ };
254
+ }
255
+ /** How often to re-ask what this device was granted, when a source is supplied. */
256
+ const DEFAULT_GRANTED_POLL_MS = 15 * 60 * 1000;
257
+ /**
258
+ * Start the connector's model OTA: the VLM watcher plus one download per granted
259
+ * specialist whose format this box can place.
260
+ *
261
+ * NON-FATAL throughout — a thrown error is swallowed and logged, and the VLM
262
+ * watcher is started FIRST so a specialist failure can never take it down.
263
+ */
264
+ function startModelOta(config, models = [], deps = {}) {
265
+ const log = deps.log ?? ((m) => console.log(m));
266
+ // 1. The VLM, FIRST and unchanged. If everything below throws, this survives.
267
+ let vlm = null;
268
+ try {
269
+ vlm = (deps.startVlmOta ?? gemmaModelOta_1.startGemmaModelOta)(config);
270
+ }
271
+ catch (err) {
272
+ log(`[model-ota] VLM watcher failed to start (non-fatal): ${err?.message || err}`);
273
+ }
274
+ // 2. The specialist watcher. Built lazily on the first granted model that has a
275
+ // destination, so a fleet with no specialists creates no manager, no models
276
+ // dir and no poll loop — byte-identical to before this module existed.
277
+ const env = deps.env ?? process.env;
278
+ const otaEnabled = config.localSensing?.modelOtaEnabled !== false;
279
+ /**
280
+ * ONE MANAGER PER GRANTING IDENTITY.
281
+ *
282
+ * `ModelManager` sends one `deviceFingerprint`/`screen_id` for every model it
283
+ * holds, and the server re-decides eligibility — including the rollout hash
284
+ * bucket — from that identity. So a model granted to screen A has to be asked
285
+ * for AS screen A; putting it on a manager that identifies as screen B asks a
286
+ * question nobody answered yes to. In practice a connector's feeds are granted
287
+ * the same set and this is one manager; it is more only when two screens on the
288
+ * box genuinely differ, which is exactly when it matters.
289
+ *
290
+ * Keyed on the identity, and every model is registered on EXACTLY ONE of them
291
+ * (first grant wins) — two managers writing the same `model.onnx` would race on
292
+ * the same `.tmp`.
293
+ */
294
+ const managers = new Map();
295
+ const startedManagers = new Set();
296
+ const watched = new Set();
297
+ const skipLogged = new Set();
298
+ let pollHandle = null;
299
+ let stopped = false;
300
+ const hostFingerprint = connectorFingerprint(config);
301
+ /** Filesystem-safe segment for a manager's own manifest directory. */
302
+ const identityDirSegment = (identity) => identity.replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 64) || 'host';
303
+ const ensureManager = (identity, screenId) => {
304
+ const existing = managers.get(identity);
305
+ if (existing)
306
+ return existing;
307
+ const apiBaseUrl = config.apiBaseUrl || 'https://api.trillboards.com';
308
+ // The manifest STATE dir, not the artifact dir — it records which version of
309
+ // each specialist is on disk. Nested under the VLM watcher's `model-ota` root
310
+ // so there is one OTA state home, but its OWN directory PER IDENTITY: two
311
+ // managers sharing one `models_manifest.json` would clobber each other's
312
+ // versions, and `saveManifest()` rewrites the whole file from its own map.
313
+ const stateRoot = config.dataDir || path.join(os.homedir(), '.trillboards');
314
+ const modelsDir = path.join(stateRoot, 'model-ota', 'specialists', identityDirSegment(identity));
315
+ const created = (deps.createModelManager ?? defaultCreateSpecialistManager)({
316
+ modelsDir,
317
+ apiBaseUrl,
318
+ deviceFingerprint: identity,
319
+ deviceScreenId: screenId,
320
+ ...(config.localSensing?.modelOtaCheckIntervalMs
321
+ ? { checkIntervalMs: config.localSensing.modelOtaCheckIntervalMs }
322
+ : {}),
323
+ autoUpdate: true,
324
+ });
325
+ created.on('modelUpdated', (evt) => {
326
+ // No serve restart and no cache clear: the detector service picks an
327
+ // arriving artifact up within one poll of it landing, with no restart. That
328
+ // is its contract, and re-implementing a restart here would be a second
329
+ // authority over a lifecycle it already owns.
330
+ log(`[model-ota] ${evt?.name} updated to v${evt?.newVersion} at ${evt?.path}`);
331
+ // A COMPLETED download frees the one concurrency slot, so ask again rather
332
+ // than waiting for the next tick. `ModelManager` defaults
333
+ // `maxConcurrentDownloads` to 1 and BREAKS out of its update loop the moment
334
+ // the slot is busy, so with N models granted at once only the first is
335
+ // fetched and the rest wait a full check interval EACH — an hour per model
336
+ // by default. Kicked only on a genuine completion, so this terminates: a
337
+ // failing model is retried by the periodic tick, never by a tight loop.
338
+ const m = managers.get(identity);
339
+ if (m?.checkForUpdates) {
340
+ Promise.resolve()
341
+ .then(() => m.checkForUpdates())
342
+ .catch((err) => log(`[model-ota] follow-up check failed (non-fatal): ${err?.message || err}`));
343
+ }
344
+ });
345
+ created.on('downloadProgress', (evt) => {
346
+ if (typeof evt?.percent === 'number' && evt.percent % 25 === 0) {
347
+ log(`[model-ota] downloading ${evt?.name}… ${evt.percent}%`);
348
+ }
349
+ });
350
+ created.on('downloadError', (evt) => {
351
+ // Includes the disk preflight's refusal, which names the bytes needed, the
352
+ // bytes free and the volume. Non-fatal by construction: nothing was written,
353
+ // so the detector keeps answering 503 with its own reason.
354
+ log(`[model-ota] ${evt?.name} update issue (non-fatal): ${evt?.error}`);
355
+ });
356
+ managers.set(identity, created);
357
+ return created;
358
+ };
359
+ const setGrantedModels = (next) => {
360
+ if (stopped)
361
+ return;
362
+ try {
363
+ if (!otaEnabled)
364
+ return;
365
+ const touched = new Set();
366
+ for (const model of Array.isArray(next) ? next : []) {
367
+ const dest = destinationForGrantedModel(model, { env });
368
+ if (dest.kind === 'skip') {
369
+ if (!skipLogged.has(dest.modelId)) {
370
+ skipLogged.add(dest.modelId);
371
+ log(`[model-ota] not delivering '${dest.modelId}': ${dest.reason}`);
372
+ }
373
+ continue;
374
+ }
375
+ if (watched.has(dest.modelId))
376
+ continue;
377
+ const screenId = typeof model.grantedByScreenId === 'string' && model.grantedByScreenId
378
+ ? model.grantedByScreenId
379
+ : null;
380
+ const identity = screenId || hostFingerprint;
381
+ const m = ensureManager(identity, screenId);
382
+ if (!m)
383
+ continue;
384
+ // The registry id IS the OTA modelType — see GrantedModel.modelId.
385
+ m.registerModel(dest.modelId, dest.filePath);
386
+ watched.add(dest.modelId);
387
+ touched.add(identity);
388
+ log(`[model-ota] watching ${dest.modelId} → ${dest.filePath}`
389
+ + ` (asking as ${screenId ? `screen ${screenId}` : `host ${hostFingerprint}`})`);
390
+ }
391
+ for (const identity of touched) {
392
+ const m = managers.get(identity);
393
+ if (!m)
394
+ continue;
395
+ if (!startedManagers.has(identity)) {
396
+ startedManagers.add(identity);
397
+ // initialize() loads the manifest + hashes what is already on disk;
398
+ // start() arms the periodic check; the immediate check is what stops a
399
+ // fresh install waiting a full interval for an artifact it has none of.
400
+ Promise.resolve()
401
+ .then(() => m.initialize())
402
+ .then(() => { m.start(); })
403
+ .then(() => (m.checkForUpdates ? m.checkForUpdates() : undefined))
404
+ .catch((err) => log(`[model-ota] init failed (non-fatal): ${err?.message || err}`));
405
+ }
406
+ else if (m.checkForUpdates) {
407
+ // Already running and a NEW model just appeared — do not make it wait
408
+ // for the next tick.
409
+ Promise.resolve()
410
+ .then(() => m.checkForUpdates())
411
+ .catch((err) => log(`[model-ota] check failed (non-fatal): ${err?.message || err}`));
412
+ }
413
+ }
414
+ }
415
+ catch (err) {
416
+ log(`[model-ota] granted-model apply failed (non-fatal): ${err?.message || err}`);
417
+ }
418
+ };
419
+ if (!otaEnabled) {
420
+ log('[model-ota] localSensing.modelOtaEnabled=false — specialist model delivery disabled');
421
+ }
422
+ else {
423
+ setGrantedModels(models);
424
+ }
425
+ // 3. Keep asking. A model can be granted (or a canary advanced) long after the
426
+ // connector started, and nothing else on this box would ever notice.
427
+ if (otaEnabled && deps.grantedModelSource) {
428
+ const source = deps.grantedModelSource;
429
+ const pollMs = config.localSensing?.modelOtaCheckIntervalMs || DEFAULT_GRANTED_POLL_MS;
430
+ const refresh = () => {
431
+ Promise.resolve()
432
+ .then(() => source.resolve())
433
+ .then((resolved) => { if (resolved)
434
+ setGrantedModels(resolved); })
435
+ .catch((err) => log(`[model-ota] granted-model refresh failed (non-fatal): ${err?.message || err}`));
436
+ };
437
+ refresh();
438
+ const setTimer = deps.setInterval ?? ((cb, ms) => setInterval(cb, ms));
439
+ pollHandle = setTimer(refresh, pollMs);
440
+ // Never hold the process open for a model check.
441
+ if (pollHandle && typeof pollHandle.unref === 'function')
442
+ pollHandle.unref();
443
+ }
444
+ return {
445
+ stop() {
446
+ stopped = true;
447
+ try {
448
+ vlm?.stop();
449
+ }
450
+ catch { /* best-effort */ }
451
+ for (const m of managers.values()) {
452
+ try {
453
+ m.stop();
454
+ }
455
+ catch { /* best-effort */ }
456
+ }
457
+ try {
458
+ if (pollHandle)
459
+ (deps.clearInterval ?? clearInterval)(pollHandle);
460
+ }
461
+ catch { /* best-effort */ }
462
+ pollHandle = null;
463
+ },
464
+ setGrantedModels,
465
+ watchedModelIds() {
466
+ return [...watched].sort();
467
+ },
468
+ };
469
+ }
470
+ /** Stable, non-PII connector fingerprint for the OTA version-check query param. */
471
+ function connectorFingerprint(config) {
472
+ if (config.screenId)
473
+ return String(config.screenId);
474
+ const raw = `${os.hostname()}|${process.platform}|${os.arch()}`;
475
+ let h = 0;
476
+ for (let i = 0; i < raw.length; i += 1)
477
+ h = (Math.imul(31, h) + raw.charCodeAt(i)) | 0;
478
+ return `conn_${(h >>> 0).toString(16)}`;
479
+ }
480
+ function defaultCreateSpecialistManager(opts) {
481
+ return new edge_core_1.ModelManager({
482
+ modelsDir: opts.modelsDir,
483
+ apiBaseUrl: opts.apiBaseUrl,
484
+ deviceFingerprint: opts.deviceFingerprint,
485
+ // The identity the SERVER re-decides eligibility under. Without it the
486
+ // download asks as a different device than the grant answered for.
487
+ ...(opts.deviceScreenId ? { deviceScreenId: opts.deviceScreenId } : {}),
488
+ ...(opts.checkIntervalMs ? { checkIntervalMs: opts.checkIntervalMs } : {}),
489
+ autoUpdate: opts.autoUpdate ?? true,
490
+ });
491
+ }
492
+ //# sourceMappingURL=modelOta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modelOta.js","sourceRoot":"","sources":["../src/modelOta.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import { CloudSensingManager, type ResolvedFeed } from './CloudSensingManager';
2
2
  import type { SharedFrameSource } from './MosaicSource';
3
3
  import type { EdgeAgentConfig } from './config';
4
+ import { SensingProfileClient } from './SensingProfileClient';
4
5
  /**
5
6
  * Optional cross-rail mosaic-pull sharing: the combined `sense` entrypoint passes
6
7
  * a `sharedSources` registry so the VLM rail attaches to the SAME SharedFrameSource
@@ -13,6 +14,26 @@ export interface SensingManagerOpts {
13
14
  /** Reuse a caller-owned recovery coordinator across shared sensing rails. */
14
15
  streamUrlResolver?: (feed: ResolvedFeed, currentUrl: string, error: Error) => Promise<string | null>;
15
16
  }
17
+ /**
18
+ * One `SensingProfileClient` per feed that carries a device token, PAIRED WITH
19
+ * THE SCREEN that token binds to — the ONE construction, so the cadence source
20
+ * and the model OTA ask the same screens the same way. A feed without a token
21
+ * cannot ask (the token IS the screen binding), and contributes nothing rather
22
+ * than erroring.
23
+ *
24
+ * The screen id rides alongside because the model OTA needs it and the response
25
+ * does not carry it back: the server decides which models a device may receive
26
+ * under `screenId`, and the DOWNLOAD has to quote that same identity or it is
27
+ * re-judged as a different device in a different rollout bucket. See
28
+ * `modelOta.GrantedModel.grantedByScreenId`. `config.screenId` is not a
29
+ * substitute — `pairedConfigToFile` stores screen ids only inside `cameras[]`.
30
+ */
31
+ export declare function profileFeedsForConfig(config: EdgeAgentConfig): Array<{
32
+ screenId: string | null;
33
+ client: SensingProfileClient;
34
+ }>;
35
+ /** The clients only — for consumers that do not need the screen identity. */
36
+ export declare function profileClientsForFeeds(config: EdgeAgentConfig): SensingProfileClient[];
16
37
  /**
17
38
  * The lifecycle surface the auto-sense supervisor drives. `CloudSensingManager`
18
39
  * already satisfies it: `start()` opens cameras + runs the capture loops;
@@ -1 +1 @@
1
- {"version":3,"file":"sensingRunMode.d.ts","sourceRoot":"","sources":["../src/sensingRunMode.ts"],"names":[],"mappings":"AACA,OAAO,EACL,mBAAmB,EAGnB,KAAK,YAAY,EAElB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAQhD;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC/C,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,CAClB,IAAI,EAAE,YAAY,EAClB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,KAAK,KACT,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC7B;AAoDD;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;;OAMG;IACH,KAAK,CAAC,IAAI,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB;;;;;;;;;;;;;;;OAeG;IACH,gBAAgB,CAAC,IAAI;QACnB,eAAe,EAAE,MAAM,CAAC;QACxB;;;;;;WAMG;QACH,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,eAAe,CAAC,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAA;SAAE,CAAC;QACvC;;;;;;WAMG;QACH,WAAW,CAAC,EAAE;YACZ,QAAQ,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,OAAO,GAAG,IAAI,CAAC;YAC1C,iBAAiB,CAAC,EAAE,OAAO,CAAC;YAC5B,UAAU,CAAC,EAAE,OAAO,CAAC;SACtB,GAAG,IAAI,CAAC;QACT,YAAY,CAAC,EAAE;YAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;YAAC,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAC;KACtE,CAAC;CACH;AAED,iFAAiF;AACjF,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3B,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,qEAAqE;AACrE,eAAO,MAAM,SAAS,EAAE,SAIvB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,QAEgD,CAAC;AAE9E;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,QAAgB,CAAC;AAE5D;;;;;;;;;GASG;AACH,eAAO,MAAM,qCAAqC,QAAiB,CAAC;AAmWpE;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,UAAU,CAAC,mBAAmB,CAAC,kBAAkB,CAAC,CAAC,GAC1D,MAAM,CAyCR;AAED;;;;;;;;;;GAUG;AACH;;;;;;;;;;GAUG;AACH,wBAAgB,8BAA8B,CAC5C,EAAE,EAAE,SAAS,EACb,GAAG,GAAE,MAAM,MAAiB,GAC3B;IAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAA;CAAE,CA+BjD;AA8ID;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,eAAe,EACvB,EAAE,GAAE,SAAqB,EACzB,IAAI,CAAC,EAAE,kBAAkB,GACxB,mBAAmB,GAAG,IAAI,CAqJ5B"}
1
+ {"version":3,"file":"sensingRunMode.d.ts","sourceRoot":"","sources":["../src/sensingRunMode.ts"],"names":[],"mappings":"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"}
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.TRANSPORT_FAILURE_RESTATE_INTERVAL_MS = exports.SENSING_HEALTH_LOG_INTERVAL_MS = exports.NO_FEEDS_MESSAGE = exports.consoleIo = void 0;
37
+ exports.profileFeedsForConfig = profileFeedsForConfig;
38
+ exports.profileClientsForFeeds = profileClientsForFeeds;
37
39
  exports.formatSensingHealth = formatSensingHealth;
38
40
  exports.createTransportFailureReporter = createTransportFailureReporter;
39
41
  exports.buildLocalSensingManager = buildLocalSensingManager;
@@ -73,19 +75,42 @@ function cadenceSourceFor(config, feeds, mode,
73
75
  * A feed that has not settled returns null and simply does not vote.
74
76
  */
75
77
  observedAnalyzeMs) {
76
- const profileClients = feeds
77
- .filter((f) => !!f.deviceToken)
78
- .map((f) => new SensingProfileClient_1.SensingProfileClient({
79
- apiBaseUrl: config.apiBaseUrl,
80
- deviceToken: f.deviceToken,
81
- }));
82
78
  return (0, deployedCadenceSource_1.buildDeployedCadenceSource)({
83
- profileClients,
79
+ profileClients: profileClientsForFeeds(config),
84
80
  mode,
85
81
  feedCount: feeds.length,
86
82
  observedAnalyzeMs,
87
83
  });
88
84
  }
85
+ /**
86
+ * One `SensingProfileClient` per feed that carries a device token, PAIRED WITH
87
+ * THE SCREEN that token binds to — the ONE construction, so the cadence source
88
+ * and the model OTA ask the same screens the same way. A feed without a token
89
+ * cannot ask (the token IS the screen binding), and contributes nothing rather
90
+ * than erroring.
91
+ *
92
+ * The screen id rides alongside because the model OTA needs it and the response
93
+ * does not carry it back: the server decides which models a device may receive
94
+ * under `screenId`, and the DOWNLOAD has to quote that same identity or it is
95
+ * re-judged as a different device in a different rollout bucket. See
96
+ * `modelOta.GrantedModel.grantedByScreenId`. `config.screenId` is not a
97
+ * substitute — `pairedConfigToFile` stores screen ids only inside `cameras[]`.
98
+ */
99
+ function profileFeedsForConfig(config) {
100
+ return (0, CloudSensingManager_1.resolveFeeds)(config)
101
+ .filter((f) => !!f.deviceToken)
102
+ .map((f) => ({
103
+ screenId: f.screenId ? String(f.screenId) : null,
104
+ client: new SensingProfileClient_1.SensingProfileClient({
105
+ apiBaseUrl: config.apiBaseUrl,
106
+ deviceToken: f.deviceToken,
107
+ }),
108
+ }));
109
+ }
110
+ /** The clients only — for consumers that do not need the screen identity. */
111
+ function profileClientsForFeeds(config) {
112
+ return profileFeedsForConfig(config).map((f) => f.client);
113
+ }
89
114
  const DEFAULT_LITERT_LM_BASE_URL = 'http://localhost:9379';
90
115
  const DEFAULT_LITERT_LM_MODEL = 'gemma-4-E2B-it.litertlm';
91
116
  /** Default sink: the real console (what the CLI entrypoints use). */