@loadstrike/loadstrike-sdk 1.0.20801 → 1.0.21101

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.
@@ -5,6 +5,10 @@ exports.planAgentScenarioAssignments = planAgentScenarioAssignments;
5
5
  const node_crypto_1 = require("node:crypto");
6
6
  const transports_js_1 = require("./transports.js");
7
7
  class LocalClusterCoordinator {
8
+ /**
9
+ * Exposes the public constructor operation.
10
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
11
+ */
8
12
  constructor(options = {}) {
9
13
  this.options = options;
10
14
  }
@@ -64,6 +68,9 @@ class LocalClusterCoordinator {
64
68
  }
65
69
  }
66
70
  exports.LocalClusterCoordinator = LocalClusterCoordinator;
71
+ /**
72
+ * Exposes the plan agent scenario assignments operation. Use this when interacting with the SDK through this surface.
73
+ */
67
74
  function planAgentScenarioAssignments(scenarioNames, agentCount, options = {}) {
68
75
  const totalAgents = Math.max(agentCount, 0);
69
76
  const assignments = Array.from({ length: totalAgents }, () => []);
@@ -119,10 +126,14 @@ function planAgentScenarioAssignments(scenarioNames, agentCount, options = {}) {
119
126
  return assignments;
120
127
  }
121
128
  class DistributedClusterCoordinator {
129
+ /**
130
+ * Exposes the public constructor operation.
131
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
132
+ */
122
133
  constructor(options) {
123
134
  this.options = options;
124
135
  }
125
- async dispatch(assignments) {
136
+ async dispatch(assignments, buildAgentRunToken) {
126
137
  const expected = Math.max(this.options.expectedAgentResults, 0);
127
138
  const timeoutMs = Math.max(this.options.commandTimeoutMs ?? 120000, 1);
128
139
  const runSubject = this.buildRunSubject();
@@ -144,6 +155,9 @@ class DistributedClusterCoordinator {
144
155
  targetScenarios: assignments[i] ?? [],
145
156
  replySubject
146
157
  };
158
+ if (buildAgentRunToken) {
159
+ command.agentRunToken = await buildAgentRunToken(command);
160
+ }
147
161
  await commandProducer.produce({
148
162
  headers: {
149
163
  "x-cluster-command-id": commandId,
@@ -205,6 +219,10 @@ class DistributedClusterCoordinator {
205
219
  }
206
220
  exports.DistributedClusterCoordinator = DistributedClusterCoordinator;
207
221
  class DistributedClusterAgent {
222
+ /**
223
+ * Exposes the public constructor operation.
224
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
225
+ */
208
226
  constructor(options) {
209
227
  this.commandConsumer = null;
210
228
  this.options = options;
@@ -254,7 +272,13 @@ class DistributedClusterAgent {
254
272
  try {
255
273
  let result;
256
274
  try {
257
- const nodeResult = await execute({ scenarioNames: command.targetScenarios });
275
+ const nodeResult = await execute({
276
+ scenarioNames: command.targetScenarios,
277
+ commandId: command.commandId,
278
+ agentRunToken: command.agentRunToken,
279
+ agentIndex: command.agentIndex,
280
+ agentCount: command.agentCount
281
+ });
258
282
  result = {
259
283
  commandId: command.commandId,
260
284
  agentId: this.options.agentId,
@@ -305,6 +329,7 @@ function parseRunCommand(value) {
305
329
  agentIndex: numberOrDefault(value.agentIndex, numberOrDefault(value.AgentIndex, 0)),
306
330
  agentCount: numberOrDefault(value.agentCount, numberOrDefault(value.AgentCount, 0)),
307
331
  targetScenarios,
332
+ agentRunToken: stringOrDefault(value.agentRunToken, stringOrDefault(value.AgentRunToken, "")),
308
333
  replySubject: stringOrDefault(value.replySubject, stringOrDefault(value.ReplySubject, ""))
309
334
  };
310
335
  }
@@ -4,6 +4,10 @@ exports.__loadstrikeTestExports = exports.CrossPlatformTrackingRuntime = exports
4
4
  const node_crypto_1 = require("node:crypto");
5
5
  const redis_1 = require("redis");
6
6
  class TrackingPayloadBuilder {
7
+ /**
8
+ * Exposes the public constructor operation.
9
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
10
+ */
7
11
  constructor() {
8
12
  this.Headers = {};
9
13
  this.body = new Uint8Array();
@@ -11,12 +15,24 @@ class TrackingPayloadBuilder {
11
15
  get Body() {
12
16
  return new Uint8Array(this.body);
13
17
  }
18
+ /**
19
+ * Sets the payload body on the builder.
20
+ * Use this when a tracking payload should be created from raw content.
21
+ */
14
22
  setBody(body) {
15
23
  this.body = normalizeTrackingBodyToBytes(body);
16
24
  }
25
+ /**
26
+ * Sets the payload body on the builder.
27
+ * Use this when a tracking payload should be created from raw content.
28
+ */
17
29
  SetBody(body) {
18
30
  this.setBody(body);
19
31
  }
32
+ /**
33
+ * Builds the configured payload or helper object.
34
+ * Use this when all builder inputs are ready to be materialized.
35
+ */
20
36
  build() {
21
37
  return createTrackingPayload({
22
38
  headers: { ...this.Headers },
@@ -27,6 +43,10 @@ class TrackingPayloadBuilder {
27
43
  jsonConvertSettings: cloneRecord(this.JsonConvertSettings)
28
44
  });
29
45
  }
46
+ /**
47
+ * Builds the configured payload or helper object.
48
+ * Use this when all builder inputs are ready to be materialized.
49
+ */
30
50
  Build() {
31
51
  return this.build();
32
52
  }
@@ -45,6 +65,10 @@ class RedisCorrelationStoreOptions {
45
65
  set EntryTtl(value) {
46
66
  this.EntryTtlSeconds = value;
47
67
  }
68
+ /**
69
+ * Exposes the public validate operation.
70
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
71
+ */
48
72
  validate() {
49
73
  if (!this.ConnectionString.trim()) {
50
74
  throw new Error("Redis ConnectionString must be provided.");
@@ -56,6 +80,10 @@ class RedisCorrelationStoreOptions {
56
80
  throw new RangeError("Redis EntryTtl must be greater than zero.");
57
81
  }
58
82
  }
83
+ /**
84
+ * Exposes the public Validate operation.
85
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
86
+ */
59
87
  Validate() {
60
88
  this.validate();
61
89
  }
@@ -65,12 +93,24 @@ class CorrelationStoreConfiguration {
65
93
  constructor() {
66
94
  this.Kind = "InMemory";
67
95
  }
96
+ /**
97
+ * Creates an in-memory correlation store configuration.
98
+ * Use this when tracking state can stay local to the process.
99
+ */
68
100
  static inMemory() {
69
101
  return new CorrelationStoreConfiguration();
70
102
  }
103
+ /**
104
+ * Creates an in-memory correlation store configuration.
105
+ * Use this when tracking state can stay local to the process.
106
+ */
71
107
  static InMemory() {
72
108
  return CorrelationStoreConfiguration.inMemory();
73
109
  }
110
+ /**
111
+ * Creates a Redis-backed correlation store configuration.
112
+ * Use this when tracking state must survive across processes or cluster nodes.
113
+ */
74
114
  static redisStore(options) {
75
115
  if (!(options instanceof RedisCorrelationStoreOptions)) {
76
116
  throw new TypeError("Redis correlation store options must be provided.");
@@ -80,9 +120,17 @@ class CorrelationStoreConfiguration {
80
120
  configuration.Redis = options;
81
121
  return configuration;
82
122
  }
123
+ /**
124
+ * Creates a Redis-backed correlation store configuration.
125
+ * Use this when tracking state must survive across processes or cluster nodes.
126
+ */
83
127
  static RedisStore(options) {
84
128
  return CorrelationStoreConfiguration.redisStore(options);
85
129
  }
130
+ /**
131
+ * Exposes the public validate operation.
132
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
133
+ */
86
134
  validate() {
87
135
  if (this.Kind === "Redis") {
88
136
  if (!(this.Redis instanceof RedisCorrelationStoreOptions)) {
@@ -91,6 +139,10 @@ class CorrelationStoreConfiguration {
91
139
  this.Redis.validate();
92
140
  }
93
141
  }
142
+ /**
143
+ * Exposes the public Validate operation.
144
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
145
+ */
94
146
  Validate() {
95
147
  this.validate();
96
148
  }
@@ -103,6 +155,10 @@ class InMemoryCorrelationStore {
103
155
  this.sourceOccurrences = new Map();
104
156
  this.destinationOccurrences = new Map();
105
157
  }
158
+ /**
159
+ * Sets the current gauge value.
160
+ * Use this when the latest observed value should replace the previous one.
161
+ */
106
162
  set(entry) {
107
163
  const normalized = normalizeCorrelationEntry(entry);
108
164
  const queue = this.pendingByTrackingId.get(normalized.trackingId) ?? [];
@@ -110,10 +166,18 @@ class InMemoryCorrelationStore {
110
166
  this.pendingByTrackingId.set(normalized.trackingId, queue);
111
167
  this.pendingByEventId.set(normalized.eventId ?? "", normalized);
112
168
  }
169
+ /**
170
+ * Exposes the public get operation.
171
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
172
+ */
113
173
  get(trackingId) {
114
174
  const queue = this.pendingByTrackingId.get(trackingId);
115
175
  return queue && queue.length > 0 ? hydrateCorrelationEntry(queue[0]) : null;
116
176
  }
177
+ /**
178
+ * Exposes the public delete operation.
179
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
180
+ */
117
181
  delete(trackingId) {
118
182
  const queue = this.pendingByTrackingId.get(trackingId);
119
183
  if (!queue || queue.length === 0) {
@@ -129,6 +193,10 @@ class InMemoryCorrelationStore {
129
193
  }
130
194
  this.pendingByTrackingId.set(trackingId, queue);
131
195
  }
196
+ /**
197
+ * Exposes the public all operation.
198
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
199
+ */
132
200
  all() {
133
201
  const entries = [];
134
202
  for (const queue of this.pendingByTrackingId.values()) {
@@ -138,21 +206,37 @@ class InMemoryCorrelationStore {
138
206
  }
139
207
  return entries;
140
208
  }
209
+ /**
210
+ * Exposes the public collectExpired operation.
211
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
212
+ */
141
213
  collectExpired(nowMs = Date.now(), maxCount = 0) {
142
214
  void nowMs;
143
215
  void maxCount;
144
216
  return [];
145
217
  }
218
+ /**
219
+ * Exposes the public incrementSourceOccurrences operation.
220
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
221
+ */
146
222
  incrementSourceOccurrences(trackingId) {
147
223
  const next = (this.sourceOccurrences.get(trackingId) ?? 0) + 1;
148
224
  this.sourceOccurrences.set(trackingId, next);
149
225
  return next;
150
226
  }
227
+ /**
228
+ * Exposes the public incrementDestinationOccurrences operation.
229
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
230
+ */
151
231
  incrementDestinationOccurrences(trackingId) {
152
232
  const next = (this.destinationOccurrences.get(trackingId) ?? 0) + 1;
153
233
  this.destinationOccurrences.set(trackingId, next);
154
234
  return next;
155
235
  }
236
+ /**
237
+ * Exposes the public registerSource operation.
238
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
239
+ */
156
240
  registerSource(trackingId, sourceTimestampUtc) {
157
241
  const entry = normalizeCorrelationEntry({
158
242
  trackingId,
@@ -165,6 +249,10 @@ class InMemoryCorrelationStore {
165
249
  this.set(entry);
166
250
  return entry;
167
251
  }
252
+ /**
253
+ * Exposes the public tryMatchDestination operation.
254
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
255
+ */
168
256
  tryMatchDestination(trackingId, destinationTimestampUtc) {
169
257
  const source = this.get(trackingId);
170
258
  if (!source) {
@@ -176,6 +264,10 @@ class InMemoryCorrelationStore {
176
264
  destinationTimestampUtc
177
265
  });
178
266
  }
267
+ /**
268
+ * Exposes the public tryExpire operation.
269
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
270
+ */
179
271
  tryExpire(eventId) {
180
272
  const entry = this.pendingByEventId.get(eventId);
181
273
  if (!entry) {
@@ -518,10 +610,18 @@ class RedisCorrelationStore {
518
610
  }
519
611
  exports.RedisCorrelationStore = RedisCorrelationStore;
520
612
  class TrackingFieldSelector {
613
+ /**
614
+ * Exposes the public constructor operation.
615
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
616
+ */
521
617
  constructor(location, path) {
522
618
  this.kind = normalizeTrackingFieldLocation(location);
523
619
  this.path = String(path ?? "");
524
620
  }
621
+ /**
622
+ * Parses a selector or configuration expression.
623
+ * Use this when a tracking selector should be created from its text form.
624
+ */
525
625
  static parse(value) {
526
626
  const normalized = (value ?? "").trim();
527
627
  const [kindRaw, ...rest] = normalized.split(":");
@@ -531,9 +631,17 @@ class TrackingFieldSelector {
531
631
  }
532
632
  return new TrackingFieldSelector(kindRaw.trim(), path);
533
633
  }
634
+ /**
635
+ * Parses a selector or configuration expression.
636
+ * Use this when a tracking selector should be created from its text form.
637
+ */
534
638
  static Parse(value) {
535
639
  return TrackingFieldSelector.parse(value);
536
640
  }
641
+ /**
642
+ * Attempts to parse a selector or configuration expression.
643
+ * Use this when invalid input should be handled without throwing.
644
+ */
537
645
  static tryParse(value) {
538
646
  try {
539
647
  return {
@@ -548,6 +656,10 @@ class TrackingFieldSelector {
548
656
  };
549
657
  }
550
658
  }
659
+ /**
660
+ * Attempts to parse a selector or configuration expression.
661
+ * Use this when invalid input should be handled without throwing.
662
+ */
551
663
  static TryParse(value) {
552
664
  return TrackingFieldSelector.tryParse(value);
553
665
  }
@@ -557,6 +669,10 @@ class TrackingFieldSelector {
557
669
  get Path() {
558
670
  return this.path;
559
671
  }
672
+ /**
673
+ * Extracts a value from the provided payload.
674
+ * Use this when a tracking selector should read data from a transport payload.
675
+ */
560
676
  extract(payload) {
561
677
  if (this.kind === "header") {
562
678
  const headers = payload.headers ?? {};
@@ -586,9 +702,17 @@ class TrackingFieldSelector {
586
702
  }
587
703
  return String(current);
588
704
  }
705
+ /**
706
+ * Exposes the public toString operation.
707
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
708
+ */
589
709
  toString() {
590
710
  return `${this.kind}:${this.path}`;
591
711
  }
712
+ /**
713
+ * Exposes the public ToString operation.
714
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
715
+ */
592
716
  ToString() {
593
717
  return this.toString();
594
718
  }
@@ -602,6 +726,10 @@ function normalizeTrackingFieldLocation(location) {
602
726
  throw new Error("Tracking field location must be Header or Json.");
603
727
  }
604
728
  class CrossPlatformTrackingRuntime {
729
+ /**
730
+ * Exposes the public constructor operation.
731
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
732
+ */
605
733
  constructor(options) {
606
734
  this.trackingIdDisplayByEventId = new Map();
607
735
  this.gatherByDisplayByComparisonKey = new Map();
@@ -819,6 +947,10 @@ class CrossPlatformTrackingRuntime {
819
947
  this.gatherByDisplayByComparisonKey.set(comparisonKey, gatherKey);
820
948
  return gatherKey;
821
949
  }
950
+ /**
951
+ * Exposes the public isTimeoutCountedAsFailure operation.
952
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
953
+ */
822
954
  isTimeoutCountedAsFailure() {
823
955
  return this.timeoutCountsAsFailure;
824
956
  }
package/dist/cjs/local.js CHANGED
@@ -86,6 +86,10 @@ const CI_ENVIRONMENT_VARIABLES = [
86
86
  "HEROKU_TEST_RUN_ID"
87
87
  ];
88
88
  class LoadStrikeLocalClient {
89
+ /**
90
+ * Exposes the public constructor operation.
91
+ * Use this when the surrounding wrapper type makes this operation the clearest way to express your intent.
92
+ */
89
93
  constructor(options = {}) {
90
94
  this.signingKeyCache = new Map();
91
95
  assertNoDisableLicenseEnforcementOption(options, "LoadStrikeLocalClient");
@@ -152,6 +156,17 @@ class LoadStrikeLocalClient {
152
156
  const context = asRecord(request.Context);
153
157
  const nodeType = normalizeNodeType(pickValue(context, "NodeType", "nodeType"));
154
158
  if (nodeType === "agent") {
159
+ const agentExecutionToken = stringOrDefault(pickValue(context, "AgentExecutionToken", "agentExecutionToken"), "").trim();
160
+ if (!agentExecutionToken) {
161
+ throw new Error("Agent node type cannot run directly. Start it from a coordinator session after controller license validation.");
162
+ }
163
+ const agentCommandId = stringOrDefault(pickValue(context, "AgentCommandId", "agentCommandId"), "").trim();
164
+ if (!agentCommandId) {
165
+ throw new Error("Agent execution is missing the coordinator command identifier.");
166
+ }
167
+ const requestedFeatures = collectRequestedFeatures(request);
168
+ const sessionId = stringOrDefault(pickValue(context, "SessionId", "sessionId"), "").trim();
169
+ await this.verifySignedAgentExecutionToken(agentExecutionToken, request, requestedFeatures, sessionId, agentCommandId, resolveTargetScenarioNames(context));
155
170
  return {};
156
171
  }
157
172
  const runnerKey = stringOrDefault(context.RunnerKey, "").trim();
@@ -258,6 +273,92 @@ class LoadStrikeLocalClient {
258
273
  enforceEntitlementClaims(parsed.payload, requestedFeatures);
259
274
  enforceRuntimePolicyClaims(parsed.payload, request);
260
275
  }
276
+ async createAgentExecutionToken(controllerRunToken, sessionId, commandId, agentIndex, agentCount, targetScenarios) {
277
+ const controller = new AbortController();
278
+ const timer = setTimeout(() => controller.abort(), this.licenseValidationTimeoutMs);
279
+ try {
280
+ const payload = {
281
+ ControllerRunToken: controllerRunToken,
282
+ SessionId: sessionId,
283
+ CommandId: commandId,
284
+ AgentIndex: agentIndex,
285
+ AgentCount: agentCount,
286
+ TargetScenarios: [...targetScenarios]
287
+ };
288
+ const { response, json } = await this.postLicensingRequest("/api/v1/licenses/agent-token", payload, controller.signal);
289
+ const agentPayload = json;
290
+ const isValid = pickValue(agentPayload, "IsValid", "isValid");
291
+ if (!response.ok || isValid !== true) {
292
+ const details = stringOrDefault(pickValue(agentPayload, "Message", "message"), "No additional details provided.");
293
+ const denialCode = stringOrDefault(pickValue(agentPayload, "DenialCode", "denialCode"), "unknown_denial");
294
+ throw new Error(`Agent execution authorization denied. Reason: ${denialCode}. ${details}`);
295
+ }
296
+ const agentRunToken = stringOrDefault(pickValue(agentPayload, "AgentRunToken", "agentRunToken"), "").trim();
297
+ if (!agentRunToken) {
298
+ throw new Error("Agent execution authorization failed: token is missing.");
299
+ }
300
+ return agentRunToken;
301
+ }
302
+ finally {
303
+ clearTimeout(timer);
304
+ }
305
+ }
306
+ async verifySignedAgentExecutionToken(runToken, request, requestedFeatures, sessionId, commandId, targetScenarios) {
307
+ const parsed = parseJwt(runToken);
308
+ const keyId = stringOrDefault(parsed.header.kid, "default");
309
+ const algorithm = stringOrDefault(parsed.header.alg, "RS256").toUpperCase();
310
+ if (algorithm !== "RS256") {
311
+ throw new Error("Agent execution authorization failed: invalid token signature.");
312
+ }
313
+ const keyRecord = await this.getSigningKey(keyId, algorithm);
314
+ if (!verifyJwtSignature(runToken, keyRecord, algorithm)) {
315
+ throw new Error("Agent execution authorization failed: invalid token signature.");
316
+ }
317
+ enforceJwtLifetime(parsed.payload);
318
+ const tokenIssuer = stringOrDefault(parsed.payload.iss, "").trim();
319
+ if (!keyRecord.issuer || tokenIssuer !== keyRecord.issuer) {
320
+ throw new Error("Agent execution authorization failed: token issuer does not match.");
321
+ }
322
+ const rawAudience = parsed.payload.aud;
323
+ const tokenAudiences = Array.isArray(rawAudience)
324
+ ? rawAudience.map((entry) => stringOrDefault(entry, "").trim()).filter((entry) => entry.length > 0)
325
+ : stringOrDefault(rawAudience, "").trim()
326
+ ? [stringOrDefault(rawAudience, "").trim()]
327
+ : [];
328
+ if (!keyRecord.audience || !tokenAudiences.includes(keyRecord.audience)) {
329
+ throw new Error("Agent execution authorization failed: token audience does not match.");
330
+ }
331
+ const tokenKind = stringOrDefault(parsed.payload.token_kind, "").trim();
332
+ if (tokenKind !== "agent_execution") {
333
+ throw new Error("Agent execution authorization failed: token kind is invalid.");
334
+ }
335
+ const tokenNodeType = stringOrDefault(parsed.payload.node_type, "").trim().toLowerCase();
336
+ if (tokenNodeType !== "agent") {
337
+ throw new Error("Agent execution authorization failed: token node type is invalid.");
338
+ }
339
+ const tokenSessionId = stringOrDefault(parsed.payload.session_id, "").trim();
340
+ if (tokenSessionId !== sessionId) {
341
+ throw new Error("Agent execution authorization failed: session id does not match.");
342
+ }
343
+ const tokenCommandId = stringOrDefault(parsed.payload.command_id, "").trim();
344
+ if (tokenCommandId !== commandId) {
345
+ throw new Error("Agent execution authorization failed: command id does not match.");
346
+ }
347
+ const tokenTargetScenarios = collectClaimStrings(parsed.payload, "target_scenario")
348
+ .map((value) => value.trim())
349
+ .filter((value) => value.length > 0)
350
+ .sort();
351
+ const expectedTargetScenarios = [...targetScenarios]
352
+ .map((value) => value.trim())
353
+ .filter((value) => value.length > 0)
354
+ .sort();
355
+ if (tokenTargetScenarios.length !== expectedTargetScenarios.length
356
+ || tokenTargetScenarios.some((value, index) => value !== expectedTargetScenarios[index])) {
357
+ throw new Error("Agent execution authorization failed: target scenarios do not match.");
358
+ }
359
+ enforceEntitlementClaims(parsed.payload, requestedFeatures);
360
+ enforceRuntimePolicyClaims(parsed.payload, request);
361
+ }
261
362
  async getSigningKey(keyId, algorithm) {
262
363
  const nowMs = Date.now();
263
364
  const normalizedKeyId = stringOrDefault(keyId, "default").trim() || "default";
@@ -440,6 +541,7 @@ function generateSessionId() {
440
541
  function collectRequestedFeatures(request) {
441
542
  const context = asRecord(request.Context);
442
543
  const scenarios = Array.isArray(request.Scenarios) ? request.Scenarios : [];
544
+ const nodeType = normalizeNodeType(pickValue(context, "NodeType", "nodeType"));
443
545
  const features = new Set([
444
546
  "core.runtime",
445
547
  "reporting.formats.standard"
@@ -453,7 +555,9 @@ function collectRequestedFeatures(request) {
453
555
  const hasAdvancedTargeting = normalizeStringArray(pickValue(context, "AgentTargetScenarios", "agentTargetScenarios")).length > 0
454
556
  || normalizeStringArray(pickValue(context, "CoordinatorTargetScenarios", "coordinatorTargetScenarios")).length > 0
455
557
  || resolveTimeoutSeconds(context, "ClusterCommandTimeoutSeconds", "clusterCommandTimeoutSeconds", "ClusterCommandTimeoutMs", "clusterCommandTimeoutMs", 120) !== 120;
456
- if (hasAdvancedTargeting) {
558
+ // Agent child contexts carry coordinator-assigned target slices. That internal
559
+ // state must not force the advanced targeting entitlement a second time.
560
+ if (hasAdvancedTargeting && nodeType !== "agent") {
457
561
  features.add("cluster.targeting_and_tuning.advanced");
458
562
  }
459
563
  if (countCustomWorkerPlugins(context) > 0) {
@@ -1890,13 +1994,16 @@ function toContractNodeType(value) {
1890
1994
  }
1891
1995
  }
1892
1996
  function countTargetedScenarios(context) {
1893
- const values = [
1894
- ...normalizeStringArray(pickValue(context, "TargetScenarios", "targetScenarios")),
1895
- ...normalizeStringArray(pickValue(context, "AgentTargetScenarios", "agentTargetScenarios")),
1896
- ...normalizeStringArray(pickValue(context, "CoordinatorTargetScenarios", "coordinatorTargetScenarios"))
1897
- ];
1997
+ const values = resolveTargetScenarioNames(context);
1898
1998
  return new Set(values.map((value) => value.trim().toLowerCase()).filter((value) => value.length > 0)).size;
1899
1999
  }
2000
+ function resolveTargetScenarioNames(context) {
2001
+ return [...new Set([
2002
+ ...normalizeStringArray(pickValue(context, "TargetScenarios", "targetScenarios")),
2003
+ ...normalizeStringArray(pickValue(context, "AgentTargetScenarios", "agentTargetScenarios")),
2004
+ ...normalizeStringArray(pickValue(context, "CoordinatorTargetScenarios", "coordinatorTargetScenarios"))
2005
+ ])];
2006
+ }
1900
2007
  function countCustomWorkerPlugins(context) {
1901
2008
  const workerPlugins = asList(pickValue(context, "WorkerPlugins", "workerPlugins"));
1902
2009
  let count = 0;
@@ -1038,6 +1038,9 @@ function buildDotnetHtmlTabs(nodeStats) {
1038
1038
  }
1039
1039
  return tabs;
1040
1040
  }
1041
+ /**
1042
+ * Exposes the build dotnet txt report operation. Use this when interacting with the SDK through this surface.
1043
+ */
1041
1044
  function buildDotnetTxtReport(nodeStats) {
1042
1045
  const scenarios = sortBySortIndex(reportArray(nodeStats, "scenarioStats", "ScenarioStats"));
1043
1046
  const testInfo = reportObject(nodeStats, "testInfo", "TestInfo");
@@ -1084,6 +1087,9 @@ function buildDotnetTxtReport(nodeStats) {
1084
1087
  }
1085
1088
  return reportLines(lines);
1086
1089
  }
1090
+ /**
1091
+ * Exposes the build dotnet csv report operation. Use this when interacting with the SDK through this surface.
1092
+ */
1087
1093
  function buildDotnetCsvReport(nodeStats) {
1088
1094
  const lines = ["ScenarioName,Requests,Ok,Fail,DurationSeconds,Rps"];
1089
1095
  for (const scenario of sortBySortIndex(reportArray(nodeStats, "scenarioStats", "ScenarioStats"))) {
@@ -1093,6 +1099,9 @@ function buildDotnetCsvReport(nodeStats) {
1093
1099
  }
1094
1100
  return reportLines(lines);
1095
1101
  }
1102
+ /**
1103
+ * Exposes the build dotnet markdown report operation. Use this when interacting with the SDK through this surface.
1104
+ */
1096
1105
  function buildDotnetMarkdownReport(nodeStats) {
1097
1106
  const scenarios = sortBySortIndex(reportArray(nodeStats, "scenarioStats", "ScenarioStats"));
1098
1107
  const testInfo = reportObject(nodeStats, "testInfo", "TestInfo");
@@ -1132,6 +1141,9 @@ function buildDotnetMarkdownReport(nodeStats) {
1132
1141
  }
1133
1142
  return reportLines(lines);
1134
1143
  }
1144
+ /**
1145
+ * Exposes the build dotnet html report operation. Use this when interacting with the SDK through this surface.
1146
+ */
1135
1147
  function buildDotnetHtmlReport(nodeStats) {
1136
1148
  const tabs = buildDotnetHtmlTabs(nodeStats);
1137
1149
  const buttonsHtml = tabs.map(([tabId, title]) => `<button class="tab-btn" data-tab="${tabId}">${escapeHtml(title)}</button>${REPORT_EOL}`).join("");