deepline 0.3.201 → 0.3.203

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 (41) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +62 -1
  2. package/dist/bundling-sources/sdk/src/http.ts +55 -6
  3. package/dist/bundling-sources/sdk/src/index.ts +1 -0
  4. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  5. package/dist/bundling-sources/sdk/src/types.ts +8 -0
  6. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +4 -2
  7. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +460 -96
  8. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +11 -1
  9. package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-invocation-fence.ts +6 -2
  10. package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +401 -2
  11. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +458 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/provider-no-progress.ts +40 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +7 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +25 -3
  15. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +1 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +119 -171
  17. package/dist/bundling-sources/shared_libs/play-runtime/runtime-work-counters.ts +37 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +238 -204
  19. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +16 -7
  20. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +136 -25
  21. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +21 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +64 -15
  23. package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +5 -5
  24. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/result-receipt-policy.ts +46 -0
  26. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +14 -5
  27. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +5 -1
  28. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +3 -9
  29. package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +1 -1
  30. package/dist/cli/index.js +362 -55
  31. package/dist/cli/index.mjs +365 -57
  32. package/dist/index.d.mts +129 -1
  33. package/dist/index.d.ts +129 -1
  34. package/dist/index.js +64 -7
  35. package/dist/index.mjs +64 -7
  36. package/dist/install-integrity.json +3 -0
  37. package/dist/release.d.mts +1 -1
  38. package/dist/release.d.ts +1 -1
  39. package/dist/release.js +1 -1
  40. package/dist/release.mjs +1 -1
  41. package/package.json +1 -1
@@ -782,6 +782,8 @@ export interface ContextOptions {
782
782
  runtimeTestFaultHeader?: string | null;
783
783
  /** Runner-owned graceful deadline: stop new rows without aborting commits. */
784
784
  rowAdmissionSignal?: AbortSignal;
785
+ /** Local runner cancellation; never serialized into a Play artifact. */
786
+ executionSignal?: AbortSignal;
785
787
  /**
786
788
  * Runtime-sheet transport selected by the runner. Daytona sandboxes use the
787
789
  * execution gateway and must never fall back to minting direct DB sessions.
@@ -823,7 +825,8 @@ export interface ContextOptions {
823
825
  * Run-level cache policy.
824
826
  * forceStepRefresh bypasses completed ctx.step receipts while preserving
825
827
  * completed provider-call receipts.
826
- * forceToolRefresh bypasses all ctx.tools.execute receipts.
828
+ * forceToolRefresh refreshes completed ctx.tools.execute receipts from prior
829
+ * runs; a resumed attempt of the current run reuses its completed receipts.
827
830
  * forceFailedToolRefresh reclaims only failed receipts, preserving completed
828
831
  * provider-call idempotency while allowing forced repair runs to make progress.
829
832
  */
@@ -1168,6 +1171,13 @@ export interface PlayCheckpoint {
1168
1171
  * staleAfterSeconds receipt buckets use this value across durable resumes.
1169
1172
  */
1170
1173
  durableCallCacheEpochMs?: number;
1174
+ /** Retry identity for a provider-owned 429 while its map row is suspended. */
1175
+ providerRetryCursors?: Record<
1176
+ string,
1177
+ { invocationAttempt: number; retryFromInvocationAttempt: number }
1178
+ >;
1179
+ /** First provider hold per credential scope, retained across durable resumes. */
1180
+ providerNoProgressStartedAtMsByScope?: Record<string, number>;
1171
1181
  /** Waterfall batches that have completed: key = `${toolName}:${provider}`, value = results array. */
1172
1182
  completedBatches: Record<string, BatchResult[]>;
1173
1183
  /** Tool call batches that have completed: key = toolId, value = sparse row-cache-key -> result map. */
@@ -333,7 +333,9 @@ export async function executeWithDurableInvocationFence(input: {
333
333
  });
334
334
  } catch (error) {
335
335
  diagnose('completion_failed', error);
336
- if (!input.replaySafeAfterDispatch) throw error;
336
+ // A lost database reply does not mean the response commit failed. Read
337
+ // the exact winner even for billed tools before failing closed; this
338
+ // never repeats the provider side effect.
337
339
  try {
338
340
  const winner = await input.store.get({
339
341
  invocationKey: input.invocationKey,
@@ -352,8 +354,10 @@ export async function executeWithDurableInvocationFence(input: {
352
354
  }
353
355
  }
354
356
  } catch {
355
- // Fall through to the fixture-safe conditional cleanup below.
357
+ // Recovery is best effort. The original commit error remains the
358
+ // authority when the winner cannot be proved.
356
359
  }
360
+ if (!input.replaySafeAfterDispatch) throw error;
357
361
  return await retryReplaySafeInvocation(begun);
358
362
  }
359
363
  if (!completed) {
@@ -4,6 +4,7 @@ export const FIXTURE_BEHAVIOR_REPLAY_VERSION = 3 as const;
4
4
  export const MAX_FIXTURE_RESPONSE_DELAY_SAMPLES = 256;
5
5
  export const MAX_FIXTURE_RESPONSE_DELAY_MS = 8 * 60_000;
6
6
  export const MAX_FIXTURE_RESPONSE_ERROR_MESSAGE_LENGTH = 500;
7
+ export const MAX_FIXTURE_RETRY_AFTER_MS = 60 * 60_000;
7
8
 
8
9
  export type FixtureBehaviorV1 = {
9
10
  version: typeof FIXTURE_BEHAVIOR_VERSION;
@@ -19,12 +20,42 @@ export type FixtureResponseSample = {
19
20
  httpError?: {
20
21
  status: number;
21
22
  message: string;
23
+ /** Omitted preserves the historical one-second 429 response. */
24
+ retryAfterMs?: number;
25
+ retryAfterFormat?: 'delta_seconds' | 'http_date';
26
+ /** Omitted preserves a permanent configured HTTP error. */
27
+ failuresBeforeSuccess?: number;
22
28
  };
23
29
  };
24
30
 
25
31
  export type FixtureBehaviorV2 = {
26
32
  version: typeof FIXTURE_BEHAVIOR_RESPONSE_VERSION;
27
33
  responseSamples: FixtureResponseSample[];
34
+ transient429?: FixtureTransient429;
35
+ sharedRateLimit?: FixtureSharedRateLimit;
36
+ };
37
+
38
+ /** Synthetic provider quotas shared by all calls in the fixture workspace. */
39
+ export type FixtureSharedRateLimit = {
40
+ requestsPerSecond?: number;
41
+ requestsPerMinute?: number;
42
+ requestsPerHour?: number;
43
+ /** Test-only target for the next hourly boundary, anchored by the first DB quota attempt. */
44
+ requestsPerHourBoundaryAfterMs?: number;
45
+ };
46
+
47
+ export const FIXTURE_RATE_WINDOWS = [
48
+ { field: 'requestsPerSecond', name: 'rps', windowMs: 1_000 },
49
+ { field: 'requestsPerMinute', name: 'rpm', windowMs: 60_000 },
50
+ { field: 'requestsPerHour', name: 'rph', windowMs: 3_600_000 },
51
+ ] as const;
52
+
53
+ export type FixtureTransient429 = {
54
+ /** Select one of every N stable tool-call identities. */
55
+ everyNthCall: number;
56
+ retryAfterMs: number;
57
+ failuresBeforeSuccess: number;
58
+ when?: { provider?: string; operation?: string };
28
59
  };
29
60
 
30
61
  export type FixtureReplayBundle = {
@@ -37,6 +68,8 @@ export type FixtureBehaviorV3 = {
37
68
  version: typeof FIXTURE_BEHAVIOR_REPLAY_VERSION;
38
69
  responseSamples: FixtureResponseSample[];
39
70
  replayBundle: FixtureReplayBundle;
71
+ transient429?: FixtureTransient429;
72
+ sharedRateLimit?: FixtureSharedRateLimit;
40
73
  };
41
74
 
42
75
  export type FixtureBehavior =
@@ -79,6 +112,8 @@ export function validateFixtureBehavior(
79
112
  ? new Set([
80
113
  'version',
81
114
  'responseSamples',
115
+ 'transient429',
116
+ 'sharedRateLimit',
82
117
  ...(record.version === FIXTURE_BEHAVIOR_REPLAY_VERSION
83
118
  ? ['replayBundle']
84
119
  : []),
@@ -221,7 +256,12 @@ export function validateFixtureBehavior(
221
256
  }
222
257
  const error = sample.httpError as Record<string, unknown>;
223
258
  const errorUnknownKeys = Object.keys(error).filter(
224
- (key) => key !== 'status' && key !== 'message',
259
+ (key) =>
260
+ key !== 'status' &&
261
+ key !== 'message' &&
262
+ key !== 'retryAfterMs' &&
263
+ key !== 'retryAfterFormat' &&
264
+ key !== 'failuresBeforeSuccess',
225
265
  );
226
266
  if (errorUnknownKeys.length > 0) {
227
267
  return {
@@ -256,7 +296,66 @@ export function validateFixtureBehavior(
256
296
  `must contain 1-${MAX_FIXTURE_RESPONSE_ERROR_MESSAGE_LENGTH} characters.`,
257
297
  };
258
298
  }
259
- httpError = { status: error.status, message: error.message.trim() };
299
+ if (
300
+ error.retryAfterMs !== undefined &&
301
+ (error.status !== 429 ||
302
+ typeof error.retryAfterMs !== 'number' ||
303
+ !Number.isSafeInteger(error.retryAfterMs) ||
304
+ error.retryAfterMs < 1 ||
305
+ error.retryAfterMs > MAX_FIXTURE_RETRY_AFTER_MS)
306
+ ) {
307
+ return {
308
+ ok: false,
309
+ error:
310
+ `fixtureBehavior.responseSamples[${index}].httpError.retryAfterMs ` +
311
+ `requires status 429 and an integer from 1 through ${MAX_FIXTURE_RETRY_AFTER_MS}.`,
312
+ };
313
+ }
314
+ if (
315
+ error.retryAfterFormat !== undefined &&
316
+ (error.status !== 429 ||
317
+ (error.retryAfterFormat !== 'delta_seconds' &&
318
+ error.retryAfterFormat !== 'http_date'))
319
+ ) {
320
+ return {
321
+ ok: false,
322
+ error:
323
+ `fixtureBehavior.responseSamples[${index}].httpError.retryAfterFormat ` +
324
+ 'requires status 429 and delta_seconds or http_date.',
325
+ };
326
+ }
327
+ if (
328
+ error.failuresBeforeSuccess !== undefined &&
329
+ (error.status !== 429 ||
330
+ typeof error.failuresBeforeSuccess !== 'number' ||
331
+ !Number.isSafeInteger(error.failuresBeforeSuccess) ||
332
+ error.failuresBeforeSuccess < 1 ||
333
+ error.failuresBeforeSuccess > 20)
334
+ ) {
335
+ return {
336
+ ok: false,
337
+ error:
338
+ `fixtureBehavior.responseSamples[${index}].httpError.failuresBeforeSuccess ` +
339
+ 'requires status 429 and an integer from 1 through 20.',
340
+ };
341
+ }
342
+ httpError = {
343
+ status: error.status,
344
+ message: error.message.trim(),
345
+ ...(error.retryAfterMs !== undefined
346
+ ? { retryAfterMs: error.retryAfterMs as number }
347
+ : {}),
348
+ ...(error.retryAfterFormat !== undefined
349
+ ? {
350
+ retryAfterFormat: error.retryAfterFormat as
351
+ | 'delta_seconds'
352
+ | 'http_date',
353
+ }
354
+ : {}),
355
+ ...(error.failuresBeforeSuccess !== undefined
356
+ ? { failuresBeforeSuccess: error.failuresBeforeSuccess as number }
357
+ : {}),
358
+ };
260
359
  }
261
360
  samples.push({
262
361
  delayMs: sample.delayMs,
@@ -264,6 +363,144 @@ export function validateFixtureBehavior(
264
363
  ...(httpError ? { httpError } : {}),
265
364
  });
266
365
  }
366
+ let sharedRateLimit: FixtureSharedRateLimit | undefined;
367
+ if (record.sharedRateLimit !== undefined) {
368
+ const plan = record.sharedRateLimit;
369
+ if (!plan || typeof plan !== 'object' || Array.isArray(plan)) {
370
+ return {
371
+ ok: false,
372
+ error: 'fixtureBehavior.sharedRateLimit must be an object.',
373
+ };
374
+ }
375
+ const planRecord = plan as Record<string, unknown>;
376
+ const entries = Object.entries(planRecord);
377
+ const boundaryAfterMs = planRecord.requestsPerHourBoundaryAfterMs;
378
+ const quotaEntries = entries.filter(
379
+ ([name]) => name !== 'requestsPerHourBoundaryAfterMs',
380
+ );
381
+ if (
382
+ quotaEntries.length === 0 ||
383
+ quotaEntries.some(
384
+ ([name, limit]) =>
385
+ !FIXTURE_RATE_WINDOWS.some((window) => window.field === name) ||
386
+ typeof limit !== 'number' ||
387
+ !Number.isSafeInteger(limit) ||
388
+ limit < 1,
389
+ )
390
+ ) {
391
+ return {
392
+ ok: false,
393
+ error:
394
+ 'fixtureBehavior.sharedRateLimit requires positive integer requestsPerSecond, requestsPerMinute, or requestsPerHour quotas.',
395
+ };
396
+ }
397
+ if (
398
+ boundaryAfterMs !== undefined &&
399
+ (planRecord.requestsPerHour === undefined ||
400
+ typeof boundaryAfterMs !== 'number' ||
401
+ !Number.isSafeInteger(boundaryAfterMs) ||
402
+ boundaryAfterMs < 1_000 ||
403
+ boundaryAfterMs > 300_000)
404
+ ) {
405
+ return {
406
+ ok: false,
407
+ error:
408
+ 'fixtureBehavior.sharedRateLimit.requestsPerHourBoundaryAfterMs requires requestsPerHour and an integer from 1000 through 300000.',
409
+ };
410
+ }
411
+ sharedRateLimit = Object.fromEntries(entries) as FixtureSharedRateLimit;
412
+ }
413
+ let transient429: FixtureTransient429 | undefined;
414
+ if (record.transient429 !== undefined) {
415
+ const selected = record.transient429;
416
+ if (
417
+ !selected ||
418
+ typeof selected !== 'object' ||
419
+ Array.isArray(selected)
420
+ ) {
421
+ return {
422
+ ok: false,
423
+ error: 'fixtureBehavior.transient429 must be an object.',
424
+ };
425
+ }
426
+ const fault = selected as Record<string, unknown>;
427
+ const unknownFaultKeys = Object.keys(fault).filter(
428
+ (key) =>
429
+ key !== 'everyNthCall' &&
430
+ key !== 'retryAfterMs' &&
431
+ key !== 'failuresBeforeSuccess' &&
432
+ key !== 'when',
433
+ );
434
+ if (unknownFaultKeys.length > 0) {
435
+ return {
436
+ ok: false,
437
+ error: `Unsupported fixtureBehavior.transient429 field "${unknownFaultKeys[0]}".`,
438
+ };
439
+ }
440
+ for (const [name, value, max] of [
441
+ ['everyNthCall', fault.everyNthCall, 10_000],
442
+ ['retryAfterMs', fault.retryAfterMs, MAX_FIXTURE_RETRY_AFTER_MS],
443
+ ['failuresBeforeSuccess', fault.failuresBeforeSuccess, 20],
444
+ ] as const) {
445
+ if (
446
+ typeof value !== 'number' ||
447
+ !Number.isSafeInteger(value) ||
448
+ value < 1 ||
449
+ value > max
450
+ ) {
451
+ return {
452
+ ok: false,
453
+ error: `fixtureBehavior.transient429.${name} must be an integer from 1 through ${max}.`,
454
+ };
455
+ }
456
+ }
457
+ let when: FixtureTransient429['when'];
458
+ if (fault.when !== undefined) {
459
+ if (
460
+ !fault.when ||
461
+ typeof fault.when !== 'object' ||
462
+ Array.isArray(fault.when)
463
+ ) {
464
+ return {
465
+ ok: false,
466
+ error: 'fixtureBehavior.transient429.when must be an object.',
467
+ };
468
+ }
469
+ const condition = fault.when as Record<string, unknown>;
470
+ const unknownConditionKeys = Object.keys(condition).filter(
471
+ (key) => key !== 'provider' && key !== 'operation',
472
+ );
473
+ if (unknownConditionKeys.length > 0) {
474
+ return {
475
+ ok: false,
476
+ error: `Unsupported fixtureBehavior.transient429.when field "${unknownConditionKeys[0]}".`,
477
+ };
478
+ }
479
+ const provider = normalizeFixtureScopeValue(condition.provider, 100);
480
+ const operation = normalizeFixtureScopeValue(condition.operation, 200);
481
+ if (
482
+ provider.error ||
483
+ operation.error ||
484
+ (!provider.value && !operation.value)
485
+ ) {
486
+ return {
487
+ ok: false,
488
+ error:
489
+ 'fixtureBehavior.transient429.when must select a valid provider or operation.',
490
+ };
491
+ }
492
+ when = {
493
+ ...(provider.value ? { provider: provider.value } : {}),
494
+ ...(operation.value ? { operation: operation.value } : {}),
495
+ };
496
+ }
497
+ transient429 = {
498
+ everyNthCall: fault.everyNthCall as number,
499
+ retryAfterMs: fault.retryAfterMs as number,
500
+ failuresBeforeSuccess: fault.failuresBeforeSuccess as number,
501
+ ...(when ? { when } : {}),
502
+ };
503
+ }
267
504
  let replayBundle: FixtureReplayBundle | undefined;
268
505
  if (record.version === FIXTURE_BEHAVIOR_REPLAY_VERSION) {
269
506
  if (
@@ -355,6 +592,8 @@ export function validateFixtureBehavior(
355
592
  behavior: {
356
593
  version: FIXTURE_BEHAVIOR_REPLAY_VERSION,
357
594
  responseSamples: samples,
595
+ ...(transient429 ? { transient429 } : {}),
596
+ ...(sharedRateLimit ? { sharedRateLimit } : {}),
358
597
  replayBundle: replayBundle!,
359
598
  },
360
599
  };
@@ -364,6 +603,8 @@ export function validateFixtureBehavior(
364
603
  behavior: {
365
604
  version: FIXTURE_BEHAVIOR_RESPONSE_VERSION,
366
605
  responseSamples: samples,
606
+ ...(transient429 ? { transient429 } : {}),
607
+ ...(sharedRateLimit ? { sharedRateLimit } : {}),
367
608
  },
368
609
  };
369
610
  }
@@ -472,6 +713,164 @@ function stableFixtureRequestHash(value: string): number {
472
713
  return hash >>> 0;
473
714
  }
474
715
 
716
+ /** Durable test-only count of injected provider 429s for one Play run. */
717
+ export function fixtureTransient429RunCounterKey(input: {
718
+ orgId: string;
719
+ runId: string;
720
+ }): string {
721
+ const orgId = input.orgId.trim();
722
+ const runId = input.runId.trim();
723
+ if (!orgId || !runId) {
724
+ throw new Error('Fixture 429 run counter requires an org and run id.');
725
+ }
726
+ return `fixture-http-error-run:${orgId}:${runId}`;
727
+ }
728
+
729
+ export const FIXTURE_RUN_CALL_COUNTER_SHARDS = 32;
730
+
731
+ export function fixtureShared429RunCounterKey(input: {
732
+ orgId: string;
733
+ runId: string;
734
+ window?: 'rps' | 'rpm' | 'rph';
735
+ }): string {
736
+ if (!input.orgId.trim() || !input.runId.trim()) {
737
+ throw new Error('Fixture shared-rate counter requires an org and run id.');
738
+ }
739
+ return `fixture-shared-rate-429-run:${input.orgId.trim()}:${input.runId.trim()}:${input.window ?? 'all'}`;
740
+ }
741
+
742
+ /** The same fixed-window contract is used for seconds, minutes, and hours.
743
+ * Run identity is intentionally absent: concurrent runs share the provider's
744
+ * quota. A different configured limit is a different synthetic provider policy. */
745
+ export function fixtureSharedRateWindows(input: {
746
+ orgId: string;
747
+ provider: string;
748
+ plan: FixtureSharedRateLimit;
749
+ }) {
750
+ if (!input.orgId.trim() || !input.provider.trim()) {
751
+ throw new Error('Fixture shared rate windows require an org and provider.');
752
+ }
753
+ return FIXTURE_RATE_WINDOWS.flatMap(({ field, name, windowMs }) => {
754
+ const limit = input.plan[field];
755
+ return limit === undefined
756
+ ? []
757
+ : [
758
+ {
759
+ name,
760
+ windowMs,
761
+ limit,
762
+ boundaryAfterMs:
763
+ name === 'rph'
764
+ ? input.plan.requestsPerHourBoundaryAfterMs
765
+ : undefined,
766
+ key: `fixture-rate:${input.orgId.trim()}:${input.provider.trim()}:${name}:${limit}`,
767
+ },
768
+ ];
769
+ });
770
+ }
771
+
772
+ export type FixtureExecuteBoundaryCounts = {
773
+ started: number;
774
+ finished: number;
775
+ expectedFailures: number;
776
+ unexpectedFailures: number;
777
+ unprovenResponses: number;
778
+ };
779
+
780
+ export const FIXTURE_EXECUTE_BOUNDARY_COUNTER_KINDS = {
781
+ started: 'boundary_started',
782
+ finished: 'boundary_finished',
783
+ expectedFailures: 'boundary_expected_failures',
784
+ unexpectedFailures: 'boundary_unexpected_failures',
785
+ unprovenResponses: 'boundary_unproven_responses',
786
+ } as const;
787
+
788
+ /** Fixed-size, run-scoped test-only counters survive every scheduler resume. */
789
+ export function fixtureRunCallCounterKey(input: {
790
+ orgId: string;
791
+ runId: string;
792
+ kind:
793
+ | 'physical'
794
+ | 'logical'
795
+ | 'success_members'
796
+ | 'distinct_success_members'
797
+ | 'duplicate_success_members'
798
+ | 'unproven_successes'
799
+ | 'quota_attempts'
800
+ | 'boundary_started'
801
+ | 'boundary_finished'
802
+ | 'boundary_expected_failures'
803
+ | 'boundary_unexpected_failures'
804
+ | 'boundary_unproven_responses';
805
+ shard: number;
806
+ }): string {
807
+ const orgId = input.orgId.trim();
808
+ const runId = input.runId.trim();
809
+ if (
810
+ !orgId ||
811
+ !runId ||
812
+ !Number.isInteger(input.shard) ||
813
+ input.shard < 0 ||
814
+ input.shard >= FIXTURE_RUN_CALL_COUNTER_SHARDS
815
+ ) {
816
+ throw new Error(
817
+ 'Fixture run call counter requires an org, run, and valid shard.',
818
+ );
819
+ }
820
+ return `fixture-run-call-${input.kind}:${orgId}:${runId}:${input.shard}`;
821
+ }
822
+
823
+ /** Detail is addressable separately from the fixed-size run counter set. */
824
+ export function fixtureQuotaAttemptKey(input: {
825
+ orgId: string;
826
+ runId: string;
827
+ shard: number;
828
+ ordinal: number;
829
+ }): string {
830
+ if (!Number.isSafeInteger(input.ordinal) || input.ordinal < 1) {
831
+ throw new Error('Fixture quota attempt requires a positive safe ordinal.');
832
+ }
833
+ return `${fixtureRunCallCounterKey({ ...input, kind: 'quota_attempts' })}:attempt:${input.ordinal}`;
834
+ }
835
+
836
+ export function fixtureRunCallCounterShard(
837
+ durableCallReceiptKey: string,
838
+ ): number {
839
+ const key = durableCallReceiptKey.trim();
840
+ if (!key)
841
+ throw new Error('Fixture call counter requires a durable receipt key.');
842
+ return stableFixtureRequestHash(key) % FIXTURE_RUN_CALL_COUNTER_SHARDS;
843
+ }
844
+
845
+ export function selectFixtureTransient429(input: {
846
+ behavior: FixtureBehavior;
847
+ stableRequestKey: string;
848
+ }): FixtureTransient429 | null {
849
+ const plan =
850
+ input.behavior.version === FIXTURE_BEHAVIOR_VERSION
851
+ ? undefined
852
+ : input.behavior.transient429;
853
+ if (!plan) return null;
854
+ const stableRequestKey = input.stableRequestKey.trim().toLowerCase();
855
+ if (!stableRequestKey) {
856
+ throw new Error('Fixture 429 selection requires a stable request key.');
857
+ }
858
+ const [provider = '', operation = ''] = stableRequestKey.split(':', 3);
859
+ if (
860
+ (plan.when?.provider && plan.when.provider !== provider) ||
861
+ (plan.when?.operation && plan.when.operation !== operation)
862
+ ) {
863
+ return null;
864
+ }
865
+ return stableFixtureRequestHash(
866
+ `fixture-transient-429-v1:${stableRequestKey}`,
867
+ ) %
868
+ plan.everyNthCall ===
869
+ 0
870
+ ? plan
871
+ : null;
872
+ }
873
+
475
874
  export function selectFixtureReplaySampleIndex(input: {
476
875
  stableRequestKey: string;
477
876
  sampleCount: number;