@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.js CHANGED
@@ -5569,7 +5569,7 @@ var ZodIssueCode = {
5569
5569
  var ZodFirstPartyTypeKind;
5570
5570
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5571
5571
  //#endregion
5572
- //#region ../types/dist/sleep-bZIhUvjd.mjs
5572
+ //#region ../types/dist/sleep-BOjuyoPC.mjs
5573
5573
  /**
5574
5574
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5575
5575
  * window to float samples (D455).
@@ -6389,7 +6389,7 @@ async function writeMembers(ctx, children, incoming, stored, pathOf) {
6389
6389
  for (const [member, node] of children) applyMember(out, member, await writeNode(ctx, node, incoming[member], stored[member], pathOf(member)), stored);
6390
6390
  return out;
6391
6391
  }
6392
- function rowId$1(row, idKey) {
6392
+ function rowId(row, idKey) {
6393
6393
  const raw = row[idKey];
6394
6394
  if (typeof raw === "string" && raw.length > 0) return raw;
6395
6395
  if (typeof raw === "number" && Number.isFinite(raw)) return String(raw);
@@ -6406,7 +6406,7 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
6406
6406
  }
6407
6407
  const storedById = /* @__PURE__ */ new Map();
6408
6408
  for (const row of storedRows) {
6409
- const id = rowId$1(row, node.idKey);
6409
+ const id = rowId(row, node.idKey);
6410
6410
  if (id !== null) storedById.set(id, row);
6411
6411
  }
6412
6412
  const kept = /* @__PURE__ */ new Set();
@@ -6416,13 +6416,13 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
6416
6416
  rows.push(row);
6417
6417
  continue;
6418
6418
  }
6419
- const id = rowId$1(row, node.idKey) ?? ctx.mintId();
6419
+ const id = rowId(row, node.idKey) ?? ctx.mintId();
6420
6420
  kept.add(id);
6421
6421
  const withId = {
6422
6422
  ...row,
6423
6423
  [node.idKey]: row[node.idKey] ?? id
6424
6424
  };
6425
- if (rowId$1(withId, node.idKey) === null) withId[node.idKey] = id;
6425
+ if (rowId(withId, node.idKey) === null) withId[node.idKey] = id;
6426
6426
  const storedRow = storedById.get(id) ?? {};
6427
6427
  rows.push(await writeMembers(ctx, node.item, withId, storedRow, (member) => arrayItemFieldPath(fieldPath, id, member)));
6428
6428
  }
@@ -9327,7 +9327,7 @@ function sleep(ms) {
9327
9327
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
9328
9328
  }
9329
9329
  //#endregion
9330
- //#region ../types/dist/err-msg--2RrYiLO.mjs
9330
+ //#region ../types/dist/err-msg-BXKzRBwg.mjs
9331
9331
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
9332
9332
  DeviceType["Camera"] = "camera";
9333
9333
  DeviceType["Hub"] = "hub";
@@ -9419,6 +9419,10 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
9419
9419
  * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
9420
9420
  * integrations sharing the same food/desiccant/hopper surface. */
9421
9421
  DeviceType["PetFeeder"] = "pet-feeder";
9422
+ /** Built-in dishwasher — program lifecycle, door / remote-start gates,
9423
+ * salt and rinse-aid levels. Installed with the `dishwasher` cap.
9424
+ * Sources: native Bosch Home Connect, reusable by similar integrations. */
9425
+ DeviceType["Dishwasher"] = "dishwasher";
9422
9426
  return DeviceType;
9423
9427
  }({});
9424
9428
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -9676,7 +9680,7 @@ function errMsg$2(err) {
9676
9680
  return String(err);
9677
9681
  }
9678
9682
  //#endregion
9679
- //#region ../types/dist/composition-DOwQB_Ua.mjs
9683
+ //#region ../types/dist/composition-CB6PMT9p.mjs
9680
9684
  /**
9681
9685
  * The ffmpeg build every CamStack node downloads, per platform and arch.
9682
9686
  *
@@ -14542,8 +14546,13 @@ var AlertSeveritySchema = _enum([
14542
14546
  "info",
14543
14547
  "success",
14544
14548
  "warning",
14545
- "error"
14549
+ "error",
14550
+ "critical"
14546
14551
  ]);
14552
+ var AlertActionSchema = object({
14553
+ label: string(),
14554
+ href: string().optional()
14555
+ });
14547
14556
  var AlertStatusSchema = _enum([
14548
14557
  "active",
14549
14558
  "in-progress",
@@ -14561,6 +14570,7 @@ var AlertSchema = object({
14561
14570
  severity: AlertSeveritySchema,
14562
14571
  title: string(),
14563
14572
  message: string(),
14573
+ action: AlertActionSchema.optional(),
14564
14574
  status: AlertStatusSchema,
14565
14575
  progress: number().optional(),
14566
14576
  read: boolean(),
@@ -55483,7 +55493,7 @@ var BatteryStatusSchema = object({
55483
55493
  */
55484
55494
  binary: boolean().optional()
55485
55495
  });
55486
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, method(object({
55496
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, method(object({
55487
55497
  deviceId: number(),
55488
55498
  /** Bound on the wait. Sensible range 3000–10000ms. */
55489
55499
  timeoutMs: number().int().min(500).max(3e4).default(8e3)
@@ -56359,6 +56369,98 @@ object({
56359
56369
  deviceId: number(),
56360
56370
  status: DeviceStatusSchema
56361
56371
  });
56372
+ /**
56373
+ * Smart-dishwasher cap — program lifecycle, door / remote-start gates,
56374
+ * consumable levels (salt, rinse aid) and common program options.
56375
+ *
56376
+ * Sources: native Bosch Home Connect (`dishwasher` device type). Reusable
56377
+ * by any integration that exposes the same program / option surface.
56378
+ *
56379
+ * `kind: 'poll'` — cloud REST APIs typically have no push channel; the
56380
+ * provider refreshes the slice on an interval and eagerly after commands.
56381
+ */
56382
+ var DishwasherStateSchema = _enum([
56383
+ "idle",
56384
+ "running",
56385
+ "paused",
56386
+ "finished",
56387
+ "error",
56388
+ "unavailable"
56389
+ ]);
56390
+ var DishwasherConsumableLevelSchema = _enum([
56391
+ "ok",
56392
+ "low",
56393
+ "empty"
56394
+ ]);
56395
+ object({
56396
+ state: DishwasherStateSchema,
56397
+ /** Active program id/name, or null when none selected / unknown. */
56398
+ program: string().nullable(),
56399
+ /** Program tokens the hardware accepts — drives the UI selector. */
56400
+ availablePrograms: array(string()),
56401
+ /** Minutes remaining on the active cycle, or null when not running. */
56402
+ remainingMinutes: number().int().nonnegative().nullable(),
56403
+ /** Cycle completion 0..100, or null when not applicable. */
56404
+ progressPercent: number().min(0).max(100).nullable(),
56405
+ /** True when the door is reported open. */
56406
+ doorOpen: boolean(),
56407
+ /** True when remote start is allowed (door closed, power on, etc.). */
56408
+ remoteStartAllowed: boolean(),
56409
+ /** Main power / switched-on state. */
56410
+ powerOn: boolean(),
56411
+ saltLevel: DishwasherConsumableLevelSchema.nullable(),
56412
+ rinseAidLevel: DishwasherConsumableLevelSchema.nullable(),
56413
+ halfLoad: boolean(),
56414
+ hygienePlus: boolean(),
56415
+ errorCode: number().nullable(),
56416
+ errorLabel: string().nullable(),
56417
+ lastFetchedAt: number()
56418
+ });
56419
+ DeviceType.Dishwasher, method(object({ deviceId: number().int().nonnegative() }), _void(), {
56420
+ kind: "mutation",
56421
+ auth: "protected"
56422
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
56423
+ kind: "mutation",
56424
+ auth: "protected"
56425
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
56426
+ kind: "mutation",
56427
+ auth: "protected"
56428
+ }), method(object({
56429
+ deviceId: number().int().nonnegative(),
56430
+ program: string().min(1)
56431
+ }), _void(), {
56432
+ kind: "mutation",
56433
+ auth: "protected"
56434
+ }), method(object({
56435
+ deviceId: number().int().nonnegative(),
56436
+ on: boolean()
56437
+ }), _void(), {
56438
+ kind: "mutation",
56439
+ auth: "protected"
56440
+ }), method(object({
56441
+ deviceId: number().int().nonnegative(),
56442
+ on: boolean()
56443
+ }), _void(), {
56444
+ kind: "mutation",
56445
+ auth: "protected"
56446
+ }), method(object({
56447
+ deviceId: number().int().nonnegative(),
56448
+ on: boolean()
56449
+ }), _void(), {
56450
+ kind: "mutation",
56451
+ auth: "protected"
56452
+ }), method(object({
56453
+ deviceId: number().int().nonnegative(),
56454
+ key: string().min(1),
56455
+ value: union([
56456
+ boolean(),
56457
+ string(),
56458
+ number()
56459
+ ])
56460
+ }), _void(), {
56461
+ kind: "mutation",
56462
+ auth: "protected"
56463
+ });
56362
56464
  object({
56363
56465
  /** Ms epoch of the last press. null = never observed since this provider started. */
56364
56466
  lastPressedAt: number().nullable(),
@@ -56924,6 +57026,13 @@ object({
56924
57026
  lastChangedAt: number()
56925
57027
  });
56926
57028
  DeviceType.Sensor;
57029
+ object({
57030
+ /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
57031
+ position: number().min(0).max(100).nullable(),
57032
+ /** Ms epoch when the slice was last updated (push or poll). */
57033
+ lastFetchedAt: number()
57034
+ });
57035
+ DeviceType.Thermostat;
56927
57036
  object({
56928
57037
  /** Whether the humidifier is currently on. */
56929
57038
  on: boolean(),
@@ -64109,7 +64218,7 @@ var AccessoryKind = {
64109
64218
  PrivacyMask: DeviceRole.PrivacyMask
64110
64219
  };
64111
64220
  AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
64112
- 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;
64221
+ 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;
64113
64222
  new Set(Object.values(DeviceType));
64114
64223
  Object.freeze({
64115
64224
  "accessories.getStatus": {
@@ -66530,6 +66639,60 @@ Object.freeze({
66530
66639
  addonId: null,
66531
66640
  access: "view"
66532
66641
  },
66642
+ "dishwasher.getStatus": {
66643
+ capName: "dishwasher",
66644
+ capScope: "device",
66645
+ addonId: null,
66646
+ access: "view"
66647
+ },
66648
+ "dishwasher.pause": {
66649
+ capName: "dishwasher",
66650
+ capScope: "device",
66651
+ addonId: null,
66652
+ access: "create"
66653
+ },
66654
+ "dishwasher.setHalfLoad": {
66655
+ capName: "dishwasher",
66656
+ capScope: "device",
66657
+ addonId: null,
66658
+ access: "create"
66659
+ },
66660
+ "dishwasher.setHygienePlus": {
66661
+ capName: "dishwasher",
66662
+ capScope: "device",
66663
+ addonId: null,
66664
+ access: "create"
66665
+ },
66666
+ "dishwasher.setOption": {
66667
+ capName: "dishwasher",
66668
+ capScope: "device",
66669
+ addonId: null,
66670
+ access: "create"
66671
+ },
66672
+ "dishwasher.setPower": {
66673
+ capName: "dishwasher",
66674
+ capScope: "device",
66675
+ addonId: null,
66676
+ access: "create"
66677
+ },
66678
+ "dishwasher.setProgram": {
66679
+ capName: "dishwasher",
66680
+ capScope: "device",
66681
+ addonId: null,
66682
+ access: "create"
66683
+ },
66684
+ "dishwasher.start": {
66685
+ capName: "dishwasher",
66686
+ capScope: "device",
66687
+ addonId: null,
66688
+ access: "create"
66689
+ },
66690
+ "dishwasher.stop": {
66691
+ capName: "dishwasher",
66692
+ capScope: "device",
66693
+ addonId: null,
66694
+ access: "create"
66695
+ },
66533
66696
  "doorbell.getStatus": {
66534
66697
  capName: "doorbell",
66535
66698
  capScope: "device",
@@ -66740,6 +66903,12 @@ Object.freeze({
66740
66903
  addonId: null,
66741
66904
  access: "view"
66742
66905
  },
66906
+ "heatingValve.getStatus": {
66907
+ capName: "heating-valve",
66908
+ capScope: "device",
66909
+ addonId: null,
66910
+ access: "view"
66911
+ },
66743
66912
  "humidifier.getStatus": {
66744
66913
  capName: "humidifier",
66745
66914
  capScope: "device",
@@ -72327,7 +72496,7 @@ Object.freeze({
72327
72496
  access: "create"
72328
72497
  }
72329
72498
  });
72330
- 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;
72499
+ 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;
72331
72500
  /**
72332
72501
  * Heuristic check — `device.features?.includes(profile.when.hasFeature)`.
72333
72502
  */
@@ -73179,6 +73348,51 @@ Object.freeze({
73179
73348
  form: "single",
73180
73349
  optional: false
73181
73350
  }],
73351
+ "dishwasher.getStatus": [{
73352
+ name: "deviceId",
73353
+ form: "single",
73354
+ optional: false
73355
+ }],
73356
+ "dishwasher.pause": [{
73357
+ name: "deviceId",
73358
+ form: "single",
73359
+ optional: false
73360
+ }],
73361
+ "dishwasher.setHalfLoad": [{
73362
+ name: "deviceId",
73363
+ form: "single",
73364
+ optional: false
73365
+ }],
73366
+ "dishwasher.setHygienePlus": [{
73367
+ name: "deviceId",
73368
+ form: "single",
73369
+ optional: false
73370
+ }],
73371
+ "dishwasher.setOption": [{
73372
+ name: "deviceId",
73373
+ form: "single",
73374
+ optional: false
73375
+ }],
73376
+ "dishwasher.setPower": [{
73377
+ name: "deviceId",
73378
+ form: "single",
73379
+ optional: false
73380
+ }],
73381
+ "dishwasher.setProgram": [{
73382
+ name: "deviceId",
73383
+ form: "single",
73384
+ optional: false
73385
+ }],
73386
+ "dishwasher.start": [{
73387
+ name: "deviceId",
73388
+ form: "single",
73389
+ optional: false
73390
+ }],
73391
+ "dishwasher.stop": [{
73392
+ name: "deviceId",
73393
+ form: "single",
73394
+ optional: false
73395
+ }],
73182
73396
  "doorbell.getStatus": [{
73183
73397
  name: "deviceId",
73184
73398
  form: "single",
@@ -73248,6 +73462,11 @@ Object.freeze({
73248
73462
  form: "single",
73249
73463
  optional: false
73250
73464
  }],
73465
+ "heatingValve.getStatus": [{
73466
+ name: "deviceId",
73467
+ form: "single",
73468
+ optional: false
73469
+ }],
73251
73470
  "humidifier.getStatus": [{
73252
73471
  name: "deviceId",
73253
73472
  form: "single",
@@ -75071,6 +75290,103 @@ Object.freeze({
75071
75290
  "network-access": "ingress",
75072
75291
  "smtp-provider": "email"
75073
75292
  });
75293
+ /** Width of a bucket the history store accepts. */
75294
+ var HISTORY_BUCKET_MS = 5 * 6e4;
75295
+ function bucketStartOf(at) {
75296
+ return Math.floor(at / HISTORY_BUCKET_MS) * HISTORY_BUCKET_MS;
75297
+ }
75298
+ function subjectId(subject) {
75299
+ return subject.kind === "device" ? `d:${subject.deviceId}` : `n:${subject.nodeId}`;
75300
+ }
75301
+ function seriesIdentity(key, subject, scope) {
75302
+ return `${key}\u0000${subjectId(subject)}\u0000${scope}`;
75303
+ }
75304
+ function toClosed(b) {
75305
+ return {
75306
+ key: b.key,
75307
+ subject: b.subject,
75308
+ scope: b.scope,
75309
+ bucketStart: b.bucketStart,
75310
+ samples: b.samples,
75311
+ sum: b.sum,
75312
+ min: b.min,
75313
+ max: b.max,
75314
+ last: b.last,
75315
+ lastTs: b.lastTs
75316
+ };
75317
+ }
75318
+ var BucketAccumulator = class {
75319
+ open = /* @__PURE__ */ new Map();
75320
+ requeued = /* @__PURE__ */ new Map();
75321
+ /** Add one observation. A non-finite value or timestamp is ignored (never counted as 0). */
75322
+ note(key, subject, scope, value, at) {
75323
+ if (!Number.isFinite(value) || !Number.isFinite(at)) return;
75324
+ const bucketStart = bucketStartOf(at);
75325
+ const id = `${seriesIdentity(key, subject, scope)}\u0000${bucketStart}`;
75326
+ const existing = this.open.get(id);
75327
+ if (existing === void 0) {
75328
+ this.open.set(id, {
75329
+ key,
75330
+ subject,
75331
+ scope,
75332
+ bucketStart,
75333
+ samples: 1,
75334
+ sum: value,
75335
+ min: value,
75336
+ max: value,
75337
+ last: value,
75338
+ lastTs: at
75339
+ });
75340
+ return;
75341
+ }
75342
+ existing.samples += 1;
75343
+ existing.sum += value;
75344
+ if (value < existing.min) existing.min = value;
75345
+ if (value > existing.max) existing.max = value;
75346
+ if (at >= existing.lastTs) {
75347
+ existing.last = value;
75348
+ existing.lastTs = at;
75349
+ }
75350
+ }
75351
+ /**
75352
+ * Buckets whose window ended at or before `now`, oldest first, requeued ones
75353
+ * included. They leave the accumulator: delivery is the caller's.
75354
+ */
75355
+ takeClosed(now) {
75356
+ const out = [];
75357
+ for (const [id, bucket] of this.open) if (bucket.bucketStart + 3e5 <= now) {
75358
+ out.push(toClosed(bucket));
75359
+ this.open.delete(id);
75360
+ }
75361
+ for (const list of this.requeued.values()) out.push(...list);
75362
+ this.requeued.clear();
75363
+ return out.toSorted((a, b) => a.bucketStart - b.bucketStart);
75364
+ }
75365
+ /**
75366
+ * Put undelivered buckets back for the next `takeClosed`. Keeps the newest
75367
+ * {@link MAX_REQUEUED_BUCKETS_PER_SERIES} per series and returns the buckets
75368
+ * it dropped (empty when none).
75369
+ */
75370
+ requeue(buckets) {
75371
+ const dropped = [];
75372
+ for (const bucket of buckets) {
75373
+ const id = seriesIdentity(bucket.key, bucket.subject, bucket.scope);
75374
+ const list = [...this.requeued.get(id) ?? [], bucket].toSorted((a, b) => a.bucketStart - b.bucketStart);
75375
+ while (list.length > 12) {
75376
+ const oldest = list.shift();
75377
+ if (oldest !== void 0) dropped.push(oldest);
75378
+ }
75379
+ this.requeued.set(id, list);
75380
+ }
75381
+ return dropped;
75382
+ }
75383
+ /** Buckets currently held (open plus requeued) — for a gauge in the producer's own status. */
75384
+ pendingBucketCount() {
75385
+ let n = this.open.size;
75386
+ for (const list of this.requeued.values()) n += list.length;
75387
+ return n;
75388
+ }
75389
+ };
75074
75390
  DeviceType.Camera;
75075
75391
  var G711_SCALE_CORRECTION_DB = {
75076
75392
  PCMU: 20 * Math.log10(4),
@@ -88699,430 +89015,127 @@ var NodeStressController = class NodeStressController {
88699
89015
  }
88700
89016
  };
88701
89017
  //#endregion
88702
- //#region src/durable/lts-aggregator.ts
88703
- /** Wall-clock bucket width. Five minutes, matching HA's statistics tier. */
88704
- var LTS_BUCKET_MS = 5 * 6e4;
88705
- /** Buckets written per transaction — bounds one statement, not the flush. */
88706
- var FLUSH_BATCH = 200;
88707
- /** How often the row cap is enforced. Rarely: it is a bound, not a deadline. */
88708
- var CAP_SWEEP_INTERVAL_MS = 6 * 36e5;
88709
- /**
88710
- * The row's identity IS its four key columns (D871): a composite
88711
- * `primaryKey` + `WITHOUT ROWID`, so the table is its own index and there is no
88712
- * TEXT `id` repeating what the row already says. The old layout kept
88713
- * `<subject>|<series>|<scope>|<bucketStart>` as a TEXT primary key beside the
88714
- * four columns — 85 MB of rows plus a 44 MB autoindex for 1.07 M rows. A
88715
- * composite-key collection is written with `upsertMany` (a bucket flushed twice
88716
- * replaces itself) and read with `query` / `count` / `deleteWhere`; the calls
88717
- * that address one row by id are refused by the engine.
88718
- */
88719
- var LTS_KEY_COLUMNS = [
88720
- "subject",
88721
- "series",
88722
- "scope",
88723
- "bucketStart"
88724
- ];
88725
- var LTS_COLUMNS = [
88726
- (
88727
- /** The camera id, or the node id. One column, because every query is
88728
- * "this thing over time" and the thing is one or the other. */
88729
- {
88730
- name: "subject",
88731
- type: "TEXT"
88732
- }),
88733
- (
88734
- /** Numeric mirror of `subject` for camera rows, so a per-camera question is
88735
- * answered by an integer index — every log line and every query about a
88736
- * device in this repo is keyed by the numeric id. `NULL` for node rows. */
88737
- {
88738
- name: "deviceId",
88739
- type: "INTEGER"
88740
- }),
88741
- {
88742
- name: "series",
88743
- type: "TEXT"
88744
- },
88745
- (
88746
- /** Sub-scope within a series: a zoneId for occupancy, `''` otherwise. */
88747
- {
88748
- name: "scope",
88749
- type: "TEXT"
88750
- }),
88751
- (
88752
- /** Bucket start, wall-clock aligned to {@link LTS_BUCKET_MS}. */
88753
- {
88754
- name: "bucketStart",
88755
- type: "INTEGER"
88756
- }),
89018
+ //#region src/node-stress-history-producer.ts
89019
+ var SERIES_KEYS = {
89020
+ "node-score": "addon:pipeline-orchestrator/node-score",
89021
+ "node-queue-pressure": "addon:pipeline-orchestrator/node-queue-pressure",
89022
+ "node-drop-ratio": "addon:pipeline-orchestrator/node-drop-ratio",
89023
+ "node-fps-deficit": "addon:pipeline-orchestrator/node-fps-deficit"
89024
+ };
89025
+ function nodeStressHistoryClient(api) {
89026
+ return {
89027
+ declareSeries: (input) => api.declareSeries.mutate(input),
89028
+ recordBuckets: (input) => api.recordBuckets.mutate({ buckets: [...input.buckets] }),
89029
+ queryNode: async (input) => {
89030
+ return { series: (await api.queryNode.query(input)).series.map((s) => ({
89031
+ scope: s.scope,
89032
+ points: s.points.flatMap((p) => {
89033
+ if (p.mean === null) return [];
89034
+ return [{
89035
+ t: p.t,
89036
+ samples: p.samples,
89037
+ sum: p.mean * p.samples,
89038
+ min: p.min,
89039
+ max: p.max
89040
+ }];
89041
+ })
89042
+ })) };
89043
+ }
89044
+ };
89045
+ }
89046
+ var SERIES_META = [
88757
89047
  {
88758
- name: "samples",
88759
- type: "INTEGER",
88760
- notNull: true
89048
+ name: "node-score",
89049
+ displayName: "Node stress score"
88761
89050
  },
88762
89051
  {
88763
- name: "sum",
88764
- type: "REAL",
88765
- notNull: true
89052
+ name: "node-queue-pressure",
89053
+ displayName: "Node queue pressure"
88766
89054
  },
88767
89055
  {
88768
- name: "min",
88769
- type: "REAL",
88770
- notNull: true
89056
+ name: "node-drop-ratio",
89057
+ displayName: "Node drop ratio"
88771
89058
  },
88772
89059
  {
88773
- name: "max",
88774
- type: "REAL",
88775
- notNull: true
89060
+ name: "node-fps-deficit",
89061
+ displayName: "Node FPS deficit"
88776
89062
  }
88777
89063
  ];
88778
- /**
88779
- * The primary key serves every per-subject read (`subject, series, scope` then a
88780
- * `bucketStart` range, already in bucket order); this index serves the read that
88781
- * names a series without a subject. The old `deviceId` index went with the TEXT
88782
- * id: nothing queries a series by the numeric mirror.
88783
- */
88784
- var LTS_INDEXES = [{
88785
- name: "idx_lts_series_bucket",
88786
- columns: ["series", "bucketStart"]
88787
- }];
88788
- function ltsBucketStart(at, bucketMs = LTS_BUCKET_MS) {
88789
- return Math.floor(at / bucketMs) * bucketMs;
88790
- }
88791
- var LtsAggregator = class {
88792
- open = /* @__PURE__ */ new Map();
88793
- /** Every (subject, series, scope) that has produced a row in this process —
88794
- * the groups the cap sweep has any reason to look at. */
88795
- groups = /* @__PURE__ */ new Set();
88796
- lastCapSweepAt = 0;
88797
- collection;
88798
- store;
89064
+ var NodeStressHistoryProducer = class {
89065
+ accum = new BucketAccumulator();
89066
+ deviceHistory;
88799
89067
  logger;
88800
89068
  nowFn;
88801
- bucketMs;
88802
- maxRows;
89069
+ declared = false;
88803
89070
  constructor(deps) {
88804
- this.collection = deps.collection;
88805
- this.store = deps.store;
89071
+ this.deviceHistory = deps.deviceHistory;
88806
89072
  this.logger = deps.logger;
88807
89073
  this.nowFn = deps.now ?? (() => Date.now());
88808
- this.bucketMs = deps.bucketMs ?? 3e5;
88809
- this.maxRows = deps.maxRowsPerSeries ?? 105120;
88810
- }
88811
- static declare(store, collection) {
88812
- return store.declareCollection.mutate({
88813
- collection,
88814
- columns: [...LTS_COLUMNS],
88815
- primaryKey: [...LTS_KEY_COLUMNS],
88816
- withoutRowid: true,
88817
- indexes: [...LTS_INDEXES]
88818
- });
88819
- }
88820
- /**
88821
- * Record one observation. Synchronous, allocation-free after the first
88822
- * sample of a bucket, and it cannot throw — it sits on paths that are
88823
- * already producing the value for another reason and must not learn a new
88824
- * failure mode.
88825
- *
88826
- * A non-finite value is DROPPED rather than folded in: one `NaN` would make
88827
- * `sum`, `min` and `max` all `NaN` for the whole bucket, turning a skewed
88828
- * row into a meaningless one.
88829
- */
88830
- note(input) {
88831
- if (!Number.isFinite(input.value)) return;
88832
- const at = input.at ?? this.nowFn();
88833
- const scope = input.scope ?? "";
88834
- const bucketStart = ltsBucketStart(at, this.bucketMs);
88835
- const key = rowId(input.subject, input.series, scope, bucketStart);
88836
- const held = this.open.get(key);
88837
- if (held === void 0) {
88838
- this.open.set(key, {
88839
- subject: input.subject,
88840
- ...input.deviceId !== void 0 ? { deviceId: input.deviceId } : {},
88841
- series: input.series,
88842
- scope,
88843
- bucketStart,
88844
- samples: 1,
88845
- sum: input.value,
88846
- min: input.value,
88847
- max: input.value
88848
- });
88849
- return;
88850
- }
88851
- held.samples += 1;
88852
- held.sum += input.value;
88853
- if (input.value < held.min) held.min = input.value;
88854
- if (input.value > held.max) held.max = input.value;
88855
- }
88856
- /** Buckets currently accumulating — diagnostics, and what a flush would write. */
88857
- openBuckets() {
88858
- return [...this.open.values()].map(toRow);
88859
89074
  }
88860
- /**
88861
- * Write every bucket that has CLOSED (its window ended at or before `now`)
88862
- * and drop it from RAM. Returns the number of rows written.
88863
- *
88864
- * The current bucket is deliberately left alone: writing it early would mean
88865
- * rewriting it on the next tick, which turns one row into up to sixty and
88866
- * offers every one of them to the checkpoint lottery (D96).
88867
- */
88868
- async flushDue(now = this.nowFn()) {
88869
- const currentBucket = ltsBucketStart(now, this.bucketMs);
88870
- const due = [...this.open.values()].filter((b) => b.bucketStart < currentBucket);
88871
- if (due.length === 0) {
88872
- await this.maybeSweepCap(now);
88873
- return 0;
88874
- }
88875
- let written = 0;
88876
- for (const batch of chunk(due, FLUSH_BATCH)) {
88877
- written += await this.writeBatch(batch);
88878
- for (const bucket of batch) this.open.delete(rowId(bucket.subject, bucket.series, bucket.scope, bucket.bucketStart));
88879
- }
88880
- await this.maybeSweepCap(now);
88881
- return written;
89075
+ async declare() {
89076
+ if (this.declared) return;
89077
+ for (const meta of SERIES_META) await this.deviceHistory.declareSeries({
89078
+ name: meta.name,
89079
+ subjectKind: "node",
89080
+ kind: "gauge",
89081
+ displayName: meta.displayName,
89082
+ ingest: "buckets"
89083
+ });
89084
+ this.declared = true;
88882
89085
  }
88883
- /**
88884
- * Upsert one batch in a single transaction. If the batch is refused as a
88885
- * whole (all-or-nothing), each bucket is retried alone so one bad row costs
88886
- * one interval, not forty. Returns the number of rows written.
88887
- */
88888
- async writeBatch(batch) {
89086
+ noteSignals(nodeId, signals, at) {
89087
+ const subject = {
89088
+ kind: "node",
89089
+ nodeId
89090
+ };
89091
+ this.accum.note(SERIES_KEYS["node-score"], subject, "", signals.score, at);
89092
+ this.accum.note(SERIES_KEYS["node-queue-pressure"], subject, "", signals.queuePressure, at);
89093
+ this.accum.note(SERIES_KEYS["node-drop-ratio"], subject, "", signals.dropRatio, at);
89094
+ this.accum.note(SERIES_KEYS["node-fps-deficit"], subject, "", signals.fpsDeficit, at);
89095
+ }
89096
+ async flushDue(now) {
89097
+ const closed = this.accum.takeClosed(now ?? this.nowFn());
89098
+ if (closed.length === 0) return 0;
88889
89099
  try {
88890
- await this.upsert(batch);
88891
- for (const bucket of batch) this.groups.add(groupKey(bucket.subject, bucket.series, bucket.scope));
88892
- return batch.length;
89100
+ return (await this.deviceHistory.recordBuckets({ buckets: closed })).accepted;
88893
89101
  } catch (err) {
88894
- if (batch.length > 1) {
88895
- let written = 0;
88896
- for (const bucket of batch) written += await this.writeBatch([bucket]);
88897
- return written;
88898
- }
88899
- const bucket = batch[0];
88900
- this.logger.warn("lts bucket write failed — this interval will be missing", {
88901
- ...bucket?.deviceId !== void 0 ? { tags: { deviceId: bucket.deviceId } } : {},
88902
- meta: {
88903
- collection: this.collection,
88904
- series: bucket?.series,
88905
- subject: bucket?.subject,
88906
- bucketStart: bucket?.bucketStart,
88907
- error: String(err)
88908
- }
88909
- });
89102
+ const dropped = this.accum.requeue(closed);
89103
+ this.logger.warn("node-stress history flush failed — requeued", { meta: {
89104
+ buckets: closed.length,
89105
+ dropped: dropped.length,
89106
+ error: err instanceof Error ? err.message : String(err)
89107
+ } });
88910
89108
  return 0;
88911
89109
  }
88912
89110
  }
88913
- async upsert(batch) {
88914
- await this.store.upsertMany.mutate({
88915
- collection: this.collection,
88916
- conflict: [...LTS_KEY_COLUMNS],
88917
- records: batch.map((bucket) => ({ data: {
88918
- subject: bucket.subject,
88919
- deviceId: bucket.deviceId ?? null,
88920
- series: bucket.series,
88921
- scope: bucket.scope,
88922
- bucketStart: bucket.bucketStart,
88923
- samples: bucket.samples,
88924
- sum: bucket.sum,
88925
- min: bucket.min,
88926
- max: bucket.max
88927
- } }))
88928
- });
88929
- }
88930
- /** Read closed buckets back. The read surface every chart will use. */
88931
- async read(query = {}) {
88932
- const where = {};
88933
- if (query.series !== void 0) where["series"] = query.series;
88934
- if (query.subject !== void 0) where["subject"] = query.subject;
88935
- if (query.scope !== void 0) where["scope"] = query.scope;
88936
- const records = await this.store.query.query({
88937
- collection: this.collection,
88938
- filter: {
88939
- ...Object.keys(where).length > 0 ? { where } : {},
88940
- ...query.from !== void 0 || query.to !== void 0 ? { whereBetween: { bucketStart: [query.from ?? 0, query.to ?? Number.MAX_SAFE_INTEGER] } } : {},
88941
- orderBy: {
88942
- field: "bucketStart",
88943
- direction: "asc"
88944
- },
88945
- limit: query.limit ?? 5e3
88946
- }
89111
+ async read(input) {
89112
+ const nodeId = input.subject;
89113
+ if (nodeId === void 0 || input.series === void 0) return [];
89114
+ const key = SERIES_KEYS[input.series];
89115
+ if (key === void 0) return [];
89116
+ const result = await this.deviceHistory.queryNode({
89117
+ nodeId,
89118
+ key,
89119
+ from: input.from ?? 0,
89120
+ to: input.to ?? Date.now(),
89121
+ resolution: "5m",
89122
+ ...input.maxPoints !== void 0 ? { maxPoints: input.maxPoints } : {}
88947
89123
  });
88948
89124
  const rows = [];
88949
- for (const record of records) {
88950
- const row = recordToRow(record.data);
88951
- if (row !== null) rows.push(row);
88952
- }
88953
- return rows;
88954
- }
88955
- /**
88956
- * Enforce the per-(subject, series, scope) row cap, at most once every six
88957
- * hours. Only groups this process has written to are examined: a group with
88958
- * no new rows cannot have crossed a cap it was under.
88959
- */
88960
- async maybeSweepCap(now) {
88961
- if (now - this.lastCapSweepAt < CAP_SWEEP_INTERVAL_MS) return;
88962
- this.lastCapSweepAt = now;
88963
- for (const group of this.groups) {
88964
- const [subject, series, scope] = group.split("\0");
88965
- if (subject === void 0 || series === void 0 || scope === void 0) continue;
88966
- const where = {
88967
- subject,
88968
- series,
88969
- scope
88970
- };
88971
- try {
88972
- const excess = await this.store.count.query({
88973
- collection: this.collection,
88974
- filter: { where }
88975
- }) - this.maxRows;
88976
- if (excess <= 0) continue;
88977
- const cutoff = (await this.store.query.query({
88978
- collection: this.collection,
88979
- filter: {
88980
- where,
88981
- orderBy: {
88982
- field: "bucketStart",
88983
- direction: "asc"
88984
- },
88985
- limit: excess
88986
- }
88987
- })).at(-1)?.data["bucketStart"];
88988
- if (typeof cutoff !== "number") continue;
88989
- const { deleted } = await this.store.deleteWhere.mutate({
88990
- collection: this.collection,
88991
- filter: {
88992
- where,
88993
- whereBetween: { bucketStart: [0, cutoff] }
88994
- }
88995
- });
88996
- this.logger.info("lts row cap enforced", { meta: {
88997
- collection: this.collection,
88998
- series,
88999
- subject,
89000
- deleted,
89001
- cap: this.maxRows
89002
- } });
89003
- } catch (err) {
89004
- this.logger.warn("lts row cap sweep failed — the series keeps growing this cycle", { meta: {
89005
- collection: this.collection,
89006
- series,
89007
- subject,
89008
- error: String(err)
89009
- } });
89010
- }
89011
- }
89012
- }
89013
- };
89014
- function chunk(items, size) {
89015
- const out = [];
89016
- for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
89017
- return out;
89018
- }
89019
- function groupKey(subject, series, scope) {
89020
- return `${subject}${series}${scope}`;
89021
- }
89022
- function rowId(subject, series, scope, bucketStart) {
89023
- return `${subject}|${series}|${scope}|${bucketStart}`;
89024
- }
89025
- function toRow(bucket) {
89026
- return {
89027
- subject: bucket.subject,
89028
- ...bucket.deviceId !== void 0 ? { deviceId: bucket.deviceId } : {},
89029
- series: bucket.series,
89030
- scope: bucket.scope,
89031
- bucketStart: bucket.bucketStart,
89032
- samples: bucket.samples,
89033
- sum: bucket.sum,
89034
- min: bucket.min,
89035
- max: bucket.max
89036
- };
89037
- }
89038
- /** Structural validation on read. A malformed row is skipped, never charted. */
89039
- function recordToRow(data) {
89040
- const subject = data["subject"];
89041
- const series = data["series"];
89042
- const scope = data["scope"];
89043
- if (typeof subject !== "string" || typeof series !== "string") return null;
89044
- const bucketStart = Number(data["bucketStart"]);
89045
- const samples = Number(data["samples"]);
89046
- const sum = Number(data["sum"]);
89047
- const min = Number(data["min"]);
89048
- const max = Number(data["max"]);
89049
- if (![
89050
- bucketStart,
89051
- samples,
89052
- sum,
89053
- min,
89054
- max
89055
- ].every((n) => Number.isFinite(n))) return null;
89056
- const rawDeviceId = data["deviceId"];
89057
- const deviceId = typeof rawDeviceId === "number" && Number.isFinite(rawDeviceId) ? rawDeviceId : void 0;
89058
- return {
89059
- subject,
89060
- ...deviceId !== void 0 ? { deviceId } : {},
89061
- series,
89062
- scope: typeof scope === "string" ? scope : "",
89063
- bucketStart,
89064
- samples,
89065
- sum,
89066
- min,
89067
- max
89068
- };
89069
- }
89070
- //#endregion
89071
- //#region src/node-stress-lts.ts
89072
- /**
89073
- * @durable class=ledger owner=pipeline-orchestrator
89074
- * write="one row per (node, series, 5-min bucket), written ONCE when the bucket closes; a node reporting no samples writes nothing"
89075
- * 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."
89076
- */
89077
- var NODE_STRESS_LTS_COLLECTION = "pipeline-orchestrator:stats-5m-v2";
89078
- var NodeStressLts = class {
89079
- lts;
89080
- constructor(deps) {
89081
- this.lts = new LtsAggregator({
89082
- collection: NODE_STRESS_LTS_COLLECTION,
89083
- store: deps.store,
89084
- logger: deps.logger,
89085
- ...deps.now !== void 0 ? { now: deps.now } : {}
89086
- });
89087
- }
89088
- static declare(store) {
89089
- return LtsAggregator.declare(store, NODE_STRESS_LTS_COLLECTION);
89090
- }
89091
- /** Fold one sweep's verdict for one node into the open buckets. */
89092
- noteSignals(nodeId, signals, at) {
89093
- this.lts.note({
89094
- subject: nodeId,
89095
- series: "node-score",
89096
- value: signals.score,
89097
- at
89098
- });
89099
- this.lts.note({
89100
- subject: nodeId,
89101
- series: "node-queue-pressure",
89102
- value: signals.queuePressure,
89103
- at
89104
- });
89105
- this.lts.note({
89125
+ for (const s of result.series) for (const p of s.points) rows.push({
89106
89126
  subject: nodeId,
89107
- series: "node-drop-ratio",
89108
- value: signals.dropRatio,
89109
- at
89110
- });
89111
- this.lts.note({
89112
- subject: nodeId,
89113
- series: "node-fps-deficit",
89114
- value: signals.fpsDeficit,
89115
- at
89127
+ series: input.series,
89128
+ scope: s.scope,
89129
+ bucketStart: p.t,
89130
+ samples: p.samples,
89131
+ sum: p.sum,
89132
+ min: p.min,
89133
+ max: p.max
89116
89134
  });
89117
- }
89118
- flushDue(now) {
89119
- return this.lts.flushDue(now);
89120
- }
89121
- read(query) {
89122
- return this.lts.read(query);
89135
+ return rows;
89123
89136
  }
89124
89137
  openBuckets() {
89125
- return this.lts.openBuckets();
89138
+ return [];
89126
89139
  }
89127
89140
  };
89128
89141
  //#endregion
@@ -95073,9 +95086,9 @@ async function buildOrchestratorControllers(deps) {
95073
95086
  kind: orchestratorRecords.kind(orchestratorRecordKinds["node-stress-moves"]),
95074
95087
  logger: deps.ctx().logger.child("node-stress")
95075
95088
  });
95076
- const nodeStressLts = new NodeStressLts({
95077
- store: deps.ctx().api.settingsStore,
95078
- logger: deps.ctx().logger.child("node-stress-lts")
95089
+ const nodeStressLts = new NodeStressHistoryProducer({
95090
+ deviceHistory: nodeStressHistoryClient(deps.ctx().api.deviceHistory),
95091
+ logger: deps.ctx().logger.child("node-stress-history")
95079
95092
  });
95080
95093
  const nodeStress = new NodeStressController({
95081
95094
  ledger,
@@ -95093,7 +95106,7 @@ async function buildOrchestratorControllers(deps) {
95093
95106
  await nodeStress.hydrate().catch((err) => {
95094
95107
  deps.ctx().logger.warn("node-stress move history unavailable — this boot starts cold", { meta: { error: errMsg$2(err) } });
95095
95108
  });
95096
- await NodeStressLts.declare(deps.ctx().api.settingsStore).catch((err) => {
95109
+ await nodeStressLts.declare().catch((err) => {
95097
95110
  deps.ctx().logger.warn("node-stress statistics unavailable — no baseline this boot", { meta: { error: errMsg$2(err) } });
95098
95111
  });
95099
95112
  const nodeStressLtsTimer = setInterval(() => {
@@ -97804,7 +97817,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
97804
97817
  /** Periodic auto-rebalance sweep timer (drift correction under hysteresis). */
97805
97818
  autoRebalanceTimer = null;
97806
97819
  /** Node-stress five-minute statistics + its flush timer (see
97807
- * `node-stress-lts.ts`). `observe` mode's first durable output. */
97820
+ * `node-stress-history-producer.ts`). `observe` mode's first durable output. */
97808
97821
  nodeStressLts = null;
97809
97822
  nodeStressLtsTimer = null;
97810
97823
  initTimestamp = 0;
@@ -99774,7 +99787,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
99774
99787
  return this.isSessionCamera(deviceId) && !this.session.hasActiveSession(deviceId);
99775
99788
  }
99776
99789
  };
99777
- /** The derived mean for one statistics row — see `node-stress-lts.ts`. */
99790
+ /** The derived mean for one closed bucket — see `node-stress-history-producer.ts`. */
99778
99791
  function withStatsMean(row) {
99779
99792
  return {
99780
99793
  ...row,