@camstack/system 1.2.63 → 1.2.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/addon-utils.js +1 -1
  4. package/dist/addon-utils.mjs +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  9. package/dist/builtins/alerts/alerts.addon.js +1 -1
  10. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +3 -3
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  13. package/dist/builtins/console-logging/index.js +1 -1
  14. package/dist/builtins/console-logging/index.mjs +1 -1
  15. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  16. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  17. package/dist/builtins/device-manager/device-manager.addon.js +10 -4
  18. package/dist/builtins/device-manager/device-manager.addon.mjs +10 -4
  19. package/dist/builtins/device-manager/device-state-mirror.d.ts +7 -1
  20. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  21. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  22. package/dist/builtins/hub-forwarder/index.js +1 -1
  23. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  24. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  25. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  26. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  27. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  28. package/dist/builtins/local-network/local-network.addon.js +1 -1
  29. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  30. package/dist/builtins/loki-logging/index.js +1 -1
  31. package/dist/builtins/loki-logging/index.mjs +1 -1
  32. package/dist/builtins/native-metrics/native-metrics.addon.js +2 -2
  33. package/dist/builtins/native-metrics/native-metrics.addon.mjs +2 -2
  34. package/dist/builtins/platform-probe/index.js +2 -2
  35. package/dist/builtins/platform-probe/index.mjs +2 -2
  36. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  37. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  38. package/dist/builtins/snapshot/index.js +1 -1
  39. package/dist/builtins/snapshot/index.mjs +1 -1
  40. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  41. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  42. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +29 -3
  43. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +29 -3
  44. package/dist/builtins/sqlite-storage/vector-index-vec.d.ts +19 -0
  45. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  46. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  47. package/dist/builtins/system-config/system-config.addon.js +1 -1
  48. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  49. package/dist/builtins/winston-logging/index.js +1 -1
  50. package/dist/builtins/winston-logging/index.mjs +1 -1
  51. package/dist/{dist-LDrOFrd-.js → dist-CIkGOAcz.js} +96 -2
  52. package/dist/{dist-BJ2w3uoN.mjs → dist-DqPyKwVB.mjs} +96 -2
  53. package/dist/http/asset-compression.d.ts +93 -0
  54. package/dist/index.js +63 -9
  55. package/dist/index.mjs +63 -9
  56. package/dist/kernel/heap-watch.d.ts +55 -0
  57. package/dist/{model-download-service-D8B-4ktF.mjs → model-download-service-CTBHzORJ.mjs} +220 -2
  58. package/dist/{model-download-service-Cpc6SWzV.js → model-download-service-Caj1kaSi.js} +220 -2
  59. package/package.json +1 -1
  60. package/dist/{manifest-python-deps-DNfmBu20.js → manifest-python-deps-BE6l8Y4W.js} +1 -1
  61. package/dist/{manifest-python-deps-CopL1_5C.mjs → manifest-python-deps-Dz1ryHIN.mjs} +1 -1
@@ -12,6 +12,25 @@ export interface RawSqliteDatabase {
12
12
  };
13
13
  loadExtension?(path: string): void;
14
14
  }
15
+ /**
16
+ * The largest `k` sqlite-vec will accept in a KNN query.
17
+ *
18
+ * This is the EXTENSION's own compile-time limit, not a number chosen here —
19
+ * measured against the real extension, which answers a larger `k` with
20
+ * `SqliteError: k value in knn query too large, provided 20000 and the limit
21
+ * is 4096` rather than with rows.
22
+ *
23
+ * It matters that this is the extension's number. The vector door looked like
24
+ * a bypass of {@link SETTINGS_QUERY_HARD_ROW_CAP}: `vector-store.cap.ts`
25
+ * declares `topK: z.number().int().positive()` with no `.max()`, and it is
26
+ * bound straight into `k = ?` on the same connection, in the same process, as
27
+ * the capped `data-store-provider.query`. It is not a bypass — sqlite-vec was
28
+ * holding the line the whole time, which caps a KNN's heap cost at 4 096 rows
29
+ * of metadata. What was actually broken is that it held the line by THROWING,
30
+ * so an over-large `topK` reached the caller as a raw SQLite error instead of
31
+ * as the bounded answer it could always have been served.
32
+ */
33
+ export declare const VEC_KNN_MAX_K = 4096;
15
34
  export interface SqliteVecIndexDeps {
16
35
  readonly db: RawSqliteDatabase;
17
36
  readonly logger: IScopedLogger;
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-LDrOFrd-.js");
6
+ const require_dist = require("../../dist-CIkGOAcz.js");
7
7
  let node_path = require("node:path");
8
8
  node_path = require_chunk.__toESM(node_path);
9
9
  let node_fs_promises = require("node:fs/promises");
@@ -1,4 +1,4 @@
1
- import { dt as BaseAddon, it as storageCapability, jt as parseJsonObject, nt as settingsStoreCapability, p as StorageLocationTypeSchema } from "../../dist-BJ2w3uoN.mjs";
1
+ import { dt as BaseAddon, it as storageCapability, jt as parseJsonObject, nt as settingsStoreCapability, p as StorageLocationTypeSchema } from "../../dist-DqPyKwVB.mjs";
2
2
  import * as path$1 from "node:path";
3
3
  import * as fs from "node:fs/promises";
4
4
  import { buildStorageLocationRegistry } from "@camstack/system";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-LDrOFrd-.js");
6
+ const require_dist = require("../../dist-CIkGOAcz.js");
7
7
  //#region src/builtins/system-config/system-config.addon.ts
8
8
  /**
9
9
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -1,4 +1,4 @@
1
- import { Ot as hydrateSchema, dt as BaseAddon, ut as errMsg } from "../../dist-BJ2w3uoN.mjs";
1
+ import { Ot as hydrateSchema, dt as BaseAddon, ut as errMsg } from "../../dist-DqPyKwVB.mjs";
2
2
  //#region src/builtins/system-config/system-config.addon.ts
3
3
  /**
4
4
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-LDrOFrd-.js");
6
+ const require_dist = require("../../dist-CIkGOAcz.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  let node_path = require("node:path");
9
9
  node_path = require_chunk.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { G as logDestinationCapability, dt as BaseAddon } from "../../dist-BJ2w3uoN.mjs";
1
+ import { G as logDestinationCapability, dt as BaseAddon } from "../../dist-DqPyKwVB.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  import * as path$1 from "node:path";
4
4
  import path from "node:path";
@@ -15278,6 +15278,43 @@ var CameraRecordingStatusSchema = zod.z.object({
15278
15278
  storageBytes: zod.z.number()
15279
15279
  });
15280
15280
  /**
15281
+ * The stages of the status fan-out that issue a BOUNDED remote read, and can
15282
+ * therefore be cut short. Every other block (`assignment`, `decoder`, `motion`,
15283
+ * `audio`) is composed from orchestrator-local state and cannot time out.
15284
+ *
15285
+ * `switches` is the operator's per-camera function switches — the read behind
15286
+ * `switchedOff`, not a block of its own.
15287
+ */
15288
+ var CameraStatusStageSchema = zod.z.enum([
15289
+ "source",
15290
+ "broker",
15291
+ "detection",
15292
+ "recording",
15293
+ "switches"
15294
+ ]);
15295
+ /**
15296
+ * WHY a stage could not be trusted.
15297
+ *
15298
+ * - `timeout` — the stage never answered and the whole status call paid the
15299
+ * full per-stage bound for it. This is the one that used to be invisible.
15300
+ * - `error` — the source answered with a rejection.
15301
+ * - `partial` — the stage RETURNED, but from an incomplete set of sources: its
15302
+ * value is a floor, not a total. `switchedOff` derived from a partial switch
15303
+ * read can be empty while the operator has switches off.
15304
+ */
15305
+ var CameraStatusDegradationReasonSchema = zod.z.enum([
15306
+ "timeout",
15307
+ "error",
15308
+ "partial"
15309
+ ]);
15310
+ /** One stage of the fan-out that could NOT be read, and how long it cost. */
15311
+ var CameraStatusDegradationSchema = zod.z.object({
15312
+ stage: CameraStatusStageSchema,
15313
+ reason: CameraStatusDegradationReasonSchema,
15314
+ /** Wall-clock ms spent on the stage before it was abandoned. */
15315
+ elapsedMs: zod.z.number()
15316
+ });
15317
+ /**
15281
15318
  * Aggregated per-camera pipeline status — server-composed, single call.
15282
15319
  *
15283
15320
  * The `assignment` and `source` blocks are always present.
@@ -15307,9 +15344,28 @@ var CameraStatusSchema = zod.z.object({
15307
15344
  * differently — a quiet camera that looks identical to a dead one is the
15308
15345
  * silence-reads-as-never-happened trap this repo keeps paying for.
15309
15346
  *
15310
- * Empty when nothing is off. Never contains a switch no provider offers.
15347
+ * Empty when nothing is off, and never contains a switch no provider offers
15348
+ * — but an empty list is only a POSITIVE claim when `degraded` does not name
15349
+ * `'switches'`. When it does, the switch set could not be read and nothing
15350
+ * here may be rendered as "the operator turned nothing off": that is the
15351
+ * D62 failure (a camera we could not read painted as broken) in the very
15352
+ * field that exists to prevent it.
15311
15353
  */
15312
15354
  switchedOff: zod.z.array(CameraSwitchIdSchema).readonly(),
15355
+ /**
15356
+ * Stages of the bounded fan-out that were CUT SHORT — a timeout or a
15357
+ * rejection — and whose block is therefore `null` because we could not
15358
+ * READ it, not because there is nothing there.
15359
+ *
15360
+ * Without this, three different facts arrive as the same `null`: "the stage
15361
+ * timed out", "the stage failed", and "this camera legitimately has no
15362
+ * decoder / no recording". Every surface that draws a conclusion from a null
15363
+ * block (or from an empty `switchedOff`) must consult this first; a stage
15364
+ * named here supports no conclusion at all, only "unknown".
15365
+ *
15366
+ * Empty on a clean read — the overwhelmingly common case.
15367
+ */
15368
+ degraded: zod.z.array(CameraStatusDegradationSchema).readonly(),
15313
15369
  /** Unix timestamp (ms) when this snapshot was composed server-side. */
15314
15370
  fetchedAt: zod.z.number()
15315
15371
  });
@@ -19525,7 +19581,29 @@ var FaceInfoSchema = zod.z.object({
19525
19581
  recognizedIdentityId: zod.z.string().optional(),
19526
19582
  identityName: zod.z.string().optional(),
19527
19583
  assigned: zod.z.boolean(),
19584
+ /**
19585
+ * The crop, inline, base64.
19586
+ *
19587
+ * **Prefer {@link cropUrl}.** At the 500 rows the Faces view asks for this
19588
+ * field alone is ~2.87 MiB, re-sent in full on every operator assign and
19589
+ * every 30 s poll, base64-inflated over the msgpack socket and held in the
19590
+ * query heap. It stays for callers that have not migrated; `includeCrops:
19591
+ * false` turns it off once they have.
19592
+ */
19528
19593
  base64: zod.z.string().optional(),
19594
+ /**
19595
+ * Same crop, as a data-plane URL for `<img src>` — the move the admin
19596
+ * snapshot surfaces made on 2026-08-08.
19597
+ *
19598
+ * Served by the `event-media` plane, which resolves a raw MediaStore key and
19599
+ * is `access: 'authenticated'`: a bare `<img>` carries the `camstack_session`
19600
+ * cookie, so no header plumbing is needed. The bytes then ride the browser's
19601
+ * HTTP cache with an ETag and `immutable`, instead of the WebSocket.
19602
+ *
19603
+ * Absent when the face has no stored crop, or when the addon has no data
19604
+ * plane — callers fall back to {@link base64}.
19605
+ */
19606
+ cropUrl: zod.z.string().optional(),
19529
19607
  /** Design B: the face bbox (pixel space) on the key frame — lets a detail
19530
19608
  * view draw the box over the native `keyFrameMediaKey` frame. Absent on
19531
19609
  * legacy rows written before design B. */
@@ -19601,7 +19679,23 @@ var faceGalleryCapability = {
19601
19679
  }),
19602
19680
  listRecentFaces: method(zod.z.object({
19603
19681
  limit: zod.z.number().int().positive().optional(),
19604
- filter: FaceFilterEnum.optional()
19682
+ filter: FaceFilterEnum.optional(),
19683
+ /**
19684
+ * Inline the base64 crop on every row. Default `true` — the existing
19685
+ * behaviour, kept so no caller breaks.
19686
+ *
19687
+ * Set `false` once the caller renders {@link FaceInfo.cropUrl}: that
19688
+ * drops ~2.87 MiB per 500-row page to a few KiB of metadata and lets
19689
+ * the browser cache the images.
19690
+ *
19691
+ * **This is an INPUT field, so it does not reach the addon until the
19692
+ * next train.** The hub router validates cap inputs against its own
19693
+ * compiled Zod, which strips a key it does not know — verified today
19694
+ * on the OUTPUT side, where an additive field DOES arrive immediately
19695
+ * (`Track.hasFace`). Until the train ships, sending `false` is
19696
+ * harmless and simply keeps the crops inline.
19697
+ */
19698
+ includeCrops: zod.z.boolean().optional()
19605
19699
  }).optional(), zod.z.array(FaceInfoSchema).readonly()),
19606
19700
  getFaceByTrack: method(zod.z.object({
19607
19701
  deviceId: zod.z.number().int(),
@@ -15278,6 +15278,43 @@ var CameraRecordingStatusSchema = z.object({
15278
15278
  storageBytes: z.number()
15279
15279
  });
15280
15280
  /**
15281
+ * The stages of the status fan-out that issue a BOUNDED remote read, and can
15282
+ * therefore be cut short. Every other block (`assignment`, `decoder`, `motion`,
15283
+ * `audio`) is composed from orchestrator-local state and cannot time out.
15284
+ *
15285
+ * `switches` is the operator's per-camera function switches — the read behind
15286
+ * `switchedOff`, not a block of its own.
15287
+ */
15288
+ var CameraStatusStageSchema = z.enum([
15289
+ "source",
15290
+ "broker",
15291
+ "detection",
15292
+ "recording",
15293
+ "switches"
15294
+ ]);
15295
+ /**
15296
+ * WHY a stage could not be trusted.
15297
+ *
15298
+ * - `timeout` — the stage never answered and the whole status call paid the
15299
+ * full per-stage bound for it. This is the one that used to be invisible.
15300
+ * - `error` — the source answered with a rejection.
15301
+ * - `partial` — the stage RETURNED, but from an incomplete set of sources: its
15302
+ * value is a floor, not a total. `switchedOff` derived from a partial switch
15303
+ * read can be empty while the operator has switches off.
15304
+ */
15305
+ var CameraStatusDegradationReasonSchema = z.enum([
15306
+ "timeout",
15307
+ "error",
15308
+ "partial"
15309
+ ]);
15310
+ /** One stage of the fan-out that could NOT be read, and how long it cost. */
15311
+ var CameraStatusDegradationSchema = z.object({
15312
+ stage: CameraStatusStageSchema,
15313
+ reason: CameraStatusDegradationReasonSchema,
15314
+ /** Wall-clock ms spent on the stage before it was abandoned. */
15315
+ elapsedMs: z.number()
15316
+ });
15317
+ /**
15281
15318
  * Aggregated per-camera pipeline status — server-composed, single call.
15282
15319
  *
15283
15320
  * The `assignment` and `source` blocks are always present.
@@ -15307,9 +15344,28 @@ var CameraStatusSchema = z.object({
15307
15344
  * differently — a quiet camera that looks identical to a dead one is the
15308
15345
  * silence-reads-as-never-happened trap this repo keeps paying for.
15309
15346
  *
15310
- * Empty when nothing is off. Never contains a switch no provider offers.
15347
+ * Empty when nothing is off, and never contains a switch no provider offers
15348
+ * — but an empty list is only a POSITIVE claim when `degraded` does not name
15349
+ * `'switches'`. When it does, the switch set could not be read and nothing
15350
+ * here may be rendered as "the operator turned nothing off": that is the
15351
+ * D62 failure (a camera we could not read painted as broken) in the very
15352
+ * field that exists to prevent it.
15311
15353
  */
15312
15354
  switchedOff: z.array(CameraSwitchIdSchema).readonly(),
15355
+ /**
15356
+ * Stages of the bounded fan-out that were CUT SHORT — a timeout or a
15357
+ * rejection — and whose block is therefore `null` because we could not
15358
+ * READ it, not because there is nothing there.
15359
+ *
15360
+ * Without this, three different facts arrive as the same `null`: "the stage
15361
+ * timed out", "the stage failed", and "this camera legitimately has no
15362
+ * decoder / no recording". Every surface that draws a conclusion from a null
15363
+ * block (or from an empty `switchedOff`) must consult this first; a stage
15364
+ * named here supports no conclusion at all, only "unknown".
15365
+ *
15366
+ * Empty on a clean read — the overwhelmingly common case.
15367
+ */
15368
+ degraded: z.array(CameraStatusDegradationSchema).readonly(),
15313
15369
  /** Unix timestamp (ms) when this snapshot was composed server-side. */
15314
15370
  fetchedAt: z.number()
15315
15371
  });
@@ -19525,7 +19581,29 @@ var FaceInfoSchema = z.object({
19525
19581
  recognizedIdentityId: z.string().optional(),
19526
19582
  identityName: z.string().optional(),
19527
19583
  assigned: z.boolean(),
19584
+ /**
19585
+ * The crop, inline, base64.
19586
+ *
19587
+ * **Prefer {@link cropUrl}.** At the 500 rows the Faces view asks for this
19588
+ * field alone is ~2.87 MiB, re-sent in full on every operator assign and
19589
+ * every 30 s poll, base64-inflated over the msgpack socket and held in the
19590
+ * query heap. It stays for callers that have not migrated; `includeCrops:
19591
+ * false` turns it off once they have.
19592
+ */
19528
19593
  base64: z.string().optional(),
19594
+ /**
19595
+ * Same crop, as a data-plane URL for `<img src>` — the move the admin
19596
+ * snapshot surfaces made on 2026-08-08.
19597
+ *
19598
+ * Served by the `event-media` plane, which resolves a raw MediaStore key and
19599
+ * is `access: 'authenticated'`: a bare `<img>` carries the `camstack_session`
19600
+ * cookie, so no header plumbing is needed. The bytes then ride the browser's
19601
+ * HTTP cache with an ETag and `immutable`, instead of the WebSocket.
19602
+ *
19603
+ * Absent when the face has no stored crop, or when the addon has no data
19604
+ * plane — callers fall back to {@link base64}.
19605
+ */
19606
+ cropUrl: z.string().optional(),
19529
19607
  /** Design B: the face bbox (pixel space) on the key frame — lets a detail
19530
19608
  * view draw the box over the native `keyFrameMediaKey` frame. Absent on
19531
19609
  * legacy rows written before design B. */
@@ -19601,7 +19679,23 @@ var faceGalleryCapability = {
19601
19679
  }),
19602
19680
  listRecentFaces: method(z.object({
19603
19681
  limit: z.number().int().positive().optional(),
19604
- filter: FaceFilterEnum.optional()
19682
+ filter: FaceFilterEnum.optional(),
19683
+ /**
19684
+ * Inline the base64 crop on every row. Default `true` — the existing
19685
+ * behaviour, kept so no caller breaks.
19686
+ *
19687
+ * Set `false` once the caller renders {@link FaceInfo.cropUrl}: that
19688
+ * drops ~2.87 MiB per 500-row page to a few KiB of metadata and lets
19689
+ * the browser cache the images.
19690
+ *
19691
+ * **This is an INPUT field, so it does not reach the addon until the
19692
+ * next train.** The hub router validates cap inputs against its own
19693
+ * compiled Zod, which strips a key it does not know — verified today
19694
+ * on the OUTPUT side, where an additive field DOES arrive immediately
19695
+ * (`Track.hasFace`). Until the train ships, sending `false` is
19696
+ * harmless and simply keeps the crops inline.
19697
+ */
19698
+ includeCrops: z.boolean().optional()
19605
19699
  }).optional(), z.array(FaceInfoSchema).readonly()),
19606
19700
  getFaceByTrack: method(z.object({
19607
19701
  deviceId: z.number().int(),
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Content-coding for addon-served static assets: `Accept-Encoding` negotiation,
3
+ * an eligibility rule, and a BOUNDED cache of the compressed bytes.
4
+ *
5
+ * ## Why the ADDON owns the encoding
6
+ *
7
+ * The hub's `/addon/:addonId/*` bridge is registered `{ compress: false }`. That
8
+ * is not an oversight — on 2026-07-16 the global `@fastify/compress` was found
9
+ * emitting an EMPTY brotli stream (`content-encoding: br`, `content-length: 0`)
10
+ * for any compressible body over its 1 KiB threshold once the body had crossed
11
+ * the UDS route bridge of a FORKED addon. It silently broke the notifier SVG
12
+ * icons; the deploy-bundle pull route had already dodged the same class with
13
+ * `compress: false`. The resolution then, and the contract now, is that an addon
14
+ * route owns its content-coding END TO END: it negotiates, it compresses, it
15
+ * stamps `content-encoding` + `Vary`, and the hub pipes those bytes through
16
+ * untouched (`proxyToUpstream` replays the upstream headers verbatim).
17
+ *
18
+ * So compression belongs HERE, in the shared file data-plane, and nowhere above
19
+ * it. Anything that re-compresses on top of this reintroduces the empty-brotli
20
+ * bug rather than doubling the ratio.
21
+ *
22
+ * ## Why the bytes are cached, and why the cache is bounded
23
+ *
24
+ * Measured on the stream-broker embed bundle (`index-<hash>.js`, 1,443,142 B):
25
+ * gzip-6 → 360,868 B, brotli-5 → 327,419 B in 27 ms, brotli-11 → 293,677 B in
26
+ * 2,233 ms. Quality 5 is the request-path choice: 77% off the wire for 27 ms
27
+ * paid ONCE. Quality 11 buys another 2.3% for eighty times the CPU, on a path
28
+ * where a real client is waiting.
29
+ *
30
+ * The cache is an LRU bounded by BOTH entry count and total bytes, and it
31
+ * refuses assets over {@link COMPRESSION_MAX_ASSET_BYTES} outright. An unbounded
32
+ * `Map` keyed by URL is how a long-lived process walks into an OOM, and this
33
+ * repo has already spent a session on one.
34
+ *
35
+ * The key carries `mtimeMs` and `size`, so a rebuild that rewrites an asset in
36
+ * place under an UNCHANGED name (an SPA shell, a service worker) can never be
37
+ * served from the previous build's bytes.
38
+ */
39
+ /** The codings this layer will produce. Identity is the absence of one. */
40
+ export type ContentEncoding = 'br' | 'gzip';
41
+ /**
42
+ * Below this the framing and the `Vary` cache split cost more than the
43
+ * compression saves. Same threshold the hub's global compressor is registered
44
+ * with, deliberately.
45
+ */
46
+ export declare const COMPRESSION_MIN_BYTES = 1024;
47
+ /**
48
+ * Above this an asset is streamed identity and never buffered. Compression
49
+ * requires the whole body in RAM twice; a multi-hundred-MB recording segment
50
+ * must not be able to do that, whatever its content type says.
51
+ */
52
+ export declare const COMPRESSION_MAX_ASSET_BYTES: number;
53
+ /** Is a body of this content type worth compressing at all? */
54
+ export declare function isCompressibleContentType(contentType: string): boolean;
55
+ /**
56
+ * Pick a coding for this request: brotli when offered, else gzip, else identity.
57
+ *
58
+ * Returns `null` for identity — including when the header is ABSENT. A client
59
+ * that offers nothing gets the raw bytes; guessing on its behalf is how a
60
+ * non-negotiating consumer (a native fetch, a probe, an OTA puller) ends up with
61
+ * a body it cannot read.
62
+ */
63
+ export declare function negotiateContentEncoding(acceptEncoding: string | undefined): ContentEncoding | null;
64
+ /** Identity of one compressed body. `mtimeMs`+`size` make a rebuild-in-place a MISS. */
65
+ export interface CompressedAssetKey {
66
+ readonly path: string;
67
+ readonly mtimeMs: number;
68
+ readonly size: number;
69
+ readonly encoding: ContentEncoding;
70
+ }
71
+ /** Counters for the cache. Exported so a test can prove the compressor ran ONCE. */
72
+ export interface AssetCompressionStats {
73
+ /** Bodies actually run through zlib. */
74
+ readonly compressions: number;
75
+ /** Requests served from already-compressed bytes. */
76
+ readonly hits: number;
77
+ readonly entries: number;
78
+ readonly bytes: number;
79
+ }
80
+ /**
81
+ * Compressed bytes for one asset, compressing at most once per key.
82
+ *
83
+ * `null` means "send the original" — the asset is too large to buffer, the
84
+ * source could not be read, or compression did not actually shrink it. Every
85
+ * failure degrades to identity, so this can only ever be an optimisation.
86
+ *
87
+ * Concurrent callers for the same key share ONE compression: a cold cache hit by
88
+ * a dozen parallel asset requests must not run zlib a dozen times.
89
+ */
90
+ export declare function getCompressedAsset(key: CompressedAssetKey, readSource: () => Promise<Buffer>): Promise<Buffer | null>;
91
+ export declare function getAssetCompressionStats(): AssetCompressionStats;
92
+ /** Test seam — the cache and its counters are process-wide. */
93
+ export declare function resetAssetCompressionCache(): void;
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("./chunk-Cek0wNdY.js");
3
- const require_dist = require("./dist-LDrOFrd-.js");
4
- const require_model_download_service = require("./model-download-service-Cpc6SWzV.js");
5
- const require_manifest_python_deps = require("./manifest-python-deps-DNfmBu20.js");
3
+ const require_dist = require("./dist-CIkGOAcz.js");
4
+ const require_model_download_service = require("./model-download-service-Caj1kaSi.js");
5
+ const require_manifest_python_deps = require("./manifest-python-deps-BE6l8Y4W.js");
6
6
  const require_resource_monitor = require("./resource-monitor-DNNomR-i.js");
7
7
  const require_builtins_sqlite_storage_filesystem_storage_addon = require("./builtins/sqlite-storage/filesystem-storage.addon.js");
8
8
  const require_builtins_sqlite_storage_sqlite_settings_addon = require("./builtins/sqlite-storage/sqlite-settings.addon.js");
@@ -41,10 +41,10 @@ let node_fs$1 = require_chunk.__toESM(node_fs, 1);
41
41
  node_fs = require_chunk.__toESM(node_fs);
42
42
  let node_path = require("node:path");
43
43
  node_path = require_chunk.__toESM(node_path, 1);
44
- let _camstack_types_addon = require("@camstack/types/addon");
45
- let node_child_process = require("node:child_process");
46
44
  let node_util = require("node:util");
47
45
  node_util = require_chunk.__toESM(node_util);
46
+ let _camstack_types_addon = require("@camstack/types/addon");
47
+ let node_child_process = require("node:child_process");
48
48
  let node_os = require("node:os");
49
49
  node_os = require_chunk.__toESM(node_os);
50
50
  let node_fs_promises = require("node:fs/promises");
@@ -139,6 +139,30 @@ var HEAP_WATCH_INTERVAL_MS = 6e4;
139
139
  * the fatal by more than one interval under realistic growth. */
140
140
  var HEAP_WATCH_WARN_RATIO = .8;
141
141
  /**
142
+ * Escalate above this share of the ceiling.
143
+ *
144
+ * 0.70 is 3002MB: clear of the measured routine max (2847MB) so steady state
145
+ * never escalates, and below {@link HEAP_WATCH_WARN_RATIO} so the fast cadence
146
+ * is already running by the time the warn is reachable.
147
+ */
148
+ var HEAP_WATCH_ESCALATE_RATIO = .7;
149
+ /**
150
+ * Release escalation below this share.
151
+ *
152
+ * Hysteresis, not a second threshold: hub-main's heap sawtooths continuously,
153
+ * so a single ratio would flap the mode on every GC. 0.60 is 2573MB — a real
154
+ * retreat rather than the trough of one collection.
155
+ */
156
+ var HEAP_WATCH_DEESCALATE_RATIO = .6;
157
+ /**
158
+ * The cadence state machine, pure so the thresholds are testable without
159
+ * allocating three gigabytes to reach them.
160
+ */
161
+ function nextMode(current, usedRatio, escalateRatio = HEAP_WATCH_ESCALATE_RATIO, deescalateRatio = HEAP_WATCH_DEESCALATE_RATIO) {
162
+ if (current === "escalated") return usedRatio < deescalateRatio ? "steady" : "escalated";
163
+ return usedRatio >= escalateRatio ? "escalated" : "steady";
164
+ }
165
+ /**
142
166
  * Reclaim once the process holds this many MB beyond what it is USING.
143
167
  *
144
168
  * Live calibration, both readings from the same hub minutes apart: 2491MB
@@ -229,6 +253,9 @@ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEA
229
253
  const now = reclaimOptions?.now ?? (() => Date.now());
230
254
  const triggerMb = reclaimOptions?.triggerMb ?? 1536;
231
255
  const minIntervalMs = reclaimOptions?.minIntervalMs ?? 6e5;
256
+ const fastIntervalMs = reclaimOptions?.fastIntervalMs ?? 2e3;
257
+ const escalateRatio = reclaimOptions?.escalateRatio ?? .7;
258
+ const deescalateRatio = reclaimOptions?.deescalateRatio ?? .6;
232
259
  let lastReclaimAt = Number.NEGATIVE_INFINITY;
233
260
  const read = () => {
234
261
  const limit = reclaimOptions?.heapLimitBytes ?? node_v8.getHeapStatistics().heap_size_limit;
@@ -249,16 +276,43 @@ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEA
249
276
  const after = read();
250
277
  sink.info(`[mem] reclaim ${label} stranded=${strandedMb(sample)}MB rss=${sample.rssMb}MB→${after.rssMb}MB freed=${sample.rssMb - after.rssMb}MB arrayBuffers=${sample.arrayBuffersMb}MB→${after.arrayBuffersMb}MB took=${now() - startedAt}ms`);
251
278
  };
279
+ let mode = "steady";
280
+ let lastLoggedAt = Number.NEGATIVE_INFINITY;
281
+ /**
282
+ * PROBING and LOGGING are separate cadences, and that separation is the whole
283
+ * point.
284
+ *
285
+ * The 2026-08-08 fatal escaped a 60s heartbeat because the burst began and
286
+ * ended between two samples. Deciding "is the heap elevated?" only when a
287
+ * line is due would reproduce that exactly: the process would still be read
288
+ * once a minute, and a 62-second climb would still be invisible. So the timer
289
+ * runs at the FAST period always — `process.memoryUsage()` is tens of
290
+ * microseconds, i.e. free at this rate — and the slow period governs only how
291
+ * often a steady process is allowed to say so.
292
+ *
293
+ * Never probe slower than the log cadence, or a caller passing a short
294
+ * `intervalMs` would silently get the long one.
295
+ */
296
+ const probeIntervalMs = Math.min(fastIntervalMs, intervalMs);
252
297
  const tick = () => {
253
298
  try {
254
299
  const sample = read();
255
- const line = format$1(label, sample);
256
- if (sample.nearLimit) sink.warn(`${line} — APPROACHING HEAP LIMIT`);
257
- else sink.info(line);
300
+ const previous = mode;
301
+ mode = nextMode(previous, sample.usedRatio, escalateRatio, deescalateRatio);
302
+ const at = now();
303
+ const due = at - lastLoggedAt >= intervalMs;
304
+ if (mode === "escalated" || due) {
305
+ lastLoggedAt = at;
306
+ const line = format$1(label, sample);
307
+ if (sample.nearLimit) sink.warn(`${line} — APPROACHING HEAP LIMIT`);
308
+ else if (mode === "escalated") sink.warn(`${line} — heap elevated, sampling every ${probeIntervalMs}ms`);
309
+ else sink.info(line);
310
+ }
311
+ if (previous === "escalated" && mode === "steady") sink.info(`[mem] ${label} heap back to routine — logging every ${intervalMs}ms`);
258
312
  maybeReclaim(sample);
259
313
  } catch {}
260
314
  };
261
- const timer = setInterval(tick, intervalMs);
315
+ const timer = setInterval(tick, probeIntervalMs);
262
316
  timer.unref?.();
263
317
  tick();
264
318
  let stopped = false;