deepline 0.3.88 → 0.3.90

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 (280) hide show
  1. package/contracts/install-commands.json +29 -0
  2. package/dist/bundling-sources/sdk/src/client.ts +652 -39
  3. package/dist/bundling-sources/sdk/src/config.ts +77 -0
  4. package/dist/bundling-sources/sdk/src/http.ts +76 -30
  5. package/dist/bundling-sources/sdk/src/index.ts +10 -0
  6. package/dist/bundling-sources/sdk/src/monitor-fleet-contract.ts +1 -1
  7. package/dist/bundling-sources/sdk/src/pagination.ts +25 -0
  8. package/dist/bundling-sources/sdk/src/play.ts +15 -1
  9. package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +3 -3
  10. package/dist/bundling-sources/sdk/src/release.ts +16 -9
  11. package/dist/bundling-sources/sdk/src/types.ts +106 -4
  12. package/dist/bundling-sources/shared_libs/integration-contracts/AGENTS.md +9 -0
  13. package/dist/bundling-sources/shared_libs/integration-contracts/README.md +3 -0
  14. package/dist/bundling-sources/shared_libs/integration-contracts/index.ts +2 -0
  15. package/dist/bundling-sources/shared_libs/monitors/analytics-install-guides.ts +77 -0
  16. package/dist/bundling-sources/shared_libs/monitors/analytics-public.ts +10 -0
  17. package/dist/bundling-sources/shared_libs/monitors/billing/charges.ts +125 -0
  18. package/dist/bundling-sources/shared_libs/monitors/billing/policy-builders.ts +686 -0
  19. package/dist/bundling-sources/shared_libs/monitors/billing/pricing.ts +54 -0
  20. package/dist/bundling-sources/shared_libs/monitors/billing/public-pricing.ts +72 -0
  21. package/dist/bundling-sources/shared_libs/monitors/billing/session.ts +45 -0
  22. package/dist/bundling-sources/shared_libs/monitors/billing/state.ts +454 -0
  23. package/dist/bundling-sources/shared_libs/monitors/billing/tamradar.ts +260 -0
  24. package/dist/bundling-sources/shared_libs/monitors/billing/types.ts +215 -0
  25. package/dist/bundling-sources/shared_libs/monitors/callback-security.ts +333 -0
  26. package/dist/bundling-sources/shared_libs/monitors/cursor-page.ts +42 -0
  27. package/dist/bundling-sources/shared_libs/monitors/deactivation-reasons.ts +16 -0
  28. package/dist/bundling-sources/shared_libs/monitors/framework-managed-payload-fields.ts +71 -0
  29. package/dist/bundling-sources/shared_libs/monitors/generated/provider-monitor-callback-admission-targets.ts +16 -0
  30. package/dist/bundling-sources/shared_libs/monitors/http-error-response.ts +270 -0
  31. package/dist/bundling-sources/shared_libs/monitors/inbound-publish-key.ts +39 -0
  32. package/dist/bundling-sources/shared_libs/monitors/inbound-queue.ts +82 -0
  33. package/dist/bundling-sources/shared_libs/monitors/inventory-reconciliation.ts +59 -0
  34. package/dist/bundling-sources/shared_libs/monitors/lifecycle-notifications.ts +471 -0
  35. package/dist/bundling-sources/shared_libs/monitors/org-monitor-limits.ts +1 -1
  36. package/dist/bundling-sources/shared_libs/monitors/package.json +36 -0
  37. package/dist/bundling-sources/shared_libs/monitors/provider-callback-verification.ts +1125 -0
  38. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +305 -0
  39. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-capability.ts +964 -0
  40. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-consumer-health.ts +56 -0
  41. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-errors.ts +131 -0
  42. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-identity-claims.ts +535 -0
  43. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-legacy-aliases.ts +16 -0
  44. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-priority-execution.ts +56 -0
  45. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-public-aliases.ts +28 -0
  46. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-runtime-read-eligibility.ts +38 -0
  47. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-state.ts +122 -0
  48. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-status.ts +10 -0
  49. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-upstream-identity.ts +112 -0
  50. package/dist/bundling-sources/shared_libs/monitors/provider-webhook-monitor-runtime.ts +300 -0
  51. package/dist/bundling-sources/shared_libs/monitors/quickstart-prompts.ts +22 -0
  52. package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-dead-letter.ts +245 -0
  53. package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-message.ts +38 -0
  54. package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-queue-ack-probe.ts +94 -0
  55. package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-queue-delivery.ts +60 -0
  56. package/dist/bundling-sources/shared_libs/monitors/signal-radars/inbound-queue.ts +60 -0
  57. package/dist/bundling-sources/shared_libs/monitors/snitcher-install-validation.ts +93 -0
  58. package/dist/bundling-sources/shared_libs/monitors/snitcher-ip2company.ts +332 -0
  59. package/dist/bundling-sources/shared_libs/monitors/snitcher-radar.ts +221 -0
  60. package/dist/bundling-sources/shared_libs/monitors/starter-templates.ts +105 -0
  61. package/dist/bundling-sources/shared_libs/monitors/tracking-provider-guidance.ts +73 -0
  62. package/dist/bundling-sources/shared_libs/monitors/tracking-provider-integrations.ts +81 -0
  63. package/dist/bundling-sources/shared_libs/monitors/tsconfig.json +10 -0
  64. package/dist/bundling-sources/shared_libs/observability/build-context.ts +13 -0
  65. package/dist/bundling-sources/shared_libs/observability/log-metadata.ts +160 -0
  66. package/dist/bundling-sources/shared_libs/observability/request-context.ts +68 -0
  67. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +654 -0
  68. package/dist/bundling-sources/shared_libs/observability/scheduled-work-manual.ts +35 -0
  69. package/dist/bundling-sources/shared_libs/observability/scheduled-work.ts +5 -0
  70. package/dist/bundling-sources/shared_libs/play-data-plane/neon-constants.ts +9 -0
  71. package/dist/bundling-sources/shared_libs/play-data-plane/r2.ts +272 -0
  72. package/dist/bundling-sources/shared_libs/play-runtime/activity-observation.ts +6 -11
  73. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +135 -12
  74. package/dist/bundling-sources/shared_libs/play-runtime/async-operation.ts +66 -1
  75. package/dist/bundling-sources/shared_libs/play-runtime/async-play-invocation.ts +77 -0
  76. package/dist/bundling-sources/shared_libs/play-runtime/bounceban-batching.ts +137 -0
  77. package/dist/bundling-sources/shared_libs/play-runtime/checkpoint-boundaries.ts +70 -0
  78. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1159 -166
  79. package/dist/bundling-sources/shared_libs/play-runtime/coordinator-headers.ts +1 -32
  80. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +103 -0
  81. package/dist/bundling-sources/shared_libs/play-runtime/daytona-payload-contract.ts +68 -0
  82. package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-janitor.ts +103 -0
  83. package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-reaper.ts +725 -0
  84. package/dist/bundling-sources/shared_libs/play-runtime/default-batch-strategies.ts +20 -1
  85. package/dist/bundling-sources/shared_libs/play-runtime/durable-call-cache.ts +29 -0
  86. package/dist/bundling-sources/shared_libs/play-runtime/execution-capabilities.ts +29 -0
  87. package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-invocation-fence.ts +442 -0
  88. package/dist/bundling-sources/shared_libs/play-runtime/execution-protocol.ts +7 -1
  89. package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +18 -0
  90. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +9 -0
  91. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +1 -1
  92. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +1 -1
  93. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/contracts.ts +66 -0
  94. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/job.ts +145 -0
  95. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/ports.ts +21 -0
  96. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/postgres-store.ts +170 -0
  97. package/dist/bundling-sources/shared_libs/play-runtime/internal-bootstrap-token.ts +1322 -0
  98. package/dist/bundling-sources/shared_libs/play-runtime/internal-step-ids.ts +2 -2
  99. package/dist/bundling-sources/shared_libs/play-runtime/lane-retirement-gate.ts +189 -0
  100. package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +112 -0
  101. package/dist/bundling-sources/shared_libs/play-runtime/map-execution-frame.ts +8 -0
  102. package/dist/bundling-sources/shared_libs/play-runtime/perf-trace.ts +127 -0
  103. package/dist/bundling-sources/shared_libs/play-runtime/play-artifact-runtime-contract.ts +5 -0
  104. package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +24 -1
  105. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/contracts.ts +56 -0
  106. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/index.ts +13 -0
  107. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/job.ts +41 -0
  108. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/ports.ts +17 -0
  109. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/postgres-adapter.ts +54 -0
  110. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/contracts.ts +114 -0
  111. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/engine-suspension.ts +108 -0
  112. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/index.ts +36 -0
  113. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/job.ts +97 -0
  114. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/ports.ts +104 -0
  115. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/postgres-fence-adapter.ts +263 -0
  116. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/worker-terminal-projection.ts +64 -0
  117. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/contracts.ts +29 -0
  118. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/gateway-adapter.ts +56 -0
  119. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/index.ts +15 -0
  120. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/job.ts +65 -0
  121. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/ports.ts +17 -0
  122. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/contracts.ts +42 -0
  123. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/index.ts +17 -0
  124. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/job.ts +142 -0
  125. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/ports.ts +42 -0
  126. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/sandbox-cleanup/postgres-adapter.ts +102 -0
  127. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/contracts.ts +60 -0
  128. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/index.ts +16 -0
  129. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/job.ts +78 -0
  130. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/ports.ts +54 -0
  131. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/postgres-adapter.ts +71 -0
  132. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-batching-registry.ts +4 -0
  133. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-semantics.ts +28 -0
  134. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +4 -0
  135. package/dist/bundling-sources/shared_libs/play-runtime/receipt-status.ts +5 -8
  136. package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +23 -2
  137. package/dist/bundling-sources/shared_libs/play-runtime/run-identity.ts +168 -0
  138. package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +1 -1
  139. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +90 -13
  140. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +6 -12
  141. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-start-admission.ts +68 -0
  142. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +21 -0
  143. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/bundle.ts +0 -1
  144. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +14 -43
  145. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +67 -213
  146. package/dist/bundling-sources/shared_libs/play-runtime/runtime-authority.ts +201 -0
  147. package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +20 -0
  148. package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +6 -0
  149. package/dist/bundling-sources/shared_libs/play-runtime/runtime-env.ts +12 -0
  150. package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +67 -0
  151. package/dist/bundling-sources/shared_libs/play-runtime/runtime-release-environment.ts +2 -0
  152. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sandbox-contract.ts +68 -0
  153. package/dist/bundling-sources/shared_libs/play-runtime/runtime-scheduler-topology.ts +38 -2
  154. package/dist/bundling-sources/shared_libs/play-runtime/runtime-telemetry.ts +108 -0
  155. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/contract.ts +4 -0
  156. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +3 -0
  157. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +37 -1
  158. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-deadline-reconciliation.ts +88 -0
  159. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-play-deploy-task.ts +124 -0
  160. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-play-run-task.ts +68 -0
  161. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-pool.ts +193 -0
  162. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-queue-lifecycle.ts +269 -0
  163. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-queue-suffix.ts +73 -0
  164. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +6642 -0
  165. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +897 -0
  166. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-worker-metrics.ts +120 -0
  167. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-worker-required-env.ts +76 -0
  168. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd.ts +476 -0
  169. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-heavy-request-admission.ts +47 -0
  170. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-progress-registry.ts +147 -0
  171. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/index.ts +27 -0
  172. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/lazy.ts +50 -0
  173. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/play-run-recovery.ts +98 -0
  174. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-budget-state.ts +260 -0
  175. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +1416 -0
  176. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +1255 -0
  177. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +942 -0
  178. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +14636 -0
  179. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +888 -0
  180. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +355 -0
  181. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-terminal-transition.ts +46 -0
  182. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runner-push.ts +60 -0
  183. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-api-origin.ts +33 -0
  184. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-gateway-health.ts +64 -0
  185. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-sandbox-capacity.ts +835 -0
  186. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-sandbox-ledger-reconciliation.ts +1218 -0
  187. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-sandbox-operation.ts +207 -0
  188. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-scheduler-schema.ts +10 -0
  189. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/scheduler-capacity.ts +123 -0
  190. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/scheduler-launch-read-observability.ts +78 -0
  191. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/submit-admission.ts +169 -0
  192. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/terminal-contract.ts +20 -0
  193. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/worker-queue-metrics.ts +34 -0
  194. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/worker-resource-metrics.ts +249 -0
  195. package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +7 -0
  196. package/dist/bundling-sources/shared_libs/play-runtime/step-progress.ts +296 -0
  197. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/native-batch.ts +3 -3
  198. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +20 -1
  199. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +60 -15
  200. package/dist/bundling-sources/shared_libs/play-runtime/tool-execution-outcome.ts +20 -0
  201. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +1 -1
  202. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +1 -1
  203. package/dist/bundling-sources/shared_libs/play-runtime/transient-service-error.ts +12 -0
  204. package/dist/bundling-sources/shared_libs/play-runtime/vercel-protection.ts +14 -1
  205. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-storage-contract.ts +165 -0
  206. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +15 -23
  207. package/dist/bundling-sources/shared_libs/play-runtime/workflow-executor-auth.ts +169 -0
  208. package/dist/bundling-sources/shared_libs/play-runtime/zerobounce-batching.ts +122 -0
  209. package/dist/bundling-sources/shared_libs/plays/artifact-types.ts +8 -3
  210. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +454 -17
  211. package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +19 -4
  212. package/dist/bundling-sources/shared_libs/plays/contracts.ts +8 -14
  213. package/dist/bundling-sources/shared_libs/plays/core.ts +11 -6
  214. package/dist/bundling-sources/shared_libs/plays/deployment-status.ts +1 -1
  215. package/dist/bundling-sources/shared_libs/plays/docflow-binding-owner.ts +16 -11
  216. package/dist/bundling-sources/shared_libs/plays/docflow.ts +1 -1
  217. package/dist/bundling-sources/shared_libs/plays/email-status.ts +1 -1
  218. package/dist/bundling-sources/shared_libs/plays/play-grep-search.ts +39 -0
  219. package/dist/bundling-sources/shared_libs/plays/resolve-static-pipeline.ts +1 -1
  220. package/dist/bundling-sources/shared_libs/plays/share/copy.ts +271 -0
  221. package/dist/bundling-sources/shared_libs/plays/share/metrics.ts +140 -0
  222. package/dist/bundling-sources/shared_libs/plays/share/public-read-model.ts +86 -0
  223. package/dist/bundling-sources/shared_libs/plays/share/seo-quality.ts +181 -0
  224. package/dist/bundling-sources/shared_libs/plays/share/static-pipeline-summary.ts +658 -0
  225. package/dist/bundling-sources/shared_libs/plays/share/types.ts +197 -0
  226. package/dist/bundling-sources/shared_libs/plays/slack-hitl-timeout-error.ts +37 -0
  227. package/dist/bundling-sources/shared_libs/plays/slug.ts +115 -0
  228. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +50 -15
  229. package/dist/bundling-sources/shared_libs/plays/tool-category-descriptions.ts +2 -2
  230. package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +1 -1
  231. package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +877 -0
  232. package/dist/bundling-sources/shared_libs/product-notifications/configuration-draft.ts +456 -0
  233. package/dist/bundling-sources/shared_libs/product-notifications/configuration.ts +566 -0
  234. package/dist/bundling-sources/shared_libs/product-notifications/content.ts +28 -0
  235. package/dist/bundling-sources/shared_libs/product-notifications/default-policy.ts +60 -0
  236. package/dist/bundling-sources/shared_libs/product-notifications/email.ts +246 -0
  237. package/dist/bundling-sources/shared_libs/product-notifications/evaluation.ts +280 -0
  238. package/dist/bundling-sources/shared_libs/product-notifications/events.ts +433 -0
  239. package/dist/bundling-sources/shared_libs/product-notifications/legacy-slack.ts +80 -0
  240. package/dist/bundling-sources/shared_libs/product-notifications/overflow.ts +18 -0
  241. package/dist/bundling-sources/shared_libs/product-notifications/rolling-limit.ts +24 -0
  242. package/dist/bundling-sources/shared_libs/product-notifications/slack-delivery.ts +624 -0
  243. package/dist/bundling-sources/shared_libs/product-notifications/slack-payload.ts +140 -0
  244. package/dist/bundling-sources/shared_libs/product-notifications/slack-transport.ts +75 -0
  245. package/dist/bundling-sources/shared_libs/product-notifications/workspace.ts +38 -0
  246. package/dist/bundling-sources/shared_libs/runtime-env.ts +12 -0
  247. package/dist/bundling-sources/shared_libs/security/email-blacklist.ts +42 -0
  248. package/dist/bundling-sources/shared_libs/security/header-safe-env.ts +35 -0
  249. package/dist/bundling-sources/shared_libs/security/internal-actor.ts +50 -0
  250. package/dist/bundling-sources/shared_libs/security/secret-redaction.ts +267 -0
  251. package/dist/bundling-sources/shared_libs/security/slack-sanitize.ts +133 -0
  252. package/dist/cli/getting-started.d.mts +52 -0
  253. package/dist/cli/getting-started.d.ts +52 -0
  254. package/dist/cli/getting-started.js +65 -0
  255. package/dist/cli/getting-started.mjs +36 -0
  256. package/dist/cli/index.d.mts +9 -0
  257. package/dist/cli/index.d.ts +9 -0
  258. package/dist/cli/index.js +1611 -453
  259. package/dist/cli/index.mjs +1596 -447
  260. package/dist/{compiler-manifest-Dbov16F7.d.mts → compiler-manifest-BaJfE-5H.d.mts} +136 -10
  261. package/dist/{compiler-manifest-Dbov16F7.d.ts → compiler-manifest-BaJfE-5H.d.ts} +136 -10
  262. package/dist/index.d.mts +910 -155
  263. package/dist/index.d.ts +910 -155
  264. package/dist/index.js +550 -64
  265. package/dist/index.mjs +550 -64
  266. package/dist/install-integrity.json +193 -4
  267. package/dist/monitor-fleet-contract.d.mts +138 -0
  268. package/dist/monitor-fleet-contract.d.ts +138 -0
  269. package/dist/monitor-fleet-contract.js +404 -0
  270. package/dist/monitor-fleet-contract.mjs +367 -0
  271. package/dist/plays/bundle-play-file.d.mts +8 -3
  272. package/dist/plays/bundle-play-file.d.ts +8 -3
  273. package/dist/plays/bundle-play-file.mjs +100 -20
  274. package/dist/release.d.mts +255 -0
  275. package/dist/release.d.ts +255 -0
  276. package/dist/release.js +215 -0
  277. package/dist/release.mjs +188 -0
  278. package/package.json +23 -4
  279. /package/dist/bundling-sources/shared_libs/{integrations/bettercontact-execution-policy.ts → integration-contracts/bettercontact/execution-policy.ts} +0 -0
  280. /package/dist/bundling-sources/shared_libs/{integrations/theirstack-execution-policy.ts → integration-contracts/theirstack/execution-policy.ts} +0 -0
@@ -0,0 +1,1416 @@
1
+ /**
2
+ * Pooled connection layer for the Postgres runtime scheduler control plane.
3
+ *
4
+ * The scheduler's hot path is short, idempotent transactions (submit, claim,
5
+ * complete, receipts). Three properties matter more than anything else here:
6
+ *
7
+ * 1. No connection churn. Every operation runs on a process-singleton
8
+ * `pg.Pool` aimed at the *pooled* (PgBouncer transaction-mode) endpoint.
9
+ * Opening a fresh `pg.Client` per operation — what the prototype did —
10
+ * costs a TLS handshake plus auth round trips per call and collapses
11
+ * under burst.
12
+ * 2. Bounded everything. Client caps, acquire timeouts, query timeouts, and
13
+ * an explicit saturation error once the acquire queue backs up, so
14
+ * callers shed load instead of stacking unbounded waiters.
15
+ * 3. Loud failure + fast recovery. A circuit breaker opens after repeated
16
+ * connection failures so a dead DB fails calls in microseconds instead
17
+ * of stacking 5s connect timeouts, and a half-open trial closes it as
18
+ * soon as the DB is back.
19
+ *
20
+ * LISTEN/NOTIFY does not work through PgBouncer transaction pooling, so the
21
+ * notify hub derives the direct (non-pooler) host for its single listener
22
+ * connection. Notifications are strictly a wakeup hint — the tables remain
23
+ * the source of truth and every waiter still reads the DB.
24
+ */
25
+ import { Client, Pool } from 'pg';
26
+ import {
27
+ RuntimeSchedulerPgBouncerCapacityError,
28
+ RuntimeSchedulerSubmissionAdmissionError,
29
+ RuntimeSchedulerSaturatedError,
30
+ } from './scheduler-capacity';
31
+ import { RUNTIME_RELIABILITY_POLICY } from '../runtime-reliability-policy';
32
+ export {
33
+ isRuntimeSchedulerCapacityError,
34
+ parseRuntimeSchedulerCapacityError,
35
+ RuntimeSchedulerCapacityError,
36
+ RuntimeSchedulerPgBouncerCapacityError,
37
+ RuntimeSchedulerSubmissionAdmissionError,
38
+ RuntimeSchedulerSaturatedError,
39
+ runtimeSchedulerCapacityRetryAfterMs,
40
+ } from './scheduler-capacity';
41
+
42
+ const PG_SSL_FILE_PARAMS = ['sslcert', 'sslkey', 'sslrootcert'] as const;
43
+
44
+ /**
45
+ * PlanetScale and Neon Postgres URLs can use `sslrootcert=system` to mean "use
46
+ * the host system CA bundle". `pg-connection-string` treats sslcert/sslkey/
47
+ * sslrootcert as file paths and tries to open a file literally named `system`,
48
+ * so normalize that sentinel before handing the URL to node-postgres. Node's
49
+ * TLS stack uses system roots by default when no custom CA is supplied.
50
+ */
51
+ export function normalizePostgresConnectionStringForPg(url: string): string {
52
+ try {
53
+ const parsed = new URL(url);
54
+ let changed = false;
55
+ for (const param of PG_SSL_FILE_PARAMS) {
56
+ if (parsed.searchParams.get(param)?.trim().toLowerCase() === 'system') {
57
+ parsed.searchParams.delete(param);
58
+ changed = true;
59
+ }
60
+ }
61
+ return changed ? parsed.toString() : url;
62
+ } catch {
63
+ return url;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Canonical env var for the workflow runtime control-plane Postgres
69
+ * (scheduler queue, leases, receipts, outbox). Hard-cutover runtime wiring
70
+ * must use this name in local dev, CI, Fly, and Vercel.
71
+ */
72
+ export function resolveWorkflowRuntimePostgresUrl(): string | null {
73
+ const url = process.env.WORKFLOW_RUNTIME_POSTGRES?.trim() || null;
74
+ return url ? normalizePostgresConnectionStringForPg(url) : null;
75
+ }
76
+
77
+ function parsePostgresUrl(url: string): URL | null {
78
+ try {
79
+ const parsed = new URL(url);
80
+ const protocol = parsed.protocol.toLowerCase();
81
+ if (protocol !== 'postgres:' && protocol !== 'postgresql:') return null;
82
+ return parsed;
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ function isPlanetScalePostgresUrl(parsed: URL): boolean {
89
+ const host = parsed.hostname.toLowerCase();
90
+ return host.includes('psdb.cloud') || host.includes('planetscale');
91
+ }
92
+
93
+ export function isKnownPooledRuntimePostgresUrl(url: string): boolean {
94
+ const parsed = parsePostgresUrl(url);
95
+ if (!parsed) return false;
96
+ if (isPlanetScalePostgresUrl(parsed)) {
97
+ return parsed.port === '6432';
98
+ }
99
+ const host = parsed.hostname.toLowerCase();
100
+ return host.includes('-pooler') || parsed.searchParams.has('pool_timeout');
101
+ }
102
+
103
+ export class RuntimeSchedulerDirectDatabaseUrlError extends Error {
104
+ constructor() {
105
+ super(
106
+ 'WORKFLOW_RUNTIME_POSTGRES points at PlanetScale direct Postgres (port 5432). ' +
107
+ 'Runtime scheduler hot paths must use the PgBouncer endpoint (port 6432); ' +
108
+ 'reserve direct connections for explicit session-only operations.',
109
+ );
110
+ this.name = 'RuntimeSchedulerDirectDatabaseUrlError';
111
+ }
112
+ }
113
+
114
+ export function assertRuntimeSchedulerHotPathUrl(url: string): void {
115
+ const parsed = parsePostgresUrl(url);
116
+ if (!parsed) return;
117
+ if (isPlanetScalePostgresUrl(parsed) && (parsed.port || '5432') === '5432') {
118
+ throw new RuntimeSchedulerDirectDatabaseUrlError();
119
+ }
120
+ }
121
+
122
+ export const RUNTIME_SCHEDULER_POOL_DEFAULTS = {
123
+ /** Per-process client cap. Workers override via options; keep small on serverless. */
124
+ maxClients: 8,
125
+ /** Refuse to construct a pool above this — protects the shared PgBouncer/Postgres. */
126
+ maxClientsHardCap: 32,
127
+ /** Max time a caller waits for a pooled connection before erroring. */
128
+ acquireTimeoutMs:
129
+ RUNTIME_RELIABILITY_POLICY.schedulerDatabase.acquireTimeoutMs,
130
+ /** Idle pooled connections are recycled after this. */
131
+ idleTimeoutMs: 30_000,
132
+ connectTimeoutMs: 5_000,
133
+ /** Client-side query timeout. Server-side SETs are unsafe through transaction pooling. */
134
+ queryTimeoutMs: RUNTIME_RELIABILITY_POLICY.schedulerDatabase.queryTimeoutMs,
135
+ /** Pending operations beyond one dense-100 burst fail fast instead of growing without bound. */
136
+ maxAcquireQueueDepth: 128,
137
+ retry: {
138
+ attempts: RUNTIME_RELIABILITY_POLICY.schedulerDatabase.retryAttempts,
139
+ baseDelayMs: RUNTIME_RELIABILITY_POLICY.schedulerDatabase.retryBaseDelayMs,
140
+ maxDelayMs: RUNTIME_RELIABILITY_POLICY.schedulerDatabase.retryMaxDelayMs,
141
+ },
142
+ breaker: {
143
+ /** Consecutive connection-category failures before the circuit opens. */
144
+ threshold: 5,
145
+ /** How long the circuit stays open before a half-open trial. */
146
+ openMs: RUNTIME_RELIABILITY_POLICY.schedulerDatabase.circuitOpenMs,
147
+ },
148
+ /**
149
+ * Keepalive ping cadence for long-lived worker processes and PgBouncer-style
150
+ * pooled endpoints.
151
+ */
152
+ keepAliveIntervalMs: 60_000,
153
+ } as const;
154
+
155
+ /**
156
+ * A control-plane process only needs a few short scheduler transactions at
157
+ * once. This shared cap keeps horizontally scaled workers and the receipt
158
+ * gateway from consuming PgBouncer's global client budget independently.
159
+ */
160
+ export const RUNTIME_SCHEDULER_CONTROL_PLANE_MAX_CLIENTS = 2;
161
+
162
+ export type RuntimeSchedulerPoolOptions = {
163
+ url: string;
164
+ applicationName?: string;
165
+ maxClients?: number;
166
+ };
167
+
168
+ export type RuntimeSchedulerQueryClient = {
169
+ query<Row extends Record<string, unknown> = Record<string, unknown>>(
170
+ sql: string,
171
+ params?: unknown[],
172
+ ): Promise<{ rows: Row[] }>;
173
+ };
174
+
175
+ export class RuntimeSchedulerCircuitOpenError extends Error {
176
+ constructor(cause: string) {
177
+ super(
178
+ `Runtime scheduler DB circuit breaker is open after repeated connection failures: ${cause}`,
179
+ );
180
+ this.name = 'RuntimeSchedulerCircuitOpenError';
181
+ }
182
+ }
183
+
184
+ type LatencyRing = {
185
+ samples: number[];
186
+ next: number;
187
+ };
188
+
189
+ const LATENCY_RING_SIZE = 512;
190
+
191
+ function pushSample(ring: LatencyRing, value: number): void {
192
+ if (ring.samples.length < LATENCY_RING_SIZE) {
193
+ ring.samples.push(value);
194
+ return;
195
+ }
196
+ ring.samples[ring.next % LATENCY_RING_SIZE] = value;
197
+ ring.next = (ring.next + 1) % LATENCY_RING_SIZE;
198
+ }
199
+
200
+ function ringQuantile(ring: LatencyRing, q: number): number | null {
201
+ if (ring.samples.length === 0) return null;
202
+ const sorted = [...ring.samples].sort((a, b) => a - b);
203
+ const idx = Math.min(sorted.length - 1, Math.ceil(q * sorted.length) - 1);
204
+ return sorted[Math.max(0, idx)] ?? null;
205
+ }
206
+
207
+ type BreakerState = {
208
+ consecutiveFailures: number;
209
+ openedAt: number | null;
210
+ lastFailureMessage: string;
211
+ halfOpenInFlight: boolean;
212
+ };
213
+
214
+ export type RuntimeSchedulerPoolPriority = 'ordinary' | 'critical';
215
+
216
+ type PoolAdmissionWaiter = {
217
+ priority: RuntimeSchedulerPoolPriority;
218
+ resolve: (release: () => void) => void;
219
+ reject: (error: Error) => void;
220
+ timeout: ReturnType<typeof setTimeout>;
221
+ signal?: AbortSignal;
222
+ onAbort?: () => void;
223
+ };
224
+
225
+ /**
226
+ * Critical work may skip one queued launch, but never an unbounded launch
227
+ * backlog. Alternation keeps already-running Plays moving without turning a
228
+ * burst of executor-token lookups into starvation for new durable launches.
229
+ */
230
+ const MAX_CONSECUTIVE_CRITICAL_ADMISSIONS = 1;
231
+
232
+ /**
233
+ * Keep priority outside `pg.Pool`: once pg-pool owns a checkout request it is
234
+ * FIFO and an already-running Play's control-plane request can sit behind an
235
+ * entire launch burst. This gate admits no more work than the existing client
236
+ * cap and only changes which bounded waiter receives the next free client.
237
+ */
238
+ export class RuntimeSchedulerPoolAdmission {
239
+ readonly #waiting: Record<
240
+ RuntimeSchedulerPoolPriority,
241
+ PoolAdmissionWaiter[]
242
+ > = {
243
+ ordinary: [],
244
+ critical: [],
245
+ };
246
+ #active = 0;
247
+ #consecutiveCritical = 0;
248
+
249
+ constructor(
250
+ readonly maxActive: number,
251
+ readonly maxWaiting: number,
252
+ readonly maxWaitMs: number = RUNTIME_SCHEDULER_POOL_DEFAULTS.acquireTimeoutMs,
253
+ ) {}
254
+
255
+ acquire(
256
+ priority: RuntimeSchedulerPoolPriority,
257
+ signal?: AbortSignal,
258
+ ): Promise<() => void> {
259
+ throwIfSchedulerAborted(signal);
260
+ if (this.#active < this.maxActive && this.waitingCount === 0) {
261
+ return Promise.resolve(this.#grant());
262
+ }
263
+ if (this.waitingCount >= this.maxWaiting) {
264
+ throw new RuntimeSchedulerSaturatedError(this.waitingCount);
265
+ }
266
+ return new Promise((resolve, reject) => {
267
+ const waiter: PoolAdmissionWaiter = {
268
+ priority,
269
+ resolve,
270
+ reject,
271
+ signal,
272
+ timeout: setTimeout(() => {
273
+ const index = this.#waiting[priority].indexOf(waiter);
274
+ if (index < 0) return;
275
+ this.#waiting[priority].splice(index, 1);
276
+ signal?.removeEventListener('abort', waiter.onAbort!);
277
+ reject(new RuntimeSchedulerSaturatedError(this.waitingCount));
278
+ }, this.maxWaitMs),
279
+ };
280
+ waiter.onAbort = () => {
281
+ const index = this.#waiting[priority].indexOf(waiter);
282
+ if (index < 0) return;
283
+ this.#waiting[priority].splice(index, 1);
284
+ clearTimeout(waiter.timeout);
285
+ reject(schedulerAbortReason(signal!));
286
+ };
287
+ signal?.addEventListener('abort', waiter.onAbort, { once: true });
288
+ this.#waiting[priority].push(waiter);
289
+ this.#drain();
290
+ });
291
+ }
292
+
293
+ get waitingCount(): number {
294
+ return this.#waiting.ordinary.length + this.#waiting.critical.length;
295
+ }
296
+
297
+ #grant(): () => void {
298
+ this.#active += 1;
299
+ let released = false;
300
+ return () => {
301
+ if (released) return;
302
+ released = true;
303
+ this.#active -= 1;
304
+ this.#drain();
305
+ };
306
+ }
307
+
308
+ #drain(): void {
309
+ while (this.#active < this.maxActive) {
310
+ const ordinary =
311
+ this.#consecutiveCritical >= MAX_CONSECUTIVE_CRITICAL_ADMISSIONS
312
+ ? this.#waiting.ordinary.shift()
313
+ : undefined;
314
+ const waiter =
315
+ ordinary ??
316
+ this.#waiting.critical.shift() ??
317
+ this.#waiting.ordinary.shift();
318
+ if (!waiter) return;
319
+ clearTimeout(waiter.timeout);
320
+ waiter.signal?.removeEventListener('abort', waiter.onAbort!);
321
+ this.#consecutiveCritical =
322
+ waiter.priority === 'critical' ? this.#consecutiveCritical + 1 : 0;
323
+ waiter.resolve(this.#grant());
324
+ }
325
+ }
326
+ }
327
+
328
+ type PoolEntry = {
329
+ pool: Pool;
330
+ maxClients: number;
331
+ admission: RuntimeSchedulerPoolAdmission;
332
+ breaker: BreakerState;
333
+ stats: {
334
+ acquires: number;
335
+ acquireWait: LatencyRing;
336
+ saturationRejections: number;
337
+ capacityRejections: number;
338
+ breakerRejections: number;
339
+ connectionErrors: number;
340
+ queryErrors: number;
341
+ retries: number;
342
+ peakTotalClients: number;
343
+ peakWaitingClients: number;
344
+ };
345
+ };
346
+
347
+ const poolRegistryKey = Symbol.for(
348
+ 'deepline.runtime-scheduler.postgres-pool.registry',
349
+ );
350
+
351
+ function getPoolRegistry(): Map<string, PoolEntry> {
352
+ const globalWithRegistry = globalThis as typeof globalThis & {
353
+ [poolRegistryKey]?: Map<string, PoolEntry>;
354
+ };
355
+ globalWithRegistry[poolRegistryKey] ??= new Map();
356
+ return globalWithRegistry[poolRegistryKey];
357
+ }
358
+
359
+ /**
360
+ * Connection-category errors are safe to retry at the operation level because
361
+ * every scheduler operation is idempotent (ON CONFLICT guards, lease tokens).
362
+ */
363
+ export function isConnectionCategoryError(error: unknown): boolean {
364
+ const seen = new Set<object>();
365
+ const inspect = (candidate: unknown): boolean => {
366
+ if (!candidate || typeof candidate !== 'object' || seen.has(candidate)) {
367
+ return false;
368
+ }
369
+ seen.add(candidate);
370
+
371
+ if (
372
+ candidate instanceof AggregateError &&
373
+ [...candidate.errors].some(inspect)
374
+ ) {
375
+ return true;
376
+ }
377
+ const cause = (candidate as { cause?: unknown }).cause;
378
+ if (inspect(cause)) return true;
379
+
380
+ const code = (candidate as { code?: unknown }).code;
381
+ if (typeof code === 'string') {
382
+ // 08* = connection exceptions; 57P01-57P03 = server shutdown/crash.
383
+ // 53300 is deliberately NOT here: PgBouncer admission pressure is a
384
+ // capacity signal, so durable work must defer instead of opening the
385
+ // connection-failure circuit and burning execution attempts.
386
+ if (code.startsWith('08')) return true;
387
+ if (code === '57P01' || code === '57P02' || code === '57P03') {
388
+ return true;
389
+ }
390
+ if (
391
+ code === 'ECONNRESET' ||
392
+ code === 'ECONNREFUSED' ||
393
+ code === 'ETIMEDOUT' ||
394
+ code === 'EPIPE' ||
395
+ code === 'ENOTFOUND'
396
+ ) {
397
+ return true;
398
+ }
399
+ }
400
+ const message = (candidate as { message?: unknown }).message;
401
+ if (typeof message === 'string') {
402
+ return (
403
+ message.includes('Connection terminated') ||
404
+ /\bterminating connection\b/i.test(message) ||
405
+ message.includes('Client has encountered a connection error') ||
406
+ message.includes('connection timeout') ||
407
+ message.includes('Query read timeout') ||
408
+ // This is the scheduler pooler's own upstream-connect failure. It is
409
+ // observed before a PostgreSQL session exists, so retrying this
410
+ // idempotent scheduler operation cannot replay customer code. Keep the
411
+ // normalization at the DB adapter boundary rather than teaching launch
412
+ // callers to parse an infrastructure string.
413
+ /\bfailed to connect to upstream\b/i.test(message)
414
+ );
415
+ }
416
+ return false;
417
+ };
418
+ return inspect(error);
419
+ }
420
+
421
+ /**
422
+ * These PostgreSQL errors abort only the current transaction. Scheduler
423
+ * operations already carry stable operation identities, so re-running the
424
+ * whole operation is safe and is materially better than turning a one-off
425
+ * lock conflict into a failed Play or an unhealthy Machine.
426
+ */
427
+ export function isRuntimeSchedulerRetryableTransactionError(
428
+ error: unknown,
429
+ ): boolean {
430
+ if (!error || typeof error !== 'object') return false;
431
+ const code = (error as { code?: unknown }).code;
432
+ return code === '40001' || code === '40P01' || code === '55P03';
433
+ }
434
+
435
+ /**
436
+ * node-postgres' pool emits this exact lowercase message only when a caller
437
+ * times out waiting in the local checkout queue. A new physical connection
438
+ * timing out is rewritten to `Connection terminated due to connection
439
+ * timeout`, so it must remain a breaker-eligible connection failure.
440
+ */
441
+ export function isRuntimeSchedulerLocalAcquireTimeout(error: unknown): boolean {
442
+ return (
443
+ error instanceof Error &&
444
+ error.message === 'timeout exceeded when trying to connect'
445
+ );
446
+ }
447
+
448
+ /**
449
+ * Connection-capacity exhaustion is a *retryable saturation* signal, not a
450
+ * broken connection: callers that own durable work must defer instead of
451
+ * burning an execution attempt or opening the connection-failure circuit.
452
+ *
453
+ * Two distinct sources land here:
454
+ * - PgBouncer admission pressure (`max_client_conn`) — SQLSTATE 53300, or a
455
+ * message mentioning `max_client_conn`.
456
+ * - Raw Postgres `max_connections` exhaustion when the runtime DB is reached
457
+ * directly (no pooler): also SQLSTATE 53300 (`too_many_connections`), but
458
+ * at *connect* time node-postgres does not always preserve `.code`, so the
459
+ * Postgres-core message forms are matched too. The canonical form is
460
+ * "remaining connection slots are reserved for roles with the SUPERUSER
461
+ * attribute"; `too many clients already` is the pgbouncer-side twin.
462
+ * Kept function name for call-site stability; it now covers both pooler and
463
+ * direct-Postgres capacity exhaustion.
464
+ */
465
+ export function isPgBouncerCapacityError(error: unknown): boolean {
466
+ if (!error || typeof error !== 'object') return false;
467
+ if ((error as { code?: unknown }).code === '53300') return true;
468
+ const message = (error as { message?: unknown }).message;
469
+ return (
470
+ typeof message === 'string' &&
471
+ /(?:max_client_conn|too many connections|too many clients|connection limit|remaining connection slots|reserved for roles with the superuser|sorry, too many clients)/i.test(
472
+ message,
473
+ )
474
+ );
475
+ }
476
+
477
+ function recordCapacityRejection(entry: PoolEntry, error: unknown): void {
478
+ entry.stats.capacityRejections += 1;
479
+ console.warn('[runtime-scheduler-pool.capacity_rejected]', {
480
+ error: error instanceof Error ? error.message : String(error),
481
+ maxClients: entry.maxClients,
482
+ totalClients: entry.pool.totalCount,
483
+ idleClients: entry.pool.idleCount,
484
+ waitingClients: entry.pool.waitingCount,
485
+ });
486
+ }
487
+
488
+ function getPoolEntry(options: RuntimeSchedulerPoolOptions): PoolEntry {
489
+ assertRuntimeSchedulerHotPathUrl(options.url);
490
+ const maxClients =
491
+ options.maxClients ?? RUNTIME_SCHEDULER_POOL_DEFAULTS.maxClients;
492
+ if (maxClients > RUNTIME_SCHEDULER_POOL_DEFAULTS.maxClientsHardCap) {
493
+ throw new Error(
494
+ `Runtime scheduler pool maxClients=${maxClients} exceeds the hard cap of ` +
495
+ `${RUNTIME_SCHEDULER_POOL_DEFAULTS.maxClientsHardCap}. Scale worker replicas ` +
496
+ 'instead of widening a single process pool.',
497
+ );
498
+ }
499
+ const registry = getPoolRegistry();
500
+ const connectionString = normalizePostgresConnectionStringForPg(options.url);
501
+ const key = `${connectionString}::${maxClients}`;
502
+ const existing = registry.get(key);
503
+ if (existing) {
504
+ // Next dev can retain the process-global pool across a module hot reload.
505
+ // Upgrade that live entry in place when this admission layer first lands.
506
+ existing.admission ??= new RuntimeSchedulerPoolAdmission(
507
+ maxClients,
508
+ RUNTIME_SCHEDULER_POOL_DEFAULTS.maxAcquireQueueDepth,
509
+ );
510
+ return existing;
511
+ }
512
+
513
+ const pool = new Pool({
514
+ connectionString,
515
+ application_name:
516
+ options.applicationName ?? 'deepline-runtime-scheduler-pool',
517
+ max: maxClients,
518
+ idleTimeoutMillis: RUNTIME_SCHEDULER_POOL_DEFAULTS.idleTimeoutMs,
519
+ connectionTimeoutMillis: RUNTIME_SCHEDULER_POOL_DEFAULTS.acquireTimeoutMs,
520
+ query_timeout: RUNTIME_SCHEDULER_POOL_DEFAULTS.queryTimeoutMs,
521
+ keepAlive: true,
522
+ });
523
+ const entry: PoolEntry = {
524
+ pool,
525
+ maxClients,
526
+ admission: new RuntimeSchedulerPoolAdmission(
527
+ maxClients,
528
+ RUNTIME_SCHEDULER_POOL_DEFAULTS.maxAcquireQueueDepth,
529
+ ),
530
+ breaker: {
531
+ consecutiveFailures: 0,
532
+ openedAt: null,
533
+ lastFailureMessage: '',
534
+ halfOpenInFlight: false,
535
+ },
536
+ stats: {
537
+ acquires: 0,
538
+ acquireWait: { samples: [], next: 0 },
539
+ saturationRejections: 0,
540
+ capacityRejections: 0,
541
+ breakerRejections: 0,
542
+ connectionErrors: 0,
543
+ queryErrors: 0,
544
+ retries: 0,
545
+ peakTotalClients: 0,
546
+ peakWaitingClients: 0,
547
+ },
548
+ };
549
+ pool.on('error', (error) => {
550
+ // Idle clients dropped by the server (failover, suspend) surface here.
551
+ // The pool replaces them on next acquire; record and move on.
552
+ entry.stats.connectionErrors += 1;
553
+ console.warn('[runtime-scheduler-pool.idle_client_error]', {
554
+ error: error instanceof Error ? error.message : String(error),
555
+ });
556
+ });
557
+ registry.set(key, entry);
558
+ return entry;
559
+ }
560
+
561
+ function breakerGate(entry: PoolEntry): void {
562
+ const { breaker } = entry;
563
+ if (breaker.openedAt === null) return;
564
+ const elapsed = Date.now() - breaker.openedAt;
565
+ if (elapsed < RUNTIME_SCHEDULER_POOL_DEFAULTS.breaker.openMs) {
566
+ entry.stats.breakerRejections += 1;
567
+ throw new RuntimeSchedulerCircuitOpenError(breaker.lastFailureMessage);
568
+ }
569
+ // Half-open: let exactly one caller through as the trial.
570
+ if (breaker.halfOpenInFlight) {
571
+ entry.stats.breakerRejections += 1;
572
+ throw new RuntimeSchedulerCircuitOpenError(breaker.lastFailureMessage);
573
+ }
574
+ breaker.halfOpenInFlight = true;
575
+ }
576
+
577
+ function recordBreakerSuccess(entry: PoolEntry): void {
578
+ entry.breaker.consecutiveFailures = 0;
579
+ entry.breaker.openedAt = null;
580
+ entry.breaker.halfOpenInFlight = false;
581
+ }
582
+
583
+ function recordBreakerFailure(entry: PoolEntry, error: unknown): void {
584
+ const { breaker } = entry;
585
+ breaker.halfOpenInFlight = false;
586
+ breaker.consecutiveFailures += 1;
587
+ breaker.lastFailureMessage =
588
+ error instanceof Error ? error.message : String(error);
589
+ if (
590
+ breaker.consecutiveFailures >=
591
+ RUNTIME_SCHEDULER_POOL_DEFAULTS.breaker.threshold
592
+ ) {
593
+ breaker.openedAt = Date.now();
594
+ }
595
+ }
596
+
597
+ function jitteredBackoffMs(attempt: number): number {
598
+ const base = Math.min(
599
+ RUNTIME_SCHEDULER_POOL_DEFAULTS.retry.maxDelayMs,
600
+ RUNTIME_SCHEDULER_POOL_DEFAULTS.retry.baseDelayMs * 2 ** attempt,
601
+ );
602
+ return Math.round(base / 2 + Math.random() * (base / 2));
603
+ }
604
+
605
+ function sleep(ms: number): Promise<void> {
606
+ return new Promise((resolve) => setTimeout(resolve, ms));
607
+ }
608
+
609
+ function schedulerAbortReason(signal: AbortSignal): Error {
610
+ return signal.reason instanceof Error
611
+ ? signal.reason
612
+ : new Error('Runtime scheduler operation was cancelled.');
613
+ }
614
+
615
+ function throwIfSchedulerAborted(signal: AbortSignal | undefined): void {
616
+ if (signal?.aborted) throw schedulerAbortReason(signal);
617
+ }
618
+
619
+ /**
620
+ * Run `fn` with a dedicated pooled connection. The connection is held for the
621
+ * duration of `fn` so multi-statement transactions (BEGIN..COMMIT) stay on one
622
+ * session. Connection-category failures destroy the bad connection and retry
623
+ * the whole operation with jittered backoff — safe because scheduler
624
+ * operations are idempotent by construction.
625
+ */
626
+ export async function withRuntimeSchedulerClient<T>(
627
+ options: RuntimeSchedulerPoolOptions,
628
+ fn: (client: RuntimeSchedulerQueryClient) => Promise<T>,
629
+ trace?: {
630
+ runId: string;
631
+ phase: string;
632
+ priority?: RuntimeSchedulerPoolPriority;
633
+ },
634
+ operationOptions: { signal?: AbortSignal } = {},
635
+ ): Promise<T> {
636
+ const signal = operationOptions.signal;
637
+ throwIfSchedulerAborted(signal);
638
+ const entry = getPoolEntry(options);
639
+ const operationStartedAt = performance.now();
640
+ const admissionStartedAt = performance.now();
641
+ const releaseAdmission = await entry.admission.acquire(
642
+ trace?.priority ?? 'ordinary',
643
+ signal,
644
+ );
645
+ const admissionWaitMs = performance.now() - admissionStartedAt;
646
+ let lastError: unknown = null;
647
+ try {
648
+ throwIfSchedulerAborted(signal);
649
+ for (
650
+ let attempt = 0;
651
+ attempt < RUNTIME_SCHEDULER_POOL_DEFAULTS.retry.attempts;
652
+ attempt += 1
653
+ ) {
654
+ throwIfSchedulerAborted(signal);
655
+ breakerGate(entry);
656
+ if (
657
+ entry.pool.waitingCount >=
658
+ RUNTIME_SCHEDULER_POOL_DEFAULTS.maxAcquireQueueDepth
659
+ ) {
660
+ entry.stats.saturationRejections += 1;
661
+ throw new RuntimeSchedulerSaturatedError(entry.pool.waitingCount);
662
+ }
663
+ const acquireStart = performance.now();
664
+ entry.stats.peakTotalClients = Math.max(
665
+ entry.stats.peakTotalClients,
666
+ entry.pool.totalCount,
667
+ );
668
+ entry.stats.peakWaitingClients = Math.max(
669
+ entry.stats.peakWaitingClients,
670
+ entry.pool.waitingCount,
671
+ );
672
+ let poolClient;
673
+ try {
674
+ poolClient = await entry.pool.connect();
675
+ } catch (error) {
676
+ if (isRuntimeSchedulerLocalAcquireTimeout(error)) {
677
+ entry.stats.saturationRejections += 1;
678
+ console.warn('[runtime-scheduler-pool.local_acquire_saturated]', {
679
+ maxClients: entry.maxClients,
680
+ totalClients: entry.pool.totalCount,
681
+ idleClients: entry.pool.idleCount,
682
+ waitingClients: entry.pool.waitingCount,
683
+ });
684
+ throw new RuntimeSchedulerSaturatedError(entry.pool.waitingCount);
685
+ }
686
+ if (isPgBouncerCapacityError(error)) {
687
+ recordCapacityRejection(entry, error);
688
+ throw new RuntimeSchedulerPgBouncerCapacityError(
689
+ error instanceof Error ? error.message : String(error),
690
+ );
691
+ }
692
+ recordBreakerFailure(entry, error);
693
+ entry.stats.connectionErrors += 1;
694
+ lastError = error;
695
+ if (attempt + 1 < RUNTIME_SCHEDULER_POOL_DEFAULTS.retry.attempts) {
696
+ entry.stats.retries += 1;
697
+ await sleep(jitteredBackoffMs(attempt));
698
+ throwIfSchedulerAborted(signal);
699
+ continue;
700
+ }
701
+ throw error;
702
+ }
703
+ entry.stats.acquires += 1;
704
+ const acquireWaitMs = performance.now() - acquireStart;
705
+ entry.stats.peakTotalClients = Math.max(
706
+ entry.stats.peakTotalClients,
707
+ entry.pool.totalCount,
708
+ );
709
+ pushSample(entry.stats.acquireWait, acquireWaitMs);
710
+
711
+ // A checked-out pg client emits 'error' (not via the pool) when its TCP
712
+ // connection dies while idle — e.g. a worker holding a client across a
713
+ // long play execution. With no listener that is an uncaught exception
714
+ // that kills the process (observed: worker crash mid-150k-row run).
715
+ // Swallow it here; the next query on the dead client fails normally and
716
+ // flows through the connection-error retry/poison path below.
717
+ let checkedOutError: unknown = null;
718
+ const onCheckedOutError = (error: Error) => {
719
+ checkedOutError = error;
720
+ entry.stats.connectionErrors += 1;
721
+ console.warn('[runtime-scheduler-pool.checked_out_client_error]', {
722
+ error: error.message,
723
+ });
724
+ };
725
+ poolClient.on('error', onCheckedOutError);
726
+ let clientReleased = false;
727
+ const releasePoolClient = (error?: Error) => {
728
+ if (clientReleased) return;
729
+ clientReleased = true;
730
+ poolClient.release(error);
731
+ };
732
+ const onAbort = () => releasePoolClient(schedulerAbortReason(signal!));
733
+ signal?.addEventListener('abort', onAbort, { once: true });
734
+
735
+ let queryCount = 0;
736
+ let queryMs = 0;
737
+ try {
738
+ throwIfSchedulerAborted(signal);
739
+ const result = await fn({
740
+ query: async (sql, params) => {
741
+ throwIfSchedulerAborted(signal);
742
+ const queryStartedAt = performance.now();
743
+ queryCount += 1;
744
+ try {
745
+ const queryResult = await poolClient.query(sql, params);
746
+ throwIfSchedulerAborted(signal);
747
+ return { rows: queryResult.rows };
748
+ } finally {
749
+ queryMs += performance.now() - queryStartedAt;
750
+ }
751
+ },
752
+ });
753
+ throwIfSchedulerAborted(signal);
754
+ recordBreakerSuccess(entry);
755
+ signal?.removeEventListener('abort', onAbort);
756
+ poolClient.removeListener('error', onCheckedOutError);
757
+ // The connection died mid-fn but fn happened to finish without another
758
+ // query — destroy the dead connection rather than pooling it.
759
+ releasePoolClient(
760
+ checkedOutError instanceof Error ? checkedOutError : undefined,
761
+ );
762
+ if (trace) {
763
+ console.info(
764
+ '[runtime-scheduler-pool.operation] ' +
765
+ JSON.stringify({
766
+ runId: trace.runId,
767
+ phase: trace.phase,
768
+ outcome: 'success',
769
+ attempt: attempt + 1,
770
+ priority: trace.priority ?? 'ordinary',
771
+ admissionWaitMs,
772
+ acquireWaitMs,
773
+ queryCount,
774
+ queryMs,
775
+ totalMs: performance.now() - operationStartedAt,
776
+ }),
777
+ );
778
+ }
779
+ return result;
780
+ } catch (error) {
781
+ if (trace) {
782
+ console.info(
783
+ '[runtime-scheduler-pool.operation] ' +
784
+ JSON.stringify({
785
+ runId: trace.runId,
786
+ phase: trace.phase,
787
+ outcome: 'failed',
788
+ attempt: attempt + 1,
789
+ priority: trace.priority ?? 'ordinary',
790
+ admissionWaitMs,
791
+ acquireWaitMs,
792
+ queryCount,
793
+ queryMs,
794
+ totalMs: performance.now() - operationStartedAt,
795
+ errorName: error instanceof Error ? error.name : 'UnknownError',
796
+ }),
797
+ );
798
+ }
799
+ signal?.removeEventListener('abort', onAbort);
800
+ poolClient.removeListener('error', onCheckedOutError);
801
+ if (signal?.aborted) {
802
+ const cancellation = schedulerAbortReason(signal);
803
+ releasePoolClient(cancellation);
804
+ throw cancellation;
805
+ }
806
+ if (isPgBouncerCapacityError(error)) {
807
+ recordCapacityRejection(entry, error);
808
+ releasePoolClient();
809
+ throw new RuntimeSchedulerPgBouncerCapacityError(
810
+ error instanceof Error ? error.message : String(error),
811
+ );
812
+ }
813
+ if (isConnectionCategoryError(error)) {
814
+ recordBreakerFailure(entry, error);
815
+ entry.stats.connectionErrors += 1;
816
+ lastError = error;
817
+ // Destroy the broken connection instead of returning it to the pool.
818
+ releasePoolClient(
819
+ error instanceof Error ? error : new Error(String(error)),
820
+ );
821
+ if (attempt + 1 < RUNTIME_SCHEDULER_POOL_DEFAULTS.retry.attempts) {
822
+ entry.stats.retries += 1;
823
+ await sleep(jitteredBackoffMs(attempt));
824
+ throwIfSchedulerAborted(signal);
825
+ continue;
826
+ }
827
+ throw error;
828
+ }
829
+ if (isRuntimeSchedulerRetryableTransactionError(error)) {
830
+ // PostgreSQL has aborted the transaction, so never return this
831
+ // client to the pool. Re-run the idempotent scheduler operation on a
832
+ // new session without opening the connection-failure breaker.
833
+ entry.stats.queryErrors += 1;
834
+ recordBreakerSuccess(entry);
835
+ lastError = error;
836
+ releasePoolClient(
837
+ error instanceof Error ? error : new Error(String(error)),
838
+ );
839
+ if (attempt + 1 < RUNTIME_SCHEDULER_POOL_DEFAULTS.retry.attempts) {
840
+ entry.stats.retries += 1;
841
+ await sleep(jitteredBackoffMs(attempt));
842
+ throwIfSchedulerAborted(signal);
843
+ continue;
844
+ }
845
+ throw error;
846
+ }
847
+ // Query-level failure on a healthy connection: count it, return the
848
+ // connection to the pool, surface the error without retry.
849
+ entry.stats.queryErrors += 1;
850
+ recordBreakerSuccess(entry);
851
+ releasePoolClient();
852
+ throw error;
853
+ }
854
+ }
855
+ throw lastError instanceof Error
856
+ ? lastError
857
+ : new Error('Runtime scheduler pool operation failed after retries.');
858
+ } finally {
859
+ releaseAdmission();
860
+ }
861
+ }
862
+
863
+ export type RuntimeSchedulerPoolStats = {
864
+ url: string;
865
+ maxClients: number;
866
+ totalCount: number;
867
+ idleCount: number;
868
+ waitingCount: number;
869
+ acquires: number;
870
+ acquireWaitP50Ms: number | null;
871
+ acquireWaitP95Ms: number | null;
872
+ saturationRejections: number;
873
+ capacityRejections: number;
874
+ breakerRejections: number;
875
+ breakerOpen: boolean;
876
+ connectionErrors: number;
877
+ queryErrors: number;
878
+ retries: number;
879
+ peakTotalClients: number;
880
+ peakWaitingClients: number;
881
+ };
882
+
883
+ export function getRuntimeSchedulerPoolStats(): RuntimeSchedulerPoolStats[] {
884
+ const registry = getPoolRegistry();
885
+ return [...registry.entries()].map(([key, entry]) => ({
886
+ url: key.split('::')[0]!.replace(/:\/\/[^@]*@/, '://***@'),
887
+ maxClients: entry.maxClients,
888
+ totalCount: entry.pool.totalCount,
889
+ idleCount: entry.pool.idleCount,
890
+ waitingCount: entry.admission.waitingCount,
891
+ acquires: entry.stats.acquires,
892
+ acquireWaitP50Ms: ringQuantile(entry.stats.acquireWait, 0.5),
893
+ acquireWaitP95Ms: ringQuantile(entry.stats.acquireWait, 0.95),
894
+ saturationRejections: entry.stats.saturationRejections,
895
+ capacityRejections: entry.stats.capacityRejections,
896
+ breakerRejections: entry.stats.breakerRejections,
897
+ breakerOpen: entry.breaker.openedAt !== null,
898
+ connectionErrors: entry.stats.connectionErrors,
899
+ queryErrors: entry.stats.queryErrors,
900
+ retries: entry.stats.retries,
901
+ peakTotalClients: entry.stats.peakTotalClients,
902
+ peakWaitingClients: entry.stats.peakWaitingClients,
903
+ }));
904
+ }
905
+
906
+ export async function closeRuntimeSchedulerPools(): Promise<void> {
907
+ const registry = getPoolRegistry();
908
+ const entries = [...registry.values()];
909
+ registry.clear();
910
+ await Promise.all([
911
+ ...entries.map((entry) => entry.pool.end()),
912
+ closePgNotifyHubs(),
913
+ ]);
914
+ }
915
+
916
+ /**
917
+ * Warm one pooled connection (TLS + auth + first query) so the first real
918
+ * operation does not pay the cold-connect cost. Also wakes a suspended Neon
919
+ * compute before user-visible traffic arrives.
920
+ */
921
+ export async function warmRuntimeSchedulerPool(
922
+ options: RuntimeSchedulerPoolOptions,
923
+ ): Promise<void> {
924
+ await withRuntimeSchedulerClient(options, async (client) => {
925
+ await client.query('SELECT 1');
926
+ });
927
+ }
928
+
929
+ /**
930
+ * Periodic SELECT 1 to keep long-lived worker connections healthy. Returns a
931
+ * stop function.
932
+ */
933
+ export function startRuntimeSchedulerKeepalive(
934
+ options: RuntimeSchedulerPoolOptions,
935
+ ): () => void {
936
+ const interval = setInterval(() => {
937
+ warmRuntimeSchedulerPool(options).catch((error) => {
938
+ console.warn('[runtime-scheduler-pool.keepalive_failed]', {
939
+ error: error instanceof Error ? error.message : String(error),
940
+ });
941
+ });
942
+ }, RUNTIME_SCHEDULER_POOL_DEFAULTS.keepAliveIntervalMs);
943
+ interval.unref?.();
944
+ return () => clearInterval(interval);
945
+ }
946
+
947
+ /**
948
+ * LISTEN/NOTIFY is unavailable through PgBouncer transaction pooling, so the
949
+ * hub connects to the direct host when one is configured. Neon-style `-pooler`
950
+ * hosts are normalized for the legacy scheduler path; other managed Postgres
951
+ * URLs are returned unchanged.
952
+ */
953
+ export function deriveDirectDatabaseUrl(url: string): string {
954
+ const explicit =
955
+ process.env.WORKFLOW_RUNTIME_POSTGRES_DIRECT?.trim() ||
956
+ process.env.DEEPLINE_RUNTIME_DATABASE_DIRECT_URL?.trim();
957
+ if (explicit) return normalizePostgresConnectionStringForPg(explicit);
958
+ try {
959
+ const parsed = new URL(url);
960
+ if (isPlanetScalePostgresUrl(parsed) && parsed.port === '6432') {
961
+ parsed.port = '5432';
962
+ }
963
+ parsed.hostname = parsed.hostname.replace('-pooler.', '.');
964
+ return normalizePostgresConnectionStringForPg(parsed.toString());
965
+ } catch {
966
+ return normalizePostgresConnectionStringForPg(url);
967
+ }
968
+ }
969
+
970
+ /**
971
+ * The runtime worker needs one direct Postgres session for its optional
972
+ * LISTEN wake hint. Preview gates must prove that this separate session budget
973
+ * is available before submitting a workload: the pooled scheduler probe alone
974
+ * cannot observe direct-session exhaustion.
975
+ */
976
+ export class RuntimeSchedulerDirectSessionCapacityError extends Error {
977
+ constructor(reason: string) {
978
+ super(
979
+ 'Runtime scheduler direct-session capacity is unavailable. ' +
980
+ 'Reclaim stale Preview worker listener owners or use an isolated direct-session endpoint. ' +
981
+ `Database error: ${reason}`,
982
+ );
983
+ this.name = 'RuntimeSchedulerDirectSessionCapacityError';
984
+ }
985
+ }
986
+
987
+ export type RuntimeSchedulerDirectSessionClient = Pick<
988
+ Client,
989
+ 'connect' | 'query' | 'end'
990
+ >;
991
+
992
+ export async function probeRuntimeSchedulerDirectSession(input: {
993
+ url: string;
994
+ applicationName?: string;
995
+ createClient?: (
996
+ config: ConstructorParameters<typeof Client>[0],
997
+ ) => RuntimeSchedulerDirectSessionClient;
998
+ }): Promise<void> {
999
+ const client = (input.createClient ?? ((config) => new Client(config)))({
1000
+ connectionString: deriveDirectDatabaseUrl(input.url),
1001
+ application_name:
1002
+ input.applicationName ?? 'deepline-runtime-direct-session-preflight',
1003
+ });
1004
+ try {
1005
+ await client.connect();
1006
+ await client.query('select 1');
1007
+ } catch (error) {
1008
+ if (isPgBouncerCapacityError(error)) {
1009
+ throw new RuntimeSchedulerDirectSessionCapacityError(
1010
+ error instanceof Error ? error.message : String(error),
1011
+ );
1012
+ }
1013
+ throw error;
1014
+ } finally {
1015
+ await client.end().catch(() => undefined);
1016
+ }
1017
+ }
1018
+
1019
+ /**
1020
+ * `standby` means this process lost an optional session advisory-lock election
1021
+ * and deliberately closed its direct connection. It will retry the election
1022
+ * later; callers continue with their durable polling fallback meanwhile.
1023
+ */
1024
+ export type PgNotifyHubState = 'connecting' | 'listening' | 'standby' | 'down';
1025
+
1026
+ export type PgNotifyHub = {
1027
+ state: () => PgNotifyHubState;
1028
+ subscribe: (handler: (payload: string) => void) => () => void;
1029
+ subscribeState: (handler: (state: PgNotifyHubState) => void) => () => void;
1030
+ stop: () => Promise<void>;
1031
+ };
1032
+
1033
+ type PgNotifyHubEntry = {
1034
+ hub: PgNotifyHub;
1035
+ refs: number;
1036
+ idleTimer: ReturnType<typeof setTimeout> | null;
1037
+ lastUsedAt: number;
1038
+ closing: Promise<boolean> | null;
1039
+ };
1040
+
1041
+ export type PgNotifyHubLease = {
1042
+ hub: PgNotifyHub;
1043
+ release: () => void;
1044
+ };
1045
+
1046
+ const pgNotifyHubsKey = Symbol.for('deepline.runtime-scheduler.pg-notify-hubs');
1047
+ const PG_NOTIFY_HUB_IDLE_MS = 30_000;
1048
+ const PG_NOTIFY_HUB_MAX_PER_PROCESS = 4;
1049
+
1050
+ function getPgNotifyHubs(): Map<string, PgNotifyHubEntry> {
1051
+ const target = globalThis as typeof globalThis & {
1052
+ [pgNotifyHubsKey]?: Map<string, PgNotifyHubEntry>;
1053
+ };
1054
+ target[pgNotifyHubsKey] ??= new Map();
1055
+ return target[pgNotifyHubsKey];
1056
+ }
1057
+
1058
+ function beginPgNotifyHubClose(
1059
+ hubs: Map<string, PgNotifyHubEntry>,
1060
+ key: string,
1061
+ entry: PgNotifyHubEntry,
1062
+ ): void {
1063
+ if (entry.closing) return;
1064
+ if (entry.idleTimer) clearTimeout(entry.idleTimer);
1065
+ entry.idleTimer = null;
1066
+ entry.closing = entry.hub
1067
+ .stop()
1068
+ .then(() => {
1069
+ if (hubs.get(key) === entry) hubs.delete(key);
1070
+ return true;
1071
+ })
1072
+ .catch((error) => {
1073
+ // Keep the failed close occupying its slot. Polling remains available,
1074
+ // and opening a replacement could exceed the physical session budget.
1075
+ console.warn('[runtime-scheduler-notify-hub.close_failed]', {
1076
+ channel: key.split('::').at(-1) ?? key,
1077
+ error: error instanceof Error ? error.message : String(error),
1078
+ });
1079
+ return false;
1080
+ });
1081
+ }
1082
+
1083
+ /**
1084
+ * Lease a bounded direct LISTEN connection. When every slot is actively leased,
1085
+ * additional callers receive null and safely use table polling instead.
1086
+ */
1087
+ export function acquirePgNotifyHub(
1088
+ input: {
1089
+ url: string;
1090
+ channel: string;
1091
+ applicationName?: string;
1092
+ leaderLockKey?: string;
1093
+ },
1094
+ lifecycle: {
1095
+ idleMs?: number;
1096
+ create?: (input: {
1097
+ url: string;
1098
+ channel: string;
1099
+ applicationName?: string;
1100
+ leaderLockKey?: string;
1101
+ }) => PgNotifyHub;
1102
+ } = {},
1103
+ ): PgNotifyHubLease | null {
1104
+ const hubs = getPgNotifyHubs();
1105
+ const key = `${input.url}::${input.channel}::${input.leaderLockKey ?? ''}`;
1106
+ let entry = hubs.get(key);
1107
+ if (entry?.closing) return null;
1108
+ if (!entry && hubs.size >= PG_NOTIFY_HUB_MAX_PER_PROCESS) {
1109
+ const idle = [...hubs.entries()]
1110
+ .filter(([, candidate]) => candidate.refs === 0 && !candidate.closing)
1111
+ .sort((left, right) => left[1].lastUsedAt - right[1].lastUsedAt)[0];
1112
+ if (!idle) return null;
1113
+ const [idleKey, idleEntry] = idle;
1114
+ beginPgNotifyHubClose(hubs, idleKey, idleEntry);
1115
+ // Keep the closing entry in the registry until its physical connection is
1116
+ // gone. This caller polls and may acquire the freed slot on its next pass.
1117
+ return null;
1118
+ }
1119
+ if (!entry) {
1120
+ entry = {
1121
+ hub: (lifecycle.create ?? createPgNotifyHub)(input),
1122
+ refs: 0,
1123
+ idleTimer: null,
1124
+ lastUsedAt: Date.now(),
1125
+ closing: null,
1126
+ };
1127
+ hubs.set(key, entry);
1128
+ }
1129
+ if (entry.idleTimer) {
1130
+ clearTimeout(entry.idleTimer);
1131
+ entry.idleTimer = null;
1132
+ }
1133
+ entry.refs += 1;
1134
+ let released = false;
1135
+ return {
1136
+ hub: entry.hub,
1137
+ release: () => {
1138
+ if (released) return;
1139
+ released = true;
1140
+ entry!.refs = Math.max(0, entry!.refs - 1);
1141
+ entry!.lastUsedAt = Date.now();
1142
+ if (entry!.refs > 0) return;
1143
+ entry!.idleTimer = setTimeout(() => {
1144
+ if (entry!.refs > 0 || hubs.get(key) !== entry) return;
1145
+ beginPgNotifyHubClose(hubs, key, entry!);
1146
+ }, lifecycle.idleMs ?? PG_NOTIFY_HUB_IDLE_MS);
1147
+ entry!.idleTimer.unref?.();
1148
+ },
1149
+ };
1150
+ }
1151
+
1152
+ export async function closePgNotifyHubs(): Promise<void> {
1153
+ const hubs = getPgNotifyHubs();
1154
+ const entries = [...hubs.entries()];
1155
+ for (const [, entry] of entries) {
1156
+ if (entry.idleTimer) clearTimeout(entry.idleTimer);
1157
+ }
1158
+ for (const [key, entry] of entries) {
1159
+ beginPgNotifyHubClose(hubs, key, entry);
1160
+ }
1161
+ const firstResults = await Promise.all(
1162
+ entries.map(([, entry]) => entry.closing ?? Promise.resolve(false)),
1163
+ );
1164
+ const retryErrors: unknown[] = [];
1165
+ await Promise.all(
1166
+ entries.map(async ([key, entry], index) => {
1167
+ if (firstResults[index]) return;
1168
+ try {
1169
+ await entry.hub.stop();
1170
+ if (hubs.get(key) === entry) hubs.delete(key);
1171
+ } catch (error) {
1172
+ retryErrors.push(error);
1173
+ }
1174
+ }),
1175
+ );
1176
+ if (retryErrors.length > 0) {
1177
+ throw new AggregateError(
1178
+ retryErrors,
1179
+ 'Failed to close one or more runtime notification hubs.',
1180
+ );
1181
+ }
1182
+ }
1183
+
1184
+ const NOTIFY_HUB_RECONNECT_BASE_MS = 250;
1185
+ const NOTIFY_HUB_CAPACITY_RECONNECT_BASE_MS = 30_000;
1186
+ const NOTIFY_HUB_RECONNECT_MAX_MS = 300_000;
1187
+ const NOTIFY_HUB_STANDBY_RETRY_MS = 30_000;
1188
+
1189
+ export function runtimeSchedulerNotifyReconnectDelayMs(input: {
1190
+ error: unknown;
1191
+ attempt: number;
1192
+ random?: number;
1193
+ }): number {
1194
+ const baseMs = isPgBouncerCapacityError(input.error)
1195
+ ? NOTIFY_HUB_CAPACITY_RECONNECT_BASE_MS
1196
+ : NOTIFY_HUB_RECONNECT_BASE_MS;
1197
+ const ceilingMs = Math.min(
1198
+ NOTIFY_HUB_RECONNECT_MAX_MS,
1199
+ baseMs * 2 ** Math.max(0, input.attempt),
1200
+ );
1201
+ const random = Math.min(1, Math.max(0, input.random ?? Math.random()));
1202
+ return Math.max(1, Math.round(ceilingMs / 2 + random * (ceilingMs / 2)));
1203
+ }
1204
+
1205
+ /**
1206
+ * One long-lived LISTEN connection per (url, channel) with auto-reconnect.
1207
+ * Strictly a wakeup hint: consumers must still read the tables for truth, so
1208
+ * a down hub degrades to polling latency, never to wrong answers.
1209
+ */
1210
+ export function createPgNotifyHub(input: {
1211
+ url: string;
1212
+ channel: string;
1213
+ applicationName?: string;
1214
+ /**
1215
+ * When set, only the session holding this advisory lock LISTENs. Losing
1216
+ * candidates close their direct connection immediately and retry later.
1217
+ * This keeps a horizontally scaled worker lane to one steady direct session.
1218
+ */
1219
+ leaderLockKey?: string;
1220
+ /** Test seam for exercising listener lifecycle failures without a real DB. */
1221
+ createClient?: (config: ConstructorParameters<typeof Client>[0]) => Client;
1222
+ }): PgNotifyHub {
1223
+ const directUrl = deriveDirectDatabaseUrl(input.url);
1224
+ const handlers = new Set<(payload: string) => void>();
1225
+ const stateHandlers = new Set<(state: PgNotifyHubState) => void>();
1226
+ let state: PgNotifyHubState = 'connecting';
1227
+ let stopped = false;
1228
+ let client: Client | null = null;
1229
+ let clientClosePromise: Promise<void> | null = null;
1230
+ let connectPromise: Promise<void> | null = null;
1231
+ let reconnectAttempt = 0;
1232
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
1233
+ const setState = (next: PgNotifyHubState): void => {
1234
+ state = next;
1235
+ for (const handler of stateHandlers) handler(next);
1236
+ };
1237
+
1238
+ const closeClient = (current: Client): Promise<void> => {
1239
+ if (client !== current) return Promise.resolve();
1240
+ if (clientClosePromise) return clientClosePromise;
1241
+ // Defer end() by one microtask so the shared promise is installed before
1242
+ // node-postgres can emit an end event. The slot remains occupied until the
1243
+ // physical close promise succeeds; a rejection retains the client so a
1244
+ // later stop can retry rather than opening a replacement session.
1245
+ const pending = Promise.resolve()
1246
+ .then(() => current.end())
1247
+ .then(() => {
1248
+ if (client === current) client = null;
1249
+ })
1250
+ .finally(() => {
1251
+ if (clientClosePromise === pending) clientClosePromise = null;
1252
+ });
1253
+ clientClosePromise = pending;
1254
+ return pending;
1255
+ };
1256
+
1257
+ const connect = async (): Promise<void> => {
1258
+ if (stopped) return;
1259
+ setState('connecting');
1260
+ const next = (input.createClient ?? ((config) => new Client(config)))({
1261
+ connectionString: directUrl,
1262
+ application_name: input.applicationName ?? 'deepline-runtime-notify-hub',
1263
+ });
1264
+ try {
1265
+ await next.connect();
1266
+ if (input.leaderLockKey) {
1267
+ const election = await next.query<{ acquired: boolean }>(
1268
+ `SELECT pg_try_advisory_lock(hashtextextended($1, 0)) AS acquired`,
1269
+ [input.leaderLockKey],
1270
+ );
1271
+ if (election.rows[0]?.acquired === false) {
1272
+ // Advisory locks are session-scoped. Do not leave a standby process
1273
+ // holding a scarce direct Postgres connection while the elected
1274
+ // worker owns the listener.
1275
+ client = next;
1276
+ setState('standby');
1277
+ await closeClient(next);
1278
+ scheduleStandbyRetry();
1279
+ return;
1280
+ }
1281
+ }
1282
+ await next.query(
1283
+ `LISTEN ${'"' + input.channel.replace(/"/g, '""') + '"'}`,
1284
+ );
1285
+ } catch (error) {
1286
+ client = next;
1287
+ setState('down');
1288
+ try {
1289
+ await closeClient(next);
1290
+ } catch (closeError) {
1291
+ throw new AggregateError(
1292
+ [error, closeError],
1293
+ 'Listener setup failed and its database session could not be closed.',
1294
+ );
1295
+ }
1296
+ scheduleReconnect(error);
1297
+ return;
1298
+ }
1299
+ if (stopped) {
1300
+ client = next;
1301
+ await closeClient(next);
1302
+ return;
1303
+ }
1304
+ client = next;
1305
+ setState('listening');
1306
+ reconnectAttempt = 0;
1307
+ next.on('notification', (message) => {
1308
+ if (typeof message.payload === 'string') {
1309
+ for (const handler of handlers) handler(message.payload);
1310
+ }
1311
+ });
1312
+ next.on('error', (error) => {
1313
+ if (stopped || client !== next) return;
1314
+ setState('down');
1315
+ void closeClient(next).then(
1316
+ () => scheduleReconnect(error),
1317
+ (closeError) => {
1318
+ console.warn('[runtime-scheduler-notify-hub.close_failed]', {
1319
+ channel: input.channel,
1320
+ error:
1321
+ closeError instanceof Error
1322
+ ? closeError.message
1323
+ : String(closeError),
1324
+ });
1325
+ },
1326
+ );
1327
+ });
1328
+ next.on('end', () => {
1329
+ if (stopped || client !== next || clientClosePromise) return;
1330
+ client = null;
1331
+ if (state === 'listening')
1332
+ scheduleReconnect(new Error('listener connection ended'));
1333
+ });
1334
+ };
1335
+
1336
+ const scheduleReconnect = (error: unknown): void => {
1337
+ if (stopped) return;
1338
+ setState('down');
1339
+ if (reconnectTimer) return;
1340
+ // A listener is only a wakeup optimization; durable polling remains the
1341
+ // correctness path. When Postgres is at connection capacity, retrying one
1342
+ // direct LISTEN session per worker every ten seconds creates a thundering
1343
+ // herd precisely when the database needs fewer connection attempts. Use a
1344
+ // slower exponential series for capacity and full jitter for every class.
1345
+ const delay = runtimeSchedulerNotifyReconnectDelayMs({
1346
+ error,
1347
+ attempt: reconnectAttempt,
1348
+ });
1349
+ reconnectAttempt += 1;
1350
+ console.warn('[runtime-scheduler-notify-hub.reconnecting]', {
1351
+ channel: input.channel,
1352
+ delayMs: delay,
1353
+ error: error instanceof Error ? error.message : String(error),
1354
+ });
1355
+ reconnectTimer = setTimeout(() => {
1356
+ reconnectTimer = null;
1357
+ startConnect();
1358
+ }, delay);
1359
+ reconnectTimer.unref?.();
1360
+ };
1361
+
1362
+ const scheduleStandbyRetry = (): void => {
1363
+ if (stopped || reconnectTimer) return;
1364
+ reconnectTimer = setTimeout(() => {
1365
+ reconnectTimer = null;
1366
+ startConnect();
1367
+ }, NOTIFY_HUB_STANDBY_RETRY_MS);
1368
+ reconnectTimer.unref?.();
1369
+ };
1370
+
1371
+ const startConnect = (): void => {
1372
+ if (stopped || connectPromise) return;
1373
+ const pending = connect();
1374
+ connectPromise = pending;
1375
+ void pending.then(
1376
+ () => {
1377
+ if (connectPromise === pending) connectPromise = null;
1378
+ },
1379
+ (error) => {
1380
+ if (connectPromise === pending) connectPromise = null;
1381
+ console.warn('[runtime-scheduler-notify-hub.connect_failed]', {
1382
+ channel: input.channel,
1383
+ error: error instanceof Error ? error.message : String(error),
1384
+ });
1385
+ },
1386
+ );
1387
+ };
1388
+
1389
+ startConnect();
1390
+
1391
+ return {
1392
+ state: () => state,
1393
+ subscribe: (handler) => {
1394
+ handlers.add(handler);
1395
+ return () => handlers.delete(handler);
1396
+ },
1397
+ subscribeState: (handler) => {
1398
+ stateHandlers.add(handler);
1399
+ handler(state);
1400
+ return () => stateHandlers.delete(handler);
1401
+ },
1402
+ stop: async () => {
1403
+ stopped = true;
1404
+ if (reconnectTimer) clearTimeout(reconnectTimer);
1405
+ handlers.clear();
1406
+ setState('down');
1407
+ stateHandlers.clear();
1408
+ const pendingConnect = connectPromise;
1409
+ if (pendingConnect) await pendingConnect;
1410
+ if (client) {
1411
+ const current = client;
1412
+ await closeClient(current);
1413
+ }
1414
+ },
1415
+ };
1416
+ }