@camstack/system 1.2.221 → 1.2.223

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_system_deps = require("./manifest-system-deps-DfpwaXkh.js");
2
+ const require_manifest_system_deps = require("./manifest-system-deps-YYKKCKSF.js");
3
3
  const require_custom_action_registry = require("./custom-action-registry-DvVMHLfJ.js");
4
4
  let node_path = require("node:path");
5
5
  node_path = require_chunk.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { A as createUdsLoggerWithControl, Ft as RUNNER_RSS_BUDGET_ENV, Jt as startRunnerHeapWatch, L as LocalChildClient, a as createUdsAddonContext, bt as resolveAddonClass, it as setWorkerNativeCapsChangeListener, n as installManifestPythonDeps, nt as getWorkerNativeCapSnapshot, on as emitHeapDiagnosticReport, ot as validateProviderRegistrations, t as installManifestSystemDeps, tt as getWorkerNativeCapProvider, yt as installManifestNativeDeps } from "./manifest-system-deps-iadzxSBr.mjs";
1
+ import { A as createUdsLoggerWithControl, Ft as RUNNER_RSS_BUDGET_ENV, Jt as startRunnerHeapWatch, L as LocalChildClient, a as createUdsAddonContext, bt as resolveAddonClass, it as setWorkerNativeCapsChangeListener, n as installManifestPythonDeps, nt as getWorkerNativeCapSnapshot, on as emitHeapDiagnosticReport, ot as validateProviderRegistrations, t as installManifestSystemDeps, tt as getWorkerNativeCapProvider, yt as installManifestNativeDeps } from "./manifest-system-deps-D4cmEnqQ.mjs";
2
2
  import { t as CustomActionRegistry } from "./custom-action-registry-DvtOs-tp.mjs";
3
3
  import { register } from "node:module";
4
4
  import * as path$1 from "node:path";
@@ -3270,7 +3270,7 @@ var SystemBackupService = class {
3270
3270
  for (const dbPath of dbPaths) try {
3271
3271
  const db = new Database(dbPath);
3272
3272
  try {
3273
- db.pragma("wal_checkpoint(TRUNCATE)");
3273
+ db.pragma("wal_checkpoint(PASSIVE)");
3274
3274
  } finally {
3275
3275
  db.close();
3276
3276
  }
@@ -3255,7 +3255,7 @@ var SystemBackupService = class {
3255
3255
  for (const dbPath of dbPaths) try {
3256
3256
  const db = new Database(dbPath);
3257
3257
  try {
3258
- db.pragma("wal_checkpoint(TRUNCATE)");
3258
+ db.pragma("wal_checkpoint(PASSIVE)");
3259
3259
  } finally {
3260
3260
  db.close();
3261
3261
  }
@@ -401,6 +401,31 @@ export declare class SqliteSettingsBackend implements ISettingsBackend {
401
401
  * inside that blob and are reached with `json_extract`.
402
402
  */
403
403
  private shapeOf;
404
+ /**
405
+ * Where a declaration LIVES, so it outlives the process that made it.
406
+ *
407
+ * `declaredCollections` used to be only an in-memory Map, and the
408
+ * declarations are made once, by each addon, at that addon's own boot.
409
+ * On 2026-09-12 this backend died with SIGBUS (a second connection
410
+ * truncated the WAL under its mmap) and `CrashSupervisor` respawned it: the
411
+ * new instance knew only the canonical KV collections, every other addon
412
+ * stayed up and never re-declared, and the cluster failed every settings
413
+ * read for six hours — the operator could not log in.
414
+ *
415
+ * The tables are durable. What the backend knows ABOUT them has to be too,
416
+ * because nothing in the system re-declares and nothing listens for this
417
+ * process restarting.
418
+ */
419
+ private static readonly DECLARATIONS_TABLE;
420
+ /** Create the meta table. Idempotent; called before anything reads it. */
421
+ private ensureDeclarationsTable;
422
+ /** Persist one declaration, replacing any earlier shape for the same table. */
423
+ private persistDeclaration;
424
+ /**
425
+ * Reload every persisted declaration. Called at init, BEFORE any addon has
426
+ * had the chance to re-declare — which on a respawn none of them will.
427
+ */
428
+ private restorePersistedDeclarations;
404
429
  declareCollection(input: {
405
430
  namespace?: string;
406
431
  collection: string;
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ require("./builtins/system-config/index.js");
27
27
  const require_builtins_winston_logging_index = require("./builtins/winston-logging/index.js");
28
28
  const require_event_loop_stall_monitor = require("./event-loop-stall-monitor-CAsHdMwP.js");
29
29
  const require_tls$1 = require("./tls-BFs4PzoW.js");
30
- const require_manifest_system_deps = require("./manifest-system-deps-DfpwaXkh.js");
30
+ const require_manifest_system_deps = require("./manifest-system-deps-YYKKCKSF.js");
31
31
  const require_resource_monitor = require("./resource-monitor-LVE1BoLs.js");
32
32
  const require_custom_action_registry = require("./custom-action-registry-DvVMHLfJ.js");
33
33
  let zod = require("zod");
@@ -78961,8 +78961,9 @@ var require_parser = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, modu
78961
78961
  const type = resolvePacketType(packet[5]);
78962
78962
  this.emit("data", type, msg);
78963
78963
  const rest = packet.slice(length);
78964
- this.bufs = rest.length > 0 ? [rest] : [];
78965
- this.bufLen = rest.length;
78964
+ const carried = rest.length > 0 && rest.length * 8 < rest.buffer.byteLength ? Buffer.from(rest) : rest;
78965
+ this.bufs = carried.length > 0 ? [carried] : [];
78966
+ this.bufLen = carried.length;
78966
78967
  }
78967
78968
  cb();
78968
78969
  }
package/dist/index.mjs CHANGED
@@ -26,7 +26,7 @@ import "./builtins/system-config/index.mjs";
26
26
  import { WinstonDestination, WinstonLoggingAddon } from "./builtins/winston-logging/index.mjs";
27
27
  import { _ as fetchJson, a as createFileDataPlaneHandler, c as parseRangeHeader, d as ModelDownloadService, f as collectModelFiles, g as ensureModel, h as downloadModel, i as startEventLoopStallMonitor, l as parseTokenizedUrl, m as downloadFile, n as STALL_TICK_MS, o as contentTypeFor, p as deleteModelFromDisk, r as attributeStall, s as createAuthenticatedFileServer, t as STALL_REPORT_MS, u as resolveFilePath, v as getModelFilePath, y as isModelDownloaded } from "./event-loop-stall-monitor-GPrVvx14.mjs";
28
28
  import { C as registerLanHttpHandler, S as readLanHttpState, _ as DEFAULT_LAN_HTTP_PORT, a as writeTlsMode, b as bindPendingLanHttp, c as loadTlsCert, d as collectCertIdentity, f as CA_VALIDITY_DAYS, g as evaluateExistingCert, h as SERVER_AUTH_OID, i as writeExtraSans, l as reissueTlsLeaf, m as MAX_LEAF_VALIDITY_DAYS, n as readTlsAccessStatus, o as validateUploadedTls, p as LEAF_RENEWAL_WINDOW_DAYS, r as readTlsMode, s as ensureTlsCert, t as readExtraSans, u as CA_COMMON_NAME, v as allFamiliesListenHost, x as closeLanHttp, y as applyLanHttp } from "./tls-2xbgg78V.mjs";
29
- import { $ as buildUdsNativeCapProxy, $t as ARRAY_BUFFER_RELEASE_MIN_DROP_RATIO, A as createUdsLoggerWithControl, An as sampleSocketDirection, At as HUB_MAIN_RSS_BUDGET_MB, B as readMoleculerFanoutMode, Bn as EVENT_PLANE_TOP_N, Bt as createV8Reclaimer, C as ipcParentLink, Cn as createSocketPlaneReader, Ct as createAddonDataPlaneFacility, D as createUdsEventBus, Dn as EMPTY_SOCKET_DIRECTION, Dt as HEAP_WATCH_INTERVAL_MS, E as createUdsEventBridge, En as readSocketQueues, Et as HEAP_RECLAIM_TRIGGER_MB, F as CapRouteError, Fn as EMPTY_CAP_ACTION_READING, Ft as RUNNER_RSS_BUDGET_ENV, G as createLocalTransport, Gt as reclaimIntervalMs, H as PARENT_ROUTED_PROVIDER_ADDON_ID, Hn as createEventPlaneReader, Ht as hubMainRssBudget, I as classifyCapRoute, In as MAX_CAP_ACTIONS_PER_PEER, It as ZERO_LOOP_DELAY, J as SocketChannel, Jt as startRunnerHeapWatch, K as UdsLocalTransportClient, Kt as shouldReclaim, L as LocalChildClient, Ln as capActionLabel, Lt as buildHeapSample, M as AGENT_CAP_FWD_SERVICE, Mn as socketDirectionMessages, Mt as RSS_BUDGET_REANNOUNCE_MIN_MS, N as CapRouteResolver, Nn as CAP_ACTION_OVERFLOW_LABEL, Nt as RSS_BUDGET_RELEASE_RATIO, O as udsChildLogToWorkerEntry, On as createSocketDirectionCounters, Ot as HEAP_WATCH_WARN_RATIO, P as callWithServiceDiscovery, Pn as CAP_ACTION_UNLABELLED, Pt as RUNNER_HEAP_WATCH_INTERVAL_MS, Q as buildNativeCapProxy, Qt as ARRAY_BUFFER_CENSUS_THRESHOLD_MB, R as LocalChildRegistry, Rn as createCapActionCensus, Rt as createDeferredHeapWatchSink, S as buildLinkChain, Sn as createSocketPlaneMeter, St as runNpm, T as createParentUnownedCallHandler, Tn as formatSocketPlane, Tt as HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS, U as __resetCapUsageRegistryForTests, Un as diffEventPlane, Ut as nextRssBudgetState, V as CapUsageRegistry, Vn as createEventPlaneMeter, Vt as describeRss, W as getCapUsageRegistry, Wn as formatEventPlane, Wt as parseRssBudgetMb, X as FrameDecoder, Xt as ARRAY_BUFFER_CENSUS_COOLDOWN_MS, Y as localEndpointPath, Yt as strandedMb, Z as encodeFrame, Zt as ARRAY_BUFFER_CENSUS_SUSTAIN_MS, _ as AddonDepsManager, _n as createMeshQueueReader, _t as CapabilityUnavailableError, a as createUdsAddonContext, an as HEAP_SPACE_REPORT_MIN_MB, at as createAddonService, b as brokerCallForCap, bn as SOCKET_PLANE_TOP_N, bt as resolveAddonClass, c as getOrInitReadinessRegistryForClient, cn as heapSpaceField, ct as capActionName, d as clusterEventTopic, dn as OUTBOUND_QUEUE_COOLDOWN_MS, dt as capServiceName, en as createArrayBufferCensusGate, et as createBrokerDeviceManagerApi, f as getBrokerEventBus, fn as OUTBOUND_QUEUE_SUSTAIN_MS, ft as parseCapAction, g as subscribePassthrough, gn as MESH_QUEUE_TOP_N, gt as CapabilityHandle, h as setNodeEventInterest, hn as formatOutboundQueueVerdict, ht as DeviceRegistry, i as createAddonContext, in as DECISIVE_HEAP_SPACES, j as AGENT_CAP_FWD_ACTION, jn as socketDirectionBytes, jt as HUB_RSS_BUDGET_ENV, k as createUdsLogger, kn as recordSocketFrame, kt as HUB_MAIN_HEAP_WATCH_LABEL, l as setHubConnected, ln as readHeapSpaces, lt as capActionSuffix, m as registerEventBusService, mn as createOutboundQueueGate, mt as serializeTypedArrays, n as installManifestPythonDeps, nn as formatArrayBufferCensus, o as getWorkerDeviceRegistry, on as emitHeapDiagnosticReport, ot as validateProviderRegistrations, p as getMoleculerEventStats, pn as OUTBOUND_QUEUE_THRESHOLD_BYTES, pt as deserializeTypedArrays, q as UdsLocalTransportServer, qt as startHeapWatch, r as adaptBrokerToCluster, rn as isArrayBufferCensusReport, rt as mountNativeCapService, s as getOrInitReadinessRegistry, sn as formatHeapSpaces, st as NATIVE_PROVIDER_SERVICE_INFIX, t as installManifestSystemDeps, tn as createInspectorArrayBufferCensus, u as EVENT_TOPIC_PREFIX, un as selectReportedSpaces, ut as capBareAction, v as createKernelHwAccel, vn as formatMeshQueue, vt as copyBundledNativeModules, w as localProviderLink, wn as diffSocketPlane, wt as HEAP_RECLAIM_MIN_INTERVAL_MS, x as brokerTransportLink, xn as SOCKET_QUEUE_TOP_N, xt as resolveNpmInvocation, y as resolveHwAccel, yn as EMPTY_SOCKET_PLANE_BASELINE, yt as installManifestNativeDeps, z as UDS_NO_ROUTE_PREFIX, zn as diffCapActionCensus, zt as createLoopDelayMeter } from "./manifest-system-deps-iadzxSBr.mjs";
29
+ import { $ as buildUdsNativeCapProxy, $t as ARRAY_BUFFER_RELEASE_MIN_DROP_RATIO, A as createUdsLoggerWithControl, An as sampleSocketDirection, At as HUB_MAIN_RSS_BUDGET_MB, B as readMoleculerFanoutMode, Bn as EVENT_PLANE_TOP_N, Bt as createV8Reclaimer, C as ipcParentLink, Cn as createSocketPlaneReader, Ct as createAddonDataPlaneFacility, D as createUdsEventBus, Dn as EMPTY_SOCKET_DIRECTION, Dt as HEAP_WATCH_INTERVAL_MS, E as createUdsEventBridge, En as readSocketQueues, Et as HEAP_RECLAIM_TRIGGER_MB, F as CapRouteError, Fn as EMPTY_CAP_ACTION_READING, Ft as RUNNER_RSS_BUDGET_ENV, G as createLocalTransport, Gt as reclaimIntervalMs, H as PARENT_ROUTED_PROVIDER_ADDON_ID, Hn as createEventPlaneReader, Ht as hubMainRssBudget, I as classifyCapRoute, In as MAX_CAP_ACTIONS_PER_PEER, It as ZERO_LOOP_DELAY, J as SocketChannel, Jt as startRunnerHeapWatch, K as UdsLocalTransportClient, Kt as shouldReclaim, L as LocalChildClient, Ln as capActionLabel, Lt as buildHeapSample, M as AGENT_CAP_FWD_SERVICE, Mn as socketDirectionMessages, Mt as RSS_BUDGET_REANNOUNCE_MIN_MS, N as CapRouteResolver, Nn as CAP_ACTION_OVERFLOW_LABEL, Nt as RSS_BUDGET_RELEASE_RATIO, O as udsChildLogToWorkerEntry, On as createSocketDirectionCounters, Ot as HEAP_WATCH_WARN_RATIO, P as callWithServiceDiscovery, Pn as CAP_ACTION_UNLABELLED, Pt as RUNNER_HEAP_WATCH_INTERVAL_MS, Q as buildNativeCapProxy, Qt as ARRAY_BUFFER_CENSUS_THRESHOLD_MB, R as LocalChildRegistry, Rn as createCapActionCensus, Rt as createDeferredHeapWatchSink, S as buildLinkChain, Sn as createSocketPlaneMeter, St as runNpm, T as createParentUnownedCallHandler, Tn as formatSocketPlane, Tt as HEAP_RECLAIM_STEADY_STATE_INTERVAL_MS, U as __resetCapUsageRegistryForTests, Un as diffEventPlane, Ut as nextRssBudgetState, V as CapUsageRegistry, Vn as createEventPlaneMeter, Vt as describeRss, W as getCapUsageRegistry, Wn as formatEventPlane, Wt as parseRssBudgetMb, X as FrameDecoder, Xt as ARRAY_BUFFER_CENSUS_COOLDOWN_MS, Y as localEndpointPath, Yt as strandedMb, Z as encodeFrame, Zt as ARRAY_BUFFER_CENSUS_SUSTAIN_MS, _ as AddonDepsManager, _n as createMeshQueueReader, _t as CapabilityUnavailableError, a as createUdsAddonContext, an as HEAP_SPACE_REPORT_MIN_MB, at as createAddonService, b as brokerCallForCap, bn as SOCKET_PLANE_TOP_N, bt as resolveAddonClass, c as getOrInitReadinessRegistryForClient, cn as heapSpaceField, ct as capActionName, d as clusterEventTopic, dn as OUTBOUND_QUEUE_COOLDOWN_MS, dt as capServiceName, en as createArrayBufferCensusGate, et as createBrokerDeviceManagerApi, f as getBrokerEventBus, fn as OUTBOUND_QUEUE_SUSTAIN_MS, ft as parseCapAction, g as subscribePassthrough, gn as MESH_QUEUE_TOP_N, gt as CapabilityHandle, h as setNodeEventInterest, hn as formatOutboundQueueVerdict, ht as DeviceRegistry, i as createAddonContext, in as DECISIVE_HEAP_SPACES, j as AGENT_CAP_FWD_ACTION, jn as socketDirectionBytes, jt as HUB_RSS_BUDGET_ENV, k as createUdsLogger, kn as recordSocketFrame, kt as HUB_MAIN_HEAP_WATCH_LABEL, l as setHubConnected, ln as readHeapSpaces, lt as capActionSuffix, m as registerEventBusService, mn as createOutboundQueueGate, mt as serializeTypedArrays, n as installManifestPythonDeps, nn as formatArrayBufferCensus, o as getWorkerDeviceRegistry, on as emitHeapDiagnosticReport, ot as validateProviderRegistrations, p as getMoleculerEventStats, pn as OUTBOUND_QUEUE_THRESHOLD_BYTES, pt as deserializeTypedArrays, q as UdsLocalTransportServer, qt as startHeapWatch, r as adaptBrokerToCluster, rn as isArrayBufferCensusReport, rt as mountNativeCapService, s as getOrInitReadinessRegistry, sn as formatHeapSpaces, st as NATIVE_PROVIDER_SERVICE_INFIX, t as installManifestSystemDeps, tn as createInspectorArrayBufferCensus, u as EVENT_TOPIC_PREFIX, un as selectReportedSpaces, ut as capBareAction, v as createKernelHwAccel, vn as formatMeshQueue, vt as copyBundledNativeModules, w as localProviderLink, wn as diffSocketPlane, wt as HEAP_RECLAIM_MIN_INTERVAL_MS, x as brokerTransportLink, xn as SOCKET_QUEUE_TOP_N, xt as resolveNpmInvocation, y as resolveHwAccel, yn as EMPTY_SOCKET_PLANE_BASELINE, yt as installManifestNativeDeps, z as UDS_NO_ROUTE_PREFIX, zn as diffCapActionCensus, zt as createLoopDelayMeter } from "./manifest-system-deps-D4cmEnqQ.mjs";
30
30
  import { n as getSinglePidStats, t as getPidStats } from "./resource-monitor-Vp0bPV3U.mjs";
31
31
  import { t as CustomActionRegistry } from "./custom-action-registry-DvtOs-tp.mjs";
32
32
  import { z } from "zod";
@@ -78954,8 +78954,9 @@ var require_parser = /* @__PURE__ */ __commonJSMin$1(((exports, module) => {
78954
78954
  const type = resolvePacketType(packet[5]);
78955
78955
  this.emit("data", type, msg);
78956
78956
  const rest = packet.slice(length);
78957
- this.bufs = rest.length > 0 ? [rest] : [];
78958
- this.bufLen = rest.length;
78957
+ const carried = rest.length > 0 && rest.length * 8 < rest.buffer.byteLength ? Buffer.from(rest) : rest;
78958
+ this.bufs = carried.length > 0 ? [carried] : [];
78959
+ this.bufLen = carried.length;
78959
78960
  }
78960
78961
  cb();
78961
78962
  }
@@ -63,6 +63,25 @@
63
63
  * — the same discipline the loop-delay histogram already follows, for the same
64
64
  * reason.
65
65
  *
66
+ * ## The unit trap (D467)
67
+ *
68
+ * `fanoutSent` / `fanoutSuppressed` are **(event × child) pairs**, not events:
69
+ * `broadcastEventToChildren` increments them inside the per-child loop, so on a
70
+ * 39-child node ONE emit produces up to 38 increments. A 2026-09-12 reading of
71
+ * `fanoutSuppressed=433461` was read as "433k events a minute, 97% discarded";
72
+ * the real production was **~11 700 events/min**. The line proves it itself —
73
+ * every pure-consumer child's `sent + suppressed` converges on 11 730, because
74
+ * that sum IS the distinct broadcast count, and the two rows that fall short
75
+ * (`pipeline-orchestrator`, `pipeline-analytics`) fall short by exactly what
76
+ * they originated. `fanoutEvents` is the production rate, counted once per emit
77
+ * outside that loop, and it is printed so the pair counters can never be read as
78
+ * one again.
79
+ *
80
+ * `fanoutCats` names PRODUCERS (`fanoutTop` names subscribers) and
81
+ * `fanoutOrphans` counts what was serialised and walked across every child with
82
+ * zero subscribers. A suppressed pair was measured at **255 ns** — 1.84 ms/s of
83
+ * event loop at this rate — so the filter is not the cost; the emit is.
84
+ *
66
85
  * ## What this is NOT evidence of
67
86
  *
68
87
  * `shadow` is the CROSS-NODE plane. The local UDS plane defaults to `filter` and
@@ -93,11 +112,24 @@ export interface ChildFanoutRegistryStats {
93
112
  readonly sent: number;
94
113
  readonly suppressed: number;
95
114
  }
115
+ /**
116
+ * D467 — one PRODUCED category, cumulative, as the registry keeps it.
117
+ *
118
+ * `broadcasts` is EMITS, not deliveries; `wanted` is the number of child
119
+ * subscriptions that matched, summed over those emits.
120
+ */
121
+ export interface CategoryFanoutSample {
122
+ readonly category: string;
123
+ readonly broadcasts: number;
124
+ readonly wanted: number;
125
+ }
96
126
  export interface ChildFanoutRegistry {
97
127
  readonly listChildren: () => readonly ChildFanoutRegistryChild[];
98
128
  readonly getChildEventStats: (childId: string) => ChildFanoutRegistryStats | null;
99
129
  readonly undeclaredChildCount: () => number;
100
130
  readonly eventFanoutMode: EventFanoutMode;
131
+ readonly categoryFanout: () => readonly CategoryFanoutSample[];
132
+ readonly categoryCensusOverflow: () => number;
101
133
  }
102
134
  /** What `getMoleculerEventStats` returns, structurally. */
103
135
  export interface CrossNodeEventStats {
@@ -113,6 +145,8 @@ export interface EventPlaneCounters {
113
145
  readonly childrenUndeclared: number;
114
146
  readonly crossNodeDelivered: number;
115
147
  readonly crossNodeSuppressed: number;
148
+ readonly categories: readonly CategoryFanoutSample[];
149
+ readonly categoryOverflow: number;
116
150
  }
117
151
  /** `undefined` = this process has no event plane to report (every forked
118
152
  * runner, and hub-main before the registry exists). */
@@ -159,12 +193,30 @@ export interface EventPlaneWindow {
159
193
  readonly crossNodeSuppressed: number;
160
194
  /** Largest `sent` first, at most {@link EVENT_PLANE_TOP_N} entries. */
161
195
  readonly top: readonly ChildFanoutDelta[];
196
+ /** Events PRODUCED in the window — emits, summed over categories. */
197
+ readonly categoryBroadcasts: number;
198
+ /** Produced events no child subscribed to. Pure waste. */
199
+ readonly orphanBroadcasts: number;
200
+ /** Largest `broadcasts` first, at most {@link EVENT_PLANE_TOP_N} entries. */
201
+ readonly topCategories: readonly CategoryFanoutDelta[];
202
+ /** The orphans alone, largest first, at most {@link EVENT_PLANE_TOP_N}. */
203
+ readonly topOrphans: readonly CategoryFanoutDelta[];
204
+ /** Broadcasts the census could not attribute because it was full. */
205
+ readonly categoryOverflow: number;
206
+ }
207
+ /** One category's production over the window since the previous read. */
208
+ export interface CategoryFanoutDelta {
209
+ readonly category: string;
210
+ readonly broadcasts: number;
211
+ readonly wanted: number;
162
212
  }
163
213
  /** Previous cumulative reading, kept between lines. */
164
214
  export interface EventPlaneBaseline {
165
215
  readonly children: ReadonlyMap<string, ChildFanoutSample>;
216
+ readonly categories: ReadonlyMap<string, CategoryFanoutSample>;
166
217
  readonly crossNodeDelivered: number;
167
218
  readonly crossNodeSuppressed: number;
219
+ readonly categoryOverflow: number;
168
220
  }
169
221
  /** A process that has never read a plane. */
170
222
  export declare const EMPTY_EVENT_PLANE_BASELINE: EventPlaneBaseline;
@@ -44,6 +44,36 @@ export interface ChildEventStats {
44
44
  sent: number;
45
45
  suppressed: number;
46
46
  }
47
+ /**
48
+ * D467 — one PRODUCED category over the life of the process.
49
+ *
50
+ * `broadcasts` counts emits, not deliveries: one call to
51
+ * {@link LocalChildRegistry.broadcastEventToChildren} is one broadcast no
52
+ * matter how many children are connected. This is the number the per-child
53
+ * `sent`/`suppressed` counters cannot give — those are (event × child) pairs,
54
+ * so on a 39-child node they read ~38x the production rate and a reader who
55
+ * sums them concludes the cluster emits an order of magnitude more than it does.
56
+ *
57
+ * `wanted` is the number of (event × child) pairs whose SUBSCRIPTION matched —
58
+ * the gate answer, not the delivery answer, so `broadcast`/`shadow` mode (which
59
+ * deliver regardless) still report the truth. `wanted === 0` over a window means
60
+ * the category was serialised and walked across every child and **nobody asked
61
+ * for it**: pure waste, and the one finding worth acting on.
62
+ */
63
+ export interface CategoryFanoutSample {
64
+ readonly category: string;
65
+ readonly broadcasts: number;
66
+ readonly wanted: number;
67
+ }
68
+ /**
69
+ * Cap on the census map. `EventCategory` is a closed enum of ~120 members, so
70
+ * 256 holds every legitimate category twice over; the cap exists because a
71
+ * hand-rolled topic (forbidden, but this is the meter that would SEE one) could
72
+ * otherwise grow the map without bound in a long-lived process. Overflow is not
73
+ * silent — {@link LocalChildRegistry.categoryCensusOverflow} counts it and the
74
+ * heartbeat prints it.
75
+ */
76
+ export declare const CATEGORY_CENSUS_MAX = 256;
47
77
  /**
48
78
  * Sentinel prefix used in the no-route error thrown when `cap-call-out` has no
49
79
  * local sibling and no `onUnownedCall` fallback. `ipcParentLink` detects this
@@ -252,6 +282,12 @@ export declare class LocalChildRegistry {
252
282
  private readonly fanoutMode;
253
283
  /** Per-child event fan-out counters (sent / suppressed). */
254
284
  private readonly childEventStats;
285
+ /** D467: per-category production census (cumulative). See {@link CategoryFanoutSample}. */
286
+ private readonly categoryStats;
287
+ /** Broadcasts whose category did not fit in {@link CATEGORY_CENSUS_MAX}. */
288
+ private categoryOverflow;
289
+ /** One INFO line the first time the census cap trips, never per event. */
290
+ private categoryOverflowLogged;
255
291
  /** Last pattern-set string logged per child, to dedup the INFO line. */
256
292
  private readonly loggedPatternSet;
257
293
  /**
@@ -279,6 +315,15 @@ export declare class LocalChildRegistry {
279
315
  * surface (shadow burn-in + operator debug).
280
316
  */
281
317
  getChildEventStats(childId: string): ChildEventStats | null;
318
+ /**
319
+ * D467: every category this node has broadcast, with its emit count and how
320
+ * many child subscriptions matched. Cumulative for the life of the process —
321
+ * the heartbeat differences it, the same discipline as the per-child
322
+ * counters. Largest `broadcasts` first.
323
+ */
324
+ categoryFanout(): readonly CategoryFanoutSample[];
325
+ /** Broadcasts the census could not attribute because it was full (cumulative). */
326
+ categoryCensusOverflow(): number;
282
327
  /**
283
328
  * This child's socket traffic — bytes and messages, both directions, split by
284
329
  * frame kind — or `null` when the child is gone or its channel keeps no
@@ -348,6 +393,15 @@ export declare class LocalChildRegistry {
348
393
  * that explicitly subscribes to a ring-denied category must still RECEIVE it.
349
394
  */
350
395
  private childWantsEvent;
396
+ /**
397
+ * The SUBSCRIPTION answer alone, with no fan-out-mode override.
398
+ *
399
+ * {@link childWantsEvent} answers "will this child be sent the event", which
400
+ * under `broadcast` is `true` for everything and therefore cannot say whether
401
+ * anyone actually asked. The D467 census needs the second question, so the
402
+ * two are separate functions and `broadcast` mode overrides only the first.
403
+ */
404
+ private childSubscribesTo;
351
405
  /**
352
406
  * D2: this node's aggregate cross-node event interest — the UNION of every
353
407
  * connected child's declared category patterns. Consumed by the parent's
@@ -513,6 +567,11 @@ export declare class LocalChildRegistry {
513
567
  * one (the originating child, to avoid echo). Fire-and-forget.
514
568
  */
515
569
  broadcastEventToChildren(event: SystemEvent, sourceNodeId: string, exceptChildId?: string): void;
570
+ /**
571
+ * Record one PRODUCED event against its category. Called once per broadcast,
572
+ * outside the per-child loop, because production is what the census measures.
573
+ */
574
+ private recordCategoryBroadcast;
516
575
  /**
517
576
  * E2: Send a `set-log-level` control message to a specific child.
518
577
  * Returns `true` if the child is currently connected and the message was
@@ -56,7 +56,9 @@ function createEventPlaneReader(sources) {
56
56
  childrenConnected: listed.length,
57
57
  childrenUndeclared: registry.undeclaredChildCount(),
58
58
  crossNodeDelivered: crossNode?.delivered ?? 0,
59
- crossNodeSuppressed: crossNode?.suppressed ?? 0
59
+ crossNodeSuppressed: crossNode?.suppressed ?? 0,
60
+ categories: registry.categoryFanout(),
61
+ categoryOverflow: registry.categoryCensusOverflow()
60
62
  };
61
63
  } catch {
62
64
  return;
@@ -76,8 +78,10 @@ var EVENT_PLANE_TOP_N = 5;
76
78
  /** A process that has never read a plane. */
77
79
  var EMPTY_EVENT_PLANE_BASELINE = {
78
80
  children: /* @__PURE__ */ new Map(),
81
+ categories: /* @__PURE__ */ new Map(),
79
82
  crossNodeDelivered: 0,
80
- crossNodeSuppressed: 0
83
+ crossNodeSuppressed: 0,
84
+ categoryOverflow: 0
81
85
  };
82
86
  /**
83
87
  * Subtract the previous cumulative reading from the current one.
@@ -104,6 +108,23 @@ function diffEventPlane(baseline, current, topN = 5) {
104
108
  suppressed
105
109
  });
106
110
  }
111
+ const categoryDeltas = [];
112
+ let categoryBroadcasts = 0;
113
+ let orphanBroadcasts = 0;
114
+ for (const sample of current.categories) {
115
+ const before = baseline.categories.get(sample.category);
116
+ const broadcasts = Math.max(0, sample.broadcasts - (before?.broadcasts ?? 0));
117
+ if (broadcasts === 0) continue;
118
+ const wanted = Math.max(0, sample.wanted - (before?.wanted ?? 0));
119
+ categoryBroadcasts += broadcasts;
120
+ if (wanted === 0) orphanBroadcasts += broadcasts;
121
+ categoryDeltas.push({
122
+ category: sample.category,
123
+ broadcasts,
124
+ wanted
125
+ });
126
+ }
127
+ const byBroadcasts = categoryDeltas.toSorted((a, b) => b.broadcasts - a.broadcasts || a.category.localeCompare(b.category));
107
128
  return {
108
129
  udsMode: current.udsMode,
109
130
  crossNodeMode: current.crossNodeMode,
@@ -113,7 +134,12 @@ function diffEventPlane(baseline, current, topN = 5) {
113
134
  fanoutSuppressed,
114
135
  crossNodeDelivered: Math.max(0, current.crossNodeDelivered - baseline.crossNodeDelivered),
115
136
  crossNodeSuppressed: Math.max(0, current.crossNodeSuppressed - baseline.crossNodeSuppressed),
116
- top: deltas.toSorted((a, b) => b.sent - a.sent || a.childId.localeCompare(b.childId)).slice(0, topN)
137
+ top: deltas.toSorted((a, b) => b.sent - a.sent || a.childId.localeCompare(b.childId)).slice(0, topN),
138
+ categoryBroadcasts,
139
+ orphanBroadcasts,
140
+ topCategories: byBroadcasts.slice(0, topN),
141
+ topOrphans: byBroadcasts.filter((c) => c.wanted === 0).slice(0, topN),
142
+ categoryOverflow: Math.max(0, current.categoryOverflow - baseline.categoryOverflow)
117
143
  };
118
144
  }
119
145
  function createEventPlaneMeter(reader, topN = 5) {
@@ -124,8 +150,10 @@ function createEventPlaneMeter(reader, topN = 5) {
124
150
  const window = diffEventPlane(baseline, current, topN);
125
151
  baseline = {
126
152
  children: new Map(current.children.map((s) => [s.childId, s])),
153
+ categories: new Map(current.categories.map((c) => [c.category, c])),
127
154
  crossNodeDelivered: current.crossNodeDelivered,
128
- crossNodeSuppressed: current.crossNodeSuppressed
155
+ crossNodeSuppressed: current.crossNodeSuppressed,
156
+ categoryOverflow: current.categoryOverflow
129
157
  };
130
158
  return window;
131
159
  } };
@@ -149,7 +177,9 @@ function createEventPlaneMeter(reader, topN = 5) {
149
177
  function formatEventPlane(window) {
150
178
  if (window === void 0) return "";
151
179
  const top = window.top.map((d) => `${d.childId}:${d.sent}/${d.suppressed}`).join(",");
152
- return ` fanoutMode=uds:${window.udsMode}/cross-node:${window.crossNodeMode} children=${window.childrenConnected} childrenUndeclared=${window.childrenUndeclared} fanoutSent=${window.fanoutSent} fanoutSuppressed=${window.fanoutSuppressed} crossNodeDelivered=${window.crossNodeDelivered} crossNodeSuppressed=${window.crossNodeSuppressed}` + (top.length === 0 ? "" : ` fanoutTop=${top}`);
180
+ const cats = window.topCategories.map((c) => `${c.category}:${c.broadcasts}/${c.wanted}`).join(",");
181
+ const orphans = window.topOrphans.map((c) => `${c.category}:${c.broadcasts}`).join(",");
182
+ return ` fanoutMode=uds:${window.udsMode}/cross-node:${window.crossNodeMode} children=${window.childrenConnected} childrenUndeclared=${window.childrenUndeclared} fanoutSent=${window.fanoutSent} fanoutSuppressed=${window.fanoutSuppressed} crossNodeDelivered=${window.crossNodeDelivered} crossNodeSuppressed=${window.crossNodeSuppressed} fanoutEvents=${window.categoryBroadcasts} fanoutOrphans=${window.orphanBroadcasts} fanoutCatsDropped=${window.categoryOverflow}` + (top.length === 0 ? "" : ` fanoutTop=${top}`) + (cats.length === 0 ? "" : ` fanoutCats=${cats}`) + (orphans.length === 0 ? "" : ` fanoutOrphanTop=${orphans}`);
153
183
  }
154
184
  //#endregion
155
185
  //#region src/kernel/transport/cap-action-census.ts
@@ -7107,6 +7137,12 @@ var LocalChildRegistry = class {
7107
7137
  fanoutMode = readFanoutMode();
7108
7138
  /** Per-child event fan-out counters (sent / suppressed). */
7109
7139
  childEventStats = /* @__PURE__ */ new Map();
7140
+ /** D467: per-category production census (cumulative). See {@link CategoryFanoutSample}. */
7141
+ categoryStats = /* @__PURE__ */ new Map();
7142
+ /** Broadcasts whose category did not fit in {@link CATEGORY_CENSUS_MAX}. */
7143
+ categoryOverflow = 0;
7144
+ /** One INFO line the first time the census cap trips, never per event. */
7145
+ categoryOverflowLogged = false;
7110
7146
  /** Last pattern-set string logged per child, to dedup the INFO line. */
7111
7147
  loggedPatternSet = /* @__PURE__ */ new Map();
7112
7148
  /**
@@ -7162,6 +7198,25 @@ var LocalChildRegistry = class {
7162
7198
  };
7163
7199
  }
7164
7200
  /**
7201
+ * D467: every category this node has broadcast, with its emit count and how
7202
+ * many child subscriptions matched. Cumulative for the life of the process —
7203
+ * the heartbeat differences it, the same discipline as the per-child
7204
+ * counters. Largest `broadcasts` first.
7205
+ */
7206
+ categoryFanout() {
7207
+ const out = [];
7208
+ for (const [category, s] of this.categoryStats) out.push({
7209
+ category,
7210
+ broadcasts: s.broadcasts,
7211
+ wanted: s.wanted
7212
+ });
7213
+ return out.toSorted((a, b) => b.broadcasts - a.broadcasts || a.category.localeCompare(b.category));
7214
+ }
7215
+ /** Broadcasts the census could not attribute because it was full (cumulative). */
7216
+ categoryCensusOverflow() {
7217
+ return this.categoryOverflow;
7218
+ }
7219
+ /**
7165
7220
  * This child's socket traffic — bytes and messages, both directions, split by
7166
7221
  * frame kind — or `null` when the child is gone or its channel keeps no
7167
7222
  * counters (an in-process or test transport).
@@ -7252,6 +7307,17 @@ var LocalChildRegistry = class {
7252
7307
  */
7253
7308
  childWantsEvent(entry, category) {
7254
7309
  if (this.fanoutMode === "broadcast") return true;
7310
+ return this.childSubscribesTo(entry, category);
7311
+ }
7312
+ /**
7313
+ * The SUBSCRIPTION answer alone, with no fan-out-mode override.
7314
+ *
7315
+ * {@link childWantsEvent} answers "will this child be sent the event", which
7316
+ * under `broadcast` is `true` for everything and therefore cannot say whether
7317
+ * anyone actually asked. The D467 census needs the second question, so the
7318
+ * two are separate functions and `broadcast` mode overrides only the first.
7319
+ */
7320
+ childSubscribesTo(entry, category) {
7255
7321
  if (entry.eventPatterns === null) return true;
7256
7322
  return entry.eventPatterns.some((p) => matchesPattern(p, category));
7257
7323
  }
@@ -7553,8 +7619,10 @@ var LocalChildRegistry = class {
7553
7619
  event,
7554
7620
  sourceNodeId
7555
7621
  };
7622
+ let subscribed = 0;
7556
7623
  for (const entry of this.children.values()) {
7557
7624
  if (entry.childId === exceptChildId) continue;
7625
+ if (this.childSubscribesTo(entry, event.category)) subscribed += 1;
7558
7626
  const wants = this.childWantsEvent(entry, event.category);
7559
7627
  const stats = this.statsFor(entry.childId);
7560
7628
  if (!wants) stats.suppressed++;
@@ -7563,6 +7631,34 @@ var LocalChildRegistry = class {
7563
7631
  entry.channel.emit(msg);
7564
7632
  }
7565
7633
  }
7634
+ this.recordCategoryBroadcast(event.category, subscribed);
7635
+ }
7636
+ /**
7637
+ * Record one PRODUCED event against its category. Called once per broadcast,
7638
+ * outside the per-child loop, because production is what the census measures.
7639
+ */
7640
+ recordCategoryBroadcast(category, subscribed) {
7641
+ const existing = this.categoryStats.get(category);
7642
+ if (existing !== void 0) {
7643
+ existing.broadcasts += 1;
7644
+ existing.wanted += subscribed;
7645
+ return;
7646
+ }
7647
+ if (this.categoryStats.size >= 256) {
7648
+ this.categoryOverflow += 1;
7649
+ if (!this.categoryOverflowLogged) {
7650
+ this.categoryOverflowLogged = true;
7651
+ this.logger?.info("event category census full — further new categories are unattributed", {
7652
+ max: 256,
7653
+ firstDropped: category
7654
+ });
7655
+ }
7656
+ return;
7657
+ }
7658
+ this.categoryStats.set(category, {
7659
+ broadcasts: 1,
7660
+ wanted: subscribed
7661
+ });
7566
7662
  }
7567
7663
  /**
7568
7664
  * E2: Send a `set-log-level` control message to a specific child.
@@ -59,7 +59,9 @@ function createEventPlaneReader(sources) {
59
59
  childrenConnected: listed.length,
60
60
  childrenUndeclared: registry.undeclaredChildCount(),
61
61
  crossNodeDelivered: crossNode?.delivered ?? 0,
62
- crossNodeSuppressed: crossNode?.suppressed ?? 0
62
+ crossNodeSuppressed: crossNode?.suppressed ?? 0,
63
+ categories: registry.categoryFanout(),
64
+ categoryOverflow: registry.categoryCensusOverflow()
63
65
  };
64
66
  } catch {
65
67
  return;
@@ -79,8 +81,10 @@ var EVENT_PLANE_TOP_N = 5;
79
81
  /** A process that has never read a plane. */
80
82
  var EMPTY_EVENT_PLANE_BASELINE = {
81
83
  children: /* @__PURE__ */ new Map(),
84
+ categories: /* @__PURE__ */ new Map(),
82
85
  crossNodeDelivered: 0,
83
- crossNodeSuppressed: 0
86
+ crossNodeSuppressed: 0,
87
+ categoryOverflow: 0
84
88
  };
85
89
  /**
86
90
  * Subtract the previous cumulative reading from the current one.
@@ -107,6 +111,23 @@ function diffEventPlane(baseline, current, topN = 5) {
107
111
  suppressed
108
112
  });
109
113
  }
114
+ const categoryDeltas = [];
115
+ let categoryBroadcasts = 0;
116
+ let orphanBroadcasts = 0;
117
+ for (const sample of current.categories) {
118
+ const before = baseline.categories.get(sample.category);
119
+ const broadcasts = Math.max(0, sample.broadcasts - (before?.broadcasts ?? 0));
120
+ if (broadcasts === 0) continue;
121
+ const wanted = Math.max(0, sample.wanted - (before?.wanted ?? 0));
122
+ categoryBroadcasts += broadcasts;
123
+ if (wanted === 0) orphanBroadcasts += broadcasts;
124
+ categoryDeltas.push({
125
+ category: sample.category,
126
+ broadcasts,
127
+ wanted
128
+ });
129
+ }
130
+ const byBroadcasts = categoryDeltas.toSorted((a, b) => b.broadcasts - a.broadcasts || a.category.localeCompare(b.category));
110
131
  return {
111
132
  udsMode: current.udsMode,
112
133
  crossNodeMode: current.crossNodeMode,
@@ -116,7 +137,12 @@ function diffEventPlane(baseline, current, topN = 5) {
116
137
  fanoutSuppressed,
117
138
  crossNodeDelivered: Math.max(0, current.crossNodeDelivered - baseline.crossNodeDelivered),
118
139
  crossNodeSuppressed: Math.max(0, current.crossNodeSuppressed - baseline.crossNodeSuppressed),
119
- top: deltas.toSorted((a, b) => b.sent - a.sent || a.childId.localeCompare(b.childId)).slice(0, topN)
140
+ top: deltas.toSorted((a, b) => b.sent - a.sent || a.childId.localeCompare(b.childId)).slice(0, topN),
141
+ categoryBroadcasts,
142
+ orphanBroadcasts,
143
+ topCategories: byBroadcasts.slice(0, topN),
144
+ topOrphans: byBroadcasts.filter((c) => c.wanted === 0).slice(0, topN),
145
+ categoryOverflow: Math.max(0, current.categoryOverflow - baseline.categoryOverflow)
120
146
  };
121
147
  }
122
148
  function createEventPlaneMeter(reader, topN = 5) {
@@ -127,8 +153,10 @@ function createEventPlaneMeter(reader, topN = 5) {
127
153
  const window = diffEventPlane(baseline, current, topN);
128
154
  baseline = {
129
155
  children: new Map(current.children.map((s) => [s.childId, s])),
156
+ categories: new Map(current.categories.map((c) => [c.category, c])),
130
157
  crossNodeDelivered: current.crossNodeDelivered,
131
- crossNodeSuppressed: current.crossNodeSuppressed
158
+ crossNodeSuppressed: current.crossNodeSuppressed,
159
+ categoryOverflow: current.categoryOverflow
132
160
  };
133
161
  return window;
134
162
  } };
@@ -152,7 +180,9 @@ function createEventPlaneMeter(reader, topN = 5) {
152
180
  function formatEventPlane(window) {
153
181
  if (window === void 0) return "";
154
182
  const top = window.top.map((d) => `${d.childId}:${d.sent}/${d.suppressed}`).join(",");
155
- return ` fanoutMode=uds:${window.udsMode}/cross-node:${window.crossNodeMode} children=${window.childrenConnected} childrenUndeclared=${window.childrenUndeclared} fanoutSent=${window.fanoutSent} fanoutSuppressed=${window.fanoutSuppressed} crossNodeDelivered=${window.crossNodeDelivered} crossNodeSuppressed=${window.crossNodeSuppressed}` + (top.length === 0 ? "" : ` fanoutTop=${top}`);
183
+ const cats = window.topCategories.map((c) => `${c.category}:${c.broadcasts}/${c.wanted}`).join(",");
184
+ const orphans = window.topOrphans.map((c) => `${c.category}:${c.broadcasts}`).join(",");
185
+ return ` fanoutMode=uds:${window.udsMode}/cross-node:${window.crossNodeMode} children=${window.childrenConnected} childrenUndeclared=${window.childrenUndeclared} fanoutSent=${window.fanoutSent} fanoutSuppressed=${window.fanoutSuppressed} crossNodeDelivered=${window.crossNodeDelivered} crossNodeSuppressed=${window.crossNodeSuppressed} fanoutEvents=${window.categoryBroadcasts} fanoutOrphans=${window.orphanBroadcasts} fanoutCatsDropped=${window.categoryOverflow}` + (top.length === 0 ? "" : ` fanoutTop=${top}`) + (cats.length === 0 ? "" : ` fanoutCats=${cats}`) + (orphans.length === 0 ? "" : ` fanoutOrphanTop=${orphans}`);
156
186
  }
157
187
  //#endregion
158
188
  //#region src/kernel/transport/cap-action-census.ts
@@ -7110,6 +7140,12 @@ var LocalChildRegistry = class {
7110
7140
  fanoutMode = readFanoutMode();
7111
7141
  /** Per-child event fan-out counters (sent / suppressed). */
7112
7142
  childEventStats = /* @__PURE__ */ new Map();
7143
+ /** D467: per-category production census (cumulative). See {@link CategoryFanoutSample}. */
7144
+ categoryStats = /* @__PURE__ */ new Map();
7145
+ /** Broadcasts whose category did not fit in {@link CATEGORY_CENSUS_MAX}. */
7146
+ categoryOverflow = 0;
7147
+ /** One INFO line the first time the census cap trips, never per event. */
7148
+ categoryOverflowLogged = false;
7113
7149
  /** Last pattern-set string logged per child, to dedup the INFO line. */
7114
7150
  loggedPatternSet = /* @__PURE__ */ new Map();
7115
7151
  /**
@@ -7165,6 +7201,25 @@ var LocalChildRegistry = class {
7165
7201
  };
7166
7202
  }
7167
7203
  /**
7204
+ * D467: every category this node has broadcast, with its emit count and how
7205
+ * many child subscriptions matched. Cumulative for the life of the process —
7206
+ * the heartbeat differences it, the same discipline as the per-child
7207
+ * counters. Largest `broadcasts` first.
7208
+ */
7209
+ categoryFanout() {
7210
+ const out = [];
7211
+ for (const [category, s] of this.categoryStats) out.push({
7212
+ category,
7213
+ broadcasts: s.broadcasts,
7214
+ wanted: s.wanted
7215
+ });
7216
+ return out.toSorted((a, b) => b.broadcasts - a.broadcasts || a.category.localeCompare(b.category));
7217
+ }
7218
+ /** Broadcasts the census could not attribute because it was full (cumulative). */
7219
+ categoryCensusOverflow() {
7220
+ return this.categoryOverflow;
7221
+ }
7222
+ /**
7168
7223
  * This child's socket traffic — bytes and messages, both directions, split by
7169
7224
  * frame kind — or `null` when the child is gone or its channel keeps no
7170
7225
  * counters (an in-process or test transport).
@@ -7255,6 +7310,17 @@ var LocalChildRegistry = class {
7255
7310
  */
7256
7311
  childWantsEvent(entry, category) {
7257
7312
  if (this.fanoutMode === "broadcast") return true;
7313
+ return this.childSubscribesTo(entry, category);
7314
+ }
7315
+ /**
7316
+ * The SUBSCRIPTION answer alone, with no fan-out-mode override.
7317
+ *
7318
+ * {@link childWantsEvent} answers "will this child be sent the event", which
7319
+ * under `broadcast` is `true` for everything and therefore cannot say whether
7320
+ * anyone actually asked. The D467 census needs the second question, so the
7321
+ * two are separate functions and `broadcast` mode overrides only the first.
7322
+ */
7323
+ childSubscribesTo(entry, category) {
7258
7324
  if (entry.eventPatterns === null) return true;
7259
7325
  return entry.eventPatterns.some((p) => matchesPattern(p, category));
7260
7326
  }
@@ -7556,8 +7622,10 @@ var LocalChildRegistry = class {
7556
7622
  event,
7557
7623
  sourceNodeId
7558
7624
  };
7625
+ let subscribed = 0;
7559
7626
  for (const entry of this.children.values()) {
7560
7627
  if (entry.childId === exceptChildId) continue;
7628
+ if (this.childSubscribesTo(entry, event.category)) subscribed += 1;
7561
7629
  const wants = this.childWantsEvent(entry, event.category);
7562
7630
  const stats = this.statsFor(entry.childId);
7563
7631
  if (!wants) stats.suppressed++;
@@ -7566,6 +7634,34 @@ var LocalChildRegistry = class {
7566
7634
  entry.channel.emit(msg);
7567
7635
  }
7568
7636
  }
7637
+ this.recordCategoryBroadcast(event.category, subscribed);
7638
+ }
7639
+ /**
7640
+ * Record one PRODUCED event against its category. Called once per broadcast,
7641
+ * outside the per-child loop, because production is what the census measures.
7642
+ */
7643
+ recordCategoryBroadcast(category, subscribed) {
7644
+ const existing = this.categoryStats.get(category);
7645
+ if (existing !== void 0) {
7646
+ existing.broadcasts += 1;
7647
+ existing.wanted += subscribed;
7648
+ return;
7649
+ }
7650
+ if (this.categoryStats.size >= 256) {
7651
+ this.categoryOverflow += 1;
7652
+ if (!this.categoryOverflowLogged) {
7653
+ this.categoryOverflowLogged = true;
7654
+ this.logger?.info("event category census full — further new categories are unattributed", {
7655
+ max: 256,
7656
+ firstDropped: category
7657
+ });
7658
+ }
7659
+ return;
7660
+ }
7661
+ this.categoryStats.set(category, {
7662
+ broadcasts: 1,
7663
+ wanted: subscribed
7664
+ });
7569
7665
  }
7570
7666
  /**
7571
7667
  * E2: Send a `set-log-level` control message to a specific child.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.221",
3
+ "version": "1.2.223",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",