@phosphorco/bb-thread-metrics 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CONTRACT.md CHANGED
@@ -398,3 +398,133 @@ selectors. They may conditionally poll at30s while interested/visible/connected,
398
398
  coalescing existing hints without a per-thread request. Covered listing rows need
399
399
  no readV1; explicit inspection outside the window may use the existing bounded
400
400
  read/detail lane. The contract package itself never polls or retries.
401
+
402
+ ### Optional recent token/count observations
403
+
404
+ A recent scope may carry `usage` with aggregate and per-provider token windows,
405
+ explicit `estimate`/`floor`/`unavailable` values, matching provider omissions,
406
+ and a descendant count with an exactness flag. Missing `usage` is unsupported,
407
+ not zero. Positive tokens do not depend on a price match or lifetime dollar
408
+ observations: wholly unpriced and bucket-only tokens can remain floors while
409
+ USD is unavailable. Unknown/legacy token inputs stay unavailable. A supplied
410
+ zero is an explicit observation attestation.
411
+
412
+ Provider identities must match the displayed dollar providers. The codec checks
413
+ componentwise conservation of known token observations and reconciles descendant
414
+ count with the loaded membership; it does not certify unseen global membership.
415
+ Omitted providers permit a larger aggregate supported subtotal, never a smaller
416
+ one. Presentation fitting removes dollar/token provider entries together and
417
+ retains aggregate supported observations with an explicit cap qualification.
418
+
419
+ Per-provider dollar contributions are the primary money presentation. Aggregate
420
+ cross-provider dollar windows remain internal reconciliation/ranking inputs and
421
+ must not be presented as a grand total. Weighted attention scores are unitless,
422
+ not currency; token counts and descendants are secondary to provider dollars.
423
+
424
+ An aggregate token `estimate` requires estimate-kind tokens for every displayed
425
+ provider and no omitted providers in that window. A floor or unavailable provider
426
+ cannot be upgraded by numerical equality alone. This requirement is independent
427
+ of USD pricing coverage: completely observed zero or positive tokens may remain
428
+ estimates while dollars are unpriced. Explicit member-cap, depth-cap or edge-cap
429
+ coverage cannot carry `descendants.exact=true`; other supplier membership
430
+ attestations still require evidence the codec cannot reconstruct.
431
+
432
+
433
+ ### Unique seven-day root inputs (unpublished0.2 producer extension)
434
+
435
+ Optional `heatInputs` is a unique all-window root-family table captured in the SAME durable rank publication as `heat`. It is independent of `rows`, whose actual fold activity covers only72h and whose money may be newer. Never join current display money into captured ranks by ID, infer missing activity timestamps, or compare cross-epoch money as equal. Each input carries its financial `asOf`, config revision, explicit attested root topology, root-domain dollar/weighted ranks and all three decisions. `family.dollarRanks/scoreRanks` retain their original scoped meaning; root ranks live only under `heat`. Unknown metadata remains absent/null; title/native personal-or-project navigation is only bounded already-collected data, not identity/membership proof.
436
+
437
+ `cohortComplete` attests the supplied ROOT table, not complete source observation or exact spend. It requires no pending or omitted input and exact omission accounting. Per-window context population coverage and each family value/attribution remain independent. Full certified windows reconcile exact supported-positive counts and first-tie ordering against the complete table; partial transport preserves published ranks but cannot give exact reweighted draft percentiles. A five-day root can appear here without any72h display row. Hidden/archive activity is included; child/deleted/unknown topology never enters this table.
438
+
439
+ Display rows PLUS captured input rows share one aggregate1000 physical-entry budget, whole4MiB canonical JSON,8KiB per entry; duplicate IDs across these two DIFFERENT captures still consume two physical entries. Raw additive fields count before output stripping. Producer packing may retain only a subset; omissions/capped membership are explicit and prevent a complete input attestation. It never reconstructs from display rows or adds a per-ID population endpoint.
440
+
441
+ Optional scope `attribution` carries per-window `unknownProvider`, `attributionComplete` and `unpriced` evidence. Known provider USD is primary presentation. Cross-provider `windows` and unitless weighted `scores` are INTERNAL reconciliation/ranking inputs, not a dollar grand total. Unknown remainder is a lower bound/unavailable reconciliation amount, NEVER an invented provider at weight1. No omitted provider IDs and exact zero remainder are required for complete attribution; complete observation/price are separately required for estimates. Explicit unpriced usage retains missing-price provenance. Legacy scopes lacking this extension remain readable diagnostics; they cannot populate the new certified heat input table. No numeric evidence is invented on migration.
442
+
443
+ The supplier still attests actual membership and ordering; codecs prove internal coherence, not unseen global source truth. Bounds are post-transport parsing containment. The optional producer remains source-only until reviewed publication, exact registry pins, fresh composition gates and host byte/time measurement; the contract extension alone is no activation/adoption/freshness proof.
444
+
445
+
446
+ ### Full publication identity and ordered consumer admission
447
+
448
+ An additive full-only `publication:{instance,sequence}` header exposes the
449
+ supplier's committed publication identity. `instance` is an opaque1–128-character
450
+ string; `sequence` is a positive safe integer. Old stored/full responses may omit
451
+ this header and remain readable for legacy diagnostics. New changed publications
452
+ from the ledger include it. A Heat capture adopter requires the header; absence
453
+ is unavailable, never inferred by parsing `revision`, rank time or a request
454
+ ordinal. Unchanged replies carry only the previously requested opaque revision,
455
+ never a new identity or recognized full-only fields.
456
+
457
+ Within one durable source history, changed committed publications strictly
458
+ increase sequence across writer-instance changes. No-op attempts and reopening
459
+ retain the exact held revision/pair/content. The publisher injects revision and
460
+ pair inside its guarded commit and excludes BOTH from the semantic digest. The
461
+ pair is validated against the stored instance/counter on hydration; reads return
462
+ the stored payload identity, not the newly registered writer. Actual encoded
463
+ header bytes count toward the same4MiB cap; no new allowance is created.
464
+
465
+ A retained owner resource, outside component mounts, must order validated full
466
+ responses by sequence before feeding Heat: lower is stale; equal accepts only
467
+ the same immutable pair/revision/content; greater with a new instance is an
468
+ ordered writer successor. Null, ordinary reconnect, facade changes and timeouts
469
+ do not reset this watermark or release actual transport admission. Source
470
+ replacement fences captured old callbacks. These are adopter obligations, not
471
+ facts proved by a stateless wire decoder or the presence of this header.
472
+
473
+ The persisted counter does NOT attest order across external database rollback,
474
+ reset or replacement. Such images may repeat or lower sequences while changed
475
+ writer UUIDs keep revisions distinct. A UUID alone must never authorize a reset;
476
+ a retained consumer holds/reports lower or equal-conflicting data until an
477
+ explicit authoritative selected-source replacement. This unit adds no restore
478
+ protocol or automatic reset. Counter exhaustion is explicit refusal, never wrap.
479
+ Settings Save CAS remains keyed by config version, not publication sequence;
480
+ settings published metadata must describe the exact held publication/config.
481
+
482
+
483
+ ### Retained snapshot client session (unpublished0.2)
484
+
485
+ The optional `/bb` `createRecentSnapshotSession(ownerBoundClient)` helper is an
486
+ imperative admission and ordering boundary, owned by the existing selected-source
487
+ resource outside React. The surface owns discovery, source-selection authority,
488
+ refresh cadence, retries, observable state and rendering. The helper creates no
489
+ subscription, refresh loop, retry queue or shared global store; construction is
490
+ quiet. It holds at most one decoded immutable full snapshot and one actual read.
491
+ A native adopter must retain the SAME session through zero interest, remount,
492
+ ordinary reconnect and facade/writer changes while a read remains outstanding.
493
+ Independent factories do not share admission. This helper alone does not prove
494
+ an adopter's resource lifetime.
495
+
496
+ `read()` sends the held opaque revision conditionally; `read({full:true})` requests
497
+ a full response. Original full envelopes are publicly validated before ordering.
498
+ A full response requires the publication pair: lower sequences fail, equal
499
+ sequences require the same immutable pair/revision/decoded content, and greater
500
+ sequences must use a new opaque revision. An ordered new writer advances the
501
+ private owner generation once; an ordinary same-source facade update does not.
502
+ Valid unchanged content returns the exact held observation object, allowing the
503
+ caller to confirm recovery without allocating or notifying for changed data.
504
+ Null, invalid, missing-identity, stale or failed responses preserve last content
505
+ and highwater. Validation/content comparison still costs bounded payload work;
506
+ this is not a claim of constant-time end-to-end decoding.
507
+
508
+ The default local observation deadline is the existing2000ms contract call
509
+ limit (configurable1–2000ms). Timeout, caller abort, inactive state or explicit
510
+ replacement rejects local observation and aborts its signal, but the actual slot
511
+ remains held until the underlying source Promise settles, even if abort is
512
+ ignored. Busy/inactive/already-aborted reads return typed local
513
+ `RecentSnapshotNotStarted` without invoking the source. A source-thrown instance
514
+ of that type is wrapped and cannot claim this session's pre-invocation proof.
515
+ There is no queued replay. A permanently held transport keeps this lane
516
+ unavailable; caller deadline never proves remote cancellation.
517
+
518
+ `updateSource` is only a same-plugin facade update and retains ordering.
519
+ `replaceSource` requires authoritative selected-source replacement, fences old
520
+ callbacks and clears held lineage while retaining any actual old transport slot.
521
+ Do not invoke it for a new UUID, timeout, remount, lower sequence or an ordinary
522
+ reconnect. External restored/reset images require authority supplied by the
523
+ native resource; this API does not establish that authority. `setActive` changes
524
+ interest without clearing the lineage. Only admitted observations may feed Heat
525
+ owner-generation transitions. Publication identity is independent of matching
526
+ Save config bytes/version, which the retained Heat adapter must still check.
527
+
528
+ These are client source conformance guarantees, not released pins, native
529
+ resource adoption, host latency/freshness or live bytes/time acceptance. No
530
+ existing consumer is switched merely by exporting this helper.
package/dist/bb.js CHANGED
@@ -1,6 +1,247 @@
1
1
  // src/bb.ts
2
2
  import { z } from "zod";
3
- import { RECENT_LIMITS, recentSnapshotInputSchema, validateRecentSnapshot, LIMITS, METHODS, describeInputSchema, describeEnvelopeSchema, capabilitiesSchema, readInputSchema, detailInputSchema, parseBounded, negotiateVersion, validateReadResult, validateDetailResult, classifyOwnerError } from "./index.js";
3
+ import { HEAT_SETTINGS_METHODS, HEAT_SETTINGS_LIMITS, bounded, validateHeatSaveInput, heatSettingsReadInputSchema, heatSettingsSaveInputSchema, heatSettingsReadResultSchema, heatSettingsSaveResultSchema } from "./index.js";
4
+ import { RECENT_LIMITS, recentSnapshotInputSchema, validateRecentSnapshot as validateRecentSnapshot2, LIMITS as LIMITS2, METHODS, describeInputSchema, describeEnvelopeSchema, capabilitiesSchema, readInputSchema, detailInputSchema, parseBounded, negotiateVersion, validateReadResult, validateDetailResult, classifyOwnerError } from "./index.js";
5
+
6
+ // src/recent-session.ts
7
+ import { LIMITS, validateRecentSnapshot } from "./index.js";
8
+
9
+ class RecentSnapshotSessionError extends Error {
10
+ reason;
11
+ constructor(reason) {
12
+ super(`Recent snapshot session: ${reason}`);
13
+ this.reason = reason;
14
+ }
15
+ }
16
+
17
+ class RecentSnapshotNotStarted extends RecentSnapshotSessionError {
18
+ notStarted = true;
19
+ constructor(reason) {
20
+ super(reason);
21
+ }
22
+ }
23
+ var clock = { later(run, ms) {
24
+ const timer = setTimeout(run, ms);
25
+ return () => clearTimeout(timer);
26
+ } };
27
+ function next(n) {
28
+ if (n === Number.MAX_SAFE_INTEGER)
29
+ throw new Error("Recent session generation exhausted");
30
+ return n + 1;
31
+ }
32
+ function sourceCopy(source) {
33
+ if (!source.pluginId || source.pluginId.length > 200 || typeof source.recentSnapshot !== "function")
34
+ throw new Error("Invalid recent snapshot source");
35
+ return { pluginId: source.pluginId, recentSnapshot: source.recentSnapshot.bind(source) };
36
+ }
37
+ function freeze(value) {
38
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
39
+ for (const child of Object.values(value))
40
+ freeze(child);
41
+ Object.freeze(value);
42
+ }
43
+ return value;
44
+ }
45
+ function same(a, b) {
46
+ if (a === b)
47
+ return true;
48
+ if (!a || !b || typeof a !== "object" || typeof b !== "object")
49
+ return false;
50
+ if (Array.isArray(a) !== Array.isArray(b))
51
+ return false;
52
+ const ak = Object.keys(a), bk = Object.keys(b);
53
+ if (ak.length !== bk.length)
54
+ return false;
55
+ return ak.every((key) => Object.hasOwn(b, key) && same(a[key], b[key]));
56
+ }
57
+ function createRecentSnapshotSession(initial, options = {}) {
58
+ const timeoutMs = options.timeoutMs ?? LIMITS.callMs;
59
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > LIMITS.callMs)
60
+ throw new Error("Invalid recent session deadline");
61
+ const time = options.clock ?? clock;
62
+ let source = sourceCopy(initial), active = true, epoch = Symbol("view"), sourceGeneration = 1, ownerGeneration = 1;
63
+ let held;
64
+ let actual;
65
+ const admit = (input, raw, current) => {
66
+ if (!current())
67
+ throw new RecentSnapshotSessionError("stale-call");
68
+ const value = validateRecentSnapshot(input, raw);
69
+ if (!current())
70
+ throw new RecentSnapshotSessionError("stale-call");
71
+ if (value.unchanged) {
72
+ if (!held || held.snapshot.revision !== value.revision)
73
+ throw new RecentSnapshotSessionError("publication-conflict");
74
+ return held;
75
+ }
76
+ const publication = value.publication;
77
+ if (!publication)
78
+ throw new RecentSnapshotSessionError("missing-publication");
79
+ if (held) {
80
+ if (publication.sequence < held.publication.sequence)
81
+ throw new RecentSnapshotSessionError("stale-publication");
82
+ if (publication.sequence === held.publication.sequence) {
83
+ if (publication.instance !== held.publication.instance || value.revision !== held.snapshot.revision || !same(value, held.snapshot))
84
+ throw new RecentSnapshotSessionError("publication-conflict");
85
+ return held;
86
+ }
87
+ if (value.revision === held.snapshot.revision)
88
+ throw new RecentSnapshotSessionError("publication-conflict");
89
+ if (publication.instance !== held.publication.instance)
90
+ ownerGeneration = next(ownerGeneration);
91
+ }
92
+ held = freeze({ snapshot: value, publication, ownerGeneration, sourceGeneration });
93
+ return held;
94
+ };
95
+ return {
96
+ get current() {
97
+ return held;
98
+ },
99
+ get outstandingReads() {
100
+ return actual ? 1 : 0;
101
+ },
102
+ get ownerGeneration() {
103
+ return ownerGeneration;
104
+ },
105
+ get sourceGeneration() {
106
+ return sourceGeneration;
107
+ },
108
+ get pluginId() {
109
+ return source.pluginId;
110
+ },
111
+ updateSource(value) {
112
+ const updated = sourceCopy(value);
113
+ if (updated.pluginId !== source.pluginId)
114
+ throw new Error("Selected source change requires explicit replacement");
115
+ source = updated;
116
+ },
117
+ replaceSource(value) {
118
+ const updated = sourceCopy(value), sg = next(sourceGeneration), og = next(ownerGeneration);
119
+ source = updated;
120
+ sourceGeneration = sg;
121
+ ownerGeneration = og;
122
+ held = undefined;
123
+ epoch = Symbol("source");
124
+ actual?.cancel?.("source-replaced");
125
+ },
126
+ setActive(value) {
127
+ if (value === active)
128
+ return;
129
+ active = value;
130
+ epoch = Symbol("view");
131
+ if (!active)
132
+ actual?.cancel?.("inactive");
133
+ },
134
+ read(options2 = {}) {
135
+ const { signal, full } = options2;
136
+ options2 = {};
137
+ if (!active)
138
+ return Promise.reject(new RecentSnapshotNotStarted("inactive"));
139
+ if (signal?.aborted)
140
+ return Promise.reject(new RecentSnapshotNotStarted("aborted-before-start"));
141
+ if (actual)
142
+ return Promise.reject(new RecentSnapshotNotStarted("busy"));
143
+ const input = !full && held ? { ifRevision: held.snapshot.revision } : {};
144
+ let selected = source;
145
+ const binding = sourceGeneration, view = epoch, control = new AbortController, slot = { cancel: undefined };
146
+ actual = slot;
147
+ let finished = false;
148
+ const current = () => !finished && active && epoch === view && sourceGeneration === binding;
149
+ let resolve, reject;
150
+ const result = new Promise((yes, no) => {
151
+ resolve = yes;
152
+ reject = no;
153
+ });
154
+ let cancelTimer;
155
+ const finish = (error, observation) => {
156
+ if (finished)
157
+ return;
158
+ finished = true;
159
+ cancelTimer?.();
160
+ cancelTimer = undefined;
161
+ signal?.removeEventListener("abort", abort);
162
+ slot.cancel = undefined;
163
+ const yes = resolve, no = reject;
164
+ resolve = undefined;
165
+ reject = undefined;
166
+ if (observation === undefined)
167
+ no?.(error);
168
+ else
169
+ yes?.(observation);
170
+ };
171
+ const cancel = (reason) => {
172
+ finish(new RecentSnapshotSessionError(reason));
173
+ control.abort();
174
+ };
175
+ const abort = () => cancel("aborted");
176
+ slot.cancel = cancel;
177
+ signal?.addEventListener("abort", abort, { once: true });
178
+ try {
179
+ cancelTimer = time.later(() => cancel("timeout"), timeoutMs);
180
+ } catch (error) {
181
+ finish(error);
182
+ actual = undefined;
183
+ return result;
184
+ }
185
+ if (finished) {
186
+ actual = undefined;
187
+ cancelTimer?.();
188
+ return result;
189
+ }
190
+ let pending;
191
+ try {
192
+ pending = Promise.resolve(selected.recentSnapshot(Object.freeze(input), control.signal));
193
+ } catch (error) {
194
+ pending = Promise.reject(error);
195
+ } finally {
196
+ selected = undefined;
197
+ }
198
+ pending.then((value) => {
199
+ try {
200
+ if (!finished)
201
+ finish(undefined, admit(input, value, current));
202
+ } catch (error) {
203
+ finish(error);
204
+ } finally {
205
+ if (actual === slot)
206
+ actual = undefined;
207
+ }
208
+ }, (error) => {
209
+ try {
210
+ finish(error instanceof RecentSnapshotNotStarted ? new Error("Recent snapshot source rejected after invocation", { cause: error }) : error);
211
+ } finally {
212
+ if (actual === slot)
213
+ actual = undefined;
214
+ }
215
+ });
216
+ return result;
217
+ }
218
+ };
219
+ }
220
+
221
+ // src/bb.ts
222
+ function registerHeatSettingsOwner(bb, port) {
223
+ bb.rpc.register({
224
+ threadMetricsHeatSettingsReadV1: { input: sdkSchema(heatSettingsReadInputSchema, HEAT_SETTINGS_LIMITS.inputBytes), output: sdkSchema(z.unknown()) },
225
+ threadMetricsHeatSettingsSaveV1: { input: sdkSchema(heatSettingsSaveInputSchema, HEAT_SETTINGS_LIMITS.inputBytes), output: sdkSchema(z.unknown()) }
226
+ }, {
227
+ threadMetricsHeatSettingsReadV1: async (raw) => bounded(heatSettingsReadResultSchema, await port.read(bounded(heatSettingsReadInputSchema, raw, HEAT_SETTINGS_LIMITS.inputBytes)), HEAT_SETTINGS_LIMITS.outputBytes),
228
+ threadMetricsHeatSettingsSaveV1: async (raw) => bounded(heatSettingsSaveResultSchema, await port.save(validateHeatSaveInput(raw)), HEAT_SETTINGS_LIMITS.outputBytes)
229
+ }, { experimental_discoverable: true });
230
+ }
231
+ function createHeatSettingsClient(sdk, pluginId) {
232
+ if (!pluginId || pluginId.length > 200)
233
+ throw new Error("Invalid target plugin id");
234
+ const call = async (method, input, signal) => sdk.plugins.callRpc({ pluginId, method, input, outputSchema: z.unknown(), ...signal ? { signal } : {} });
235
+ return {
236
+ read: async (input, signal) => bounded(heatSettingsReadResultSchema, await call(HEAT_SETTINGS_METHODS.read, bounded(heatSettingsReadInputSchema, input, HEAT_SETTINGS_LIMITS.inputBytes), signal), HEAT_SETTINGS_LIMITS.outputBytes),
237
+ save: async (input, signal) => {
238
+ const result = bounded(heatSettingsSaveResultSchema, await call(HEAT_SETTINGS_METHODS.save, validateHeatSaveInput(input), signal), HEAT_SETTINGS_LIMITS.outputBytes);
239
+ if (result.operationKey !== input.operationKey)
240
+ throw new Error("Save operation mismatch");
241
+ return result;
242
+ }
243
+ };
244
+ }
4
245
  function sdkSchema(schema, maxBytes) {
5
246
  return { "~standard": { version: 1, vendor: "bb-thread-metrics", jsonSchema: { input: () => z.toJSONSchema(schema, { io: "input", unrepresentable: "any" }), output: () => z.toJSONSchema(schema, { io: "output", unrepresentable: "any" }) }, validate(value) {
6
247
  try {
@@ -14,34 +255,38 @@ function registerThreadMetricsOwner(bb, port) {
14
255
  const capabilities = capabilitiesSchema.parse(port.capabilities);
15
256
  if (Boolean(capabilities.recentSnapshotV1) !== Boolean(port.recentSnapshot))
16
257
  throw new Error("Recent snapshot capability/handler mismatch");
258
+ if (Boolean(capabilities.heatSettingsV1) !== Boolean(port.heatSettings))
259
+ throw new Error("Heat settings capability/handler mismatch");
17
260
  if (capabilities.detail !== Boolean(port.detail))
18
261
  throw new Error("Detail capability/handler mismatch");
19
262
  const describe = () => ({ protocol: "bb-thread-metrics", versions: [1], capabilities });
20
263
  const contract = {
21
- threadMetricsDescribe: { input: sdkSchema(describeInputSchema, LIMITS.inputBytes), output: sdkSchema(z.unknown()) },
22
- threadMetricsReadV1: { input: sdkSchema(readInputSchema, LIMITS.inputBytes), output: sdkSchema(z.unknown()) }
264
+ threadMetricsDescribe: { input: sdkSchema(describeInputSchema, LIMITS2.inputBytes), output: sdkSchema(z.unknown()) },
265
+ threadMetricsReadV1: { input: sdkSchema(readInputSchema, LIMITS2.inputBytes), output: sdkSchema(z.unknown()) }
23
266
  };
24
267
  const handlers = {
25
268
  threadMetricsDescribe: (raw) => {
26
- parseBounded(describeInputSchema, raw, LIMITS.inputBytes);
269
+ parseBounded(describeInputSchema, raw, LIMITS2.inputBytes);
27
270
  return describe();
28
271
  },
29
272
  threadMetricsReadV1: async (raw) => {
30
- const input = parseBounded(readInputSchema, raw, LIMITS.inputBytes);
273
+ const input = parseBounded(readInputSchema, raw, LIMITS2.inputBytes);
31
274
  if (input.series && !capabilities.series)
32
275
  throw new Error("Series not supported");
33
276
  return validateReadResult(input, await port.read(input));
34
277
  }
35
278
  };
36
279
  bb.rpc.register(contract, handlers, { experimental_discoverable: true });
280
+ if (port.heatSettings)
281
+ registerHeatSettingsOwner(bb, port.heatSettings);
37
282
  if (port.recentSnapshot)
38
283
  bb.rpc.register({ threadMetricsRecentSnapshotV1: { input: sdkSchema(recentSnapshotInputSchema, RECENT_LIMITS.inputBytes), output: sdkSchema(z.unknown()) } }, { threadMetricsRecentSnapshotV1: async (raw) => {
39
284
  const input = parseBounded(recentSnapshotInputSchema, raw, RECENT_LIMITS.inputBytes);
40
- return validateRecentSnapshot(input, await port.recentSnapshot(input));
285
+ return validateRecentSnapshot2(input, await port.recentSnapshot(input));
41
286
  } }, { experimental_discoverable: true });
42
287
  if (port.detail)
43
- bb.rpc.register({ threadMetricsDetailV1: { input: sdkSchema(detailInputSchema, LIMITS.inputBytes), output: sdkSchema(z.unknown()) } }, { threadMetricsDetailV1: async (raw) => {
44
- const input = parseBounded(detailInputSchema, raw, LIMITS.inputBytes);
288
+ bb.rpc.register({ threadMetricsDetailV1: { input: sdkSchema(detailInputSchema, LIMITS2.inputBytes), output: sdkSchema(z.unknown()) } }, { threadMetricsDetailV1: async (raw) => {
289
+ const input = parseBounded(detailInputSchema, raw, LIMITS2.inputBytes);
45
290
  return validateDetailResult(input, await port.detail(input));
46
291
  } }, { experimental_discoverable: true });
47
292
  }
@@ -55,30 +300,30 @@ function createThreadMetricsClient(sdk, pluginId) {
55
300
  pluginId,
56
301
  async describe(signal) {
57
302
  const raw = await call(METHODS.describe, {}, signal);
58
- return negotiateVersion(parseBounded(describeEnvelopeSchema, raw, LIMITS.describeBytes));
303
+ return negotiateVersion(parseBounded(describeEnvelopeSchema, raw, LIMITS2.describeBytes));
59
304
  },
60
305
  async recentSnapshot(raw = {}, signal) {
61
306
  const input = parseBounded(recentSnapshotInputSchema, raw, RECENT_LIMITS.inputBytes);
62
- return validateRecentSnapshot(input, await call(METHODS.recentSnapshot, input, signal));
307
+ return validateRecentSnapshot2(input, await call(METHODS.recentSnapshot, input, signal));
63
308
  },
64
309
  async read(raw, signal) {
65
- const input = parseBounded(readInputSchema, raw, LIMITS.inputBytes);
310
+ const input = parseBounded(readInputSchema, raw, LIMITS2.inputBytes);
66
311
  return validateReadResult(input, await call(METHODS.read, input, signal));
67
312
  },
68
313
  async detail(raw, signal) {
69
- const input = parseBounded(detailInputSchema, raw, LIMITS.inputBytes);
314
+ const input = parseBounded(detailInputSchema, raw, LIMITS2.inputBytes);
70
315
  return validateDetailResult(input, await call(METHODS.detail, input, signal));
71
316
  }
72
317
  };
73
318
  }
74
319
  async function discoverThreadMetricsOwners(options) {
75
- const bounded = (v, max) => {
320
+ const bounded2 = (v, max) => {
76
321
  const n = v ?? max;
77
322
  if (!Number.isInteger(n) || n < 1 || n > max)
78
323
  throw new Error("Invalid discovery budget");
79
324
  return n;
80
325
  };
81
- const count = bounded(options.maxCandidates, LIMITS.candidates), concurrency = bounded(options.concurrency, LIMITS.concurrency), callMs = bounded(options.callMs, LIMITS.callMs), deadlineMs = bounded(options.deadlineMs, LIMITS.discoveryMs);
326
+ const count = bounded2(options.maxCandidates, LIMITS2.candidates), concurrency = bounded2(options.concurrency, LIMITS2.concurrency), callMs = bounded2(options.callMs, LIMITS2.callMs), deadlineMs = bounded2(options.deadlineMs, LIMITS2.discoveryMs);
82
327
  const pass = new AbortController, rows = new Map, active = new Map;
83
328
  let done = false, ending = false, listedReady = false, omittedCount = 0, eligibleCount = 0, started = 0;
84
329
  const timers = new Set;
@@ -131,11 +376,11 @@ async function discoverThreadMetricsOwners(options) {
131
376
  return;
132
377
  listedReady = true;
133
378
  const unique = new Map;
134
- for (const p of listed.plugins.slice(0, LIMITS.listedPlugins))
379
+ for (const p of listed.plugins.slice(0, LIMITS2.listedPlugins))
135
380
  if (p.id.length <= 200 && (p.status === "running" || p.status === "degraded"))
136
381
  unique.set(p.id, p);
137
382
  const candidates = [...unique.values()].slice(0, count);
138
- omittedCount = unique.size - candidates.length + Math.max(0, listed.plugins.length - LIMITS.listedPlugins);
383
+ omittedCount = unique.size - candidates.length + Math.max(0, listed.plugins.length - LIMITS2.listedPlugins);
139
384
  eligibleCount = candidates.length;
140
385
  let index = 0;
141
386
  async function worker() {
@@ -203,6 +448,11 @@ function createThreadMetricsDiscovery(sdk) {
203
448
  }
204
449
  export {
205
450
  registerThreadMetricsOwner,
451
+ registerHeatSettingsOwner,
206
452
  createThreadMetricsDiscovery,
207
- createThreadMetricsClient
453
+ createThreadMetricsClient,
454
+ createRecentSnapshotSession,
455
+ createHeatSettingsClient,
456
+ RecentSnapshotSessionError,
457
+ RecentSnapshotNotStarted
208
458
  };