@camstack/system 1.2.144 → 1.2.146

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 (62) hide show
  1. package/dist/addon-runner.js +2 -1
  2. package/dist/addon-runner.mjs +2 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/console-logging/index.js +1 -1
  12. package/dist/builtins/console-logging/index.mjs +1 -1
  13. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  14. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  15. package/dist/builtins/device-manager/device-manager.addon.js +157 -35
  16. package/dist/builtins/device-manager/device-manager.addon.mjs +157 -35
  17. package/dist/builtins/device-manager/device-queries.d.ts +22 -0
  18. package/dist/builtins/device-manager/device-row-store.d.ts +18 -0
  19. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  20. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  21. package/dist/builtins/hub-forwarder/index.js +1 -1
  22. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  23. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  24. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  25. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  26. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  27. package/dist/builtins/local-network/local-network.addon.js +1 -1
  28. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  29. package/dist/builtins/loki-logging/index.js +1 -1
  30. package/dist/builtins/loki-logging/index.mjs +1 -1
  31. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  32. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  33. package/dist/builtins/platform-probe/index.js +1 -1
  34. package/dist/builtins/platform-probe/index.mjs +1 -1
  35. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  36. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  37. package/dist/builtins/snapshot/index.js +1 -1
  38. package/dist/builtins/snapshot/index.mjs +1 -1
  39. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  40. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  41. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +14 -1
  42. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  43. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  44. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +2 -1
  45. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +2 -1
  46. package/dist/builtins/system-config/system-config.addon.js +1 -1
  47. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  48. package/dist/builtins/winston-logging/index.js +1 -1
  49. package/dist/builtins/winston-logging/index.mjs +1 -1
  50. package/dist/{dist-D3lqzV40.js → dist-BVU5JADq.js} +401 -24
  51. package/dist/{dist-6rM7chzb.mjs → dist-CG7kwIZp.mjs} +401 -24
  52. package/dist/index.d.ts +2 -2
  53. package/dist/index.js +60 -11
  54. package/dist/index.mjs +52 -12
  55. package/dist/kernel/heap-watch.d.ts +209 -1
  56. package/dist/kernel/moleculer/children-batch-loader.d.ts +46 -0
  57. package/dist/kernel/moleculer/process-service.d.ts +15 -6
  58. package/dist/{manifest-python-deps-BuhBXCg0.mjs → manifest-python-deps-CVeckyXx.mjs} +390 -8
  59. package/dist/{manifest-python-deps-B_mCU6gz.js → manifest-python-deps-CktMcXzS.js} +443 -7
  60. package/dist/{retired-settings-keys-BtvClSPu.mjs → retired-settings-keys-Dm1z4mO4.mjs} +1 -1
  61. package/dist/{retired-settings-keys-OtUQtEbq.js → retired-settings-keys-_PLI9w0k.js} +1 -1
  62. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("./chunk-Cek0wNdY.js");
3
- const require_dist = require("./dist-D3lqzV40.js");
3
+ const require_dist = require("./dist-BVU5JADq.js");
4
4
  const require_builtins_alerts_alerts_addon = require("./builtins/alerts/alerts.addon.js");
5
5
  require("./builtins/alerts/index.js");
6
6
  const require_formatter = require("./formatter-DqAKDlvN.js");
@@ -26,7 +26,7 @@ require("./builtins/system-config/index.js");
26
26
  const require_builtins_winston_logging_index = require("./builtins/winston-logging/index.js");
27
27
  const require_file_data_plane = require("./file-data-plane-DO8KbxCe.js");
28
28
  const require_tls$1 = require("./tls-u8QCJCFE.js");
29
- const require_manifest_python_deps = require("./manifest-python-deps-B_mCU6gz.js");
29
+ const require_manifest_python_deps = require("./manifest-python-deps-CktMcXzS.js");
30
30
  const require_resource_monitor = require("./resource-monitor-CdnzxBLP.js");
31
31
  const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
32
32
  let zod = require("zod");
@@ -94270,7 +94270,8 @@ async function getPidStats$1(pids) {
94270
94270
  }
94271
94271
  var EMPTY_HEAP_DECLARATION = {
94272
94272
  profile: void 0,
94273
- maxOldSpaceMb: void 0
94273
+ maxOldSpaceMb: void 0,
94274
+ rssBudgetMb: void 0
94274
94275
  };
94275
94276
  /**
94276
94277
  * Resolve the declared heap profile + old-space ceiling for one addon by reading its package.json
@@ -94291,10 +94292,12 @@ function readAddonHeapDeclaration(spec) {
94291
94292
  const parsed = JSON.parse(raw);
94292
94293
  const profile = extractHeapProfile(parsed, spec.addonId);
94293
94294
  const maxOldSpaceMb = extractMaxOldSpaceMb(parsed, spec.addonId);
94294
- if (profile !== void 0 || maxOldSpaceMb !== void 0) {
94295
+ const rssBudgetMb = extractRssBudgetMb(parsed, spec.addonId);
94296
+ if (profile !== void 0 || maxOldSpaceMb !== void 0 || rssBudgetMb !== void 0) {
94295
94297
  declaration = {
94296
94298
  profile,
94297
- maxOldSpaceMb
94299
+ maxOldSpaceMb,
94300
+ rssBudgetMb
94298
94301
  };
94299
94302
  break;
94300
94303
  }
@@ -94324,6 +94327,15 @@ function extractMaxOldSpaceMb(parsed, addonId) {
94324
94327
  const value = readManifestAddons(parsed).find((a) => a.id === addonId)?.execution?.maxOldSpaceMb;
94325
94328
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : void 0;
94326
94329
  }
94330
+ /**
94331
+ * `execution.rssBudgetMb` for one addon. Only a POSITIVE finite number is a budget: unlike
94332
+ * `maxOldSpaceMb`, `0` here is not an escape hatch that lifts a cap, it is an absent budget —
94333
+ * and an absent budget is reported as "unwatched", never rounded into a default.
94334
+ */
94335
+ function extractRssBudgetMb(parsed, addonId) {
94336
+ const value = readManifestAddons(parsed).find((a) => a.id === addonId)?.execution?.rssBudgetMb;
94337
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : void 0;
94338
+ }
94327
94339
  function readManifestAddons(parsed) {
94328
94340
  if (typeof parsed !== "object" || parsed === null) return [];
94329
94341
  const camstack = parsed.camstack;
@@ -94349,10 +94361,20 @@ function isHeavyRunner(addons) {
94349
94361
  * one-off burst becomes the runner's permanent RSS.
94350
94362
  *
94351
94363
  * Measured on the live hub, 2026-08-17: the `recorder` runner reaches **2122 MB RSS** and stays
94352
- * there after the deferred archive walk (99,809 segments in 84s), while the `RecordingIndex` it
94353
- * actually retains is arithmetically ~21 MB (a `SegmentRow` is 5 numbers + 3 strings ≈ 207 B). The
94354
- * same addon on two agent nodes carrying the same code and a smaller archive sits at **58 MB**.
94355
- * The ~2 GB is walk garbage held as high-water in a heap that had no reason to collect it.
94364
+ * there. The same addon on two agent nodes carrying the same code and a smaller archive sits at
94365
+ * **58 MB**.
94366
+ *
94367
+ * This paragraph used to attribute that ~2 GB to "walk garbage" from the deferred archive walk,
94368
+ * and to argue from a ~21 MB `RecordingIndex`. Both were wrong, and the error cost a session on
94369
+ * 2026-08-27. The deferred walk had NO CALLER and never ran (it has since been deleted). And the
94370
+ * index is not 21 MB: a 2026-08-27 profile of the live recorder read `heapUsed = 1606 MB` against
94371
+ * `external = 8 MB`, with an `smaps` census putting 1510 MB in V8's 256 KB page ranges against
94372
+ * 54 MB of native heap — LIVE JavaScript retainers, not high-water garbage. The retained set is
94373
+ * ~7.1 M `SegmentRow`s plus their path strings and Map entries (13 cameras x 2 profiles x 8 640
94374
+ * segments/day x ~30 days), reseeded at boot by `SegmentHourLedger.hydrateIndex`.
94375
+ *
94376
+ * The lesson for anyone sizing this flag: a heap number needs a RETAINER, and "garbage the GC has
94377
+ * no reason to collect" is the explanation to reach for last, not first.
94356
94378
  *
94357
94379
  * ## Why 1024
94358
94380
  *
@@ -94369,8 +94391,7 @@ function isHeavyRunner(addons) {
94369
94391
  *
94370
94392
  * A runner whose working set genuinely does not fit declares its own number in its manifest
94371
94393
  * (`execution.maxOldSpaceMb`) — the number belongs next to the addon that knows its working set.
94372
- * `recorder` (3072 MB; peak 2341 MB heapTotal during the deferred archive walk) is the one
94373
- * shipped addon that does. `stream-broker` does NOT, despite 1585 MB peak RSS: ~1 GB of that is
94394
+ * `recorder` (3072 MB; peak 2341 MB heapTotal) is the one shipped addon that does. `stream-broker` does NOT, despite 1585 MB peak RSS: ~1 GB of that is
94374
94395
  * `arrayBuffers` (47 RTP rings), which this flag does not bound and never will — its actual old
94375
94396
  * space peaks at 224 MB. Size the declaration from `heapTotal` in the `[mem]` heartbeat, never
94376
94397
  * from RSS.
@@ -94399,6 +94420,22 @@ function runnerMaxOldSpaceMb(addons, heavy) {
94399
94420
  return Math.max(...declared);
94400
94421
  }
94401
94422
  /**
94423
+ * The runner's declared RSS budget in MB, or `undefined` when NOBODY declared one.
94424
+ *
94425
+ * Same rule as the old-space ceiling, for the same reason (D2/D29): the runner takes the MAXIMUM
94426
+ * any co-located addon declares, because a runner is respawned as a whole and co-location must
94427
+ * never silence the hungriest member. It would be strictly worse here than for a heap ceiling —
94428
+ * an under-sized budget does not kill anything, it just cries wolf, and a watchdog that cries
94429
+ * wolf is a watchdog that gets muted.
94430
+ *
94431
+ * `undefined` propagates as an ABSENT env var, and the child then announces itself unwatched.
94432
+ * There is deliberately no class default: see `AddonExecution.rssBudgetMb`.
94433
+ */
94434
+ function runnerRssBudgetMb(addons) {
94435
+ const declared = addons.map((a) => readAddonHeapDeclaration(a).rssBudgetMb).filter((mb) => mb !== void 0);
94436
+ return declared.length === 0 ? void 0 : Math.max(...declared);
94437
+ }
94438
+ /**
94402
94439
  * Per-placement V8 heap flags (Block D). LIGHT runners cap their young generation
94403
94440
  * (`--max-semi-space-size`) to reclaim V8's over-reservation — a measured ~15 MB/runner win — and
94404
94441
  * keep an opt-in old-space cap (`CAMSTACK_RUNNER_LIGHT_MAX_OLD_MB`, unset = uncapped). HEAVY
@@ -94496,6 +94533,7 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
94496
94533
  const nodeId = buildNodeId(runnerId);
94497
94534
  const runnerPath = node_path.resolve(__dirname, "addon-runner.js");
94498
94535
  const heavy = isHeavyRunner(addons);
94536
+ const rssBudgetMb = runnerRssBudgetMb(addons);
94499
94537
  const childEnv = {
94500
94538
  ...process.env,
94501
94539
  CAMSTACK_RUNNER_ID: runnerId,
@@ -94509,6 +94547,8 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
94509
94547
  ...applyRunnerNativeAllocator(process.env),
94510
94548
  ...env
94511
94549
  };
94550
+ if (rssBudgetMb === void 0) delete childEnv[require_manifest_python_deps.RUNNER_RSS_BUDGET_ENV];
94551
+ else childEnv[require_manifest_python_deps.RUNNER_RSS_BUDGET_ENV] = String(rssBudgetMb);
94512
94552
  const heapFlags = runnerHeapFlags(addons);
94513
94553
  capturedBroker?.logger.info(`[${runnerId}] heap profile: ${heavy ? "heavy" : "light"} flags=[${heapFlags.join(" ")}] arenas=${childEnv["MALLOC_ARENA_MAX"] ?? "glibc-default"} vips=${childEnv["VIPS_CONCURRENCY"] ?? "sharp-default"}`);
94514
94554
  const child = spawnFn(process.execPath, [...heapFlags, runnerPath], {
@@ -95308,6 +95348,8 @@ exports.HEAP_WATCH_INTERVAL_MS = require_manifest_python_deps.HEAP_WATCH_INTERVA
95308
95348
  exports.HEAP_WATCH_WARN_RATIO = require_manifest_python_deps.HEAP_WATCH_WARN_RATIO;
95309
95349
  exports.HUB_CAP_FWD_ACTION = require_manifest_python_deps.HUB_CAP_FWD_ACTION;
95310
95350
  exports.HUB_CAP_FWD_SERVICE = require_manifest_python_deps.HUB_CAP_FWD_SERVICE;
95351
+ exports.HUB_MAIN_RSS_BUDGET_MB = require_manifest_python_deps.HUB_MAIN_RSS_BUDGET_MB;
95352
+ exports.HUB_RSS_BUDGET_ENV = require_manifest_python_deps.HUB_RSS_BUDGET_ENV;
95311
95353
  exports.HubForwarderAddon = require_builtins_hub_forwarder_index.HubForwarderAddon$1;
95312
95354
  exports.HubForwarderDestination = require_builtins_hub_forwarder_index.HubForwarderDestination$1;
95313
95355
  exports.HubLogForwarder = HubLogForwarder;
@@ -95347,7 +95389,10 @@ exports.PipelineValidator = PipelineValidator;
95347
95389
  exports.PythonEnvManager = PythonEnvManager;
95348
95390
  exports.QUARANTINE_DIRNAME = QUARANTINE_DIRNAME;
95349
95391
  exports.RESTART_MARKER_FILE = RESTART_MARKER_FILE;
95392
+ exports.RSS_BUDGET_REANNOUNCE_MIN_MS = require_manifest_python_deps.RSS_BUDGET_REANNOUNCE_MIN_MS;
95393
+ exports.RSS_BUDGET_RELEASE_RATIO = require_manifest_python_deps.RSS_BUDGET_RELEASE_RATIO;
95350
95394
  exports.RUNNER_HEAP_WATCH_INTERVAL_MS = require_manifest_python_deps.RUNNER_HEAP_WATCH_INTERVAL_MS;
95395
+ exports.RUNNER_RSS_BUDGET_ENV = require_manifest_python_deps.RUNNER_RSS_BUDGET_ENV;
95351
95396
  exports.RUNTIME_DEFAULTS = require_dist.RUNTIME_DEFAULTS;
95352
95397
  exports.ReadinessRegistry = require_dist.ReadinessRegistry;
95353
95398
  exports.ReadinessTimeoutError = require_dist.ReadinessTimeoutError;
@@ -95444,6 +95489,7 @@ exports.createV8Reclaimer = require_manifest_python_deps.createV8Reclaimer;
95444
95489
  exports.deleteModelFromDisk = require_file_data_plane.deleteModelFromDisk;
95445
95490
  exports.deriveAgentListenPort = deriveAgentListenPort;
95446
95491
  exports.describeProviderKindDrift = describeProviderKindDrift;
95492
+ exports.describeRss = require_manifest_python_deps.describeRss;
95447
95493
  exports.detectWorkspacePackagesDir = detectWorkspacePackagesDir;
95448
95494
  Object.defineProperty(exports, "downloadBinary", {
95449
95495
  enumerable: true,
@@ -95520,6 +95566,7 @@ exports.getSinglePidStats = require_resource_monitor.getSinglePidStats;
95520
95566
  exports.getWorkerDeviceRegistry = require_manifest_python_deps.getWorkerDeviceRegistry;
95521
95567
  exports.hasDotNode = hasDotNode;
95522
95568
  exports.hashClusterSecret = hashClusterSecret;
95569
+ exports.hubMainRssBudget = require_manifest_python_deps.hubMainRssBudget;
95523
95570
  exports.installManifestNativeDeps = require_manifest_python_deps.installManifestNativeDeps;
95524
95571
  exports.installManifestPythonDeps = require_manifest_python_deps.installManifestPythonDeps;
95525
95572
  exports.installPackageFromNpm = installPackageFromNpm;
@@ -95548,8 +95595,10 @@ exports.loadTlsCert = require_tls$1.loadTlsCert;
95548
95595
  exports.localEndpointPath = require_manifest_python_deps.localEndpointPath;
95549
95596
  exports.localProviderLink = require_manifest_python_deps.localProviderLink;
95550
95597
  exports.mountNativeCapService = require_manifest_python_deps.mountNativeCapService;
95598
+ exports.nextRssBudgetState = require_manifest_python_deps.nextRssBudgetState;
95551
95599
  exports.parseCapAction = require_manifest_python_deps.parseCapAction;
95552
95600
  exports.parseRangeHeader = require_file_data_plane.parseRangeHeader;
95601
+ exports.parseRssBudgetMb = require_manifest_python_deps.parseRssBudgetMb;
95553
95602
  exports.parseTokenizedUrl = require_file_data_plane.parseTokenizedUrl;
95554
95603
  exports.partitionIsolatedBuiltinIds = partitionIsolatedBuiltinIds;
95555
95604
  exports.proxyToUpstream = proxyToUpstream;
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM$1, r as __exportAll, t as __commonJSMin$1 } from "./chunk-CNf5ZN-e.mjs";
2
- import { $ as kebabToCamel, Bt as emitDownForOwnedCaps, C as addonSettingsCapability, Dt as DATAPLANE_SECRET_HEADER$1, G as extractNestedAddonId, Ht as expandCapMethods, It as asJsonObject$1, J as isCollectionArrayMethod, Jt as readinessKey, Kt as parseJsonObject, Lt as asNumber, Nt as ReadinessRegistry, Ot as DEVICE_SETTINGS_CONTRIBUTION_METHODS, Pt as ReadinessTimeoutError, Q as isVoidInput, Qt as EventCategory$1, Rt as asString$1, Tt as errMsg$1, Xt as scopeKey, Y as isObjectInput, Yt as resolveCapMount, dt as resolveMethodAuth, et as lifecycleJobSchema, f as LOG_LEVEL_RANK, it as looseSchema, kt as DEVICE_STATUS_METHOD, m as RUNTIME_DEFAULTS, mt as scopesAllowDeviceCap, p as METHOD_ACCESS_MAP, pt as scopesAllowAddon, q as isArrayOutputSchema, qt as parseJsonUnknown$1, rt as logLevelAtMost, st as objectInputDeclaresAddonId, t as ALL_CAPABILITY_DEFINITIONS, ut as procedureAuthKey, zt as createEvent } from "./dist-6rM7chzb.mjs";
2
+ import { $ as kebabToCamel, Bt as emitDownForOwnedCaps, C as addonSettingsCapability, Dt as DATAPLANE_SECRET_HEADER$1, G as extractNestedAddonId, Ht as expandCapMethods, It as asJsonObject$1, J as isCollectionArrayMethod, Jt as readinessKey, Kt as parseJsonObject, Lt as asNumber, Nt as ReadinessRegistry, Ot as DEVICE_SETTINGS_CONTRIBUTION_METHODS, Pt as ReadinessTimeoutError, Q as isVoidInput, Qt as EventCategory$1, Rt as asString$1, Tt as errMsg$1, Xt as scopeKey, Y as isObjectInput, Yt as resolveCapMount, dt as resolveMethodAuth, et as lifecycleJobSchema, f as LOG_LEVEL_RANK, it as looseSchema, kt as DEVICE_STATUS_METHOD, m as RUNTIME_DEFAULTS, mt as scopesAllowDeviceCap, p as METHOD_ACCESS_MAP, pt as scopesAllowAddon, q as isArrayOutputSchema, qt as parseJsonUnknown$1, rt as logLevelAtMost, st as objectInputDeclaresAddonId, t as ALL_CAPABILITY_DEFINITIONS, ut as procedureAuthKey, zt as createEvent } from "./dist-CG7kwIZp.mjs";
3
3
  import { AlertCenterAddon } from "./builtins/alerts/alerts.addon.mjs";
4
4
  import "./builtins/alerts/index.mjs";
5
5
  import { t as formatLogLine } from "./formatter-B7qW8bPJ.mjs";
@@ -25,7 +25,7 @@ import "./builtins/system-config/index.mjs";
25
25
  import { WinstonDestination, WinstonLoggingAddon } from "./builtins/winston-logging/index.mjs";
26
26
  import { a as parseTokenizedUrl, c as collectModelFiles, d as downloadModel, f as ensureModel, h as isModelDownloaded, i as parseRangeHeader, l as deleteModelFromDisk, m as getModelFilePath, n as contentTypeFor, o as resolveFilePath, p as fetchJson, r as createAuthenticatedFileServer, s as ModelDownloadService, t as createFileDataPlaneHandler, u as downloadFile } from "./file-data-plane-BhKdxJgf.mjs";
27
27
  import { C as registerLanHttpHandler, S as readLanHttpState, _ as DEFAULT_LAN_HTTP_PORT, a as writeTlsMode, b as bindPendingLanHttp, c as loadTlsCert, d as collectCertIdentity, f as CA_VALIDITY_DAYS, g as evaluateExistingCert, h as SERVER_AUTH_OID, i as writeExtraSans, l as reissueTlsLeaf, m as MAX_LEAF_VALIDITY_DAYS, n as readTlsAccessStatus, o as validateUploadedTls, p as LEAF_RENEWAL_WINDOW_DAYS, r as readTlsMode, s as ensureTlsCert, t as readExtraSans, u as CA_COMMON_NAME, v as allFamiliesListenHost, x as closeLanHttp, y as applyLanHttp } from "./tls-CQhPGSJm.mjs";
28
- import { $ as buildUdsNativeCapProxy, A as createParentUnownedCallHandler, At as createV8Reclaimer, B as AGENT_CAP_FWD_SERVICE, C as brokerTransportLink, Ct as createAddonDataPlaneFacility, D as HUB_CAP_FWD_ACTION, Dt as HEAP_WATCH_WARN_RATIO, E as localProviderLink, Et as HEAP_WATCH_INTERVAL_MS, F as createUdsLoggerWithControl, G as createLocalTransport, H as CapRouteError, I as LocalChildClient, J as SocketChannel, K as UdsLocalTransportClient, L as LocalChildRegistry, M as createUdsEventBus, Mt as startHeapWatch, N as udsChildLogToWorkerEntry, Nt as startRunnerHeapWatch, O as HUB_CAP_FWD_SERVICE, Ot as RUNNER_HEAP_WATCH_INTERVAL_MS, P as createUdsLogger, Pt as strandedMb, Q as buildNativeCapProxy, R as UDS_NO_ROUTE_PREFIX, S as brokerCallForCap, St as runNpm, T as ipcParentLink, Tt as HEAP_RECLAIM_TRIGGER_MB, U as classifyCapRoute, V as CapRouteResolver, W as callWithServiceDiscovery, X as FrameDecoder, Y as localEndpointPath, Z as encodeFrame, _ as createKernelHwAccel, _t as CapabilityUnavailableError, a as getWorkerDeviceRegistry, at as createAddonService, b as __resetCapUsageRegistryForTests, bt as resolveAddonClass, c as setHubConnected, ct as capActionName, d as getBrokerEventBus, dt as capServiceName, et as createBrokerDeviceManagerApi, f as getMoleculerEventStats, ft as parseCapAction, g as AddonDepsManager, gt as CapabilityHandle, h as subscribePassthrough, ht as DeviceRegistry, i as createUdsAddonContext, j as createUdsEventBridge, jt as shouldReclaim, k as createHubCapForwardService, kt as buildHeapSample, l as EVENT_TOPIC_PREFIX, lt as capActionSuffix, m as setNodeEventInterest, mt as serializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as validateProviderRegistrations, p as registerEventBusService, pt as deserializeTypedArrays, q as UdsLocalTransportServer, r as createAddonContext, rt as mountNativeCapService, s as getOrInitReadinessRegistryForClient, st as NATIVE_PROVIDER_SERVICE_INFIX, t as installManifestPythonDeps, u as clusterEventTopic, ut as capBareAction, v as resolveHwAccel, vt as copyBundledNativeModules, w as buildLinkChain, wt as HEAP_RECLAIM_MIN_INTERVAL_MS, x as getCapUsageRegistry, xt as resolveNpmInvocation, y as CapUsageRegistry, yt as installManifestNativeDeps, z as AGENT_CAP_FWD_ACTION } from "./manifest-python-deps-BuhBXCg0.mjs";
28
+ import { $ as buildUdsNativeCapProxy, A as createParentUnownedCallHandler, At as RSS_BUDGET_REANNOUNCE_MIN_MS, B as AGENT_CAP_FWD_SERVICE, Bt as shouldReclaim, C as brokerTransportLink, Ct as createAddonDataPlaneFacility, D as HUB_CAP_FWD_ACTION, Dt as HEAP_WATCH_WARN_RATIO, E as localProviderLink, Et as HEAP_WATCH_INTERVAL_MS, F as createUdsLoggerWithControl, Ft as createV8Reclaimer, G as createLocalTransport, H as CapRouteError, Ht as startRunnerHeapWatch, I as LocalChildClient, It as describeRss, J as SocketChannel, K as UdsLocalTransportClient, L as LocalChildRegistry, Lt as hubMainRssBudget, M as createUdsEventBus, Mt as RUNNER_HEAP_WATCH_INTERVAL_MS, N as udsChildLogToWorkerEntry, Nt as RUNNER_RSS_BUDGET_ENV, O as HUB_CAP_FWD_SERVICE, Ot as HUB_MAIN_RSS_BUDGET_MB, P as createUdsLogger, Pt as buildHeapSample, Q as buildNativeCapProxy, R as UDS_NO_ROUTE_PREFIX, Rt as nextRssBudgetState, S as brokerCallForCap, St as runNpm, T as ipcParentLink, Tt as HEAP_RECLAIM_TRIGGER_MB, U as classifyCapRoute, Ut as strandedMb, V as CapRouteResolver, Vt as startHeapWatch, W as callWithServiceDiscovery, X as FrameDecoder, Y as localEndpointPath, Z as encodeFrame, _ as createKernelHwAccel, _t as CapabilityUnavailableError, a as getWorkerDeviceRegistry, at as createAddonService, b as __resetCapUsageRegistryForTests, bt as resolveAddonClass, c as setHubConnected, ct as capActionName, d as getBrokerEventBus, dt as capServiceName, et as createBrokerDeviceManagerApi, f as getMoleculerEventStats, ft as parseCapAction, g as AddonDepsManager, gt as CapabilityHandle, h as subscribePassthrough, ht as DeviceRegistry, i as createUdsAddonContext, j as createUdsEventBridge, jt as RSS_BUDGET_RELEASE_RATIO, k as createHubCapForwardService, kt as HUB_RSS_BUDGET_ENV, l as EVENT_TOPIC_PREFIX, lt as capActionSuffix, m as setNodeEventInterest, mt as serializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as validateProviderRegistrations, p as registerEventBusService, pt as deserializeTypedArrays, q as UdsLocalTransportServer, r as createAddonContext, rt as mountNativeCapService, s as getOrInitReadinessRegistryForClient, st as NATIVE_PROVIDER_SERVICE_INFIX, t as installManifestPythonDeps, u as clusterEventTopic, ut as capBareAction, v as resolveHwAccel, vt as copyBundledNativeModules, w as buildLinkChain, wt as HEAP_RECLAIM_MIN_INTERVAL_MS, x as getCapUsageRegistry, xt as resolveNpmInvocation, y as CapUsageRegistry, yt as installManifestNativeDeps, z as AGENT_CAP_FWD_ACTION, zt as parseRssBudgetMb } from "./manifest-python-deps-CVeckyXx.mjs";
29
29
  import { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-BWmQ5i-o.mjs";
30
30
  import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
31
31
  import { z } from "zod";
@@ -94263,7 +94263,8 @@ async function getPidStats$1(pids) {
94263
94263
  }
94264
94264
  var EMPTY_HEAP_DECLARATION = {
94265
94265
  profile: void 0,
94266
- maxOldSpaceMb: void 0
94266
+ maxOldSpaceMb: void 0,
94267
+ rssBudgetMb: void 0
94267
94268
  };
94268
94269
  /**
94269
94270
  * Resolve the declared heap profile + old-space ceiling for one addon by reading its package.json
@@ -94284,10 +94285,12 @@ function readAddonHeapDeclaration(spec) {
94284
94285
  const parsed = JSON.parse(raw);
94285
94286
  const profile = extractHeapProfile(parsed, spec.addonId);
94286
94287
  const maxOldSpaceMb = extractMaxOldSpaceMb(parsed, spec.addonId);
94287
- if (profile !== void 0 || maxOldSpaceMb !== void 0) {
94288
+ const rssBudgetMb = extractRssBudgetMb(parsed, spec.addonId);
94289
+ if (profile !== void 0 || maxOldSpaceMb !== void 0 || rssBudgetMb !== void 0) {
94288
94290
  declaration = {
94289
94291
  profile,
94290
- maxOldSpaceMb
94292
+ maxOldSpaceMb,
94293
+ rssBudgetMb
94291
94294
  };
94292
94295
  break;
94293
94296
  }
@@ -94317,6 +94320,15 @@ function extractMaxOldSpaceMb(parsed, addonId) {
94317
94320
  const value = readManifestAddons(parsed).find((a) => a.id === addonId)?.execution?.maxOldSpaceMb;
94318
94321
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : void 0;
94319
94322
  }
94323
+ /**
94324
+ * `execution.rssBudgetMb` for one addon. Only a POSITIVE finite number is a budget: unlike
94325
+ * `maxOldSpaceMb`, `0` here is not an escape hatch that lifts a cap, it is an absent budget —
94326
+ * and an absent budget is reported as "unwatched", never rounded into a default.
94327
+ */
94328
+ function extractRssBudgetMb(parsed, addonId) {
94329
+ const value = readManifestAddons(parsed).find((a) => a.id === addonId)?.execution?.rssBudgetMb;
94330
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : void 0;
94331
+ }
94320
94332
  function readManifestAddons(parsed) {
94321
94333
  if (typeof parsed !== "object" || parsed === null) return [];
94322
94334
  const camstack = parsed.camstack;
@@ -94342,10 +94354,20 @@ function isHeavyRunner(addons) {
94342
94354
  * one-off burst becomes the runner's permanent RSS.
94343
94355
  *
94344
94356
  * Measured on the live hub, 2026-08-17: the `recorder` runner reaches **2122 MB RSS** and stays
94345
- * there after the deferred archive walk (99,809 segments in 84s), while the `RecordingIndex` it
94346
- * actually retains is arithmetically ~21 MB (a `SegmentRow` is 5 numbers + 3 strings ≈ 207 B). The
94347
- * same addon on two agent nodes carrying the same code and a smaller archive sits at **58 MB**.
94348
- * The ~2 GB is walk garbage held as high-water in a heap that had no reason to collect it.
94357
+ * there. The same addon on two agent nodes carrying the same code and a smaller archive sits at
94358
+ * **58 MB**.
94359
+ *
94360
+ * This paragraph used to attribute that ~2 GB to "walk garbage" from the deferred archive walk,
94361
+ * and to argue from a ~21 MB `RecordingIndex`. Both were wrong, and the error cost a session on
94362
+ * 2026-08-27. The deferred walk had NO CALLER and never ran (it has since been deleted). And the
94363
+ * index is not 21 MB: a 2026-08-27 profile of the live recorder read `heapUsed = 1606 MB` against
94364
+ * `external = 8 MB`, with an `smaps` census putting 1510 MB in V8's 256 KB page ranges against
94365
+ * 54 MB of native heap — LIVE JavaScript retainers, not high-water garbage. The retained set is
94366
+ * ~7.1 M `SegmentRow`s plus their path strings and Map entries (13 cameras x 2 profiles x 8 640
94367
+ * segments/day x ~30 days), reseeded at boot by `SegmentHourLedger.hydrateIndex`.
94368
+ *
94369
+ * The lesson for anyone sizing this flag: a heap number needs a RETAINER, and "garbage the GC has
94370
+ * no reason to collect" is the explanation to reach for last, not first.
94349
94371
  *
94350
94372
  * ## Why 1024
94351
94373
  *
@@ -94362,8 +94384,7 @@ function isHeavyRunner(addons) {
94362
94384
  *
94363
94385
  * A runner whose working set genuinely does not fit declares its own number in its manifest
94364
94386
  * (`execution.maxOldSpaceMb`) — the number belongs next to the addon that knows its working set.
94365
- * `recorder` (3072 MB; peak 2341 MB heapTotal during the deferred archive walk) is the one
94366
- * shipped addon that does. `stream-broker` does NOT, despite 1585 MB peak RSS: ~1 GB of that is
94387
+ * `recorder` (3072 MB; peak 2341 MB heapTotal) is the one shipped addon that does. `stream-broker` does NOT, despite 1585 MB peak RSS: ~1 GB of that is
94367
94388
  * `arrayBuffers` (47 RTP rings), which this flag does not bound and never will — its actual old
94368
94389
  * space peaks at 224 MB. Size the declaration from `heapTotal` in the `[mem]` heartbeat, never
94369
94390
  * from RSS.
@@ -94392,6 +94413,22 @@ function runnerMaxOldSpaceMb(addons, heavy) {
94392
94413
  return Math.max(...declared);
94393
94414
  }
94394
94415
  /**
94416
+ * The runner's declared RSS budget in MB, or `undefined` when NOBODY declared one.
94417
+ *
94418
+ * Same rule as the old-space ceiling, for the same reason (D2/D29): the runner takes the MAXIMUM
94419
+ * any co-located addon declares, because a runner is respawned as a whole and co-location must
94420
+ * never silence the hungriest member. It would be strictly worse here than for a heap ceiling —
94421
+ * an under-sized budget does not kill anything, it just cries wolf, and a watchdog that cries
94422
+ * wolf is a watchdog that gets muted.
94423
+ *
94424
+ * `undefined` propagates as an ABSENT env var, and the child then announces itself unwatched.
94425
+ * There is deliberately no class default: see `AddonExecution.rssBudgetMb`.
94426
+ */
94427
+ function runnerRssBudgetMb(addons) {
94428
+ const declared = addons.map((a) => readAddonHeapDeclaration(a).rssBudgetMb).filter((mb) => mb !== void 0);
94429
+ return declared.length === 0 ? void 0 : Math.max(...declared);
94430
+ }
94431
+ /**
94395
94432
  * Per-placement V8 heap flags (Block D). LIGHT runners cap their young generation
94396
94433
  * (`--max-semi-space-size`) to reclaim V8's over-reservation — a measured ~15 MB/runner win — and
94397
94434
  * keep an opt-in old-space cap (`CAMSTACK_RUNNER_LIGHT_MAX_OLD_MB`, unset = uncapped). HEAVY
@@ -94489,6 +94526,7 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
94489
94526
  const nodeId = buildNodeId(runnerId);
94490
94527
  const runnerPath = path$39.resolve(__dirname, "addon-runner.js");
94491
94528
  const heavy = isHeavyRunner(addons);
94529
+ const rssBudgetMb = runnerRssBudgetMb(addons);
94492
94530
  const childEnv = {
94493
94531
  ...process.env,
94494
94532
  CAMSTACK_RUNNER_ID: runnerId,
@@ -94502,6 +94540,8 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
94502
94540
  ...applyRunnerNativeAllocator(process.env),
94503
94541
  ...env
94504
94542
  };
94543
+ if (rssBudgetMb === void 0) delete childEnv[RUNNER_RSS_BUDGET_ENV];
94544
+ else childEnv[RUNNER_RSS_BUDGET_ENV] = String(rssBudgetMb);
94505
94545
  const heapFlags = runnerHeapFlags(addons);
94506
94546
  capturedBroker?.logger.info(`[${runnerId}] heap profile: ${heavy ? "heavy" : "light"} flags=[${heapFlags.join(" ")}] arenas=${childEnv["MALLOC_ARENA_MAX"] ?? "glibc-default"} vips=${childEnv["VIPS_CONCURRENCY"] ?? "sharp-default"}`);
94507
94547
  const child = spawnFn(process.execPath, [...heapFlags, runnerPath], {
@@ -95243,4 +95283,4 @@ var LifecycleJobEngine = class {
95243
95283
  }
95244
95284
  };
95245
95285
  //#endregion
95246
- export { AGENT_CAP_FWD_ACTION, AGENT_CAP_FWD_SERVICE, AGENT_READINESS_SERVICE_NAME, ALL_CAPABILITY_DEFINITIONS, AddonApiFactory, AddonDepsManager, AddonEngineManager, AddonHealthMonitor, AddonInstaller, AddonLoader, AddonManifest, AddonRouteRegistry, AlertCenterAddon, ApiKeyManager, AuthManager, CA_COMMON_NAME, CA_VALIDITY_DAYS, CLUSTER_SECRET_MISMATCH_TYPE, CLUSTER_SECRET_REJECTED_EXIT_CODE, CORE_CAP_SERVICE_NAME, CapRouteError, CapRouteResolver, CapUsageRegistry, CapabilityHandle, CapabilityRegistry, CapabilityUnavailableError, ConfigManager, ConfigStore, ConsoleDestination, ConsoleLoggingAddon, CoreBlocksAddon, CustomActionRegistry, DEFAULT_DATA_PATH, DEFAULT_LAN_HTTP_PORT, DEFAULT_LOG_LEVEL, DEVICE_SETTINGS_CONTRIBUTION_METHODS, DEVICE_STATUS_METHOD, DataPlaneRegistry, DeviceManagerAddon, DeviceRegistry, DeviceStore, EVENT_TOPIC_PREFIX, EngineManagerResolver, EventBus, FeatureManager, FilesystemStorageAddon, FilesystemStorageProvider, FrameDecoder, FsStorageBackend, HEALTH_MONITOR_GRACE_PERIOD_MS, HEALTH_MONITOR_RETRY_INTERVALS_MS, HEALTH_MONITOR_TICK_MS, HEAP_RECLAIM_MIN_INTERVAL_MS, HEAP_RECLAIM_TRIGGER_MB, HEAP_WATCH_INTERVAL_MS, HEAP_WATCH_WARN_RATIO, HUB_CAP_FWD_ACTION, HUB_CAP_FWD_SERVICE, HubForwarderAddon, HubForwarderDestination, HubLogForwarder, HubNodeRegistry, INFRA_CAPABILITIES, IntegrationRegistry, JobJournal, LEAF_RENEWAL_WINDOW_DAYS, LifecycleJobEngine, LifecycleStateMachine, LivenessMonitorAddon, LocalAuthAddon, LocalChildClient, LocalChildRegistry, LogManager, LogRingBuffer, LoggingGate, LokiDestination, LokiLoggingAddon, MAX_LEAF_VALIDITY_DAYS, METHOD_ACCESS_MAP, ModelDownloadService, NATIVE_PROVIDER_SERVICE_INFIX, NATIVE_SCAN_DEPTH, NativeMetricsAddon, NativeMetricsProvider, NetworkQualityTracker, NotificationService, PYTHON_VERSION, PipelineRunner, PipelineValidator, PythonEnvManager, QUARANTINE_DIRNAME, RESTART_MARKER_FILE, RUNNER_HEAP_WATCH_INTERVAL_MS, RUNTIME_DEFAULTS, ReadinessRegistry, ReadinessTimeoutError, ReplEngine, RingBuffer, SERVER_AUTH_OID, ScopedLogger, ScopedTokenManager, SocketChannel, SqliteSettingsAddon, SqliteSettingsBackend, StagingArea, StorageLocationManager, StorageManager, StorageOrchestratorAddon, StorageOrchestratorService, SystemConfigAddon, SystemEventBus, TRADITIONAL_NATIVE_PACKAGES, ToastService, UDS_NO_ROUTE_PREFIX, UdsLocalTransportClient, UdsLocalTransportServer, UserManager, WinstonDestination, WinstonLoggingAddon, __resetCapUsageRegistryForTests, __resetLoggingGateForTests, adaptBrokerToCluster, addonSettingsCapability, allFamiliesListenHost, applyLanHttp, bindPendingLanHttp, bootstrapSchema, brokerCallForCap, brokerTransportLink, buildBinaryPath, buildCapRouters, buildHeapSample, buildLinkChain, buildNativeCapProxy, buildNodeManifest, buildStorageLocationRegistry, buildUdsNativeCapProxy, builderMountedCapNames, callRegisterNodeWithRetry, callWithServiceDiscovery, canServeDataPlane, capActionName, capActionSuffix, capBareAction, capServiceName, classifyAddonDir, classifyCapRoute, clearPendingRestart, closeLanHttp, clusterEventTopic, clusterSecretMatches, collectCertIdentity, collectModelFiles, contentTypeFor, copyDirRecursive, copyExtraFileDirs, createAddonContext, createAddonDataPlaneFacility, createAddonService, createAuthenticatedFileServer, createBroker, createBrokerDeviceManagerApi, createCoreCapService, createDoorSettingsView, createFileDataPlaneHandler, createHubCapForwardService, createHubService, createKernelHwAccel, createLocalTransport, createParentUnownedCallHandler, createProcessService, createReadinessService, createReadinessServiceForRegistry, createScopedProcessManager, createStreamProbeBrokerService, createUdsAddonContext, createUdsEventBridge, createUdsEventBus, createUdsLogger, createUdsLoggerWithControl, createV8Reclaimer, deleteModelFromDisk, deriveAgentListenPort, describeProviderKindDrift, detectWorkspacePackagesDir, downloadBinary, downloadFile, downloadModel, emitDownForOwnedCaps, encodeFrame, ensureAddonNativePrebuilds, ensureBinary, ensureDir, ensureFfmpeg, ensureLibraryBuilt, ensureModel, ensureNativePrebuilds, ensurePython, ensureTlsCert, evaluateExistingCert, expandCapMethods, fetchJson, findInPath, formatLogLine, getBrokerEventBus, getCapUsageRegistry, getFfmpegDownloadUrl, getLoggingGate, getModelFilePath, getMoleculerEventStats, getOrInitReadinessRegistry, getOrInitReadinessRegistryForClient, getPidStats, getPlatformInfo, getPythonDownloadUrl, getRestartMarkerPath, getSinglePidStats, getWorkerDeviceRegistry, hasDotNode, hashClusterSecret, installManifestNativeDeps, installManifestPythonDeps, installPackageFromNpm, installPythonPackages, installPythonRequirements, ipcParentLink, isAddonDeploySource, isArrayOutputSchema, isClusterSecretMismatchError, isCollectionArrayMethod, isInfraCapability, isModelDownloaded, isSourceNewer, isolatedBuiltinPhase, loadTlsCert, localEndpointPath, localProviderLink, mountNativeCapService, parseCapAction, parseRangeHeader, parseTokenizedUrl, partitionIsolatedBuiltinIds, proxyToUpstream, quarantineAddonResidue, readExtraSans, readLanHttpState, readPendingRestart, readTlsAccessStatus, readTlsMode, readinessKey, registerEventBusService, registerLanHttpHandler, reissueTlsLeaf, resolveFilePath, resolveHwAccel, resolveNpmInvocation, runHubAddonBoot, runNpm, scheduleSelfRestart, scopeKey, scopesAllowAddon, scopesAllowDeviceCap, selectAddonResidue, serializeTypedArrays, setHubConnected, setNodeEventInterest, shouldReclaim, startHeapWatch, startRunnerHeapWatch, strandedMb, stripCamstackDeps, subscribePassthrough, udsChildLogToWorkerEntry, validateProviderRegistrations, validateUploadedTls, waitUntilReady, writeExtraSans, writePendingRestart, writeTlsMode };
95286
+ export { AGENT_CAP_FWD_ACTION, AGENT_CAP_FWD_SERVICE, AGENT_READINESS_SERVICE_NAME, ALL_CAPABILITY_DEFINITIONS, AddonApiFactory, AddonDepsManager, AddonEngineManager, AddonHealthMonitor, AddonInstaller, AddonLoader, AddonManifest, AddonRouteRegistry, AlertCenterAddon, ApiKeyManager, AuthManager, CA_COMMON_NAME, CA_VALIDITY_DAYS, CLUSTER_SECRET_MISMATCH_TYPE, CLUSTER_SECRET_REJECTED_EXIT_CODE, CORE_CAP_SERVICE_NAME, CapRouteError, CapRouteResolver, CapUsageRegistry, CapabilityHandle, CapabilityRegistry, CapabilityUnavailableError, ConfigManager, ConfigStore, ConsoleDestination, ConsoleLoggingAddon, CoreBlocksAddon, CustomActionRegistry, DEFAULT_DATA_PATH, DEFAULT_LAN_HTTP_PORT, DEFAULT_LOG_LEVEL, DEVICE_SETTINGS_CONTRIBUTION_METHODS, DEVICE_STATUS_METHOD, DataPlaneRegistry, DeviceManagerAddon, DeviceRegistry, DeviceStore, EVENT_TOPIC_PREFIX, EngineManagerResolver, EventBus, FeatureManager, FilesystemStorageAddon, FilesystemStorageProvider, FrameDecoder, FsStorageBackend, HEALTH_MONITOR_GRACE_PERIOD_MS, HEALTH_MONITOR_RETRY_INTERVALS_MS, HEALTH_MONITOR_TICK_MS, HEAP_RECLAIM_MIN_INTERVAL_MS, HEAP_RECLAIM_TRIGGER_MB, HEAP_WATCH_INTERVAL_MS, HEAP_WATCH_WARN_RATIO, HUB_CAP_FWD_ACTION, HUB_CAP_FWD_SERVICE, HUB_MAIN_RSS_BUDGET_MB, HUB_RSS_BUDGET_ENV, HubForwarderAddon, HubForwarderDestination, HubLogForwarder, HubNodeRegistry, INFRA_CAPABILITIES, IntegrationRegistry, JobJournal, LEAF_RENEWAL_WINDOW_DAYS, LifecycleJobEngine, LifecycleStateMachine, LivenessMonitorAddon, LocalAuthAddon, LocalChildClient, LocalChildRegistry, LogManager, LogRingBuffer, LoggingGate, LokiDestination, LokiLoggingAddon, MAX_LEAF_VALIDITY_DAYS, METHOD_ACCESS_MAP, ModelDownloadService, NATIVE_PROVIDER_SERVICE_INFIX, NATIVE_SCAN_DEPTH, NativeMetricsAddon, NativeMetricsProvider, NetworkQualityTracker, NotificationService, PYTHON_VERSION, PipelineRunner, PipelineValidator, PythonEnvManager, QUARANTINE_DIRNAME, RESTART_MARKER_FILE, RSS_BUDGET_REANNOUNCE_MIN_MS, RSS_BUDGET_RELEASE_RATIO, RUNNER_HEAP_WATCH_INTERVAL_MS, RUNNER_RSS_BUDGET_ENV, RUNTIME_DEFAULTS, ReadinessRegistry, ReadinessTimeoutError, ReplEngine, RingBuffer, SERVER_AUTH_OID, ScopedLogger, ScopedTokenManager, SocketChannel, SqliteSettingsAddon, SqliteSettingsBackend, StagingArea, StorageLocationManager, StorageManager, StorageOrchestratorAddon, StorageOrchestratorService, SystemConfigAddon, SystemEventBus, TRADITIONAL_NATIVE_PACKAGES, ToastService, UDS_NO_ROUTE_PREFIX, UdsLocalTransportClient, UdsLocalTransportServer, UserManager, WinstonDestination, WinstonLoggingAddon, __resetCapUsageRegistryForTests, __resetLoggingGateForTests, adaptBrokerToCluster, addonSettingsCapability, allFamiliesListenHost, applyLanHttp, bindPendingLanHttp, bootstrapSchema, brokerCallForCap, brokerTransportLink, buildBinaryPath, buildCapRouters, buildHeapSample, buildLinkChain, buildNativeCapProxy, buildNodeManifest, buildStorageLocationRegistry, buildUdsNativeCapProxy, builderMountedCapNames, callRegisterNodeWithRetry, callWithServiceDiscovery, canServeDataPlane, capActionName, capActionSuffix, capBareAction, capServiceName, classifyAddonDir, classifyCapRoute, clearPendingRestart, closeLanHttp, clusterEventTopic, clusterSecretMatches, collectCertIdentity, collectModelFiles, contentTypeFor, copyDirRecursive, copyExtraFileDirs, createAddonContext, createAddonDataPlaneFacility, createAddonService, createAuthenticatedFileServer, createBroker, createBrokerDeviceManagerApi, createCoreCapService, createDoorSettingsView, createFileDataPlaneHandler, createHubCapForwardService, createHubService, createKernelHwAccel, createLocalTransport, createParentUnownedCallHandler, createProcessService, createReadinessService, createReadinessServiceForRegistry, createScopedProcessManager, createStreamProbeBrokerService, createUdsAddonContext, createUdsEventBridge, createUdsEventBus, createUdsLogger, createUdsLoggerWithControl, createV8Reclaimer, deleteModelFromDisk, deriveAgentListenPort, describeProviderKindDrift, describeRss, detectWorkspacePackagesDir, downloadBinary, downloadFile, downloadModel, emitDownForOwnedCaps, encodeFrame, ensureAddonNativePrebuilds, ensureBinary, ensureDir, ensureFfmpeg, ensureLibraryBuilt, ensureModel, ensureNativePrebuilds, ensurePython, ensureTlsCert, evaluateExistingCert, expandCapMethods, fetchJson, findInPath, formatLogLine, getBrokerEventBus, getCapUsageRegistry, getFfmpegDownloadUrl, getLoggingGate, getModelFilePath, getMoleculerEventStats, getOrInitReadinessRegistry, getOrInitReadinessRegistryForClient, getPidStats, getPlatformInfo, getPythonDownloadUrl, getRestartMarkerPath, getSinglePidStats, getWorkerDeviceRegistry, hasDotNode, hashClusterSecret, hubMainRssBudget, installManifestNativeDeps, installManifestPythonDeps, installPackageFromNpm, installPythonPackages, installPythonRequirements, ipcParentLink, isAddonDeploySource, isArrayOutputSchema, isClusterSecretMismatchError, isCollectionArrayMethod, isInfraCapability, isModelDownloaded, isSourceNewer, isolatedBuiltinPhase, loadTlsCert, localEndpointPath, localProviderLink, mountNativeCapService, nextRssBudgetState, parseCapAction, parseRangeHeader, parseRssBudgetMb, parseTokenizedUrl, partitionIsolatedBuiltinIds, proxyToUpstream, quarantineAddonResidue, readExtraSans, readLanHttpState, readPendingRestart, readTlsAccessStatus, readTlsMode, readinessKey, registerEventBusService, registerLanHttpHandler, reissueTlsLeaf, resolveFilePath, resolveHwAccel, resolveNpmInvocation, runHubAddonBoot, runNpm, scheduleSelfRestart, scopeKey, scopesAllowAddon, scopesAllowDeviceCap, selectAddonResidue, serializeTypedArrays, setHubConnected, setNodeEventInterest, shouldReclaim, startHeapWatch, startRunnerHeapWatch, strandedMb, stripCamstackDeps, subscribePassthrough, udsChildLogToWorkerEntry, validateProviderRegistrations, validateUploadedTls, waitUntilReady, writeExtraSans, writePendingRestart, writeTlsMode };
@@ -207,10 +207,103 @@ export interface HeapWatchSink {
207
207
  * The subtrahend is heapUSED, not heapTotal, deliberately: the pages V8
208
208
  * committed at a sawtooth peak and no longer needs are counted inside heapTotal,
209
209
  * and they are precisely what this number exists to see.
210
+ *
211
+ * ## What this number can and CANNOT be asked
212
+ *
213
+ * It answers exactly one question: *are there pages a `last-resort` GC could
214
+ * hand back?* For that it is correct, and {@link shouldReclaim} is the only
215
+ * gate that should ever be built on it.
216
+ *
217
+ * It can never answer *is this process too big?*, because `external` is
218
+ * subtracted by construction. A process whose growth is entirely `external` —
219
+ * Buffers, `arrayBuffers`, what a native addon mallocs — moves `rss` and the
220
+ * subtrahend by the same amount, so this number is **algebraically constant
221
+ * while the process triples**. That is not a threshold set too high; the
222
+ * quantity cannot represent the event.
223
+ *
224
+ * It cost 11.4GB and a ~154 second stall of the whole container to establish
225
+ * that. See {@link RssBudgetOptions} for the number that does answer it.
210
226
  */
211
227
  export declare function strandedMb(s: HeapSample): number;
212
228
  /** Whether it is worth paying a stop-the-world to get those pages back. */
213
229
  export declare function shouldReclaim(s: HeapSample, triggerMb?: number): boolean;
230
+ /**
231
+ * Which of the three MEASURED growth shapes dominates a process's footprint.
232
+ *
233
+ * These are not categories invented for a log line. They are what `smaps` found
234
+ * on the three live processes on 2026-08-27, and each one has a different lever:
235
+ *
236
+ * | process | evidence | shape |
237
+ * | --- | --- | --- |
238
+ * | `hub/recorder` | 1509.8MB in V8's 256KB page ranges, 53.9MB `brk`, `external` 8MB | `v8-heap` |
239
+ * | `hub/stream-broker` | 242.6MB V8 pages but **1159.1MB `brk`**, `arrayBuffers` 829MB | `external` |
240
+ * | `hub-main` | 1295.1MB V8 pages, 566.7MB `brk`, **2.7GB across 13 anonymous mappings >=64MB** | `native-residue` |
241
+ *
242
+ * - `v8-heap` — live JS retainers. `--max-old-space-size` is the bound, and the
243
+ * fix is the retained set (the recorder's ~7.1M `SegmentRow`s).
244
+ * - `external` — Buffers and native `external`. **No V8 flag bounds any of
245
+ * it**; `hub/pipeline-analytics` reached 5735MB with `--max-old-space-size=1024`
246
+ * in force the whole time.
247
+ * - `native-residue` — memory already freed that the allocator has not returned
248
+ * to the kernel. This is what the reclaim pass exists for, and the only shape
249
+ * where `MALLOC_ARENA_MAX` / `VIPS_CONCURRENCY` are the lever — both of which
250
+ * are ALREADY applied here, so a `native-residue` line means that lever is
251
+ * spent and the answer is upstream.
252
+ *
253
+ * Reporting only "it is big" leaves the operator to guess between three
254
+ * different fixes, two of which cannot work.
255
+ */
256
+ export type RssGrowthShape = 'v8-heap' | 'external' | 'native-residue';
257
+ /** Where a process's footprint stands against the budget its role declared. */
258
+ export type RssBudgetState = 'within' | 'over';
259
+ /** The footprint, split into the three parts that imply different fixes. */
260
+ export interface RssBreakdown {
261
+ readonly rssMb: number;
262
+ readonly budgetMb: number;
263
+ /** How far past the budget. 0 when within — "how far over" has no meaning below it. */
264
+ readonly overMb: number;
265
+ readonly heapUsedMb: number;
266
+ readonly externalMb: number;
267
+ /** `rss − heapUsed − external`: native the other two numbers do not name. */
268
+ readonly nativeResidueMb: number;
269
+ readonly arrayBuffersMb: number;
270
+ readonly shape: RssGrowthShape;
271
+ }
272
+ /**
273
+ * Split a sample into the three shapes and name the dominant one.
274
+ *
275
+ * `nativeResidue` is clamped at 0: `process.memoryUsage()` reads its fields at
276
+ * slightly different instants, so `heapUsed + external` can transiently exceed
277
+ * `rss` and a negative residue would misclassify the sample.
278
+ */
279
+ export declare function describeRss(s: HeapSample, budgetMb: number): RssBreakdown;
280
+ /**
281
+ * Release the alarm below this share of the budget.
282
+ *
283
+ * Hysteresis, not a second budget: every process here sawtooths (the reclaim
284
+ * pass alone moves hub-main by ~1.6GB), so a single threshold would print a
285
+ * pair of lines on every cycle. 0.9 of the budget is a real retreat.
286
+ */
287
+ export declare const RSS_BUDGET_RELEASE_RATIO = 0.9;
288
+ /**
289
+ * Floor on how often a crossing may be ANNOUNCED again.
290
+ *
291
+ * Hysteresis alone bounds flapping around the edge; it does not bound a process
292
+ * that genuinely swings across the budget every reclaim cycle
293
+ * ({@link HEAP_RECLAIM_MIN_INTERVAL_MS} is 2 minutes, so that is up to 30 pairs
294
+ * of lines an hour, forever). A process that oscillates across its budget is ONE
295
+ * story, and the operator needs it told once per quarter-hour, not thirty times.
296
+ *
297
+ * This is the discipline the rest of this module already follows and the one the
298
+ * log needed: on 2026-08-27 two real alarms were buried under 83,198 Hikvision
299
+ * heartbeat lines in 12h. A watchdog that shouts is a watchdog that gets muted.
300
+ */
301
+ export declare const RSS_BUDGET_REANNOUNCE_MIN_MS = 900000;
302
+ /**
303
+ * The alarm state machine, pure so the thresholds are testable without
304
+ * allocating five gigabytes to reach them.
305
+ */
306
+ export declare function nextRssBudgetState(current: RssBudgetState, rssMb: number, budgetMb: number, releaseRatio?: number): RssBudgetState;
214
307
  /** A full collection that also asks V8 to hand the pages back to the OS. */
215
308
  export type HeapReclaimer = () => void;
216
309
  /**
@@ -240,13 +333,71 @@ export interface HeapReclaimOptions {
240
333
  /** Ratio at which it releases. See {@link HEAP_WATCH_DEESCALATE_RATIO}. */
241
334
  readonly deescalateRatio?: number;
242
335
  }
336
+ /**
337
+ * The RSS budget for ONE process, and where the number came from.
338
+ *
339
+ * ## Why a DECLARED budget, and not a derived one
340
+ *
341
+ * Every bound this system had was derived, and each one is blind to most of a
342
+ * process: `--max-old-space-size` bounds old space only, and `strandedMb`
343
+ * subtracts `external` by construction. `hub/pipeline-analytics` was killed at
344
+ * 11.4GB with a 1024MB old-space ceiling in force and a stranded number that
345
+ * never moved. RSS is the quantity the host's OOM killer actually reads, so RSS
346
+ * is the quantity that needs a ceiling — and RSS cannot be derived from any flag,
347
+ * because most of it is not V8's.
348
+ *
349
+ * ## Why there is no single constant
350
+ *
351
+ * The legitimate footprint differs by an order of magnitude BY ROLE, measured
352
+ * on the same host on the same day:
353
+ *
354
+ * - `hub/recorder` legitimately holds ~1.5GB of live V8 index (~7.1M
355
+ * `SegmentRow`s, ~30 days x 13 cameras x 2 profiles) and declares a 3072MB
356
+ * old space to hold it;
357
+ * - `hub/stream-broker` legitimately holds ~900MB of `arrayBuffers` (47 RTP
358
+ * pre-roll rings) in the glibc heap, with an old space of only 224MB;
359
+ * - `hub/pipeline-analytics` has no legitimate multi-GB working set at all —
360
+ * it died at 11.4GB holding a 536MB heap.
361
+ *
362
+ * One constant would either wave through analytics' 11GB or scream at the
363
+ * recorder's entirely correct 2GB. So the number lives **next to the role that
364
+ * knows it**: an addon declares `execution.rssBudgetMb` in its own manifest,
365
+ * exactly as it already declares `execution.maxOldSpaceMb`, and the runner takes
366
+ * the MAXIMUM any co-located addon declares (D2/D29 — a runner is respawned as a
367
+ * whole, so co-location must never silence the hungriest member).
368
+ *
369
+ * hub-main has no manifest, and its entrypoint is the LEAST deployable surface
370
+ * in this system (the Dockerfile's own comment: a backend change is train-bound
371
+ * by construction, and an image build does not ship it). Its budget therefore
372
+ * lives in `@camstack/server`, which `applyServerUpdate` replaces the same day —
373
+ * see {@link HUB_MAIN_RSS_BUDGET_MB}.
374
+ *
375
+ * ## And a process with no declaration is UNWATCHED, out loud
376
+ *
377
+ * `budgetMb: undefined` does not select a default. Defaulting would be the same
378
+ * mistake in a new place: a number nobody measured, applied to a process nobody
379
+ * profiled, producing either silence or noise with equal authority. The process
380
+ * says once, at boot, that nothing is watching it. Most runners are in this
381
+ * state today and that is the honest report — measurements exist for four
382
+ * processes, so four processes carry a budget.
383
+ */
384
+ export interface RssBudgetOptions {
385
+ /** MB. `undefined` → this process is declared UNWATCHED and says so at boot. */
386
+ readonly budgetMb: number | undefined;
387
+ /** Named in every line, so a budget that is wrong is traceable to its author. */
388
+ readonly source: string;
389
+ /** See {@link RSS_BUDGET_RELEASE_RATIO}. */
390
+ readonly releaseRatio?: number;
391
+ /** See {@link RSS_BUDGET_REANNOUNCE_MIN_MS}. */
392
+ readonly reannounceMinMs?: number;
393
+ }
243
394
  /**
244
395
  * Start the heartbeat. Returns a stop function.
245
396
  *
246
397
  * `unref`ed so it can never hold the process open, and every read is wrapped:
247
398
  * a diagnostic that can crash the process it watches is worse than none.
248
399
  */
249
- export declare function startHeapWatch(label?: string, sink?: HeapWatchSink, intervalMs?: number, reclaimOptions?: HeapReclaimOptions, loopDelay?: LoopDelayMeter | undefined, execArgv?: readonly string[], announceCeilingOrigin?: boolean): () => void;
400
+ export declare function startHeapWatch(label?: string, sink?: HeapWatchSink, intervalMs?: number, reclaimOptions?: HeapReclaimOptions, loopDelay?: LoopDelayMeter | undefined, execArgv?: readonly string[], announceCeilingOrigin?: boolean, rssBudget?: RssBudgetOptions): () => void;
250
401
  /**
251
402
  * Steady heartbeat period for a HEAVY addon runner.
252
403
  *
@@ -284,7 +435,64 @@ export interface RunnerHeapWatchOptions {
284
435
  readonly intervalMs?: number;
285
436
  /** Test injection. Production omits it and gets {@link createV8Reclaimer}. */
286
437
  readonly reclaimOptions?: HeapReclaimOptions;
438
+ /**
439
+ * `CAMSTACK_RUNNER_RSS_BUDGET_MB` as the parent passed it — the MAXIMUM
440
+ * `execution.rssBudgetMb` any co-located addon declares, resolved from the
441
+ * manifests in `process-service.ts`. Absent means no co-located addon declared
442
+ * one, and the runner reports itself UNWATCHED. See {@link RssBudgetOptions}.
443
+ */
444
+ readonly rssBudgetMb?: string;
287
445
  }
446
+ /**
447
+ * Parse a budget carried across a process boundary as an env string.
448
+ *
449
+ * Anything that is not a positive finite integer is `undefined` — i.e. the
450
+ * process is UNWATCHED and says so. A malformed budget must never round to a
451
+ * silent default: that is the failure this whole mechanism exists to remove.
452
+ */
453
+ export declare function parseRssBudgetMb(raw: string | undefined): number | undefined;
454
+ /** Env var carrying a runner's declared RSS budget from parent to child. */
455
+ export declare const RUNNER_RSS_BUDGET_ENV = "CAMSTACK_RUNNER_RSS_BUDGET_MB";
456
+ /** Operator override for hub-main's budget, applied without a release. */
457
+ export declare const HUB_RSS_BUDGET_ENV = "CAMSTACK_HUB_RSS_BUDGET_MB";
458
+ /**
459
+ * hub-main's declared RSS budget, in MB.
460
+ *
461
+ * ## Why the number lives HERE and not in the image
462
+ *
463
+ * hub-main has no manifest to declare it in, and the obvious alternative — the
464
+ * entrypoint that already knew about `CAMSTACK_HUB_HEAP_MB` — is the least
465
+ * deployable surface in this system. The Dockerfile says so itself: the image
466
+ * never contains this repo's backend, so a value written there reaches a running
467
+ * hub only on an image rebuild. `main.ts` is what `applyServerUpdate` replaces,
468
+ * which is exactly why the heartbeat was started from there in the first place.
469
+ * A budget that cannot ship is not a budget.
470
+ *
471
+ * ## Why 4096
472
+ *
473
+ * hub-main's entitlement is its old-space ceiling plus its measured native
474
+ * floor. The floor is 438MB, established live across five consecutive
475
+ * `gc(last-resort)` passes that landed within 4MB of each other
476
+ * (4559/4561/4560/4562/4563MB) — sqlite pages, libvips, sharp, thread stacks and
477
+ * node's own text. Against the 3072MB ceiling in force on 2026-08-27 that is
478
+ * ~3.5GB of legitimate footprint, and 4096 leaves it ~600MB of slack.
479
+ *
480
+ * **This WILL fire on the hub as measured on 2026-08-27, and that is the point.**
481
+ * That process sat at 4.87GiB with `external` at 360MB: ~2.8GB of it was native
482
+ * memory already freed and never returned, spread across 13 anonymous mappings
483
+ * >=64MB, with only 1295MB in V8 pages. `MALLOC_ARENA_MAX=2` and
484
+ * `VIPS_CONCURRENCY=1` were already applied — verified from `/proc/<pid>/environ`
485
+ * — so that lever is spent and the residue stands. The alarm reporting a
486
+ * genuinely over-budget process is the mechanism working; a number picked to sit
487
+ * just above today's RSS would be a rubber stamp.
488
+ *
489
+ * An operator who decides that residue is the cost of doing business moves it in
490
+ * one place with {@link HUB_RSS_BUDGET_ENV}, and the decision is on the record
491
+ * instead of being absorbed by a threshold nobody set.
492
+ */
493
+ export declare const HUB_MAIN_RSS_BUDGET_MB = 4096;
494
+ /** hub-main's budget: the declared number, or the operator's override. */
495
+ export declare function hubMainRssBudget(env?: NodeJS.ProcessEnv): RssBudgetOptions;
288
496
  /**
289
497
  * Start the heap watch + stranded-page reclaimer for ONE addon runner, iff it
290
498
  * is a heavy one. Returns the stop function, or undefined when nothing started.
@@ -0,0 +1,46 @@
1
+ import { IScopedLogger } from '@camstack/types';
2
+ /**
3
+ * The child fields the accessory reconcile reads. Deliberately narrower than
4
+ * `DeviceInfo`: the loader is a transport, and a transport that pins the whole
5
+ * projection would have to change every time an unrelated field does.
6
+ */
7
+ export interface ReconcileChildRow {
8
+ readonly id: number;
9
+ readonly stableId: string;
10
+ readonly name: string;
11
+ readonly linkDeviceId?: number | null;
12
+ }
13
+ /**
14
+ * Narrow an untyped cap answer to the fields the reconcile reads.
15
+ *
16
+ * `callDeviceManager` is an untyped boundary, and the answer may come from a
17
+ * hub of a different vintage. A row that does not carry an id, a stableId and
18
+ * a name is not a child this code can prune ON, so it is dropped rather than
19
+ * cast into shape — the prune step DELETES, and a half-parsed row is the input
20
+ * that would make it delete the wrong thing.
21
+ */
22
+ export declare function parseChildRows(value: unknown): readonly ReconcileChildRow[] | null;
23
+ /**
24
+ * Narrow a `getChildrenBatch` answer: a record keyed by parent id as a string.
25
+ *
26
+ * An EMPTY record is a valid answer — "none of these parents has children" —
27
+ * and so is what a hub that answers the call but knows none of the parents
28
+ * would send. Both mean "nothing to prune", which is the safe direction. Only
29
+ * a shape that is not a record at all is `null`, and the loader treats that as
30
+ * a failed batch and re-asks per parent.
31
+ */
32
+ export declare function parseChildrenByParent(value: unknown): ReadonlyMap<number, readonly ReconcileChildRow[]> | null;
33
+ export interface ChildrenBatchLoaderOptions {
34
+ /** One `device-manager.getChildrenBatch` call. Rejects like any RPC. */
35
+ readonly fetchBatch: (parentDeviceIds: readonly number[]) => Promise<ReadonlyMap<number, readonly ReconcileChildRow[]>>;
36
+ /** One `device-manager.getChildren` call — the fallback, and the old path. */
37
+ readonly fetchOne: (parentDeviceId: number) => Promise<readonly ReconcileChildRow[]>;
38
+ readonly logger: IScopedLogger;
39
+ /** Test seam. Production takes {@link DEVICE_CHILDREN_BATCH_MAX}. */
40
+ readonly maxBatch?: number;
41
+ }
42
+ export interface ChildrenBatchLoader {
43
+ /** The direct children of one parent, coalesced with every other in-flight ask. */
44
+ load(parentDeviceId: number): Promise<readonly ReconcileChildRow[]>;
45
+ }
46
+ export declare function createChildrenBatchLoader(opts: ChildrenBatchLoaderOptions): ChildrenBatchLoader;