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
@@ -307,7 +307,7 @@ type BreakerState = {
307
307
  consecutiveFailures: number;
308
308
  openedAt: number | null;
309
309
  lastFailureMessage: string;
310
- halfOpenInFlight: boolean;
310
+ halfOpenProbe: symbol | null;
311
311
  };
312
312
 
313
313
  export type RuntimeSchedulerPoolPriority = 'ordinary' | 'critical';
@@ -645,6 +645,9 @@ function getPoolEntry(options: RuntimeSchedulerPoolOptions): PoolEntry {
645
645
  maxAcquireQueueDepth,
646
646
  maxAcquireQueueWaitMs,
647
647
  );
648
+ // A pool created by the old breaker used a boolean claim. The new token
649
+ // does not inherit a stranded half-open claim across a hot reload.
650
+ existing.breaker.halfOpenProbe ??= null;
648
651
  return existing;
649
652
  }
650
653
 
@@ -671,7 +674,7 @@ function getPoolEntry(options: RuntimeSchedulerPoolOptions): PoolEntry {
671
674
  consecutiveFailures: 0,
672
675
  openedAt: null,
673
676
  lastFailureMessage: '',
674
- halfOpenInFlight: false,
677
+ halfOpenProbe: null,
675
678
  },
676
679
  stats: {
677
680
  acquires: 0,
@@ -698,31 +701,39 @@ function getPoolEntry(options: RuntimeSchedulerPoolOptions): PoolEntry {
698
701
  return entry;
699
702
  }
700
703
 
701
- function breakerGate(entry: PoolEntry): void {
704
+ function breakerGate(entry: PoolEntry): symbol | null {
702
705
  const { breaker } = entry;
703
- if (breaker.openedAt === null) return;
706
+ if (breaker.openedAt === null) return null;
704
707
  const elapsed = Date.now() - breaker.openedAt;
705
708
  if (elapsed < RUNTIME_SCHEDULER_POOL_DEFAULTS.breaker.openMs) {
706
709
  entry.stats.breakerRejections += 1;
707
710
  throw new RuntimeSchedulerCircuitOpenError(breaker.lastFailureMessage);
708
711
  }
709
712
  // Half-open: let exactly one caller through as the trial.
710
- if (breaker.halfOpenInFlight) {
713
+ if (breaker.halfOpenProbe !== null) {
711
714
  entry.stats.breakerRejections += 1;
712
715
  throw new RuntimeSchedulerCircuitOpenError(breaker.lastFailureMessage);
713
716
  }
714
- breaker.halfOpenInFlight = true;
717
+ const probe = Symbol('runtime-scheduler-half-open-probe');
718
+ breaker.halfOpenProbe = probe;
719
+ return probe;
715
720
  }
716
721
 
717
722
  function recordBreakerSuccess(entry: PoolEntry): void {
718
723
  entry.breaker.consecutiveFailures = 0;
719
724
  entry.breaker.openedAt = null;
720
- entry.breaker.halfOpenInFlight = false;
725
+ entry.breaker.halfOpenProbe = null;
721
726
  }
722
727
 
723
- function recordBreakerFailure(entry: PoolEntry, error: unknown): void {
728
+ function recordBreakerFailure(
729
+ entry: PoolEntry,
730
+ error: unknown,
731
+ probe: symbol | null,
732
+ ): void {
724
733
  const { breaker } = entry;
725
- breaker.halfOpenInFlight = false;
734
+ if (probe !== null && breaker.halfOpenProbe === probe) {
735
+ breaker.halfOpenProbe = null;
736
+ }
726
737
  breaker.consecutiveFailures += 1;
727
738
  breaker.lastFailureMessage =
728
739
  error instanceof Error ? error.message : String(error);
@@ -798,190 +809,46 @@ export async function withRuntimeSchedulerClient<T>(
798
809
  throwIfSchedulerAborted(signal);
799
810
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
800
811
  throwIfSchedulerAborted(signal);
801
- breakerGate(entry);
802
- if (entry.pool.waitingCount >= entry.maxAcquireQueueDepth) {
803
- entry.stats.saturationRejections += 1;
804
- throw new RuntimeSchedulerSaturatedError(entry.pool.waitingCount);
805
- }
806
- const acquireStart = performance.now();
807
- entry.stats.peakTotalClients = Math.max(
808
- entry.stats.peakTotalClients,
809
- entry.pool.totalCount,
810
- );
811
- entry.stats.peakWaitingClients = Math.max(
812
- entry.stats.peakWaitingClients,
813
- entry.pool.waitingCount,
814
- );
815
- let poolClient;
812
+ const halfOpenProbe = breakerGate(entry);
813
+ let capacityObserved = false;
816
814
  try {
817
- poolClient = await entry.pool.connect();
818
- } catch (error) {
819
- if (isRuntimeSchedulerLocalAcquireTimeout(error)) {
815
+ if (entry.pool.waitingCount >= entry.maxAcquireQueueDepth) {
820
816
  entry.stats.saturationRejections += 1;
821
- console.warn('[runtime-scheduler-pool.local_acquire_saturated]', {
822
- maxClients: entry.maxClients,
823
- totalClients: entry.pool.totalCount,
824
- idleClients: entry.pool.idleCount,
825
- waitingClients: entry.pool.waitingCount,
826
- });
827
817
  throw new RuntimeSchedulerSaturatedError(entry.pool.waitingCount);
828
818
  }
829
- if (isPgBouncerCapacityError(error)) {
830
- recordCapacityRejection(entry, error);
831
- throw new RuntimeSchedulerPgBouncerCapacityError(
832
- error instanceof Error ? error.message : String(error),
833
- );
834
- }
835
- recordBreakerFailure(entry, error);
836
- entry.stats.connectionErrors += 1;
837
- lastError = error;
838
- if (attempt + 1 < maxAttempts) {
839
- entry.stats.retries += 1;
840
- await sleep(jitteredBackoffMs(attempt));
841
- throwIfSchedulerAborted(signal);
842
- continue;
843
- }
844
- throw error;
845
- }
846
- entry.stats.acquires += 1;
847
- const acquireWaitMs = performance.now() - acquireStart;
848
- entry.stats.peakTotalClients = Math.max(
849
- entry.stats.peakTotalClients,
850
- entry.pool.totalCount,
851
- );
852
- pushSample(entry.stats.acquireWait, acquireWaitMs);
853
-
854
- // A checked-out pg client emits 'error' (not via the pool) when its TCP
855
- // connection dies while idle — e.g. a worker holding a client across a
856
- // long play execution. With no listener that is an uncaught exception
857
- // that kills the process (observed: worker crash mid-150k-row run).
858
- // Swallow it here; the next query on the dead client fails normally and
859
- // flows through the connection-error retry/poison path below.
860
- let checkedOutError: unknown = null;
861
- const onCheckedOutError = (error: Error) => {
862
- checkedOutError = error;
863
- entry.stats.connectionErrors += 1;
864
- console.warn('[runtime-scheduler-pool.checked_out_client_error]', {
865
- error: error.message,
866
- });
867
- };
868
- poolClient.on('error', onCheckedOutError);
869
- let clientReleased = false;
870
- const releasePoolClient = (error?: Error) => {
871
- if (clientReleased) return;
872
- clientReleased = true;
873
- poolClient.release(error);
874
- };
875
- const onAbort = () => releasePoolClient(schedulerAbortReason(signal!));
876
- signal?.addEventListener('abort', onAbort, { once: true });
877
-
878
- let queryCount = 0;
879
- let queryMs = 0;
880
- try {
881
- throwIfSchedulerAborted(signal);
882
- const result = await fn({
883
- query: async (sql, params) => {
884
- throwIfSchedulerAborted(signal);
885
- const queryStartedAt = performance.now();
886
- queryCount += 1;
887
- try {
888
- try {
889
- const queryResult = await poolClient.query(sql, params);
890
- throwIfSchedulerAborted(signal);
891
- return { rows: queryResult.rows };
892
- } catch (error) {
893
- // node-postgres' query timeout and an AbortSignal reject the
894
- // promise, but the server may still be processing the SQL.
895
- // Never return that socket to the pool: doing so lets the
896
- // next retry overlap the first statement on the same backend.
897
- if (isRuntimeSchedulerQueryCancellationError(error)) {
898
- releasePoolClient(
899
- error instanceof Error ? error : new Error(String(error)),
900
- );
901
- }
902
- throw error;
903
- }
904
- } finally {
905
- queryMs += performance.now() - queryStartedAt;
906
- }
907
- },
908
- });
909
- throwIfSchedulerAborted(signal);
910
- recordBreakerSuccess(entry);
911
- signal?.removeEventListener('abort', onAbort);
912
- poolClient.removeListener('error', onCheckedOutError);
913
- // The connection died mid-fn but fn happened to finish without another
914
- // query — destroy the dead connection rather than pooling it.
915
- releasePoolClient(
916
- checkedOutError instanceof Error ? checkedOutError : undefined,
819
+ const acquireStart = performance.now();
820
+ entry.stats.peakTotalClients = Math.max(
821
+ entry.stats.peakTotalClients,
822
+ entry.pool.totalCount,
917
823
  );
918
- if (trace) {
919
- console.info(
920
- '[runtime-scheduler-pool.operation] ' +
921
- JSON.stringify({
922
- runId: trace.runId,
923
- phase: trace.phase,
924
- outcome: 'success',
925
- attempt: attempt + 1,
926
- priority: trace.priority ?? 'ordinary',
927
- admissionWaitMs,
928
- acquireWaitMs,
929
- queryCount,
930
- queryMs,
931
- totalMs: performance.now() - operationStartedAt,
932
- }),
933
- );
934
- }
935
- return result;
936
- } catch (error) {
937
- if (trace) {
938
- console.info(
939
- '[runtime-scheduler-pool.operation] ' +
940
- JSON.stringify({
941
- runId: trace.runId,
942
- phase: trace.phase,
943
- outcome: 'failed',
944
- attempt: attempt + 1,
945
- priority: trace.priority ?? 'ordinary',
946
- admissionWaitMs,
947
- acquireWaitMs,
948
- queryCount,
949
- queryMs,
950
- totalMs: performance.now() - operationStartedAt,
951
- errorName: error instanceof Error ? error.name : 'UnknownError',
952
- }),
953
- );
954
- }
955
- signal?.removeEventListener('abort', onAbort);
956
- poolClient.removeListener('error', onCheckedOutError);
957
- if (signal?.aborted) {
958
- const cancellation = schedulerAbortReason(signal);
959
- releasePoolClient(cancellation);
960
- throw cancellation;
961
- }
962
- if (isPgBouncerCapacityError(error)) {
963
- recordCapacityRejection(entry, error);
964
- releasePoolClient();
965
- throw new RuntimeSchedulerPgBouncerCapacityError(
966
- error instanceof Error ? error.message : String(error),
967
- );
968
- }
969
- if (isRuntimeSchedulerQueryCancellationError(error)) {
970
- entry.stats.queryErrors += 1;
971
- recordBreakerSuccess(entry);
972
- releasePoolClient(
973
- error instanceof Error ? error : new Error(String(error)),
974
- );
975
- throw error;
976
- }
977
- if (isConnectionCategoryError(error)) {
978
- recordBreakerFailure(entry, error);
824
+ entry.stats.peakWaitingClients = Math.max(
825
+ entry.stats.peakWaitingClients,
826
+ entry.pool.waitingCount,
827
+ );
828
+ let poolClient;
829
+ try {
830
+ poolClient = await entry.pool.connect();
831
+ } catch (error) {
832
+ if (isRuntimeSchedulerLocalAcquireTimeout(error)) {
833
+ entry.stats.saturationRejections += 1;
834
+ console.warn('[runtime-scheduler-pool.local_acquire_saturated]', {
835
+ maxClients: entry.maxClients,
836
+ totalClients: entry.pool.totalCount,
837
+ idleClients: entry.pool.idleCount,
838
+ waitingClients: entry.pool.waitingCount,
839
+ });
840
+ throw new RuntimeSchedulerSaturatedError(entry.pool.waitingCount);
841
+ }
842
+ if (isPgBouncerCapacityError(error)) {
843
+ recordCapacityRejection(entry, error);
844
+ capacityObserved = true;
845
+ throw new RuntimeSchedulerPgBouncerCapacityError(
846
+ error instanceof Error ? error.message : String(error),
847
+ );
848
+ }
849
+ recordBreakerFailure(entry, error, halfOpenProbe);
979
850
  entry.stats.connectionErrors += 1;
980
851
  lastError = error;
981
- // Destroy the broken connection instead of returning it to the pool.
982
- releasePoolClient(
983
- error instanceof Error ? error : new Error(String(error)),
984
- );
985
852
  if (attempt + 1 < maxAttempts) {
986
853
  entry.stats.retries += 1;
987
854
  await sleep(jitteredBackoffMs(attempt));
@@ -990,30 +857,197 @@ export async function withRuntimeSchedulerClient<T>(
990
857
  }
991
858
  throw error;
992
859
  }
993
- if (isRuntimeSchedulerRetryableTransactionError(error)) {
994
- // PostgreSQL has aborted the transaction, so never return this
995
- // client to the pool. Re-run the idempotent scheduler operation on a
996
- // new session without opening the connection-failure breaker.
997
- entry.stats.queryErrors += 1;
860
+ entry.stats.acquires += 1;
861
+ const acquireWaitMs = performance.now() - acquireStart;
862
+ entry.stats.peakTotalClients = Math.max(
863
+ entry.stats.peakTotalClients,
864
+ entry.pool.totalCount,
865
+ );
866
+ pushSample(entry.stats.acquireWait, acquireWaitMs);
867
+
868
+ // A checked-out pg client emits 'error' (not via the pool) when its TCP
869
+ // connection dies while idle — e.g. a worker holding a client across a
870
+ // long play execution. With no listener that is an uncaught exception
871
+ // that kills the process (observed: worker crash mid-150k-row run).
872
+ // Swallow it here; the next query on the dead client fails normally and
873
+ // flows through the connection-error retry/poison path below.
874
+ let checkedOutError: unknown = null;
875
+ const onCheckedOutError = (error: Error) => {
876
+ checkedOutError = error;
877
+ entry.stats.connectionErrors += 1;
878
+ console.warn('[runtime-scheduler-pool.checked_out_client_error]', {
879
+ error: error.message,
880
+ });
881
+ };
882
+ poolClient.on('error', onCheckedOutError);
883
+ let clientReleased = false;
884
+ const releasePoolClient = (error?: Error) => {
885
+ if (clientReleased) return;
886
+ clientReleased = true;
887
+ poolClient.release(error);
888
+ };
889
+ const onAbort = () => releasePoolClient(schedulerAbortReason(signal!));
890
+ signal?.addEventListener('abort', onAbort, { once: true });
891
+
892
+ let queryCount = 0;
893
+ let queryMs = 0;
894
+ try {
895
+ throwIfSchedulerAborted(signal);
896
+ const result = await fn({
897
+ query: async (sql, params) => {
898
+ throwIfSchedulerAborted(signal);
899
+ const queryStartedAt = performance.now();
900
+ queryCount += 1;
901
+ try {
902
+ try {
903
+ const queryResult = await poolClient.query(sql, params);
904
+ throwIfSchedulerAborted(signal);
905
+ return { rows: queryResult.rows };
906
+ } catch (error) {
907
+ // node-postgres' query timeout and an AbortSignal reject the
908
+ // promise, but the server may still be processing the SQL.
909
+ // Never return that socket to the pool: doing so lets the
910
+ // next retry overlap the first statement on the same backend.
911
+ if (isRuntimeSchedulerQueryCancellationError(error)) {
912
+ releasePoolClient(
913
+ error instanceof Error ? error : new Error(String(error)),
914
+ );
915
+ }
916
+ throw error;
917
+ }
918
+ } finally {
919
+ queryMs += performance.now() - queryStartedAt;
920
+ }
921
+ },
922
+ });
923
+ throwIfSchedulerAborted(signal);
998
924
  recordBreakerSuccess(entry);
999
- lastError = error;
925
+ signal?.removeEventListener('abort', onAbort);
926
+ poolClient.removeListener('error', onCheckedOutError);
927
+ // The connection died mid-fn but fn happened to finish without another
928
+ // query — destroy the dead connection rather than pooling it.
1000
929
  releasePoolClient(
1001
- error instanceof Error ? error : new Error(String(error)),
930
+ checkedOutError instanceof Error ? checkedOutError : undefined,
1002
931
  );
1003
- if (attempt + 1 < maxAttempts) {
1004
- entry.stats.retries += 1;
1005
- await sleep(jitteredBackoffMs(attempt));
1006
- throwIfSchedulerAborted(signal);
1007
- continue;
932
+ if (trace) {
933
+ console.info(
934
+ '[runtime-scheduler-pool.operation] ' +
935
+ JSON.stringify({
936
+ runId: trace.runId,
937
+ phase: trace.phase,
938
+ outcome: 'success',
939
+ attempt: attempt + 1,
940
+ priority: trace.priority ?? 'ordinary',
941
+ admissionWaitMs,
942
+ acquireWaitMs,
943
+ queryCount,
944
+ queryMs,
945
+ totalMs: performance.now() - operationStartedAt,
946
+ }),
947
+ );
1008
948
  }
949
+ return result;
950
+ } catch (error) {
951
+ if (trace) {
952
+ console.info(
953
+ '[runtime-scheduler-pool.operation] ' +
954
+ JSON.stringify({
955
+ runId: trace.runId,
956
+ phase: trace.phase,
957
+ outcome: 'failed',
958
+ attempt: attempt + 1,
959
+ priority: trace.priority ?? 'ordinary',
960
+ admissionWaitMs,
961
+ acquireWaitMs,
962
+ queryCount,
963
+ queryMs,
964
+ totalMs: performance.now() - operationStartedAt,
965
+ errorName:
966
+ error instanceof Error ? error.name : 'UnknownError',
967
+ }),
968
+ );
969
+ }
970
+ signal?.removeEventListener('abort', onAbort);
971
+ poolClient.removeListener('error', onCheckedOutError);
972
+ if (signal?.aborted) {
973
+ const cancellation = schedulerAbortReason(signal);
974
+ releasePoolClient(cancellation);
975
+ throw cancellation;
976
+ }
977
+ if (isPgBouncerCapacityError(error)) {
978
+ recordCapacityRejection(entry, error);
979
+ capacityObserved = true;
980
+ releasePoolClient();
981
+ throw new RuntimeSchedulerPgBouncerCapacityError(
982
+ error instanceof Error ? error.message : String(error),
983
+ );
984
+ }
985
+ if (isRuntimeSchedulerQueryCancellationError(error)) {
986
+ entry.stats.queryErrors += 1;
987
+ recordBreakerSuccess(entry);
988
+ releasePoolClient(
989
+ error instanceof Error ? error : new Error(String(error)),
990
+ );
991
+ throw error;
992
+ }
993
+ if (isConnectionCategoryError(error)) {
994
+ recordBreakerFailure(entry, error, halfOpenProbe);
995
+ entry.stats.connectionErrors += 1;
996
+ lastError = error;
997
+ // Destroy the broken connection instead of returning it to the pool.
998
+ releasePoolClient(
999
+ error instanceof Error ? error : new Error(String(error)),
1000
+ );
1001
+ if (attempt + 1 < maxAttempts) {
1002
+ entry.stats.retries += 1;
1003
+ await sleep(jitteredBackoffMs(attempt));
1004
+ throwIfSchedulerAborted(signal);
1005
+ continue;
1006
+ }
1007
+ throw error;
1008
+ }
1009
+ if (isRuntimeSchedulerRetryableTransactionError(error)) {
1010
+ // PostgreSQL has aborted the transaction, so never return this
1011
+ // client to the pool. Re-run the idempotent scheduler operation on a
1012
+ // new session without opening the connection-failure breaker.
1013
+ entry.stats.queryErrors += 1;
1014
+ recordBreakerSuccess(entry);
1015
+ lastError = error;
1016
+ releasePoolClient(
1017
+ error instanceof Error ? error : new Error(String(error)),
1018
+ );
1019
+ if (attempt + 1 < maxAttempts) {
1020
+ entry.stats.retries += 1;
1021
+ await sleep(jitteredBackoffMs(attempt));
1022
+ throwIfSchedulerAborted(signal);
1023
+ continue;
1024
+ }
1025
+ throw error;
1026
+ }
1027
+ // Query-level failure on a healthy connection: count it, return the
1028
+ // connection to the pool, surface the error without retry.
1029
+ entry.stats.queryErrors += 1;
1030
+ recordBreakerSuccess(entry);
1031
+ releasePoolClient();
1009
1032
  throw error;
1010
1033
  }
1011
- // Query-level failure on a healthy connection: count it, return the
1012
- // connection to the pool, surface the error without retry.
1013
- entry.stats.queryErrors += 1;
1014
- recordBreakerSuccess(entry);
1015
- releasePoolClient();
1016
- throw error;
1034
+ } finally {
1035
+ // A canceled, locally saturated, or capacity-rejected trial did not
1036
+ // call either breaker recorder. Never let it own the probe forever.
1037
+ // Only the trial that claimed this token may release it: another
1038
+ // operation can close and reopen the breaker while this one awaits.
1039
+ if (
1040
+ halfOpenProbe !== null &&
1041
+ entry.breaker.halfOpenProbe === halfOpenProbe
1042
+ ) {
1043
+ entry.breaker.halfOpenProbe = null;
1044
+ if (capacityObserved && entry.breaker.openedAt !== null) {
1045
+ // Capacity is not a connection failure. Keep the existing open
1046
+ // state and try once more after the ordinary cooldown instead of
1047
+ // probing the still-full database on every request.
1048
+ entry.breaker.openedAt = Date.now();
1049
+ }
1050
+ }
1017
1051
  }
1018
1052
  }
1019
1053
  throw lastError instanceof Error
@@ -3080,6 +3080,13 @@ async function penalizeRateStateStatements(
3080
3080
  GREATEST(h.retry_at_ms, COALESCE($5::bigint, nv.now_ms + $2::bigint))
3081
3081
  - COALESCE(h.rate_observation_started_at_ms, nv.now_ms)
3082
3082
  END) AS cycle_ms,
3083
+ CASE
3084
+ WHEN $5::bigint IS NOT NULL AND h.rate_successes <= 0 THEN
3085
+ CASE WHEN h.seen_backpressure THEN
3086
+ LEAST(60000, GREATEST(1000, h.rate_recovery_interval_ms * 2))
3087
+ ELSE 1000 END
3088
+ ELSE NULL
3089
+ END AS no_progress_probe_interval_ms,
3083
3090
  nv.now_ms
3084
3091
  FROM held h CROSS JOIN now_value nv
3085
3092
  ), throttle_ceiling AS (
@@ -3087,12 +3094,12 @@ async function penalizeRateStateStatements(
3087
3094
  GREATEST(${MIN_ADAPTIVE_RPS_FLOOR},
3088
3095
  CASE WHEN e.seen_backpressure AND e.rate_successes = 0 THEN
3089
3096
  CASE WHEN $5::bigint IS NOT NULL
3090
- -- An explicit Retry-After is only the provider's next safe
3091
- -- dispatch deadline. It is not a rate measurement; with no
3092
- -- successful-call evidence, use one conservative probe per
3093
- -- second after that deadline instead of deriving a permanent
3094
- -- RPM limit from the length of the hold.
3095
- THEN GREATEST(e.refill_before, 1000.0 / e.cycle_ms)
3097
+ -- A fresh provider deadline is not a throughput measurement.
3098
+ -- Repeated probes with no successful work are evidence that
3099
+ -- the scope is still unavailable, however. Space subsequent
3100
+ -- probes exponentially (up to one per minute) across every
3101
+ -- run sharing this provider credential.
3102
+ THEN 1000.0 / e.no_progress_probe_interval_ms
3096
3103
  ELSE e.refill_before END
3097
3104
  ELSE e.refill_before / 2 END
3098
3105
  ) AS next_ceiling_rps
@@ -3115,7 +3122,9 @@ async function penalizeRateStateStatements(
3115
3122
  rate_throttle_ceiling_rps = CASE WHEN e.new_episode
3116
3123
  THEN e.next_ceiling_rps ELSE e.rate_throttle_ceiling_rps END,
3117
3124
  rate_observation_started_at_ms = CASE WHEN e.new_episode THEN NULL ELSE e.rate_observation_started_at_ms END,
3118
- rate_recovery_interval_ms = CASE WHEN e.new_episode THEN e.cycle_ms ELSE e.rate_recovery_interval_ms END,
3125
+ rate_recovery_interval_ms = CASE WHEN e.new_episode
3126
+ THEN COALESCE(e.no_progress_probe_interval_ms, e.cycle_ms)
3127
+ ELSE e.rate_recovery_interval_ms END,
3119
3128
  last_ramp_at_ms = CASE WHEN e.new_episode THEN e.deadline ELSE e.last_ramp_at_ms END,
3120
3129
  -- All pre-429 permits are revalidated against this durable hold
3121
3130
  -- before dispatch. Their obsolete schedule debt must not add a