deepline 0.1.294 → 0.1.295

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.
@@ -155,7 +155,7 @@ export const SDK_RELEASE = {
155
155
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
156
156
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
157
157
  // Operators use the checkout-local deepline-admin binary instead.
158
- version: '0.1.294',
158
+ version: '0.1.295',
159
159
  contracts: {
160
160
  api: {
161
161
  name: 'sdk-http-api',
@@ -228,13 +228,14 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
228
228
 
229
229
  const ordered = [...rules].sort((a, b) => a.ruleId.localeCompare(b.ruleId));
230
230
  const rulesKey = rulesSignature(ordered);
231
+ const laneKey = rateLaneKey(bucketId, rulesKey);
231
232
 
232
233
  // Drawing a cached permit is a synchronous, single-threaded critical
233
234
  // section (`drawFromBlock` has no `await`), so concurrent acquires can never
234
235
  // hand out the same permit — no lock is needed for the fast path. The ONLY
235
236
  // thing that needs coordination is the block refill, which we single-flight
236
- // per bucket: one network round-trip (and any window-exhausted sleep) serves
237
- // the whole waiting wave, then everyone re-draws. This replaces the old
237
+ // per bucket + rule set: one network round-trip (and any window-exhausted
238
+ // sleep) serves the whole matching wave, then everyone re-draws. This replaces the old
238
239
  // per-bucket lock that held across the RPC + up-to-5s sleep and thereby
239
240
  // serialized every map row behind one sleeping acquirer.
240
241
  while (true) {
@@ -243,7 +244,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
243
244
  ? signal.reason
244
245
  : new Error('Rate-state acquire aborted.');
245
246
  }
246
- const localPermit = this.drawFromBlock(bucketId, rulesKey);
247
+ const localPermit = this.drawFromBlock(bucketId, laneKey);
247
248
  if (localPermit) {
248
249
  const waitMs = localPermit.scheduledAtMs - this.now();
249
250
  if (waitMs > 0) await this.sleep(waitMs);
@@ -258,19 +259,20 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
258
259
  // Count this acquirer as waiting on the refill so the single-flight refill
259
260
  // can size its block to the live wave. Decremented once a block lands (or
260
261
  // the refill errors) — see the finally in `refillBlock`.
261
- this.waiters.set(bucketId, (this.waiters.get(bucketId) ?? 0) + 1);
262
+ this.waiters.set(laneKey, (this.waiters.get(laneKey) ?? 0) + 1);
262
263
  try {
263
264
  await this.refillBlock(
264
265
  bucketId,
265
266
  input.rateScopeToken?.trim() || null,
266
267
  ordered,
267
268
  rulesKey,
269
+ laneKey,
268
270
  signal,
269
271
  );
270
272
  } finally {
271
- const remaining = (this.waiters.get(bucketId) ?? 1) - 1;
272
- if (remaining > 0) this.waiters.set(bucketId, remaining);
273
- else this.waiters.delete(bucketId);
273
+ const remaining = (this.waiters.get(laneKey) ?? 1) - 1;
274
+ if (remaining > 0) this.waiters.set(laneKey, remaining);
275
+ else this.waiters.delete(laneKey);
274
276
  }
275
277
  }
276
278
  }
@@ -285,7 +287,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
285
287
  * without reserving a single slot that cannot be used.
286
288
  */
287
289
  private requestedForBucket(
288
- bucketId: string,
290
+ laneKey: string,
289
291
  rules: readonly PacingRule[],
290
292
  ): number {
291
293
  const floor = APP_RUNTIME_RATE_STATE_LEASE_BLOCK_SIZE;
@@ -294,7 +296,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
294
296
  );
295
297
  const horizonBudget = Math.max(
296
298
  1,
297
- Math.ceil(this.effectiveRps.get(bucketId) ?? configuredRps),
299
+ Math.ceil(this.effectiveRps.get(laneKey) ?? configuredRps),
298
300
  );
299
301
  const rateBudget = Math.min(
300
302
  horizonBudget,
@@ -303,7 +305,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
303
305
  .filter((value) => Number.isFinite(value) && value > 0),
304
306
  );
305
307
  if (!Number.isFinite(rateBudget) || rateBudget <= 0) return floor;
306
- const waiters = this.waiters.get(bucketId) ?? 1;
308
+ const waiters = this.waiters.get(laneKey) ?? 1;
307
309
  const concurrencyLimits = rules.flatMap((rule) =>
308
310
  rule.maxConcurrency != null &&
309
311
  Number.isFinite(rule.maxConcurrency) &&
@@ -391,22 +393,24 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
391
393
  rateScopeToken: string | null,
392
394
  ordered: PacingRule[],
393
395
  rulesKey: string,
396
+ laneKey: string,
394
397
  signal?: AbortSignal,
395
398
  ): Promise<void> {
396
- const existing = this.refills.get(bucketId);
399
+ const existing = this.refills.get(laneKey);
397
400
  if (existing) return existing;
398
401
  const promise = this.performRefill(
399
402
  bucketId,
400
403
  rateScopeToken,
401
404
  ordered,
402
405
  rulesKey,
406
+ laneKey,
403
407
  signal,
404
408
  ).finally(() => {
405
- if (this.refills.get(bucketId) === promise) {
406
- this.refills.delete(bucketId);
409
+ if (this.refills.get(laneKey) === promise) {
410
+ this.refills.delete(laneKey);
407
411
  }
408
412
  });
409
- this.refills.set(bucketId, promise);
413
+ this.refills.set(laneKey, promise);
410
414
  return promise;
411
415
  }
412
416
 
@@ -415,6 +419,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
415
419
  rateScopeToken: string | null,
416
420
  ordered: PacingRule[],
417
421
  rulesKey: string,
422
+ laneKey: string,
418
423
  signal?: AbortSignal,
419
424
  ): Promise<void> {
420
425
  let consecutiveFailures = 0;
@@ -435,24 +440,25 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
435
440
  if (signal?.aborted) return;
436
441
  }
437
442
  try {
438
- const observedSuccesses = this.pendingSuccesses.get(bucketId) ?? 0;
443
+ const successKey = pendingSuccessKey(bucketId, ordered);
444
+ const observedSuccesses = this.pendingSuccesses.get(successKey) ?? 0;
439
445
  const response = await this.port.acquire({
440
446
  bucketId,
441
447
  rateScopeToken,
442
448
  // Stamp adaptiveMaxRps on default-pacing rules so the DB row's ceiling
443
449
  // matches the old in-memory AIMD range.
444
450
  rules: withAdaptiveMaxRps(ordered),
445
- requested: this.requestedForBucket(bucketId, ordered),
451
+ requested: this.requestedForBucket(laneKey, ordered),
446
452
  observedSuccesses,
447
453
  schedulerSchema: this.schedulerSchema,
448
454
  });
449
455
  if (observedSuccesses > 0) {
450
456
  const remaining = Math.max(
451
457
  0,
452
- (this.pendingSuccesses.get(bucketId) ?? 0) - observedSuccesses,
458
+ (this.pendingSuccesses.get(successKey) ?? 0) - observedSuccesses,
453
459
  );
454
- if (remaining > 0) this.pendingSuccesses.set(bucketId, remaining);
455
- else this.pendingSuccesses.delete(bucketId);
460
+ if (remaining > 0) this.pendingSuccesses.set(successKey, remaining);
461
+ else this.pendingSuccesses.delete(successKey);
456
462
  }
457
463
  this.recordCooldown(bucketId, response);
458
464
  if (
@@ -460,7 +466,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
460
466
  Number(response.effectiveRequestsPerSecond) > 0
461
467
  ) {
462
468
  this.effectiveRps.set(
463
- bucketId,
469
+ laneKey,
464
470
  Number(response.effectiveRequestsPerSecond),
465
471
  );
466
472
  }
@@ -472,6 +478,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
472
478
  rateScopeToken,
473
479
  ordered,
474
480
  rulesKey,
481
+ laneKey,
475
482
  response,
476
483
  );
477
484
  return;
@@ -523,6 +530,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
523
530
  rateScopeToken: string | null,
524
531
  ordered: PacingRule[],
525
532
  rulesKey: string,
533
+ laneKey: string,
526
534
  response: {
527
535
  granted: number;
528
536
  leaseIds?: unknown;
@@ -579,6 +587,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
579
587
  hasConcurrency ? (leaseIds[index] ?? null) : null,
580
588
  ]),
581
589
  rulesKey,
590
+ laneKey,
582
591
  ordered,
583
592
  );
584
593
  }
@@ -589,7 +598,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
589
598
  cooldownMs: number;
590
599
  }): Promise<void> {
591
600
  if (input.cooldownMs <= 0) return;
592
- this.drainBlock(input.bucketId);
601
+ this.drainBlocksForBucket(input.bucketId);
593
602
  try {
594
603
  await this.port.penalize({
595
604
  bucketId: input.bucketId,
@@ -609,10 +618,14 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
609
618
  }
610
619
  }
611
620
 
612
- observeSuccess(input: { bucketId: string }): void {
621
+ observeSuccess(input: {
622
+ bucketId: string;
623
+ rules: readonly PacingRule[];
624
+ }): void {
625
+ const key = pendingSuccessKey(input.bucketId, input.rules);
613
626
  this.pendingSuccesses.set(
614
- input.bucketId,
615
- Math.min(1_000_000, (this.pendingSuccesses.get(input.bucketId) ?? 0) + 1),
627
+ key,
628
+ Math.min(1_000_000, (this.pendingSuccesses.get(key) ?? 0) + 1),
616
629
  );
617
630
  }
618
631
 
@@ -645,12 +658,13 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
645
658
  rateScopeToken: string | null,
646
659
  permits: LeasedBlock['permits'],
647
660
  rulesKey: string,
661
+ laneKey: string,
648
662
  rules: PacingRule[],
649
663
  ): void {
650
664
  const lastScheduledAtMs = permits.at(-1)?.[0] ?? this.now();
651
665
  const freshExpiresAt =
652
666
  Math.max(this.now(), lastScheduledAtMs) + leasedBlockTtlMs(rules);
653
- const existing = this.blocks.get(bucketId);
667
+ const existing = this.blocks.get(laneKey);
654
668
  if (
655
669
  existing &&
656
670
  existing.rulesKey === rulesKey &&
@@ -668,7 +682,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
668
682
  existing.rules,
669
683
  existing.permits.flatMap(([, leaseId]) => (leaseId ? [leaseId] : [])),
670
684
  );
671
- this.blocks.set(bucketId, {
685
+ this.blocks.set(laneKey, {
672
686
  permits,
673
687
  rateScopeToken,
674
688
  expiresAt: freshExpiresAt,
@@ -679,12 +693,12 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
679
693
 
680
694
  private drawFromBlock(
681
695
  bucketId: string,
682
- rulesKey: string,
696
+ laneKey: string,
683
697
  ): { permit: PacingPermit; scheduledAtMs: number } | null {
684
- const block = this.blocks.get(bucketId);
698
+ const block = this.blocks.get(laneKey);
685
699
  if (!block) return null;
686
- if (block.rulesKey !== rulesKey || block.expiresAt <= this.now()) {
687
- this.blocks.delete(bucketId);
700
+ if (block.expiresAt <= this.now()) {
701
+ this.blocks.delete(laneKey);
688
702
  // Unused window permits are forfeited on TTL expiry (the server already
689
703
  // decremented the provider window for them, and the rule-scoped TTL is
690
704
  // capped so at most one block's worth is stranded per bucket/window).
@@ -699,10 +713,10 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
699
713
  }
700
714
  const scheduled = block.permits.shift();
701
715
  if (!scheduled) {
702
- this.blocks.delete(bucketId);
716
+ this.blocks.delete(laneKey);
703
717
  return null;
704
718
  }
705
- if (block.permits.length === 0) this.blocks.delete(bucketId);
719
+ if (block.permits.length === 0) this.blocks.delete(laneKey);
706
720
  return {
707
721
  permit: this.permitFor(
708
722
  bucketId,
@@ -714,16 +728,18 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
714
728
  };
715
729
  }
716
730
 
717
- private drainBlock(bucketId: string): void {
718
- const block = this.blocks.get(bucketId);
719
- if (!block) return;
720
- this.blocks.delete(bucketId);
721
- this.releaseReserved(
722
- bucketId,
723
- block.rateScopeToken,
724
- block.rules,
725
- block.permits.flatMap(([, leaseId]) => (leaseId ? [leaseId] : [])),
726
- );
731
+ private drainBlocksForBucket(bucketId: string): void {
732
+ const prefix = `${bucketId}\0`;
733
+ for (const [laneKey, block] of this.blocks) {
734
+ if (!laneKey.startsWith(prefix)) continue;
735
+ this.blocks.delete(laneKey);
736
+ this.releaseReserved(
737
+ bucketId,
738
+ block.rateScopeToken,
739
+ block.rules,
740
+ block.permits.flatMap(([, leaseId]) => (leaseId ? [leaseId] : [])),
741
+ );
742
+ }
727
743
  }
728
744
 
729
745
  private releaseReserved(
@@ -814,20 +830,22 @@ function isDefaultPacingRuleId(ruleId: string): boolean {
814
830
  }
815
831
 
816
832
  /**
817
- * Stamp `adaptiveMaxRps` onto default-pacing rules so the DB row's ceiling matches
818
- * the old in-memory AIMD range (base * 25). Declared rules pass through untouched
819
- * (the DB pins them at base). The field is extra on the wire; the server's
820
- * `normalizeRules` reads it, everyone else ignores it.
833
+ * Preserve explicit advisory ceilings and stamp the legacy base * 25 ceiling
834
+ * only onto undeclared default-pacing rules. Declared rules without a ceiling
835
+ * remain pinned at base.
821
836
  */
822
837
  function withAdaptiveMaxRps(rules: readonly PacingRule[]): PacingRule[] {
823
838
  return rules.map((rule) =>
824
- isDefaultPacingRuleId(rule.ruleId)
825
- ? ({
826
- ...rule,
827
- adaptiveMaxRps:
828
- rule.requestsPerWindow * DEFAULT_PACING_ADAPTIVE_MAX_RPS_MULTIPLIER,
829
- } as PacingRule)
830
- : rule,
839
+ rule.adaptiveMaxRps != null
840
+ ? rule
841
+ : isDefaultPacingRuleId(rule.ruleId)
842
+ ? ({
843
+ ...rule,
844
+ adaptiveMaxRps:
845
+ rule.requestsPerWindow *
846
+ DEFAULT_PACING_ADAPTIVE_MAX_RPS_MULTIPLIER,
847
+ } as PacingRule)
848
+ : rule,
831
849
  );
832
850
  }
833
851
 
@@ -835,12 +853,23 @@ function rulesSignature(rules: readonly PacingRule[]): string {
835
853
  return [...rules]
836
854
  .map(
837
855
  (rule) =>
838
- `${rule.ruleId}:${rule.requestsPerWindow}:${rule.windowMs}:${rule.maxConcurrency ?? ''}`,
856
+ `${rule.ruleId}:${rule.requestsPerWindow}:${rule.windowMs}:${rule.adaptiveMaxRps ?? ''}:${rule.maxConcurrency ?? ''}`,
839
857
  )
840
858
  .sort()
841
859
  .join('|');
842
860
  }
843
861
 
862
+ function rateLaneKey(bucketId: string, rulesKey: string): string {
863
+ return `${bucketId}\0${rulesKey}`;
864
+ }
865
+
866
+ function pendingSuccessKey(
867
+ bucketId: string,
868
+ rules: readonly PacingRule[],
869
+ ): string {
870
+ return rateLaneKey(bucketId, rulesSignature(rules));
871
+ }
872
+
844
873
  function leasedBlockTtlMs(rules: readonly PacingRule[]): number {
845
874
  const shortestWindowMs = Math.min(
846
875
  ...rules
@@ -267,6 +267,7 @@ export function createPlayExecutionGovernor(
267
267
  });
268
268
  const budgetState = input.budgetState ?? new InMemoryBudgetStateBackend();
269
269
  const providerByTool = new Map<string, string>();
270
+ const pacingRulesByTool = new Map<string, PacingRule[]>();
270
271
  const scopeByTool = new Map<string, [string, string]>();
271
272
  const providerConcurrencySlotsByTool = new Map<string, Semaphore>();
272
273
 
@@ -308,6 +309,7 @@ export function createPlayExecutionGovernor(
308
309
  // row is authoritative.
309
310
  const pacing = declaredPacing ?? defaultPacingForTool(toolId, policy);
310
311
  providerByTool.set(toolId, pacing.provider);
312
+ pacingRulesByTool.set(toolId, pacing.rules);
311
313
  return pacing;
312
314
  }
313
315
  const pacing = adaptiveAdmission.resolvePacing({
@@ -317,6 +319,7 @@ export function createPlayExecutionGovernor(
317
319
  fallbackPacing: defaultPacingForTool(toolId, policy),
318
320
  });
319
321
  providerByTool.set(toolId, pacing.provider);
322
+ pacingRulesByTool.set(toolId, pacing.rules);
320
323
  return pacing;
321
324
  }
322
325
 
@@ -486,9 +489,13 @@ export function createPlayExecutionGovernor(
486
489
 
487
490
  observeProviderSuccess(success) {
488
491
  if (dbAuthoritativePacing) {
492
+ if (!success.toolId) return;
489
493
  const rateScope = scopeFor(success.toolId, success.provider);
494
+ const rules = pacingRulesByTool.get(success.toolId);
495
+ if (!rules) return;
490
496
  input.rateState.observeSuccess?.({
491
497
  bucketId: rateScope[0],
498
+ rules,
492
499
  });
493
500
  return;
494
501
  }
@@ -11,6 +11,8 @@ export interface PacingRule {
11
11
  readonly ruleId: string;
12
12
  readonly requestsPerWindow: number;
13
13
  readonly windowMs: number;
14
+ /** Optional learned-rate ceiling in requests per second. */
15
+ readonly adaptiveMaxRps?: number | null;
14
16
  /**
15
17
  * Optional simultaneous-in-flight cap for this rule.
16
18
  *
@@ -35,6 +37,7 @@ export interface PlayQueueHint {
35
37
  ruleId: string;
36
38
  requestsPerWindow: number;
37
39
  windowMs: number;
40
+ adaptiveMaxRps?: number | null;
38
41
  maxConcurrency: number | null;
39
42
  }
40
43
 
@@ -88,6 +91,7 @@ export interface RateStateBackend {
88
91
  observeSuccess?(input: {
89
92
  bucketId: string;
90
93
  rateScopeToken?: string | null;
94
+ rules: readonly PacingRule[];
91
95
  }): void;
92
96
  }
93
97
 
@@ -8,7 +8,12 @@ export type ResolvedPacingPolicy = {
8
8
 
9
9
  type PacingPolicyQueueHint = Pick<
10
10
  PlayQueueHint,
11
- 'provider' | 'ruleId' | 'requestsPerWindow' | 'windowMs' | 'maxConcurrency'
11
+ | 'provider'
12
+ | 'ruleId'
13
+ | 'requestsPerWindow'
14
+ | 'windowMs'
15
+ | 'adaptiveMaxRps'
16
+ | 'maxConcurrency'
12
17
  >;
13
18
 
14
19
  function isFinitePositiveNumber(value: unknown): value is number {
@@ -35,6 +40,9 @@ export function pacingPolicyFromQueueHints(
35
40
  ruleId: hint.ruleId,
36
41
  requestsPerWindow: hint.requestsPerWindow,
37
42
  windowMs: hint.windowMs,
43
+ adaptiveMaxRps: isFinitePositiveNumber(hint.adaptiveMaxRps)
44
+ ? hint.adaptiveMaxRps
45
+ : null,
38
46
  maxConcurrency:
39
47
  typeof hint.maxConcurrency === 'number' &&
40
48
  Number.isFinite(hint.maxConcurrency) &&
@@ -65,6 +73,8 @@ export function pacingPolicyFromUnknownQueueHints(
65
73
  : Number.NaN,
66
74
  windowMs:
67
75
  typeof hint.windowMs === 'number' ? hint.windowMs : Number.NaN,
76
+ adaptiveMaxRps:
77
+ typeof hint.adaptiveMaxRps === 'number' ? hint.adaptiveMaxRps : null,
68
78
  maxConcurrency:
69
79
  typeof hint.maxConcurrency === 'number' ? hint.maxConcurrency : null,
70
80
  })),
package/dist/cli/index.js CHANGED
@@ -718,7 +718,7 @@ var SDK_RELEASE = {
718
718
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
719
719
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
720
720
  // Operators use the checkout-local deepline-admin binary instead.
721
- version: "0.1.294",
721
+ version: "0.1.295",
722
722
  contracts: {
723
723
  api: {
724
724
  name: "sdk-http-api",
@@ -703,7 +703,7 @@ var SDK_RELEASE = {
703
703
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
704
704
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
705
705
  // Operators use the checkout-local deepline-admin binary instead.
706
- version: "0.1.294",
706
+ version: "0.1.295",
707
707
  contracts: {
708
708
  api: {
709
709
  name: "sdk-http-api",
package/dist/index.js CHANGED
@@ -438,7 +438,7 @@ var SDK_RELEASE = {
438
438
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
439
439
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
440
440
  // Operators use the checkout-local deepline-admin binary instead.
441
- version: "0.1.294",
441
+ version: "0.1.295",
442
442
  contracts: {
443
443
  api: {
444
444
  name: "sdk-http-api",
package/dist/index.mjs CHANGED
@@ -367,7 +367,7 @@ var SDK_RELEASE = {
367
367
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
368
368
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
369
369
  // Operators use the checkout-local deepline-admin binary instead.
370
- version: "0.1.294",
370
+ version: "0.1.295",
371
371
  contracts: {
372
372
  api: {
373
373
  name: "sdk-http-api",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.1.294",
3
+ "version": "0.1.295",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {