@camstack/addon-pipeline-orchestrator 1.2.325 → 1.2.327

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
@@ -5418,7 +5418,7 @@ var ZodIssueCode = {
5418
5418
  var ZodFirstPartyTypeKind;
5419
5419
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5420
5420
  //#endregion
5421
- //#region ../types/dist/sleep-DRqxtc0C.mjs
5421
+ //#region ../types/dist/sleep-bwsTj221.mjs
5422
5422
  /**
5423
5423
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5424
5424
  * window to float samples (D455).
@@ -6685,7 +6685,6 @@ function createHandle(raw, opts) {
6685
6685
  const writeAddon = (patch) => write("addon", opts.addonTree(), void 0, patch, () => readStoredAddonBlob(raw), (p) => raw.writeAddonStore(p));
6686
6686
  const readAddonStoreResult = raw.readAddonStoreResult;
6687
6687
  const readAddonStoreKeyResult = raw.readAddonStoreKeyResult;
6688
- const readDeviceStoreBatch = raw.readDeviceStoreBatch;
6689
6688
  const overrides = {
6690
6689
  readAddonStore: async () => resolveRead("addon", await raw.readAddonStore()),
6691
6690
  ...readAddonStoreResult !== void 0 ? { readAddonStoreResult: async () => {
@@ -6709,12 +6708,12 @@ function createHandle(raw, opts) {
6709
6708
  };
6710
6709
  } } : {},
6711
6710
  readDeviceStore: async (deviceId) => resolveRead(`device:${deviceId}`, await raw.readDeviceStore(deviceId)),
6712
- ...readDeviceStoreBatch !== void 0 ? { readDeviceStoreBatch: async (deviceIds) => {
6713
- const rows = await readDeviceStoreBatch.call(raw, deviceIds);
6711
+ readDeviceStoreBatch: async (deviceIds) => {
6712
+ const rows = await raw.readDeviceStoreBatch(deviceIds);
6714
6713
  const out = /* @__PURE__ */ new Map();
6715
6714
  for (const [deviceId, blob] of rows) out.set(deviceId, await resolveRead(`device:${deviceId}`, blob));
6716
6715
  return out;
6717
- } } : {},
6716
+ },
6718
6717
  writeAddonStore: async (patch) => {
6719
6718
  await writeAddon(patch);
6720
6719
  },
@@ -7541,7 +7540,7 @@ var BaseAddon = class {
7541
7540
  return this._addonSecretTree;
7542
7541
  }
7543
7542
  deviceSecretTree() {
7544
- this._deviceSecretTree ??= secretTreeFromSchema(this.deviceSettingsSchema());
7543
+ this._deviceSecretTree ??= mergeSecretStatePaths(secretTreeFromSchema(this.deviceSettingsSchema()), this.secretStatePaths);
7545
7544
  return this._deviceSecretTree;
7546
7545
  }
7547
7546
  /**
@@ -7617,42 +7616,135 @@ var BaseAddon = class {
7617
7616
  this[timerOf] = timer;
7618
7617
  }
7619
7618
  /**
7620
- * Wrap `ctx.settings.readAddonStore()` with a short retry budget so a
7621
- * transient settings-store outage (mid-restart of sqlite-settings, tsx-watch
7622
- * swap, or any race where the SqliteSettingsBackend is between shutdown and
7623
- * re-initialize) doesn't propagate up into `initialize()` and leave the
7619
+ * Wrap `ctx.settings.readAddonStore()` with a retry budget so a transient
7620
+ * settings-store outage doesn't propagate up into `initialize()` and leave the
7624
7621
  * addon permanently broken.
7625
7622
  *
7626
- * Retry only the two known infra fingerprints. Anything else propagates so
7627
- * real bugs surface immediately. After the budget expires we fall back to
7628
- * `{}` (defaults) — the addon's first successful patch will rehydrate.
7623
+ * Two kinds of outage, two budgets:
7624
+ * - the store's own restart fingerprints (`SqliteSettingsBackend not
7625
+ * initialized`, `provider not available`): a short budget, then the addon
7626
+ * falls back to `{}` (defaults) - its first successful patch rehydrates;
7627
+ * - a TRANSPORT failure (the hub is restarting while this runner boots:
7628
+ * `discovery deadline exceeded`, `parent could not route`, a refused or
7629
+ * timed-out connection). Since D870 a runner's settings read THROWS instead
7630
+ * of answering `{}`, so without a budget a hub restart ends in a respawn
7631
+ * loop. A long, bounded budget; only after it the error propagates - the
7632
+ * addon never runs on defaults for a store it could not ask.
7633
+ * Anything else propagates immediately so real bugs surface.
7629
7634
  */
7630
7635
  async readAddonStoreWithRetry() {
7631
7636
  const settings = this.rawSettings();
7632
7637
  if (!settings) return {};
7633
- const delaysMs = [
7634
- 150,
7635
- 350,
7636
- 600,
7637
- 900
7638
- ];
7639
7638
  let lastErr;
7640
- for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
7639
+ for (let attempt = 0;; attempt++) try {
7641
7640
  const stored = await settings.readAddonStore() ?? {};
7642
7641
  this.settingsStoreReady = true;
7643
7642
  return stored;
7644
7643
  } catch (err) {
7645
7644
  lastErr = err;
7646
- const msg = err instanceof Error ? err.message : String(err);
7647
- if (!(msg.includes("SqliteSettingsBackend not initialized") || msg.includes("provider not available"))) throw err;
7648
- if (attempt === delaysMs.length) break;
7649
- await new Promise((r) => setTimeout(r, delaysMs[attempt]));
7645
+ const kind = classifyStoreError(err);
7646
+ if (kind === "permanent") throw err;
7647
+ const delay = (kind === "transport" ? TRANSPORT_RETRY_DELAYS_MS : RESTART_RETRY_DELAYS_MS)[attempt];
7648
+ if (delay === void 0) {
7649
+ if (kind === "transport") throw err;
7650
+ break;
7651
+ }
7652
+ await new Promise((r) => setTimeout(r, delay));
7650
7653
  }
7651
7654
  this.settingsStoreReady = false;
7652
7655
  this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
7653
7656
  return {};
7654
7657
  }
7655
7658
  };
7659
+ /** The settings store's own restart fingerprints. */
7660
+ var RESTART_RETRY_DELAYS_MS = [
7661
+ 150,
7662
+ 350,
7663
+ 600,
7664
+ 900
7665
+ ];
7666
+ /** A hub restarting under a booting runner: ~100 s in total. */
7667
+ var TRANSPORT_RETRY_DELAYS_MS = [
7668
+ 150,
7669
+ 350,
7670
+ 600,
7671
+ 900,
7672
+ 2e3,
7673
+ 5e3,
7674
+ 1e4,
7675
+ 2e4,
7676
+ 3e4,
7677
+ 3e4
7678
+ ];
7679
+ var RESTART_FINGERPRINTS = ["SqliteSettingsBackend not initialized", "provider not available"];
7680
+ /** Node / socket error codes of a connection that did not happen or broke. */
7681
+ var TRANSPORT_CODES = new Set([
7682
+ "ECONNREFUSED",
7683
+ "ECONNRESET",
7684
+ "ETIMEDOUT",
7685
+ "EPIPE",
7686
+ "EHOSTUNREACH",
7687
+ "ENETUNREACH"
7688
+ ]);
7689
+ /** tRPC error codes that mean "the other side is not answering now", not "your call is wrong". */
7690
+ var UNAVAILABLE_TRPC_CODES = new Set([
7691
+ "TIMEOUT",
7692
+ "SERVICE_UNAVAILABLE",
7693
+ "BAD_GATEWAY",
7694
+ "GATEWAY_TIMEOUT"
7695
+ ]);
7696
+ /** The exact phrases OUR transport raises when it cannot reach the hub. */
7697
+ var TRANSPORT_PHRASES = [
7698
+ "discovery deadline exceeded",
7699
+ "parent could not route child unowned cap call",
7700
+ "UDS request timed out after",
7701
+ "channel closed"
7702
+ ];
7703
+ /** Moleculer error `type`s of a call that could not be served right now. */
7704
+ var MOLECULER_TRANSIENT_TYPES = new Set([
7705
+ "REQUEST_TIMEOUT",
7706
+ "SERVICE_NOT_FOUND",
7707
+ "SERVICE_NOT_AVAILABLE",
7708
+ "REQUEST_REJECTED",
7709
+ "BROKER_DISCONNECTED"
7710
+ ]);
7711
+ function stringProp(value, key) {
7712
+ if (typeof value !== "object" || value === null) return void 0;
7713
+ const found = Reflect.get(value, key);
7714
+ return typeof found === "string" ? found : void 0;
7715
+ }
7716
+ /** The code an error carries: its own, its cause's, or a tRPC `data.code`. */
7717
+ function errorCode(err) {
7718
+ const out = {};
7719
+ const cause = typeof err === "object" && err !== null ? Reflect.get(err, "cause") : void 0;
7720
+ const data = typeof err === "object" && err !== null ? Reflect.get(err, "data") : void 0;
7721
+ const node = stringProp(err, "code") ?? stringProp(cause, "code");
7722
+ if (node !== void 0) out.node = node;
7723
+ const trpc = stringProp(data, "code");
7724
+ if (trpc !== void 0) out.trpc = trpc;
7725
+ return out;
7726
+ }
7727
+ /**
7728
+ * Is a settings read failure worth retrying, and with which budget? Only
7729
+ * EXPLICIT signals count: a socket error code, a tRPC "unavailable" code, or
7730
+ * one of the exact phrases our own transport raises. A message that merely
7731
+ * contains "timed out" or "unreachable" is not evidence of anything, and
7732
+ * `settings store not wired` is a wiring bug that retrying cannot fix.
7733
+ */
7734
+ function classifyStoreError(err) {
7735
+ const message = err instanceof Error ? err.message : String(err);
7736
+ if (RESTART_FINGERPRINTS.some((f) => message.includes(f))) return "restart";
7737
+ const code = errorCode(err);
7738
+ if (code.node !== void 0 && TRANSPORT_CODES.has(code.node)) return "transport";
7739
+ if (code.trpc !== void 0 && UNAVAILABLE_TRPC_CODES.has(code.trpc)) return "transport";
7740
+ if (typeof err === "object" && err !== null) {
7741
+ if (Reflect.get(err, "retryable") === true) return "transport";
7742
+ const type = stringProp(err, "type");
7743
+ if (type !== void 0 && MOLECULER_TRANSIENT_TYPES.has(type)) return "transport";
7744
+ }
7745
+ if (TRANSPORT_PHRASES.some((f) => message.includes(f))) return "transport";
7746
+ return "permanent";
7747
+ }
7656
7748
  /**
7657
7749
  * Collect the keys of every field marked `perNode: true`, recursing into
7658
7750
  * layout containers (`group` fields and `sub-tabs` tabs) the same way
@@ -12798,11 +12890,10 @@ var ClipModelMetaSchema = object({
12798
12890
  /**
12799
12891
  * The `vector-store` index this model's vectors live in. ONE feature space
12800
12892
  * per index: two models share an index only when their vectors are ranked
12801
- * against each other, which for CLIP means never across families. MobileCLIP
12802
- * S1 and S2 share `object-clip` for history (separated by the row's
12803
- * `modelId`); any other model gets its own.
12893
+ * against each other, which for CLIP means never across families. Every
12894
+ * model gets its own.
12804
12895
  */
12805
- vectorIndex: string().regex(/^object-clip(-[a-z0-9][a-z0-9-]*)?$/),
12896
+ vectorIndex: string().regex(/^object-clip-[a-z0-9][a-z0-9-]*$/),
12806
12897
  /**
12807
12898
  * The text encoder's tokenizer, a declared SIBLING file of its onnx. The name
12808
12899
  * must be unique per tokenizer: every model's siblings land in ONE flat
@@ -16974,6 +17065,28 @@ id: union([string(), number()]) }), {
16974
17065
  kind: "mutation",
16975
17066
  auth: "admin"
16976
17067
  });
17068
+ /** One table of the settings database. */
17069
+ var StorageTableSchema = object({
17070
+ /** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
17071
+ name: string(),
17072
+ /** The owner prefix of a scoped name (`recorder` for `recorder:addon-settings`), else null. */
17073
+ owner: string().nullable(),
17074
+ rows: number().int().nonnegative(),
17075
+ /** Bytes the table's pages occupy (`dbstat`); null when the engine cannot measure. */
17076
+ bytes: number().int().nonnegative().nullable(),
17077
+ /** Declared by an addon since this engine process started (a running addon claims it). */
17078
+ declaredThisBoot: boolean()
17079
+ });
17080
+ method(object({ ownerPrefix: string().min(1).optional() }), object({ tables: array(StorageTableSchema) }), { auth: "admin" }), method(object({
17081
+ collection: string().min(1),
17082
+ expectRows: number().int().nonnegative()
17083
+ }), object({
17084
+ dropped: literal(true),
17085
+ rows: number().int().nonnegative()
17086
+ }), {
17087
+ kind: "mutation",
17088
+ auth: "admin"
17089
+ });
16977
17090
  /**
16978
17091
  * `keychain` — the hub's encrypted store of secret values (D787).
16979
17092
  *
@@ -17515,6 +17628,16 @@ method(object({}), KeychainStatusSchema, { auth: "admin" }), method(object({}),
17515
17628
  kind: "mutation",
17516
17629
  auth: "admin",
17517
17630
  caller: "required"
17631
+ }), method(object({
17632
+ ref: string().min(1),
17633
+ value: string().min(1)
17634
+ }), object({
17635
+ ref: string(),
17636
+ version: number().int()
17637
+ }), {
17638
+ kind: "mutation",
17639
+ auth: "admin",
17640
+ caller: "required"
17518
17641
  }), method(object({ refs: array(string().min(1)).max(1e3) }), object({
17519
17642
  deleted: array(string()).readonly(),
17520
17643
  kept: array(object({
@@ -56730,7 +56853,19 @@ var TlsStatusSchema = object({
56730
56853
  sans: array(string()),
56731
56854
  caCertPem: string().nullable(),
56732
56855
  reissueError: string().nullable(),
56733
- restartRequired: boolean()
56856
+ restartRequired: boolean(),
56857
+ /** An uploaded certificate is served but no CA is kept for it and it is not publicly signed (D873). */
56858
+ caMissing: boolean(),
56859
+ /** Does the hub's own generated CA carry a critical name constraint? `null` while it has none. */
56860
+ localCaConstrained: boolean().nullable(),
56861
+ /** ISO time the generated CA was replaced by a constrained one; devices must re-download it. */
56862
+ caReplacedAt: string().nullable(),
56863
+ /** The uploaded certificate kept on disk, selectable again after "Revert to generated". */
56864
+ storedUpload: object({
56865
+ leafFingerprintSha256: string(),
56866
+ validTo: string(),
56867
+ hasCa: boolean()
56868
+ }).nullable()
56734
56869
  });
56735
56870
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
56736
56871
  /**
@@ -56786,6 +56921,9 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
56786
56921
  }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
56787
56922
  kind: "mutation",
56788
56923
  auth: "admin"
56924
+ }), method(_void(), TlsStatusSchema, {
56925
+ kind: "mutation",
56926
+ auth: "admin"
56789
56927
  });
56790
56928
  object({
56791
56929
  /** Lifecycle state of the lock. `jammed` means the motor reported
@@ -65921,6 +66059,12 @@ Object.freeze({
65921
66059
  addonId: null,
65922
66060
  access: "create"
65923
66061
  },
66062
+ "keychainAdmin.importSecret": {
66063
+ capName: "keychain-admin",
66064
+ capScope: "system",
66065
+ addonId: null,
66066
+ access: "create"
66067
+ },
65924
66068
  "keychainAdmin.listAudit": {
65925
66069
  capName: "keychain-admin",
65926
66070
  capScope: "system",
@@ -66317,6 +66461,12 @@ Object.freeze({
66317
66461
  addonId: null,
66318
66462
  access: "create"
66319
66463
  },
66464
+ "localNetwork.useStoredUploadedCertificate": {
66465
+ capName: "local-network",
66466
+ capScope: "system",
66467
+ addonId: null,
66468
+ access: "create"
66469
+ },
66320
66470
  "lockControl.getStatus": {
66321
66471
  capName: "lock-control",
66322
66472
  capScope: "device",
@@ -69701,6 +69851,18 @@ Object.freeze({
69701
69851
  addonId: null,
69702
69852
  access: "create"
69703
69853
  },
69854
+ "storageAdmin.dropCollection": {
69855
+ capName: "storage-admin",
69856
+ capScope: "system",
69857
+ addonId: null,
69858
+ access: "delete"
69859
+ },
69860
+ "storageAdmin.listTables": {
69861
+ capName: "storage-admin",
69862
+ capScope: "system",
69863
+ addonId: null,
69864
+ access: "view"
69865
+ },
69704
69866
  "storageEvictable.evict": {
69705
69867
  capName: "storage-evictable",
69706
69868
  capScope: "system",
@@ -74746,7 +74908,11 @@ var REMOVED_MODEL_IDS = {
74746
74908
  "face-embedding": ["arcface-r100", "inception-resnet-v1"],
74747
74909
  "plate-detection": ["yolov8n-plate"],
74748
74910
  "plate-ocr": ["vgg-english-g2"],
74749
- "clip-embedding": ["mobileclip-s1", "mobileclip-s2"],
74911
+ "clip-embedding": [
74912
+ "mobileclip-s0",
74913
+ "mobileclip-s1",
74914
+ "mobileclip-s2"
74915
+ ],
74750
74916
  "package-detection": ["yolov8n-package"],
74751
74917
  "bird-classifier": ["bird-classifier-aiy-edgetpu", "bird-nabirds-404"],
74752
74918
  "animal-classifier": ["animals-10"],
@@ -76643,6 +76809,22 @@ var DurableLedger = class DurableLedger {
76643
76809
  }
76644
76810
  }
76645
76811
  /**
76812
+ * Drop every MIRRORED row about `deviceId` (per the spec's `deviceIdOf`),
76813
+ * leaving the durable rows to the caller's `deleteWhere` (D871). Returns how
76814
+ * many mirror entries went; a spec without `deviceIdOf` evicts nothing.
76815
+ */
76816
+ evictDevice(deviceId) {
76817
+ const of = this.spec.deviceIdOf;
76818
+ if (of === void 0) return 0;
76819
+ let evicted = 0;
76820
+ for (const [key, row] of [...this.mirror.entries()]) {
76821
+ if (of(row) !== deviceId) continue;
76822
+ this.mirror.delete(key);
76823
+ evicted += 1;
76824
+ }
76825
+ return evicted;
76826
+ }
76827
+ /**
76646
76828
  * Drop every mirrored key NOT in `activeKeys`. Returns how many rows went.
76647
76829
  *
76648
76830
  * **The caller must hold an AUTHORITATIVE active set.** A prune driven by a
@@ -83974,15 +84156,10 @@ var DeviceFeaturesMirror = class {
83974
84156
  async hydrate() {
83975
84157
  const settings = this.settings;
83976
84158
  if (settings === null) return 0;
83977
- const list = settings.listDeviceStoreIds;
83978
- if (list !== void 0) {
83979
- const ids = await list.call(settings);
83980
- const batch = settings.readDeviceStoreBatch;
83981
- const blobs = batch === void 0 ? new Map(await Promise.all(ids.map(async (id) => [id, await settings.readDeviceStore(id)]))) : await batch.call(settings, ids);
83982
- for (const [deviceId, blob] of blobs) {
83983
- const row = rowOf(deviceId, blob[DEVICE_FEATURES_KEY]);
83984
- if (row !== null) this.local.set(deviceId, row);
83985
- }
84159
+ const ids = await settings.listDeviceStoreIds();
84160
+ for (const [deviceId, blob] of await settings.readDeviceStoreBatch(ids)) {
84161
+ const row = rowOf(deviceId, blob[DEVICE_FEATURES_KEY]);
84162
+ if (row !== null) this.local.set(deviceId, row);
83986
84163
  }
83987
84164
  this.logger.info("device-features mirror restored", { meta: { devices: this.local.size } });
83988
84165
  return this.local.size;
@@ -87640,25 +87817,33 @@ var NodeStressController = class NodeStressController {
87640
87817
  //#region src/durable/lts-aggregator.ts
87641
87818
  /** Wall-clock bucket width. Five minutes, matching HA's statistics tier. */
87642
87819
  var LTS_BUCKET_MS = 5 * 6e4;
87820
+ /** Buckets written per transaction — bounds one statement, not the flush. */
87821
+ var FLUSH_BATCH = 200;
87643
87822
  /** How often the row cap is enforced. Rarely: it is a bound, not a deadline. */
87644
87823
  var CAP_SWEEP_INTERVAL_MS = 6 * 36e5;
87824
+ /**
87825
+ * The row's identity IS its four key columns (D871): a composite
87826
+ * `primaryKey` + `WITHOUT ROWID`, so the table is its own index and there is no
87827
+ * TEXT `id` repeating what the row already says. The old layout kept
87828
+ * `<subject>|<series>|<scope>|<bucketStart>` as a TEXT primary key beside the
87829
+ * four columns — 85 MB of rows plus a 44 MB autoindex for 1.07 M rows. A
87830
+ * composite-key collection is written with `upsertMany` (a bucket flushed twice
87831
+ * replaces itself) and read with `query` / `count` / `deleteWhere`; the calls
87832
+ * that address one row by id are refused by the engine.
87833
+ */
87834
+ var LTS_KEY_COLUMNS = [
87835
+ "subject",
87836
+ "series",
87837
+ "scope",
87838
+ "bucketStart"
87839
+ ];
87645
87840
  var LTS_COLUMNS = [
87646
- (
87647
- /** `<subject>|<series>|<scope>|<bucketStart>` — deterministic, so a bucket
87648
- * flushed twice replaces itself rather than doubling. */
87649
- {
87650
- name: "id",
87651
- type: "TEXT",
87652
- primaryKey: true,
87653
- notNull: true
87654
- }),
87655
87841
  (
87656
87842
  /** The camera id, or the node id. One column, because every query is
87657
87843
  * "this thing over time" and the thing is one or the other. */
87658
87844
  {
87659
87845
  name: "subject",
87660
- type: "TEXT",
87661
- notNull: true
87846
+ type: "TEXT"
87662
87847
  }),
87663
87848
  (
87664
87849
  /** Numeric mirror of `subject` for camera rows, so a per-camera question is
@@ -87670,22 +87855,19 @@ var LTS_COLUMNS = [
87670
87855
  }),
87671
87856
  {
87672
87857
  name: "series",
87673
- type: "TEXT",
87674
- notNull: true
87858
+ type: "TEXT"
87675
87859
  },
87676
87860
  (
87677
87861
  /** Sub-scope within a series: a zoneId for occupancy, `''` otherwise. */
87678
87862
  {
87679
87863
  name: "scope",
87680
- type: "TEXT",
87681
- notNull: true
87864
+ type: "TEXT"
87682
87865
  }),
87683
87866
  (
87684
87867
  /** Bucket start, wall-clock aligned to {@link LTS_BUCKET_MS}. */
87685
87868
  {
87686
87869
  name: "bucketStart",
87687
- type: "INTEGER",
87688
- notNull: true
87870
+ type: "INTEGER"
87689
87871
  }),
87690
87872
  {
87691
87873
  name: "samples",
@@ -87708,12 +87890,15 @@ var LTS_COLUMNS = [
87708
87890
  notNull: true
87709
87891
  }
87710
87892
  ];
87893
+ /**
87894
+ * The primary key serves every per-subject read (`subject, series, scope` then a
87895
+ * `bucketStart` range, already in bucket order); this index serves the read that
87896
+ * names a series without a subject. The old `deviceId` index went with the TEXT
87897
+ * id: nothing queries a series by the numeric mirror.
87898
+ */
87711
87899
  var LTS_INDEXES = [{
87712
87900
  name: "idx_lts_series_bucket",
87713
87901
  columns: ["series", "bucketStart"]
87714
- }, {
87715
- name: "idx_lts_device",
87716
- columns: ["deviceId"]
87717
87902
  }];
87718
87903
  function ltsBucketStart(at, bucketMs = LTS_BUCKET_MS) {
87719
87904
  return Math.floor(at / bucketMs) * bucketMs;
@@ -87742,6 +87927,8 @@ var LtsAggregator = class {
87742
87927
  return store.declareCollection.mutate({
87743
87928
  collection,
87744
87929
  columns: [...LTS_COLUMNS],
87930
+ primaryKey: [...LTS_KEY_COLUMNS],
87931
+ withoutRowid: true,
87745
87932
  indexes: [...LTS_INDEXES]
87746
87933
  });
87747
87934
  }
@@ -87801,43 +87988,60 @@ var LtsAggregator = class {
87801
87988
  return 0;
87802
87989
  }
87803
87990
  let written = 0;
87804
- for (const bucket of due) {
87805
- const key = rowId(bucket.subject, bucket.series, bucket.scope, bucket.bucketStart);
87806
- try {
87807
- await this.store.set.mutate({
87808
- collection: this.collection,
87809
- key,
87810
- value: {
87811
- subject: bucket.subject,
87812
- ...bucket.deviceId !== void 0 ? { deviceId: bucket.deviceId } : {},
87813
- series: bucket.series,
87814
- scope: bucket.scope,
87815
- bucketStart: bucket.bucketStart,
87816
- samples: bucket.samples,
87817
- sum: bucket.sum,
87818
- min: bucket.min,
87819
- max: bucket.max
87820
- }
87821
- });
87822
- written += 1;
87823
- this.groups.add(groupKey(bucket.subject, bucket.series, bucket.scope));
87824
- } catch (err) {
87825
- this.logger.warn("lts bucket write failed — this interval will be missing", {
87826
- ...bucket.deviceId !== void 0 ? { tags: { deviceId: bucket.deviceId } } : {},
87827
- meta: {
87828
- collection: this.collection,
87829
- series: bucket.series,
87830
- subject: bucket.subject,
87831
- bucketStart: bucket.bucketStart,
87832
- error: String(err)
87833
- }
87834
- });
87835
- }
87836
- this.open.delete(key);
87991
+ for (const batch of chunk(due, FLUSH_BATCH)) {
87992
+ written += await this.writeBatch(batch);
87993
+ for (const bucket of batch) this.open.delete(rowId(bucket.subject, bucket.series, bucket.scope, bucket.bucketStart));
87837
87994
  }
87838
87995
  await this.maybeSweepCap(now);
87839
87996
  return written;
87840
87997
  }
87998
+ /**
87999
+ * Upsert one batch in a single transaction. If the batch is refused as a
88000
+ * whole (all-or-nothing), each bucket is retried alone so one bad row costs
88001
+ * one interval, not forty. Returns the number of rows written.
88002
+ */
88003
+ async writeBatch(batch) {
88004
+ try {
88005
+ await this.upsert(batch);
88006
+ for (const bucket of batch) this.groups.add(groupKey(bucket.subject, bucket.series, bucket.scope));
88007
+ return batch.length;
88008
+ } catch (err) {
88009
+ if (batch.length > 1) {
88010
+ let written = 0;
88011
+ for (const bucket of batch) written += await this.writeBatch([bucket]);
88012
+ return written;
88013
+ }
88014
+ const bucket = batch[0];
88015
+ this.logger.warn("lts bucket write failed — this interval will be missing", {
88016
+ ...bucket?.deviceId !== void 0 ? { tags: { deviceId: bucket.deviceId } } : {},
88017
+ meta: {
88018
+ collection: this.collection,
88019
+ series: bucket?.series,
88020
+ subject: bucket?.subject,
88021
+ bucketStart: bucket?.bucketStart,
88022
+ error: String(err)
88023
+ }
88024
+ });
88025
+ return 0;
88026
+ }
88027
+ }
88028
+ async upsert(batch) {
88029
+ await this.store.upsertMany.mutate({
88030
+ collection: this.collection,
88031
+ conflict: [...LTS_KEY_COLUMNS],
88032
+ records: batch.map((bucket) => ({ data: {
88033
+ subject: bucket.subject,
88034
+ deviceId: bucket.deviceId ?? null,
88035
+ series: bucket.series,
88036
+ scope: bucket.scope,
88037
+ bucketStart: bucket.bucketStart,
88038
+ samples: bucket.samples,
88039
+ sum: bucket.sum,
88040
+ min: bucket.min,
88041
+ max: bucket.max
88042
+ } }))
88043
+ });
88044
+ }
87841
88045
  /** Read closed buckets back. The read surface every chart will use. */
87842
88046
  async read(query = {}) {
87843
88047
  const where = {};
@@ -87922,6 +88126,11 @@ var LtsAggregator = class {
87922
88126
  }
87923
88127
  }
87924
88128
  };
88129
+ function chunk(items, size) {
88130
+ const out = [];
88131
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
88132
+ return out;
88133
+ }
87925
88134
  function groupKey(subject, series, scope) {
87926
88135
  return `${subject}${series}${scope}`;
87927
88136
  }
@@ -87980,7 +88189,7 @@ function recordToRow(data) {
87980
88189
  * write="one row per (node, series, 5-min bucket), written ONCE when the bucket closes; a node reporting no samples writes nothing"
87981
88190
  * 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."
87982
88191
  */
87983
- var NODE_STRESS_LTS_COLLECTION = "pipeline-orchestrator:stats-5m";
88192
+ var NODE_STRESS_LTS_COLLECTION = "pipeline-orchestrator:stats-5m-v2";
87984
88193
  var NodeStressLts = class {
87985
88194
  lts;
87986
88195
  constructor(deps) {