@senpi/runtime 3.0.65 → 3.0.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/actions/event-derivation.d.ts.map +1 -1
  2. package/dist/actions/event-derivation.js +2 -0
  3. package/dist/actions/event-derivation.js.map +1 -1
  4. package/dist/cli/deploy-render.d.ts +3 -1
  5. package/dist/cli/deploy-render.d.ts.map +1 -1
  6. package/dist/cli/deploy-render.js +233 -11
  7. package/dist/cli/deploy-render.js.map +1 -1
  8. package/dist/cli/guide-commands.d.ts.map +1 -1
  9. package/dist/cli/guide-commands.js +9 -7
  10. package/dist/cli/guide-commands.js.map +1 -1
  11. package/dist/cli/senpi-commands.js +4 -4
  12. package/dist/cli/senpi-commands.js.map +1 -1
  13. package/dist/deploy/deadlines.d.ts +3 -3
  14. package/dist/deploy/deadlines.js +3 -3
  15. package/dist/deploy/job.d.ts +16 -0
  16. package/dist/deploy/job.d.ts.map +1 -1
  17. package/dist/deploy/job.js +7 -1
  18. package/dist/deploy/job.js.map +1 -1
  19. package/dist/deploy/orchestrator.d.ts +2 -1
  20. package/dist/deploy/orchestrator.d.ts.map +1 -1
  21. package/dist/deploy/orchestrator.js +402 -84
  22. package/dist/deploy/orchestrator.js.map +1 -1
  23. package/dist/deploy/proof-gate.d.ts.map +1 -1
  24. package/dist/deploy/proof-gate.js +8 -3
  25. package/dist/deploy/proof-gate.js.map +1 -1
  26. package/dist/deploy/register.js +1 -1
  27. package/dist/health/types.d.ts +6 -0
  28. package/dist/health/types.d.ts.map +1 -1
  29. package/dist/health/types.js.map +1 -1
  30. package/dist/index.d.ts +16 -4
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +234 -21
  33. package/dist/index.js.map +1 -1
  34. package/dist/runtime/runtime.d.ts.map +1 -1
  35. package/dist/runtime/runtime.js +7 -1
  36. package/dist/runtime/runtime.js.map +1 -1
  37. package/dist/scanners/engine/engine.d.ts +5 -0
  38. package/dist/scanners/engine/engine.d.ts.map +1 -1
  39. package/dist/scanners/engine/engine.js +1 -0
  40. package/dist/scanners/engine/engine.js.map +1 -1
  41. package/dist/scanners/runtime-module.d.ts.map +1 -1
  42. package/dist/scanners/runtime-module.js +5 -0
  43. package/dist/scanners/runtime-module.js.map +1 -1
  44. package/dist/senpi/client.d.ts.map +1 -1
  45. package/dist/senpi/client.js +72 -5
  46. package/dist/senpi/client.js.map +1 -1
  47. package/dist/senpi/types.d.ts +18 -0
  48. package/dist/senpi/types.d.ts.map +1 -1
  49. package/dist/telemetry/event-builders.d.ts +12 -0
  50. package/dist/telemetry/event-builders.d.ts.map +1 -1
  51. package/dist/telemetry/event-builders.js +2 -0
  52. package/dist/telemetry/event-builders.js.map +1 -1
  53. package/dist/telemetry/event-store.d.ts +6 -0
  54. package/dist/telemetry/event-store.d.ts.map +1 -1
  55. package/dist/telemetry/event-store.js +6 -6
  56. package/dist/telemetry/event-store.js.map +1 -1
  57. package/dist/telemetry/live-telemetry.d.ts +153 -0
  58. package/dist/telemetry/live-telemetry.d.ts.map +1 -0
  59. package/dist/telemetry/live-telemetry.js +471 -0
  60. package/dist/telemetry/live-telemetry.js.map +1 -0
  61. package/dist/telemetry/scanner-stats.d.ts +136 -0
  62. package/dist/telemetry/scanner-stats.d.ts.map +1 -0
  63. package/dist/telemetry/scanner-stats.js +304 -0
  64. package/dist/telemetry/scanner-stats.js.map +1 -0
  65. package/dist/telemetry/strategy-topology.d.ts +99 -0
  66. package/dist/telemetry/strategy-topology.d.ts.map +1 -0
  67. package/dist/telemetry/strategy-topology.js +93 -0
  68. package/dist/telemetry/strategy-topology.js.map +1 -0
  69. package/dist/telemetry/telemetry-publisher.d.ts +63 -0
  70. package/dist/telemetry/telemetry-publisher.d.ts.map +1 -0
  71. package/dist/telemetry/telemetry-publisher.js +229 -0
  72. package/dist/telemetry/telemetry-publisher.js.map +1 -0
  73. package/dist/telemetry/timeline-buffer.d.ts +95 -0
  74. package/dist/telemetry/timeline-buffer.d.ts.map +1 -0
  75. package/dist/telemetry/timeline-buffer.js +237 -0
  76. package/dist/telemetry/timeline-buffer.js.map +1 -0
  77. package/dist/telemetry/tracing.d.ts +9 -0
  78. package/dist/telemetry/tracing.d.ts.map +1 -1
  79. package/dist/telemetry/tracing.js +36 -0
  80. package/dist/telemetry/tracing.js.map +1 -1
  81. package/dist/utils/event-catalog.d.ts +8 -0
  82. package/dist/utils/event-catalog.d.ts.map +1 -1
  83. package/dist/utils/event-catalog.js.map +1 -1
  84. package/dist/utils/ulid.d.ts +9 -0
  85. package/dist/utils/ulid.d.ts.map +1 -0
  86. package/dist/utils/ulid.js +43 -0
  87. package/dist/utils/ulid.js.map +1 -0
  88. package/dist/validate/proof.js +4 -1
  89. package/dist/validate/proof.js.map +1 -1
  90. package/dist/validate/run.d.ts.map +1 -1
  91. package/dist/validate/run.js +4 -1
  92. package/dist/validate/run.js.map +1 -1
  93. package/openclaw.plugin.json +1 -1
  94. package/package.json +1 -1
@@ -0,0 +1,471 @@
1
+ /**
2
+ * The publisher: the piece that turns the buffer and the three builders into frames on the gateway
3
+ * websocket, plus the snapshot a client reads when it connects.
4
+ *
5
+ * Two things about the transport shape the whole module:
6
+ * - The only route to the websocket is a captured `context.broadcast`, and `context` exists only
7
+ * inside a gateway RPC handler — the plugin api object has no push surface at all. So the handle is
8
+ * captured from the FIRST request of ANY kind, not only from the subscribe method: a box whose
9
+ * client never calls subscribe would otherwise hold nothing to publish through, silently.
10
+ * - The gateway drops any event name that does not start with `plugin.`, and closes the socket with
11
+ * 1008 for a slow client unless the call passes `dropIfSlow`. Both are enforced here rather than at
12
+ * each call site, because either mistake is invisible until a client is missing frames.
13
+ *
14
+ * Three cadences, because the three payloads cost different things to build:
15
+ * - timeline, on the flush tick — a drain plus a filter, cheap, and the one a live screen feels.
16
+ * - stats, on the sample tick — a scan of the whole held window and an async read of every runtime's
17
+ * health and system state, so it runs an order of magnitude less often.
18
+ * - topology, on the sample tick but published only when it changes — it rides the same gather as
19
+ * stats, and nothing in the plugin signals a topology change, so "on change" means polled and
20
+ * diffed rather than pushed.
21
+ *
22
+ * The snapshot is what stops a client that joins mid-life from seeing an empty screen: it is read
23
+ * from the buffer's held window, which the flush drain never evicts.
24
+ */
25
+ import { createLogger } from "../utils/logger.js";
26
+ import { withSpanSync } from "./tracing.js";
27
+ import { ADMITTED_EVENT_NAMES, readAndResetTapCounters } from "./telemetry-publisher.js";
28
+ import { getTimelineBuffer, teardownTimelineBuffer, } from "./timeline-buffer.js";
29
+ import { buildScannerStats, } from "./scanner-stats.js";
30
+ import { buildStrategyTopology, } from "./strategy-topology.js";
31
+ const logger = createLogger("[senpi-core][live-telemetry]");
32
+ /** The gateway drops any broadcast whose name does not carry this prefix, with no error. */
33
+ export const EVENT_TIMELINE = "plugin.senpi.timeline";
34
+ export const EVENT_STATS = "plugin.senpi.scanner.stats";
35
+ export const EVENT_TOPOLOGY = "plugin.senpi.strategies";
36
+ /**
37
+ * Byte ceiling for one frame. The bridge's north read limit is 256 KiB and a breach closes the
38
+ * client socket with 1009, so the budget is set at half of it: the payload envelope the gateway wraps
39
+ * around ours, and the growth of a single record's attribute bag, both have to fit inside the margin.
40
+ */
41
+ export const MAX_FRAME_BYTES = 128 * 1024;
42
+ /** Flush cadence for the timeline. A live screen feels this number. */
43
+ export const DEFAULT_FLUSH_INTERVAL_MS = 1_000;
44
+ /**
45
+ * Sample cadence for stats and topology. Both are built from one async read of every runtime's health
46
+ * and system state, so they share a tick. Five seconds is roughly a third of the default bucket width
47
+ * — fast enough that a counter visibly moves and a hung scan is seen while it is still running, slow
48
+ * enough that the per-scanner store read behind the system state is not a poll loop.
49
+ */
50
+ export const DEFAULT_SAMPLE_INTERVAL_MS = 5_000;
51
+ /**
52
+ * The PUBLICATION list: what leaves on the timeline event. A name being on this list AND on the
53
+ * admission list is the normal case, not a duplication — `signal.outcome` is the decision row the feed
54
+ * is built around and the source of `opens`/`closes`/`skips`, and the two `scaffold.tick_*` names are
55
+ * the source of `errors` and the rows behind the Errors filter. A failing external scanner has to
56
+ * leave a visible line, not only a number that silently goes up.
57
+ *
58
+ * The two lists are separate so admission can be widened for a counter the feed does not display,
59
+ * which is the direction that has to stay open: the display filter belongs here at publish time,
60
+ * where it is still recoverable, never at append time, where it is not. Today nothing is admitted for
61
+ * counting alone, so the sets are equal — the seam is what matters, not the difference.
62
+ */
63
+ export const PUBLISHED_EVENT_NAMES = new Set(ADMITTED_EVENT_NAMES);
64
+ /**
65
+ * The share of its own interval a tick may take before it is warned about. A fraction rather than a
66
+ * constant, so a deployment that changes a cadence gets a proportional threshold: 250ms on the
67
+ * one-second timeline flush, 1.25s on the five-second sample.
68
+ *
69
+ * A quarter, not a whole interval. Both ticks share an event loop with the serial trading path, so a
70
+ * tick holding it for a quarter of its own period is already adding latency to trading; and a tick
71
+ * that fills its interval has already started stacking on the next one, which is too late to be told.
72
+ */
73
+ export const SLOW_TICK_FRACTION = 0.25;
74
+ /**
75
+ * How often the timing summary is logged, and the floor between two slow-tick warns of the same kind.
76
+ * The flush tick runs every second, so a line per tick would be tens of thousands a day for numbers
77
+ * that only mean anything aggregated — and a repeating tick that is slow is slow every time.
78
+ */
79
+ export const TIMING_REPORT_INTERVAL_MS = 60_000;
80
+ /** What the builders see when the facts could not be read: nothing, rather than a stale guess. */
81
+ const NO_FACTS = {
82
+ strategies: [],
83
+ runningRuntimeIds: { has: () => false },
84
+ unwiredRuntimeIds: { has: () => false },
85
+ scannerState: [],
86
+ };
87
+ function toWireRecord(record) {
88
+ return {
89
+ seq: record.seq,
90
+ ts: record.ts,
91
+ name: record.name,
92
+ level: record.level,
93
+ address: record.address,
94
+ ...(record.scannerId !== undefined ? { scannerId: record.scannerId } : {}),
95
+ body: record.body,
96
+ attrs: record.attrs,
97
+ };
98
+ }
99
+ /** A cursor is a resume point, so anything that is not a whole non-negative number means "from the start". */
100
+ function normalizeAfterSeq(raw) {
101
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0)
102
+ return 0;
103
+ return Math.floor(raw);
104
+ }
105
+ /** Everything but `generatedAt`, which changes on every build and would make every topology "changed". */
106
+ function topologySignature(payload) {
107
+ return JSON.stringify({ v: payload.v, strategies: payload.strategies });
108
+ }
109
+ /** Microsecond precision. A sub-millisecond tick is the interesting case; its 17th digit is not. */
110
+ function roundMs(ms) {
111
+ return Math.round(ms * 1_000) / 1_000;
112
+ }
113
+ export function createLiveTelemetry(options) {
114
+ const ownsProcessBuffer = options.buffer === undefined;
115
+ const buffer = options.buffer ?? getTimelineBuffer();
116
+ const now = options.now ?? Date.now;
117
+ const monotonicMs = options.monotonicMs ?? (() => performance.now());
118
+ const flushIntervalMs = options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
119
+ const sampleIntervalMs = options.sampleIntervalMs ?? DEFAULT_SAMPLE_INTERVAL_MS;
120
+ // When this epoch began watching: `since` falls back to it while the buffer holds nothing, so an
121
+ // empty window reads as "watching since X, nothing happened" rather than "no data".
122
+ const bufferStartedAt = now();
123
+ let broadcast;
124
+ let flushTimer;
125
+ let sampleTimer;
126
+ let sampleInFlight = false;
127
+ let batchSeq = 0;
128
+ let lastTopologySignature;
129
+ // Both guards sit on per-second paths, so each warns once until the condition clears.
130
+ let warnedNoBroadcast = false;
131
+ let warnedBroadcastFailure = false;
132
+ let warnedOversizedRecord = false;
133
+ // What the ticks cost over one report window. Bucket width and buffer retention are deliberately
134
+ // unfixed by the design, and these are the numbers they get sized from.
135
+ const timelineWindow = { ticks: 0, msMax: 0, records: 0, payloadBytes: 0 };
136
+ const statsWindow = { ticks: 0, msMax: 0, recordsScanned: 0, payloadBytesMax: 0 };
137
+ const tapWindow = { calls: 0, nsTotal: 0, nsMax: 0 };
138
+ let lastReportAt = bufferStartedAt;
139
+ const lastSlowWarnAt = { timeline: 0, stats: 0 };
140
+ /** True when the frame reached the gateway, so a caller counts bytes only for what went out. */
141
+ function publish(event, payload) {
142
+ if (broadcast === undefined) {
143
+ if (!warnedNoBroadcast) {
144
+ warnedNoBroadcast = true;
145
+ logger.warn("live telemetry has no gateway broadcast handle yet; nothing is published until a client calls any senpi gateway method", { "senpi.telemetry.event": event });
146
+ }
147
+ return false;
148
+ }
149
+ try {
150
+ // `dropIfSlow` is not optional: without it the gateway closes the socket with 1008 for a slow
151
+ // client instead of skipping the one frame it could not fit.
152
+ broadcast(event, payload, { dropIfSlow: true });
153
+ warnedBroadcastFailure = false;
154
+ return true;
155
+ }
156
+ catch (err) {
157
+ if (!warnedBroadcastFailure) {
158
+ warnedBroadcastFailure = true;
159
+ logger.warn("live telemetry broadcast failed; frames are being lost", {
160
+ "senpi.telemetry.event": event,
161
+ err: err instanceof Error ? err.message : String(err),
162
+ });
163
+ }
164
+ return false;
165
+ }
166
+ }
167
+ /** Record what a tick cost, and warn when it is eating its own interval. */
168
+ function noteTick(kind, elapsedMs, intervalMs) {
169
+ const window = kind === "timeline" ? timelineWindow : statsWindow;
170
+ window.ticks += 1;
171
+ if (elapsedMs > window.msMax)
172
+ window.msMax = elapsedMs;
173
+ const thresholdMs = intervalMs * SLOW_TICK_FRACTION;
174
+ if (elapsedMs <= thresholdMs)
175
+ return;
176
+ const at = now();
177
+ // A tick that is slow is slow every time, and these repeat on the order of seconds.
178
+ if (at - lastSlowWarnAt[kind] < TIMING_REPORT_INTERVAL_MS)
179
+ return;
180
+ lastSlowWarnAt[kind] = at;
181
+ logger.warn("live telemetry tick is slow relative to its own interval", {
182
+ "senpi.telemetry.tick": kind,
183
+ "senpi.telemetry.tick_ms": roundMs(elapsedMs),
184
+ "senpi.telemetry.tick_threshold_ms": thresholdMs,
185
+ "senpi.telemetry.tick_interval_ms": intervalMs,
186
+ });
187
+ }
188
+ /**
189
+ * One aggregated line per window, off the flush tick. Logged and not only traced: the OTel SDK
190
+ * stands up only when an OTLP endpoint is configured, so on a box with no collector there are no
191
+ * spans and nothing that says there are none.
192
+ */
193
+ function reportTiming() {
194
+ const at = now();
195
+ const windowMs = at - lastReportAt;
196
+ if (windowMs < TIMING_REPORT_INTERVAL_MS)
197
+ return;
198
+ lastReportAt = at;
199
+ logger.info("live telemetry tick timing", {
200
+ "senpi.telemetry.window_ms": windowMs,
201
+ "senpi.telemetry.timeline_ticks": timelineWindow.ticks,
202
+ "senpi.telemetry.timeline_ms_max": roundMs(timelineWindow.msMax),
203
+ "senpi.telemetry.records_flushed": timelineWindow.records,
204
+ "senpi.telemetry.timeline_payload_bytes": timelineWindow.payloadBytes,
205
+ "senpi.telemetry.stats_ticks": statsWindow.ticks,
206
+ "senpi.telemetry.stats_ms_max": roundMs(statsWindow.msMax),
207
+ "senpi.telemetry.records_scanned": statsWindow.recordsScanned,
208
+ "senpi.telemetry.stats_payload_bytes_max": statsWindow.payloadBytesMax,
209
+ "senpi.telemetry.tap_calls": tapWindow.calls,
210
+ "senpi.telemetry.tap_ns_total": tapWindow.nsTotal,
211
+ // No tap call is an idle box, not a division: the average of nothing is nothing.
212
+ "senpi.telemetry.tap_ns_avg": tapWindow.calls === 0 ? 0 : Math.round(tapWindow.nsTotal / tapWindow.calls),
213
+ "senpi.telemetry.tap_ns_p_max": tapWindow.nsMax,
214
+ });
215
+ timelineWindow.ticks = 0;
216
+ timelineWindow.msMax = 0;
217
+ timelineWindow.records = 0;
218
+ timelineWindow.payloadBytes = 0;
219
+ statsWindow.ticks = 0;
220
+ statsWindow.msMax = 0;
221
+ statsWindow.recordsScanned = 0;
222
+ statsWindow.payloadBytesMax = 0;
223
+ tapWindow.calls = 0;
224
+ tapWindow.nsTotal = 0;
225
+ tapWindow.nsMax = 0;
226
+ }
227
+ function noteOversizedRecord(record, bytes) {
228
+ if (warnedOversizedRecord)
229
+ return;
230
+ warnedOversizedRecord = true;
231
+ logger.warn("live telemetry dropped a record too large for one frame", {
232
+ "senpi.event.name": record.name,
233
+ "senpi.telemetry.record_bytes": bytes,
234
+ "senpi.telemetry.frame_limit_bytes": MAX_FRAME_BYTES,
235
+ });
236
+ }
237
+ /**
238
+ * Split records into frames that each serialise under the byte budget. A record that cannot fit a
239
+ * frame by itself is dropped rather than sent: an oversized frame does not lose one line, it closes
240
+ * the client's socket with 1009 and loses the stream.
241
+ */
242
+ function toFrames(records) {
243
+ const frames = [];
244
+ let current = [];
245
+ let bytes = 0;
246
+ for (const record of records) {
247
+ const wire = toWireRecord(record);
248
+ // Bytes, not string length: the read limit is counted on the wire, and one non-ASCII
249
+ // character costs up to three of them.
250
+ const size = Buffer.byteLength(JSON.stringify(wire));
251
+ if (size > MAX_FRAME_BYTES) {
252
+ noteOversizedRecord(wire, size);
253
+ continue;
254
+ }
255
+ if (current.length > 0 && bytes + size > MAX_FRAME_BYTES) {
256
+ frames.push(current);
257
+ current = [];
258
+ bytes = 0;
259
+ }
260
+ current.push(wire);
261
+ bytes += size;
262
+ }
263
+ if (current.length > 0)
264
+ frames.push(current);
265
+ return frames;
266
+ }
267
+ function published(records) {
268
+ return records.filter((record) => PUBLISHED_EVENT_NAMES.has(record.name));
269
+ }
270
+ /** `null` when the facts could not be read — the caller decides whether to publish without them. */
271
+ async function readFacts() {
272
+ try {
273
+ return await options.collectFacts();
274
+ }
275
+ catch (err) {
276
+ // Health and state come from live runtime handles, which can be mid-teardown. A tick that
277
+ // cannot read them reports nothing rather than taking the stream down.
278
+ logger.warn("live telemetry could not read runtime facts", {
279
+ err: err instanceof Error ? err.message : String(err),
280
+ });
281
+ return null;
282
+ }
283
+ }
284
+ function buildStats(facts, sampledAt,
285
+ // The whole held window, not the drained batch: buckets are computed on demand from the same
286
+ // records the snapshot replays.
287
+ records) {
288
+ return buildScannerStats({
289
+ epoch: buffer.epoch,
290
+ records,
291
+ scannerState: facts.scannerState,
292
+ bufferStartedAt,
293
+ sampledAt,
294
+ });
295
+ }
296
+ function buildTopology(facts) {
297
+ return buildStrategyTopology({
298
+ epoch: buffer.epoch,
299
+ strategies: facts.strategies,
300
+ runningRuntimeIds: facts.runningRuntimeIds,
301
+ unwiredRuntimeIds: facts.unwiredRuntimeIds,
302
+ readHealth: facts.readHealth,
303
+ now,
304
+ });
305
+ }
306
+ function flushTimeline() {
307
+ const startedAt = monotonicMs();
308
+ // The drain advances the publish cursor whether or not a handle exists, so a box nobody is
309
+ // watching does not build a backlog that lands as one flood on the first connect. The snapshot
310
+ // covers that client instead — `drain` evicts nothing.
311
+ const batch = published(buffer.drain());
312
+ if (batch.length > 0) {
313
+ withSpanSync("telemetry.publish", {}, (span) => {
314
+ const sendStartedAt = monotonicMs();
315
+ let payloadBytes = 0;
316
+ let recordsFlushed = 0;
317
+ for (const records of toFrames(batch)) {
318
+ const payload = {
319
+ v: 1,
320
+ epoch: buffer.epoch,
321
+ batchSeq: ++batchSeq,
322
+ records,
323
+ };
324
+ // Measured on the frame handed to broadcast. A sum of record sizes is an intermediate that
325
+ // omits the envelope and the separators, and those are real bytes against the read limit.
326
+ const bytes = Buffer.byteLength(JSON.stringify(payload));
327
+ if (!publish(EVENT_TIMELINE, payload))
328
+ continue;
329
+ payloadBytes += bytes;
330
+ recordsFlushed += records.length;
331
+ }
332
+ span.setAttributes({
333
+ records_flushed: recordsFlushed,
334
+ payload_bytes: payloadBytes,
335
+ broadcast_ms: monotonicMs() - sendStartedAt,
336
+ });
337
+ timelineWindow.records += recordsFlushed;
338
+ timelineWindow.payloadBytes += payloadBytes;
339
+ });
340
+ }
341
+ noteTick("timeline", monotonicMs() - startedAt, flushIntervalMs);
342
+ reportTiming();
343
+ }
344
+ async function sample() {
345
+ // A gather slower than the tick would otherwise stack ticks on top of each other.
346
+ if (sampleInFlight)
347
+ return;
348
+ sampleInFlight = true;
349
+ const startedAt = monotonicMs();
350
+ try {
351
+ const facts = await readFacts();
352
+ if (facts === null)
353
+ return;
354
+ const stats = withSpanSync("telemetry.stats.compute", {}, (span) => {
355
+ // The tap counters are read here because this is the span that carries them: reading them on
356
+ // the faster tick would put a one-second window on a five-second measurement.
357
+ const tap = readAndResetTapCounters();
358
+ const records = buffer.read();
359
+ const payload = buildStats(facts, now(), records);
360
+ const payloadBytes = Buffer.byteLength(JSON.stringify(payload));
361
+ const bucketsEmitted = payload.scanners.reduce((n, entry) => n + entry.buckets.length, 0);
362
+ span.setAttributes({
363
+ records_scanned: records.length,
364
+ scanners: payload.scanners.length,
365
+ buckets_emitted: bucketsEmitted,
366
+ payload_bytes: payloadBytes,
367
+ tap_ns_total: tap.nsTotal,
368
+ tap_calls: tap.calls,
369
+ tap_ns_p_max: tap.nsMax,
370
+ });
371
+ statsWindow.recordsScanned += records.length;
372
+ if (payloadBytes > statsWindow.payloadBytesMax)
373
+ statsWindow.payloadBytesMax = payloadBytes;
374
+ tapWindow.calls += tap.calls;
375
+ tapWindow.nsTotal += tap.nsTotal;
376
+ if (tap.nsMax > tapWindow.nsMax)
377
+ tapWindow.nsMax = tap.nsMax;
378
+ return payload;
379
+ });
380
+ publish(EVENT_STATS, stats);
381
+ const topology = buildTopology(facts);
382
+ const signature = topologySignature(topology);
383
+ if (signature === lastTopologySignature)
384
+ return;
385
+ // Remembered only once the frame actually left. The ticks start before any client connects, so
386
+ // the first topology is built with no handle to send it through — recording that as delivered
387
+ // would suppress every later publish of a topology that then never changes again.
388
+ if (publish(EVENT_TOPOLOGY, topology))
389
+ lastTopologySignature = signature;
390
+ }
391
+ finally {
392
+ sampleInFlight = false;
393
+ // The whole tick, including the runtime read: that is what has to fit the interval, and it is
394
+ // the half the compute span cannot see.
395
+ noteTick("stats", monotonicMs() - startedAt, sampleIntervalMs);
396
+ }
397
+ }
398
+ return {
399
+ epoch: buffer.epoch,
400
+ captureBroadcast(ctx, methodName) {
401
+ if (broadcast !== undefined)
402
+ return;
403
+ const candidate = ctx?.context
404
+ ?.broadcast;
405
+ if (typeof candidate !== "function")
406
+ return;
407
+ broadcast = candidate;
408
+ warnedNoBroadcast = false;
409
+ logger.info("live telemetry captured the gateway broadcast handle", {
410
+ "senpi.gateway.method": methodName,
411
+ });
412
+ },
413
+ async snapshot(params) {
414
+ // A seq only means something inside its epoch. A cursor minted by a previous run names records
415
+ // this one never had — most sharply after a restart, where the client's seq sits far above a
416
+ // counter that began again at 1 and "nothing after it" would hide the whole window. So unless
417
+ // the cursor names this epoch it is dropped entirely and everything held is served.
418
+ const afterSeq = params?.afterEpoch === buffer.epoch ? normalizeAfterSeq(params?.afterSeq) : 0;
419
+ const held = buffer.read();
420
+ // A cursor below the eviction frontier lost a record. The oldest record still held cannot
421
+ // answer this: the rings are per address, so one strategy's untouched low seqs sit under
422
+ // another's evictions. `afterSeq: 0` is a fresh client asking for whatever is held, not a
423
+ // resume, so it is never a gap.
424
+ let truncated = afterSeq > 0 && buffer.highestDroppedSeq() > afterSeq;
425
+ const owed = published(held.filter((record) => record.seq > afterSeq));
426
+ const frames = toFrames(owed);
427
+ // A response is one frame, so an over-budget window sends the newest slice and says so.
428
+ const records = frames.length === 0 ? [] : frames[frames.length - 1];
429
+ if (frames.length > 1)
430
+ truncated = true;
431
+ // A snapshot still answers when the facts are unreadable: the recent window is the part a
432
+ // joining client cannot reconstruct, and it comes from the buffer alone.
433
+ const facts = (await readFacts()) ?? NO_FACTS;
434
+ const sampledAt = now();
435
+ return {
436
+ epoch: buffer.epoch,
437
+ topology: buildTopology(facts),
438
+ stats: buildStats(facts, sampledAt, held),
439
+ recent: { v: 1, epoch: buffer.epoch, batchSeq: ++batchSeq, records },
440
+ truncated,
441
+ };
442
+ },
443
+ flushTimeline,
444
+ sample,
445
+ start() {
446
+ if (flushTimer !== undefined)
447
+ return;
448
+ flushTimer = setInterval(flushTimeline, flushIntervalMs);
449
+ sampleTimer = setInterval(() => void sample(), sampleIntervalMs);
450
+ // Telemetry must never be the reason a process stays alive.
451
+ flushTimer.unref?.();
452
+ sampleTimer.unref?.();
453
+ },
454
+ stop() {
455
+ if (flushTimer !== undefined)
456
+ clearInterval(flushTimer);
457
+ if (sampleTimer !== undefined)
458
+ clearInterval(sampleTimer);
459
+ flushTimer = undefined;
460
+ sampleTimer = undefined;
461
+ broadcast = undefined;
462
+ lastTopologySignature = undefined;
463
+ buffer.teardown();
464
+ // The next reader mints a fresh epoch, so a restarted seq run cannot collide with the one a
465
+ // client already holds.
466
+ if (ownsProcessBuffer)
467
+ teardownTimelineBuffer();
468
+ },
469
+ };
470
+ }
471
+ //# sourceMappingURL=live-telemetry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-telemetry.js","sourceRoot":"","sources":["../../src/telemetry/live-telemetry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The `plugin.senpi.scanner.stats` payload: per-scanner activity counters plus the per-scanner state
3
+ * a summary screen needs, built as one pure function of its inputs.
4
+ *
5
+ * Two halves, from two sources:
6
+ * - The **counters** are computed on demand by scanning the recent-window buffer — no second data
7
+ * structure, no pre-aggregation on the write path, no retention setting of their own. Records are
8
+ * grouped by `(address, scannerId)` and bucketed by timestamp, because `scannerId` alone collides
9
+ * across strategies (the engine keys on the same pair).
10
+ * - The **state fields** are not derivable from records at all — health, schedule and liveness live in
11
+ * the health/system-state providers — so they arrive as {@link ScannerStateSnapshot} inputs and are
12
+ * joined on the same key. A scanner known to only one side still gets an entry: counters without
13
+ * state read `unknown`, state without counters carries an empty bucket list.
14
+ *
15
+ * The box never picks the window. It emits fixed-width buckets and the frontend sums however many it
16
+ * wants, so one payload serves every client and every window at once. `since` is what makes that sum
17
+ * honest: it is the stamp of the oldest record still held, and when nothing is held it is the moment
18
+ * this epoch began watching — "I have been watching since X and nothing happened" is a different
19
+ * statement from "no data", and only one of them is true.
20
+ */
21
+ import type { ComponentHealth } from "../health/types.js";
22
+ import type { TimelineRecord } from "./timeline-buffer.js";
23
+ /**
24
+ * Bucket width in ms. It sets two things at once: the payload cost per active scanner, and the finest
25
+ * window a frontend can ask for (it can sum buckets, never split one).
26
+ *
27
+ * 15s divides the windows a summary screen actually shows — 1m is 4 buckets, 5m is 20, 15m is 60 — so
28
+ * no rendered window ever needs a partial bucket. It is also about two ticks of the fastest scanner
29
+ * the runtime will schedule (built-in cadence is floored at 7s in `runtime-schema.ts`), which keeps a
30
+ * bucket from hiding a run while still letting "it went quiet 30 seconds ago" show up. Buckets are
31
+ * emitted sparsely, so an idle scanner costs nothing regardless of this value.
32
+ *
33
+ * This is the number to change once real payload sizes are measured — see also the `bucketMs` input,
34
+ * which overrides it per call.
35
+ */
36
+ export declare const DEFAULT_BUCKET_MS = 15000;
37
+ /**
38
+ * Ceiling on the buckets one payload may carry, across every scanner and every address.
39
+ *
40
+ * Buckets are the only part of this payload that grows without a bound of its own. A scanner
41
+ * producing one counted record per run fills one bucket per run, so a retention window of N records
42
+ * is N buckets on a single scanner, and every address the box runs adds its own series — the state
43
+ * rows do not, because there is one per registered scanner. One bucket serialises to about sixty
44
+ * bytes, so this budgets them at roughly 60 KiB and leaves the rest of the frame to the rows.
45
+ *
46
+ * Nothing downstream survives a breach: the bridge's north read limit is 256 KiB and exceeding it
47
+ * closes the client socket, which loses the stream rather than one sample.
48
+ *
49
+ * The trim cuts at a bucket boundary, so a payload may carry up to one extra bucket per scanner —
50
+ * splitting a timestamp across scanners would misalign the series a client sums.
51
+ */
52
+ export declare const MAX_STATS_BUCKETS = 1000;
53
+ /**
54
+ * The four counters, which sum to the decisions counted in the bucket. `closes` is its own field
55
+ * because an accepted close is none of the other three: it opened nothing, it was not declined, and
56
+ * nothing failed — folding it into any of them replaces one wrong number with another.
57
+ */
58
+ export interface ScannerStatsBucket {
59
+ /** Bucket start, epoch ms, aligned to a `bucketMs` grid so buckets line up across scanners. */
60
+ readonly t: number;
61
+ readonly opens: number;
62
+ readonly closes: number;
63
+ readonly skips: number;
64
+ readonly errors: number;
65
+ }
66
+ /**
67
+ * One scanner's state as the health/system-state providers report it. These fields have no
68
+ * representation in the buffer, so the builder takes them rather than reaching into the runtime.
69
+ *
70
+ * `inFlightSince` is a timestamp, not a boolean: a scan running for 200ms is normal and one running
71
+ * for four minutes on a 15s interval is hung, and a boolean cannot tell those apart.
72
+ */
73
+ export interface ScannerStateSnapshot {
74
+ readonly address: string;
75
+ readonly scannerId: string;
76
+ readonly health: ComponentHealth;
77
+ readonly degradedReason?: string | null;
78
+ readonly enabled: boolean;
79
+ readonly inFlightSince: number | null;
80
+ readonly lastRunStartedAt: number | null;
81
+ readonly lastRunFinishedAt: number | null;
82
+ readonly lastRunStatus: string | null;
83
+ readonly lastRunReason: string | null;
84
+ readonly nextRunAt: number | null;
85
+ readonly lastAliveAt: number | null;
86
+ /** Null when no supervision row covers this scanner — a known zero is a different statement. */
87
+ readonly restartCount: number | null;
88
+ }
89
+ /**
90
+ * One scanner on the wire. `scannerId` is absent — not null — when the records could not be
91
+ * attributed to a scanner; the client buckets those separately rather than seeing them folded into a
92
+ * named scanner's numbers. Every state field is `null` when no snapshot covers this scanner, which is
93
+ * a different statement from a known `false` or a known `0`.
94
+ */
95
+ export interface ScannerStatsEntry {
96
+ readonly address: string;
97
+ readonly scannerId?: string;
98
+ readonly health: ComponentHealth;
99
+ readonly degradedReason: string | null;
100
+ readonly enabled: boolean | null;
101
+ readonly inFlightSince: number | null;
102
+ readonly lastRunStartedAt: number | null;
103
+ readonly lastRunFinishedAt: number | null;
104
+ readonly lastRunStatus: string | null;
105
+ readonly lastRunReason: string | null;
106
+ readonly nextRunAt: number | null;
107
+ readonly lastAliveAt: number | null;
108
+ readonly restartCount: number | null;
109
+ /** Only buckets with activity, ascending by `t`. A gap means zero, not missing. */
110
+ readonly buckets: ScannerStatsBucket[];
111
+ }
112
+ export interface ScannerStatsPayload {
113
+ readonly v: 1;
114
+ readonly epoch: string;
115
+ readonly sampledAt: number;
116
+ /** Oldest record still held, or the buffer's start time when nothing is held. Never absent. */
117
+ readonly since: number;
118
+ readonly bucketMs: number;
119
+ readonly scanners: ScannerStatsEntry[];
120
+ }
121
+ export interface BuildScannerStatsInput {
122
+ /** The buffer's epoch ULID. A change means the box restarted. */
123
+ readonly epoch: string;
124
+ /** Every record the counters are derived from — the buffer's full held window. */
125
+ readonly records: readonly TimelineRecord[];
126
+ /** Per-scanner state from the health/system-state providers. */
127
+ readonly scannerState: readonly ScannerStateSnapshot[];
128
+ /** When this epoch began watching. Used as `since` only while the buffer holds nothing. */
129
+ readonly bufferStartedAt: number;
130
+ readonly sampledAt: number;
131
+ /** Overrides {@link DEFAULT_BUCKET_MS}. Ignored unless it is a finite width of at least 1ms. */
132
+ readonly bucketMs?: number;
133
+ }
134
+ /** Build one `plugin.senpi.scanner.stats` payload. Pure: no timers, no clocks, no runtime reads. */
135
+ export declare function buildScannerStats(input: BuildScannerStatsInput): ScannerStatsPayload;
136
+ //# sourceMappingURL=scanner-stats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scanner-stats.d.ts","sourceRoot":"","sources":["../../src/telemetry/scanner-stats.ts"],"names":[],"mappings":"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"}