@camstack/system 1.2.124 → 1.2.126

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.
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-Cek0wNdY.js");
2
- const require_manifest_python_deps = require("./manifest-python-deps-DB_4H3tR.js");
2
+ const require_manifest_python_deps = require("./manifest-python-deps-BTkFwAk_.js");
3
3
  const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
4
4
  let node_path = require("node:path");
5
5
  node_path = require_chunk.__toESM(node_path);
@@ -10,229 +10,6 @@ let node_v8 = require("node:v8");
10
10
  node_v8 = require_chunk.__toESM(node_v8);
11
11
  let _camstack_types_addon = require("@camstack/types/addon");
12
12
  let node_url = require("node:url");
13
- //#region src/kernel/moleculer/worker-device-restore.ts
14
- /**
15
- * Worker-side device restore helper.
16
- *
17
- * Shared by `process-runner` (single-addon subprocess) and
18
- * `group-runner` (group subprocess). Calls the worker addon's
19
- * `restoreDevices(SavedDevice[])` with devices read from the hub's
20
- * `device-manager` capability. Blocks on the `system.ready-state`
21
- * readiness of the hub's `device-manager` via
22
- * `ReadinessRegistry.awaitReady`, then issues the calls ONCE.
23
- *
24
- * Lives in its own module so it can be imported without triggering
25
- * the runner entry-point side effects (each runner is also an
26
- * executable that calls `main()` at import time).
27
- */
28
- async function runWorkerDeviceRestoreWithRetry(addon, context, addonId, sourceNodeId) {
29
- const log = context.logger;
30
- log?.debug?.(`[worker-restore] entry: addon="${addonId}" sourceNodeId="${sourceNodeId}"`);
31
- const restoreFn = addon.restoreDevices;
32
- if (typeof restoreFn !== "function") {
33
- log?.warn?.(`[worker-restore] "${addonId}": no restoreDevices function — skipping`);
34
- return;
35
- }
36
- const api = context.api;
37
- if (!api) {
38
- log?.warn?.(`[worker-restore] "${addonId}": context.api missing — skipping`);
39
- return;
40
- }
41
- const bus = context.eventBus;
42
- if (!bus) {
43
- log?.warn?.(`[worker-restore] "${addonId}": context.eventBus missing — skipping`);
44
- return;
45
- }
46
- const shared = context.kernel?.readinessRegistry ?? null;
47
- const registry = shared ?? new _camstack_types_addon.ReadinessRegistry({
48
- eventBus: bus,
49
- sourceNodeId,
50
- logger: context.logger
51
- });
52
- log?.debug?.(`[worker-restore] "${addonId}": awaiting device-manager readiness on hub...`);
53
- try {
54
- await registry.awaitReady("device-manager", {
55
- type: "node",
56
- nodeId: "hub"
57
- }, { timeoutMs: 6e4 });
58
- log?.debug?.(`[worker-restore] "${addonId}": device-manager READY`);
59
- } catch (err) {
60
- if (err instanceof _camstack_types_addon.ReadinessTimeoutError) {
61
- context.logger?.warn?.(`[worker-restore] device-manager not ready within ${err.waitedMs}ms — skipping for "${addonId}"`);
62
- return;
63
- }
64
- throw err;
65
- } finally {
66
- if (!shared && registry instanceof _camstack_types_addon.ReadinessRegistry) registry.close();
67
- }
68
- try {
69
- const deviceManager = Reflect.get(api, "deviceManager");
70
- log?.debug?.(`[worker-restore] "${addonId}": calling listPersistedByAddon...`);
71
- const rowsResult = await deviceManager.listPersistedByAddon.query({ addonId });
72
- log?.debug?.(`[worker-restore] "${addonId}": got ${Array.isArray(rowsResult) ? rowsResult.length : 0} row(s)`);
73
- const rows = Array.isArray(rowsResult) ? rowsResult : [];
74
- if (rows.length === 0) {
75
- context.logger?.info?.(`[worker-restore] no persisted devices to restore for addon "${addonId}"`);
76
- return;
77
- }
78
- const savedDevices = await Promise.all(rows.map(async (row) => {
79
- const config = await deviceManager.loadConfig.query({ deviceId: row.id });
80
- return {
81
- id: row.id,
82
- stableId: row.stableId,
83
- type: row.type,
84
- name: row.name,
85
- parentDeviceId: row.parentDeviceId,
86
- config: config ?? {}
87
- };
88
- }));
89
- await restoreFn.call(addon, savedDevices);
90
- log?.info?.(`[worker-restore] "${addonId}": restored ${savedDevices.length} device(s)`);
91
- } catch (err) {
92
- log?.warn?.(`[worker-restore] "${addonId}": restoreDevices threw: ${(0, _camstack_types_addon.errMsg)(err)}`);
93
- }
94
- }
95
- //#endregion
96
- //#region src/kernel/moleculer/register-framework-resolver.ts
97
- /**
98
- * Register the ESM resolver hook so this runner's addon imports of the
99
- * host-provided packages (@camstack/system, @camstack/shm-ring, …) resolve from
100
- * `frameworkDir/node_modules` instead of failing to walk up from the addon's
101
- * isolated `/data/addons/<addon>` folder. No-op in dev (frameworkDir unset),
102
- * where workspace symlinks already resolve the framework.
103
- */
104
- function registerFrameworkResolver(frameworkDir) {
105
- if (!frameworkDir) return;
106
- (0, node_module.register)((0, node_url.pathToFileURL)(node_path.join(__dirname, "framework-resolver-hook.mjs")), {
107
- parentURL: (0, node_url.pathToFileURL)(`${__dirname}/`).href,
108
- data: { frameworkDir }
109
- });
110
- }
111
- /**
112
- * Start warning if `initialize()` has not returned within `firstMs`.
113
- *
114
- * The caller MUST `clear()` in a `finally`, so a throwing initialize (which the
115
- * runner turns into `process.exit(1)`) does not keep the timer alive.
116
- */
117
- function startInitWatchdog(options) {
118
- const now = options.now ?? Date.now;
119
- const firstMs = options.firstMs ?? 6e4;
120
- const repeatMs = options.repeatMs ?? 3e5;
121
- const startedAt = now();
122
- let timer;
123
- let cleared = false;
124
- const schedule = (ms) => {
125
- timer = setTimeout(fire, ms);
126
- timer.unref?.();
127
- };
128
- function fire() {
129
- if (cleared) return;
130
- const elapsedMs = now() - startedAt;
131
- options.warn(`addon "${options.addonId}" initialize() has not returned after ${Math.round(elapsedMs / 1e3)}s — its capability manifest is NOT published, so every caller of ${options.declaredCapabilities.length > 0 ? options.declaredCapabilities.join(", ") : "(none declared)"} sees "no provider registered"`, {
132
- addonId: options.addonId,
133
- elapsedMs,
134
- unpublishedCapabilities: [...options.declaredCapabilities]
135
- });
136
- schedule(repeatMs);
137
- }
138
- schedule(firstMs);
139
- return { clear() {
140
- cleared = true;
141
- if (timer !== void 0) {
142
- clearTimeout(timer);
143
- timer = void 0;
144
- }
145
- } };
146
- }
147
- //#endregion
148
- //#region src/kernel/moleculer/child-cap-dispatch.ts
149
- /** Type guard narrowing a dynamically-read provider member to a callable method. */
150
- function isCapMethod(value) {
151
- return typeof value === "function";
152
- }
153
- /**
154
- * Extract a string `addonId` out of arbitrary method args — the fallback
155
- * provider-selection hint for calls that arrive WITHOUT the out-of-band
156
- * `CapCallMessage.addonId` (e.g. a sibling child's unowned collection call
157
- * forwarded by the parent, where the raw input still carries `addonId`).
158
- */
159
- function extractArgsAddonId(args) {
160
- if (args === null || typeof args !== "object") return void 0;
161
- const raw = Reflect.get(args, "addonId");
162
- return typeof raw === "string" ? raw : void 0;
163
- }
164
- /**
165
- * Creates a unified dispatcher that routes a {@link CapCallInput} to either a
166
- * device-scoped or singleton provider, then invokes the requested method.
167
- *
168
- * Pure module — no Moleculer, no broker, no addon-runner dependency.
169
- */
170
- function createChildCapDispatch(deps) {
171
- return async (call) => {
172
- const { capName, method, args, deviceId } = call;
173
- const addonId = call.addonId ?? extractArgsAddonId(args);
174
- let provider;
175
- if (deviceId !== void 0) {
176
- provider = deps.getDeviceProvider(capName, deviceId) ?? deps.getSingletonProvider(capName, addonId);
177
- if (provider == null) throw new Error(`child-cap-dispatch: no provider for '${capName}' (device '${String(deviceId)}' or singleton)`);
178
- } else {
179
- provider = deps.getSingletonProvider(capName, addonId);
180
- if (provider == null) throw new Error(`child-cap-dispatch: no singleton provider for '${capName}'`);
181
- }
182
- const fn = Reflect.get(provider, method);
183
- if (!isCapMethod(fn)) {
184
- if (deps.isCollectionListMethod(capName, method)) return [];
185
- throw new Error(`child-cap-dispatch: method '${method}' not found on provider '${capName}'`);
186
- }
187
- return await fn.call(provider, args);
188
- };
189
- }
190
- //#endregion
191
- //#region src/kernel/moleculer/child-cap-descriptors.ts
192
- /**
193
- * Builds the {@link ChildCapDescriptor} array that the UDS child transport
194
- * sends to the parent on registration.
195
- *
196
- * - Singleton caps come from the runner UNION manifest (all caps registered by
197
- * every addon in this runner). One descriptor per unique (addonId, capName)
198
- * pair, mode `'singleton'` and no `deviceId`. The `addonId` stamp lets the
199
- * parent's manifest builder register each hosted addon's caps under the
200
- * REAL addon id — a GROUPED runner (`execution.group`) hosts several addons,
201
- * and deduplicating by capName alone would collapse two co-located providers
202
- * of the same collection cap (e.g. `network-access` from cloudflare-tunnel
203
- * AND tailscale-ingress) into one descriptor registered under a synthetic
204
- * `addonId = childId` (the group name) — the "@camstack/addon-net-access"
205
- * admin-UI bug.
206
- * - Device-scoped caps come from the native-cap snapshot — one descriptor per
207
- * (capName, deviceId) pair, mode `'collection'`, stamped with the owning
208
- * addon's id.
209
- *
210
- * Pure module — extracted from `addon-runner.ts` (whose `main()` runs at
211
- * import time) so it is unit-testable.
212
- */
213
- function buildChildCapDescriptors(manifest, nativeCapSnapshot) {
214
- const descriptors = [];
215
- const seen = /* @__PURE__ */ new Set();
216
- for (const entry of manifest) for (const capName of entry.capabilities) {
217
- const key = `${entry.addonId}::${capName}`;
218
- if (!seen.has(key)) {
219
- seen.add(key);
220
- descriptors.push({
221
- capName,
222
- mode: "singleton",
223
- addonId: entry.addonId
224
- });
225
- }
226
- }
227
- for (const { capName, addonId, deviceIds } of nativeCapSnapshot) for (const deviceId of deviceIds) descriptors.push({
228
- capName,
229
- mode: "collection",
230
- deviceId,
231
- addonId
232
- });
233
- return descriptors;
234
- }
235
- //#endregion
236
13
  //#region src/kernel/moleculer/child-addon-call-dispatch.ts
237
14
  function isRecord$1(value) {
238
15
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -324,6 +101,229 @@ function createChildAddonCallDispatch(deps) {
324
101
  };
325
102
  }
326
103
  //#endregion
104
+ //#region src/kernel/moleculer/child-cap-descriptors.ts
105
+ /**
106
+ * Builds the {@link ChildCapDescriptor} array that the UDS child transport
107
+ * sends to the parent on registration.
108
+ *
109
+ * - Singleton caps come from the runner UNION manifest (all caps registered by
110
+ * every addon in this runner). One descriptor per unique (addonId, capName)
111
+ * pair, mode `'singleton'` and no `deviceId`. The `addonId` stamp lets the
112
+ * parent's manifest builder register each hosted addon's caps under the
113
+ * REAL addon id — a GROUPED runner (`execution.group`) hosts several addons,
114
+ * and deduplicating by capName alone would collapse two co-located providers
115
+ * of the same collection cap (e.g. `network-access` from cloudflare-tunnel
116
+ * AND tailscale-ingress) into one descriptor registered under a synthetic
117
+ * `addonId = childId` (the group name) — the "@camstack/addon-net-access"
118
+ * admin-UI bug.
119
+ * - Device-scoped caps come from the native-cap snapshot — one descriptor per
120
+ * (capName, deviceId) pair, mode `'collection'`, stamped with the owning
121
+ * addon's id.
122
+ *
123
+ * Pure module — extracted from `addon-runner.ts` (whose `main()` runs at
124
+ * import time) so it is unit-testable.
125
+ */
126
+ function buildChildCapDescriptors(manifest, nativeCapSnapshot) {
127
+ const descriptors = [];
128
+ const seen = /* @__PURE__ */ new Set();
129
+ for (const entry of manifest) for (const capName of entry.capabilities) {
130
+ const key = `${entry.addonId}::${capName}`;
131
+ if (!seen.has(key)) {
132
+ seen.add(key);
133
+ descriptors.push({
134
+ capName,
135
+ mode: "singleton",
136
+ addonId: entry.addonId
137
+ });
138
+ }
139
+ }
140
+ for (const { capName, addonId, deviceIds } of nativeCapSnapshot) for (const deviceId of deviceIds) descriptors.push({
141
+ capName,
142
+ mode: "collection",
143
+ deviceId,
144
+ addonId
145
+ });
146
+ return descriptors;
147
+ }
148
+ //#endregion
149
+ //#region src/kernel/moleculer/child-cap-dispatch.ts
150
+ /** Type guard narrowing a dynamically-read provider member to a callable method. */
151
+ function isCapMethod(value) {
152
+ return typeof value === "function";
153
+ }
154
+ /**
155
+ * Extract a string `addonId` out of arbitrary method args — the fallback
156
+ * provider-selection hint for calls that arrive WITHOUT the out-of-band
157
+ * `CapCallMessage.addonId` (e.g. a sibling child's unowned collection call
158
+ * forwarded by the parent, where the raw input still carries `addonId`).
159
+ */
160
+ function extractArgsAddonId(args) {
161
+ if (args === null || typeof args !== "object") return void 0;
162
+ const raw = Reflect.get(args, "addonId");
163
+ return typeof raw === "string" ? raw : void 0;
164
+ }
165
+ /**
166
+ * Creates a unified dispatcher that routes a {@link CapCallInput} to either a
167
+ * device-scoped or singleton provider, then invokes the requested method.
168
+ *
169
+ * Pure module — no Moleculer, no broker, no addon-runner dependency.
170
+ */
171
+ function createChildCapDispatch(deps) {
172
+ return async (call) => {
173
+ const { capName, method, args, deviceId } = call;
174
+ const addonId = call.addonId ?? extractArgsAddonId(args);
175
+ let provider;
176
+ if (deviceId !== void 0) {
177
+ provider = deps.getDeviceProvider(capName, deviceId) ?? deps.getSingletonProvider(capName, addonId);
178
+ if (provider == null) throw new Error(`child-cap-dispatch: no provider for '${capName}' (device '${String(deviceId)}' or singleton)`);
179
+ } else {
180
+ provider = deps.getSingletonProvider(capName, addonId);
181
+ if (provider == null) throw new Error(`child-cap-dispatch: no singleton provider for '${capName}'`);
182
+ }
183
+ const fn = Reflect.get(provider, method);
184
+ if (!isCapMethod(fn)) {
185
+ if (deps.isCollectionListMethod(capName, method)) return [];
186
+ throw new Error(`child-cap-dispatch: method '${method}' not found on provider '${capName}'`);
187
+ }
188
+ return await fn.call(provider, args);
189
+ };
190
+ }
191
+ /**
192
+ * Start warning if `initialize()` has not returned within `firstMs`.
193
+ *
194
+ * The caller MUST `clear()` in a `finally`, so a throwing initialize (which the
195
+ * runner turns into `process.exit(1)`) does not keep the timer alive.
196
+ */
197
+ function startInitWatchdog(options) {
198
+ const now = options.now ?? Date.now;
199
+ const firstMs = options.firstMs ?? 6e4;
200
+ const repeatMs = options.repeatMs ?? 3e5;
201
+ const startedAt = now();
202
+ let timer;
203
+ let cleared = false;
204
+ const schedule = (ms) => {
205
+ timer = setTimeout(fire, ms);
206
+ timer.unref?.();
207
+ };
208
+ function fire() {
209
+ if (cleared) return;
210
+ const elapsedMs = now() - startedAt;
211
+ options.warn(`addon "${options.addonId}" initialize() has not returned after ${Math.round(elapsedMs / 1e3)}s — its capability manifest is NOT published, so every caller of ${options.declaredCapabilities.length > 0 ? options.declaredCapabilities.join(", ") : "(none declared)"} sees "no provider registered"`, {
212
+ addonId: options.addonId,
213
+ elapsedMs,
214
+ unpublishedCapabilities: [...options.declaredCapabilities]
215
+ });
216
+ schedule(repeatMs);
217
+ }
218
+ schedule(firstMs);
219
+ return { clear() {
220
+ cleared = true;
221
+ if (timer !== void 0) {
222
+ clearTimeout(timer);
223
+ timer = void 0;
224
+ }
225
+ } };
226
+ }
227
+ //#endregion
228
+ //#region src/kernel/moleculer/register-framework-resolver.ts
229
+ /**
230
+ * Register the ESM resolver hook so this runner's addon imports of the
231
+ * host-provided packages (@camstack/system, @camstack/shm-ring, …) resolve from
232
+ * `frameworkDir/node_modules` instead of failing to walk up from the addon's
233
+ * isolated `/data/addons/<addon>` folder. No-op in dev (frameworkDir unset),
234
+ * where workspace symlinks already resolve the framework.
235
+ */
236
+ function registerFrameworkResolver(frameworkDir) {
237
+ if (!frameworkDir) return;
238
+ (0, node_module.register)((0, node_url.pathToFileURL)(node_path.join(__dirname, "framework-resolver-hook.mjs")), {
239
+ parentURL: (0, node_url.pathToFileURL)(`${__dirname}/`).href,
240
+ data: { frameworkDir }
241
+ });
242
+ }
243
+ //#endregion
244
+ //#region src/kernel/moleculer/worker-device-restore.ts
245
+ /**
246
+ * Worker-side device restore helper.
247
+ *
248
+ * Shared by `process-runner` (single-addon subprocess) and
249
+ * `group-runner` (group subprocess). Calls the worker addon's
250
+ * `restoreDevices(SavedDevice[])` with devices read from the hub's
251
+ * `device-manager` capability. Blocks on the `system.ready-state`
252
+ * readiness of the hub's `device-manager` via
253
+ * `ReadinessRegistry.awaitReady`, then issues the calls ONCE.
254
+ *
255
+ * Lives in its own module so it can be imported without triggering
256
+ * the runner entry-point side effects (each runner is also an
257
+ * executable that calls `main()` at import time).
258
+ */
259
+ async function runWorkerDeviceRestoreWithRetry(addon, context, addonId, sourceNodeId) {
260
+ const log = context.logger;
261
+ log?.debug?.(`[worker-restore] entry: addon="${addonId}" sourceNodeId="${sourceNodeId}"`);
262
+ const restoreFn = addon.restoreDevices;
263
+ if (typeof restoreFn !== "function") {
264
+ log?.warn?.(`[worker-restore] "${addonId}": no restoreDevices function — skipping`);
265
+ return;
266
+ }
267
+ const api = context.api;
268
+ if (!api) {
269
+ log?.warn?.(`[worker-restore] "${addonId}": context.api missing — skipping`);
270
+ return;
271
+ }
272
+ const bus = context.eventBus;
273
+ if (!bus) {
274
+ log?.warn?.(`[worker-restore] "${addonId}": context.eventBus missing — skipping`);
275
+ return;
276
+ }
277
+ const shared = context.kernel?.readinessRegistry ?? null;
278
+ const registry = shared ?? new _camstack_types_addon.ReadinessRegistry({
279
+ eventBus: bus,
280
+ sourceNodeId,
281
+ logger: context.logger
282
+ });
283
+ log?.debug?.(`[worker-restore] "${addonId}": awaiting device-manager readiness on hub...`);
284
+ try {
285
+ await registry.awaitReady("device-manager", {
286
+ type: "node",
287
+ nodeId: "hub"
288
+ }, { timeoutMs: 6e4 });
289
+ log?.debug?.(`[worker-restore] "${addonId}": device-manager READY`);
290
+ } catch (err) {
291
+ if (err instanceof _camstack_types_addon.ReadinessTimeoutError) {
292
+ context.logger?.warn?.(`[worker-restore] device-manager not ready within ${err.waitedMs}ms — skipping for "${addonId}"`);
293
+ return;
294
+ }
295
+ throw err;
296
+ } finally {
297
+ if (!shared && registry instanceof _camstack_types_addon.ReadinessRegistry) registry.close();
298
+ }
299
+ try {
300
+ const deviceManager = Reflect.get(api, "deviceManager");
301
+ log?.debug?.(`[worker-restore] "${addonId}": calling listPersistedByAddon...`);
302
+ const rowsResult = await deviceManager.listPersistedByAddon.query({ addonId });
303
+ log?.debug?.(`[worker-restore] "${addonId}": got ${Array.isArray(rowsResult) ? rowsResult.length : 0} row(s)`);
304
+ const rows = Array.isArray(rowsResult) ? rowsResult : [];
305
+ if (rows.length === 0) {
306
+ context.logger?.info?.(`[worker-restore] no persisted devices to restore for addon "${addonId}"`);
307
+ return;
308
+ }
309
+ const savedDevices = await Promise.all(rows.map(async (row) => {
310
+ const config = await deviceManager.loadConfig.query({ deviceId: row.id });
311
+ return {
312
+ id: row.id,
313
+ stableId: row.stableId,
314
+ type: row.type,
315
+ name: row.name,
316
+ parentDeviceId: row.parentDeviceId,
317
+ config: config ?? {}
318
+ };
319
+ }));
320
+ await restoreFn.call(addon, savedDevices);
321
+ log?.info?.(`[worker-restore] "${addonId}": restored ${savedDevices.length} device(s)`);
322
+ } catch (err) {
323
+ log?.warn?.(`[worker-restore] "${addonId}": restoreDevices threw: ${(0, _camstack_types_addon.errMsg)(err)}`);
324
+ }
325
+ }
326
+ //#endregion
327
327
  //#region src/kernel/moleculer/addon-runner.ts
328
328
  /**
329
329
  * Addon Runner — THE unified entry point for spawned subprocesses.
@@ -1,4 +1,4 @@
1
- import { I as createUdsLoggerWithControl, L as LocalChildClient, Pt as startRunnerHeapWatch, at as setWorkerNativeCapsChangeListener, bt as installManifestNativeDeps, i as createUdsAddonContext, nt as getWorkerNativeCapProvider, rt as getWorkerNativeCapSnapshot, st as validateProviderRegistrations, t as installManifestPythonDeps, xt as resolveAddonClass } from "./manifest-python-deps-D1Ydw7J0.mjs";
1
+ import { I as createUdsLoggerWithControl, L as LocalChildClient, Pt as startRunnerHeapWatch, at as setWorkerNativeCapsChangeListener, bt as installManifestNativeDeps, i as createUdsAddonContext, nt as getWorkerNativeCapProvider, rt as getWorkerNativeCapSnapshot, st as validateProviderRegistrations, t as installManifestPythonDeps, xt as resolveAddonClass } from "./manifest-python-deps-Dmcnb287.mjs";
2
2
  import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
3
3
  import { register } from "node:module";
4
4
  import * as path$1 from "node:path";
@@ -6,229 +6,6 @@ import * as fs from "node:fs";
6
6
  import * as v8 from "node:v8";
7
7
  import { ReadinessRegistry, ReadinessTimeoutError, errMsg, isCollectionArrayMethodName, normalizeAddonInitResult } from "@camstack/types/addon";
8
8
  import { pathToFileURL } from "node:url";
9
- //#region src/kernel/moleculer/worker-device-restore.ts
10
- /**
11
- * Worker-side device restore helper.
12
- *
13
- * Shared by `process-runner` (single-addon subprocess) and
14
- * `group-runner` (group subprocess). Calls the worker addon's
15
- * `restoreDevices(SavedDevice[])` with devices read from the hub's
16
- * `device-manager` capability. Blocks on the `system.ready-state`
17
- * readiness of the hub's `device-manager` via
18
- * `ReadinessRegistry.awaitReady`, then issues the calls ONCE.
19
- *
20
- * Lives in its own module so it can be imported without triggering
21
- * the runner entry-point side effects (each runner is also an
22
- * executable that calls `main()` at import time).
23
- */
24
- async function runWorkerDeviceRestoreWithRetry(addon, context, addonId, sourceNodeId) {
25
- const log = context.logger;
26
- log?.debug?.(`[worker-restore] entry: addon="${addonId}" sourceNodeId="${sourceNodeId}"`);
27
- const restoreFn = addon.restoreDevices;
28
- if (typeof restoreFn !== "function") {
29
- log?.warn?.(`[worker-restore] "${addonId}": no restoreDevices function — skipping`);
30
- return;
31
- }
32
- const api = context.api;
33
- if (!api) {
34
- log?.warn?.(`[worker-restore] "${addonId}": context.api missing — skipping`);
35
- return;
36
- }
37
- const bus = context.eventBus;
38
- if (!bus) {
39
- log?.warn?.(`[worker-restore] "${addonId}": context.eventBus missing — skipping`);
40
- return;
41
- }
42
- const shared = context.kernel?.readinessRegistry ?? null;
43
- const registry = shared ?? new ReadinessRegistry({
44
- eventBus: bus,
45
- sourceNodeId,
46
- logger: context.logger
47
- });
48
- log?.debug?.(`[worker-restore] "${addonId}": awaiting device-manager readiness on hub...`);
49
- try {
50
- await registry.awaitReady("device-manager", {
51
- type: "node",
52
- nodeId: "hub"
53
- }, { timeoutMs: 6e4 });
54
- log?.debug?.(`[worker-restore] "${addonId}": device-manager READY`);
55
- } catch (err) {
56
- if (err instanceof ReadinessTimeoutError) {
57
- context.logger?.warn?.(`[worker-restore] device-manager not ready within ${err.waitedMs}ms — skipping for "${addonId}"`);
58
- return;
59
- }
60
- throw err;
61
- } finally {
62
- if (!shared && registry instanceof ReadinessRegistry) registry.close();
63
- }
64
- try {
65
- const deviceManager = Reflect.get(api, "deviceManager");
66
- log?.debug?.(`[worker-restore] "${addonId}": calling listPersistedByAddon...`);
67
- const rowsResult = await deviceManager.listPersistedByAddon.query({ addonId });
68
- log?.debug?.(`[worker-restore] "${addonId}": got ${Array.isArray(rowsResult) ? rowsResult.length : 0} row(s)`);
69
- const rows = Array.isArray(rowsResult) ? rowsResult : [];
70
- if (rows.length === 0) {
71
- context.logger?.info?.(`[worker-restore] no persisted devices to restore for addon "${addonId}"`);
72
- return;
73
- }
74
- const savedDevices = await Promise.all(rows.map(async (row) => {
75
- const config = await deviceManager.loadConfig.query({ deviceId: row.id });
76
- return {
77
- id: row.id,
78
- stableId: row.stableId,
79
- type: row.type,
80
- name: row.name,
81
- parentDeviceId: row.parentDeviceId,
82
- config: config ?? {}
83
- };
84
- }));
85
- await restoreFn.call(addon, savedDevices);
86
- log?.info?.(`[worker-restore] "${addonId}": restored ${savedDevices.length} device(s)`);
87
- } catch (err) {
88
- log?.warn?.(`[worker-restore] "${addonId}": restoreDevices threw: ${errMsg(err)}`);
89
- }
90
- }
91
- //#endregion
92
- //#region src/kernel/moleculer/register-framework-resolver.ts
93
- /**
94
- * Register the ESM resolver hook so this runner's addon imports of the
95
- * host-provided packages (@camstack/system, @camstack/shm-ring, …) resolve from
96
- * `frameworkDir/node_modules` instead of failing to walk up from the addon's
97
- * isolated `/data/addons/<addon>` folder. No-op in dev (frameworkDir unset),
98
- * where workspace symlinks already resolve the framework.
99
- */
100
- function registerFrameworkResolver(frameworkDir) {
101
- if (!frameworkDir) return;
102
- register(pathToFileURL(path$1.join(__dirname, "framework-resolver-hook.mjs")), {
103
- parentURL: pathToFileURL(`${__dirname}/`).href,
104
- data: { frameworkDir }
105
- });
106
- }
107
- /**
108
- * Start warning if `initialize()` has not returned within `firstMs`.
109
- *
110
- * The caller MUST `clear()` in a `finally`, so a throwing initialize (which the
111
- * runner turns into `process.exit(1)`) does not keep the timer alive.
112
- */
113
- function startInitWatchdog(options) {
114
- const now = options.now ?? Date.now;
115
- const firstMs = options.firstMs ?? 6e4;
116
- const repeatMs = options.repeatMs ?? 3e5;
117
- const startedAt = now();
118
- let timer;
119
- let cleared = false;
120
- const schedule = (ms) => {
121
- timer = setTimeout(fire, ms);
122
- timer.unref?.();
123
- };
124
- function fire() {
125
- if (cleared) return;
126
- const elapsedMs = now() - startedAt;
127
- options.warn(`addon "${options.addonId}" initialize() has not returned after ${Math.round(elapsedMs / 1e3)}s — its capability manifest is NOT published, so every caller of ${options.declaredCapabilities.length > 0 ? options.declaredCapabilities.join(", ") : "(none declared)"} sees "no provider registered"`, {
128
- addonId: options.addonId,
129
- elapsedMs,
130
- unpublishedCapabilities: [...options.declaredCapabilities]
131
- });
132
- schedule(repeatMs);
133
- }
134
- schedule(firstMs);
135
- return { clear() {
136
- cleared = true;
137
- if (timer !== void 0) {
138
- clearTimeout(timer);
139
- timer = void 0;
140
- }
141
- } };
142
- }
143
- //#endregion
144
- //#region src/kernel/moleculer/child-cap-dispatch.ts
145
- /** Type guard narrowing a dynamically-read provider member to a callable method. */
146
- function isCapMethod(value) {
147
- return typeof value === "function";
148
- }
149
- /**
150
- * Extract a string `addonId` out of arbitrary method args — the fallback
151
- * provider-selection hint for calls that arrive WITHOUT the out-of-band
152
- * `CapCallMessage.addonId` (e.g. a sibling child's unowned collection call
153
- * forwarded by the parent, where the raw input still carries `addonId`).
154
- */
155
- function extractArgsAddonId(args) {
156
- if (args === null || typeof args !== "object") return void 0;
157
- const raw = Reflect.get(args, "addonId");
158
- return typeof raw === "string" ? raw : void 0;
159
- }
160
- /**
161
- * Creates a unified dispatcher that routes a {@link CapCallInput} to either a
162
- * device-scoped or singleton provider, then invokes the requested method.
163
- *
164
- * Pure module — no Moleculer, no broker, no addon-runner dependency.
165
- */
166
- function createChildCapDispatch(deps) {
167
- return async (call) => {
168
- const { capName, method, args, deviceId } = call;
169
- const addonId = call.addonId ?? extractArgsAddonId(args);
170
- let provider;
171
- if (deviceId !== void 0) {
172
- provider = deps.getDeviceProvider(capName, deviceId) ?? deps.getSingletonProvider(capName, addonId);
173
- if (provider == null) throw new Error(`child-cap-dispatch: no provider for '${capName}' (device '${String(deviceId)}' or singleton)`);
174
- } else {
175
- provider = deps.getSingletonProvider(capName, addonId);
176
- if (provider == null) throw new Error(`child-cap-dispatch: no singleton provider for '${capName}'`);
177
- }
178
- const fn = Reflect.get(provider, method);
179
- if (!isCapMethod(fn)) {
180
- if (deps.isCollectionListMethod(capName, method)) return [];
181
- throw new Error(`child-cap-dispatch: method '${method}' not found on provider '${capName}'`);
182
- }
183
- return await fn.call(provider, args);
184
- };
185
- }
186
- //#endregion
187
- //#region src/kernel/moleculer/child-cap-descriptors.ts
188
- /**
189
- * Builds the {@link ChildCapDescriptor} array that the UDS child transport
190
- * sends to the parent on registration.
191
- *
192
- * - Singleton caps come from the runner UNION manifest (all caps registered by
193
- * every addon in this runner). One descriptor per unique (addonId, capName)
194
- * pair, mode `'singleton'` and no `deviceId`. The `addonId` stamp lets the
195
- * parent's manifest builder register each hosted addon's caps under the
196
- * REAL addon id — a GROUPED runner (`execution.group`) hosts several addons,
197
- * and deduplicating by capName alone would collapse two co-located providers
198
- * of the same collection cap (e.g. `network-access` from cloudflare-tunnel
199
- * AND tailscale-ingress) into one descriptor registered under a synthetic
200
- * `addonId = childId` (the group name) — the "@camstack/addon-net-access"
201
- * admin-UI bug.
202
- * - Device-scoped caps come from the native-cap snapshot — one descriptor per
203
- * (capName, deviceId) pair, mode `'collection'`, stamped with the owning
204
- * addon's id.
205
- *
206
- * Pure module — extracted from `addon-runner.ts` (whose `main()` runs at
207
- * import time) so it is unit-testable.
208
- */
209
- function buildChildCapDescriptors(manifest, nativeCapSnapshot) {
210
- const descriptors = [];
211
- const seen = /* @__PURE__ */ new Set();
212
- for (const entry of manifest) for (const capName of entry.capabilities) {
213
- const key = `${entry.addonId}::${capName}`;
214
- if (!seen.has(key)) {
215
- seen.add(key);
216
- descriptors.push({
217
- capName,
218
- mode: "singleton",
219
- addonId: entry.addonId
220
- });
221
- }
222
- }
223
- for (const { capName, addonId, deviceIds } of nativeCapSnapshot) for (const deviceId of deviceIds) descriptors.push({
224
- capName,
225
- mode: "collection",
226
- deviceId,
227
- addonId
228
- });
229
- return descriptors;
230
- }
231
- //#endregion
232
9
  //#region src/kernel/moleculer/child-addon-call-dispatch.ts
233
10
  function isRecord$1(value) {
234
11
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -320,6 +97,229 @@ function createChildAddonCallDispatch(deps) {
320
97
  };
321
98
  }
322
99
  //#endregion
100
+ //#region src/kernel/moleculer/child-cap-descriptors.ts
101
+ /**
102
+ * Builds the {@link ChildCapDescriptor} array that the UDS child transport
103
+ * sends to the parent on registration.
104
+ *
105
+ * - Singleton caps come from the runner UNION manifest (all caps registered by
106
+ * every addon in this runner). One descriptor per unique (addonId, capName)
107
+ * pair, mode `'singleton'` and no `deviceId`. The `addonId` stamp lets the
108
+ * parent's manifest builder register each hosted addon's caps under the
109
+ * REAL addon id — a GROUPED runner (`execution.group`) hosts several addons,
110
+ * and deduplicating by capName alone would collapse two co-located providers
111
+ * of the same collection cap (e.g. `network-access` from cloudflare-tunnel
112
+ * AND tailscale-ingress) into one descriptor registered under a synthetic
113
+ * `addonId = childId` (the group name) — the "@camstack/addon-net-access"
114
+ * admin-UI bug.
115
+ * - Device-scoped caps come from the native-cap snapshot — one descriptor per
116
+ * (capName, deviceId) pair, mode `'collection'`, stamped with the owning
117
+ * addon's id.
118
+ *
119
+ * Pure module — extracted from `addon-runner.ts` (whose `main()` runs at
120
+ * import time) so it is unit-testable.
121
+ */
122
+ function buildChildCapDescriptors(manifest, nativeCapSnapshot) {
123
+ const descriptors = [];
124
+ const seen = /* @__PURE__ */ new Set();
125
+ for (const entry of manifest) for (const capName of entry.capabilities) {
126
+ const key = `${entry.addonId}::${capName}`;
127
+ if (!seen.has(key)) {
128
+ seen.add(key);
129
+ descriptors.push({
130
+ capName,
131
+ mode: "singleton",
132
+ addonId: entry.addonId
133
+ });
134
+ }
135
+ }
136
+ for (const { capName, addonId, deviceIds } of nativeCapSnapshot) for (const deviceId of deviceIds) descriptors.push({
137
+ capName,
138
+ mode: "collection",
139
+ deviceId,
140
+ addonId
141
+ });
142
+ return descriptors;
143
+ }
144
+ //#endregion
145
+ //#region src/kernel/moleculer/child-cap-dispatch.ts
146
+ /** Type guard narrowing a dynamically-read provider member to a callable method. */
147
+ function isCapMethod(value) {
148
+ return typeof value === "function";
149
+ }
150
+ /**
151
+ * Extract a string `addonId` out of arbitrary method args — the fallback
152
+ * provider-selection hint for calls that arrive WITHOUT the out-of-band
153
+ * `CapCallMessage.addonId` (e.g. a sibling child's unowned collection call
154
+ * forwarded by the parent, where the raw input still carries `addonId`).
155
+ */
156
+ function extractArgsAddonId(args) {
157
+ if (args === null || typeof args !== "object") return void 0;
158
+ const raw = Reflect.get(args, "addonId");
159
+ return typeof raw === "string" ? raw : void 0;
160
+ }
161
+ /**
162
+ * Creates a unified dispatcher that routes a {@link CapCallInput} to either a
163
+ * device-scoped or singleton provider, then invokes the requested method.
164
+ *
165
+ * Pure module — no Moleculer, no broker, no addon-runner dependency.
166
+ */
167
+ function createChildCapDispatch(deps) {
168
+ return async (call) => {
169
+ const { capName, method, args, deviceId } = call;
170
+ const addonId = call.addonId ?? extractArgsAddonId(args);
171
+ let provider;
172
+ if (deviceId !== void 0) {
173
+ provider = deps.getDeviceProvider(capName, deviceId) ?? deps.getSingletonProvider(capName, addonId);
174
+ if (provider == null) throw new Error(`child-cap-dispatch: no provider for '${capName}' (device '${String(deviceId)}' or singleton)`);
175
+ } else {
176
+ provider = deps.getSingletonProvider(capName, addonId);
177
+ if (provider == null) throw new Error(`child-cap-dispatch: no singleton provider for '${capName}'`);
178
+ }
179
+ const fn = Reflect.get(provider, method);
180
+ if (!isCapMethod(fn)) {
181
+ if (deps.isCollectionListMethod(capName, method)) return [];
182
+ throw new Error(`child-cap-dispatch: method '${method}' not found on provider '${capName}'`);
183
+ }
184
+ return await fn.call(provider, args);
185
+ };
186
+ }
187
+ /**
188
+ * Start warning if `initialize()` has not returned within `firstMs`.
189
+ *
190
+ * The caller MUST `clear()` in a `finally`, so a throwing initialize (which the
191
+ * runner turns into `process.exit(1)`) does not keep the timer alive.
192
+ */
193
+ function startInitWatchdog(options) {
194
+ const now = options.now ?? Date.now;
195
+ const firstMs = options.firstMs ?? 6e4;
196
+ const repeatMs = options.repeatMs ?? 3e5;
197
+ const startedAt = now();
198
+ let timer;
199
+ let cleared = false;
200
+ const schedule = (ms) => {
201
+ timer = setTimeout(fire, ms);
202
+ timer.unref?.();
203
+ };
204
+ function fire() {
205
+ if (cleared) return;
206
+ const elapsedMs = now() - startedAt;
207
+ options.warn(`addon "${options.addonId}" initialize() has not returned after ${Math.round(elapsedMs / 1e3)}s — its capability manifest is NOT published, so every caller of ${options.declaredCapabilities.length > 0 ? options.declaredCapabilities.join(", ") : "(none declared)"} sees "no provider registered"`, {
208
+ addonId: options.addonId,
209
+ elapsedMs,
210
+ unpublishedCapabilities: [...options.declaredCapabilities]
211
+ });
212
+ schedule(repeatMs);
213
+ }
214
+ schedule(firstMs);
215
+ return { clear() {
216
+ cleared = true;
217
+ if (timer !== void 0) {
218
+ clearTimeout(timer);
219
+ timer = void 0;
220
+ }
221
+ } };
222
+ }
223
+ //#endregion
224
+ //#region src/kernel/moleculer/register-framework-resolver.ts
225
+ /**
226
+ * Register the ESM resolver hook so this runner's addon imports of the
227
+ * host-provided packages (@camstack/system, @camstack/shm-ring, …) resolve from
228
+ * `frameworkDir/node_modules` instead of failing to walk up from the addon's
229
+ * isolated `/data/addons/<addon>` folder. No-op in dev (frameworkDir unset),
230
+ * where workspace symlinks already resolve the framework.
231
+ */
232
+ function registerFrameworkResolver(frameworkDir) {
233
+ if (!frameworkDir) return;
234
+ register(pathToFileURL(path$1.join(__dirname, "framework-resolver-hook.mjs")), {
235
+ parentURL: pathToFileURL(`${__dirname}/`).href,
236
+ data: { frameworkDir }
237
+ });
238
+ }
239
+ //#endregion
240
+ //#region src/kernel/moleculer/worker-device-restore.ts
241
+ /**
242
+ * Worker-side device restore helper.
243
+ *
244
+ * Shared by `process-runner` (single-addon subprocess) and
245
+ * `group-runner` (group subprocess). Calls the worker addon's
246
+ * `restoreDevices(SavedDevice[])` with devices read from the hub's
247
+ * `device-manager` capability. Blocks on the `system.ready-state`
248
+ * readiness of the hub's `device-manager` via
249
+ * `ReadinessRegistry.awaitReady`, then issues the calls ONCE.
250
+ *
251
+ * Lives in its own module so it can be imported without triggering
252
+ * the runner entry-point side effects (each runner is also an
253
+ * executable that calls `main()` at import time).
254
+ */
255
+ async function runWorkerDeviceRestoreWithRetry(addon, context, addonId, sourceNodeId) {
256
+ const log = context.logger;
257
+ log?.debug?.(`[worker-restore] entry: addon="${addonId}" sourceNodeId="${sourceNodeId}"`);
258
+ const restoreFn = addon.restoreDevices;
259
+ if (typeof restoreFn !== "function") {
260
+ log?.warn?.(`[worker-restore] "${addonId}": no restoreDevices function — skipping`);
261
+ return;
262
+ }
263
+ const api = context.api;
264
+ if (!api) {
265
+ log?.warn?.(`[worker-restore] "${addonId}": context.api missing — skipping`);
266
+ return;
267
+ }
268
+ const bus = context.eventBus;
269
+ if (!bus) {
270
+ log?.warn?.(`[worker-restore] "${addonId}": context.eventBus missing — skipping`);
271
+ return;
272
+ }
273
+ const shared = context.kernel?.readinessRegistry ?? null;
274
+ const registry = shared ?? new ReadinessRegistry({
275
+ eventBus: bus,
276
+ sourceNodeId,
277
+ logger: context.logger
278
+ });
279
+ log?.debug?.(`[worker-restore] "${addonId}": awaiting device-manager readiness on hub...`);
280
+ try {
281
+ await registry.awaitReady("device-manager", {
282
+ type: "node",
283
+ nodeId: "hub"
284
+ }, { timeoutMs: 6e4 });
285
+ log?.debug?.(`[worker-restore] "${addonId}": device-manager READY`);
286
+ } catch (err) {
287
+ if (err instanceof ReadinessTimeoutError) {
288
+ context.logger?.warn?.(`[worker-restore] device-manager not ready within ${err.waitedMs}ms — skipping for "${addonId}"`);
289
+ return;
290
+ }
291
+ throw err;
292
+ } finally {
293
+ if (!shared && registry instanceof ReadinessRegistry) registry.close();
294
+ }
295
+ try {
296
+ const deviceManager = Reflect.get(api, "deviceManager");
297
+ log?.debug?.(`[worker-restore] "${addonId}": calling listPersistedByAddon...`);
298
+ const rowsResult = await deviceManager.listPersistedByAddon.query({ addonId });
299
+ log?.debug?.(`[worker-restore] "${addonId}": got ${Array.isArray(rowsResult) ? rowsResult.length : 0} row(s)`);
300
+ const rows = Array.isArray(rowsResult) ? rowsResult : [];
301
+ if (rows.length === 0) {
302
+ context.logger?.info?.(`[worker-restore] no persisted devices to restore for addon "${addonId}"`);
303
+ return;
304
+ }
305
+ const savedDevices = await Promise.all(rows.map(async (row) => {
306
+ const config = await deviceManager.loadConfig.query({ deviceId: row.id });
307
+ return {
308
+ id: row.id,
309
+ stableId: row.stableId,
310
+ type: row.type,
311
+ name: row.name,
312
+ parentDeviceId: row.parentDeviceId,
313
+ config: config ?? {}
314
+ };
315
+ }));
316
+ await restoreFn.call(addon, savedDevices);
317
+ log?.info?.(`[worker-restore] "${addonId}": restored ${savedDevices.length} device(s)`);
318
+ } catch (err) {
319
+ log?.warn?.(`[worker-restore] "${addonId}": restoreDevices threw: ${errMsg(err)}`);
320
+ }
321
+ }
322
+ //#endregion
323
323
  //#region src/kernel/moleculer/addon-runner.ts
324
324
  /**
325
325
  * Addon Runner — THE unified entry point for spawned subprocesses.
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ const require_builtins_system_config_system_config_addon = require("./builtins/s
25
25
  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
- const require_manifest_python_deps = require("./manifest-python-deps-DB_4H3tR.js");
28
+ const require_manifest_python_deps = require("./manifest-python-deps-BTkFwAk_.js");
29
29
  const require_resource_monitor = require("./resource-monitor-CdnzxBLP.js");
30
30
  const require_lan_http_bind = require("./lan-http-bind-DmgpFP6_.js");
31
31
  const require_custom_action_registry = require("./custom-action-registry-jY0NOZK8.js");
@@ -93976,6 +93976,34 @@ var CrashSupervisor = class {
93976
93976
  this.crashes.delete(runnerId);
93977
93977
  }
93978
93978
  };
93979
+ /**
93980
+ * Resolve the native-allocator env for a child runner from the parent's env.
93981
+ *
93982
+ * Precedence, highest first:
93983
+ * 1. `CAMSTACK_RUNNER_NATIVE_ALLOCATOR=off` — kill switch, returns nothing and
93984
+ * restores the glibc/sharp defaults. Recoverable without a rebuild.
93985
+ * 2. A value the operator already put in the environment (`MALLOC_ARENA_MAX`,
93986
+ * `VIPS_CONCURRENCY`) — inherited verbatim, never overwritten.
93987
+ * 3. `CAMSTACK_RUNNER_MALLOC_ARENA_MAX` — the CamStack override.
93988
+ * 4. The defaults above.
93989
+ *
93990
+ * A nonsense override falls back to the default rather than being passed through:
93991
+ * glibc reads `MALLOC_ARENA_MAX=0` as "unbounded", which is the very state this
93992
+ * exists to leave.
93993
+ */
93994
+ function runnerNativeAllocatorEnv(parentEnv, platform = process.platform) {
93995
+ if (platform !== "linux") return {};
93996
+ if (parentEnv["CAMSTACK_RUNNER_NATIVE_ALLOCATOR"] === "off") return {};
93997
+ return {
93998
+ MALLOC_ARENA_MAX: parentEnv["MALLOC_ARENA_MAX"] ?? String(positiveInt(parentEnv["CAMSTACK_RUNNER_MALLOC_ARENA_MAX"], 2)),
93999
+ VIPS_CONCURRENCY: parentEnv["VIPS_CONCURRENCY"] ?? String(1)
94000
+ };
94001
+ }
94002
+ function positiveInt(raw, fallback) {
94003
+ if (raw === void 0) return fallback;
94004
+ const n = Number.parseInt(raw, 10);
94005
+ return Number.isFinite(n) && n > 0 ? n : fallback;
94006
+ }
93979
94007
  //#endregion
93980
94008
  //#region src/kernel/moleculer/process-service.ts
93981
94009
  /**
@@ -94136,7 +94164,18 @@ function isHeavyRunner(addons) {
94136
94164
  *
94137
94165
  * A runner whose working set genuinely does not fit declares its own number in its manifest
94138
94166
  * (`execution.maxOldSpaceMb`) — the number belongs next to the addon that knows its working set.
94139
- * `stream-broker` (1323 MB measured, frame buffers) is the one shipped addon that does.
94167
+ * `recorder` (3072 MB; peak 2341 MB heapTotal during the deferred archive walk) is the one
94168
+ * shipped addon that does. `stream-broker` does NOT, despite 1585 MB peak RSS: ~1 GB of that is
94169
+ * `arrayBuffers` (47 RTP rings), which this flag does not bound and never will — its actual old
94170
+ * space peaks at 224 MB. Size the declaration from `heapTotal` in the `[mem]` heartbeat, never
94171
+ * from RSS.
94172
+ *
94173
+ * ## What this flag CANNOT do
94174
+ *
94175
+ * It bounds old space and nothing else — not `external`, not `arrayBuffers`, not what a native
94176
+ * addon mallocs. `hub/pipeline-analytics` reached 7334 MB RSS with this flag set to 1024 the
94177
+ * whole time, because 96% of it was native. That half is bounded in `runner-native-allocator.ts`.
94178
+ * Reaching for a bigger number here when RSS is the complaint is the standard wrong move.
94140
94179
  */
94141
94180
  var HEAVY_MAX_OLD_MB_DEFAULT = 1024;
94142
94181
  /**
@@ -94262,10 +94301,11 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
94262
94301
  CAMSTACK_LOG_LEVEL: process.env["CAMSTACK_LOG_LEVEL"] ?? "info",
94263
94302
  ...parentTcpPort !== void 0 ? { CAMSTACK_PARENT_TCP_PORT: String(parentTcpPort) } : {},
94264
94303
  ...parentUdsPath !== void 0 ? { CAMSTACK_PARENT_UDS_PATH: parentUdsPath } : {},
94304
+ ...runnerNativeAllocatorEnv(process.env),
94265
94305
  ...env
94266
94306
  };
94267
94307
  const heapFlags = runnerHeapFlags(addons);
94268
- capturedBroker?.logger.info(`[${runnerId}] heap profile: ${heavy ? "heavy" : "light"} flags=[${heapFlags.join(" ")}]`);
94308
+ 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"}`);
94269
94309
  const child = spawnFn(process.execPath, [...heapFlags, runnerPath], {
94270
94310
  env: childEnv,
94271
94311
  stdio: [
package/dist/index.mjs CHANGED
@@ -24,7 +24,7 @@ import { SystemConfigAddon } from "./builtins/system-config/system-config.addon.
24
24
  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
- import { $ as buildNativeCapProxy, A as createHubCapForwardService, At as buildHeapSample, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as runNpm, D as localProviderLink, Dt as HEAP_WATCH_INTERVAL_MS, E as ipcParentLink, Et as HEAP_RECLAIM_TRIGGER_MB, F as createUdsLogger, Ft as strandedMb, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, Mt as shouldReclaim, N as createUdsEventBus, Nt as startHeapWatch, O as HUB_CAP_FWD_ACTION, Ot as HEAP_WATCH_WARN_RATIO, P as udsChildLogToWorkerEntry, Pt as startRunnerHeapWatch, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as resolveNpmInvocation, T as ipcChildLink, Tt as HEAP_RECLAIM_MIN_INTERVAL_MS, 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, jt as createV8Reclaimer, k as HUB_CAP_FWD_SERVICE, kt as RUNNER_HEAP_WATCH_INTERVAL_MS, 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 resolveAddonClass, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-D1Ydw7J0.mjs";
27
+ import { $ as buildNativeCapProxy, A as createHubCapForwardService, At as buildHeapSample, B as AGENT_CAP_FWD_ACTION, C as brokerTransportLink, Ct as runNpm, D as localProviderLink, Dt as HEAP_WATCH_INTERVAL_MS, E as ipcParentLink, Et as HEAP_RECLAIM_TRIGGER_MB, F as createUdsLogger, Ft as strandedMb, G as callWithServiceDiscovery, H as CapRouteResolver, I as createUdsLoggerWithControl, J as UdsLocalTransportServer, K as createLocalTransport, L as LocalChildClient, M as createUdsEventBridge, Mt as shouldReclaim, N as createUdsEventBus, Nt as startHeapWatch, O as HUB_CAP_FWD_ACTION, Ot as HEAP_WATCH_WARN_RATIO, P as udsChildLogToWorkerEntry, Pt as startRunnerHeapWatch, Q as encodeFrame, R as LocalChildRegistry, S as brokerCallForCap, St as resolveNpmInvocation, T as ipcChildLink, Tt as HEAP_RECLAIM_MIN_INTERVAL_MS, 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, jt as createV8Reclaimer, k as HUB_CAP_FWD_SERVICE, kt as RUNNER_HEAP_WATCH_INTERVAL_MS, 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 resolveAddonClass, y as CapUsageRegistry, yt as copyBundledNativeModules, z as UDS_NO_ROUTE_PREFIX } from "./manifest-python-deps-Dmcnb287.mjs";
28
28
  import { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-BWmQ5i-o.mjs";
29
29
  import { C as evaluateExistingCert, S as SERVER_AUTH_OID, _ as CA_COMMON_NAME, a as closeLanHttp, b as LEAF_RENEWAL_WINDOW_DAYS, c as readExtraSans, d as writeExtraSans, f as writeTlsMode, g as reissueTlsLeaf, h as loadTlsCert, i as bindPendingLanHttp, l as readTlsAccessStatus, m as ensureTlsCert, n as allFamiliesListenHost, o as readLanHttpState, p as validateUploadedTls, r as applyLanHttp, s as registerLanHttpHandler, t as DEFAULT_LAN_HTTP_PORT, u as readTlsMode, v as collectCertIdentity, x as MAX_LEAF_VALIDITY_DAYS, y as CA_VALIDITY_DAYS } from "./lan-http-bind-jKrj6OjQ.mjs";
30
30
  import { t as CustomActionRegistry } from "./custom-action-registry-F__gp_VX.mjs";
@@ -93969,6 +93969,34 @@ var CrashSupervisor = class {
93969
93969
  this.crashes.delete(runnerId);
93970
93970
  }
93971
93971
  };
93972
+ /**
93973
+ * Resolve the native-allocator env for a child runner from the parent's env.
93974
+ *
93975
+ * Precedence, highest first:
93976
+ * 1. `CAMSTACK_RUNNER_NATIVE_ALLOCATOR=off` — kill switch, returns nothing and
93977
+ * restores the glibc/sharp defaults. Recoverable without a rebuild.
93978
+ * 2. A value the operator already put in the environment (`MALLOC_ARENA_MAX`,
93979
+ * `VIPS_CONCURRENCY`) — inherited verbatim, never overwritten.
93980
+ * 3. `CAMSTACK_RUNNER_MALLOC_ARENA_MAX` — the CamStack override.
93981
+ * 4. The defaults above.
93982
+ *
93983
+ * A nonsense override falls back to the default rather than being passed through:
93984
+ * glibc reads `MALLOC_ARENA_MAX=0` as "unbounded", which is the very state this
93985
+ * exists to leave.
93986
+ */
93987
+ function runnerNativeAllocatorEnv(parentEnv, platform = process.platform) {
93988
+ if (platform !== "linux") return {};
93989
+ if (parentEnv["CAMSTACK_RUNNER_NATIVE_ALLOCATOR"] === "off") return {};
93990
+ return {
93991
+ MALLOC_ARENA_MAX: parentEnv["MALLOC_ARENA_MAX"] ?? String(positiveInt(parentEnv["CAMSTACK_RUNNER_MALLOC_ARENA_MAX"], 2)),
93992
+ VIPS_CONCURRENCY: parentEnv["VIPS_CONCURRENCY"] ?? String(1)
93993
+ };
93994
+ }
93995
+ function positiveInt(raw, fallback) {
93996
+ if (raw === void 0) return fallback;
93997
+ const n = Number.parseInt(raw, 10);
93998
+ return Number.isFinite(n) && n > 0 ? n : fallback;
93999
+ }
93972
94000
  //#endregion
93973
94001
  //#region src/kernel/moleculer/process-service.ts
93974
94002
  /**
@@ -94129,7 +94157,18 @@ function isHeavyRunner(addons) {
94129
94157
  *
94130
94158
  * A runner whose working set genuinely does not fit declares its own number in its manifest
94131
94159
  * (`execution.maxOldSpaceMb`) — the number belongs next to the addon that knows its working set.
94132
- * `stream-broker` (1323 MB measured, frame buffers) is the one shipped addon that does.
94160
+ * `recorder` (3072 MB; peak 2341 MB heapTotal during the deferred archive walk) is the one
94161
+ * shipped addon that does. `stream-broker` does NOT, despite 1585 MB peak RSS: ~1 GB of that is
94162
+ * `arrayBuffers` (47 RTP rings), which this flag does not bound and never will — its actual old
94163
+ * space peaks at 224 MB. Size the declaration from `heapTotal` in the `[mem]` heartbeat, never
94164
+ * from RSS.
94165
+ *
94166
+ * ## What this flag CANNOT do
94167
+ *
94168
+ * It bounds old space and nothing else — not `external`, not `arrayBuffers`, not what a native
94169
+ * addon mallocs. `hub/pipeline-analytics` reached 7334 MB RSS with this flag set to 1024 the
94170
+ * whole time, because 96% of it was native. That half is bounded in `runner-native-allocator.ts`.
94171
+ * Reaching for a bigger number here when RSS is the complaint is the standard wrong move.
94133
94172
  */
94134
94173
  var HEAVY_MAX_OLD_MB_DEFAULT = 1024;
94135
94174
  /**
@@ -94255,10 +94294,11 @@ function createProcessService(parentNodeId, dataDir, deps, parentTcpPort, parent
94255
94294
  CAMSTACK_LOG_LEVEL: process.env["CAMSTACK_LOG_LEVEL"] ?? "info",
94256
94295
  ...parentTcpPort !== void 0 ? { CAMSTACK_PARENT_TCP_PORT: String(parentTcpPort) } : {},
94257
94296
  ...parentUdsPath !== void 0 ? { CAMSTACK_PARENT_UDS_PATH: parentUdsPath } : {},
94297
+ ...runnerNativeAllocatorEnv(process.env),
94258
94298
  ...env
94259
94299
  };
94260
94300
  const heapFlags = runnerHeapFlags(addons);
94261
- capturedBroker?.logger.info(`[${runnerId}] heap profile: ${heavy ? "heavy" : "light"} flags=[${heapFlags.join(" ")}]`);
94301
+ 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"}`);
94262
94302
  const child = spawnFn(process.execPath, [...heapFlags, runnerPath], {
94263
94303
  env: childEnv,
94264
94304
  stdio: [
@@ -291,14 +291,17 @@ export interface RunnerHeapWatchOptions {
291
291
  *
292
292
  * ## Why heavy runners get hub-main's mechanism
293
293
  *
294
- * The media-path runners deliberately run with NO old-space ceiling
295
- * (`maxOldSpaceMb: 0` a flat cap turned a disk-stall backlog into
296
- * `Ineffective mark-compacts near heap limit` and killed recording, twice on
297
- * 2026-08-17). Uncapped, V8 feels no pressure, so a one-off burst becomes the
298
- * runner's permanent RSS: the recorder measured 2122MB holding a ~50MB index.
299
- * hub-main had exactly this shape and its reclaimer is proven live — 11 passes
300
- * in 4h, 375–1652MB returned per pass at 550–680ms each. Same mechanism, same
301
- * `shouldReclaim` stranded-over-trigger gate; never a second implementation.
294
+ * Because a heap ceiling cannot reach what actually strands in them. Every
295
+ * media-path runner now carries an old-space ceiling sized from its own
296
+ * measured peak (`addon-pipeline` manifest + its `manifest-heap-ceilings` spec),
297
+ * and that changes nothing here: what a heavy runner strands is NATIVE —
298
+ * `external`, `arrayBuffers`, glibc arena free lists none of which
299
+ * `--max-old-space-size` bounds. Measured 2026-08-25, `hub/pipeline-analytics`
300
+ * reported `stranded=2552MB` while holding an 81MB V8 heap under a 1024MB
301
+ * ceiling that was in force the whole time. hub-main had exactly this shape and
302
+ * its reclaimer is proven live — 11 passes in 4h, 375–1652MB returned per pass
303
+ * at 550–680ms each. Same mechanism, same `shouldReclaim` stranded-over-trigger
304
+ * gate; never a second implementation.
302
305
  *
303
306
  * ## The stall, priced for the runners that get it
304
307
  *
@@ -36,7 +36,18 @@ interface RunnerAddonSpec {
36
36
  *
37
37
  * A runner whose working set genuinely does not fit declares its own number in its manifest
38
38
  * (`execution.maxOldSpaceMb`) — the number belongs next to the addon that knows its working set.
39
- * `stream-broker` (1323 MB measured, frame buffers) is the one shipped addon that does.
39
+ * `recorder` (3072 MB; peak 2341 MB heapTotal during the deferred archive walk) is the one
40
+ * shipped addon that does. `stream-broker` does NOT, despite 1585 MB peak RSS: ~1 GB of that is
41
+ * `arrayBuffers` (47 RTP rings), which this flag does not bound and never will — its actual old
42
+ * space peaks at 224 MB. Size the declaration from `heapTotal` in the `[mem]` heartbeat, never
43
+ * from RSS.
44
+ *
45
+ * ## What this flag CANNOT do
46
+ *
47
+ * It bounds old space and nothing else — not `external`, not `arrayBuffers`, not what a native
48
+ * addon mallocs. `hub/pipeline-analytics` reached 7334 MB RSS with this flag set to 1024 the
49
+ * whole time, because 96% of it was native. That half is bounded in `runner-native-allocator.ts`.
50
+ * Reaching for a bigger number here when RSS is the complaint is the standard wrong move.
40
51
  */
41
52
  export declare const HEAVY_MAX_OLD_MB_DEFAULT = 1024;
42
53
  interface SpawnedProcess {
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Native-allocator environment for a forked addon runner.
3
+ *
4
+ * ## The half of a runner's memory that no V8 flag reaches
5
+ *
6
+ * `--max-old-space-size` bounds V8's old space and nothing else. It does not
7
+ * bound `external`, it does not bound `arrayBuffers`, and it does not bound what
8
+ * a native addon (`sharp`/`libvips`, decoders, codecs) allocates through `malloc`.
9
+ *
10
+ * Measured on the live hub, 2026-08-25, over a ~6h window of `[mem]` heartbeats:
11
+ *
12
+ * hub/pipeline-analytics rss 309-7334MB heapTotal max 343MB external max 5479MB
13
+ *
14
+ * It already carried `--max-old-space-size=1024`, so the flag was in force the
15
+ * whole time and the process still reached 7.3GB — because 96% of it was never
16
+ * V8's to bound. Its reclaim line names the shape exactly:
17
+ *
18
+ * reclaim hub/pipeline-analytics stranded=2552MB rss=2765MB→2647MB
19
+ * freed=118MB arrayBuffers=81MB→29MB took=58ms
20
+ *
21
+ * The `arrayBuffers` FLOOR returns to ~1MB every cycle, so this is not a leak.
22
+ * It is glibc holding freed pages: with `MALLOC_ARENA_MAX` unset, glibc opens up
23
+ * to `8 × ncpu` arenas — 160 on this 20-core host — and each keeps its own free
24
+ * list, which is returned to the OS only when the top of that arena is free.
25
+ *
26
+ * ## Why this matters more than the cgroup number suggests
27
+ *
28
+ * The container is capped at 24GiB and `memory.current` rides at 21-24GB. Near
29
+ * the limit the kernel performs direct reclaim IN THE CONTEXT OF THE TASK THAT
30
+ * ALLOCATES, so every process in the container stalls together — measured as
31
+ * four idle, mutually unrelated runners sharing one ~39s stall in the same
32
+ * sample. Returning stranded pages is therefore not a tidiness win; it is what
33
+ * keeps unrelated runners off the direct-reclaim path.
34
+ *
35
+ * ## Why `VIPS_CONCURRENCY` is welded to it
36
+ *
37
+ * `sharp` reads `MALLOC_ARENA_MAX` in its NATIVE binding (not in JS — grepping
38
+ * the bundle finds nothing). Unset on glibc, sharp pins libvips concurrency to 1
39
+ * as its own fragmentation defence; the moment the variable appears it restores
40
+ * concurrency to `availableParallelism()`. Verified in the live container:
41
+ *
42
+ * MALLOC_ARENA_MAX unset → sharp.concurrency() === 1
43
+ * MALLOC_ARENA_MAX=2 → sharp.concurrency() === 20
44
+ * MALLOC_ARENA_MAX=2 VIPS_CONCURRENCY=1 → sharp.concurrency() === 1
45
+ *
46
+ * Shipping the arena bound alone would therefore have quietly shipped a 1→20
47
+ * libvips threading change to the busiest addon on a host already at ~75% CPU,
48
+ * under the banner of a memory fix. This module ships the allocator change and
49
+ * ONLY the allocator change; raising libvips concurrency is a separate decision.
50
+ *
51
+ * ## What it costs, measured
52
+ *
53
+ * Same container, same 12-image corpus, same pipeline as analytics (decode →
54
+ * extract → resize → encode → raw), 3 interleaved pairs so host load cancels,
55
+ * concurrency pinned to 1 on both arms:
56
+ *
57
+ * arenas default : mean 51.9ms p50 60.0ms p90 67.3ms p99 70.7ms rss 505MB
58
+ * arenas=2 : mean 51.5ms p50 57.6ms p90 67.2ms p99 72.9ms rss 354MB
59
+ *
60
+ * Latency is unchanged inside noise (-0.8% mean, +3.1% p99); the same work is
61
+ * done in 30% less RSS.
62
+ */
63
+ /** Bounded glibc arenas per forked runner. */
64
+ export declare const RUNNER_MALLOC_ARENA_MAX_DEFAULT = 2;
65
+ /**
66
+ * libvips worker threads per forked runner. `1` is not a tuning choice — it is
67
+ * sharp's OWN default on glibc, pinned here so that bounding the arenas does not
68
+ * change it as a side effect. See the module header.
69
+ */
70
+ export declare const RUNNER_VIPS_CONCURRENCY_DEFAULT = 1;
71
+ /**
72
+ * The native-allocator variables to add to a runner's environment. Both keys are
73
+ * present or both are absent — see the `VIPS_CONCURRENCY` note in the module
74
+ * header for why one without the other is the dangerous configuration.
75
+ */
76
+ export interface RunnerNativeAllocatorEnv {
77
+ readonly MALLOC_ARENA_MAX?: string;
78
+ readonly VIPS_CONCURRENCY?: string;
79
+ }
80
+ /**
81
+ * Resolve the native-allocator env for a child runner from the parent's env.
82
+ *
83
+ * Precedence, highest first:
84
+ * 1. `CAMSTACK_RUNNER_NATIVE_ALLOCATOR=off` — kill switch, returns nothing and
85
+ * restores the glibc/sharp defaults. Recoverable without a rebuild.
86
+ * 2. A value the operator already put in the environment (`MALLOC_ARENA_MAX`,
87
+ * `VIPS_CONCURRENCY`) — inherited verbatim, never overwritten.
88
+ * 3. `CAMSTACK_RUNNER_MALLOC_ARENA_MAX` — the CamStack override.
89
+ * 4. The defaults above.
90
+ *
91
+ * A nonsense override falls back to the default rather than being passed through:
92
+ * glibc reads `MALLOC_ARENA_MAX=0` as "unbounded", which is the very state this
93
+ * exists to leave.
94
+ */
95
+ export declare function runnerNativeAllocatorEnv(parentEnv: NodeJS.ProcessEnv, platform?: NodeJS.Platform): RunnerNativeAllocatorEnv;
@@ -422,14 +422,17 @@ var RUNNER_HEAP_WATCH_INTERVAL_MS = 3e5;
422
422
  *
423
423
  * ## Why heavy runners get hub-main's mechanism
424
424
  *
425
- * The media-path runners deliberately run with NO old-space ceiling
426
- * (`maxOldSpaceMb: 0` a flat cap turned a disk-stall backlog into
427
- * `Ineffective mark-compacts near heap limit` and killed recording, twice on
428
- * 2026-08-17). Uncapped, V8 feels no pressure, so a one-off burst becomes the
429
- * runner's permanent RSS: the recorder measured 2122MB holding a ~50MB index.
430
- * hub-main had exactly this shape and its reclaimer is proven live — 11 passes
431
- * in 4h, 375–1652MB returned per pass at 550–680ms each. Same mechanism, same
432
- * `shouldReclaim` stranded-over-trigger gate; never a second implementation.
425
+ * Because a heap ceiling cannot reach what actually strands in them. Every
426
+ * media-path runner now carries an old-space ceiling sized from its own
427
+ * measured peak (`addon-pipeline` manifest + its `manifest-heap-ceilings` spec),
428
+ * and that changes nothing here: what a heavy runner strands is NATIVE —
429
+ * `external`, `arrayBuffers`, glibc arena free lists none of which
430
+ * `--max-old-space-size` bounds. Measured 2026-08-25, `hub/pipeline-analytics`
431
+ * reported `stranded=2552MB` while holding an 81MB V8 heap under a 1024MB
432
+ * ceiling that was in force the whole time. hub-main had exactly this shape and
433
+ * its reclaimer is proven live — 11 passes in 4h, 375–1652MB returned per pass
434
+ * at 550–680ms each. Same mechanism, same `shouldReclaim` stranded-over-trigger
435
+ * gate; never a second implementation.
433
436
  *
434
437
  * ## The stall, priced for the runners that get it
435
438
  *
@@ -453,7 +456,7 @@ function startRunnerHeapWatch(options) {
453
456
  const reclaimer = createV8Reclaimer();
454
457
  reclaimOptions = reclaimer === void 0 ? void 0 : { reclaim: reclaimer };
455
458
  }
456
- return startHeapWatch(options.label, options.sink, intervalMs, reclaimOptions, void 0, void 0, false);
459
+ return startHeapWatch(options.label, options.sink, intervalMs, reclaimOptions, void 0, void 0, true);
457
460
  }
458
461
  //#endregion
459
462
  //#region src/kernel/moleculer/addon-data-plane-facility.ts
@@ -418,14 +418,17 @@ var RUNNER_HEAP_WATCH_INTERVAL_MS = 3e5;
418
418
  *
419
419
  * ## Why heavy runners get hub-main's mechanism
420
420
  *
421
- * The media-path runners deliberately run with NO old-space ceiling
422
- * (`maxOldSpaceMb: 0` a flat cap turned a disk-stall backlog into
423
- * `Ineffective mark-compacts near heap limit` and killed recording, twice on
424
- * 2026-08-17). Uncapped, V8 feels no pressure, so a one-off burst becomes the
425
- * runner's permanent RSS: the recorder measured 2122MB holding a ~50MB index.
426
- * hub-main had exactly this shape and its reclaimer is proven live — 11 passes
427
- * in 4h, 375–1652MB returned per pass at 550–680ms each. Same mechanism, same
428
- * `shouldReclaim` stranded-over-trigger gate; never a second implementation.
421
+ * Because a heap ceiling cannot reach what actually strands in them. Every
422
+ * media-path runner now carries an old-space ceiling sized from its own
423
+ * measured peak (`addon-pipeline` manifest + its `manifest-heap-ceilings` spec),
424
+ * and that changes nothing here: what a heavy runner strands is NATIVE —
425
+ * `external`, `arrayBuffers`, glibc arena free lists none of which
426
+ * `--max-old-space-size` bounds. Measured 2026-08-25, `hub/pipeline-analytics`
427
+ * reported `stranded=2552MB` while holding an 81MB V8 heap under a 1024MB
428
+ * ceiling that was in force the whole time. hub-main had exactly this shape and
429
+ * its reclaimer is proven live — 11 passes in 4h, 375–1652MB returned per pass
430
+ * at 550–680ms each. Same mechanism, same `shouldReclaim` stranded-over-trigger
431
+ * gate; never a second implementation.
429
432
  *
430
433
  * ## The stall, priced for the runners that get it
431
434
  *
@@ -449,7 +452,7 @@ function startRunnerHeapWatch(options) {
449
452
  const reclaimer = createV8Reclaimer();
450
453
  reclaimOptions = reclaimer === void 0 ? void 0 : { reclaim: reclaimer };
451
454
  }
452
- return startHeapWatch(options.label, options.sink, intervalMs, reclaimOptions, void 0, void 0, false);
455
+ return startHeapWatch(options.label, options.sink, intervalMs, reclaimOptions, void 0, void 0, true);
453
456
  }
454
457
  //#endregion
455
458
  //#region src/kernel/moleculer/addon-data-plane-facility.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.124",
3
+ "version": "1.2.126",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",