deepline 0.3.183 → 0.3.185

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 (34) hide show
  1. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  2. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +53 -0
  3. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +751 -193
  4. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +4 -0
  5. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +3 -1
  6. package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +1 -0
  7. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +437 -150
  8. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +47 -4
  9. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +3 -1
  10. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +18 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/map-execution-frame.ts +3 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +22 -2
  13. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +6 -1
  14. package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +11 -5
  15. package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +102 -12
  16. package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +8 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +136 -28
  18. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-scope.ts +80 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +897 -207
  20. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +33 -1
  21. package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +21 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +120 -34
  23. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +82 -2
  24. package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +7 -2
  25. package/dist/cli/index.js +28 -3
  26. package/dist/cli/index.mjs +28 -3
  27. package/dist/index.js +1 -1
  28. package/dist/index.mjs +1 -1
  29. package/dist/install-integrity.json +1 -0
  30. package/dist/release.d.mts +1 -1
  31. package/dist/release.d.ts +1 -1
  32. package/dist/release.js +1 -1
  33. package/dist/release.mjs +1 -1
  34. package/package.json +1 -1
@@ -1288,10 +1288,14 @@ export async function parkAndResolveSuspension(input: {
1288
1288
  boundaryId: suspension.boundaryId,
1289
1289
  eventKey: suspension.eventKey,
1290
1290
  timeoutMs: suspension.timeoutMs,
1291
+ ...(suspension.eventExpiresAtMs !== undefined
1292
+ ? { eventExpiresAtMs: suspension.eventExpiresAtMs }
1293
+ : {}),
1294
+ ...(suspension.postTimeoutPark ? { postTimeoutPark: true } : {}),
1291
1295
  ...(suspension.messageRef ? { messageRef: suspension.messageRef } : {}),
1292
1296
  },
1293
1297
  claimSignal: input.claimSignal,
1294
- });
1298
+ }).then((result) => result.checkpoint);
1295
1299
  }
1296
1300
 
1297
1301
  if (suspension.kind === 'integration_event_batch') {
@@ -1299,13 +1303,25 @@ export async function parkAndResolveSuspension(input: {
1299
1303
  for (const boundary of [...suspension.boundaries].sort(
1300
1304
  (a, b) => a.timeoutMs - b.timeoutMs,
1301
1305
  )) {
1302
- resolvedCheckpoint = await resolveIntegrationEvent({
1306
+ // An earlier event may have resolved while the provider timer elapsed.
1307
+ // Do not enter another event wait after that provider row is due.
1308
+ if (
1309
+ suspension.providerWakeAtMs !== undefined &&
1310
+ suspension.providerWakeAtMs <= Date.now() &&
1311
+ boundary.eventExpiresAtMs !== undefined &&
1312
+ boundary.eventExpiresAtMs > Date.now()
1313
+ )
1314
+ return resolvedCheckpoint;
1315
+ const resolved = await resolveIntegrationEvent({
1303
1316
  ctx,
1304
1317
  runId,
1305
1318
  checkpoint: resolvedCheckpoint,
1306
1319
  boundary,
1307
1320
  claimSignal: input.claimSignal,
1321
+ providerWakeAtMs: suspension.providerWakeAtMs,
1308
1322
  });
1323
+ resolvedCheckpoint = resolved.checkpoint;
1324
+ if (resolved.providerWake) return resolvedCheckpoint;
1309
1325
  }
1310
1326
  return resolvedCheckpoint;
1311
1327
  }
@@ -1382,13 +1398,16 @@ async function resolveIntegrationEvent(input: {
1382
1398
  boundaryId: string;
1383
1399
  eventKey: string;
1384
1400
  timeoutMs: number;
1401
+ eventExpiresAtMs?: number;
1402
+ postTimeoutPark?: boolean;
1385
1403
  messageRef?: { channel: string; ts: string };
1386
1404
  };
1405
+ providerWakeAtMs?: number;
1387
1406
  claimSignal: (
1388
1407
  eventKey: string,
1389
1408
  boundaryId: string | null,
1390
1409
  ) => Promise<PostgresSchedulerSignalRecord | null>;
1391
- }): Promise<PlayCheckpoint> {
1410
+ }): Promise<{ checkpoint: PlayCheckpoint; providerWake: boolean }> {
1392
1411
  const { ctx, runId, checkpoint, boundary } = input;
1393
1412
  // Suspensions written before `messageRef` became part of the suspension
1394
1413
  // contract still have the armed Slack message reference in their checkpoint.
@@ -1403,31 +1422,89 @@ async function resolveIntegrationEvent(input: {
1403
1422
  (checkpointBoundary?.kind === 'integration_event'
1404
1423
  ? checkpointBoundary.messageRef
1405
1424
  : undefined);
1425
+ const checkpointOutput =
1426
+ checkpointBoundary?.kind === 'integration_event'
1427
+ ? checkpointBoundary.output
1428
+ : null;
1429
+ const postTimeoutPark =
1430
+ boundary.postTimeoutPark === true ||
1431
+ (checkpointOutput !== null &&
1432
+ typeof checkpointOutput === 'object' &&
1433
+ !Array.isArray(checkpointOutput) &&
1434
+ (checkpointOutput as Record<string, unknown>).timed_out === true);
1435
+ const originalEventExpiresAtMs =
1436
+ boundary.eventExpiresAtMs ??
1437
+ (checkpointBoundary?.kind === 'integration_event'
1438
+ ? checkpointBoundary.eventExpiresAtMs
1439
+ : undefined);
1440
+ const terminalParkAnchorMs =
1441
+ checkpointBoundary?.kind === 'integration_event'
1442
+ ? (checkpointBoundary.completedAt ?? Date.now())
1443
+ : Date.now();
1444
+ const eventExpiresAtMs = postTimeoutPark
1445
+ ? (boundary.eventExpiresAtMs ??
1446
+ terminalParkAnchorMs + Math.max(0, boundary.timeoutMs))
1447
+ : (originalEventExpiresAtMs ??
1448
+ Date.now() + Math.max(0, boundary.timeoutMs));
1449
+ // Old event checkpoints have only an Absurd step timer, not an absolute
1450
+ // expiry. Reuse that original step instead of granting a new full timeout
1451
+ // whenever a provider row wakes.
1452
+ const providerWakeAtMs =
1453
+ postTimeoutPark || originalEventExpiresAtMs === undefined
1454
+ ? undefined
1455
+ : input.providerWakeAtMs;
1406
1456
 
1407
1457
  // Belt-and-suspenders: an early durable signal short-circuits the wait.
1408
- const early = await input.claimSignal(boundary.eventKey, boundary.boundaryId);
1458
+ const early = postTimeoutPark
1459
+ ? null
1460
+ : await input.claimSignal(boundary.eventKey, boundary.boundaryId);
1409
1461
  if (early) {
1410
- return absurdCheckpointWithIntegrationEvent({
1411
- checkpoint,
1412
- boundaryId: boundary.boundaryId,
1413
- eventKey: boundary.eventKey,
1414
- timeoutMs: boundary.timeoutMs,
1415
- ...(messageRef ? { messageRef } : {}),
1416
- output: absurdIntegrationEventBoundaryOutput({
1462
+ return {
1463
+ checkpoint: absurdCheckpointWithIntegrationEvent({
1464
+ checkpoint,
1465
+ boundaryId: boundary.boundaryId,
1417
1466
  eventKey: boundary.eventKey,
1418
- signalData: signalData(early),
1419
- messageRef,
1420
- timedOut: false,
1467
+ timeoutMs: boundary.timeoutMs,
1468
+ ...(messageRef ? { messageRef } : {}),
1469
+ output: absurdIntegrationEventBoundaryOutput({
1470
+ eventKey: boundary.eventKey,
1471
+ signalData: signalData(early),
1472
+ messageRef,
1473
+ timedOut: false,
1474
+ }),
1421
1475
  }),
1422
- });
1476
+ providerWake: false,
1477
+ };
1478
+ }
1479
+
1480
+ if (
1481
+ providerWakeAtMs !== undefined &&
1482
+ providerWakeAtMs <= Date.now() &&
1483
+ eventExpiresAtMs > Date.now()
1484
+ ) {
1485
+ return { checkpoint: absurdCheckpointBase(checkpoint), providerWake: true };
1423
1486
  }
1424
1487
 
1425
1488
  const eventName = absurdIntegrationEventName(runId, boundary.eventKey);
1489
+ const eventWaitMs = Math.max(0, eventExpiresAtMs - Date.now());
1490
+ const providerWaitMs =
1491
+ providerWakeAtMs === undefined
1492
+ ? Number.POSITIVE_INFINITY
1493
+ : Math.max(0, providerWakeAtMs - Date.now());
1494
+ const waitMs = Math.min(eventWaitMs, providerWaitMs);
1426
1495
  let timedOut = false;
1427
1496
  try {
1428
1497
  await ctx.awaitEvent(eventName, {
1429
- stepName: boundary.boundaryId,
1430
- timeout: Math.max(0, boundary.timeoutMs) / 1000,
1498
+ // Absurd persists waits by step name. A provider timer must never
1499
+ // inherit an earlier event timeout (or vice versa) on replay.
1500
+ stepName: postTimeoutPark
1501
+ ? `${boundary.boundaryId}:terminal-park:${terminalParkAnchorMs}`
1502
+ : providerWakeAtMs === undefined
1503
+ ? boundary.boundaryId
1504
+ : `${boundary.boundaryId}:provider-wake:${providerWakeAtMs}`,
1505
+ // Absurd floors seconds. Round up so a positive remainder never wakes
1506
+ // before the provider's authoritative not-before time.
1507
+ timeout: Math.ceil(waitMs / 1_000),
1431
1508
  });
1432
1509
  } catch (error) {
1433
1510
  if (error instanceof TimeoutError) {
@@ -1438,6 +1515,25 @@ async function resolveIntegrationEvent(input: {
1438
1515
  }
1439
1516
  }
1440
1517
 
1518
+ if (postTimeoutPark) {
1519
+ return {
1520
+ checkpoint: absurdCheckpointWithIntegrationEvent({
1521
+ checkpoint,
1522
+ boundaryId: boundary.boundaryId,
1523
+ eventKey: boundary.eventKey,
1524
+ timeoutMs: boundary.timeoutMs,
1525
+ ...(messageRef ? { messageRef } : {}),
1526
+ output:
1527
+ checkpointOutput !== null &&
1528
+ typeof checkpointOutput === 'object' &&
1529
+ !Array.isArray(checkpointOutput)
1530
+ ? (checkpointOutput as Record<string, unknown>)
1531
+ : { timed_out: true },
1532
+ }),
1533
+ providerWake: false,
1534
+ };
1535
+ }
1536
+
1441
1537
  // Claim the authoritative durable signal row for the payload — the absurd
1442
1538
  // event carries no payload we trust. On timeout, sweep once more before
1443
1539
  // giving up (mirrors today's belt-and-suspenders resume).
@@ -1445,20 +1541,32 @@ async function resolveIntegrationEvent(input: {
1445
1541
  boundary.eventKey,
1446
1542
  boundary.boundaryId,
1447
1543
  );
1544
+ if (
1545
+ !claimed &&
1546
+ timedOut &&
1547
+ providerWakeAtMs !== undefined &&
1548
+ providerWakeAtMs < eventExpiresAtMs &&
1549
+ Date.now() < eventExpiresAtMs
1550
+ ) {
1551
+ return { checkpoint: absurdCheckpointBase(checkpoint), providerWake: true };
1552
+ }
1448
1553
  const resolvedTimedOut = timedOut && !claimed;
1449
- return absurdCheckpointWithIntegrationEvent({
1450
- checkpoint,
1451
- boundaryId: boundary.boundaryId,
1452
- eventKey: boundary.eventKey,
1453
- timeoutMs: boundary.timeoutMs,
1454
- ...(messageRef ? { messageRef } : {}),
1455
- output: absurdIntegrationEventBoundaryOutput({
1554
+ return {
1555
+ checkpoint: absurdCheckpointWithIntegrationEvent({
1556
+ checkpoint,
1557
+ boundaryId: boundary.boundaryId,
1456
1558
  eventKey: boundary.eventKey,
1457
- signalData: signalData(claimed),
1458
- messageRef,
1459
- timedOut: resolvedTimedOut,
1559
+ timeoutMs: boundary.timeoutMs,
1560
+ ...(messageRef ? { messageRef } : {}),
1561
+ output: absurdIntegrationEventBoundaryOutput({
1562
+ eventKey: boundary.eventKey,
1563
+ signalData: signalData(claimed),
1564
+ messageRef,
1565
+ timedOut: resolvedTimedOut,
1566
+ }),
1460
1567
  }),
1461
- });
1568
+ providerWake: false,
1569
+ };
1462
1570
  }
1463
1571
 
1464
1572
  /**
@@ -0,0 +1,80 @@
1
+ /**
2
+ * One quota scope owns both learning evidence and the dispatch budget. Rule
3
+ * rows retain configured ceilings and concurrency; they must not independently
4
+ * learn from copies of the same provider feedback. These CTEs are shared by
5
+ * pure-rate and concurrency-bearing admission, in the same atomic grant SQL.
6
+ */
7
+ const AIMD_RECOVERY_STEP_MS = 1_000;
8
+ // Increase relative to the learned rate, not by a fixed RPS amount: a long
9
+ // Retry-After can imply a very small quota, where even +1 RPS would be an
10
+ // enormous and repeated over-probe. A 25% clean-success step still recovers a
11
+ // healthy 1->5 RPS provider in eight observations instead of seventeen.
12
+ const AIMD_ADDITIVE_STEP_FRACTION = 0.25;
13
+
14
+ export function acquireRateScopeSql(
15
+ table: string,
16
+ successesParameter: string,
17
+ ): string {
18
+ return `provider_hold_rows AS (
19
+ SELECT * FROM ${table} WHERE bucket_id = $1 FOR UPDATE
20
+ ), provider_hold AS (
21
+ SELECT h.*,
22
+ LEAST(
23
+ GREATEST(CASE WHEN h.seen_backpressure THEN 1 ELSE h.capacity END, h.refill_per_sec),
24
+ h.tokens + GREATEST(0, nv.now_ms - h.refilled_at_ms) * h.refill_per_sec / 1000.0
25
+ ) AS tokens_after_refill,
26
+ ${successesParameter}::int = 0
27
+ AND nv.now_ms - GREATEST(h.refilled_at_ms, h.next_dispatch_at_ms, h.retry_at_ms)
28
+ > GREATEST(1000, 2000.0 / h.refill_per_sec) AS observation_was_idle,
29
+ CASE WHEN ${successesParameter}::int > 0
30
+ AND h.rate_observation_started_at_ms IS NOT NULL
31
+ AND nv.now_ms - h.last_ramp_at_ms >= CASE WHEN h.seen_backpressure
32
+ THEN LEAST(h.rate_recovery_interval_ms, ${AIMD_RECOVERY_STEP_MS})
33
+ ELSE h.rate_recovery_interval_ms END
34
+ AND h.retry_at_ms <= nv.now_ms
35
+ THEN LEAST(h.max_rps, CASE WHEN h.seen_backpressure
36
+ THEN h.refill_per_sec * (1 + ${AIMD_ADDITIVE_STEP_FRACTION})
37
+ ELSE h.refill_per_sec * 2 END)
38
+ ELSE h.refill_per_sec END AS next_refill_per_sec,
39
+ CASE WHEN ${successesParameter}::int > 0
40
+ AND h.rate_observation_started_at_ms IS NOT NULL
41
+ AND nv.now_ms - h.last_ramp_at_ms >= CASE WHEN h.seen_backpressure
42
+ THEN LEAST(h.rate_recovery_interval_ms, ${AIMD_RECOVERY_STEP_MS})
43
+ ELSE h.rate_recovery_interval_ms END
44
+ AND h.retry_at_ms <= nv.now_ms
45
+ THEN nv.now_ms ELSE h.last_ramp_at_ms END AS next_ramp_at_ms
46
+ FROM provider_hold_rows h CROSS JOIN now_value nv
47
+ )`;
48
+ }
49
+
50
+ export function updateRateScopeSql(table: string): string {
51
+ return `updated_scope AS (
52
+ UPDATE ${table} h SET
53
+ tokens = ph.tokens_after_refill - (SELECT granted FROM grant_calc),
54
+ refilled_at_ms = nv.now_ms,
55
+ refill_per_sec = ph.next_refill_per_sec,
56
+ last_ramp_at_ms = ph.next_ramp_at_ms,
57
+ rate_observation_started_at_ms = CASE
58
+ WHEN (SELECT granted FROM grant_calc) > 0 AND ph.observation_was_idle THEN nv.now_ms
59
+ WHEN (SELECT granted FROM grant_calc) > 0 THEN COALESCE(ph.rate_observation_started_at_ms, nv.now_ms)
60
+ ELSE ph.rate_observation_started_at_ms END,
61
+ rate_successes = CASE
62
+ WHEN (SELECT granted FROM grant_calc) > 0
63
+ AND ph.rate_observation_started_at_ms IS NULL THEN 0
64
+ WHEN (SELECT granted FROM grant_calc) > 0 AND ph.observation_was_idle THEN 0
65
+ ELSE ph.rate_successes END,
66
+ rate_throttle_ceiling_rps = CASE
67
+ WHEN (SELECT granted FROM grant_calc) > 0
68
+ AND ph.rate_observation_started_at_ms IS NULL THEN NULL
69
+ ELSE ph.rate_throttle_ceiling_rps END,
70
+ next_dispatch_at_ms = CASE WHEN (SELECT granted FROM grant_calc) > 0 THEN
71
+ (SELECT first_at_ms FROM schedule_calc)
72
+ + CEIL((SELECT granted FROM grant_calc)::numeric / (SELECT burst_size FROM schedule_calc))::bigint
73
+ * (SELECT spacing_ms FROM schedule_calc) * (SELECT burst_size FROM schedule_calc)
74
+ ELSE ph.next_dispatch_at_ms END
75
+ FROM provider_hold ph CROSS JOIN now_value nv
76
+ WHERE h.bucket_id = $1
77
+ RETURNING h.bucket_id, h.rate_observation_started_at_ms, h.rate_successes,
78
+ h.rate_recovery_interval_ms, h.seen_backpressure
79
+ )`;
80
+ }