@camstack/addon-pipeline-orchestrator 1.2.601 → 1.2.602

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/index.mjs CHANGED
@@ -5541,7 +5541,7 @@ var ZodIssueCode = {
5541
5541
  var ZodFirstPartyTypeKind;
5542
5542
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5543
5543
  //#endregion
5544
- //#region ../types/dist/sleep-bZIhUvjd.mjs
5544
+ //#region ../types/dist/sleep-BOjuyoPC.mjs
5545
5545
  /**
5546
5546
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5547
5547
  * window to float samples (D455).
@@ -6361,7 +6361,7 @@ async function writeMembers(ctx, children, incoming, stored, pathOf) {
6361
6361
  for (const [member, node] of children) applyMember(out, member, await writeNode(ctx, node, incoming[member], stored[member], pathOf(member)), stored);
6362
6362
  return out;
6363
6363
  }
6364
- function rowId$1(row, idKey) {
6364
+ function rowId(row, idKey) {
6365
6365
  const raw = row[idKey];
6366
6366
  if (typeof raw === "string" && raw.length > 0) return raw;
6367
6367
  if (typeof raw === "number" && Number.isFinite(raw)) return String(raw);
@@ -6378,7 +6378,7 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
6378
6378
  }
6379
6379
  const storedById = /* @__PURE__ */ new Map();
6380
6380
  for (const row of storedRows) {
6381
- const id = rowId$1(row, node.idKey);
6381
+ const id = rowId(row, node.idKey);
6382
6382
  if (id !== null) storedById.set(id, row);
6383
6383
  }
6384
6384
  const kept = /* @__PURE__ */ new Set();
@@ -6388,13 +6388,13 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
6388
6388
  rows.push(row);
6389
6389
  continue;
6390
6390
  }
6391
- const id = rowId$1(row, node.idKey) ?? ctx.mintId();
6391
+ const id = rowId(row, node.idKey) ?? ctx.mintId();
6392
6392
  kept.add(id);
6393
6393
  const withId = {
6394
6394
  ...row,
6395
6395
  [node.idKey]: row[node.idKey] ?? id
6396
6396
  };
6397
- if (rowId$1(withId, node.idKey) === null) withId[node.idKey] = id;
6397
+ if (rowId(withId, node.idKey) === null) withId[node.idKey] = id;
6398
6398
  const storedRow = storedById.get(id) ?? {};
6399
6399
  rows.push(await writeMembers(ctx, node.item, withId, storedRow, (member) => arrayItemFieldPath(fieldPath, id, member)));
6400
6400
  }
@@ -9299,7 +9299,7 @@ function sleep(ms) {
9299
9299
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
9300
9300
  }
9301
9301
  //#endregion
9302
- //#region ../types/dist/err-msg--2RrYiLO.mjs
9302
+ //#region ../types/dist/err-msg-BXKzRBwg.mjs
9303
9303
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
9304
9304
  DeviceType["Camera"] = "camera";
9305
9305
  DeviceType["Hub"] = "hub";
@@ -9391,6 +9391,10 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
9391
9391
  * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
9392
9392
  * integrations sharing the same food/desiccant/hopper surface. */
9393
9393
  DeviceType["PetFeeder"] = "pet-feeder";
9394
+ /** Built-in dishwasher — program lifecycle, door / remote-start gates,
9395
+ * salt and rinse-aid levels. Installed with the `dishwasher` cap.
9396
+ * Sources: native Bosch Home Connect, reusable by similar integrations. */
9397
+ DeviceType["Dishwasher"] = "dishwasher";
9394
9398
  return DeviceType;
9395
9399
  }({});
9396
9400
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -9648,7 +9652,7 @@ function errMsg$2(err) {
9648
9652
  return String(err);
9649
9653
  }
9650
9654
  //#endregion
9651
- //#region ../types/dist/composition-DOwQB_Ua.mjs
9655
+ //#region ../types/dist/composition-CB6PMT9p.mjs
9652
9656
  /**
9653
9657
  * The ffmpeg build every CamStack node downloads, per platform and arch.
9654
9658
  *
@@ -14514,8 +14518,13 @@ var AlertSeveritySchema = _enum([
14514
14518
  "info",
14515
14519
  "success",
14516
14520
  "warning",
14517
- "error"
14521
+ "error",
14522
+ "critical"
14518
14523
  ]);
14524
+ var AlertActionSchema = object({
14525
+ label: string(),
14526
+ href: string().optional()
14527
+ });
14519
14528
  var AlertStatusSchema = _enum([
14520
14529
  "active",
14521
14530
  "in-progress",
@@ -14533,6 +14542,7 @@ var AlertSchema = object({
14533
14542
  severity: AlertSeveritySchema,
14534
14543
  title: string(),
14535
14544
  message: string(),
14545
+ action: AlertActionSchema.optional(),
14536
14546
  status: AlertStatusSchema,
14537
14547
  progress: number().optional(),
14538
14548
  read: boolean(),
@@ -55455,7 +55465,7 @@ var BatteryStatusSchema = object({
55455
55465
  */
55456
55466
  binary: boolean().optional()
55457
55467
  });
55458
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, method(object({
55468
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, method(object({
55459
55469
  deviceId: number(),
55460
55470
  /** Bound on the wait. Sensible range 3000–10000ms. */
55461
55471
  timeoutMs: number().int().min(500).max(3e4).default(8e3)
@@ -56331,6 +56341,98 @@ object({
56331
56341
  deviceId: number(),
56332
56342
  status: DeviceStatusSchema
56333
56343
  });
56344
+ /**
56345
+ * Smart-dishwasher cap — program lifecycle, door / remote-start gates,
56346
+ * consumable levels (salt, rinse aid) and common program options.
56347
+ *
56348
+ * Sources: native Bosch Home Connect (`dishwasher` device type). Reusable
56349
+ * by any integration that exposes the same program / option surface.
56350
+ *
56351
+ * `kind: 'poll'` — cloud REST APIs typically have no push channel; the
56352
+ * provider refreshes the slice on an interval and eagerly after commands.
56353
+ */
56354
+ var DishwasherStateSchema = _enum([
56355
+ "idle",
56356
+ "running",
56357
+ "paused",
56358
+ "finished",
56359
+ "error",
56360
+ "unavailable"
56361
+ ]);
56362
+ var DishwasherConsumableLevelSchema = _enum([
56363
+ "ok",
56364
+ "low",
56365
+ "empty"
56366
+ ]);
56367
+ object({
56368
+ state: DishwasherStateSchema,
56369
+ /** Active program id/name, or null when none selected / unknown. */
56370
+ program: string().nullable(),
56371
+ /** Program tokens the hardware accepts — drives the UI selector. */
56372
+ availablePrograms: array(string()),
56373
+ /** Minutes remaining on the active cycle, or null when not running. */
56374
+ remainingMinutes: number().int().nonnegative().nullable(),
56375
+ /** Cycle completion 0..100, or null when not applicable. */
56376
+ progressPercent: number().min(0).max(100).nullable(),
56377
+ /** True when the door is reported open. */
56378
+ doorOpen: boolean(),
56379
+ /** True when remote start is allowed (door closed, power on, etc.). */
56380
+ remoteStartAllowed: boolean(),
56381
+ /** Main power / switched-on state. */
56382
+ powerOn: boolean(),
56383
+ saltLevel: DishwasherConsumableLevelSchema.nullable(),
56384
+ rinseAidLevel: DishwasherConsumableLevelSchema.nullable(),
56385
+ halfLoad: boolean(),
56386
+ hygienePlus: boolean(),
56387
+ errorCode: number().nullable(),
56388
+ errorLabel: string().nullable(),
56389
+ lastFetchedAt: number()
56390
+ });
56391
+ DeviceType.Dishwasher, method(object({ deviceId: number().int().nonnegative() }), _void(), {
56392
+ kind: "mutation",
56393
+ auth: "protected"
56394
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
56395
+ kind: "mutation",
56396
+ auth: "protected"
56397
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
56398
+ kind: "mutation",
56399
+ auth: "protected"
56400
+ }), method(object({
56401
+ deviceId: number().int().nonnegative(),
56402
+ program: string().min(1)
56403
+ }), _void(), {
56404
+ kind: "mutation",
56405
+ auth: "protected"
56406
+ }), method(object({
56407
+ deviceId: number().int().nonnegative(),
56408
+ on: boolean()
56409
+ }), _void(), {
56410
+ kind: "mutation",
56411
+ auth: "protected"
56412
+ }), method(object({
56413
+ deviceId: number().int().nonnegative(),
56414
+ on: boolean()
56415
+ }), _void(), {
56416
+ kind: "mutation",
56417
+ auth: "protected"
56418
+ }), method(object({
56419
+ deviceId: number().int().nonnegative(),
56420
+ on: boolean()
56421
+ }), _void(), {
56422
+ kind: "mutation",
56423
+ auth: "protected"
56424
+ }), method(object({
56425
+ deviceId: number().int().nonnegative(),
56426
+ key: string().min(1),
56427
+ value: union([
56428
+ boolean(),
56429
+ string(),
56430
+ number()
56431
+ ])
56432
+ }), _void(), {
56433
+ kind: "mutation",
56434
+ auth: "protected"
56435
+ });
56334
56436
  object({
56335
56437
  /** Ms epoch of the last press. null = never observed since this provider started. */
56336
56438
  lastPressedAt: number().nullable(),
@@ -56896,6 +56998,13 @@ object({
56896
56998
  lastChangedAt: number()
56897
56999
  });
56898
57000
  DeviceType.Sensor;
57001
+ object({
57002
+ /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
57003
+ position: number().min(0).max(100).nullable(),
57004
+ /** Ms epoch when the slice was last updated (push or poll). */
57005
+ lastFetchedAt: number()
57006
+ });
57007
+ DeviceType.Thermostat;
56899
57008
  object({
56900
57009
  /** Whether the humidifier is currently on. */
56901
57010
  on: boolean(),
@@ -64081,7 +64190,7 @@ var AccessoryKind = {
64081
64190
  PrivacyMask: DeviceRole.PrivacyMask
64082
64191
  };
64083
64192
  AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
64084
- DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
64193
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher;
64085
64194
  new Set(Object.values(DeviceType));
64086
64195
  Object.freeze({
64087
64196
  "accessories.getStatus": {
@@ -66502,6 +66611,60 @@ Object.freeze({
66502
66611
  addonId: null,
66503
66612
  access: "view"
66504
66613
  },
66614
+ "dishwasher.getStatus": {
66615
+ capName: "dishwasher",
66616
+ capScope: "device",
66617
+ addonId: null,
66618
+ access: "view"
66619
+ },
66620
+ "dishwasher.pause": {
66621
+ capName: "dishwasher",
66622
+ capScope: "device",
66623
+ addonId: null,
66624
+ access: "create"
66625
+ },
66626
+ "dishwasher.setHalfLoad": {
66627
+ capName: "dishwasher",
66628
+ capScope: "device",
66629
+ addonId: null,
66630
+ access: "create"
66631
+ },
66632
+ "dishwasher.setHygienePlus": {
66633
+ capName: "dishwasher",
66634
+ capScope: "device",
66635
+ addonId: null,
66636
+ access: "create"
66637
+ },
66638
+ "dishwasher.setOption": {
66639
+ capName: "dishwasher",
66640
+ capScope: "device",
66641
+ addonId: null,
66642
+ access: "create"
66643
+ },
66644
+ "dishwasher.setPower": {
66645
+ capName: "dishwasher",
66646
+ capScope: "device",
66647
+ addonId: null,
66648
+ access: "create"
66649
+ },
66650
+ "dishwasher.setProgram": {
66651
+ capName: "dishwasher",
66652
+ capScope: "device",
66653
+ addonId: null,
66654
+ access: "create"
66655
+ },
66656
+ "dishwasher.start": {
66657
+ capName: "dishwasher",
66658
+ capScope: "device",
66659
+ addonId: null,
66660
+ access: "create"
66661
+ },
66662
+ "dishwasher.stop": {
66663
+ capName: "dishwasher",
66664
+ capScope: "device",
66665
+ addonId: null,
66666
+ access: "create"
66667
+ },
66505
66668
  "doorbell.getStatus": {
66506
66669
  capName: "doorbell",
66507
66670
  capScope: "device",
@@ -66712,6 +66875,12 @@ Object.freeze({
66712
66875
  addonId: null,
66713
66876
  access: "view"
66714
66877
  },
66878
+ "heatingValve.getStatus": {
66879
+ capName: "heating-valve",
66880
+ capScope: "device",
66881
+ addonId: null,
66882
+ access: "view"
66883
+ },
66715
66884
  "humidifier.getStatus": {
66716
66885
  capName: "humidifier",
66717
66886
  capScope: "device",
@@ -72299,7 +72468,7 @@ Object.freeze({
72299
72468
  access: "create"
72300
72469
  }
72301
72470
  });
72302
- DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
72471
+ DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher;
72303
72472
  /**
72304
72473
  * Heuristic check — `device.features?.includes(profile.when.hasFeature)`.
72305
72474
  */
@@ -73151,6 +73320,51 @@ Object.freeze({
73151
73320
  form: "single",
73152
73321
  optional: false
73153
73322
  }],
73323
+ "dishwasher.getStatus": [{
73324
+ name: "deviceId",
73325
+ form: "single",
73326
+ optional: false
73327
+ }],
73328
+ "dishwasher.pause": [{
73329
+ name: "deviceId",
73330
+ form: "single",
73331
+ optional: false
73332
+ }],
73333
+ "dishwasher.setHalfLoad": [{
73334
+ name: "deviceId",
73335
+ form: "single",
73336
+ optional: false
73337
+ }],
73338
+ "dishwasher.setHygienePlus": [{
73339
+ name: "deviceId",
73340
+ form: "single",
73341
+ optional: false
73342
+ }],
73343
+ "dishwasher.setOption": [{
73344
+ name: "deviceId",
73345
+ form: "single",
73346
+ optional: false
73347
+ }],
73348
+ "dishwasher.setPower": [{
73349
+ name: "deviceId",
73350
+ form: "single",
73351
+ optional: false
73352
+ }],
73353
+ "dishwasher.setProgram": [{
73354
+ name: "deviceId",
73355
+ form: "single",
73356
+ optional: false
73357
+ }],
73358
+ "dishwasher.start": [{
73359
+ name: "deviceId",
73360
+ form: "single",
73361
+ optional: false
73362
+ }],
73363
+ "dishwasher.stop": [{
73364
+ name: "deviceId",
73365
+ form: "single",
73366
+ optional: false
73367
+ }],
73154
73368
  "doorbell.getStatus": [{
73155
73369
  name: "deviceId",
73156
73370
  form: "single",
@@ -73220,6 +73434,11 @@ Object.freeze({
73220
73434
  form: "single",
73221
73435
  optional: false
73222
73436
  }],
73437
+ "heatingValve.getStatus": [{
73438
+ name: "deviceId",
73439
+ form: "single",
73440
+ optional: false
73441
+ }],
73223
73442
  "humidifier.getStatus": [{
73224
73443
  name: "deviceId",
73225
73444
  form: "single",
@@ -75043,6 +75262,103 @@ Object.freeze({
75043
75262
  "network-access": "ingress",
75044
75263
  "smtp-provider": "email"
75045
75264
  });
75265
+ /** Width of a bucket the history store accepts. */
75266
+ var HISTORY_BUCKET_MS = 5 * 6e4;
75267
+ function bucketStartOf(at) {
75268
+ return Math.floor(at / HISTORY_BUCKET_MS) * HISTORY_BUCKET_MS;
75269
+ }
75270
+ function subjectId(subject) {
75271
+ return subject.kind === "device" ? `d:${subject.deviceId}` : `n:${subject.nodeId}`;
75272
+ }
75273
+ function seriesIdentity(key, subject, scope) {
75274
+ return `${key}\u0000${subjectId(subject)}\u0000${scope}`;
75275
+ }
75276
+ function toClosed(b) {
75277
+ return {
75278
+ key: b.key,
75279
+ subject: b.subject,
75280
+ scope: b.scope,
75281
+ bucketStart: b.bucketStart,
75282
+ samples: b.samples,
75283
+ sum: b.sum,
75284
+ min: b.min,
75285
+ max: b.max,
75286
+ last: b.last,
75287
+ lastTs: b.lastTs
75288
+ };
75289
+ }
75290
+ var BucketAccumulator = class {
75291
+ open = /* @__PURE__ */ new Map();
75292
+ requeued = /* @__PURE__ */ new Map();
75293
+ /** Add one observation. A non-finite value or timestamp is ignored (never counted as 0). */
75294
+ note(key, subject, scope, value, at) {
75295
+ if (!Number.isFinite(value) || !Number.isFinite(at)) return;
75296
+ const bucketStart = bucketStartOf(at);
75297
+ const id = `${seriesIdentity(key, subject, scope)}\u0000${bucketStart}`;
75298
+ const existing = this.open.get(id);
75299
+ if (existing === void 0) {
75300
+ this.open.set(id, {
75301
+ key,
75302
+ subject,
75303
+ scope,
75304
+ bucketStart,
75305
+ samples: 1,
75306
+ sum: value,
75307
+ min: value,
75308
+ max: value,
75309
+ last: value,
75310
+ lastTs: at
75311
+ });
75312
+ return;
75313
+ }
75314
+ existing.samples += 1;
75315
+ existing.sum += value;
75316
+ if (value < existing.min) existing.min = value;
75317
+ if (value > existing.max) existing.max = value;
75318
+ if (at >= existing.lastTs) {
75319
+ existing.last = value;
75320
+ existing.lastTs = at;
75321
+ }
75322
+ }
75323
+ /**
75324
+ * Buckets whose window ended at or before `now`, oldest first, requeued ones
75325
+ * included. They leave the accumulator: delivery is the caller's.
75326
+ */
75327
+ takeClosed(now) {
75328
+ const out = [];
75329
+ for (const [id, bucket] of this.open) if (bucket.bucketStart + 3e5 <= now) {
75330
+ out.push(toClosed(bucket));
75331
+ this.open.delete(id);
75332
+ }
75333
+ for (const list of this.requeued.values()) out.push(...list);
75334
+ this.requeued.clear();
75335
+ return out.toSorted((a, b) => a.bucketStart - b.bucketStart);
75336
+ }
75337
+ /**
75338
+ * Put undelivered buckets back for the next `takeClosed`. Keeps the newest
75339
+ * {@link MAX_REQUEUED_BUCKETS_PER_SERIES} per series and returns the buckets
75340
+ * it dropped (empty when none).
75341
+ */
75342
+ requeue(buckets) {
75343
+ const dropped = [];
75344
+ for (const bucket of buckets) {
75345
+ const id = seriesIdentity(bucket.key, bucket.subject, bucket.scope);
75346
+ const list = [...this.requeued.get(id) ?? [], bucket].toSorted((a, b) => a.bucketStart - b.bucketStart);
75347
+ while (list.length > 12) {
75348
+ const oldest = list.shift();
75349
+ if (oldest !== void 0) dropped.push(oldest);
75350
+ }
75351
+ this.requeued.set(id, list);
75352
+ }
75353
+ return dropped;
75354
+ }
75355
+ /** Buckets currently held (open plus requeued) — for a gauge in the producer's own status. */
75356
+ pendingBucketCount() {
75357
+ let n = this.open.size;
75358
+ for (const list of this.requeued.values()) n += list.length;
75359
+ return n;
75360
+ }
75361
+ };
75046
75362
  DeviceType.Camera;
75047
75363
  var G711_SCALE_CORRECTION_DB = {
75048
75364
  PCMU: 20 * Math.log10(4),
@@ -88671,430 +88987,127 @@ var NodeStressController = class NodeStressController {
88671
88987
  }
88672
88988
  };
88673
88989
  //#endregion
88674
- //#region src/durable/lts-aggregator.ts
88675
- /** Wall-clock bucket width. Five minutes, matching HA's statistics tier. */
88676
- var LTS_BUCKET_MS = 5 * 6e4;
88677
- /** Buckets written per transaction — bounds one statement, not the flush. */
88678
- var FLUSH_BATCH = 200;
88679
- /** How often the row cap is enforced. Rarely: it is a bound, not a deadline. */
88680
- var CAP_SWEEP_INTERVAL_MS = 6 * 36e5;
88681
- /**
88682
- * The row's identity IS its four key columns (D871): a composite
88683
- * `primaryKey` + `WITHOUT ROWID`, so the table is its own index and there is no
88684
- * TEXT `id` repeating what the row already says. The old layout kept
88685
- * `<subject>|<series>|<scope>|<bucketStart>` as a TEXT primary key beside the
88686
- * four columns — 85 MB of rows plus a 44 MB autoindex for 1.07 M rows. A
88687
- * composite-key collection is written with `upsertMany` (a bucket flushed twice
88688
- * replaces itself) and read with `query` / `count` / `deleteWhere`; the calls
88689
- * that address one row by id are refused by the engine.
88690
- */
88691
- var LTS_KEY_COLUMNS = [
88692
- "subject",
88693
- "series",
88694
- "scope",
88695
- "bucketStart"
88696
- ];
88697
- var LTS_COLUMNS = [
88698
- (
88699
- /** The camera id, or the node id. One column, because every query is
88700
- * "this thing over time" and the thing is one or the other. */
88701
- {
88702
- name: "subject",
88703
- type: "TEXT"
88704
- }),
88705
- (
88706
- /** Numeric mirror of `subject` for camera rows, so a per-camera question is
88707
- * answered by an integer index — every log line and every query about a
88708
- * device in this repo is keyed by the numeric id. `NULL` for node rows. */
88709
- {
88710
- name: "deviceId",
88711
- type: "INTEGER"
88712
- }),
88713
- {
88714
- name: "series",
88715
- type: "TEXT"
88716
- },
88717
- (
88718
- /** Sub-scope within a series: a zoneId for occupancy, `''` otherwise. */
88719
- {
88720
- name: "scope",
88721
- type: "TEXT"
88722
- }),
88723
- (
88724
- /** Bucket start, wall-clock aligned to {@link LTS_BUCKET_MS}. */
88725
- {
88726
- name: "bucketStart",
88727
- type: "INTEGER"
88728
- }),
88990
+ //#region src/node-stress-history-producer.ts
88991
+ var SERIES_KEYS = {
88992
+ "node-score": "addon:pipeline-orchestrator/node-score",
88993
+ "node-queue-pressure": "addon:pipeline-orchestrator/node-queue-pressure",
88994
+ "node-drop-ratio": "addon:pipeline-orchestrator/node-drop-ratio",
88995
+ "node-fps-deficit": "addon:pipeline-orchestrator/node-fps-deficit"
88996
+ };
88997
+ function nodeStressHistoryClient(api) {
88998
+ return {
88999
+ declareSeries: (input) => api.declareSeries.mutate(input),
89000
+ recordBuckets: (input) => api.recordBuckets.mutate({ buckets: [...input.buckets] }),
89001
+ queryNode: async (input) => {
89002
+ return { series: (await api.queryNode.query(input)).series.map((s) => ({
89003
+ scope: s.scope,
89004
+ points: s.points.flatMap((p) => {
89005
+ if (p.mean === null) return [];
89006
+ return [{
89007
+ t: p.t,
89008
+ samples: p.samples,
89009
+ sum: p.mean * p.samples,
89010
+ min: p.min,
89011
+ max: p.max
89012
+ }];
89013
+ })
89014
+ })) };
89015
+ }
89016
+ };
89017
+ }
89018
+ var SERIES_META = [
88729
89019
  {
88730
- name: "samples",
88731
- type: "INTEGER",
88732
- notNull: true
89020
+ name: "node-score",
89021
+ displayName: "Node stress score"
88733
89022
  },
88734
89023
  {
88735
- name: "sum",
88736
- type: "REAL",
88737
- notNull: true
89024
+ name: "node-queue-pressure",
89025
+ displayName: "Node queue pressure"
88738
89026
  },
88739
89027
  {
88740
- name: "min",
88741
- type: "REAL",
88742
- notNull: true
89028
+ name: "node-drop-ratio",
89029
+ displayName: "Node drop ratio"
88743
89030
  },
88744
89031
  {
88745
- name: "max",
88746
- type: "REAL",
88747
- notNull: true
89032
+ name: "node-fps-deficit",
89033
+ displayName: "Node FPS deficit"
88748
89034
  }
88749
89035
  ];
88750
- /**
88751
- * The primary key serves every per-subject read (`subject, series, scope` then a
88752
- * `bucketStart` range, already in bucket order); this index serves the read that
88753
- * names a series without a subject. The old `deviceId` index went with the TEXT
88754
- * id: nothing queries a series by the numeric mirror.
88755
- */
88756
- var LTS_INDEXES = [{
88757
- name: "idx_lts_series_bucket",
88758
- columns: ["series", "bucketStart"]
88759
- }];
88760
- function ltsBucketStart(at, bucketMs = LTS_BUCKET_MS) {
88761
- return Math.floor(at / bucketMs) * bucketMs;
88762
- }
88763
- var LtsAggregator = class {
88764
- open = /* @__PURE__ */ new Map();
88765
- /** Every (subject, series, scope) that has produced a row in this process —
88766
- * the groups the cap sweep has any reason to look at. */
88767
- groups = /* @__PURE__ */ new Set();
88768
- lastCapSweepAt = 0;
88769
- collection;
88770
- store;
89036
+ var NodeStressHistoryProducer = class {
89037
+ accum = new BucketAccumulator();
89038
+ deviceHistory;
88771
89039
  logger;
88772
89040
  nowFn;
88773
- bucketMs;
88774
- maxRows;
89041
+ declared = false;
88775
89042
  constructor(deps) {
88776
- this.collection = deps.collection;
88777
- this.store = deps.store;
89043
+ this.deviceHistory = deps.deviceHistory;
88778
89044
  this.logger = deps.logger;
88779
89045
  this.nowFn = deps.now ?? (() => Date.now());
88780
- this.bucketMs = deps.bucketMs ?? 3e5;
88781
- this.maxRows = deps.maxRowsPerSeries ?? 105120;
88782
- }
88783
- static declare(store, collection) {
88784
- return store.declareCollection.mutate({
88785
- collection,
88786
- columns: [...LTS_COLUMNS],
88787
- primaryKey: [...LTS_KEY_COLUMNS],
88788
- withoutRowid: true,
88789
- indexes: [...LTS_INDEXES]
88790
- });
88791
- }
88792
- /**
88793
- * Record one observation. Synchronous, allocation-free after the first
88794
- * sample of a bucket, and it cannot throw — it sits on paths that are
88795
- * already producing the value for another reason and must not learn a new
88796
- * failure mode.
88797
- *
88798
- * A non-finite value is DROPPED rather than folded in: one `NaN` would make
88799
- * `sum`, `min` and `max` all `NaN` for the whole bucket, turning a skewed
88800
- * row into a meaningless one.
88801
- */
88802
- note(input) {
88803
- if (!Number.isFinite(input.value)) return;
88804
- const at = input.at ?? this.nowFn();
88805
- const scope = input.scope ?? "";
88806
- const bucketStart = ltsBucketStart(at, this.bucketMs);
88807
- const key = rowId(input.subject, input.series, scope, bucketStart);
88808
- const held = this.open.get(key);
88809
- if (held === void 0) {
88810
- this.open.set(key, {
88811
- subject: input.subject,
88812
- ...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
88813
- series: input.series,
88814
- scope,
88815
- bucketStart,
88816
- samples: 1,
88817
- sum: input.value,
88818
- min: input.value,
88819
- max: input.value
88820
- });
88821
- return;
88822
- }
88823
- held.samples += 1;
88824
- held.sum += input.value;
88825
- if (input.value < held.min) held.min = input.value;
88826
- if (input.value > held.max) held.max = input.value;
88827
- }
88828
- /** Buckets currently accumulating — diagnostics, and what a flush would write. */
88829
- openBuckets() {
88830
- return [...this.open.values()].map(toRow);
88831
89046
  }
88832
- /**
88833
- * Write every bucket that has CLOSED (its window ended at or before `now`)
88834
- * and drop it from RAM. Returns the number of rows written.
88835
- *
88836
- * The current bucket is deliberately left alone: writing it early would mean
88837
- * rewriting it on the next tick, which turns one row into up to sixty and
88838
- * offers every one of them to the checkpoint lottery (D96).
88839
- */
88840
- async flushDue(now = this.nowFn()) {
88841
- const currentBucket = ltsBucketStart(now, this.bucketMs);
88842
- const due = [...this.open.values()].filter((b) => b.bucketStart < currentBucket);
88843
- if (due.length === 0) {
88844
- await this.maybeSweepCap(now);
88845
- return 0;
88846
- }
88847
- let written = 0;
88848
- for (const batch of chunk(due, FLUSH_BATCH)) {
88849
- written += await this.writeBatch(batch);
88850
- for (const bucket of batch) this.open.delete(rowId(bucket.subject, bucket.series, bucket.scope, bucket.bucketStart));
88851
- }
88852
- await this.maybeSweepCap(now);
88853
- return written;
89047
+ async declare() {
89048
+ if (this.declared) return;
89049
+ for (const meta of SERIES_META) await this.deviceHistory.declareSeries({
89050
+ name: meta.name,
89051
+ subjectKind: "node",
89052
+ kind: "gauge",
89053
+ displayName: meta.displayName,
89054
+ ingest: "buckets"
89055
+ });
89056
+ this.declared = true;
88854
89057
  }
88855
- /**
88856
- * Upsert one batch in a single transaction. If the batch is refused as a
88857
- * whole (all-or-nothing), each bucket is retried alone so one bad row costs
88858
- * one interval, not forty. Returns the number of rows written.
88859
- */
88860
- async writeBatch(batch) {
89058
+ noteSignals(nodeId, signals, at) {
89059
+ const subject = {
89060
+ kind: "node",
89061
+ nodeId
89062
+ };
89063
+ this.accum.note(SERIES_KEYS["node-score"], subject, "", signals.score, at);
89064
+ this.accum.note(SERIES_KEYS["node-queue-pressure"], subject, "", signals.queuePressure, at);
89065
+ this.accum.note(SERIES_KEYS["node-drop-ratio"], subject, "", signals.dropRatio, at);
89066
+ this.accum.note(SERIES_KEYS["node-fps-deficit"], subject, "", signals.fpsDeficit, at);
89067
+ }
89068
+ async flushDue(now) {
89069
+ const closed = this.accum.takeClosed(now ?? this.nowFn());
89070
+ if (closed.length === 0) return 0;
88861
89071
  try {
88862
- await this.upsert(batch);
88863
- for (const bucket of batch) this.groups.add(groupKey(bucket.subject, bucket.series, bucket.scope));
88864
- return batch.length;
89072
+ return (await this.deviceHistory.recordBuckets({ buckets: closed })).accepted;
88865
89073
  } catch (err) {
88866
- if (batch.length > 1) {
88867
- let written = 0;
88868
- for (const bucket of batch) written += await this.writeBatch([bucket]);
88869
- return written;
88870
- }
88871
- const bucket = batch[0];
88872
- this.logger.warn("lts bucket write failed — this interval will be missing", {
88873
- ...bucket?.deviceId !== void 0 ? { tags: { deviceId: bucket.deviceId } } : {},
88874
- meta: {
88875
- collection: this.collection,
88876
- series: bucket?.series,
88877
- subject: bucket?.subject,
88878
- bucketStart: bucket?.bucketStart,
88879
- error: String(err)
88880
- }
88881
- });
89074
+ const dropped = this.accum.requeue(closed);
89075
+ this.logger.warn("node-stress history flush failed — requeued", { meta: {
89076
+ buckets: closed.length,
89077
+ dropped: dropped.length,
89078
+ error: err instanceof Error ? err.message : String(err)
89079
+ } });
88882
89080
  return 0;
88883
89081
  }
88884
89082
  }
88885
- async upsert(batch) {
88886
- await this.store.upsertMany.mutate({
88887
- collection: this.collection,
88888
- conflict: [...LTS_KEY_COLUMNS],
88889
- records: batch.map((bucket) => ({ data: {
88890
- subject: bucket.subject,
88891
- deviceId: bucket.deviceId ?? null,
88892
- series: bucket.series,
88893
- scope: bucket.scope,
88894
- bucketStart: bucket.bucketStart,
88895
- samples: bucket.samples,
88896
- sum: bucket.sum,
88897
- min: bucket.min,
88898
- max: bucket.max
88899
- } }))
88900
- });
88901
- }
88902
- /** Read closed buckets back. The read surface every chart will use. */
88903
- async read(query = {}) {
88904
- const where = {};
88905
- if (query.series !== void 0) where["series"] = query.series;
88906
- if (query.subject !== void 0) where["subject"] = query.subject;
88907
- if (query.scope !== void 0) where["scope"] = query.scope;
88908
- const records = await this.store.query.query({
88909
- collection: this.collection,
88910
- filter: {
88911
- ...Object.keys(where).length > 0 ? { where } : {},
88912
- ...query.from !== void 0 || query.to !== void 0 ? { whereBetween: { bucketStart: [query.from ?? 0, query.to ?? Number.MAX_SAFE_INTEGER] } } : {},
88913
- orderBy: {
88914
- field: "bucketStart",
88915
- direction: "asc"
88916
- },
88917
- limit: query.limit ?? 5e3
88918
- }
89083
+ async read(input) {
89084
+ const nodeId = input.subject;
89085
+ if (nodeId === void 0 || input.series === void 0) return [];
89086
+ const key = SERIES_KEYS[input.series];
89087
+ if (key === void 0) return [];
89088
+ const result = await this.deviceHistory.queryNode({
89089
+ nodeId,
89090
+ key,
89091
+ from: input.from ?? 0,
89092
+ to: input.to ?? Date.now(),
89093
+ resolution: "5m",
89094
+ ...input.maxPoints !== void 0 ? { maxPoints: input.maxPoints } : {}
88919
89095
  });
88920
89096
  const rows = [];
88921
- for (const record of records) {
88922
- const row = recordToRow(record.data);
88923
- if (row !== null) rows.push(row);
88924
- }
88925
- return rows;
88926
- }
88927
- /**
88928
- * Enforce the per-(subject, series, scope) row cap, at most once every six
88929
- * hours. Only groups this process has written to are examined: a group with
88930
- * no new rows cannot have crossed a cap it was under.
88931
- */
88932
- async maybeSweepCap(now) {
88933
- if (now - this.lastCapSweepAt < CAP_SWEEP_INTERVAL_MS) return;
88934
- this.lastCapSweepAt = now;
88935
- for (const group of this.groups) {
88936
- const [subject, series, scope] = group.split("\0");
88937
- if (subject === void 0 || series === void 0 || scope === void 0) continue;
88938
- const where = {
88939
- subject,
88940
- series,
88941
- scope
88942
- };
88943
- try {
88944
- const excess = await this.store.count.query({
88945
- collection: this.collection,
88946
- filter: { where }
88947
- }) - this.maxRows;
88948
- if (excess <= 0) continue;
88949
- const cutoff = (await this.store.query.query({
88950
- collection: this.collection,
88951
- filter: {
88952
- where,
88953
- orderBy: {
88954
- field: "bucketStart",
88955
- direction: "asc"
88956
- },
88957
- limit: excess
88958
- }
88959
- })).at(-1)?.data["bucketStart"];
88960
- if (typeof cutoff !== "number") continue;
88961
- const { deleted } = await this.store.deleteWhere.mutate({
88962
- collection: this.collection,
88963
- filter: {
88964
- where,
88965
- whereBetween: { bucketStart: [0, cutoff] }
88966
- }
88967
- });
88968
- this.logger.info("lts row cap enforced", { meta: {
88969
- collection: this.collection,
88970
- series,
88971
- subject,
88972
- deleted,
88973
- cap: this.maxRows
88974
- } });
88975
- } catch (err) {
88976
- this.logger.warn("lts row cap sweep failed — the series keeps growing this cycle", { meta: {
88977
- collection: this.collection,
88978
- series,
88979
- subject,
88980
- error: String(err)
88981
- } });
88982
- }
88983
- }
88984
- }
88985
- };
88986
- function chunk(items, size) {
88987
- const out = [];
88988
- for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
88989
- return out;
88990
- }
88991
- function groupKey(subject, series, scope) {
88992
- return `${subject}${series}${scope}`;
88993
- }
88994
- function rowId(subject, series, scope, bucketStart) {
88995
- return `${subject}|${series}|${scope}|${bucketStart}`;
88996
- }
88997
- function toRow(bucket) {
88998
- return {
88999
- subject: bucket.subject,
89000
- ...bucket.deviceId !== void 0 ? { deviceId: bucket.deviceId } : {},
89001
- series: bucket.series,
89002
- scope: bucket.scope,
89003
- bucketStart: bucket.bucketStart,
89004
- samples: bucket.samples,
89005
- sum: bucket.sum,
89006
- min: bucket.min,
89007
- max: bucket.max
89008
- };
89009
- }
89010
- /** Structural validation on read. A malformed row is skipped, never charted. */
89011
- function recordToRow(data) {
89012
- const subject = data["subject"];
89013
- const series = data["series"];
89014
- const scope = data["scope"];
89015
- if (typeof subject !== "string" || typeof series !== "string") return null;
89016
- const bucketStart = Number(data["bucketStart"]);
89017
- const samples = Number(data["samples"]);
89018
- const sum = Number(data["sum"]);
89019
- const min = Number(data["min"]);
89020
- const max = Number(data["max"]);
89021
- if (![
89022
- bucketStart,
89023
- samples,
89024
- sum,
89025
- min,
89026
- max
89027
- ].every((n) => Number.isFinite(n))) return null;
89028
- const rawDeviceId = data["deviceId"];
89029
- const deviceId = typeof rawDeviceId === "number" && Number.isFinite(rawDeviceId) ? rawDeviceId : void 0;
89030
- return {
89031
- subject,
89032
- ...deviceId !== void 0 ? { deviceId } : {},
89033
- series,
89034
- scope: typeof scope === "string" ? scope : "",
89035
- bucketStart,
89036
- samples,
89037
- sum,
89038
- min,
89039
- max
89040
- };
89041
- }
89042
- //#endregion
89043
- //#region src/node-stress-lts.ts
89044
- /**
89045
- * @durable class=ledger owner=pipeline-orchestrator
89046
- * write="one row per (node, series, 5-min bucket), written ONCE when the bucket closes; a node reporting no samples writes nothing"
89047
- * retention="row CAP per (subject, series, scope) — 105,120 rows ≈ one year of 5-minute buckets. Not an age sweep: an LTS row summarises an interval and has no owner to be orphaned from."
89048
- */
89049
- var NODE_STRESS_LTS_COLLECTION = "pipeline-orchestrator:stats-5m-v2";
89050
- var NodeStressLts = class {
89051
- lts;
89052
- constructor(deps) {
89053
- this.lts = new LtsAggregator({
89054
- collection: NODE_STRESS_LTS_COLLECTION,
89055
- store: deps.store,
89056
- logger: deps.logger,
89057
- ...deps.now !== void 0 ? { now: deps.now } : {}
89058
- });
89059
- }
89060
- static declare(store) {
89061
- return LtsAggregator.declare(store, NODE_STRESS_LTS_COLLECTION);
89062
- }
89063
- /** Fold one sweep's verdict for one node into the open buckets. */
89064
- noteSignals(nodeId, signals, at) {
89065
- this.lts.note({
89066
- subject: nodeId,
89067
- series: "node-score",
89068
- value: signals.score,
89069
- at
89070
- });
89071
- this.lts.note({
89072
- subject: nodeId,
89073
- series: "node-queue-pressure",
89074
- value: signals.queuePressure,
89075
- at
89076
- });
89077
- this.lts.note({
89097
+ for (const s of result.series) for (const p of s.points) rows.push({
89078
89098
  subject: nodeId,
89079
- series: "node-drop-ratio",
89080
- value: signals.dropRatio,
89081
- at
89082
- });
89083
- this.lts.note({
89084
- subject: nodeId,
89085
- series: "node-fps-deficit",
89086
- value: signals.fpsDeficit,
89087
- at
89099
+ series: input.series,
89100
+ scope: s.scope,
89101
+ bucketStart: p.t,
89102
+ samples: p.samples,
89103
+ sum: p.sum,
89104
+ min: p.min,
89105
+ max: p.max
89088
89106
  });
89089
- }
89090
- flushDue(now) {
89091
- return this.lts.flushDue(now);
89092
- }
89093
- read(query) {
89094
- return this.lts.read(query);
89107
+ return rows;
89095
89108
  }
89096
89109
  openBuckets() {
89097
- return this.lts.openBuckets();
89110
+ return [];
89098
89111
  }
89099
89112
  };
89100
89113
  //#endregion
@@ -95045,9 +95058,9 @@ async function buildOrchestratorControllers(deps) {
95045
95058
  kind: orchestratorRecords.kind(orchestratorRecordKinds["node-stress-moves"]),
95046
95059
  logger: deps.ctx().logger.child("node-stress")
95047
95060
  });
95048
- const nodeStressLts = new NodeStressLts({
95049
- store: deps.ctx().api.settingsStore,
95050
- logger: deps.ctx().logger.child("node-stress-lts")
95061
+ const nodeStressLts = new NodeStressHistoryProducer({
95062
+ deviceHistory: nodeStressHistoryClient(deps.ctx().api.deviceHistory),
95063
+ logger: deps.ctx().logger.child("node-stress-history")
95051
95064
  });
95052
95065
  const nodeStress = new NodeStressController({
95053
95066
  ledger,
@@ -95065,7 +95078,7 @@ async function buildOrchestratorControllers(deps) {
95065
95078
  await nodeStress.hydrate().catch((err) => {
95066
95079
  deps.ctx().logger.warn("node-stress move history unavailable — this boot starts cold", { meta: { error: errMsg$2(err) } });
95067
95080
  });
95068
- await NodeStressLts.declare(deps.ctx().api.settingsStore).catch((err) => {
95081
+ await nodeStressLts.declare().catch((err) => {
95069
95082
  deps.ctx().logger.warn("node-stress statistics unavailable — no baseline this boot", { meta: { error: errMsg$2(err) } });
95070
95083
  });
95071
95084
  const nodeStressLtsTimer = setInterval(() => {
@@ -97776,7 +97789,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
97776
97789
  /** Periodic auto-rebalance sweep timer (drift correction under hysteresis). */
97777
97790
  autoRebalanceTimer = null;
97778
97791
  /** Node-stress five-minute statistics + its flush timer (see
97779
- * `node-stress-lts.ts`). `observe` mode's first durable output. */
97792
+ * `node-stress-history-producer.ts`). `observe` mode's first durable output. */
97780
97793
  nodeStressLts = null;
97781
97794
  nodeStressLtsTimer = null;
97782
97795
  initTimestamp = 0;
@@ -99746,7 +99759,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
99746
99759
  return this.isSessionCamera(deviceId) && !this.session.hasActiveSession(deviceId);
99747
99760
  }
99748
99761
  };
99749
- /** The derived mean for one statistics row — see `node-stress-lts.ts`. */
99762
+ /** The derived mean for one closed bucket — see `node-stress-history-producer.ts`. */
99750
99763
  function withStatsMean(row) {
99751
99764
  return {
99752
99765
  ...row,