@camstack/addon-post-analysis 1.2.92 → 1.2.93
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{dist-VnaTfwAL.mjs → dist-3QRyOemL.mjs} +321 -3
- package/dist/{dist-BfrtqGyh.js → dist-DYoWeRRt.js} +338 -2
- package/dist/embedding-encoder/index.js +171 -4
- package/dist/embedding-encoder/index.mjs +171 -4
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BIDGLXRs.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-C4-ljPpt.mjs} +3 -3
- package/dist/pipeline-analytics/{hostInit-CpWvyhen.mjs → hostInit-BoUejl7E.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +32 -1
- package/dist/pipeline-analytics/index.mjs +32 -1
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +3 -2
|
@@ -29,7 +29,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
29
29
|
enumerable: true
|
|
30
30
|
}) : target, mod));
|
|
31
31
|
//#endregion
|
|
32
|
-
//#region ../types/dist/event-category-
|
|
32
|
+
//#region ../types/dist/event-category-XfKNtfCc.mjs
|
|
33
33
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
34
34
|
EventCategory["SystemBoot"] = "system.boot";
|
|
35
35
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -68,6 +68,22 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
68
68
|
EventCategory["AddonInstalled"] = "addon.installed";
|
|
69
69
|
EventCategory["AddonUninstalled"] = "addon.uninstalled";
|
|
70
70
|
EventCategory["AddonCrashed"] = "addon.crashed";
|
|
71
|
+
/**
|
|
72
|
+
* A RUNNER the D6 crash circuit-breaker gave up on — terminal.
|
|
73
|
+
*
|
|
74
|
+
* `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
|
|
75
|
+
* respawned"); this is the one that never resolves itself. Emitted exactly
|
|
76
|
+
* once per trip, by `process-service.ts`, carrying the node, the runner, the
|
|
77
|
+
* addons it hosted and the crash count that tripped the breaker.
|
|
78
|
+
*
|
|
79
|
+
* It exists because a runner marked terminally `failed` used to be SILENT:
|
|
80
|
+
* on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
|
|
81
|
+
* errors, the breaker stopped respawning it (correctly), `/health` kept
|
|
82
|
+
* returning 200, and fleet-wide recording was gone for 3h15 before the
|
|
83
|
+
* operator's phone told him. The Notification Center's system-event intake
|
|
84
|
+
* consumes this category and maps it to the `addon-crash-loop` kind.
|
|
85
|
+
*/
|
|
86
|
+
EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
|
|
71
87
|
EventCategory["AddonError"] = "addon.error";
|
|
72
88
|
EventCategory["AddonPageReady"] = "addon.page-ready";
|
|
73
89
|
EventCategory["AddonWidgetReady"] = "addon.widget-ready";
|
|
@@ -14845,6 +14861,7 @@ var NcSystemEventKindSchema = _enum([
|
|
|
14845
14861
|
"node-offline",
|
|
14846
14862
|
"node-inference-unavailable",
|
|
14847
14863
|
"detection-blind",
|
|
14864
|
+
"addon-crash-loop",
|
|
14848
14865
|
"addon-update-available",
|
|
14849
14866
|
"server-update-available",
|
|
14850
14867
|
"alarm-triggered",
|
|
@@ -15970,6 +15987,10 @@ var NC_CONDITION_CATALOG = [
|
|
|
15970
15987
|
value: "detection-blind",
|
|
15971
15988
|
label: "Camera detecting nothing"
|
|
15972
15989
|
},
|
|
15990
|
+
{
|
|
15991
|
+
value: "addon-crash-loop",
|
|
15992
|
+
label: "Addon stopped after repeated crashes"
|
|
15993
|
+
},
|
|
15973
15994
|
{
|
|
15974
15995
|
value: "addon-update-available",
|
|
15975
15996
|
label: "Addon update available"
|
|
@@ -38568,7 +38589,7 @@ function systemEventFilterApplies(kind, filter) {
|
|
|
38568
38589
|
switch (filter) {
|
|
38569
38590
|
case "deviceIds": return kind.startsWith("device-") || kind.startsWith("stream-") || kind === "detection-blind" || kind === "alarm-triggered" || kind === "export-completed";
|
|
38570
38591
|
case "deviceTypes": return kind.startsWith("device-") || kind === "detection-blind";
|
|
38571
|
-
case "nodeIds": return kind.startsWith("node-") || kind === "addon-update-available" || kind === "addon-updated" || kind === "server-update-available" || kind === "server-updated";
|
|
38592
|
+
case "nodeIds": return kind.startsWith("node-") || kind === "addon-crash-loop" || kind === "addon-update-available" || kind === "addon-updated" || kind === "server-update-available" || kind === "server-updated";
|
|
38572
38593
|
case "packageNames": return kind.endsWith("update-available") || kind === "addon-updated" || kind === "server-updated";
|
|
38573
38594
|
}
|
|
38574
38595
|
}
|
|
@@ -39025,6 +39046,303 @@ function cosineSimilarity(a, b) {
|
|
|
39025
39046
|
function hfModelUrl(repo, path) {
|
|
39026
39047
|
return `https://huggingface.co/${repo}/resolve/main/${path}`;
|
|
39027
39048
|
}
|
|
39049
|
+
var MB = 1024 * 1024;
|
|
39050
|
+
var DEFAULT_POOL_MEMORY_POLICY = {
|
|
39051
|
+
sampleIntervalMs: 6e4,
|
|
39052
|
+
baselineSettleSamples: 5,
|
|
39053
|
+
baselineSampleCount: 3,
|
|
39054
|
+
restartMultiple: 4,
|
|
39055
|
+
floorBytes: 1024 * MB,
|
|
39056
|
+
ceilingBytes: 3072 * MB,
|
|
39057
|
+
cooldownMs: 30 * 6e4,
|
|
39058
|
+
maxRestartsPerWindow: 6,
|
|
39059
|
+
restartWindowMs: 1440 * 6e4
|
|
39060
|
+
};
|
|
39061
|
+
/**
|
|
39062
|
+
* Resolve the policy from env overrides (`CAMSTACK_POOL_MEM_*`). Garbage or
|
|
39063
|
+
* absent values keep the default — an operator typo must never disable the
|
|
39064
|
+
* bound or set it to zero.
|
|
39065
|
+
*/
|
|
39066
|
+
function resolvePoolMemoryPolicy(env) {
|
|
39067
|
+
const num = (key, fallback, min) => {
|
|
39068
|
+
const raw = env[key];
|
|
39069
|
+
if (raw === void 0) return fallback;
|
|
39070
|
+
const parsed = Number(raw);
|
|
39071
|
+
return Number.isFinite(parsed) && parsed >= min ? parsed : fallback;
|
|
39072
|
+
};
|
|
39073
|
+
const d = DEFAULT_POOL_MEMORY_POLICY;
|
|
39074
|
+
return {
|
|
39075
|
+
sampleIntervalMs: num("CAMSTACK_POOL_MEM_INTERVAL_MS", d.sampleIntervalMs, 5e3),
|
|
39076
|
+
baselineSettleSamples: d.baselineSettleSamples,
|
|
39077
|
+
baselineSampleCount: d.baselineSampleCount,
|
|
39078
|
+
restartMultiple: num("CAMSTACK_POOL_MEM_MULTIPLE", d.restartMultiple, 1.5),
|
|
39079
|
+
floorBytes: num("CAMSTACK_POOL_MEM_FLOOR_MB", d.floorBytes / MB, 128) * MB,
|
|
39080
|
+
ceilingBytes: num("CAMSTACK_POOL_MEM_CEILING_MB", d.ceilingBytes / MB, 256) * MB,
|
|
39081
|
+
cooldownMs: num("CAMSTACK_POOL_MEM_COOLDOWN_MS", d.cooldownMs, 6e4),
|
|
39082
|
+
maxRestartsPerWindow: num("CAMSTACK_POOL_MEM_MAX_RESTARTS", d.maxRestartsPerWindow, 1),
|
|
39083
|
+
restartWindowMs: num("CAMSTACK_POOL_MEM_RESTART_WINDOW_MS", d.restartWindowMs, 6e4)
|
|
39084
|
+
};
|
|
39085
|
+
}
|
|
39086
|
+
/** Parse the fields this watchdog needs out of `/proc/<pid>/status` text.
|
|
39087
|
+
* Returns null when VmRSS is missing (dead pid, kernel thread, bad read). */
|
|
39088
|
+
function parseProcStatus(text) {
|
|
39089
|
+
const kb = (label) => {
|
|
39090
|
+
const match = text.match(new RegExp(`^${label}:\\s+(\\d+)\\s*kB`, "m"));
|
|
39091
|
+
return match ? Number(match[1]) * 1024 : null;
|
|
39092
|
+
};
|
|
39093
|
+
const rssBytes = kb("VmRSS");
|
|
39094
|
+
if (rssBytes === null) return null;
|
|
39095
|
+
const threadsMatch = text.match(/^Threads:\s+(\d+)/m);
|
|
39096
|
+
return {
|
|
39097
|
+
rssBytes,
|
|
39098
|
+
vmBytes: kb("VmSize") ?? 0,
|
|
39099
|
+
hwmBytes: kb("VmHWM") ?? 0,
|
|
39100
|
+
swapBytes: kb("VmSwap") ?? 0,
|
|
39101
|
+
threads: threadsMatch ? Number(threadsMatch[1]) : 0
|
|
39102
|
+
};
|
|
39103
|
+
}
|
|
39104
|
+
function initialPoolMemoryState() {
|
|
39105
|
+
return {
|
|
39106
|
+
settleSeen: 0,
|
|
39107
|
+
baselineWindow: [],
|
|
39108
|
+
baselineBytes: null,
|
|
39109
|
+
restartsAt: [],
|
|
39110
|
+
lastRestartAt: null
|
|
39111
|
+
};
|
|
39112
|
+
}
|
|
39113
|
+
function median(values) {
|
|
39114
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
39115
|
+
return sorted[Math.floor(sorted.length / 2)];
|
|
39116
|
+
}
|
|
39117
|
+
function advanceBaseline(state, rssBytes, policy) {
|
|
39118
|
+
if (state.baselineBytes !== null) return state;
|
|
39119
|
+
if (state.settleSeen < policy.baselineSettleSamples) return {
|
|
39120
|
+
...state,
|
|
39121
|
+
settleSeen: state.settleSeen + 1
|
|
39122
|
+
};
|
|
39123
|
+
const window = [...state.baselineWindow, rssBytes];
|
|
39124
|
+
if (window.length < policy.baselineSampleCount) return {
|
|
39125
|
+
...state,
|
|
39126
|
+
baselineWindow: window
|
|
39127
|
+
};
|
|
39128
|
+
return {
|
|
39129
|
+
...state,
|
|
39130
|
+
baselineWindow: window,
|
|
39131
|
+
baselineBytes: median(window)
|
|
39132
|
+
};
|
|
39133
|
+
}
|
|
39134
|
+
/** The one place a pool's restart threshold is computed. */
|
|
39135
|
+
function poolMemoryThreshold(baselineBytes, policy) {
|
|
39136
|
+
if (baselineBytes === null) return policy.ceilingBytes;
|
|
39137
|
+
return Math.min(policy.ceilingBytes, Math.max(policy.floorBytes, policy.restartMultiple * baselineBytes));
|
|
39138
|
+
}
|
|
39139
|
+
function pruneRestarts(restartsAt, nowMs, policy) {
|
|
39140
|
+
return restartsAt.filter((t) => nowMs - t < policy.restartWindowMs);
|
|
39141
|
+
}
|
|
39142
|
+
/**
|
|
39143
|
+
* Evaluate one RSS sample. Pure: returns the successor state plus the verdict.
|
|
39144
|
+
* The caller performs (and logs) the restart, then commits it via
|
|
39145
|
+
* {@link commitWatchdogRestart}.
|
|
39146
|
+
*/
|
|
39147
|
+
function evaluatePoolMemory(state, rssBytes, nowMs, policy) {
|
|
39148
|
+
const next = advanceBaseline(state, rssBytes, policy);
|
|
39149
|
+
const thresholdBytes = poolMemoryThreshold(next.baselineBytes, policy);
|
|
39150
|
+
if (rssBytes <= thresholdBytes) return {
|
|
39151
|
+
state: next,
|
|
39152
|
+
action: next.baselineBytes === null ? "baseline-pending" : "ok",
|
|
39153
|
+
baselineBytes: next.baselineBytes,
|
|
39154
|
+
thresholdBytes
|
|
39155
|
+
};
|
|
39156
|
+
const inWindow = pruneRestarts(next.restartsAt, nowMs, policy);
|
|
39157
|
+
const pruned = {
|
|
39158
|
+
...next,
|
|
39159
|
+
restartsAt: inWindow
|
|
39160
|
+
};
|
|
39161
|
+
if (inWindow.length >= policy.maxRestartsPerWindow) return {
|
|
39162
|
+
state: pruned,
|
|
39163
|
+
action: "exhausted",
|
|
39164
|
+
baselineBytes: next.baselineBytes,
|
|
39165
|
+
thresholdBytes
|
|
39166
|
+
};
|
|
39167
|
+
if (pruned.lastRestartAt !== null && nowMs - pruned.lastRestartAt < policy.cooldownMs) return {
|
|
39168
|
+
state: pruned,
|
|
39169
|
+
action: "cooldown",
|
|
39170
|
+
baselineBytes: next.baselineBytes,
|
|
39171
|
+
thresholdBytes
|
|
39172
|
+
};
|
|
39173
|
+
return {
|
|
39174
|
+
state: pruned,
|
|
39175
|
+
action: "restart",
|
|
39176
|
+
baselineBytes: next.baselineBytes,
|
|
39177
|
+
thresholdBytes
|
|
39178
|
+
};
|
|
39179
|
+
}
|
|
39180
|
+
/** Record a performed watchdog restart: budget consumed, baseline reset —
|
|
39181
|
+
* the recreated pool is a NEW process and gets a fresh settle window. */
|
|
39182
|
+
function commitWatchdogRestart(state, nowMs) {
|
|
39183
|
+
return {
|
|
39184
|
+
...initialPoolMemoryState(),
|
|
39185
|
+
restartsAt: [...state.restartsAt, nowMs],
|
|
39186
|
+
lastRestartAt: nowMs
|
|
39187
|
+
};
|
|
39188
|
+
}
|
|
39189
|
+
/** Reset baseline tracking (pool process replaced OUTSIDE the watchdog —
|
|
39190
|
+
* kernel OOM kill + respawn, idle-reaper eviction, tuning respawn). The
|
|
39191
|
+
* restart budget is kept: it bounds the WATCHDOG, not the pool. */
|
|
39192
|
+
function resetPoolBaseline(state) {
|
|
39193
|
+
return {
|
|
39194
|
+
...initialPoolMemoryState(),
|
|
39195
|
+
restartsAt: state.restartsAt,
|
|
39196
|
+
lastRestartAt: state.lastRestartAt
|
|
39197
|
+
};
|
|
39198
|
+
}
|
|
39199
|
+
/**
|
|
39200
|
+
* Pick ONE pool to restart this sweep — the worst overage ratio. Restarting
|
|
39201
|
+
* several pools at once (e.g. after a model rollout grew every baseline)
|
|
39202
|
+
* would take detection down on every camera simultaneously; one per sweep
|
|
39203
|
+
* means the survivors keep serving while the worst offender reloads, and the
|
|
39204
|
+
* next sweep (one interval later) takes the next one.
|
|
39205
|
+
*/
|
|
39206
|
+
function pickRestartCandidate(candidates) {
|
|
39207
|
+
if (candidates.length === 0) return null;
|
|
39208
|
+
let worst = candidates[0];
|
|
39209
|
+
for (const c of candidates) if (c.rssBytes / c.thresholdBytes > worst.rssBytes / worst.thresholdBytes) worst = c;
|
|
39210
|
+
return worst.key;
|
|
39211
|
+
}
|
|
39212
|
+
var PoolMemoryWatchdog = class {
|
|
39213
|
+
pools = /* @__PURE__ */ new Map();
|
|
39214
|
+
timer = null;
|
|
39215
|
+
sweeping = false;
|
|
39216
|
+
stopped = false;
|
|
39217
|
+
opts;
|
|
39218
|
+
now;
|
|
39219
|
+
setTimer;
|
|
39220
|
+
clearTimer;
|
|
39221
|
+
constructor(opts) {
|
|
39222
|
+
this.opts = opts;
|
|
39223
|
+
this.now = opts.now ?? (() => Date.now());
|
|
39224
|
+
this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
39225
|
+
this.clearTimer = opts.clearTimer ?? ((t) => clearTimeout(t));
|
|
39226
|
+
}
|
|
39227
|
+
start() {
|
|
39228
|
+
if (this.timer || this.stopped) return;
|
|
39229
|
+
this.arm();
|
|
39230
|
+
}
|
|
39231
|
+
stop() {
|
|
39232
|
+
this.stopped = true;
|
|
39233
|
+
if (this.timer) {
|
|
39234
|
+
this.clearTimer(this.timer);
|
|
39235
|
+
this.timer = null;
|
|
39236
|
+
}
|
|
39237
|
+
}
|
|
39238
|
+
arm() {
|
|
39239
|
+
const timer = this.setTimer(() => {
|
|
39240
|
+
this.sweep().finally(() => {
|
|
39241
|
+
if (!this.stopped) this.arm();
|
|
39242
|
+
});
|
|
39243
|
+
}, this.opts.policy.sampleIntervalMs);
|
|
39244
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
39245
|
+
this.timer = timer;
|
|
39246
|
+
}
|
|
39247
|
+
/** One sweep: sample → log every pool → restart at most one. Public so the
|
|
39248
|
+
* loop is testable without fake global timers. */
|
|
39249
|
+
async sweep() {
|
|
39250
|
+
if (this.sweeping) return;
|
|
39251
|
+
this.sweeping = true;
|
|
39252
|
+
try {
|
|
39253
|
+
await this.doSweep();
|
|
39254
|
+
} catch (err) {
|
|
39255
|
+
this.opts.log.warn("pool memory sweep failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
39256
|
+
} finally {
|
|
39257
|
+
this.sweeping = false;
|
|
39258
|
+
}
|
|
39259
|
+
}
|
|
39260
|
+
async doSweep() {
|
|
39261
|
+
const nowMs = this.now();
|
|
39262
|
+
const samples = await this.opts.sample();
|
|
39263
|
+
const candidates = [];
|
|
39264
|
+
const verdictByKey = /* @__PURE__ */ new Map();
|
|
39265
|
+
for (const t of samples) {
|
|
39266
|
+
const tracked = this.pools.get(t.key);
|
|
39267
|
+
let state = tracked?.state ?? initialPoolMemoryState();
|
|
39268
|
+
if (tracked && tracked.pids.length > 0 && !samePids(tracked.pids, t.pids)) {
|
|
39269
|
+
this.opts.log.warn("pool process changed outside the watchdog — re-baselining", { meta: {
|
|
39270
|
+
poolKey: t.key,
|
|
39271
|
+
previousPids: tracked.pids,
|
|
39272
|
+
pids: t.pids
|
|
39273
|
+
} });
|
|
39274
|
+
state = resetPoolBaseline(state);
|
|
39275
|
+
}
|
|
39276
|
+
const verdict = evaluatePoolMemory(state, t.rssBytes, nowMs, this.opts.policy);
|
|
39277
|
+
this.pools.set(t.key, {
|
|
39278
|
+
state: verdict.state,
|
|
39279
|
+
pids: t.pids
|
|
39280
|
+
});
|
|
39281
|
+
verdictByKey.set(t.key, verdict);
|
|
39282
|
+
this.opts.log.info("pool memory", { meta: {
|
|
39283
|
+
poolKey: t.key,
|
|
39284
|
+
pids: t.pids,
|
|
39285
|
+
rssMb: Math.round(t.rssBytes / MB),
|
|
39286
|
+
baselineMb: verdict.baselineBytes === null ? null : Math.round(verdict.baselineBytes / MB),
|
|
39287
|
+
thresholdMb: Math.round(verdict.thresholdBytes / MB),
|
|
39288
|
+
action: verdict.action,
|
|
39289
|
+
...t.meta
|
|
39290
|
+
} });
|
|
39291
|
+
if (verdict.action === "restart") candidates.push({
|
|
39292
|
+
key: t.key,
|
|
39293
|
+
rssBytes: t.rssBytes,
|
|
39294
|
+
thresholdBytes: verdict.thresholdBytes
|
|
39295
|
+
});
|
|
39296
|
+
else if (verdict.action === "cooldown") this.opts.log.warn("pool over memory threshold but inside restart cooldown", { meta: {
|
|
39297
|
+
poolKey: t.key,
|
|
39298
|
+
rssMb: Math.round(t.rssBytes / MB),
|
|
39299
|
+
thresholdMb: Math.round(verdict.thresholdBytes / MB),
|
|
39300
|
+
cooldownMs: this.opts.policy.cooldownMs
|
|
39301
|
+
} });
|
|
39302
|
+
else if (verdict.action === "exhausted") this.opts.log.error("pool memory watchdog gave up — restart budget exhausted, pool leaks faster than restarts can pay for; needs a heap profile (py-spy / tracemalloc)", { meta: {
|
|
39303
|
+
poolKey: t.key,
|
|
39304
|
+
rssMb: Math.round(t.rssBytes / MB),
|
|
39305
|
+
thresholdMb: Math.round(verdict.thresholdBytes / MB),
|
|
39306
|
+
maxRestartsPerWindow: this.opts.policy.maxRestartsPerWindow,
|
|
39307
|
+
restartWindowMs: this.opts.policy.restartWindowMs
|
|
39308
|
+
} });
|
|
39309
|
+
}
|
|
39310
|
+
for (const key of [...this.pools.keys()]) if (!samples.some((t) => t.key === key)) this.pools.delete(key);
|
|
39311
|
+
const pickedKey = pickRestartCandidate(candidates);
|
|
39312
|
+
if (pickedKey === null) return;
|
|
39313
|
+
const picked = candidates.find((c) => c.key === pickedKey);
|
|
39314
|
+
const verdict = verdictByKey.get(pickedKey);
|
|
39315
|
+
const tracked = this.pools.get(pickedKey);
|
|
39316
|
+
const restartsInWindow = tracked.state.restartsAt.length + 1;
|
|
39317
|
+
this.opts.log.warn("pool memory watchdog restarting pool — RSS over threshold", { meta: {
|
|
39318
|
+
poolKey: pickedKey,
|
|
39319
|
+
pids: tracked.pids,
|
|
39320
|
+
rssMb: Math.round(picked.rssBytes / MB),
|
|
39321
|
+
thresholdMb: Math.round(picked.thresholdBytes / MB),
|
|
39322
|
+
baselineMb: verdict.baselineBytes === null ? null : Math.round(verdict.baselineBytes / MB),
|
|
39323
|
+
restartsInWindow,
|
|
39324
|
+
deferredCandidates: candidates.filter((c) => c.key !== pickedKey).map((c) => c.key)
|
|
39325
|
+
} });
|
|
39326
|
+
try {
|
|
39327
|
+
await this.opts.restart(pickedKey);
|
|
39328
|
+
this.pools.set(pickedKey, {
|
|
39329
|
+
state: commitWatchdogRestart(tracked.state, this.now()),
|
|
39330
|
+
pids: []
|
|
39331
|
+
});
|
|
39332
|
+
} catch (err) {
|
|
39333
|
+
this.opts.log.error("pool memory watchdog restart failed", { meta: {
|
|
39334
|
+
poolKey: pickedKey,
|
|
39335
|
+
error: err instanceof Error ? err.message : String(err)
|
|
39336
|
+
} });
|
|
39337
|
+
}
|
|
39338
|
+
}
|
|
39339
|
+
};
|
|
39340
|
+
function samePids(a, b) {
|
|
39341
|
+
if (a.length !== b.length) return false;
|
|
39342
|
+
const sortedA = [...a].sort((x, y) => x - y);
|
|
39343
|
+
const sortedB = [...b].sort((x, y) => x - y);
|
|
39344
|
+
return sortedA.every((v, i) => v === sortedB[i]);
|
|
39345
|
+
}
|
|
39028
39346
|
/**
|
|
39029
39347
|
* Vector wire codec — base64 of little-endian Float32.
|
|
39030
39348
|
*
|
|
@@ -39234,6 +39552,12 @@ Object.defineProperty(exports, "OpsLogEntrySchema", {
|
|
|
39234
39552
|
return OpsLogEntrySchema;
|
|
39235
39553
|
}
|
|
39236
39554
|
});
|
|
39555
|
+
Object.defineProperty(exports, "PoolMemoryWatchdog", {
|
|
39556
|
+
enumerable: true,
|
|
39557
|
+
get: function() {
|
|
39558
|
+
return PoolMemoryWatchdog;
|
|
39559
|
+
}
|
|
39560
|
+
});
|
|
39237
39561
|
Object.defineProperty(exports, "RECORDING_EXPORT_MAX_READ_BYTES", {
|
|
39238
39562
|
enumerable: true,
|
|
39239
39563
|
get: function() {
|
|
@@ -39504,6 +39828,12 @@ Object.defineProperty(exports, "object", {
|
|
|
39504
39828
|
return object;
|
|
39505
39829
|
}
|
|
39506
39830
|
});
|
|
39831
|
+
Object.defineProperty(exports, "parseProcStatus", {
|
|
39832
|
+
enumerable: true,
|
|
39833
|
+
get: function() {
|
|
39834
|
+
return parseProcStatus;
|
|
39835
|
+
}
|
|
39836
|
+
});
|
|
39507
39837
|
Object.defineProperty(exports, "partialRecord", {
|
|
39508
39838
|
enumerable: true,
|
|
39509
39839
|
get: function() {
|
|
@@ -39540,6 +39870,12 @@ Object.defineProperty(exports, "record", {
|
|
|
39540
39870
|
return record;
|
|
39541
39871
|
}
|
|
39542
39872
|
});
|
|
39873
|
+
Object.defineProperty(exports, "resolvePoolMemoryPolicy", {
|
|
39874
|
+
enumerable: true,
|
|
39875
|
+
get: function() {
|
|
39876
|
+
return resolvePoolMemoryPolicy;
|
|
39877
|
+
}
|
|
39878
|
+
});
|
|
39543
39879
|
Object.defineProperty(exports, "sceneMonitorCapability", {
|
|
39544
39880
|
enumerable: true,
|
|
39545
39881
|
get: function() {
|
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-DYoWeRRt.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
let node_fs$1 = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
node_fs = require_dist.__toESM(node_fs);
|
|
@@ -10,9 +10,56 @@ let node_path = require("node:path");
|
|
|
10
10
|
let node_path$1 = require_dist.__toESM(node_path, 1);
|
|
11
11
|
node_path = require_dist.__toESM(node_path);
|
|
12
12
|
require("node:crypto");
|
|
13
|
+
let node_child_process = require("node:child_process");
|
|
13
14
|
let node_util = require("node:util");
|
|
14
15
|
let node_zlib = require("node:zlib");
|
|
15
|
-
|
|
16
|
+
//#region src/embedding-encoder/shared/process-memory.ts
|
|
17
|
+
/**
|
|
18
|
+
* Read a child process's memory footprint for the pool memory watchdog.
|
|
19
|
+
*
|
|
20
|
+
* Deliberate twin of `packages/addon-pipeline/src/shared/process-memory.ts`:
|
|
21
|
+
* addons must not import each other, and `@camstack/system` stays EXTERNAL in
|
|
22
|
+
* addon bundles (a symbol added there would be missing at runtime until the
|
|
23
|
+
* next framework train). The pure parse (`parseProcStatus`) and the whole
|
|
24
|
+
* watchdog policy live once in `@camstack/types` (inlined at build time);
|
|
25
|
+
* only this thin `fs`/`ps` read is duplicated.
|
|
26
|
+
*
|
|
27
|
+
* Linux (the fleet): `/proc/<pid>/status` — RSS, VmSize, VmHWM (peak RSS ≈
|
|
28
|
+
* getrusage maxrss), VmSwap, Threads in one read. macOS (dev): `ps -o rss=`
|
|
29
|
+
* gives RSS alone. Returns null when the pid is gone — the CALLER logs the
|
|
30
|
+
* dropped sample.
|
|
31
|
+
*/
|
|
32
|
+
var IS_LINUX = process.platform === "linux";
|
|
33
|
+
async function readProcessMemory(pid) {
|
|
34
|
+
if (IS_LINUX) try {
|
|
35
|
+
return require_dist.parseProcStatus(await node_fs.promises.readFile(`/proc/${pid}/status`, "utf8"));
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return readViaPs(pid);
|
|
40
|
+
}
|
|
41
|
+
function readViaPs(pid) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
(0, node_child_process.execFile)("ps", [
|
|
44
|
+
"-o",
|
|
45
|
+
"rss=",
|
|
46
|
+
"-p",
|
|
47
|
+
String(pid)
|
|
48
|
+
], { timeout: 2e3 }, (err, stdout) => {
|
|
49
|
+
if (err) return resolve(null);
|
|
50
|
+
const kb = Number(stdout.trim());
|
|
51
|
+
if (!Number.isFinite(kb) || kb <= 0) return resolve(null);
|
|
52
|
+
resolve({
|
|
53
|
+
rssBytes: kb * 1024,
|
|
54
|
+
vmBytes: 0,
|
|
55
|
+
hwmBytes: 0,
|
|
56
|
+
swapBytes: 0,
|
|
57
|
+
threads: 0
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
16
63
|
//#region ../system/dist/file-data-plane-CuE_hBli.mjs
|
|
17
64
|
function isNonEmptyFile(filePath) {
|
|
18
65
|
return node_fs$1.existsSync(filePath) && node_fs$1.statSync(filePath).size > 0;
|
|
@@ -499,8 +546,20 @@ var PythonRawTensorEngine = class {
|
|
|
499
546
|
if (ready.length !== 1 || ready[0] !== 1) throw new Error("PythonRawTensorEngine: unexpected ready frame");
|
|
500
547
|
this.log.info("ONNX raw-tensor engine ready (embedded Python)", { meta: { modelPath: this.modelPath } });
|
|
501
548
|
}
|
|
549
|
+
/** Native pid of the Python subprocess — sampled by the pool memory
|
|
550
|
+
* watchdog (`/proc/<pid>/status`). */
|
|
551
|
+
getPid() {
|
|
552
|
+
return this.process?.pid ?? null;
|
|
553
|
+
}
|
|
554
|
+
/** Inference requests served since spawn — the denominator that separates
|
|
555
|
+
* "RSS grew per unit of work" (leak) from "RSS grew because it worked". */
|
|
556
|
+
getRequestCount() {
|
|
557
|
+
return this.requestCount;
|
|
558
|
+
}
|
|
559
|
+
requestCount = 0;
|
|
502
560
|
async run(input, inputShape) {
|
|
503
561
|
if (!this.process?.stdin) throw new Error("PythonRawTensorEngine: not initialized — call initialize() first");
|
|
562
|
+
this.requestCount++;
|
|
504
563
|
const ndims = inputShape.length;
|
|
505
564
|
const meta = Buffer.allocUnsafe(1 + ndims * 4);
|
|
506
565
|
meta.writeUInt8(ndims, 0);
|
|
@@ -625,9 +684,21 @@ var PythonTextEncoderEngine = class {
|
|
|
625
684
|
tokenizerPath: this.tokenizerPath
|
|
626
685
|
} });
|
|
627
686
|
}
|
|
687
|
+
/** Native pid of the Python subprocess — sampled by the pool memory
|
|
688
|
+
* watchdog (`/proc/<pid>/status`). */
|
|
689
|
+
getPid() {
|
|
690
|
+
return this.process?.pid ?? null;
|
|
691
|
+
}
|
|
692
|
+
/** Encode requests served since spawn — the denominator that separates
|
|
693
|
+
* "RSS grew per unit of work" (leak) from "RSS grew because it worked". */
|
|
694
|
+
getRequestCount() {
|
|
695
|
+
return this.requestCount;
|
|
696
|
+
}
|
|
697
|
+
requestCount = 0;
|
|
628
698
|
/** Tokenize + encode `text` into the model embedding (float32). */
|
|
629
699
|
async encode(text) {
|
|
630
700
|
if (!this.process?.stdin) throw new Error("PythonTextEncoderEngine: not initialized — call initialize() first");
|
|
701
|
+
this.requestCount++;
|
|
631
702
|
const payload = Buffer.from(text, "utf-8");
|
|
632
703
|
const lenBuf = Buffer.allocUnsafe(4);
|
|
633
704
|
lenBuf.writeUInt32LE(payload.length, 0);
|
|
@@ -753,6 +824,22 @@ var EmbeddingEncoderAddon = class extends require_dist.BaseAddon {
|
|
|
753
824
|
imageRawEngine = null;
|
|
754
825
|
textEngine = null;
|
|
755
826
|
models = null;
|
|
827
|
+
/**
|
|
828
|
+
* RSS bound + periodic memory telemetry for the two Python engines
|
|
829
|
+
* (`raw_tensor_inference.py` — one of the four leaking pools in the
|
|
830
|
+
* 2026-08-18 OOM audit — and `text_encoder_inference.py`). Shared
|
|
831
|
+
* watchdog from `@camstack/types`; a restart here is dispose + null, and
|
|
832
|
+
* the next encode lazily re-resolves the engine (the exact recovery an
|
|
833
|
+
* operator model change already uses). Started in `onInitialize`,
|
|
834
|
+
* stopped in `onShutdown`.
|
|
835
|
+
*/
|
|
836
|
+
memoryGuard = null;
|
|
837
|
+
/** Single-flight guards for the lazy engine builds — a watchdog restart
|
|
838
|
+
* followed by two concurrent encodes must not spawn the engine twice
|
|
839
|
+
* (the same bug detection-pipeline fixed three times; see its
|
|
840
|
+
* `engineFactoryInflight`). */
|
|
841
|
+
imageEngineInflight = null;
|
|
842
|
+
textEngineInflight = null;
|
|
756
843
|
constructor() {
|
|
757
844
|
super({
|
|
758
845
|
modelId: DEFAULT_CLIP_MODEL,
|
|
@@ -763,11 +850,75 @@ var EmbeddingEncoderAddon = class extends require_dist.BaseAddon {
|
|
|
763
850
|
async onInitialize() {
|
|
764
851
|
const modelsDir = await this.resolveModelsDir();
|
|
765
852
|
this.models = new ModelDownloadService(modelsDir, [...CLIP_IMAGE_MODELS, ...CLIP_TEXT_MODELS]);
|
|
853
|
+
this.memoryGuard = new require_dist.PoolMemoryWatchdog({
|
|
854
|
+
policy: require_dist.resolvePoolMemoryPolicy(process.env),
|
|
855
|
+
log: this.ctx.logger.child("pool-memory"),
|
|
856
|
+
sample: () => this.sampleEngineMemory(),
|
|
857
|
+
restart: (key) => this.restartEngineForMemory(key)
|
|
858
|
+
});
|
|
859
|
+
this.memoryGuard.start();
|
|
766
860
|
return [{
|
|
767
861
|
capability: require_dist.embeddingEncoderCapability,
|
|
768
862
|
provider: this
|
|
769
863
|
}];
|
|
770
864
|
}
|
|
865
|
+
/** Watchdog sample: one telemetry row per live Python engine. A pid the
|
|
866
|
+
* read cannot see is logged, never silently skipped. */
|
|
867
|
+
async sampleEngineMemory() {
|
|
868
|
+
const engines = [{
|
|
869
|
+
key: "clip-image",
|
|
870
|
+
pid: this.imageRawEngine?.getPid() ?? null,
|
|
871
|
+
requests: this.imageRawEngine?.getRequestCount() ?? 0
|
|
872
|
+
}, {
|
|
873
|
+
key: "clip-text",
|
|
874
|
+
pid: this.textEngine?.getPid() ?? null,
|
|
875
|
+
requests: this.textEngine?.getRequestCount() ?? 0
|
|
876
|
+
}];
|
|
877
|
+
const out = [];
|
|
878
|
+
for (const engine of engines) {
|
|
879
|
+
if (engine.pid === null) continue;
|
|
880
|
+
const mem = await readProcessMemory(engine.pid);
|
|
881
|
+
if (!mem) {
|
|
882
|
+
this.ctx.logger.warn("embedding engine memory sample dropped", { meta: {
|
|
883
|
+
poolKey: engine.key,
|
|
884
|
+
pid: engine.pid
|
|
885
|
+
} });
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
const mb = (bytes) => Math.round(bytes / (1024 * 1024));
|
|
889
|
+
out.push({
|
|
890
|
+
key: engine.key,
|
|
891
|
+
pids: [engine.pid],
|
|
892
|
+
rssBytes: mem.rssBytes,
|
|
893
|
+
meta: {
|
|
894
|
+
modelId: this.config.modelId,
|
|
895
|
+
vmMb: mb(mem.vmBytes),
|
|
896
|
+
peakRssMb: mb(mem.hwmBytes),
|
|
897
|
+
swapMb: mb(mem.swapBytes),
|
|
898
|
+
threads: mem.threads,
|
|
899
|
+
requestsServed: engine.requests
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
return out;
|
|
904
|
+
}
|
|
905
|
+
/** Watchdog restart: dispose + null; the next encode re-resolves lazily
|
|
906
|
+
* through the single-flight ensure. An in-flight encode fails once — its
|
|
907
|
+
* callers are the retry-until-landed bulk passes (D58) and RPCs that
|
|
908
|
+
* surface errors, never silent loss. */
|
|
909
|
+
async restartEngineForMemory(key) {
|
|
910
|
+
if (key === "clip-image") {
|
|
911
|
+
const engine = this.imageRawEngine;
|
|
912
|
+
if (!engine) return;
|
|
913
|
+
this.imageRawEngine = null;
|
|
914
|
+
await engine.dispose();
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
const engine = this.textEngine;
|
|
918
|
+
if (!engine) return;
|
|
919
|
+
this.textEngine = null;
|
|
920
|
+
await engine.dispose();
|
|
921
|
+
}
|
|
771
922
|
async encode(input) {
|
|
772
923
|
const { crop, width, height } = input;
|
|
773
924
|
await this.ensureImageEngine();
|
|
@@ -811,17 +962,31 @@ var EmbeddingEncoderAddon = class extends require_dist.BaseAddon {
|
|
|
811
962
|
}
|
|
812
963
|
async ensureImageEngine() {
|
|
813
964
|
if (this.imageRawEngine) return;
|
|
965
|
+
if (this.imageEngineInflight) return this.imageEngineInflight;
|
|
814
966
|
const meta = getModelMeta(this.config.modelId);
|
|
815
967
|
const imageEntry = CLIP_IMAGE_MODELS.find((m) => m.id === meta.imageModelId);
|
|
816
968
|
if (!imageEntry) throw new Error(`EmbeddingEncoderAddon: unknown image model "${meta.imageModelId}"`);
|
|
817
|
-
|
|
969
|
+
const inflight = this.resolveForEntry(imageEntry, "image");
|
|
970
|
+
this.imageEngineInflight = inflight;
|
|
971
|
+
try {
|
|
972
|
+
await inflight;
|
|
973
|
+
} finally {
|
|
974
|
+
this.imageEngineInflight = null;
|
|
975
|
+
}
|
|
818
976
|
}
|
|
819
977
|
async ensureTextEngine() {
|
|
820
978
|
if (this.textEngine) return;
|
|
979
|
+
if (this.textEngineInflight) return this.textEngineInflight;
|
|
821
980
|
const meta = getModelMeta(this.config.modelId);
|
|
822
981
|
const textEntry = CLIP_TEXT_MODELS.find((m) => m.id === meta.textModelId);
|
|
823
982
|
if (!textEntry) throw new Error(`EmbeddingEncoderAddon: unknown text model "${meta.textModelId}"`);
|
|
824
|
-
|
|
983
|
+
const inflight = this.resolveForEntry(textEntry, "text");
|
|
984
|
+
this.textEngineInflight = inflight;
|
|
985
|
+
try {
|
|
986
|
+
await inflight;
|
|
987
|
+
} finally {
|
|
988
|
+
this.textEngineInflight = null;
|
|
989
|
+
}
|
|
825
990
|
}
|
|
826
991
|
async resolveForEntry(entry, target) {
|
|
827
992
|
const engineLogger = this.ctx.logger.withTags({
|
|
@@ -847,6 +1012,8 @@ var EmbeddingEncoderAddon = class extends require_dist.BaseAddon {
|
|
|
847
1012
|
this.textEngine = textEngine;
|
|
848
1013
|
}
|
|
849
1014
|
async onShutdown() {
|
|
1015
|
+
this.memoryGuard?.stop();
|
|
1016
|
+
this.memoryGuard = null;
|
|
850
1017
|
await this.imageRawEngine?.dispose();
|
|
851
1018
|
await this.textEngine?.dispose();
|
|
852
1019
|
}
|