deepline 0.3.184 → 0.3.186

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 (42) hide show
  1. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  2. package/dist/bundling-sources/sdk/src/types.ts +4 -0
  3. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +53 -0
  4. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +751 -193
  5. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +4 -0
  6. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +3 -1
  7. package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +1 -0
  8. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +437 -150
  9. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +47 -4
  10. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +3 -1
  11. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +18 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/launch-identity.ts +229 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/map-execution-frame.ts +3 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/contracts.ts +16 -1
  15. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/postgres-adapter.ts +3 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +22 -2
  17. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +6 -1
  18. package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +11 -5
  19. package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +102 -12
  20. package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +8 -0
  21. package/dist/bundling-sources/shared_libs/play-runtime/runtime-telemetry.ts +9 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +7 -0
  23. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +136 -28
  24. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-scope.ts +80 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +897 -207
  26. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +271 -25
  27. package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +21 -0
  28. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +120 -34
  29. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +82 -2
  30. package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +7 -2
  31. package/dist/cli/index.js +29 -3
  32. package/dist/cli/index.mjs +29 -3
  33. package/dist/index.d.mts +4 -0
  34. package/dist/index.d.ts +4 -0
  35. package/dist/index.js +1 -1
  36. package/dist/index.mjs +1 -1
  37. package/dist/install-integrity.json +2 -0
  38. package/dist/release.d.mts +1 -1
  39. package/dist/release.d.ts +1 -1
  40. package/dist/release.js +1 -1
  41. package/dist/release.mjs +1 -1
  42. package/package.json +1 -1
@@ -157,6 +157,11 @@ import {
157
157
  import { LEGACY_MAX_CONCURRENT_EXTERNAL_CALLS } from '../governor/policy';
158
158
  import { requireRuntimeAuthorityDescriptor } from '../execution-capabilities';
159
159
  import { stringifyPostgresJson } from '../postgres-json';
160
+ import {
161
+ buildPlayLaunchIdentity,
162
+ withPlayLaunchIdentityDigest,
163
+ } from '../launch-identity';
164
+ import { PlayRunLaunchConflictError } from '../play-runs/admission/contracts';
160
165
  import { mintPlayRunAttemptRuntimeToken } from '../runtime-authority';
161
166
  import { withActiveSpan } from '../../observability/tracing';
162
167
  import type {
@@ -269,7 +274,10 @@ const POSTGRES_SCHEDULER_SCHEMA_INIT_STATEMENT_TIMEOUT_MS = 60_000;
269
274
  // deployment retries do not re-enter the old cross-table DDL transaction.
270
275
  // v45 adds the bucket-leading expiration index used by rate-reservation
271
276
  // cleanup. Shared schemas build it concurrently through the deploy migrator.
272
- export const POSTGRES_SCHEDULER_SCHEMA_VERSION = 45;
277
+ // v46 adds a bounded provider-scope Retry-After hold with shared adaptive-rate
278
+ // state. The scope owns learning and dispatch across runs and tool rules;
279
+ // existing deadline rows expand additively and initialize on their first seed.
280
+ export const POSTGRES_SCHEDULER_SCHEMA_VERSION = 46;
273
281
  const POSTGRES_SCHEDULER_SCHEMA_MIGRATIONS_TABLE =
274
282
  'scheduler_schema_migrations';
275
283
  export const POSTGRES_SCHEDULER_OPEN_RECOVERY_INDEX =
@@ -1065,6 +1073,8 @@ export type PostgresSchedulerSchemaShapeRequirements = Readonly<{
1065
1073
  cronScheduleSchema?: boolean;
1066
1074
  /** v43 added per-run fair-share tracking for rate buckets; v42 ignores it. */
1067
1075
  rateRequesterFairShare?: boolean;
1076
+ /** v45 added durable Retry-After holds shared across rules and runs. */
1077
+ rateScopeRetryAfter?: boolean;
1068
1078
  }>;
1069
1079
 
1070
1080
  export async function hasCurrentPostgresSchedulerSchemaShape(
@@ -1371,6 +1381,31 @@ export async function hasCurrentPostgresSchedulerSchemaShape(
1371
1381
  )
1372
1382
  )
1373
1383
  )
1384
+ AND (
1385
+ $36::boolean = false
1386
+ OR (
1387
+ to_regclass($37) IS NOT NULL
1388
+ AND (
1389
+ SELECT count(DISTINCT column_name)
1390
+ FROM information_schema.columns
1391
+ WHERE table_schema = $1
1392
+ AND table_name = 'rate_scope_holds'
1393
+ AND column_name IN (
1394
+ 'bucket_id', 'retry_at_ms', 'refill_per_sec', 'capacity', 'max_rps',
1395
+ 'tokens', 'refilled_at_ms', 'next_dispatch_at_ms', 'seen_backpressure',
1396
+ 'cool_until_ms', 'last_ramp_at_ms', 'rate_observation_started_at_ms',
1397
+ 'rate_successes', 'rate_recovery_interval_ms',
1398
+ 'rate_throttle_ceiling_rps'
1399
+ )
1400
+ ) = 15
1401
+ AND (
1402
+ SELECT count(DISTINCT column_name)
1403
+ FROM information_schema.columns
1404
+ WHERE table_schema = $1 AND table_name = 'rate_bucket'
1405
+ AND column_name IN ('rate_observation_started_at_ms', 'rate_successes', 'rate_recovery_interval_ms')
1406
+ ) = 3
1407
+ )
1408
+ )
1374
1409
  AND (
1375
1410
  $32::boolean = false
1376
1411
  OR (
@@ -1695,6 +1730,8 @@ export async function hasCurrentPostgresSchedulerSchemaShape(
1695
1730
  `${quotedSchema}.${quoteIdent(POSTGRES_SCHEDULER_CRON_SCHEDULES_TABLE)}`,
1696
1731
  requirements.rateRequesterFairShare !== false,
1697
1732
  `${quotedSchema}.${quoteIdent('rate_requesters')}`,
1733
+ requirements.rateScopeRetryAfter !== false,
1734
+ `${quotedSchema}.${quoteIdent('rate_scope_holds')}`,
1698
1735
  ],
1699
1736
  );
1700
1737
  if (currentShape.rows[0]?.ready !== true) return false;
@@ -2166,6 +2203,99 @@ async function ensurePostgresSchedulerCronScheduleSchema(
2166
2203
  `);
2167
2204
  }
2168
2205
 
2206
+ /**
2207
+ * Install the launch-admission functions on every bootstrap path.
2208
+ *
2209
+ * These are function-body compatibility changes, not schema DDL. An already
2210
+ * current scheduler schema takes the fast path below, so keeping these only in
2211
+ * the full bootstrap leaves that estate with the old whole-envelope equality.
2212
+ */
2213
+ async function ensurePostgresSchedulerLaunchAdmissionFunctions(
2214
+ client: PostgresSchedulerQueryClient,
2215
+ options?: Pick<PostgresSchedulerOptions, 'schema'>,
2216
+ ): Promise<void> {
2217
+ const quotedSchema = quoteIdent(schemaName(options));
2218
+ await client.query(`
2219
+ CREATE OR REPLACE FUNCTION ${quotedSchema}.runtime_launch_admission_identity(
2220
+ launch_json jsonb
2221
+ ) RETURNS jsonb
2222
+ LANGUAGE sql
2223
+ IMMUTABLE
2224
+ STRICT
2225
+ PARALLEL SAFE
2226
+ AS $$
2227
+ SELECT jsonb_set(
2228
+ jsonb_set(
2229
+ launch_json - ARRAY[
2230
+ 'launchIdentityDigest',
2231
+ 'executorToken',
2232
+ 'inputFileId',
2233
+ 'preloadedDbSessionUnwrapKey',
2234
+ 'preloadedDbSessions',
2235
+ 'submittedAtMs',
2236
+ 'runtimeDeployVersion',
2237
+ 'absurdReleaseId',
2238
+ 'absurdReleaseEnvironment',
2239
+ 'coordinatorUrl',
2240
+ 'coordinatorInternalToken',
2241
+ 'baseUrl',
2242
+ 'runtimeApiBaseUrl',
2243
+ 'vercelProtectionBypassToken'
2244
+ ]::text[],
2245
+ '{maxConcurrentExternalCalls}',
2246
+ coalesce(
2247
+ launch_json->'maxConcurrentExternalCalls',
2248
+ to_jsonb(${LEGACY_MAX_CONCURRENT_EXTERNAL_CALLS}::integer)
2249
+ ),
2250
+ true
2251
+ ),
2252
+ '{maxConcurrentRows}',
2253
+ coalesce(launch_json->'maxConcurrentRows', 'null'::jsonb),
2254
+ true
2255
+ )
2256
+ $$
2257
+ `);
2258
+ await client.query(`
2259
+ CREATE OR REPLACE FUNCTION ${quotedSchema}.runtime_launch_admission_matches(
2260
+ existing_launch_json jsonb,
2261
+ incoming_launch_json jsonb
2262
+ ) RETURNS boolean
2263
+ LANGUAGE sql
2264
+ IMMUTABLE
2265
+ STRICT
2266
+ PARALLEL SAFE
2267
+ AS $$
2268
+ SELECT CASE
2269
+ WHEN nullif(btrim(coalesce(existing_launch_json->>'launchIdentityDigest', '')), '') IS NOT NULL
2270
+ AND nullif(btrim(coalesce(incoming_launch_json->>'launchIdentityDigest', '')), '') IS NOT NULL
2271
+ THEN btrim(existing_launch_json->>'launchIdentityDigest')
2272
+ = btrim(incoming_launch_json->>'launchIdentityDigest')
2273
+ AND (
2274
+ NOT (
2275
+ existing_launch_json ? 'runtimeSandboxPlacementPolicyId'
2276
+ AND incoming_launch_json ? 'runtimeSandboxPlacementPolicyId'
2277
+ )
2278
+ OR existing_launch_json->'runtimeSandboxPlacementPolicyId'
2279
+ IS NOT DISTINCT FROM incoming_launch_json->'runtimeSandboxPlacementPolicyId'
2280
+ )
2281
+ WHEN existing_launch_json ? 'runtimeSandboxPlacementPolicyId'
2282
+ AND incoming_launch_json ? 'runtimeSandboxPlacementPolicyId'
2283
+ THEN ${quotedSchema}.runtime_launch_admission_identity(
2284
+ existing_launch_json - 'launchIdentityDigest'
2285
+ ) = ${quotedSchema}.runtime_launch_admission_identity(
2286
+ incoming_launch_json - 'launchIdentityDigest'
2287
+ )
2288
+ ELSE ${quotedSchema}.runtime_launch_admission_identity(
2289
+ existing_launch_json - 'launchIdentityDigest'
2290
+ ) - ARRAY['runtimeSandboxPlacementPolicyId', 'launchIdentityDigest']::text[] =
2291
+ ${quotedSchema}.runtime_launch_admission_identity(
2292
+ incoming_launch_json - 'launchIdentityDigest'
2293
+ ) - ARRAY['runtimeSandboxPlacementPolicyId', 'launchIdentityDigest']::text[]
2294
+ END
2295
+ $$
2296
+ `);
2297
+ }
2298
+
2169
2299
  export async function ensurePostgresSchedulerSchema(
2170
2300
  client: PostgresSchedulerQueryClient,
2171
2301
  options?: Pick<PostgresSchedulerOptions, 'schema'> & {
@@ -2300,6 +2430,7 @@ export async function ensurePostgresSchedulerSchema(
2300
2430
  const hasLegacyAdmissionRows =
2301
2431
  legacyAdmissionRows?.rows[0]?.present === true;
2302
2432
  if (schemaCoreIsCurrent) {
2433
+ await ensurePostgresSchedulerLaunchAdmissionFunctions(client, options);
2303
2434
  if (!schemaIsCurrent) {
2304
2435
  await ensurePostgresSchedulerTerminalComputeBillingOutboxTrigger(
2305
2436
  client,
@@ -3671,6 +3802,7 @@ export async function ensurePostgresSchedulerSchema(
3671
3802
  [POSTGRES_SCHEDULER_SCHEMA_VERSION],
3672
3803
  );
3673
3804
  await ensurePostgresSchedulerCronScheduleSchema(client, options);
3805
+ await ensurePostgresSchedulerLaunchAdmissionFunctions(client, options);
3674
3806
  if (hadOutbox.rows[0]?.present !== true) {
3675
3807
  await client.query(
3676
3808
  `
@@ -4377,6 +4509,45 @@ export async function submitPostgresAbsurdPlayRun(
4377
4509
  };
4378
4510
  }
4379
4511
 
4512
+ function launchIdentityConflictFields(
4513
+ existingLaunchJson: unknown,
4514
+ incomingLaunch: PlaySchedulerSubmitInput,
4515
+ ): string[] {
4516
+ if (
4517
+ !existingLaunchJson ||
4518
+ typeof existingLaunchJson !== 'object' ||
4519
+ Array.isArray(existingLaunchJson)
4520
+ ) {
4521
+ return [];
4522
+ }
4523
+ const existingIdentity = buildPlayLaunchIdentity(
4524
+ existingLaunchJson as Record<string, unknown>,
4525
+ );
4526
+ const incomingIdentity = buildPlayLaunchIdentity(incomingLaunch);
4527
+ const fields = Object.keys(incomingIdentity).filter(
4528
+ (field) =>
4529
+ stableStringify(existingIdentity[field]) !==
4530
+ stableStringify(incomingIdentity[field]),
4531
+ );
4532
+ if (
4533
+ Object.prototype.hasOwnProperty.call(
4534
+ existingLaunchJson,
4535
+ 'runtimeSandboxPlacementPolicyId',
4536
+ ) &&
4537
+ Object.prototype.hasOwnProperty.call(
4538
+ incomingLaunch,
4539
+ 'runtimeSandboxPlacementPolicyId',
4540
+ ) &&
4541
+ stableStringify(
4542
+ (existingLaunchJson as Record<string, unknown>)
4543
+ .runtimeSandboxPlacementPolicyId,
4544
+ ) !== stableStringify(incomingLaunch.runtimeSandboxPlacementPolicyId)
4545
+ ) {
4546
+ fields.push('runtimeSandboxPlacementPolicyId');
4547
+ }
4548
+ return fields.sort();
4549
+ }
4550
+
4380
4551
  async function submitPostgresSchedulerRunTransaction(
4381
4552
  client: PostgresSchedulerQueryClient,
4382
4553
  input: PlaySchedulerSubmitInput,
@@ -4415,7 +4586,10 @@ async function submitPostgresSchedulerRunTransaction(
4415
4586
  existingOnly: requestedExistingOnly,
4416
4587
  ...durableInput
4417
4588
  } = input;
4418
- input = durableInput;
4589
+ // Every scheduler writer converges through this seam, including the cron
4590
+ // successor path and Workpool retries. Stamp after ephemeral admission proof
4591
+ // fields are removed so they can never change the durable launch identity.
4592
+ input = withPlayLaunchIdentityDigest(durableInput);
4419
4593
  const existingOnlySql = requestedExistingOnly === true ? 'true' : 'false';
4420
4594
  const releaseId = input.absurdReleaseId?.trim() || null;
4421
4595
  const legacyPreviewRelease =
@@ -4499,6 +4673,7 @@ async function submitPostgresSchedulerRunTransaction(
4499
4673
  run_created: boolean;
4500
4674
  task_created: boolean | null;
4501
4675
  task_recovery_required: boolean;
4676
+ existing_launch_json: unknown | null;
4502
4677
  }>(
4503
4678
  `
4504
4679
  WITH run_lock AS MATERIALIZED (
@@ -4546,6 +4721,7 @@ async function submitPostgresSchedulerRunTransaction(
4546
4721
  coalesce(runs.admission_json, runs.launch_json),
4547
4722
  $8::jsonb
4548
4723
  ) AS matches_launch,
4724
+ coalesce(runs.admission_json, runs.launch_json) AS launch_json,
4549
4725
  runs.absurd_task_id,
4550
4726
  runs.status
4551
4727
  FROM admission_lock
@@ -4752,6 +4928,7 @@ async function submitPostgresSchedulerRunTransaction(
4752
4928
  AND existing_run.absurd_task_id IS NULL
4753
4929
  )
4754
4930
  ) AS task_recovery_required,
4931
+ (SELECT launch_json FROM existing_run) AS existing_launch_json,
4755
4932
  CASE WHEN EXISTS (SELECT 1 FROM run_insert)
4756
4933
  OR EXISTS (SELECT 1 FROM run_recovery)
4757
4934
  THEN pg_notify($10, $11)
@@ -4808,9 +4985,13 @@ async function submitPostgresSchedulerRunTransaction(
4808
4985
  );
4809
4986
  }
4810
4987
  if (row?.accepted === false && row.existing_run && !row.matching_existing) {
4811
- throw new Error(
4812
- `Play run "${input.runId}" already exists with a different launch.`,
4813
- );
4988
+ throw new PlayRunLaunchConflictError(input.runId, {
4989
+ conflictReason: 'launch_identity_mismatch',
4990
+ conflictingFields: launchIdentityConflictFields(
4991
+ row.existing_launch_json,
4992
+ input,
4993
+ ),
4994
+ });
4814
4995
  }
4815
4996
  if (row?.accepted === false && !row?.existing_run && requestedExistingOnly) {
4816
4997
  throw new Error(
@@ -5112,6 +5293,14 @@ export class PostgresSchedulerCronScheduleConfigurationError extends Error {
5112
5293
  }
5113
5294
  }
5114
5295
 
5296
+ /** A deterministic Run identity conflict cannot be repaired by retrying the same fire. */
5297
+ export class PostgresSchedulerCronLaunchConflictError extends PostgresSchedulerCronScheduleConfigurationError {
5298
+ constructor(message: string) {
5299
+ super(message);
5300
+ this.name = 'PostgresSchedulerCronLaunchConflictError';
5301
+ }
5302
+ }
5303
+
5115
5304
  /**
5116
5305
  * The timer stores the facts needed to reconstruct a pinned admission, not a
5117
5306
  * previously prepared launch. Executor tokens, preloaded session keys,
@@ -5456,7 +5645,7 @@ function cronOccurrenceLaunch(input: {
5456
5645
  `Cron schedule "${input.schedule.scheduleId}" did not receive a fresh authority for its pinned occurrence.`,
5457
5646
  );
5458
5647
  }
5459
- return {
5648
+ return withPlayLaunchIdentityDigest({
5460
5649
  ...input.launch,
5461
5650
  schedulerSchema:
5462
5651
  input.schedulerSchema ?? input.launch.schedulerSchema ?? null,
@@ -5469,7 +5658,7 @@ function cronOccurrenceLaunch(input: {
5469
5658
  ),
5470
5659
  triggerSource: 'cron',
5471
5660
  requiresRunCreatedProjection: true,
5472
- };
5661
+ });
5473
5662
  }
5474
5663
 
5475
5664
  export type FirePostgresSchedulerCronScheduleInput = {
@@ -5492,14 +5681,26 @@ export type FirePostgresSchedulerCronScheduleInput = {
5492
5681
  * CTE inserts the immutable Run, its play-run task, run.created fact, next
5493
5682
  * cron-fire task, and next cursor. A task replay after commit sees a changed
5494
5683
  * cursor and is a harmless `superseded` no-op.
5684
+ *
5685
+ * A re-fire of the same occurrence whose deterministic Run already exists is
5686
+ * an idempotent re-fire, not a no-op. The cursor advances when the existing
5687
+ * launch identity matches, even if this statement did not create the Run.
5688
+ * A genuine identity mismatch is returned as `stalled` so it cannot silently
5689
+ * freeze the schedule behind an indistinguishable replay.
5495
5690
  */
5496
5691
  export async function firePostgresSchedulerCronSchedule(
5497
5692
  client: PostgresSchedulerQueryClient,
5498
5693
  input: FirePostgresSchedulerCronScheduleInput,
5499
5694
  options?: Pick<PostgresSchedulerOptions, 'schema'>,
5500
5695
  ): Promise<
5501
- | { status: 'admitted'; runId: string; successorDueAtMs: number }
5696
+ | {
5697
+ status: 'admitted';
5698
+ runId: string;
5699
+ successorDueAtMs: number;
5700
+ reused: boolean;
5701
+ }
5502
5702
  | { status: 'superseded' }
5703
+ | { status: 'stalled'; runId: string; reason: string }
5503
5704
  > {
5504
5705
  assertPostgresSchedulerCronScheduleInput(input);
5505
5706
  const schedule = await readPostgresSchedulerCronSchedule(
@@ -5566,7 +5767,11 @@ export async function firePostgresSchedulerCronSchedule(
5566
5767
  orgId: launch.orgId,
5567
5768
  });
5568
5769
  const result = await client.query<{
5569
- admitted: boolean;
5770
+ advanced: boolean;
5771
+ run_created: boolean;
5772
+ schedule_current: boolean;
5773
+ existing_run: boolean;
5774
+ matching_existing: boolean;
5570
5775
  release_accepting: boolean;
5571
5776
  }>(
5572
5777
  `
@@ -5591,16 +5796,28 @@ export async function firePostgresSchedulerCronSchedule(
5591
5796
  AND release_gate.accepting
5592
5797
  FOR UPDATE
5593
5798
  ), existing_run AS MATERIALIZED (
5594
- SELECT runs.run_id
5799
+ SELECT runs.run_id,
5800
+ ${quoteIdent(schemaName(options))}.runtime_launch_admission_matches(
5801
+ coalesce(runs.admission_json, runs.launch_json),
5802
+ $6::jsonb
5803
+ ) AS matches_launch
5595
5804
  FROM matching_schedule
5596
5805
  JOIN ${tableName('runs', options)} AS runs ON runs.run_id = $5::text
5806
+ ), occurrence AS MATERIALIZED (
5807
+ SELECT EXISTS (SELECT 1 FROM matching_schedule) AS schedule_current,
5808
+ EXISTS (SELECT 1 FROM existing_run) AS existing_run,
5809
+ coalesce(
5810
+ (SELECT matches_launch FROM existing_run),
5811
+ false
5812
+ ) AS matching_existing
5597
5813
  ), play_task_request AS (
5598
5814
  SELECT $7::text AS queue_name,
5599
5815
  $8::text AS task_name,
5600
5816
  $9::jsonb AS params,
5601
5817
  $10::jsonb AS options
5602
- WHERE EXISTS (SELECT 1 FROM matching_schedule)
5603
- AND NOT EXISTS (SELECT 1 FROM existing_run)
5818
+ FROM occurrence
5819
+ WHERE occurrence.schedule_current
5820
+ AND NOT occurrence.existing_run
5604
5821
  ), play_task_spawn AS MATERIALIZED (
5605
5822
  SELECT spawned.task_id::text AS task_id
5606
5823
  FROM play_task_request
@@ -5636,15 +5853,25 @@ export async function firePostgresSchedulerCronSchedule(
5636
5853
  'coordinatorInternalToken',
5637
5854
  'vercelProtectionBypassToken'
5638
5855
  ]::text[]
5639
- WHERE EXISTS (SELECT 1 FROM matching_schedule)
5640
- AND NOT EXISTS (SELECT 1 FROM existing_run)
5856
+ FROM occurrence
5857
+ WHERE occurrence.schedule_current
5858
+ AND NOT occurrence.existing_run
5641
5859
  RETURNING run_id
5860
+ ), occurrence_admitted AS MATERIALIZED (
5861
+ SELECT true AS admitted
5862
+ FROM occurrence
5863
+ WHERE EXISTS (SELECT 1 FROM run_insert)
5864
+ OR (
5865
+ occurrence.schedule_current
5866
+ AND occurrence.existing_run
5867
+ AND occurrence.matching_existing
5868
+ )
5642
5869
  ), successor_task_request AS (
5643
5870
  SELECT $11::text AS queue_name,
5644
5871
  $12::text AS task_name,
5645
5872
  $13::jsonb AS params,
5646
5873
  $14::jsonb AS options
5647
- WHERE EXISTS (SELECT 1 FROM run_insert)
5874
+ WHERE EXISTS (SELECT 1 FROM occurrence_admitted)
5648
5875
  ), successor_task_spawn AS MATERIALIZED (
5649
5876
  SELECT spawned.task_id::text AS task_id
5650
5877
  FROM successor_task_request
@@ -5718,10 +5945,14 @@ export async function firePostgresSchedulerCronSchedule(
5718
5945
  AND schedule.state = 'active'
5719
5946
  AND schedule.current_due_at = to_timestamp($3::double precision / 1000.0)
5720
5947
  AND schedule.current_fire_generation = $15::bigint
5721
- AND EXISTS (SELECT 1 FROM run_insert)
5948
+ AND EXISTS (SELECT 1 FROM occurrence_admitted)
5722
5949
  RETURNING schedule_id
5723
5950
  )
5724
- SELECT EXISTS (SELECT 1 FROM schedule_advance) AS admitted,
5951
+ SELECT EXISTS (SELECT 1 FROM schedule_advance) AS advanced,
5952
+ EXISTS (SELECT 1 FROM run_insert) AS run_created,
5953
+ (SELECT schedule_current FROM occurrence) AS schedule_current,
5954
+ (SELECT existing_run FROM occurrence) AS existing_run,
5955
+ (SELECT matching_existing FROM occurrence) AS matching_existing,
5725
5956
  coalesce(
5726
5957
  (SELECT accepting
5727
5958
  FROM ${tableName('runtime_release_admission', options)}
@@ -5748,19 +5979,34 @@ export async function firePostgresSchedulerCronSchedule(
5748
5979
  releaseAdmissionKey,
5749
5980
  ],
5750
5981
  );
5751
- if (result.rows[0]?.release_accepting !== true) {
5982
+ const fired = result.rows[0];
5983
+ if (fired?.release_accepting !== true) {
5752
5984
  throw new Error(
5753
5985
  `Cron schedule "${schedule.scheduleId}" cannot fire because Absurd release "${releaseAdmissionKey}" is not accepting admissions.`,
5754
5986
  );
5755
5987
  }
5756
- if (result.rows[0]?.admitted !== true) {
5757
- return { status: 'superseded' };
5988
+ if (fired.advanced === true) {
5989
+ return {
5990
+ status: 'admitted',
5991
+ runId: launch.runId,
5992
+ successorDueAtMs,
5993
+ reused: fired.run_created !== true,
5994
+ };
5758
5995
  }
5759
- return {
5760
- status: 'admitted',
5761
- runId: launch.runId,
5762
- successorDueAtMs,
5763
- };
5996
+ if (
5997
+ fired.schedule_current === true &&
5998
+ fired.existing_run === true &&
5999
+ fired.matching_existing !== true
6000
+ ) {
6001
+ return {
6002
+ status: 'stalled',
6003
+ runId: launch.runId,
6004
+ reason:
6005
+ `Cron schedule "${schedule.scheduleId}" generation ${schedule.generation} cannot advance past ` +
6006
+ `occurrence ${schedule.dueAtMs}: Run "${launch.runId}" already exists with a different launch identity.`,
6007
+ };
6008
+ }
6009
+ return { status: 'superseded' };
5764
6010
  }
5765
6011
 
5766
6012
  /** Fence a malformed/invalid timer so it cannot repeatedly consume workers. */
@@ -14,6 +14,9 @@ export type PlayExecutionSuspension =
14
14
  boundaryId: string;
15
15
  eventKey: string;
16
16
  timeoutMs: number;
17
+ eventExpiresAtMs?: number;
18
+ /** A terminal timeout's deliberate long park, not its original event wait. */
19
+ postTimeoutPark?: boolean;
17
20
  messageRef?: {
18
21
  channel: string;
19
22
  ts: string;
@@ -21,10 +24,13 @@ export type PlayExecutionSuspension =
21
24
  }
22
25
  | {
23
26
  kind: 'integration_event_batch';
27
+ /** Earliest pending provider row; absence preserves older event-only waits. */
28
+ providerWakeAtMs?: number;
24
29
  boundaries: Array<{
25
30
  boundaryId: string;
26
31
  eventKey: string;
27
32
  timeoutMs: number;
33
+ eventExpiresAtMs?: number;
28
34
  messageRef?: {
29
35
  channel: string;
30
36
  ts: string;
@@ -123,6 +129,7 @@ export class PlayRowExecutionSuspendedError extends Error {
123
129
  boundaryId: string;
124
130
  eventKey: string;
125
131
  timeoutMs: number;
132
+ eventExpiresAtMs?: number;
126
133
  messageRef?: {
127
134
  channel: string;
128
135
  ts: string;
@@ -133,6 +140,7 @@ export class PlayRowExecutionSuspendedError extends Error {
133
140
  boundaryId: string;
134
141
  eventKey: string;
135
142
  timeoutMs: number;
143
+ eventExpiresAtMs?: number;
136
144
  messageRef?: {
137
145
  channel: string;
138
146
  ts: string;
@@ -146,6 +154,19 @@ export class PlayRowExecutionSuspendedError extends Error {
146
154
  }
147
155
  }
148
156
 
157
+ /** A provider wait is a control handoff, never a failed cell or receipt. */
158
+ export class PlayRowProviderBackpressureError extends Error {
159
+ constructor(
160
+ readonly provider: string,
161
+ readonly retryAtMs: number,
162
+ ) {
163
+ super(
164
+ `Play row is waiting for ${provider} provider backpressure to clear.`,
165
+ );
166
+ this.name = 'PlayRowProviderBackpressureError';
167
+ }
168
+ }
169
+
149
170
  export function isPlayRowExecutionSuspendedError(
150
171
  value: unknown,
151
172
  ): value is PlayRowExecutionSuspendedError {