@camstack/system 1.2.59 → 1.2.61

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 (55) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  9. package/dist/builtins/console-logging/index.js +1 -1
  10. package/dist/builtins/console-logging/index.mjs +1 -1
  11. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  12. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  13. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  14. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  15. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  16. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  17. package/dist/builtins/hub-forwarder/index.js +1 -1
  18. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  19. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  20. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  21. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  22. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  23. package/dist/builtins/local-network/local-network.addon.js +1 -1
  24. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  25. package/dist/builtins/loki-logging/index.js +1 -1
  26. package/dist/builtins/loki-logging/index.mjs +1 -1
  27. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  28. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  29. package/dist/builtins/platform-probe/index.js +1 -1
  30. package/dist/builtins/platform-probe/index.mjs +1 -1
  31. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  32. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  33. package/dist/builtins/snapshot/index.js +1 -1
  34. package/dist/builtins/snapshot/index.mjs +1 -1
  35. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  36. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  37. package/dist/builtins/sqlite-storage/prefix-range.d.ts +50 -0
  38. package/dist/builtins/sqlite-storage/query-bounds.d.ts +116 -0
  39. package/dist/builtins/sqlite-storage/sqlite-pragmas.d.ts +12 -0
  40. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +38 -2
  41. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +281 -19
  42. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +281 -19
  43. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  44. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  45. package/dist/builtins/system-config/system-config.addon.js +1 -1
  46. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  47. package/dist/builtins/winston-logging/index.js +1 -1
  48. package/dist/builtins/winston-logging/index.mjs +1 -1
  49. package/dist/{dist-CV6XTApk.js → dist-BN681PgO.js} +974 -18
  50. package/dist/{dist-BqJJWCS8.mjs → dist-L6QPJWsr.mjs} +974 -18
  51. package/dist/index.d.ts +2 -2
  52. package/dist/index.js +131 -9
  53. package/dist/index.mjs +126 -9
  54. package/dist/kernel/heap-watch.d.ts +48 -1
  55. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { startHeapWatch, buildHeapSample, HEAP_WATCH_INTERVAL_MS, HEAP_WATCH_WARN_RATIO, } from './kernel/heap-watch.js';
2
- export type { HeapSample, HeapWatchSink } from './kernel/heap-watch.js';
1
+ export { startHeapWatch, buildHeapSample, createV8Reclaimer, shouldReclaim, strandedMb, HEAP_WATCH_INTERVAL_MS, HEAP_WATCH_WARN_RATIO, HEAP_RECLAIM_TRIGGER_MB, HEAP_RECLAIM_MIN_INTERVAL_MS, } from './kernel/heap-watch.js';
2
+ export type { HeapSample, HeapWatchSink, HeapReclaimer, HeapReclaimOptions, } from './kernel/heap-watch.js';
3
3
  export { ensureBinary, downloadBinary, findInPath, getPlatformInfo, buildBinaryPath, ensureFfmpeg, getFfmpegDownloadUrl, ensurePython, installPythonPackages, installPythonRequirements, getPythonDownloadUrl, PYTHON_VERSION, } from '@camstack/types/node';
4
4
  export type { PlatformInfo } from '@camstack/types/node';
5
5
  export { ALL_CAPABILITY_DEFINITIONS, METHOD_ACCESS_MAP, expandCapMethods, isArrayOutputSchema, isCollectionArrayMethod, addonSettingsCapability, DEVICE_SETTINGS_CONTRIBUTION_METHODS, DEVICE_STATUS_METHOD, } from '@camstack/types';
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-CV6XTApk.js");
3
+ const require_dist = require("./dist-BN681PgO.js");
4
4
  const require_model_download_service = require("./model-download-service-Cpc6SWzV.js");
5
5
  const require_manifest_python_deps = require("./manifest-python-deps-d4acYXVV.js");
6
6
  const require_resource_monitor = require("./resource-monitor-DNNomR-i.js");
@@ -30,6 +30,8 @@ require("./builtins/device-manager/index.js");
30
30
  const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
31
31
  let node_v8 = require("node:v8");
32
32
  node_v8 = require_chunk.__toESM(node_v8);
33
+ let node_vm = require("node:vm");
34
+ node_vm = require_chunk.__toESM(node_vm);
33
35
  let _camstack_types_node = require("@camstack/types/node");
34
36
  let zod = require("zod");
35
37
  let node_crypto = require("node:crypto");
@@ -43,8 +45,6 @@ let _camstack_types_addon = require("@camstack/types/addon");
43
45
  let node_child_process = require("node:child_process");
44
46
  let node_util = require("node:util");
45
47
  node_util = require_chunk.__toESM(node_util);
46
- let node_vm = require("node:vm");
47
- node_vm = require_chunk.__toESM(node_vm);
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");
@@ -60,13 +60,15 @@ path = require_chunk.__toESM(path, 1);
60
60
  let module$1 = require("module");
61
61
  //#region src/kernel/heap-watch.ts
62
62
  /**
63
- * heap-watch — hub-main's own memory, on the record.
63
+ * heap-watch — hub-main's own memory, on the record, and returned to the OS.
64
64
  *
65
65
  * ## Why this exists
66
66
  *
67
- * hub-main is launched with `--max-old-space-size=1536` (docker-entrypoint) and
68
- * has logged NOTHING about its own heap. When it died on 2026-08-01/02 the only
69
- * trace was V8's own stderr line:
67
+ * hub-main used to be launched with `--max-old-space-size=1536` (docker-entrypoint;
68
+ * the cap was removed on 2026-08-02, so the live ceiling is now V8's own default —
69
+ * 4288MB in this container and this module reads it at runtime rather than
70
+ * assuming a number). It logged NOTHING about its own heap. When it died on
71
+ * 2026-08-01/02 the only trace was V8's own stderr line:
70
72
  *
71
73
  * FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
72
74
  *
@@ -94,6 +96,41 @@ let module$1 = require("module");
94
96
  * `--max-old-space-size` and are most of the gap between a 1.5GB cap and the
95
97
  * ~2.05GB RSS observed live: a diagnosis that only looks at the JS heap
96
98
  * misreads that process by 500MB.
99
+ *
100
+ * ## And then the heartbeat explained the "off-heap leak", which was not one
101
+ *
102
+ * 2026-08-08, measured on the live hub over 1019 consecutive samples and seven
103
+ * separate boots: **hub-main's RSS is a HIGH-WATER MARK, not a leak.** In every
104
+ * one of those 1019 samples RSS was >= the running maximum of
105
+ * `heapTotal + external`, never once below it, and
106
+ * `RSS_peak - peak(heapTotal + external)` landed between 874MB and 1639MB on
107
+ * every boot — a roughly constant native floor on top of a number that only
108
+ * ever goes up. RSS tracked the highest point the process ever reached and
109
+ * stayed there: 3.3GB -> 4.5GB -> 5.2GB, and 7.8GB on the worst boot.
110
+ *
111
+ * Two things push that peak, and neither is a retained object:
112
+ *
113
+ * - the JS heap sawtooths to 2.0-2.8GB because the ceiling is 4288MB and
114
+ * nothing creates pressure before then;
115
+ * - `arrayBuffers` accumulates DEAD Buffers — up to 3.65GB — because V8 only
116
+ * sweeps them on a major GC. Eleven collapses were captured, and every
117
+ * single one coincides with `heapUsed` dropping to its ~650MB floor. No code
118
+ * released a reference at 13:23:03; a mark-compact ran.
119
+ *
120
+ * At the peak V8 has committed the pages, and **an ordinary major GC does not
121
+ * give them back.** Proven on the production hub: `smaps` showed 10,819 mapped
122
+ * 256KB pages (V8's `kRegularPageSize`) holding 2704MB while V8 itself reported
123
+ * `total_physical_size` of only 846MB — ~7,800 pages committed, resident, and
124
+ * outside V8's own accounting. One `gc({ flavor: 'last-resort' })` took 417ms
125
+ * and returned RSS 3355MB -> 1237MB, pages 10,819 -> 2,963.
126
+ *
127
+ * So the fix for the "~3GB/2h off-heap growth" is not a retainer hunt. It is to
128
+ * ask V8 for the pages back when the process is visibly holding more than it is
129
+ * using — which is what `shouldReclaim` decides and what the heartbeat now does.
130
+ *
131
+ * The cost is a 250-450ms stop-the-world on the hub's router process, bounded to
132
+ * once per `HEAP_RECLAIM_MIN_INTERVAL_MS`. The alternative, measured, is a
133
+ * ratchet to 7.8GB and the OOM that ends it.
97
134
  */
98
135
  /** Heartbeat period. Slow enough to be free, fast enough that the sample before
99
136
  * a fatal is recent. */
@@ -101,6 +138,20 @@ var HEAP_WATCH_INTERVAL_MS = 6e4;
101
138
  /** Warn above this share of the heap ceiling. Chosen so the warning precedes
102
139
  * the fatal by more than one interval under realistic growth. */
103
140
  var HEAP_WATCH_WARN_RATIO = .8;
141
+ /**
142
+ * Reclaim once the process holds this many MB beyond what it is USING.
143
+ *
144
+ * Live calibration, both readings from the same hub minutes apart: 2491MB
145
+ * stranded before the reclaim, 438MB after. 438MB is this process's irreducible
146
+ * native floor (sqlite pages, libvips, sharp, thread stacks, node's own text) —
147
+ * collecting against it would buy nothing and cost a stall every minute. 1536
148
+ * sits clear of that floor and still fires ~1.1GB before the ratchet is worth
149
+ * anything, so it triggers on the real condition and stops as soon as it is fixed.
150
+ */
151
+ var HEAP_RECLAIM_TRIGGER_MB = 1536;
152
+ /** Floor on how often the stop-the-world may run. 250-450ms measured, so ten
153
+ * minutes keeps the worst case under 0.1% of the router's time. */
154
+ var HEAP_RECLAIM_MIN_INTERVAL_MS = 6e5;
104
155
  var MB = (bytes) => Math.round(bytes / 1048576);
105
156
  /**
106
157
  * Build a sample from raw readings. Pure so the thresholds are testable without
@@ -123,6 +174,47 @@ var consoleSink = {
123
174
  info: (line) => console.log(line),
124
175
  warn: (line) => console.warn(line)
125
176
  };
177
+ /**
178
+ * RSS the process is holding but not using.
179
+ *
180
+ * The subtrahend is heapUSED, not heapTotal, deliberately: the pages V8
181
+ * committed at a sawtooth peak and no longer needs are counted inside heapTotal,
182
+ * and they are precisely what this number exists to see.
183
+ */
184
+ function strandedMb(s) {
185
+ return s.rssMb - s.heapUsedMb - s.externalMb;
186
+ }
187
+ /** Whether it is worth paying a stop-the-world to get those pages back. */
188
+ function shouldReclaim(s, triggerMb = HEAP_RECLAIM_TRIGGER_MB) {
189
+ return strandedMb(s) > triggerMb;
190
+ }
191
+ function isGcFunction(value) {
192
+ return typeof value === "function";
193
+ }
194
+ /**
195
+ * Build the reclaimer without requiring `--expose-gc` on argv.
196
+ *
197
+ * The flag is turned on, the function captured, and the flag turned straight
198
+ * back off — a global `gc` left reachable by application code is a footgun, and
199
+ * requiring an argv change would mean this cannot ship without a new image.
200
+ * Verified against the live hub before being written: 266ms, RSS 1453 -> 1237.
201
+ *
202
+ * Returns undefined if V8 declines, so the caller keeps a heartbeat either way.
203
+ */
204
+ function createV8Reclaimer() {
205
+ try {
206
+ node_v8.setFlagsFromString("--expose-gc");
207
+ const gc = node_vm.runInNewContext("gc");
208
+ node_v8.setFlagsFromString("--no-expose-gc");
209
+ if (!isGcFunction(gc)) return void 0;
210
+ return () => gc({
211
+ execution: "sync",
212
+ flavor: "last-resort"
213
+ });
214
+ } catch {
215
+ return;
216
+ }
217
+ }
126
218
  function format$1(label, s) {
127
219
  return `[mem] ${label} rss=${s.rssMb}MB heapUsed=${s.heapUsedMb}MB heapTotal=${s.heapTotalMb}MB heapLimit=${s.heapLimitMb}MB used=${Math.round(s.usedRatio * 100)}% external=${s.externalMb}MB arrayBuffers=${s.arrayBuffersMb}MB`;
128
220
  }
@@ -132,13 +224,38 @@ function format$1(label, s) {
132
224
  * `unref`ed so it can never hold the process open, and every read is wrapped:
133
225
  * a diagnostic that can crash the process it watches is worse than none.
134
226
  */
135
- function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEAP_WATCH_INTERVAL_MS) {
227
+ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEAP_WATCH_INTERVAL_MS, reclaimOptions) {
228
+ const readMemory = reclaimOptions?.readMemory ?? (() => process.memoryUsage());
229
+ const now = reclaimOptions?.now ?? (() => Date.now());
230
+ const triggerMb = reclaimOptions?.triggerMb ?? 1536;
231
+ const minIntervalMs = reclaimOptions?.minIntervalMs ?? 6e5;
232
+ let lastReclaimAt = Number.NEGATIVE_INFINITY;
233
+ const read = () => {
234
+ const limit = reclaimOptions?.heapLimitBytes ?? node_v8.getHeapStatistics().heap_size_limit;
235
+ return buildHeapSample(readMemory(), limit);
236
+ };
237
+ const maybeReclaim = (sample) => {
238
+ if (reclaimOptions === void 0) return;
239
+ if (!shouldReclaim(sample, triggerMb)) return;
240
+ if (now() - lastReclaimAt < minIntervalMs) return;
241
+ lastReclaimAt = now();
242
+ const startedAt = now();
243
+ try {
244
+ reclaimOptions.reclaim();
245
+ } catch (error) {
246
+ sink.warn(`[mem] reclaim failed — ${error instanceof Error ? error.message : String(error)}`);
247
+ return;
248
+ }
249
+ const after = read();
250
+ 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
+ };
136
252
  const tick = () => {
137
253
  try {
138
- const sample = buildHeapSample(process.memoryUsage(), node_v8.getHeapStatistics().heap_size_limit);
254
+ const sample = read();
139
255
  const line = format$1(label, sample);
140
256
  if (sample.nearLimit) sink.warn(`${line} — APPROACHING HEAP LIMIT`);
141
257
  else sink.info(line);
258
+ maybeReclaim(sample);
142
259
  } catch {}
143
260
  };
144
261
  const timer = setInterval(tick, intervalMs);
@@ -94566,6 +94683,8 @@ exports.FsStorageBackend = FsStorageBackend;
94566
94683
  exports.HEALTH_MONITOR_GRACE_PERIOD_MS = HEALTH_MONITOR_GRACE_PERIOD_MS;
94567
94684
  exports.HEALTH_MONITOR_RETRY_INTERVALS_MS = HEALTH_MONITOR_RETRY_INTERVALS_MS;
94568
94685
  exports.HEALTH_MONITOR_TICK_MS = HEALTH_MONITOR_TICK_MS;
94686
+ exports.HEAP_RECLAIM_MIN_INTERVAL_MS = HEAP_RECLAIM_MIN_INTERVAL_MS;
94687
+ exports.HEAP_RECLAIM_TRIGGER_MB = HEAP_RECLAIM_TRIGGER_MB;
94569
94688
  exports.HEAP_WATCH_INTERVAL_MS = HEAP_WATCH_INTERVAL_MS;
94570
94689
  exports.HEAP_WATCH_WARN_RATIO = HEAP_WATCH_WARN_RATIO;
94571
94690
  exports.HUB_CAP_FWD_ACTION = require_manifest_python_deps.HUB_CAP_FWD_ACTION;
@@ -94687,6 +94806,7 @@ exports.createUdsEventBridge = require_manifest_python_deps.createUdsEventBridge
94687
94806
  exports.createUdsEventBus = require_manifest_python_deps.createUdsEventBus;
94688
94807
  exports.createUdsLogger = require_manifest_python_deps.createUdsLogger;
94689
94808
  exports.createUdsLoggerWithControl = require_manifest_python_deps.createUdsLoggerWithControl;
94809
+ exports.createV8Reclaimer = createV8Reclaimer;
94690
94810
  exports.deleteModelFromDisk = require_model_download_service.deleteModelFromDisk;
94691
94811
  exports.deriveAgentListenPort = deriveAgentListenPort;
94692
94812
  exports.describeProviderKindDrift = describeProviderKindDrift;
@@ -94809,7 +94929,9 @@ exports.scopesAllowDeviceCap = scopesAllowDeviceCap;
94809
94929
  exports.serializeTypedArrays = require_manifest_python_deps.serializeTypedArrays;
94810
94930
  exports.setHubConnected = require_manifest_python_deps.setHubConnected;
94811
94931
  exports.setNodeEventInterest = require_manifest_python_deps.setNodeEventInterest;
94932
+ exports.shouldReclaim = shouldReclaim;
94812
94933
  exports.startHeapWatch = startHeapWatch;
94934
+ exports.strandedMb = strandedMb;
94813
94935
  exports.stripCamstackDeps = stripCamstackDeps;
94814
94936
  exports.subscribePassthrough = require_manifest_python_deps.subscribePassthrough;
94815
94937
  exports.udsChildLogToWorkerEntry = require_manifest_python_deps.udsChildLogToWorkerEntry;
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 procedureAuthKey, Ct as asJsonObject$1, Dt as emitDownForOwnedCaps, Et as createEvent, Ft as readinessKey, H as kebabToCamel, It as resolveCapMount, K as logLevelAtMost, L as isArrayOutputSchema, Lt as scopeKey, Nt as parseJsonObject, P as extractNestedAddonId, Pt as parseJsonUnknown$1, R as isCollectionArrayMethod, Tt as asString$1, U as lifecycleJobSchema, V as isVoidInput, X as objectInputDeclaresAddonId, bt as ReadinessRegistry, c as METHOD_ACCESS_MAP, ft as errMsg$1, gt as DEVICE_STATUS_METHOD, h as addonSettingsCapability, ht as DEVICE_SETTINGS_CONTRIBUTION_METHODS, kt as expandCapMethods, l as RUNTIME_DEFAULTS, mt as DATAPLANE_SECRET_HEADER$1, q as looseSchema, t as ALL_CAPABILITY_DEFINITIONS, wt as asNumber, xt as ReadinessTimeoutError, z as isObjectInput, zt as EventCategory$1 } from "./dist-BqJJWCS8.mjs";
2
+ import { $ as procedureAuthKey, Ct as asJsonObject$1, Dt as emitDownForOwnedCaps, Et as createEvent, Ft as readinessKey, H as kebabToCamel, It as resolveCapMount, K as logLevelAtMost, L as isArrayOutputSchema, Lt as scopeKey, Nt as parseJsonObject, P as extractNestedAddonId, Pt as parseJsonUnknown$1, R as isCollectionArrayMethod, Tt as asString$1, U as lifecycleJobSchema, V as isVoidInput, X as objectInputDeclaresAddonId, bt as ReadinessRegistry, c as METHOD_ACCESS_MAP, ft as errMsg$1, gt as DEVICE_STATUS_METHOD, h as addonSettingsCapability, ht as DEVICE_SETTINGS_CONTRIBUTION_METHODS, kt as expandCapMethods, l as RUNTIME_DEFAULTS, mt as DATAPLANE_SECRET_HEADER$1, q as looseSchema, t as ALL_CAPABILITY_DEFINITIONS, wt as asNumber, xt as ReadinessTimeoutError, z as isObjectInput, zt as EventCategory$1 } from "./dist-L6QPJWsr.mjs";
3
3
  import { a as downloadModel, c as getModelFilePath, d as contentTypeFor, f as createAuthenticatedFileServer, h as resolveFilePath, i as downloadFile, l as isModelDownloaded, m as parseTokenizedUrl, n as collectModelFiles, o as ensureModel, p as parseRangeHeader, r as deleteModelFromDisk, s as fetchJson, t as ModelDownloadService, u as createFileDataPlaneHandler } from "./model-download-service-D8B-4ktF.mjs";
4
4
  import { $ as buildNativeCapProxy, A as createHubCapForwardService, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as resolveAddonClass, D as localProviderLink, E as ipcParentLink, F as createUdsLogger, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, N as createUdsEventBus, O as HUB_CAP_FWD_ACTION, P as udsChildLogToWorkerEntry, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as runNpm, T as ipcChildLink, U as CapRouteError, V as AGENT_CAP_FWD_SERVICE, W as classifyCapRoute, X as localEndpointPath, Y as SocketChannel, Z as FrameDecoder, _ as createKernelHwAccel, _t as CapabilityHandle, a as getWorkerDeviceRegistry, b as __resetCapUsageRegistryForTests, bt as installManifestNativeDeps, c as setHubConnected, ct as NATIVE_PROVIDER_SERVICE_INFIX, d as getBrokerEventBus, dt as capBareAction, et as buildUdsNativeCapProxy, f as getMoleculerEventStats, ft as capServiceName, g as AddonDepsManager, gt as DeviceRegistry, h as subscribePassthrough, ht as serializeTypedArrays, i as createUdsAddonContext, it as mountNativeCapService, j as createParentUnownedCallHandler, k as HUB_CAP_FWD_SERVICE, l as EVENT_TOPIC_PREFIX, lt as capActionName, m as setNodeEventInterest, mt as deserializeTypedArrays, n as adaptBrokerToCluster, o as getOrInitReadinessRegistry, ot as createAddonService, p as registerEventBusService, pt as parseCapAction, q as UdsLocalTransportClient, r as createAddonContext, s as getOrInitReadinessRegistryForClient, st as validateProviderRegistrations, t as installManifestPythonDeps, tt as createBrokerDeviceManagerApi, u as clusterEventTopic, ut as capActionSuffix, v as resolveHwAccel, vt as CapabilityUnavailableError, w as buildLinkChain, wt as createAddonDataPlaneFacility, x as getCapUsageRegistry, xt as resolveNpmInvocation, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-CX5tGRqf.mjs";
5
5
  import { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-BkP504Vq.mjs";
@@ -28,6 +28,7 @@ import { DeviceManagerAddon } from "./builtins/device-manager/device-manager.add
28
28
  import "./builtins/device-manager/index.mjs";
29
29
  import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
30
30
  import * as v8 from "node:v8";
31
+ import * as vm from "node:vm";
31
32
  import { PYTHON_VERSION, buildBinaryPath, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, installPythonPackages, installPythonRequirements } from "@camstack/types/node";
32
33
  import { z } from "zod";
33
34
  import { X509Certificate, createHash, randomUUID, timingSafeEqual } from "node:crypto";
@@ -40,7 +41,6 @@ import { asJsonObject, errMsg, parseJsonUnknown } from "@camstack/types/addon";
40
41
  import { execFile, spawn } from "node:child_process";
41
42
  import * as util$10 from "node:util";
42
43
  import { promisify } from "node:util";
43
- import * as vm from "node:vm";
44
44
  import * as os$18 from "node:os";
45
45
  import { lstat, readdir, readlink, realpath } from "node:fs/promises";
46
46
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -52,13 +52,15 @@ import "path";
52
52
  import { createRequire } from "module";
53
53
  //#region src/kernel/heap-watch.ts
54
54
  /**
55
- * heap-watch — hub-main's own memory, on the record.
55
+ * heap-watch — hub-main's own memory, on the record, and returned to the OS.
56
56
  *
57
57
  * ## Why this exists
58
58
  *
59
- * hub-main is launched with `--max-old-space-size=1536` (docker-entrypoint) and
60
- * has logged NOTHING about its own heap. When it died on 2026-08-01/02 the only
61
- * trace was V8's own stderr line:
59
+ * hub-main used to be launched with `--max-old-space-size=1536` (docker-entrypoint;
60
+ * the cap was removed on 2026-08-02, so the live ceiling is now V8's own default —
61
+ * 4288MB in this container and this module reads it at runtime rather than
62
+ * assuming a number). It logged NOTHING about its own heap. When it died on
63
+ * 2026-08-01/02 the only trace was V8's own stderr line:
62
64
  *
63
65
  * FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
64
66
  *
@@ -86,6 +88,41 @@ import { createRequire } from "module";
86
88
  * `--max-old-space-size` and are most of the gap between a 1.5GB cap and the
87
89
  * ~2.05GB RSS observed live: a diagnosis that only looks at the JS heap
88
90
  * misreads that process by 500MB.
91
+ *
92
+ * ## And then the heartbeat explained the "off-heap leak", which was not one
93
+ *
94
+ * 2026-08-08, measured on the live hub over 1019 consecutive samples and seven
95
+ * separate boots: **hub-main's RSS is a HIGH-WATER MARK, not a leak.** In every
96
+ * one of those 1019 samples RSS was >= the running maximum of
97
+ * `heapTotal + external`, never once below it, and
98
+ * `RSS_peak - peak(heapTotal + external)` landed between 874MB and 1639MB on
99
+ * every boot — a roughly constant native floor on top of a number that only
100
+ * ever goes up. RSS tracked the highest point the process ever reached and
101
+ * stayed there: 3.3GB -> 4.5GB -> 5.2GB, and 7.8GB on the worst boot.
102
+ *
103
+ * Two things push that peak, and neither is a retained object:
104
+ *
105
+ * - the JS heap sawtooths to 2.0-2.8GB because the ceiling is 4288MB and
106
+ * nothing creates pressure before then;
107
+ * - `arrayBuffers` accumulates DEAD Buffers — up to 3.65GB — because V8 only
108
+ * sweeps them on a major GC. Eleven collapses were captured, and every
109
+ * single one coincides with `heapUsed` dropping to its ~650MB floor. No code
110
+ * released a reference at 13:23:03; a mark-compact ran.
111
+ *
112
+ * At the peak V8 has committed the pages, and **an ordinary major GC does not
113
+ * give them back.** Proven on the production hub: `smaps` showed 10,819 mapped
114
+ * 256KB pages (V8's `kRegularPageSize`) holding 2704MB while V8 itself reported
115
+ * `total_physical_size` of only 846MB — ~7,800 pages committed, resident, and
116
+ * outside V8's own accounting. One `gc({ flavor: 'last-resort' })` took 417ms
117
+ * and returned RSS 3355MB -> 1237MB, pages 10,819 -> 2,963.
118
+ *
119
+ * So the fix for the "~3GB/2h off-heap growth" is not a retainer hunt. It is to
120
+ * ask V8 for the pages back when the process is visibly holding more than it is
121
+ * using — which is what `shouldReclaim` decides and what the heartbeat now does.
122
+ *
123
+ * The cost is a 250-450ms stop-the-world on the hub's router process, bounded to
124
+ * once per `HEAP_RECLAIM_MIN_INTERVAL_MS`. The alternative, measured, is a
125
+ * ratchet to 7.8GB and the OOM that ends it.
89
126
  */
90
127
  /** Heartbeat period. Slow enough to be free, fast enough that the sample before
91
128
  * a fatal is recent. */
@@ -93,6 +130,20 @@ var HEAP_WATCH_INTERVAL_MS = 6e4;
93
130
  /** Warn above this share of the heap ceiling. Chosen so the warning precedes
94
131
  * the fatal by more than one interval under realistic growth. */
95
132
  var HEAP_WATCH_WARN_RATIO = .8;
133
+ /**
134
+ * Reclaim once the process holds this many MB beyond what it is USING.
135
+ *
136
+ * Live calibration, both readings from the same hub minutes apart: 2491MB
137
+ * stranded before the reclaim, 438MB after. 438MB is this process's irreducible
138
+ * native floor (sqlite pages, libvips, sharp, thread stacks, node's own text) —
139
+ * collecting against it would buy nothing and cost a stall every minute. 1536
140
+ * sits clear of that floor and still fires ~1.1GB before the ratchet is worth
141
+ * anything, so it triggers on the real condition and stops as soon as it is fixed.
142
+ */
143
+ var HEAP_RECLAIM_TRIGGER_MB = 1536;
144
+ /** Floor on how often the stop-the-world may run. 250-450ms measured, so ten
145
+ * minutes keeps the worst case under 0.1% of the router's time. */
146
+ var HEAP_RECLAIM_MIN_INTERVAL_MS = 6e5;
96
147
  var MB = (bytes) => Math.round(bytes / 1048576);
97
148
  /**
98
149
  * Build a sample from raw readings. Pure so the thresholds are testable without
@@ -115,6 +166,47 @@ var consoleSink = {
115
166
  info: (line) => console.log(line),
116
167
  warn: (line) => console.warn(line)
117
168
  };
169
+ /**
170
+ * RSS the process is holding but not using.
171
+ *
172
+ * The subtrahend is heapUSED, not heapTotal, deliberately: the pages V8
173
+ * committed at a sawtooth peak and no longer needs are counted inside heapTotal,
174
+ * and they are precisely what this number exists to see.
175
+ */
176
+ function strandedMb(s) {
177
+ return s.rssMb - s.heapUsedMb - s.externalMb;
178
+ }
179
+ /** Whether it is worth paying a stop-the-world to get those pages back. */
180
+ function shouldReclaim(s, triggerMb = HEAP_RECLAIM_TRIGGER_MB) {
181
+ return strandedMb(s) > triggerMb;
182
+ }
183
+ function isGcFunction(value) {
184
+ return typeof value === "function";
185
+ }
186
+ /**
187
+ * Build the reclaimer without requiring `--expose-gc` on argv.
188
+ *
189
+ * The flag is turned on, the function captured, and the flag turned straight
190
+ * back off — a global `gc` left reachable by application code is a footgun, and
191
+ * requiring an argv change would mean this cannot ship without a new image.
192
+ * Verified against the live hub before being written: 266ms, RSS 1453 -> 1237.
193
+ *
194
+ * Returns undefined if V8 declines, so the caller keeps a heartbeat either way.
195
+ */
196
+ function createV8Reclaimer() {
197
+ try {
198
+ v8.setFlagsFromString("--expose-gc");
199
+ const gc = vm.runInNewContext("gc");
200
+ v8.setFlagsFromString("--no-expose-gc");
201
+ if (!isGcFunction(gc)) return void 0;
202
+ return () => gc({
203
+ execution: "sync",
204
+ flavor: "last-resort"
205
+ });
206
+ } catch {
207
+ return;
208
+ }
209
+ }
118
210
  function format$1(label, s) {
119
211
  return `[mem] ${label} rss=${s.rssMb}MB heapUsed=${s.heapUsedMb}MB heapTotal=${s.heapTotalMb}MB heapLimit=${s.heapLimitMb}MB used=${Math.round(s.usedRatio * 100)}% external=${s.externalMb}MB arrayBuffers=${s.arrayBuffersMb}MB`;
120
212
  }
@@ -124,13 +216,38 @@ function format$1(label, s) {
124
216
  * `unref`ed so it can never hold the process open, and every read is wrapped:
125
217
  * a diagnostic that can crash the process it watches is worse than none.
126
218
  */
127
- function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEAP_WATCH_INTERVAL_MS) {
219
+ function startHeapWatch(label = "hub-main", sink = consoleSink, intervalMs = HEAP_WATCH_INTERVAL_MS, reclaimOptions) {
220
+ const readMemory = reclaimOptions?.readMemory ?? (() => process.memoryUsage());
221
+ const now = reclaimOptions?.now ?? (() => Date.now());
222
+ const triggerMb = reclaimOptions?.triggerMb ?? 1536;
223
+ const minIntervalMs = reclaimOptions?.minIntervalMs ?? 6e5;
224
+ let lastReclaimAt = Number.NEGATIVE_INFINITY;
225
+ const read = () => {
226
+ const limit = reclaimOptions?.heapLimitBytes ?? v8.getHeapStatistics().heap_size_limit;
227
+ return buildHeapSample(readMemory(), limit);
228
+ };
229
+ const maybeReclaim = (sample) => {
230
+ if (reclaimOptions === void 0) return;
231
+ if (!shouldReclaim(sample, triggerMb)) return;
232
+ if (now() - lastReclaimAt < minIntervalMs) return;
233
+ lastReclaimAt = now();
234
+ const startedAt = now();
235
+ try {
236
+ reclaimOptions.reclaim();
237
+ } catch (error) {
238
+ sink.warn(`[mem] reclaim failed — ${error instanceof Error ? error.message : String(error)}`);
239
+ return;
240
+ }
241
+ const after = read();
242
+ 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`);
243
+ };
128
244
  const tick = () => {
129
245
  try {
130
- const sample = buildHeapSample(process.memoryUsage(), v8.getHeapStatistics().heap_size_limit);
246
+ const sample = read();
131
247
  const line = format$1(label, sample);
132
248
  if (sample.nearLimit) sink.warn(`${line} — APPROACHING HEAP LIMIT`);
133
249
  else sink.info(line);
250
+ maybeReclaim(sample);
134
251
  } catch {}
135
252
  };
136
253
  const timer = setInterval(tick, intervalMs);
@@ -94510,4 +94627,4 @@ var LifecycleJobEngine = class {
94510
94627
  }
94511
94628
  };
94512
94629
  //#endregion
94513
- 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, 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, 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_WATCH_INTERVAL_MS, HEAP_WATCH_WARN_RATIO, HUB_CAP_FWD_ACTION, HUB_CAP_FWD_SERVICE, HubForwarderAddon, HubForwarderDestination, HubLogForwarder, HubNodeRegistry, INFRA_CAPABILITIES, IntegrationRegistry, JobJournal, LifecycleJobEngine, LifecycleStateMachine, LivenessMonitorAddon, LocalAuthAddon, LocalChildClient, LocalChildRegistry, LogManager, LogRingBuffer, LokiDestination, LokiLoggingAddon, METHOD_ACCESS_MAP, ModelDownloadService, NATIVE_PROVIDER_SERVICE_INFIX, NATIVE_SCAN_DEPTH, NativeMetricsAddon, NativeMetricsProvider, NetworkQualityTracker, NotificationService, PYTHON_VERSION, PipelineRunner, PipelineValidator, PythonEnvManager, RESTART_MARKER_FILE, RUNTIME_DEFAULTS, ReadinessRegistry, ReadinessTimeoutError, ReplEngine, RingBuffer, 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, adaptBrokerToCluster, addonSettingsCapability, bootstrapSchema, brokerCallForCap, brokerTransportLink, buildBinaryPath, buildCapRouters, buildHeapSample, buildLinkChain, buildNativeCapProxy, buildNodeManifest, buildStorageLocationRegistry, buildUdsNativeCapProxy, builderMountedCapNames, callRegisterNodeWithRetry, callWithServiceDiscovery, capActionName, capActionSuffix, capBareAction, capServiceName, classifyCapRoute, clearPendingRestart, clusterEventTopic, clusterSecretMatches, collectModelFiles, contentTypeFor, copyDirRecursive, copyExtraFileDirs, createAddonContext, createAddonDataPlaneFacility, createAddonService, createAuthenticatedFileServer, createBroker, createBrokerDeviceManagerApi, createCoreCapService, createFileDataPlaneHandler, createHubCapForwardService, createHubService, createKernelHwAccel, createLocalTransport, createParentUnownedCallHandler, createProcessService, createReadinessService, createReadinessServiceForRegistry, createScopedProcessManager, createStreamProbeBrokerService, createUdsAddonContext, createUdsEventBridge, createUdsEventBus, createUdsLogger, createUdsLoggerWithControl, deleteModelFromDisk, deriveAgentListenPort, describeProviderKindDrift, detectWorkspacePackagesDir, downloadBinary, downloadFile, downloadModel, emitDownForOwnedCaps, encodeFrame, ensureAddonNativePrebuilds, ensureBinary, ensureDir, ensureFfmpeg, ensureLibraryBuilt, ensureModel, ensureNativePrebuilds, ensurePython, ensureTlsCert, expandCapMethods, fetchJson, findInPath, formatLogLine, getBrokerEventBus, getCapUsageRegistry, getFfmpegDownloadUrl, getModelFilePath, getMoleculerEventStats, getOrInitReadinessRegistry, getOrInitReadinessRegistryForClient, getPidStats, getPlatformInfo, getPythonDownloadUrl, getRestartMarkerPath, getSinglePidStats, getWorkerDeviceRegistry, hasDotNode, hashClusterSecret, installManifestNativeDeps, installManifestPythonDeps, installPackageFromNpm, installPythonPackages, installPythonRequirements, ipcChildLink, ipcParentLink, isAddonDeploySource, isArrayOutputSchema, isClusterSecretMismatchError, isCollectionArrayMethod, isInfraCapability, isModelDownloaded, isSourceNewer, loadTlsCert, localEndpointPath, localProviderLink, mountNativeCapService, parseCapAction, parseRangeHeader, parseTokenizedUrl, proxyToUpstream, readPendingRestart, readinessKey, registerEventBusService, resolveFilePath, resolveHwAccel, resolveNpmInvocation, runNpm, scheduleSelfRestart, scopeKey, scopesAllowDeviceCap, serializeTypedArrays, setHubConnected, setNodeEventInterest, startHeapWatch, stripCamstackDeps, subscribePassthrough, udsChildLogToWorkerEntry, validateProviderRegistrations, writePendingRestart };
94630
+ 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, 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, 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, LifecycleJobEngine, LifecycleStateMachine, LivenessMonitorAddon, LocalAuthAddon, LocalChildClient, LocalChildRegistry, LogManager, LogRingBuffer, LokiDestination, LokiLoggingAddon, METHOD_ACCESS_MAP, ModelDownloadService, NATIVE_PROVIDER_SERVICE_INFIX, NATIVE_SCAN_DEPTH, NativeMetricsAddon, NativeMetricsProvider, NetworkQualityTracker, NotificationService, PYTHON_VERSION, PipelineRunner, PipelineValidator, PythonEnvManager, RESTART_MARKER_FILE, RUNTIME_DEFAULTS, ReadinessRegistry, ReadinessTimeoutError, ReplEngine, RingBuffer, 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, adaptBrokerToCluster, addonSettingsCapability, bootstrapSchema, brokerCallForCap, brokerTransportLink, buildBinaryPath, buildCapRouters, buildHeapSample, buildLinkChain, buildNativeCapProxy, buildNodeManifest, buildStorageLocationRegistry, buildUdsNativeCapProxy, builderMountedCapNames, callRegisterNodeWithRetry, callWithServiceDiscovery, capActionName, capActionSuffix, capBareAction, capServiceName, classifyCapRoute, clearPendingRestart, clusterEventTopic, clusterSecretMatches, collectModelFiles, contentTypeFor, copyDirRecursive, copyExtraFileDirs, createAddonContext, createAddonDataPlaneFacility, createAddonService, createAuthenticatedFileServer, createBroker, createBrokerDeviceManagerApi, createCoreCapService, 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, expandCapMethods, fetchJson, findInPath, formatLogLine, getBrokerEventBus, getCapUsageRegistry, getFfmpegDownloadUrl, getModelFilePath, getMoleculerEventStats, getOrInitReadinessRegistry, getOrInitReadinessRegistryForClient, getPidStats, getPlatformInfo, getPythonDownloadUrl, getRestartMarkerPath, getSinglePidStats, getWorkerDeviceRegistry, hasDotNode, hashClusterSecret, installManifestNativeDeps, installManifestPythonDeps, installPackageFromNpm, installPythonPackages, installPythonRequirements, ipcChildLink, ipcParentLink, isAddonDeploySource, isArrayOutputSchema, isClusterSecretMismatchError, isCollectionArrayMethod, isInfraCapability, isModelDownloaded, isSourceNewer, loadTlsCert, localEndpointPath, localProviderLink, mountNativeCapService, parseCapAction, parseRangeHeader, parseTokenizedUrl, proxyToUpstream, readPendingRestart, readinessKey, registerEventBusService, resolveFilePath, resolveHwAccel, resolveNpmInvocation, runNpm, scheduleSelfRestart, scopeKey, scopesAllowDeviceCap, serializeTypedArrays, setHubConnected, setNodeEventInterest, shouldReclaim, startHeapWatch, strandedMb, stripCamstackDeps, subscribePassthrough, udsChildLogToWorkerEntry, validateProviderRegistrations, writePendingRestart };
@@ -4,6 +4,20 @@ export declare const HEAP_WATCH_INTERVAL_MS = 60000;
4
4
  /** Warn above this share of the heap ceiling. Chosen so the warning precedes
5
5
  * the fatal by more than one interval under realistic growth. */
6
6
  export declare const HEAP_WATCH_WARN_RATIO = 0.8;
7
+ /**
8
+ * Reclaim once the process holds this many MB beyond what it is USING.
9
+ *
10
+ * Live calibration, both readings from the same hub minutes apart: 2491MB
11
+ * stranded before the reclaim, 438MB after. 438MB is this process's irreducible
12
+ * native floor (sqlite pages, libvips, sharp, thread stacks, node's own text) —
13
+ * collecting against it would buy nothing and cost a stall every minute. 1536
14
+ * sits clear of that floor and still fires ~1.1GB before the ratchet is worth
15
+ * anything, so it triggers on the real condition and stops as soon as it is fixed.
16
+ */
17
+ export declare const HEAP_RECLAIM_TRIGGER_MB = 1536;
18
+ /** Floor on how often the stop-the-world may run. 250-450ms measured, so ten
19
+ * minutes keeps the worst case under 0.1% of the router's time. */
20
+ export declare const HEAP_RECLAIM_MIN_INTERVAL_MS = 600000;
7
21
  export interface HeapSample {
8
22
  readonly rssMb: number;
9
23
  readonly heapUsedMb: number;
@@ -27,10 +41,43 @@ export interface HeapWatchSink {
27
41
  readonly info: (line: string) => void;
28
42
  readonly warn: (line: string) => void;
29
43
  }
44
+ /**
45
+ * RSS the process is holding but not using.
46
+ *
47
+ * The subtrahend is heapUSED, not heapTotal, deliberately: the pages V8
48
+ * committed at a sawtooth peak and no longer needs are counted inside heapTotal,
49
+ * and they are precisely what this number exists to see.
50
+ */
51
+ export declare function strandedMb(s: HeapSample): number;
52
+ /** Whether it is worth paying a stop-the-world to get those pages back. */
53
+ export declare function shouldReclaim(s: HeapSample, triggerMb?: number): boolean;
54
+ /** A full collection that also asks V8 to hand the pages back to the OS. */
55
+ export type HeapReclaimer = () => void;
56
+ /**
57
+ * Build the reclaimer without requiring `--expose-gc` on argv.
58
+ *
59
+ * The flag is turned on, the function captured, and the flag turned straight
60
+ * back off — a global `gc` left reachable by application code is a footgun, and
61
+ * requiring an argv change would mean this cannot ship without a new image.
62
+ * Verified against the live hub before being written: 266ms, RSS 1453 -> 1237.
63
+ *
64
+ * Returns undefined if V8 declines, so the caller keeps a heartbeat either way.
65
+ */
66
+ export declare function createV8Reclaimer(): HeapReclaimer | undefined;
67
+ /** Wiring for the reclaim pass. `readMemory`/`heapLimitBytes` exist so a test
68
+ * can reach a gigabyte-scale state without allocating one. */
69
+ export interface HeapReclaimOptions {
70
+ readonly reclaim: HeapReclaimer;
71
+ readonly readMemory?: () => NodeJS.MemoryUsage;
72
+ readonly heapLimitBytes?: number;
73
+ readonly triggerMb?: number;
74
+ readonly minIntervalMs?: number;
75
+ readonly now?: () => number;
76
+ }
30
77
  /**
31
78
  * Start the heartbeat. Returns a stop function.
32
79
  *
33
80
  * `unref`ed so it can never hold the process open, and every read is wrapped:
34
81
  * a diagnostic that can crash the process it watches is worse than none.
35
82
  */
36
- export declare function startHeapWatch(label?: string, sink?: HeapWatchSink, intervalMs?: number): () => void;
83
+ export declare function startHeapWatch(label?: string, sink?: HeapWatchSink, intervalMs?: number, reclaimOptions?: HeapReclaimOptions): () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.59",
3
+ "version": "1.2.61",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",