@lensmcp/cluster 1.25.9 → 1.25.10

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 (133) hide show
  1. package/README.md +1 -1
  2. package/basic-ssl.js +1 -1
  3. package/build-host.js +1 -1
  4. package/build-scope-patterns.js +1 -1
  5. package/bundle-parent.js +1 -1
  6. package/checkout-cache.js +1 -1
  7. package/create-webpack-dev.js +1 -1
  8. package/create-webpack-prod.js +1 -1
  9. package/dev-devtool.js +1 -1
  10. package/devserver-notifier.js +1 -1
  11. package/devserver-transport.js +1 -1
  12. package/error-stack-release.js +1 -1
  13. package/executors/build/build.impl.js +1 -1
  14. package/executors/gateway/body-stall.js +1 -1
  15. package/executors/gateway/canonical-path.js +1 -1
  16. package/executors/gateway/client-ip.js +1 -1
  17. package/executors/gateway/gateway-errors.js +1 -1
  18. package/executors/gateway/gateway.impl.js +1 -1
  19. package/executors/gateway/gateway.lib.js +1 -1
  20. package/executors/gateway/health-check.js +1 -1
  21. package/executors/gateway/jwks-verify.js +1 -1
  22. package/executors/gateway/jwt-claims.js +1 -1
  23. package/executors/gateway/main.prod-gateway.js +2 -2
  24. package/executors/gateway/main.rollout.js +4 -4
  25. package/executors/gateway/manifest.js +1 -1
  26. package/executors/gateway/metrics.js +1 -1
  27. package/executors/gateway/otel-tracing.js +1 -1
  28. package/executors/gateway/prod-config.js +1 -1
  29. package/executors/gateway/prod-gateway.lib.js +1 -1
  30. package/executors/gateway/prod-runtime/access-log.js +1 -1
  31. package/executors/gateway/prod-runtime/app.js +1 -1
  32. package/executors/gateway/prod-runtime/auth.js +1 -1
  33. package/executors/gateway/prod-runtime/body-stall.js +1 -1
  34. package/executors/gateway/prod-runtime/cors.js +1 -1
  35. package/executors/gateway/prod-runtime/edge.js +1 -1
  36. package/executors/gateway/prod-runtime/handler.js +1 -1
  37. package/executors/gateway/prod-runtime/hooks.js +1 -1
  38. package/executors/gateway/prod-runtime/observability.js +1 -1
  39. package/executors/gateway/prod-runtime/rollout.js +1 -1
  40. package/executors/gateway/prod-runtime/routing.js +1 -1
  41. package/executors/gateway/prod-runtime/server.js +1 -1
  42. package/executors/gateway/prod-runtime/trust.js +1 -1
  43. package/executors/gateway/prod-runtime/upgrade.js +7 -7
  44. package/executors/gateway/prod-runtime/upstream.js +1 -1
  45. package/executors/gateway/providers-prod.js +1 -1
  46. package/executors/gateway/rate-limit.js +2 -2
  47. package/executors/gateway/registry-source.js +1 -1
  48. package/executors/gateway/rollout-ops.js +2 -2
  49. package/executors/gateway/runtime/act.js +1 -1
  50. package/executors/gateway/runtime/auth.js +1 -1
  51. package/executors/gateway/runtime/chooser.js +12 -12
  52. package/executors/gateway/runtime/control.js +2 -2
  53. package/executors/gateway/runtime/dev-auth.js +1 -1
  54. package/executors/gateway/runtime/discovery.js +1 -1
  55. package/executors/gateway/runtime/edge.js +1 -1
  56. package/executors/gateway/runtime/guest-graph.js +2 -2
  57. package/executors/gateway/runtime/guest-replay.js +2 -2
  58. package/executors/gateway/runtime/handler.js +1 -1
  59. package/executors/gateway/runtime/hmr-reload.js +1 -1
  60. package/executors/gateway/runtime/hooks.js +1 -1
  61. package/executors/gateway/runtime/instance.js +2 -2
  62. package/executors/gateway/runtime/lens-children.js +1 -1
  63. package/executors/gateway/runtime/lifecycle.js +3 -3
  64. package/executors/gateway/runtime/observability.js +2 -2
  65. package/executors/gateway/runtime/own-log.d.ts +18 -3
  66. package/executors/gateway/runtime/own-log.js +4 -3
  67. package/executors/gateway/runtime/pod-env.js +2 -2
  68. package/executors/gateway/runtime/pod-output.js +1 -1
  69. package/executors/gateway/runtime/pod-probe.js +1 -1
  70. package/executors/gateway/runtime/process-names.js +1 -1
  71. package/executors/gateway/runtime/project-graph-store.js +1 -1
  72. package/executors/gateway/runtime/proxy.js +1 -1
  73. package/executors/gateway/runtime/registration-guards.js +1 -1
  74. package/executors/gateway/runtime/route-registry.js +1 -1
  75. package/executors/gateway/runtime/scope.js +1 -1
  76. package/executors/gateway/runtime/serve-launch.js +3 -3
  77. package/executors/gateway/runtime/server.js +2 -2
  78. package/executors/gateway/runtime/service-keys.js +1 -1
  79. package/executors/gateway/runtime/shared-build.js +4 -4
  80. package/executors/gateway/runtime/source-poller.d.ts +323 -22
  81. package/executors/gateway/runtime/source-poller.js +1 -1
  82. package/executors/gateway/runtime/types.d.ts +31 -10
  83. package/executors/gateway/runtime/types.js +1 -1
  84. package/executors/gateway/runtime/upgrade.js +1 -1
  85. package/executors/gateway/runtime/workspace-log.js +1 -1
  86. package/executors/gateway/runtime/workspace-middleware.js +1 -1
  87. package/executors/gateway/runtime/workspace-registry.js +1 -1
  88. package/executors/gateway/ssrf-guard.js +1 -1
  89. package/executors/serve/serve.impl.js +1 -1
  90. package/executors/trust/hostname.js +1 -1
  91. package/executors/trust/trust.impl.js +3 -3
  92. package/exit-reason.js +2 -2
  93. package/fork-ts-check-reporter.js +1 -1
  94. package/gateway.js +1 -1
  95. package/gc-notice.js +1 -1
  96. package/graph-snapshot.js +1 -1
  97. package/handoff-retire.js +1 -1
  98. package/heap-snapshot.js +1 -1
  99. package/hot-swap-registries.js +1 -1
  100. package/index.js +1 -1
  101. package/inspector-secret.js +1 -1
  102. package/main.devserver.js +11 -11
  103. package/notice-generation.js +1 -1
  104. package/package.json +4 -4
  105. package/perf/heavy-barrel-import.js +1 -1
  106. package/perf/perf-config.js +1 -1
  107. package/perf/perf-rule-events.js +2 -2
  108. package/perf/perf-rules-analysis.js +1 -1
  109. package/perf/perf-rules-plugin.js +1 -1
  110. package/perf/source-scan.js +5 -5
  111. package/perf/workspace-libraries.js +2 -2
  112. package/pod-memory-events.js +2 -2
  113. package/pod-memory.js +1 -1
  114. package/pod-sock.js +1 -1
  115. package/process-name.js +1 -1
  116. package/redis-disconnect-fix.js +1 -1
  117. package/schema-first-instance.js +1 -1
  118. package/serve-runner.js +1 -1
  119. package/service-build-events.js +1 -1
  120. package/shared-build/attach.js +2 -2
  121. package/shared-build/host.js +3 -3
  122. package/shared-build/protocol.js +1 -1
  123. package/shared-build/shared-compilation.js +3 -3
  124. package/shared-build/signature.js +1 -1
  125. package/stdio-guard.js +1 -1
  126. package/swap-mode.js +1 -1
  127. package/swap-recycle.js +1 -1
  128. package/tsgo-check-plugin.js +4 -4
  129. package/typecheck-bus.js +3 -3
  130. package/typecheck-queue.js +1 -1
  131. package/worker-supervisor.d.ts +1 -1
  132. package/worker-supervisor.js +2 -2
  133. package/workspace-paths.js +1 -1
@@ -5,18 +5,63 @@ export declare const sourcesEnabled: () => boolean;
5
5
  export declare const sourcesTimeoutMs: () => number;
6
6
  /** Polls in flight at once, across every service (`LENSMCP_SOURCES_CONCURRENCY`, default 4). */
7
7
  export declare const sourcesConcurrency: () => number;
8
- /** The closed set of kinds, and the ones this release reads. MIRROR of `SOURCE_KINDS` / `SOURCE_KINDS_READ`. */
8
+ /**
9
+ * The closed set of kinds, and the ones this release reads — all of them since phase 3, so a kind outside the set is the
10
+ * only one refused. MIRROR of `SOURCE_KINDS` / `SOURCE_KINDS_READ`.
11
+ */
9
12
  export declare const SOURCE_KINDS: readonly ["queue", "loop", "store", "traffic"];
10
- export declare const SOURCE_KINDS_READ: readonly ["queue"];
13
+ export declare const SOURCE_KINDS_READ: readonly ["queue", "loop", "store", "traffic"];
11
14
  export type SourceKindRead = (typeof SOURCE_KINDS_READ)[number];
12
- /** MIRROR of `QUEUE_WIRE_SCHEMA`. */
15
+ /** MIRROR of `QUEUE_WIRE_SCHEMA` / `LOOP_WIRE_SCHEMA` / `STORE_WIRE_SCHEMA` / `TRAFFIC_WIRE_SCHEMA`. */
13
16
  export declare const QUEUE_WIRE_SCHEMA = "lensmcp.queue/v1";
17
+ export declare const LOOP_WIRE_SCHEMA = "lensmcp.loop/v1";
18
+ export declare const STORE_WIRE_SCHEMA = "lensmcp.store/v1";
19
+ export declare const TRAFFIC_WIRE_SCHEMA = "lensmcp.traffic/v1";
20
+ /** Where each kind's last good snapshot rides on a report — never another kind's field. MIRROR of `SOURCE_SNAPSHOT_FIELD`. */
21
+ export declare const SOURCE_SNAPSHOT_FIELD: {
22
+ readonly queue: "snapshot";
23
+ readonly loop: "loop";
24
+ readonly store: "store";
25
+ readonly traffic: "traffic";
26
+ };
27
+ /** How a loop's run ended: it worked, it failed, or it chose not to (a gate held it). MIRROR of `LOOP_OUTCOMES`. */
28
+ export declare const LOOP_OUTCOMES: readonly ["ok", "failed", "skipped"];
29
+ export type LoopOutcome = (typeof LOOP_OUTCOMES)[number];
30
+ /** Which way a traffic channel's messages go, seen from the service. */
31
+ export declare const TRAFFIC_DIRECTIONS: readonly ["inbound", "outbound"];
32
+ export type TrafficDirection = (typeof TRAFFIC_DIRECTIONS)[number];
14
33
  export declare const SOURCE_DEFAULT_INTERVAL_SEC = 10;
15
34
  export declare const SOURCE_MIN_INTERVAL_SEC = 2;
35
+ /** MIRROR of `QUEUE_DEFAULT_OLDEST_WAIT_MIN` / `QUEUE_DEFAULT_STALLED_MIN` / `LOOP_DEFAULT_IDLE_MIN`. */
16
36
  export declare const QUEUE_DEFAULT_OLDEST_WAIT_MIN = 10;
37
+ export declare const QUEUE_DEFAULT_STALLED_MIN = 15;
38
+ export declare const LOOP_DEFAULT_IDLE_MIN = 10;
39
+ /** MIRROR of `STORE_DEFAULT_QUOTA_PCT` / `STORE_DEFAULT_ERROR_RATE_PCT` / `STORE_DEFAULT_ORPHAN_MIN`. */
40
+ export declare const STORE_DEFAULT_QUOTA_PCT = 90;
41
+ export declare const STORE_DEFAULT_ERROR_RATE_PCT = 5;
42
+ export declare const STORE_DEFAULT_ORPHAN_MIN = 60;
43
+ /** MIRROR of `TRAFFIC_DEFAULT_FAILURE_PCT` / `TRAFFIC_DEFAULT_P95_MS` / `TRAFFIC_DEFAULT_RATE_LIMITED`. */
44
+ export declare const TRAFFIC_DEFAULT_FAILURE_PCT = 5;
45
+ export declare const TRAFFIC_DEFAULT_P95_MS = 2000;
46
+ export declare const TRAFFIC_DEFAULT_RATE_LIMITED = 1;
47
+ /**
48
+ * A report carries the snapshot in full when it changed, and at least this often even when it did not — so a lens
49
+ * that starts later (its replay is a few MB of the bus) holds one within a minute. MIRROR of `SOURCE_FULL_REPORT_MS`.
50
+ */
51
+ export declare const SOURCE_FULL_REPORT_MS = 60000;
52
+ /** A loop's own period may be at most a day: a longer one is not a loop the lens can watch run. */
53
+ export declare const LOOP_MAX_INTERVAL_SEC = 86400;
54
+ /**
55
+ * A loop's, a store's or a traffic channel's `lastError` is cut to this many characters (not refused: the error is the
56
+ * thing worth reading).
57
+ */
58
+ export declare const SOURCE_ERROR_MAX = 500;
17
59
  /** A snapshot larger than this is `malformed` — read no further. */
18
60
  export declare const SOURCE_MAX_BYTES: number;
19
- /** Groups one snapshot may hold; a producer with more caps it and says `truncated: { groups }`. */
61
+ /**
62
+ * Groups (a queue's, a store's) or channels (a traffic source's) one snapshot may hold; a producer with more caps it
63
+ * and says `truncated: { groups }` / `truncated: { channels }`.
64
+ */
20
65
  export declare const SOURCE_MAX_GROUPS = 500;
21
66
  /** Sources one service may declare. */
22
67
  export declare const SOURCE_MAX_PER_SERVICE = 16;
@@ -30,27 +75,60 @@ export declare const SOURCE_BACKOFF_MAX_MS = 60000;
30
75
  export declare const sourceStaleAfterMs: (reportEveryMs: number) => number;
31
76
  /** How long to wait before polling again after `failures` failed polls in a row: the interval, then doubling, capped. */
32
77
  export declare function sourceBackoffMs(intervalMs: number, failures: number): number;
33
- export interface QueueThresholds {
34
- /** Warn once a group's oldest pending item has waited longer than this. */
35
- oldestWaitMin: number;
36
- /** Accepted now; judged once the lens keeps a history of snapshots (phase 2). */
78
+ /**
79
+ * A source's thresholds — each kind's own, every one filled with its default when not given, so the lens judges each.
80
+ * Minutes unless the name says otherwise (`…Pct` a percent, `p95Ms` milliseconds, `rateLimited` a count). MIRROR of
81
+ * `SourceThresholds`.
82
+ */
83
+ export interface SourceThresholds {
84
+ /** A queue: warn once a group's oldest pending item has waited longer than this. */
85
+ oldestWaitMin?: number;
86
+ /** A queue: warn once a group's pending work has not moved (nothing taken, nothing finished) for this long. */
37
87
  stalledMin?: number;
88
+ /** A loop: warn once it has run and taken nothing for this long while the queue it `feeds` has work. */
89
+ idleMin?: number;
90
+ /** A store: warn once its bytes are at or over this percent of its quota. */
91
+ quotaPct?: number;
92
+ /** A store: warn once its window's errors are over this percent of its operations. */
93
+ errorRatePct?: number;
94
+ /** A store: warn once it has grown for this long and deleted nothing — what nothing cleans up. */
95
+ orphanMin?: number;
96
+ /** A traffic channel: warn once its failures are over this percent of its messages. */
97
+ failurePct?: number;
98
+ /** A traffic channel: warn once its p95 is over this many milliseconds. */
99
+ p95Ms?: number;
100
+ /** A traffic channel: warn once its counterpart answered «too many requests» this many times in the window. */
101
+ rateLimited?: number;
38
102
  }
39
- /** One `cluster.sources` entry, as the gateway polls it. */
103
+ /** One `cluster.sources` entry, as the gateway reads it. */
40
104
  export interface SourceDecl {
41
105
  kind: SourceKindRead;
42
106
  /** Names the source in the lens (`product-analysis outbox`). Unique per service. */
43
107
  name: string;
44
- /** A dev route ON THE SERVICE, sent to its pod as-is (the edge's `prependPrefix` never applies). */
45
- path: string;
108
+ /**
109
+ * A dev route ON THE SERVICE, sent to its pod as-is (the edge's `prependPrefix` never applies). Absent only on a
110
+ * PUSH-ONLY loop or traffic source — declared, never polled: the lens reads the events the app pushes. A loop that runs
111
+ * in a worker process (FoodGuard's admission poll) has no route an API pod could answer for it.
112
+ */
113
+ path?: string;
114
+ /**
115
+ * How often the gateway POLLS it, in seconds — for a polled loop, not the loop's own period (that one is on the wire).
116
+ * On a push-only LOOP it IS the loop's own period (the lens judges «overdue» by it); on push-only traffic nothing reads it.
117
+ */
46
118
  intervalSec: number;
47
- thresholds: QueueThresholds;
119
+ thresholds: SourceThresholds;
120
+ /** A loop: the NAME of the queue source on the same service that it drains. Absent when none, or none of that name. */
121
+ feeds?: string;
48
122
  }
49
123
  /**
50
124
  * `cluster.sources` read back, as `parseWorkerSpecs` reads `workers`: what cannot be used is left out and said in
51
- * `problems` — a bad entry never stops the others, and never passes silently. An unknown kind is said by name; a kind
52
- * of the closed set this release does not read yet is said as such; an entry with a key its kind does not know is
53
- * refused whole, so a misspelt `intervalSecs` is said rather than polled at the default.
125
+ * `problems` — a bad entry never stops the others, and never passes silently. A kind outside the closed set is said by
126
+ * name; an entry with a key its kind does not know is refused whole, so a misspelt `intervalSecs` is said rather than
127
+ * polled at the default (and `feeds` on anything but a loop is such a key); a threshold is refused in its own unit. A
128
+ * route is required of every kind but `traffic`, whose entry without one is PUSH-ONLY (declared, never polled) — a
129
+ * queue, a loop or a store without one would have nothing to say. A loop's `feeds` that names no queue source of this
130
+ * service costs only the link: the loop is read without it, and that is said — the loop's own numbers are still worth
131
+ * reading.
54
132
  */
55
133
  export declare function parseSourceDecls(raw: unknown): {
56
134
  sources: SourceDecl[];
@@ -81,6 +159,127 @@ export declare function parseQueueSnapshot(body: unknown): {
81
159
  ok: false;
82
160
  reason: string;
83
161
  };
162
+ /**
163
+ * A loop snapshot as the WIRE says it (`lensmcp.loop/v1`), validated — what a background loop, scheduler or sweep says
164
+ * of itself. Only `at`, `intervalSec` (the LOOP's own period, not the poll's) and `lastRunAt` are required.
165
+ */
166
+ export interface LoopWire {
167
+ at: string;
168
+ intervalSec: number;
169
+ /** When its last run began; `null`: it has not run since the service started. */
170
+ lastRunAt: string | null;
171
+ lastDurationMs?: number;
172
+ lastOutcome?: LoopOutcome;
173
+ /** Items its last run took. */
174
+ lastItems?: number;
175
+ nextRunAt?: string | null;
176
+ /** Over the service's own window of `windowSec`. */
177
+ runsInWindow?: number;
178
+ itemsInWindow?: number;
179
+ failuresInWindow?: number;
180
+ windowSec?: number;
181
+ /** The last failed run's error, cut to {@link SOURCE_ERROR_MAX} characters. */
182
+ lastError?: string;
183
+ }
184
+ /**
185
+ * A loop snapshot, validated as {@link parseQueueSnapshot} validates a queue's: any departure is a REASON and the source
186
+ * is `malformed` — never a loop that «did nothing». `lastRunAt` must be SAID — `null` when the loop has not run since the
187
+ * service started: an absent one cannot be told from a producer that forgot it. `lastError` is the one field CUT rather
188
+ * than refused: a long stack trace is exactly what a failing loop says, and its head is the part worth reading.
189
+ */
190
+ export declare function parseLoopSnapshot(body: unknown): {
191
+ ok: true;
192
+ loop: LoopWire;
193
+ } | {
194
+ ok: false;
195
+ reason: string;
196
+ };
197
+ /** One group of a store snapshot — a bucket, a tenant's folder — as the WIRE names it. */
198
+ export interface StoreWireGroup {
199
+ key: string;
200
+ labels?: Record<string, string>;
201
+ objects: number;
202
+ bytes: number;
203
+ }
204
+ /**
205
+ * A store snapshot as the WIRE says it (`lensmcp.store/v1`), validated — what a storage backend the taps do not model
206
+ * says of itself. Only `at`, `objects` and `bytes` are required.
207
+ */
208
+ export interface StoreWire {
209
+ at: string;
210
+ objects: number;
211
+ bytes: number;
212
+ /** What it may hold; `null`: no quota it knows of. */
213
+ quotaBytes?: number | null;
214
+ /** Operations of every kind over the service's window of `windowSec` — what `errorsInWindow` is a rate of. */
215
+ opsInWindow?: number;
216
+ writesInWindow?: number;
217
+ deletesInWindow?: number;
218
+ errorsInWindow?: number;
219
+ windowSec?: number;
220
+ lastWriteAt?: string | null;
221
+ /** The last failed operation's error, cut to {@link SOURCE_ERROR_MAX} characters. */
222
+ lastError?: string;
223
+ groups?: StoreWireGroup[];
224
+ truncated?: {
225
+ groups: number;
226
+ };
227
+ }
228
+ /**
229
+ * A store snapshot, validated as a queue's is: any departure is a REASON and the source is `malformed` — never an empty
230
+ * store. `objects` and `bytes` must be SAID: a store that cannot count itself has nothing the lens can judge. `groups`
231
+ * is optional — a store with no buckets to tell apart says none — but a list given is held to a queue's group rules.
232
+ * `lastError` is cut, never refused, as a loop's is.
233
+ */
234
+ export declare function parseStoreSnapshot(body: unknown): {
235
+ ok: true;
236
+ store: StoreWire;
237
+ } | {
238
+ ok: false;
239
+ reason: string;
240
+ };
241
+ /** One channel of a traffic snapshot, as the WIRE names it. */
242
+ export interface TrafficWireChannel {
243
+ key: string;
244
+ labels?: Record<string, string>;
245
+ direction?: TrafficDirection;
246
+ /** Who is at the other end — a partner, a host, a queue. */
247
+ counterpart?: string;
248
+ /** Over the service's window of `windowSec`. */
249
+ messagesInWindow?: number;
250
+ failuresInWindow?: number;
251
+ /** Answered «too many requests» (an HTTP 429, a quota) — counted apart from `failuresInWindow`. */
252
+ rateLimitedInWindow?: number;
253
+ p95Ms?: number;
254
+ lastAt?: string | null;
255
+ /** Cut to {@link SOURCE_ERROR_MAX} characters. */
256
+ lastError?: string;
257
+ }
258
+ /**
259
+ * A traffic snapshot as the WIRE says it (`lensmcp.traffic/v1`), validated — messages over channels the taps do not
260
+ * classify. Only `at` and `channels` are required.
261
+ */
262
+ export interface TrafficWire {
263
+ at: string;
264
+ windowSec?: number;
265
+ channels: TrafficWireChannel[];
266
+ truncated?: {
267
+ channels: number;
268
+ };
269
+ }
270
+ /**
271
+ * A traffic snapshot, validated as a queue's is: any departure is a REASON and the source is `malformed` — never «no
272
+ * traffic». `channels` must be SAID; an empty list that parses is a real «nothing went over it». A channel's numbers are
273
+ * all optional (a webhook may know its messages and not their p95), but one given is held to its type: a `p95Ms` may be
274
+ * fractional, a count may not. `lastError` is cut, never refused.
275
+ */
276
+ export declare function parseTrafficSnapshot(body: unknown): {
277
+ ok: true;
278
+ traffic: TrafficWire;
279
+ } | {
280
+ ok: false;
281
+ reason: string;
282
+ };
84
283
  export type SourcePollState = 'current' | 'unreachable' | 'refused' | 'malformed' | 'not-running';
85
284
  /** A group on the bus: the wire's `key` travels as `group` — the bus redacts any field NAMED `key`. */
86
285
  export interface QueueGroupRaw {
@@ -106,14 +305,61 @@ export interface QueueSnapshotRaw {
106
305
  truncated?: {
107
306
  groups: number;
108
307
  };
308
+ /** Names its content (the groups and `truncated`), not its times — {@link sourceDigest}. */
309
+ digest: string;
310
+ }
311
+ /** A loop snapshot on the bus: the wire's fields, when the gateway received it, and what it said ({@link sourceDigest}). */
312
+ export interface LoopSnapshotRaw extends LoopWire {
313
+ receivedAt: number;
314
+ /** Names every field but `at`/`receivedAt`. */
315
+ digest: string;
316
+ }
317
+ /** A store's group on the bus: the wire's `key` travels as `group`, as a queue's does. */
318
+ export interface StoreGroupRaw {
319
+ group: string;
320
+ labels?: Record<string, string>;
321
+ objects: number;
322
+ bytes: number;
323
+ }
324
+ /** A store snapshot on the bus: the wire's fields, its groups the largest first, when it arrived, and its digest. */
325
+ export interface StoreSnapshotRaw extends Omit<StoreWire, 'groups'> {
326
+ receivedAt: number;
327
+ /** Names every field but `at`/`receivedAt`. */
328
+ digest: string;
329
+ groups?: StoreGroupRaw[];
330
+ }
331
+ /** A traffic channel on the bus: the wire's `key` travels as `channel`. */
332
+ export interface TrafficChannelRaw extends Omit<TrafficWireChannel, 'key'> {
333
+ channel: string;
334
+ }
335
+ /** A traffic snapshot on the bus: the wire's fields, its channels the most messages first, when it arrived, its digest. */
336
+ export interface TrafficSnapshotRaw extends Omit<TrafficWire, 'channels'> {
337
+ receivedAt: number;
338
+ /** Names every field but `at`/`receivedAt`. */
339
+ digest: string;
340
+ channels: TrafficChannelRaw[];
341
+ }
342
+ /** A last good snapshot as the poller keeps it — of its source's kind. */
343
+ export type KeptSnapshot = QueueSnapshotRaw | LoopSnapshotRaw | StoreSnapshotRaw | TrafficSnapshotRaw;
344
+ /**
345
+ * In place of the snapshot, on a report whose last good snapshot is the one an earlier report carried in full: its
346
+ * digest, and its own `at`/`receivedAt` — fresh after a `current` poll that read the same again, the held one's after a
347
+ * poll that did not read. MIRROR of `SourceSameRaw`.
348
+ */
349
+ export interface SourceSameRaw {
350
+ digest: string;
351
+ at: string;
352
+ receivedAt: number;
109
353
  }
110
354
  /** A declaration on the bus. `sourceKind`, because `kind` is the event's own. */
111
355
  export interface SourceDeclRaw {
112
356
  sourceKind: SourceKindRead;
113
357
  name: string;
114
- path: string;
358
+ /** Absent only on a push-only loop or traffic source — which is declared, and never polled. */
359
+ path?: string;
115
360
  intervalSec: number;
116
- thresholds: QueueThresholds;
361
+ thresholds: SourceThresholds;
362
+ feeds?: string;
117
363
  }
118
364
  export interface SourcePollRaw extends SourceDeclRaw {
119
365
  kind: 'source-poll';
@@ -126,7 +372,16 @@ export interface SourcePollRaw extends SourceDeclRaw {
126
372
  durationMs?: number;
127
373
  reportEveryMs: number;
128
374
  failures?: number;
375
+ /** A queue's last good snapshot, in full. */
129
376
  snapshot?: QueueSnapshotRaw;
377
+ /** A loop's last good snapshot, in full. */
378
+ loop?: LoopSnapshotRaw;
379
+ /** A store's. */
380
+ store?: StoreSnapshotRaw;
381
+ /** A traffic source's (a push-only one is never polled, so never reported). */
382
+ traffic?: TrafficSnapshotRaw;
383
+ /** In place of the kind's snapshot: the last good snapshot is unchanged since the report that carried it in full. */
384
+ same?: SourceSameRaw;
130
385
  }
131
386
  export interface SourceDeclarationRaw {
132
387
  kind: 'source-declaration';
@@ -134,24 +389,64 @@ export interface SourceDeclarationRaw {
134
389
  sources: SourceDeclRaw[];
135
390
  problems: string[];
136
391
  }
392
+ /** A last good snapshot in full, under its kind's own field ({@link SOURCE_SNAPSHOT_FIELD}). */
393
+ export type SourceFull = {
394
+ snapshot: QueueSnapshotRaw;
395
+ } | {
396
+ loop: LoopSnapshotRaw;
397
+ } | {
398
+ store: StoreSnapshotRaw;
399
+ } | {
400
+ traffic: TrafficSnapshotRaw;
401
+ };
402
+ /** What one report carries of the last good snapshot: in full, or `same` in its place. */
403
+ export type SourceCarried = SourceFull | {
404
+ same: SourceSameRaw;
405
+ };
406
+ /** `kept` under its kind's field — never another kind's: the contract refuses a report that carries one. */
407
+ export declare const sourceFull: (kind: SourceKindRead, kept: KeptSnapshot) => SourceFull;
408
+ /**
409
+ * Names a snapshot's CONTENT — never when it was measured or received: sha1 of its canonical JSON, the first 16 hex
410
+ * characters. Two polls that read the same have the same digest, so the second report can say `same` instead of
411
+ * carrying it all again. Not a secret and not a check against tampering — a name, short enough to ride every report.
412
+ */
413
+ export declare function sourceDigest(content: unknown): string;
137
414
  /** The snapshot as the bus carries it: every group, the oldest-waiting first (then the largest); totals over them. */
138
415
  export declare function keepQueueSnapshot(at: string, groups: readonly QueueWireGroup[], receivedAt: number, truncated?: {
139
416
  groups: number;
140
417
  }): QueueSnapshotRaw;
141
- /** One poll, as the bus carries it. */
418
+ /** A loop snapshot as the bus carries it: the wire's fields, when it arrived, and its digest. */
419
+ export declare function keepLoopSnapshot(loop: LoopWire, receivedAt: number): LoopSnapshotRaw;
420
+ /**
421
+ * A store snapshot as the bus carries it: the wire's fields, its groups the largest (by bytes, then objects) first and
422
+ * keyed `group`, when it arrived, and its digest — of every field but its times, so a write (`lastWriteAt`) is a change,
423
+ * and the same groups answered in another order are not.
424
+ */
425
+ export declare function keepStoreSnapshot(store: StoreWire, receivedAt: number): StoreSnapshotRaw;
426
+ /**
427
+ * A traffic snapshot as the bus carries it: the wire's fields, its channels the most messages first and keyed
428
+ * `channel`, when it arrived, and its digest — of every field but its times.
429
+ */
430
+ export declare function keepTrafficSnapshot(traffic: TrafficWire, receivedAt: number): TrafficSnapshotRaw;
431
+ /** `same` for a kept snapshot (any kind's): its digest, and its own times. */
432
+ export declare const sourceSame: (kept: KeptSnapshot) => SourceSameRaw;
433
+ /** One poll, as the bus carries it — with what it carries of the last good snapshot, if one is held. */
142
434
  export declare function buildSourcePollRaw(project: string, decl: SourceDecl, poll: {
143
435
  state: SourcePollState;
144
436
  polledAt: number;
145
437
  detail?: string;
146
438
  status?: number;
147
439
  durationMs?: number;
148
- }, reportEveryMs: number, failures: number, snapshot: QueueSnapshotRaw | undefined): SourcePollRaw;
440
+ }, reportEveryMs: number, failures: number, carried?: SourceCarried): SourcePollRaw;
149
441
  export declare function buildSourceDeclarationRaw(project: string, parsed: {
150
442
  sources: readonly SourceDecl[];
151
443
  problems: readonly string[];
152
444
  }): SourceDeclarationRaw;
153
- /** The event's title: what a person reads in the event list. */
154
- export declare function sourcePollTitle(raw: SourcePollRaw): string;
445
+ /**
446
+ * The event's title: what a person reads in the event list. `held` is the last good snapshot the poller holds — a
447
+ * report that carries `same` in its place is still titled with what it says.
448
+ */
449
+ export declare function sourcePollTitle(raw: SourcePollRaw, held?: KeptSnapshot): string;
155
450
  export type SourceHttpOutcome = {
156
451
  ok: true;
157
452
  status: number;
@@ -201,8 +496,14 @@ export interface SourceProbeView {
201
496
  failures: number;
202
497
  /** When the next poll is due (backed off while it fails). */
203
498
  nextAt: number;
204
- /** The last good snapshot, kept whatever the state, with its age. */
499
+ /** A queue's last good snapshot, kept whatever the state, with its age. */
205
500
  snapshot?: QueueSnapshotRaw;
501
+ /** A loop's — the same rules. */
502
+ loop?: LoopSnapshotRaw;
503
+ /** A store's — the same rules. */
504
+ store?: StoreSnapshotRaw;
505
+ /** A traffic source's — the same rules. A push-only one has no probe: there is nothing to poll. */
506
+ traffic?: TrafficSnapshotRaw;
206
507
  ageMs?: number;
207
508
  }
208
509
  export interface SourcePoller {
@@ -1 +1 @@
1
- "use strict";var k=Object.defineProperty;var x=(e,s)=>k(e,"name",{value:s,configurable:!0});var U=Object.defineProperty,v=x((e,s)=>U(e,"name",{value:s,configurable:!0}),"v"),b=Object.defineProperty,l=v((e,s)=>b(e,"name",{value:s,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.sourceStaleAfterMs=exports.SOURCE_BACKOFF_MAX_MS=exports.SOURCE_DECLARATION_REPORT_MS=exports.SOURCE_NOT_RUNNING_REPORT_MS=exports.SOURCE_MAX_PER_SERVICE=exports.SOURCE_MAX_GROUPS=exports.SOURCE_MAX_BYTES=exports.QUEUE_DEFAULT_OLDEST_WAIT_MIN=exports.SOURCE_MIN_INTERVAL_SEC=exports.SOURCE_DEFAULT_INTERVAL_SEC=exports.QUEUE_WIRE_SCHEMA=exports.SOURCE_KINDS_READ=exports.SOURCE_KINDS=exports.sourcesConcurrency=exports.sourcesTimeoutMs=exports.sourcesEnabled=void 0,exports.sourceBackoffMs=sourceBackoffMs,exports.parseSourceDecls=parseSourceDecls,exports.parseQueueSnapshot=parseQueueSnapshot,exports.keepQueueSnapshot=keepQueueSnapshot,exports.buildSourcePollRaw=buildSourcePollRaw,exports.buildSourceDeclarationRaw=buildSourceDeclarationRaw,exports.sourcePollTitle=sourcePollTitle,exports.requestOverSocket=requestOverSocket,exports.createSourcePoller=createSourcePoller;const tslib_1=require("tslib"),http=tslib_1.__importStar(require("node:http")),discovery_1=require("./discovery"),readEnv=l(e=>process.env[e],"readEnv"),envInt=l((e,s,n)=>{const u=Number(readEnv(e));return Number.isInteger(u)&&u>=n?u:s},"envInt"),sourcesEnabled=l(()=>readEnv("LENSMCP_SOURCES")!=="0","sourcesEnabled");exports.sourcesEnabled=sourcesEnabled;const sourcesTimeoutMs=l(()=>envInt("LENSMCP_SOURCES_TIMEOUT_MS",3e3,100),"sourcesTimeoutMs");exports.sourcesTimeoutMs=sourcesTimeoutMs;const sourcesConcurrency=l(()=>envInt("LENSMCP_SOURCES_CONCURRENCY",4,1),"sourcesConcurrency");exports.sourcesConcurrency=sourcesConcurrency,exports.SOURCE_KINDS=["queue","loop","store","traffic"],exports.SOURCE_KINDS_READ=["queue"],exports.QUEUE_WIRE_SCHEMA="lensmcp.queue/v1",exports.SOURCE_DEFAULT_INTERVAL_SEC=10,exports.SOURCE_MIN_INTERVAL_SEC=2,exports.QUEUE_DEFAULT_OLDEST_WAIT_MIN=10,exports.SOURCE_MAX_BYTES=256*1024,exports.SOURCE_MAX_GROUPS=500,exports.SOURCE_MAX_PER_SERVICE=16,exports.SOURCE_NOT_RUNNING_REPORT_MS=6e4,exports.SOURCE_DECLARATION_REPORT_MS=5*6e4,exports.SOURCE_BACKOFF_MAX_MS=6e4;const sourceStaleAfterMs=l(e=>Math.max(3e4,3*Math.max(0,e)),"sourceStaleAfterMs");exports.sourceStaleAfterMs=sourceStaleAfterMs;function sourceBackoffMs(e,s){return s<=1?e:Math.min(e*2**Math.min(s-1,10),Math.max(e,exports.SOURCE_BACKOFF_MAX_MS))}x(sourceBackoffMs,"sourceBackoffMs"),v(sourceBackoffMs,"sourceBackoffMs"),l(sourceBackoffMs,"sourceBackoffMs");const ENTRY_KEYS=["kind","name","path","intervalSec","thresholds"],THRESHOLD_KEYS={queue:["oldestWaitMin","stalledMin"]},PATH_RE=/^\/(?!\/)[^\s#\u0000-\u001f\u007f]{0,511}$/,NAME_RE=/^[^\u0000-\u001f\u007f]{1,100}$/,isObj=l(e=>typeof e=="object"&&e!==null&&!Array.isArray(e),"isObj"),minutes=l(e=>typeof e=="number"&&Number.isFinite(e)&&e>0&&e<=10080,"minutes");function parseSourceDecls(e){const s=[],n=[];return e===void 0?{sources:s,problems:n}:Array.isArray(e)?(e.forEach((u,p)=>{const f=`sources[${p}]`;if(!isObj(u)){n.push(`${f} is not an object \u2014 not read`);return}const E=typeof u.name=="string"&&u.name.trim()?`${f} "${u.name}"`:f,i=u.kind;if(typeof i!="string"||!exports.SOURCE_KINDS.includes(i)){n.push(`${E}: kind ${JSON.stringify(i??null)} is not a source kind (${exports.SOURCE_KINDS.join(", ")}) \u2014 not read`);return}if(!exports.SOURCE_KINDS_READ.includes(i)){n.push(`${E}: kind "${i}" is not read by this release (it reads ${exports.SOURCE_KINDS_READ.join(", ")}) \u2014 not read`);return}const r=i,h=Object.keys(u).filter(o=>!ENTRY_KEYS.includes(o));if(h.length){n.push(`${E}: unknown key${h.length===1?"":"s"} ${h.map(o=>JSON.stringify(o)).join(", ")} for a ${r} source (known: ${ENTRY_KEYS.join(", ")}) \u2014 not read`);return}const a=u.name;if(typeof a!="string"||!a.trim()||!NAME_RE.test(a)){n.push(`${f}: name ${JSON.stringify(a??null)} must be 1\u2013100 characters, not blank \u2014 not read`);return}if(s.some(o=>o.name===a)){n.push(`${E}: the name is declared twice on this service \u2014 read once`);return}const _=u.path;if(typeof _!="string"||!PATH_RE.test(_)||_.split("?")[0].split("/").includes("..")){n.push(`${E}: path ${JSON.stringify(_??null)} must be a route on the service, starting with "/" (e.g. "/api/dev/lens/backlog") \u2014 not read`);return}const m=u.intervalSec;if(m!==void 0&&!(typeof m=="number"&&Number.isInteger(m)&&m>=exports.SOURCE_MIN_INTERVAL_SEC&&m<=3600)){n.push(`${E}: intervalSec ${JSON.stringify(m)} must be a whole number of seconds from ${exports.SOURCE_MIN_INTERVAL_SEC} to 3600 \u2014 not read`);return}const S=u.thresholds;if(S!==void 0&&!isObj(S)){n.push(`${E}: thresholds must be an object \u2014 not read`);return}const O=isObj(S)?S:{},y=Object.keys(O).filter(o=>!THRESHOLD_KEYS[r].includes(o));if(y.length){n.push(`${E}: unknown threshold${y.length===1?"":"s"} ${y.map(o=>JSON.stringify(o)).join(", ")} for a ${r} source (known: ${THRESHOLD_KEYS[r].join(", ")}) \u2014 not read`);return}const t=THRESHOLD_KEYS[r].find(o=>O[o]!==void 0&&!minutes(O[o]));if(t){n.push(`${E}: thresholds.${t} ${JSON.stringify(O[t])} must be minutes, more than 0 and at most 10080 \u2014 not read`);return}if(s.length>=exports.SOURCE_MAX_PER_SERVICE){n.push(`${E}: more than ${exports.SOURCE_MAX_PER_SERVICE} sources on one service \u2014 not read`);return}s.push({kind:r,name:a,path:_,intervalSec:m??exports.SOURCE_DEFAULT_INTERVAL_SEC,thresholds:{oldestWaitMin:O.oldestWaitMin??exports.QUEUE_DEFAULT_OLDEST_WAIT_MIN,...O.stalledMin!==void 0?{stalledMin:O.stalledMin}:{}}})}),{sources:s,problems:n}):{sources:s,problems:["`sources` is not a list of { kind, name, path } entries \u2014 no source is read"]}}x(parseSourceDecls,"parseSourceDecls"),v(parseSourceDecls,"parseSourceDecls"),l(parseSourceDecls,"parseSourceDecls");const ISO_RE=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2})$/,isIso=l(e=>typeof e=="string"&&ISO_RE.test(e)&&!Number.isNaN(Date.parse(e)),"isIso"),isCount=l(e=>typeof e=="number"&&Number.isSafeInteger(e)&&e>=0,"isCount");function parseQueueSnapshot(e){const s=l(i=>({ok:!1,reason:i}),"fail");if(!isObj(e))return s("the snapshot is not a JSON object");if(e.schema!==exports.QUEUE_WIRE_SCHEMA)return s(`schema is ${e.schema===void 0?"absent":JSON.stringify(e.schema).slice(0,60)}, not "${exports.QUEUE_WIRE_SCHEMA}"`);const n=e.at;if(!isIso(n))return s("`at` is not an ISO time");const u=e.truncated;if(u!==void 0&&!(isObj(u)&&isCount(u.groups)))return s("`truncated` must be { groups: <how many were dropped> }, or absent");const p=e.groups;if(!Array.isArray(p))return s("`groups` is not a list");if(p.length>exports.SOURCE_MAX_GROUPS)return s(`${p.length} groups \u2014 at most ${exports.SOURCE_MAX_GROUPS}; cap them and say \`truncated: { groups: <dropped> }\``);const f=new Set,E=[];for(let i=0;i<p.length;i++){const r=p[i],h=`groups[${i}]`;if(!isObj(r))return s(`${h} is not an object`);const a=r.key;if(typeof a!="string"||a.length===0||a.length>200)return s(`${h}.key must be a string of 1\u2013200 characters`);if(f.has(a))return s(`${h}.key ${JSON.stringify(a)} appears twice`);if(f.add(a),!isCount(r.pending))return s(`${h}.pending must be a whole number \u2265 0`);for(const S of["inFlight","failed","completedInWindow"])if(r[S]!==void 0&&!isCount(r[S]))return s(`${h}.${S} must be a whole number \u2265 0, or absent`);const _=r.oldestPendingAt;if(_!=null&&!isIso(_))return s(`${h}.oldestPendingAt must be an ISO time, null, or absent`);const m=r.labels;if(m!==void 0){if(!isObj(m))return s(`${h}.labels must be an object of strings`);const S=Object.entries(m);if(S.length>16)return s(`${h}.labels has more than 16 entries`);for(const[O,y]of S)if(O.length===0||O.length>64||typeof y!="string"||y.length>200)return s(`${h}.labels.${O.slice(0,64)} must be a string of at most 200 characters`)}E.push({key:a,...m!==void 0?{labels:m}:{},pending:r.pending,...r.inFlight!==void 0?{inFlight:r.inFlight}:{},...r.failed!==void 0?{failed:r.failed}:{},..._!==void 0?{oldestPendingAt:_}:{},...r.completedInWindow!==void 0?{completedInWindow:r.completedInWindow}:{}})}return{ok:!0,at:n,groups:E,...u!==void 0?{truncated:{groups:u.groups}}:{}}}x(parseQueueSnapshot,"parseQueueSnapshot"),v(parseQueueSnapshot,"parseQueueSnapshot"),l(parseQueueSnapshot,"parseQueueSnapshot");const declRaw=l(e=>({sourceKind:e.kind,name:e.name,path:e.path,intervalSec:e.intervalSec,thresholds:{...e.thresholds}}),"declRaw"),oldestOf=l(e=>e.pending>0&&e.oldestPendingAt?Date.parse(e.oldestPendingAt):Number.POSITIVE_INFINITY,"oldestOf");function keepQueueSnapshot(e,s,n,u){const p={groups:s.length,pending:0,inFlight:0,failed:0};let f;for(const i of s)p.pending+=i.pending,p.inFlight+=i.inFlight??0,p.failed+=i.failed??0,i.completedInWindow!==void 0&&(f=(f??0)+i.completedInWindow);f!==void 0&&(p.completedInWindow=f);const E=[...s].sort((i,r)=>oldestOf(i)-oldestOf(r)||r.pending-i.pending||(i.key<r.key?-1:i.key>r.key?1:0));return{at:e,receivedAt:n,totals:p,groups:E.map(({key:i,...r})=>({group:i,...r})),...u?{truncated:{groups:u.groups}}:{}}}x(keepQueueSnapshot,"keepQueueSnapshot"),v(keepQueueSnapshot,"keepQueueSnapshot"),l(keepQueueSnapshot,"keepQueueSnapshot");function buildSourcePollRaw(e,s,n,u,p,f){return{kind:"source-poll",provenance:"reported",project:e,...declRaw(s),state:n.state,...n.detail!==void 0?{detail:n.detail}:{},...n.status!==void 0?{status:n.status}:{},polledAt:n.polledAt,...n.durationMs!==void 0?{durationMs:n.durationMs}:{},reportEveryMs:u,...p>0?{failures:p}:{},...f?{snapshot:f}:{}}}x(buildSourcePollRaw,"buildSourcePollRaw"),v(buildSourcePollRaw,"buildSourcePollRaw"),l(buildSourcePollRaw,"buildSourcePollRaw");function buildSourceDeclarationRaw(e,s){return{kind:"source-declaration",project:e,sources:s.sources.map(declRaw),problems:[...s.problems]}}x(buildSourceDeclarationRaw,"buildSourceDeclarationRaw"),v(buildSourceDeclarationRaw,"buildSourceDeclarationRaw"),l(buildSourceDeclarationRaw,"buildSourceDeclarationRaw");const span=l(e=>{const s=Math.round(e/6e4);return e<6e4?`${Math.max(0,Math.round(e/1e3))}s`:s<120?`${s}m`:`${Math.floor(s/60)}h${s%60?` ${s%60}m`:""}`},"span");function sourcePollTitle(e){const s=`${e.sourceKind} ${e.project}/${e.name} (reported)`;if(e.state!=="current"||!e.snapshot)return`${s}: ${e.state}${e.detail?` \u2014 ${e.detail}`:""}`;const n=e.snapshot,u=n.groups.find(f=>f.pending>0&&f.oldestPendingAt),p=u?.oldestPendingAt?Date.parse(n.at)-Date.parse(u.oldestPendingAt):void 0;return`${s}: ${n.totals.pending} pending in ${n.totals.groups} group${n.totals.groups===1?"":"s"}${p!==void 0?`, oldest waiting ${span(Math.max(0,p))}`:""}`}x(sourcePollTitle,"sourcePollTitle"),v(sourcePollTitle,"sourcePollTitle"),l(sourcePollTitle,"sourcePollTitle");function requestOverSocket(e,s,n,u,p=exports.SOURCE_MAX_BYTES){return new Promise(f=>{let E=!1,i;const r=l(a=>{E||(E=!0,clearTimeout(h),f(a))},"finish"),h=setTimeout(()=>{r({ok:!1,code:"ETIMEDOUT",message:`no answer within ${u} ms`}),i?.destroy()},u);h.unref?.();try{i=http.request({socketPath:e,path:s,method:"GET",agent:!1,headers:n},a=>{const _=[];let m=0;a.on("data",S=>{if(!E){if(m+=S.length,m>p){r({ok:!0,status:a.statusCode??0,body:"",truncated:!0}),a.destroy();return}_.push(S)}}),a.on("end",()=>r({ok:!0,status:a.statusCode??0,body:Buffer.concat(_).toString("utf8"),truncated:!1})),a.on("error",S=>r({ok:!1,code:S.code??"ERESPONSE",message:S.message}))}),i.on("error",a=>r({ok:!1,code:a.code??"EREQUEST",message:a.message})),i.end()}catch(a){r({ok:!1,code:a.code??"EREQUEST",message:a.message})}})}x(requestOverSocket,"requestOverSocket"),v(requestOverSocket,"requestOverSocket"),l(requestOverSocket,"requestOverSocket");const snippet=l(e=>e.replace(/\s+/g," ").trim().slice(0,160),"snippet");function createSourcePoller(e){const s=e.now??Date.now,n=e.timeoutMs??(0,exports.sourcesTimeoutMs)(),u=Math.max(1,e.maxConcurrent??(0,exports.sourcesConcurrency)()),p=e.socketOf??(t=>(0,discovery_1.pickSock)(t.pool,e.scanTtlMs??1500)),f=e.request??requestOverSocket,E=e.log??(t=>console.warn(t)),i=new WeakMap,r=new Map;let h=0,a;const _=l(t=>{const o=t.decl;let d=i.get(o);if(!d){d=parseSourceDecls(t.decl.sources),i.set(o,d);for(const c of d.problems)E(`[gateway] ${t.project}: cluster.sources \u2014 ${c}`)}const M=s();return(d.reportedAt===void 0||M-d.reportedAt>=exports.SOURCE_DECLARATION_REPORT_MS)&&(d.reportedAt=M,e.emit(t,d.problems.length?"warning":"info",d.problems.length?`sources ${t.project}: ${d.problems.length} cluster.sources entr${d.problems.length===1?"y":"ies"} not read \u2014 ${d.problems[0]}`:`sources ${t.project}: reads ${d.sources.map(c=>`${c.kind} "${c.name}"`).join(", ")}`,`source-declaration:${t.wsKey}:${t.project}`,buildSourceDeclarationRaw(t.project,d))),d},"declarationOf"),m=l(t=>{if(t.state==="starting")return{notRunning:"it is starting \u2014 read once it is up"};const o=p(t);return o?{socket:o}:{notRunning:t.state==="up"?"it has no live pod socket \u2014 no pod runs it now":"it is scaled to zero \u2014 the lens does not start a service to read a source"}},"reach"),S=l((t,o)=>{const d=t.last;if(!d)return;const M=s();if(!o&&t.reportedAt!==void 0&&M-t.reportedAt<t.reportEveryMs)return;t.reportedAt=M;const c=buildSourcePollRaw(t.svc.project,t.decl,d,t.reportEveryMs,t.failures,t.good),R=d.state==="unreachable"||d.state==="refused"||d.state==="malformed";e.emit(t.svc,R?"warning":"info",sourcePollTitle(c),`source:${t.svc.wsKey}:${t.svc.project}:${t.decl.name}`,c)},"report"),O=l(async t=>{const o=s(),d=t.decl.intervalSec*1e3;t.inFlight=!0,h+=1;const M=t.last?.state;try{const c=m(t.svc);if("notRunning"in c){t.last={state:"not-running",detail:c.notRunning,polledAt:o},t.failures=0,t.nextAt=o+d,t.reportEveryMs=exports.SOURCE_NOT_RUNNING_REPORT_MS,S(t,M!=="not-running");return}const R=e.serviceKey(t.svc),g=await f(c.socket,t.decl.path,{accept:"application/json","x-lensmcp-probe":`source:${t.decl.kind}`,...R?{"x-internal-token":R}:{}},n,exports.SOURCE_MAX_BYTES),$=Math.max(0,s()-o);if(g.ok===!1){const A=g.code==="ECONNREFUSED"||g.code==="ENOENT";t.last=A&&t.svc.state!=="up"?{state:"not-running",detail:`nothing listens on its pod socket \u2014 the service is ${t.svc.state}`,polledAt:o,durationMs:$}:{state:"unreachable",detail:g.code==="ETIMEDOUT"?g.message:`${g.code}: ${snippet(g.message)}`,polledAt:o,durationMs:$}}else if(g.status<200||g.status>299){const A=snippet(g.body);t.last={state:"refused",status:g.status,detail:`HTTP ${g.status}${A?`: ${A}`:""}`,polledAt:o,durationMs:$}}else if(g.truncated)t.last={state:"malformed",detail:`the snapshot is larger than ${exports.SOURCE_MAX_BYTES/1024} KiB`,polledAt:o,durationMs:$};else{let A;try{A=parseQueueSnapshot(JSON.parse(g.body))}catch{A={ok:!1,reason:`not JSON: ${snippet(g.body)||"(an empty body)"}`}}A.ok===!0?(t.good=keepQueueSnapshot(A.at,A.groups,s(),A.truncated),t.last={state:"current",polledAt:o,durationMs:$}):t.last={state:"malformed",detail:A.reason,polledAt:o,durationMs:$}}const C=t.last.state==="unreachable"||t.last.state==="refused"||t.last.state==="malformed";t.failures=C?t.failures+1:0;const N=C?sourceBackoffMs(d,t.failures):d;t.nextAt=o+N,t.reportEveryMs=t.last.state==="not-running"?exports.SOURCE_NOT_RUNNING_REPORT_MS:N,S(t,t.last.state!=="not-running"||M!=="not-running")}catch(c){t.last={state:"unreachable",detail:`the poll failed: ${snippet(c?.message??String(c))}`,polledAt:o},t.failures+=1,t.nextAt=o+sourceBackoffMs(d,t.failures),t.reportEveryMs=t.nextAt-o,S(t,!0)}finally{t.inFlight=!1,h-=1}},"pollOne"),y=l(async()=>{if(e.stopped?.())return;const t=new Set;for(const c of e.services())if(c.decl.sources!==void 0)for(const R of _(c).sources){const g=`${c.wsKey} ${c.project} ${R.name}`;t.add(g);const $=r.get(g);$?($.svc!==c||$.decl!==R)&&($.svc=c,$.decl=R):r.set(g,{svc:c,decl:R,nextAt:0,inFlight:!1,failures:0,reportEveryMs:R.intervalSec*1e3})}for(const[c,R]of[...r.entries()])!t.has(c)&&!R.inFlight&&r.delete(c);const o=s(),d=[...r.values()].filter(c=>!c.inFlight&&o>=c.nextAt).sort((c,R)=>c.nextAt-R.nextAt),M=[];for(const c of d){if(h>=u)break;M.push(O(c))}await Promise.all(M)},"tick");return{tick:y,start(t=1e3){return a&&clearInterval(a),a=setInterval(()=>{y().catch(o=>console.error("[gateway] source poll error:",o))},t),a.unref(),a},stop(){a&&clearInterval(a),a=void 0},snapshot(){const t=s();return[...r.values()].map(o=>{const d=o.last!==void 0&&t-o.last.polledAt>(0,exports.sourceStaleAfterMs)(o.reportEveryMs);return{wsKey:o.svc.wsKey,project:o.svc.project,name:o.decl.name,kind:o.decl.kind,path:o.decl.path,state:o.last?d?"stale":o.last.state:"never-polled",...o.last?.detail!==void 0?{detail:o.last.detail}:{},...o.last?.status!==void 0?{status:o.last.status}:{},...o.last?{polledAt:o.last.polledAt}:{},failures:o.failures,nextAt:o.nextAt,...o.good?{snapshot:o.good,ageMs:Math.max(0,t-o.good.receivedAt)}:{}}})}}}x(createSourcePoller,"createSourcePoller"),v(createSourcePoller,"createSourcePoller"),l(createSourcePoller,"createSourcePoller");
1
+ "use strict";var C=Object.defineProperty;var g=(e,t)=>C(e,"name",{value:t,configurable:!0});var U=Object.defineProperty,u=g((e,t)=>U(e,"name",{value:t,configurable:!0}),"u");Object.defineProperty(exports,"__esModule",{value:!0}),exports.sourceSame=exports.sourceFull=exports.sourceStaleAfterMs=exports.SOURCE_BACKOFF_MAX_MS=exports.SOURCE_DECLARATION_REPORT_MS=exports.SOURCE_NOT_RUNNING_REPORT_MS=exports.SOURCE_MAX_PER_SERVICE=exports.SOURCE_MAX_GROUPS=exports.SOURCE_MAX_BYTES=exports.SOURCE_ERROR_MAX=exports.LOOP_MAX_INTERVAL_SEC=exports.SOURCE_FULL_REPORT_MS=exports.TRAFFIC_DEFAULT_RATE_LIMITED=exports.TRAFFIC_DEFAULT_P95_MS=exports.TRAFFIC_DEFAULT_FAILURE_PCT=exports.STORE_DEFAULT_ORPHAN_MIN=exports.STORE_DEFAULT_ERROR_RATE_PCT=exports.STORE_DEFAULT_QUOTA_PCT=exports.LOOP_DEFAULT_IDLE_MIN=exports.QUEUE_DEFAULT_STALLED_MIN=exports.QUEUE_DEFAULT_OLDEST_WAIT_MIN=exports.SOURCE_MIN_INTERVAL_SEC=exports.SOURCE_DEFAULT_INTERVAL_SEC=exports.TRAFFIC_DIRECTIONS=exports.LOOP_OUTCOMES=exports.SOURCE_SNAPSHOT_FIELD=exports.TRAFFIC_WIRE_SCHEMA=exports.STORE_WIRE_SCHEMA=exports.LOOP_WIRE_SCHEMA=exports.QUEUE_WIRE_SCHEMA=exports.SOURCE_KINDS_READ=exports.SOURCE_KINDS=exports.sourcesConcurrency=exports.sourcesTimeoutMs=exports.sourcesEnabled=void 0,exports.sourceBackoffMs=sourceBackoffMs,exports.parseSourceDecls=parseSourceDecls,exports.parseQueueSnapshot=parseQueueSnapshot,exports.parseLoopSnapshot=parseLoopSnapshot,exports.parseStoreSnapshot=parseStoreSnapshot,exports.parseTrafficSnapshot=parseTrafficSnapshot,exports.sourceDigest=sourceDigest,exports.keepQueueSnapshot=keepQueueSnapshot,exports.keepLoopSnapshot=keepLoopSnapshot,exports.keepStoreSnapshot=keepStoreSnapshot,exports.keepTrafficSnapshot=keepTrafficSnapshot,exports.buildSourcePollRaw=buildSourcePollRaw,exports.buildSourceDeclarationRaw=buildSourceDeclarationRaw,exports.sourcePollTitle=sourcePollTitle,exports.requestOverSocket=requestOverSocket,exports.createSourcePoller=createSourcePoller;const tslib_1=require("tslib"),node_crypto_1=require("node:crypto"),http=tslib_1.__importStar(require("node:http")),discovery_1=require("./discovery"),readEnv=u(e=>process.env[e],"readEnv"),envInt=u((e,t,o)=>{const n=Number(readEnv(e));return Number.isInteger(n)&&n>=o?n:t},"envInt"),sourcesEnabled=u(()=>readEnv("LENSMCP_SOURCES")!=="0","sourcesEnabled");exports.sourcesEnabled=sourcesEnabled;const sourcesTimeoutMs=u(()=>envInt("LENSMCP_SOURCES_TIMEOUT_MS",3e3,100),"sourcesTimeoutMs");exports.sourcesTimeoutMs=sourcesTimeoutMs;const sourcesConcurrency=u(()=>envInt("LENSMCP_SOURCES_CONCURRENCY",4,1),"sourcesConcurrency");exports.sourcesConcurrency=sourcesConcurrency,exports.SOURCE_KINDS=["queue","loop","store","traffic"],exports.SOURCE_KINDS_READ=["queue","loop","store","traffic"],exports.QUEUE_WIRE_SCHEMA="lensmcp.queue/v1",exports.LOOP_WIRE_SCHEMA="lensmcp.loop/v1",exports.STORE_WIRE_SCHEMA="lensmcp.store/v1",exports.TRAFFIC_WIRE_SCHEMA="lensmcp.traffic/v1",exports.SOURCE_SNAPSHOT_FIELD={queue:"snapshot",loop:"loop",store:"store",traffic:"traffic"},exports.LOOP_OUTCOMES=["ok","failed","skipped"],exports.TRAFFIC_DIRECTIONS=["inbound","outbound"],exports.SOURCE_DEFAULT_INTERVAL_SEC=10,exports.SOURCE_MIN_INTERVAL_SEC=2,exports.QUEUE_DEFAULT_OLDEST_WAIT_MIN=10,exports.QUEUE_DEFAULT_STALLED_MIN=15,exports.LOOP_DEFAULT_IDLE_MIN=10,exports.STORE_DEFAULT_QUOTA_PCT=90,exports.STORE_DEFAULT_ERROR_RATE_PCT=5,exports.STORE_DEFAULT_ORPHAN_MIN=60,exports.TRAFFIC_DEFAULT_FAILURE_PCT=5,exports.TRAFFIC_DEFAULT_P95_MS=2e3,exports.TRAFFIC_DEFAULT_RATE_LIMITED=1,exports.SOURCE_FULL_REPORT_MS=6e4,exports.LOOP_MAX_INTERVAL_SEC=86400,exports.SOURCE_ERROR_MAX=500,exports.SOURCE_MAX_BYTES=256*1024,exports.SOURCE_MAX_GROUPS=500,exports.SOURCE_MAX_PER_SERVICE=16,exports.SOURCE_NOT_RUNNING_REPORT_MS=6e4,exports.SOURCE_DECLARATION_REPORT_MS=5*6e4,exports.SOURCE_BACKOFF_MAX_MS=6e4;const sourceStaleAfterMs=u(e=>Math.max(3e4,3*Math.max(0,e)),"sourceStaleAfterMs");exports.sourceStaleAfterMs=sourceStaleAfterMs;function sourceBackoffMs(e,t){return t<=1?e:Math.min(e*2**Math.min(t-1,10),Math.max(e,exports.SOURCE_BACKOFF_MAX_MS))}g(sourceBackoffMs,"sourceBackoffMs"),u(sourceBackoffMs,"sourceBackoffMs");const isPolled=u(e=>e.path!==void 0,"isPolled"),ENTRY_KEYS={queue:["kind","name","path","intervalSec","thresholds"],loop:["kind","name","path","intervalSec","thresholds","feeds"],store:["kind","name","path","intervalSec","thresholds"],traffic:["kind","name","path","intervalSec","thresholds"]},finite=u(e=>typeof e=="number"&&Number.isFinite(e),"finite"),MINUTES={ok:u(e=>finite(e)&&e>0&&e<=10080,"ok"),must:"must be minutes, more than 0 and at most 10080"},PERCENT={ok:u(e=>finite(e)&&e>0&&e<=100,"ok"),must:"must be a percent, more than 0 and at most 100"},MILLISECONDS={ok:u(e=>finite(e)&&e>0&&e<=6e5,"ok"),must:"must be milliseconds, more than 0 and at most 600000"},COUNT={ok:u(e=>Number.isInteger(e)&&e>=1&&e<=1e6,"ok"),must:"must be a count, a whole number from 1 to 1000000"},THRESHOLDS={queue:{oldestWaitMin:{def:exports.QUEUE_DEFAULT_OLDEST_WAIT_MIN,...MINUTES},stalledMin:{def:exports.QUEUE_DEFAULT_STALLED_MIN,...MINUTES}},loop:{idleMin:{def:exports.LOOP_DEFAULT_IDLE_MIN,...MINUTES}},store:{quotaPct:{def:exports.STORE_DEFAULT_QUOTA_PCT,...PERCENT},errorRatePct:{def:exports.STORE_DEFAULT_ERROR_RATE_PCT,...PERCENT},orphanMin:{def:exports.STORE_DEFAULT_ORPHAN_MIN,...MINUTES}},traffic:{failurePct:{def:exports.TRAFFIC_DEFAULT_FAILURE_PCT,...PERCENT},p95Ms:{def:exports.TRAFFIC_DEFAULT_P95_MS,...MILLISECONDS},rateLimited:{def:exports.TRAFFIC_DEFAULT_RATE_LIMITED,...COUNT}}},PATH_RE=/^\/(?!\/)[^\s#\u0000-\u001f\u007f]{0,511}$/,NAME_RE=/^[^\u0000-\u001f\u007f]{1,100}$/,isObj=u(e=>typeof e=="object"&&e!==null&&!Array.isArray(e),"isObj");function parseSourceDecls(e){const t=[],o=[],n=[];if(e===void 0)return{sources:t,problems:o};if(!Array.isArray(e))return{sources:t,problems:["`sources` is not a list of { kind, name, path } entries \u2014 no source is read"]};e.forEach((i,l)=>{const E=`sources[${l}]`;if(!isObj(i)){o.push(`${E} is not an object \u2014 not read`);return}const d=typeof i.name=="string"&&i.name.trim()?`${E} "${i.name}"`:E,r=i.kind;if(typeof r!="string"||!exports.SOURCE_KINDS_READ.includes(r)){o.push(`${d}: kind ${JSON.stringify(r??null)} is not a source kind (${exports.SOURCE_KINDS.join(", ")}) \u2014 not read`);return}const a=r,p=Object.keys(i).filter(_=>!ENTRY_KEYS[a].includes(_));if(p.length){o.push(`${d}: unknown key${p.length===1?"":"s"} ${p.map(_=>JSON.stringify(_)).join(", ")} for a ${a} source (known: ${ENTRY_KEYS[a].join(", ")}) \u2014 not read`);return}const R=i.name;if(typeof R!="string"||!R.trim()||!NAME_RE.test(R)){o.push(`${E}: name ${JSON.stringify(R??null)} must be 1\u2013100 characters, not blank \u2014 not read`);return}if(t.some(_=>_.name===R)){o.push(`${d}: the name is declared twice on this service \u2014 read once`);return}const h=i.path;if(h===void 0&&a!=="traffic"&&a!=="loop"){o.push(`${d}: a ${a} source needs a path \u2014 the route on the service that answers "lensmcp.${a}/v1" (only a loop or a traffic source may go without one, as push-only) \u2014 not read`);return}if(h!==void 0&&(typeof h!="string"||!PATH_RE.test(h)||h.split("?")[0].split("/").includes(".."))){o.push(`${d}: path ${JSON.stringify(h)} must be a route on the service, starting with "/" (e.g. "/api/dev/lens/backlog") \u2014 not read`);return}const m=i.intervalSec;if(m!==void 0&&!(typeof m=="number"&&Number.isInteger(m)&&m>=exports.SOURCE_MIN_INTERVAL_SEC&&m<=3600)){o.push(`${d}: intervalSec ${JSON.stringify(m)} must be a whole number of seconds from ${exports.SOURCE_MIN_INTERVAL_SEC} to 3600 \u2014 not read`);return}const A=i.thresholds;if(A!==void 0&&!isObj(A)){o.push(`${d}: thresholds must be an object \u2014 not read`);return}const b=isObj(A)?A:{},I=THRESHOLDS[a],s=Object.keys(I),c=Object.keys(b).filter(_=>!s.includes(_));if(c.length){o.push(`${d}: unknown threshold${c.length===1?"":"s"} ${c.map(_=>JSON.stringify(_)).join(", ")} for a ${a} source (known: ${s.join(", ")}) \u2014 not read`);return}const f=s.find(_=>b[_]!==void 0&&!I[_].ok(b[_]));if(f){o.push(`${d}: thresholds.${f} ${JSON.stringify(b[f])} ${I[f].must} \u2014 not read`);return}if(t.length>=exports.SOURCE_MAX_PER_SERVICE){o.push(`${d}: more than ${exports.SOURCE_MAX_PER_SERVICE} sources on one service \u2014 not read`);return}const O={};for(const _ of s)O[_]=b[_]??I[_].def;const S={kind:a,name:R,...h!==void 0?{path:h}:{},intervalSec:m??exports.SOURCE_DEFAULT_INTERVAL_SEC,thresholds:O};t.push(S),a==="loop"&&i.feeds!==void 0&&n.push({decl:S,label:d,feeds:i.feeds})});for(const{decl:i,label:l,feeds:E}of n)typeof E!="string"||!E.trim()?o.push(`${l}: feeds ${JSON.stringify(E)} must be the name of a queue source on this service \u2014 read without it`):t.some(d=>d.kind==="queue"&&d.name===E)?i.feeds=E:o.push(`${l}: feeds ${JSON.stringify(E)} names no queue source on this service \u2014 read without it`);return{sources:t,problems:o}}g(parseSourceDecls,"parseSourceDecls"),u(parseSourceDecls,"parseSourceDecls");const ISO_RE=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2})$/,isIso=u(e=>typeof e=="string"&&ISO_RE.test(e)&&!Number.isNaN(Date.parse(e)),"isIso"),isCount=u(e=>typeof e=="number"&&Number.isSafeInteger(e)&&e>=0,"isCount"),notSchema=u((e,t)=>`schema is ${e===void 0?"absent":JSON.stringify(e).slice(0,60)}, not "${t}"`,"notSchema"),keyWrong=u((e,t,o)=>{if(typeof e!="string"||e.length===0||e.length>200)return`${t}.key must be a string of 1\u2013200 characters`;if(o.has(e))return`${t}.key ${JSON.stringify(e)} appears twice`;o.add(e)},"keyWrong"),labelsWrong=u((e,t)=>{if(e===void 0)return;if(!isObj(e))return`${t}.labels must be an object of strings`;const o=Object.entries(e);if(o.length>16)return`${t}.labels has more than 16 entries`;for(const[n,i]of o)if(n.length===0||n.length>64||typeof i!="string"||i.length>200)return`${t}.labels.${n.slice(0,64)} must be a string of at most 200 characters`},"labelsWrong"),overCap=u((e,t)=>`${e} ${t} \u2014 at most ${exports.SOURCE_MAX_GROUPS}; cap them and say \`truncated: { ${t}: <dropped> }\``,"overCap");function parseQueueSnapshot(e){const t=u(d=>({ok:!1,reason:d}),"fail");if(!isObj(e))return t("the snapshot is not a JSON object");if(e.schema!==exports.QUEUE_WIRE_SCHEMA)return t(notSchema(e.schema,exports.QUEUE_WIRE_SCHEMA));const o=e.at;if(!isIso(o))return t("`at` is not an ISO time");const n=e.truncated;if(n!==void 0&&!(isObj(n)&&isCount(n.groups)))return t("`truncated` must be { groups: <how many were dropped> }, or absent");const i=e.groups;if(!Array.isArray(i))return t("`groups` is not a list");if(i.length>exports.SOURCE_MAX_GROUPS)return t(overCap(i.length,"groups"));const l=new Set,E=[];for(let d=0;d<i.length;d++){const r=i[d],a=`groups[${d}]`;if(!isObj(r))return t(`${a} is not an object`);const p=keyWrong(r.key,a,l);if(p)return t(p);if(!isCount(r.pending))return t(`${a}.pending must be a whole number \u2265 0`);for(const A of["inFlight","failed","completedInWindow"])if(r[A]!==void 0&&!isCount(r[A]))return t(`${a}.${A} must be a whole number \u2265 0, or absent`);const R=r.oldestPendingAt;if(R!=null&&!isIso(R))return t(`${a}.oldestPendingAt must be an ISO time, null, or absent`);const h=r.labels,m=labelsWrong(h,a);if(m)return t(m);E.push({key:r.key,...h!==void 0?{labels:h}:{},pending:r.pending,...r.inFlight!==void 0?{inFlight:r.inFlight}:{},...r.failed!==void 0?{failed:r.failed}:{},...R!==void 0?{oldestPendingAt:R}:{},...r.completedInWindow!==void 0?{completedInWindow:r.completedInWindow}:{}})}return{ok:!0,at:o,groups:E,...n!==void 0?{truncated:{groups:n.groups}}:{}}}g(parseQueueSnapshot,"parseQueueSnapshot"),u(parseQueueSnapshot,"parseQueueSnapshot");const LOOP_COUNTS=["lastDurationMs","lastItems","runsInWindow","itemsInWindow","failuresInWindow","windowSec"],cutText=u((e,t)=>{if(e.length<=t)return e;const o=t-1,n=e.charCodeAt(o-1);return`${e.slice(0,n>=55296&&n<=56319?o-1:o)}\u2026`},"cutText");function parseLoopSnapshot(e){const t=u(a=>({ok:!1,reason:a}),"fail");if(!isObj(e))return t("the snapshot is not a JSON object");if(e.schema!==exports.LOOP_WIRE_SCHEMA)return t(notSchema(e.schema,exports.LOOP_WIRE_SCHEMA));const o=e.at;if(!isIso(o))return t("`at` is not an ISO time");const n=e.intervalSec;if(!(typeof n=="number"&&Number.isFinite(n)&&n>0&&n<=exports.LOOP_MAX_INTERVAL_SEC))return t(`\`intervalSec\` must be the loop's own period in seconds, more than 0 and at most ${exports.LOOP_MAX_INTERVAL_SEC}`);const i=e.lastRunAt;if(i===void 0)return t("`lastRunAt` is absent \u2014 say null if the loop has not run since the service started");if(i!==null&&!isIso(i))return t("`lastRunAt` must be an ISO time, or null if the loop has not run since the service started");for(const a of LOOP_COUNTS)if(e[a]!==void 0&&!isCount(e[a]))return t(`\`${a}\` must be a whole number \u2265 0, or absent`);const l=e.lastOutcome;if(l!==void 0&&!exports.LOOP_OUTCOMES.includes(l))return t(`\`lastOutcome\` must be ${exports.LOOP_OUTCOMES.join(", ")}, or absent`);const E=e.nextRunAt;if(E!=null&&!isIso(E))return t("`nextRunAt` must be an ISO time, null, or absent");const d=e.lastError;if(d!==void 0&&typeof d!="string")return t("`lastError` must be a string, or absent");const r=u(a=>e[a]!==void 0?{[a]:e[a]}:{},"count");return{ok:!0,loop:{at:o,intervalSec:n,lastRunAt:i,...r("lastDurationMs"),...l!==void 0?{lastOutcome:l}:{},...r("lastItems"),...E!==void 0?{nextRunAt:E}:{},...r("runsInWindow"),...r("itemsInWindow"),...r("failuresInWindow"),...r("windowSec"),...d!==void 0?{lastError:cutText(d,exports.SOURCE_ERROR_MAX)}:{}}}}g(parseLoopSnapshot,"parseLoopSnapshot"),u(parseLoopSnapshot,"parseLoopSnapshot");const STORE_COUNTS=["opsInWindow","writesInWindow","deletesInWindow","errorsInWindow","windowSec"];function parseStoreSnapshot(e){const t=u(p=>({ok:!1,reason:p}),"fail");if(!isObj(e))return t("the snapshot is not a JSON object");if(e.schema!==exports.STORE_WIRE_SCHEMA)return t(notSchema(e.schema,exports.STORE_WIRE_SCHEMA));const o=e.at;if(!isIso(o))return t("`at` is not an ISO time");for(const p of["objects","bytes"])if(!isCount(e[p]))return t(`\`${p}\` must be a whole number \u2265 0`);const n=e.quotaBytes;if(n!=null&&!isCount(n))return t("`quotaBytes` must be a whole number \u2265 0, null when it has no quota, or absent");for(const p of STORE_COUNTS)if(e[p]!==void 0&&!isCount(e[p]))return t(`\`${p}\` must be a whole number \u2265 0, or absent`);const i=e.lastWriteAt;if(i!=null&&!isIso(i))return t("`lastWriteAt` must be an ISO time, null, or absent");const l=e.lastError;if(l!==void 0&&typeof l!="string")return t("`lastError` must be a string, or absent");const E=e.truncated;if(E!==void 0&&!(isObj(E)&&isCount(E.groups)))return t("`truncated` must be { groups: <how many were dropped> }, or absent");const d=e.groups;let r;if(d!==void 0){if(!Array.isArray(d))return t("`groups` must be a list, or absent");if(d.length>exports.SOURCE_MAX_GROUPS)return t(overCap(d.length,"groups"));const p=new Set;r=[];for(let R=0;R<d.length;R++){const h=d[R],m=`groups[${R}]`;if(!isObj(h))return t(`${m} is not an object`);const A=keyWrong(h.key,m,p);if(A)return t(A);for(const s of["objects","bytes"])if(!isCount(h[s]))return t(`${m}.${s} must be a whole number \u2265 0`);const b=h.labels,I=labelsWrong(b,m);if(I)return t(I);r.push({key:h.key,...b!==void 0?{labels:b}:{},objects:h.objects,bytes:h.bytes})}}const a=u(p=>e[p]!==void 0?{[p]:e[p]}:{},"count");return{ok:!0,store:{at:o,objects:e.objects,bytes:e.bytes,...n!==void 0?{quotaBytes:n}:{},...a("opsInWindow"),...a("writesInWindow"),...a("deletesInWindow"),...a("errorsInWindow"),...a("windowSec"),...i!==void 0?{lastWriteAt:i}:{},...l!==void 0?{lastError:cutText(l,exports.SOURCE_ERROR_MAX)}:{},...r?{groups:r}:{},...E!==void 0?{truncated:{groups:E.groups}}:{}}}}g(parseStoreSnapshot,"parseStoreSnapshot"),u(parseStoreSnapshot,"parseStoreSnapshot");const TRAFFIC_COUNTS=["messagesInWindow","failuresInWindow","rateLimitedInWindow"];function parseTrafficSnapshot(e){const t=u(r=>({ok:!1,reason:r}),"fail");if(!isObj(e))return t("the snapshot is not a JSON object");if(e.schema!==exports.TRAFFIC_WIRE_SCHEMA)return t(notSchema(e.schema,exports.TRAFFIC_WIRE_SCHEMA));const o=e.at;if(!isIso(o))return t("`at` is not an ISO time");const n=e.windowSec;if(n!==void 0&&!isCount(n))return t("`windowSec` must be a whole number \u2265 0, or absent");const i=e.truncated;if(i!==void 0&&!(isObj(i)&&isCount(i.channels)))return t("`truncated` must be { channels: <how many were dropped> }, or absent");const l=e.channels;if(!Array.isArray(l))return t("`channels` is not a list");if(l.length>exports.SOURCE_MAX_GROUPS)return t(overCap(l.length,"channels"));const E=new Set,d=[];for(let r=0;r<l.length;r++){const a=l[r],p=`channels[${r}]`;if(!isObj(a))return t(`${p} is not an object`);const R=keyWrong(a.key,p,E);if(R)return t(R);const h=a.direction;if(h!==void 0&&!exports.TRAFFIC_DIRECTIONS.includes(h))return t(`${p}.direction must be ${exports.TRAFFIC_DIRECTIONS.join(", ")}, or absent`);const m=a.counterpart;if(m!==void 0&&!(typeof m=="string"&&m.length>0&&m.length<=200))return t(`${p}.counterpart must be a string of 1\u2013200 characters, or absent`);for(const O of TRAFFIC_COUNTS)if(a[O]!==void 0&&!isCount(a[O]))return t(`${p}.${O} must be a whole number \u2265 0, or absent`);const A=a.p95Ms;if(A!==void 0&&!(finite(A)&&A>=0))return t(`${p}.p95Ms must be milliseconds \u2265 0, or absent`);const b=a.lastAt;if(b!=null&&!isIso(b))return t(`${p}.lastAt must be an ISO time, null, or absent`);const I=a.lastError;if(I!==void 0&&typeof I!="string")return t(`${p}.lastError must be a string, or absent`);const s=a.labels,c=labelsWrong(s,p);if(c)return t(c);const f=u(O=>a[O]!==void 0?{[O]:a[O]}:{},"count");d.push({key:a.key,...s!==void 0?{labels:s}:{},...h!==void 0?{direction:h}:{},...m!==void 0?{counterpart:m}:{},...f("messagesInWindow"),...f("failuresInWindow"),...f("rateLimitedInWindow"),...A!==void 0?{p95Ms:A}:{},...b!==void 0?{lastAt:b}:{},...I!==void 0?{lastError:cutText(I,exports.SOURCE_ERROR_MAX)}:{}})}return{ok:!0,traffic:{at:o,...n!==void 0?{windowSec:n}:{},channels:d,...i!==void 0?{truncated:{channels:i.channels}}:{}}}}g(parseTrafficSnapshot,"parseTrafficSnapshot"),u(parseTrafficSnapshot,"parseTrafficSnapshot");const sourceFull=u((e,t)=>({[exports.SOURCE_SNAPSHOT_FIELD[e]]:t}),"sourceFull");exports.sourceFull=sourceFull;const declRaw=u(e=>({sourceKind:e.kind,name:e.name,...e.path!==void 0?{path:e.path}:{},intervalSec:e.intervalSec,thresholds:{...e.thresholds},...e.feeds!==void 0?{feeds:e.feeds}:{}}),"declRaw"),canonical=u(e=>{if(Array.isArray(e))return e.map(canonical);if(!isObj(e))return e;const t={};for(const o of Object.keys(e).sort())t[o]=canonical(e[o]);return t},"canonical");function sourceDigest(e){return(0,node_crypto_1.createHash)("sha1").update(JSON.stringify(canonical(e))??"null").digest("hex").slice(0,16)}g(sourceDigest,"sourceDigest"),u(sourceDigest,"sourceDigest");const oldestOf=u(e=>e.pending>0&&e.oldestPendingAt?Date.parse(e.oldestPendingAt):Number.POSITIVE_INFINITY,"oldestOf");function keepQueueSnapshot(e,t,o,n){const i={groups:t.length,pending:0,inFlight:0,failed:0};let l;for(const r of t)i.pending+=r.pending,i.inFlight+=r.inFlight??0,i.failed+=r.failed??0,r.completedInWindow!==void 0&&(l=(l??0)+r.completedInWindow);l!==void 0&&(i.completedInWindow=l);const E=[...t].sort((r,a)=>oldestOf(r)-oldestOf(a)||a.pending-r.pending||(r.key<a.key?-1:r.key>a.key?1:0)).map(({key:r,...a})=>({group:r,...a})),d=n?{groups:n.groups}:void 0;return{at:e,receivedAt:o,totals:i,groups:E,...d?{truncated:d}:{},digest:sourceDigest({groups:E,truncated:d??null})}}g(keepQueueSnapshot,"keepQueueSnapshot"),u(keepQueueSnapshot,"keepQueueSnapshot");function keepLoopSnapshot(e,t){const{at:o,...n}=e;return{at:o,receivedAt:t,digest:sourceDigest(n),...n}}g(keepLoopSnapshot,"keepLoopSnapshot"),u(keepLoopSnapshot,"keepLoopSnapshot");const byKey=u((e,t)=>e<t?-1:e>t?1:0,"byKey");function keepStoreSnapshot(e,t){const{at:o,groups:n,truncated:i,...l}=e,E=n?[...n].sort((r,a)=>a.bytes-r.bytes||a.objects-r.objects||byKey(r.key,a.key)).map(({key:r,...a})=>({group:r,...a})):void 0,d={...l,...E?{groups:E}:{},...i?{truncated:{groups:i.groups}}:{}};return{at:o,receivedAt:t,digest:sourceDigest(d),...d}}g(keepStoreSnapshot,"keepStoreSnapshot"),u(keepStoreSnapshot,"keepStoreSnapshot");const messagesOf=u(e=>e.messagesInWindow??-1,"messagesOf");function keepTrafficSnapshot(e,t){const{at:o,channels:n,truncated:i,...l}=e,E=[...n].sort((r,a)=>messagesOf(a)-messagesOf(r)||byKey(r.key,a.key)).map(({key:r,...a})=>({channel:r,...a})),d={...l,channels:E,...i?{truncated:{channels:i.channels}}:{}};return{at:o,receivedAt:t,digest:sourceDigest(d),...d}}g(keepTrafficSnapshot,"keepTrafficSnapshot"),u(keepTrafficSnapshot,"keepTrafficSnapshot");const sourceSame=u(e=>({digest:e.digest,at:e.at,receivedAt:e.receivedAt}),"sourceSame");exports.sourceSame=sourceSame;function buildSourcePollRaw(e,t,o,n,i,l){return{kind:"source-poll",provenance:"reported",project:e,...declRaw(t),state:o.state,...o.detail!==void 0?{detail:o.detail}:{},...o.status!==void 0?{status:o.status}:{},polledAt:o.polledAt,...o.durationMs!==void 0?{durationMs:o.durationMs}:{},reportEveryMs:n,...i>0?{failures:i}:{},...l??{}}}g(buildSourcePollRaw,"buildSourcePollRaw"),u(buildSourcePollRaw,"buildSourcePollRaw");function buildSourceDeclarationRaw(e,t){return{kind:"source-declaration",project:e,sources:t.sources.map(declRaw),problems:[...t.problems]}}g(buildSourceDeclarationRaw,"buildSourceDeclarationRaw"),u(buildSourceDeclarationRaw,"buildSourceDeclarationRaw");const span=u(e=>{const t=Math.round(e/6e4);return e<6e4?`${Math.max(0,Math.round(e/1e3))}s`:t<120?`${t}m`:`${Math.floor(t/60)}h${t%60?` ${t%60}m`:""}`},"span"),loopWords=u(e=>`${e.lastRunAt===null?"has not run since the service started":`last run ${span(Math.max(0,Date.parse(e.at)-Date.parse(e.lastRunAt)))} before it was measured`}${e.lastOutcome?`, ${e.lastOutcome}`:""}${e.lastItems!==void 0?`, ${e.lastItems} item${e.lastItems===1?"":"s"}`:""}`,"loopWords"),plural=u((e,t)=>`${e} ${t}${e===1?"":"s"}`,"plural"),byteSize=u(e=>{const t=["B","kB","MB","GB","TB","PB"];let o=e,n=0;for(;o>=999.5&&n<t.length-1;)o/=1e3,n+=1;return n===0?`${e} B`:`${o<9.95?o.toFixed(1):Math.round(o)} ${t[n]}`},"byteSize"),storeWords=u(e=>{const t=typeof e.quotaBytes=="number"&&e.quotaBytes>0?` of ${byteSize(e.quotaBytes)} (${Math.round(e.bytes/e.quotaBytes*1e3)/10}%)`:"",o=e.errorsInWindow?`, ${plural(e.errorsInWindow,"error")}${e.windowSec?` in ${span(e.windowSec*1e3)}`:""}`:"";return`${plural(e.objects,"object")}, ${byteSize(e.bytes)}${t}${o}`},"storeWords"),trafficWords=u(e=>{let t,o=0,n=0;for(const l of e.channels)l.messagesInWindow!==void 0&&(t=(t??0)+l.messagesInWindow),o+=l.failuresInWindow??0,n+=l.rateLimitedInWindow??0;const i=t!==void 0?`, ${plural(t,"message")}${e.windowSec?` in ${span(e.windowSec*1e3)}`:""}`:"";return`${plural(e.channels.length,"channel")}${i}${o?`, ${o} failed`:""}${n?`, ${n} rate-limited`:""}`},"trafficWords");function sourcePollTitle(e,t){const o=`${e.sourceKind} ${e.project}/${e.name} (reported)`,n=t??e[exports.SOURCE_SNAPSHOT_FIELD[e.sourceKind]];if(e.state!=="current"||!n)return`${o}: ${e.state}${e.detail?` \u2014 ${e.detail}`:""}`;if(e.sourceKind==="loop")return`${o}: ${loopWords(n)}`;if(e.sourceKind==="store")return`${o}: ${storeWords(n)}`;if(e.sourceKind==="traffic")return`${o}: ${trafficWords(n)}`;const i=n,l=i.groups.find(d=>d.pending>0&&d.oldestPendingAt),E=l?.oldestPendingAt?Date.parse(i.at)-Date.parse(l.oldestPendingAt):void 0;return`${o}: ${i.totals.pending} pending in ${i.totals.groups} group${i.totals.groups===1?"":"s"}${E!==void 0?`, oldest waiting ${span(Math.max(0,E))}`:""}`}g(sourcePollTitle,"sourcePollTitle"),u(sourcePollTitle,"sourcePollTitle");function requestOverSocket(e,t,o,n,i=exports.SOURCE_MAX_BYTES){return new Promise(l=>{let E=!1,d;const r=u(p=>{E||(E=!0,clearTimeout(a),l(p))},"finish"),a=setTimeout(()=>{r({ok:!1,code:"ETIMEDOUT",message:`no answer within ${n} ms`}),d?.destroy()},n);a.unref?.();try{d=http.request({socketPath:e,path:t,method:"GET",agent:!1,headers:o},p=>{const R=[];let h=0;p.on("data",m=>{if(!E){if(h+=m.length,h>i){r({ok:!0,status:p.statusCode??0,body:"",truncated:!0}),p.destroy();return}R.push(m)}}),p.on("end",()=>r({ok:!0,status:p.statusCode??0,body:Buffer.concat(R).toString("utf8"),truncated:!1})),p.on("error",m=>r({ok:!1,code:m.code??"ERESPONSE",message:m.message}))}),d.on("error",p=>r({ok:!1,code:p.code??"EREQUEST",message:p.message})),d.end()}catch(p){r({ok:!1,code:p.code??"EREQUEST",message:p.message})}})}g(requestOverSocket,"requestOverSocket"),u(requestOverSocket,"requestOverSocket");const snippet=u(e=>e.replace(/\s+/g," ").trim().slice(0,160),"snippet");function readSnapshot(e,t,o){let n;try{n=JSON.parse(t)}catch{return{ok:!1,reason:`not JSON: ${snippet(t)||"(an empty body)"}`}}if(e==="loop"){const l=parseLoopSnapshot(n);return l.ok===!0?{ok:!0,kept:keepLoopSnapshot(l.loop,o)}:l}if(e==="store"){const l=parseStoreSnapshot(n);return l.ok===!0?{ok:!0,kept:keepStoreSnapshot(l.store,o)}:l}if(e==="traffic"){const l=parseTrafficSnapshot(n);return l.ok===!0?{ok:!0,kept:keepTrafficSnapshot(l.traffic,o)}:l}const i=parseQueueSnapshot(n);return i.ok===!0?{ok:!0,kept:keepQueueSnapshot(i.at,i.groups,o,i.truncated)}:i}g(readSnapshot,"readSnapshot"),u(readSnapshot,"readSnapshot");function createSourcePoller(e){const t=e.now??Date.now,o=e.timeoutMs??(0,exports.sourcesTimeoutMs)(),n=Math.max(1,e.maxConcurrent??(0,exports.sourcesConcurrency)()),i=e.socketOf??(s=>(0,discovery_1.pickSock)(s.pool,e.scanTtlMs??1500)),l=e.request??requestOverSocket,E=e.log??(s=>console.warn(s)),d=new WeakMap,r=new Map;let a=0,p;const R=u(s=>{const c=s.decl;let f=d.get(c);if(!f){f=parseSourceDecls(s.decl.sources),d.set(c,f);for(const S of f.problems)E(`[gateway] ${s.project}: cluster.sources \u2014 ${S}`)}const O=t();return(f.reportedAt===void 0||O-f.reportedAt>=exports.SOURCE_DECLARATION_REPORT_MS)&&(f.reportedAt=O,e.emit(s,f.problems.length?"warning":"info",f.problems.length?`sources ${s.project}: ${f.problems.length} cluster.sources entr${f.problems.length===1?"y":"ies"} not read \u2014 ${f.problems[0]}`:`sources ${s.project}: reads ${f.sources.map(S=>`${S.kind} "${S.name}"${isPolled(S)?"":" (push-only)"}`).join(", ")}`,`source-declaration:${s.wsKey}:${s.project}`,buildSourceDeclarationRaw(s.project,f))),f},"declarationOf"),h=u(s=>{if(s.state==="starting")return{notRunning:"it is starting \u2014 read once it is up"};const c=i(s);return c?{socket:c}:{notRunning:s.state==="up"?"it has no live pod socket \u2014 no pod runs it now":"it is scaled to zero \u2014 the lens does not start a service to read a source"}},"reach"),m=u((s,c)=>{const f=s.good;if(f)return s.full&&s.full.digest===f.digest&&c-s.full.at<exports.SOURCE_FULL_REPORT_MS?{same:(0,exports.sourceSame)(f)}:(s.full={digest:f.digest,at:c},(0,exports.sourceFull)(s.decl.kind,f))},"carried"),A=u((s,c)=>{const f=s.last;if(!f)return;const O=t();if(!c&&s.reportedAt!==void 0&&O-s.reportedAt<s.reportEveryMs)return;s.reportedAt=O;const S=buildSourcePollRaw(s.svc.project,s.decl,f,s.reportEveryMs,s.failures,m(s,O)),_=f.state==="unreachable"||f.state==="refused"||f.state==="malformed";e.emit(s.svc,_?"warning":"info",sourcePollTitle(S,s.good),`source:${s.svc.wsKey}:${s.svc.project}:${s.decl.name}`,S)},"report"),b=u(async s=>{const c=t(),f=s.decl.intervalSec*1e3;s.inFlight=!0,a+=1;const O=s.last?.state;try{const S=h(s.svc);if("notRunning"in S){s.last={state:"not-running",detail:S.notRunning,polledAt:c},s.failures=0,s.nextAt=c+f,s.reportEveryMs=exports.SOURCE_NOT_RUNNING_REPORT_MS,A(s,O!=="not-running");return}const _=e.serviceKey(s.svc),T=await l(S.socket,s.decl.path,{accept:"application/json","x-lensmcp-probe":`source:${s.decl.kind}`,..._?{"x-internal-token":_}:{}},o,exports.SOURCE_MAX_BYTES),x=Math.max(0,t()-c);if(T.ok===!1){const $=T.code==="ECONNREFUSED"||T.code==="ENOENT";s.last=$&&s.svc.state!=="up"?{state:"not-running",detail:`nothing listens on its pod socket \u2014 the service is ${s.svc.state}`,polledAt:c,durationMs:x}:{state:"unreachable",detail:T.code==="ETIMEDOUT"?T.message:`${T.code}: ${snippet(T.message)}`,polledAt:c,durationMs:x}}else if(T.status<200||T.status>299){const $=snippet(T.body);s.last={state:"refused",status:T.status,detail:`HTTP ${T.status}${$?`: ${$}`:""}`,polledAt:c,durationMs:x}}else if(T.truncated)s.last={state:"malformed",detail:`the snapshot is larger than ${exports.SOURCE_MAX_BYTES/1024} KiB`,polledAt:c,durationMs:x};else{const $=readSnapshot(s.decl.kind,T.body,t());$.ok===!0?(s.good=$.kept,s.last={state:"current",polledAt:c,durationMs:x}):s.last={state:"malformed",detail:$.reason,polledAt:c,durationMs:x}}const v=s.last.state==="unreachable"||s.last.state==="refused"||s.last.state==="malformed";s.failures=v?s.failures+1:0;const M=v?sourceBackoffMs(f,s.failures):f;s.nextAt=c+M,s.reportEveryMs=s.last.state==="not-running"?exports.SOURCE_NOT_RUNNING_REPORT_MS:M,A(s,s.last.state!=="not-running"||O!=="not-running")}catch(S){s.last={state:"unreachable",detail:`the poll failed: ${snippet(S?.message??String(S))}`,polledAt:c},s.failures+=1,s.nextAt=c+sourceBackoffMs(f,s.failures),s.reportEveryMs=s.nextAt-c,A(s,!0)}finally{s.inFlight=!1,a-=1}},"pollOne"),I=u(async()=>{if(e.stopped?.())return;const s=new Set;for(const S of e.services())if(S.decl.sources!==void 0)for(const _ of R(S).sources){if(!isPolled(_))continue;const T=`${S.wsKey} ${S.project} ${_.kind} ${_.name}`;s.add(T);const x=r.get(T);x?(x.svc!==S||x.decl!==_)&&(x.svc=S,x.decl=_):r.set(T,{svc:S,decl:_,nextAt:0,inFlight:!1,failures:0,reportEveryMs:_.intervalSec*1e3})}for(const[S,_]of[...r.entries()])!s.has(S)&&!_.inFlight&&r.delete(S);const c=t(),f=[...r.values()].filter(S=>!S.inFlight&&c>=S.nextAt).sort((S,_)=>S.nextAt-_.nextAt),O=[];for(const S of f){if(a>=n)break;O.push(b(S))}await Promise.all(O)},"tick");return{tick:I,start(s=1e3){return p&&clearInterval(p),p=setInterval(()=>{I().catch(c=>console.error("[gateway] source poll error:",c))},s),p.unref(),p},stop(){p&&clearInterval(p),p=void 0},snapshot(){const s=t();return[...r.values()].map(c=>{const f=c.last!==void 0&&s-c.last.polledAt>(0,exports.sourceStaleAfterMs)(c.reportEveryMs);return{wsKey:c.svc.wsKey,project:c.svc.project,name:c.decl.name,kind:c.decl.kind,path:c.decl.path,state:c.last?f?"stale":c.last.state:"never-polled",...c.last?.detail!==void 0?{detail:c.last.detail}:{},...c.last?.status!==void 0?{status:c.last.status}:{},...c.last?{polledAt:c.last.polledAt}:{},failures:c.failures,nextAt:c.nextAt,...c.good?{...(0,exports.sourceFull)(c.decl.kind,c.good),ageMs:Math.max(0,s-c.good.receivedAt)}:{}}})}}}g(createSourcePoller,"createSourcePoller"),u(createSourcePoller,"createSourcePoller");
@@ -91,26 +91,47 @@ export interface ClusterProjectDecl {
91
91
  */
92
92
  firstByteMs?: number;
93
93
  /**
94
- * CUSTOM SOURCES (issues/open/foodguard-81): what this service REPORTS that the lens cannot observe — work it keeps
95
- * waiting in its OWN storage (an outbox table), which no event can show. Each entry is a typed kind (`queue` · `loop` ·
96
- * `store` · `traffic`; this release reads `queue`) with a dev route ON THE SERVICE that answers the kind's snapshot
97
- * (`lensmcp.queue/v1`: `{ schema, at, truncated?, groups: [{ key, labels?, pending, inFlight?, failed?,
98
- * oldestPendingAt?, completedInWindow? }] }`). The dev gateway reads it from a RUNNING pod every `intervalSec`
94
+ * CUSTOM SOURCES (issues/closed/foodguard-81): what this service REPORTS that the lens cannot observe — work it keeps
95
+ * waiting in its OWN storage (an outbox table), which no event can show, the background loop that drains it, a
96
+ * storage backend the taps do not model, and messages over a channel they do not classify. Each entry is a typed kind
97
+ * (`queue` · `loop` · `store` · `traffic`; this release reads all four) with a dev route ON THE SERVICE that answers
98
+ * the kind's snapshot — `lensmcp.queue/v1`: `{ schema, at, truncated?, groups: [{ key, labels?, pending, inFlight?,
99
+ * failed?, oldestPendingAt?, completedInWindow? }] }`; `lensmcp.loop/v1`: `{ schema, at, intervalSec, lastRunAt,
100
+ * lastDurationMs?, lastOutcome?, lastItems?, nextRunAt?, runsInWindow?, itemsInWindow?, failuresInWindow?, windowSec?,
101
+ * lastError? }`; `lensmcp.store/v1`: `{ schema, at, objects, bytes, quotaBytes?, opsInWindow?, writesInWindow?,
102
+ * deletesInWindow?, errorsInWindow?, windowSec?, lastWriteAt?, lastError?, truncated?, groups?: [{ key, labels?,
103
+ * objects, bytes }] }`; `lensmcp.traffic/v1`: `{ schema, at, windowSec?, truncated?, channels: [{ key, labels?,
104
+ * direction?, counterpart?, messagesInWindow?, failuresInWindow?, rateLimitedInWindow?, p95Ms?, lastAt?, lastError?
105
+ * }] }`. A `traffic` entry WITHOUT a `path` is push-only: declared, never polled — the lens counts the `inbound` /
106
+ * `outbound` events the app pushes. The dev gateway reads every other entry from a RUNNING pod every `intervalSec`
99
107
  * (default 10, at least 2) over the pod's socket — never waking a service scaled to zero — and the lens shows it,
100
- * labelled REPORTED (`observe.sources`, `source.get`, `process://sources`, Data › Queues), warning once a group's
101
- * oldest pending item has waited longer than `thresholds.oldestWaitMin` (default 10). The lens never connects to the
102
- * app's database; the prod gateway ignores the key. Pod (`service`) declarations only. Parsed by `source-poller.ts`
103
- * `parseSourceDecls`: an unknown kind or a malformed entry is left out and said, never silently.
108
+ * labelled REPORTED (`observe.sources`, `source.get`, `process://sources`, the dashboard's Data views), judged against
109
+ * each kind's `thresholds` (every one filled with its default): a queue's `oldestWaitMin` (10) and `stalledMin` (15)
110
+ * and a loop's `idleMin` (10), in minutes; a store's `quotaPct` (90) and `errorRatePct` (5), percents, and
111
+ * `orphanMin` (60), minutes; a traffic source's `failurePct` (5), a percent, `p95Ms` (2000), milliseconds, and
112
+ * `rateLimited` (1), a count of «too many requests» answers. A loop's `feeds` names the queue source of this service
113
+ * it drains. The lens never connects to the app's database or store; the prod gateway ignores the key. Pod
114
+ * (`service`) declarations only. Parsed by `source-poller.ts` `parseSourceDecls`: an unknown kind or a malformed
115
+ * entry is left out and said, never silently; a `feeds` that names no queue source is dropped, and said.
104
116
  */
105
117
  sources?: ReadonlyArray<{
106
118
  kind: string;
107
119
  name: string;
108
- path: string;
120
+ /** Required of every kind but `traffic`, whose entry without one is push-only. */
121
+ path?: string;
109
122
  intervalSec?: number;
110
123
  thresholds?: {
111
124
  oldestWaitMin?: number;
112
125
  stalledMin?: number;
126
+ idleMin?: number;
127
+ quotaPct?: number;
128
+ errorRatePct?: number;
129
+ orphanMin?: number;
130
+ failurePct?: number;
131
+ p95Ms?: number;
132
+ rateLimited?: number;
113
133
  };
134
+ feeds?: string;
114
135
  }>;
115
136
  /**
116
137
  * FRONTEND apps only: run this app UNDER the lens. When true, the gateway